Commit 59b53afae0

59b53afae046158c24eae03bb7e21d535dd6f8ad

parent: 7718ac2b7f

Unsigned

cmc <hello@cleberg.net> · 2026-04-13 00:47 UTC

feat: add theme and high-density display modes

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

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 = 38;
518 CURRENT_PROJECT_VERSION = 39;
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 = 2.16.2;
535 MARKETING_VERSION = 2.17.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 = 38;
555 CURRENT_PROJECT_VERSION = 39;
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 = 2.16.2;
572 MARKETING_VERSION = 2.17.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 = 38;
635 CURRENT_PROJECT_VERSION = 39;
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 = 2.16.2;
645 MARKETING_VERSION = 2.17.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 = 38;
664 CURRENT_PROJECT_VERSION = 39;
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 = 2.16.2;
674 MARKETING_VERSION = 2.17.0;
675675 PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
676676 PRODUCT_NAME = "$(TARGET_NAME)";
677677 SKIP_INSTALL = YES;
Hutch/App/AppStorageKeys.swift +2
@@ -13,4 +13,6 @@ enum AppStorageKeys {
1313 static let homeBuildsExpanded = "homeBuildsExpanded"
1414 static let buildsAutoRefreshInterval = "buildsAutoRefreshInterval"
1515 static let buildsRepoFilter = "buildsRepoFilter"
16 static let appTheme = "appTheme"
17 static let displayDensity = "displayDensity"
1618}
Hutch/App/HutchApp.swift +5
@@ -4,12 +4,17 @@ import SwiftUI
44struct HutchApp: App {
55 @State private var appState = AppState()
66 @State private var networkMonitor = NetworkMonitor()
7 @AppStorage(AppStorageKeys.appTheme) private var appTheme: AppTheme = .system
8 @AppStorage(AppStorageKeys.displayDensity) private var displayDensity: DisplayDensity = .standard
79
810 var body: some Scene {
911 WindowGroup {
1012 RootView()
1113 .environment(appState)
1214 .environment(networkMonitor)
15 .environment(\.displayDensity, displayDensity)
16 .environment(\.defaultMinListRowHeight, displayDensity == .compact ? 36 : 44)
17 .preferredColorScheme(appTheme.colorScheme)
1318 .onOpenURL { url in
1419 if let link = DeepLink(url: url) {
1520 appState.pendingDeepLink = link
Hutch/App/ThemeManager.swift added +76
@@ -0,0 +1,76 @@
1import SwiftUI
2
3// MARK: - Theme & Density Types
4
5enum AppTheme: String, CaseIterable, Identifiable {
6 case system
7 case light
8 case dark
9
10 var id: String { rawValue }
11
12 var label: String {
13 switch self {
14 case .system: "System"
15 case .light: "Light"
16 case .dark: "Dark"
17 }
18 }
19
20 var colorScheme: ColorScheme? {
21 switch self {
22 case .system: nil
23 case .light: .light
24 case .dark: .dark
25 }
26 }
27}
28
29enum DisplayDensity: String, CaseIterable, Identifiable {
30 case standard
31 case compact
32
33 var id: String { rawValue }
34
35 var label: String {
36 switch self {
37 case .standard: "Standard"
38 case .compact: "Compact"
39 }
40 }
41}
42
43// MARK: - Environment Keys
44
45private struct DisplayDensityKey: EnvironmentKey {
46 static let defaultValue: DisplayDensity = .standard
47}
48
49extension EnvironmentValues {
50 var displayDensity: DisplayDensity {
51 get { self[DisplayDensityKey.self] }
52 set { self[DisplayDensityKey.self] = newValue }
53 }
54}
55
56// MARK: - Themed List Modifier
57
58/// Combined modifier for List/Form that applies compact section spacing.
59/// Apply once per List or Form.
60struct ThemedListStyle: ViewModifier {
61 @Environment(\.displayDensity) private var density
62
63 func body(content: Content) -> some View {
64 content
65 .listSectionSpacing(density == .compact ? .compact : .default)
66 .contentMargins(.vertical, density == .compact ? 2 : 8, for: .scrollContent)
67 .controlSize(density == .compact ? .small : .regular)
68 }
69}
70
71extension View {
72 /// Apply themed appearance (density spacing) to a List or Form.
73 func themedList() -> some View {
74 modifier(ThemedListStyle())
75 }
76}
Hutch/Views/Builds/BuildDetailView.swift +1
@@ -392,6 +392,7 @@ private struct EditResubmitBuildSheet: View {
392392 .disabled(manifest.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || viewModel.isSubmittingEditedBuild)
393393 }
394394 }
395 .themedList()
395396 }
396397 }
397398}
Hutch/Views/Builds/BuildListView.swift +1
@@ -162,6 +162,7 @@ struct BuildListView: View {
162162 .listRowSeparator(.hidden)
163163 }
164164 }
165 .themedList()
165166 .listStyle(.plain)
166167 .searchable(
167168 text: $vm.searchText,
Hutch/Views/Home/HomeView.swift +1
@@ -65,6 +65,7 @@ struct HomeView: View {
6565 assignedTicketsSection(viewModel)
6666 recentBuildsSection(viewModel)
6767 }
68 .themedList()
6869 .listStyle(.insetGrouped)
6970 .overlay {
7071 if viewModel.isLoadingProjects && viewModel.isLoadingAssignedTickets && viewModel.isLoadingRecentBuilds &&
Hutch/Views/Inbox/InboxView.swift +1
@@ -68,6 +68,7 @@ struct InboxView: View {
6868 }
6969 }
7070 }
71 .themedList()
7172 .searchable(
7273 text: $vm.searchText,
7374 placement: .navigationBarDrawer(displayMode: .always),
Hutch/Views/Lookup/LookupView.swift +1
@@ -411,6 +411,7 @@ struct LookupView: View {
411411 }
412412 }
413413 }
414 .themedList()
414415 .formStyle(.grouped)
415416 .srhtErrorBanner(error: $vm.error)
416417 .sheet(item: vm.resultBinding) { result in
Hutch/Views/More/MoreView.swift +1
@@ -63,6 +63,7 @@ struct MoreView: View {
6363 Text("These SourceHut services are not supported in-app, as the SourceHut API does not support them, and will open in your browser.")
6464 }
6565 }
66 .themedList()
6667 .navigationTitle("More")
6768 .refreshable {
6869 await ensureViewModel().loadSystemStatus(forceRefresh: true)
Hutch/Views/More/ProfileView.swift +1
@@ -87,6 +87,7 @@ struct ProfileView: View {
8787 patSection(viewModel)
8888 }
8989 }
90 .themedList()
9091 .overlay {
9192 if viewModel.isLoading, viewModel.profile == nil {
9293 SRHTLoadingStateView(message: "Loading profile…")
Hutch/Views/Pastes/PasteDetailView.swift +1
@@ -259,6 +259,7 @@ private struct PasteVisibilitySheet: View {
259259 ProgressView()
260260 }
261261 }
262 .themedList()
262263 }
263264 }
264265
Hutch/Views/Pastes/PasteListView.swift +1
@@ -110,6 +110,7 @@ struct PasteListView: View {
110110 .listRowSeparator(.hidden)
111111 }
112112 }
113 .themedList()
113114 .listStyle(.plain)
114115 .searchable(
115116 text: $vm.searchText,
Hutch/Views/Repositories/RepositoryListView.swift +1
@@ -84,6 +84,7 @@ struct RepositoryListView: View {
8484 .listRowSeparator(.hidden)
8585 }
8686 }
87 .themedList()
8788 .listStyle(.plain)
8889 .searchable(text: $vm.searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: "Search repositories")
8990 .overlay {
Hutch/Views/Settings/SettingsView.swift +25
@@ -2,16 +2,20 @@ import SwiftUI
22
33struct SettingsView: View {
44 @Environment(AppState.self) private var appState
5 @AppStorage(AppStorageKeys.appTheme) private var appTheme: AppTheme = .system
6 @AppStorage(AppStorageKeys.displayDensity) private var displayDensity: DisplayDensity = .standard
57 @AppStorage(AppStorageKeys.swipeActionsEnabled) private var swipeActionsEnabled = true
68 @AppStorage(AppStorageKeys.contributionGraphsEnabled) private var contributionGraphsEnabled = true
79 @State private var pendingDestructiveAction: SettingsDestructiveAction?
810
911 var body: some View {
1012 Form {
13 appearanceSection()
1114 behaviorSection()
1215 authenticationSection()
1316 aboutSection()
1417 }
18 .themedList()
1519 .navigationTitle("Settings")
1620 .alert(
1721 pendingDestructiveAction?.title ?? "",
@@ -44,6 +48,26 @@ struct SettingsView: View {
4448 }
4549 }
4650
51 @ViewBuilder
52 private func appearanceSection() -> some View {
53 Section {
54 Picker("Theme", selection: $appTheme) {
55 ForEach(AppTheme.allCases) { theme in
56 Text(theme.label).tag(theme)
57 }
58 }
59 Picker("Density", selection: $displayDensity) {
60 ForEach(DisplayDensity.allCases) { density in
61 Text(density.label).tag(density)
62 }
63 }
64 } header: {
65 Text("Appearance")
66 } footer: {
67 Text("Compact density reduces spacing throughout the app.")
68 }
69 }
70
4771 @ViewBuilder
4872 private func behaviorSection() -> some View {
4973 Section {
@@ -192,6 +216,7 @@ private struct AboutView: View {
192216 .foregroundStyle(.secondary)
193217 }
194218 }
219 .themedList()
195220 .navigationTitle("About")
196221 .navigationBarTitleDisplayMode(.inline)
197222 }