74 lines
3.0 KiB
Swift
74 lines
3.0 KiB
Swift
|
|
// ConnectivitySettings.swift
|
||
|
|
// Jarvis — persisted connection profiles: a Direct/LAN address and a Remote/VPN
|
||
|
|
// address for the same server, the chosen provider, and the auto-switch policy.
|
||
|
|
|
||
|
|
import Foundation
|
||
|
|
|
||
|
|
@MainActor
|
||
|
|
final class ConnectivitySettings: ObservableObject {
|
||
|
|
static let shared = ConnectivitySettings()
|
||
|
|
|
||
|
|
private let key = "jarvis_connectivity_v2"
|
||
|
|
|
||
|
|
@Published var provider: VPNProvider { didSet { persist() } }
|
||
|
|
@Published var directHost: String { didSet { persist() } } // LAN, e.g. 192.168.30.50:8080
|
||
|
|
@Published var remoteHost: String { didSet { persist() } } // Tailscale/VPN address
|
||
|
|
/// "Use <provider> when remote" — when off, only the LAN address is ever used.
|
||
|
|
@Published var remoteEnabled: Bool { didSet { persist() } }
|
||
|
|
/// "Auto-connect" — re-resolve automatically when the network path changes.
|
||
|
|
@Published var autoConnect: Bool { didSet { persist() } }
|
||
|
|
/// Debounce before switching endpoints after a network change (seconds).
|
||
|
|
@Published var switchDelaySeconds: Int { didSet { persist() } }
|
||
|
|
|
||
|
|
private struct Stored: Codable {
|
||
|
|
var provider: VPNProvider
|
||
|
|
var directHost: String
|
||
|
|
var remoteHost: String
|
||
|
|
var remoteEnabled: Bool?
|
||
|
|
var autoConnect: Bool?
|
||
|
|
var switchDelaySeconds: Int?
|
||
|
|
}
|
||
|
|
|
||
|
|
private init() {
|
||
|
|
if let data = UserDefaults.standard.data(forKey: key),
|
||
|
|
let s = try? JSONDecoder().decode(Stored.self, from: data) {
|
||
|
|
provider = s.provider
|
||
|
|
directHost = s.directHost
|
||
|
|
remoteHost = s.remoteHost
|
||
|
|
remoteEnabled = s.remoteEnabled ?? true
|
||
|
|
autoConnect = s.autoConnect ?? true
|
||
|
|
switchDelaySeconds = s.switchDelaySeconds ?? 5
|
||
|
|
} else {
|
||
|
|
provider = .tailscale
|
||
|
|
directHost = UserDefaults.standard.string(forKey: "jarvis_server_host") ?? ""
|
||
|
|
remoteHost = ""
|
||
|
|
remoteEnabled = true
|
||
|
|
autoConnect = true
|
||
|
|
switchDelaySeconds = 5
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private func persist() {
|
||
|
|
let s = Stored(provider: provider, directHost: directHost, remoteHost: remoteHost,
|
||
|
|
remoteEnabled: remoteEnabled, autoConnect: autoConnect,
|
||
|
|
switchDelaySeconds: switchDelaySeconds)
|
||
|
|
if let data = try? JSONEncoder().encode(s) {
|
||
|
|
UserDefaults.standard.set(data, forKey: key)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Whether the remote endpoint participates in resolution right now.
|
||
|
|
var remoteActiveInPolicy: Bool { provider.needsRemote && remoteEnabled }
|
||
|
|
|
||
|
|
/// Hosts to probe, in priority order (LAN first), de-duplicated and trimmed.
|
||
|
|
var candidates: [String] {
|
||
|
|
let d = directHost.trimmingCharacters(in: .whitespaces)
|
||
|
|
let r = remoteActiveInPolicy ? remoteHost.trimmingCharacters(in: .whitespaces) : ""
|
||
|
|
var seen = Set<String>(); var out: [String] = []
|
||
|
|
for h in [d, r] where !h.isEmpty && !seen.contains(h) { seen.insert(h); out.append(h) }
|
||
|
|
return out
|
||
|
|
}
|
||
|
|
|
||
|
|
var isConfigured: Bool { !candidates.isEmpty }
|
||
|
|
}
|