mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 13:33:42 +00:00
Improve seek gesture
This commit is contained in:
parent
d5f8ad4eec
commit
e444dc3c79
@ -16,6 +16,7 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
var controls: PlayerControlsModel!
|
||||
var playerTime: PlayerTimeModel!
|
||||
var networkState: NetworkStateModel!
|
||||
var seek: SeekModel!
|
||||
|
||||
var stream: Stream?
|
||||
var video: Video?
|
||||
@ -145,7 +146,7 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
avPlayer.replaceCurrentItem(with: nil)
|
||||
}
|
||||
|
||||
func seek(to time: CMTime, seekType _: PlayerTimeModel.SeekType, completionHandler: ((Bool) -> Void)?) {
|
||||
func seek(to time: CMTime, seekType _: SeekType, completionHandler: ((Bool) -> Void)?) {
|
||||
guard !model.live else { return }
|
||||
|
||||
avPlayer.seek(
|
||||
|
@ -17,6 +17,7 @@ final class MPVBackend: PlayerBackend {
|
||||
var controls: PlayerControlsModel!
|
||||
var playerTime: PlayerTimeModel!
|
||||
var networkState: NetworkStateModel!
|
||||
var seek: SeekModel!
|
||||
|
||||
var stream: Stream?
|
||||
var video: Video?
|
||||
@ -299,7 +300,7 @@ final class MPVBackend: PlayerBackend {
|
||||
client?.stop()
|
||||
}
|
||||
|
||||
func seek(to time: CMTime, seekType _: PlayerTimeModel.SeekType, completionHandler: ((Bool) -> Void)?) {
|
||||
func seek(to time: CMTime, seekType _: SeekType, completionHandler: ((Bool) -> Void)?) {
|
||||
client?.seek(to: time) { [weak self] _ in
|
||||
self?.getTimeUpdates()
|
||||
self?.updateControls()
|
||||
|
@ -9,6 +9,7 @@ protocol PlayerBackend {
|
||||
var model: PlayerModel! { get set }
|
||||
var controls: PlayerControlsModel! { get set }
|
||||
var playerTime: PlayerTimeModel! { get set }
|
||||
var seek: SeekModel! { get set }
|
||||
var networkState: NetworkStateModel! { get set }
|
||||
|
||||
var stream: Stream? { get set }
|
||||
@ -41,8 +42,8 @@ protocol PlayerBackend {
|
||||
|
||||
func stop()
|
||||
|
||||
func seek(to time: CMTime, seekType: PlayerTimeModel.SeekType, completionHandler: ((Bool) -> Void)?)
|
||||
func seek(to seconds: Double, seekType: PlayerTimeModel.SeekType, completionHandler: ((Bool) -> Void)?)
|
||||
func seek(to time: CMTime, seekType: SeekType, completionHandler: ((Bool) -> Void)?)
|
||||
func seek(to seconds: Double, seekType: SeekType, completionHandler: ((Bool) -> Void)?)
|
||||
|
||||
func setRate(_ rate: Float)
|
||||
|
||||
@ -67,21 +68,21 @@ protocol PlayerBackend {
|
||||
}
|
||||
|
||||
extension PlayerBackend {
|
||||
func seek(to time: CMTime, seekType: PlayerTimeModel.SeekType, completionHandler: ((Bool) -> Void)? = nil) {
|
||||
playerTime.registerSeek(at: time, type: seekType, restore: currentTime)
|
||||
func seek(to time: CMTime, seekType: SeekType, completionHandler: ((Bool) -> Void)? = nil) {
|
||||
seek.registerSeek(at: time, type: seekType, restore: currentTime)
|
||||
seek(to: time, seekType: seekType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func seek(to seconds: Double, seekType: PlayerTimeModel.SeekType, completionHandler: ((Bool) -> Void)? = nil) {
|
||||
func seek(to seconds: Double, seekType: SeekType, completionHandler: ((Bool) -> Void)? = nil) {
|
||||
let seconds = CMTime.secondsInDefaultTimescale(seconds)
|
||||
playerTime.registerSeek(at: seconds, type: seekType, restore: currentTime)
|
||||
seek.registerSeek(at: seconds, type: seekType, restore: currentTime)
|
||||
seek(to: seconds, seekType: seekType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func seek(relative time: CMTime, seekType: PlayerTimeModel.SeekType, completionHandler: ((Bool) -> Void)? = nil) {
|
||||
func seek(relative time: CMTime, seekType: SeekType, completionHandler: ((Bool) -> Void)? = nil) {
|
||||
if let currentTime = currentTime, let duration = playerItemDuration {
|
||||
let seekTime = min(max(0, currentTime.seconds + time.seconds), duration.seconds)
|
||||
playerTime.registerSeek(at: .secondsInDefaultTimescale(seekTime), type: seekType, restore: currentTime)
|
||||
seek.registerSeek(at: .secondsInDefaultTimescale(seekTime), type: seekType, restore: currentTime)
|
||||
seek(to: seekTime, seekType: seekType, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
|
@ -91,14 +91,14 @@ final class PlayerModel: ObservableObject {
|
||||
@Published var stream: Stream?
|
||||
@Published var currentRate: Float = 1.0 { didSet { backend.setRate(currentRate) } }
|
||||
|
||||
@Published var qualityProfileSelection: QualityProfile? { didSet { handleQualityProfileChange() }}
|
||||
@Published var qualityProfileSelection: QualityProfile? { didSet { handleQualityProfileChange() } }
|
||||
|
||||
@Published var availableStreams = [Stream]() { didSet { handleAvailableStreamsChange() } }
|
||||
@Published var streamSelection: Stream? { didSet { rebuildTVMenu() } }
|
||||
|
||||
@Published var queue = [PlayerQueueItem]() { didSet { handleQueueChange() } }
|
||||
@Published var currentItem: PlayerQueueItem! { didSet { handleCurrentItemChange() } }
|
||||
@Published var videoBeingOpened: Video? { didSet { playerTime.reset() } }
|
||||
@Published var videoBeingOpened: Video? { didSet { seek.reset() } }
|
||||
@Published var historyVideos = [Video]()
|
||||
|
||||
@Published var preservedTime: CMTime?
|
||||
@ -148,6 +148,13 @@ final class PlayerModel: ObservableObject {
|
||||
backend.networkState.player = self
|
||||
}
|
||||
}}
|
||||
var seek: SeekModel { didSet {
|
||||
backends.forEach { backend in
|
||||
var backend = backend
|
||||
backend.seek = seek
|
||||
backend.seek.player = self
|
||||
}
|
||||
}}
|
||||
var navigation: NavigationModel
|
||||
|
||||
var context: NSManagedObjectContext = PersistenceController.shared.container.viewContext
|
||||
@ -193,7 +200,8 @@ final class PlayerModel: ObservableObject {
|
||||
controls: PlayerControlsModel = PlayerControlsModel(),
|
||||
navigation: NavigationModel = NavigationModel(),
|
||||
playerTime: PlayerTimeModel = PlayerTimeModel(),
|
||||
networkState: NetworkStateModel = NetworkStateModel()
|
||||
networkState: NetworkStateModel = NetworkStateModel(),
|
||||
seek: SeekModel = SeekModel()
|
||||
) {
|
||||
self.accounts = accounts
|
||||
self.comments = comments
|
||||
@ -201,6 +209,7 @@ final class PlayerModel: ObservableObject {
|
||||
self.navigation = navigation
|
||||
self.playerTime = playerTime
|
||||
self.networkState = networkState
|
||||
self.seek = seek
|
||||
|
||||
self.avPlayerBackend = AVPlayerBackend(
|
||||
model: self,
|
||||
@ -244,7 +253,7 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
#endif
|
||||
|
||||
if !presentingPlayer { presentingPlayer = true }
|
||||
presentingPlayer = true
|
||||
|
||||
#if os(macOS)
|
||||
Windows.player.open()
|
||||
|
@ -3,32 +3,11 @@ import Foundation
|
||||
import SwiftUI
|
||||
|
||||
final class PlayerTimeModel: ObservableObject {
|
||||
enum SeekType: Equatable {
|
||||
case segmentSkip(String)
|
||||
case segmentRestore
|
||||
case userInteracted
|
||||
case loopRestart
|
||||
case backendSync
|
||||
|
||||
var presentable: Bool {
|
||||
self != .backendSync
|
||||
}
|
||||
}
|
||||
|
||||
static let timePlaceholder = "--:--"
|
||||
|
||||
@Published var currentTime = CMTime.zero
|
||||
@Published var duration = CMTime.zero
|
||||
|
||||
@Published var lastSeekTime: CMTime?
|
||||
@Published var lastSeekType: SeekType?
|
||||
@Published var restoreSeekTime: CMTime?
|
||||
|
||||
@Published var gestureSeek = 0.0
|
||||
@Published var gestureStart = 0.0
|
||||
|
||||
@Published var seekOSDDismissed = true
|
||||
|
||||
var player: PlayerModel!
|
||||
|
||||
var forceHours: Bool {
|
||||
@ -55,70 +34,4 @@ final class PlayerTimeModel: ObservableObject {
|
||||
guard let withoutSegmentsDuration = player?.playerItemDurationWithoutSponsorSegments?.seconds else { return Self.timePlaceholder }
|
||||
return withoutSegmentsDuration.formattedAsPlaybackTime(forceHours: forceHours) ?? Self.timePlaceholder
|
||||
}
|
||||
|
||||
var lastSeekPlaybackTime: String {
|
||||
guard let time = lastSeekTime else { return 0.formattedAsPlaybackTime(allowZero: true, forceHours: forceHours) ?? Self.timePlaceholder }
|
||||
return time.seconds.formattedAsPlaybackTime(allowZero: true, forceHours: forceHours) ?? Self.timePlaceholder
|
||||
}
|
||||
|
||||
var restoreSeekPlaybackTime: String {
|
||||
guard let time = restoreSeekTime else { return Self.timePlaceholder }
|
||||
return time.seconds.formattedAsPlaybackTime(allowZero: true, forceHours: forceHours) ?? Self.timePlaceholder
|
||||
}
|
||||
|
||||
var gestureSeekDestinationTime: Double {
|
||||
min(duration.seconds, max(0, gestureStart + gestureSeek))
|
||||
}
|
||||
|
||||
var gestureSeekDestinationPlaybackTime: String {
|
||||
guard gestureSeek != 0 else { return Self.timePlaceholder }
|
||||
return gestureSeekDestinationTime.formattedAsPlaybackTime(allowZero: true, forceHours: forceHours) ?? Self.timePlaceholder
|
||||
}
|
||||
|
||||
func onSeekGestureStart(completionHandler: (() -> Void)? = nil) {
|
||||
player.backend.getTimeUpdates()
|
||||
player.backend.updateControls {
|
||||
self.gestureStart = self.currentTime.seconds
|
||||
completionHandler?()
|
||||
}
|
||||
}
|
||||
|
||||
func onSeekGestureEnd() {
|
||||
player.backend.updateControls()
|
||||
player.backend.seek(to: gestureSeekDestinationTime, seekType: .userInteracted)
|
||||
}
|
||||
|
||||
func registerSeek(at time: CMTime, type: SeekType, restore restoreTime: CMTime? = nil) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
withAnimation {
|
||||
self?.lastSeekTime = time
|
||||
self?.lastSeekType = type
|
||||
self?.restoreSeekTime = restoreTime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func restoreTime() {
|
||||
guard let time = restoreSeekTime else { return }
|
||||
switch lastSeekType {
|
||||
case .segmentSkip:
|
||||
player.restoreLastSkippedSegment()
|
||||
default:
|
||||
player?.backend.seek(to: time, seekType: .userInteracted)
|
||||
}
|
||||
}
|
||||
|
||||
func resetSeek() {
|
||||
withAnimation {
|
||||
lastSeekTime = nil
|
||||
lastSeekType = nil
|
||||
}
|
||||
}
|
||||
|
||||
func reset() {
|
||||
currentTime = .zero
|
||||
duration = .zero
|
||||
resetSeek()
|
||||
gestureSeek = 0
|
||||
}
|
||||
}
|
||||
|
160
Model/SeekModel.swift
Normal file
160
Model/SeekModel.swift
Normal file
@ -0,0 +1,160 @@
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
final class SeekModel: ObservableObject {
|
||||
@Published var currentTime = CMTime.zero
|
||||
@Published var duration = CMTime.zero
|
||||
|
||||
@Published var lastSeekTime: CMTime? { didSet { onSeek() } }
|
||||
@Published var lastSeekType: SeekType?
|
||||
@Published var restoreSeekTime: CMTime?
|
||||
|
||||
@Published var gestureSeek: Double?
|
||||
@Published var gestureStart: Double?
|
||||
|
||||
@Published var presentingOSD = false
|
||||
|
||||
var player: PlayerModel!
|
||||
|
||||
var dismissTimer: Timer?
|
||||
|
||||
var isSeeking: Bool {
|
||||
gestureSeek != nil
|
||||
}
|
||||
|
||||
var progress: Double {
|
||||
let seconds = duration.seconds
|
||||
guard seconds.isFinite, seconds > 0 else { return 0 }
|
||||
|
||||
if isSeeking {
|
||||
return gestureSeekDestinationTime / seconds
|
||||
}
|
||||
|
||||
guard let seekTime = lastSeekTime else {
|
||||
return currentTime.seconds / seconds
|
||||
}
|
||||
|
||||
return seekTime.seconds / seconds
|
||||
}
|
||||
|
||||
var lastSeekPlaybackTime: String {
|
||||
guard let time = lastSeekTime else { return 0.formattedAsPlaybackTime(allowZero: true, forceHours: forceHours) ?? PlayerTimeModel.timePlaceholder }
|
||||
return time.seconds.formattedAsPlaybackTime(allowZero: true, forceHours: forceHours) ?? PlayerTimeModel.timePlaceholder
|
||||
}
|
||||
|
||||
var restoreSeekPlaybackTime: String {
|
||||
guard let time = restoreSeekTime else { return PlayerTimeModel.timePlaceholder }
|
||||
return time.seconds.formattedAsPlaybackTime(allowZero: true, forceHours: forceHours) ?? PlayerTimeModel.timePlaceholder
|
||||
}
|
||||
|
||||
var gestureSeekDestinationTime: Double {
|
||||
guard let gestureSeek = gestureSeek, let gestureStart = gestureStart else { return -1 }
|
||||
return min(duration.seconds, max(0, gestureStart + gestureSeek))
|
||||
}
|
||||
|
||||
var gestureSeekDestinationPlaybackTime: String {
|
||||
guard gestureSeek != 0 else { return PlayerTimeModel.timePlaceholder }
|
||||
return gestureSeekDestinationTime.formattedAsPlaybackTime(allowZero: true, forceHours: forceHours) ?? PlayerTimeModel.timePlaceholder
|
||||
}
|
||||
|
||||
var durationPlaybackTime: String {
|
||||
if player?.currentItem.isNil ?? true {
|
||||
return PlayerTimeModel.timePlaceholder
|
||||
}
|
||||
|
||||
return duration.seconds.formattedAsPlaybackTime() ?? PlayerTimeModel.timePlaceholder
|
||||
}
|
||||
|
||||
func showOSD() {
|
||||
guard !presentingOSD else { return }
|
||||
|
||||
withAnimation(.easeIn(duration: 0.1)) { self.presentingOSD = true }
|
||||
}
|
||||
|
||||
func hideOSD() {
|
||||
guard presentingOSD else { return }
|
||||
|
||||
withAnimation(.easeIn(duration: 0.1)) { self.presentingOSD = false }
|
||||
}
|
||||
|
||||
func hideOSDWithDelay() {
|
||||
dismissTimer?.invalidate()
|
||||
dismissTimer = Delay.by(3) { self.hideOSD() }
|
||||
}
|
||||
|
||||
func updateCurrentTime(completionHandler: (() -> Void?)? = nil) {
|
||||
player.backend.getTimeUpdates()
|
||||
DispatchQueue.main.async {
|
||||
self.currentTime = self.player.backend.currentTime ?? .zero
|
||||
self.duration = self.player.backend.playerItemDuration ?? .zero
|
||||
completionHandler?()
|
||||
}
|
||||
}
|
||||
|
||||
func onSeekGestureStart() {
|
||||
updateCurrentTime {
|
||||
self.gestureStart = self.currentTime.seconds
|
||||
self.dismissTimer?.invalidate()
|
||||
self.showOSD()
|
||||
}
|
||||
|
||||
//
|
||||
// player.backend.updateControls {
|
||||
// self.gestureStart = self.currentTime.seconds
|
||||
// completionHandler?()
|
||||
// }
|
||||
}
|
||||
|
||||
func onSeekGestureEnd() {
|
||||
dismissTimer?.invalidate()
|
||||
dismissTimer = Delay.by(3) { self.hideOSD() }
|
||||
player.backend.seek(to: gestureSeekDestinationTime, seekType: .userInteracted)
|
||||
}
|
||||
|
||||
func onSeek() {
|
||||
guard !lastSeekTime.isNil else { return }
|
||||
gestureSeek = nil
|
||||
gestureStart = nil
|
||||
showOSD()
|
||||
hideOSDWithDelay()
|
||||
}
|
||||
|
||||
func registerSeek(at time: CMTime, type: SeekType, restore restoreTime: CMTime? = nil) {
|
||||
updateCurrentTime {
|
||||
withAnimation {
|
||||
self.lastSeekTime = time
|
||||
self.lastSeekType = type
|
||||
self.restoreSeekTime = restoreTime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func restoreTime() {
|
||||
guard let time = restoreSeekTime else { return }
|
||||
switch lastSeekType {
|
||||
case .segmentSkip:
|
||||
player.restoreLastSkippedSegment()
|
||||
default:
|
||||
player.backend.seek(to: time, seekType: .userInteracted)
|
||||
}
|
||||
}
|
||||
|
||||
func resetSeek() {
|
||||
withAnimation {
|
||||
lastSeekTime = nil
|
||||
lastSeekType = nil
|
||||
}
|
||||
}
|
||||
|
||||
func reset() {
|
||||
currentTime = .zero
|
||||
duration = .zero
|
||||
resetSeek()
|
||||
gestureSeek = nil
|
||||
}
|
||||
|
||||
var forceHours: Bool {
|
||||
duration.seconds >= 60 * 60
|
||||
}
|
||||
}
|
13
Model/SeekType.swift
Normal file
13
Model/SeekType.swift
Normal file
@ -0,0 +1,13 @@
|
||||
import Foundation
|
||||
|
||||
enum SeekType: Equatable {
|
||||
case segmentSkip(String)
|
||||
case segmentRestore
|
||||
case userInteracted
|
||||
case loopRestart
|
||||
case backendSync
|
||||
|
||||
var presentable: Bool {
|
||||
self != .backendSync
|
||||
}
|
||||
}
|
@ -7,10 +7,7 @@ struct Seek: View {
|
||||
#endif
|
||||
|
||||
@EnvironmentObject<PlayerControlsModel> private var controls
|
||||
@EnvironmentObject<PlayerTimeModel> private var model
|
||||
|
||||
@State private var dismissTimer: Timer?
|
||||
@State private var isSeeking = false
|
||||
@EnvironmentObject<SeekModel> private var model
|
||||
|
||||
private var updateThrottle = Throttle(interval: 2)
|
||||
|
||||
@ -20,12 +17,12 @@ struct Seek: View {
|
||||
var body: some View {
|
||||
Button(action: model.restoreTime) {
|
||||
VStack(spacing: playerControlsLayout.osdSpacing) {
|
||||
ProgressBar(value: progress)
|
||||
ProgressBar(value: model.progress)
|
||||
.frame(maxHeight: playerControlsLayout.osdProgressBarHeight)
|
||||
|
||||
timeline
|
||||
|
||||
if isSeeking {
|
||||
if model.isSeeking {
|
||||
Divider()
|
||||
gestureSeekTime
|
||||
.foregroundColor(.secondary)
|
||||
@ -84,38 +81,10 @@ struct Seek: View {
|
||||
.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 ?
|
||||
let text = model.isSeeking ?
|
||||
"\(model.gestureSeekDestinationPlaybackTime)/\(model.durationPlaybackTime)" :
|
||||
"\(model.lastSeekPlaybackTime)/\(model.durationPlaybackTime)"
|
||||
|
||||
@ -141,21 +110,10 @@ struct Seek: View {
|
||||
}
|
||||
|
||||
var visible: Bool {
|
||||
guard !(model.lastSeekTime.isNil && !isSeeking) else { return false }
|
||||
guard !(model.lastSeekTime.isNil && !model.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
|
||||
return !controls.presentingControls && !controls.presentingOverlays && model.presentingOSD
|
||||
}
|
||||
|
||||
var projectedChapter: Chapter? {
|
||||
|
@ -51,7 +51,7 @@ struct PlayerControls: View {
|
||||
#if os(tvOS)
|
||||
.offset(x: 10, y: 10)
|
||||
.focused($focusedField, equals: .seekOSD)
|
||||
.onChange(of: player.playerTime.lastSeekTime) { _ in
|
||||
.onChange(of: player.seek.lastSeekTime) { _ in
|
||||
if !model.presentingControls {
|
||||
focusedField = .seekOSD
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ProgressBar: View {
|
||||
@ -11,7 +10,7 @@ struct ProgressBar: View {
|
||||
.opacity(0.3)
|
||||
.foregroundColor(Color.secondary)
|
||||
|
||||
Rectangle().frame(width: min(CGFloat(self.value) * geometry.size.width, geometry.size.width), height: geometry.size.height)
|
||||
Rectangle().frame(width: min(Double(self.value) * geometry.size.width, geometry.size.width), height: geometry.size.height)
|
||||
.foregroundColor(Color.accentColor)
|
||||
.animation(.linear)
|
||||
}.cornerRadius(45.0)
|
||||
|
@ -72,7 +72,7 @@ struct TVControls: UIViewRepresentable {
|
||||
}
|
||||
|
||||
@objc func handleTap(sender _: UITapGestureRecognizer) {
|
||||
if !model.presentingControls, model.player.playerTime.seekOSDDismissed {
|
||||
if !model.presentingControls {
|
||||
model.show()
|
||||
}
|
||||
}
|
||||
|
@ -38,14 +38,19 @@ extension VideoPlayerView {
|
||||
|
||||
if !isVerticalDrag, abs(horizontalDrag) > 15, !isHorizontalDrag {
|
||||
isHorizontalDrag = true
|
||||
player.playerTime.resetSeek()
|
||||
player.seek.onSeekGestureStart()
|
||||
viewDragOffset = 0
|
||||
}
|
||||
|
||||
if horizontalPlayerGestureEnabled, isHorizontalDrag {
|
||||
player.playerTime.onSeekGestureStart {
|
||||
let timeSeek = (player.playerTime.duration.seconds / player.playerSize.width) * horizontalDrag * seekGestureSpeed
|
||||
player.playerTime.gestureSeek = timeSeek
|
||||
player.seek.updateCurrentTime {
|
||||
let time = player.backend.playerItemDuration?.seconds ?? 0
|
||||
if player.seek.gestureStart.isNil {
|
||||
player.seek.gestureStart = time
|
||||
}
|
||||
let timeSeek = (time / player.playerSize.width) * horizontalDrag * seekGestureSpeed
|
||||
|
||||
player.seek.gestureSeek = timeSeek
|
||||
}
|
||||
return
|
||||
}
|
||||
@ -72,7 +77,7 @@ extension VideoPlayerView {
|
||||
private func onPlayerDragGestureEnded() {
|
||||
if horizontalPlayerGestureEnabled, isHorizontalDrag {
|
||||
isHorizontalDrag = false
|
||||
player.playerTime.onSeekGestureEnd()
|
||||
player.seek.onSeekGestureEnd()
|
||||
}
|
||||
|
||||
isVerticalDrag = false
|
||||
|
@ -44,6 +44,7 @@ struct YatteeApp: App {
|
||||
@StateObject private var playlists = PlaylistsModel()
|
||||
@StateObject private var recents = RecentsModel()
|
||||
@StateObject private var search = SearchModel()
|
||||
@StateObject private var seek = SeekModel()
|
||||
@StateObject private var settings = SettingsModel()
|
||||
@StateObject private var subscriptions = SubscriptionsModel()
|
||||
@StateObject private var thumbnails = ThumbnailsModel()
|
||||
@ -65,6 +66,7 @@ struct YatteeApp: App {
|
||||
.environmentObject(playerTime)
|
||||
.environmentObject(playlists)
|
||||
.environmentObject(recents)
|
||||
.environmentObject(seek)
|
||||
.environmentObject(settings)
|
||||
.environmentObject(subscriptions)
|
||||
.environmentObject(thumbnails)
|
||||
@ -139,6 +141,7 @@ struct YatteeApp: App {
|
||||
.environmentObject(playlists)
|
||||
.environmentObject(recents)
|
||||
.environmentObject(search)
|
||||
.environmentObject(seek)
|
||||
.environmentObject(subscriptions)
|
||||
.environmentObject(thumbnails)
|
||||
.handlesExternalEvents(preferring: Set(["player", "*"]), allowing: Set(["player", "*"]))
|
||||
@ -203,6 +206,7 @@ struct YatteeApp: App {
|
||||
player.navigation = navigation
|
||||
player.networkState = networkState
|
||||
player.playerTime = playerTime
|
||||
player.seek = seek
|
||||
|
||||
if !accounts.current.isNil {
|
||||
player.restoreQueue()
|
||||
|
@ -297,6 +297,12 @@
|
||||
37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* AccountValidator.swift */; };
|
||||
37484C3226FCB8F900287258 /* AccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* AccountValidator.swift */; };
|
||||
37484C3326FCB8F900287258 /* AccountValidator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37484C3026FCB8F900287258 /* AccountValidator.swift */; };
|
||||
374AB3D728BCAF0000DF56FB /* SeekModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374AB3D628BCAF0000DF56FB /* SeekModel.swift */; };
|
||||
374AB3D828BCAF0000DF56FB /* SeekModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374AB3D628BCAF0000DF56FB /* SeekModel.swift */; };
|
||||
374AB3D928BCAF0000DF56FB /* SeekModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374AB3D628BCAF0000DF56FB /* SeekModel.swift */; };
|
||||
374AB3DB28BCAF7E00DF56FB /* SeekType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374AB3DA28BCAF7E00DF56FB /* SeekType.swift */; };
|
||||
374AB3DC28BCAF7E00DF56FB /* SeekType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374AB3DA28BCAF7E00DF56FB /* SeekType.swift */; };
|
||||
374AB3DD28BCAF7E00DF56FB /* SeekType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374AB3DA28BCAF7E00DF56FB /* SeekType.swift */; };
|
||||
374C053527242D9F009BDDBE /* SponsorBlockSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053427242D9F009BDDBE /* SponsorBlockSettings.swift */; };
|
||||
374C053627242D9F009BDDBE /* SponsorBlockSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053427242D9F009BDDBE /* SponsorBlockSettings.swift */; };
|
||||
374C053727242D9F009BDDBE /* SponsorBlockSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053427242D9F009BDDBE /* SponsorBlockSettings.swift */; };
|
||||
@ -1072,6 +1078,8 @@
|
||||
3749BF7027ADA135000480FF /* stream_cb.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = stream_cb.h; sourceTree = "<group>"; };
|
||||
3749BF7127ADA135000480FF /* qthelper.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = qthelper.hpp; sourceTree = "<group>"; };
|
||||
3749BF9227ADA142000480FF /* BridgingHeader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = "<group>"; };
|
||||
374AB3D628BCAF0000DF56FB /* SeekModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SeekModel.swift; path = Model/SeekModel.swift; sourceTree = SOURCE_ROOT; };
|
||||
374AB3DA28BCAF7E00DF56FB /* SeekType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeekType.swift; sourceTree = "<group>"; };
|
||||
374C053427242D9F009BDDBE /* SponsorBlockSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockSettings.swift; sourceTree = "<group>"; };
|
||||
374C053A2724614F009BDDBE /* PlayerTVMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerTVMenu.swift; sourceTree = "<group>"; };
|
||||
374C053E272472C0009BDDBE /* PlayerSponsorBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerSponsorBlock.swift; sourceTree = "<group>"; };
|
||||
@ -2112,6 +2120,8 @@
|
||||
375EC95C289EEEE000751258 /* QualityProfile.swift */,
|
||||
375EC969289F232600751258 /* QualityProfilesModel.swift */,
|
||||
37C194C626F6A9C8005D3B96 /* RecentsModel.swift */,
|
||||
374AB3D628BCAF0000DF56FB /* SeekModel.swift */,
|
||||
374AB3DA28BCAF7E00DF56FB /* SeekType.swift */,
|
||||
37EAD86E267B9ED100D9E01B /* Segment.swift */,
|
||||
37F0F4E9286F397E00C06C2E /* SettingsModel.swift */,
|
||||
37CEE4BC2677B670005A1EFE /* SingleAssetStream.swift */,
|
||||
@ -2817,6 +2827,7 @@
|
||||
37D2E0D028B67DBC00F64D52 /* AnimationCompletionObserverModifier.swift in Sources */,
|
||||
3727B74A27872A920021C15E /* VisualEffectBlur-iOS.swift in Sources */,
|
||||
37977583268922F600DD52A8 /* InvidiousAPI.swift in Sources */,
|
||||
374AB3D728BCAF0000DF56FB /* SeekModel.swift in Sources */,
|
||||
37130A5F277657300033018A /* PersistenceController.swift in Sources */,
|
||||
37FD43E32704847C0073EE42 /* View+Fixtures.swift in Sources */,
|
||||
3776ADD6287381240078EBC4 /* Captions.swift in Sources */,
|
||||
@ -2828,6 +2839,7 @@
|
||||
37F64FE426FE70A60081B69E /* RedrawOnModifier.swift in Sources */,
|
||||
37EBD8C427AF0DA800F1C24B /* PlayerBackend.swift in Sources */,
|
||||
376A33E02720CAD6000C1D6B /* VideosApp.swift in Sources */,
|
||||
374AB3DB28BCAF7E00DF56FB /* SeekType.swift in Sources */,
|
||||
37192D5728B179D60012EEDD /* ChaptersView.swift in Sources */,
|
||||
37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */,
|
||||
37C0698227260B2100F7F6CB /* ThumbnailsModel.swift in Sources */,
|
||||
@ -3055,6 +3067,7 @@
|
||||
3756C2A72861131100E4B059 /* NetworkState.swift in Sources */,
|
||||
3729037F2739E47400EA99F6 /* MenuCommands.swift in Sources */,
|
||||
37C0698327260B2100F7F6CB /* ThumbnailsModel.swift in Sources */,
|
||||
374AB3DC28BCAF7E00DF56FB /* SeekType.swift in Sources */,
|
||||
376B2E0826F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
||||
37CC3F4D270CFE1700608308 /* PlayerQueueView.swift in Sources */,
|
||||
37B81B0026D2CA3700675966 /* VideoDetails.swift in Sources */,
|
||||
@ -3151,6 +3164,7 @@
|
||||
37A5DBC9285E371400CA4DD1 /* ControlBackgroundModifier.swift in Sources */,
|
||||
3797758C2689345500DD52A8 /* Store.swift in Sources */,
|
||||
371B7E622759706A00D21217 /* CommentsView.swift in Sources */,
|
||||
374AB3D828BCAF0000DF56FB /* SeekModel.swift in Sources */,
|
||||
375EC95E289EEEE000751258 /* QualityProfile.swift in Sources */,
|
||||
37141674267A8E10006CA35D /* Country.swift in Sources */,
|
||||
3703100027B04DCC00ECDDAA /* PlayerControls.swift in Sources */,
|
||||
@ -3408,6 +3422,7 @@
|
||||
37484C2B26FC83FF00287258 /* AccountForm.swift in Sources */,
|
||||
37FB2860272225E800A57617 /* ContentItemView.swift in Sources */,
|
||||
37F7D82E289EB05F00E2B3D0 /* SettingsPickerModifier.swift in Sources */,
|
||||
374AB3DD28BCAF7E00DF56FB /* SeekType.swift in Sources */,
|
||||
374C053727242D9F009BDDBE /* SponsorBlockSettings.swift in Sources */,
|
||||
377ABC42286E4AD5009C986F /* InstancesManifest.swift in Sources */,
|
||||
37C069802725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */,
|
||||
@ -3449,6 +3464,7 @@
|
||||
3705B184267B4E4900704544 /* TrendingCategory.swift in Sources */,
|
||||
37E084AD2753D95F00039B7D /* AccountsNavigationLink.swift in Sources */,
|
||||
371B7E6C2759791900D21217 /* CommentsModel.swift in Sources */,
|
||||
374AB3D928BCAF0000DF56FB /* SeekModel.swift in Sources */,
|
||||
375E45F927B1AC4700BA7902 /* PlayerControlsModel.swift in Sources */,
|
||||
3782B95627557E4E00990149 /* SearchView.swift in Sources */,
|
||||
3761ABFF26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
|
||||
|
Loading…
Reference in New Issue
Block a user