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>
83 lines
4 KiB
Swift
83 lines
4 KiB
Swift
import Foundation
|
|
|
|
/// Has this Mac joined an install? The app gates on install membership — without
|
|
/// a joined install there is nothing to browse, play, or download, so the UI shell
|
|
/// gates on this: not joined → only the intro/join page renders.
|
|
///
|
|
/// "Joined" is a one-way latch persisted as `~/.config/tv-anarchy/fleet.json`
|
|
/// (internal name for the install membership marker). On launch, an install that
|
|
/// predates the latch is grandfathered in by evidence on disk (an existing
|
|
/// devices.json, mesh credentials, or a live 10.9.0.x mesh address) — so the Mac
|
|
/// never sees the intro page. Detection is files-first and offline-safe.
|
|
public struct FleetState: Sendable {
|
|
/// The persisted join record.
|
|
public struct Record: Codable, Sendable, Equatable {
|
|
public var joined: Bool
|
|
public var device: String?
|
|
public var address: String?
|
|
public var joinedAt: Date?
|
|
}
|
|
|
|
private let configDir: URL
|
|
private let meshStore: MeshClientStore
|
|
private static let fm = FileManager.default
|
|
|
|
public init(configDir: URL? = nil, meshStore: MeshClientStore = MeshClientStore()) {
|
|
self.configDir = configDir ?? FileManager.default.homeDirectoryForCurrentUser
|
|
.appendingPathComponent(".config/tv-anarchy", isDirectory: true)
|
|
self.meshStore = meshStore
|
|
}
|
|
|
|
private var markerURL: URL { configDir.appendingPathComponent("fleet.json") }
|
|
private var devicesURL: URL { configDir.appendingPathComponent("devices.json") }
|
|
|
|
public func record() -> Record? {
|
|
guard let data = try? Data(contentsOf: markerURL) else { return nil }
|
|
return try? JSONDecoder().decode(Record.self, from: data)
|
|
}
|
|
|
|
/// The gate. Checks the latch first; otherwise grandfathers an existing
|
|
/// install (and writes the latch so the answer is stable from then on).
|
|
/// `probeInterfaces: false` keeps tests/file-only callers hermetic.
|
|
public func isJoined(probeInterfaces: Bool = true) -> Bool {
|
|
if record()?.joined == true { return true }
|
|
// Pre-latch evidence this Mac already belongs to the install:
|
|
// • a devices.json — the app has run (and was usable) here before,
|
|
// • issued mesh credentials — this Mac is the enrolling/trusted node,
|
|
// • a 10.9.0.x address — the system tunnel is configured regardless.
|
|
let grandfathered = Self.fm.fileExists(atPath: devicesURL.path)
|
|
|| !meshStore.list().isEmpty
|
|
|| (probeInterfaces && Self.hasMeshAddress())
|
|
if grandfathered { try? latch(device: nil, address: nil) }
|
|
return grandfathered
|
|
}
|
|
|
|
/// Complete a join from a scanned/pasted wg-quick config: validate, persist
|
|
/// the credentials in the shared mesh-client store (same layout the
|
|
/// enrolling side and `wg-phone-add` use), and latch. The tunnel itself is
|
|
/// NOT brought up here — actuation stays outside the app; at home the LAN
|
|
/// works without it.
|
|
@discardableResult
|
|
public func join(configText: String, fallbackDevice: String) throws -> WGQuickConfig {
|
|
let parsed = try WGQuickParser.parse(configText)
|
|
let device = parsed.device ?? fallbackDevice
|
|
try meshStore.save(MeshClient(device: device, privateKey: parsed.privateKey,
|
|
publicKey: parsed.publicKey, address: parsed.address))
|
|
try latch(device: device, address: parsed.address)
|
|
return parsed
|
|
}
|
|
|
|
private func latch(device: String?, address: String?) throws {
|
|
try Self.fm.createDirectory(at: configDir, withIntermediateDirectories: true)
|
|
let rec = Record(joined: true, device: device, address: address, joinedAt: Date())
|
|
let enc = JSONEncoder()
|
|
enc.outputFormatting = [.prettyPrinted, .sortedKeys]
|
|
try enc.encode(rec).write(to: markerURL, options: .atomic)
|
|
}
|
|
|
|
/// True when any local interface carries a mesh (10.9.0.x) address — the
|
|
/// system WireGuard tunnel is configured on this Mac.
|
|
public static func hasMeshAddress() -> Bool {
|
|
LocalNetwork.meshAddress() != nil
|
|
}
|
|
}
|