aboutsummaryrefslogtreecommitdiff
path: root/Map
diff options
context:
space:
mode:
authorRuben Beltran del Rio <git@r.bdr.sh>2025-07-08 21:21:08 +0200
committerRuben Beltran del Rio <git@r.bdr.sh>2025-07-08 21:21:08 +0200
commitc21a68c807dcb84b4ce910ee0f73238a8019ffe9 (patch)
treeae1e5e0fc7a846daf74623c6cd6270612ff0e56f /Map
parent8f6da65a680cdd30ec541b3b226137004a1e4681 (diff)
Allow export/import of preferences
Diffstat (limited to 'Map')
-rw-r--r--Map/Data/UserPreferences.swift322
-rw-r--r--Map/Localizable.xcstrings209
-rw-r--r--Map/Presentation/Commands/MapCommands.swift8
-rw-r--r--Map/Presentation/Complex Components/MapRender/MapRenderView.swift17
-rw-r--r--Map/Presentation/MapEditor.swift13
-rw-r--r--Map/Presentation/Preferences/EditorPreferencesView.swift11
-rw-r--r--Map/Presentation/Preferences/GeneralPreferencesView.swift187
-rw-r--r--Map/Presentation/Preferences/MapPreferencesView.swift19
-rw-r--r--Map/Presentation/Preferences/StagesPreferencesView.swift7
-rw-r--r--Map/Presentation/Preferences/TemplatesPreferencesView.swift34
-rw-r--r--Map/Presentation/PreferencesWindow.swift16
11 files changed, 761 insertions, 82 deletions
diff --git a/Map/Data/UserPreferences.swift b/Map/Data/UserPreferences.swift
new file mode 100644
index 0000000..cf9dd7e
--- /dev/null
+++ b/Map/Data/UserPreferences.swift
@@ -0,0 +1,322 @@
+// Copyright (C) 2024 Rubén Beltrán del Río
+
+// This program is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+
+// You should have received a copy of the GNU General Public License
+// along with this program. If not, see https://map.tranquil.systems.
+
+import Foundation
+
+// MARK: - JSON Export/Import Structures
+struct UserPreferencesJSON: Codable {
+ // MARK: - Map Preferences
+ var showMapBackground: Bool?
+ var useCustomFont: Bool?
+ var customFontName: String?
+ var defaultExportFormat: String?
+ var useSmartLabelPositioning: Bool?
+
+ // MARK: - Editor Preferences
+ var useSmartEditor: Bool?
+ var useCustomEditorFont: Bool?
+ var customEditorFontName: String?
+ var editorFontSize: Double?
+
+ // MARK: - Templates (as actual objects)
+ var mapTemplates: [Template]?
+
+ // MARK: - Stages (as actual objects)
+ var customStages: [CustomStage]?
+
+ // MARK: - UI Preferences
+ var viewStyle: String?
+ var zoom: Double?
+}
+
+struct UserPreferences: Codable {
+
+ // MARK: - Map Preferences
+ var showMapBackground: Bool
+ var useCustomFont: Bool
+ var customFontName: String
+ var defaultExportFormat: String
+ var useSmartLabelPositioning: Bool
+
+ // MARK: - Editor Preferences
+ var useSmartEditor: Bool
+ var useCustomEditorFont: Bool
+ var customEditorFontName: String
+ var editorFontSize: Double
+
+ // MARK: - Templates
+ var mapTemplates: Data
+
+ // MARK: - Stages
+ var customStages: Data
+
+ // MARK: - UI Preferences
+ var viewStyle: String
+ var zoom: Double
+
+ // MARK: - Default Values
+ struct Defaults {
+ static let showMapBackground = true
+ static let useCustomFont = false
+ static let customFontName = "Helvetica"
+ static let defaultExportFormat = "png"
+ static let useSmartLabelPositioning = true
+
+ static let useSmartEditor = false
+ static let useCustomEditorFont = false
+ static let customEditorFontName = "Menlo"
+ static let editorFontSize = 14.0
+
+ static let viewStyle = "horizontal"
+ static let zoom = 1.0
+
+ // Computed properties for complex defaults
+ static var mapTemplates: Data {
+ 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
+ )
+
+ let templates = [defaultTemplate, emptyTemplate]
+ return (try? JSONEncoder().encode(templates)) ?? Data()
+ }
+
+ static var customStages: Data {
+ let defaultStage = CustomStage.cynefinDefault()
+ let stages = [defaultStage]
+ return (try? JSONEncoder().encode(stages)) ?? Data()
+ }
+ }
+
+ // MARK: - Initialization
+ init() {
+ self.showMapBackground = Defaults.showMapBackground
+ self.useCustomFont = Defaults.useCustomFont
+ self.customFontName = Defaults.customFontName
+ self.defaultExportFormat = Defaults.defaultExportFormat
+ self.useSmartLabelPositioning = Defaults.useSmartLabelPositioning
+
+ self.useSmartEditor = Defaults.useSmartEditor
+ self.useCustomEditorFont = Defaults.useCustomEditorFont
+ self.customEditorFontName = Defaults.customEditorFontName
+ self.editorFontSize = Defaults.editorFontSize
+
+ self.mapTemplates = Defaults.mapTemplates
+ self.customStages = Defaults.customStages
+
+ self.viewStyle = Defaults.viewStyle
+ self.zoom = Defaults.zoom
+ }
+
+ // MARK: - JSON Methods
+ func toJSON() -> String? {
+ let encoder = JSONEncoder()
+ encoder.outputFormatting = .prettyPrinted
+
+ do {
+ // Convert to JSON-friendly format
+ let jsonPrefs = UserPreferencesJSON(
+ showMapBackground: showMapBackground,
+ useCustomFont: useCustomFont,
+ customFontName: customFontName,
+ defaultExportFormat: defaultExportFormat,
+ useSmartLabelPositioning: useSmartLabelPositioning,
+ useSmartEditor: useSmartEditor,
+ useCustomEditorFont: useCustomEditorFont,
+ customEditorFontName: customEditorFontName,
+ editorFontSize: editorFontSize,
+ mapTemplates: templatesFromData(mapTemplates),
+ customStages: stagesFromData(customStages),
+ viewStyle: viewStyle,
+ zoom: zoom
+ )
+
+ let data = try encoder.encode(jsonPrefs)
+ return String(data: data, encoding: .utf8)
+ } catch {
+ return nil
+ }
+ }
+
+ mutating func updateFromJSON(_ json: String) -> Bool {
+ guard let data = json.data(using: .utf8) else { return false }
+ do {
+ let decoder = JSONDecoder()
+ let jsonPrefs = try decoder.decode(UserPreferencesJSON.self, from: data)
+
+ // Convert from JSON format back to UserPreferences
+ // Only update values that are present in the JSON (partial updates)
+ if let value = jsonPrefs.showMapBackground {
+ self.showMapBackground = value
+ }
+ if let value = jsonPrefs.useCustomFont {
+ self.useCustomFont = value
+ }
+ if let value = jsonPrefs.customFontName {
+ self.customFontName = value
+ }
+ if let value = jsonPrefs.defaultExportFormat {
+ self.defaultExportFormat = value
+ }
+ if let value = jsonPrefs.useSmartLabelPositioning {
+ self.useSmartLabelPositioning = value
+ }
+ if let value = jsonPrefs.useSmartEditor {
+ self.useSmartEditor = value
+ }
+ if let value = jsonPrefs.useCustomEditorFont {
+ self.useCustomEditorFont = value
+ }
+ if let value = jsonPrefs.customEditorFontName {
+ self.customEditorFontName = value
+ }
+ if let value = jsonPrefs.editorFontSize {
+ self.editorFontSize = value
+ }
+ if let value = jsonPrefs.mapTemplates {
+ self.mapTemplates = dataFromTemplates(value)
+ }
+ if let value = jsonPrefs.customStages {
+ self.customStages = dataFromStages(value)
+ }
+ if let value = jsonPrefs.viewStyle {
+ self.viewStyle = value
+ }
+ if let value = jsonPrefs.zoom {
+ self.zoom = value
+ }
+
+ apply()
+ return true
+ } catch {
+ print("JSON import error: \(error)")
+ return false
+ }
+ }
+
+ // MARK: - Helper Methods for Data/Object Conversion
+ private func templatesFromData(_ data: Data) -> [Template] {
+ guard let templates = try? JSONDecoder().decode([Template].self, from: data) else {
+ return []
+ }
+ return templates
+ }
+
+ private func dataFromTemplates(_ templates: [Template]) -> Data {
+ guard let data = try? JSONEncoder().encode(templates) else {
+ return Data()
+ }
+ return data
+ }
+
+ private func stagesFromData(_ data: Data) -> [CustomStage] {
+ guard let stages = try? JSONDecoder().decode([CustomStage].self, from: data) else {
+ return []
+ }
+ return stages
+ }
+
+ private func dataFromStages(_ stages: [CustomStage]) -> Data {
+ guard let data = try? JSONEncoder().encode(stages) else {
+ return Data()
+ }
+ return data
+ }
+
+ // MARK: - Reset to Defaults
+ mutating func resetToDefaults() {
+ self = UserPreferences()
+ apply()
+ }
+
+ // MARK: - Apply to UserDefaults
+ private func apply() {
+ let defaults = UserDefaults.standard
+
+ // Map Preferences
+ defaults.set(showMapBackground, forKey: "showMapBackground")
+ defaults.set(useCustomFont, forKey: "useCustomFont")
+ defaults.set(customFontName, forKey: "customFontName")
+ defaults.set(defaultExportFormat, forKey: "defaultExportFormat")
+ defaults.set(useSmartLabelPositioning, forKey: "useSmartLabelPositioning")
+
+ // Editor Preferences
+ defaults.set(useSmartEditor, forKey: "useSmartEditor")
+ defaults.set(useCustomEditorFont, forKey: "useCustomEditorFont")
+ defaults.set(customEditorFontName, forKey: "customEditorFontName")
+ defaults.set(editorFontSize, forKey: "editorFontSize")
+
+ // Templates
+ defaults.set(mapTemplates, forKey: "mapTemplates")
+
+ // Stages
+ defaults.set(customStages, forKey: "customStages")
+
+ // UI Preferences
+ defaults.set(viewStyle, forKey: "viewStyle")
+ defaults.set(zoom, forKey: "zoom")
+ }
+
+ // MARK: - Load from UserDefaults
+ static func loadFromUserDefaults() -> UserPreferences {
+ let defaults = UserDefaults.standard
+
+ var preferences = UserPreferences()
+
+ // Map Preferences
+ preferences.showMapBackground =
+ defaults.object(forKey: "showMapBackground") as? Bool ?? Defaults.showMapBackground
+ preferences.useCustomFont =
+ defaults.object(forKey: "useCustomFont") as? Bool ?? Defaults.useCustomFont
+ preferences.customFontName =
+ defaults.object(forKey: "customFontName") as? String ?? Defaults.customFontName
+ preferences.defaultExportFormat =
+ defaults.object(forKey: "defaultExportFormat") as? String ?? Defaults.defaultExportFormat
+ preferences.useSmartLabelPositioning =
+ defaults.object(forKey: "useSmartLabelPositioning") as? Bool
+ ?? Defaults.useSmartLabelPositioning
+
+ // Editor Preferences
+ preferences.useSmartEditor =
+ defaults.object(forKey: "useSmartEditor") as? Bool ?? Defaults.useSmartEditor
+ preferences.useCustomEditorFont =
+ defaults.object(forKey: "useCustomEditorFont") as? Bool ?? Defaults.useCustomEditorFont
+ preferences.customEditorFontName =
+ defaults.object(forKey: "customEditorFontName") as? String ?? Defaults.customEditorFontName
+ preferences.editorFontSize =
+ defaults.object(forKey: "editorFontSize") as? Double ?? Defaults.editorFontSize
+
+ // Templates
+ preferences.mapTemplates =
+ defaults.object(forKey: "mapTemplates") as? Data ?? Defaults.mapTemplates
+
+ // Stages
+ preferences.customStages =
+ defaults.object(forKey: "customStages") as? Data ?? Defaults.customStages
+
+ // UI Preferences
+ preferences.viewStyle = defaults.object(forKey: "viewStyle") as? String ?? Defaults.viewStyle
+ preferences.zoom = defaults.object(forKey: "zoom") as? Double ?? Defaults.zoom
+
+ return preferences
+ }
+}
diff --git a/Map/Localizable.xcstrings b/Map/Localizable.xcstrings
index d7cc56a..8f9ddce 100644
--- a/Map/Localizable.xcstrings
+++ b/Map/Localizable.xcstrings
@@ -219,15 +219,6 @@
}
}
},
- "General Preferences" : {
-
- },
- "General settings will be available here in a future version." : {
-
- },
- "Key" : {
- "extractionState" : "manual"
- },
"map_editor.zoom_in" : {
"extractionState" : "manual",
"localizations" : {
@@ -256,7 +247,7 @@
"en" : {
"stringUnit" : {
"state" : "translated",
- "value" : "Anchor (95, 80)\nUsers (90, 20)\nWalking App (70, 20)\nWalking App Database (50, 60)\nUser Interface (80, 50)\nWeb Server (60, 30)\nCRM (30, 70)\n\nUsers -> Walking App\nUsers -> User Interface\nWalking App -> Walking App Database\nWalking App -> Web Server\nUser Interface -> Web Server\nWeb Server -> CRM\n\n[Note] (80, 5) Social walking app for everyone\n\n[Group] Client Side, Walking App, User Interface\n\n[Evolution] Walking App +5\n\n[Inertia] Unreliable Data"
+ "value" : "Anchor (95, 80)\nUsers (90, 20)\nWalking App (70, 20)\nWalking App Database (50, 60)\nUser Interface (80, 50)\nWeb Server (60, 30)\nCRM (30, 70)\n\nUsers -> Walking App\nUsers -> User Interface\nWalking App -> Walking App Database\nWalking App -> Web Server\nUser Interface -> Web Server\nWeb Server -> CRM\n\n[Note] (80, 5) Social walking app for everyone\n\n[Group] Client Side, Walking App, User Interface\n\n[Evolution] Walking App +5\n\n[Inertia] CRM"
}
}
}
@@ -393,6 +384,193 @@
}
}
},
+ "preferences.general.export.failed.message" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Could not save the preferences file. Preferences were not reset."
+ }
+ }
+ }
+ },
+ "preferences.general.export.failed.ok" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "OK"
+ }
+ }
+ }
+ },
+ "preferences.general.export.failed.title" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Export Failed"
+ }
+ }
+ }
+ },
+ "preferences.general.export.filename" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "map-preferences"
+ }
+ }
+ }
+ },
+ "preferences.general.import.error.file_read" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Could not read the preferences file."
+ }
+ }
+ }
+ },
+ "preferences.general.import.error.invalid_format" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "The preferences file format is invalid."
+ }
+ }
+ }
+ },
+ "preferences.general.import.error.ok" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "OK"
+ }
+ }
+ }
+ },
+ "preferences.general.import.error.title" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Import Error"
+ }
+ }
+ }
+ },
+ "preferences.general.preferences.export" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Export to JSON"
+ }
+ }
+ }
+ },
+ "preferences.general.preferences.import" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Import from JSON"
+ }
+ }
+ }
+ },
+ "preferences.general.preferences.reset" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Reset All Preferences to Defaults"
+ }
+ }
+ }
+ },
+ "preferences.general.preferences.title" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Preferences:"
+ }
+ }
+ }
+ },
+ "preferences.general.reset.cancel" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Cancel"
+ }
+ }
+ }
+ },
+ "preferences.general.reset.export_and_reset" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Export and Reset"
+ }
+ }
+ }
+ },
+ "preferences.general.reset.message" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "This will delete any custom evolution stages or templates that you have added."
+ }
+ }
+ }
+ },
+ "preferences.general.reset.title" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Are you sure you want to reset preferences to defaults?"
+ }
+ }
+ }
+ },
+ "preferences.general.reset.without_export" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Reset without Exporting"
+ }
+ }
+ }
+ },
"preferences.map.export.default_format" : {
"extractionState" : "manual",
"localizations" : {
@@ -448,6 +626,17 @@
}
}
},
+ "preferences.map.map_style.use_smart_label_positioning" : {
+ "extractionState" : "manual",
+ "localizations" : {
+ "en" : {
+ "stringUnit" : {
+ "state" : "translated",
+ "value" : "Use smart label positioning"
+ }
+ }
+ }
+ },
"preferences.menu.editor" : {
"extractionState" : "manual",
"localizations" : {
diff --git a/Map/Presentation/Commands/MapCommands.swift b/Map/Presentation/Commands/MapCommands.swift
index 2d9809d..a298956 100644
--- a/Map/Presentation/Commands/MapCommands.swift
+++ b/Map/Presentation/Commands/MapCommands.swift
@@ -17,9 +17,11 @@ import UniformTypeIdentifiers
struct MapCommands: Commands {
- @AppStorage("viewStyle") var viewStyle: ViewStyle = .horizontal
- @AppStorage("zoom") var zoom = 1.0
- @AppStorage("mapTemplates") private var templatesData: Data = Data()
+ @AppStorage("viewStyle") var viewStyle: ViewStyle =
+ ViewStyle(rawValue: UserPreferences.Defaults.viewStyle) ?? .horizontal
+ @AppStorage("zoom") var zoom = UserPreferences.Defaults.zoom
+ @AppStorage("mapTemplates") private var templatesData: Data = UserPreferences.Defaults
+ .mapTemplates
@FocusedBinding(\.document) var document: MapDocument?
@FocusedValue(\.fileURL) var url: URL?
@FocusedBinding(\.selectedEvolution) var selectedEvolution: StageType?
diff --git a/Map/Presentation/Complex Components/MapRender/MapRenderView.swift b/Map/Presentation/Complex Components/MapRender/MapRenderView.swift
index 9e1f4a2..72a765d 100644
--- a/Map/Presentation/Complex Components/MapRender/MapRenderView.swift
+++ b/Map/Presentation/Complex Components/MapRender/MapRenderView.swift
@@ -21,9 +21,13 @@ struct MapRenderView: View {
@Binding var document: MapDocument
@Binding var evolution: StageType
- @AppStorage("showMapBackground") var showMapBackground: Bool = true
- @AppStorage("useCustomFont") var useCustomFont: Bool = false
- @AppStorage("customFontName") var customFontName: String = "Helvetica"
+ @AppStorage("showMapBackground") var showMapBackground: Bool = UserPreferences.Defaults
+ .showMapBackground
+ @AppStorage("useCustomFont") var useCustomFont: Bool = UserPreferences.Defaults.useCustomFont
+ @AppStorage("customFontName") var customFontName: String = UserPreferences.Defaults.customFontName
+ @AppStorage("useSmartLabelPositioning") private var useSmartLabelPositioning = UserPreferences
+ .Defaults.useSmartLabelPositioning
+
var stage: Stage {
Stage.stages(evolution)
}
@@ -53,6 +57,8 @@ struct MapRenderView: View {
let positions = await Task.detached(priority: .userInitiated) {
var positions: [Int: CGPoint] = [:]
+ guard await useSmartLabelPositioning else { return positions }
+
let axisLines = [
Line(start: CGPoint(x: 0, y: 0), end: CGPoint(x: 0, y: mapSize.height)),
Line(
@@ -133,7 +139,10 @@ struct MapRenderView: View {
.onAppear {
parseMapInBackground()
}
- .onChange(of: document.text) { _, _ in
+ .onChange(of: document.text) {
+ parseMapInBackground()
+ }
+ .onChange(of: useSmartLabelPositioning) {
parseMapInBackground()
}
.onDisappear {
diff --git a/Map/Presentation/MapEditor.swift b/Map/Presentation/MapEditor.swift
index 73302df..11545e2 100644
--- a/Map/Presentation/MapEditor.swift
+++ b/Map/Presentation/MapEditor.swift
@@ -22,13 +22,16 @@ struct MapEditor: View {
private let changeDebouncer: Debouncer = Debouncer(seconds: 0.05)
- @AppStorage("viewStyle") var viewStyle: ViewStyle = .horizontal
- @AppStorage("useCustomEditorFont") var useCustomEditorFont: Bool = false
- @AppStorage("customEditorFontName") var customEditorFontName: String = "Menlo"
- @AppStorage("editorFontSize") var editorFontSize: Double = 14
+ @AppStorage("viewStyle") var viewStyle: ViewStyle =
+ ViewStyle(rawValue: UserPreferences.Defaults.viewStyle) ?? .horizontal
+ @AppStorage("useCustomEditorFont") var useCustomEditorFont: Bool = UserPreferences.Defaults
+ .useCustomEditorFont
+ @AppStorage("customEditorFontName") var customEditorFontName: String = UserPreferences.Defaults
+ .customEditorFontName
+ @AppStorage("editorFontSize") var editorFontSize: Double = UserPreferences.Defaults.editorFontSize
let zoomRange = Constants.kMinZoom...Constants.kMaxZoom
- @AppStorage("zoom") var zoom = 1.0
+ @AppStorage("zoom") var zoom = UserPreferences.Defaults.zoom
@State var lastZoom = 1.0
@State var searchTerm = ""
@State var selectedTerm = 0
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
diff --git a/Map/Presentation/PreferencesWindow.swift b/Map/Presentation/PreferencesWindow.swift
index 06153ed..fa5bd4c 100644
--- a/Map/Presentation/PreferencesWindow.swift
+++ b/Map/Presentation/PreferencesWindow.swift
@@ -17,7 +17,7 @@ import SwiftUI
enum PreferencesTab: CaseIterable {
- // case general
+ case general
case editor
case map
case stages
@@ -25,8 +25,8 @@ enum PreferencesTab: CaseIterable {
var localizedStringKey: LocalizedStringKey {
switch self {
- // case .general:
- // return "preferences.menu.general"
+ case .general:
+ return "preferences.menu.general"
case .editor:
return "preferences.menu.editor"
case .map:
@@ -40,8 +40,8 @@ enum PreferencesTab: CaseIterable {
var systemImage: String {
switch self {
- // case .general:
- // return "gearshape"
+ case .general:
+ return "gearshape"
case .editor:
return "text.word.spacing"
case .map:
@@ -55,13 +55,13 @@ enum PreferencesTab: CaseIterable {
}
struct PreferencesWindow: View {
- @State private var selectedTab: PreferencesTab = .editor
+ @State private var selectedTab: PreferencesTab = .general
var body: some View {
Group {
switch selectedTab {
- // case .general:
- // GeneralPreferencesView()
+ case .general:
+ GeneralPreferencesView()
case .editor:
EditorPreferencesView()
case .map: