iOS Build 23
This commit is contained in:
@@ -458,24 +458,36 @@ struct AddAttemptView: View {
|
||||
let difficulty = DifficultyGrade(
|
||||
system: selectedDifficultySystem, grade: newProblemGrade)
|
||||
|
||||
// Save images and get paths
|
||||
var imagePaths: [String] = []
|
||||
for data in imageData {
|
||||
if let relativePath = ImageManager.shared.saveImageData(data) {
|
||||
imagePaths.append(relativePath)
|
||||
}
|
||||
}
|
||||
|
||||
let newProblem = Problem(
|
||||
gymId: gym.id,
|
||||
name: newProblemName.isEmpty ? nil : newProblemName,
|
||||
climbType: selectedClimbType,
|
||||
difficulty: difficulty,
|
||||
imagePaths: imagePaths
|
||||
imagePaths: []
|
||||
)
|
||||
|
||||
dataManager.addProblem(newProblem)
|
||||
|
||||
if !imageData.isEmpty {
|
||||
var imagePaths: [String] = []
|
||||
|
||||
for (index, data) in imageData.enumerated() {
|
||||
let deterministicName = ImageNamingUtils.generateImageFilename(
|
||||
problemId: newProblem.id.uuidString, imageIndex: index)
|
||||
|
||||
if let relativePath = ImageManager.shared.saveImageData(
|
||||
data, withName: deterministicName)
|
||||
{
|
||||
imagePaths.append(relativePath)
|
||||
}
|
||||
}
|
||||
|
||||
if !imagePaths.isEmpty {
|
||||
let updatedProblem = newProblem.updated(imagePaths: imagePaths)
|
||||
dataManager.updateProblem(updatedProblem)
|
||||
}
|
||||
}
|
||||
|
||||
let attempt = Attempt(
|
||||
sessionId: session.id,
|
||||
problemId: newProblem.id,
|
||||
@@ -1218,24 +1230,36 @@ struct EditAttemptView: View {
|
||||
let difficulty = DifficultyGrade(
|
||||
system: selectedDifficultySystem, grade: newProblemGrade)
|
||||
|
||||
// Save images and get paths
|
||||
var imagePaths: [String] = []
|
||||
for data in imageData {
|
||||
if let relativePath = ImageManager.shared.saveImageData(data) {
|
||||
imagePaths.append(relativePath)
|
||||
}
|
||||
}
|
||||
|
||||
let newProblem = Problem(
|
||||
gymId: gym.id,
|
||||
name: newProblemName.isEmpty ? nil : newProblemName,
|
||||
climbType: selectedClimbType,
|
||||
difficulty: difficulty,
|
||||
imagePaths: imagePaths
|
||||
imagePaths: []
|
||||
)
|
||||
|
||||
dataManager.addProblem(newProblem)
|
||||
|
||||
if !imageData.isEmpty {
|
||||
var imagePaths: [String] = []
|
||||
|
||||
for (index, data) in imageData.enumerated() {
|
||||
let deterministicName = ImageNamingUtils.generateImageFilename(
|
||||
problemId: newProblem.id.uuidString, imageIndex: index)
|
||||
|
||||
if let relativePath = ImageManager.shared.saveImageData(
|
||||
data, withName: deterministicName)
|
||||
{
|
||||
imagePaths.append(relativePath)
|
||||
}
|
||||
}
|
||||
|
||||
if !imagePaths.isEmpty {
|
||||
let updatedProblem = newProblem.updated(imagePaths: imagePaths)
|
||||
dataManager.updateProblem(updatedProblem)
|
||||
}
|
||||
}
|
||||
|
||||
let updatedAttempt = attempt.updated(
|
||||
problemId: newProblem.id,
|
||||
result: selectedResult,
|
||||
@@ -1329,16 +1353,18 @@ struct ProblemSelectionImageView: View {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
if let data = ImageManager.shared.loadImageData(fromPath: imagePath),
|
||||
let image = UIImage(data: data)
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
Task {
|
||||
let data = await MainActor.run {
|
||||
ImageManager.shared.loadImageData(fromPath: imagePath)
|
||||
}
|
||||
|
||||
if let data = data, let image = UIImage(data: data) {
|
||||
await MainActor.run {
|
||||
self.uiImage = image
|
||||
self.isLoading = false
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
await MainActor.run {
|
||||
self.hasFailed = true
|
||||
self.isLoading = false
|
||||
}
|
||||
|
||||
@@ -556,21 +556,25 @@ struct AddEditProblemView: View {
|
||||
|
||||
let difficulty = DifficultyGrade(system: selectedDifficultySystem, grade: difficultyGrade)
|
||||
|
||||
// Save new image data and combine with existing paths
|
||||
var allImagePaths = imagePaths
|
||||
if isEditing, let problem = existingProblem {
|
||||
var allImagePaths = imagePaths
|
||||
|
||||
// Only save NEW images (those beyond the existing imagePaths count)
|
||||
let newImagesStartIndex = imagePaths.count
|
||||
if imageData.count > newImagesStartIndex {
|
||||
for i in newImagesStartIndex..<imageData.count {
|
||||
let data = imageData[i]
|
||||
if let relativePath = ImageManager.shared.saveImageData(data) {
|
||||
allImagePaths.append(relativePath)
|
||||
let newImagesStartIndex = imagePaths.count
|
||||
if imageData.count > newImagesStartIndex {
|
||||
for i in newImagesStartIndex..<imageData.count {
|
||||
let data = imageData[i]
|
||||
let imageIndex = allImagePaths.count
|
||||
let deterministicName = ImageNamingUtils.generateImageFilename(
|
||||
problemId: problem.id.uuidString, imageIndex: imageIndex)
|
||||
|
||||
if let relativePath = ImageManager.shared.saveImageData(
|
||||
data, withName: deterministicName)
|
||||
{
|
||||
allImagePaths.append(relativePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isEditing, let problem = existingProblem {
|
||||
let updatedProblem = problem.updated(
|
||||
name: trimmedName.isEmpty ? nil : trimmedName,
|
||||
description: trimmedDescription.isEmpty ? nil : trimmedDescription,
|
||||
@@ -595,11 +599,32 @@ struct AddEditProblemView: View {
|
||||
|
||||
tags: trimmedTags,
|
||||
location: trimmedLocation.isEmpty ? nil : trimmedLocation,
|
||||
imagePaths: allImagePaths,
|
||||
imagePaths: [],
|
||||
dateSet: dateSet,
|
||||
notes: trimmedNotes.isEmpty ? nil : trimmedNotes
|
||||
)
|
||||
|
||||
dataManager.addProblem(newProblem)
|
||||
|
||||
if !imageData.isEmpty {
|
||||
var imagePaths: [String] = []
|
||||
|
||||
for (index, data) in imageData.enumerated() {
|
||||
let deterministicName = ImageNamingUtils.generateImageFilename(
|
||||
problemId: newProblem.id.uuidString, imageIndex: index)
|
||||
|
||||
if let relativePath = ImageManager.shared.saveImageData(
|
||||
data, withName: deterministicName)
|
||||
{
|
||||
imagePaths.append(relativePath)
|
||||
}
|
||||
}
|
||||
|
||||
if !imagePaths.isEmpty {
|
||||
let updatedProblem = newProblem.updated(imagePaths: imagePaths)
|
||||
dataManager.updateProblem(updatedProblem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dismiss()
|
||||
|
||||
@@ -486,16 +486,18 @@ struct ProblemDetailImageView: View {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
if let data = ImageManager.shared.loadImageData(fromPath: imagePath),
|
||||
let image = UIImage(data: data)
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
Task {
|
||||
let data = await MainActor.run {
|
||||
ImageManager.shared.loadImageData(fromPath: imagePath)
|
||||
}
|
||||
|
||||
if let data = data, let image = UIImage(data: data) {
|
||||
await MainActor.run {
|
||||
self.uiImage = image
|
||||
self.isLoading = false
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
await MainActor.run {
|
||||
self.hasFailed = true
|
||||
self.isLoading = false
|
||||
}
|
||||
@@ -550,16 +552,18 @@ struct ProblemDetailImageFullView: View {
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
if let data = ImageManager.shared.loadImageData(fromPath: imagePath),
|
||||
let image = UIImage(data: data)
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
Task {
|
||||
let data = await MainActor.run {
|
||||
ImageManager.shared.loadImageData(fromPath: imagePath)
|
||||
}
|
||||
|
||||
if let data = data, let image = UIImage(data: data) {
|
||||
await MainActor.run {
|
||||
self.uiImage = image
|
||||
self.isLoading = false
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
await MainActor.run {
|
||||
self.hasFailed = true
|
||||
self.isLoading = false
|
||||
}
|
||||
|
||||
@@ -484,7 +484,7 @@ struct ProblemImageView: View {
|
||||
@State private var isLoading = true
|
||||
@State private var hasFailed = false
|
||||
|
||||
private static var imageCache: NSCache<NSString, UIImage> = {
|
||||
private static let imageCache: NSCache<NSString, UIImage> = {
|
||||
let cache = NSCache<NSString, UIImage>()
|
||||
cache.countLimit = 100
|
||||
cache.totalCostLimit = 50 * 1024 * 1024 // 50MB
|
||||
@@ -531,31 +531,28 @@ struct ProblemImageView: View {
|
||||
return
|
||||
}
|
||||
|
||||
let cacheKey = NSString(string: imagePath)
|
||||
// Load image asynchronously
|
||||
Task { @MainActor in
|
||||
let cacheKey = NSString(string: imagePath)
|
||||
|
||||
// Check cache first
|
||||
if let cachedImage = Self.imageCache.object(forKey: cacheKey) {
|
||||
self.uiImage = cachedImage
|
||||
self.isLoading = false
|
||||
return
|
||||
}
|
||||
// Check cache first
|
||||
if let cachedImage = Self.imageCache.object(forKey: cacheKey) {
|
||||
self.uiImage = cachedImage
|
||||
self.isLoading = false
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
// Load image data
|
||||
if let data = ImageManager.shared.loadImageData(fromPath: imagePath),
|
||||
let image = UIImage(data: data)
|
||||
{
|
||||
// Cache the image
|
||||
Self.imageCache.setObject(image, forKey: cacheKey)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.uiImage = image
|
||||
self.isLoading = false
|
||||
}
|
||||
self.uiImage = image
|
||||
self.isLoading = false
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.hasFailed = true
|
||||
self.isLoading = false
|
||||
}
|
||||
self.hasFailed = true
|
||||
self.isLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,6 +80,10 @@ 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
|
||||
|
||||
var body: some View {
|
||||
Section("Data Management") {
|
||||
@@ -117,6 +121,48 @@ 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
|
||||
}) {
|
||||
HStack {
|
||||
if isDeletingImages {
|
||||
ProgressView()
|
||||
.scaleEffect(0.8)
|
||||
Text("Deleting Images...")
|
||||
.foregroundColor(.secondary)
|
||||
} else {
|
||||
Image(systemName: "trash")
|
||||
.foregroundColor(.red)
|
||||
Text("Delete All Images")
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
.disabled(isDeletingImages)
|
||||
.foregroundColor(.red)
|
||||
|
||||
// Reset All Data
|
||||
Button(action: {
|
||||
showingResetAlert = true
|
||||
@@ -140,6 +186,26 @@ 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) {
|
||||
deleteAllImages()
|
||||
}
|
||||
} message: {
|
||||
Text(
|
||||
"This will permanently delete ALL image files from your device.\n\nProblems will keep their references but the actual image files will be removed. This cannot be undone.\n\nConsider exporting your data first if you want to keep your images."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func exportDataAsync() {
|
||||
@@ -152,6 +218,75 @@ 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 {
|
||||
await MainActor.run {
|
||||
deleteAllImageFiles()
|
||||
isDeletingImages = false
|
||||
dataManager.successMessage = "All images deleted successfully!"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func deleteAllImageFiles() {
|
||||
let imageManager = ImageManager.shared
|
||||
let fileManager = FileManager.default
|
||||
|
||||
// Delete all images from the images directory
|
||||
let imagesDir = imageManager.imagesDirectory
|
||||
do {
|
||||
let imageFiles = try fileManager.contentsOfDirectory(
|
||||
at: imagesDir, includingPropertiesForKeys: nil)
|
||||
var deletedCount = 0
|
||||
|
||||
for imageFile in imageFiles {
|
||||
do {
|
||||
try fileManager.removeItem(at: imageFile)
|
||||
deletedCount += 1
|
||||
} catch {
|
||||
print("Failed to delete image: \(imageFile.lastPathComponent)")
|
||||
}
|
||||
}
|
||||
|
||||
print("Deleted \(deletedCount) image files")
|
||||
} catch {
|
||||
print("Failed to access images directory: \(error)")
|
||||
}
|
||||
|
||||
// Delete all images from backup directory
|
||||
let backupDir = imageManager.backupDirectory
|
||||
do {
|
||||
let backupFiles = try fileManager.contentsOfDirectory(
|
||||
at: backupDir, includingPropertiesForKeys: nil)
|
||||
for backupFile in backupFiles {
|
||||
try? fileManager.removeItem(at: backupFile)
|
||||
}
|
||||
} catch {
|
||||
print("Failed to access backup directory: \(error)")
|
||||
}
|
||||
|
||||
// Clear image paths from all problems
|
||||
let updatedProblems = dataManager.problems.map { problem in
|
||||
problem.updated(imagePaths: [])
|
||||
}
|
||||
|
||||
for problem in updatedProblems {
|
||||
dataManager.updateProblem(problem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct AppInfoSection: View {
|
||||
|
||||
Reference in New Issue
Block a user