[Android] 1.9.2
All checks were successful
OpenClimb Docker Deploy / build-and-push (push) Successful in 2m29s
All checks were successful
OpenClimb Docker Deploy / build-and-push (push) Successful in 2m29s
This commit is contained in:
39
ios/OpenClimb/Components/AsyncImageView.swift
Normal file
39
ios/OpenClimb/Components/AsyncImageView.swift
Normal file
@@ -0,0 +1,39 @@
|
||||
import SwiftUI
|
||||
|
||||
struct AsyncImageView: View {
|
||||
let imagePath: String
|
||||
let targetSize: CGSize
|
||||
|
||||
@State private var image: UIImage?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.fill(Color(.systemGray6))
|
||||
|
||||
if let image = image {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.transition(.opacity.animation(.easeInOut(duration: 0.3)))
|
||||
} else {
|
||||
Image(systemName: "photo")
|
||||
.font(.system(size: 24))
|
||||
.foregroundColor(Color(.systemGray3))
|
||||
}
|
||||
}
|
||||
.frame(width: targetSize.width, height: targetSize.height)
|
||||
.clipped()
|
||||
.cornerRadius(8)
|
||||
.task(id: imagePath) {
|
||||
if self.image != nil {
|
||||
self.image = nil
|
||||
}
|
||||
|
||||
self.image = await ImageManager.shared.loadThumbnail(
|
||||
fromPath: imagePath,
|
||||
targetSize: targetSize
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ class SyncService: ObservableObject {
|
||||
@Published var syncError: String?
|
||||
@Published var isConnected = false
|
||||
@Published var isTesting = false
|
||||
@Published var isOfflineMode = false
|
||||
|
||||
private let userDefaults = UserDefaults.standard
|
||||
private var syncTask: Task<Void, Never>?
|
||||
@@ -19,8 +20,9 @@ class SyncService: ObservableObject {
|
||||
static let serverURL = "sync_server_url"
|
||||
static let authToken = "sync_auth_token"
|
||||
static let lastSyncTime = "last_sync_time"
|
||||
static let isConnected = "sync_is_connected"
|
||||
static let isConnected = "is_connected"
|
||||
static let autoSyncEnabled = "auto_sync_enabled"
|
||||
static let offlineMode = "offline_mode"
|
||||
}
|
||||
|
||||
var serverURL: String {
|
||||
@@ -46,12 +48,9 @@ class SyncService: ObservableObject {
|
||||
if let lastSync = userDefaults.object(forKey: Keys.lastSyncTime) as? Date {
|
||||
self.lastSyncTime = lastSync
|
||||
}
|
||||
self.isConnected = userDefaults.bool(forKey: Keys.isConnected)
|
||||
|
||||
// Perform image naming migration on initialization
|
||||
Task {
|
||||
await performImageNamingMigration()
|
||||
}
|
||||
isConnected = userDefaults.bool(forKey: Keys.isConnected)
|
||||
isAutoSyncEnabled = userDefaults.object(forKey: Keys.autoSyncEnabled) as? Bool ?? true
|
||||
isOfflineMode = userDefaults.bool(forKey: Keys.offlineMode)
|
||||
}
|
||||
|
||||
func downloadData() async throws -> ClimbDataBackup {
|
||||
@@ -211,6 +210,11 @@ class SyncService: ObservableObject {
|
||||
}
|
||||
|
||||
func syncWithServer(dataManager: ClimbingDataManager) async throws {
|
||||
if isOfflineMode {
|
||||
print("Sync skipped: Offline mode is enabled.")
|
||||
return
|
||||
}
|
||||
|
||||
guard isConfigured else {
|
||||
throw SyncError.notConfigured
|
||||
}
|
||||
@@ -1025,105 +1029,7 @@ class SyncService: ObservableObject {
|
||||
syncTask?.cancel()
|
||||
}
|
||||
|
||||
// MARK: - Image Naming Migration
|
||||
|
||||
private func performImageNamingMigration() async {
|
||||
let migrationKey = "image_naming_migration_completed_v2"
|
||||
guard !userDefaults.bool(forKey: migrationKey) else {
|
||||
print("Image naming migration already completed")
|
||||
return
|
||||
}
|
||||
|
||||
print("Starting image naming migration...")
|
||||
var updateCount = 0
|
||||
let imageManager = ImageManager.shared
|
||||
|
||||
// Get all problems from UserDefaults
|
||||
if let problemsData = userDefaults.data(forKey: "problems"),
|
||||
var problems = try? JSONDecoder().decode([Problem].self, from: problemsData)
|
||||
{
|
||||
|
||||
for problemIndex in 0..<problems.count {
|
||||
let problem = problems[problemIndex]
|
||||
guard !problem.imagePaths.isEmpty else { continue }
|
||||
|
||||
var updatedImagePaths: [String] = []
|
||||
var hasChanges = false
|
||||
|
||||
for (imageIndex, imagePath) in problem.imagePaths.enumerated() {
|
||||
let currentFilename = URL(fileURLWithPath: imagePath).lastPathComponent
|
||||
let consistentFilename = ImageNamingUtils.generateImageFilename(
|
||||
problemId: problem.id.uuidString, imageIndex: imageIndex)
|
||||
|
||||
if currentFilename != consistentFilename {
|
||||
let oldPath = imageManager.imagesDirectory.appendingPathComponent(
|
||||
currentFilename
|
||||
).path
|
||||
let newPath = imageManager.imagesDirectory.appendingPathComponent(
|
||||
consistentFilename
|
||||
).path
|
||||
|
||||
if FileManager.default.fileExists(atPath: oldPath) {
|
||||
do {
|
||||
try FileManager.default.moveItem(atPath: oldPath, toPath: newPath)
|
||||
updatedImagePaths.append(consistentFilename)
|
||||
hasChanges = true
|
||||
updateCount += 1
|
||||
print("Migrated image: \(currentFilename) -> \(consistentFilename)")
|
||||
} catch {
|
||||
print("Failed to migrate image \(currentFilename): \(error)")
|
||||
updatedImagePaths.append(imagePath)
|
||||
}
|
||||
} else {
|
||||
updatedImagePaths.append(imagePath)
|
||||
}
|
||||
} else {
|
||||
updatedImagePaths.append(imagePath)
|
||||
}
|
||||
}
|
||||
|
||||
if hasChanges {
|
||||
// Decode problem to dictionary, update imagePaths, re-encode
|
||||
if let problemData = try? JSONEncoder().encode(problem),
|
||||
var problemDict = try? JSONSerialization.jsonObject(with: problemData)
|
||||
as? [String: Any]
|
||||
{
|
||||
problemDict["imagePaths"] = updatedImagePaths
|
||||
problemDict["updatedAt"] = ISO8601DateFormatter().string(from: Date())
|
||||
if let updatedData = try? JSONSerialization.data(
|
||||
withJSONObject: problemDict),
|
||||
let updatedProblem = try? JSONDecoder().decode(
|
||||
Problem.self, from: updatedData)
|
||||
{
|
||||
problems[problemIndex] = updatedProblem
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if updateCount > 0 {
|
||||
if let updatedData = try? JSONEncoder().encode(problems) {
|
||||
userDefaults.set(updatedData, forKey: "problems")
|
||||
print("Updated \(updateCount) image paths in UserDefaults")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
userDefaults.set(true, forKey: migrationKey)
|
||||
print("Image naming migration completed, updated \(updateCount) images")
|
||||
|
||||
// Notify ClimbingDataManager to reload data if images were updated
|
||||
if updateCount > 0 {
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(
|
||||
name: NSNotification.Name("ImageMigrationCompleted"),
|
||||
object: nil,
|
||||
userInfo: ["updateCount": updateCount]
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Merging
|
||||
// MARK: - Safe Merge Functions
|
||||
|
||||
private func mergeGyms(local: [Gym], server: [BackupGym], deletedItems: [DeletedItem]) -> [Gym]
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import Foundation
|
||||
import ImageIO
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
class ImageManager {
|
||||
static let shared = ImageManager()
|
||||
|
||||
private let thumbnailCache = NSCache<NSString, UIImage>()
|
||||
private let fileManager = FileManager.default
|
||||
private let appSupportDirectoryName = "OpenClimb"
|
||||
private let imagesDirectoryName = "Images"
|
||||
@@ -479,6 +481,51 @@ class ImageManager {
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadThumbnail(fromPath path: String, targetSize: CGSize) async -> UIImage? {
|
||||
let cacheKey = "\(path)-\(targetSize.width)x\(targetSize.height)" as NSString
|
||||
|
||||
if let cachedImage = thumbnailCache.object(forKey: cacheKey) {
|
||||
return cachedImage
|
||||
}
|
||||
|
||||
guard let imageData = loadImageData(fromPath: path) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let options: [CFString: Any] = [
|
||||
kCGImageSourceCreateThumbnailFromImageIfAbsent: true,
|
||||
kCGImageSourceCreateThumbnailWithTransform: true,
|
||||
kCGImageSourceShouldCacheImmediately: true,
|
||||
kCGImageSourceThumbnailMaxPixelSize: max(targetSize.width, targetSize.height)
|
||||
* UIScreen.main.scale,
|
||||
]
|
||||
|
||||
guard let imageSource = CGImageSourceCreateWithData(imageData as CFData, nil) else {
|
||||
return UIImage(data: imageData)
|
||||
}
|
||||
|
||||
let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, nil) as? [CFString: Any]
|
||||
let orientation = properties?[kCGImagePropertyOrientation] as? UInt32 ?? 1
|
||||
|
||||
if let cgImage = CGImageSourceCreateThumbnailAtIndex(
|
||||
imageSource, 0, options as CFDictionary)
|
||||
{
|
||||
let imageOrientation = UIImage.Orientation(rawValue: Int(orientation - 1)) ?? .up
|
||||
let thumbnail = UIImage(
|
||||
cgImage: cgImage, scale: UIScreen.main.scale, orientation: imageOrientation)
|
||||
|
||||
thumbnailCache.setObject(thumbnail, forKey: cacheKey)
|
||||
return thumbnail
|
||||
} else {
|
||||
if let fallbackImage = UIImage(data: imageData) {
|
||||
thumbnailCache.setObject(fallbackImage, forKey: cacheKey)
|
||||
return fallbackImage
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func imageExists(atPath path: String) -> Bool {
|
||||
let primaryPath = getFullPath(from: path)
|
||||
let backupPath = backupDirectory.appendingPathComponent(getRelativePath(from: path))
|
||||
@@ -854,72 +901,4 @@ class ImageManager {
|
||||
}
|
||||
}
|
||||
|
||||
func migrateImageNamesToDeterministic(dataManager: ClimbingDataManager) {
|
||||
print("Starting migration of image names to deterministic format...")
|
||||
|
||||
var migrationCount = 0
|
||||
var updatedProblems: [Problem] = []
|
||||
|
||||
for problem in dataManager.problems {
|
||||
guard !problem.imagePaths.isEmpty else { continue }
|
||||
|
||||
var newImagePaths: [String] = []
|
||||
var problemNeedsUpdate = false
|
||||
|
||||
for (index, imagePath) in problem.imagePaths.enumerated() {
|
||||
let currentFilename = URL(fileURLWithPath: imagePath).lastPathComponent
|
||||
|
||||
if ImageNamingUtils.isValidImageFilename(currentFilename) {
|
||||
newImagePaths.append(imagePath)
|
||||
continue
|
||||
}
|
||||
|
||||
let deterministicName = ImageNamingUtils.generateImageFilename(
|
||||
problemId: problem.id.uuidString, imageIndex: index)
|
||||
|
||||
let oldPath = imagesDirectory.appendingPathComponent(currentFilename)
|
||||
let newPath = imagesDirectory.appendingPathComponent(deterministicName)
|
||||
|
||||
if fileManager.fileExists(atPath: oldPath.path) {
|
||||
do {
|
||||
try fileManager.moveItem(at: oldPath, to: newPath)
|
||||
|
||||
let oldBackupPath = backupDirectory.appendingPathComponent(currentFilename)
|
||||
let newBackupPath = backupDirectory.appendingPathComponent(
|
||||
deterministicName)
|
||||
|
||||
if fileManager.fileExists(atPath: oldBackupPath.path) {
|
||||
try? fileManager.moveItem(at: oldBackupPath, to: newBackupPath)
|
||||
}
|
||||
|
||||
newImagePaths.append(deterministicName)
|
||||
problemNeedsUpdate = true
|
||||
migrationCount += 1
|
||||
|
||||
print("Migrated: \(currentFilename) → \(deterministicName)")
|
||||
|
||||
} catch {
|
||||
print("Failed to migrate \(currentFilename): \(error)")
|
||||
newImagePaths.append(imagePath)
|
||||
}
|
||||
} else {
|
||||
print("Warning: Image file not found: \(currentFilename)")
|
||||
newImagePaths.append(imagePath)
|
||||
}
|
||||
}
|
||||
|
||||
if problemNeedsUpdate {
|
||||
let updatedProblem = problem.updated(imagePaths: newImagePaths)
|
||||
updatedProblems.append(updatedProblem)
|
||||
}
|
||||
}
|
||||
|
||||
for updatedProblem in updatedProblems {
|
||||
dataManager.updateProblem(updatedProblem)
|
||||
}
|
||||
|
||||
print(
|
||||
"Migration completed: \(migrationCount) images renamed, \(updatedProblems.count) problems updated"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ struct OrientationAwareImage: View {
|
||||
}
|
||||
|
||||
private func loadImageWithCorrectOrientation() {
|
||||
Task {
|
||||
Task.detached(priority: .userInitiated) {
|
||||
let correctedImage = await loadAndCorrectImage()
|
||||
await MainActor.run {
|
||||
self.uiImage = correctedImage
|
||||
@@ -48,17 +48,10 @@ struct OrientationAwareImage: View {
|
||||
}
|
||||
|
||||
private func loadAndCorrectImage() async -> UIImage? {
|
||||
// Load image data from ImageManager
|
||||
guard
|
||||
let data = await MainActor.run(body: {
|
||||
ImageManager.shared.loadImageData(fromPath: imagePath)
|
||||
})
|
||||
else { return nil }
|
||||
guard let data = ImageManager.shared.loadImageData(fromPath: imagePath) else { return nil }
|
||||
|
||||
// Create UIImage from data
|
||||
guard let originalImage = UIImage(data: data) else { return nil }
|
||||
|
||||
// Apply orientation correction
|
||||
return correctImageOrientation(originalImage)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,8 +9,26 @@ struct ProblemsView: View {
|
||||
@State private var showingSearch = false
|
||||
@FocusState private var isSearchFocused: Bool
|
||||
|
||||
private var filteredProblems: [Problem] {
|
||||
var filtered = dataManager.problems
|
||||
@State private var cachedFilteredProblems: [Problem] = []
|
||||
|
||||
private func updateFilteredProblems() {
|
||||
Task(priority: .userInitiated) {
|
||||
let result = await computeFilteredProblems()
|
||||
// Switch back to the main thread to update the UI
|
||||
await MainActor.run {
|
||||
cachedFilteredProblems = result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func computeFilteredProblems() async -> [Problem] {
|
||||
// Capture dependencies for safe background processing
|
||||
let problems = dataManager.problems
|
||||
let searchText = self.searchText
|
||||
let selectedClimbType = self.selectedClimbType
|
||||
let selectedGym = self.selectedGym
|
||||
|
||||
var filtered = problems
|
||||
|
||||
// Apply search filter
|
||||
if !searchText.isEmpty {
|
||||
@@ -93,19 +111,19 @@ struct ProblemsView: View {
|
||||
FilterSection(
|
||||
selectedClimbType: $selectedClimbType,
|
||||
selectedGym: $selectedGym,
|
||||
filteredProblems: filteredProblems
|
||||
filteredProblems: cachedFilteredProblems
|
||||
)
|
||||
.padding()
|
||||
.background(.regularMaterial)
|
||||
}
|
||||
|
||||
if filteredProblems.isEmpty {
|
||||
if cachedFilteredProblems.isEmpty {
|
||||
EmptyProblemsView(
|
||||
isEmpty: dataManager.problems.isEmpty,
|
||||
isFiltered: !dataManager.problems.isEmpty
|
||||
)
|
||||
} else {
|
||||
ProblemsList(problems: filteredProblems)
|
||||
ProblemsList(problems: cachedFilteredProblems)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -158,6 +176,21 @@ struct ProblemsView: View {
|
||||
AddEditProblemView()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
updateFilteredProblems()
|
||||
}
|
||||
.onChange(of: dataManager.problems) {
|
||||
updateFilteredProblems()
|
||||
}
|
||||
.onChange(of: searchText) {
|
||||
updateFilteredProblems()
|
||||
}
|
||||
.onChange(of: selectedClimbType) {
|
||||
updateFilteredProblems()
|
||||
}
|
||||
.onChange(of: selectedGym) {
|
||||
updateFilteredProblems()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,6 +302,7 @@ struct ProblemsList: View {
|
||||
@EnvironmentObject var dataManager: ClimbingDataManager
|
||||
@State private var problemToDelete: Problem?
|
||||
@State private var problemToEdit: Problem?
|
||||
@State private var animationKey = 0
|
||||
|
||||
var body: some View {
|
||||
List(problems, id: \.id) { problem in
|
||||
@@ -309,8 +343,11 @@ struct ProblemsList: View {
|
||||
}
|
||||
.animation(
|
||||
.spring(response: 0.5, dampingFraction: 0.8, blendDuration: 0.1),
|
||||
value: problems.map { "\($0.id):\($0.isActive)" }.joined()
|
||||
value: animationKey
|
||||
)
|
||||
.onChange(of: problems) {
|
||||
animationKey += 1
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
.scrollIndicators(.hidden)
|
||||
@@ -344,6 +381,12 @@ struct ProblemRow: View {
|
||||
dataManager.gym(withId: problem.gymId)
|
||||
}
|
||||
|
||||
private var isCompleted: Bool {
|
||||
dataManager.attempts.contains { attempt in
|
||||
attempt.problemId == problem.id && attempt.result.isSuccessful
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack {
|
||||
@@ -361,10 +404,24 @@ struct ProblemRow: View {
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .trailing, spacing: 4) {
|
||||
Text(problem.difficulty.grade)
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.blue)
|
||||
HStack(spacing: 8) {
|
||||
if !problem.imagePaths.isEmpty {
|
||||
Image(systemName: "photo")
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
|
||||
if isCompleted {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.foregroundColor(.green)
|
||||
}
|
||||
|
||||
Text(problem.difficulty.grade)
|
||||
.font(.title2)
|
||||
.fontWeight(.bold)
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
|
||||
Text(problem.climbType.displayName)
|
||||
.font(.caption)
|
||||
@@ -396,17 +453,6 @@ struct ProblemRow: View {
|
||||
}
|
||||
}
|
||||
|
||||
if !problem.imagePaths.isEmpty {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHStack(spacing: 8) {
|
||||
ForEach(problem.imagePaths.prefix(3), id: \.self) { imagePath in
|
||||
ProblemImageView(imagePath: imagePath)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 4)
|
||||
}
|
||||
}
|
||||
|
||||
if !problem.isActive {
|
||||
Text("Reset / No Longer Set")
|
||||
.font(.caption)
|
||||
@@ -478,17 +524,6 @@ struct EmptyProblemsView: View {
|
||||
}
|
||||
}
|
||||
|
||||
struct ProblemImageView: View {
|
||||
let imagePath: String
|
||||
|
||||
var body: some View {
|
||||
OrientationAwareImage.fill(imagePath: imagePath)
|
||||
.frame(width: 60, height: 60)
|
||||
.clipped()
|
||||
.cornerRadius(8)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ProblemsView()
|
||||
.environmentObject(ClimbingDataManager.preview)
|
||||
|
||||
@@ -80,8 +80,7 @@ struct DataManagementSection: View {
|
||||
@Binding var activeSheet: SheetType?
|
||||
@State private var showingResetAlert = false
|
||||
@State private var isExporting = false
|
||||
@State private var isMigrating = false
|
||||
@State private var showingMigrationAlert = false
|
||||
|
||||
@State private var isDeletingImages = false
|
||||
@State private var showingDeleteImagesAlert = false
|
||||
|
||||
@@ -121,27 +120,6 @@ struct DataManagementSection: View {
|
||||
}
|
||||
.foregroundColor(.primary)
|
||||
|
||||
// Migrate Image Names
|
||||
Button(action: {
|
||||
showingMigrationAlert = true
|
||||
}) {
|
||||
HStack {
|
||||
if isMigrating {
|
||||
ProgressView()
|
||||
.scaleEffect(0.8)
|
||||
Text("Migrating Images...")
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
Image(systemName: "photo.badge.arrow.down")
|
||||
.foregroundColor(.orange)
|
||||
Text("Fix Image Names")
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.disabled(isMigrating)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
// Delete All Images
|
||||
Button(action: {
|
||||
showingDeleteImagesAlert = true
|
||||
@@ -186,16 +164,7 @@ struct DataManagementSection: View {
|
||||
"Are you sure you want to reset all data? This will permanently delete:\n\n• All gyms and their information\n• All problems and their images\n• All climbing sessions\n• All attempts and progress data\n\nThis action cannot be undone. Consider exporting your data first."
|
||||
)
|
||||
}
|
||||
.alert("Fix Image Names", isPresented: $showingMigrationAlert) {
|
||||
Button("Cancel", role: .cancel) {}
|
||||
Button("Fix Names") {
|
||||
migrateImageNames()
|
||||
}
|
||||
} message: {
|
||||
Text(
|
||||
"This will rename all existing image files to use a consistent naming system across devices.\n\nThis improves sync reliability between iOS and Android. Your images will not be lost, only renamed.\n\nThis is safe to run multiple times."
|
||||
)
|
||||
}
|
||||
|
||||
.alert("Delete All Images", isPresented: $showingDeleteImagesAlert) {
|
||||
Button("Cancel", role: .cancel) {}
|
||||
Button("Delete", role: .destructive) {
|
||||
@@ -219,17 +188,6 @@ struct DataManagementSection: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func migrateImageNames() {
|
||||
isMigrating = true
|
||||
Task {
|
||||
await MainActor.run {
|
||||
ImageManager.shared.migrateImageNamesToDeterministic(dataManager: dataManager)
|
||||
isMigrating = false
|
||||
dataManager.successMessage = "Image names fixed successfully!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteAllImages() {
|
||||
isDeletingImages = true
|
||||
Task {
|
||||
|
||||
Reference in New Issue
Block a user