Commit 0bb46ffe82
Unsigned
Layout: unified · split
Hutch.xcodeproj/project.pbxproj +8 −8
| @@ -515,7 +515,7 @@ | |||
| 515 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; | 515 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; |
| 516 | CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements; | 516 | CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements; |
| 517 | CODE_SIGN_STYLE = Automatic; | 517 | CODE_SIGN_STYLE = Automatic; |
| 518 | CURRENT_PROJECT_VERSION = 73; | 518 | CURRENT_PROJECT_VERSION = 74; |
| 519 | DEVELOPMENT_TEAM = ZCNAX3VL9D; | 519 | DEVELOPMENT_TEAM = ZCNAX3VL9D; |
| 520 | ENABLE_PREVIEWS = YES; | 520 | ENABLE_PREVIEWS = YES; |
| 521 | GENERATE_INFOPLIST_FILE = YES; | 521 | GENERATE_INFOPLIST_FILE = YES; |
| @@ -532,7 +532,7 @@ | |||
| 532 | "$(inherited)", | 532 | "$(inherited)", |
| 533 | "@executable_path/Frameworks", | 533 | "@executable_path/Frameworks", |
| 534 | ); | 534 | ); |
| 535 | MARKETING_VERSION = 3.1.5; | 535 | MARKETING_VERSION = 3.1.6; |
| 536 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; | 536 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; |
| 537 | PRODUCT_NAME = "$(TARGET_NAME)"; | 537 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 538 | STRING_CATALOG_GENERATE_SYMBOLS = YES; | 538 | STRING_CATALOG_GENERATE_SYMBOLS = YES; |
| @@ -552,7 +552,7 @@ | |||
| 552 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; | 552 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; |
| 553 | CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements; | 553 | CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements; |
| 554 | CODE_SIGN_STYLE = Automatic; | 554 | CODE_SIGN_STYLE = Automatic; |
| 555 | CURRENT_PROJECT_VERSION = 73; | 555 | CURRENT_PROJECT_VERSION = 74; |
| 556 | DEVELOPMENT_TEAM = ZCNAX3VL9D; | 556 | DEVELOPMENT_TEAM = ZCNAX3VL9D; |
| 557 | ENABLE_PREVIEWS = YES; | 557 | ENABLE_PREVIEWS = YES; |
| 558 | GENERATE_INFOPLIST_FILE = YES; | 558 | GENERATE_INFOPLIST_FILE = YES; |
| @@ -569,7 +569,7 @@ | |||
| 569 | "$(inherited)", | 569 | "$(inherited)", |
| 570 | "@executable_path/Frameworks", | 570 | "@executable_path/Frameworks", |
| 571 | ); | 571 | ); |
| 572 | MARKETING_VERSION = 3.1.5; | 572 | MARKETING_VERSION = 3.1.6; |
| 573 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; | 573 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; |
| 574 | PRODUCT_NAME = "$(TARGET_NAME)"; | 574 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 575 | STRING_CATALOG_GENERATE_SYMBOLS = YES; | 575 | STRING_CATALOG_GENERATE_SYMBOLS = YES; |
| @@ -632,7 +632,7 @@ | |||
| 632 | APPLICATION_EXTENSION_API_ONLY = YES; | 632 | APPLICATION_EXTENSION_API_ONLY = YES; |
| 633 | CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements; | 633 | CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements; |
| 634 | CODE_SIGN_STYLE = Automatic; | 634 | CODE_SIGN_STYLE = Automatic; |
| 635 | CURRENT_PROJECT_VERSION = 73; | 635 | CURRENT_PROJECT_VERSION = 74; |
| 636 | DEVELOPMENT_TEAM = ZCNAX3VL9D; | 636 | DEVELOPMENT_TEAM = ZCNAX3VL9D; |
| 637 | GENERATE_INFOPLIST_FILE = NO; | 637 | GENERATE_INFOPLIST_FILE = NO; |
| 638 | INFOPLIST_FILE = HutchWidgetExtension/Info.plist; | 638 | INFOPLIST_FILE = HutchWidgetExtension/Info.plist; |
| @@ -642,7 +642,7 @@ | |||
| 642 | "@executable_path/Frameworks", | 642 | "@executable_path/Frameworks", |
| 643 | "@executable_path/../../Frameworks", | 643 | "@executable_path/../../Frameworks", |
| 644 | ); | 644 | ); |
| 645 | MARKETING_VERSION = 3.1.5; | 645 | MARKETING_VERSION = 3.1.6; |
| 646 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; | 646 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; |
| 647 | PRODUCT_NAME = "$(TARGET_NAME)"; | 647 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 648 | SKIP_INSTALL = YES; | 648 | SKIP_INSTALL = YES; |
| @@ -661,7 +661,7 @@ | |||
| 661 | APPLICATION_EXTENSION_API_ONLY = YES; | 661 | APPLICATION_EXTENSION_API_ONLY = YES; |
| 662 | CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements; | 662 | CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements; |
| 663 | CODE_SIGN_STYLE = Automatic; | 663 | CODE_SIGN_STYLE = Automatic; |
| 664 | CURRENT_PROJECT_VERSION = 73; | 664 | CURRENT_PROJECT_VERSION = 74; |
| 665 | DEVELOPMENT_TEAM = ZCNAX3VL9D; | 665 | DEVELOPMENT_TEAM = ZCNAX3VL9D; |
| 666 | GENERATE_INFOPLIST_FILE = NO; | 666 | GENERATE_INFOPLIST_FILE = NO; |
| 667 | INFOPLIST_FILE = HutchWidgetExtension/Info.plist; | 667 | INFOPLIST_FILE = HutchWidgetExtension/Info.plist; |
| @@ -671,7 +671,7 @@ | |||
| 671 | "@executable_path/Frameworks", | 671 | "@executable_path/Frameworks", |
| 672 | "@executable_path/../../Frameworks", | 672 | "@executable_path/../../Frameworks", |
| 673 | ); | 673 | ); |
| 674 | MARKETING_VERSION = 3.1.5; | 674 | MARKETING_VERSION = 3.1.6; |
| 675 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; | 675 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; |
| 676 | PRODUCT_NAME = "$(TARGET_NAME)"; | 676 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 677 | SKIP_INSTALL = YES; | 677 | SKIP_INSTALL = YES; |
Hutch/App/RootView.swift +3
| @@ -336,6 +336,7 @@ enum MoreRoute: Hashable { | |||
| 336 | case profile | 336 | case profile |
| 337 | case systemStatus | 337 | case systemStatus |
| 338 | case settings | 338 | case settings |
| 339 | case about | ||
| 339 | case mailingList(InboxMailingListReference) | 340 | case mailingList(InboxMailingListReference) |
| 340 | case thread(InboxThreadSummary) | 341 | case thread(InboxThreadSummary) |
| 341 | case manPageBrowser | 342 | case manPageBrowser |
| @@ -363,6 +364,8 @@ private struct MoreNavigationRoot: View { | |||
| 363 | SystemStatusView() | 364 | SystemStatusView() |
| 364 | case .settings: | 365 | case .settings: |
| 365 | SettingsView() | 366 | SettingsView() |
| 367 | case .about: | ||
| 368 | AboutView() | ||
| 366 | case .mailingList(let mailingList): | 369 | case .mailingList(let mailingList): |
| 367 | MailingListDetailView(mailingList: mailingList) | 370 | MailingListDetailView(mailingList: mailingList) |
| 368 | case .thread(let thread): | 371 | case .thread(let thread): |
Hutch/Views/Home/HomePrototypeView.swift deleted −7
| @@ -1,7 +0,0 @@ | |||
| 1 | import SwiftUI | ||
| 2 | |||
| 3 | struct HomePrototypeView: View { | ||
| 4 | var body: some View { | ||
| 5 | HomeView() | ||
| 6 | } | ||
| 7 | } | ||
Hutch/Views/Home/HomeView.swift +1 −1
| @@ -118,7 +118,7 @@ struct HomeView: View { | |||
| 118 | private var recentSection: some View { | 118 | private var recentSection: some View { |
| 119 | if !recentItems.isEmpty { | 119 | if !recentItems.isEmpty { |
| 120 | Section("Recent") { | 120 | Section("Recent") { |
| 121 | ForEach(recentItems) { item in | 121 | ForEach(recentItems.prefix(3)) { item in |
| 122 | Button { | 122 | Button { |
| 123 | openRecentItem(item) | 123 | openRecentItem(item) |
| 124 | } label: { | 124 | } label: { |
Hutch/Views/Lookup/LookupView.swift +2
| @@ -441,6 +441,8 @@ struct LookupView: View { | |||
| 441 | SystemStatusView() | 441 | SystemStatusView() |
| 442 | case .settings: | 442 | case .settings: |
| 443 | SettingsView() | 443 | SettingsView() |
| 444 | case .about: | ||
| 445 | AboutView() | ||
| 444 | case .mailingList(let mailingList): | 446 | case .mailingList(let mailingList): |
| 445 | MailingListDetailView(mailingList: mailingList) | 447 | MailingListDetailView(mailingList: mailingList) |
| 446 | case .thread(let thread): | 448 | case .thread(let thread): |
Hutch/Views/More/AboutView.swift added +146
| @@ -0,0 +1,146 @@ | |||
| 1 | import StoreKit | ||
| 2 | import SwiftUI | ||
| 3 | |||
| 4 | struct AboutView: View { | ||
| 5 | @Environment(AppState.self) private var appState | ||
| 6 | private let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String | ||
| 7 | ?? Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String | ||
| 8 | ?? "Hutch" | ||
| 9 | private let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String | ||
| 10 | ?? "Unknown" | ||
| 11 | private let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String | ||
| 12 | ?? "Unknown" | ||
| 13 | @State private var developerRevealCount = 0 | ||
| 14 | @State private var storeViewModel = TipStoreViewModel() | ||
| 15 | |||
| 16 | private var developerToolsVisible: Bool { | ||
| 17 | appState.isDebugModeEnabled || developerRevealCount >= 5 | ||
| 18 | } | ||
| 19 | |||
| 20 | private var developerRevealFooterText: String { | ||
| 21 | developerRevealCount >= 5 | ||
| 22 | ? "Debug toggle unlocked. Scroll down to Developer to enable it." | ||
| 23 | : "Tap the build number 5 times to reveal the debug toggle." | ||
| 24 | } | ||
| 25 | |||
| 26 | var body: some View { | ||
| 27 | Form { | ||
| 28 | Section { | ||
| 29 | VStack(alignment: .leading, spacing: 6) { | ||
| 30 | Text(appName) | ||
| 31 | .font(.title2.weight(.semibold)) | ||
| 32 | Text("A native SourceHut client for iOS.") | ||
| 33 | .font(.subheadline) | ||
| 34 | .foregroundStyle(.secondary) | ||
| 35 | } | ||
| 36 | .padding(.vertical, 4) | ||
| 37 | .themedRow() | ||
| 38 | |||
| 39 | LabeledContent("Version", value: version) | ||
| 40 | .onTapGesture { | ||
| 41 | developerRevealCount = min(developerRevealCount + 1, 5) | ||
| 42 | } | ||
| 43 | .themedRow() | ||
| 44 | LabeledContent("Build", value: build) | ||
| 45 | .onTapGesture { | ||
| 46 | developerRevealCount = min(developerRevealCount + 1, 5) | ||
| 47 | } | ||
| 48 | .themedRow() | ||
| 49 | } footer: { | ||
| 50 | Text(developerRevealFooterText) | ||
| 51 | } | ||
| 52 | |||
| 53 | Section("Links") { | ||
| 54 | Link(destination: URL(string: "https://sr.ht")!) { | ||
| 55 | SwiftUI.Label("SourceHut", systemImage: "link") | ||
| 56 | } | ||
| 57 | .themedRow() | ||
| 58 | Link(destination: URL(string: "https://man.sr.ht")!) { | ||
| 59 | SwiftUI.Label("SourceHut Manuals", systemImage: "book") | ||
| 60 | } | ||
| 61 | .themedRow() | ||
| 62 | Link(destination: URL(string: "https://sr.ht/~ccleberg/Hutch")!) { | ||
| 63 | SwiftUI.Label("Project Repository", systemImage: "folder") | ||
| 64 | } | ||
| 65 | .themedRow() | ||
| 66 | } | ||
| 67 | |||
| 68 | Section("Support") { | ||
| 69 | Link(destination: URL(string: "mailto:hello@cleberg.net")!) { | ||
| 70 | SwiftUI.Label("Email Support", systemImage: "envelope") | ||
| 71 | } | ||
| 72 | .themedRow() | ||
| 73 | } | ||
| 74 | |||
| 75 | Section("Privacy") { | ||
| 76 | Text("Hutch uses your SourceHut personal access token to make requests on your behalf. The token is stored locally in the iOS keychain.") | ||
| 77 | .font(.subheadline) | ||
| 78 | .foregroundStyle(.secondary) | ||
| 79 | .themedRow() | ||
| 80 | |||
| 81 | Link(destination: URL(string: "https://zerolabs.sh/hutch/privacy-policy/")!) { | ||
| 82 | SwiftUI.Label("Privacy Policy", systemImage: "hand.raised") | ||
| 83 | } | ||
| 84 | .themedRow() | ||
| 85 | } | ||
| 86 | |||
| 87 | Section("Acknowledgements") { | ||
| 88 | Text("Built for SourceHut users who want quick access to repositories, builds, and tickets on iOS.") | ||
| 89 | .font(.subheadline) | ||
| 90 | .foregroundStyle(.secondary) | ||
| 91 | .themedRow() | ||
| 92 | } | ||
| 93 | |||
| 94 | Section { | ||
| 95 | if storeViewModel.isLoading { | ||
| 96 | ProgressView() | ||
| 97 | .themedRow() | ||
| 98 | } else if storeViewModel.products.isEmpty { | ||
| 99 | Text("Tips unavailable") | ||
| 100 | .foregroundStyle(.secondary) | ||
| 101 | .themedRow() | ||
| 102 | } else { | ||
| 103 | ForEach(storeViewModel.products, id: \.id) { product in | ||
| 104 | Button { | ||
| 105 | Task { | ||
| 106 | await storeViewModel.purchase(product) | ||
| 107 | } | ||
| 108 | } label: { | ||
| 109 | HStack { | ||
| 110 | Text(product.displayName) | ||
| 111 | Spacer() | ||
| 112 | Text(product.displayPrice) | ||
| 113 | .foregroundStyle(.secondary) | ||
| 114 | } | ||
| 115 | } | ||
| 116 | .themedRow() | ||
| 117 | } | ||
| 118 | } | ||
| 119 | } header: { | ||
| 120 | Text("Support Development") | ||
| 121 | } footer: { | ||
| 122 | Text("Tips help cover the costs of development and App Store distribution. They are one-time purchases and do not unlock any features.") | ||
| 123 | } | ||
| 124 | |||
| 125 | if developerToolsVisible { | ||
| 126 | Section { | ||
| 127 | Toggle("Debug Mode", isOn: Binding( | ||
| 128 | get: { appState.isDebugModeEnabled }, | ||
| 129 | set: { appState.isDebugModeEnabled = $0 } | ||
| 130 | )) | ||
| 131 | .themedRow() | ||
| 132 | } header: { | ||
| 133 | Text("Developer") | ||
| 134 | } footer: { | ||
| 135 | Text("Shows raw API payloads and diagnostic details on builds and tickets screens. This stays hidden until explicitly enabled.") | ||
| 136 | } | ||
| 137 | } | ||
| 138 | } | ||
| 139 | .themedList() | ||
| 140 | .navigationTitle("About") | ||
| 141 | .navigationBarTitleDisplayMode(.inline) | ||
| 142 | .task { | ||
| 143 | await storeViewModel.loadProducts() | ||
| 144 | } | ||
| 145 | } | ||
| 146 | } | ||
Hutch/Views/More/MoreView.swift +5
| @@ -61,6 +61,11 @@ struct MoreView: View { | |||
| 61 | Label("Settings", systemImage: "gear") | 61 | Label("Settings", systemImage: "gear") |
| 62 | } | 62 | } |
| 63 | .themedRow() | 63 | .themedRow() |
| 64 | |||
| 65 | NavigationLink(value: MoreRoute.about) { | ||
| 66 | Label("About Hutch", systemImage: "info.circle") | ||
| 67 | } | ||
| 68 | .themedRow() | ||
| 64 | } | 69 | } |
| 65 | 70 | ||
| 66 | Section { | 71 | Section { |
Hutch/Views/More/TipStoreViewModel.swift added +44
| @@ -0,0 +1,44 @@ | |||
| 1 | import StoreKit | ||
| 2 | |||
| 3 | @Observable | ||
| 4 | final class TipStoreViewModel { | ||
| 5 | var products: [Product] = [] | ||
| 6 | var isLoading = false | ||
| 7 | var errorMessage: String? | ||
| 8 | |||
| 9 | private let productIDs = [ | ||
| 10 | "net.cleberg.hutch.tip.small", | ||
| 11 | "net.cleberg.hutch.tip.medium", | ||
| 12 | "net.cleberg.hutch.tip.large" | ||
| 13 | ] | ||
| 14 | |||
| 15 | func loadProducts() async { | ||
| 16 | isLoading = true | ||
| 17 | defer { isLoading = false } | ||
| 18 | do { | ||
| 19 | let fetched = try await Product.products(for: productIDs) | ||
| 20 | // Sort by price ascending to maintain small/medium/large order | ||
| 21 | products = fetched.sorted { $0.price < $1.price } | ||
| 22 | } catch { | ||
| 23 | errorMessage = error.localizedDescription | ||
| 24 | } | ||
| 25 | } | ||
| 26 | |||
| 27 | func purchase(_ product: Product) async { | ||
| 28 | do { | ||
| 29 | let result = try await product.purchase() | ||
| 30 | switch result { | ||
| 31 | case .success(let verification): | ||
| 32 | if case .verified(let transaction) = verification { | ||
| 33 | await transaction.finish() | ||
| 34 | } | ||
| 35 | case .userCancelled, .pending: | ||
| 36 | break | ||
| 37 | @unknown default: | ||
| 38 | break | ||
| 39 | } | ||
| 40 | } catch { | ||
| 41 | errorMessage = error.localizedDescription | ||
| 42 | } | ||
| 43 | } | ||
| 44 | } | ||
Hutch/Views/Settings/SettingsView.swift −134
| @@ -14,7 +14,6 @@ struct SettingsView: View { | |||
| 14 | appearanceSection() | 14 | appearanceSection() |
| 15 | behaviorSection() | 15 | behaviorSection() |
| 16 | authenticationSection() | 16 | authenticationSection() |
| 17 | aboutSection() | ||
| 18 | } | 17 | } |
| 19 | .themedList() | 18 | .themedList() |
| 20 | .navigationTitle("Settings") | 19 | .navigationTitle("Settings") |
| @@ -142,17 +141,6 @@ struct SettingsView: View { | |||
| 142 | } | 141 | } |
| 143 | } | 142 | } |
| 144 | 143 | ||
| 145 | @ViewBuilder | ||
| 146 | private func aboutSection() -> some View { | ||
| 147 | Section("App") { | ||
| 148 | NavigationLink { | ||
| 149 | AboutView() | ||
| 150 | } label: { | ||
| 151 | SwiftUI.Label("About Hutch", systemImage: "info.circle") | ||
| 152 | } | ||
| 153 | .themedRow() | ||
| 154 | } | ||
| 155 | } | ||
| 156 | } | 144 | } |
| 157 | 145 | ||
| 158 | func settingsBioAttributedString(_ markdown: String) -> AttributedString { | 146 | func settingsBioAttributedString(_ markdown: String) -> AttributedString { |
| @@ -191,125 +179,3 @@ private enum SettingsDestructiveAction { | |||
| 191 | } | 179 | } |
| 192 | } | 180 | } |
| 193 | 181 | ||
| 194 | private struct AboutView: View { | ||
| 195 | @Environment(AppState.self) private var appState | ||
| 196 | private let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String | ||
| 197 | ?? Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String | ||
| 198 | ?? "Hutch" | ||
| 199 | private let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String | ||
| 200 | ?? "Unknown" | ||
| 201 | private let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String | ||
| 202 | ?? "Unknown" | ||
| 203 | @State private var developerRevealCount = 0 | ||
| 204 | |||
| 205 | private var developerToolsVisible: Bool { | ||
| 206 | appState.isDebugModeEnabled || developerRevealCount >= 5 | ||
| 207 | } | ||
| 208 | |||
| 209 | private var developerRevealFooterText: String { | ||
| 210 | developerRevealCount >= 5 | ||
| 211 | ? "Debug toggle unlocked. Scroll down to Developer to enable it." | ||
| 212 | : "Tap the build number 5 times to reveal the debug toggle." | ||
| 213 | } | ||
| 214 | |||
| 215 | var body: some View { | ||
| 216 | Form { | ||
| 217 | Section { | ||
| 218 | VStack(alignment: .leading, spacing: 6) { | ||
| 219 | Text(appName) | ||
| 220 | .font(.title2.weight(.semibold)) | ||
| 221 | Text("A native SourceHut client for iOS.") | ||
| 222 | .font(.subheadline) | ||
| 223 | .foregroundStyle(.secondary) | ||
| 224 | } | ||
| 225 | .padding(.vertical, 4) | ||
| 226 | .themedRow() | ||
| 227 | |||
| 228 | LabeledContent("Version", value: version) | ||
| 229 | .onTapGesture { | ||
| 230 | developerRevealCount = min(developerRevealCount + 1, 5) | ||
| 231 | } | ||
| 232 | .themedRow() | ||
| 233 | LabeledContent("Build", value: build) | ||
| 234 | .onTapGesture { | ||
| 235 | developerRevealCount = min(developerRevealCount + 1, 5) | ||
| 236 | } | ||
| 237 | .themedRow() | ||
| 238 | } footer: { | ||
| 239 | Text(developerRevealFooterText) | ||
| 240 | } | ||
| 241 | |||
| 242 | Section("Links") { | ||
| 243 | Link(destination: URL(string: "https://sr.ht")!) { | ||
| 244 | SwiftUI.Label("SourceHut", systemImage: "link") | ||
| 245 | } | ||
| 246 | .themedRow() | ||
| 247 | Link(destination: URL(string: "https://man.sr.ht")!) { | ||
| 248 | SwiftUI.Label("SourceHut Manuals", systemImage: "book") | ||
| 249 | } | ||
| 250 | .themedRow() | ||
| 251 | Link(destination: URL(string: "https://sr.ht/~ccleberg/Hutch")!) { | ||
| 252 | SwiftUI.Label("Project Repository", systemImage: "folder") | ||
| 253 | } | ||
| 254 | .themedRow() | ||
| 255 | } | ||
| 256 | |||
| 257 | Section("Support") { | ||
| 258 | Link(destination: URL(string: "mailto:hello@cleberg.net")!) { | ||
| 259 | SwiftUI.Label("Email Support", systemImage: "envelope") | ||
| 260 | } | ||
| 261 | .themedRow() | ||
| 262 | } | ||
| 263 | |||
| 264 | Section("Privacy") { | ||
| 265 | Text("Hutch uses your SourceHut personal access token to make requests on your behalf. The token is stored locally in the iOS keychain.") | ||
| 266 | .font(.subheadline) | ||
| 267 | .foregroundStyle(.secondary) | ||
| 268 | .themedRow() | ||
| 269 | |||
| 270 | Link(destination: URL(string: "https://zerolabs.sh/hutch/privacy-policy/")!) { | ||
| 271 | SwiftUI.Label("Privacy Policy", systemImage: "hand.raised") | ||
| 272 | } | ||
| 273 | .themedRow() | ||
| 274 | } | ||
| 275 | |||
| 276 | Section("Acknowledgements") { | ||
| 277 | Text("Built for SourceHut users who want quick access to repositories, builds, and tickets on iOS.") | ||
| 278 | .font(.subheadline) | ||
| 279 | .foregroundStyle(.secondary) | ||
| 280 | .themedRow() | ||
| 281 | } | ||
| 282 | |||
| 283 | if developerToolsVisible { | ||
| 284 | Section { | ||
| 285 | Toggle("Debug Mode", isOn: Binding( | ||
| 286 | get: { appState.isDebugModeEnabled }, | ||
| 287 | set: { appState.isDebugModeEnabled = $0 } | ||
| 288 | )) | ||
| 289 | .themedRow() | ||
| 290 | |||
| 291 | NavigationLink { | ||
| 292 | HomePrototypeView() | ||
| 293 | } label: { | ||
| 294 | SwiftUI.Label("Home Prototype", systemImage: "house") | ||
| 295 | } | ||
| 296 | .themedRow() | ||
| 297 | |||
| 298 | NavigationLink { | ||
| 299 | WorkPrototypeView() | ||
| 300 | } label: { | ||
| 301 | SwiftUI.Label("Work Prototype", systemImage: "tray.full") | ||
| 302 | } | ||
| 303 | .themedRow() | ||
| 304 | } header: { | ||
| 305 | Text("Developer") | ||
| 306 | } footer: { | ||
| 307 | Text("Shows raw API payloads and diagnostic details on builds and tickets screens. Home Prototype explores the dashboard structure, while Work Prototype evaluates the personal queue surface. This stays hidden until explicitly enabled.") | ||
| 308 | } | ||
| 309 | } | ||
| 310 | } | ||
| 311 | .themedList() | ||
| 312 | .navigationTitle("About") | ||
| 313 | .navigationBarTitleDisplayMode(.inline) | ||
| 314 | } | ||
| 315 | } | ||
Hutch/Views/Work/WorkPrototypeView.swift deleted −7
| @@ -1,7 +0,0 @@ | |||
| 1 | import SwiftUI | ||
| 2 | |||
| 3 | struct WorkPrototypeView: View { | ||
| 4 | var body: some View { | ||
| 5 | WorkView() | ||
| 6 | } | ||
| 7 | } | ||