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>
70 lines
3 KiB
Swift
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) }
|
|
}
|