mirror of
https://github.com/yattee/yattee.git
synced 2025-08-05 18:24:02 +00:00
Controls layouts, gestures and settings
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import Defaults
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
@@ -5,6 +6,27 @@ struct Buffering: View {
|
||||
var reason = "Buffering stream..."
|
||||
var state: String?
|
||||
|
||||
#if os(iOS)
|
||||
@Environment(\.verticalSizeClass) private var verticalSizeClass
|
||||
#endif
|
||||
|
||||
@EnvironmentObject<PlayerModel> private var player
|
||||
|
||||
@Default(.playerControlsLayout) private var regularPlayerControlsLayout
|
||||
@Default(.fullScreenPlayerControlsLayout) private var fullScreenPlayerControlsLayout
|
||||
|
||||
var playerControlsLayout: PlayerControlsLayout {
|
||||
fullScreenLayout ? fullScreenPlayerControlsLayout : regularPlayerControlsLayout
|
||||
}
|
||||
|
||||
var fullScreenLayout: Bool {
|
||||
#if os(iOS)
|
||||
player.playingFullScreen || verticalSizeClass == .compact
|
||||
#else
|
||||
player.playingFullScreen
|
||||
#endif
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 2) {
|
||||
ProgressView()
|
||||
@@ -17,10 +39,10 @@ struct Buffering: View {
|
||||
.progressViewStyle(.circular)
|
||||
|
||||
Text(reason)
|
||||
.font(.caption)
|
||||
.font(.system(size: playerControlsLayout.timeFontSize))
|
||||
if let state = state {
|
||||
Text(state)
|
||||
.font(.caption2.monospacedDigit())
|
||||
.font(.system(size: playerControlsLayout.bufferingStateFontSize).monospacedDigit())
|
||||
}
|
||||
}
|
||||
.padding(8)
|
||||
|
186
Shared/Player/Controls/OSD/Seek.swift
Normal file
186
Shared/Player/Controls/OSD/Seek.swift
Normal file
@@ -0,0 +1,186 @@
|
||||
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) {
|
||||
VStack(spacing: 2) {
|
||||
ProgressBar(value: progress)
|
||||
.frame(maxHeight: 5)
|
||||
|
||||
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))
|
||||
}
|
||||
if let segment = projectedSegment {
|
||||
Text(SponsorBlockAPI.categoryDescription(segment.category) ?? "Sponsor")
|
||||
.font(.system(size: playerControlsLayout.segmentFontSize))
|
||||
.foregroundColor(Color("AppRedColor"))
|
||||
}
|
||||
} else {
|
||||
if !model.restoreSeekTime.isNil {
|
||||
Divider()
|
||||
Label(model.restoreSeekPlaybackTime, systemImage: "arrow.counterclockwise")
|
||||
.foregroundColor(.secondary)
|
||||
.font(.system(size: playerControlsLayout.chapterFontSize).monospacedDigit())
|
||||
.frame(height: playerControlsLayout.chapterFontSize + 5)
|
||||
}
|
||||
|
||||
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)
|
||||
.padding(10)
|
||||
#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())
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user