// 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: - Enums enum AppearanceStyle: String, CaseIterable, Codable { case system = "system" case light = "light" case dark = "dark" var localizedName: String { switch self { case .system: return String(localized: "preferences.appearance.style.system") case .light: return String(localized: "preferences.appearance.style.light") case .dark: return String(localized: "preferences.appearance.style.dark") } } } enum QuickLookPreviewStyle: String, CaseIterable, Codable { case map = "map" case highlightedText = "highlighted_text" case plainText = "plain_text" var localizedName: String { switch self { case .map: return String(localized: "preferences.quick_look.preview_style.map") case .highlightedText: return String(localized: "preferences.quick_look.preview_style.highlighted_text") case .plainText: return String(localized: "preferences.quick_look.preview_style.plain_text") } } } // MARK: - JSON Export/Import Structures struct UserPreferencesJSON: Codable { // MARK: - Appearance Preferences var appearanceStyle: String? // 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? var softWrapLines: Bool? var highlightMissingComponents: Bool? // 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? // MARK: - QuickLook Preferences var quickLookPreviewStyle: String? } struct UserPreferences: Codable { // MARK: - Appearance Preferences var appearanceStyle: String // 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 var softWrapLines: Bool var highlightMissingComponents: Bool // MARK: - Templates var mapTemplates: Data // MARK: - Stages var customStages: Data // MARK: - UI Preferences var viewStyle: String var zoom: Double // MARK: - QuickLook Preferences var quickLookPreviewStyle: String // MARK: - Default Values struct Defaults { static let appearanceStyle = AppearanceStyle.system.rawValue static let showMapBackground = true static let useCustomFont = false static let customFontName = "Helvetica" static let defaultExportFormat = "png" static let useSmartLabelPositioning = true static let useSmartEditor = true static let useCustomEditorFont = false static let customEditorFontName = "Menlo" static let editorFontSize = 14.0 static let softWrapLines = false static let highlightMissingComponents = true static let viewStyle = "horizontal" static let zoom = 1.0 static let quickLookPreviewStyle = QuickLookPreviewStyle.map.rawValue // 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.appearanceStyle = Defaults.appearanceStyle 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.softWrapLines = Defaults.softWrapLines self.highlightMissingComponents = Defaults.highlightMissingComponents self.mapTemplates = Defaults.mapTemplates self.customStages = Defaults.customStages self.viewStyle = Defaults.viewStyle self.zoom = Defaults.zoom self.quickLookPreviewStyle = Defaults.quickLookPreviewStyle } // MARK: - JSON Methods func toJSON() -> String? { let encoder = JSONEncoder() encoder.outputFormatting = .prettyPrinted do { // Convert to JSON-friendly format let jsonPrefs = UserPreferencesJSON( appearanceStyle: appearanceStyle, showMapBackground: showMapBackground, useCustomFont: useCustomFont, customFontName: customFontName, defaultExportFormat: defaultExportFormat, useSmartLabelPositioning: useSmartLabelPositioning, useSmartEditor: useSmartEditor, useCustomEditorFont: useCustomEditorFont, customEditorFontName: customEditorFontName, editorFontSize: editorFontSize, softWrapLines: softWrapLines, highlightMissingComponents: highlightMissingComponents, mapTemplates: templatesFromData(mapTemplates), customStages: stagesFromData(customStages), viewStyle: viewStyle, zoom: zoom, quickLookPreviewStyle: quickLookPreviewStyle ) 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.appearanceStyle { self.appearanceStyle = value } 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.softWrapLines { self.softWrapLines = value } if let value = jsonPrefs.highlightMissingComponents { self.highlightMissingComponents = 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 } if let value = jsonPrefs.quickLookPreviewStyle { self.quickLookPreviewStyle = 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 let sharedDefaults = UserDefaults(suiteName: "group.systems.tranquil.Map") // Appearance Preferences defaults.set(appearanceStyle, forKey: "appearanceStyle") // 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") defaults.set(softWrapLines, forKey: "softWrapLines") defaults.set(highlightMissingComponents, forKey: "highlightMissingComponents") // Templates defaults.set(mapTemplates, forKey: "mapTemplates") // Stages defaults.set(customStages, forKey: "customStages") // UI Preferences defaults.set(viewStyle, forKey: "viewStyle") defaults.set(zoom, forKey: "zoom") // QuickLook Preferences - write to both standard and shared UserDefaults defaults.set(quickLookPreviewStyle, forKey: "quickLookPreviewStyle") sharedDefaults?.set(quickLookPreviewStyle, forKey: "quickLookPreviewStyle") } // MARK: - Load from UserDefaults static func loadFromUserDefaults() -> UserPreferences { let defaults = UserDefaults.standard var preferences = UserPreferences() // Appearance Preferences preferences.appearanceStyle = defaults.object(forKey: "appearanceStyle") as? String ?? Defaults.appearanceStyle // 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 preferences.softWrapLines = defaults.object(forKey: "softWrapLines") as? Bool ?? Defaults.softWrapLines preferences.highlightMissingComponents = defaults.object(forKey: "highlightMissingComponents") as? Bool ?? Defaults.highlightMissingComponents // 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 // QuickLook Preferences preferences.quickLookPreviewStyle = defaults.object(forKey: "quickLookPreviewStyle") as? String ?? Defaults.quickLookPreviewStyle return preferences } }