diff --git a/ios/Ascently.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate b/ios/Ascently.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate index 4891551..255aac5 100644 Binary files a/ios/Ascently.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate and b/ios/Ascently.xcodeproj/project.xcworkspace/xcuserdata/atridad.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/ios/Ascently/Views/AddEdit/AddAttemptView.swift b/ios/Ascently/Views/AddEdit/AddAttemptView.swift index d36f91a..f0abbf3 100644 --- a/ios/Ascently/Views/AddEdit/AddAttemptView.swift +++ b/ios/Ascently/Views/AddEdit/AddAttemptView.swift @@ -745,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) { @@ -818,12 +767,7 @@ struct EditAttemptView: View { var body: some View { NavigationStack { Form { - if !showingCreateProblem { - ProblemSelectionSection() - } else { - CreateProblemSection() - } - + ProblemSection() AttemptDetailsSection() } .navigationTitle("Edit Attempt") @@ -839,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) } } } @@ -1148,125 +858,16 @@ struct EditAttemptView: View { } } - 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() } }