Files
nucleic-remote-ios/NucleicRemote/NucleicRemote/Views/SettingsView.swift
T

155 lines
6.6 KiB
Swift
Raw Normal View History

import SwiftUI
2026-06-26 15:07:58 -07:00
import NucleicProtocol
struct SettingsView: View {
@EnvironmentObject var store: RemoteStore
@State private var showScanner = false
@AppStorage("nucleic.showRawEvents") private var showRaw = false
@AppStorage("nucleic.showLockEvents") private var showLockEvents = true
2026-06-26 15:07:58 -07:00
@AppStorage(HeartbeatSettings.shareAnonymousUsageKey) private var shareAnonymousUsage = true
@AppStorage(AppAppearance.storageKey) private var appearanceRaw = AppAppearance.system.rawValue
@AppStorage(AppTextSize.storageKey) private var textSizeRaw = AppTextSize.medium.rawValue
@AppStorage(ColorVisionMode.storageKey) private var colorVisionRaw = ColorVisionMode.standard.rawValue
var body: some View {
NavigationStack {
Form {
if store.demoMode {
Section {
Button(role: .destructive) { store.exitDemo() } label: {
Label("Leave demo", systemImage: "arrow.uturn.left")
}
} header: {
Text("Demo")
} footer: {
Text("You're exploring Nucleic with sample data and a simulated agent. Leave the demo to pair with your Mac.")
}
}
Section {
Picker("Theme", selection: $appearanceRaw) {
ForEach(AppAppearance.allCases) { appearance in
Text(appearance.label).tag(appearance.rawValue)
}
}
Picker("Text size", selection: $textSizeRaw) {
ForEach(AppTextSize.allCases) { size in
Text(size.label).tag(size.rawValue)
}
}
Picker("Color vision", selection: $colorVisionRaw) {
ForEach(ColorVisionMode.allCases) { mode in
Text(mode.label).tag(mode.rawValue)
}
}
} header: {
Text("Appearance")
} footer: {
Text("Color vision remaps status colors to hues that stay distinguishable. Every status also carries a text label, so color is never the only cue.")
}
Section("Connection") {
LabeledContent("Status", value: store.connectivity.label)
if !store.hostName.isEmpty {
LabeledContent("Mac", value: store.hostName)
}
Button("Reconnect") { store.reconnect() }
.disabled(!store.isPaired)
}
Section("This device") {
LabeledContent("Scope", value: store.grantedScope.rawValue.capitalized)
LabeledContent("Key fingerprint", value: store.deviceFingerprint)
.font(.footnote.monospaced())
}
Section {
Toggle("Show lock events", isOn: $showLockEvents)
Toggle("Show raw events", isOn: $showRaw)
} header: {
Text("Transcript")
} footer: {
Text("Raw events are unrecognized backend output, shown for debugging.")
}
Section {
2026-06-26 15:08:00 -07:00
Toggle("Share anonymous usage", isOn: $shareAnonymousUsage)
} header: {
Text("Privacy")
} footer: {
Text("A once-a-day anonymous ping — a random install ID, app version, iOS version, and language — so we can count active installs. No account, no IP, no content.")
}
Section {
Button {
showScanner = true
} label: {
Label("Pair with a Mac", systemImage: "qrcode.viewfinder")
}
if store.isPaired {
Button(role: .destructive) { store.unpair() } label: {
Label("Unpair this device", systemImage: "minus.circle")
}
}
} footer: {
Text("Code and diffs are end-to-end encrypted. A relay (later) can't read them.")
}
}
.navigationTitle("Settings")
.sheet(isPresented: $showScanner) {
PairingScannerView { payload in
showScanner = false
store.pair(with: payload)
}
}
}
}
}
/// Shown before pairing (UX_IOS §7): explain the flow and launch the scanner.
struct PairingIntroView: View {
@EnvironmentObject var store: RemoteStore
@State private var showScanner = false
var body: some View {
VStack(spacing: 24) {
Image(systemName: "qrcode.viewfinder").font(.system(size: 72)).foregroundStyle(.tint)
Text("Pair with your Mac").font(.title2.weight(.semibold))
Text("On your Mac, open Nucleic ▸ Settings ▸ Add iPhone to show a QR code, then scan it here.")
.multilineTextAlignment(.center)
.foregroundStyle(.secondary)
.padding(.horizontal, 32)
Button {
showScanner = true
} label: {
Label("Scan QR code", systemImage: "camera").font(.headline).padding(.horizontal)
}
.buttonStyle(.borderedProminent)
VStack(spacing: 6) {
Button { store.enterDemo() } label: {
Label("Explore a demo — no Mac needed", systemImage: "play.circle")
}
.buttonStyle(.bordered)
Text("Sample data and a simulated agent — no Mac or account required.")
.font(.footnote)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal, 32)
}
.padding(.top, 8)
}
.padding()
// Fill the available height so the enclosing VStack reaches full screen and the
// BuildBanner/DemoBanner strips stay pinned at the top. Without this the intro only
// claims its intrinsic height, so the stack gets centered and the banner drifts down.
.frame(maxWidth: .infinity, maxHeight: .infinity)
.sheet(isPresented: $showScanner) {
PairingScannerView { payload in
showScanner = false
store.pair(with: payload)
}
}
}
}