Commit 59b53afae0
Unsigned
Layout: unified · split
Hutch.xcodeproj/project.pbxproj +8 −8
| @@ -515,7 +515,7 @@ | ||
| 515 | 515 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; |
| 516 | 516 | CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements; |
| 517 | 517 | CODE_SIGN_STYLE = Automatic; |
| 518 | CURRENT_PROJECT_VERSION = 38; | |
| 518 | CURRENT_PROJECT_VERSION = 39; | |
| 519 | 519 | DEVELOPMENT_TEAM = ZCNAX3VL9D; |
| 520 | 520 | ENABLE_PREVIEWS = YES; |
| 521 | 521 | GENERATE_INFOPLIST_FILE = YES; |
| @@ -532,7 +532,7 @@ | ||
| 532 | 532 | "$(inherited)", |
| 533 | 533 | "@executable_path/Frameworks", |
| 534 | 534 | ); |
| 535 | MARKETING_VERSION = 2.16.2; | |
| 535 | MARKETING_VERSION = 2.17.0; | |
| 536 | 536 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; |
| 537 | 537 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 538 | 538 | STRING_CATALOG_GENERATE_SYMBOLS = YES; |
| @@ -552,7 +552,7 @@ | ||
| 552 | 552 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; |
| 553 | 553 | CODE_SIGN_ENTITLEMENTS = Hutch/Hutch.entitlements; |
| 554 | 554 | CODE_SIGN_STYLE = Automatic; |
| 555 | CURRENT_PROJECT_VERSION = 38; | |
| 555 | CURRENT_PROJECT_VERSION = 39; | |
| 556 | 556 | DEVELOPMENT_TEAM = ZCNAX3VL9D; |
| 557 | 557 | ENABLE_PREVIEWS = YES; |
| 558 | 558 | GENERATE_INFOPLIST_FILE = YES; |
| @@ -569,7 +569,7 @@ | ||
| 569 | 569 | "$(inherited)", |
| 570 | 570 | "@executable_path/Frameworks", |
| 571 | 571 | ); |
| 572 | MARKETING_VERSION = 2.16.2; | |
| 572 | MARKETING_VERSION = 2.17.0; | |
| 573 | 573 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch; |
| 574 | 574 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 575 | 575 | STRING_CATALOG_GENERATE_SYMBOLS = YES; |
| @@ -632,7 +632,7 @@ | ||
| 632 | 632 | APPLICATION_EXTENSION_API_ONLY = YES; |
| 633 | 633 | CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements; |
| 634 | 634 | CODE_SIGN_STYLE = Automatic; |
| 635 | CURRENT_PROJECT_VERSION = 38; | |
| 635 | CURRENT_PROJECT_VERSION = 39; | |
| 636 | 636 | DEVELOPMENT_TEAM = ZCNAX3VL9D; |
| 637 | 637 | GENERATE_INFOPLIST_FILE = NO; |
| 638 | 638 | INFOPLIST_FILE = HutchWidgetExtension/Info.plist; |
| @@ -642,7 +642,7 @@ | ||
| 642 | 642 | "@executable_path/Frameworks", |
| 643 | 643 | "@executable_path/../../Frameworks", |
| 644 | 644 | ); |
| 645 | MARKETING_VERSION = 2.16.2; | |
| 645 | MARKETING_VERSION = 2.17.0; | |
| 646 | 646 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; |
| 647 | 647 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 648 | 648 | SKIP_INSTALL = YES; |
| @@ -661,7 +661,7 @@ | ||
| 661 | 661 | APPLICATION_EXTENSION_API_ONLY = YES; |
| 662 | 662 | CODE_SIGN_ENTITLEMENTS = HutchWidgetExtension/HutchWidgetExtension.entitlements; |
| 663 | 663 | CODE_SIGN_STYLE = Automatic; |
| 664 | CURRENT_PROJECT_VERSION = 38; | |
| 664 | CURRENT_PROJECT_VERSION = 39; | |
| 665 | 665 | DEVELOPMENT_TEAM = ZCNAX3VL9D; |
| 666 | 666 | GENERATE_INFOPLIST_FILE = NO; |
| 667 | 667 | INFOPLIST_FILE = HutchWidgetExtension/Info.plist; |
| @@ -671,7 +671,7 @@ | ||
| 671 | 671 | "@executable_path/Frameworks", |
| 672 | 672 | "@executable_path/../../Frameworks", |
| 673 | 673 | ); |
| 674 | MARKETING_VERSION = 2.16.2; | |
| 674 | MARKETING_VERSION = 2.17.0; | |
| 675 | 675 | PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension; |
| 676 | 676 | PRODUCT_NAME = "$(TARGET_NAME)"; |
| 677 | 677 | SKIP_INSTALL = YES; |
Hutch/App/AppStorageKeys.swift +2
| @@ -13,4 +13,6 @@ enum AppStorageKeys { | ||
| 13 | 13 | static let homeBuildsExpanded = "homeBuildsExpanded" |
| 14 | 14 | static let buildsAutoRefreshInterval = "buildsAutoRefreshInterval" |
| 15 | 15 | static let buildsRepoFilter = "buildsRepoFilter" |
| 16 | static let appTheme = "appTheme" | |
| 17 | static let displayDensity = "displayDensity" | |
| 16 | 18 | } |
Hutch/App/HutchApp.swift +5
| @@ -4,12 +4,17 @@ import SwiftUI | ||
| 4 | 4 | struct HutchApp: App { |
| 5 | 5 | @State private var appState = AppState() |
| 6 | 6 | @State private var networkMonitor = NetworkMonitor() |
| 7 | @AppStorage(AppStorageKeys.appTheme) private var appTheme: AppTheme = .system | |
| 8 | @AppStorage(AppStorageKeys.displayDensity) private var displayDensity: DisplayDensity = .standard | |
| 7 | 9 | |
| 8 | 10 | var body: some Scene { |
| 9 | 11 | WindowGroup { |
| 10 | 12 | RootView() |
| 11 | 13 | .environment(appState) |
| 12 | 14 | .environment(networkMonitor) |
| 15 | .environment(\.displayDensity, displayDensity) | |
| 16 | .environment(\.defaultMinListRowHeight, displayDensity == .compact ? 36 : 44) | |
| 17 | .preferredColorScheme(appTheme.colorScheme) | |
| 13 | 18 | .onOpenURL { url in |
| 14 | 19 | if let link = DeepLink(url: url) { |
| 15 | 20 | appState.pendingDeepLink = link |
Hutch/App/ThemeManager.swift added +76
| @@ -0,0 +1,76 @@ | ||
| 1 | import SwiftUI | |
| 2 | ||
| 3 | // MARK: - Theme & Density Types | |
| 4 | ||
| 5 | enum 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 | ||
| 29 | enum 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 | ||
| 45 | private struct DisplayDensityKey: EnvironmentKey { | |
| 46 | static let defaultValue: DisplayDensity = .standard | |
| 47 | } | |
| 48 | ||
| 49 | extension 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. | |
| 60 | struct 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 | ||
| 71 | extension 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 { | ||
| 392 | 392 | .disabled(manifest.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || viewModel.isSubmittingEditedBuild) |
| 393 | 393 | } |
| 394 | 394 | } |
| 395 | .themedList() | |
| 395 | 396 | } |
| 396 | 397 | } |
| 397 | 398 | } |
Hutch/Views/Builds/BuildListView.swift +1
| @@ -162,6 +162,7 @@ struct BuildListView: View { | ||
| 162 | 162 | .listRowSeparator(.hidden) |
| 163 | 163 | } |
| 164 | 164 | } |
| 165 | .themedList() | |
| 165 | 166 | .listStyle(.plain) |
| 166 | 167 | .searchable( |
| 167 | 168 | text: $vm.searchText, |
Hutch/Views/Home/HomeView.swift +1
| @@ -65,6 +65,7 @@ struct HomeView: View { | ||
| 65 | 65 | assignedTicketsSection(viewModel) |
| 66 | 66 | recentBuildsSection(viewModel) |
| 67 | 67 | } |
| 68 | .themedList() | |
| 68 | 69 | .listStyle(.insetGrouped) |
| 69 | 70 | .overlay { |
| 70 | 71 | if viewModel.isLoadingProjects && viewModel.isLoadingAssignedTickets && viewModel.isLoadingRecentBuilds && |
Hutch/Views/Inbox/InboxView.swift +1
| @@ -68,6 +68,7 @@ struct InboxView: View { | ||
| 68 | 68 | } |
| 69 | 69 | } |
| 70 | 70 | } |
| 71 | .themedList() | |
| 71 | 72 | .searchable( |
| 72 | 73 | text: $vm.searchText, |
| 73 | 74 | placement: .navigationBarDrawer(displayMode: .always), |
Hutch/Views/Lookup/LookupView.swift +1
| @@ -411,6 +411,7 @@ struct LookupView: View { | ||
| 411 | 411 | } |
| 412 | 412 | } |
| 413 | 413 | } |
| 414 | .themedList() | |
| 414 | 415 | .formStyle(.grouped) |
| 415 | 416 | .srhtErrorBanner(error: $vm.error) |
| 416 | 417 | .sheet(item: vm.resultBinding) { result in |
Hutch/Views/More/MoreView.swift +1
| @@ -63,6 +63,7 @@ struct MoreView: View { | ||
| 63 | 63 | Text("These SourceHut services are not supported in-app, as the SourceHut API does not support them, and will open in your browser.") |
| 64 | 64 | } |
| 65 | 65 | } |
| 66 | .themedList() | |
| 66 | 67 | .navigationTitle("More") |
| 67 | 68 | .refreshable { |
| 68 | 69 | await ensureViewModel().loadSystemStatus(forceRefresh: true) |
Hutch/Views/More/ProfileView.swift +1
| @@ -87,6 +87,7 @@ struct ProfileView: View { | ||
| 87 | 87 | patSection(viewModel) |
| 88 | 88 | } |
| 89 | 89 | } |
| 90 | .themedList() | |
| 90 | 91 | .overlay { |
| 91 | 92 | if viewModel.isLoading, viewModel.profile == nil { |
| 92 | 93 | SRHTLoadingStateView(message: "Loading profile…") |
Hutch/Views/Pastes/PasteDetailView.swift +1
| @@ -259,6 +259,7 @@ private struct PasteVisibilitySheet: View { | ||
| 259 | 259 | ProgressView() |
| 260 | 260 | } |
| 261 | 261 | } |
| 262 | .themedList() | |
| 262 | 263 | } |
| 263 | 264 | } |
| 264 | 265 | |
Hutch/Views/Pastes/PasteListView.swift +1
| @@ -110,6 +110,7 @@ struct PasteListView: View { | ||
| 110 | 110 | .listRowSeparator(.hidden) |
| 111 | 111 | } |
| 112 | 112 | } |
| 113 | .themedList() | |
| 113 | 114 | .listStyle(.plain) |
| 114 | 115 | .searchable( |
| 115 | 116 | text: $vm.searchText, |
Hutch/Views/Repositories/RepositoryListView.swift +1
| @@ -84,6 +84,7 @@ struct RepositoryListView: View { | ||
| 84 | 84 | .listRowSeparator(.hidden) |
| 85 | 85 | } |
| 86 | 86 | } |
| 87 | .themedList() | |
| 87 | 88 | .listStyle(.plain) |
| 88 | 89 | .searchable(text: $vm.searchText, placement: .navigationBarDrawer(displayMode: .always), prompt: "Search repositories") |
| 89 | 90 | .overlay { |
Hutch/Views/Settings/SettingsView.swift +25
| @@ -2,16 +2,20 @@ import SwiftUI | ||
| 2 | 2 | |
| 3 | 3 | struct SettingsView: View { |
| 4 | 4 | @Environment(AppState.self) private var appState |
| 5 | @AppStorage(AppStorageKeys.appTheme) private var appTheme: AppTheme = .system | |
| 6 | @AppStorage(AppStorageKeys.displayDensity) private var displayDensity: DisplayDensity = .standard | |
| 5 | 7 | @AppStorage(AppStorageKeys.swipeActionsEnabled) private var swipeActionsEnabled = true |
| 6 | 8 | @AppStorage(AppStorageKeys.contributionGraphsEnabled) private var contributionGraphsEnabled = true |
| 7 | 9 | @State private var pendingDestructiveAction: SettingsDestructiveAction? |
| 8 | 10 | |
| 9 | 11 | var body: some View { |
| 10 | 12 | Form { |
| 13 | appearanceSection() | |
| 11 | 14 | behaviorSection() |
| 12 | 15 | authenticationSection() |
| 13 | 16 | aboutSection() |
| 14 | 17 | } |
| 18 | .themedList() | |
| 15 | 19 | .navigationTitle("Settings") |
| 16 | 20 | .alert( |
| 17 | 21 | pendingDestructiveAction?.title ?? "", |
| @@ -44,6 +48,26 @@ struct SettingsView: View { | ||
| 44 | 48 | } |
| 45 | 49 | } |
| 46 | 50 | |
| 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 | ||
| 47 | 71 | @ViewBuilder |
| 48 | 72 | private func behaviorSection() -> some View { |
| 49 | 73 | Section { |
| @@ -192,6 +216,7 @@ private struct AboutView: View { | ||
| 192 | 216 | .foregroundStyle(.secondary) |
| 193 | 217 | } |
| 194 | 218 | } |
| 219 | .themedList() | |
| 195 | 220 | .navigationTitle("About") |
| 196 | 221 | .navigationBarTitleDisplayMode(.inline) |
| 197 | 222 | } |