Improve seek gesture

This commit is contained in:
Arkadiusz Fal 2022-08-29 13:55:23 +02:00
parent d5f8ad4eec
commit e444dc3c79
14 changed files with 238 additions and 158 deletions

View File

@ -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(

View File

@ -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()

View File

@ -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)
}
}

View File

@ -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()

View File

@ -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
View 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
View 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
}
}

View File

@ -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? {

View File

@ -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
}

View File

@ -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)

View File

@ -72,7 +72,7 @@ struct TVControls: UIViewRepresentable {
}
@objc func handleTap(sender _: UITapGestureRecognizer) {
if !model.presentingControls, model.player.playerTime.seekOSDDismissed {
if !model.presentingControls {
model.show()
}
}

View File

@ -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

View File

@ -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()

View File

@ -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 */,