krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v2.14.0: Hutch/Views/Settings/SettingsView.swift · raw
1import SwiftUI
2
3struct SettingsView: View {
4 @Environment(AppState.self) private var appState
5 @AppStorage(AppStorageKeys.swipeActionsEnabled) private var swipeActionsEnabled = true
6 @AppStorage(AppStorageKeys.contributionGraphsEnabled) private var contributionGraphsEnabled = true
7 @State private var pendingDestructiveAction: SettingsDestructiveAction?
8
9 var body: some View {
10 Form {
11 behaviorSection()
12 authenticationSection()
13 aboutSection()
14 }
15 .navigationTitle("Settings")
16 .alert(
17 pendingDestructiveAction?.title ?? "",
18 isPresented: Binding(
19 get: { pendingDestructiveAction != nil },
20 set: { isPresented in
21 if !isPresented {
22 pendingDestructiveAction = nil
23 }
24 }
25 )
26 ) {
27 Button("Cancel", role: .cancel) {}
28 Button(pendingDestructiveAction?.confirmationLabel ?? "Confirm", role: .destructive) {
29 guard let action = pendingDestructiveAction else { return }
30 pendingDestructiveAction = nil
31 Task {
32 switch action {
33 case .resetAppData:
34 await appState.resetAppData()
35 case .signOut:
36 await appState.signOut()
37 }
38 }
39 }
40 } message: {
41 if let pendingDestructiveAction {
42 Text(pendingDestructiveAction.message)
43 }
44 }
45 }
46
47 @ViewBuilder
48 private func behaviorSection() -> some View {
49 Section {
50 Toggle("Swipe actions", isOn: $swipeActionsEnabled)
51 Toggle("Contribution graphs", isOn: $contributionGraphsEnabled)
52 .onChange(of: contributionGraphsEnabled) { _, newValue in
53 ContributionWidgetContextStore.setEnabled(newValue)
54 }
55 } header: {
56 Text("Behavior")
57 } footer: {
58 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.")
59 }
60 }
61
62 @ViewBuilder
63 private func authenticationSection() -> some View {
64 Section {
65 HStack {
66 Image(systemName: "key.fill")
67 .foregroundStyle(.secondary)
68 Text("Personal access token in use")
69 .font(.subheadline)
70 .foregroundStyle(.secondary)
71 }
72 .alignmentGuide(.listRowSeparatorLeading) { _ in 0 }
73
74 Button("Reset App Data", role: .destructive) {
75 pendingDestructiveAction = .resetAppData
76 }
77
78 Button("Sign Out", role: .destructive) {
79 pendingDestructiveAction = .signOut
80 }
81 } header: {
82 Text("Authentication")
83 } footer: {
84 Text("Hutch stores your SourceHut token in the iOS keychain. Reset App Data removes saved token data, local settings, cached responses, cookies, and embedded web data on this device.")
85 }
86 }
87
88 @ViewBuilder
89 private func aboutSection() -> some View {
90 Section("App") {
91 NavigationLink {
92 AboutView()
93 } label: {
94 SwiftUI.Label("About Hutch", systemImage: "info.circle")
95 }
96 }
97 }
98}
99
100func settingsBioAttributedString(_ markdown: String) -> AttributedString {
101 profileBioAttributedString(markdown)
102}
103
104private enum SettingsDestructiveAction {
105 case resetAppData
106 case signOut
107
108 var title: String {
109 switch self {
110 case .resetAppData:
111 "Reset App Data?"
112 case .signOut:
113 "Sign Out?"
114 }
115 }
116
117 var confirmationLabel: String {
118 switch self {
119 case .resetAppData:
120 "Reset App Data"
121 case .signOut:
122 "Sign Out"
123 }
124 }
125
126 var message: String {
127 switch self {
128 case .resetAppData:
129 "This signs you out and removes saved token data, local settings, cached responses, cookies, and embedded web content on this device."
130 case .signOut:
131 "This signs you out of Hutch and clears saved authentication state on this device."
132 }
133 }
134}
135
136private struct AboutView: View {
137 private let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String
138 ?? Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String
139 ?? "Hutch"
140 private let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
141 ?? "Unknown"
142 private let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String
143 ?? "Unknown"
144
145 var body: some View {
146 Form {
147 Section {
148 VStack(alignment: .leading, spacing: 6) {
149 Text(appName)
150 .font(.title2.weight(.semibold))
151 Text("A native SourceHut client for iOS.")
152 .font(.subheadline)
153 .foregroundStyle(.secondary)
154 }
155 .padding(.vertical, 4)
156
157 LabeledContent("Version", value: version)
158 LabeledContent("Build", value: build)
159 }
160
161 Section("Links") {
162 Link(destination: URL(string: "https://sr.ht")!) {
163 SwiftUI.Label("SourceHut", systemImage: "link")
164 }
165 Link(destination: URL(string: "https://man.sr.ht")!) {
166 SwiftUI.Label("SourceHut Manuals", systemImage: "book")
167 }
168 Link(destination: URL(string: "https://sr.ht/~ccleberg/Hutch")!) {
169 SwiftUI.Label("Project Repository", systemImage: "folder")
170 }
171 }
172
173 Section("Support") {
174 Link(destination: URL(string: "mailto:hello@cleberg.net")!) {
175 SwiftUI.Label("Email Support", systemImage: "envelope")
176 }
177 }
178
179 Section("Privacy") {
180 Text("Hutch uses your SourceHut personal access token to make requests on your behalf. The token is stored locally in the iOS keychain.")
181 .font(.subheadline)
182 .foregroundStyle(.secondary)
183
184 Link(destination: URL(string: "https://zerolabs.sh/hutch/privacy-policy/")!) {
185 SwiftUI.Label("Privacy Policy", systemImage: "hand.raised")
186 }
187 }
188
189 Section("Acknowledgements") {
190 Text("Built for SourceHut users who want quick access to repositories, builds, and tickets on iOS.")
191 .font(.subheadline)
192 .foregroundStyle(.secondary)
193 }
194 }
195 .navigationTitle("About")
196 .navigationBarTitleDisplayMode(.inline)
197 }
198}