// 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? var softWrapLines: 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? } 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 var softWrapLines: Bool // 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 softWrapLines = false 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.softWrapLines = Defaults.softWrapLines 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, softWrapLines: softWrapLines, 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.softWrapLines { self.softWrapLines = 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") defaults.set(softWrapLines, forKey: "softWrapLines") // 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 preferences.softWrapLines = defaults.object(forKey: "softWrapLines") as? Bool ?? Defaults.softWrapLines // 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 } }