2022-08-28 17:18:49 +00:00
|
|
|
import Defaults
|
|
|
|
import SwiftUI
|
|
|
|
|
|
|
|
struct Seek: View {
|
|
|
|
#if os(iOS)
|
|
|
|
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
|
|
|
#endif
|
|
|
|
|
|
|
|
@EnvironmentObject<PlayerControlsModel> private var controls
|
|
|
|
@EnvironmentObject<PlayerTimeModel> private var model
|
|
|
|
|
|
|
|
@State private var dismissTimer: Timer?
|
|
|
|
@State private var isSeeking = false
|
|
|
|
|
|
|
|
private var updateThrottle = Throttle(interval: 2)
|
|
|
|
|
|
|
|
@Default(.playerControlsLayout) private var regularPlayerControlsLayout
|
|
|
|
@Default(.fullScreenPlayerControlsLayout) private var fullScreenPlayerControlsLayout
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
Button(action: model.restoreTime) {
|
2022-08-28 22:21:12 +00:00
|
|
|
VStack(spacing: playerControlsLayout.osdSpacing) {
|
2022-08-28 17:18:49 +00:00
|
|
|
ProgressBar(value: progress)
|
2022-08-28 22:21:12 +00:00
|
|
|
.frame(maxHeight: playerControlsLayout.osdProgressBarHeight)
|
2022-08-28 17:18:49 +00:00
|
|
|
|
|
|
|
timeline
|
|
|
|
|
|
|
|
if isSeeking {
|
|
|
|
Divider()
|
|
|
|
gestureSeekTime
|
|
|
|
.foregroundColor(.secondary)
|
|
|
|
.font(.system(size: playerControlsLayout.chapterFontSize).monospacedDigit())
|
|
|
|
.frame(height: playerControlsLayout.chapterFontSize + 5)
|
|
|
|
|
|
|
|
if let chapter = projectedChapter {
|
|
|
|
Divider()
|
|
|
|
Text(chapter.title)
|
|
|
|
.multilineTextAlignment(.center)
|
|
|
|
.font(.system(size: playerControlsLayout.chapterFontSize))
|
2022-08-28 22:21:12 +00:00
|
|
|
.fixedSize(horizontal: false, vertical: true)
|
2022-08-28 17:18:49 +00:00
|
|
|
}
|
|
|
|
if let segment = projectedSegment {
|
|
|
|
Text(SponsorBlockAPI.categoryDescription(segment.category) ?? "Sponsor")
|
|
|
|
.font(.system(size: playerControlsLayout.segmentFontSize))
|
|
|
|
.foregroundColor(Color("AppRedColor"))
|
|
|
|
}
|
|
|
|
} else {
|
2022-08-28 22:21:12 +00:00
|
|
|
#if !os(tvOS)
|
|
|
|
if !model.restoreSeekTime.isNil {
|
|
|
|
Divider()
|
|
|
|
Label(model.restoreSeekPlaybackTime, systemImage: "arrow.counterclockwise")
|
|
|
|
.foregroundColor(.secondary)
|
|
|
|
.font(.system(size: playerControlsLayout.chapterFontSize).monospacedDigit())
|
|
|
|
.frame(height: playerControlsLayout.chapterFontSize + 5)
|
|
|
|
}
|
|
|
|
#endif
|
2022-08-28 17:18:49 +00:00
|
|
|
Group {
|
|
|
|
switch model.lastSeekType {
|
|
|
|
case let .segmentSkip(category):
|
|
|
|
Divider()
|
|
|
|
Text(SponsorBlockAPI.categoryDescription(category) ?? "Sponsor")
|
|
|
|
.font(.system(size: playerControlsLayout.segmentFontSize))
|
|
|
|
.foregroundColor(Color("AppRedColor"))
|
|
|
|
default:
|
|
|
|
EmptyView()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
#if os(tvOS)
|
|
|
|
.frame(minWidth: 250, minHeight: 100)
|
2022-08-28 22:21:12 +00:00
|
|
|
.padding(30)
|
2022-08-28 17:18:49 +00:00
|
|
|
#endif
|
|
|
|
.frame(maxWidth: playerControlsLayout.seekOSDWidth)
|
|
|
|
.padding(2)
|
|
|
|
.modifier(ControlBackgroundModifier())
|
|
|
|
.clipShape(RoundedRectangle(cornerRadius: 3))
|
|
|
|
.foregroundColor(.primary)
|
|
|
|
}
|
|
|
|
#if os(tvOS)
|
|
|
|
.fixedSize()
|
|
|
|
.buttonStyle(.card)
|
|
|
|
#else
|
|
|
|
.buttonStyle(.plain)
|
|
|
|
#endif
|
|
|
|
.opacity(visible || YatteeApp.isForPreviews ? 1 : 0)
|
|
|
|
.onChange(of: model.lastSeekTime) { _ in
|
|
|
|
isSeeking = false
|
|
|
|
dismissTimer?.invalidate()
|
|
|
|
dismissTimer = Delay.by(3) {
|
|
|
|
withAnimation(.easeIn(duration: 0.1)) { model.seekOSDDismissed = true }
|
|
|
|
}
|
|
|
|
|
|
|
|
if model.seekOSDDismissed {
|
|
|
|
withAnimation(.easeIn(duration: 0.1)) { self.model.seekOSDDismissed = false }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.onChange(of: model.gestureSeek) { newValue in
|
|
|
|
let newIsSeekingValue = isSeeking || model.gestureSeek != 0
|
|
|
|
if !isSeeking, newIsSeekingValue {
|
|
|
|
model.onSeekGestureStart()
|
|
|
|
}
|
|
|
|
isSeeking = newIsSeekingValue
|
|
|
|
guard newValue != 0 else { return }
|
|
|
|
updateThrottle.execute {
|
|
|
|
model.player.backend.getTimeUpdates()
|
|
|
|
model.player.backend.updateControls()
|
|
|
|
}
|
|
|
|
|
|
|
|
dismissTimer?.invalidate()
|
|
|
|
if model.seekOSDDismissed {
|
|
|
|
withAnimation(.easeIn(duration: 0.1)) { self.model.seekOSDDismissed = false }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var timeline: some View {
|
|
|
|
let text = model.gestureSeek != 0 && model.lastSeekTime.isNil ?
|
|
|
|
"\(model.gestureSeekDestinationPlaybackTime)/\(model.durationPlaybackTime)" :
|
|
|
|
"\(model.lastSeekPlaybackTime)/\(model.durationPlaybackTime)"
|
|
|
|
|
|
|
|
return Text(text)
|
|
|
|
.fontWeight(.bold)
|
|
|
|
.font(.system(size: playerControlsLayout.projectedTimeFontSize).monospacedDigit())
|
|
|
|
}
|
|
|
|
|
|
|
|
var gestureSeekTime: some View {
|
|
|
|
var seek = model.gestureSeekDestinationTime - model.currentTime.seconds
|
|
|
|
if seek > 0 {
|
|
|
|
seek = min(seek, model.duration.seconds - model.currentTime.seconds)
|
|
|
|
} else {
|
|
|
|
seek = min(seek, model.currentTime.seconds)
|
|
|
|
}
|
|
|
|
let timeText = abs(seek)
|
|
|
|
.formattedAsPlaybackTime(allowZero: true, forceHours: model.forceHours) ?? ""
|
|
|
|
|
|
|
|
return Label(
|
|
|
|
timeText,
|
|
|
|
systemImage: seek >= 0 ? "goforward.plus" : "gobackward.minus"
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
var visible: Bool {
|
|
|
|
guard !(model.lastSeekTime.isNil && !isSeeking) else { return false }
|
|
|
|
if let type = model.lastSeekType, !type.presentable { return false }
|
|
|
|
|
|
|
|
return !controls.presentingControls && !controls.presentingOverlays && !model.seekOSDDismissed
|
|
|
|
}
|
|
|
|
|
|
|
|
var progress: Double {
|
|
|
|
if isSeeking {
|
|
|
|
return model.gestureSeekDestinationTime / model.duration.seconds
|
|
|
|
}
|
|
|
|
|
|
|
|
guard model.duration.seconds.isFinite, model.duration.seconds > 0 else { return 0 }
|
|
|
|
guard let seekTime = model.lastSeekTime else { return model.currentTime.seconds / model.duration.seconds }
|
|
|
|
|
|
|
|
return seekTime.seconds / model.duration.seconds
|
|
|
|
}
|
|
|
|
|
|
|
|
var projectedChapter: Chapter? {
|
|
|
|
(model.player?.currentVideo?.chapters ?? []).last { $0.start <= model.gestureSeekDestinationTime }
|
|
|
|
}
|
|
|
|
|
|
|
|
var projectedSegment: Segment? {
|
|
|
|
(model.player?.sponsorBlock.segments ?? []).first { $0.timeInSegment(.secondsInDefaultTimescale(model.gestureSeekDestinationTime)) }
|
|
|
|
}
|
|
|
|
|
|
|
|
var playerControlsLayout: PlayerControlsLayout {
|
|
|
|
fullScreenLayout ? fullScreenPlayerControlsLayout : regularPlayerControlsLayout
|
|
|
|
}
|
|
|
|
|
|
|
|
var fullScreenLayout: Bool {
|
|
|
|
guard let player = model.player else { return false }
|
|
|
|
#if os(iOS)
|
|
|
|
return player.playingFullScreen || verticalSizeClass == .compact
|
|
|
|
#else
|
|
|
|
return player.playingFullScreen
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct Seek_Previews: PreviewProvider {
|
|
|
|
static var previews: some View {
|
|
|
|
Seek()
|
|
|
|
.environmentObject(PlayerTimeModel())
|
|
|
|
}
|
|
|
|
}
|