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>
96 lines
4.4 KiB
Swift
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"
|
|
}
|
|
}
|