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

96 lines
4.4 KiB
Swift

// User-editable connection + playback settings, persisted to UserDefaults.
// `networkCachingMs` is the VLCKit input buffer ("Settings including buffer" in
// the product ask) higher absorbs more network jitter at the cost of seek
// latency; it's passed to VLCMedia as --network-caching.
//
// Two hosts: `host` (home LAN) and `fallbackHost` (the WireGuard address).
// `probeHosts()` health-checks them in order and routes every client through
// whichever answered, so leaving the house doesn't mean editing Settings.
import Foundation
@MainActor
final class BridgeSettings: ObservableObject {
private let store = UserDefaults.standard
@Published var host: String { didSet { store.set(host, forKey: Keys.host) } }
@Published var fallbackHost: String { didSet { store.set(fallbackHost, forKey: Keys.fallbackHost) } }
@Published var port: Int { didSet { store.set(port, forKey: Keys.port) } }
@Published var token: String { didSet { store.set(token, forKey: Keys.token) } }
@Published var networkCachingMs: Int { didSet { store.set(networkCachingMs, forKey: Keys.buffer) } }
@Published var prefetchEnabled: Bool { didSet { store.set(prefetchEnabled, forKey: Keys.prefetchOn) } }
@Published var prefetchCount: Int { didSet { store.set(prefetchCount, forKey: Keys.prefetchN) } }
// Flight pack: keep the next N episodes of EVERY in-progress show downloaded,
// under a total storage budget.
@Published var packEnabled: Bool { didSet { store.set(packEnabled, forKey: Keys.packOn) } }
@Published var packEpisodesPerShow: Int { didSet { store.set(packEpisodesPerShow, forKey: Keys.packN) } }
@Published var packBudgetGB: Int { didSet { store.set(packBudgetGB, forKey: Keys.packGB) } }
/// The host that most recently answered /healthz (primary preferred).
@Published private(set) var activeHost: String
init() {
let h = store.string(forKey: Keys.host) ?? "127.0.0.1"
host = h
activeHost = h
fallbackHost = store.string(forKey: Keys.fallbackHost) ?? ""
let p = store.integer(forKey: Keys.port)
port = p == 0 ? 8787 : p
token = store.string(forKey: Keys.token) ?? ""
let buf = store.integer(forKey: Keys.buffer)
networkCachingMs = buf == 0 ? 1500 : buf
prefetchEnabled = store.object(forKey: Keys.prefetchOn) as? Bool ?? true
let n = store.integer(forKey: Keys.prefetchN)
prefetchCount = n == 0 ? 3 : n
packEnabled = store.object(forKey: Keys.packOn) as? Bool ?? false
let pn = store.integer(forKey: Keys.packN)
packEpisodesPerShow = pn == 0 ? 2 : pn
let gb = store.integer(forKey: Keys.packGB)
packBudgetGB = gb == 0 ? 20 : gb
}
var baseURL: URL? {
URL(string: "http://\(activeHost):\(port)")
}
var client: BridgeClient? {
guard let baseURL else { return nil }
return BridgeClient(baseURL: baseURL, token: token.isEmpty ? nil : token)
}
/// Try the primary, then the fallback; flip `activeHost` to the first that
/// answers. Cheap (2s timeout per host) callers run it on foreground.
func probeHosts() async {
for candidate in [host, fallbackHost] where !candidate.isEmpty {
if await healthz(host: candidate) {
if activeHost != candidate { activeHost = candidate }
return
}
}
// Nobody answered: keep the primary so error messages point at it.
activeHost = host
}
private func healthz(host: String) async -> Bool {
guard let url = URL(string: "http://\(host):\(port)/healthz") else { return false }
var request = URLRequest(url: url)
request.timeoutInterval = 2
guard let (_, response) = try? await URLSession.shared.data(for: request),
let http = response as? HTTPURLResponse else { return false }
return http.statusCode == 200
}
private enum Keys {
static let host = "bridge.host"
static let fallbackHost = "bridge.fallbackHost"
static let port = "bridge.port"
static let token = "bridge.token"
static let buffer = "bridge.networkCachingMs"
static let prefetchOn = "bridge.prefetchEnabled"
static let prefetchN = "bridge.prefetchCount"
static let packOn = "pack.enabled"
static let packN = "pack.episodesPerShow"
static let packGB = "pack.budgetGB"
}
}