tv-anarchy/Sources/TVAnarchyiOS/FleetGate.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

70 lines
3 KiB
Swift

// The iOS install join gate (Devices pillar): loading member | needsJoin (same phase machine as the
// macOS app, different notion of membership). A phone is a bridge CLIENT
// membership means "a bridge has been configured", latched in UserDefaults (internal FleetGate),
// NOT a live reachability probe: a flight with downloads ready must still open
// the app. Installs that predate the gate are grandfathered by an already-
// edited host (the default 127.0.0.1 is meaningless on a phone).
// Product language per v2 plan: "install", "Join install", "Device Mesh". Internal names legacy.
import Foundation
enum FleetPhase {
case loading, member, needsJoin
}
enum FleetGate {
private static let joinedKey = "fleet.joined"
@MainActor
static func isMember(settings: BridgeSettings) -> Bool {
let store = UserDefaults.standard
if store.bool(forKey: joinedKey) { return true }
let grandfathered = settings.host != "127.0.0.1" || !settings.fallbackHost.isEmpty
if grandfathered { latch() }
return grandfathered
}
static func latch() {
UserDefaults.standard.set(true, forKey: joinedKey)
}
}
/// What a scanned QR (or pasted/typed text) turned out to be.
enum JoinPayload: Equatable {
/// The app-setup payload the Mac shows as QR 2
/// (`tvanarchy://bridge?host=&fallback=&port=` rendered by
/// `MeshJoin.phoneSetupURL` on the macOS side; change the shapes together).
case setup(host: String, fallback: String?, port: Int)
/// The MESH config (QR 1) right install, wrong app: that one belongs to
/// the WireGuard app, which owns the tunnel on iOS.
case wireGuardConfig
case invalid
static func parse(_ raw: String) -> JoinPayload {
let text = raw.trimmingCharacters(in: .whitespacesAndNewlines)
if text.contains("[Interface]") && text.contains("PrivateKey") { return .wireGuardConfig }
if let url = URL(string: text), url.scheme == "tvanarchy", url.host == "bridge",
let comps = URLComponents(url: url, resolvingAgainstBaseURL: false) {
func q(_ name: String) -> String? {
comps.queryItems?.first { $0.name == name }?.value?.nonEmpty
}
guard let host = q("host") else { return .invalid }
return .setup(host: host, fallback: q("fallback"),
port: q("port").flatMap(Int.init) ?? 8787)
}
// Bare "host" / "host:port" typed or encoded plainly.
let parts = text.split(separator: ":")
if parts.count <= 2, let host = parts.first?.nonEmpty, host.allSatisfy({ $0.isNumber || $0 == "." || $0.isLetter || $0 == "-" }) {
let port = parts.count == 2 ? Int(parts[1]) : nil
if parts.count == 2 && port == nil { return .invalid }
return .setup(host: String(host), fallback: nil, port: port ?? 8787)
}
return .invalid
}
}
private extension StringProtocol {
var nonEmpty: String? { isEmpty ? nil : String(self) }
}