krz/hutch
an ios client for sourcehut
clone: git clone https://gitbay.org/krz/hutch.git
v2.10.3: 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 } header: {
53 Text("Behavior")
54 } footer: {
55 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.")
56 }
57 }
58
59 @ViewBuilder
60 private func authenticationSection() -> some View {
61 Section {
62 HStack {
63 Image(systemName: "key.fill")
64 .foregroundStyle(.secondary)
65 Text("Personal access token in use")
66 .font(.subheadline)
67 .foregroundStyle(.secondary)
68 }
69 .alignmentGuide(.listRowSeparatorLeading) { _ in 0 }
70
71 Button("Reset App Data", role: .destructive) {
72 pendingDestructiveAction = .resetAppData
73 }
74
75 Button("Sign Out", role: .destructive) {
76 pendingDestructiveAction = .signOut
77 }
78 } header: {
79 Text("Authentication")
80 } footer: {
81 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.")
82 }
83 }
84
85 @ViewBuilder
86 private func aboutSection() -> some View {
87 Section("App") {
88 NavigationLink {
89 AboutView()
90 } label: {
91 SwiftUI.Label("About Hutch", systemImage: "info.circle")
92 }
93 }
94 }
95}
96
97func settingsBioAttributedString(_ markdown: String) -> AttributedString {
98 profileBioAttributedString(markdown)
99}
100
101private enum SettingsDestructiveAction {
102 case resetAppData
103 case signOut
104
105 var title: String {
106 switch self {
107 case .resetAppData:
108 "Reset App Data?"
109 case .signOut:
110 "Sign Out?"
111 }
112 }
113
114 var confirmationLabel: String {
115 switch self {
116 case .resetAppData:
117 "Reset App Data"
118 case .signOut:
119 "Sign Out"
120 }
121 }
122
123 var message: String {
124 switch self {
125 case .resetAppData:
126 "This signs you out and removes saved token data, local settings, cached responses, cookies, and embedded web content on this device."
127 case .signOut:
128 "This signs you out of Hutch and clears saved authentication state on this device."
129 }
130 }
131}
132
133private struct AboutView: View {
134 private let appName = Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String
135 ?? Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String
136 ?? "Hutch"
137 private let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String
138 ?? "Unknown"
139 private let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String
140 ?? "Unknown"
141
142 var body: some View {
143 Form {
144 Section {
145 VStack(alignment: .leading, spacing: 6) {
146 Text(appName)
147 .font(.title2.weight(.semibold))
148 Text("A native SourceHut client for iOS.")
149 .font(.subheadline)
150 .foregroundStyle(.secondary)
151 }
152 .padding(.vertical, 4)
153
154 LabeledContent("Version", value: version)
155 LabeledContent("Build", value: build)
156 }
157
158 Section("Links") {
159 Link(destination: URL(string: "https://sr.ht")!) {
160 SwiftUI.Label("SourceHut", systemImage: "link")
161 }
162 Link(destination: URL(string: "https://man.sr.ht")!) {
163 SwiftUI.Label("SourceHut Manuals", systemImage: "book")
164 }
165 Link(destination: URL(string: "https://sr.ht/~ccleberg/Hutch")!) {
166 SwiftUI.Label("Project Repository", systemImage: "folder")
167 }
168 }
169
170 Section("Support") {
171 Link(destination: URL(string: "mailto:hello@cleberg.net")!) {
172 SwiftUI.Label("Email Support", systemImage: "envelope")
173 }
174 }
175
176 Section("Privacy") {
177 Text("Hutch uses your SourceHut personal access token to make requests on your behalf. The token is stored locally in the iOS keychain.")
178 .font(.subheadline)
179 .foregroundStyle(.secondary)
180
181 Link(destination: URL(string: "https://zerolabs.sh/hutch/privacy-policy/")!) {
182 SwiftUI.Label("Privacy Policy", systemImage: "hand.raised")
183 }
184 }
185
186 Section("Acknowledgements") {
187 Text("Built for SourceHut users who want quick access to repositories, builds, and tickets on iOS.")
188 .font(.subheadline)
189 .foregroundStyle(.secondary)
190 }
191 }
192 .navigationTitle("About")
193 .navigationBarTitleDisplayMode(.inline)
194 }
195}