summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Hutch.xcodeproj/project.pbxproj16
-rw-r--r--Hutch/App/AppStorageKeys.swift2
-rw-r--r--Hutch/App/HutchApp.swift5
-rw-r--r--Hutch/App/ThemeManager.swift76
-rw-r--r--Hutch/Views/Builds/BuildDetailView.swift1
-rw-r--r--Hutch/Views/Builds/BuildListView.swift1
-rw-r--r--Hutch/Views/Home/HomeView.swift1
-rw-r--r--Hutch/Views/Inbox/InboxView.swift1
-rw-r--r--Hutch/Views/Lookup/LookupView.swift1
-rw-r--r--Hutch/Views/More/MoreView.swift1
-rw-r--r--Hutch/Views/More/ProfileView.swift1
-rw-r--r--Hutch/Views/Pastes/PasteDetailView.swift1
-rw-r--r--Hutch/Views/Pastes/PasteListView.swift1
-rw-r--r--Hutch/Views/Repositories/RepositoryListView.swift1
-rw-r--r--Hutch/Views/Settings/SettingsView.swift25
15 files changed, 126 insertions, 8 deletions
diff --git a/Hutch.xcodeproj/project.pbxproj b/Hutch.xcodeproj/project.pbxproj
index 51c63c4..4328148 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 = 38;
+ CURRENT_PROJECT_VERSION = 39;
DEVELOPMENT_TEAM = ZCNAX3VL9D;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -532,7 +532,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 2.16.2;
+ MARKETING_VERSION = 2.17.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 = 38;
+ CURRENT_PROJECT_VERSION = 39;
DEVELOPMENT_TEAM = ZCNAX3VL9D;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -569,7 +569,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
- MARKETING_VERSION = 2.16.2;
+ MARKETING_VERSION = 2.17.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 = 38;
+ CURRENT_PROJECT_VERSION = 39;
DEVELOPMENT_TEAM = ZCNAX3VL9D;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = HutchWidgetExtension/Info.plist;
@@ -642,7 +642,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 2.16.2;
+ MARKETING_VERSION = 2.17.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 = 38;
+ CURRENT_PROJECT_VERSION = 39;
DEVELOPMENT_TEAM = ZCNAX3VL9D;
GENERATE_INFOPLIST_FILE = NO;
INFOPLIST_FILE = HutchWidgetExtension/Info.plist;
@@ -671,7 +671,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
- MARKETING_VERSION = 2.16.2;
+ MARKETING_VERSION = 2.17.0;
PRODUCT_BUNDLE_IDENTIFIER = net.cleberg.Hutch.HutchWidgetExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
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)
}