Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
77f7092287
|
|||
|
ed25cf7ecd
|
|||
|
255f85c2df
|
|||
|
a3d47d29c5
|
|||
|
b94b823986
|
|||
|
58d84af29b
|
|||
|
12f9463e8c
|
@@ -18,8 +18,8 @@ android {
|
|||||||
applicationId = "com.atridad.ascently"
|
applicationId = "com.atridad.ascently"
|
||||||
minSdk = 31
|
minSdk = 31
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 51
|
versionCode = 52
|
||||||
versionName = "2.5.1"
|
versionName = "2.5.2"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,19 +10,19 @@ androidxTestExt = "1.3.0"
|
|||||||
androidxTestRunner = "1.7.0"
|
androidxTestRunner = "1.7.0"
|
||||||
androidxTestRules = "1.7.0"
|
androidxTestRules = "1.7.0"
|
||||||
lifecycleRuntimeKtx = "2.10.0"
|
lifecycleRuntimeKtx = "2.10.0"
|
||||||
activityCompose = "1.12.2"
|
activityCompose = "1.12.3"
|
||||||
composeBom = "2025.12.01"
|
composeBom = "2026.01.01"
|
||||||
room = "2.8.4"
|
room = "2.8.4"
|
||||||
navigation = "2.9.6"
|
navigation = "2.9.7"
|
||||||
viewmodel = "2.10.0"
|
viewmodel = "2.10.0"
|
||||||
kotlinxSerialization = "1.9.0"
|
kotlinxSerialization = "1.10.0"
|
||||||
kotlinxCoroutines = "1.10.2"
|
kotlinxCoroutines = "1.10.2"
|
||||||
coil = "2.7.0"
|
coil = "2.7.0"
|
||||||
ksp = "2.2.20-2.0.3"
|
ksp = "2.2.20-2.0.3"
|
||||||
exifinterface = "1.4.2"
|
exifinterface = "1.4.2"
|
||||||
healthConnect = "1.1.0"
|
healthConnect = "1.1.0"
|
||||||
detekt = "1.23.8"
|
detekt = "1.23.8"
|
||||||
spotless = "8.1.0"
|
spotless = "8.2.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
|
||||||
|
|||||||
@@ -466,7 +466,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 50;
|
CURRENT_PROJECT_VERSION = 53;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
|
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -518,7 +518,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Ascently/Ascently.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 50;
|
CURRENT_PROJECT_VERSION = 53;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
|
DRIVERKIT_DEPLOYMENT_TARGET = 24.6;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
@@ -610,7 +610,7 @@
|
|||||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||||
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 50;
|
CURRENT_PROJECT_VERSION = 53;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
||||||
@@ -641,7 +641,7 @@
|
|||||||
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||||
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
CODE_SIGN_ENTITLEMENTS = SessionStatusLiveExtension.entitlements;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 50;
|
CURRENT_PROJECT_VERSION = 53;
|
||||||
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
DEVELOPMENT_TEAM = 4BC9Y2LL4B;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
INFOPLIST_FILE = SessionStatusLive/Info.plist;
|
||||||
|
|||||||
Binary file not shown.
@@ -373,35 +373,19 @@ struct AddAttemptView: View {
|
|||||||
Section("Additional Details") {
|
Section("Additional Details") {
|
||||||
TextField("Highest Hold (Optional)", text: $highestHold)
|
TextField("Highest Hold (Optional)", text: $highestHold)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
TextField("Notes (Optional)", text: $notes, axis: .vertical)
|
||||||
Text("Notes (Optional)")
|
.lineLimit(3...6)
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
TextEditor(text: $notes)
|
LabeledContent("Duration (seconds)") {
|
||||||
.frame(minHeight: 80)
|
|
||||||
.padding(8)
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 8)
|
|
||||||
.fill(.quaternary)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Text("Duration (seconds)")
|
|
||||||
Spacer()
|
|
||||||
TextField("0", value: $duration, format: .number)
|
TextField("0", value: $duration, format: .number)
|
||||||
.keyboardType(.numberPad)
|
.keyboardType(.numberPad)
|
||||||
.textFieldStyle(.roundedBorder)
|
.multilineTextAlignment(.trailing)
|
||||||
.frame(width: 80)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack {
|
LabeledContent("Rest Time (seconds)") {
|
||||||
Text("Rest Time (seconds)")
|
|
||||||
Spacer()
|
|
||||||
TextField("0", value: $restTime, format: .number)
|
TextField("0", value: $restTime, format: .number)
|
||||||
.keyboardType(.numberPad)
|
.keyboardType(.numberPad)
|
||||||
.textFieldStyle(.roundedBorder)
|
.multilineTextAlignment(.trailing)
|
||||||
.frame(width: 80)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -761,65 +745,14 @@ struct EditAttemptView: View {
|
|||||||
@EnvironmentObject var themeManager: ThemeManager
|
@EnvironmentObject var themeManager: ThemeManager
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
@State private var selectedProblem: Problem?
|
|
||||||
@State private var selectedResult: AttemptResult
|
@State private var selectedResult: AttemptResult
|
||||||
@State private var highestHold: String
|
@State private var highestHold: String
|
||||||
@State private var notes: String
|
@State private var notes: String
|
||||||
@State private var duration: Int
|
@State private var duration: Int
|
||||||
@State private var restTime: Int
|
@State private var restTime: Int
|
||||||
@State private var showingCreateProblem = false
|
|
||||||
|
|
||||||
// New problem creation state
|
private var problem: Problem? {
|
||||||
@State private var newProblemName = ""
|
dataManager.problem(withId: attempt.problemId)
|
||||||
@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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
init(attempt: Attempt) {
|
init(attempt: Attempt) {
|
||||||
@@ -834,12 +767,7 @@ struct EditAttemptView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
Form {
|
Form {
|
||||||
if !showingCreateProblem {
|
ProblemSection()
|
||||||
ProblemSelectionSection()
|
|
||||||
} else {
|
|
||||||
CreateProblemSection()
|
|
||||||
}
|
|
||||||
|
|
||||||
AttemptDetailsSection()
|
AttemptDetailsSection()
|
||||||
}
|
}
|
||||||
.navigationTitle("Edit Attempt")
|
.navigationTitle("Edit Attempt")
|
||||||
@@ -855,269 +783,35 @@ struct EditAttemptView: View {
|
|||||||
Button("Update") {
|
Button("Update") {
|
||||||
updateAttempt()
|
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
|
@ViewBuilder
|
||||||
private func ProblemSelectionSection() -> some View {
|
private func ProblemSection() -> some View {
|
||||||
Section("Select Problem") {
|
Section("Problem") {
|
||||||
if availableProblems.isEmpty {
|
if let problem = problem {
|
||||||
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
|
|
||||||
HStack {
|
HStack {
|
||||||
Text(climbType.displayName)
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Spacer()
|
Text(problem.name ?? "Unnamed Problem")
|
||||||
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")
|
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.foregroundColor(themeManager.accentColor)
|
HStack(spacing: 8) {
|
||||||
Text("\(imageData.count) of 5 photos added")
|
Text(problem.climbType.displayName)
|
||||||
.font(.caption)
|
.font(.subheadline)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
Text("•")
|
||||||
Spacer()
|
.foregroundColor(.secondary)
|
||||||
Image(systemName: "chevron.right")
|
Text(problem.difficulty.grade)
|
||||||
.foregroundColor(.secondary)
|
.font(.subheadline)
|
||||||
.font(.caption)
|
.foregroundColor(.secondary)
|
||||||
}
|
|
||||||
.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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 1)
|
Spacer()
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Text("Problem not found")
|
||||||
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1147,158 +841,33 @@ struct EditAttemptView: View {
|
|||||||
Section("Additional Details") {
|
Section("Additional Details") {
|
||||||
TextField("Highest Hold (Optional)", text: $highestHold)
|
TextField("Highest Hold (Optional)", text: $highestHold)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
TextField("Notes (Optional)", text: $notes, axis: .vertical)
|
||||||
Text("Notes (Optional)")
|
.lineLimit(3...6)
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
TextEditor(text: $notes)
|
LabeledContent("Duration (seconds)") {
|
||||||
.frame(minHeight: 80)
|
|
||||||
.padding(8)
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 8)
|
|
||||||
.fill(.quaternary)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack {
|
|
||||||
Text("Duration (seconds)")
|
|
||||||
Spacer()
|
|
||||||
TextField("0", value: $duration, format: .number)
|
TextField("0", value: $duration, format: .number)
|
||||||
.keyboardType(.numberPad)
|
.keyboardType(.numberPad)
|
||||||
.textFieldStyle(.roundedBorder)
|
.multilineTextAlignment(.trailing)
|
||||||
.frame(width: 80)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
HStack {
|
LabeledContent("Rest Time (seconds)") {
|
||||||
Text("Rest Time (seconds)")
|
|
||||||
Spacer()
|
|
||||||
TextField("0", value: $restTime, format: .number)
|
TextField("0", value: $restTime, format: .number)
|
||||||
.keyboardType(.numberPad)
|
.keyboardType(.numberPad)
|
||||||
.textFieldStyle(.roundedBorder)
|
.multilineTextAlignment(.trailing)
|
||||||
.frame(width: 80)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
private func updateAttempt() {
|
||||||
if showingCreateProblem {
|
let updatedAttempt = attempt.updated(
|
||||||
guard let gym = gym else { return }
|
result: selectedResult,
|
||||||
|
highestHold: highestHold.isEmpty ? nil : highestHold,
|
||||||
let difficulty = DifficultyGrade(
|
notes: notes.isEmpty ? nil : notes,
|
||||||
system: selectedDifficultySystem, grade: newProblemGrade)
|
duration: duration > 0 ? duration : nil,
|
||||||
|
restTime: restTime > 0 ? restTime : nil
|
||||||
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 = []
|
|
||||||
|
|
||||||
|
dataManager.updateAttempt(updatedAttempt)
|
||||||
dismiss()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ struct AddEditProblemView: View {
|
|||||||
@State private var tags = ""
|
@State private var tags = ""
|
||||||
@State private var notes = ""
|
@State private var notes = ""
|
||||||
@State private var isActive = true
|
@State private var isActive = true
|
||||||
|
@State private var dateSet = Date()
|
||||||
@State private var imagePaths: [String] = []
|
@State private var imagePaths: [String] = []
|
||||||
@State private var imageData: [Data] = []
|
@State private var imageData: [Data] = []
|
||||||
@State private var showingPhotoOptions = false
|
@State private var showingPhotoOptions = false
|
||||||
@@ -26,6 +27,9 @@ struct AddEditProblemView: View {
|
|||||||
@State private var showingImagePicker = false
|
@State private var showingImagePicker = false
|
||||||
@State private var imageSource: UIImagePickerController.SourceType = .photoLibrary
|
@State private var imageSource: UIImagePickerController.SourceType = .photoLibrary
|
||||||
@State private var isEditing = false
|
@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? {
|
private var existingProblem: Problem? {
|
||||||
guard let problemId = problemId else { return nil }
|
guard let problemId = problemId else { return nil }
|
||||||
@@ -52,6 +56,10 @@ struct AddEditProblemView: View {
|
|||||||
Form {
|
Form {
|
||||||
GymSelectionSection()
|
GymSelectionSection()
|
||||||
BasicInfoSection()
|
BasicInfoSection()
|
||||||
|
PhotosSection()
|
||||||
|
ClimbTypeSection()
|
||||||
|
DifficultySection()
|
||||||
|
LocationDetailsSection()
|
||||||
AdditionalInfoSection()
|
AdditionalInfoSection()
|
||||||
}
|
}
|
||||||
.navigationTitle(isEditing ? "Edit Problem" : "New Problem")
|
.navigationTitle(isEditing ? "Edit Problem" : "New Problem")
|
||||||
@@ -65,15 +73,22 @@ struct AddEditProblemView: View {
|
|||||||
|
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
Button("Save") {
|
Button("Save") {
|
||||||
saveProblem()
|
if canSave {
|
||||||
|
saveProblem()
|
||||||
|
} else {
|
||||||
|
showingGradeError = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.disabled(!canSave)
|
.disabled(!canSave && !showingGradeError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
setupInitialClimbType()
|
setupInitialClimbType()
|
||||||
loadExistingProblem()
|
loadExistingProblem()
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
isLoaded = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showingPhotoOptions) {
|
.sheet(isPresented: $showingPhotoOptions) {
|
||||||
PhotoOptionSheet(
|
PhotoOptionSheet(
|
||||||
@@ -155,43 +170,158 @@ struct AddEditProblemView: View {
|
|||||||
Section("Problem Details") {
|
Section("Problem Details") {
|
||||||
TextField("Problem Name (Optional)", text: $name)
|
TextField("Problem Name (Optional)", text: $name)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
TextField("Description (Optional)", text: $description, axis: .vertical)
|
||||||
Text("Description (Optional)")
|
.lineLimit(3...6)
|
||||||
.font(.headline)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TextEditor(text: $description)
|
@ViewBuilder
|
||||||
.frame(minHeight: 80)
|
private func PhotosSection() -> some View {
|
||||||
.padding(8)
|
Section("Photos (Optional)") {
|
||||||
.background(
|
Button(action: {
|
||||||
RoundedRectangle(cornerRadius: 8)
|
showingPhotoOptions = true
|
||||||
.fill(.quaternary)
|
}) {
|
||||||
)
|
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
|
@ViewBuilder
|
||||||
private func AdditionalInfoSection() -> some View {
|
private func ClimbTypeSection() -> some View {
|
||||||
Section("Additional Information") {
|
Section("Climb Type") {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
ForEach(ClimbType.allCases, id: \.self) { type in
|
||||||
Text("Notes (Optional)")
|
HStack {
|
||||||
.font(.headline)
|
Text(type.displayName)
|
||||||
|
Spacer()
|
||||||
TextEditor(text: $notes)
|
if selectedClimbType == type {
|
||||||
.frame(minHeight: 80)
|
Image(systemName: "checkmark.circle.fill")
|
||||||
.padding(8)
|
.foregroundColor(themeManager.accentColor)
|
||||||
.background(
|
} else {
|
||||||
RoundedRectangle(cornerRadius: 8)
|
Image(systemName: "circle")
|
||||||
.fill(.quaternary)
|
.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)
|
Toggle("Problem is currently active", isOn: $isActive)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var canSave: Bool {
|
private var canSave: Bool {
|
||||||
selectedGym != nil && difficultyGrade.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
|
selectedGym != nil && !difficultyGrade.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupInitialClimbType() {
|
private func setupInitialClimbType() {
|
||||||
@@ -199,7 +329,6 @@ struct AddEditProblemView: View {
|
|||||||
selectedGym = dataManager.gym(withId: gymId)
|
selectedGym = dataManager.gym(withId: gymId)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always ensure a gym is selected if available and none is currently selected
|
|
||||||
if selectedGym == nil && !dataManager.gyms.isEmpty {
|
if selectedGym == nil && !dataManager.gyms.isEmpty {
|
||||||
selectedGym = dataManager.gyms.first
|
selectedGym = dataManager.gyms.first
|
||||||
}
|
}
|
||||||
@@ -219,13 +348,17 @@ struct AddEditProblemView: View {
|
|||||||
tags = problem.tags.joined(separator: ", ")
|
tags = problem.tags.joined(separator: ", ")
|
||||||
notes = problem.notes ?? ""
|
notes = problem.notes ?? ""
|
||||||
isActive = problem.isActive
|
isActive = problem.isActive
|
||||||
|
if let date = problem.dateSet {
|
||||||
|
dateSet = date
|
||||||
|
}
|
||||||
imagePaths = problem.imagePaths
|
imagePaths = problem.imagePaths
|
||||||
|
|
||||||
// Load image data for preview
|
|
||||||
imageData = []
|
imageData = []
|
||||||
|
originalImages = []
|
||||||
for imagePath in problem.imagePaths {
|
for imagePath in problem.imagePaths {
|
||||||
if let data = ImageManager.shared.loadImageData(fromPath: imagePath) {
|
if let data = ImageManager.shared.loadImageData(fromPath: imagePath) {
|
||||||
imageData.append(data)
|
imageData.append(data)
|
||||||
|
originalImages.append((path: imagePath, data: data))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,12 +375,25 @@ struct AddEditProblemView: View {
|
|||||||
$0.trimmingCharacters(in: .whitespacesAndNewlines)
|
$0.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
}.filter { !$0.isEmpty }
|
}.filter { !$0.isEmpty }
|
||||||
|
|
||||||
let tempImagePaths = imagePaths.filter { !$0.isEmpty && !imagePaths.contains($0) }
|
var finalPaths: [String] = []
|
||||||
for imagePath in tempImagePaths {
|
var preservedPaths: Set<String> = []
|
||||||
_ = ImageManager.shared.deleteImage(atPath: imagePath)
|
|
||||||
|
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 {
|
if isEditing, let problem = existingProblem {
|
||||||
let updatedProblem = problem.updated(
|
let updatedProblem = problem.updated(
|
||||||
@@ -257,8 +403,9 @@ struct AddEditProblemView: View {
|
|||||||
difficulty: DifficultyGrade(system: selectedDifficultySystem, grade: difficultyGrade),
|
difficulty: DifficultyGrade(system: selectedDifficultySystem, grade: difficultyGrade),
|
||||||
tags: trimmedTags,
|
tags: trimmedTags,
|
||||||
location: trimmedLocation.isEmpty ? nil : trimmedLocation,
|
location: trimmedLocation.isEmpty ? nil : trimmedLocation,
|
||||||
imagePaths: newImagePaths.isEmpty ? [] : newImagePaths,
|
imagePaths: finalPaths,
|
||||||
isActive: isActive,
|
isActive: isActive,
|
||||||
|
dateSet: dateSet,
|
||||||
notes: trimmedNotes.isEmpty ? nil : trimmedNotes
|
notes: trimmedNotes.isEmpty ? nil : trimmedNotes
|
||||||
)
|
)
|
||||||
dataManager.updateProblem(updatedProblem)
|
dataManager.updateProblem(updatedProblem)
|
||||||
@@ -272,8 +419,8 @@ struct AddEditProblemView: View {
|
|||||||
difficulty: DifficultyGrade(system: selectedDifficultySystem, grade: difficultyGrade),
|
difficulty: DifficultyGrade(system: selectedDifficultySystem, grade: difficultyGrade),
|
||||||
tags: trimmedTags,
|
tags: trimmedTags,
|
||||||
location: trimmedLocation.isEmpty ? nil : trimmedLocation,
|
location: trimmedLocation.isEmpty ? nil : trimmedLocation,
|
||||||
imagePaths: newImagePaths.isEmpty ? [] : newImagePaths,
|
imagePaths: finalPaths,
|
||||||
dateSet: Date(),
|
dateSet: dateSet,
|
||||||
notes: trimmedNotes.isEmpty ? nil : trimmedNotes
|
notes: trimmedNotes.isEmpty ? nil : trimmedNotes
|
||||||
)
|
)
|
||||||
dataManager.addProblem(problem)
|
dataManager.addProblem(problem)
|
||||||
|
|||||||
Reference in New Issue
Block a user