From e444dc3c790d69f584890ffb243a97be4bc0be36 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Mon, 29 Aug 2022 13:55:23 +0200 Subject: [PATCH] Improve seek gesture --- Model/Player/Backends/AVPlayerBackend.swift | 3 +- Model/Player/Backends/MPVBackend.swift | 3 +- Model/Player/Backends/PlayerBackend.swift | 17 ++- Model/Player/PlayerModel.swift | 17 ++- Model/Player/PlayerTimeModel.swift | 87 ----------- Model/SeekModel.swift | 160 ++++++++++++++++++++ Model/SeekType.swift | 13 ++ Shared/Player/Controls/OSD/Seek.swift | 54 +------ Shared/Player/Controls/PlayerControls.swift | 2 +- Shared/Player/Controls/ProgressBar.swift | 3 +- Shared/Player/Controls/TVControls.swift | 2 +- Shared/Player/PlayerDragGesture.swift | 15 +- Shared/YatteeApp.swift | 4 + Yattee.xcodeproj/project.pbxproj | 16 ++ 14 files changed, 238 insertions(+), 158 deletions(-) create mode 100644 Model/SeekModel.swift create mode 100644 Model/SeekType.swift diff --git a/Model/Player/Backends/AVPlayerBackend.swift b/Model/Player/Backends/AVPlayerBackend.swift index 70ea2914..4e45818b 100644 --- a/Model/Player/Backends/AVPlayerBackend.swift +++ b/Model/Player/Backends/AVPlayerBackend.swift @@ -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( diff --git a/Model/Player/Backends/MPVBackend.swift b/Model/Player/Backends/MPVBackend.swift index 16d073ab..c89daa1c 100644 --- a/Model/Player/Backends/MPVBackend.swift +++ b/Model/Player/Backends/MPVBackend.swift @@ -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() diff --git a/Model/Player/Backends/PlayerBackend.swift b/Model/Player/Backends/PlayerBackend.swift index 5ab56894..e15d3838 100644 --- a/Model/Player/Backends/PlayerBackend.swift +++ b/Model/Player/Backends/PlayerBackend.swift @@ -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) } } diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index 9c7ba576..ea6d3dda 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -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() diff --git a/Model/Player/PlayerTimeModel.swift b/Model/Player/PlayerTimeModel.swift index d85b203b..6797fb85 100644 --- a/Model/Player/PlayerTimeModel.swift +++ b/Model/Player/PlayerTimeModel.swift @@ -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 - } } diff --git a/Model/SeekModel.swift b/Model/SeekModel.swift new file mode 100644 index 00000000..641a5f9f --- /dev/null +++ b/Model/SeekModel.swift @@ -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 + } +} diff --git a/Model/SeekType.swift b/Model/SeekType.swift new file mode 100644 index 00000000..62c021fd --- /dev/null +++ b/Model/SeekType.swift @@ -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 + } +} diff --git a/Shared/Player/Controls/OSD/Seek.swift b/Shared/Player/Controls/OSD/Seek.swift index e1307f8b..875f440c 100644 --- a/Shared/Player/Controls/OSD/Seek.swift +++ b/Shared/Player/Controls/OSD/Seek.swift @@ -7,10 +7,7 @@ struct Seek: View { #endif @EnvironmentObject private var controls - @EnvironmentObject private var model - - @State private var dismissTimer: Timer? - @State private var isSeeking = false + @EnvironmentObject 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? { diff --git a/Shared/Player/Controls/PlayerControls.swift b/Shared/Player/Controls/PlayerControls.swift index 08de87cb..1e216829 100644 --- a/Shared/Player/Controls/PlayerControls.swift +++ b/Shared/Player/Controls/PlayerControls.swift @@ -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 } diff --git a/Shared/Player/Controls/ProgressBar.swift b/Shared/Player/Controls/ProgressBar.swift index 4cd243ce..7d2f2f7c 100644 --- a/Shared/Player/Controls/ProgressBar.swift +++ b/Shared/Player/Controls/ProgressBar.swift @@ -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) diff --git a/Shared/Player/Controls/TVControls.swift b/Shared/Player/Controls/TVControls.swift index d670f0a3..da122fa8 100644 --- a/Shared/Player/Controls/TVControls.swift +++ b/Shared/Player/Controls/TVControls.swift @@ -72,7 +72,7 @@ struct TVControls: UIViewRepresentable { } @objc func handleTap(sender _: UITapGestureRecognizer) { - if !model.presentingControls, model.player.playerTime.seekOSDDismissed { + if !model.presentingControls { model.show() } } diff --git a/Shared/Player/PlayerDragGesture.swift b/Shared/Player/PlayerDragGesture.swift index 2358e385..16a013f6 100644 --- a/Shared/Player/PlayerDragGesture.swift +++ b/Shared/Player/PlayerDragGesture.swift @@ -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 diff --git a/Shared/YatteeApp.swift b/Shared/YatteeApp.swift index 5301e983..782cad34 100644 --- a/Shared/YatteeApp.swift +++ b/Shared/YatteeApp.swift @@ -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() diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index f8e8c03b..e7c797aa 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -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 = ""; }; 3749BF7127ADA135000480FF /* qthelper.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = qthelper.hpp; sourceTree = ""; }; 3749BF9227ADA142000480FF /* BridgingHeader.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = ""; }; + 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 = ""; }; 374C053427242D9F009BDDBE /* SponsorBlockSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SponsorBlockSettings.swift; sourceTree = ""; }; 374C053A2724614F009BDDBE /* PlayerTVMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerTVMenu.swift; sourceTree = ""; }; 374C053E272472C0009BDDBE /* PlayerSponsorBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerSponsorBlock.swift; sourceTree = ""; }; @@ -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 */,