// 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 SwiftUI struct Template: Identifiable, Codable, Hashable { var id = UUID() var name: String var content: String var isDefault: Bool = false init(name: String, content: String, isDefault: Bool = false) { self.name = name self.content = content self.isDefault = isDefault } func hash(into hasher: inout Hasher) { hasher.combine(id) } static func == (lhs: Template, rhs: Template) -> Bool { lhs.id == rhs.id } } struct TemplatesPreferencesView: View { @AppStorage("mapTemplates") private var templatesData: Data = Data() @State private var selectedTemplate: Template? @State private var showingAddSheet = false @State private var newTemplateName = "" private var templates: Binding<[Template]> { Binding( get: { if let decoded = try? JSONDecoder().decode([Template].self, from: templatesData) { return decoded } return [] }, set: { newValue in if let encoded = try? JSONEncoder().encode(newValue) { templatesData = encoded } } ) } private var selectedTemplateContent: Binding { Binding( get: { guard let selected = selectedTemplate, let template = templates.wrappedValue.first(where: { $0.id == selected.id }) else { return "" } return template.content }, set: { newContent in guard let selected = selectedTemplate, let index = templates.wrappedValue.firstIndex(where: { $0.id == selected.id }) else { return } var currentTemplates = templates.wrappedValue currentTemplates[index].content = newContent templates.wrappedValue = currentTemplates } ) } var body: some View { VStack(alignment: .leading, spacing: Dimensions.Spacing.regular) { Text("preferences.templates.title") .font(.Theme.Title.emphasized) .lineSpacing(Dimensions.LineHeight.title - Dimensions.FontSize.title) Text("preferences.templates.explanation") .font(.Theme.Body.regular) .kerning(Dimensions.Kerning.body) .lineSpacing(Dimensions.LineHeight.body - Dimensions.FontSize.body) .padding(.top, Dimensions.Spacing.loose) .padding(.bottom, Dimensions.Spacing.regular) HStack(spacing: 0) { VStack(alignment: .leading, spacing: Dimensions.Spacing.regular) { HStack { Text("preferences.templates.table.title") .font(.Theme.Body.emphasized) Spacer() Button(action: { showingAddSheet = true }) { Image(systemName: "plus") } .buttonStyle(.borderless) Button(action: { removeTemplate(selectedTemplate) }) { Image(systemName: "minus") } .buttonStyle(.borderless) .disabled(selectedTemplate == nil) } List(templates.wrappedValue) { template in HStack { Button(action: { setDefaultTemplate(template) }) { Image(systemName: template.isDefault ? "largecircle.fill.circle" : "circle") .foregroundColor( selectedTemplate?.id == template.id ? Color.white : (template.isDefault ? Color.Theme.UI.accent : Color.Theme.UI.foreground.opacity(0.5)) ) } .buttonStyle(.plain) .help("Set as default template") Text(template.name) .tag(template) .foregroundColor( selectedTemplate?.id == template.id ? Color.white : Color.Theme.UI.foreground ) Spacer() } .padding(Dimensions.Spacing.cozy) .background(selectedTemplate?.id == template.id ? Color.Theme.UI.accent : Color.clear) .clipShape(RoundedRectangle(cornerRadius: Dimensions.Preferences.Sidebar.radius)) .padding(Dimensions.Spacing.coziest) .listRowInsets(EdgeInsets()) .listRowSeparator(.hidden) .onTapGesture { selectedTemplate = template } } .listStyle(.plain) } .frame(width: Dimensions.Preferences.Sidebar.width) Divider() VStack(alignment: .leading, spacing: 8) { if let selected = selectedTemplate { HStack { Text("Template: \(selected.name)") .font(.headline) Spacer() } MapTextEditor(text: selectedTemplateContent) } else { VStack { Spacer() Text("Select a template to edit") .foregroundColor(.secondary) Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) } } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(.leading) } .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 } } .sheet(isPresented: $showingAddSheet) { VStack(spacing: Dimensions.Spacing.indulgent) { Text("Add New Template") .font(.Theme.Body.emphasized) TextField("Template Name", text: $newTemplateName) .textFieldStyle(.roundedBorder) .font(.Theme.Body.regular) HStack { Button("Cancel") { showingAddSheet = false newTemplateName = "" } Spacer() Button("Add") { addTemplate() } .disabled(newTemplateName.isEmpty) } } .padding(Dimensions.Spacing.loose) .frame(width: 300, height: 120) } }.padding(.vertical, Dimensions.Spacing.loose) .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 ? String(localized: "map_template.empty.title") : newTemplateName, content: "" ) var currentTemplates = templates.wrappedValue currentTemplates.append(newTemplate) templates.wrappedValue = currentTemplates selectedTemplate = newTemplate showingAddSheet = false newTemplateName = "" } private func removeTemplate(_ template: Template?) { guard let template else { return } var currentTemplates = templates.wrappedValue currentTemplates.removeAll { $0 == template } templates.wrappedValue = currentTemplates selectedTemplate = templates.wrappedValue.first } private func setDefaultTemplate(_ template: Template) { // Direct assignment to binding - this will automatically save via @AppStorage templates.wrappedValue = templates.wrappedValue.map { t in var newTemplate = t newTemplate.isDefault = (t.id == template.id) return newTemplate } } } #Preview { TemplatesPreferencesView() }