tv-anarchy/Sources/TVAnarchyiOS/PlayerScreen.swift
Natalie f0669f1ca8 feat(ios): TVAnarchyiOS app target + UI tests
(cherry picked from commit 7f8f4b0dd92358ba687f8230a922d8f316cb06e9)
2026-06-09 05:50:26 -07:00

113 lines
3.6 KiB
Swift

// Full-screen video with an auto-hiding control overlay. The VLCKit drawable is
// a plain UIView bridged via UIViewRepresentable; all transport goes through
// VLCPlayerModel.
import SwiftUI
import MobileVLCKit
import LilithDesignTokens
struct PlayerScreen: View {
let title: String
let url: URL
let networkCachingMs: Int
@StateObject private var model = VLCPlayerModel()
@State private var controlsVisible = true
@State private var scrubValue: Double = 0
var body: some View {
ZStack {
Color.black.ignoresSafeArea()
VLCVideoView(player: model.player)
.ignoresSafeArea()
if model.buffering {
ProgressView().tint(.white).scaleEffect(1.4)
}
if controlsVisible {
controls
.transition(.opacity)
}
}
.contentShape(Rectangle())
.onTapGesture {
withAnimation { controlsVisible.toggle() }
}
.navigationTitle(title)
.navigationBarTitleDisplayMode(.inline)
.onAppear { model.start(url: url, networkCachingMs: networkCachingMs) }
.onDisappear { model.teardown() }
}
private var controls: some View {
VStack {
Spacer()
HStack(spacing: 40) {
Button { model.skip(seconds: -10) } label: {
Image(systemName: "gobackward.10").font(.title)
}
Button { model.togglePlay() } label: {
Image(systemName: model.isPlaying ? "pause.fill" : "play.fill")
.font(.system(size: 48))
}
.accessibilityIdentifier("playPauseButton")
Button { model.skip(seconds: 30) } label: {
Image(systemName: "goforward.30").font(.title)
}
}
.foregroundStyle(.white)
.padding(.bottom, 12)
HStack(spacing: 10) {
Text(model.elapsed)
.font(.caption.monospacedDigit())
.accessibilityIdentifier("elapsed") // UI test asserts this advances
Slider(
value: $scrubValue,
in: 0...1,
onEditingChanged: { editing in
if editing {
model.beginScrub()
} else {
model.commitScrub(to: scrubValue)
}
}
)
.tint(AppColors.primary)
Text(model.remaining)
.font(.caption.monospacedDigit())
}
.foregroundStyle(.white)
.padding(.horizontal)
.padding(.bottom, 24)
}
.background(
LinearGradient(
colors: [.clear, .black.opacity(0.6)],
startPoint: .center,
endPoint: .bottom
)
.ignoresSafeArea()
)
// Keep the slider in sync with playback unless the user is dragging it.
.onReceive(model.$position) { p in
scrubValue = p
}
}
}
/// Hosts VLCKit's video output. The drawable must be set on a live UIView, so we
/// hand the player's drawable to the view we create here.
struct VLCVideoView: UIViewRepresentable {
let player: VLCMediaPlayer
func makeUIView(context: Context) -> UIView {
let view = UIView()
view.backgroundColor = .black
player.drawable = view
return view
}
func updateUIView(_ uiView: UIView, context: Context) {}
}