diff options
| author | Ruben Beltran del Rio <git@r.bdr.sh> | 2025-07-05 11:29:17 +0200 |
|---|---|---|
| committer | Ruben Beltran del Rio <git@r.bdr.sh> | 2025-07-05 11:29:17 +0200 |
| commit | cb4a713afc81326485becba4fa3a635f112114d4 (patch) | |
| tree | e3efea48ccb7be53ed681eb9ed333af39d93ce67 /Map/Presentation | |
| parent | e21ead77ffdff206d1ae17e5ce93ea34c4227414 (diff) | |
Add custom stages
Diffstat (limited to 'Map/Presentation')
9 files changed, 613 insertions, 19 deletions
diff --git a/Map/Presentation/Base Components/EvolutionPicker/EvolutionPickerMenu.swift b/Map/Presentation/Base Components/EvolutionPicker/EvolutionPickerMenu.swift index 3a396aa..9478346 100644 --- a/Map/Presentation/Base Components/EvolutionPicker/EvolutionPickerMenu.swift +++ b/Map/Presentation/Base Components/EvolutionPicker/EvolutionPickerMenu.swift @@ -3,43 +3,47 @@ import SwiftUI struct EvolutionPickerMenu: View { @Binding var selectedEvolution: StageType + - private func select(_ evolution: StageType) { - selectedEvolution = evolution + private func select(_ stageType: StageType) { + selectedEvolution = stageType } var body: some View { VStack(alignment: .leading, spacing: 1.0) { Section(header: EvolutionPickerMenuHeader(key: "stage_type.section.types")) { ForEach(StageType.types) { stage in - EvolutionPickerMenuItem(stage: stage, isSelected: selectedEvolution == stage) + EvolutionPickerMenuItem(stageType: stage, isSelected: selectedEvolution.id == stage.id) .onTapGesture { - selectedEvolution = stage + select(stage) } } } Section(header: EvolutionPickerMenuHeader(key: "stage_type.section.characteristics")) { ForEach(StageType.characteristics) { stage in - EvolutionPickerMenuItem(stage: stage, isSelected: selectedEvolution == stage) + EvolutionPickerMenuItem(stageType: stage, isSelected: selectedEvolution.id == stage.id) .onTapGesture { - selectedEvolution = stage + select(stage) } } } Section(header: EvolutionPickerMenuHeader(key: "stage_type.section.general_properties")) { ForEach(StageType.properties) { stage in - EvolutionPickerMenuItem(stage: stage, isSelected: selectedEvolution == stage) + EvolutionPickerMenuItem(stageType: stage, isSelected: selectedEvolution.id == stage.id) .onTapGesture { - selectedEvolution = stage + select(stage) } } } - Section(header: EvolutionPickerMenuHeader(key: "stage_type.section.other")) { - ForEach(StageType.custom) { stage in - EvolutionPickerMenuItem(stage: stage, isSelected: selectedEvolution == stage) - .onTapGesture { - selectedEvolution = stage - } + Section(header: EvolutionPickerMenuHeader(key: "stage_type.section.custom")) { + ForEach(StageType.customStages) { customStage in + EvolutionPickerMenuItem( + stageType: customStage, + isSelected: selectedEvolution.id == customStage.id + ) + .onTapGesture { + select(customStage) + } } } } diff --git a/Map/Presentation/Base Components/EvolutionPicker/EvolutionPickerMenuItem.swift b/Map/Presentation/Base Components/EvolutionPicker/EvolutionPickerMenuItem.swift index 1731e8d..cac1b08 100644 --- a/Map/Presentation/Base Components/EvolutionPicker/EvolutionPickerMenuItem.swift +++ b/Map/Presentation/Base Components/EvolutionPicker/EvolutionPickerMenuItem.swift @@ -4,7 +4,7 @@ struct EvolutionPickerMenuItem: View { @State var isHovered: Bool = false - let stage: StageType + let stageType: StageType let isSelected: Bool var body: some View { @@ -12,7 +12,7 @@ struct EvolutionPickerMenuItem: View { Image(systemName: "checkmark") .opacity(isSelected ? 1.0 : 0.0) .bold(true) - Text(Stage.title(stage)) + Text(Stage.title(stageType)) .font(isSelected ? .Theme.Body.emphasized : .Theme.Body.regular) .padding(Dimensions.Spacing.cozy) Spacer() @@ -27,6 +27,6 @@ struct EvolutionPickerMenuItem: View { } #Preview { - EvolutionPickerMenuItem(stage: .behavior, isSelected: false) - EvolutionPickerMenuItem(stage: .ubiquity, isSelected: true) + EvolutionPickerMenuItem(stageType: .behavior, isSelected: false) + EvolutionPickerMenuItem(stageType: .ubiquity, isSelected: true) } diff --git a/Map/Presentation/Base Components/MapRender/MapAxes.swift b/Map/Presentation/Base Components/MapRender/MapAxes.swift index 48e0b6d..3a94a13 100644 --- a/Map/Presentation/Base Components/MapRender/MapAxes.swift +++ b/Map/Presentation/Base Components/MapRender/MapAxes.swift @@ -94,6 +94,6 @@ struct MapAxes: View { #Preview { MapAxes( mapSize: CGSize(width: 200.0, height: 200.0), lineWidth: CGFloat(1.0), - evolution: Stage.stages(.general), stages: [25.0, 50.0, 75.0] + evolution: Stage.stages(StageType.general), stages: [25.0, 50.0, 75.0] ).padding(50.0) } diff --git a/Map/Presentation/Preferences/AppearancePreferencesView.swift b/Map/Presentation/Preferences/AppearancePreferencesView.swift new file mode 100644 index 0000000..a71276d --- /dev/null +++ b/Map/Presentation/Preferences/AppearancePreferencesView.swift @@ -0,0 +1,36 @@ +// 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 AppearancePreferencesView: View { + var body: some View { + VStack { + Text("Appearance Preferences") + .font(.largeTitle) + .padding() + + Text("Appearance settings will be available here in a future version.") + .foregroundColor(.secondary) + + Spacer() + } + .padding() + } +} + +#Preview { + AppearancePreferencesView() +} diff --git a/Map/Presentation/Preferences/GeneralPreferencesView.swift b/Map/Presentation/Preferences/GeneralPreferencesView.swift new file mode 100644 index 0000000..9eb829e --- /dev/null +++ b/Map/Presentation/Preferences/GeneralPreferencesView.swift @@ -0,0 +1,36 @@ +// 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 GeneralPreferencesView: View { + var body: some View { + VStack { + Text("General Preferences") + .font(.largeTitle) + .padding() + + Text("General settings will be available here in a future version.") + .foregroundColor(.secondary) + + Spacer() + } + .padding() + } +} + +#Preview { + GeneralPreferencesView() +} diff --git a/Map/Presentation/Preferences/MapPreferencesView.swift b/Map/Presentation/Preferences/MapPreferencesView.swift new file mode 100644 index 0000000..fb5f8c2 --- /dev/null +++ b/Map/Presentation/Preferences/MapPreferencesView.swift @@ -0,0 +1,36 @@ +// 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 MapPreferencesView: View { + var body: some View { + VStack { + Text("Map Preferences") + .font(.largeTitle) + .padding() + + Text("Map settings will be available here in a future version.") + .foregroundColor(.secondary) + + Spacer() + } + .padding() + } +} + +#Preview { + MapPreferencesView() +} diff --git a/Map/Presentation/Preferences/StagesPreferencesView.swift b/Map/Presentation/Preferences/StagesPreferencesView.swift new file mode 100644 index 0000000..7131034 --- /dev/null +++ b/Map/Presentation/Preferences/StagesPreferencesView.swift @@ -0,0 +1,165 @@ +// 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 StagesPreferencesView: View { + @AppStorage("customStages") private var customStagesData: Data = Data() + @State private var customStages: [CustomStage] = [] + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + Text("preferences.stages.title") + .font(.Theme.Title.emphasized) + .lineSpacing(Dimensions.FontSize.title - Dimensions.FontSize.title) + Text("preferences.stages.explanation") + .font(.Theme.Body.emphasized) + .lineSpacing(Dimensions.FontSize.body - Dimensions.FontSize.body) + + HStack { + Spacer() + Button(action: addStage) { + Image(systemName: "plus") + } + .buttonStyle(.borderless) + } + + Table(customStages) { + TableColumn("Name") { customStage in + TextField( + "Stage Name", + text: Binding( + get: { customStage.name }, + set: { newValue in + updateStageName(customStage.id, newValue) + } + ) + ) + .textFieldStyle(.roundedBorder) + } + .width(120) + + TableColumn("Stage I") { customStage in + TextField( + "Stage I", + text: Binding( + get: { customStage.stage.i }, + set: { newValue in + updateStageValue(customStage.id, \.i, newValue) + } + ) + ) + .textFieldStyle(.roundedBorder) + } + + TableColumn("Stage II") { customStage in + TextField( + "Stage II", + text: Binding( + get: { customStage.stage.ii }, + set: { newValue in + updateStageValue(customStage.id, \.ii, newValue) + } + ) + ) + .textFieldStyle(.roundedBorder) + } + + TableColumn("Stage III") { customStage in + TextField( + "Stage III", + text: Binding( + get: { customStage.stage.iii }, + set: { newValue in + updateStageValue(customStage.id, \.iii, newValue) + } + ) + ) + .textFieldStyle(.roundedBorder) + } + + TableColumn("Stage IV") { customStage in + TextField( + "Stage IV", + text: Binding( + get: { customStage.stage.iv }, + set: { newValue in + updateStageValue(customStage.id, \.iv, newValue) + } + ) + ) + .textFieldStyle(.roundedBorder) + } + + TableColumn("") { customStage in + Button(action: { removeStage(customStage.id) }) { + Image(systemName: "minus") + } + .buttonStyle(.borderless) + } + .width(30) + } + .tableStyle(.bordered(alternatesRowBackgrounds: true)) + + Spacer() + } + .padding() + .onAppear { + loadStages() + } + } + + private func loadStages() { + if let decoded = try? JSONDecoder().decode([CustomStage].self, from: customStagesData) { + customStages = decoded + } else { + customStages = [CustomStage()] + } + } + + private func saveStages() { + if let encoded = try? JSONEncoder().encode(customStages) { + customStagesData = encoded + } + } + + private func addStage() { + customStages.append(CustomStage()) + saveStages() + } + + private func removeStage(_ id: UUID) { + customStages.removeAll { $0.id == id } + saveStages() + } + + private func updateStageName(_ id: UUID, _ newName: String) { + if let index = customStages.firstIndex(where: { $0.id == id }) { + customStages[index].name = newName + saveStages() + } + } + + private func updateStageValue(_ id: UUID, _ keyPath: WritableKeyPath<Stage, String>, _ value: String) { + if let index = customStages.firstIndex(where: { $0.id == id }) { + customStages[index].stage[keyPath: keyPath] = value + saveStages() + } + } +} + +#Preview { + StagesPreferencesView() +} diff --git a/Map/Presentation/Preferences/TemplatesPreferencesView.swift b/Map/Presentation/Preferences/TemplatesPreferencesView.swift new file mode 100644 index 0000000..806c22b --- /dev/null +++ b/Map/Presentation/Preferences/TemplatesPreferencesView.swift @@ -0,0 +1,231 @@ +// 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 + + init(name: String, content: String) { + self.name = name + self.content = content + } + + func hash(into hasher: inout Hasher) { + hasher.combine(id) + } + + static func == (lhs: Template, rhs: Template) -> Bool { + lhs.id == rhs.id + } +} + +struct SimpleTextEditor: NSViewRepresentable { + @Binding var text: String + + func makeNSView(context: Context) -> NSScrollView { + let scrollView = NSTextView.scrollableTextView() + let textView = scrollView.documentView as! NSTextView + + textView.backgroundColor = .textBackgroundColor + textView.allowsUndo = true + textView.delegate = context.coordinator + textView.string = text + textView.isEditable = true + textView.font = .monospacedSystemFont(ofSize: 14.0, weight: .regular) + + return scrollView + } + + func updateNSView(_ nsView: NSScrollView, context: Context) { + let textView = nsView.documentView as! NSTextView + if textView.string != text { + textView.string = text + } + } + + func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + class Coordinator: NSObject, NSTextViewDelegate { + var parent: SimpleTextEditor + + init(_ parent: SimpleTextEditor) { + self.parent = parent + } + + func textDidChange(_ notification: Notification) { + if let textView = notification.object as? NSTextView { + parent.text = textView.string + } + } + } +} + +struct TemplatesPreferencesView: View { + @AppStorage("mapTemplates") private var templatesData: Data = Data() + @State private var templates: [Template] = [] + @State private var selectedTemplate: Template? + @State private var templateText: String = "" + @State private var showingAddSheet = false + @State private var newTemplateName = "" + + var body: some View { + HStack(spacing: 0) { + VStack(alignment: .leading, spacing: 8) { + HStack { + Text("Templates") + .font(.headline) + + Spacer() + + Button(action: { showingAddSheet = true }) { + Image(systemName: "plus") + } + .buttonStyle(.borderless) + + Button(action: removeTemplate) { + Image(systemName: "minus") + } + .buttonStyle(.borderless) + .disabled(selectedTemplate == nil) + } + + List(templates, selection: $selectedTemplate) { template in + Text(template.name) + .tag(template) + } + .listStyle(.sidebar) + } + .frame(width: 200) + + Divider() + + VStack(alignment: .leading, spacing: 8) { + if let selected = selectedTemplate { + HStack { + Text("Template: \(selected.name)") + .font(.headline) + Spacer() + } + + SimpleTextEditor(text: $templateText) + .onChange(of: templateText) { _, newValue in + updateSelectedTemplate(newValue) + } + } else { + VStack { + Spacer() + Text("Select a template to edit") + .foregroundColor(.secondary) + Spacer() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .padding(.leading) + } + .padding() + .onAppear { + loadTemplates() + } + .onChange(of: selectedTemplate) { _, newSelection in + if let template = newSelection { + templateText = template.content + } + } + .sheet(isPresented: $showingAddSheet) { + VStack(spacing: 16) { + Text("Add New Template") + .font(.headline) + + TextField("Template Name", text: $newTemplateName) + .textFieldStyle(.roundedBorder) + + HStack { + Button("Cancel") { + showingAddSheet = false + newTemplateName = "" + } + + Spacer() + + Button("Add") { + addTemplate() + } + .disabled(newTemplateName.isEmpty) + } + } + .padding() + .frame(width: 300, height: 120) + } + } + + private func loadTemplates() { + if let decoded = try? JSONDecoder().decode([Template].self, from: templatesData) { + templates = decoded + } else { + templates = [ + Template( + name: "Basic Map", + content: + "Component A (90, 20)\nComponent B (70, 40)\nComponent A -> Component B\n\n[Note] (50, 10) This is a sample template" + ) + ] + saveTemplates() + } + } + + private func saveTemplates() { + if let encoded = try? JSONEncoder().encode(templates) { + templatesData = encoded + } + } + + private func addTemplate() { + let newTemplate = Template(name: newTemplateName, content: "") + templates.append(newTemplate) + selectedTemplate = newTemplate + templateText = "" + saveTemplates() + showingAddSheet = false + newTemplateName = "" + } + + private func removeTemplate() { + guard let selected = selectedTemplate else { return } + templates.removeAll { $0.id == selected.id } + selectedTemplate = nil + templateText = "" + saveTemplates() + } + + private func updateSelectedTemplate(_ newContent: String) { + guard let selected = selectedTemplate, + let index = templates.firstIndex(where: { $0.id == selected.id }) + else { return } + + templates[index].content = newContent + saveTemplates() + } +} + +#Preview { + TemplatesPreferencesView() +} diff --git a/Map/Presentation/PreferencesWindow.swift b/Map/Presentation/PreferencesWindow.swift new file mode 100644 index 0000000..22bd987 --- /dev/null +++ b/Map/Presentation/PreferencesWindow.swift @@ -0,0 +1,86 @@ +// 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 + +enum PreferencesTab: String, CaseIterable { + case general = "General" + case appearance = "Appearance" + case map = "Map" + case stages = "Stages" + case templates = "Templates" + + var systemImage: String { + switch self { + case .general: + return "gearshape" + case .appearance: + return "paintpalette" + case .map: + return "map" + case .stages: + return "list.number" + case .templates: + return "doc.text" + } + } +} + +struct PreferencesWindow: View { + @State private var selectedTab: PreferencesTab = .general + + var body: some View { + Group { + switch selectedTab { + case .general: + GeneralPreferencesView() + case .appearance: + AppearancePreferencesView() + case .map: + MapPreferencesView() + case .stages: + StagesPreferencesView() + case .templates: + TemplatesPreferencesView() + } + } + .frame(width: 600, height: 400) + .toolbar { + ToolbarItem(placement: .principal) { + HStack(spacing: 12) { + ForEach(PreferencesTab.allCases, id: \.self) { tab in + Button(action: { selectedTab = tab }) { + VStack(spacing: 2) { + Image(systemName: tab.systemImage) + .font(.system(size: 16)) + Text(tab.rawValue) + .font(.caption2) + } + .frame(width: 60, height: 40) + .background(selectedTab == tab ? Color.accentColor.opacity(0.2) : Color.clear) + .foregroundColor(selectedTab == tab ? .accentColor : .primary) + .cornerRadius(6) + } + .buttonStyle(.plain) + } + } + } + } + } +} + +#Preview { + PreferencesWindow() +} |