import SwiftUI import TVAnarchyCore /// The locked front door. Until this Mac has joined an install (via Device Mesh) /// there's nothing to show, so a fresh install renders ONLY this page: what the /// app is, plus the two ways in (scan the join QR an already-trusted device /// displays from Settings → Device Mesh, or paste the same config as text). /// Joining persists the credentials and flips the shell over to the real UI. struct IntroView: View { let onJoined: () -> Void private enum Mode { case welcome, scanning, pasting, joined } @State private var mode: Mode = .welcome @State private var pasted = "" @State private var error: String? @State private var joinedConfig: WGQuickConfig? private let fleet = FleetState() var body: some View { VStack(spacing: 18) { Spacer(minLength: 24) Image(systemName: "antenna.radiowaves.left.and.right.circle.fill") .font(.system(size: 64)).foregroundStyle(.tint) Text("TVAnarchy").font(.largeTitle).bold() Text("Your media install's remote control — browse the library, play anywhere, manage downloads. Everything lives on the devices in your install, so the first step is joining this Mac to it.") .font(.callout).foregroundStyle(.secondary) .multilineTextAlignment(.center).frame(maxWidth: 420) switch mode { case .welcome: welcome case .scanning: scanner case .pasting: paster case .joined: done } if let error { Text(error).font(.caption).foregroundStyle(.red) .frame(maxWidth: 420) } Spacer(minLength: 24) } .padding(32) .frame(minWidth: 560, minHeight: 540) } private var welcome: some View { VStack(spacing: 10) { Text("On a Mac that's already part of the install, open Settings → Device Mesh, enroll this Mac by name, and bring the QR here.") .font(.caption).foregroundStyle(.secondary) .multilineTextAlignment(.center).frame(maxWidth: 420) HStack(spacing: 12) { Button { error = nil; mode = .scanning } label: { Label("Scan join QR", systemImage: "qrcode.viewfinder") } .controlSize(.large).buttonStyle(.borderedProminent) Button { error = nil; mode = .pasting } label: { Label("Paste config", systemImage: "doc.on.clipboard") } .controlSize(.large) } } } private var scanner: some View { VStack(spacing: 10) { QRScannerView { payload in join(payload) } .frame(width: 360, height: 270) .clipShape(RoundedRectangle(cornerRadius: 12)) .overlay(RoundedRectangle(cornerRadius: 12).strokeBorder(.quaternary)) Text("Hold the join QR up to this Mac's camera.") .font(.caption).foregroundStyle(.secondary) Button("Cancel") { error = nil; mode = .welcome } } } private var paster: some View { VStack(spacing: 10) { TextEditor(text: $pasted) .font(.caption.monospaced()) .frame(width: 420, height: 180) .overlay(RoundedRectangle(cornerRadius: 6).strokeBorder(.quaternary)) Text("Paste the WireGuard config from the enrolling Mac (Copy config under its QR).") .font(.caption).foregroundStyle(.secondary) HStack { Button("Cancel") { error = nil; mode = .welcome } Button("Join") { join(pasted) } .buttonStyle(.borderedProminent) .disabled(pasted.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) } } } private var done: some View { VStack(spacing: 10) { Image(systemName: "checkmark.seal.fill").font(.system(size: 40)).foregroundStyle(.green) Text("Joined as \(joinedConfig?.device ?? "this Mac") · \(joinedConfig?.address ?? "")") .font(.headline) Text("Credentials are saved. At home the install is reachable over the LAN right away; for anywhere-access, import the same config into the WireGuard app and enable the tunnel.") .font(.caption).foregroundStyle(.secondary) .multilineTextAlignment(.center).frame(maxWidth: 420) HStack(spacing: 12) { if let conf = joinedConfig { Button("Copy config for WireGuard") { copyToClipboard(conf.text) } } Button("Enter TVAnarchy") { onJoined() } .buttonStyle(.borderedProminent).controlSize(.large) } } } private func join(_ text: String) { let host = Host.current().localizedName?.lowercased() .map { $0.isLetter || $0.isNumber ? $0 : "-" } ?? [] let fallback = String(host).split(separator: "-").joined(separator: "-") do { joinedConfig = try fleet.join(configText: text, fallbackDevice: fallback.isEmpty ? "this-mac" : fallback) error = nil mode = .joined } catch { // Keep scanning/pasting — a half-read QR or stray clipboard content // shouldn't bounce the user back to the start. self.error = error.localizedDescription } } }