diff options
Diffstat (limited to 'Map/Presentation')
9 files changed, 240 insertions, 72 deletions
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: |