diff options
| author | Christian Cleberg <[email protected]> | 2026-04-12 19:47:57 -0500 |
|---|---|---|
| committer | Christian Cleberg <[email protected]> | 2026-04-12 19:47:57 -0500 |
| commit | 1354f2464180ece6d13296a543a964ddf6d2d393 (patch) | |
| tree | 5cf6ddb89c950941067e2a1080a150911d5fb4e7 /Hutch | |
| parent | 4ad1b74c5887ddf0445fde29ffacfc6eb5d49b3f (diff) | |
| download | hutch-1354f2464180ece6d13296a543a964ddf6d2d393.tar.gz hutch-1354f2464180ece6d13296a543a964ddf6d2d393.tar.bz2 hutch-1354f2464180ece6d13296a543a964ddf6d2d393.zip | |
feat: add theme and high-density display modes
Refs: https://todo.sr.ht/~ccleberg/hutch/24
Implements: https://todo.sr.ht/~ccleberg/hutch/25
Diffstat (limited to 'Hutch')
| -rw-r--r-- | Hutch/App/AppStorageKeys.swift | 2 | ||||
| -rw-r--r-- | Hutch/App/HutchApp.swift | 5 | ||||
| -rw-r--r-- | Hutch/App/ThemeManager.swift | 76 | ||||
| -rw-r--r-- | Hutch/Views/Builds/BuildDetailView.swift | 1 | ||||
| -rw-r--r-- | Hutch/Views/Builds/BuildListView.swift | 1 | ||||
| -rw-r--r-- | Hutch/Views/Home/HomeView.swift | 1 | ||||
| -rw-r--r-- | Hutch/Views/Inbox/InboxView.swift | 1 | ||||
| -rw-r--r-- | Hutch/Views/Lookup/LookupView.swift | 1 | ||||
| -rw-r--r-- | Hutch/Views/More/MoreView.swift | 1 | ||||
| -rw-r--r-- | Hutch/Views/More/ProfileView.swift | 1 | ||||
| -rw-r--r-- | Hutch/Views/Pastes/PasteDetailView.swift | 1 | ||||
| -rw-r--r-- | Hutch/Views/Pastes/PasteListView.swift | 1 | ||||
| -rw-r--r-- | Hutch/Views/Repositories/RepositoryListView.swift | 1 | ||||
| -rw-r--r-- | Hutch/Views/Settings/SettingsView.swift | 25 |
14 files changed, 118 insertions, 0 deletions
diff --git a/Hutch/App/AppStorageKeys.swift b/Hutch/App/AppStorageKeys.swift index ff309d4..eaf54f0 100644 --- a/Hutch/App/AppStorageKeys.swift +++ b/Hutch/App/AppStorageKeys.swift @@ -13,4 +13,6 @@ enum AppStorageKeys { static let homeBuildsExpanded = "homeBuildsExpanded" static let buildsAutoRefreshInterval = "buildsAutoRefreshInterval" static let buildsRepoFilter = "buildsRepoFilter" + static let appTheme = "appTheme" + static let displayDensity = "displayDensity" } diff --git a/Hutch/App/HutchApp.swift b/Hutch/App/HutchApp.swift index e65833c..016ddd0 100644 --- a/Hutch/App/HutchApp.swift +++ b/Hutch/App/HutchApp.swift @@ -4,12 +4,17 @@ import SwiftUI struct HutchApp: App { @State private var appState = AppState() @State private var networkMonitor = NetworkMonitor() + @AppStorage(AppStorageKeys.appTheme) private var appTheme: AppTheme = .system + @AppStorage(AppStorageKeys.displayDensity) private var displayDensity: DisplayDensity = .standard var body: some Scene { WindowGroup { RootView() .environment(appState) .environment(networkMonitor) + .environment(\.displayDensity, displayDensity) + .environment(\.defaultMinListRowHeight, displayDensity == .compact ? 36 : 44) + .preferredColorScheme(appTheme.colorScheme) .onOpenURL { url in if let link = DeepLink(url: url) { appState.pendingDeepLink = link diff --git a/Hutch/App/ThemeManager.swift b/Hutch/App/ThemeManager.swift new file mode 100644 index 0000000..ee74921 --- /dev/null +++ b/Hutch/App/ThemeManager.swift @@ -0,0 +1,76 @@ +import SwiftUI + +// MARK: - Theme & Density Types + +enum AppTheme: String, CaseIterable, Identifiable { + case system + case light + case dark + + var id: String { rawValue } + + var label: String { + switch self { + case .system: "System" + case .light: "Light" + case .dark: "Dark" + } + } + + var colorScheme: ColorScheme? { + switch self { + case .system: nil + case .light: .light + case .dark: .dark + } + } +} + +enum DisplayDensity: String, CaseIterable, Identifiable { + case standard + case compact + + var id: String { rawValue } + + var label: String { + switch self { + case .standard: "Standard" + case .compact: "Compact" + } + } +} + +// MARK: - Environment Keys + +private struct DisplayDensityKey: EnvironmentKey { + static let defaultValue: DisplayDensity = .standard +} + +extension EnvironmentValues { + var displayDensity: DisplayDensity { + get { self[DisplayDensityKey.self] } + set { self[DisplayDensityKey.self] = newValue } + } +} + +// MARK: - Themed List Modifier + +/// Combined modifier for List/Form that applies compact section spacing. +/// Apply once per List or Form. +struct ThemedListStyle: ViewModifier { + @Environment(\.displayDensity) private var density + + 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) + } +} + +extension View { + /// Apply themed appearance (density spacing) to a List or Form. + func themedList() -> some View { + modifier(ThemedListStyle()) + } +} diff --git a/Hutch/Views/Builds/BuildDetailView.swift b/Hutch/Views/Builds/BuildDetailView.swift index 5070536..2f0bc6a 100644 --- a/Hutch/Views/Builds/BuildDetailView.swift +++ b/Hutch/Views/Builds/BuildDetailView.swift @@ -392,6 +392,7 @@ private struct EditResubmitBuildSheet: View { .disabled(manifest.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || viewModel.isSubmittingEditedBuild) } } + .themedList() } } } diff --git a/Hutch/Views/Builds/BuildListView.swift b/Hutch/Views/Builds/BuildListView.swift index b53d64b..2423406 100644 --- a/Hutch/Views/Builds/BuildListView.swift +++ b/Hutch/Views/Builds/BuildListView.swift @@ -162,6 +162,7 @@ struct BuildListView: View { .listRowSeparator(.hidden) } } + .themedList() .listStyle(.plain) .searchable( text: $vm.searchText, diff --git a/Hutch/Views/Home/HomeView.swift b/Hutch/Views/Home/HomeView.swift index 180f0f3..bbae296 100644 --- a/Hutch/Views/Home/HomeView.swift +++ b/Hutch/Views/Home/HomeView.swift @@ -65,6 +65,7 @@ struct HomeView: View { assignedTicketsSection(viewModel) recentBuildsSection(viewModel) } + .themedList() .listStyle(.insetGrouped) .overlay { if viewModel.isLoadingProjects && viewModel.isLoadingAssignedTickets && viewModel.isLoadingRecentBuilds && diff --git a/Hutch/Views/Inbox/InboxView.swift b/Hutch/Views/Inbox/InboxView.swift index 2447a6e..f8753d7 100644 --- a/Hutch/Views/Inbox/InboxView.swift +++ b/Hutch/Views/Inbox/InboxView.swift @@ -68,6 +68,7 @@ struct InboxView: View { } } } + .themedList() .searchable( text: $vm.searchText, placement: .navigationBarDrawer(displayMode: .always), diff --git a/Hutch/Views/Lookup/LookupView.swift b/Hutch/Views/Lookup/LookupView.swift index e0adc6b..bfb532f 100644 --- a/Hutch/Views/Lookup/LookupView.swift +++ b/Hutch/Views/Lookup/LookupView.swift @@ -411,6 +411,7 @@ struct LookupView: View { } } } + .themedList() .formStyle(.grouped) .srhtErrorBanner(error: $vm.error) .sheet(item: vm.resultBinding) { result in diff --git a/Hutch/Views/More/MoreView.swift b/Hutch/Views/More/MoreView.swift index 6325954..caec3d5 100644 --- a/Hutch/Views/More/MoreView.swift +++ b/Hutch/Views/More/MoreView.swift @@ -63,6 +63,7 @@ struct MoreView: View { Text("These SourceHut services are not supported in-app, as the SourceHut API does not support them, and will open in your browser.") } } + .themedList() .navigationTitle("More") .refreshable { await ensureViewModel().loadSystemStatus(forceRefresh: true) diff --git a/Hutch/Views/More/ProfileView.swift b/Hutch/Views/More/ProfileView.swift index 53c5ce3..673b298 100644 --- a/Hutch/Views/More/ProfileView.swift +++ b/Hutch/Views/More/ProfileView.swift @@ -87,6 +87,7 @@ struct ProfileView: View { patSection(viewModel) } } + .themedList() .overlay { if viewModel.isLoading, viewModel.profile == nil { SRHTLoadingStateView(message: "Loading profileā¦") diff --git a/Hutch/Views/Pastes/PasteDetailView.swift b/Hutch/Views/Pastes/PasteDetailView.swift index 19cfd52..a73e784 100644 --- a/Hutch/Views/Pastes/PasteDetailView.swift +++ b/Hutch/Views/Pastes/PasteDetailView.swift @@ -259,6 +259,7 @@ private struct PasteVisibilitySheet: View { ProgressView() } } + .themedList() } } diff --git a/Hutch/Views/Pastes/PasteListView.swift b/Hutch/Views/Pastes/PasteListView.swift index 22f6ff9..ffd49ab 100644 --- a/Hutch/Views/Pastes/PasteListView.swift +++ b/Hutch/Views/Pastes/PasteListView.swift @@ -110,6 +110,7 @@ struct PasteListView: View { .listRowSeparator(.hidden) } } + .themedList() .listStyle(.plain) .searchable( text: $vm.searchText, diff --git a/Hutch/Views/Repositories/RepositoryListView.swift b/Hutch/Views/Repositories/RepositoryListView.swift index aec0357..fb090b2 100644 --- a/Hutch/Views/Repositories/RepositoryListView.swift +++ b/Hutch/Views/Repositories/RepositoryListView.swift @@ -84,6 +84,7 @@ struct RepositoryListView: View { .listRowSeparator(.hidden) } } + .themedList() .listStyle(.plain) .searchable(text: $vm.searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: "Search repositories") .overlay { diff --git a/Hutch/Views/Settings/SettingsView.swift b/Hutch/Views/Settings/SettingsView.swift index 4fb3f9c..5abac90 100644 --- a/Hutch/Views/Settings/SettingsView.swift +++ b/Hutch/Views/Settings/SettingsView.swift @@ -2,16 +2,20 @@ import SwiftUI struct SettingsView: View { @Environment(AppState.self) private var appState + @AppStorage(AppStorageKeys.appTheme) private var appTheme: AppTheme = .system + @AppStorage(AppStorageKeys.displayDensity) private var displayDensity: DisplayDensity = .standard @AppStorage(AppStorageKeys.swipeActionsEnabled) private var swipeActionsEnabled = true @AppStorage(AppStorageKeys.contributionGraphsEnabled) private var contributionGraphsEnabled = true @State private var pendingDestructiveAction: SettingsDestructiveAction? var body: some View { Form { + appearanceSection() behaviorSection() authenticationSection() aboutSection() } + .themedList() .navigationTitle("Settings") .alert( pendingDestructiveAction?.title ?? "", @@ -45,6 +49,26 @@ struct SettingsView: View { } @ViewBuilder + private func appearanceSection() -> some View { + Section { + Picker("Theme", selection: $appTheme) { + ForEach(AppTheme.allCases) { theme in + Text(theme.label).tag(theme) + } + } + Picker("Density", selection: $displayDensity) { + ForEach(DisplayDensity.allCases) { density in + Text(density.label).tag(density) + } + } + } header: { + Text("Appearance") + } footer: { + Text("Compact density reduces spacing throughout the app.") + } + } + + @ViewBuilder private func behaviorSection() -> some View { Section { Toggle("Swipe actions", isOn: $swipeActionsEnabled) @@ -192,6 +216,7 @@ private struct AboutView: View { .foregroundStyle(.secondary) } } + .themedList() .navigationTitle("About") .navigationBarTitleDisplayMode(.inline) } |
