O p t i m i z e
This commit is contained in:
Binary file not shown.
52
ios/Ascently/Components/ImagePicker.swift
Normal file
52
ios/Ascently/Components/ImagePicker.swift
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import PhotosUI
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ImagePicker: UIViewControllerRepresentable {
|
||||||
|
@Binding var selectedImages: [Data]
|
||||||
|
let sourceType: UIImagePickerController.SourceType
|
||||||
|
let selectionLimit: Int
|
||||||
|
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
func makeUIViewController(context: Context) -> UIImagePickerController {
|
||||||
|
let picker = UIImagePickerController()
|
||||||
|
picker.delegate = context.coordinator
|
||||||
|
picker.sourceType = sourceType
|
||||||
|
picker.allowsEditing = false
|
||||||
|
if sourceType == .photoLibrary {
|
||||||
|
picker.modalPresentationStyle = .automatic
|
||||||
|
}
|
||||||
|
return picker
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {
|
||||||
|
// No-op
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCoordinator() -> Coordinator {
|
||||||
|
Coordinator(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
|
||||||
|
let parent: ImagePicker
|
||||||
|
|
||||||
|
init(_ parent: ImagePicker) {
|
||||||
|
self.parent = parent
|
||||||
|
}
|
||||||
|
|
||||||
|
func imagePickerController(
|
||||||
|
_ picker: UIImagePickerController,
|
||||||
|
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]
|
||||||
|
) {
|
||||||
|
if let image = info[.originalImage] as? UIImage,
|
||||||
|
let data = image.jpegData(compressionQuality: 0.8) {
|
||||||
|
parent.selectedImages.append(data)
|
||||||
|
}
|
||||||
|
parent.dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
|
||||||
|
parent.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
99
ios/Ascently/Utils/AppExtensions.swift
Normal file
99
ios/Ascently/Utils/AppExtensions.swift
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
enum AppSettings {
|
||||||
|
enum Keys {
|
||||||
|
static let accentColor = "accentColorData"
|
||||||
|
static let syncServerURL = "sync_server_url"
|
||||||
|
static let syncAuthToken = "sync_auth_token"
|
||||||
|
static let lastSyncTime = "last_sync_time"
|
||||||
|
static let syncIsConnected = "is_connected"
|
||||||
|
static let autoSyncEnabled = "auto_sync_enabled"
|
||||||
|
static let offlineMode = "offline_mode"
|
||||||
|
static let syncProviderType = "sync_provider_type"
|
||||||
|
static let healthKitEnabled = "healthkit_enabled"
|
||||||
|
static let autoBackupEnabled = "auto_backup_enabled"
|
||||||
|
static let lastBackupTime = "last_backup_time"
|
||||||
|
static let defaultClimbType = "default_climb_type"
|
||||||
|
static let defaultDifficultySystem = "default_difficulty_system"
|
||||||
|
}
|
||||||
|
|
||||||
|
static func set<T>(_ value: T, forKey key: String) {
|
||||||
|
UserDefaults.standard.set(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func get<T>(_ type: T.Type, forKey key: String, defaultValue: T) -> T {
|
||||||
|
guard let value = UserDefaults.standard.object(forKey: key) as? T else {
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
static func remove(forKey key: String) {
|
||||||
|
UserDefaults.standard.removeObject(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func getString(forKey key: String, defaultValue: String = "") -> String {
|
||||||
|
return UserDefaults.standard.string(forKey: key) ?? defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
static func setString(_ value: String, forKey key: String) {
|
||||||
|
UserDefaults.standard.set(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func getBool(forKey key: String, defaultValue: Bool = false) -> Bool {
|
||||||
|
return UserDefaults.standard.bool(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func setBool(_ value: Bool, forKey key: String) {
|
||||||
|
UserDefaults.standard.set(value, forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func getDate(forKey key: String) -> Date? {
|
||||||
|
return UserDefaults.standard.object(forKey: key) as? Date
|
||||||
|
}
|
||||||
|
|
||||||
|
static func setDate(_ value: Date?, forKey key: String) {
|
||||||
|
if let date = value {
|
||||||
|
UserDefaults.standard.set(date, forKey: key)
|
||||||
|
} else {
|
||||||
|
UserDefaults.standard.removeObject(forKey: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum AppError: LocalizedError {
|
||||||
|
case validationFailed(String)
|
||||||
|
case dataCorruption(String)
|
||||||
|
case syncFailed(String)
|
||||||
|
case networkError(String)
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .validationFailed(let message):
|
||||||
|
return "Validation failed: \(message)"
|
||||||
|
case .dataCorruption(let message):
|
||||||
|
return "Data corruption: \(message)"
|
||||||
|
case .syncFailed(let message):
|
||||||
|
return "Sync failed: \(message)"
|
||||||
|
case .networkError(let message):
|
||||||
|
return "Network error: \(message)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension View {
|
||||||
|
func errorMessage(_ error: AppError?) -> some View {
|
||||||
|
Group {
|
||||||
|
if let error = error {
|
||||||
|
Text(error.localizedDescription)
|
||||||
|
.foregroundColor(.red)
|
||||||
|
.font(.caption)
|
||||||
|
.padding(.horizontal)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.background(Color.red.opacity(0.1))
|
||||||
|
.cornerRadius(4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
ios/Ascently/Utils/DataHelpers.swift
Normal file
18
ios/Ascently/Utils/DataHelpers.swift
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct DataHelper {
|
||||||
|
static func trimString(_ string: String) -> String {
|
||||||
|
string.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func isEmptyOrNil(_ string: String) -> String? {
|
||||||
|
let trimmed = trimString(string)
|
||||||
|
return trimmed.isEmpty ? nil : trimmed
|
||||||
|
}
|
||||||
|
|
||||||
|
static func splitTags(_ tags: String) -> [String] {
|
||||||
|
return tags.split(separator: ",")
|
||||||
|
.compactMap { trimString(String($0)) }
|
||||||
|
.filter { !$0.isEmpty }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
import PhotosUI
|
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import PhotosUI
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
struct AddEditProblemView: View {
|
struct AddEditProblemView: View {
|
||||||
let problemId: UUID?
|
let problemId: UUID?
|
||||||
let gymId: UUID?
|
|
||||||
@EnvironmentObject var dataManager: ClimbingDataManager
|
@EnvironmentObject var dataManager: ClimbingDataManager
|
||||||
@EnvironmentObject var themeManager: ThemeManager
|
@EnvironmentObject var themeManager: ThemeManager
|
||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
@@ -11,63 +11,40 @@ struct AddEditProblemView: View {
|
|||||||
@State private var selectedGym: Gym?
|
@State private var selectedGym: Gym?
|
||||||
@State private var name = ""
|
@State private var name = ""
|
||||||
@State private var description = ""
|
@State private var description = ""
|
||||||
@State private var selectedClimbType: ClimbType = .boulder
|
@State private var selectedClimbType: ClimbType
|
||||||
@State private var selectedDifficultySystem: DifficultySystem = .vScale
|
@State private var selectedDifficultySystem: DifficultySystem
|
||||||
@State private var difficultyGrade = ""
|
@State private var difficultyGrade = ""
|
||||||
|
@State private var availableDifficultySystems: [DifficultySystem] = []
|
||||||
@State private var location = ""
|
@State private var location = ""
|
||||||
@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 selectedPhotos: [PhotosPickerItem] = []
|
|
||||||
@State private var imageData: [Data] = []
|
@State private var imageData: [Data] = []
|
||||||
|
@State private var showingPhotoOptions = false
|
||||||
|
@State private var showingCamera = false
|
||||||
|
@State private var showingImagePicker = false
|
||||||
|
@State private var imageSource: UIImagePickerController.SourceType = .photoLibrary
|
||||||
@State private var isEditing = false
|
@State private var isEditing = false
|
||||||
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 existingProblem: Problem? {
|
private var existingProblem: Problem? {
|
||||||
guard let problemId = problemId else { return nil }
|
guard let problemId = problemId else { return nil }
|
||||||
return dataManager.problem(withId: problemId)
|
return dataManager.problem(withId: problemId)
|
||||||
}
|
}
|
||||||
|
|
||||||
private var availableClimbTypes: [ClimbType] {
|
private var existingProblemGym: Gym? {
|
||||||
selectedGym?.supportedClimbTypes ?? ClimbType.allCases
|
guard let problem = existingProblem else { return nil }
|
||||||
|
return dataManager.gym(withId: problem.gymId)
|
||||||
}
|
}
|
||||||
|
|
||||||
var availableDifficultySystems: [DifficultySystem] {
|
private var gymId: UUID? {
|
||||||
guard let gym = selectedGym else {
|
return selectedGym?.id ?? existingProblemGym?.id
|
||||||
return DifficultySystem.systemsForClimbType(selectedClimbType)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let compatibleSystems = DifficultySystem.systemsForClimbType(selectedClimbType)
|
init(problemId: UUID? = nil) {
|
||||||
let gymSupportedSystems = gym.difficultySystems.filter { system in
|
|
||||||
compatibleSystems.contains(system)
|
|
||||||
}
|
|
||||||
|
|
||||||
return gymSupportedSystems.isEmpty ? compatibleSystems : gymSupportedSystems
|
|
||||||
}
|
|
||||||
|
|
||||||
private var availableGrades: [String] {
|
|
||||||
selectedDifficultySystem.availableGrades
|
|
||||||
}
|
|
||||||
|
|
||||||
init(problemId: UUID? = nil, gymId: UUID? = nil) {
|
|
||||||
self.problemId = problemId
|
self.problemId = problemId
|
||||||
self.gymId = gymId
|
self._selectedClimbType = State(initialValue: .boulder)
|
||||||
|
self._selectedDifficultySystem = State(initialValue: .vScale)
|
||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -75,14 +52,9 @@ struct AddEditProblemView: View {
|
|||||||
Form {
|
Form {
|
||||||
GymSelectionSection()
|
GymSelectionSection()
|
||||||
BasicInfoSection()
|
BasicInfoSection()
|
||||||
PhotosSection()
|
|
||||||
ClimbTypeSection()
|
|
||||||
DifficultySection()
|
|
||||||
LocationSection()
|
|
||||||
TagsSection()
|
|
||||||
AdditionalInfoSection()
|
AdditionalInfoSection()
|
||||||
}
|
}
|
||||||
.navigationTitle(isEditing ? "Edit Problem" : "Add Problem")
|
.navigationTitle(isEditing ? "Edit Problem" : "New Problem")
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.toolbar {
|
.toolbar {
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
@@ -100,73 +72,41 @@ struct AddEditProblemView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
setupInitialClimbType()
|
||||||
loadExistingProblem()
|
loadExistingProblem()
|
||||||
setupInitialGym()
|
|
||||||
}
|
}
|
||||||
.onChange(of: dataManager.gyms) {
|
.sheet(isPresented: $showingPhotoOptions) {
|
||||||
if selectedGym == nil && !dataManager.gyms.isEmpty {
|
|
||||||
selectedGym = dataManager.gyms.first
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onChange(of: selectedGym) {
|
|
||||||
updateAvailableOptions()
|
|
||||||
}
|
|
||||||
.onChange(of: selectedClimbType) {
|
|
||||||
updateDifficultySystem()
|
|
||||||
}
|
|
||||||
.onChange(of: selectedDifficultySystem) {
|
|
||||||
resetGradeIfNeeded()
|
|
||||||
}
|
|
||||||
.sheet(
|
|
||||||
item: $activeSheet,
|
|
||||||
onDismiss: {
|
|
||||||
if isCameraActionPending {
|
|
||||||
showCamera = true
|
|
||||||
isCameraActionPending = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if isPhotoPickerActionPending {
|
|
||||||
showPhotoPicker = true
|
|
||||||
isPhotoPickerActionPending = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) { sheetType in
|
|
||||||
switch sheetType {
|
|
||||||
case .photoOptions:
|
|
||||||
PhotoOptionSheet(
|
PhotoOptionSheet(
|
||||||
selectedPhotos: $selectedPhotos,
|
selectedPhotos: .constant([]),
|
||||||
imageData: $imageData,
|
imageData: $imageData,
|
||||||
maxImages: 5,
|
maxImages: 5,
|
||||||
onCameraSelected: {
|
onCameraSelected: {
|
||||||
isCameraActionPending = true
|
showingCamera = true
|
||||||
activeSheet = nil
|
|
||||||
},
|
},
|
||||||
onPhotoLibrarySelected: {
|
onPhotoLibrarySelected: {
|
||||||
isPhotoPickerActionPending = true
|
showingImagePicker = true
|
||||||
},
|
},
|
||||||
onDismiss: {
|
onDismiss: {
|
||||||
activeSheet = nil
|
showingPhotoOptions = false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
.sheet(isPresented: $showingCamera) {
|
||||||
.fullScreenCover(isPresented: $showCamera) {
|
CameraImagePicker { image in
|
||||||
CameraImagePicker { capturedImage in
|
if let data = image.jpegData(compressionQuality: 0.8) {
|
||||||
if let jpegData = capturedImage.jpegData(compressionQuality: 0.8) {
|
imageData.append(data)
|
||||||
imageData.append(jpegData)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.photosPicker(
|
.sheet(isPresented: $showingImagePicker) {
|
||||||
isPresented: $showPhotoPicker,
|
ImagePicker(
|
||||||
selection: $selectedPhotos,
|
selectedImages: Binding(
|
||||||
maxSelectionCount: 5 - imageData.count,
|
get: { imageData },
|
||||||
matching: .images
|
set: { imageData = $0 }
|
||||||
|
),
|
||||||
|
sourceType: imageSource,
|
||||||
|
selectionLimit: 5
|
||||||
)
|
)
|
||||||
.onChange(of: selectedPhotos) {
|
|
||||||
Task {
|
|
||||||
await loadSelectedPhotos()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,6 +118,16 @@ struct AddEditProblemView: View {
|
|||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
} else {
|
} else {
|
||||||
ForEach(dataManager.gyms, id: \.id) { gym in
|
ForEach(dataManager.gyms, id: \.id) { gym in
|
||||||
|
gymRow(gym: gym, isSelected: selectedGym?.id == gym.id)
|
||||||
|
.onTapGesture {
|
||||||
|
selectedGym = gym
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func gymRow(gym: Gym, isSelected: Bool) -> some View {
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(gym.name)
|
Text(gym.name)
|
||||||
@@ -192,18 +142,12 @@ struct AddEditProblemView: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
if selectedGym?.id == gym.id {
|
if isSelected {
|
||||||
Image(systemName: "checkmark.circle.fill")
|
Image(systemName: "checkmark.circle.fill")
|
||||||
.foregroundColor(themeManager.accentColor)
|
.foregroundColor(themeManager.accentColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {
|
|
||||||
selectedGym = gym
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
@@ -226,217 +170,6 @@ struct AddEditProblemView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private func ClimbTypeSection() -> some View {
|
|
||||||
if selectedGym != nil {
|
|
||||||
Section("Climb Type") {
|
|
||||||
ForEach(availableClimbTypes, id: \.self) { climbType in
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private func DifficultySection() -> some View {
|
|
||||||
Section("Difficulty") {
|
|
||||||
// Difficulty System
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
Text("Difficulty System")
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Grade Selection
|
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
|
||||||
Text("Grade (Required)")
|
|
||||||
.font(.headline)
|
|
||||||
|
|
||||||
if selectedDifficultySystem == .custom || availableGrades.isEmpty {
|
|
||||||
TextField("Enter custom grade (numbers only)", text: $difficultyGrade)
|
|
||||||
.textFieldStyle(.roundedBorder)
|
|
||||||
.keyboardType(.numberPad)
|
|
||||||
.onChange(of: difficultyGrade) {
|
|
||||||
// Filter out non-numeric characters
|
|
||||||
difficultyGrade = difficultyGrade.filter { $0.isNumber }
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Menu {
|
|
||||||
if !difficultyGrade.isEmpty {
|
|
||||||
Button("Clear Selection") {
|
|
||||||
difficultyGrade = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
Divider()
|
|
||||||
}
|
|
||||||
|
|
||||||
ForEach(availableGrades, id: \.self) { grade in
|
|
||||||
Button(grade) {
|
|
||||||
difficultyGrade = grade
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} label: {
|
|
||||||
HStack {
|
|
||||||
Text(difficultyGrade.isEmpty ? "Select Grade" : difficultyGrade)
|
|
||||||
.foregroundColor(difficultyGrade.isEmpty ? .secondary : .primary)
|
|
||||||
.fontWeight(difficultyGrade.isEmpty ? .regular : .semibold)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Image(systemName: "chevron.down")
|
|
||||||
.foregroundColor(.secondary)
|
|
||||||
.font(.caption)
|
|
||||||
}
|
|
||||||
.padding()
|
|
||||||
.background(
|
|
||||||
RoundedRectangle(cornerRadius: 8)
|
|
||||||
.fill(.gray.opacity(0.1))
|
|
||||||
.stroke(
|
|
||||||
difficultyGrade.isEmpty
|
|
||||||
? .red.opacity(0.5) : .gray.opacity(0.3), lineWidth: 1)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
}
|
|
||||||
|
|
||||||
if difficultyGrade.isEmpty {
|
|
||||||
Text("Please select a grade to continue")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(.red)
|
|
||||||
.italic()
|
|
||||||
} else {
|
|
||||||
Text("Selected: \(difficultyGrade)")
|
|
||||||
.font(.caption)
|
|
||||||
.foregroundColor(themeManager.accentColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private func LocationSection() -> some View {
|
|
||||||
Section("Location & Details") {
|
|
||||||
TextField(
|
|
||||||
"Location (Optional)", text: $location, prompt: Text("e.g., 'Cave area', 'Wall 3'"))
|
|
||||||
|
|
||||||
DatePicker(
|
|
||||||
"Date Set",
|
|
||||||
selection: $dateSet,
|
|
||||||
displayedComponents: [.date]
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private func TagsSection() -> some View {
|
|
||||||
Section("Tags (Optional)") {
|
|
||||||
TextField("Tags", text: $tags, prompt: Text("e.g., crimpy, dynamic (comma-separated)"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
|
||||||
private func PhotosSection() -> some View {
|
|
||||||
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)
|
|
||||||
.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]) {
|
|
||||||
ZStack(alignment: .topTrailing) {
|
|
||||||
Image(uiImage: uiImage)
|
|
||||||
.resizable()
|
|
||||||
.aspectRatio(contentMode: .fill)
|
|
||||||
.frame(width: 80, height: 80)
|
|
||||||
.clipped()
|
|
||||||
.cornerRadius(8)
|
|
||||||
|
|
||||||
Button(action: {
|
|
||||||
imageData.remove(at: index)
|
|
||||||
if index < imagePaths.count {
|
|
||||||
imagePaths.remove(at: index)
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Image(systemName: "xmark.circle.fill")
|
|
||||||
.foregroundColor(.red)
|
|
||||||
.background(Circle().fill(.white))
|
|
||||||
.font(.system(size: 18))
|
|
||||||
}
|
|
||||||
.offset(x: 4, y: -4)
|
|
||||||
}
|
|
||||||
.frame(width: 88, height: 88) // Extra space for button
|
|
||||||
} else {
|
|
||||||
RoundedRectangle(cornerRadius: 8)
|
|
||||||
.fill(.gray.opacity(0.3))
|
|
||||||
.frame(width: 80, height: 80)
|
|
||||||
.overlay {
|
|
||||||
Image(systemName: "photo")
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 1)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func AdditionalInfoSection() -> some View {
|
private func AdditionalInfoSection() -> some View {
|
||||||
Section("Additional Information") {
|
Section("Additional Information") {
|
||||||
@@ -458,11 +191,10 @@ struct AddEditProblemView: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var canSave: Bool {
|
private var canSave: Bool {
|
||||||
selectedGym != nil
|
selectedGym != nil && difficultyGrade.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false
|
||||||
&& !difficultyGrade.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func setupInitialGym() {
|
private func setupInitialClimbType() {
|
||||||
if let gymId = gymId {
|
if let gymId = gymId {
|
||||||
selectedGym = dataManager.gym(withId: gymId)
|
selectedGym = dataManager.gym(withId: gymId)
|
||||||
}
|
}
|
||||||
@@ -496,140 +228,59 @@ struct AddEditProblemView: View {
|
|||||||
imageData.append(data)
|
imageData.append(data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let dateSet = problem.dateSet {
|
|
||||||
self.dateSet = dateSet
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private func updateAvailableOptions() {
|
|
||||||
guard let gym = selectedGym else { return }
|
|
||||||
|
|
||||||
// Auto-select climb type if there's only one available
|
|
||||||
if gym.supportedClimbTypes.count == 1, selectedClimbType != gym.supportedClimbTypes.first! {
|
|
||||||
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! {
|
|
||||||
selectedDifficultySystem = available.first!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func resetGradeIfNeeded() {
|
|
||||||
let availableGrades = selectedDifficultySystem.availableGrades
|
|
||||||
if !availableGrades.isEmpty && !availableGrades.contains(difficultyGrade) {
|
|
||||||
difficultyGrade = ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func loadSelectedPhotos() async {
|
|
||||||
for item in selectedPhotos {
|
|
||||||
if let data = try? await item.loadTransferable(type: Data.self) {
|
|
||||||
imageData.append(data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
selectedPhotos.removeAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func saveProblem() {
|
private func saveProblem() {
|
||||||
guard let gym = selectedGym, canSave else { return }
|
guard let gym = selectedGym else { return }
|
||||||
|
|
||||||
let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmedName = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let trimmedDescription = description.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmedDescription = description.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
|
|
||||||
let trimmedLocation = location.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmedLocation = location.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let trimmedNotes = notes.trimmingCharacters(in: .whitespacesAndNewlines)
|
let trimmedNotes = notes.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
let trimmedTags = tags.split(separator: ",").map {
|
let trimmedTags = tags.split(separator: ",").map {
|
||||||
$0.trimmingCharacters(in: .whitespacesAndNewlines)
|
$0.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
}.filter { !$0.isEmpty }
|
}.filter { !$0.isEmpty }
|
||||||
|
|
||||||
let difficulty = DifficultyGrade(system: selectedDifficultySystem, grade: difficultyGrade)
|
let tempImagePaths = imagePaths.filter { !$0.isEmpty && !imagePaths.contains($0) }
|
||||||
|
for imagePath in tempImagePaths {
|
||||||
|
ImageManager.shared.deleteImage(atPath: imagePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
let newImagePaths = imagePaths.filter { !$0.isEmpty }
|
||||||
|
|
||||||
if isEditing, let problem = existingProblem {
|
if isEditing, let problem = existingProblem {
|
||||||
var allImagePaths = imagePaths
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let updatedProblem = problem.updated(
|
let updatedProblem = problem.updated(
|
||||||
name: trimmedName.isEmpty ? nil : trimmedName,
|
name: trimmedName.isEmpty ? nil : trimmedName,
|
||||||
description: trimmedDescription.isEmpty ? nil : trimmedDescription,
|
description: trimmedDescription.isEmpty ? nil : trimmedDescription,
|
||||||
climbType: selectedClimbType,
|
climbType: selectedClimbType,
|
||||||
difficulty: difficulty,
|
difficulty: DifficultyGrade(system: selectedDifficultySystem, grade: difficultyGrade),
|
||||||
|
|
||||||
tags: trimmedTags,
|
tags: trimmedTags,
|
||||||
location: trimmedLocation.isEmpty ? nil : trimmedLocation,
|
location: trimmedLocation.isEmpty ? nil : trimmedLocation,
|
||||||
imagePaths: allImagePaths,
|
imagePaths: newImagePaths.isEmpty ? [] : newImagePaths,
|
||||||
isActive: isActive,
|
isActive: isActive,
|
||||||
dateSet: dateSet,
|
|
||||||
notes: trimmedNotes.isEmpty ? nil : trimmedNotes
|
notes: trimmedNotes.isEmpty ? nil : trimmedNotes
|
||||||
)
|
)
|
||||||
dataManager.updateProblem(updatedProblem)
|
dataManager.updateProblem(updatedProblem)
|
||||||
|
dismiss()
|
||||||
} else {
|
} else {
|
||||||
let newProblem = Problem(
|
let problem = Problem(
|
||||||
gymId: gym.id,
|
gymId: gym.id,
|
||||||
name: trimmedName.isEmpty ? nil : trimmedName,
|
name: trimmedName.isEmpty ? nil : trimmedName,
|
||||||
description: trimmedDescription.isEmpty ? nil : trimmedDescription,
|
description: trimmedDescription.isEmpty ? nil : trimmedDescription,
|
||||||
climbType: selectedClimbType,
|
climbType: selectedClimbType,
|
||||||
difficulty: difficulty,
|
difficulty: DifficultyGrade(system: selectedDifficultySystem, grade: difficultyGrade),
|
||||||
|
|
||||||
tags: trimmedTags,
|
tags: trimmedTags,
|
||||||
location: trimmedLocation.isEmpty ? nil : trimmedLocation,
|
location: trimmedLocation.isEmpty ? nil : trimmedLocation,
|
||||||
imagePaths: [],
|
imagePaths: newImagePaths.isEmpty ? [] : newImagePaths,
|
||||||
dateSet: dateSet,
|
dateSet: Date(),
|
||||||
notes: trimmedNotes.isEmpty ? nil : trimmedNotes
|
notes: trimmedNotes.isEmpty ? nil : trimmedNotes
|
||||||
)
|
)
|
||||||
|
dataManager.addProblem(problem)
|
||||||
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()
|
dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
AddEditProblemView()
|
AddEditProblemView()
|
||||||
|
|||||||
@@ -56,6 +56,16 @@ struct AddEditSessionView: View {
|
|||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
} else {
|
} else {
|
||||||
ForEach(dataManager.gyms, id: \.id) { gym in
|
ForEach(dataManager.gyms, id: \.id) { gym in
|
||||||
|
gymRow(gym: gym, isSelected: selectedGym?.id == gym.id)
|
||||||
|
.onTapGesture {
|
||||||
|
selectedGym = gym
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func gymRow(gym: Gym, isSelected: Bool) -> some View {
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(gym.name)
|
Text(gym.name)
|
||||||
@@ -70,18 +80,12 @@ struct AddEditSessionView: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
if selectedGym?.id == gym.id {
|
if isSelected {
|
||||||
Image(systemName: "checkmark.circle.fill")
|
Image(systemName: "checkmark.circle.fill")
|
||||||
.foregroundColor(themeManager.accentColor)
|
.foregroundColor(themeManager.accentColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {
|
|
||||||
selectedGym = gym
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ struct CalendarView: View {
|
|||||||
if let activeSession = dataManager.activeSession,
|
if let activeSession = dataManager.activeSession,
|
||||||
let gym = dataManager.gym(withId: activeSession.gymId)
|
let gym = dataManager.gym(withId: activeSession.gymId)
|
||||||
{
|
{
|
||||||
ActiveSessionBanner(session: activeSession, gym: gym)
|
ActiveSessionBanner(session: activeSession, gym: gym, onNavigateToSession: onNavigateToSession)
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.padding(.top, 8)
|
.padding(.top, 8)
|
||||||
.padding(.bottom, 16)
|
.padding(.bottom, 16)
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ struct SessionsView: View {
|
|||||||
EmptySessionsView()
|
EmptySessionsView()
|
||||||
} else {
|
} else {
|
||||||
if viewMode == .list {
|
if viewMode == .list {
|
||||||
SessionsList()
|
SessionsList(onNavigateToSession: { sessionId in
|
||||||
|
selectedSessionId = sessionId
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
CalendarView(
|
CalendarView(
|
||||||
sessions: completedSessions,
|
sessions: completedSessions,
|
||||||
@@ -108,6 +110,7 @@ struct SessionsView: View {
|
|||||||
struct SessionsList: View {
|
struct SessionsList: View {
|
||||||
@EnvironmentObject var dataManager: ClimbingDataManager
|
@EnvironmentObject var dataManager: ClimbingDataManager
|
||||||
@State private var sessionToDelete: ClimbSession?
|
@State private var sessionToDelete: ClimbSession?
|
||||||
|
var onNavigateToSession: (UUID) -> Void
|
||||||
|
|
||||||
private var completedSessions: [ClimbSession] {
|
private var completedSessions: [ClimbSession] {
|
||||||
dataManager.sessions
|
dataManager.sessions
|
||||||
@@ -121,7 +124,11 @@ struct SessionsList: View {
|
|||||||
let gym = dataManager.gym(withId: activeSession.gymId)
|
let gym = dataManager.gym(withId: activeSession.gymId)
|
||||||
{
|
{
|
||||||
Section {
|
Section {
|
||||||
ActiveSessionBanner(session: activeSession, gym: gym)
|
ActiveSessionBanner(
|
||||||
|
session: activeSession,
|
||||||
|
gym: gym,
|
||||||
|
onNavigateToSession: onNavigateToSession
|
||||||
|
)
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.listRowInsets(EdgeInsets(top: 16, leading: 0, bottom: 24, trailing: 0))
|
.listRowInsets(EdgeInsets(top: 16, leading: 0, bottom: 24, trailing: 0))
|
||||||
.listRowBackground(Color.clear)
|
.listRowBackground(Color.clear)
|
||||||
@@ -183,8 +190,7 @@ struct ActiveSessionBanner: View {
|
|||||||
let session: ClimbSession
|
let session: ClimbSession
|
||||||
let gym: Gym
|
let gym: Gym
|
||||||
@EnvironmentObject var dataManager: ClimbingDataManager
|
@EnvironmentObject var dataManager: ClimbingDataManager
|
||||||
|
var onNavigateToSession: (UUID) -> Void
|
||||||
@State private var navigateToDetail = false
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
@@ -214,7 +220,7 @@ struct ActiveSessionBanner: View {
|
|||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
navigateToDetail = true
|
onNavigateToSession(session.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
@@ -237,9 +243,7 @@ struct ActiveSessionBanner: View {
|
|||||||
.fill(.green.opacity(0.1))
|
.fill(.green.opacity(0.1))
|
||||||
.stroke(.green.opacity(0.3), lineWidth: 1)
|
.stroke(.green.opacity(0.3), lineWidth: 1)
|
||||||
)
|
)
|
||||||
.navigationDestination(isPresented: $navigateToDetail) {
|
|
||||||
SessionDetailView(sessionId: session.id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
90
ios/Ascently/Views/Settings/AppearanceView.swift
Normal file
90
ios/Ascently/Views/Settings/AppearanceView.swift
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct AppearanceView: View {
|
||||||
|
@EnvironmentObject var themeManager: ThemeManager
|
||||||
|
|
||||||
|
let columns = [
|
||||||
|
GridItem(.adaptive(minimum: 44))
|
||||||
|
]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
Section("Appearance") {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
Text("Accent Color")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.textCase(.uppercase)
|
||||||
|
|
||||||
|
LazyVGrid(columns: columns, spacing: 12) {
|
||||||
|
ForEach(ThemeManager.presetColors, id: \.self) { color in
|
||||||
|
Circle()
|
||||||
|
.fill(color)
|
||||||
|
.frame(width: 44, height: 44)
|
||||||
|
.overlay(
|
||||||
|
ZStack {
|
||||||
|
if isSelected(color) {
|
||||||
|
Image(systemName: "checkmark")
|
||||||
|
.font(.headline)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.shadow(radius: 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.onTapGesture {
|
||||||
|
withAnimation {
|
||||||
|
themeManager.accentColor = color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.accessibilityLabel(colorDescription(for: color))
|
||||||
|
.accessibilityAddTraits(isSelected(color) ? .isSelected : [])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isSelected(.blue) {
|
||||||
|
Button("Reset to Default") {
|
||||||
|
withAnimation {
|
||||||
|
themeManager.resetToDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.foregroundColor(.red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle("Appearance")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isSelected(_ color: Color) -> Bool {
|
||||||
|
let selectedUIColor = UIColor(themeManager.accentColor)
|
||||||
|
let targetUIColor = UIColor(color)
|
||||||
|
|
||||||
|
return selectedUIColor == targetUIColor
|
||||||
|
}
|
||||||
|
|
||||||
|
private func colorDescription(for color: Color) -> String {
|
||||||
|
switch color {
|
||||||
|
case .blue: return "Blue"
|
||||||
|
case .purple: return "Purple"
|
||||||
|
case .pink: return "Pink"
|
||||||
|
case .red: return "Red"
|
||||||
|
case .orange: return "Orange"
|
||||||
|
case .green: return "Green"
|
||||||
|
case .teal: return "Teal"
|
||||||
|
case .indigo: return "Indigo"
|
||||||
|
case .mint: return "Mint"
|
||||||
|
case Color(uiColor: .systemBrown): return "Brown"
|
||||||
|
case Color(uiColor: .systemCyan): return "Cyan"
|
||||||
|
default: return "Color"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
NavigationView {
|
||||||
|
AppearanceView()
|
||||||
|
.environmentObject(ThemeManager())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -881,12 +881,12 @@ struct SyncSettingsView: View {
|
|||||||
|
|
||||||
if selectedProvider == .server {
|
if selectedProvider == .server {
|
||||||
Section {
|
Section {
|
||||||
TextField("Server URL", text: $serverURL, prompt: Text("http://your-server:8080"))
|
TextField("Server URL", text: $serverURL)
|
||||||
.keyboardType(.URL)
|
.keyboardType(.URL)
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
|
|
||||||
TextField("Auth Token", text: $authToken, prompt: Text("your-secret-token"))
|
TextField("Auth Token", text: $authToken)
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
.disableAutocorrection(true)
|
.disableAutocorrection(true)
|
||||||
} header: {
|
} header: {
|
||||||
|
|||||||
Reference in New Issue
Block a user