diff options
| author | Christian Cleberg <[email protected]> | 2026-04-13 19:06:54 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-13 19:06:54 -0500 |
| commit | a6ad2b9815d72575206bdca860eef9db850cdf1c (patch) | |
| tree | b4d5f27aa1655e5e40bab178550d6e2576d7df8b | |
| parent | 02a44d115b3784231cad640e992d9c18a7f67b40 (diff) | |
| download | hutch-a6ad2b9815d72575206bdca860eef9db850cdf1c.tar.gz hutch-a6ad2b9815d72575206bdca860eef9db850cdf1c.tar.bz2 hutch-a6ad2b9815d72575206bdca860eef9db850cdf1c.zip | |
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
40 files changed, 439 insertions, 16 deletions
diff --git a/Hutch.xcodeproj/project.pbxproj b/Hutch.xcodeproj/project.pbxproj index dda781d..befab2b 100644 --- a/Hutch.xcodeproj/project.pbxproj +++ b/Hutch.xcodeproj/project.pbxproj @@ -515,7 +515,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 67; + CURRENT_PROJECT_VERSION = 68; DEVELOPMENT_TEAM = ZCNAX3VL9D; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -532,7 +532,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.0.6; + MARKETING_VERSION = 3.1.0; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -552,7 +552,7 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 67; + CURRENT_PROJECT_VERSION = 68; DEVELOPMENT_TEAM = ZCNAX3VL9D; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -569,7 +569,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 3.0.6; + MARKETING_VERSION = 3.1.0; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; PRODUCT_NAME = "$(TARGET_NAME)"; STRING_CATALOG_GENERATE_SYMBOLS = YES; @@ -632,7 +632,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 67; + CURRENT_PROJECT_VERSION = 68; DEVELOPMENT_TEAM = ZCNAX3VL9D; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = HutchWidgetExtension/Info.plist; @@ -642,7 +642,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.0.6; + MARKETING_VERSION = 3.1.0; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -661,7 +661,7 @@ APPLICATION_EXTENSION_API_ONLY = YES; CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 67; + CURRENT_PROJECT_VERSION = 68; DEVELOPMENT_TEAM = ZCNAX3VL9D; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = HutchWidgetExtension/Info.plist; @@ -671,7 +671,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 3.0.6; + MARKETING_VERSION = 3.1.0; PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; diff --git a/Hutch/App/HutchApp.swift b/Hutch/App/HutchApp.swift index 0fb1875..9c643d9 100644 --- a/Hutch/App/HutchApp.swift +++ b/Hutch/App/HutchApp.swift @@ -13,7 +13,9 @@ struct HutchApp: App { .environment(appState) .environment(networkMonitor) .environment(\.displayDensity, displayDensity) + .environment(\.isAMOLEDTheme, appTheme == .amoled) .environment(\.defaultMinListRowHeight, displayDensity == .compact ? 36 : 44) + .background(appTheme == .amoled ? Color.black : Color.clear) .preferredColorScheme(appTheme.colorScheme) .onOpenURL { url in if let link = DeepLink(url: url) { diff --git a/Hutch/App/RootView.swift b/Hutch/App/RootView.swift index b4cf3bd..ed029bb 100644 --- a/Hutch/App/RootView.swift +++ b/Hutch/App/RootView.swift @@ -4,6 +4,7 @@ import SwiftUI /// full-screen sheet for token entry on first launch. struct RootView: View { @Environment(AppState.self) private var appState + @Environment(\.isAMOLEDTheme) private var isAMOLED @State private var homePath = NavigationPath() @State private var morePath = NavigationPath() @State private var repoPath = NavigationPath() @@ -125,6 +126,7 @@ struct RootView: View { .id(appState.sessionIdentity) .defaultAppStorage(appState.accountDefaults) .modifier(SidebarAdaptableTabStyle()) + .modifier(AMOLEDToolbarStyle(isAMOLED: isAMOLED)) .modifier(TabKeyboardShortcuts(selectedTab: Binding( get: { appState.selectedTab }, set: { appState.selectedTab = $0 } @@ -427,6 +429,25 @@ private struct TabKeyboardShortcuts: ViewModifier { } } +// MARK: - AMOLED Toolbar Styling + +/// Applies true-black backgrounds to the tab bar and navigation bar when the AMOLED theme is active. +private struct AMOLEDToolbarStyle: ViewModifier { + let isAMOLED: Bool + + func body(content: Content) -> some View { + if isAMOLED { + content + .toolbarBackground(Color.black, for: .tabBar) + .toolbarBackground(.visible, for: .tabBar) + .toolbarBackground(Color.black, for: .navigationBar) + .toolbarBackground(.visible, for: .navigationBar) + } else { + content + } + } +} + // MARK: - iPad Sidebar Adaptable /// Applies `.tabViewStyle(.sidebarAdaptable)` on iOS 18+ so the tab bar diff --git a/Hutch/App/ThemeManager.swift b/Hutch/App/ThemeManager.swift index ee74921..af54094 100644 --- a/Hutch/App/ThemeManager.swift +++ b/Hutch/App/ThemeManager.swift @@ -6,6 +6,7 @@ enum AppTheme: String, CaseIterable, Identifiable { case system case light case dark + case amoled var id: String { rawValue } @@ -14,6 +15,7 @@ enum AppTheme: String, CaseIterable, Identifiable { case .system: "System" case .light: "Light" case .dark: "Dark" + case .amoled: "AMOLED" } } @@ -22,6 +24,7 @@ enum AppTheme: String, CaseIterable, Identifiable { case .system: nil case .light: .light case .dark: .dark + case .amoled: .dark } } } @@ -46,25 +49,51 @@ private struct DisplayDensityKey: EnvironmentKey { static let defaultValue: DisplayDensity = .standard } +private struct IsAMOLEDThemeKey: EnvironmentKey { + static let defaultValue: Bool = false +} + extension EnvironmentValues { var displayDensity: DisplayDensity { get { self[DisplayDensityKey.self] } set { self[DisplayDensityKey.self] = newValue } } + + var isAMOLEDTheme: Bool { + get { self[IsAMOLEDThemeKey.self] } + set { self[IsAMOLEDThemeKey.self] = newValue } + } } // MARK: - Themed List Modifier -/// Combined modifier for List/Form that applies compact section spacing. +/// Combined modifier for List/Form that applies compact section spacing and AMOLED black backgrounds. /// Apply once per List or Form. struct ThemedListStyle: ViewModifier { @Environment(\.displayDensity) private var density + @Environment(\.isAMOLEDTheme) private var isAMOLED func body(content: Content) -> some View { content .listSectionSpacing(density == .compact ? .compact : .default) .contentMargins(.vertical, density == .compact ? 2 : 8, for: .scrollContent) .controlSize(density == .compact ? .small : .regular) + .modifier(AMOLEDListBackground(isAMOLED: isAMOLED)) + } +} + +/// Hides the default grouped list background and replaces it with true black for OLED screens. +private struct AMOLEDListBackground: ViewModifier { + let isAMOLED: Bool + + func body(content: Content) -> some View { + if isAMOLED { + content + .scrollContentBackground(.hidden) + .background(Color.black) + } else { + content + } } } @@ -74,3 +103,21 @@ extension View { modifier(ThemedListStyle()) } } + +// MARK: - Themed Row Modifier + +struct ThemedRowStyle: ViewModifier { + @Environment(\.isAMOLEDTheme) private var isAMOLED + + func body(content: Content) -> some View { + content + .listRowBackground(isAMOLED ? Color.black : nil) + } +} + +extension View { + /// Apply to each row view inside a List or Form section to get a true-black background in AMOLED mode. + func themedRow() -> some View { + modifier(ThemedRowStyle()) + } +} diff --git a/Hutch/Views/Auth/AuthView.swift b/Hutch/Views/Auth/AuthView.swift index 5a249ef..d114981 100644 --- a/Hutch/Views/Auth/AuthView.swift +++ b/Hutch/Views/Auth/AuthView.swift @@ -15,6 +15,7 @@ struct TokenEntryView: View { Form { Section { Text("Enter your SourceHut personal access token to connect.") + .themedRow() } header: { Text("Welcome to Hutch") } footer: { @@ -27,6 +28,7 @@ struct TokenEntryView: View { .autocorrectionDisabled() .textInputAutocapitalization(.never) .disabled(isConnecting) + .themedRow() } header: { Text("Token") } @@ -40,6 +42,7 @@ struct TokenEntryView: View { .foregroundStyle(.red) } .foregroundStyle(.red) + .themedRow() } } @@ -56,22 +59,26 @@ struct TokenEntryView: View { } } .disabled(tokenTrimmed.isEmpty || isConnecting) + .themedRow() } Section { Link(destination: createAccountURL) { Label("Create SourceHut account", systemImage: "person.badge.plus") } + .themedRow() Link(destination: personalAccessTokensURL) { Label("Create Personal Access Token", systemImage: "key") } + .themedRow() } header: { Text("Need an account?") } footer: { Text("These links open SourceHut in your browser. After signing up, create a Personal Access Token there and paste it here.") } } + .themedList() .navigationTitle("Hutch") } } diff --git a/Hutch/Views/Builds/BuildDetailView.swift b/Hutch/Views/Builds/BuildDetailView.swift index 5895f48..46fb42d 100644 --- a/Hutch/Views/Builds/BuildDetailView.swift +++ b/Hutch/Views/Builds/BuildDetailView.swift @@ -176,26 +176,34 @@ struct BuildDetailView: View { .font(.subheadline.weight(.medium)) } } + .themedRow() if let note = job.note, !note.isEmpty { LabeledContent("Note", value: note) + .themedRow() } if let image = job.image { LabeledContent("Image", value: image) + .themedRow() } if !job.tags.isEmpty { LabeledContent("Tags", value: job.tags.joined(separator: ", ")) + .themedRow() } if let visibility = job.visibility { LabeledContent("Visibility", value: visibility.rawValue.capitalized) + .themedRow() } LabeledContent("Owner", value: job.owner.canonicalName) + .themedRow() LabeledContent("Created", value: job.created.relativeDescription) + .themedRow() LabeledContent("Updated", value: job.updated.relativeDescription) + .themedRow() } if appState.isDebugModeEnabled { @@ -211,9 +219,11 @@ struct BuildDetailView: View { url: \(SRHTWebURL.build(jobId: job.id, ownerCanonicalName: job.owner.canonicalName)?.absoluteString ?? "unavailable") """ ) + .themedRow() if let rawJobResponse = viewModel.rawJobResponse { DebugTextBlock(title: "Raw Response", content: rawJobResponse) + .themedRow() } } } @@ -237,6 +247,7 @@ struct BuildDetailView: View { } } .disabled(isOpeningRepository) + .themedRow() } } @@ -248,6 +259,7 @@ struct BuildDetailView: View { openURL(url) } } + .themedRow() } header: { Text("Artifacts") } footer: { @@ -275,6 +287,7 @@ struct BuildDetailView: View { } } .foregroundStyle(.primary) + .themedRow() } header: { HStack(spacing: 6) { TaskStatusIcon(status: task.status) @@ -299,6 +312,7 @@ struct BuildDetailView: View { } } .disabled(viewModel.isCancelling) + .themedRow() } } @@ -319,6 +333,7 @@ struct BuildDetailView: View { } } .disabled(viewModel.isRebuilding) + .themedRow() Button { showEditResubmitSheet = true @@ -326,11 +341,13 @@ struct BuildDetailView: View { Text("Edit & Resubmit") } .disabled(viewModel.isSubmittingEditedBuild) + .themedRow() } footer: { Text("Creates a new build using this job’s saved manifest, tags, note, and visibility.") } } } + .themedList() .task(id: job.id) { RecentActivityStore.recordBuild( jobId: job.id, @@ -463,26 +480,33 @@ private struct EditResubmitBuildSheet: View { .lineLimit(12...24) .textInputAutocapitalization(.never) .autocorrectionDisabled() + .themedRow() } Section("Build Options") { TextField("Note (optional)", text: $note) + .themedRow() TextField("Tags (comma-separated, optional)", text: $tagsText) .textInputAutocapitalization(.never) .autocorrectionDisabled() + .themedRow() Picker("Visibility", selection: $visibility) { Text("Public").tag(Visibility.public) Text("Unlisted").tag(Visibility.unlisted) Text("Private").tag(Visibility.private) } + .themedRow() Toggle("Start build now", isOn: $execute) + .themedRow() Toggle("Allow build secrets", isOn: $secrets) + .themedRow() } Section { 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.") .font(.footnote) .foregroundStyle(.secondary) + .themedRow() } if let actionError = viewModel.actionError { @@ -494,6 +518,7 @@ private struct EditResubmitBuildSheet: View { .foregroundStyle(.red) } .foregroundStyle(.red) + .themedRow() } } } diff --git a/Hutch/Views/Builds/BuildListView.swift b/Hutch/Views/Builds/BuildListView.swift index f4647e4..fc91ea1 100644 --- a/Hutch/Views/Builds/BuildListView.swift +++ b/Hutch/Views/Builds/BuildListView.swift @@ -5,6 +5,7 @@ struct BuildListView: View { @AppStorage(AppStorageKeys.buildsAutoRefreshInterval) private var autoRefreshRawValue = 0 @AppStorage(AppStorageKeys.buildsRepoFilter) private var savedRepoFilter = "" @Environment(AppState.self) private var appState + @Environment(\.isAMOLEDTheme) private var isAMOLED @State private var viewModel: BuildListViewModel? @State private var showSubmitSheet = false @State private var submittedJobId: Int? @@ -134,7 +135,7 @@ struct BuildListView: View { .padding(.top, 6) .padding(.bottom, 10) .listRowInsets(EdgeInsets()) - .listRowBackground(Color.clear) + .listRowBackground(isAMOLED ? Color.black : Color.clear) .listRowSeparator(.hidden) ForEach(viewModel.filteredJobs) { job in @@ -181,6 +182,7 @@ struct BuildListView: View { await viewModel.loadMoreIfNeeded(currentItem: job) } } + .themedRow() if viewModel.isLoadingMore { HStack { @@ -189,6 +191,7 @@ struct BuildListView: View { Spacer() } .listRowSeparator(.hidden) + .themedRow() } } } @@ -279,26 +282,33 @@ private struct SubmitBuildSheet: View { .lineLimit(12...24) .textInputAutocapitalization(.never) .autocorrectionDisabled() + .themedRow() } Section("Build Options") { TextField("Note (optional)", text: $note) + .themedRow() TextField("Tags (comma-separated, optional)", text: $tagsText) .textInputAutocapitalization(.never) .autocorrectionDisabled() + .themedRow() Picker("Visibility", selection: $visibility) { Text("Public").tag(Visibility.public) Text("Unlisted").tag(Visibility.unlisted) Text("Private").tag(Visibility.private) } + .themedRow() Toggle("Start build now", isOn: $execute) + .themedRow() Toggle("Allow build secrets", isOn: $secrets) + .themedRow() } Section { Text("You need a valid builds.sr.ht manifest and a token with BUILDS:RW.") .font(.footnote) .foregroundStyle(.secondary) + .themedRow() } if let error = viewModel.error { @@ -310,9 +320,11 @@ private struct SubmitBuildSheet: View { .foregroundStyle(.red) } .foregroundStyle(.red) + .themedRow() } } } + .themedList() .navigationTitle("Submit Build") .navigationBarTitleDisplayMode(.inline) .onDisappear { diff --git a/Hutch/Views/Home/HomeView.swift b/Hutch/Views/Home/HomeView.swift index 105b62b..b10926a 100644 --- a/Hutch/Views/Home/HomeView.swift +++ b/Hutch/Views/Home/HomeView.swift @@ -94,6 +94,7 @@ struct HomeView: View { ) } .buttonStyle(.plain) + .themedRow() } } } @@ -109,6 +110,7 @@ struct HomeView: View { emphasis: .action ) } + .themedRow() } } @@ -126,6 +128,7 @@ struct HomeView: View { .disabled(isOpeningRecentItem) .listRowSeparator(.hidden) } + .themedRow() } } } @@ -143,6 +146,7 @@ struct HomeView: View { emphasis: .monitoring ) } + .themedRow() } } @@ -156,6 +160,7 @@ struct HomeView: View { } label: { HomeCompactMessageRow(text: "Pin projects for quick access", systemImage: "pin") } + .themedRow() } else { LazyVGrid( columns: [ @@ -174,6 +179,7 @@ struct HomeView: View { } } .padding(.vertical, 2) + .themedRow() } } } diff --git a/Hutch/Views/Inbox/ThreadDetailView.swift b/Hutch/Views/Inbox/ThreadDetailView.swift index bde8172..028964c 100644 --- a/Hutch/Views/Inbox/ThreadDetailView.swift +++ b/Hutch/Views/Inbox/ThreadDetailView.swift @@ -123,6 +123,7 @@ struct ThreadDetailView: View { .foregroundStyle(.secondary) } .padding(.vertical, 4) + .themedRow() } Section("Related") { @@ -138,6 +139,7 @@ struct ThreadDetailView: View { } label: { Label(thread.listDisplayName, systemImage: "list.bullet") } + .themedRow() if let repo = self.thread.repo { Button { @@ -157,6 +159,7 @@ struct ThreadDetailView: View { } } .disabled(isOpeningRepository) + .themedRow() } } @@ -165,6 +168,7 @@ struct ThreadDetailView: View { Text(partialWarning) .font(.caption) .foregroundStyle(.secondary) + .themedRow() } } @@ -184,8 +188,10 @@ struct ThreadDetailView: View { } ) } + .themedRow() } } + .themedList() .listStyle(.plain) .toolbar { ToolbarItem(placement: .topBarTrailing) { diff --git a/Hutch/Views/Lists/MailingListListView.swift b/Hutch/Views/Lists/MailingListListView.swift index eca7343..2fcc640 100644 --- a/Hutch/Views/Lists/MailingListListView.swift +++ b/Hutch/Views/Lists/MailingListListView.swift @@ -142,7 +142,9 @@ struct MailingListListView: View { .padding(.vertical, 2) } } + .themedRow() } + .themedList() .listStyle(.plain) .searchable( text: $vm.searchText, diff --git a/Hutch/Views/Lookup/LookupView.swift b/Hutch/Views/Lookup/LookupView.swift index e53dd33..f45082d 100644 --- a/Hutch/Views/Lookup/LookupView.swift +++ b/Hutch/Views/Lookup/LookupView.swift @@ -356,6 +356,7 @@ struct LookupView: View { } } .pickerStyle(.menu) + .themedRow() TextField( vm.selectedType.inputLabel, @@ -368,6 +369,7 @@ struct LookupView: View { .onSubmit { Task { await vm.lookup() } } + .themedRow() } Section { @@ -382,6 +384,7 @@ struct LookupView: View { ProgressView() } } + .themedRow() } if !vm.history.isEmpty { @@ -403,11 +406,13 @@ struct LookupView: View { } .disabled(vm.isLooking) } + .themedRow() Button("Clear History", role: .destructive) { vm.clearHistory() } .disabled(vm.isLooking) + .themedRow() } } } diff --git a/Hutch/Views/Lookup/UserProfileView.swift b/Hutch/Views/Lookup/UserProfileView.swift index 4fbb5a0..8472a90 100644 --- a/Hutch/Views/Lookup/UserProfileView.swift +++ b/Hutch/Views/Lookup/UserProfileView.swift @@ -2,6 +2,7 @@ import SwiftUI struct UserProfileView: View { @Environment(AppState.self) private var appState + @Environment(\.isAMOLEDTheme) private var isAMOLED @AppStorage(AppStorageKeys.contributionGraphsEnabled, store: .standard) private var contributionGraphsEnabled = true let user: User @@ -55,33 +56,41 @@ struct UserProfileView: View { } Spacer() } - .listRowBackground(Color.clear) + .listRowBackground(isAMOLED ? Color.black : Color.clear) } } Section { LabeledContent("Username", value: user.username) + .themedRow() LabeledContent("Canonical Name", value: user.canonicalName) + .themedRow() if let userType = user.userType { LabeledContent("User Type", value: userType) + .themedRow() } if let pronouns = user.pronouns { LabeledContent("Pronouns", value: pronouns) + .themedRow() } if let suspensionNotice = user.suspensionNotice { LabeledContent("Suspension Notice", value: suspensionNotice) + .themedRow() } } Section { LabeledContent("Email", value: user.email) + .themedRow() if let urlString = user.url, let url = URL(string: urlString) { LabeledContent("URL") { Link(urlString, destination: url) } + .themedRow() } if let location = user.location { LabeledContent("Location", value: location) + .themedRow() } } @@ -91,6 +100,7 @@ struct UserProfileView: View { .frame(maxWidth: .infinity, alignment: .leading) .tint(.accentColor) .textSelection(.enabled) + .themedRow() } } @@ -98,9 +108,11 @@ struct UserProfileView: View { Section { if let created = user.created { LabeledContent("Joined", value: formattedTimestamp(created)) + .themedRow() } if let updated = user.updated { LabeledContent("Updated", value: formattedTimestamp(updated)) + .themedRow() } } } @@ -118,15 +130,18 @@ struct UserProfileView: View { error: viewModel.contributionsError ?? viewModel.contributionStatusText, isIndexedButEmpty: viewModel.isContributionActivityIndexedButEmpty ) + .themedRow() } } Section { if viewModel.isLoadingRepositories && viewModel.repositories.isEmpty { ProgressView() + .themedRow() } else if viewModel.repositories.isEmpty { Text("No public repositories.") .foregroundStyle(.secondary) + .themedRow() } else { ForEach(viewModel.repositories.prefix(4)) { repo in NavigationLink { @@ -137,10 +152,12 @@ struct UserProfileView: View { RepositoryRowView(repository: repo, buildStatus: .none) } } + .themedRow() if viewModel.repositories.count > 4 { NavigationLink("See All") { UserRepositoriesView(viewModel: viewModel) } + .themedRow() } } } header: { @@ -150,9 +167,11 @@ struct UserProfileView: View { Section { if viewModel.isLoadingTrackers && viewModel.trackers.isEmpty { ProgressView() + .themedRow() } else if viewModel.trackers.isEmpty { Text("No public trackers.") .foregroundStyle(.secondary) + .themedRow() } else { ForEach(viewModel.trackers.prefix(4)) { tracker in NavigationLink { @@ -161,10 +180,12 @@ struct UserProfileView: View { UserProfileTrackerRowView(tracker: tracker) } } + .themedRow() if viewModel.trackers.count > 4 { NavigationLink("See All") { UserTrackersView(viewModel: viewModel) } + .themedRow() } } } header: { @@ -172,6 +193,7 @@ struct UserProfileView: View { } } } + .themedList() .listStyle(.insetGrouped) .navigationTitle(user.canonicalName) .navigationBarTitleDisplayMode(.inline) diff --git a/Hutch/Views/Lookup/UserRepositoriesView.swift b/Hutch/Views/Lookup/UserRepositoriesView.swift index 7aeea10..c2ca65c 100644 --- a/Hutch/Views/Lookup/UserRepositoriesView.swift +++ b/Hutch/Views/Lookup/UserRepositoriesView.swift @@ -14,7 +14,9 @@ struct UserRepositoriesView: View { RepositoryRowView(repository: repo, buildStatus: .none) } } + .themedRow() } + .themedList() .listStyle(.plain) .navigationTitle("Repositories") .navigationBarTitleDisplayMode(.inline) diff --git a/Hutch/Views/Lookup/UserTrackersView.swift b/Hutch/Views/Lookup/UserTrackersView.swift index 17916da..c98d839 100644 --- a/Hutch/Views/Lookup/UserTrackersView.swift +++ b/Hutch/Views/Lookup/UserTrackersView.swift @@ -12,7 +12,9 @@ struct UserTrackersView: View { UserProfileTrackerRowView(tracker: tracker) } } + .themedRow() } + .themedList() .listStyle(.plain) .navigationTitle("Trackers") .navigationBarTitleDisplayMode(.inline) diff --git a/Hutch/Views/More/AccountSwitcherView.swift b/Hutch/Views/More/AccountSwitcherView.swift index 5fde9c5..ce69279 100644 --- a/Hutch/Views/More/AccountSwitcherView.swift +++ b/Hutch/Views/More/AccountSwitcherView.swift @@ -48,6 +48,7 @@ struct AccountSwitcherView: View { pendingRemoval = appState.accounts[index] } } + .themedRow() } Section { @@ -57,8 +58,10 @@ struct AccountSwitcherView: View { Label("Add Account", systemImage: "plus.circle") } .disabled(isSwitching) + .themedRow() } } + .themedList() .navigationTitle("Accounts") .navigationBarTitleDisplayMode(.inline) .toolbar { diff --git a/Hutch/Views/More/AddAccountView.swift b/Hutch/Views/More/AddAccountView.swift index cfaa2b2..c15db09 100644 --- a/Hutch/Views/More/AddAccountView.swift +++ b/Hutch/Views/More/AddAccountView.swift @@ -15,6 +15,7 @@ struct AddAccountView: View { SecureField("Personal Access Token", text: $token) .autocorrectionDisabled() .textInputAutocapitalization(.never) + .themedRow() } footer: { Text("Generate a token at meta.sr.ht → OAuth2 clients.") } @@ -23,9 +24,11 @@ struct AddAccountView: View { Section { Text(errorMessage) .foregroundStyle(.red) + .themedRow() } } } + .themedList() .navigationTitle("Add Account") .navigationBarTitleDisplayMode(.inline) .toolbar { diff --git a/Hutch/Views/More/ManPageBrowserView.swift b/Hutch/Views/More/ManPageBrowserView.swift index f854c79..c22eafe 100644 --- a/Hutch/Views/More/ManPageBrowserView.swift +++ b/Hutch/Views/More/ManPageBrowserView.swift @@ -25,8 +25,10 @@ struct ManPageBrowserView: View { Text(doc.title) } } + .themedRow() } } + .themedList() .navigationTitle("Man Pages") .navigationBarTitleDisplayMode(.inline) } diff --git a/Hutch/Views/More/MoreView.swift b/Hutch/Views/More/MoreView.swift index 8be25ca..4666e53 100644 --- a/Hutch/Views/More/MoreView.swift +++ b/Hutch/Views/More/MoreView.swift @@ -16,24 +16,29 @@ struct MoreView: View { NavigationLink(value: MoreRoute.lookup) { Label("Look Up", systemImage: "magnifyingglass") } + .themedRow() } Section("Other Services") { NavigationLink(value: MoreRoute.projects) { Label("Projects", systemImage: "square.stack.3d.up") } + .themedRow() NavigationLink(value: MoreRoute.lists) { Label("Mailing Lists", systemImage: "list.bullet.rectangle") } + .themedRow() NavigationLink(value: MoreRoute.manPageBrowser) { Label("Man Pages", systemImage: "book") } + .themedRow() NavigationLink(value: MoreRoute.pastes) { Label("Pastes", systemImage: "doc.on.clipboard") } + .themedRow() NavigationLink(value: MoreRoute.systemStatus) { SystemStatusSummaryRow( @@ -43,16 +48,19 @@ struct MoreView: View { isShowingStaleData: viewModel?.isShowingStaleSystemStatus ?? false ) } + .themedRow() } Section("Meta") { NavigationLink(value: MoreRoute.profile) { Label("Profile", systemImage: "person.text.rectangle") } + .themedRow() NavigationLink(value: MoreRoute.settings) { Label("Settings", systemImage: "gear") } + .themedRow() } Section { @@ -61,6 +69,7 @@ struct MoreView: View { Label(item.title, systemImage: "safari") } } + .themedRow() } header: { Text("External Links") } footer: { diff --git a/Hutch/Views/More/ProfileView.swift b/Hutch/Views/More/ProfileView.swift index db3cb53..c53a89d 100644 --- a/Hutch/Views/More/ProfileView.swift +++ b/Hutch/Views/More/ProfileView.swift @@ -79,6 +79,7 @@ struct ProfileView: View { error: contributionViewModel.contributionsError ?? contributionViewModel.contributionStatusText, isIndexedButEmpty: contributionViewModel.isContributionActivityIndexedButEmpty ) + .themedRow() } } @@ -185,6 +186,7 @@ struct ProfileView: View { } } .padding(.vertical, 4) + .themedRow() if let bio = profile.bio, !bio.isEmpty { VStack(alignment: .leading, spacing: 2) { @@ -193,36 +195,44 @@ struct ProfileView: View { .foregroundStyle(.secondary) ProfileBioView(markdown: bio) } + .themedRow() } if let location = profile.location, !location.isEmpty { LabeledContent("Location", value: location) + .themedRow() } if let url = profile.url, !url.isEmpty { LabeledContent("URL", value: url) + .themedRow() } if let status = profile.paymentStatus { LabeledContent("Payment", value: status.capitalized) + .themedRow() } if let sub = profile.subscription { if let status = sub.status { LabeledContent("Subscription", value: status.capitalized) + .themedRow() } if let interval = sub.interval { LabeledContent("Interval", value: interval.capitalized) + .themedRow() } } Button("Edit Profile") { viewModel.isEditingProfile = true } + .themedRow() SRHTShareButton(url: SRHTWebURL.profile(canonicalName: profile.canonicalName), target: .profile) { SwiftUI.Label("Share Profile", systemImage: "square.and.arrow.up") } + .themedRow() } } @@ -262,11 +272,13 @@ struct ProfileView: View { } } } + .themedRow() if viewModel.isAddingSSHKey { TextField("Paste SSH public key", text: $vm.newSSHKey, axis: .vertical) .font(.caption.monospaced()) .lineLimit(3...6) + .themedRow() HStack { Button("Cancel") { @@ -280,12 +292,14 @@ struct ProfileView: View { .buttonStyle(.borderedProminent) .disabled(viewModel.newSSHKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) } + .themedRow() } else { Button { viewModel.isAddingSSHKey = true } label: { SwiftUI.Label("Add SSH Key", systemImage: "key") } + .themedRow() } } header: { Text("SSH Keys") @@ -316,11 +330,13 @@ struct ProfileView: View { } } } + .themedRow() if viewModel.isAddingPGPKey { TextField("Paste PGP public key", text: $vm.newPGPKey, axis: .vertical) .font(.caption.monospaced()) .lineLimit(3...6) + .themedRow() HStack { Button("Cancel") { @@ -334,12 +350,14 @@ struct ProfileView: View { .buttonStyle(.borderedProminent) .disabled(viewModel.newPGPKey.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) } + .themedRow() } else { Button { viewModel.isAddingPGPKey = true } label: { SwiftUI.Label("Add PGP Key", systemImage: "key.fill") } + .themedRow() } } header: { Text("PGP Keys") @@ -357,10 +375,12 @@ struct ProfileView: View { ProgressView() Spacer() } + .themedRow() } else if viewModel.personalAccessTokens.isEmpty { Button("Load Tokens") { Task { await viewModel.loadPersonalAccessTokens() } } + .themedRow() } else { ForEach(viewModel.personalAccessTokens) { token in VStack(alignment: .leading, spacing: 4) { @@ -390,6 +410,7 @@ struct ProfileView: View { } } } + .themedRow() } } header: { Text("Personal Access Tokens") @@ -426,6 +447,7 @@ private struct EditProfileSheet: View { let profile: UserProfile let viewModel: SettingsViewModel + @Environment(\.isAMOLEDTheme) private var isAMOLED @State private var email: String @State private var url: String @State private var location: String @@ -492,7 +514,7 @@ private struct EditProfileSheet: View { } Spacer() } - .listRowBackground(Color.clear) + .listRowBackground(isAMOLED ? Color.black : Color.clear) if profile.avatar != nil || avatarPreview != nil { HStack { @@ -506,7 +528,7 @@ private struct EditProfileSheet: View { .disabled(viewModel.isUploadingAvatar) Spacer() } - .listRowBackground(Color.clear) + .listRowBackground(isAMOLED ? Color.black : Color.clear) .listRowSeparator(.hidden) } } @@ -522,6 +544,7 @@ private struct EditProfileSheet: View { .autocorrectionDisabled() .textInputAutocapitalization(.never) } + .themedRow() VStack(alignment: .leading, spacing: 4) { Text("URL") @@ -533,6 +556,7 @@ private struct EditProfileSheet: View { .autocorrectionDisabled() .textInputAutocapitalization(.never) } + .themedRow() VStack(alignment: .leading, spacing: 4) { Text("Location") @@ -540,6 +564,7 @@ private struct EditProfileSheet: View { .foregroundStyle(.secondary) TextField("Enter location", text: $location) } + .themedRow() VStack(alignment: .leading, spacing: 4) { Text("Bio") @@ -548,8 +573,10 @@ private struct EditProfileSheet: View { TextField("Enter bio", text: $bio, axis: .vertical) .lineLimit(3...6) } + .themedRow() } } + .themedList() .navigationTitle("Edit Profile") .navigationBarTitleDisplayMode(.inline) .onChange(of: selectedPhoto) { _, newItem in diff --git a/Hutch/Views/Pastes/PasteDetailView.swift b/Hutch/Views/Pastes/PasteDetailView.swift index 7eb1853..c6fe11a 100644 --- a/Hutch/Views/Pastes/PasteDetailView.swift +++ b/Hutch/Views/Pastes/PasteDetailView.swift @@ -326,13 +326,18 @@ private struct PasteInfoSheet: View { List { Section("Paste") { LabeledContent("ID", value: paste.id) + .themedRow() LabeledContent("Owner", value: paste.user.canonicalName) + .themedRow() LabeledContent("Created", value: paste.created.relativeDescription) + .themedRow() LabeledContent("Visibility") { VisibilityBadge(visibility: paste.visibility) } + .themedRow() if paste.files.count > 1 { LabeledContent("Files", value: "\(paste.files.count)") + .themedRow() } } @@ -340,6 +345,7 @@ private struct PasteInfoSheet: View { Section("File") { if let filename = file.filename, !filename.isEmpty { LabeledContent("Filename", value: filename) + .themedRow() } LabeledContent("Hash") { Text(file.hash) @@ -347,6 +353,7 @@ private struct PasteInfoSheet: View { .foregroundStyle(.secondary) .textSelection(.enabled) } + .themedRow() } } } @@ -407,6 +414,7 @@ private struct PasteVisibilitySheet: View { } .buttonStyle(.plain) } + .themedRow() } .listStyle(.insetGrouped) .navigationTitle("Visibility") diff --git a/Hutch/Views/Pastes/PasteListView.swift b/Hutch/Views/Pastes/PasteListView.swift index 9363b08..dfd472b 100644 --- a/Hutch/Views/Pastes/PasteListView.swift +++ b/Hutch/Views/Pastes/PasteListView.swift @@ -101,6 +101,7 @@ struct PasteListView: View { await viewModel.loadMoreIfNeeded(currentItem: paste) } } + .themedRow() if viewModel.isLoadingMore { HStack { @@ -109,6 +110,7 @@ struct PasteListView: View { Spacer() } .listRowSeparator(.hidden) + .themedRow() } } .themedList() @@ -302,12 +304,14 @@ private struct CreatePasteSheet: View { files = [PasteUploadDraft()] } } + .themedRow() Button { files.append(PasteUploadDraft()) } label: { Label("Add File", systemImage: "plus") } + .themedRow() } Section("Visibility") { @@ -316,21 +320,25 @@ private struct CreatePasteSheet: View { Text("Unlisted").tag(Visibility.unlisted) Text("Private").tag(Visibility.private) } + .themedRow() } Section { 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.") .font(.footnote) .foregroundStyle(.secondary) + .themedRow() } if let error = viewModel.error { Section { Label(error, systemImage: "exclamationmark.triangle.fill") .foregroundStyle(.red) + .themedRow() } } } + .themedList() .navigationTitle("New Paste") .navigationBarTitleDisplayMode(.inline) .toolbar { diff --git a/Hutch/Views/Projects/ProjectDetailView.swift b/Hutch/Views/Projects/ProjectDetailView.swift index dc8b24d..b5c6cdb 100644 --- a/Hutch/Views/Projects/ProjectDetailView.swift +++ b/Hutch/Views/Projects/ProjectDetailView.swift @@ -103,6 +103,7 @@ struct ProjectDetailView: View { } } .padding(.vertical, 4) + .themedRow() } } @@ -127,6 +128,7 @@ struct ProjectDetailView: View { } .buttonStyle(.plain) } + .themedRow() } } } @@ -155,6 +157,7 @@ struct ProjectDetailView: View { } .buttonStyle(.plain) } + .themedRow() } } } @@ -183,6 +186,7 @@ struct ProjectDetailView: View { } .buttonStyle(.plain) } + .themedRow() } } } @@ -205,6 +209,7 @@ struct ProjectDetailView: View { } .buttonStyle(.plain) } + .themedRow() } } } @@ -218,6 +223,7 @@ struct ProjectDetailView: View { systemImage: "square.stack.3d.up.slash", description: Text("This project doesn’t currently expose repositories, trackers, mailing lists, or external links.") ) + .themedRow() } } } diff --git a/Hutch/Views/Projects/ProjectMailingListView.swift b/Hutch/Views/Projects/ProjectMailingListView.swift index 4ff93e9..d7c0abc 100644 --- a/Hutch/Views/Projects/ProjectMailingListView.swift +++ b/Hutch/Views/Projects/ProjectMailingListView.swift @@ -332,7 +332,9 @@ struct MailingListDetailView: View { .tint(thread.isUnread ? .blue : .gray) } } + .themedRow() } + .themedList() .listStyle(.plain) .searchable( text: $vm.searchText, diff --git a/Hutch/Views/Projects/ProjectsListView.swift b/Hutch/Views/Projects/ProjectsListView.swift index 5786462..12a5149 100644 --- a/Hutch/Views/Projects/ProjectsListView.swift +++ b/Hutch/Views/Projects/ProjectsListView.swift @@ -79,6 +79,7 @@ struct ProjectsListView: View { .buttonStyle(.plain) .alignmentGuide(.listRowSeparatorLeading) { _ in 0 } } + .themedRow() } .themedList() .listStyle(.plain) diff --git a/Hutch/Views/Repositories/ArtifactsView.swift b/Hutch/Views/Repositories/ArtifactsView.swift index ef3b972..b8caf4c 100644 --- a/Hutch/Views/Repositories/ArtifactsView.swift +++ b/Hutch/Views/Repositories/ArtifactsView.swift @@ -13,9 +13,11 @@ struct ArtifactsView: View { openURL(artifact.url) } } + .themedRow() } } } + .themedList() .listStyle(.insetGrouped) .overlay { if viewModel.isLoadingArtifacts, viewModel.referenceArtifacts.isEmpty { diff --git a/Hutch/Views/Repositories/CommitLogView.swift b/Hutch/Views/Repositories/CommitLogView.swift index bc8ccc9..85e4d7a 100644 --- a/Hutch/Views/Repositories/CommitLogView.swift +++ b/Hutch/Views/Repositories/CommitLogView.swift @@ -13,6 +13,7 @@ struct CommitLogView: View { await viewModel.loadMoreCommitsIfNeeded(currentItem: commit) } } + .themedRow() if viewModel.isLoadingMoreCommits { HStack { @@ -21,8 +22,10 @@ struct CommitLogView: View { Spacer() } .listRowSeparator(.hidden) + .themedRow() } } + .themedList() .listStyle(.plain) .overlay { if viewModel.isLoadingCommits, viewModel.commits.isEmpty { diff --git a/Hutch/Views/Repositories/FileTreeView.swift b/Hutch/Views/Repositories/FileTreeView.swift index ec33822..b49f361 100644 --- a/Hutch/Views/Repositories/FileTreeView.swift +++ b/Hutch/Views/Repositories/FileTreeView.swift @@ -233,7 +233,9 @@ private struct FileTreeContentView: View { await viewModel.navigateInto(entry: entry) } } + .themedRow() } + .themedList() .listStyle(.plain) .refreshable { await viewModel.loadRootTree() @@ -848,6 +850,7 @@ private struct RefPickerSheet: View { ) } .buttonStyle(.plain) + .themedRow() } if !viewModel.branches.isEmpty { @@ -868,6 +871,7 @@ private struct RefPickerSheet: View { } .buttonStyle(.plain) } + .themedRow() } } @@ -889,9 +893,11 @@ private struct RefPickerSheet: View { } .buttonStyle(.plain) } + .themedRow() } } } + .themedList() .listStyle(.insetGrouped) .navigationTitle("Select Ref") .navigationBarTitleDisplayMode(.inline) diff --git a/Hutch/Views/Repositories/HgRepositoryDetailView.swift b/Hutch/Views/Repositories/HgRepositoryDetailView.swift index 8362467..047a495 100644 --- a/Hutch/Views/Repositories/HgRepositoryDetailView.swift +++ b/Hutch/Views/Repositories/HgRepositoryDetailView.swift @@ -396,7 +396,9 @@ struct HgRepositoryDetailView: View { .onTapGesture { Task { await viewModel.openFile(file) } } + .themedRow() } + .themedList() .listStyle(.plain) } } @@ -454,6 +456,7 @@ struct HgRepositoryDetailView: View { await viewModel.loadMoreLogIfNeeded(currentItem: revision) } } + .themedRow() if viewModel.isLoadingMoreLog { HStack { @@ -462,8 +465,10 @@ struct HgRepositoryDetailView: View { Spacer() } .listRowSeparator(.hidden) + .themedRow() } } + .themedList() .listStyle(.plain) .overlay { if viewModel.isLoadingLog, viewModel.log.isEmpty { @@ -498,7 +503,9 @@ struct HgRepositoryDetailView: View { } else { List(revisions) { revision in namedRevisionRow(revision) + .themedRow() } + .themedList() .listStyle(.plain) } } @@ -699,6 +706,7 @@ private struct HgBrowseRefPickerSheet: View { ) } .buttonStyle(.plain) + .themedRow() } if !viewModel.branches.isEmpty { @@ -719,6 +727,7 @@ private struct HgBrowseRefPickerSheet: View { } .buttonStyle(.plain) } + .themedRow() } } @@ -740,6 +749,7 @@ private struct HgBrowseRefPickerSheet: View { } .buttonStyle(.plain) } + .themedRow() } } @@ -761,9 +771,11 @@ private struct HgBrowseRefPickerSheet: View { } .buttonStyle(.plain) } + .themedRow() } } } + .themedList() .listStyle(.insetGrouped) .navigationTitle("Select Ref") .navigationBarTitleDisplayMode(.inline) diff --git a/Hutch/Views/Repositories/HgRepositorySettingsView.swift b/Hutch/Views/Repositories/HgRepositorySettingsView.swift index a6aa728..e3c97e0 100644 --- a/Hutch/Views/Repositories/HgRepositorySettingsView.swift +++ b/Hutch/Views/Repositories/HgRepositorySettingsView.swift @@ -49,6 +49,7 @@ struct HgRepositorySettingsView: View { histeditSection(viewModel) deleteSection(viewModel) } + .themedList() .srhtErrorBanner(error: $vm.error) .alert( "Permanently delete \(repository.owner.canonicalName)/\(repository.name)?", @@ -101,25 +102,30 @@ struct HgRepositorySettingsView: View { Text("\(repository.owner.canonicalName)/\(repository.name)") .font(.body.monospaced()) } + .themedRow() LabeledContent("Forge") { Text(repositoryForgeLabel(repository.service)) } + .themedRow() LabeledContent("Visibility") { Text(repositoryVisibilityLabel(viewModel.editedVisibility)) } + .themedRow() } Section("Repository Details") { TextField("Description", text: Bindable(viewModel).editedDescription, axis: .vertical) .lineLimit(3...6) + .themedRow() Picker("Visibility", selection: Bindable(viewModel).editedVisibility) { Text("Public").tag(Visibility.public) Text("Unlisted").tag(Visibility.unlisted) Text("Private").tag(Visibility.private) } + .themedRow() Button { Task { @@ -135,6 +141,7 @@ struct HgRepositorySettingsView: View { } } .disabled(viewModel.isSavingInfo || !viewModel.isInfoDirty) + .themedRow() } } @@ -147,9 +154,11 @@ struct HgRepositorySettingsView: View { ProgressView() Spacer() } + .themedRow() } else if viewModel.acls.isEmpty { Text("No access entries yet.") .foregroundStyle(.secondary) + .themedRow() } else { ForEach(viewModel.acls) { entry in HStack { @@ -167,6 +176,7 @@ struct HgRepositorySettingsView: View { } } } + .themedRow() } HStack { @@ -192,9 +202,11 @@ struct HgRepositorySettingsView: View { } .disabled(viewModel.isAddingACL || viewModel.newACLEntity.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) } + .themedRow() Text("Add a SourceHut user and choose read-only or read/write access.") .font(.caption) .foregroundStyle(.secondary) + .themedRow() } } @@ -202,9 +214,11 @@ struct HgRepositorySettingsView: View { private func featuresSection(_ viewModel: HgRepositorySettingsViewModel) -> some View { Section("Sensitive Settings") { Toggle("Hide this repository from public listings", isOn: Bindable(viewModel).editedNonPublishing) + .themedRow() Text("Changes stay pending until you save this section.") .font(.caption) .foregroundStyle(.secondary) + .themedRow() Button { Task { @@ -220,6 +234,7 @@ struct HgRepositorySettingsView: View { } } .disabled(viewModel.isSavingInfo || !viewModel.isInfoDirty) + .themedRow() } } @@ -230,16 +245,19 @@ struct HgRepositorySettingsView: View { .autocorrectionDisabled() .textInputAutocapitalization(.never) .disabled(true) + .themedRow() Text("Removing revisions is not available through the public hg.sr.ht API, so Hutch can’t do this yet.") .font(.caption) .foregroundStyle(.secondary) + .themedRow() Button("Remove Revision", role: .destructive) { // Not implemented: the hg.sr.ht API does not expose a histedit endpoint. // This button is disabled until the API supports revision removal. } .disabled(true) + .themedRow() } } @@ -258,6 +276,7 @@ struct HgRepositorySettingsView: View { } } .disabled(viewModel.isDeleting) + .themedRow() } header: { Text("Danger Zone") } diff --git a/Hutch/Views/Repositories/ReferencesListView.swift b/Hutch/Views/Repositories/ReferencesListView.swift index 6470a42..0e3be0a 100644 --- a/Hutch/Views/Repositories/ReferencesListView.swift +++ b/Hutch/Views/Repositories/ReferencesListView.swift @@ -10,6 +10,7 @@ struct ReferencesListView: View { ForEach(viewModel.branches, id: \.name) { ref in ReferenceRow(reference: ref, prefix: "refs/heads/") } + .themedRow() } } @@ -18,9 +19,11 @@ struct ReferencesListView: View { ForEach(viewModel.tags, id: \.name) { ref in ReferenceRow(reference: ref, prefix: "refs/tags/") } + .themedRow() } } } + .themedList() .listStyle(.insetGrouped) .overlay { if viewModel.isLoadingRefs, viewModel.branches.isEmpty, viewModel.tags.isEmpty { diff --git a/Hutch/Views/Repositories/RepositoryACLView.swift b/Hutch/Views/Repositories/RepositoryACLView.swift index 0368b4f..29ded84 100644 --- a/Hutch/Views/Repositories/RepositoryACLView.swift +++ b/Hutch/Views/Repositories/RepositoryACLView.swift @@ -6,6 +6,7 @@ struct RepositoryACLView: View { let showsDoneButton: Bool @Environment(\.dismiss) private var dismiss + @Environment(\.isAMOLEDTheme) private var isAMOLED @State private var viewModel: RepositoryACLViewModel? @State private var pendingDeletion: RepositoryACLEntry? @State private var showAddSheet = false @@ -70,7 +71,7 @@ struct RepositoryACLView: View { Text("Only the repository owner currently has access.") } .frame(maxWidth: .infinity) - .listRowBackground(Color.clear) + .listRowBackground(isAMOLED ? Color.black : Color.clear) } else { Section { ForEach(viewModel.visibleEntries) { entry in @@ -86,9 +87,11 @@ struct RepositoryACLView: View { } ) } + .themedRow() } } } + .themedList() .listStyle(.insetGrouped) .refreshable { await viewModel.load() @@ -192,11 +195,13 @@ private struct RepositoryACLAddUserView: View { TextField("Username or ~username", text: $viewModel.addUsername) .autocorrectionDisabled() .textInputAutocapitalization(.never) + .themedRow() if let validation = inlineValidationMessage { Text(validation) .font(.caption) .foregroundStyle(.secondary) + .themedRow() } } @@ -207,8 +212,10 @@ private struct RepositoryACLAddUserView: View { } } .pickerStyle(.segmented) + .themedRow() } } + .themedList() .navigationTitle("Add User") .navigationBarTitleDisplayMode(.inline) .toolbar { diff --git a/Hutch/Views/Repositories/RepositoryListView.swift b/Hutch/Views/Repositories/RepositoryListView.swift index 0258a58..08ebb5c 100644 --- a/Hutch/Views/Repositories/RepositoryListView.swift +++ b/Hutch/Views/Repositories/RepositoryListView.swift @@ -78,6 +78,7 @@ struct RepositoryListView: View { .foregroundStyle(.secondary) } .listRowSeparator(.hidden) + .themedRow() } ForEach(viewModel.repositories) { repo in @@ -89,6 +90,7 @@ struct RepositoryListView: View { } .alignmentGuide(.listRowSeparatorLeading) { _ in 0 } } + .themedRow() if viewModel.isLoadingMore { HStack { @@ -97,6 +99,7 @@ struct RepositoryListView: View { Spacer() } .listRowSeparator(.hidden) + .themedRow() } } .themedList() @@ -223,16 +226,20 @@ private struct CreateRepositorySheet: View { Text(service.displayName).tag(service) } } + .themedRow() TextField("Repository name", text: $name) .textInputAutocapitalization(.never) .autocorrectionDisabled() + .themedRow() TextField("Short description (optional)", text: $description, axis: .vertical) .lineLimit(2...4) + .themedRow() Picker("Visibility", selection: $visibility) { Text("Public").tag(Visibility.public) Text("Unlisted").tag(Visibility.unlisted) Text("Private").tag(Visibility.private) } + .themedRow() } Section("Import Existing Repository") { @@ -241,16 +248,20 @@ private struct CreateRepositorySheet: View { .textInputAutocapitalization(.never) .autocorrectionDisabled() .keyboardType(.URL) + .themedRow() Text("Import an existing Git repository from a remote URL.") .font(.footnote) .foregroundStyle(.secondary) + .themedRow() } else { Text("Importing a Mercurial repository from a remote URL is not available through the public API.") .font(.footnote) .foregroundStyle(.secondary) + .themedRow() } } } + .themedList() .navigationTitle(service == .git ? "New Git Repository" : "New Mercurial Repository") .navigationBarTitleDisplayMode(.inline) .toolbar { diff --git a/Hutch/Views/Repositories/RepositorySettingsView.swift b/Hutch/Views/Repositories/RepositorySettingsView.swift index 4736619..44b50ae 100644 --- a/Hutch/Views/Repositories/RepositorySettingsView.swift +++ b/Hutch/Views/Repositories/RepositorySettingsView.swift @@ -51,6 +51,7 @@ struct RepositorySettingsView: View { visibilitySection(viewModel) deleteSection(viewModel) } + .themedList() .srhtErrorBanner(error: $vm.error) .alert( visibilityConfirmationTitle(for: viewModel), @@ -97,15 +98,18 @@ struct RepositorySettingsView: View { Text("\(viewModel.repository.owner.canonicalName)/\(viewModel.repository.name)") .font(.body.monospaced()) } + .themedRow() LabeledContent("Default Branch") { Text(viewModel.currentDefaultBranchName) .font(.body.monospaced()) } + .themedRow() LabeledContent("Visibility") { Text(repositoryVisibilityLabel(viewModel.repository.visibility)) } + .themedRow() } } @@ -115,22 +119,27 @@ struct RepositorySettingsView: View { TextField("Repository name", text: Bindable(viewModel).editedName) .autocorrectionDisabled() .textInputAutocapitalization(.never) + .themedRow() TextField("Description", text: Bindable(viewModel).editedDescription, axis: .vertical) .lineLimit(2...4) + .themedRow() if let metadataValidationMessage = viewModel.metadataValidationMessage { Text(metadataValidationMessage) .font(.caption) .foregroundStyle(.red) + .themedRow() } else if viewModel.normalizedEditedName != viewModel.repository.name { Text("Changing the repository name updates the repository URL.") .font(.caption) .foregroundStyle(.secondary) + .themedRow() } else { Text("Name and description stay pending until you save this section.") .font(.caption) .foregroundStyle(.secondary) + .themedRow() } Button { @@ -153,6 +162,7 @@ struct RepositorySettingsView: View { !viewModel.isMetadataDirty || viewModel.metadataValidationMessage != nil ) + .themedRow() } header: { Text("Repository Details") } @@ -165,10 +175,12 @@ struct RepositorySettingsView: View { Text(viewModel.currentDefaultBranchName) .font(.body.monospaced()) } + .themedRow() if viewModel.branches.isEmpty { Text("This repository doesn't have any branches yet.") .foregroundStyle(.secondary) + .themedRow() } else { Picker("Branch", selection: Bindable(viewModel).editedHead) { ForEach(viewModel.availableBranchNames, id: \.self) { branch in @@ -177,10 +189,12 @@ struct RepositorySettingsView: View { .tag(branch) } } + .themedRow() Text("Changes stay pending until you set the new default branch.") .font(.caption) .foregroundStyle(.secondary) + .themedRow() Button { Task { @@ -202,6 +216,7 @@ struct RepositorySettingsView: View { !viewModel.isDefaultBranchDirty || viewModel.defaultBranchValidationMessage != nil ) + .themedRow() } } header: { Text("Default Branch") @@ -216,10 +231,12 @@ struct RepositorySettingsView: View { Text("Unlisted").tag(Visibility.unlisted) Text("Private").tag(Visibility.private) } + .themedRow() Text("Visibility changes apply immediately after you confirm them.") .font(.caption) .foregroundStyle(.secondary) + .themedRow() Button { showVisibilityConfirmation = true @@ -233,6 +250,7 @@ struct RepositorySettingsView: View { } } .disabled(viewModel.isMutating || !viewModel.isVisibilityDirty) + .themedRow() } header: { Text("Sensitive Settings") } @@ -253,6 +271,7 @@ struct RepositorySettingsView: View { } } .disabled(viewModel.isMutating) + .themedRow() } header: { Text("Danger Zone") } diff --git a/Hutch/Views/Settings/SettingsView.swift b/Hutch/Views/Settings/SettingsView.swift index 44b3ba5..4ed9216 100644 --- a/Hutch/Views/Settings/SettingsView.swift +++ b/Hutch/Views/Settings/SettingsView.swift @@ -60,11 +60,13 @@ struct SettingsView: View { Text(theme.label).tag(theme) } } + .themedRow() Picker("Density", selection: $displayDensity) { ForEach(DisplayDensity.allCases) { density in Text(density.label).tag(density) } } + .themedRow() } header: { Text("Appearance") } footer: { @@ -76,10 +78,12 @@ struct SettingsView: View { private func behaviorSection() -> some View { Section { Toggle("Swipe actions", isOn: $swipeActionsEnabled) + .themedRow() Toggle("Contribution graphs", isOn: $contributionGraphsEnabled) .onChange(of: contributionGraphsEnabled) { _, newValue in ContributionWidgetContextStore.setEnabled(newValue) } + .themedRow() } header: { Text("Behavior") } footer: { @@ -101,12 +105,14 @@ struct SettingsView: View { } } .alignmentGuide(.listRowSeparatorLeading) { _ in 0 } + .themedRow() Button { showAccountSwitcher = true } label: { Label("Manage Accounts", systemImage: "person.2") } + .themedRow() HStack { Image(systemName: "lock.shield") @@ -116,14 +122,17 @@ struct SettingsView: View { .foregroundStyle(.secondary) } .alignmentGuide(.listRowSeparatorLeading) { _ in 0 } + .themedRow() Button("Reset App Data", role: .destructive) { pendingDestructiveAction = .resetAppData } + .themedRow() Button("Sign Out", role: .destructive) { pendingDestructiveAction = .signOut } + .themedRow() } header: { Text("Authentication") } footer: { @@ -139,6 +148,7 @@ struct SettingsView: View { } label: { SwiftUI.Label("About Hutch", systemImage: "info.circle") } + .themedRow() } } } @@ -211,15 +221,18 @@ private struct AboutView: View { .foregroundStyle(.secondary) } .padding(.vertical, 4) + .themedRow() LabeledContent("Version", value: version) .onTapGesture { developerRevealCount = min(developerRevealCount + 1, 5) } + .themedRow() LabeledContent("Build", value: build) .onTapGesture { developerRevealCount = min(developerRevealCount + 1, 5) } + .themedRow() } footer: { Text(developerRevealFooterText) } @@ -228,34 +241,41 @@ private struct AboutView: View { Link(destination: URL(string: "https://sr.ht")!) { SwiftUI.Label("SourceHut", systemImage: "link") } + .themedRow() Link(destination: URL(string: "https://man.sr.ht")!) { SwiftUI.Label("SourceHut Manuals", systemImage: "book") } + .themedRow() Link(destination: URL(string: "https://sr.ht/~ccleberg/Hutch")!) { SwiftUI.Label("Project Repository", systemImage: "folder") } + .themedRow() } Section("Support") { Link(destination: URL(string: "mailto:[email protected]")!) { SwiftUI.Label("Email Support", systemImage: "envelope") } + .themedRow() } Section("Privacy") { Text("Hutch uses your SourceHut personal access token to make requests on your behalf. The token is stored locally in the iOS keychain.") .font(.subheadline) .foregroundStyle(.secondary) + .themedRow() Link(destination: URL(string: "https://zerolabs.sh/hutch/privacy-policy/")!) { SwiftUI.Label("Privacy Policy", systemImage: "hand.raised") } + .themedRow() } Section("Acknowledgements") { Text("Built for SourceHut users who want quick access to repositories, builds, and tickets on iOS.") .font(.subheadline) .foregroundStyle(.secondary) + .themedRow() } if developerToolsVisible { @@ -264,18 +284,21 @@ private struct AboutView: View { get: { appState.isDebugModeEnabled }, set: { appState.isDebugModeEnabled = $0 } )) + .themedRow() NavigationLink { HomePrototypeView() } label: { SwiftUI.Label("Home Prototype", systemImage: "house") } + .themedRow() NavigationLink { WorkPrototypeView() } label: { SwiftUI.Label("Work Prototype", systemImage: "tray.full") } + .themedRow() } header: { Text("Developer") } footer: { diff --git a/Hutch/Views/SystemStatus/SystemStatusView.swift b/Hutch/Views/SystemStatus/SystemStatusView.swift index 08a46eb..a3ce5b3 100644 --- a/Hutch/Views/SystemStatus/SystemStatusView.swift +++ b/Hutch/Views/SystemStatus/SystemStatusView.swift @@ -36,6 +36,7 @@ struct SystemStatusView: View { Label(staleDataMessage, systemImage: "clock.arrow.trianglehead.counterclockwise.rotate.90") .font(.subheadline) .foregroundStyle(.secondary) + .themedRow() } } @@ -47,6 +48,7 @@ struct SystemStatusView: View { recentIncidentsSection(viewModel.recentIncidents) } + .themedList() .listStyle(.insetGrouped) .refreshable { await viewModel.load(forceRefresh: true) @@ -99,6 +101,7 @@ struct SystemStatusView: View { .font(.subheadline.weight(.medium)) } .padding(.vertical, 4) + .themedRow() } } @@ -119,6 +122,7 @@ struct SystemStatusView: View { } .padding(.vertical, 2) } + .themedRow() } } @@ -129,6 +133,7 @@ struct SystemStatusView: View { ForEach(incidents) { incident in incidentRow(incident) } + .themedRow() } } } @@ -142,10 +147,12 @@ struct SystemStatusView: View { systemImage: "clock.arrow.trianglehead.counterclockwise.rotate.90", description: Text("The status feed didn’t return any recent incidents.") ) + .themedRow() } else { ForEach(incidents) { incident in incidentRow(incident) } + .themedRow() } } } diff --git a/Hutch/Views/Tickets/TicketDetailView.swift b/Hutch/Views/Tickets/TicketDetailView.swift index 32ef89c..0028083 100644 --- a/Hutch/Views/Tickets/TicketDetailView.swift +++ b/Hutch/Views/Tickets/TicketDetailView.swift @@ -739,8 +739,10 @@ private struct ResolveSheet: View { } .pickerStyle(.inline) .labelsHidden() + .themedRow() } } + .themedList() .navigationTitle("Resolve Ticket") .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -801,6 +803,7 @@ private struct AssignSheet: View { .buttonStyle(.plain) } } + .themedRow() } } @@ -809,6 +812,7 @@ private struct AssignSheet: View { .textContentType(.username) .autocorrectionDisabled() .textInputAutocapitalization(.never) + .themedRow() Button("Add Assignee") { let name = username.trimmingCharacters(in: .whitespacesAndNewlines) @@ -822,8 +826,10 @@ private struct AssignSheet: View { username.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || viewModel.isPerformingAction ) + .themedRow() } } + .themedList() .navigationTitle("Assignees") .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -877,7 +883,9 @@ private struct LabelsSheet: View { } } } + .themedRow() } + .themedList() } } .navigationTitle("Labels") @@ -917,11 +925,14 @@ private struct CreateLabelSheet: View { Section("Label Details") { TextField("Label name", text: $labelName) .autocorrectionDisabled() + .themedRow() } Section("Colors") { ColorPicker("Background color", selection: $backgroundColor, supportsOpacity: false) + .themedRow() ColorPicker("Text color", selection: $foregroundColor, supportsOpacity: false) + .themedRow() } Section("Preview") { @@ -936,8 +947,10 @@ private struct CreateLabelSheet: View { .clipShape(Capsule()) Spacer() } + .themedRow() } } + .themedList() .navigationTitle("New Label") .navigationBarTitleDisplayMode(.inline) .toolbar { diff --git a/Hutch/Views/Tickets/TicketListView.swift b/Hutch/Views/Tickets/TicketListView.swift index e58d194..940a282 100644 --- a/Hutch/Views/Tickets/TicketListView.swift +++ b/Hutch/Views/Tickets/TicketListView.swift @@ -6,6 +6,7 @@ struct TicketListView: View { @AppStorage(AppStorageKeys.swipeActionsEnabled, store: .standard) private var swipeActionsEnabled = true @Environment(AppState.self) private var appState + @Environment(\.isAMOLEDTheme) private var isAMOLED @Environment(\.dismiss) private var dismiss @Environment(\.openURL) private var openURL @State private var tracker: TrackerSummary @@ -277,7 +278,7 @@ struct TicketListView: View { vm.deleteSavedFilter(savedFilter) } .listRowInsets(EdgeInsets()) - .listRowBackground(Color.clear) + .listRowBackground(isAMOLED ? Color.black : Color.clear) .listRowSeparator(.hidden) ForEach(viewModel.filteredTickets) { ticket in @@ -344,6 +345,7 @@ struct TicketListView: View { await viewModel.loadMoreIfNeeded(currentItem: ticket) } } + .themedRow() if viewModel.isLoadingMore { HStack { @@ -352,9 +354,11 @@ struct TicketListView: View { Spacer() } .listRowSeparator(.hidden) + .themedRow() } } } + .themedList() .listStyle(.plain) .listSectionSpacing(.compact) .searchable( @@ -636,10 +640,13 @@ private struct CreateTicketSheet: View { Form { Section("Ticket Details") { TextField("Title", text: $subject) + .themedRow() TextField("Description (optional)", text: $descriptionText, axis: .vertical) .lineLimit(6...12) + .themedRow() } } + .themedList() .navigationTitle("New Ticket") .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -705,7 +712,9 @@ private struct TicketLabelsSheet: View { } } } + .themedRow() } + .themedList() } } else { ContentUnavailableView( @@ -921,6 +930,7 @@ private struct TicketFilterLabelsSheet: View { } } .padding(.vertical, 4) + .themedRow() } } @@ -940,8 +950,10 @@ private struct TicketFilterLabelsSheet: View { } .buttonStyle(.plain) } + .themedRow() } } + .themedList() } } .navigationTitle("Filter Labels") @@ -982,8 +994,10 @@ private struct SaveTicketFilterSheet: View { Section("Name") { TextField("Filter name", text: $name) .textInputAutocapitalization(.words) + .themedRow() } } + .themedList() .navigationTitle("Save Filter") .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -1085,6 +1099,7 @@ private struct BulkResolveSheet: View { Text("\(viewModel.selectedTicketCount) ticket\(viewModel.selectedTicketCount == 1 ? "" : "s") selected") .font(.subheadline) .foregroundStyle(.secondary) + .themedRow() } Section("Resolution") { @@ -1095,8 +1110,10 @@ private struct BulkResolveSheet: View { } .pickerStyle(.inline) .labelsHidden() + .themedRow() } } + .themedList() .navigationTitle("Close Tickets") .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -1133,6 +1150,7 @@ private struct BulkAssignSheet: View { Text("\(viewModel.selectedTicketCount) ticket\(viewModel.selectedTicketCount == 1 ? "" : "s") selected") .font(.subheadline) .foregroundStyle(.secondary) + .themedRow() } if let currentUser { @@ -1146,6 +1164,7 @@ private struct BulkAssignSheet: View { } } .disabled(viewModel.selectedTicketCount == 0 || viewModel.isPerformingAction) + .themedRow() } } @@ -1154,8 +1173,10 @@ private struct BulkAssignSheet: View { .textContentType(.username) .autocorrectionDisabled() .textInputAutocapitalization(.never) + .themedRow() } } + .themedList() .navigationTitle("Assign Tickets") .navigationBarTitleDisplayMode(.inline) .toolbar { diff --git a/Hutch/Views/Tickets/TrackerListView.swift b/Hutch/Views/Tickets/TrackerListView.swift index db0fdf2..9ca3340 100644 --- a/Hutch/Views/Tickets/TrackerListView.swift +++ b/Hutch/Views/Tickets/TrackerListView.swift @@ -162,6 +162,7 @@ struct TrackerListView: View { await viewModel.loadMoreIfNeeded(currentItem: tracker) } } + .themedRow() if viewModel.isLoadingMore { HStack { @@ -170,8 +171,10 @@ struct TrackerListView: View { Spacer() } .listRowSeparator(.hidden) + .themedRow() } } + .themedList() .listStyle(.plain) .searchable( text: $vm.searchText, diff --git a/Hutch/Views/Tickets/TrackerManagementView.swift b/Hutch/Views/Tickets/TrackerManagementView.swift index cf923b6..4e04bbd 100644 --- a/Hutch/Views/Tickets/TrackerManagementView.swift +++ b/Hutch/Views/Tickets/TrackerManagementView.swift @@ -654,22 +654,27 @@ struct TrackerEditorSheet: View { TextField("Tracker name", text: $name) .textInputAutocapitalization(.never) .autocorrectionDisabled() + .themedRow() TextField("Short description (optional)", text: $description, axis: .vertical) .lineLimit(2...4) + .themedRow() Picker("Visibility", selection: $visibility) { Text("Public").tag(Visibility.public) Text("Unlisted").tag(Visibility.unlisted) Text("Private").tag(Visibility.private) } + .themedRow() } if let error, !error.isEmpty { Section { Text(error) .foregroundStyle(.red) + .themedRow() } } } + .themedList() .navigationTitle(title) .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -718,10 +723,12 @@ struct TrackerACLManagementSheet: View { List { Section("Default Access") { TrackerPermissionSummary(permissions: viewModel.defaultACL.permissions) + .themedRow() Button("Update Default ACL") { editingDefaultACL = true } .disabled(viewModel.isSavingACL) + .themedRow() } Section { @@ -731,9 +738,11 @@ struct TrackerACLManagementSheet: View { ProgressView() Spacer() } + .themedRow() } else if viewModel.acls.isEmpty { Text("No tracker-specific ACLs yet.") .foregroundStyle(.secondary) + .themedRow() } else { ForEach(viewModel.acls) { entry in VStack(alignment: .leading, spacing: 6) { @@ -756,6 +765,7 @@ struct TrackerACLManagementSheet: View { .tint(.blue) } } + .themedRow() } } header: { Text("User ACLs") @@ -763,6 +773,7 @@ struct TrackerACLManagementSheet: View { Text("Each ACL must include all five permission flags.") } } + .themedList() .navigationTitle("ACLs") .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -908,6 +919,7 @@ private struct TrackerACLEditorSheet: View { TextField("Username or ~username", text: $username) .autocorrectionDisabled() .textInputAutocapitalization(.never) + .themedRow() } permissionSection @@ -916,9 +928,11 @@ private struct TrackerACLEditorSheet: View { Section { Text(error) .foregroundStyle(.red) + .themedRow() } } } + .themedList() .navigationTitle(title) .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -950,10 +964,15 @@ private struct TrackerACLEditorSheet: View { private var permissionSection: some View { Section("Permissions") { Toggle("Browse", isOn: $browse) + .themedRow() Toggle("Submit", isOn: $submit) + .themedRow() Toggle("Comment", isOn: $comment) + .themedRow() Toggle("Edit", isOn: $edit) + .themedRow() Toggle("Triage", isOn: $triage) + .themedRow() } } @@ -1003,19 +1022,26 @@ private struct TrackerDefaultACLEditorSheet: View { Form { Section("Permissions") { Toggle("Browse", isOn: $browse) + .themedRow() Toggle("Submit", isOn: $submit) + .themedRow() Toggle("Comment", isOn: $comment) + .themedRow() Toggle("Edit", isOn: $edit) + .themedRow() Toggle("Triage", isOn: $triage) + .themedRow() } if let error, !error.isEmpty { Section { Text(error) .foregroundStyle(.red) + .themedRow() } } } + .themedList() .navigationTitle("Default ACL") .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -1073,6 +1099,7 @@ struct TrackerLabelManagementSheet: View { Text("Labels are managed here and reused throughout the tracker.") .font(.footnote) .foregroundStyle(.secondary) + .themedRow() } if viewModel.isLoadingLabels { @@ -1081,12 +1108,14 @@ struct TrackerLabelManagementSheet: View { ProgressView() Spacer() } + .themedRow() } else if viewModel.labels.isEmpty { ContentUnavailableView( "No Labels", systemImage: "tag", description: Text("Create labels for triage and organization.") ) + .themedRow() } else { ForEach(viewModel.labels) { label in Button { @@ -1110,8 +1139,10 @@ struct TrackerLabelManagementSheet: View { } } } + .themedRow() } } + .themedList() .navigationTitle("Labels") .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -1261,8 +1292,11 @@ private struct TrackerLabelEditorSheet: View { Form { Section("Details") { TextField("Label name", text: $name) + .themedRow() ColorPicker("Foreground", selection: $foregroundColor, supportsOpacity: false) + .themedRow() ColorPicker("Background", selection: $backgroundColor, supportsOpacity: false) + .themedRow() } Section("Preview") { @@ -1274,15 +1308,18 @@ private struct TrackerLabelEditorSheet: View { foregroundColor: foregroundColor.hexString ) ) + .themedRow() } if let error, !error.isEmpty { Section { Text(error) .foregroundStyle(.red) + .themedRow() } } } + .themedList() .navigationTitle(title) .navigationBarTitleDisplayMode(.inline) .toolbar { diff --git a/Hutch/Views/Work/WorkView.swift b/Hutch/Views/Work/WorkView.swift index a7fbd1c..2326236 100644 --- a/Hutch/Views/Work/WorkView.swift +++ b/Hutch/Views/Work/WorkView.swift @@ -11,6 +11,7 @@ struct WorkView: View { @AppStorage(AppStorageKeys.swipeActionsEnabled, store: .standard) private var swipeActionsEnabled = true @Environment(AppState.self) private var appState + @Environment(\.isAMOLEDTheme) private var isAMOLED @Environment(\.scenePhase) private var scenePhase @State private var viewModel: HomeViewModel? @State private var scope: Scope = .all @@ -74,6 +75,7 @@ struct WorkView: View { } } .padding(.vertical, 2) + .themedRow() } } @@ -88,6 +90,7 @@ struct WorkView: View { if workCount(viewModel) == 0 { Section { WorkCompactMessageRow(text: "Nothing to do", systemImage: "checkmark.circle") + .themedRow() } } } @@ -100,7 +103,7 @@ struct WorkView: View { } } .pickerStyle(.segmented) - .listRowBackground(Color.clear) + .listRowBackground(isAMOLED ? Color.black : Color.clear) .listRowInsets(EdgeInsets()) } } @@ -110,9 +113,11 @@ struct WorkView: View { Section { if isLoadingUnread(viewModel) { WorkLoadingRow(label: "Loading unread threads") + .themedRow() } else if viewModel.unreadInboxThreads.isEmpty { if compactWhenEmpty { WorkCompactMessageRow(text: "No unread threads", systemImage: "tray") + .themedRow() } } else { ForEach(viewModel.unreadInboxThreads) { thread in @@ -137,6 +142,7 @@ struct WorkView: View { } } } + .themedRow() } } header: { Text("Unread Threads") @@ -155,8 +161,10 @@ struct WorkView: View { Section { if viewModel.isLoadingAssignedTickets && viewModel.assignedTickets.isEmpty { WorkLoadingRow(label: "Loading assigned tickets") + .themedRow() } else if viewModel.assignedTickets.isEmpty { WorkCompactMessageRow(text: "No assigned tickets", systemImage: "person.crop.circle.badge.checkmark") + .themedRow() } else { ForEach(viewModel.assignedTickets) { ticket in NavigationLink { @@ -200,6 +208,7 @@ struct WorkView: View { } } } + .themedRow() } } header: { Text("Assigned Tickets") |
