jellyflood/Swiftfin tvOS/Views/VideoPlayerContainerState/PlaybackControls/Components/PlaybackProgress.swift

117 lines
3.5 KiB
Swift

//
// Swiftfin is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, you can obtain one at https://mozilla.org/MPL/2.0/.
//
// Copyright (c) 2025 Jellyfin & Jellyfin Contributors
//
import Defaults
import JellyfinAPI
import SwiftUI
// TODO: bar color default to style
// TODO: remove compact buttons?
// TODO: capsule scale on editing
// TODO: possible issue with runTimeSeconds == 0
// TODO: live tv
extension VideoPlayer.PlaybackControls {
struct PlaybackProgress: View {
@Default(.VideoPlayer.Overlay.chapterSlider)
private var chapterSlider
@EnvironmentObject
private var containerState: VideoPlayerContainerState
@EnvironmentObject
private var manager: MediaPlayerManager
@EnvironmentObject
private var scrubbedSecondsBox: PublishedBox<Duration>
@FocusState
private var isFocused: Bool
@State
private var sliderSize = CGSize.zero
private var isScrubbing: Bool {
get {
containerState.isScrubbing
}
nonmutating set {
containerState.isScrubbing = newValue
}
}
private var previewXOffset: CGFloat {
let p = sliderSize.width * scrubbedProgress
return clamp(p, min: 100, max: sliderSize.width - 100)
}
private var scrubbedProgress: Double {
guard let runtime = manager.item.runtime, runtime > .zero else { return 0 }
return scrubbedSeconds / runtime
}
private var scrubbedSeconds: Duration {
scrubbedSecondsBox.value
}
@ViewBuilder
private var liveIndicator: some View {
Text("Live")
.font(.subheadline)
.fontWeight(.semibold)
.foregroundStyle(.white)
.padding(.horizontal, 16)
.padding(.vertical, 4)
.background {
Capsule()
.fill(Color.gray)
}
}
@ViewBuilder
private var capsuleSlider: some View {
let resolution: Double = 100
CapsuleSlider(
value: $scrubbedSecondsBox.value.map(
getter: {
guard let runtime = manager.item.runtime, runtime > .zero else { return 0 }
return clamp(($0.seconds / runtime.seconds) * resolution, min: 0, max: resolution)
},
setter: { (manager.item.runtime ?? .zero) * ($0 / resolution) }
),
total: resolution
)
.onEditingChanged { isEditing in
isScrubbing = isEditing
print(isEditing)
}
.frame(height: 50)
}
var body: some View {
VStack(spacing: 10) {
if manager.item.isLiveStream {
liveIndicator
.frame(maxWidth: .infinity, alignment: .leading)
} else {
capsuleSlider
.trackingSize($sliderSize)
SplitTimeStamp()
}
}
.focused($isFocused)
.scaleEffect(isFocused ? 1.0 : 0.95)
.animation(.easeInOut(duration: 0.3), value: isFocused)
.foregroundStyle(isFocused ? Color.white : Color.white.opacity(0.8))
}
}
}