// 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 TemplatesPreferencesView: View { @AppStorage("mapTemplates") private var templatesData: Data = UserPreferences.Defaults .mapTemplates @State private var selectedTemplate: Template? @State private var showingAddSheet = false @State private var newTemplateName = "" @State private var showingHelpPopover = false 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) { HStack(spacing: Dimensions.Spacing.cozy) { VStack(alignment: .leading, spacing: Dimensions.Spacing.regular) { List(templates.wrappedValue) { template in HStack(spacing: Dimensions.Spacing.coziest) { if selectedTemplate?.id == template.id { Button(action: { setDefaultTemplate(template) }) { Image(systemName: template.isDefault ? "largecircle.fill.circle" : "circle") .foregroundColor(Color.white) } .buttonStyle(.plain) .keyboardShortcut("d", modifiers: [.command]) .help("preferences.templates.help.set_as_default") } else { Button(action: { setDefaultTemplate(template) }) { Image(systemName: template.isDefault ? "largecircle.fill.circle" : "circle") .foregroundColor( template.isDefault ? Color.Theme.UI.accent : Color.Theme.UI.foreground.opacity(0.5)) } .buttonStyle(.plain) .help("preferences.templates.help.set_as_default") } Text(template.name) .font(.Theme.Body.regular) .kerning(Dimensions.Kerning.body) .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) .border(Color.Theme.UI.foreground.opacity(0.2), width: 1) HStack(spacing: Dimensions.Spacing.regular) { Spacer() Button(action: { showingHelpPopover = true }) { Image(systemName: "questionmark.circle") } .buttonStyle(.borderless) .popover(isPresented: $showingHelpPopover, arrowEdge: .bottom) { Text("preferences.templates.explanation") .font(.Theme.Body.regular) .kerning(Dimensions.Kerning.body) .lineSpacing(Dimensions.LineHeight.body - Dimensions.FontSize.body) .frame(width: 300) .padding(Dimensions.Spacing.regular) } HStack(spacing: Dimensions.Spacing.coziest) { Button(action: { showingAddSheet = true }) { Image(systemName: "plus") } .keyboardShortcut("t", modifiers: [.command]) .help("preferences.templates.help.add") Button(action: { removeTemplate(selectedTemplate) }) { Image(systemName: "minus") } .keyboardShortcut(.delete, modifiers: [.command]) .help("preferences.templates.help.remove \(selectedTemplate?.name ?? "")") .disabled(selectedTemplate == nil) } } } .frame(width: Dimensions.Preferences.Sidebar.width) Divider() VStack(alignment: .leading, spacing: Dimensions.Spacing.regular) { if selectedTemplate != nil { MapTextEditor(text: selectedTemplateContent) .border(Color.Theme.UI.foreground.opacity(0.2), width: 1) } else { VStack { Spacer() Text("preferences.templates.empty_view") .foregroundColor(.secondary) Spacer() } .frame(maxWidth: .infinity, maxHeight: .infinity) } } .frame(maxWidth: .infinity, maxHeight: .infinity) } .onAppear { // 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("preferences.templates.new_template.title") .font(.Theme.Body.emphasized) .kerning(Dimensions.Kerning.body) TextField("preferences.templates.new_template.placeholder", text: $newTemplateName) .textFieldStyle(.roundedBorder) .font(.Theme.Body.regular) .kerning(Dimensions.Kerning.body) .onSubmit { addTemplate() } HStack(spacing: Dimensions.Spacing.coziest) { Spacer() Button( action: { showingAddSheet = false newTemplateName = "" }, label: { Text("preferences.templates.new_template.cancel") .font(.Theme.Body.regular) .kerning(Dimensions.Kerning.body) } ) .keyboardShortcut(.escape) Button( action: { addTemplate() }, label: { Text("preferences.templates.new_template.add") .font(.Theme.Body.regular) .kerning(Dimensions.Kerning.body) } ) .keyboardShortcut(.return) .disabled(newTemplateName.isEmpty) } } .padding(Dimensions.Spacing.loose) .frame(width: 300, height: 120) } }.padding(.vertical, Dimensions.Spacing.loose) .padding(.horizontal, Dimensions.Spacing.indulgent) } 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() }