tv-anarchy/Sources/TVAnarchy/IntroView.swift
Natalie 4a2ceb9781 feat(offline): inline star-to-keep and trash-to-cull on cache rows
Surface the existing pin (keep-from-cull) and per-file delete actions as
visible inline buttons on each offline cache row instead of context-menu-only:
a star toggles protection from auto-cull (and restore-if-missing), a trash
culls that file early. Aligns wording/icons to the star metaphor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 00:12:41 -04:00

127 lines
5.5 KiB
Swift

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
}
}
}