development
swift 211 lines 8.86 KB
Raw
1 import SwiftUI
2
3 struct SettingsView: View {
4 @Environment(\.dismiss) private var dismiss
5 @EnvironmentObject private var appState: AppState
6 @EnvironmentObject private var favoritesStore: FavoritesStore
7 @EnvironmentObject private var recentSearchStore: RecentSearchStore
8 @EnvironmentObject private var guideEngine: OndeGuideEngine
9 @EnvironmentObject private var accountStore: AccountStore
10
11 private var versionString: String {
12 let version =
13 Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
14 ?? "1.0"
15 let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "1"
16 return "Version \(version) (\(build))"
17 }
18
19 var body: some View {
20 ScrollView {
21 VStack(alignment: .leading, spacing: 18) {
22 #if os(tvOS)
23 TVPageHeader("Settings")
24 #endif
25
26 #if os(macOS)
27 HStack {
28 Spacer()
29
30 Button("Close") {
31 dismiss()
32 }
33 .font(KleponTypography.bodySecondary.weight(.semibold))
34 .foregroundStyle(KleponColor.accent)
35 .keyboardShortcut(.cancelAction)
36 }
37 #endif
38
39 #if DEBUG
40 DebugSettingsCard()
41 #endif
42
43 AccountSettingsCard()
44
45 KleponCard {
46 VStack(alignment: .leading, spacing: 12) {
47 Text("Private guide")
48 .font(KleponTypography.cardTitle)
49 .foregroundStyle(KleponColor.textPrimary)
50
51 Text(guideEngine.availability.title)
52 .font(KleponTypography.bodySecondary)
53 .foregroundStyle(
54 guideEngine.availability.isFailure
55 ? KleponColor.highlight : KleponColor.textSecondary)
56
57 Text(guideEngine.availability.detail)
58 .font(KleponTypography.caption)
59 .foregroundStyle(KleponColor.textSecondary)
60
61 HStack(spacing: 10) {
62 KleponChip(
63 title: guideEngine.estimatedDownloadDescription,
64 icon: "arrow.down.circle")
65 KleponChip(
66 title: guideEngine.storageUsedDescription, icon: "internaldrive")
67 }
68
69 KleponActionButton(
70 title: guideEngine.availability.actionTitle,
71 systemImage: guideEngine.availability == .ready
72 ? "checkmark.circle.fill" : "arrow.down.circle",
73 isLoading: guideEngine.availability.isBusy,
74 isDisabled: guideEngine.availability == .ready
75 ) {
76 Task {
77 await guideEngine.prepareIfNeeded(
78 forceReload: guideEngine.availability.isFailure)
79 }
80 }
81
82 if guideEngine.storageUsedDescription != "Not downloaded yet" {
83 KleponActionButton(
84 title: "Remove private guide from this device",
85 systemImage: "trash",
86 tone: .secondary
87 ) {
88 Task {
89 await guideEngine.removePrivateGuide()
90 }
91 }
92 }
93 }
94 }
95
96 KleponCard {
97 VStack(alignment: .leading, spacing: 12) {
98 Text("App info")
99 .font(KleponTypography.cardTitle)
100 .foregroundStyle(KleponColor.textPrimary)
101
102 KleponMetadataRow(title: "App", value: "Klepon: Taste of Indonesia")
103 KleponMetadataRow(title: "Publisher", value: "Splitfire AB")
104 KleponMetadataRow(title: "Build", value: versionString)
105 }
106 }
107
108 KleponCard {
109 VStack(alignment: .leading, spacing: 10) {
110 Text("Privacy")
111 .font(KleponTypography.cardTitle)
112 .foregroundStyle(KleponColor.textPrimary)
113
114 Text(
115 "Browseable food content is bundled with the app. Private answers use a one-time on-device guide download when you choose to enable it."
116 )
117 .font(KleponTypography.bodySecondary)
118 .foregroundStyle(KleponColor.textSecondary)
119 }
120 }
121
122 KleponCard {
123 VStack(alignment: .leading, spacing: 12) {
124 Text("Browsing data")
125 .font(KleponTypography.cardTitle)
126 .foregroundStyle(KleponColor.textPrimary)
127
128 KleponMetadataRow(
129 title: "Saved", value: "\(favoritesStore.favoriteIDs.count) items")
130 KleponMetadataRow(
131 title: "Recent searches",
132 value: "\(recentSearchStore.queries.count) items")
133 KleponMetadataRow(
134 title: "Recently viewed",
135 value: "\(appState.recentlyViewedStore.entryIDs.count) items")
136
137 if !recentSearchStore.queries.isEmpty {
138 KleponActionButton(title: "Clear recent searches", tone: .secondary) {
139 recentSearchStore.clear()
140 }
141 }
142
143 if !appState.recentlyViewedStore.entryIDs.isEmpty {
144 KleponActionButton(title: "Clear recently viewed", tone: .secondary) {
145 appState.recentlyViewedStore.clear()
146 }
147 }
148
149 if !favoritesStore.favoriteIDs.isEmpty {
150 KleponActionButton(title: "Clear saved list", tone: .secondary) {
151 favoritesStore.clear()
152 }
153 }
154 }
155 }
156
157 KleponCard {
158 VStack(alignment: .leading, spacing: 12) {
159 Text("Onboarding")
160 .font(KleponTypography.cardTitle)
161 .foregroundStyle(KleponColor.textPrimary)
162
163 Text(
164 "If you want to see Klepon’s welcome and privacy intro again, you can show it on the next launch path."
165 )
166 .font(KleponTypography.bodySecondary)
167 .foregroundStyle(KleponColor.textSecondary)
168
169 KleponActionButton(title: "Show welcome again", tone: .secondary) {
170 appState.hasCompletedOnboarding = false
171 #if !os(tvOS)
172 dismiss()
173 #endif
174 }
175 }
176 }
177
178 KleponCard {
179 VStack(alignment: .leading, spacing: 10) {
180 Text("Technology")
181 .font(KleponTypography.cardTitle)
182 .foregroundStyle(KleponColor.textPrimary)
183
184 Text(
185 "Klepon’s private answer layer is powered on-device by Onde Inference. The technology stays in the background so the product can feel like a guide first."
186 )
187 .font(KleponTypography.bodySecondary)
188 .foregroundStyle(KleponColor.textSecondary)
189 }
190 }
191 }
192 .padding(.horizontal, 20)
193 .padding(.vertical, 16)
194 }
195 .background(KleponColor.background.ignoresSafeArea())
196 .navigationTitle("Settings")
197 #if os(tvOS)
198 .toolbar(.hidden, for: .navigationBar)
199 #endif
200 #if !os(tvOS)
201 .toolbar {
202 ToolbarItem(placement: .kleponPrimaryAction) {
203 Button("Done") {
204 dismiss()
205 }
206 .tint(KleponColor.accent)
207 }
208 }
209 #endif
210 }
211 }