krz/hutch

an ios client for sourcehut

clone: git clone https://gitbay.org/krz/hutch.git

v3.1.6: 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