Commit a07148fa91

a07148fa918f65b9aa2119d655cb3707a2b9855e

parent: e8db7f85bc

Unsigned

cmc <hello@cleberg.net> · 2026-04-14 00:06 UTC

feat(theme): AMOLED true-black rows via themedRow()

Apply listRowBackground(Color.black) per row in AMOLED mode using ThemedRowStyle
and themedRow() across Lists and Forms. Use themedList() for scroll/grouped
backgrounds. Treat segmented and clear list rows with isAMOLED-aware
listRowBackground where Color.clear was required. Removes reliance on UIKit
appearance for list cells on iOS 16+.

Implements: https://todo.sr.ht/~ccleberg/hutch/24

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 = 67; 518 CURRENT_PROJECT_VERSION = 68;
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.0.6; 535 MARKETING_VERSION = 3.1.0;
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 = 67; 555 CURRENT_PROJECT_VERSION = 68;
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.0.6; 572 MARKETING_VERSION = 3.1.0;
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 = 67; 635 CURRENT_PROJECT_VERSION = 68;
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.0.6; 645 MARKETING_VERSION = 3.1.0;
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 = 67; 664 CURRENT_PROJECT_VERSION = 68;
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.0.6; 674 MARKETING_VERSION = 3.1.0;
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/HutchApp.swift +2
@@ -13,7 +13,9 @@ struct HutchApp: App {
13 .environment(appState) 13 .environment(appState)
14 .environment(networkMonitor) 14 .environment(networkMonitor)
15 .environment(\.displayDensity, displayDensity) 15 .environment(\.displayDensity, displayDensity)
16 .environment(\.isAMOLEDTheme, appTheme == .amoled)
16 .environment(\.defaultMinListRowHeight, displayDensity == .compact ? 36 : 44) 17 .environment(\.defaultMinListRowHeight, displayDensity == .compact ? 36 : 44)
18 .background(appTheme == .amoled ? Color.black : Color.clear)
17 .preferredColorScheme(appTheme.colorScheme) 19 .preferredColorScheme(appTheme.colorScheme)
18 .onOpenURL { url in 20 .onOpenURL { url in
19 if let link = DeepLink(url: url) { 21 if let link = DeepLink(url: url) {
Hutch/App/RootView.swift +21
@@ -4,6 +4,7 @@ import SwiftUI
4/// full-screen sheet for token entry on first launch. 4/// full-screen sheet for token entry on first launch.
5struct RootView: View { 5struct RootView: View {
6 @Environment(AppState.self) private var appState 6 @Environment(AppState.self) private var appState
7 @Environment(\.isAMOLEDTheme) private var isAMOLED
7 @State private var homePath = NavigationPath() 8 @State private var homePath = NavigationPath()
8 @State private var morePath = NavigationPath() 9 @State private var morePath = NavigationPath()
9 @State private var repoPath = NavigationPath() 10 @State private var repoPath = NavigationPath()
@@ -125,6 +126,7 @@ struct RootView: View {
125 .id(appState.sessionIdentity) 126 .id(appState.sessionIdentity)
126 .defaultAppStorage(appState.accountDefaults) 127 .defaultAppStorage(appState.accountDefaults)
127 .modifier(SidebarAdaptableTabStyle()) 128 .modifier(SidebarAdaptableTabStyle())
129 .modifier(AMOLEDToolbarStyle(isAMOLED: isAMOLED))
128 .modifier(TabKeyboardShortcuts(selectedTab: Binding( 130 .modifier(TabKeyboardShortcuts(selectedTab: Binding(
129 get: { appState.selectedTab }, 131 get: { appState.selectedTab },
130 set: { appState.selectedTab = $0 } 132 set: { appState.selectedTab = $0 }
@@ -427,6 +429,25 @@ private struct TabKeyboardShortcuts: ViewModifier {
427 } 429 }
428} 430}
429 431
432// MARK: - AMOLED Toolbar Styling
433
434/// Applies true-black backgrounds to the tab bar and navigation bar when the AMOLED theme is active.
435private struct AMOLEDToolbarStyle: ViewModifier {
436 let isAMOLED: Bool
437
438 func body(content: Content) -> some View {
439 if isAMOLED {
440 content
441 .toolbarBackground(Color.black, for: .tabBar)
442 .toolbarBackground(.visible, for: .tabBar)
443 .toolbarBackground(Color.black, for: .navigationBar)
444 .toolbarBackground(.visible, for: .navigationBar)
445 } else {
446 content
447 }
448 }
449}
450
430// MARK: - iPad Sidebar Adaptable 451// MARK: - iPad Sidebar Adaptable
431 452
432/// Applies `.tabViewStyle(.sidebarAdaptable)` on iOS 18+ so the tab bar 453/// Applies `.tabViewStyle(.sidebarAdaptable)` on iOS 18+ so the tab bar
Hutch/App/ThemeManager.swift +48 −1
@@ -6,6 +6,7 @@ enum AppTheme: String, CaseIterable, Identifiable {
6 case system 6 case system
7 case light 7 case light
8 case dark 8 case dark
9 case amoled
9 10
10 var id: String { rawValue } 11 var id: String { rawValue }
11 12
@@ -14,6 +15,7 @@ enum AppTheme: String, CaseIterable, Identifiable {
14 case .system: "System" 15 case .system: "System"
15 case .light: "Light" 16 case .light: "Light"
16 case .dark: "Dark" 17 case .dark: "Dark"
18 case .amoled: "AMOLED"
17 } 19 }
18 } 20 }
19 21
@@ -22,6 +24,7 @@ enum AppTheme: String, CaseIterable, Identifiable {
22 case .system: nil 24 case .system: nil
23 case .light: .light 25 case .light: .light
24 case .dark: .dark 26 case .dark: .dark
27 case .amoled: .dark
25 } 28 }
26 } 29 }
27} 30}
@@ -46,25 +49,51 @@ private struct DisplayDensityKey: EnvironmentKey {
46 static let defaultValue: DisplayDensity = .standard 49 static let defaultValue: DisplayDensity = .standard
47} 50}
48 51
52private struct IsAMOLEDThemeKey: EnvironmentKey {
53 static let defaultValue: Bool = false
54}
55
49extension EnvironmentValues { 56extension EnvironmentValues {
50 var displayDensity: DisplayDensity { 57 var displayDensity: DisplayDensity {
51 get { self[DisplayDensityKey.self] } 58 get { self[DisplayDensityKey.self] }
52 set { self[DisplayDensityKey.self] = newValue } 59 set { self[DisplayDensityKey.self] = newValue }
53 } 60 }
61
62 var isAMOLEDTheme: Bool {
63 get { self[IsAMOLEDThemeKey.self] }
64 set { self[IsAMOLEDThemeKey.self] = newValue }
65 }
54} 66}
55 67
56// MARK: - Themed List Modifier 68// MARK: - Themed List Modifier
57 69
58/// Combined modifier for List/Form that applies compact section spacing. 70/// Combined modifier for List/Form that applies compact section spacing and AMOLED black backgrounds.
59/// Apply once per List or Form. 71/// Apply once per List or Form.
60struct ThemedListStyle: ViewModifier { 72struct ThemedListStyle: ViewModifier {
61 @Environment(\.displayDensity) private var density 73 @Environment(\.displayDensity) private var density
74 @Environment(\.isAMOLEDTheme) private var isAMOLED
62 75
63 func body(content: Content) -> some View { 76 func body(content: Content) -> some View {
64 content 77 content
65 .listSectionSpacing(density == .compact ? .compact : .default) 78 .listSectionSpacing(density == .compact ? .compact : .default)
66 .contentMargins(.vertical, density == .compact ? 2 : 8, for: .scrollContent) 79 .contentMargins(.vertical, density == .compact ? 2 : 8, for: .scrollContent)
67 .controlSize(density == .compact ? .small : .regular) 80 .controlSize(density == .compact ? .small : .regular)
81 .modifier(AMOLEDListBackground(isAMOLED: isAMOLED))
82 }
83}
84
85/// Hides the default grouped list background and replaces it with true black for OLED screens.
86private struct AMOLEDListBackground: ViewModifier {
87 let isAMOLED: Bool
88
89 func body(content: Content) -> some View {
90 if isAMOLED {
91 content
92 .scrollContentBackground(.hidden)
93 .background(Color.black)
94 } else {
95 content
96 }
68 } 97 }
69} 98}
70 99
@@ -74,3 +103,21 @@ extension View {
74 modifier(ThemedListStyle()) 103 modifier(ThemedListStyle())
75 } 104 }
76} 105}
106
107// MARK: - Themed Row Modifier
108
109struct ThemedRowStyle: ViewModifier {
110 @Environment(\.isAMOLEDTheme) private var isAMOLED
111
112 func body(content: Content) -> some View {
113 content
114 .listRowBackground(isAMOLED ? Color.black : nil)
115 }
116}
117
118extension View {
119 /// Apply to each row view inside a List or Form section to get a true-black background in AMOLED mode.
120 func themedRow() -> some View {
121 modifier(ThemedRowStyle())
122 }
123}
Hutch/Views/Auth/AuthView.swift +7
@@ -15,6 +15,7 @@ struct TokenEntryView: View {
15 Form { 15 Form {
16 Section { 16 Section {
17 Text("Enter your SourceHut personal access token to connect.") 17 Text("Enter your SourceHut personal access token to connect.")
18 .themedRow()
18 } header: { 19 } header: {
19 Text("Welcome to Hutch") 20 Text("Welcome to Hutch")
20 } footer: { 21 } footer: {
@@ -27,6 +28,7 @@ struct TokenEntryView: View {
27 .autocorrectionDisabled() 28 .autocorrectionDisabled()
28 .textInputAutocapitalization(.never) 29 .textInputAutocapitalization(.never)
29 .disabled(isConnecting) 30 .disabled(isConnecting)
31 .themedRow()
30 } header: { 32 } header: {
31 Text("Token") 33 Text("Token")
32 } 34 }
@@ -40,6 +42,7 @@ struct TokenEntryView: View {
40 .foregroundStyle(.red) 42 .foregroundStyle(.red)
41 } 43 }
42 .foregroundStyle(.red) 44 .foregroundStyle(.red)
45 .themedRow()
43 } 46 }
44 } 47 }
45 48
@@ -56,22 +59,26 @@ struct TokenEntryView: View {
56 } 59 }
57 } 60 }
58 .disabled(tokenTrimmed.isEmpty || isConnecting) 61 .disabled(tokenTrimmed.isEmpty || isConnecting)
62 .themedRow()
59 } 63 }
60 64
61 Section { 65 Section {
62 Link(destination: createAccountURL) { 66 Link(destination: createAccountURL) {
63 Label("Create SourceHut account", systemImage: "person.badge.plus") 67 Label("Create SourceHut account", systemImage: "person.badge.plus")
64 } 68 }
69 .themedRow()
65 70
66 Link(destination: personalAccessTokensURL) { 71 Link(destination: personalAccessTokensURL) {
67 Label("Create Personal Access Token", systemImage: "key") 72 Label("Create Personal Access Token", systemImage: "key")
68 } 73 }
74 .themedRow()
69 } header: { 75 } header: {
70 Text("Need an account?") 76 Text("Need an account?")
71 } footer: { 77 } footer: {
72 Text("These links open SourceHut in your browser. After signing up, create a Personal Access Token there and paste it here.") 78 Text("These links open SourceHut in your browser. After signing up, create a Personal Access Token there and paste it here.")
73 } 79 }
74 } 80 }
81 .themedList()
75 .navigationTitle("Hutch") 82 .navigationTitle("Hutch")
76 } 83 }
77 } 84 }
Hutch/Views/Builds/BuildDetailView.swift +25
@@ -176,26 +176,34 @@ struct BuildDetailView: View {
176 .font(.subheadline.weight(.medium)) 176 .font(.subheadline.weight(.medium))
177 } 177 }
178 } 178 }
179 .themedRow()
179 180
180 if let note = job.note, !note.isEmpty { 181 if let note = job.note, !note.isEmpty {
181 LabeledContent("Note", value: note) 182 LabeledContent("Note", value: note)
183 .themedRow()
182 } 184 }
183 185
184 if let image = job.image { 186 if let image = job.image {
185 LabeledContent("Image", value: image) 187 LabeledContent("Image", value: image)
188 .themedRow()
186 } 189 }
187 190
188 if !job.tags.isEmpty { 191 if !job.tags.isEmpty {
189 LabeledContent("Tags", value: job.tags.joined(separator: ", ")) 192 LabeledContent("Tags", value: job.tags.joined(separator: ", "))
193 .themedRow()
190 } 194 }
191 195
192 if let visibility = job.visibility { 196 if let visibility = job.visibility {
193 LabeledContent("Visibility", value: visibility.rawValue.capitalized) 197 LabeledContent("Visibility", value: visibility.rawValue.capitalized)
198 .themedRow()
194 } 199 }
195 200
196 LabeledContent("Owner", value: job.owner.canonicalName) 201 LabeledContent("Owner", value: job.owner.canonicalName)
202 .themedRow()
197 LabeledContent("Created", value: job.created.relativeDescription) 203 LabeledContent("Created", value: job.created.relativeDescription)
204 .themedRow()
198 LabeledContent("Updated", value: job.updated.relativeDescription) 205 LabeledContent("Updated", value: job.updated.relativeDescription)
206 .themedRow()
199 } 207 }
200 208
201 if appState.isDebugModeEnabled { 209 if appState.isDebugModeEnabled {
@@ -211,9 +219,11 @@ struct BuildDetailView: View {
211 url: \(SRHTWebURL.build(jobId: job.id, ownerCanonicalName: job.owner.canonicalName)?.absoluteString ?? "unavailable") 219 url: \(SRHTWebURL.build(jobId: job.id, ownerCanonicalName: job.owner.canonicalName)?.absoluteString ?? "unavailable")
212 """ 220 """
213 ) 221 )
222 .themedRow()
214 223
215 if let rawJobResponse = viewModel.rawJobResponse { 224 if let rawJobResponse = viewModel.rawJobResponse {
216 DebugTextBlock(title: "Raw Response", content: rawJobResponse) 225 DebugTextBlock(title: "Raw Response", content: rawJobResponse)
226 .themedRow()
217 } 227 }
218 } 228 }
219 } 229 }
@@ -237,6 +247,7 @@ struct BuildDetailView: View {
237 } 247 }
238 } 248 }
239 .disabled(isOpeningRepository) 249 .disabled(isOpeningRepository)
250 .themedRow()
240 } 251 }
241 } 252 }
242 253
@@ -248,6 +259,7 @@ struct BuildDetailView: View {
248 openURL(url) 259 openURL(url)
249 } 260 }
250 } 261 }
262 .themedRow()
251 } header: { 263 } header: {
252 Text("Artifacts") 264 Text("Artifacts")
253 } footer: { 265 } footer: {
@@ -275,6 +287,7 @@ struct BuildDetailView: View {
275 } 287 }
276 } 288 }
277 .foregroundStyle(.primary) 289 .foregroundStyle(.primary)
290 .themedRow()
278 } header: { 291 } header: {
279 HStack(spacing: 6) { 292 HStack(spacing: 6) {
280 TaskStatusIcon(status: task.status) 293 TaskStatusIcon(status: task.status)
@@ -299,6 +312,7 @@ struct BuildDetailView: View {
299 } 312 }
300 } 313 }
301 .disabled(viewModel.isCancelling) 314 .disabled(viewModel.isCancelling)
315 .themedRow()
302 } 316 }
303 } 317 }
304 318
@@ -319,6 +333,7 @@ struct BuildDetailView: View {
319 } 333 }
320 } 334 }
321 .disabled(viewModel.isRebuilding) 335 .disabled(viewModel.isRebuilding)
336 .themedRow()
322 337
323 Button { 338 Button {
324 showEditResubmitSheet = true 339 showEditResubmitSheet = true
@@ -326,11 +341,13 @@ struct BuildDetailView: View {
326 Text("Edit & Resubmit") 341 Text("Edit & Resubmit")
327 } 342 }
328 .disabled(viewModel.isSubmittingEditedBuild) 343 .disabled(viewModel.isSubmittingEditedBuild)
344 .themedRow()
329 } footer: { 345 } footer: {
330 Text("Creates a new build using this job’s saved manifest, tags, note, and visibility.") 346 Text("Creates a new build using this job’s saved manifest, tags, note, and visibility.")
331 } 347 }
332 } 348 }
333 } 349 }
350 .themedList()
334 .task(id: job.id) { 351 .task(id: job.id) {
335 RecentActivityStore.recordBuild( 352 RecentActivityStore.recordBuild(
336 jobId: job.id, 353 jobId: job.id,
@@ -463,26 +480,33 @@ private struct EditResubmitBuildSheet: View {
463 .lineLimit(12...24) 480 .lineLimit(12...24)
464 .textInputAutocapitalization(.never) 481 .textInputAutocapitalization(.never)
465 .autocorrectionDisabled() 482 .autocorrectionDisabled()
483 .themedRow()
466 } 484 }
467 485
468 Section("Build Options") { 486 Section("Build Options") {
469 TextField("Note (optional)", text: $note) 487 TextField("Note (optional)", text: $note)
488 .themedRow()
470 TextField("Tags (comma-separated, optional)", text: $tagsText) 489 TextField("Tags (comma-separated, optional)", text: $tagsText)
471 .textInputAutocapitalization(.never) 490 .textInputAutocapitalization(.never)
472 .autocorrectionDisabled() 491 .autocorrectionDisabled()
492 .themedRow()
473 Picker("Visibility", selection: $visibility) { 493 Picker("Visibility", selection: $visibility) {
474 Text("Public").tag(Visibility.public) 494 Text("Public").tag(Visibility.public)
475 Text("Unlisted").tag(Visibility.unlisted) 495 Text("Unlisted").tag(Visibility.unlisted)
476 Text("Private").tag(Visibility.private) 496 Text("Private").tag(Visibility.private)
477 } 497 }
498 .themedRow()
478 Toggle("Start build now", isOn: $execute) 499 Toggle("Start build now", isOn: $execute)
500 .themedRow()
479 Toggle("Allow build secrets", isOn: $secrets) 501 Toggle("Allow build secrets", isOn: $secrets)
502 .themedRow()
480 } 503 }
481 504
482 Section { 505 Section {
483 Text("This submits a new build. “Start build now” and “Allow build secrets” use local defaults because the current job does not include those original values.") 506 Text("This submits a new build. “Start build now” and “Allow build secrets” use local defaults because the current job does not include those original values.")
484 .font(.footnote) 507 .font(.footnote)
485 .foregroundStyle(.secondary) 508 .foregroundStyle(.secondary)
509 .themedRow()
486 } 510 }
487 511
488 if let actionError = viewModel.actionError { 512 if let actionError = viewModel.actionError {
@@ -494,6 +518,7 @@ private struct EditResubmitBuildSheet: View {
494 .foregroundStyle(.red) 518 .foregroundStyle(.red)
495 } 519 }
496 .foregroundStyle(.red) 520 .foregroundStyle(.red)
521 .themedRow()
497 } 522 }
498 } 523 }
499 } 524 }
Hutch/Views/Builds/BuildListView.swift +13 −1
@@ -5,6 +5,7 @@ struct BuildListView: View {
5 @AppStorage(AppStorageKeys.buildsAutoRefreshInterval) private var autoRefreshRawValue = 0 5 @AppStorage(AppStorageKeys.buildsAutoRefreshInterval) private var autoRefreshRawValue = 0
6 @AppStorage(AppStorageKeys.buildsRepoFilter) private var savedRepoFilter = "" 6 @AppStorage(AppStorageKeys.buildsRepoFilter) private var savedRepoFilter = ""
7 @Environment(AppState.self) private var appState 7 @Environment(AppState.self) private var appState
8 @Environment(\.isAMOLEDTheme) private var isAMOLED
8 @State private var viewModel: BuildListViewModel? 9 @State private var viewModel: BuildListViewModel?
9 @State private var showSubmitSheet = false 10 @State private var showSubmitSheet = false
10 @State private var submittedJobId: Int? 11 @State private var submittedJobId: Int?
@@ -134,7 +135,7 @@ struct BuildListView: View {
134 .padding(.top, 6) 135 .padding(.top, 6)
135 .padding(.bottom, 10) 136 .padding(.bottom, 10)
136 .listRowInsets(EdgeInsets()) 137 .listRowInsets(EdgeInsets())
137 .listRowBackground(Color.clear) 138 .listRowBackground(isAMOLED ? Color.black : Color.clear)
138 .listRowSeparator(.hidden) 139 .listRowSeparator(.hidden)
139 140
140 ForEach(viewModel.filteredJobs) { job in 141 ForEach(viewModel.filteredJobs) { job in
@@ -181,6 +182,7 @@ struct BuildListView: View {
181 await viewModel.loadMoreIfNeeded(currentItem: job) 182 await viewModel.loadMoreIfNeeded(currentItem: job)
182 } 183 }
183 } 184 }
185 .themedRow()
184 186
185 if viewModel.isLoadingMore { 187 if viewModel.isLoadingMore {
186 HStack { 188 HStack {
@@ -189,6 +191,7 @@ struct BuildListView: View {
189 Spacer() 191 Spacer()
190 } 192 }
191 .listRowSeparator(.hidden) 193 .listRowSeparator(.hidden)
194 .themedRow()
192 } 195 }
193 } 196 }
194 } 197 }
@@ -279,26 +282,33 @@ private struct SubmitBuildSheet: View {
279 .lineLimit(12...24) 282 .lineLimit(12...24)
280 .textInputAutocapitalization(.never) 283 .textInputAutocapitalization(.never)
281 .autocorrectionDisabled() 284 .autocorrectionDisabled()
285 .themedRow()
282 } 286 }
283 287
284 Section("Build Options") { 288 Section("Build Options") {
285 TextField("Note (optional)", text: $note) 289 TextField("Note (optional)", text: $note)
290 .themedRow()
286 TextField("Tags (comma-separated, optional)", text: $tagsText) 291 TextField("Tags (comma-separated, optional)", text: $tagsText)
287 .textInputAutocapitalization(.never) 292 .textInputAutocapitalization(.never)
288 .autocorrectionDisabled() 293 .autocorrectionDisabled()
294 .themedRow()
289 Picker("Visibility", selection: $visibility) { 295 Picker("Visibility", selection: $visibility) {
290 Text("Public").tag(Visibility.public) 296 Text("Public").tag(Visibility.public)
291 Text("Unlisted").tag(Visibility.unlisted) 297 Text("Unlisted").tag(Visibility.unlisted)
292 Text("Private").tag(Visibility.private) 298 Text("Private").tag(Visibility.private)
293 } 299 }
300 .themedRow()
294 Toggle("Start build now", isOn: $execute) 301 Toggle("Start build now", isOn: $execute)
302 .themedRow()
295 Toggle("Allow build secrets", isOn: $secrets) 303 Toggle("Allow build secrets", isOn: $secrets)
304 .themedRow()
296 } 305 }
297 306
298 Section { 307 Section {
299 Text("You need a valid builds.sr.ht manifest and a token with BUILDS:RW.") 308 Text("You need a valid builds.sr.ht manifest and a token with BUILDS:RW.")
300 .font(.footnote) 309 .font(.footnote)
301 .foregroundStyle(.secondary) 310 .foregroundStyle(.secondary)
311 .themedRow()
302 } 312 }
303 313
304 if let error = viewModel.error { 314 if let error = viewModel.error {
@@ -310,9 +320,11 @@ private struct SubmitBuildSheet: View {
310 .foregroundStyle(.red) 320 .foregroundStyle(.red)
311 } 321 }
312 .foregroundStyle(.red) 322 .foregroundStyle(.red)
323 .themedRow()
313 } 324 }
314 } 325 }
315 } 326 }
327 .themedList()
316 .navigationTitle("Submit Build") 328 .navigationTitle("Submit Build")
317 .navigationBarTitleDisplayMode(.inline) 329 .navigationBarTitleDisplayMode(.inline)
318 .onDisappear { 330 .onDisappear {
Hutch/Views/Home/HomeView.swift +6
@@ -94,6 +94,7 @@ struct HomeView: View {
94 ) 94 )
95 } 95 }
96 .buttonStyle(.plain) 96 .buttonStyle(.plain)
97 .themedRow()
97 } 98 }
98 } 99 }
99 } 100 }
@@ -109,6 +110,7 @@ struct HomeView: View {
109 emphasis: .action 110 emphasis: .action
110 ) 111 )
111 } 112 }
113 .themedRow()
112 } 114 }
113 } 115 }
114 116
@@ -126,6 +128,7 @@ struct HomeView: View {
126 .disabled(isOpeningRecentItem) 128 .disabled(isOpeningRecentItem)
127 .listRowSeparator(.hidden) 129 .listRowSeparator(.hidden)
128 } 130 }
131 .themedRow()
129 } 132 }
130 } 133 }
131 } 134 }
@@ -143,6 +146,7 @@ struct HomeView: View {
143 emphasis: .monitoring 146 emphasis: .monitoring
144 ) 147 )
145 } 148 }
149 .themedRow()
146 } 150 }
147 } 151 }
148 152
@@ -156,6 +160,7 @@ struct HomeView: View {
156 } label: { 160 } label: {
157 HomeCompactMessageRow(text: "Pin projects for quick access", systemImage: "pin") 161 HomeCompactMessageRow(text: "Pin projects for quick access", systemImage: "pin")
158 } 162 }
163 .themedRow()
159 } else { 164 } else {
160 LazyVGrid( 165 LazyVGrid(
161 columns: [ 166 columns: [
@@ -174,6 +179,7 @@ struct HomeView: View {
174 } 179 }
175 } 180 }
176 .padding(.vertical, 2) 181 .padding(.vertical, 2)
182 .themedRow()
177 } 183 }
178 } 184 }
179 } 185 }
Hutch/Views/Inbox/ThreadDetailView.swift +6
@@ -123,6 +123,7 @@ struct ThreadDetailView: View {
123 .foregroundStyle(.secondary) 123 .foregroundStyle(.secondary)
124 } 124 }
125 .padding(.vertical, 4) 125 .padding(.vertical, 4)
126 .themedRow()
126 } 127 }
127 128
128 Section("Related") { 129 Section("Related") {
@@ -138,6 +139,7 @@ struct ThreadDetailView: View {
138 } label: { 139 } label: {
139 Label(thread.listDisplayName, systemImage: "list.bullet") 140 Label(thread.listDisplayName, systemImage: "list.bullet")
140 } 141 }
142 .themedRow()
141 143
142 if let repo = self.thread.repo { 144 if let repo = self.thread.repo {
143 Button { 145 Button {
@@ -157,6 +159,7 @@ struct ThreadDetailView: View {
157 } 159 }
158 } 160 }
159 .disabled(isOpeningRepository) 161 .disabled(isOpeningRepository)
162 .themedRow()
160 } 163 }
161 } 164 }
162 165
@@ -165,6 +168,7 @@ struct ThreadDetailView: View {
165 Text(partialWarning) 168 Text(partialWarning)
166 .font(.caption) 169 .font(.caption)
167 .foregroundStyle(.secondary) 170 .foregroundStyle(.secondary)
171 .themedRow()
168 } 172 }
169 } 173 }
170 174
@@ -184,8 +188,10 @@ struct ThreadDetailView: View {
184 } 188 }
185 ) 189 )
186 } 190 }
191 .themedRow()
187 } 192 }
188 } 193 }
194 .themedList()
189 .listStyle(.plain) 195 .listStyle(.plain)
190 .toolbar { 196 .toolbar {
191 ToolbarItem(placement: .topBarTrailing) { 197 ToolbarItem(placement: .topBarTrailing) {
Hutch/Views/Lists/MailingListListView.swift +2
@@ -142,7 +142,9 @@ struct MailingListListView: View {
142 .padding(.vertical, 2) 142 .padding(.vertical, 2)
143 } 143 }
144 } 144 }
145 .themedRow()
145 } 146 }
147 .themedList()
146 .listStyle(.plain) 148 .listStyle(.plain)
147 .searchable( 149 .searchable(
148 text: $vm.searchText, 150 text: $vm.searchText,
Hutch/Views/Lookup/LookupView.swift +5
@@ -356,6 +356,7 @@ struct LookupView: View {
356 } 356 }
357 } 357 }
358 .pickerStyle(.menu) 358 .pickerStyle(.menu)
359 .themedRow()
359 360
360 TextField( 361 TextField(
361 vm.selectedType.inputLabel, 362 vm.selectedType.inputLabel,
@@ -368,6 +369,7 @@ struct LookupView: View {
368 .onSubmit { 369 .onSubmit {
369 Task { await vm.lookup() } 370 Task { await vm.lookup() }
370 } 371 }
372 .themedRow()
371 } 373 }
372 374
373 Section { 375 Section {
@@ -382,6 +384,7 @@ struct LookupView: View {
382 ProgressView() 384 ProgressView()
383 } 385 }
384 } 386 }
387 .themedRow()
385 } 388 }
386 389
387 if !vm.history.isEmpty { 390 if !vm.history.isEmpty {
@@ -403,11 +406,13 @@ struct LookupView: View {
403 } 406 }
404 .disabled(vm.isLooking) 407 .disabled(vm.isLooking)
405 } 408 }
409 .themedRow()
406 410
407 Button("Clear History", role: .destructive) { 411 Button("Clear History", role: .destructive) {
408 vm.clearHistory() 412 vm.clearHistory()
409 } 413 }
410 .disabled(vm.isLooking) 414 .disabled(vm.isLooking)
415 .themedRow()
411 } 416 }
412 } 417 }
413 } 418 }
Hutch/Views/Lookup/UserProfileView.swift +23 −1
@@ -2,6 +2,7 @@ import SwiftUI
2 2
3struct UserProfileView: View { 3struct UserProfileView: View {
4 @Environment(AppState.self) private var appState 4 @Environment(AppState.self) private var appState
5 @Environment(\.isAMOLEDTheme) private var isAMOLED
5 @AppStorage(AppStorageKeys.contributionGraphsEnabled, store: .standard) private var contributionGraphsEnabled = true 6 @AppStorage(AppStorageKeys.contributionGraphsEnabled, store: .standard) private var contributionGraphsEnabled = true
6 7
7 let user: User 8 let user: User
@@ -55,33 +56,41 @@ struct UserProfileView: View {
55 } 56 }
56 Spacer() 57 Spacer()
57 } 58 }
58 .listRowBackground(Color.clear) 59 .listRowBackground(isAMOLED ? Color.black : Color.clear)
59 } 60 }
60 } 61 }
61 62
62 Section { 63 Section {
63 LabeledContent("Username", value: user.username) 64 LabeledContent("Username", value: user.username)
65 .themedRow()
64 LabeledContent("Canonical Name", value: user.canonicalName) 66 LabeledContent("Canonical Name", value: user.canonicalName)
67 .themedRow()
65 if let userType = user.userType { 68 if let userType = user.userType {
66 LabeledContent("User Type", value: userType) 69 LabeledContent("User Type", value: userType)
70 .themedRow()
67 } 71 }
68 if let pronouns = user.pronouns { 72 if let pronouns = user.pronouns {
69 LabeledContent("Pronouns", value: pronouns) 73 LabeledContent("Pronouns", value: pronouns)
74 .themedRow()
70 } 75 }
71 if let suspensionNotice = user.suspensionNotice { 76 if let suspensionNotice = user.suspensionNotice {
72 LabeledContent("Suspension Notice", value: suspensionNotice) 77 LabeledContent("Suspension Notice", value: suspensionNotice)
78 .themedRow()
73 } 79 }
74 } 80 }
75 81
76 Section { 82 Section {
77 LabeledContent("Email", value: user.email) 83 LabeledContent("Email", value: user.email)
84 .themedRow()
78 if let urlString = user.url, let url = URL(string: urlString) { 85 if let urlString = user.url, let url = URL(string: urlString) {
79 LabeledContent("URL") { 86 LabeledContent("URL") {
80 Link(urlString, destination: url) 87 Link(urlString, destination: url)
81 } 88 }
89 .themedRow()
82 } 90 }
83 if let location = user.location { 91 if let location = user.location {
84 LabeledContent("Location", value: location) 92 LabeledContent("Location", value: location)
93 .themedRow()
85 } 94 }
86 } 95 }
87 96
@@ -91,6 +100,7 @@ struct UserProfileView: View {
91 .frame(maxWidth: .infinity, alignment: .leading) 100 .frame(maxWidth: .infinity, alignment: .leading)
92 .tint(.accentColor) 101 .tint(.accentColor)
93 .textSelection(.enabled) 102 .textSelection(.enabled)
103 .themedRow()
94 } 104 }
95 } 105 }
96 106
@@ -98,9 +108,11 @@ struct UserProfileView: View {
98 Section { 108 Section {
99 if let created = user.created { 109 if let created = user.created {
100 LabeledContent("Joined", value: formattedTimestamp(created)) 110 LabeledContent("Joined", value: formattedTimestamp(created))
111 .themedRow()
101 } 112 }
102 if let updated = user.updated { 113 if let updated = user.updated {
103 LabeledContent("Updated", value: formattedTimestamp(updated)) 114 LabeledContent("Updated", value: formattedTimestamp(updated))
115 .themedRow()
104 } 116 }
105 } 117 }
106 } 118 }
@@ -118,15 +130,18 @@ struct UserProfileView: View {
118 error: viewModel.contributionsError ?? viewModel.contributionStatusText, 130 error: viewModel.contributionsError ?? viewModel.contributionStatusText,
119 isIndexedButEmpty: viewModel.isContributionActivityIndexedButEmpty 131 isIndexedButEmpty: viewModel.isContributionActivityIndexedButEmpty
120 ) 132 )
133 .themedRow()
121 } 134 }
122 } 135 }
123 136
124 Section { 137 Section {
125 if viewModel.isLoadingRepositories && viewModel.repositories.isEmpty { 138 if viewModel.isLoadingRepositories && viewModel.repositories.isEmpty {
126 ProgressView() 139 ProgressView()
140 .themedRow()
127 } else if viewModel.repositories.isEmpty { 141 } else if viewModel.repositories.isEmpty {
128 Text("No public repositories.") 142 Text("No public repositories.")
129 .foregroundStyle(.secondary) 143 .foregroundStyle(.secondary)
144 .themedRow()
130 } else { 145 } else {
131 ForEach(viewModel.repositories.prefix(4)) { repo in 146 ForEach(viewModel.repositories.prefix(4)) { repo in
132 NavigationLink { 147 NavigationLink {
@@ -137,10 +152,12 @@ struct UserProfileView: View {
137 RepositoryRowView(repository: repo, buildStatus: .none) 152 RepositoryRowView(repository: repo, buildStatus: .none)
138 } 153 }
139 } 154 }
155 .themedRow()
140 if viewModel.repositories.count > 4 { 156 if viewModel.repositories.count > 4 {
141 NavigationLink("See All") { 157 NavigationLink("See All") {
142 UserRepositoriesView(viewModel: viewModel) 158 UserRepositoriesView(viewModel: viewModel)
143 } 159 }
160 .themedRow()
144 } 161 }
145 } 162 }
146 } header: { 163 } header: {
@@ -150,9 +167,11 @@ struct UserProfileView: View {
150 Section { 167 Section {
151 if viewModel.isLoadingTrackers && viewModel.trackers.isEmpty { 168 if viewModel.isLoadingTrackers && viewModel.trackers.isEmpty {
152 ProgressView() 169 ProgressView()
170 .themedRow()
153 } else if viewModel.trackers.isEmpty { 171 } else if viewModel.trackers.isEmpty {
154 Text("No public trackers.") 172 Text("No public trackers.")
155 .foregroundStyle(.secondary) 173 .foregroundStyle(.secondary)
174 .themedRow()
156 } else { 175 } else {
157 ForEach(viewModel.trackers.prefix(4)) { tracker in 176 ForEach(viewModel.trackers.prefix(4)) { tracker in
158 NavigationLink { 177 NavigationLink {
@@ -161,10 +180,12 @@ struct UserProfileView: View {
161 UserProfileTrackerRowView(tracker: tracker) 180 UserProfileTrackerRowView(tracker: tracker)
162 } 181 }
163 } 182 }
183 .themedRow()
164 if viewModel.trackers.count > 4 { 184 if viewModel.trackers.count > 4 {
165 NavigationLink("See All") { 185 NavigationLink("See All") {
166 UserTrackersView(viewModel: viewModel) 186 UserTrackersView(viewModel: viewModel)
167 } 187 }
188 .themedRow()
168 } 189 }
169 } 190 }
170 } header: { 191 } header: {
@@ -172,6 +193,7 @@ struct UserProfileView: View {
172 } 193 }
173 } 194 }
174 } 195 }
196 .themedList()
175 .listStyle(.insetGrouped) 197 .listStyle(.insetGrouped)
176 .navigationTitle(user.canonicalName) 198 .navigationTitle(user.canonicalName)
177 .navigationBarTitleDisplayMode(.inline) 199 .navigationBarTitleDisplayMode(.inline)
Hutch/Views/Lookup/UserRepositoriesView.swift +2
@@ -14,7 +14,9 @@ struct UserRepositoriesView: View {
14 RepositoryRowView(repository: repo, buildStatus: .none) 14 RepositoryRowView(repository: repo, buildStatus: .none)
15 } 15 }
16 } 16 }
17 .themedRow()
17 } 18 }
19 .themedList()
18 .listStyle(.plain) 20 .listStyle(.plain)
19 .navigationTitle("Repositories") 21 .navigationTitle("Repositories")
20 .navigationBarTitleDisplayMode(.inline) 22 .navigationBarTitleDisplayMode(.inline)
Hutch/Views/Lookup/UserTrackersView.swift +2
@@ -12,7 +12,9 @@ struct UserTrackersView: View {
12 UserProfileTrackerRowView(tracker: tracker) 12 UserProfileTrackerRowView(tracker: tracker)
13 } 13 }
14 } 14 }
15 .themedRow()
15 } 16 }
17 .themedList()
16 .listStyle(.plain) 18 .listStyle(.plain)
17 .navigationTitle("Trackers") 19 .navigationTitle("Trackers")
18 .navigationBarTitleDisplayMode(.inline) 20 .navigationBarTitleDisplayMode(.inline)
Hutch/Views/More/AccountSwitcherView.swift +3
@@ -48,6 +48,7 @@ struct AccountSwitcherView: View {
48 pendingRemoval = appState.accounts[index] 48 pendingRemoval = appState.accounts[index]
49 } 49 }
50 } 50 }
51 .themedRow()
51 } 52 }
52 53
53 Section { 54 Section {
@@ -57,8 +58,10 @@ struct AccountSwitcherView: View {
57 Label("Add Account", systemImage: "plus.circle") 58 Label("Add Account", systemImage: "plus.circle")
58 } 59 }
59 .disabled(isSwitching) 60 .disabled(isSwitching)
61 .themedRow()
60 } 62 }
61 } 63 }
64 .themedList()
62 .navigationTitle("Accounts") 65 .navigationTitle("Accounts")
63 .navigationBarTitleDisplayMode(.inline) 66 .navigationBarTitleDisplayMode(.inline)
64 .toolbar { 67 .toolbar {
Hutch/Views/More/AddAccountView.swift +3
@@ -15,6 +15,7 @@ struct AddAccountView: View {
15 SecureField("Personal Access Token", text: $token) 15 SecureField("Personal Access Token", text: $token)
16 .autocorrectionDisabled() 16 .autocorrectionDisabled()
17 .textInputAutocapitalization(.never) 17 .textInputAutocapitalization(.never)
18 .themedRow()
18 } footer: { 19 } footer: {
19 Text("Generate a token at meta.sr.ht → OAuth2 clients.") 20 Text("Generate a token at meta.sr.ht → OAuth2 clients.")
20 } 21 }
@@ -23,9 +24,11 @@ struct AddAccountView: View {
23 Section { 24 Section {
24 Text(errorMessage) 25 Text(errorMessage)
25 .foregroundStyle(.red) 26 .foregroundStyle(.red)
27 .themedRow()
26 } 28 }
27 } 29 }
28 } 30 }
31 .themedList()
29 .navigationTitle("Add Account") 32 .navigationTitle("Add Account")
30 .navigationBarTitleDisplayMode(.inline) 33 .navigationBarTitleDisplayMode(.inline)
31 .toolbar { 34 .toolbar {
Hutch/Views/More/ManPageBrowserView.swift +2
@@ -25,8 +25,10 @@ struct ManPageBrowserView: View {
25 Text(doc.title) 25 Text(doc.title)
26 } 26 }
27 } 27 }
28 .themedRow()
28 } 29 }
29 } 30 }
31 .themedList()
30 .navigationTitle("Man Pages") 32 .navigationTitle("Man Pages")
31 .navigationBarTitleDisplayMode(.inline) 33 .navigationBarTitleDisplayMode(.inline)
32 } 34 }
Hutch/Views/More/MoreView.swift +9
@@ -16,24 +16,29 @@ struct MoreView: View {
16 NavigationLink(value: MoreRoute.lookup) { 16 NavigationLink(value: MoreRoute.lookup) {
17 Label("Look Up", systemImage: "magnifyingglass") 17 Label("Look Up", systemImage: "magnifyingglass")
18 } 18 }
19 .themedRow()
19 } 20 }
20 21
21 Section("Other Services") { 22 Section("Other Services") {
22 NavigationLink(value: MoreRoute.projects) { 23 NavigationLink(value: MoreRoute.projects) {
23 Label("Projects", systemImage: "square.stack.3d.up") 24 Label("Projects", systemImage: "square.stack.3d.up")
24 } 25 }
26 .themedRow()
25 27
26 NavigationLink(value: MoreRoute.lists) { 28 NavigationLink(value: MoreRoute.lists) {
27 Label("Mailing Lists", systemImage: "list.bullet.rectangle") 29 Label("Mailing Lists", systemImage: "list.bullet.rectangle")
28 } 30 }
31 .themedRow()
29 32
30 NavigationLink(value: MoreRoute.manPageBrowser) { 33 NavigationLink(value: MoreRoute.manPageBrowser) {
31 Label("Man Pages", systemImage: "book") 34 Label("Man Pages", systemImage: "book")
32 } 35 }
36 .themedRow()
33 37
34 NavigationLink(value: MoreRoute.pastes) { 38 NavigationLink(value: MoreRoute.pastes) {
35 Label("Pastes", systemImage: "doc.on.clipboard") 39 Label("Pastes", systemImage: "doc.on.clipboard")
36 } 40 }
41 .themedRow()
37 42
38 NavigationLink(value: MoreRoute.systemStatus) { 43 NavigationLink(value: MoreRoute.systemStatus) {
39 SystemStatusSummaryRow( 44 SystemStatusSummaryRow(
@@ -43,16 +48,19 @@ struct MoreView: View {
43 isShowingStaleData: viewModel?.isShowingStaleSystemStatus ?? false 48 isShowingStaleData: viewModel?.isShowingStaleSystemStatus ?? false
44 ) 49 )
45 } 50 }
51 .themedRow()
46 } 52 }
47 53
48 Section("Meta") { 54 Section("Meta") {
49 NavigationLink(value: MoreRoute.profile) { 55 NavigationLink(value: MoreRoute.profile) {
50 Label("Profile", systemImage: "person.text.rectangle") 56 Label("Profile", systemImage: "person.text.rectangle")
51 } 57 }
58 .themedRow()
52 59
53 NavigationLink(value: MoreRoute.settings) { 60 NavigationLink(value: MoreRoute.settings) {
54 Label("Settings", systemImage: "gear") 61 Label("Settings", systemImage: "gear")
55 } 62 }
63 .themedRow()
56 } 64 }
57 65
58 Section { 66 Section {
@@ -61,6 +69,7 @@ struct MoreView: View {
61 Label(item.title, systemImage: "safari") 69 Label(item.title, systemImage: "safari")
62 } 70 }
63 } 71 }
72 .themedRow()
64 } header: { 73 } header: {
65 Text("External Links") 74 Text("External Links")
66 } footer: { 75 } footer: {
Hutch/Views/More/ProfileView.swift +29 −2
@@ -79,6 +79,7 @@ struct ProfileView: View {
79 error: contributionViewModel.contributionsError ?? contributionViewModel.contributionStatusText, 79 error: contributionViewModel.contributionsError ?? contributionViewModel.contributionStatusText,
80 isIndexedButEmpty: contributionViewModel.isContributionActivityIndexedButEmpty 80 isIndexedButEmpty: contributionViewModel.isContributionActivityIndexedButEmpty
81 ) 81 )
82 .themedRow()
82 } 83 }
83 } 84 }
84 85
@@ -185,6 +186,7 @@ struct ProfileView: View {
185 } 186 }
186 } 187 }
187 .padding(.vertical, 4) 188 .padding(.vertical, 4)
189 .themedRow()
188 190
189 if let bio = profile.bio, !bio.isEmpty { 191 if let bio = profile.bio, !bio.isEmpty {
190 VStack(alignment: .leading, spacing: 2) { 192 VStack(alignment: .leading, spacing: 2) {
@@ -193,36 +195,44 @@ struct ProfileView: View {
193 .foregroundStyle(.secondary) 195 .foregroundStyle(.secondary)
194 ProfileBioView(markdown: bio) 196 ProfileBioView(markdown: bio)
195 } 197 }
198 .themedRow()
196 } 199 }
197 200
198 if let location = profile.location, !location.isEmpty { 201 if let location = profile.location, !location.isEmpty {
199 LabeledContent("Location", value: location) 202 LabeledContent("Location", value: location)
203 .themedRow()
200 } 204 }
201 205
202 if let url = profile.url, !url.isEmpty { 206 if let url = profile.url, !url.isEmpty {
203 LabeledContent("URL", value: url) 207 LabeledContent("URL", value: url)
208 .themedRow()
204 } 209 }
205 210
206 if let status = profile.paymentStatus { 211 if let status = profile.paymentStatus {
207 LabeledContent("Payment", value: status.capitalized) 212 LabeledContent("Payment", value: status.capitalized)
213 .themedRow()
208 } 214 }
209 215
210 if let sub = profile.subscription { 216 if let sub = profile.subscription {
211 if let status = sub.status { 217 if let status = sub.status {
212 LabeledContent("Subscription", value: status.capitalized) 218 LabeledContent("Subscription", value: status.capitalized)
219 .themedRow()
213 } 220 }
214 if let interval = sub.interval { 221 if let interval = sub.interval {
215 LabeledContent("Interval", value: interval.capitalized) 222 LabeledContent("Interval", value: interval.capitalized)
223 .themedRow()
216 } 224 }
217 } 225 }
218 226
219 Button("Edit Profile") { 227 Button("Edit Profile") {
220 viewModel.isEditingProfile = true 228 viewModel.isEditingProfile = true
221 } 229 }
230 .themedRow()
222 231
223 SRHTShareButton(url: SRHTWebURL.profile(canonicalName: profile.canonicalName), target: .profile) { 232 SRHTShareButton(url: SRHTWebURL.profile(canonicalName: profile.canonicalName), target: .profile) {
224 SwiftUI.Label("Share Profile", systemImage: "square.and.arrow.up") 233 SwiftUI.Label("Share Profile", systemImage: "square.and.arrow.up")
225 } 234 }
235 .themedRow()
226 } 236 }
227 } 237 }
228 238
@@ -262,11 +272,13 @@ struct ProfileView: View {
262 } 272 }
263 } 273 }
264 } 274 }
275 .themedRow()
265 276
266 if viewModel.isAddingSSHKey { 277 if viewModel.isAddingSSHKey {
267 TextField("Paste SSH public key", text: $vm.newSSHKey, axis: .vertical) 278 TextField("Paste SSH public key", text: $vm.newSSHKey, axis: .vertical)
268 .font(.caption.monospaced()) 279 .font(.caption.monospaced())
269 .lineLimit(3...6) 280 .lineLimit(3...6)
281 .themedRow()
270 282
271 HStack { 283 HStack {
272 Button("Cancel") { 284 Button("Cancel") {
@@ -280,12 +292,14 @@ struct ProfileView: View {
280 .buttonStyle(.borderedProminent) 292 .buttonStyle(.borderedProminent)
281 .disabled(viewModel.newSSHKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) 293 .disabled(viewModel.newSSHKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
282 } 294 }
295 .themedRow()
283 } else { 296 } else {
284 Button { 297 Button {
285 viewModel.isAddingSSHKey = true 298 viewModel.isAddingSSHKey = true
286 } label: { 299 } label: {
287 SwiftUI.Label("Add SSH Key", systemImage: "key") 300 SwiftUI.Label("Add SSH Key", systemImage: "key")
288 } 301 }
302 .themedRow()
289 } 303 }
290 } header: { 304 } header: {
291 Text("SSH Keys") 305 Text("SSH Keys")
@@ -316,11 +330,13 @@ struct ProfileView: View {
316 } 330 }
317 } 331 }
318 } 332 }
333 .themedRow()
319 334
320 if viewModel.isAddingPGPKey { 335 if viewModel.isAddingPGPKey {
321 TextField("Paste PGP public key", text: $vm.newPGPKey, axis: .vertical) 336 TextField("Paste PGP public key", text: $vm.newPGPKey, axis: .vertical)
322 .font(.caption.monospaced()) 337 .font(.caption.monospaced())
323 .lineLimit(3...6) 338 .lineLimit(3...6)
339 .themedRow()
324 340
325 HStack { 341 HStack {
326 Button("Cancel") { 342 Button("Cancel") {
@@ -334,12 +350,14 @@ struct ProfileView: View {
334 .buttonStyle(.borderedProminent) 350 .buttonStyle(.borderedProminent)
335 .disabled(viewModel.newPGPKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) 351 .disabled(viewModel.newPGPKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
336 } 352 }
353 .themedRow()
337 } else { 354 } else {
338 Button { 355 Button {
339 viewModel.isAddingPGPKey = true 356 viewModel.isAddingPGPKey = true
340 } label: { 357 } label: {
341 SwiftUI.Label("Add PGP Key", systemImage: "key.fill") 358 SwiftUI.Label("Add PGP Key", systemImage: "key.fill")
342 } 359 }
360 .themedRow()
343 } 361 }
344 } header: { 362 } header: {
345 Text("PGP Keys") 363 Text("PGP Keys")
@@ -357,10 +375,12 @@ struct ProfileView: View {
357 ProgressView() 375 ProgressView()
358 Spacer() 376 Spacer()
359 } 377 }
378 .themedRow()
360 } else if viewModel.personalAccessTokens.isEmpty { 379 } else if viewModel.personalAccessTokens.isEmpty {
361 Button("Load Tokens") { 380 Button("Load Tokens") {
362 Task { await viewModel.loadPersonalAccessTokens() } 381 Task { await viewModel.loadPersonalAccessTokens() }
363 } 382 }
383 .themedRow()
364 } else { 384 } else {
365 ForEach(viewModel.personalAccessTokens) { token in 385 ForEach(viewModel.personalAccessTokens) { token in
366 VStack(alignment: .leading, spacing: 4) { 386 VStack(alignment: .leading, spacing: 4) {
@@ -390,6 +410,7 @@ struct ProfileView: View {
390 } 410 }
391 } 411 }
392 } 412 }
413 .themedRow()
393 } 414 }
394 } header: { 415 } header: {
395 Text("Personal Access Tokens") 416 Text("Personal Access Tokens")
@@ -426,6 +447,7 @@ private struct EditProfileSheet: View {
426 let profile: UserProfile 447 let profile: UserProfile
427 let viewModel: SettingsViewModel 448 let viewModel: SettingsViewModel
428 449
450 @Environment(\.isAMOLEDTheme) private var isAMOLED
429 @State private var email: String 451 @State private var email: String
430 @State private var url: String 452 @State private var url: String
431 @State private var location: String 453 @State private var location: String
@@ -492,7 +514,7 @@ private struct EditProfileSheet: View {
492 } 514 }
493 Spacer() 515 Spacer()
494 } 516 }
495 .listRowBackground(Color.clear) 517 .listRowBackground(isAMOLED ? Color.black : Color.clear)
496 518
497 if profile.avatar != nil || avatarPreview != nil { 519 if profile.avatar != nil || avatarPreview != nil {
498 HStack { 520 HStack {
@@ -506,7 +528,7 @@ private struct EditProfileSheet: View {
506 .disabled(viewModel.isUploadingAvatar) 528 .disabled(viewModel.isUploadingAvatar)
507 Spacer() 529 Spacer()
508 } 530 }
509 .listRowBackground(Color.clear) 531 .listRowBackground(isAMOLED ? Color.black : Color.clear)
510 .listRowSeparator(.hidden) 532 .listRowSeparator(.hidden)
511 } 533 }
512 } 534 }
@@ -522,6 +544,7 @@ private struct EditProfileSheet: View {
522 .autocorrectionDisabled() 544 .autocorrectionDisabled()
523 .textInputAutocapitalization(.never) 545 .textInputAutocapitalization(.never)
524 } 546 }
547 .themedRow()
525 548
526 VStack(alignment: .leading, spacing: 4) { 549 VStack(alignment: .leading, spacing: 4) {
527 Text("URL") 550 Text("URL")
@@ -533,6 +556,7 @@ private struct EditProfileSheet: View {
533 .autocorrectionDisabled() 556 .autocorrectionDisabled()
534 .textInputAutocapitalization(.never) 557 .textInputAutocapitalization(.never)
535 } 558 }
559 .themedRow()
536 560
537 VStack(alignment: .leading, spacing: 4) { 561 VStack(alignment: .leading, spacing: 4) {
538 Text("Location") 562 Text("Location")
@@ -540,6 +564,7 @@ private struct EditProfileSheet: View {
540 .foregroundStyle(.secondary) 564 .foregroundStyle(.secondary)
541 TextField("Enter location", text: $location) 565 TextField("Enter location", text: $location)
542 } 566 }
567 .themedRow()
543 568
544 VStack(alignment: .leading, spacing: 4) { 569 VStack(alignment: .leading, spacing: 4) {
545 Text("Bio") 570 Text("Bio")
@@ -548,8 +573,10 @@ private struct EditProfileSheet: View {
548 TextField("Enter bio", text: $bio, axis: .vertical) 573 TextField("Enter bio", text: $bio, axis: .vertical)
549 .lineLimit(3...6) 574 .lineLimit(3...6)
550 } 575 }
576 .themedRow()
551 } 577 }
552 } 578 }
579 .themedList()
553 .navigationTitle("Edit Profile") 580 .navigationTitle("Edit Profile")
554 .navigationBarTitleDisplayMode(.inline) 581 .navigationBarTitleDisplayMode(.inline)
555 .onChange(of: selectedPhoto) { _, newItem in 582 .onChange(of: selectedPhoto) { _, newItem in
Hutch/Views/Pastes/PasteDetailView.swift +8
@@ -326,13 +326,18 @@ private struct PasteInfoSheet: View {
326 List { 326 List {
327 Section("Paste") { 327 Section("Paste") {
328 LabeledContent("ID", value: paste.id) 328 LabeledContent("ID", value: paste.id)
329 .themedRow()
329 LabeledContent("Owner", value: paste.user.canonicalName) 330 LabeledContent("Owner", value: paste.user.canonicalName)
331 .themedRow()
330 LabeledContent("Created", value: paste.created.relativeDescription) 332 LabeledContent("Created", value: paste.created.relativeDescription)
333 .themedRow()
331 LabeledContent("Visibility") { 334 LabeledContent("Visibility") {
332 VisibilityBadge(visibility: paste.visibility) 335 VisibilityBadge(visibility: paste.visibility)
333 } 336 }
337 .themedRow()
334 if paste.files.count > 1 { 338 if paste.files.count > 1 {
335 LabeledContent("Files", value: "\(paste.files.count)") 339 LabeledContent("Files", value: "\(paste.files.count)")
340 .themedRow()
336 } 341 }
337 } 342 }
338 343
@@ -340,6 +345,7 @@ private struct PasteInfoSheet: View {
340 Section("File") { 345 Section("File") {
341 if let filename = file.filename, !filename.isEmpty { 346 if let filename = file.filename, !filename.isEmpty {
342 LabeledContent("Filename", value: filename) 347 LabeledContent("Filename", value: filename)
348 .themedRow()
343 } 349 }
344 LabeledContent("Hash") { 350 LabeledContent("Hash") {
345 Text(file.hash) 351 Text(file.hash)
@@ -347,6 +353,7 @@ private struct PasteInfoSheet: View {
347 .foregroundStyle(.secondary) 353 .foregroundStyle(.secondary)
348 .textSelection(.enabled) 354 .textSelection(.enabled)
349 } 355 }
356 .themedRow()
350 } 357 }
351 } 358 }
352 } 359 }
@@ -407,6 +414,7 @@ private struct PasteVisibilitySheet: View {
407 } 414 }
408 .buttonStyle(.plain) 415 .buttonStyle(.plain)
409 } 416 }
417 .themedRow()
410 } 418 }
411 .listStyle(.insetGrouped) 419 .listStyle(.insetGrouped)
412 .navigationTitle("Visibility") 420 .navigationTitle("Visibility")
Hutch/Views/Pastes/PasteListView.swift +8
@@ -101,6 +101,7 @@ struct PasteListView: View {
101 await viewModel.loadMoreIfNeeded(currentItem: paste) 101 await viewModel.loadMoreIfNeeded(currentItem: paste)
102 } 102 }
103 } 103 }
104 .themedRow()
104 105
105 if viewModel.isLoadingMore { 106 if viewModel.isLoadingMore {
106 HStack { 107 HStack {
@@ -109,6 +110,7 @@ struct PasteListView: View {
109 Spacer() 110 Spacer()
110 } 111 }
111 .listRowSeparator(.hidden) 112 .listRowSeparator(.hidden)
113 .themedRow()
112 } 114 }
113 } 115 }
114 .themedList() 116 .themedList()
@@ -302,12 +304,14 @@ private struct CreatePasteSheet: View {
302 files = [PasteUploadDraft()] 304 files = [PasteUploadDraft()]
303 } 305 }
304 } 306 }
307 .themedRow()
305 308
306 Button { 309 Button {
307 files.append(PasteUploadDraft()) 310 files.append(PasteUploadDraft())
308 } label: { 311 } label: {
309 Label("Add File", systemImage: "plus") 312 Label("Add File", systemImage: "plus")
310 } 313 }
314 .themedRow()
311 } 315 }
312 316
313 Section("Visibility") { 317 Section("Visibility") {
@@ -316,21 +320,25 @@ private struct CreatePasteSheet: View {
316 Text("Unlisted").tag(Visibility.unlisted) 320 Text("Unlisted").tag(Visibility.unlisted)
317 Text("Private").tag(Visibility.private) 321 Text("Private").tag(Visibility.private)
318 } 322 }
323 .themedRow()
319 } 324 }
320 325
321 Section { 326 Section {
322 Text("Paste contents are uploaded as UTF-8 text files. Hutch can change visibility later, but the API does not support editing file contents after creation.") 327 Text("Paste contents are uploaded as UTF-8 text files. Hutch can change visibility later, but the API does not support editing file contents after creation.")
323 .font(.footnote) 328 .font(.footnote)
324 .foregroundStyle(.secondary) 329 .foregroundStyle(.secondary)
330 .themedRow()
325 } 331 }
326 332
327 if let error = viewModel.error { 333 if let error = viewModel.error {
328 Section { 334 Section {
329 Label(error, systemImage: "exclamationmark.triangle.fill") 335 Label(error, systemImage: "exclamationmark.triangle.fill")
330 .foregroundStyle(.red) 336 .foregroundStyle(.red)
337 .themedRow()
331 } 338 }
332 } 339 }
333 } 340 }
341 .themedList()
334 .navigationTitle("New Paste") 342 .navigationTitle("New Paste")
335 .navigationBarTitleDisplayMode(.inline) 343 .navigationBarTitleDisplayMode(.inline)
336 .toolbar { 344 .toolbar {
Hutch/Views/Projects/ProjectDetailView.swift +6
@@ -103,6 +103,7 @@ struct ProjectDetailView: View {
103 } 103 }
104 } 104 }
105 .padding(.vertical, 4) 105 .padding(.vertical, 4)
106 .themedRow()
106 } 107 }
107 } 108 }
108 109
@@ -127,6 +128,7 @@ struct ProjectDetailView: View {
127 } 128 }
128 .buttonStyle(.plain) 129 .buttonStyle(.plain)
129 } 130 }
131 .themedRow()
130 } 132 }
131 } 133 }
132 } 134 }
@@ -155,6 +157,7 @@ struct ProjectDetailView: View {
155 } 157 }
156 .buttonStyle(.plain) 158 .buttonStyle(.plain)
157 } 159 }
160 .themedRow()
158 } 161 }
159 } 162 }
160 } 163 }
@@ -183,6 +186,7 @@ struct ProjectDetailView: View {
183 } 186 }
184 .buttonStyle(.plain) 187 .buttonStyle(.plain)
185 } 188 }
189 .themedRow()
186 } 190 }
187 } 191 }
188 } 192 }
@@ -205,6 +209,7 @@ struct ProjectDetailView: View {
205 } 209 }
206 .buttonStyle(.plain) 210 .buttonStyle(.plain)
207 } 211 }
212 .themedRow()
208 } 213 }
209 } 214 }
210 } 215 }
@@ -218,6 +223,7 @@ struct ProjectDetailView: View {
218 systemImage: "square.stack.3d.up.slash", 223 systemImage: "square.stack.3d.up.slash",
219 description: Text("This project doesn’t currently expose repositories, trackers, mailing lists, or external links.") 224 description: Text("This project doesn’t currently expose repositories, trackers, mailing lists, or external links.")
220 ) 225 )
226 .themedRow()
221 } 227 }
222 } 228 }
223 } 229 }
Hutch/Views/Projects/ProjectMailingListView.swift +2
@@ -332,7 +332,9 @@ struct MailingListDetailView: View {
332 .tint(thread.isUnread ? .blue : .gray) 332 .tint(thread.isUnread ? .blue : .gray)
333 } 333 }
334 } 334 }
335 .themedRow()
335 } 336 }
337 .themedList()
336 .listStyle(.plain) 338 .listStyle(.plain)
337 .searchable( 339 .searchable(
338 text: $vm.searchText, 340 text: $vm.searchText,
Hutch/Views/Projects/ProjectsListView.swift +1
@@ -79,6 +79,7 @@ struct ProjectsListView: View {
79 .buttonStyle(.plain) 79 .buttonStyle(.plain)
80 .alignmentGuide(.listRowSeparatorLeading) { _ in 0 } 80 .alignmentGuide(.listRowSeparatorLeading) { _ in 0 }
81 } 81 }
82 .themedRow()
82 } 83 }
83 .themedList() 84 .themedList()
84 .listStyle(.plain) 85 .listStyle(.plain)
Hutch/Views/Repositories/ArtifactsView.swift +2
@@ -13,9 +13,11 @@ struct ArtifactsView: View {
13 openURL(artifact.url) 13 openURL(artifact.url)
14 } 14 }
15 } 15 }
16 .themedRow()
16 } 17 }
17 } 18 }
18 } 19 }
20 .themedList()
19 .listStyle(.insetGrouped) 21 .listStyle(.insetGrouped)
20 .overlay { 22 .overlay {
21 if viewModel.isLoadingArtifacts, viewModel.referenceArtifacts.isEmpty { 23 if viewModel.isLoadingArtifacts, viewModel.referenceArtifacts.isEmpty {
Hutch/Views/Repositories/CommitLogView.swift +3
@@ -13,6 +13,7 @@ struct CommitLogView: View {
13 await viewModel.loadMoreCommitsIfNeeded(currentItem: commit) 13 await viewModel.loadMoreCommitsIfNeeded(currentItem: commit)
14 } 14 }
15 } 15 }
16 .themedRow()
16 17
17 if viewModel.isLoadingMoreCommits { 18 if viewModel.isLoadingMoreCommits {
18 HStack { 19 HStack {
@@ -21,8 +22,10 @@ struct CommitLogView: View {
21 Spacer() 22 Spacer()
22 } 23 }
23 .listRowSeparator(.hidden) 24 .listRowSeparator(.hidden)
25 .themedRow()
24 } 26 }
25 } 27 }
28 .themedList()
26 .listStyle(.plain) 29 .listStyle(.plain)
27 .overlay { 30 .overlay {
28 if viewModel.isLoadingCommits, viewModel.commits.isEmpty { 31 if viewModel.isLoadingCommits, viewModel.commits.isEmpty {
Hutch/Views/Repositories/FileTreeView.swift +6
@@ -233,7 +233,9 @@ private struct FileTreeContentView: View {
233 await viewModel.navigateInto(entry: entry) 233 await viewModel.navigateInto(entry: entry)
234 } 234 }
235 } 235 }
236 .themedRow()
236 } 237 }
238 .themedList()
237 .listStyle(.plain) 239 .listStyle(.plain)
238 .refreshable { 240 .refreshable {
239 await viewModel.loadRootTree() 241 await viewModel.loadRootTree()
@@ -848,6 +850,7 @@ private struct RefPickerSheet: View {
848 ) 850 )
849 } 851 }
850 .buttonStyle(.plain) 852 .buttonStyle(.plain)
853 .themedRow()
851 } 854 }
852 855
853 if !viewModel.branches.isEmpty { 856 if !viewModel.branches.isEmpty {
@@ -868,6 +871,7 @@ private struct RefPickerSheet: View {
868 } 871 }
869 .buttonStyle(.plain) 872 .buttonStyle(.plain)
870 } 873 }
874 .themedRow()
871 } 875 }
872 } 876 }
873 877
@@ -889,9 +893,11 @@ private struct RefPickerSheet: View {
889 } 893 }
890 .buttonStyle(.plain) 894 .buttonStyle(.plain)
891 } 895 }
896 .themedRow()
892 } 897 }
893 } 898 }
894 } 899 }
900 .themedList()
895 .listStyle(.insetGrouped) 901 .listStyle(.insetGrouped)
896 .navigationTitle("Select Ref") 902 .navigationTitle("Select Ref")
897 .navigationBarTitleDisplayMode(.inline) 903 .navigationBarTitleDisplayMode(.inline)
Hutch/Views/Repositories/HgRepositoryDetailView.swift +12
@@ -396,7 +396,9 @@ struct HgRepositoryDetailView: View {
396 .onTapGesture { 396 .onTapGesture {
397 Task { await viewModel.openFile(file) } 397 Task { await viewModel.openFile(file) }
398 } 398 }
399 .themedRow()
399 } 400 }
401 .themedList()
400 .listStyle(.plain) 402 .listStyle(.plain)
401 } 403 }
402 } 404 }
@@ -454,6 +456,7 @@ struct HgRepositoryDetailView: View {
454 await viewModel.loadMoreLogIfNeeded(currentItem: revision) 456 await viewModel.loadMoreLogIfNeeded(currentItem: revision)
455 } 457 }
456 } 458 }
459 .themedRow()
457 460
458 if viewModel.isLoadingMoreLog { 461 if viewModel.isLoadingMoreLog {
459 HStack { 462 HStack {
@@ -462,8 +465,10 @@ struct HgRepositoryDetailView: View {
462 Spacer() 465 Spacer()
463 } 466 }
464 .listRowSeparator(.hidden) 467 .listRowSeparator(.hidden)
468 .themedRow()
465 } 469 }
466 } 470 }
471 .themedList()
467 .listStyle(.plain) 472 .listStyle(.plain)
468 .overlay { 473 .overlay {
469 if viewModel.isLoadingLog, viewModel.log.isEmpty { 474 if viewModel.isLoadingLog, viewModel.log.isEmpty {
@@ -498,7 +503,9 @@ struct HgRepositoryDetailView: View {
498 } else { 503 } else {
499 List(revisions) { revision in 504 List(revisions) { revision in
500 namedRevisionRow(revision) 505 namedRevisionRow(revision)
506 .themedRow()
501 } 507 }
508 .themedList()
502 .listStyle(.plain) 509 .listStyle(.plain)
503 } 510 }
504 } 511 }
@@ -699,6 +706,7 @@ private struct HgBrowseRefPickerSheet: View {
699 ) 706 )
700 } 707 }
701 .buttonStyle(.plain) 708 .buttonStyle(.plain)
709 .themedRow()
702 } 710 }
703 711
704 if !viewModel.branches.isEmpty { 712 if !viewModel.branches.isEmpty {
@@ -719,6 +727,7 @@ private struct HgBrowseRefPickerSheet: View {
719 } 727 }
720 .buttonStyle(.plain) 728 .buttonStyle(.plain)
721 } 729 }
730 .themedRow()
722 } 731 }
723 } 732 }
724 733
@@ -740,6 +749,7 @@ private struct HgBrowseRefPickerSheet: View {
740 } 749 }
741 .buttonStyle(.plain) 750 .buttonStyle(.plain)
742 } 751 }
752 .themedRow()
743 } 753 }
744 } 754 }
745 755
@@ -761,9 +771,11 @@ private struct HgBrowseRefPickerSheet: View {
761 } 771 }
762 .buttonStyle(.plain) 772 .buttonStyle(.plain)
763 } 773 }
774 .themedRow()
764 } 775 }
765 } 776 }
766 } 777 }
778 .themedList()
767 .listStyle(.insetGrouped) 779 .listStyle(.insetGrouped)
768 .navigationTitle("Select Ref") 780 .navigationTitle("Select Ref")
769 .navigationBarTitleDisplayMode(.inline) 781 .navigationBarTitleDisplayMode(.inline)
Hutch/Views/Repositories/HgRepositorySettingsView.swift +19
@@ -49,6 +49,7 @@ struct HgRepositorySettingsView: View {
49 histeditSection(viewModel) 49 histeditSection(viewModel)
50 deleteSection(viewModel) 50 deleteSection(viewModel)
51 } 51 }
52 .themedList()
52 .srhtErrorBanner(error: $vm.error) 53 .srhtErrorBanner(error: $vm.error)
53 .alert( 54 .alert(
54 "Permanently delete \(repository.owner.canonicalName)/\(repository.name)?", 55 "Permanently delete \(repository.owner.canonicalName)/\(repository.name)?",
@@ -101,25 +102,30 @@ struct HgRepositorySettingsView: View {
101 Text("\(repository.owner.canonicalName)/\(repository.name)") 102 Text("\(repository.owner.canonicalName)/\(repository.name)")
102 .font(.body.monospaced()) 103 .font(.body.monospaced())
103 } 104 }
105 .themedRow()
104 106
105 LabeledContent("Forge") { 107 LabeledContent("Forge") {
106 Text(repositoryForgeLabel(repository.service)) 108 Text(repositoryForgeLabel(repository.service))
107 } 109 }
110 .themedRow()
108 111
109 LabeledContent("Visibility") { 112 LabeledContent("Visibility") {
110 Text(repositoryVisibilityLabel(viewModel.editedVisibility)) 113 Text(repositoryVisibilityLabel(viewModel.editedVisibility))
111 } 114 }
115 .themedRow()
112 } 116 }
113 117
114 Section("Repository Details") { 118 Section("Repository Details") {
115 TextField("Description", text: Bindable(viewModel).editedDescription, axis: .vertical) 119 TextField("Description", text: Bindable(viewModel).editedDescription, axis: .vertical)
116 .lineLimit(3...6) 120 .lineLimit(3...6)
121 .themedRow()
117 122
118 Picker("Visibility", selection: Bindable(viewModel).editedVisibility) { 123 Picker("Visibility", selection: Bindable(viewModel).editedVisibility) {
119 Text("Public").tag(Visibility.public) 124 Text("Public").tag(Visibility.public)
120 Text("Unlisted").tag(Visibility.unlisted) 125 Text("Unlisted").tag(Visibility.unlisted)
121 Text("Private").tag(Visibility.private) 126 Text("Private").tag(Visibility.private)
122 } 127 }
128 .themedRow()
123 129
124 Button { 130 Button {
125 Task { 131 Task {
@@ -135,6 +141,7 @@ struct HgRepositorySettingsView: View {
135 } 141 }
136 } 142 }
137 .disabled(viewModel.isSavingInfo || !viewModel.isInfoDirty) 143 .disabled(viewModel.isSavingInfo || !viewModel.isInfoDirty)
144 .themedRow()
138 } 145 }
139 } 146 }
140 147
@@ -147,9 +154,11 @@ struct HgRepositorySettingsView: View {
147 ProgressView() 154 ProgressView()
148 Spacer() 155 Spacer()
149 } 156 }
157 .themedRow()
150 } else if viewModel.acls.isEmpty { 158 } else if viewModel.acls.isEmpty {
151 Text("No access entries yet.") 159 Text("No access entries yet.")
152 .foregroundStyle(.secondary) 160 .foregroundStyle(.secondary)
161 .themedRow()
153 } else { 162 } else {
154 ForEach(viewModel.acls) { entry in 163 ForEach(viewModel.acls) { entry in
155 HStack { 164 HStack {
@@ -167,6 +176,7 @@ struct HgRepositorySettingsView: View {
167 } 176 }
168 } 177 }
169 } 178 }
179 .themedRow()
170 } 180 }
171 181
172 HStack { 182 HStack {
@@ -192,9 +202,11 @@ struct HgRepositorySettingsView: View {
192 } 202 }
193 .disabled(viewModel.isAddingACL || viewModel.newACLEntity.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) 203 .disabled(viewModel.isAddingACL || viewModel.newACLEntity.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
194 } 204 }
205 .themedRow()
195 Text("Add a SourceHut user and choose read-only or read/write access.") 206 Text("Add a SourceHut user and choose read-only or read/write access.")
196 .font(.caption) 207 .font(.caption)
197 .foregroundStyle(.secondary) 208 .foregroundStyle(.secondary)
209 .themedRow()
198 } 210 }
199 } 211 }
200 212
@@ -202,9 +214,11 @@ struct HgRepositorySettingsView: View {
202 private func featuresSection(_ viewModel: HgRepositorySettingsViewModel) -> some View { 214 private func featuresSection(_ viewModel: HgRepositorySettingsViewModel) -> some View {
203 Section("Sensitive Settings") { 215 Section("Sensitive Settings") {
204 Toggle("Hide this repository from public listings", isOn: Bindable(viewModel).editedNonPublishing) 216 Toggle("Hide this repository from public listings", isOn: Bindable(viewModel).editedNonPublishing)
217 .themedRow()
205 Text("Changes stay pending until you save this section.") 218 Text("Changes stay pending until you save this section.")
206 .font(.caption) 219 .font(.caption)
207 .foregroundStyle(.secondary) 220 .foregroundStyle(.secondary)
221 .themedRow()
208 222
209 Button { 223 Button {
210 Task { 224 Task {
@@ -220,6 +234,7 @@ struct HgRepositorySettingsView: View {
220 } 234 }
221 } 235 }
222 .disabled(viewModel.isSavingInfo || !viewModel.isInfoDirty) 236 .disabled(viewModel.isSavingInfo || !viewModel.isInfoDirty)
237 .themedRow()
223 } 238 }
224 } 239 }
225 240
@@ -230,16 +245,19 @@ struct HgRepositorySettingsView: View {
230 .autocorrectionDisabled() 245 .autocorrectionDisabled()
231 .textInputAutocapitalization(.never) 246 .textInputAutocapitalization(.never)
232 .disabled(true) 247 .disabled(true)
248 .themedRow()
233 249
234 Text("Removing revisions is not available through the public hg.sr.ht API, so Hutch can’t do this yet.") 250 Text("Removing revisions is not available through the public hg.sr.ht API, so Hutch can’t do this yet.")
235 .font(.caption) 251 .font(.caption)
236 .foregroundStyle(.secondary) 252 .foregroundStyle(.secondary)
253 .themedRow()
237 254
238 Button("Remove Revision", role: .destructive) { 255 Button("Remove Revision", role: .destructive) {
239 // Not implemented: the hg.sr.ht API does not expose a histedit endpoint. 256 // Not implemented: the hg.sr.ht API does not expose a histedit endpoint.
240 // This button is disabled until the API supports revision removal. 257 // This button is disabled until the API supports revision removal.
241 } 258 }
242 .disabled(true) 259 .disabled(true)
260 .themedRow()
243 } 261 }
244 } 262 }
245 263
@@ -258,6 +276,7 @@ struct HgRepositorySettingsView: View {
258 } 276 }
259 } 277 }
260 .disabled(viewModel.isDeleting) 278 .disabled(viewModel.isDeleting)
279 .themedRow()
261 } header: { 280 } header: {
262 Text("Danger Zone") 281 Text("Danger Zone")
263 } 282 }
Hutch/Views/Repositories/ReferencesListView.swift +3
@@ -10,6 +10,7 @@ struct ReferencesListView: View {
10 ForEach(viewModel.branches, id: \.name) { ref in 10 ForEach(viewModel.branches, id: \.name) { ref in
11 ReferenceRow(reference: ref, prefix: "refs/heads/") 11 ReferenceRow(reference: ref, prefix: "refs/heads/")
12 } 12 }
13 .themedRow()
13 } 14 }
14 } 15 }
15 16
@@ -18,9 +19,11 @@ struct ReferencesListView: View {
18 ForEach(viewModel.tags, id: \.name) { ref in 19 ForEach(viewModel.tags, id: \.name) { ref in
19 ReferenceRow(reference: ref, prefix: "refs/tags/") 20 ReferenceRow(reference: ref, prefix: "refs/tags/")
20 } 21 }
22 .themedRow()
21 } 23 }
22 } 24 }
23 } 25 }
26 .themedList()
24 .listStyle(.insetGrouped) 27 .listStyle(.insetGrouped)
25 .overlay { 28 .overlay {
26 if viewModel.isLoadingRefs, viewModel.branches.isEmpty, viewModel.tags.isEmpty { 29 if viewModel.isLoadingRefs, viewModel.branches.isEmpty, viewModel.tags.isEmpty {
Hutch/Views/Repositories/RepositoryACLView.swift +8 −1
@@ -6,6 +6,7 @@ struct RepositoryACLView: View {
6 let showsDoneButton: Bool 6 let showsDoneButton: Bool
7 7
8 @Environment(\.dismiss) private var dismiss 8 @Environment(\.dismiss) private var dismiss
9 @Environment(\.isAMOLEDTheme) private var isAMOLED
9 @State private var viewModel: RepositoryACLViewModel? 10 @State private var viewModel: RepositoryACLViewModel?
10 @State private var pendingDeletion: RepositoryACLEntry? 11 @State private var pendingDeletion: RepositoryACLEntry?
11 @State private var showAddSheet = false 12 @State private var showAddSheet = false
@@ -70,7 +71,7 @@ struct RepositoryACLView: View {
70 Text("Only the repository owner currently has access.") 71 Text("Only the repository owner currently has access.")
71 } 72 }
72 .frame(maxWidth: .infinity) 73 .frame(maxWidth: .infinity)
73 .listRowBackground(Color.clear) 74 .listRowBackground(isAMOLED ? Color.black : Color.clear)
74 } else { 75 } else {
75 Section { 76 Section {
76 ForEach(viewModel.visibleEntries) { entry in 77 ForEach(viewModel.visibleEntries) { entry in
@@ -86,9 +87,11 @@ struct RepositoryACLView: View {
86 } 87 }
87 ) 88 )
88 } 89 }
90 .themedRow()
89 } 91 }
90 } 92 }
91 } 93 }
94 .themedList()
92 .listStyle(.insetGrouped) 95 .listStyle(.insetGrouped)
93 .refreshable { 96 .refreshable {
94 await viewModel.load() 97 await viewModel.load()
@@ -192,11 +195,13 @@ private struct RepositoryACLAddUserView: View {
192 TextField("Username or ~username", text: $viewModel.addUsername) 195 TextField("Username or ~username", text: $viewModel.addUsername)
193 .autocorrectionDisabled() 196 .autocorrectionDisabled()
194 .textInputAutocapitalization(.never) 197 .textInputAutocapitalization(.never)
198 .themedRow()
195 199
196 if let validation = inlineValidationMessage { 200 if let validation = inlineValidationMessage {
197 Text(validation) 201 Text(validation)
198 .font(.caption) 202 .font(.caption)
199 .foregroundStyle(.secondary) 203 .foregroundStyle(.secondary)
204 .themedRow()
200 } 205 }
201 } 206 }
202 207
@@ -207,8 +212,10 @@ private struct RepositoryACLAddUserView: View {
207 } 212 }
208 } 213 }
209 .pickerStyle(.segmented) 214 .pickerStyle(.segmented)
215 .themedRow()
210 } 216 }
211 } 217 }
218 .themedList()
212 .navigationTitle("Add User") 219 .navigationTitle("Add User")
213 .navigationBarTitleDisplayMode(.inline) 220 .navigationBarTitleDisplayMode(.inline)
214 .toolbar { 221 .toolbar {
Hutch/Views/Repositories/RepositoryListView.swift +11
@@ -78,6 +78,7 @@ struct RepositoryListView: View {
78 .foregroundStyle(.secondary) 78 .foregroundStyle(.secondary)
79 } 79 }
80 .listRowSeparator(.hidden) 80 .listRowSeparator(.hidden)
81 .themedRow()
81 } 82 }
82 83
83 ForEach(viewModel.repositories) { repo in 84 ForEach(viewModel.repositories) { repo in
@@ -89,6 +90,7 @@ struct RepositoryListView: View {
89 } 90 }
90 .alignmentGuide(.listRowSeparatorLeading) { _ in 0 } 91 .alignmentGuide(.listRowSeparatorLeading) { _ in 0 }
91 } 92 }
93 .themedRow()
92 94
93 if viewModel.isLoadingMore { 95 if viewModel.isLoadingMore {
94 HStack { 96 HStack {
@@ -97,6 +99,7 @@ struct RepositoryListView: View {
97 Spacer() 99 Spacer()
98 } 100 }
99 .listRowSeparator(.hidden) 101 .listRowSeparator(.hidden)
102 .themedRow()
100 } 103 }
101 } 104 }
102 .themedList() 105 .themedList()
@@ -223,16 +226,20 @@ private struct CreateRepositorySheet: View {
223 Text(service.displayName).tag(service) 226 Text(service.displayName).tag(service)
224 } 227 }
225 } 228 }
229 .themedRow()
226 TextField("Repository name", text: $name) 230 TextField("Repository name", text: $name)
227 .textInputAutocapitalization(.never) 231 .textInputAutocapitalization(.never)
228 .autocorrectionDisabled() 232 .autocorrectionDisabled()
233 .themedRow()
229 TextField("Short description (optional)", text: $description, axis: .vertical) 234 TextField("Short description (optional)", text: $description, axis: .vertical)
230 .lineLimit(2...4) 235 .lineLimit(2...4)
236 .themedRow()
231 Picker("Visibility", selection: $visibility) { 237 Picker("Visibility", selection: $visibility) {
232 Text("Public").tag(Visibility.public) 238 Text("Public").tag(Visibility.public)
233 Text("Unlisted").tag(Visibility.unlisted) 239 Text("Unlisted").tag(Visibility.unlisted)
234 Text("Private").tag(Visibility.private) 240 Text("Private").tag(Visibility.private)
235 } 241 }
242 .themedRow()
236 } 243 }
237 244
238 Section("Import Existing Repository") { 245 Section("Import Existing Repository") {
@@ -241,16 +248,20 @@ private struct CreateRepositorySheet: View {
241 .textInputAutocapitalization(.never) 248 .textInputAutocapitalization(.never)
242 .autocorrectionDisabled() 249 .autocorrectionDisabled()
243 .keyboardType(.URL) 250 .keyboardType(.URL)
251 .themedRow()
244 Text("Import an existing Git repository from a remote URL.") 252 Text("Import an existing Git repository from a remote URL.")
245 .font(.footnote) 253 .font(.footnote)
246 .foregroundStyle(.secondary) 254 .foregroundStyle(.secondary)
255 .themedRow()
247 } else { 256 } else {
248 Text("Importing a Mercurial repository from a remote URL is not available through the public API.") 257 Text("Importing a Mercurial repository from a remote URL is not available through the public API.")
249 .font(.footnote) 258 .font(.footnote)
250 .foregroundStyle(.secondary) 259 .foregroundStyle(.secondary)
260 .themedRow()
251 } 261 }
252 } 262 }
253 } 263 }
264 .themedList()
254 .navigationTitle(service == .git ? "New Git Repository" : "New Mercurial Repository") 265 .navigationTitle(service == .git ? "New Git Repository" : "New Mercurial Repository")
255 .navigationBarTitleDisplayMode(.inline) 266 .navigationBarTitleDisplayMode(.inline)
256 .toolbar { 267 .toolbar {
Hutch/Views/Repositories/RepositorySettingsView.swift +19
@@ -51,6 +51,7 @@ struct RepositorySettingsView: View {
51 visibilitySection(viewModel) 51 visibilitySection(viewModel)
52 deleteSection(viewModel) 52 deleteSection(viewModel)
53 } 53 }
54 .themedList()
54 .srhtErrorBanner(error: $vm.error) 55 .srhtErrorBanner(error: $vm.error)
55 .alert( 56 .alert(
56 visibilityConfirmationTitle(for: viewModel), 57 visibilityConfirmationTitle(for: viewModel),
@@ -97,15 +98,18 @@ struct RepositorySettingsView: View {
97 Text("\(viewModel.repository.owner.canonicalName)/\(viewModel.repository.name)") 98 Text("\(viewModel.repository.owner.canonicalName)/\(viewModel.repository.name)")
98 .font(.body.monospaced()) 99 .font(.body.monospaced())
99 } 100 }
101 .themedRow()
100 102
101 LabeledContent("Default Branch") { 103 LabeledContent("Default Branch") {
102 Text(viewModel.currentDefaultBranchName) 104 Text(viewModel.currentDefaultBranchName)
103 .font(.body.monospaced()) 105 .font(.body.monospaced())
104 } 106 }
107 .themedRow()
105 108
106 LabeledContent("Visibility") { 109 LabeledContent("Visibility") {
107 Text(repositoryVisibilityLabel(viewModel.repository.visibility)) 110 Text(repositoryVisibilityLabel(viewModel.repository.visibility))
108 } 111 }
112 .themedRow()
109 } 113 }
110 } 114 }
111 115
@@ -115,22 +119,27 @@ struct RepositorySettingsView: View {
115 TextField("Repository name", text: Bindable(viewModel).editedName) 119 TextField("Repository name", text: Bindable(viewModel).editedName)
116 .autocorrectionDisabled() 120 .autocorrectionDisabled()
117 .textInputAutocapitalization(.never) 121 .textInputAutocapitalization(.never)
122 .themedRow()
118 123
119 TextField("Description", text: Bindable(viewModel).editedDescription, axis: .vertical) 124 TextField("Description", text: Bindable(viewModel).editedDescription, axis: .vertical)
120 .lineLimit(2...4) 125 .lineLimit(2...4)
126 .themedRow()
121 127
122 if let metadataValidationMessage = viewModel.metadataValidationMessage { 128 if let metadataValidationMessage = viewModel.metadataValidationMessage {
123 Text(metadataValidationMessage) 129 Text(metadataValidationMessage)
124 .font(.caption) 130 .font(.caption)
125 .foregroundStyle(.red) 131 .foregroundStyle(.red)
132 .themedRow()
126 } else if viewModel.normalizedEditedName != viewModel.repository.name { 133 } else if viewModel.normalizedEditedName != viewModel.repository.name {
127 Text("Changing the repository name updates the repository URL.") 134 Text("Changing the repository name updates the repository URL.")
128 .font(.caption) 135 .font(.caption)
129 .foregroundStyle(.secondary) 136 .foregroundStyle(.secondary)
137 .themedRow()
130 } else { 138 } else {
131 Text("Name and description stay pending until you save this section.") 139 Text("Name and description stay pending until you save this section.")
132 .font(.caption) 140 .font(.caption)
133 .foregroundStyle(.secondary) 141 .foregroundStyle(.secondary)
142 .themedRow()
134 } 143 }
135 144
136 Button { 145 Button {
@@ -153,6 +162,7 @@ struct RepositorySettingsView: View {
153 !viewModel.isMetadataDirty || 162 !viewModel.isMetadataDirty ||
154 viewModel.metadataValidationMessage != nil 163 viewModel.metadataValidationMessage != nil
155 ) 164 )
165 .themedRow()
156 } header: { 166 } header: {
157 Text("Repository Details") 167 Text("Repository Details")
158 } 168 }
@@ -165,10 +175,12 @@ struct RepositorySettingsView: View {
165 Text(viewModel.currentDefaultBranchName) 175 Text(viewModel.currentDefaultBranchName)
166 .font(.body.monospaced()) 176 .font(.body.monospaced())
167 } 177 }
178 .themedRow()
168 179
169 if viewModel.branches.isEmpty { 180 if viewModel.branches.isEmpty {
170 Text("This repository doesn't have any branches yet.") 181 Text("This repository doesn't have any branches yet.")
171 .foregroundStyle(.secondary) 182 .foregroundStyle(.secondary)
183 .themedRow()
172 } else { 184 } else {
173 Picker("Branch", selection: Bindable(viewModel).editedHead) { 185 Picker("Branch", selection: Bindable(viewModel).editedHead) {
174 ForEach(viewModel.availableBranchNames, id: \.self) { branch in 186 ForEach(viewModel.availableBranchNames, id: \.self) { branch in
@@ -177,10 +189,12 @@ struct RepositorySettingsView: View {
177 .tag(branch) 189 .tag(branch)
178 } 190 }
179 } 191 }
192 .themedRow()
180 193
181 Text("Changes stay pending until you set the new default branch.") 194 Text("Changes stay pending until you set the new default branch.")
182 .font(.caption) 195 .font(.caption)
183 .foregroundStyle(.secondary) 196 .foregroundStyle(.secondary)
197 .themedRow()
184 198
185 Button { 199 Button {
186 Task { 200 Task {
@@ -202,6 +216,7 @@ struct RepositorySettingsView: View {
202 !viewModel.isDefaultBranchDirty || 216 !viewModel.isDefaultBranchDirty ||
203 viewModel.defaultBranchValidationMessage != nil 217 viewModel.defaultBranchValidationMessage != nil
204 ) 218 )
219 .themedRow()
205 } 220 }
206 } header: { 221 } header: {
207 Text("Default Branch") 222 Text("Default Branch")
@@ -216,10 +231,12 @@ struct RepositorySettingsView: View {
216 Text("Unlisted").tag(Visibility.unlisted) 231 Text("Unlisted").tag(Visibility.unlisted)
217 Text("Private").tag(Visibility.private) 232 Text("Private").tag(Visibility.private)
218 } 233 }
234 .themedRow()
219 235
220 Text("Visibility changes apply immediately after you confirm them.") 236 Text("Visibility changes apply immediately after you confirm them.")
221 .font(.caption) 237 .font(.caption)
222 .foregroundStyle(.secondary) 238 .foregroundStyle(.secondary)
239 .themedRow()
223 240
224 Button { 241 Button {
225 showVisibilityConfirmation = true 242 showVisibilityConfirmation = true
@@ -233,6 +250,7 @@ struct RepositorySettingsView: View {
233 } 250 }
234 } 251 }
235 .disabled(viewModel.isMutating || !viewModel.isVisibilityDirty) 252 .disabled(viewModel.isMutating || !viewModel.isVisibilityDirty)
253 .themedRow()
236 } header: { 254 } header: {
237 Text("Sensitive Settings") 255 Text("Sensitive Settings")
238 } 256 }
@@ -253,6 +271,7 @@ struct RepositorySettingsView: View {
253 } 271 }
254 } 272 }
255 .disabled(viewModel.isMutating) 273 .disabled(viewModel.isMutating)
274 .themedRow()
256 } header: { 275 } header: {
257 Text("Danger Zone") 276 Text("Danger Zone")
258 } 277 }
Hutch/Views/Settings/SettingsView.swift +23
@@ -60,11 +60,13 @@ struct SettingsView: View {
60 Text(theme.label).tag(theme) 60 Text(theme.label).tag(theme)
61 } 61 }
62 } 62 }
63 .themedRow()
63 Picker("Density", selection: $displayDensity) { 64 Picker("Density", selection: $displayDensity) {
64 ForEach(DisplayDensity.allCases) { density in 65 ForEach(DisplayDensity.allCases) { density in
65 Text(density.label).tag(density) 66 Text(density.label).tag(density)
66 } 67 }
67 } 68 }
69 .themedRow()
68 } header: { 70 } header: {
69 Text("Appearance") 71 Text("Appearance")
70 } footer: { 72 } footer: {
@@ -76,10 +78,12 @@ struct SettingsView: View {
76 private func behaviorSection() -> some View { 78 private func behaviorSection() -> some View {
77 Section { 79 Section {
78 Toggle("Swipe actions", isOn: $swipeActionsEnabled) 80 Toggle("Swipe actions", isOn: $swipeActionsEnabled)
81 .themedRow()
79 Toggle("Contribution graphs", isOn: $contributionGraphsEnabled) 82 Toggle("Contribution graphs", isOn: $contributionGraphsEnabled)
80 .onChange(of: contributionGraphsEnabled) { _, newValue in 83 .onChange(of: contributionGraphsEnabled) { _, newValue in
81 ContributionWidgetContextStore.setEnabled(newValue) 84 ContributionWidgetContextStore.setEnabled(newValue)
82 } 85 }
86 .themedRow()
83 } header: { 87 } header: {
84 Text("Behavior") 88 Text("Behavior")
85 } footer: { 89 } footer: {
@@ -101,12 +105,14 @@ struct SettingsView: View {
101 } 105 }
102 } 106 }
103 .alignmentGuide(.listRowSeparatorLeading) { _ in 0 } 107 .alignmentGuide(.listRowSeparatorLeading) { _ in 0 }
108 .themedRow()
104 109
105 Button { 110 Button {
106 showAccountSwitcher = true 111 showAccountSwitcher = true
107 } label: { 112 } label: {
108 Label("Manage Accounts", systemImage: "person.2") 113 Label("Manage Accounts", systemImage: "person.2")
109 } 114 }
115 .themedRow()
110 116
111 HStack { 117 HStack {
112 Image(systemName: "lock.shield") 118 Image(systemName: "lock.shield")
@@ -116,14 +122,17 @@ struct SettingsView: View {
116 .foregroundStyle(.secondary) 122 .foregroundStyle(.secondary)
117 } 123 }
118 .alignmentGuide(.listRowSeparatorLeading) { _ in 0 } 124 .alignmentGuide(.listRowSeparatorLeading) { _ in 0 }
125 .themedRow()
119 126
120 Button("Reset App Data", role: .destructive) { 127 Button("Reset App Data", role: .destructive) {
121 pendingDestructiveAction = .resetAppData 128 pendingDestructiveAction = .resetAppData
122 } 129 }
130 .themedRow()
123 131
124 Button("Sign Out", role: .destructive) { 132 Button("Sign Out", role: .destructive) {
125 pendingDestructiveAction = .signOut 133 pendingDestructiveAction = .signOut
126 } 134 }
135 .themedRow()
127 } header: { 136 } header: {
128 Text("Authentication") 137 Text("Authentication")
129 } footer: { 138 } footer: {
@@ -139,6 +148,7 @@ struct SettingsView: View {
139 } label: { 148 } label: {
140 SwiftUI.Label("About Hutch", systemImage: "info.circle") 149 SwiftUI.Label("About Hutch", systemImage: "info.circle")
141 } 150 }
151 .themedRow()
142 } 152 }
143 } 153 }
144} 154}
@@ -211,15 +221,18 @@ private struct AboutView: View {
211 .foregroundStyle(.secondary) 221 .foregroundStyle(.secondary)
212 } 222 }
213 .padding(.vertical, 4) 223 .padding(.vertical, 4)
224 .themedRow()
214 225
215 LabeledContent("Version", value: version) 226 LabeledContent("Version", value: version)
216 .onTapGesture { 227 .onTapGesture {
217 developerRevealCount = min(developerRevealCount + 1, 5) 228 developerRevealCount = min(developerRevealCount + 1, 5)
218 } 229 }
230 .themedRow()
219 LabeledContent("Build", value: build) 231 LabeledContent("Build", value: build)
220 .onTapGesture { 232 .onTapGesture {
221 developerRevealCount = min(developerRevealCount + 1, 5) 233 developerRevealCount = min(developerRevealCount + 1, 5)
222 } 234 }
235 .themedRow()
223 } footer: { 236 } footer: {
224 Text(developerRevealFooterText) 237 Text(developerRevealFooterText)
225 } 238 }
@@ -228,34 +241,41 @@ private struct AboutView: View {
228 Link(destination: URL(string: "https://sr.ht")!) { 241 Link(destination: URL(string: "https://sr.ht")!) {
229 SwiftUI.Label("SourceHut", systemImage: "link") 242 SwiftUI.Label("SourceHut", systemImage: "link")
230 } 243 }
244 .themedRow()
231 Link(destination: URL(string: "https://man.sr.ht")!) { 245 Link(destination: URL(string: "https://man.sr.ht")!) {
232 SwiftUI.Label("SourceHut Manuals", systemImage: "book") 246 SwiftUI.Label("SourceHut Manuals", systemImage: "book")
233 } 247 }
248 .themedRow()
234 Link(destination: URL(string: "https://sr.ht/~ccleberg/Hutch")!) { 249 Link(destination: URL(string: "https://sr.ht/~ccleberg/Hutch")!) {
235 SwiftUI.Label("Project Repository", systemImage: "folder") 250 SwiftUI.Label("Project Repository", systemImage: "folder")
236 } 251 }
252 .themedRow()
237 } 253 }
238 254
239 Section("Support") { 255 Section("Support") {
240 Link(destination: URL(string: "mailto:hello@cleberg.net")!) { 256 Link(destination: URL(string: "mailto:hello@cleberg.net")!) {
241 SwiftUI.Label("Email Support", systemImage: "envelope") 257 SwiftUI.Label("Email Support", systemImage: "envelope")
242 } 258 }
259 .themedRow()
243 } 260 }
244 261
245 Section("Privacy") { 262 Section("Privacy") {
246 Text("Hutch uses your SourceHut personal access token to make requests on your behalf. The token is stored locally in the iOS keychain.") 263 Text("Hutch uses your SourceHut personal access token to make requests on your behalf. The token is stored locally in the iOS keychain.")
247 .font(.subheadline) 264 .font(.subheadline)
248 .foregroundStyle(.secondary) 265 .foregroundStyle(.secondary)
266 .themedRow()
249 267
250 Link(destination: URL(string: "https://zerolabs.sh/hutch/privacy-policy/")!) { 268 Link(destination: URL(string: "https://zerolabs.sh/hutch/privacy-policy/")!) {
251 SwiftUI.Label("Privacy Policy", systemImage: "hand.raised") 269 SwiftUI.Label("Privacy Policy", systemImage: "hand.raised")
252 } 270 }
271 .themedRow()
253 } 272 }
254 273
255 Section("Acknowledgements") { 274 Section("Acknowledgements") {
256 Text("Built for SourceHut users who want quick access to repositories, builds, and tickets on iOS.") 275 Text("Built for SourceHut users who want quick access to repositories, builds, and tickets on iOS.")
257 .font(.subheadline) 276 .font(.subheadline)
258 .foregroundStyle(.secondary) 277 .foregroundStyle(.secondary)
278 .themedRow()
259 } 279 }
260 280
261 if developerToolsVisible { 281 if developerToolsVisible {
@@ -264,18 +284,21 @@ private struct AboutView: View {
264 get: { appState.isDebugModeEnabled }, 284 get: { appState.isDebugModeEnabled },
265 set: { appState.isDebugModeEnabled = $0 } 285 set: { appState.isDebugModeEnabled = $0 }
266 )) 286 ))
287 .themedRow()
267 288
268 NavigationLink { 289 NavigationLink {
269 HomePrototypeView() 290 HomePrototypeView()
270 } label: { 291 } label: {
271 SwiftUI.Label("Home Prototype", systemImage: "house") 292 SwiftUI.Label("Home Prototype", systemImage: "house")
272 } 293 }
294 .themedRow()
273 295
274 NavigationLink { 296 NavigationLink {
275 WorkPrototypeView() 297 WorkPrototypeView()
276 } label: { 298 } label: {
277 SwiftUI.Label("Work Prototype", systemImage: "tray.full") 299 SwiftUI.Label("Work Prototype", systemImage: "tray.full")
278 } 300 }
301 .themedRow()
279 } header: { 302 } header: {
280 Text("Developer") 303 Text("Developer")
281 } footer: { 304 } footer: {
Hutch/Views/SystemStatus/SystemStatusView.swift +7
@@ -36,6 +36,7 @@ struct SystemStatusView: View {
36 Label(staleDataMessage, systemImage: "clock.arrow.trianglehead.counterclockwise.rotate.90") 36 Label(staleDataMessage, systemImage: "clock.arrow.trianglehead.counterclockwise.rotate.90")
37 .font(.subheadline) 37 .font(.subheadline)
38 .foregroundStyle(.secondary) 38 .foregroundStyle(.secondary)
39 .themedRow()
39 } 40 }
40 } 41 }
41 42
@@ -47,6 +48,7 @@ struct SystemStatusView: View {
47 48
48 recentIncidentsSection(viewModel.recentIncidents) 49 recentIncidentsSection(viewModel.recentIncidents)
49 } 50 }
51 .themedList()
50 .listStyle(.insetGrouped) 52 .listStyle(.insetGrouped)
51 .refreshable { 53 .refreshable {
52 await viewModel.load(forceRefresh: true) 54 await viewModel.load(forceRefresh: true)
@@ -99,6 +101,7 @@ struct SystemStatusView: View {
99 .font(.subheadline.weight(.medium)) 101 .font(.subheadline.weight(.medium))
100 } 102 }
101 .padding(.vertical, 4) 103 .padding(.vertical, 4)
104 .themedRow()
102 } 105 }
103 } 106 }
104 107
@@ -119,6 +122,7 @@ struct SystemStatusView: View {
119 } 122 }
120 .padding(.vertical, 2) 123 .padding(.vertical, 2)
121 } 124 }
125 .themedRow()
122 } 126 }
123 } 127 }
124 128
@@ -129,6 +133,7 @@ struct SystemStatusView: View {
129 ForEach(incidents) { incident in 133 ForEach(incidents) { incident in
130 incidentRow(incident) 134 incidentRow(incident)
131 } 135 }
136 .themedRow()
132 } 137 }
133 } 138 }
134 } 139 }
@@ -142,10 +147,12 @@ struct SystemStatusView: View {
142 systemImage: "clock.arrow.trianglehead.counterclockwise.rotate.90", 147 systemImage: "clock.arrow.trianglehead.counterclockwise.rotate.90",
143 description: Text("The status feed didn’t return any recent incidents.") 148 description: Text("The status feed didn’t return any recent incidents.")
144 ) 149 )
150 .themedRow()
145 } else { 151 } else {
146 ForEach(incidents) { incident in 152 ForEach(incidents) { incident in
147 incidentRow(incident) 153 incidentRow(incident)
148 } 154 }
155 .themedRow()
149 } 156 }
150 } 157 }
151 } 158 }
Hutch/Views/Tickets/TicketDetailView.swift +13
@@ -739,8 +739,10 @@ private struct ResolveSheet: View {
739 } 739 }
740 .pickerStyle(.inline) 740 .pickerStyle(.inline)
741 .labelsHidden() 741 .labelsHidden()
742 .themedRow()
742 } 743 }
743 } 744 }
745 .themedList()
744 .navigationTitle("Resolve Ticket") 746 .navigationTitle("Resolve Ticket")
745 .navigationBarTitleDisplayMode(.inline) 747 .navigationBarTitleDisplayMode(.inline)
746 .toolbar { 748 .toolbar {
@@ -801,6 +803,7 @@ private struct AssignSheet: View {
801 .buttonStyle(.plain) 803 .buttonStyle(.plain)
802 } 804 }
803 } 805 }
806 .themedRow()
804 } 807 }
805 } 808 }
806 809
@@ -809,6 +812,7 @@ private struct AssignSheet: View {
809 .textContentType(.username) 812 .textContentType(.username)
810 .autocorrectionDisabled() 813 .autocorrectionDisabled()
811 .textInputAutocapitalization(.never) 814 .textInputAutocapitalization(.never)
815 .themedRow()
812 816
813 Button("Add Assignee") { 817 Button("Add Assignee") {
814 let name = username.trimmingCharacters(in: .whitespacesAndNewlines) 818 let name = username.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -822,8 +826,10 @@ private struct AssignSheet: View {
822 username.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty 826 username.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
823 || viewModel.isPerformingAction 827 || viewModel.isPerformingAction
824 ) 828 )
829 .themedRow()
825 } 830 }
826 } 831 }
832 .themedList()
827 .navigationTitle("Assignees") 833 .navigationTitle("Assignees")
828 .navigationBarTitleDisplayMode(.inline) 834 .navigationBarTitleDisplayMode(.inline)
829 .toolbar { 835 .toolbar {
@@ -877,7 +883,9 @@ private struct LabelsSheet: View {
877 } 883 }
878 } 884 }
879 } 885 }
886 .themedRow()
880 } 887 }
888 .themedList()
881 } 889 }
882 } 890 }
883 .navigationTitle("Labels") 891 .navigationTitle("Labels")
@@ -917,11 +925,14 @@ private struct CreateLabelSheet: View {
917 Section("Label Details") { 925 Section("Label Details") {
918 TextField("Label name", text: $labelName) 926 TextField("Label name", text: $labelName)
919 .autocorrectionDisabled() 927 .autocorrectionDisabled()
928 .themedRow()
920 } 929 }
921 930
922 Section("Colors") { 931 Section("Colors") {
923 ColorPicker("Background color", selection: $backgroundColor, supportsOpacity: false) 932 ColorPicker("Background color", selection: $backgroundColor, supportsOpacity: false)
933 .themedRow()
924 ColorPicker("Text color", selection: $foregroundColor, supportsOpacity: false) 934 ColorPicker("Text color", selection: $foregroundColor, supportsOpacity: false)
935 .themedRow()
925 } 936 }
926 937
927 Section("Preview") { 938 Section("Preview") {
@@ -936,8 +947,10 @@ private struct CreateLabelSheet: View {
936 .clipShape(Capsule()) 947 .clipShape(Capsule())
937 Spacer() 948 Spacer()
938 } 949 }
950 .themedRow()
939 } 951 }
940 } 952 }
953 .themedList()
941 .navigationTitle("New Label") 954 .navigationTitle("New Label")
942 .navigationBarTitleDisplayMode(.inline) 955 .navigationBarTitleDisplayMode(.inline)
943 .toolbar { 956 .toolbar {
Hutch/Views/Tickets/TicketListView.swift +22 −1
@@ -6,6 +6,7 @@ struct TicketListView: View {
6 6
7 @AppStorage(AppStorageKeys.swipeActionsEnabled, store: .standard) private var swipeActionsEnabled = true 7 @AppStorage(AppStorageKeys.swipeActionsEnabled, store: .standard) private var swipeActionsEnabled = true
8 @Environment(AppState.self) private var appState 8 @Environment(AppState.self) private var appState
9 @Environment(\.isAMOLEDTheme) private var isAMOLED
9 @Environment(\.dismiss) private var dismiss 10 @Environment(\.dismiss) private var dismiss
10 @Environment(\.openURL) private var openURL 11 @Environment(\.openURL) private var openURL
11 @State private var tracker: TrackerSummary 12 @State private var tracker: TrackerSummary
@@ -277,7 +278,7 @@ struct TicketListView: View {
277 vm.deleteSavedFilter(savedFilter) 278 vm.deleteSavedFilter(savedFilter)
278 } 279 }
279 .listRowInsets(EdgeInsets()) 280 .listRowInsets(EdgeInsets())
280 .listRowBackground(Color.clear) 281 .listRowBackground(isAMOLED ? Color.black : Color.clear)
281 .listRowSeparator(.hidden) 282 .listRowSeparator(.hidden)
282 283
283 ForEach(viewModel.filteredTickets) { ticket in 284 ForEach(viewModel.filteredTickets) { ticket in
@@ -344,6 +345,7 @@ struct TicketListView: View {
344 await viewModel.loadMoreIfNeeded(currentItem: ticket) 345 await viewModel.loadMoreIfNeeded(currentItem: ticket)
345 } 346 }
346 } 347 }
348 .themedRow()
347 349
348 if viewModel.isLoadingMore { 350 if viewModel.isLoadingMore {
349 HStack { 351 HStack {
@@ -352,9 +354,11 @@ struct TicketListView: View {
352 Spacer() 354 Spacer()
353 } 355 }
354 .listRowSeparator(.hidden) 356 .listRowSeparator(.hidden)
357 .themedRow()
355 } 358 }
356 } 359 }
357 } 360 }
361 .themedList()
358 .listStyle(.plain) 362 .listStyle(.plain)
359 .listSectionSpacing(.compact) 363 .listSectionSpacing(.compact)
360 .searchable( 364 .searchable(
@@ -636,10 +640,13 @@ private struct CreateTicketSheet: View {
636 Form { 640 Form {
637 Section("Ticket Details") { 641 Section("Ticket Details") {
638 TextField("Title", text: $subject) 642 TextField("Title", text: $subject)
643 .themedRow()
639 TextField("Description (optional)", text: $descriptionText, axis: .vertical) 644 TextField("Description (optional)", text: $descriptionText, axis: .vertical)
640 .lineLimit(6...12) 645 .lineLimit(6...12)
646 .themedRow()
641 } 647 }
642 } 648 }
649 .themedList()
643 .navigationTitle("New Ticket") 650 .navigationTitle("New Ticket")
644 .navigationBarTitleDisplayMode(.inline) 651 .navigationBarTitleDisplayMode(.inline)
645 .toolbar { 652 .toolbar {
@@ -705,7 +712,9 @@ private struct TicketLabelsSheet: View {
705 } 712 }
706 } 713 }
707 } 714 }
715 .themedRow()
708 } 716 }
717 .themedList()
709 } 718 }
710 } else { 719 } else {
711 ContentUnavailableView( 720 ContentUnavailableView(
@@ -921,6 +930,7 @@ private struct TicketFilterLabelsSheet: View {
921 } 930 }
922 } 931 }
923 .padding(.vertical, 4) 932 .padding(.vertical, 4)
933 .themedRow()
924 } 934 }
925 } 935 }
926 936
@@ -940,8 +950,10 @@ private struct TicketFilterLabelsSheet: View {
940 } 950 }
941 .buttonStyle(.plain) 951 .buttonStyle(.plain)
942 } 952 }
953 .themedRow()
943 } 954 }
944 } 955 }
956 .themedList()
945 } 957 }
946 } 958 }
947 .navigationTitle("Filter Labels") 959 .navigationTitle("Filter Labels")
@@ -982,8 +994,10 @@ private struct SaveTicketFilterSheet: View {
982 Section("Name") { 994 Section("Name") {
983 TextField("Filter name", text: $name) 995 TextField("Filter name", text: $name)
984 .textInputAutocapitalization(.words) 996 .textInputAutocapitalization(.words)
997 .themedRow()
985 } 998 }
986 } 999 }
1000 .themedList()
987 .navigationTitle("Save Filter") 1001 .navigationTitle("Save Filter")
988 .navigationBarTitleDisplayMode(.inline) 1002 .navigationBarTitleDisplayMode(.inline)
989 .toolbar { 1003 .toolbar {
@@ -1085,6 +1099,7 @@ private struct BulkResolveSheet: View {
1085 Text("\(viewModel.selectedTicketCount) ticket\(viewModel.selectedTicketCount == 1 ? "" : "s") selected") 1099 Text("\(viewModel.selectedTicketCount) ticket\(viewModel.selectedTicketCount == 1 ? "" : "s") selected")
1086 .font(.subheadline) 1100 .font(.subheadline)
1087 .foregroundStyle(.secondary) 1101 .foregroundStyle(.secondary)
1102 .themedRow()
1088 } 1103 }
1089 1104
1090 Section("Resolution") { 1105 Section("Resolution") {
@@ -1095,8 +1110,10 @@ private struct BulkResolveSheet: View {
1095 } 1110 }
1096 .pickerStyle(.inline) 1111 .pickerStyle(.inline)
1097 .labelsHidden() 1112 .labelsHidden()
1113 .themedRow()
1098 } 1114 }
1099 } 1115 }
1116 .themedList()
1100 .navigationTitle("Close Tickets") 1117 .navigationTitle("Close Tickets")
1101 .navigationBarTitleDisplayMode(.inline) 1118 .navigationBarTitleDisplayMode(.inline)
1102 .toolbar { 1119 .toolbar {
@@ -1133,6 +1150,7 @@ private struct BulkAssignSheet: View {
1133 Text("\(viewModel.selectedTicketCount) ticket\(viewModel.selectedTicketCount == 1 ? "" : "s") selected") 1150 Text("\(viewModel.selectedTicketCount) ticket\(viewModel.selectedTicketCount == 1 ? "" : "s") selected")
1134 .font(.subheadline) 1151 .font(.subheadline)
1135 .foregroundStyle(.secondary) 1152 .foregroundStyle(.secondary)
1153 .themedRow()
1136 } 1154 }
1137 1155
1138 if let currentUser { 1156 if let currentUser {
@@ -1146,6 +1164,7 @@ private struct BulkAssignSheet: View {
1146 } 1164 }
1147 } 1165 }
1148 .disabled(viewModel.selectedTicketCount == 0 || viewModel.isPerformingAction) 1166 .disabled(viewModel.selectedTicketCount == 0 || viewModel.isPerformingAction)
1167 .themedRow()
1149 } 1168 }
1150 } 1169 }
1151 1170
@@ -1154,8 +1173,10 @@ private struct BulkAssignSheet: View {
1154 .textContentType(.username) 1173 .textContentType(.username)
1155 .autocorrectionDisabled() 1174 .autocorrectionDisabled()
1156 .textInputAutocapitalization(.never) 1175 .textInputAutocapitalization(.never)
1176 .themedRow()
1157 } 1177 }
1158 } 1178 }
1179 .themedList()
1159 .navigationTitle("Assign Tickets") 1180 .navigationTitle("Assign Tickets")
1160 .navigationBarTitleDisplayMode(.inline) 1181 .navigationBarTitleDisplayMode(.inline)
1161 .toolbar { 1182 .toolbar {
Hutch/Views/Tickets/TrackerListView.swift +3
@@ -162,6 +162,7 @@ struct TrackerListView: View {
162 await viewModel.loadMoreIfNeeded(currentItem: tracker) 162 await viewModel.loadMoreIfNeeded(currentItem: tracker)
163 } 163 }
164 } 164 }
165 .themedRow()
165 166
166 if viewModel.isLoadingMore { 167 if viewModel.isLoadingMore {
167 HStack { 168 HStack {
@@ -170,8 +171,10 @@ struct TrackerListView: View {
170 Spacer() 171 Spacer()
171 } 172 }
172 .listRowSeparator(.hidden) 173 .listRowSeparator(.hidden)
174 .themedRow()
173 } 175 }
174 } 176 }
177 .themedList()
175 .listStyle(.plain) 178 .listStyle(.plain)
176 .searchable( 179 .searchable(
177 text: $vm.searchText, 180 text: $vm.searchText,
Hutch/Views/Tickets/TrackerManagementView.swift +37
@@ -654,22 +654,27 @@ struct TrackerEditorSheet: View {
654 TextField("Tracker name", text: $name) 654 TextField("Tracker name", text: $name)
655 .textInputAutocapitalization(.never) 655 .textInputAutocapitalization(.never)
656 .autocorrectionDisabled() 656 .autocorrectionDisabled()
657 .themedRow()
657 TextField("Short description (optional)", text: $description, axis: .vertical) 658 TextField("Short description (optional)", text: $description, axis: .vertical)
658 .lineLimit(2...4) 659 .lineLimit(2...4)
660 .themedRow()
659 Picker("Visibility", selection: $visibility) { 661 Picker("Visibility", selection: $visibility) {
660 Text("Public").tag(Visibility.public) 662 Text("Public").tag(Visibility.public)
661 Text("Unlisted").tag(Visibility.unlisted) 663 Text("Unlisted").tag(Visibility.unlisted)
662 Text("Private").tag(Visibility.private) 664 Text("Private").tag(Visibility.private)
663 } 665 }
666 .themedRow()
664 } 667 }
665 668
666 if let error, !error.isEmpty { 669 if let error, !error.isEmpty {
667 Section { 670 Section {
668 Text(error) 671 Text(error)
669 .foregroundStyle(.red) 672 .foregroundStyle(.red)
673 .themedRow()
670 } 674 }
671 } 675 }
672 } 676 }
677 .themedList()
673 .navigationTitle(title) 678 .navigationTitle(title)
674 .navigationBarTitleDisplayMode(.inline) 679 .navigationBarTitleDisplayMode(.inline)
675 .toolbar { 680 .toolbar {
@@ -718,10 +723,12 @@ struct TrackerACLManagementSheet: View {
718 List { 723 List {
719 Section("Default Access") { 724 Section("Default Access") {
720 TrackerPermissionSummary(permissions: viewModel.defaultACL.permissions) 725 TrackerPermissionSummary(permissions: viewModel.defaultACL.permissions)
726 .themedRow()
721 Button("Update Default ACL") { 727 Button("Update Default ACL") {
722 editingDefaultACL = true 728 editingDefaultACL = true
723 } 729 }
724 .disabled(viewModel.isSavingACL) 730 .disabled(viewModel.isSavingACL)
731 .themedRow()
725 } 732 }
726 733
727 Section { 734 Section {
@@ -731,9 +738,11 @@ struct TrackerACLManagementSheet: View {
731 ProgressView() 738 ProgressView()
732 Spacer() 739 Spacer()
733 } 740 }
741 .themedRow()
734 } else if viewModel.acls.isEmpty { 742 } else if viewModel.acls.isEmpty {
735 Text("No tracker-specific ACLs yet.") 743 Text("No tracker-specific ACLs yet.")
736 .foregroundStyle(.secondary) 744 .foregroundStyle(.secondary)
745 .themedRow()
737 } else { 746 } else {
738 ForEach(viewModel.acls) { entry in 747 ForEach(viewModel.acls) { entry in
739 VStack(alignment: .leading, spacing: 6) { 748 VStack(alignment: .leading, spacing: 6) {
@@ -756,6 +765,7 @@ struct TrackerACLManagementSheet: View {
756 .tint(.blue) 765 .tint(.blue)
757 } 766 }
758 } 767 }
768 .themedRow()
759 } 769 }
760 } header: { 770 } header: {
761 Text("User ACLs") 771 Text("User ACLs")
@@ -763,6 +773,7 @@ struct TrackerACLManagementSheet: View {
763 Text("Each ACL must include all five permission flags.") 773 Text("Each ACL must include all five permission flags.")
764 } 774 }
765 } 775 }
776 .themedList()
766 .navigationTitle("ACLs") 777 .navigationTitle("ACLs")
767 .navigationBarTitleDisplayMode(.inline) 778 .navigationBarTitleDisplayMode(.inline)
768 .toolbar { 779 .toolbar {
@@ -908,6 +919,7 @@ private struct TrackerACLEditorSheet: View {
908 TextField("Username or ~username", text: $username) 919 TextField("Username or ~username", text: $username)
909 .autocorrectionDisabled() 920 .autocorrectionDisabled()
910 .textInputAutocapitalization(.never) 921 .textInputAutocapitalization(.never)
922 .themedRow()
911 } 923 }
912 924
913 permissionSection 925 permissionSection
@@ -916,9 +928,11 @@ private struct TrackerACLEditorSheet: View {
916 Section { 928 Section {
917 Text(error) 929 Text(error)
918 .foregroundStyle(.red) 930 .foregroundStyle(.red)
931 .themedRow()
919 } 932 }
920 } 933 }
921 } 934 }
935 .themedList()
922 .navigationTitle(title) 936 .navigationTitle(title)
923 .navigationBarTitleDisplayMode(.inline) 937 .navigationBarTitleDisplayMode(.inline)
924 .toolbar { 938 .toolbar {
@@ -950,10 +964,15 @@ private struct TrackerACLEditorSheet: View {
950 private var permissionSection: some View { 964 private var permissionSection: some View {
951 Section("Permissions") { 965 Section("Permissions") {
952 Toggle("Browse", isOn: $browse) 966 Toggle("Browse", isOn: $browse)
967 .themedRow()
953 Toggle("Submit", isOn: $submit) 968 Toggle("Submit", isOn: $submit)
969 .themedRow()
954 Toggle("Comment", isOn: $comment) 970 Toggle("Comment", isOn: $comment)
971 .themedRow()
955 Toggle("Edit", isOn: $edit) 972 Toggle("Edit", isOn: $edit)
973 .themedRow()
956 Toggle("Triage", isOn: $triage) 974 Toggle("Triage", isOn: $triage)
975 .themedRow()
957 } 976 }
958 } 977 }
959 978
@@ -1003,19 +1022,26 @@ private struct TrackerDefaultACLEditorSheet: View {
1003 Form { 1022 Form {
1004 Section("Permissions") { 1023 Section("Permissions") {
1005 Toggle("Browse", isOn: $browse) 1024 Toggle("Browse", isOn: $browse)
1025 .themedRow()
1006 Toggle("Submit", isOn: $submit) 1026 Toggle("Submit", isOn: $submit)
1027 .themedRow()
1007 Toggle("Comment", isOn: $comment) 1028 Toggle("Comment", isOn: $comment)
1029 .themedRow()
1008 Toggle("Edit", isOn: $edit) 1030 Toggle("Edit", isOn: $edit)
1031 .themedRow()
1009 Toggle("Triage", isOn: $triage) 1032 Toggle("Triage", isOn: $triage)
1033 .themedRow()
1010 } 1034 }
1011 1035
1012 if let error, !error.isEmpty { 1036 if let error, !error.isEmpty {
1013 Section { 1037 Section {
1014 Text(error) 1038 Text(error)
1015 .foregroundStyle(.red) 1039 .foregroundStyle(.red)
1040 .themedRow()
1016 } 1041 }
1017 } 1042 }
1018 } 1043 }
1044 .themedList()
1019 .navigationTitle("Default ACL") 1045 .navigationTitle("Default ACL")
1020 .navigationBarTitleDisplayMode(.inline) 1046 .navigationBarTitleDisplayMode(.inline)
1021 .toolbar { 1047 .toolbar {
@@ -1073,6 +1099,7 @@ struct TrackerLabelManagementSheet: View {
1073 Text("Labels are managed here and reused throughout the tracker.") 1099 Text("Labels are managed here and reused throughout the tracker.")
1074 .font(.footnote) 1100 .font(.footnote)
1075 .foregroundStyle(.secondary) 1101 .foregroundStyle(.secondary)
1102 .themedRow()
1076 } 1103 }
1077 1104
1078 if viewModel.isLoadingLabels { 1105 if viewModel.isLoadingLabels {
@@ -1081,12 +1108,14 @@ struct TrackerLabelManagementSheet: View {
1081 ProgressView() 1108 ProgressView()
1082 Spacer() 1109 Spacer()
1083 } 1110 }
1111 .themedRow()
1084 } else if viewModel.labels.isEmpty { 1112 } else if viewModel.labels.isEmpty {
1085 ContentUnavailableView( 1113 ContentUnavailableView(
1086 "No Labels", 1114 "No Labels",
1087 systemImage: "tag", 1115 systemImage: "tag",
1088 description: Text("Create labels for triage and organization.") 1116 description: Text("Create labels for triage and organization.")
1089 ) 1117 )
1118 .themedRow()
1090 } else { 1119 } else {
1091 ForEach(viewModel.labels) { label in 1120 ForEach(viewModel.labels) { label in
1092 Button { 1121 Button {
@@ -1110,8 +1139,10 @@ struct TrackerLabelManagementSheet: View {
1110 } 1139 }
1111 } 1140 }
1112 } 1141 }
1142 .themedRow()
1113 } 1143 }
1114 } 1144 }
1145 .themedList()
1115 .navigationTitle("Labels") 1146 .navigationTitle("Labels")
1116 .navigationBarTitleDisplayMode(.inline) 1147 .navigationBarTitleDisplayMode(.inline)
1117 .toolbar { 1148 .toolbar {
@@ -1261,8 +1292,11 @@ private struct TrackerLabelEditorSheet: View {
1261 Form { 1292 Form {
1262 Section("Details") { 1293 Section("Details") {
1263 TextField("Label name", text: $name) 1294 TextField("Label name", text: $name)
1295 .themedRow()
1264 ColorPicker("Foreground", selection: $foregroundColor, supportsOpacity: false) 1296 ColorPicker("Foreground", selection: $foregroundColor, supportsOpacity: false)
1297 .themedRow()
1265 ColorPicker("Background", selection: $backgroundColor, supportsOpacity: false) 1298 ColorPicker("Background", selection: $backgroundColor, supportsOpacity: false)
1299 .themedRow()
1266 } 1300 }
1267 1301
1268 Section("Preview") { 1302 Section("Preview") {
@@ -1274,15 +1308,18 @@ private struct TrackerLabelEditorSheet: View {
1274 foregroundColor: foregroundColor.hexString 1308 foregroundColor: foregroundColor.hexString
1275 ) 1309 )
1276 ) 1310 )
1311 .themedRow()
1277 } 1312 }
1278 1313
1279 if let error, !error.isEmpty { 1314 if let error, !error.isEmpty {
1280 Section { 1315 Section {
1281 Text(error) 1316 Text(error)
1282 .foregroundStyle(.red) 1317 .foregroundStyle(.red)
1318 .themedRow()
1283 } 1319 }
1284 } 1320 }
1285 } 1321 }
1322 .themedList()
1286 .navigationTitle(title) 1323 .navigationTitle(title)
1287 .navigationBarTitleDisplayMode(.inline) 1324 .navigationBarTitleDisplayMode(.inline)
1288 .toolbar { 1325 .toolbar {
Hutch/Views/Work/WorkView.swift +10 −1
@@ -11,6 +11,7 @@ struct WorkView: View {
11 11
12 @AppStorage(AppStorageKeys.swipeActionsEnabled, store: .standard) private var swipeActionsEnabled = true 12 @AppStorage(AppStorageKeys.swipeActionsEnabled, store: .standard) private var swipeActionsEnabled = true
13 @Environment(AppState.self) private var appState 13 @Environment(AppState.self) private var appState
14 @Environment(\.isAMOLEDTheme) private var isAMOLED
14 @Environment(\.scenePhase) private var scenePhase 15 @Environment(\.scenePhase) private var scenePhase
15 @State private var viewModel: HomeViewModel? 16 @State private var viewModel: HomeViewModel?
16 @State private var scope: Scope = .all 17 @State private var scope: Scope = .all
@@ -74,6 +75,7 @@ struct WorkView: View {
74 } 75 }
75 } 76 }
76 .padding(.vertical, 2) 77 .padding(.vertical, 2)
78 .themedRow()
77 } 79 }
78 } 80 }
79 81
@@ -88,6 +90,7 @@ struct WorkView: View {
88 if workCount(viewModel) == 0 { 90 if workCount(viewModel) == 0 {
89 Section { 91 Section {
90 WorkCompactMessageRow(text: "Nothing to do", systemImage: "checkmark.circle") 92 WorkCompactMessageRow(text: "Nothing to do", systemImage: "checkmark.circle")
93 .themedRow()
91 } 94 }
92 } 95 }
93 } 96 }
@@ -100,7 +103,7 @@ struct WorkView: View {
100 } 103 }
101 } 104 }
102 .pickerStyle(.segmented) 105 .pickerStyle(.segmented)
103 .listRowBackground(Color.clear) 106 .listRowBackground(isAMOLED ? Color.black : Color.clear)
104 .listRowInsets(EdgeInsets()) 107 .listRowInsets(EdgeInsets())
105 } 108 }
106 } 109 }
@@ -110,9 +113,11 @@ struct WorkView: View {
110 Section { 113 Section {
111 if isLoadingUnread(viewModel) { 114 if isLoadingUnread(viewModel) {
112 WorkLoadingRow(label: "Loading unread threads") 115 WorkLoadingRow(label: "Loading unread threads")
116 .themedRow()
113 } else if viewModel.unreadInboxThreads.isEmpty { 117 } else if viewModel.unreadInboxThreads.isEmpty {
114 if compactWhenEmpty { 118 if compactWhenEmpty {
115 WorkCompactMessageRow(text: "No unread threads", systemImage: "tray") 119 WorkCompactMessageRow(text: "No unread threads", systemImage: "tray")
120 .themedRow()
116 } 121 }
117 } else { 122 } else {
118 ForEach(viewModel.unreadInboxThreads) { thread in 123 ForEach(viewModel.unreadInboxThreads) { thread in
@@ -137,6 +142,7 @@ struct WorkView: View {
137 } 142 }
138 } 143 }
139 } 144 }
145 .themedRow()
140 } 146 }
141 } header: { 147 } header: {
142 Text("Unread Threads") 148 Text("Unread Threads")
@@ -155,8 +161,10 @@ struct WorkView: View {
155 Section { 161 Section {
156 if viewModel.isLoadingAssignedTickets && viewModel.assignedTickets.isEmpty { 162 if viewModel.isLoadingAssignedTickets && viewModel.assignedTickets.isEmpty {
157 WorkLoadingRow(label: "Loading assigned tickets") 163 WorkLoadingRow(label: "Loading assigned tickets")
164 .themedRow()
158 } else if viewModel.assignedTickets.isEmpty { 165 } else if viewModel.assignedTickets.isEmpty {
159 WorkCompactMessageRow(text: "No assigned tickets", systemImage: "person.crop.circle.badge.checkmark") 166 WorkCompactMessageRow(text: "No assigned tickets", systemImage: "person.crop.circle.badge.checkmark")
167 .themedRow()
160 } else { 168 } else {
161 ForEach(viewModel.assignedTickets) { ticket in 169 ForEach(viewModel.assignedTickets) { ticket in
162 NavigationLink { 170 NavigationLink {
@@ -200,6 +208,7 @@ struct WorkView: View {
200 } 208 }
201 } 209 }
202 } 210 }
211 .themedRow()
203 } 212 }
204 } header: { 213 } header: {
205 Text("Assigned Tickets") 214 Text("Assigned Tickets")