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