krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v3.1.7: Hutch/Views/Settings/SettingsView.swift · raw
1import SwiftUI
2
3struct SettingsView: View {
4 @Environment(AppState.self) private var appState
5 @AppStorage(AppStorageKeys.appTheme, store: .standard) private var appTheme: AppTheme = .system
6 @AppStorage(AppStorageKeys.displayDensity, store: .standard) private var displayDensity: DisplayDensity = .standard
7 @AppStorage(AppStorageKeys.swipeActionsEnabled, store: .standard) private var swipeActionsEnabled = true
8 @AppStorage(AppStorageKeys.contributionGraphsEnabled, store: .standard) private var contributionGraphsEnabled = true
9 @State private var pendingDestructiveAction: SettingsDestructiveAction?
10 @State private var showAccountSwitcher = false
11
12 var body: some View {
13 Form {
14 appearanceSection()
15 behaviorSection()
16 authenticationSection()
17 }
18 .themedList()
19 .navigationTitle("Settings")
20 .sheet(isPresented: $showAccountSwitcher) {
21 AccountSwitcherView()
22 }
23 .alert(
24 pendingDestructiveAction?.title ?? "",
25 isPresented: Binding(
26 get: { pendingDestructiveAction != nil },
27 set: { isPresented in
28 if !isPresented {
29 pendingDestructiveAction = nil
30 }
31 }
32 )
33 ) {
34 Button("Cancel", role: .cancel) {
35 /* Dismiss only; destructive action is separate. */
36 }
37 Button(pendingDestructiveAction?.confirmationLabel ?? "Confirm", role: .destructive) {
38 guard let action = pendingDestructiveAction else { return }
39 pendingDestructiveAction = nil
40 Task {
41 switch action {
42 case .resetAppData:
43 await appState.resetAppData()
44 case .signOut:
45 await appState.signOut()
46 }
47 }
48 }
49 } message: {
50 if let pendingDestructiveAction {
51 Text(pendingDestructiveAction.message)
52 }
53 }
54 }
55
56 @ViewBuilder
57 private func appearanceSection() -> some View {
58 Section {
59 Picker("Theme", selection: $appTheme) {
60 ForEach(AppTheme.allCases) { theme in
61 Text(theme.label).tag(theme)
62 }
63 }
64 .themedRow()
65 Picker("Density", selection: $displayDensity) {
66 ForEach(DisplayDensity.allCases) { density in
67 Text(density.label).tag(density)
68 }
69 }
70 .themedRow()
71 } header: {
72 Text("Appearance")
73 } footer: {
74 Text("Compact density reduces spacing throughout the app.")
75 }
76 }
77
78 @ViewBuilder
79 private func behaviorSection() -> some View {
80 Section {
81 Toggle("Swipe actions", isOn: $swipeActionsEnabled)
82 .themedRow()
83 Toggle("Contribution graphs", isOn: $contributionGraphsEnabled)
84 .onChange(of: contributionGraphsEnabled) { _, newValue in
85 ContributionWidgetContextStore.setEnabled(newValue)
86 }
87 .themedRow()
88 } header: {
89 Text("Behavior")
90 } footer: {
91 Text("When enabled, swipe list rows to quickly take actions like resolving tickets, cancelling builds, and deleting pastes. Contribution graphs controls whether SourceHut activity heatmaps appear in lookup profiles.")
92 }
93 }
94
95 @ViewBuilder
96 private func authenticationSection() -> some View {
97 Section {
98 HStack {
99 Image(systemName: "key.fill")
100 .foregroundStyle(.secondary)
101 VStack(alignment: .leading, spacing: 2) {
102 Text(appState.currentUser?.canonicalName ?? "No active account")
103 Text("\(appState.accounts.count) saved account\(appState.accounts.count == 1 ? "" : "s")")
104 .font(.caption)
105 .foregroundStyle(.secondary)
106 }
107 }
108 .alignmentGuide(.listRowSeparatorLeading) { _ in 0 }
109 .themedRow()
110
111 Button {
112 showAccountSwitcher = true
113 } label: {
114 Label("Manage Accounts", systemImage: "person.2")
115 }
116 .themedRow()
117
118 HStack {
119 Image(systemName: "lock.shield")
120 .foregroundStyle(.secondary)
121 Text("Tokens are stored separately per account in the iOS keychain")
122 .font(.subheadline)
123 .foregroundStyle(.secondary)
124 }
125 .alignmentGuide(.listRowSeparatorLeading) { _ in 0 }
126 .themedRow()
127
128 Button("Reset App Data", role: .destructive) {
129 pendingDestructiveAction = .resetAppData
130 }
131 .themedRow()
132
133 Button("Sign Out", role: .destructive) {
134 pendingDestructiveAction = .signOut
135 }
136 .themedRow()
137 } header: {
138 Text("Authentication")
139 } footer: {
140 Text("Account switching keeps local caches and saved state isolated per account. Sign Out removes all saved accounts from this device. Reset App Data also clears local settings, cached responses, cookies, and embedded web data.")
141 }
142 }
143
144}
145
146func settingsBioAttributedString(_ markdown: String) -> AttributedString {
147 profileBioAttributedString(markdown)
148}
149
150private enum SettingsDestructiveAction {
151 case resetAppData
152 case signOut
153
154 var title: String {
155 switch self {
156 case .resetAppData:
157 "Reset App Data?"
158 case .signOut:
159 "Sign Out?"
160 }
161 }
162
163 var confirmationLabel: String {
164 switch self {
165 case .resetAppData:
166 "Reset App Data"
167 case .signOut:
168 "Sign Out"
169 }
170 }
171
172 var message: String {
173 switch self {
174 case .resetAppData:
175 "This signs you out and removes saved token data, local settings, cached responses, cookies, and embedded web content on this device."
176 case .signOut:
177 "This signs you out of Hutch and clears saved authentication state on this device."
178 }
179 }
180}
181