152 lines
7.8 KiB
Swift
152 lines
7.8 KiB
Swift
import SwiftUI
|
||
import TVAnarchyCore
|
||
|
||
/// Add/edit a device: its role (`type`, which presets services), its overridable
|
||
/// `services`, and its player backend. Flat fields → a `DeviceConfig` on save.
|
||
/// Kind-specific sections appear for the chosen backend (VLC / mpv-SSH / QuickTime).
|
||
struct DeviceEditView: View {
|
||
let existing: DeviceConfig?
|
||
let onSave: (DeviceConfig) -> Void
|
||
@Environment(\.dismiss) private var dismiss
|
||
|
||
@State private var name: String
|
||
@State private var type: DeviceType
|
||
@State private var services: DeviceServices
|
||
@State private var kind: HostKind
|
||
// VLC
|
||
@State private var vlcHost: String
|
||
@State private var vlcPort: String
|
||
// mpv-ipc
|
||
@State private var endpoints: String
|
||
@State private var socket: String
|
||
@State private var sudo: Bool
|
||
@State private var socat: String
|
||
@State private var volumeScale: String
|
||
@State private var helperBin: String
|
||
// roku (ECP)
|
||
@State private var rokuHost: String
|
||
@State private var rokuPort: String
|
||
|
||
init(existing: DeviceConfig?, onSave: @escaping (DeviceConfig) -> Void) {
|
||
self.existing = existing
|
||
self.onSave = onSave
|
||
let t = existing?.type ?? .laptop
|
||
_name = State(initialValue: existing?.name ?? "")
|
||
_type = State(initialValue: t)
|
||
_services = State(initialValue: existing?.services ?? t.defaultServices)
|
||
_kind = State(initialValue: existing?.kind == .blacktv ? .mpvIPC : (existing?.kind ?? .mpvIPC))
|
||
_vlcHost = State(initialValue: existing?.vlc?.host ?? "127.0.0.1")
|
||
_vlcPort = State(initialValue: existing?.vlc.map { String($0.port) } ?? "8080")
|
||
_endpoints = State(initialValue: (existing?.mpv?.endpoints ?? existing?.ssh?.endpoints ?? []).joined(separator: ", "))
|
||
_socket = State(initialValue: existing?.mpv?.socket ?? "/tmp/mpv.sock")
|
||
_sudo = State(initialValue: existing?.mpv?.sudo ?? true)
|
||
_socat = State(initialValue: existing?.mpv?.socat ?? "socat")
|
||
_volumeScale = State(initialValue: existing?.mpv.map { String($0.volumeScale) } ?? "130")
|
||
_helperBin = State(initialValue: existing?.commands?.launchFile?.first ?? existing?.ssh?.bin ?? "/usr/local/bin/black-tv")
|
||
_rokuHost = State(initialValue: existing?.roku?.host ?? "")
|
||
_rokuPort = State(initialValue: existing?.roku.map { String($0.port) } ?? "8060")
|
||
}
|
||
|
||
var body: some View {
|
||
VStack(alignment: .leading, spacing: 0) {
|
||
Text(existing == nil ? "Add Device" : "Edit Device").font(.title2).bold().padding()
|
||
Form {
|
||
Section {
|
||
TextField("Name", text: $name)
|
||
Picker("Type", selection: $type) {
|
||
ForEach(DeviceType.allCases) { Label($0.label, systemImage: $0.icon).tag($0) }
|
||
}
|
||
.onChange(of: type) { _, newType in services = newType.defaultServices }
|
||
Text("The type presets services below — override any toggle. ‘*’ services are designed but not yet actuated.")
|
||
.font(.caption).foregroundStyle(.secondary)
|
||
}
|
||
Section("Services") {
|
||
Toggle("Stream (playback target)", isOn: $services.stream)
|
||
Toggle("Offline cache (next episodes of recent shows)", isOn: $services.offlineCache)
|
||
Toggle("Seed while playing *", isOn: $services.ttlSeed)
|
||
Toggle("Hold copies / custody *", isOn: $services.custody)
|
||
Toggle("Public-swarm face *", isOn: $services.publicSwarmFace)
|
||
Toggle("F2F relay *", isOn: $services.f2fRelay)
|
||
Toggle("Mesh anchor / registry *", isOn: $services.meshAnchor)
|
||
}
|
||
Section {
|
||
Picker("Backend", selection: $kind) {
|
||
ForEach(HostKind.editable) { Text($0.label).tag($0) }
|
||
}
|
||
} header: { Text("Player backend") }
|
||
switch kind {
|
||
case .vlc:
|
||
Section("VLC HTTP") {
|
||
TextField("Address", text: $vlcHost)
|
||
TextField("Port", text: $vlcPort)
|
||
Text("Password is read from the VLC config, not stored here.")
|
||
.font(.caption).foregroundStyle(.secondary)
|
||
}
|
||
case .mpvIPC:
|
||
Section("mpv over SSH") {
|
||
TextField("SSH endpoints (comma-separated, in try order)", text: $endpoints)
|
||
.help("e.g. lilith@10.0.0.11, lilith@10.9.0.4")
|
||
TextField("mpv IPC socket", text: $socket)
|
||
Toggle("Socket needs sudo", isOn: $sudo)
|
||
TextField("socat binary", text: $socat)
|
||
TextField("Volume scale (max %)", text: $volumeScale)
|
||
TextField("Helper command (launch/releases/stats)", text: $helperBin)
|
||
.help("e.g. /usr/local/bin/black-tv — delegated ops the IPC can't do")
|
||
}
|
||
case .quicktime:
|
||
Section("QuickTime") {
|
||
Text("Local playback via AppleScript. No configuration needed.")
|
||
.font(.caption).foregroundStyle(.secondary)
|
||
}
|
||
case .roku:
|
||
Section("Roku ECP") {
|
||
TextField("Address", text: $rokuHost)
|
||
.help("The Roku's LAN IP — discoverable via SSDP (ST: roku:ecp)")
|
||
TextField("Port", text: $rokuPort)
|
||
Text("Transport control of the stick's own playback (no auth — LAN REST). The library never plays TO a Roku; that needs the planned dev channel.")
|
||
.font(.caption).foregroundStyle(.secondary)
|
||
}
|
||
case .blacktv:
|
||
EmptyView()
|
||
}
|
||
}
|
||
.formStyle(.grouped)
|
||
HStack {
|
||
Spacer()
|
||
Button("Cancel") { dismiss() }
|
||
Button("Save") { onSave(build()); dismiss() }
|
||
.keyboardShortcut(.defaultAction)
|
||
.disabled(name.trimmingCharacters(in: .whitespaces).isEmpty)
|
||
}
|
||
.padding()
|
||
}
|
||
.frame(width: 460, height: 600)
|
||
}
|
||
|
||
private func build() -> DeviceConfig {
|
||
let did = existing?.id ?? slug(name)
|
||
switch kind {
|
||
case .vlc:
|
||
return DeviceConfig(id: did, name: name, kind: .vlc, type: type, services: services,
|
||
vlc: VLCConn(host: vlcHost, port: Int(vlcPort) ?? 8080))
|
||
case .quicktime:
|
||
return DeviceConfig(id: did, name: name, kind: .quicktime, type: type, services: services)
|
||
case .roku:
|
||
return DeviceConfig(id: did, name: name, kind: .roku, type: type, services: services,
|
||
roku: RokuConn(host: rokuHost, port: Int(rokuPort) ?? 8060))
|
||
case .mpvIPC, .blacktv:
|
||
let eps = endpoints.split(whereSeparator: { ", \n\t".contains($0) }).map(String.init)
|
||
let mpv = MpvConn(endpoints: eps, socket: socket, sudo: sudo,
|
||
socat: socat, volumeScale: Int(volumeScale) ?? 130)
|
||
let cmds = helperBin.trimmingCharacters(in: .whitespaces).isEmpty
|
||
? nil : CommandsConfig.blackTVDefaults(bin: helperBin)
|
||
return DeviceConfig(id: did, name: name, kind: .mpvIPC, type: type, services: services,
|
||
mpv: mpv, commands: cmds)
|
||
}
|
||
}
|
||
|
||
private func slug(_ s: String) -> String {
|
||
let base = s.lowercased().map { $0.isLetter || $0.isNumber ? $0 : "-" }
|
||
return String(base).split(separator: "-").joined(separator: "-")
|
||
}
|
||
}
|