Compare commits

..

6 Commits

Author SHA1 Message Date
77f7092287 New build... apple pls 2026-03-03 17:11:12 -07:00
ed25cf7ecd Bump 2026-02-02 10:00:47 -07:00
255f85c2df Ok fixed more nonsense 2026-02-02 10:00:07 -07:00
a3d47d29c5 OOps 2026-02-02 09:47:17 -07:00
b94b823986 Fixed add/edit Attempts View 2026-02-02 09:41:28 -07:00
58d84af29b Fixed add/edit problems 2026-02-02 09:36:02 -07:00
4 changed files with 227 additions and 511 deletions

View File

@@ -466,7 +466,7 @@
CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 50;
CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
ENABLE_PREVIEWS = YES;
@@ -518,7 +518,7 @@
CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 50;
CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
ENABLE_PREVIEWS = YES;
@@ -610,7 +610,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 50;
CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SessionStatusLive/Info.plist;
@@ -641,7 +641,7 @@
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 50;
CURRENT_PROJECT_VERSION = 53;
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = SessionStatusLive/Info.plist;

View File

@@ -373,35 +373,19 @@ struct AddAttemptView: View {
Section("Additional Details") {
TextField("Highest Hold (Optional)", text: $highestHold)
VStack(alignment: .leading, spacing: 8) {
Text("Notes (Optional)")
.font(.headline)
TextField("Notes (Optional)", text: $notes, axis: .vertical)
.lineLimit(3...6)
TextEditor(text: $notes)
.frame(minHeight: 80)
.padding(8)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(.quaternary)
)
}
HStack {
Text("Duration (seconds)")
Spacer()
LabeledContent("Duration (seconds)") {
TextField("0", value: $duration, format: .number)
.keyboardType(.numberPad)
.textFieldStyle(.roundedBorder)
.frame(width: 80)
.multilineTextAlignment(.trailing)
}
HStack {
Text("Rest Time (seconds)")
Spacer()
LabeledContent("Rest Time (seconds)") {
TextField("0", value: $restTime, format: .number)
.keyboardType(.numberPad)
.textFieldStyle(.roundedBorder)
.frame(width: 80)
.multilineTextAlignment(.trailing)
}
}
}
@@ -761,65 +745,14 @@ struct EditAttemptView: View {
@EnvironmentObject var themeManager: ThemeManager
@Environment(\.dismiss) private var dismiss
@State private var selectedProblem: Problem?
@State private var selectedResult: AttemptResult
@State private var highestHold: String
@State private var notes: String
@State private var duration: Int
@State private var restTime: Int
@State private var showingCreateProblem = false
// New problem creation state
@State private var newProblemName = ""
@State private var newProblemGrade = ""
@State private var selectedClimbType: ClimbType = .boulder
@State private var selectedDifficultySystem: DifficultySystem = .vScale
@State private var selectedPhotos: [PhotosPickerItem] = []
@State private var imageData: [Data] = []
enum SheetType: Identifiable {
case photoOptions
var id: Int {
switch self {
case .photoOptions: return 0
}
}
}
@State private var activeSheet: SheetType?
@State private var showCamera = false
@State private var showPhotoPicker = false
@State private var isPhotoPickerActionPending = false
@State private var isCameraActionPending = false
private var availableProblems: [Problem] {
guard let session = dataManager.session(withId: attempt.sessionId) else {
return []
}
return dataManager.problems.filter { $0.isActive && $0.gymId == session.gymId }
}
private var gym: Gym? {
guard let session = dataManager.session(withId: attempt.sessionId) else {
return nil
}
return dataManager.gym(withId: session.gymId)
}
private var availableClimbTypes: [ClimbType] {
gym?.supportedClimbTypes ?? []
}
private var availableDifficultySystems: [DifficultySystem] {
guard let gym = gym else { return [] }
return DifficultySystem.systemsForClimbType(selectedClimbType).filter { system in
gym.difficultySystems.contains(system)
}
}
private var availableGrades: [String] {
selectedDifficultySystem.availableGrades
private var problem: Problem? {
dataManager.problem(withId: attempt.problemId)
}
init(attempt: Attempt) {
@@ -834,12 +767,7 @@ struct EditAttemptView: View {
var body: some View {
NavigationStack {
Form {
if !showingCreateProblem {
ProblemSelectionSection()
} else {
CreateProblemSection()
}
ProblemSection()
AttemptDetailsSection()
}
.navigationTitle("Edit Attempt")
@@ -855,269 +783,35 @@ struct EditAttemptView: View {
Button("Update") {
updateAttempt()
}
.disabled(!canSave)
}
}
}
.onAppear {
selectedProblem = dataManager.problem(withId: attempt.problemId)
setupInitialValues()
}
.onChange(of: selectedClimbType) {
updateDifficultySystem()
}
.onChange(of: selectedDifficultySystem) {
resetGradeIfNeeded()
}
.onChange(of: selectedPhotos) {
Task {
await loadSelectedPhotos()
}
}
.photosPicker(
isPresented: $showPhotoPicker,
selection: $selectedPhotos,
maxSelectionCount: 5 - imageData.count,
matching: .images
)
.sheet(
item: $activeSheet,
onDismiss: {
if isCameraActionPending {
showCamera = true
isCameraActionPending = false
return
}
if isPhotoPickerActionPending {
showPhotoPicker = true
isPhotoPickerActionPending = false
}
}
) { sheetType in
switch sheetType {
case .photoOptions:
PhotoOptionSheet(
selectedPhotos: $selectedPhotos,
imageData: $imageData,
maxImages: 5,
onCameraSelected: {
isCameraActionPending = true
activeSheet = nil
},
onPhotoLibrarySelected: {
isPhotoPickerActionPending = true
},
onDismiss: {
activeSheet = nil
}
)
}
}
.fullScreenCover(isPresented: $showCamera) {
CameraImagePicker { capturedImage in
if let jpegData = capturedImage.jpegData(compressionQuality: 0.8) {
imageData.append(jpegData)
}
}
}
}
@ViewBuilder
private func ProblemSelectionSection() -> some View {
Section("Select Problem") {
if availableProblems.isEmpty {
VStack(alignment: .leading, spacing: 12) {
Text("No active problems in this gym")
.foregroundColor(.secondary)
Button("Create New Problem") {
showingCreateProblem = true
}
.buttonStyle(.borderedProminent)
.tint(themeManager.accentColor)
}
.padding(.vertical, 8)
} else {
LazyVGrid(
columns: Array(repeating: GridItem(.flexible(), spacing: 8), count: 2),
spacing: 8
) {
ForEach(availableProblems, id: \.id) { problem in
ProblemSelectionCard(
problem: problem,
isSelected: selectedProblem?.id == problem.id
) {
selectedProblem = problem
}
}
}
.padding(.vertical, 8)
Button("Create New Problem") {
showingCreateProblem = true
}
.foregroundColor(themeManager.accentColor)
}
}
}
@ViewBuilder
private func CreateProblemSection() -> some View {
Section {
HStack {
Text("Create New Problem")
.font(.headline)
Spacer()
Button("Back") {
showingCreateProblem = false
selectedPhotos = []
imageData = []
}
.foregroundColor(themeManager.accentColor)
}
}
Section("Problem Details") {
TextField("Problem Name", text: $newProblemName)
}
Section("Climb Type") {
ForEach(availableClimbTypes, id: \.self) { climbType in
private func ProblemSection() -> some View {
Section("Problem") {
if let problem = problem {
HStack {
Text(climbType.displayName)
Spacer()
if selectedClimbType == climbType {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(themeManager.accentColor)
} else {
Image(systemName: "circle")
.foregroundColor(.gray)
}
}
.contentShape(Rectangle())
.onTapGesture {
selectedClimbType = climbType
}
}
}
Section("Difficulty") {
VStack(alignment: .leading, spacing: 12) {
Text("Difficulty System")
.font(.subheadline)
.fontWeight(.medium)
ForEach(availableDifficultySystems, id: \.self) { system in
HStack {
Text(system.displayName)
Spacer()
if selectedDifficultySystem == system {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(themeManager.accentColor)
} else {
Image(systemName: "circle")
.foregroundColor(.gray)
}
}
.contentShape(Rectangle())
.onTapGesture {
selectedDifficultySystem = system
}
}
}
if selectedDifficultySystem == .custom {
TextField("Grade (Required - numbers only)", text: $newProblemGrade)
.keyboardType(.numberPad)
.onChange(of: newProblemGrade) {
// Filter out non-numeric characters
newProblemGrade = newProblemGrade.filter { $0.isNumber }
}
} else {
VStack(alignment: .leading, spacing: 8) {
Text("Grade (Required)")
.font(.subheadline)
.fontWeight(.medium)
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack(spacing: 8) {
ForEach(availableGrades, id: \.self) { grade in
Button(grade) {
newProblemGrade = grade
}
.buttonStyle(.bordered)
.controlSize(.small)
.tint(newProblemGrade == grade ? themeManager.accentColor : .gray)
}
}
.padding(.horizontal, 1)
}
}
}
}
Section("Photos (Optional)") {
Button(action: {
activeSheet = .photoOptions
}) {
HStack {
Image(systemName: "camera.fill")
.foregroundColor(themeManager.accentColor)
.font(.title2)
VStack(alignment: .leading, spacing: 2) {
Text("Add Photos")
VStack(alignment: .leading, spacing: 4) {
Text(problem.name ?? "Unnamed Problem")
.font(.headline)
.foregroundColor(themeManager.accentColor)
Text("\(imageData.count) of 5 photos added")
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
Image(systemName: "chevron.right")
.foregroundColor(.secondary)
.font(.caption)
}
.padding(.vertical, 4)
}
.disabled(imageData.count >= 5)
if !imageData.isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ForEach(imageData.indices, id: \.self) { index in
if let uiImage = UIImage(data: imageData[index]) {
Image(uiImage: uiImage)
.resizable()
.aspectRatio(contentMode: .fill)
.frame(width: 80, height: 80)
.clipped()
.cornerRadius(8)
.overlay(alignment: .topTrailing) {
Button(action: {
imageData.remove(at: index)
}) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.red)
.background(Circle().fill(.white))
}
.offset(x: 8, y: -8)
}
} else {
RoundedRectangle(cornerRadius: 8)
.fill(.gray.opacity(0.3))
.frame(width: 80, height: 80)
.overlay {
Image(systemName: "photo")
.foregroundColor(.gray)
}
}
HStack(spacing: 8) {
Text(problem.climbType.displayName)
.font(.subheadline)
.foregroundColor(.secondary)
Text("")
.foregroundColor(.secondary)
Text(problem.difficulty.grade)
.font(.subheadline)
.foregroundColor(.secondary)
}
}
.padding(.horizontal, 1)
Spacer()
}
} else {
Text("Problem not found")
.foregroundColor(.secondary)
}
}
}
@@ -1147,158 +841,33 @@ struct EditAttemptView: View {
Section("Additional Details") {
TextField("Highest Hold (Optional)", text: $highestHold)
VStack(alignment: .leading, spacing: 8) {
Text("Notes (Optional)")
.font(.headline)
TextField("Notes (Optional)", text: $notes, axis: .vertical)
.lineLimit(3...6)
TextEditor(text: $notes)
.frame(minHeight: 80)
.padding(8)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(.quaternary)
)
}
HStack {
Text("Duration (seconds)")
Spacer()
LabeledContent("Duration (seconds)") {
TextField("0", value: $duration, format: .number)
.keyboardType(.numberPad)
.textFieldStyle(.roundedBorder)
.frame(width: 80)
.multilineTextAlignment(.trailing)
}
HStack {
Text("Rest Time (seconds)")
Spacer()
LabeledContent("Rest Time (seconds)") {
TextField("0", value: $restTime, format: .number)
.keyboardType(.numberPad)
.textFieldStyle(.roundedBorder)
.frame(width: 80)
.multilineTextAlignment(.trailing)
}
}
}
private var canSave: Bool {
if showingCreateProblem {
return !newProblemGrade.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
} else {
return selectedProblem != nil
}
}
private func setupInitialValues() {
guard let gym = gym else { return }
// Auto-select climb type if there's only one available
if gym.supportedClimbTypes.count == 1 {
selectedClimbType = gym.supportedClimbTypes.first!
}
updateDifficultySystem()
}
private func updateDifficultySystem() {
let available = availableDifficultySystems
if !available.contains(selectedDifficultySystem) {
selectedDifficultySystem = available.first ?? .custom
}
if available.count == 1 {
selectedDifficultySystem = available.first!
}
}
private func resetGradeIfNeeded() {
let availableGrades = selectedDifficultySystem.availableGrades
if !availableGrades.isEmpty && !availableGrades.contains(newProblemGrade) {
newProblemGrade = ""
}
}
private func loadSelectedPhotos() async {
var newImageData: [Data] = []
for item in selectedPhotos {
if let data = try? await item.loadTransferable(type: Data.self) {
newImageData.append(data)
}
}
await MainActor.run {
imageData.append(contentsOf: newImageData)
selectedPhotos.removeAll()
}
}
private func updateAttempt() {
if showingCreateProblem {
guard let gym = gym else { return }
let difficulty = DifficultyGrade(
system: selectedDifficultySystem, grade: newProblemGrade)
let newProblem = Problem(
gymId: gym.id,
name: newProblemName.isEmpty ? nil : newProblemName,
climbType: selectedClimbType,
difficulty: difficulty,
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,
highestHold: highestHold.isEmpty ? nil : highestHold,
notes: notes.isEmpty ? nil : notes,
duration: duration > 0 ? duration : nil,
restTime: restTime > 0 ? restTime : nil
)
dataManager.updateAttempt(updatedAttempt)
} else {
guard selectedProblem != nil else { return }
let updatedAttempt = attempt.updated(
problemId: selectedProblem?.id,
result: selectedResult,
highestHold: highestHold.isEmpty ? nil : highestHold,
notes: notes.isEmpty ? nil : notes,
duration: duration > 0 ? duration : nil,
restTime: restTime > 0 ? restTime : nil
)
dataManager.updateAttempt(updatedAttempt)
}
// Clear photo states after saving
selectedPhotos = []
imageData = []
let updatedAttempt = attempt.updated(
result: selectedResult,
highestHold: highestHold.isEmpty ? nil : highestHold,
notes: notes.isEmpty ? nil : notes,
duration: duration > 0 ? duration : nil,
restTime: restTime > 0 ? restTime : nil
)
dataManager.updateAttempt(updatedAttempt)
dismiss()
}
}

View File

@@ -19,6 +19,7 @@ struct AddEditProblemView: View {
@State private var tags = ""
@State private var notes = ""
@State private var isActive = true
@State private var dateSet = Date()
@State private var imagePaths: [String] = []
@State private var imageData: [Data] = []
@State private var showingPhotoOptions = false
@@ -26,6 +27,9 @@ struct AddEditProblemView: View {
@State private var showingImagePicker = false
@State private var imageSource: UIImagePickerController.SourceType = .photoLibrary
@State private var isEditing = false
@State private var showingGradeError = false
@State private var isLoaded = false
@State private var originalImages: [(path: String, data: Data)] = []
private var existingProblem: Problem? {
guard let problemId = problemId else { return nil }
@@ -52,6 +56,10 @@ struct AddEditProblemView: View {
Form {
GymSelectionSection()
BasicInfoSection()
PhotosSection()
ClimbTypeSection()
DifficultySection()
LocationDetailsSection()
AdditionalInfoSection()
}
.navigationTitle(isEditing ? "Edit Problem" : "New Problem")
@@ -65,15 +73,22 @@ struct AddEditProblemView: View {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") {
saveProblem()
if canSave {
saveProblem()
} else {
showingGradeError = true
}
}
.disabled(!canSave)
.disabled(!canSave && !showingGradeError)
}
}
}
.onAppear {
setupInitialClimbType()
loadExistingProblem()
DispatchQueue.main.async {
isLoaded = true
}
}
.sheet(isPresented: $showingPhotoOptions) {
PhotoOptionSheet(
@@ -155,43 +170,158 @@ struct AddEditProblemView: View {
Section("Problem Details") {
TextField("Problem Name (Optional)", text: $name)
VStack(alignment: .leading, spacing: 8) {
Text("Description (Optional)")
.font(.headline)
TextField("Description (Optional)", text: $description, axis: .vertical)
.lineLimit(3...6)
}
}
TextEditor(text: $description)
.frame(minHeight: 80)
.padding(8)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(.quaternary)
)
@ViewBuilder
private func PhotosSection() -> some View {
Section("Photos (Optional)") {
Button(action: {
showingPhotoOptions = true
}) {
HStack {
Image(systemName: "camera.fill")
.foregroundColor(themeManager.accentColor)
VStack(alignment: .leading) {
Text("Add Photos")
.foregroundColor(themeManager.accentColor)
Text("\(imageData.count) of 5 photos added")
.font(.caption)
.foregroundColor(.secondary)
}
Spacer()
Image(systemName: "chevron.right")
.font(.caption)
.foregroundColor(.secondary)
}
}
if !imageData.isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 8) {
ForEach(imageData.indices, id: \.self) { index in
if let uiImage = UIImage(data: imageData[index]) {
ZStack(alignment: .topTrailing) {
Image(uiImage: uiImage)
.resizable()
.scaledToFill()
.frame(width: 80, height: 80)
.clipShape(RoundedRectangle(cornerRadius: 8))
Button(action: {
imageData.remove(at: index)
}) {
Image(systemName: "xmark.circle.fill")
.foregroundColor(.white)
.background(Circle().fill(Color.black.opacity(0.5)))
}
.padding(4)
}
}
}
}
.padding(.vertical, 4)
}
}
}
}
@ViewBuilder
private func AdditionalInfoSection() -> some View {
Section("Additional Information") {
VStack(alignment: .leading, spacing: 8) {
Text("Notes (Optional)")
.font(.headline)
TextEditor(text: $notes)
.frame(minHeight: 80)
.padding(8)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(.quaternary)
)
private func ClimbTypeSection() -> some View {
Section("Climb Type") {
ForEach(ClimbType.allCases, id: \.self) { type in
HStack {
Text(type.displayName)
Spacer()
if selectedClimbType == type {
Image(systemName: "checkmark.circle.fill")
.foregroundColor(themeManager.accentColor)
} else {
Image(systemName: "circle")
.foregroundColor(.secondary)
}
}
.contentShape(Rectangle())
.onTapGesture {
selectedClimbType = type
}
}
}
}
@ViewBuilder
private func DifficultySection() -> some View {
Section("Difficulty") {
Picker("Difficulty System", selection: $selectedDifficultySystem) {
ForEach(DifficultySystem.systemsForClimbType(selectedClimbType), id: \.self) { system in
Text(system.displayName).tag(system)
}
}
.onChange(of: selectedDifficultySystem) { oldValue, newValue in
if isLoaded {
difficultyGrade = ""
}
}
if selectedDifficultySystem == .custom {
HStack {
Text("Grade")
Spacer()
TextField("Numbers only", text: $difficultyGrade)
.multilineTextAlignment(.trailing)
.keyboardType(.numberPad)
}
} else {
Picker("Grade", selection: $difficultyGrade) {
if difficultyGrade.isEmpty {
Text("Select Grade").tag("")
}
ForEach(selectedDifficultySystem.availableGrades, id: \.self) { grade in
Text(grade).tag(grade)
}
}
.pickerStyle(.menu)
}
if showingGradeError && difficultyGrade.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
Text("Please select a grade to continue")
.font(.caption)
.foregroundColor(.red)
.italic()
}
}
}
@ViewBuilder
private func LocationDetailsSection() -> some View {
Section("Location & Details") {
TextField("e.g., 'Cave area', 'Wall 3'", text: $location)
DatePicker("Date Set", selection: $dateSet, displayedComponents: .date)
}
}
@ViewBuilder
private func AdditionalInfoSection() -> some View {
Section("Tags (Optional)") {
TextField("e.g., crimpy, dynamic (comma-separated)", text: $tags)
}
Section("Additional Information") {
TextField("Notes (Optional)", text: $notes, axis: .vertical)
.lineLimit(3...6)
Toggle("Problem is currently active", isOn: $isActive)
}
}
private var canSave: Bool {
selectedGym != nil && difficultyGrade.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
selectedGym != nil && !difficultyGrade.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
private func setupInitialClimbType() {
@@ -199,7 +329,6 @@ struct AddEditProblemView: View {
selectedGym = dataManager.gym(withId: gymId)
}
// Always ensure a gym is selected if available and none is currently selected
if selectedGym == nil && !dataManager.gyms.isEmpty {
selectedGym = dataManager.gyms.first
}
@@ -219,13 +348,17 @@ struct AddEditProblemView: View {
tags = problem.tags.joined(separator: ", ")
notes = problem.notes ?? ""
isActive = problem.isActive
if let date = problem.dateSet {
dateSet = date
}
imagePaths = problem.imagePaths
// Load image data for preview
imageData = []
originalImages = []
for imagePath in problem.imagePaths {
if let data = ImageManager.shared.loadImageData(fromPath: imagePath) {
imageData.append(data)
originalImages.append((path: imagePath, data: data))
}
}
}
@@ -242,12 +375,25 @@ struct AddEditProblemView: View {
$0.trimmingCharacters(in: .whitespacesAndNewlines)
}.filter { !$0.isEmpty }
let tempImagePaths = imagePaths.filter { !$0.isEmpty && !imagePaths.contains($0) }
for imagePath in tempImagePaths {
_ = ImageManager.shared.deleteImage(atPath: imagePath)
var finalPaths: [String] = []
var preservedPaths: Set<String> = []
for data in imageData {
if let existing = originalImages.first(where: { $0.data == data }) {
finalPaths.append(existing.path)
preservedPaths.insert(existing.path)
} else {
if let newPath = ImageManager.shared.saveImageData(data) {
finalPaths.append(newPath)
}
}
}
let newImagePaths = imagePaths.filter { !$0.isEmpty }
for original in originalImages {
if !preservedPaths.contains(original.path) {
_ = ImageManager.shared.deleteImage(atPath: original.path)
}
}
if isEditing, let problem = existingProblem {
let updatedProblem = problem.updated(
@@ -257,8 +403,9 @@ struct AddEditProblemView: View {
difficulty: DifficultyGrade(system: selectedDifficultySystem, grade: difficultyGrade),
tags: trimmedTags,
location: trimmedLocation.isEmpty ? nil : trimmedLocation,
imagePaths: newImagePaths.isEmpty ? [] : newImagePaths,
imagePaths: finalPaths,
isActive: isActive,
dateSet: dateSet,
notes: trimmedNotes.isEmpty ? nil : trimmedNotes
)
dataManager.updateProblem(updatedProblem)
@@ -272,8 +419,8 @@ struct AddEditProblemView: View {
difficulty: DifficultyGrade(system: selectedDifficultySystem, grade: difficultyGrade),
tags: trimmedTags,
location: trimmedLocation.isEmpty ? nil : trimmedLocation,
imagePaths: newImagePaths.isEmpty ? [] : newImagePaths,
dateSet: Date(),
imagePaths: finalPaths,
dateSet: dateSet,
notes: trimmedNotes.isEmpty ? nil : trimmedNotes
)
dataManager.addProblem(problem)