Fixed add/edit problems
This commit is contained in:
Binary file not shown.
@@ -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) { _ 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)
|
||||
|
||||
Reference in New Issue
Block a user