diff options
| author | Ruben Beltran del Rio <git@r.bdr.sh> | 2025-07-08 21:21:08 +0200 |
|---|---|---|
| committer | Ruben Beltran del Rio <git@r.bdr.sh> | 2025-07-08 21:21:08 +0200 |
| commit | c21a68c807dcb84b4ce910ee0f73238a8019ffe9 (patch) | |
| tree | ae1e5e0fc7a846daf74623c6cd6270612ff0e56f /Map/Presentation/Preferences | |
| parent | 8f6da65a680cdd30ec541b3b226137004a1e4681 (diff) | |
Allow export/import of preferences
Diffstat (limited to 'Map/Presentation/Preferences')
5 files changed, 206 insertions, 52 deletions
diff --git a/Map/Presentation/Preferences/EditorPreferencesView.swift b/Map/Presentation/Preferences/EditorPreferencesView.swift index cf584ff..91b332f 100644 --- a/Map/Presentation/Preferences/EditorPreferencesView.swift +++ b/Map/Presentation/Preferences/EditorPreferencesView.swift @@ -17,10 +17,13 @@ import SwiftUI struct EditorPreferencesView: View { - @AppStorage("useSmartEditor") private var useSmartEditor = false - @AppStorage("useCustomEditorFont") private var useCustomEditorFont = false - @AppStorage("customEditorFontName") private var customEditorFontName = "Menlo" - @AppStorage("editorFontSize") private var editorFontSize: Double = 14 + @AppStorage("useSmartEditor") private var useSmartEditor = UserPreferences.Defaults.useSmartEditor + @AppStorage("useCustomEditorFont") private var useCustomEditorFont = UserPreferences.Defaults + .useCustomEditorFont + @AppStorage("customEditorFontName") private var customEditorFontName = UserPreferences.Defaults + .customEditorFontName + @AppStorage("editorFontSize") private var editorFontSize: Double = UserPreferences.Defaults + .editorFontSize var monospacedFonts: [String] { let fontManager = NSFontManager.shared diff --git a/Map/Presentation/Preferences/GeneralPreferencesView.swift b/Map/Presentation/Preferences/GeneralPreferencesView.swift index 9eb829e..a70d608 100644 --- a/Map/Presentation/Preferences/GeneralPreferencesView.swift +++ b/Map/Presentation/Preferences/GeneralPreferencesView.swift @@ -14,20 +14,193 @@ // along with this program. If not, see https://map.tranquil.systems. import SwiftUI +import UniformTypeIdentifiers struct GeneralPreferencesView: View { + @State private var showingResetAlert = false + @State private var showingExportDialog = false + @State private var showingImportDialog = false + @State private var showingImportError = false + @State private var importErrorMessage = "" + @State private var showingExportFailedAlert = false + @State private var exportShouldReset = false + var body: some View { - VStack { - Text("General Preferences") - .font(.largeTitle) - .padding() + VStack(alignment: .center, spacing: Dimensions.Spacing.loose) { + HStack(alignment: .firstTextBaseline, spacing: Dimensions.Spacing.regular) { + Text("preferences.general.preferences.title") + .font(.Theme.Body.emphasized) + .frame(maxWidth: 250, alignment: .trailing) + + VStack(alignment: .leading, spacing: Dimensions.Spacing.regular) { + HStack(spacing: Dimensions.Spacing.cozy) { + Button( + action: { + exportShouldReset = false + showingExportDialog = true + }, + label: { + Text("preferences.general.preferences.export") + .font(.Theme.Body.regular) + } + ) + .buttonStyle(.bordered) - Text("General settings will be available here in a future version.") - .foregroundColor(.secondary) + Button( + action: { + showingImportDialog = true + }, + label: { + Text("preferences.general.preferences.import") + .font(.Theme.Body.regular) + } + ) + .buttonStyle(.bordered) + } + + Button( + action: { + showingResetAlert = true + }, + label: { + Text("preferences.general.preferences.reset") + .font(.Theme.Body.regular) + .foregroundColor(Color.Theme.jasperRed) + } + ) + .buttonStyle(.borderless) + } + .frame(maxWidth: .infinity, alignment: .leading) + } Spacer() } - .padding() + .padding(.vertical, Dimensions.Spacing.loose) + .padding(.horizontal, Dimensions.Spacing.indulgent) + .alert("preferences.general.reset.title", isPresented: $showingResetAlert) { + Button("preferences.general.reset.cancel", role: .cancel) {} + Button("preferences.general.reset.without_export", role: .destructive) { + resetPreferences() + } + Button("preferences.general.reset.export_and_reset") { + exportShouldReset = true + showingExportDialog = true + } + } message: { + Text("preferences.general.reset.message") + } + .alert("preferences.general.import.error.title", isPresented: $showingImportError) { + Button("preferences.general.import.error.ok") {} + } message: { + Text(importErrorMessage) + } + .alert("preferences.general.export.failed.title", isPresented: $showingExportFailedAlert) { + Button("preferences.general.export.failed.ok") {} + } message: { + Text("preferences.general.export.failed.message") + } + .fileExporter( + isPresented: $showingExportDialog, + document: PreferencesDocument(), + contentType: .json, + defaultFilename: String(localized: "preferences.general.export.filename") + ) { result in + handleExportResult(result) + } + .fileImporter( + isPresented: $showingImportDialog, + allowedContentTypes: [.json], + allowsMultipleSelection: false + ) { result in + handleImport(result) + } + } + + private func resetPreferences() { + var preferences = UserPreferences() + preferences.resetToDefaults() + } + + private func handleExportResult(_ result: Result<URL, Error>) { + switch result { + case .success(_): + // Export successful + if exportShouldReset { + // Reset preferences if this was an export-and-reset operation + resetPreferences() + } + // Reset the flag + exportShouldReset = false + case .failure(let error): + if exportShouldReset { + // Export failed during export-and-reset - show alert and don't reset + showingExportFailedAlert = true + exportShouldReset = false + } else { + // Regular export failed - just log it + print("Export error: \(error)") + } + } + } + + private func handleImport(_ result: Result<[URL], Error>) { + switch result { + case .success(let urls): + guard let url = urls.first else { return } + + // Start accessing the security-scoped resource + guard url.startAccessingSecurityScopedResource() else { + importErrorMessage = String(localized: "preferences.general.import.error.file_read") + showingImportError = true + return + } + + // Ensure we stop accessing the resource when done + defer { + url.stopAccessingSecurityScopedResource() + } + + do { + let jsonData = try Data(contentsOf: url) + let jsonString = String(data: jsonData, encoding: .utf8) ?? "" + + var preferences = UserPreferences.loadFromUserDefaults() + if preferences.updateFromJSON(jsonString) { + // Import successful + } else { + importErrorMessage = String(localized: "preferences.general.import.error.invalid_format") + showingImportError = true + } + } catch { + importErrorMessage = String(localized: "preferences.general.import.error.file_read") + showingImportError = true + } + + case .failure(let error): + importErrorMessage = error.localizedDescription + showingImportError = true + } + } +} + +// Document wrapper for file export +struct PreferencesDocument: FileDocument { + static var readableContentTypes: [UTType] { [.json] } + + var preferences: UserPreferences + + init() { + self.preferences = UserPreferences.loadFromUserDefaults() + } + + init(configuration: ReadConfiguration) throws { + self.preferences = UserPreferences() + } + + func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { + let jsonString = preferences.toJSON() ?? "{}" + let data = jsonString.data(using: .utf8) ?? Data() + return FileWrapper(regularFileWithContents: data) } } diff --git a/Map/Presentation/Preferences/MapPreferencesView.swift b/Map/Presentation/Preferences/MapPreferencesView.swift index f6c909c..71e9d6c 100644 --- a/Map/Presentation/Preferences/MapPreferencesView.swift +++ b/Map/Presentation/Preferences/MapPreferencesView.swift @@ -16,10 +16,14 @@ import SwiftUI struct MapPreferencesView: View { - @AppStorage("showMapBackground") private var showBackground = true - @AppStorage("useCustomFont") private var useCustomFont = false - @AppStorage("customFontName") private var customFontName = "Helvetica" - @AppStorage("defaultExportFormat") private var defaultExportFormat = "png" + @AppStorage("showMapBackground") private var showBackground = UserPreferences.Defaults + .showMapBackground + @AppStorage("useCustomFont") private var useCustomFont = UserPreferences.Defaults.useCustomFont + @AppStorage("customFontName") private var customFontName = UserPreferences.Defaults.customFontName + @AppStorage("defaultExportFormat") private var defaultExportFormat = UserPreferences.Defaults + .defaultExportFormat + @AppStorage("useSmartLabelPositioning") private var useSmartLabelPositioning = UserPreferences + .Defaults.useSmartLabelPositioning var body: some View { VStack(alignment: .center, spacing: Dimensions.Spacing.loose) { @@ -37,6 +41,13 @@ struct MapPreferencesView: View { }) Toggle( + isOn: $useSmartLabelPositioning, + label: { + Text("preferences.map.map_style.use_smart_label_positioning") + .font(.Theme.Body.regular) + }) + + Toggle( isOn: $useCustomFont, label: { Text("preferences.map.map_style.use_custom_font") diff --git a/Map/Presentation/Preferences/StagesPreferencesView.swift b/Map/Presentation/Preferences/StagesPreferencesView.swift index 6d2154c..542a7fe 100644 --- a/Map/Presentation/Preferences/StagesPreferencesView.swift +++ b/Map/Presentation/Preferences/StagesPreferencesView.swift @@ -16,7 +16,8 @@ import SwiftUI struct StagesPreferencesView: View { - @AppStorage("customStages") private var customStagesData: Data = Data() + @AppStorage("customStages") private var customStagesData: Data = UserPreferences.Defaults + .customStages @State private var customStages: [CustomStage] = [] @State private var showingHelpPopover = false @@ -128,10 +129,6 @@ struct StagesPreferencesView: View { private func loadStages() { if let decoded = try? JSONDecoder().decode([CustomStage].self, from: customStagesData) { customStages = decoded - } else { - // First use - create default Cynefin stage - customStages = [CustomStage.cynefinDefault()] - saveStages() // Save the default to UserDefaults } } diff --git a/Map/Presentation/Preferences/TemplatesPreferencesView.swift b/Map/Presentation/Preferences/TemplatesPreferencesView.swift index 5de2428..375ed68 100644 --- a/Map/Presentation/Preferences/TemplatesPreferencesView.swift +++ b/Map/Presentation/Preferences/TemplatesPreferencesView.swift @@ -37,7 +37,8 @@ struct Template: Identifiable, Codable, Hashable { } struct TemplatesPreferencesView: View { - @AppStorage("mapTemplates") private var templatesData: Data = Data() + @AppStorage("mapTemplates") private var templatesData: Data = UserPreferences.Defaults + .mapTemplates @State private var selectedTemplate: Template? @State private var showingAddSheet = false @State private var newTemplateName = "" @@ -183,12 +184,6 @@ struct TemplatesPreferencesView: View { .frame(maxWidth: .infinity, maxHeight: .infinity) } .onAppear { - // Initialize default templates if needed - if templates.wrappedValue.isEmpty - || !templates.wrappedValue.contains(where: { $0.isDefault }) - { - createDefaultTemplates() - } // Set first template as selected if none is selected if selectedTemplate == nil && !templates.wrappedValue.isEmpty { selectedTemplate = templates.wrappedValue.first @@ -244,31 +239,6 @@ struct TemplatesPreferencesView: View { .padding(.horizontal, Dimensions.Spacing.indulgent) } - private func createDefaultTemplates() { - let defaultTemplate = Template( - name: String(localized: "map_template.default.title"), - content: String(localized: "map_template.default.content"), - isDefault: true - ) - - let emptyTemplate = Template( - name: String(localized: "map_template.empty.title"), - content: "", - isDefault: false - ) - - if templates.wrappedValue.isEmpty { - templates.wrappedValue = [defaultTemplate, emptyTemplate] - } else { - // Add default template if none exists - if !templates.wrappedValue.contains(where: { $0.isDefault }) { - var currentTemplates = templates.wrappedValue - currentTemplates.insert(defaultTemplate, at: 0) - templates.wrappedValue = currentTemplates - } - } - } - private func addTemplate() { let newTemplate = Template( name: newTemplateName.isEmpty |