// Remote tab: control the Black TV (mpv on black's HDMI). Polls status and // issues transport commands through the bridge's /remote endpoints. import SwiftUI import LilithDesignTokens struct RemoteView: View { @EnvironmentObject private var settings: BridgeSettings @State private var status: RemoteStatus? @State private var errorText: String? @State private var volume: Double = 100 var body: some View { NavigationStack { ZStack { AppColors.background.ignoresSafeArea() VStack(spacing: AppSpacing.xl) { nowPlaying transport volumeControl if let errorText { Text(errorText).font(AppTypography.caption()).foregroundStyle(AppColors.Semantic.error) } Spacer() } .padding(AppSpacing.lg) } .navigationTitle("Black TV") .task { await pollLoop() } } } private var nowPlaying: some View { VStack(spacing: AppSpacing.sm) { Image(systemName: "tv") .font(.system(size: 56)) .foregroundStyle(status?.playing == true ? AppColors.primary : AppColors.textTertiary) .padding(.top, AppSpacing.xl) Text(status?.title ?? "Nothing playing") .font(AppTypography.h5()) .foregroundStyle(AppColors.textPrimary) .multilineTextAlignment(.center) .lineLimit(2) if let s = status, let pos = s.position, let dur = s.duration, dur > 0 { ProgressView(value: min(1, pos / dur)).tint(AppColors.primary) } } } private var transport: some View { HStack(spacing: AppSpacing.xl) { transportButton("backward.end.fill") { await send { try await $0.remoteCommand(action: "prev") } } transportButton("gobackward.30") { await send { try await $0.remoteCommand(action: "seek", value: -30) } } transportButton(status?.paused == true ? "play.fill" : "pause.fill", big: true) { await send { try await $0.remoteCommand(action: "playpause") } } transportButton("goforward.30") { await send { try await $0.remoteCommand(action: "seek", value: 30) } } transportButton("forward.end.fill") { await send { try await $0.remoteCommand(action: "next") } } } .foregroundStyle(AppColors.textPrimary) } private var volumeControl: some View { HStack(spacing: AppSpacing.md) { Image(systemName: "speaker.fill").foregroundStyle(AppColors.textSecondary) Slider(value: $volume, in: 0...130, step: 1, onEditingChanged: { editing in if !editing { Task { await send { try await $0.remoteCommand(action: "volume", value: volume) } } } }) .tint(AppColors.primary) Image(systemName: "speaker.wave.3.fill").foregroundStyle(AppColors.textSecondary) } } private func transportButton(_ symbol: String, big: Bool = false, _ action: @escaping () async -> Void) -> some View { Button { Task { await action() } } label: { Image(systemName: symbol).font(.system(size: big ? 44 : 26)) } .buttonStyle(.plain) } private func send(_ op: (BridgeClient) async throws -> Void) async { guard let client = settings.client else { return } do { try await op(client) errorText = nil status = try? await client.remoteStatus() } catch { errorText = error.localizedDescription } } private func pollLoop() async { while !Task.isCancelled { if let client = settings.client { if let s = try? await client.remoteStatus() { status = s if let v = s.volume { volume = v } errorText = nil } } try? await Task.sleep(for: .seconds(3)) } } }