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 @@
515515 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
516516 CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements;
517517 CODE_SIGN_STYLE = Automatic;
518 CURRENT_PROJECT_VERSION = 67;
518 CURRENT_PROJECT_VERSION = 68;
519519 DEVELOPMENT_TEAM = ZCNAX3VL9D;
520520 ENABLE_PREVIEWS = YES;
521521 GENERATE_INFOPLIST_FILE = YES;
@@ -532,7 +532,7 @@
532532 "$(inherited)",
533533 "@executable_path/Frameworks",
534534 );
535 MARKETING_VERSION = 3.0.6;
535 MARKETING_VERSION = 3.1.0;
536536 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
537537 PRODUCT_NAME = "$(TARGET_NAME)";
538538 STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -552,7 +552,7 @@
552552 ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
553553 CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements;
554554 CODE_SIGN_STYLE = Automatic;
555 CURRENT_PROJECT_VERSION = 67;
555 CURRENT_PROJECT_VERSION = 68;
556556 DEVELOPMENT_TEAM = ZCNAX3VL9D;
557557 ENABLE_PREVIEWS = YES;
558558 GENERATE_INFOPLIST_FILE = YES;
@@ -569,7 +569,7 @@
569569 "$(inherited)",
570570 "@executable_path/Frameworks",
571571 );
572 MARKETING_VERSION = 3.0.6;
572 MARKETING_VERSION = 3.1.0;
573573 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch;
574574 PRODUCT_NAME = "$(TARGET_NAME)";
575575 STRING_CATALOG_GENERATE_SYMBOLS = YES;
@@ -632,7 +632,7 @@
632632 APPLICATION_EXTENSION_API_ONLY = YES;
633633 CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements;
634634 CODE_SIGN_STYLE = Automatic;
635 CURRENT_PROJECT_VERSION = 67;
635 CURRENT_PROJECT_VERSION = 68;
636636 DEVELOPMENT_TEAM = ZCNAX3VL9D;
637637 GENERATE_INFOPLIST_FILE = NO;
638638 INFOPLIST_FILE = HutchWidgetExtension/Info.plist;
@@ -642,7 +642,7 @@
642642 "@executable_path/Frameworks",
643643 "@executable_path/../../Frameworks",
644644 );
645 MARKETING_VERSION = 3.0.6;
645 MARKETING_VERSION = 3.1.0;
646646 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
647647 PRODUCT_NAME = "$(TARGET_NAME)";
648648 SKIP_INSTALL = YES;
@@ -661,7 +661,7 @@
661661 APPLICATION_EXTENSION_API_ONLY = YES;
662662 CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements;
663663 CODE_SIGN_STYLE = Automatic;
664 CURRENT_PROJECT_VERSION = 67;
664 CURRENT_PROJECT_VERSION = 68;
665665 DEVELOPMENT_TEAM = ZCNAX3VL9D;
666666 GENERATE_INFOPLIST_FILE = NO;
667667 INFOPLIST_FILE = HutchWidgetExtension/Info.plist;
@@ -671,7 +671,7 @@
671671 "@executable_path/Frameworks",
672672 "@executable_path/../../Frameworks",
673673 );
674 MARKETING_VERSION = 3.0.6;
674 MARKETING_VERSION = 3.1.0;
675675 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
676676 PRODUCT_NAME = "$(TARGET_NAME)";
677677 SKIP_INSTALL = YES;
Hutch/App/HutchApp.swift +2
@@ -13,7 +13,9 @@ struct HutchApp: App {
1313 .environment(appState)
1414 .environment(networkMonitor)
1515 .environment(\.displayDensity, displayDensity)
16 .environment(\.isAMOLEDTheme, appTheme == .amoled)
1617 .environment(\.defaultMinListRowHeight, displayDensity == .compact ? 36 : 44)
18 .background(appTheme == .amoled ? Color.black : Color.clear)
1719 .preferredColorScheme(appTheme.colorScheme)
1820 .onOpenURL { url in
1921 if let link = DeepLink(url: url) {
Hutch/App/RootView.swift +21
@@ -4,6 +4,7 @@ import SwiftUI
44/// full-screen sheet for token entry on first launch.
55struct RootView: View {
66 @Environment(AppState.self) private var appState
7 @Environment(\.isAMOLEDTheme) private var isAMOLED
78 @State private var homePath = NavigationPath()
89 @State private var morePath = NavigationPath()
910 @State private var repoPath = NavigationPath()
@@ -125,6 +126,7 @@ struct RootView: View {
125126 .id(appState.sessionIdentity)
126127 .defaultAppStorage(appState.accountDefaults)
127128 .modifier(SidebarAdaptableTabStyle())
129 .modifier(AMOLEDToolbarStyle(isAMOLED: isAMOLED))
128130 .modifier(TabKeyboardShortcuts(selectedTab: Binding(
129131 get: { appState.selectedTab },
130132 set: { appState.selectedTab = $0 }
@@ -427,6 +429,25 @@ private struct TabKeyboardShortcuts: ViewModifier {
427429 }
428430}
429431
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
430451// MARK: - iPad Sidebar Adaptable
431452
432453/// 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 {
66 case system
77 case light
88 case dark
9 case amoled
910
1011 var id: String { rawValue }
1112
@@ -14,6 +15,7 @@ enum AppTheme: String, CaseIterable, Identifiable {
1415 case .system: "System"
1516 case .light: "Light"
1617 case .dark: "Dark"
18 case .amoled: "AMOLED"
1719 }
1820 }
1921
@@ -22,6 +24,7 @@ enum AppTheme: String, CaseIterable, Identifiable {
2224 case .system: nil
2325 case .light: .light
2426 case .dark: .dark
27 case .amoled: .dark
2528 }
2629 }
2730}
@@ -46,25 +49,51 @@ private struct DisplayDensityKey: EnvironmentKey {
4649 static let defaultValue: DisplayDensity = .standard
4750}
4851
52private struct IsAMOLEDThemeKey: EnvironmentKey {
53 static let defaultValue: Bool = false
54}
55
4956extension EnvironmentValues {
5057 var displayDensity: DisplayDensity {
5158 get { self[DisplayDensityKey.self] }
5259 set { self[DisplayDensityKey.self] = newValue }
5360 }
61
62 var isAMOLEDTheme: Bool {
63 get { self[IsAMOLEDThemeKey.self] }
64 set { self[IsAMOLEDThemeKey.self] = newValue }
65 }
5466}
5567
5668// MARK: - Themed List Modifier
5769
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.
5971/// Apply once per List or Form.
6072struct ThemedListStyle: ViewModifier {
6173 @Environment(\.displayDensity) private var density
74 @Environment(\.isAMOLEDTheme) private var isAMOLED
6275
6376 func body(content: Content) -> some View {
6477 content
6578 .listSectionSpacing(density == .compact ? .compact : .default)
6679 .contentMargins(.vertical, density == .compact ? 2 : 8, for: .scrollContent)
6780 .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 }
6897 }
6998}
7099
@@ -74,3 +103,21 @@ extension View {
74103 modifier(ThemedListStyle())
75104 }
76105}
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 {
1515 Form {
1616 Section {
1717 Text("Enter your SourceHut personal access token to connect.")
18 .themedRow()
1819 } header: {
1920 Text("Welcome to Hutch")
2021 } footer: {
@@ -27,6 +28,7 @@ struct TokenEntryView: View {
2728 .autocorrectionDisabled()
2829 .textInputAutocapitalization(.never)
2930 .disabled(isConnecting)
31 .themedRow()
3032 } header: {
3133 Text("Token")
3234 }
@@ -40,6 +42,7 @@ struct TokenEntryView: View {
4042 .foregroundStyle(.red)
4143 }
4244 .foregroundStyle(.red)
45 .themedRow()
4346 }
4447 }
4548
@@ -56,22 +59,26 @@ struct TokenEntryView: View {
5659 }
5760 }
5861 .disabled(tokenTrimmed.isEmpty || isConnecting)
62 .themedRow()
5963 }
6064
6165 Section {
6266 Link(destination: createAccountURL) {
6367 Label("Create SourceHut account", systemImage: "person.badge.plus")
6468 }
69 .themedRow()
6570
6671 Link(destination: personalAccessTokensURL) {
6772 Label("Create Personal Access Token", systemImage: "key")
6873 }
74 .themedRow()
6975 } header: {
7076 Text("Need an account?")
7177 } footer: {
7278 Text("These links open SourceHut in your browser. After signing up, create a Personal Access Token there and paste it here.")
7379 }
7480 }
81 .themedList()
7582 .navigationTitle("Hutch")
7683 }
7784 }
Hutch/Views/Builds/BuildDetailView.swift +25
@@ -176,26 +176,34 @@ struct BuildDetailView: View {
176176 .font(.subheadline.weight(.medium))
177177 }
178178 }
179 .themedRow()
179180
180181 if let note = job.note, !note.isEmpty {
181182 LabeledContent("Note", value: note)
183 .themedRow()
182184 }
183185
184186 if let image = job.image {
185187 LabeledContent("Image", value: image)
188 .themedRow()
186189 }
187190
188191 if !job.tags.isEmpty {
189192 LabeledContent("Tags", value: job.tags.joined(separator: ", "))
193 .themedRow()
190194 }
191195
192196 if let visibility = job.visibility {
193197 LabeledContent("Visibility", value: visibility.rawValue.capitalized)
198 .themedRow()
194199 }
195200
196201 LabeledContent("Owner", value: job.owner.canonicalName)
202 .themedRow()
197203 LabeledContent("Created", value: job.created.relativeDescription)
204 .themedRow()
198205 LabeledContent("Updated", value: job.updated.relativeDescription)
206 .themedRow()
199207 }
200208
201209 if appState.isDebugModeEnabled {
@@ -211,9 +219,11 @@ struct BuildDetailView: View {
211219 url: \(SRHTWebURL.build(jobId: job.id, ownerCanonicalName: job.owner.canonicalName)?.absoluteString ?? "unavailable")
212220 """
213221 )
222 .themedRow()
214223
215224 if let rawJobResponse = viewModel.rawJobResponse {
216225 DebugTextBlock(title: "Raw Response", content: rawJobResponse)
226 .themedRow()
217227 }
218228 }
219229 }
@@ -237,6 +247,7 @@ struct BuildDetailView: View {
237247 }
238248 }
239249 .disabled(isOpeningRepository)
250 .themedRow()
240251 }
241252 }
242253
@@ -248,6 +259,7 @@ struct BuildDetailView: View {
248259 openURL(url)
249260 }
250261 }
262 .themedRow()
251263 } header: {
252264 Text("Artifacts")
253265 } footer: {
@@ -275,6 +287,7 @@ struct BuildDetailView: View {
275287 }
276288 }
277289 .foregroundStyle(.primary)
290 .themedRow()
278291 } header: {
279292 HStack(spacing: 6) {
280293 TaskStatusIcon(status: task.status)
@@ -299,6 +312,7 @@ struct BuildDetailView: View {
299312 }
300313 }
301314 .disabled(viewModel.isCancelling)
315 .themedRow()
302316 }
303317 }
304318
@@ -319,6 +333,7 @@ struct BuildDetailView: View {
319333 }
320334 }
321335 .disabled(viewModel.isRebuilding)
336 .themedRow()
322337
323338 Button {
324339 showEditResubmitSheet = true
@@ -326,11 +341,13 @@ struct BuildDetailView: View {
326341 Text("Edit & Resubmit")
327342 }
328343 .disabled(viewModel.isSubmittingEditedBuild)
344 .themedRow()
329345 } footer: {
330346 Text("Creates a new build using this job’s saved manifest, tags, note, and visibility.")
331347 }
332348 }
333349 }
350 .themedList()
334351 .task(id: job.id) {
335352 RecentActivityStore.recordBuild(
336353 jobId: job.id,
@@ -463,26 +480,33 @@ private struct EditResubmitBuildSheet: View {
463480 .lineLimit(12...24)
464481 .textInputAutocapitalization(.never)
465482 .autocorrectionDisabled()
483 .themedRow()
466484 }
467485
468486 Section("Build Options") {
469487 TextField("Note (optional)", text: $note)
488 .themedRow()
470489 TextField("Tags (comma-separated, optional)", text: $tagsText)
471490 .textInputAutocapitalization(.never)
472491 .autocorrectionDisabled()
492 .themedRow()
473493 Picker("Visibility", selection: $visibility) {
474494 Text("Public").tag(Visibility.public)
475495 Text("Unlisted").tag(Visibility.unlisted)
476496 Text("Private").tag(Visibility.private)
477497 }
498 .themedRow()
478499 Toggle("Start build now", isOn: $execute)
500 .themedRow()
479501 Toggle("Allow build secrets", isOn: $secrets)
502 .themedRow()
480503 }
481504
482505 Section {
483506 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.")
484507 .font(.footnote)
485508 .foregroundStyle(.secondary)
509 .themedRow()
486510 }
487511
488512 if let actionError = viewModel.actionError {
@@ -494,6 +518,7 @@ private struct EditResubmitBuildSheet: View {
494518 .foregroundStyle(.red)
495519 }
496520 .foregroundStyle(.red)
521 .themedRow()
497522 }
498523 }
499524 }
Hutch/Views/Builds/BuildListView.swift +13 −1
@@ -5,6 +5,7 @@ struct BuildListView: View {
55 @AppStorage(AppStorageKeys.buildsAutoRefreshInterval) private var autoRefreshRawValue = 0
66 @AppStorage(AppStorageKeys.buildsRepoFilter) private var savedRepoFilter = ""
77 @Environment(AppState.self) private var appState
8 @Environment(\.isAMOLEDTheme) private var isAMOLED
89 @State private var viewModel: BuildListViewModel?
910 @State private var showSubmitSheet = false
1011 @State private var submittedJobId: Int?
@@ -134,7 +135,7 @@ struct BuildListView: View {
134135 .padding(.top, 6)
135136 .padding(.bottom, 10)
136137 .listRowInsets(EdgeInsets())
137 .listRowBackground(Color.clear)
138 .listRowBackground(isAMOLED ? Color.black : Color.clear)
138139 .listRowSeparator(.hidden)
139140
140141 ForEach(viewModel.filteredJobs) { job in
@@ -181,6 +182,7 @@ struct BuildListView: View {
181182 await viewModel.loadMoreIfNeeded(currentItem: job)
182183 }
183184 }
185 .themedRow()
184186
185187 if viewModel.isLoadingMore {
186188 HStack {
@@ -189,6 +191,7 @@ struct BuildListView: View {
189191 Spacer()
190192 }
191193 .listRowSeparator(.hidden)
194 .themedRow()
192195 }
193196 }
194197 }
@@ -279,26 +282,33 @@ private struct SubmitBuildSheet: View {
279282 .lineLimit(12...24)
280283 .textInputAutocapitalization(.never)
281284 .autocorrectionDisabled()
285 .themedRow()
282286 }
283287
284288 Section("Build Options") {
285289 TextField("Note (optional)", text: $note)
290 .themedRow()
286291 TextField("Tags (comma-separated, optional)", text: $tagsText)
287292 .textInputAutocapitalization(.never)
288293 .autocorrectionDisabled()
294 .themedRow()
289295 Picker("Visibility", selection: $visibility) {
290296 Text("Public").tag(Visibility.public)
291297 Text("Unlisted").tag(Visibility.unlisted)
292298 Text("Private").tag(Visibility.private)
293299 }
300 .themedRow()
294301 Toggle("Start build now", isOn: $execute)
302 .themedRow()
295303 Toggle("Allow build secrets", isOn: $secrets)
304 .themedRow()
296305 }
297306
298307 Section {
299308 Text("You need a valid builds.sr.ht manifest and a token with BUILDS:RW.")
300309 .font(.footnote)
301310 .foregroundStyle(.secondary)
311 .themedRow()
302312 }
303313
304314 if let error = viewModel.error {
@@ -310,9 +320,11 @@ private struct SubmitBuildSheet: View {
310320 .foregroundStyle(.red)
311321 }
312322 .foregroundStyle(.red)
323 .themedRow()
313324 }
314325 }
315326 }
327 .themedList()
316328 .navigationTitle("Submit Build")
317329 .navigationBarTitleDisplayMode(.inline)
318330 .onDisappear {
Hutch/Views/Home/HomeView.swift +6
@@ -94,6 +94,7 @@ struct HomeView: View {
9494 )
9595 }
9696 .buttonStyle(.plain)
97 .themedRow()
9798 }
9899 }
99100 }
@@ -109,6 +110,7 @@ struct HomeView: View {
109110 emphasis: .action
110111 )
111112 }
113 .themedRow()
112114 }
113115 }
114116
@@ -126,6 +128,7 @@ struct HomeView: View {
126128 .disabled(isOpeningRecentItem)
127129 .listRowSeparator(.hidden)
128130 }
131 .themedRow()
129132 }
130133 }
131134 }
@@ -143,6 +146,7 @@ struct HomeView: View {
143146 emphasis: .monitoring
144147 )
145148 }
149 .themedRow()
146150 }
147151 }
148152
@@ -156,6 +160,7 @@ struct HomeView: View {
156160 } label: {
157161 HomeCompactMessageRow(text: "Pin projects for quick access", systemImage: "pin")
158162 }
163 .themedRow()
159164 } else {
160165 LazyVGrid(
161166 columns: [
@@ -174,6 +179,7 @@ struct HomeView: View {
174179 }
175180 }
176181 .padding(.vertical, 2)
182 .themedRow()
177183 }
178184 }
179185 }
Hutch/Views/Inbox/ThreadDetailView.swift +6
@@ -123,6 +123,7 @@ struct ThreadDetailView: View {
123123 .foregroundStyle(.secondary)
124124 }
125125 .padding(.vertical, 4)
126 .themedRow()
126127 }
127128
128129 Section("Related") {
@@ -138,6 +139,7 @@ struct ThreadDetailView: View {
138139 } label: {
139140 Label(thread.listDisplayName, systemImage: "list.bullet")
140141 }
142 .themedRow()
141143
142144 if let repo = self.thread.repo {
143145 Button {
@@ -157,6 +159,7 @@ struct ThreadDetailView: View {
157159 }
158160 }
159161 .disabled(isOpeningRepository)
162 .themedRow()
160163 }
161164 }
162165
@@ -165,6 +168,7 @@ struct ThreadDetailView: View {
165168 Text(partialWarning)
166169 .font(.caption)
167170 .foregroundStyle(.secondary)
171 .themedRow()
168172 }
169173 }
170174
@@ -184,8 +188,10 @@ struct ThreadDetailView: View {
184188 }
185189 )
186190 }
191 .themedRow()
187192 }
188193 }
194 .themedList()
189195 .listStyle(.plain)
190196 .toolbar {
191197 ToolbarItem(placement: .topBarTrailing) {
Hutch/Views/Lists/MailingListListView.swift +2
@@ -142,7 +142,9 @@ struct MailingListListView: View {
142142 .padding(.vertical, 2)
143143 }
144144 }
145 .themedRow()
145146 }
147 .themedList()
146148 .listStyle(.plain)
147149 .searchable(
148150 text: $vm.searchText,
Hutch/Views/Lookup/LookupView.swift +5
@@ -356,6 +356,7 @@ struct LookupView: View {
356356 }
357357 }
358358 .pickerStyle(.menu)
359 .themedRow()
359360
360361 TextField(
361362 vm.selectedType.inputLabel,
@@ -368,6 +369,7 @@ struct LookupView: View {
368369 .onSubmit {
369370 Task { await vm.lookup() }
370371 }
372 .themedRow()
371373 }
372374
373375 Section {
@@ -382,6 +384,7 @@ struct LookupView: View {
382384 ProgressView()
383385 }
384386 }
387 .themedRow()
385388 }
386389
387390 if !vm.history.isEmpty {
@@ -403,11 +406,13 @@ struct LookupView: View {
403406 }
404407 .disabled(vm.isLooking)
405408 }
409 .themedRow()
406410
407411 Button("Clear History", role: .destructive) {
408412 vm.clearHistory()
409413 }
410414 .disabled(vm.isLooking)
415 .themedRow()
411416 }
412417 }
413418 }
Hutch/Views/Lookup/UserProfileView.swift +23 −1
@@ -2,6 +2,7 @@ import SwiftUI
22
33struct UserProfileView: View {
44 @Environment(AppState.self) private var appState
5 @Environment(\.isAMOLEDTheme) private var isAMOLED
56 @AppStorage(AppStorageKeys.contributionGraphsEnabled, store: .standard) private var contributionGraphsEnabled = true
67
78 let user: User
@@ -55,33 +56,41 @@ struct UserProfileView: View {
5556 }
5657 Spacer()
5758 }
58 .listRowBackground(Color.clear)
59 .listRowBackground(isAMOLED ? Color.black : Color.clear)
5960 }
6061 }
6162
6263 Section {
6364 LabeledContent("Username", value: user.username)
65 .themedRow()
6466 LabeledContent("Canonical Name", value: user.canonicalName)
67 .themedRow()
6568 if let userType = user.userType {
6669 LabeledContent("User Type", value: userType)
70 .themedRow()
6771 }
6872 if let pronouns = user.pronouns {
6973 LabeledContent("Pronouns", value: pronouns)
74 .themedRow()
7075 }
7176 if let suspensionNotice = user.suspensionNotice {
7277 LabeledContent("Suspension Notice", value: suspensionNotice)
78 .themedRow()
7379 }
7480 }
7581
7682 Section {
7783 LabeledContent("Email", value: user.email)
84 .themedRow()
7885 if let urlString = user.url, let url = URL(string: urlString) {
7986 LabeledContent("URL") {
8087 Link(urlString, destination: url)
8188 }
89 .themedRow()
8290 }
8391 if let location = user.location {
8492 LabeledContent("Location", value: location)
93 .themedRow()
8594 }
8695 }
8796
@@ -91,6 +100,7 @@ struct UserProfileView: View {
91100 .frame(maxWidth: .infinity, alignment: .leading)
92101 .tint(.accentColor)
93102 .textSelection(.enabled)
103 .themedRow()
94104 }
95105 }
96106
@@ -98,9 +108,11 @@ struct UserProfileView: View {
98108 Section {
99109 if let created = user.created {
100110 LabeledContent("Joined", value: formattedTimestamp(created))
111 .themedRow()
101112 }
102113 if let updated = user.updated {
103114 LabeledContent("Updated", value: formattedTimestamp(updated))
115 .themedRow()
104116 }
105117 }
106118 }
@@ -118,15 +130,18 @@ struct UserProfileView: View {
118130 error: viewModel.contributionsError ?? viewModel.contributionStatusText,
119131 isIndexedButEmpty: viewModel.isContributionActivityIndexedButEmpty
120132 )
133 .themedRow()
121134 }
122135 }
123136
124137 Section {
125138 if viewModel.isLoadingRepositories && viewModel.repositories.isEmpty {
126139 ProgressView()
140 .themedRow()
127141 } else if viewModel.repositories.isEmpty {
128142 Text("No public repositories.")
129143 .foregroundStyle(.secondary)
144 .themedRow()
130145 } else {
131146 ForEach(viewModel.repositories.prefix(4)) { repo in
132147 NavigationLink {
@@ -137,10 +152,12 @@ struct UserProfileView: View {
137152 RepositoryRowView(repository: repo, buildStatus: .none)
138153 }
139154 }
155 .themedRow()
140156 if viewModel.repositories.count > 4 {
141157 NavigationLink("See All") {
142158 UserRepositoriesView(viewModel: viewModel)
143159 }
160 .themedRow()
144161 }
145162 }
146163 } header: {
@@ -150,9 +167,11 @@ struct UserProfileView: View {
150167 Section {
151168 if viewModel.isLoadingTrackers && viewModel.trackers.isEmpty {
152169 ProgressView()
170 .themedRow()
153171 } else if viewModel.trackers.isEmpty {
154172 Text("No public trackers.")
155173 .foregroundStyle(.secondary)
174 .themedRow()
156175 } else {
157176 ForEach(viewModel.trackers.prefix(4)) { tracker in
158177 NavigationLink {
@@ -161,10 +180,12 @@ struct UserProfileView: View {
161180 UserProfileTrackerRowView(tracker: tracker)
162181 }
163182 }
183 .themedRow()
164184 if viewModel.trackers.count > 4 {
165185 NavigationLink("See All") {
166186 UserTrackersView(viewModel: viewModel)
167187 }
188 .themedRow()
168189 }
169190 }
170191 } header: {
@@ -172,6 +193,7 @@ struct UserProfileView: View {
172193 }
173194 }
174195 }
196 .themedList()
175197 .listStyle(.insetGrouped)
176198 .navigationTitle(user.canonicalName)
177199 .navigationBarTitleDisplayMode(.inline)
Hutch/Views/Lookup/UserRepositoriesView.swift +2
@@ -14,7 +14,9 @@ struct UserRepositoriesView: View {
1414 RepositoryRowView(repository: repo, buildStatus: .none)
1515 }
1616 }
17 .themedRow()
1718 }
19 .themedList()
1820 .listStyle(.plain)
1921 .navigationTitle("Repositories")
2022 .navigationBarTitleDisplayMode(.inline)
Hutch/Views/Lookup/UserTrackersView.swift +2
@@ -12,7 +12,9 @@ struct UserTrackersView: View {
1212 UserProfileTrackerRowView(tracker: tracker)
1313 }
1414 }
15 .themedRow()
1516 }
17 .themedList()
1618 .listStyle(.plain)
1719 .navigationTitle("Trackers")
1820 .navigationBarTitleDisplayMode(.inline)
Hutch/Views/More/AccountSwitcherView.swift +3
@@ -48,6 +48,7 @@ struct AccountSwitcherView: View {
4848 pendingRemoval = appState.accounts[index]
4949 }
5050 }
51 .themedRow()
5152 }
5253
5354 Section {
@@ -57,8 +58,10 @@ struct AccountSwitcherView: View {
5758 Label("Add Account", systemImage: "plus.circle")
5859 }
5960 .disabled(isSwitching)
61 .themedRow()
6062 }
6163 }
64 .themedList()
6265 .navigationTitle("Accounts")
6366 .navigationBarTitleDisplayMode(.inline)
6467 .toolbar {
Hutch/Views/More/AddAccountView.swift +3
@@ -15,6 +15,7 @@ struct AddAccountView: View {
1515 SecureField("Personal Access Token", text: $token)
1616 .autocorrectionDisabled()
1717 .textInputAutocapitalization(.never)
18 .themedRow()
1819 } footer: {
1920 Text("Generate a token at meta.sr.ht → OAuth2 clients.")
2021 }
@@ -23,9 +24,11 @@ struct AddAccountView: View {
2324 Section {
2425 Text(errorMessage)
2526 .foregroundStyle(.red)
27 .themedRow()
2628 }
2729 }
2830 }
31 .themedList()
2932 .navigationTitle("Add Account")
3033 .navigationBarTitleDisplayMode(.inline)
3134 .toolbar {
Hutch/Views/More/ManPageBrowserView.swift +2
@@ -25,8 +25,10 @@ struct ManPageBrowserView: View {
2525 Text(doc.title)
2626 }
2727 }
28 .themedRow()
2829 }
2930 }
31 .themedList()
3032 .navigationTitle("Man Pages")
3133 .navigationBarTitleDisplayMode(.inline)
3234 }
Hutch/Views/More/MoreView.swift +9
@@ -16,24 +16,29 @@ struct MoreView: View {
1616 NavigationLink(value: MoreRoute.lookup) {
1717 Label("Look Up", systemImage: "magnifyingglass")
1818 }
19 .themedRow()
1920 }
2021
2122 Section("Other Services") {
2223 NavigationLink(value: MoreRoute.projects) {
2324 Label("Projects", systemImage: "square.stack.3d.up")
2425 }
26 .themedRow()
2527
2628 NavigationLink(value: MoreRoute.lists) {
2729 Label("Mailing Lists", systemImage: "list.bullet.rectangle")
2830 }
31 .themedRow()
2932
3033 NavigationLink(value: MoreRoute.manPageBrowser) {
3134 Label("Man Pages", systemImage: "book")
3235 }
36 .themedRow()
3337
3438 NavigationLink(value: MoreRoute.pastes) {
3539 Label("Pastes", systemImage: "doc.on.clipboard")
3640 }
41 .themedRow()
3742
3843 NavigationLink(value: MoreRoute.systemStatus) {
3944 SystemStatusSummaryRow(
@@ -43,16 +48,19 @@ struct MoreView: View {
4348 isShowingStaleData: viewModel?.isShowingStaleSystemStatus ?? false
4449 )
4550 }
51 .themedRow()
4652 }
4753
4854 Section("Meta") {
4955 NavigationLink(value: MoreRoute.profile) {
5056 Label("Profile", systemImage: "person.text.rectangle")
5157 }
58 .themedRow()
5259
5360 NavigationLink(value: MoreRoute.settings) {
5461 Label("Settings", systemImage: "gear")
5562 }
63 .themedRow()
5664 }
5765
5866 Section {
@@ -61,6 +69,7 @@ struct MoreView: View {
6169 Label(item.title, systemImage: "safari")
6270 }
6371 }
72 .themedRow()
6473 } header: {
6574 Text("External Links")
6675 } footer: {
Hutch/Views/More/ProfileView.swift +29 −2
@@ -79,6 +79,7 @@ struct ProfileView: View {
7979 error: contributionViewModel.contributionsError ?? contributionViewModel.contributionStatusText,
8080 isIndexedButEmpty: contributionViewModel.isContributionActivityIndexedButEmpty
8181 )
82 .themedRow()
8283 }
8384 }
8485
@@ -185,6 +186,7 @@ struct ProfileView: View {
185186 }
186187 }
187188 .padding(.vertical, 4)
189 .themedRow()
188190
189191 if let bio = profile.bio, !bio.isEmpty {
190192 VStack(alignment: .leading, spacing: 2) {
@@ -193,36 +195,44 @@ struct ProfileView: View {
193195 .foregroundStyle(.secondary)
194196 ProfileBioView(markdown: bio)
195197 }
198 .themedRow()
196199 }
197200
198201 if let location = profile.location, !location.isEmpty {
199202 LabeledContent("Location", value: location)
203 .themedRow()
200204 }
201205
202206 if let url = profile.url, !url.isEmpty {
203207 LabeledContent("URL", value: url)
208 .themedRow()
204209 }
205210
206211 if let status = profile.paymentStatus {
207212 LabeledContent("Payment", value: status.capitalized)
213 .themedRow()
208214 }
209215
210216 if let sub = profile.subscription {
211217 if let status = sub.status {
212218 LabeledContent("Subscription", value: status.capitalized)
219 .themedRow()
213220 }
214221 if let interval = sub.interval {
215222 LabeledContent("Interval", value: interval.capitalized)
223 .themedRow()
216224 }
217225 }
218226
219227 Button("Edit Profile") {
220228 viewModel.isEditingProfile = true
221229 }
230 .themedRow()
222231
223232 SRHTShareButton(url: SRHTWebURL.profile(canonicalName: profile.canonicalName), target: .profile) {
224233 SwiftUI.Label("Share Profile", systemImage: "square.and.arrow.up")
225234 }
235 .themedRow()
226236 }
227237 }
228238
@@ -262,11 +272,13 @@ struct ProfileView: View {
262272 }
263273 }
264274 }
275 .themedRow()
265276
266277 if viewModel.isAddingSSHKey {
267278 TextField("Paste SSH public key", text: $vm.newSSHKey, axis: .vertical)
268279 .font(.caption.monospaced())
269280 .lineLimit(3...6)
281 .themedRow()
270282
271283 HStack {
272284 Button("Cancel") {
@@ -280,12 +292,14 @@ struct ProfileView: View {
280292 .buttonStyle(.borderedProminent)
281293 .disabled(viewModel.newSSHKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
282294 }
295 .themedRow()
283296 } else {
284297 Button {
285298 viewModel.isAddingSSHKey = true
286299 } label: {
287300 SwiftUI.Label("Add SSH Key", systemImage: "key")
288301 }
302 .themedRow()
289303 }
290304 } header: {
291305 Text("SSH Keys")
@@ -316,11 +330,13 @@ struct ProfileView: View {
316330 }
317331 }
318332 }
333 .themedRow()
319334
320335 if viewModel.isAddingPGPKey {
321336 TextField("Paste PGP public key", text: $vm.newPGPKey, axis: .vertical)
322337 .font(.caption.monospaced())
323338 .lineLimit(3...6)
339 .themedRow()
324340
325341 HStack {
326342 Button("Cancel") {
@@ -334,12 +350,14 @@ struct ProfileView: View {
334350 .buttonStyle(.borderedProminent)
335351 .disabled(viewModel.newPGPKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
336352 }
353 .themedRow()
337354 } else {
338355 Button {
339356 viewModel.isAddingPGPKey = true
340357 } label: {
341358 SwiftUI.Label("Add PGP Key", systemImage: "key.fill")
342359 }
360 .themedRow()
343361 }
344362 } header: {
345363 Text("PGP Keys")
@@ -357,10 +375,12 @@ struct ProfileView: View {
357375 ProgressView()
358376 Spacer()
359377 }
378 .themedRow()
360379 } else if viewModel.personalAccessTokens.isEmpty {
361380 Button("Load Tokens") {
362381 Task { await viewModel.loadPersonalAccessTokens() }
363382 }
383 .themedRow()
364384 } else {
365385 ForEach(viewModel.personalAccessTokens) { token in
366386 VStack(alignment: .leading, spacing: 4) {
@@ -390,6 +410,7 @@ struct ProfileView: View {
390410 }
391411 }
392412 }
413 .themedRow()
393414 }
394415 } header: {
395416 Text("Personal Access Tokens")
@@ -426,6 +447,7 @@ private struct EditProfileSheet: View {
426447 let profile: UserProfile
427448 let viewModel: SettingsViewModel
428449
450 @Environment(\.isAMOLEDTheme) private var isAMOLED
429451 @State private var email: String
430452 @State private var url: String
431453 @State private var location: String
@@ -492,7 +514,7 @@ private struct EditProfileSheet: View {
492514 }
493515 Spacer()
494516 }
495 .listRowBackground(Color.clear)
517 .listRowBackground(isAMOLED ? Color.black : Color.clear)
496518
497519 if profile.avatar != nil || avatarPreview != nil {
498520 HStack {
@@ -506,7 +528,7 @@ private struct EditProfileSheet: View {
506528 .disabled(viewModel.isUploadingAvatar)
507529 Spacer()
508530 }
509 .listRowBackground(Color.clear)
531 .listRowBackground(isAMOLED ? Color.black : Color.clear)
510532 .listRowSeparator(.hidden)
511533 }
512534 }
@@ -522,6 +544,7 @@ private struct EditProfileSheet: View {
522544 .autocorrectionDisabled()
523545 .textInputAutocapitalization(.never)
524546 }
547 .themedRow()
525548
526549 VStack(alignment: .leading, spacing: 4) {
527550 Text("URL")
@@ -533,6 +556,7 @@ private struct EditProfileSheet: View {
533556 .autocorrectionDisabled()
534557 .textInputAutocapitalization(.never)
535558 }
559 .themedRow()
536560
537561 VStack(alignment: .leading, spacing: 4) {
538562 Text("Location")
@@ -540,6 +564,7 @@ private struct EditProfileSheet: View {
540564 .foregroundStyle(.secondary)
541565 TextField("Enter location", text: $location)
542566 }
567 .themedRow()
543568
544569 VStack(alignment: .leading, spacing: 4) {
545570 Text("Bio")
@@ -548,8 +573,10 @@ private struct EditProfileSheet: View {
548573 TextField("Enter bio", text: $bio, axis: .vertical)
549574 .lineLimit(3...6)
550575 }
576 .themedRow()
551577 }
552578 }
579 .themedList()
553580 .navigationTitle("Edit Profile")
554581 .navigationBarTitleDisplayMode(.inline)
555582 .onChange(of: selectedPhoto) { _, newItem in
Hutch/Views/Pastes/PasteDetailView.swift +8
@@ -326,13 +326,18 @@ private struct PasteInfoSheet: View {
326326 List {
327327 Section("Paste") {
328328 LabeledContent("ID", value: paste.id)
329 .themedRow()
329330 LabeledContent("Owner", value: paste.user.canonicalName)
331 .themedRow()
330332 LabeledContent("Created", value: paste.created.relativeDescription)
333 .themedRow()
331334 LabeledContent("Visibility") {
332335 VisibilityBadge(visibility: paste.visibility)
333336 }
337 .themedRow()
334338 if paste.files.count > 1 {
335339 LabeledContent("Files", value: "\(paste.files.count)")
340 .themedRow()
336341 }
337342 }
338343
@@ -340,6 +345,7 @@ private struct PasteInfoSheet: View {
340345 Section("File") {
341346 if let filename = file.filename, !filename.isEmpty {
342347 LabeledContent("Filename", value: filename)
348 .themedRow()
343349 }
344350 LabeledContent("Hash") {
345351 Text(file.hash)
@@ -347,6 +353,7 @@ private struct PasteInfoSheet: View {
347353 .foregroundStyle(.secondary)
348354 .textSelection(.enabled)
349355 }
356 .themedRow()
350357 }
351358 }
352359 }
@@ -407,6 +414,7 @@ private struct PasteVisibilitySheet: View {
407414 }
408415 .buttonStyle(.plain)
409416 }
417 .themedRow()
410418 }
411419 .listStyle(.insetGrouped)
412420 .navigationTitle("Visibility")
Hutch/Views/Pastes/PasteListView.swift +8
@@ -101,6 +101,7 @@ struct PasteListView: View {
101101 await viewModel.loadMoreIfNeeded(currentItem: paste)
102102 }
103103 }
104 .themedRow()
104105
105106 if viewModel.isLoadingMore {
106107 HStack {
@@ -109,6 +110,7 @@ struct PasteListView: View {
109110 Spacer()
110111 }
111112 .listRowSeparator(.hidden)
113 .themedRow()
112114 }
113115 }
114116 .themedList()
@@ -302,12 +304,14 @@ private struct CreatePasteSheet: View {
302304 files = [PasteUploadDraft()]
303305 }
304306 }
307 .themedRow()
305308
306309 Button {
307310 files.append(PasteUploadDraft())
308311 } label: {
309312 Label("Add File", systemImage: "plus")
310313 }
314 .themedRow()
311315 }
312316
313317 Section("Visibility") {
@@ -316,21 +320,25 @@ private struct CreatePasteSheet: View {
316320 Text("Unlisted").tag(Visibility.unlisted)
317321 Text("Private").tag(Visibility.private)
318322 }
323 .themedRow()
319324 }
320325
321326 Section {
322327 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.")
323328 .font(.footnote)
324329 .foregroundStyle(.secondary)
330 .themedRow()
325331 }
326332
327333 if let error = viewModel.error {
328334 Section {
329335 Label(error, systemImage: "exclamationmark.triangle.fill")
330336 .foregroundStyle(.red)
337 .themedRow()
331338 }
332339 }
333340 }
341 .themedList()
334342 .navigationTitle("New Paste")
335343 .navigationBarTitleDisplayMode(.inline)
336344 .toolbar {
Hutch/Views/Projects/ProjectDetailView.swift +6
@@ -103,6 +103,7 @@ struct ProjectDetailView: View {
103103 }
104104 }
105105 .padding(.vertical, 4)
106 .themedRow()
106107 }
107108 }
108109
@@ -127,6 +128,7 @@ struct ProjectDetailView: View {
127128 }
128129 .buttonStyle(.plain)
129130 }
131 .themedRow()
130132 }
131133 }
132134 }
@@ -155,6 +157,7 @@ struct ProjectDetailView: View {
155157 }
156158 .buttonStyle(.plain)
157159 }
160 .themedRow()
158161 }
159162 }
160163 }
@@ -183,6 +186,7 @@ struct ProjectDetailView: View {
183186 }
184187 .buttonStyle(.plain)
185188 }
189 .themedRow()
186190 }
187191 }
188192 }
@@ -205,6 +209,7 @@ struct ProjectDetailView: View {
205209 }
206210 .buttonStyle(.plain)
207211 }
212 .themedRow()
208213 }
209214 }
210215 }
@@ -218,6 +223,7 @@ struct ProjectDetailView: View {
218223 systemImage: "square.stack.3d.up.slash",
219224 description: Text("This project doesn’t currently expose repositories, trackers, mailing lists, or external links.")
220225 )
226 .themedRow()
221227 }
222228 }
223229 }
Hutch/Views/Projects/ProjectMailingListView.swift +2
@@ -332,7 +332,9 @@ struct MailingListDetailView: View {
332332 .tint(thread.isUnread ? .blue : .gray)
333333 }
334334 }
335 .themedRow()
335336 }
337 .themedList()
336338 .listStyle(.plain)
337339 .searchable(
338340 text: $vm.searchText,
Hutch/Views/Projects/ProjectsListView.swift +1
@@ -79,6 +79,7 @@ struct ProjectsListView: View {
7979 .buttonStyle(.plain)
8080 .alignmentGuide(.listRowSeparatorLeading) { _ in 0 }
8181 }
82 .themedRow()
8283 }
8384 .themedList()
8485 .listStyle(.plain)
Hutch/Views/Repositories/ArtifactsView.swift +2
@@ -13,9 +13,11 @@ struct ArtifactsView: View {
1313 openURL(artifact.url)
1414 }
1515 }
16 .themedRow()
1617 }
1718 }
1819 }
20 .themedList()
1921 .listStyle(.insetGrouped)
2022 .overlay {
2123 if viewModel.isLoadingArtifacts, viewModel.referenceArtifacts.isEmpty {
Hutch/Views/Repositories/CommitLogView.swift +3
@@ -13,6 +13,7 @@ struct CommitLogView: View {
1313 await viewModel.loadMoreCommitsIfNeeded(currentItem: commit)
1414 }
1515 }
16 .themedRow()
1617
1718 if viewModel.isLoadingMoreCommits {
1819 HStack {
@@ -21,8 +22,10 @@ struct CommitLogView: View {
2122 Spacer()
2223 }
2324 .listRowSeparator(.hidden)
25 .themedRow()
2426 }
2527 }
28 .themedList()
2629 .listStyle(.plain)
2730 .overlay {
2831 if viewModel.isLoadingCommits, viewModel.commits.isEmpty {
Hutch/Views/Repositories/FileTreeView.swift +6
@@ -233,7 +233,9 @@ private struct FileTreeContentView: View {
233233 await viewModel.navigateInto(entry: entry)
234234 }
235235 }
236 .themedRow()
236237 }
238 .themedList()
237239 .listStyle(.plain)
238240 .refreshable {
239241 await viewModel.loadRootTree()
@@ -848,6 +850,7 @@ private struct RefPickerSheet: View {
848850 )
849851 }
850852 .buttonStyle(.plain)
853 .themedRow()
851854 }
852855
853856 if !viewModel.branches.isEmpty {
@@ -868,6 +871,7 @@ private struct RefPickerSheet: View {
868871 }
869872 .buttonStyle(.plain)
870873 }
874 .themedRow()
871875 }
872876 }
873877
@@ -889,9 +893,11 @@ private struct RefPickerSheet: View {
889893 }
890894 .buttonStyle(.plain)
891895 }
896 .themedRow()
892897 }
893898 }
894899 }
900 .themedList()
895901 .listStyle(.insetGrouped)
896902 .navigationTitle("Select Ref")
897903 .navigationBarTitleDisplayMode(.inline)
Hutch/Views/Repositories/HgRepositoryDetailView.swift +12
@@ -396,7 +396,9 @@ struct HgRepositoryDetailView: View {
396396 .onTapGesture {
397397 Task { await viewModel.openFile(file) }
398398 }
399 .themedRow()
399400 }
401 .themedList()
400402 .listStyle(.plain)
401403 }
402404 }
@@ -454,6 +456,7 @@ struct HgRepositoryDetailView: View {
454456 await viewModel.loadMoreLogIfNeeded(currentItem: revision)
455457 }
456458 }
459 .themedRow()
457460
458461 if viewModel.isLoadingMoreLog {
459462 HStack {
@@ -462,8 +465,10 @@ struct HgRepositoryDetailView: View {
462465 Spacer()
463466 }
464467 .listRowSeparator(.hidden)
468 .themedRow()
465469 }
466470 }
471 .themedList()
467472 .listStyle(.plain)
468473 .overlay {
469474 if viewModel.isLoadingLog, viewModel.log.isEmpty {
@@ -498,7 +503,9 @@ struct HgRepositoryDetailView: View {
498503 } else {
499504 List(revisions) { revision in
500505 namedRevisionRow(revision)
506 .themedRow()
501507 }
508 .themedList()
502509 .listStyle(.plain)
503510 }
504511 }
@@ -699,6 +706,7 @@ private struct HgBrowseRefPickerSheet: View {
699706 )
700707 }
701708 .buttonStyle(.plain)
709 .themedRow()
702710 }
703711
704712 if !viewModel.branches.isEmpty {
@@ -719,6 +727,7 @@ private struct HgBrowseRefPickerSheet: View {
719727 }
720728 .buttonStyle(.plain)
721729 }
730 .themedRow()
722731 }
723732 }
724733
@@ -740,6 +749,7 @@ private struct HgBrowseRefPickerSheet: View {
740749 }
741750 .buttonStyle(.plain)
742751 }
752 .themedRow()
743753 }
744754 }
745755
@@ -761,9 +771,11 @@ private struct HgBrowseRefPickerSheet: View {
761771 }
762772 .buttonStyle(.plain)
763773 }
774 .themedRow()
764775 }
765776 }
766777 }
778 .themedList()
767779 .listStyle(.insetGrouped)
768780 .navigationTitle("Select Ref")
769781 .navigationBarTitleDisplayMode(.inline)
Hutch/Views/Repositories/HgRepositorySettingsView.swift +19
@@ -49,6 +49,7 @@ struct HgRepositorySettingsView: View {
4949 histeditSection(viewModel)
5050 deleteSection(viewModel)
5151 }
52 .themedList()
5253 .srhtErrorBanner(error: $vm.error)
5354 .alert(
5455 "Permanently delete \(repository.owner.canonicalName)/\(repository.name)?",
@@ -101,25 +102,30 @@ struct HgRepositorySettingsView: View {
101102 Text("\(repository.owner.canonicalName)/\(repository.name)")
102103 .font(.body.monospaced())
103104 }
105 .themedRow()
104106
105107 LabeledContent("Forge") {
106108 Text(repositoryForgeLabel(repository.service))
107109 }
110 .themedRow()
108111
109112 LabeledContent("Visibility") {
110113 Text(repositoryVisibilityLabel(viewModel.editedVisibility))
111114 }
115 .themedRow()
112116 }
113117
114118 Section("Repository Details") {
115119 TextField("Description", text: Bindable(viewModel).editedDescription, axis: .vertical)
116120 .lineLimit(3...6)
121 .themedRow()
117122
118123 Picker("Visibility", selection: Bindable(viewModel).editedVisibility) {
119124 Text("Public").tag(Visibility.public)
120125 Text("Unlisted").tag(Visibility.unlisted)
121126 Text("Private").tag(Visibility.private)
122127 }
128 .themedRow()
123129
124130 Button {
125131 Task {
@@ -135,6 +141,7 @@ struct HgRepositorySettingsView: View {
135141 }
136142 }
137143 .disabled(viewModel.isSavingInfo || !viewModel.isInfoDirty)
144 .themedRow()
138145 }
139146 }
140147
@@ -147,9 +154,11 @@ struct HgRepositorySettingsView: View {
147154 ProgressView()
148155 Spacer()
149156 }
157 .themedRow()
150158 } else if viewModel.acls.isEmpty {
151159 Text("No access entries yet.")
152160 .foregroundStyle(.secondary)
161 .themedRow()
153162 } else {
154163 ForEach(viewModel.acls) { entry in
155164 HStack {
@@ -167,6 +176,7 @@ struct HgRepositorySettingsView: View {
167176 }
168177 }
169178 }
179 .themedRow()
170180 }
171181
172182 HStack {
@@ -192,9 +202,11 @@ struct HgRepositorySettingsView: View {
192202 }
193203 .disabled(viewModel.isAddingACL || viewModel.newACLEntity.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
194204 }
205 .themedRow()
195206 Text("Add a SourceHut user and choose read-only or read/write access.")
196207 .font(.caption)
197208 .foregroundStyle(.secondary)
209 .themedRow()
198210 }
199211 }
200212
@@ -202,9 +214,11 @@ struct HgRepositorySettingsView: View {
202214 private func featuresSection(_ viewModel: HgRepositorySettingsViewModel) -> some View {
203215 Section("Sensitive Settings") {
204216 Toggle("Hide this repository from public listings", isOn: Bindable(viewModel).editedNonPublishing)
217 .themedRow()
205218 Text("Changes stay pending until you save this section.")
206219 .font(.caption)
207220 .foregroundStyle(.secondary)
221 .themedRow()
208222
209223 Button {
210224 Task {
@@ -220,6 +234,7 @@ struct HgRepositorySettingsView: View {
220234 }
221235 }
222236 .disabled(viewModel.isSavingInfo || !viewModel.isInfoDirty)
237 .themedRow()
223238 }
224239 }
225240
@@ -230,16 +245,19 @@ struct HgRepositorySettingsView: View {
230245 .autocorrectionDisabled()
231246 .textInputAutocapitalization(.never)
232247 .disabled(true)
248 .themedRow()
233249
234250 Text("Removing revisions is not available through the public hg.sr.ht API, so Hutch can’t do this yet.")
235251 .font(.caption)
236252 .foregroundStyle(.secondary)
253 .themedRow()
237254
238255 Button("Remove Revision", role: .destructive) {
239256 // Not implemented: the hg.sr.ht API does not expose a histedit endpoint.
240257 // This button is disabled until the API supports revision removal.
241258 }
242259 .disabled(true)
260 .themedRow()
243261 }
244262 }
245263
@@ -258,6 +276,7 @@ struct HgRepositorySettingsView: View {
258276 }
259277 }
260278 .disabled(viewModel.isDeleting)
279 .themedRow()
261280 } header: {
262281 Text("Danger Zone")
263282 }
Hutch/Views/Repositories/ReferencesListView.swift +3
@@ -10,6 +10,7 @@ struct ReferencesListView: View {
1010 ForEach(viewModel.branches, id: \.name) { ref in
1111 ReferenceRow(reference: ref, prefix: "refs/heads/")
1212 }
13 .themedRow()
1314 }
1415 }
1516
@@ -18,9 +19,11 @@ struct ReferencesListView: View {
1819 ForEach(viewModel.tags, id: \.name) { ref in
1920 ReferenceRow(reference: ref, prefix: "refs/tags/")
2021 }
22 .themedRow()
2123 }
2224 }
2325 }
26 .themedList()
2427 .listStyle(.insetGrouped)
2528 .overlay {
2629 if viewModel.isLoadingRefs, viewModel.branches.isEmpty, viewModel.tags.isEmpty {
Hutch/Views/Repositories/RepositoryACLView.swift +8 −1
@@ -6,6 +6,7 @@ struct RepositoryACLView: View {
66 let showsDoneButton: Bool
77
88 @Environment(\.dismiss) private var dismiss
9 @Environment(\.isAMOLEDTheme) private var isAMOLED
910 @State private var viewModel: RepositoryACLViewModel?
1011 @State private var pendingDeletion: RepositoryACLEntry?
1112 @State private var showAddSheet = false
@@ -70,7 +71,7 @@ struct RepositoryACLView: View {
7071 Text("Only the repository owner currently has access.")
7172 }
7273 .frame(maxWidth: .infinity)
73 .listRowBackground(Color.clear)
74 .listRowBackground(isAMOLED ? Color.black : Color.clear)
7475 } else {
7576 Section {
7677 ForEach(viewModel.visibleEntries) { entry in
@@ -86,9 +87,11 @@ struct RepositoryACLView: View {
8687 }
8788 )
8889 }
90 .themedRow()
8991 }
9092 }
9193 }
94 .themedList()
9295 .listStyle(.insetGrouped)
9396 .refreshable {
9497 await viewModel.load()
@@ -192,11 +195,13 @@ private struct RepositoryACLAddUserView: View {
192195 TextField("Username or ~username", text: $viewModel.addUsername)
193196 .autocorrectionDisabled()
194197 .textInputAutocapitalization(.never)
198 .themedRow()
195199
196200 if let validation = inlineValidationMessage {
197201 Text(validation)
198202 .font(.caption)
199203 .foregroundStyle(.secondary)
204 .themedRow()
200205 }
201206 }
202207
@@ -207,8 +212,10 @@ private struct RepositoryACLAddUserView: View {
207212 }
208213 }
209214 .pickerStyle(.segmented)
215 .themedRow()
210216 }
211217 }
218 .themedList()
212219 .navigationTitle("Add User")
213220 .navigationBarTitleDisplayMode(.inline)
214221 .toolbar {
Hutch/Views/Repositories/RepositoryListView.swift +11
@@ -78,6 +78,7 @@ struct RepositoryListView: View {
7878 .foregroundStyle(.secondary)
7979 }
8080 .listRowSeparator(.hidden)
81 .themedRow()
8182 }
8283
8384 ForEach(viewModel.repositories) { repo in
@@ -89,6 +90,7 @@ struct RepositoryListView: View {
8990 }
9091 .alignmentGuide(.listRowSeparatorLeading) { _ in 0 }
9192 }
93 .themedRow()
9294
9395 if viewModel.isLoadingMore {
9496 HStack {
@@ -97,6 +99,7 @@ struct RepositoryListView: View {
9799 Spacer()
98100 }
99101 .listRowSeparator(.hidden)
102 .themedRow()
100103 }
101104 }
102105 .themedList()
@@ -223,16 +226,20 @@ private struct CreateRepositorySheet: View {
223226 Text(service.displayName).tag(service)
224227 }
225228 }
229 .themedRow()
226230 TextField("Repository name", text: $name)
227231 .textInputAutocapitalization(.never)
228232 .autocorrectionDisabled()
233 .themedRow()
229234 TextField("Short description (optional)", text: $description, axis: .vertical)
230235 .lineLimit(2...4)
236 .themedRow()
231237 Picker("Visibility", selection: $visibility) {
232238 Text("Public").tag(Visibility.public)
233239 Text("Unlisted").tag(Visibility.unlisted)
234240 Text("Private").tag(Visibility.private)
235241 }
242 .themedRow()
236243 }
237244
238245 Section("Import Existing Repository") {
@@ -241,16 +248,20 @@ private struct CreateRepositorySheet: View {
241248 .textInputAutocapitalization(.never)
242249 .autocorrectionDisabled()
243250 .keyboardType(.URL)
251 .themedRow()
244252 Text("Import an existing Git repository from a remote URL.")
245253 .font(.footnote)
246254 .foregroundStyle(.secondary)
255 .themedRow()
247256 } else {
248257 Text("Importing a Mercurial repository from a remote URL is not available through the public API.")
249258 .font(.footnote)
250259 .foregroundStyle(.secondary)
260 .themedRow()
251261 }
252262 }
253263 }
264 .themedList()
254265 .navigationTitle(service == .git ? "New Git Repository" : "New Mercurial Repository")
255266 .navigationBarTitleDisplayMode(.inline)
256267 .toolbar {
Hutch/Views/Repositories/RepositorySettingsView.swift +19
@@ -51,6 +51,7 @@ struct RepositorySettingsView: View {
5151 visibilitySection(viewModel)
5252 deleteSection(viewModel)
5353 }
54 .themedList()
5455 .srhtErrorBanner(error: $vm.error)
5556 .alert(
5657 visibilityConfirmationTitle(for: viewModel),
@@ -97,15 +98,18 @@ struct RepositorySettingsView: View {
9798 Text("\(viewModel.repository.owner.canonicalName)/\(viewModel.repository.name)")
9899 .font(.body.monospaced())
99100 }
101 .themedRow()
100102
101103 LabeledContent("Default Branch") {
102104 Text(viewModel.currentDefaultBranchName)
103105 .font(.body.monospaced())
104106 }
107 .themedRow()
105108
106109 LabeledContent("Visibility") {
107110 Text(repositoryVisibilityLabel(viewModel.repository.visibility))
108111 }
112 .themedRow()
109113 }
110114 }
111115
@@ -115,22 +119,27 @@ struct RepositorySettingsView: View {
115119 TextField("Repository name", text: Bindable(viewModel).editedName)
116120 .autocorrectionDisabled()
117121 .textInputAutocapitalization(.never)
122 .themedRow()
118123
119124 TextField("Description", text: Bindable(viewModel).editedDescription, axis: .vertical)
120125 .lineLimit(2...4)
126 .themedRow()
121127
122128 if let metadataValidationMessage = viewModel.metadataValidationMessage {
123129 Text(metadataValidationMessage)
124130 .font(.caption)
125131 .foregroundStyle(.red)
132 .themedRow()
126133 } else if viewModel.normalizedEditedName != viewModel.repository.name {
127134 Text("Changing the repository name updates the repository URL.")
128135 .font(.caption)
129136 .foregroundStyle(.secondary)
137 .themedRow()
130138 } else {
131139 Text("Name and description stay pending until you save this section.")
132140 .font(.caption)
133141 .foregroundStyle(.secondary)
142 .themedRow()
134143 }
135144
136145 Button {
@@ -153,6 +162,7 @@ struct RepositorySettingsView: View {
153162 !viewModel.isMetadataDirty ||
154163 viewModel.metadataValidationMessage != nil
155164 )
165 .themedRow()
156166 } header: {
157167 Text("Repository Details")
158168 }
@@ -165,10 +175,12 @@ struct RepositorySettingsView: View {
165175 Text(viewModel.currentDefaultBranchName)
166176 .font(.body.monospaced())
167177 }
178 .themedRow()
168179
169180 if viewModel.branches.isEmpty {
170181 Text("This repository doesn't have any branches yet.")
171182 .foregroundStyle(.secondary)
183 .themedRow()
172184 } else {
173185 Picker("Branch", selection: Bindable(viewModel).editedHead) {
174186 ForEach(viewModel.availableBranchNames, id: \.self) { branch in
@@ -177,10 +189,12 @@ struct RepositorySettingsView: View {
177189 .tag(branch)
178190 }
179191 }
192 .themedRow()
180193
181194 Text("Changes stay pending until you set the new default branch.")
182195 .font(.caption)
183196 .foregroundStyle(.secondary)
197 .themedRow()
184198
185199 Button {
186200 Task {
@@ -202,6 +216,7 @@ struct RepositorySettingsView: View {
202216 !viewModel.isDefaultBranchDirty ||
203217 viewModel.defaultBranchValidationMessage != nil
204218 )
219 .themedRow()
205220 }
206221 } header: {
207222 Text("Default Branch")
@@ -216,10 +231,12 @@ struct RepositorySettingsView: View {
216231 Text("Unlisted").tag(Visibility.unlisted)
217232 Text("Private").tag(Visibility.private)
218233 }
234 .themedRow()
219235
220236 Text("Visibility changes apply immediately after you confirm them.")
221237 .font(.caption)
222238 .foregroundStyle(.secondary)
239 .themedRow()
223240
224241 Button {
225242 showVisibilityConfirmation = true
@@ -233,6 +250,7 @@ struct RepositorySettingsView: View {
233250 }
234251 }
235252 .disabled(viewModel.isMutating || !viewModel.isVisibilityDirty)
253 .themedRow()
236254 } header: {
237255 Text("Sensitive Settings")
238256 }
@@ -253,6 +271,7 @@ struct RepositorySettingsView: View {
253271 }
254272 }
255273 .disabled(viewModel.isMutating)
274 .themedRow()
256275 } header: {
257276 Text("Danger Zone")
258277 }
Hutch/Views/Settings/SettingsView.swift +23
@@ -60,11 +60,13 @@ struct SettingsView: View {
6060 Text(theme.label).tag(theme)
6161 }
6262 }
63 .themedRow()
6364 Picker("Density", selection: $displayDensity) {
6465 ForEach(DisplayDensity.allCases) { density in
6566 Text(density.label).tag(density)
6667 }
6768 }
69 .themedRow()
6870 } header: {
6971 Text("Appearance")
7072 } footer: {
@@ -76,10 +78,12 @@ struct SettingsView: View {
7678 private func behaviorSection() -> some View {
7779 Section {
7880 Toggle("Swipe actions", isOn: $swipeActionsEnabled)
81 .themedRow()
7982 Toggle("Contribution graphs", isOn: $contributionGraphsEnabled)
8083 .onChange(of: contributionGraphsEnabled) { _, newValue in
8184 ContributionWidgetContextStore.setEnabled(newValue)
8285 }
86 .themedRow()
8387 } header: {
8488 Text("Behavior")
8589 } footer: {
@@ -101,12 +105,14 @@ struct SettingsView: View {
101105 }
102106 }
103107 .alignmentGuide(.listRowSeparatorLeading) { _ in 0 }
108 .themedRow()
104109
105110 Button {
106111 showAccountSwitcher = true
107112 } label: {
108113 Label("Manage Accounts", systemImage: "person.2")
109114 }
115 .themedRow()
110116
111117 HStack {
112118 Image(systemName: "lock.shield")
@@ -116,14 +122,17 @@ struct SettingsView: View {
116122 .foregroundStyle(.secondary)
117123 }
118124 .alignmentGuide(.listRowSeparatorLeading) { _ in 0 }
125 .themedRow()
119126
120127 Button("Reset App Data", role: .destructive) {
121128 pendingDestructiveAction = .resetAppData
122129 }
130 .themedRow()
123131
124132 Button("Sign Out", role: .destructive) {
125133 pendingDestructiveAction = .signOut
126134 }
135 .themedRow()
127136 } header: {
128137 Text("Authentication")
129138 } footer: {
@@ -139,6 +148,7 @@ struct SettingsView: View {
139148 } label: {
140149 SwiftUI.Label("About Hutch", systemImage: "info.circle")
141150 }
151 .themedRow()
142152 }
143153 }
144154}
@@ -211,15 +221,18 @@ private struct AboutView: View {
211221 .foregroundStyle(.secondary)
212222 }
213223 .padding(.vertical, 4)
224 .themedRow()
214225
215226 LabeledContent("Version", value: version)
216227 .onTapGesture {
217228 developerRevealCount = min(developerRevealCount + 1, 5)
218229 }
230 .themedRow()
219231 LabeledContent("Build", value: build)
220232 .onTapGesture {
221233 developerRevealCount = min(developerRevealCount + 1, 5)
222234 }
235 .themedRow()
223236 } footer: {
224237 Text(developerRevealFooterText)
225238 }
@@ -228,34 +241,41 @@ private struct AboutView: View {
228241 Link(destination: URL(string: "https://sr.ht")!) {
229242 SwiftUI.Label("SourceHut", systemImage: "link")
230243 }
244 .themedRow()
231245 Link(destination: URL(string: "https://man.sr.ht")!) {
232246 SwiftUI.Label("SourceHut Manuals", systemImage: "book")
233247 }
248 .themedRow()
234249 Link(destination: URL(string: "https://sr.ht/~ccleberg/Hutch")!) {
235250 SwiftUI.Label("Project Repository", systemImage: "folder")
236251 }
252 .themedRow()
237253 }
238254
239255 Section("Support") {
240256 Link(destination: URL(string: "mailto:hello@cleberg.net")!) {
241257 SwiftUI.Label("Email Support", systemImage: "envelope")
242258 }
259 .themedRow()
243260 }
244261
245262 Section("Privacy") {
246263 Text("Hutch uses your SourceHut personal access token to make requests on your behalf. The token is stored locally in the iOS keychain.")
247264 .font(.subheadline)
248265 .foregroundStyle(.secondary)
266 .themedRow()
249267
250268 Link(destination: URL(string: "https://zerolabs.sh/hutch/privacy-policy/")!) {
251269 SwiftUI.Label("Privacy Policy", systemImage: "hand.raised")
252270 }
271 .themedRow()
253272 }
254273
255274 Section("Acknowledgements") {
256275 Text("Built for SourceHut users who want quick access to repositories, builds, and tickets on iOS.")
257276 .font(.subheadline)
258277 .foregroundStyle(.secondary)
278 .themedRow()
259279 }
260280
261281 if developerToolsVisible {
@@ -264,18 +284,21 @@ private struct AboutView: View {
264284 get: { appState.isDebugModeEnabled },
265285 set: { appState.isDebugModeEnabled = $0 }
266286 ))
287 .themedRow()
267288
268289 NavigationLink {
269290 HomePrototypeView()
270291 } label: {
271292 SwiftUI.Label("Home Prototype", systemImage: "house")
272293 }
294 .themedRow()
273295
274296 NavigationLink {
275297 WorkPrototypeView()
276298 } label: {
277299 SwiftUI.Label("Work Prototype", systemImage: "tray.full")
278300 }
301 .themedRow()
279302 } header: {
280303 Text("Developer")
281304 } footer: {
Hutch/Views/SystemStatus/SystemStatusView.swift +7
@@ -36,6 +36,7 @@ struct SystemStatusView: View {
3636 Label(staleDataMessage, systemImage: "clock.arrow.trianglehead.counterclockwise.rotate.90")
3737 .font(.subheadline)
3838 .foregroundStyle(.secondary)
39 .themedRow()
3940 }
4041 }
4142
@@ -47,6 +48,7 @@ struct SystemStatusView: View {
4748
4849 recentIncidentsSection(viewModel.recentIncidents)
4950 }
51 .themedList()
5052 .listStyle(.insetGrouped)
5153 .refreshable {
5254 await viewModel.load(forceRefresh: true)
@@ -99,6 +101,7 @@ struct SystemStatusView: View {
99101 .font(.subheadline.weight(.medium))
100102 }
101103 .padding(.vertical, 4)
104 .themedRow()
102105 }
103106 }
104107
@@ -119,6 +122,7 @@ struct SystemStatusView: View {
119122 }
120123 .padding(.vertical, 2)
121124 }
125 .themedRow()
122126 }
123127 }
124128
@@ -129,6 +133,7 @@ struct SystemStatusView: View {
129133 ForEach(incidents) { incident in
130134 incidentRow(incident)
131135 }
136 .themedRow()
132137 }
133138 }
134139 }
@@ -142,10 +147,12 @@ struct SystemStatusView: View {
142147 systemImage: "clock.arrow.trianglehead.counterclockwise.rotate.90",
143148 description: Text("The status feed didn’t return any recent incidents.")
144149 )
150 .themedRow()
145151 } else {
146152 ForEach(incidents) { incident in
147153 incidentRow(incident)
148154 }
155 .themedRow()
149156 }
150157 }
151158 }
Hutch/Views/Tickets/TicketDetailView.swift +13
@@ -739,8 +739,10 @@ private struct ResolveSheet: View {
739739 }
740740 .pickerStyle(.inline)
741741 .labelsHidden()
742 .themedRow()
742743 }
743744 }
745 .themedList()
744746 .navigationTitle("Resolve Ticket")
745747 .navigationBarTitleDisplayMode(.inline)
746748 .toolbar {
@@ -801,6 +803,7 @@ private struct AssignSheet: View {
801803 .buttonStyle(.plain)
802804 }
803805 }
806 .themedRow()
804807 }
805808 }
806809
@@ -809,6 +812,7 @@ private struct AssignSheet: View {
809812 .textContentType(.username)
810813 .autocorrectionDisabled()
811814 .textInputAutocapitalization(.never)
815 .themedRow()
812816
813817 Button("Add Assignee") {
814818 let name = username.trimmingCharacters(in: .whitespacesAndNewlines)
@@ -822,8 +826,10 @@ private struct AssignSheet: View {
822826 username.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
823827 || viewModel.isPerformingAction
824828 )
829 .themedRow()
825830 }
826831 }
832 .themedList()
827833 .navigationTitle("Assignees")
828834 .navigationBarTitleDisplayMode(.inline)
829835 .toolbar {
@@ -877,7 +883,9 @@ private struct LabelsSheet: View {
877883 }
878884 }
879885 }
886 .themedRow()
880887 }
888 .themedList()
881889 }
882890 }
883891 .navigationTitle("Labels")
@@ -917,11 +925,14 @@ private struct CreateLabelSheet: View {
917925 Section("Label Details") {
918926 TextField("Label name", text: $labelName)
919927 .autocorrectionDisabled()
928 .themedRow()
920929 }
921930
922931 Section("Colors") {
923932 ColorPicker("Background color", selection: $backgroundColor, supportsOpacity: false)
933 .themedRow()
924934 ColorPicker("Text color", selection: $foregroundColor, supportsOpacity: false)
935 .themedRow()
925936 }
926937
927938 Section("Preview") {
@@ -936,8 +947,10 @@ private struct CreateLabelSheet: View {
936947 .clipShape(Capsule())
937948 Spacer()
938949 }
950 .themedRow()
939951 }
940952 }
953 .themedList()
941954 .navigationTitle("New Label")
942955 .navigationBarTitleDisplayMode(.inline)
943956 .toolbar {
Hutch/Views/Tickets/TicketListView.swift +22 −1
@@ -6,6 +6,7 @@ struct TicketListView: View {
66
77 @AppStorage(AppStorageKeys.swipeActionsEnabled, store: .standard) private var swipeActionsEnabled = true
88 @Environment(AppState.self) private var appState
9 @Environment(\.isAMOLEDTheme) private var isAMOLED
910 @Environment(\.dismiss) private var dismiss
1011 @Environment(\.openURL) private var openURL
1112 @State private var tracker: TrackerSummary
@@ -277,7 +278,7 @@ struct TicketListView: View {
277278 vm.deleteSavedFilter(savedFilter)
278279 }
279280 .listRowInsets(EdgeInsets())
280 .listRowBackground(Color.clear)
281 .listRowBackground(isAMOLED ? Color.black : Color.clear)
281282 .listRowSeparator(.hidden)
282283
283284 ForEach(viewModel.filteredTickets) { ticket in
@@ -344,6 +345,7 @@ struct TicketListView: View {
344345 await viewModel.loadMoreIfNeeded(currentItem: ticket)
345346 }
346347 }
348 .themedRow()
347349
348350 if viewModel.isLoadingMore {
349351 HStack {
@@ -352,9 +354,11 @@ struct TicketListView: View {
352354 Spacer()
353355 }
354356 .listRowSeparator(.hidden)
357 .themedRow()
355358 }
356359 }
357360 }
361 .themedList()
358362 .listStyle(.plain)
359363 .listSectionSpacing(.compact)
360364 .searchable(
@@ -636,10 +640,13 @@ private struct CreateTicketSheet: View {
636640 Form {
637641 Section("Ticket Details") {
638642 TextField("Title", text: $subject)
643 .themedRow()
639644 TextField("Description (optional)", text: $descriptionText, axis: .vertical)
640645 .lineLimit(6...12)
646 .themedRow()
641647 }
642648 }
649 .themedList()
643650 .navigationTitle("New Ticket")
644651 .navigationBarTitleDisplayMode(.inline)
645652 .toolbar {
@@ -705,7 +712,9 @@ private struct TicketLabelsSheet: View {
705712 }
706713 }
707714 }
715 .themedRow()
708716 }
717 .themedList()
709718 }
710719 } else {
711720 ContentUnavailableView(
@@ -921,6 +930,7 @@ private struct TicketFilterLabelsSheet: View {
921930 }
922931 }
923932 .padding(.vertical, 4)
933 .themedRow()
924934 }
925935 }
926936
@@ -940,8 +950,10 @@ private struct TicketFilterLabelsSheet: View {
940950 }
941951 .buttonStyle(.plain)
942952 }
953 .themedRow()
943954 }
944955 }
956 .themedList()
945957 }
946958 }
947959 .navigationTitle("Filter Labels")
@@ -982,8 +994,10 @@ private struct SaveTicketFilterSheet: View {
982994 Section("Name") {
983995 TextField("Filter name", text: $name)
984996 .textInputAutocapitalization(.words)
997 .themedRow()
985998 }
986999 }
1000 .themedList()
9871001 .navigationTitle("Save Filter")
9881002 .navigationBarTitleDisplayMode(.inline)
9891003 .toolbar {
@@ -1085,6 +1099,7 @@ private struct BulkResolveSheet: View {
10851099 Text("\(viewModel.selectedTicketCount) ticket\(viewModel.selectedTicketCount == 1 ? "" : "s") selected")
10861100 .font(.subheadline)
10871101 .foregroundStyle(.secondary)
1102 .themedRow()
10881103 }
10891104
10901105 Section("Resolution") {
@@ -1095,8 +1110,10 @@ private struct BulkResolveSheet: View {
10951110 }
10961111 .pickerStyle(.inline)
10971112 .labelsHidden()
1113 .themedRow()
10981114 }
10991115 }
1116 .themedList()
11001117 .navigationTitle("Close Tickets")
11011118 .navigationBarTitleDisplayMode(.inline)
11021119 .toolbar {
@@ -1133,6 +1150,7 @@ private struct BulkAssignSheet: View {
11331150 Text("\(viewModel.selectedTicketCount) ticket\(viewModel.selectedTicketCount == 1 ? "" : "s") selected")
11341151 .font(.subheadline)
11351152 .foregroundStyle(.secondary)
1153 .themedRow()
11361154 }
11371155
11381156 if let currentUser {
@@ -1146,6 +1164,7 @@ private struct BulkAssignSheet: View {
11461164 }
11471165 }
11481166 .disabled(viewModel.selectedTicketCount == 0 || viewModel.isPerformingAction)
1167 .themedRow()
11491168 }
11501169 }
11511170
@@ -1154,8 +1173,10 @@ private struct BulkAssignSheet: View {
11541173 .textContentType(.username)
11551174 .autocorrectionDisabled()
11561175 .textInputAutocapitalization(.never)
1176 .themedRow()
11571177 }
11581178 }
1179 .themedList()
11591180 .navigationTitle("Assign Tickets")
11601181 .navigationBarTitleDisplayMode(.inline)
11611182 .toolbar {
Hutch/Views/Tickets/TrackerListView.swift +3
@@ -162,6 +162,7 @@ struct TrackerListView: View {
162162 await viewModel.loadMoreIfNeeded(currentItem: tracker)
163163 }
164164 }
165 .themedRow()
165166
166167 if viewModel.isLoadingMore {
167168 HStack {
@@ -170,8 +171,10 @@ struct TrackerListView: View {
170171 Spacer()
171172 }
172173 .listRowSeparator(.hidden)
174 .themedRow()
173175 }
174176 }
177 .themedList()
175178 .listStyle(.plain)
176179 .searchable(
177180 text: $vm.searchText,
Hutch/Views/Tickets/TrackerManagementView.swift +37
@@ -654,22 +654,27 @@ struct TrackerEditorSheet: View {
654654 TextField("Tracker name", text: $name)
655655 .textInputAutocapitalization(.never)
656656 .autocorrectionDisabled()
657 .themedRow()
657658 TextField("Short description (optional)", text: $description, axis: .vertical)
658659 .lineLimit(2...4)
660 .themedRow()
659661 Picker("Visibility", selection: $visibility) {
660662 Text("Public").tag(Visibility.public)
661663 Text("Unlisted").tag(Visibility.unlisted)
662664 Text("Private").tag(Visibility.private)
663665 }
666 .themedRow()
664667 }
665668
666669 if let error, !error.isEmpty {
667670 Section {
668671 Text(error)
669672 .foregroundStyle(.red)
673 .themedRow()
670674 }
671675 }
672676 }
677 .themedList()
673678 .navigationTitle(title)
674679 .navigationBarTitleDisplayMode(.inline)
675680 .toolbar {
@@ -718,10 +723,12 @@ struct TrackerACLManagementSheet: View {
718723 List {
719724 Section("Default Access") {
720725 TrackerPermissionSummary(permissions: viewModel.defaultACL.permissions)
726 .themedRow()
721727 Button("Update Default ACL") {
722728 editingDefaultACL = true
723729 }
724730 .disabled(viewModel.isSavingACL)
731 .themedRow()
725732 }
726733
727734 Section {
@@ -731,9 +738,11 @@ struct TrackerACLManagementSheet: View {
731738 ProgressView()
732739 Spacer()
733740 }
741 .themedRow()
734742 } else if viewModel.acls.isEmpty {
735743 Text("No tracker-specific ACLs yet.")
736744 .foregroundStyle(.secondary)
745 .themedRow()
737746 } else {
738747 ForEach(viewModel.acls) { entry in
739748 VStack(alignment: .leading, spacing: 6) {
@@ -756,6 +765,7 @@ struct TrackerACLManagementSheet: View {
756765 .tint(.blue)
757766 }
758767 }
768 .themedRow()
759769 }
760770 } header: {
761771 Text("User ACLs")
@@ -763,6 +773,7 @@ struct TrackerACLManagementSheet: View {
763773 Text("Each ACL must include all five permission flags.")
764774 }
765775 }
776 .themedList()
766777 .navigationTitle("ACLs")
767778 .navigationBarTitleDisplayMode(.inline)
768779 .toolbar {
@@ -908,6 +919,7 @@ private struct TrackerACLEditorSheet: View {
908919 TextField("Username or ~username", text: $username)
909920 .autocorrectionDisabled()
910921 .textInputAutocapitalization(.never)
922 .themedRow()
911923 }
912924
913925 permissionSection
@@ -916,9 +928,11 @@ private struct TrackerACLEditorSheet: View {
916928 Section {
917929 Text(error)
918930 .foregroundStyle(.red)
931 .themedRow()
919932 }
920933 }
921934 }
935 .themedList()
922936 .navigationTitle(title)
923937 .navigationBarTitleDisplayMode(.inline)
924938 .toolbar {
@@ -950,10 +964,15 @@ private struct TrackerACLEditorSheet: View {
950964 private var permissionSection: some View {
951965 Section("Permissions") {
952966 Toggle("Browse", isOn: $browse)
967 .themedRow()
953968 Toggle("Submit", isOn: $submit)
969 .themedRow()
954970 Toggle("Comment", isOn: $comment)
971 .themedRow()
955972 Toggle("Edit", isOn: $edit)
973 .themedRow()
956974 Toggle("Triage", isOn: $triage)
975 .themedRow()
957976 }
958977 }
959978
@@ -1003,19 +1022,26 @@ private struct TrackerDefaultACLEditorSheet: View {
10031022 Form {
10041023 Section("Permissions") {
10051024 Toggle("Browse", isOn: $browse)
1025 .themedRow()
10061026 Toggle("Submit", isOn: $submit)
1027 .themedRow()
10071028 Toggle("Comment", isOn: $comment)
1029 .themedRow()
10081030 Toggle("Edit", isOn: $edit)
1031 .themedRow()
10091032 Toggle("Triage", isOn: $triage)
1033 .themedRow()
10101034 }
10111035
10121036 if let error, !error.isEmpty {
10131037 Section {
10141038 Text(error)
10151039 .foregroundStyle(.red)
1040 .themedRow()
10161041 }
10171042 }
10181043 }
1044 .themedList()
10191045 .navigationTitle("Default ACL")
10201046 .navigationBarTitleDisplayMode(.inline)
10211047 .toolbar {
@@ -1073,6 +1099,7 @@ struct TrackerLabelManagementSheet: View {
10731099 Text("Labels are managed here and reused throughout the tracker.")
10741100 .font(.footnote)
10751101 .foregroundStyle(.secondary)
1102 .themedRow()
10761103 }
10771104
10781105 if viewModel.isLoadingLabels {
@@ -1081,12 +1108,14 @@ struct TrackerLabelManagementSheet: View {
10811108 ProgressView()
10821109 Spacer()
10831110 }
1111 .themedRow()
10841112 } else if viewModel.labels.isEmpty {
10851113 ContentUnavailableView(
10861114 "No Labels",
10871115 systemImage: "tag",
10881116 description: Text("Create labels for triage and organization.")
10891117 )
1118 .themedRow()
10901119 } else {
10911120 ForEach(viewModel.labels) { label in
10921121 Button {
@@ -1110,8 +1139,10 @@ struct TrackerLabelManagementSheet: View {
11101139 }
11111140 }
11121141 }
1142 .themedRow()
11131143 }
11141144 }
1145 .themedList()
11151146 .navigationTitle("Labels")
11161147 .navigationBarTitleDisplayMode(.inline)
11171148 .toolbar {
@@ -1261,8 +1292,11 @@ private struct TrackerLabelEditorSheet: View {
12611292 Form {
12621293 Section("Details") {
12631294 TextField("Label name", text: $name)
1295 .themedRow()
12641296 ColorPicker("Foreground", selection: $foregroundColor, supportsOpacity: false)
1297 .themedRow()
12651298 ColorPicker("Background", selection: $backgroundColor, supportsOpacity: false)
1299 .themedRow()
12661300 }
12671301
12681302 Section("Preview") {
@@ -1274,15 +1308,18 @@ private struct TrackerLabelEditorSheet: View {
12741308 foregroundColor: foregroundColor.hexString
12751309 )
12761310 )
1311 .themedRow()
12771312 }
12781313
12791314 if let error, !error.isEmpty {
12801315 Section {
12811316 Text(error)
12821317 .foregroundStyle(.red)
1318 .themedRow()
12831319 }
12841320 }
12851321 }
1322 .themedList()
12861323 .navigationTitle(title)
12871324 .navigationBarTitleDisplayMode(.inline)
12881325 .toolbar {
Hutch/Views/Work/WorkView.swift +10 −1
@@ -11,6 +11,7 @@ struct WorkView: View {
1111
1212 @AppStorage(AppStorageKeys.swipeActionsEnabled, store: .standard) private var swipeActionsEnabled = true
1313 @Environment(AppState.self) private var appState
14 @Environment(\.isAMOLEDTheme) private var isAMOLED
1415 @Environment(\.scenePhase) private var scenePhase
1516 @State private var viewModel: HomeViewModel?
1617 @State private var scope: Scope = .all
@@ -74,6 +75,7 @@ struct WorkView: View {
7475 }
7576 }
7677 .padding(.vertical, 2)
78 .themedRow()
7779 }
7880 }
7981
@@ -88,6 +90,7 @@ struct WorkView: View {
8890 if workCount(viewModel) == 0 {
8991 Section {
9092 WorkCompactMessageRow(text: "Nothing to do", systemImage: "checkmark.circle")
93 .themedRow()
9194 }
9295 }
9396 }
@@ -100,7 +103,7 @@ struct WorkView: View {
100103 }
101104 }
102105 .pickerStyle(.segmented)
103 .listRowBackground(Color.clear)
106 .listRowBackground(isAMOLED ? Color.black : Color.clear)
104107 .listRowInsets(EdgeInsets())
105108 }
106109 }
@@ -110,9 +113,11 @@ struct WorkView: View {
110113 Section {
111114 if isLoadingUnread(viewModel) {
112115 WorkLoadingRow(label: "Loading unread threads")
116 .themedRow()
113117 } else if viewModel.unreadInboxThreads.isEmpty {
114118 if compactWhenEmpty {
115119 WorkCompactMessageRow(text: "No unread threads", systemImage: "tray")
120 .themedRow()
116121 }
117122 } else {
118123 ForEach(viewModel.unreadInboxThreads) { thread in
@@ -137,6 +142,7 @@ struct WorkView: View {
137142 }
138143 }
139144 }
145 .themedRow()
140146 }
141147 } header: {
142148 Text("Unread Threads")
@@ -155,8 +161,10 @@ struct WorkView: View {
155161 Section {
156162 if viewModel.isLoadingAssignedTickets && viewModel.assignedTickets.isEmpty {
157163 WorkLoadingRow(label: "Loading assigned tickets")
164 .themedRow()
158165 } else if viewModel.assignedTickets.isEmpty {
159166 WorkCompactMessageRow(text: "No assigned tickets", systemImage: "person.crop.circle.badge.checkmark")
167 .themedRow()
160168 } else {
161169 ForEach(viewModel.assignedTickets) { ticket in
162170 NavigationLink {
@@ -200,6 +208,7 @@ struct WorkView: View {
200208 }
201209 }
202210 }
211 .themedRow()
203212 }
204213 } header: {
205214 Text("Assigned Tickets")