aboutsummaryrefslogtreecommitdiff
path: root/Map/Presentation
diff options
context:
space:
mode:
authorRuben Beltran del Rio <git@r.bdr.sh>2025-07-05 11:29:17 +0200
committerRuben Beltran del Rio <git@r.bdr.sh>2025-07-05 11:29:17 +0200
commitcb4a713afc81326485becba4fa3a635f112114d4 (patch)
treee3efea48ccb7be53ed681eb9ed333af39d93ce67 /Map/Presentation
parente21ead77ffdff206d1ae17e5ce93ea34c4227414 (diff)
Add custom stages
Diffstat (limited to 'Map/Presentation')
-rw-r--r--Map/Presentation/Base Components/EvolutionPicker/EvolutionPickerMenu.swift32
-rw-r--r--Map/Presentation/Base Components/EvolutionPicker/EvolutionPickerMenuItem.swift8
-rw-r--r--Map/Presentation/Base Components/MapRender/MapAxes.swift2
-rw-r--r--Map/Presentation/Preferences/AppearancePreferencesView.swift36
-rw-r--r--Map/Presentation/Preferences/GeneralPreferencesView.swift36
-rw-r--r--Map/Presentation/Preferences/MapPreferencesView.swift36
-rw-r--r--Map/Presentation/Preferences/StagesPreferencesView.swift165
-rw-r--r--Map/Presentation/Preferences/TemplatesPreferencesView.swift231
-rw-r--r--Map/Presentation/PreferencesWindow.swift86
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()
+}