2026-06-13 01:12:55 -07:00
import SwiftUI
2026-06-26 15:07:58 -07:00
import NucleicProtocol
2026-06-13 01:12:55 -07:00
struct SettingsView : View {
@ EnvironmentObject var store : RemoteStore
@ State private var showScanner = false
2026-06-21 04:03:53 -07:00
@ 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
2026-07-02 03:43:13 -07:00
@ 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
2026-06-13 01:12:55 -07:00
var body : some View {
NavigationStack {
Form {
2026-07-02 19:26:43 -07:00
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." )
}
}
2026-07-02 03:43:13 -07:00
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." )
}
2026-06-13 01:12:55 -07:00
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 {
2026-06-21 04:03:53 -07:00
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 {
2026-06-13 01:12:55 -07:00
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 )
2026-07-02 19:26:43 -07:00
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 )
2026-06-13 01:12:55 -07:00
}
. padding ()
2026-07-02 19:34:09 -07:00
// 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 )
2026-06-13 01:12:55 -07:00
. sheet ( isPresented : $ showScanner ) {
PairingScannerView { payload in
showScanner = false
store . pair ( with : payload )
}
}
}
}