mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 21:43:41 +00:00
Persistence for queue, history and last played
This commit is contained in:
parent
68b5abd122
commit
19bb4955a2
10
Extensions/CMTime+DefaultTimescale.swift
Normal file
10
Extensions/CMTime+DefaultTimescale.swift
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import CoreMedia
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension CMTime {
|
||||||
|
static let defaultTimescale: CMTimeScale = 1_000_000
|
||||||
|
|
||||||
|
static func secondsInDefaultTimescale(_ seconds: TimeInterval) -> CMTime {
|
||||||
|
CMTime(seconds: seconds, preferredTimescale: CMTime.defaultTimescale)
|
||||||
|
}
|
||||||
|
}
|
@ -23,4 +23,26 @@ protocol VideosAPI {
|
|||||||
func playlistVideos(_ id: String) -> Resource?
|
func playlistVideos(_ id: String) -> Resource?
|
||||||
|
|
||||||
func channelPlaylist(_ id: String) -> Resource?
|
func channelPlaylist(_ id: String) -> Resource?
|
||||||
|
|
||||||
|
func loadDetails(_ item: PlayerQueueItem, completionHandler: @escaping (PlayerQueueItem) -> Void)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension VideosAPI {
|
||||||
|
func loadDetails(_ item: PlayerQueueItem, completionHandler: @escaping (PlayerQueueItem) -> Void = { _ in }) {
|
||||||
|
guard (item.video?.streams ?? []).isEmpty else {
|
||||||
|
completionHandler(item)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
video(item.videoID).load().onSuccess { response in
|
||||||
|
guard let video: Video = response.typedContent() else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var newItem = item
|
||||||
|
newItem.video = video
|
||||||
|
|
||||||
|
completionHandler(newItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -26,10 +26,10 @@ final class PlayerModel: ObservableObject {
|
|||||||
@Published var availableStreams = [Stream]() { didSet { rebuildTVMenu() } }
|
@Published var availableStreams = [Stream]() { didSet { rebuildTVMenu() } }
|
||||||
@Published var streamSelection: Stream? { didSet { rebuildTVMenu() } }
|
@Published var streamSelection: Stream? { didSet { rebuildTVMenu() } }
|
||||||
|
|
||||||
@Published var queue = [PlayerQueueItem]()
|
@Published var queue = [PlayerQueueItem]() { didSet { Defaults[.queue] = queue } }
|
||||||
@Published var currentItem: PlayerQueueItem!
|
@Published var currentItem: PlayerQueueItem! { didSet { Defaults[.lastPlayed] = currentItem } }
|
||||||
|
@Published var history = [PlayerQueueItem]() { didSet { Defaults[.history] = history } }
|
||||||
|
|
||||||
@Published var history = [PlayerQueueItem]()
|
|
||||||
@Published var savedTime: CMTime?
|
@Published var savedTime: CMTime?
|
||||||
|
|
||||||
@Published var playerNavigationLinkActive = false
|
@Published var playerNavigationLinkActive = false
|
||||||
@ -43,23 +43,54 @@ final class PlayerModel: ObservableObject {
|
|||||||
var instances: InstancesModel
|
var instances: InstancesModel
|
||||||
|
|
||||||
var composition = AVMutableComposition()
|
var composition = AVMutableComposition()
|
||||||
var timeObserver: Any?
|
|
||||||
private var shouldResumePlaying = true
|
private var frequentTimeObserver: Any?
|
||||||
|
private var infrequentTimeObserver: Any?
|
||||||
|
private var playerTimeControlStatusObserver: Any?
|
||||||
|
|
||||||
private var statusObservation: NSKeyValueObservation?
|
private var statusObservation: NSKeyValueObservation?
|
||||||
|
|
||||||
#if os(macOS)
|
var autoPlayItems = false
|
||||||
var playerTimeControlStatusObserver: Any?
|
|
||||||
#endif
|
|
||||||
|
|
||||||
init(accounts: AccountsModel? = nil, instances: InstancesModel? = nil) {
|
init(accounts: AccountsModel? = nil, instances: InstancesModel? = nil) {
|
||||||
self.accounts = accounts ?? AccountsModel()
|
self.accounts = accounts ?? AccountsModel()
|
||||||
self.instances = instances ?? InstancesModel()
|
self.instances = instances ?? InstancesModel()
|
||||||
addItemDidPlayToEndTimeObserver()
|
addItemDidPlayToEndTimeObserver()
|
||||||
addTimeObserver()
|
addFrequentTimeObserver()
|
||||||
|
addInfrequentTimeObserver()
|
||||||
|
addPlayerTimeControlStatusObserver()
|
||||||
|
}
|
||||||
|
|
||||||
#if os(macOS)
|
func loadHistoryDetails() {
|
||||||
addPlayerTimeControlStatusObserver()
|
guard !accounts.current.isNil else {
|
||||||
#endif
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
queue = Defaults[.queue]
|
||||||
|
queue.forEach { item in
|
||||||
|
accounts.api.loadDetails(item) { newItem in
|
||||||
|
if let index = self.queue.firstIndex(where: { $0.id == item.id }) {
|
||||||
|
self.queue[index] = newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
history = Defaults[.history]
|
||||||
|
history.forEach { item in
|
||||||
|
accounts.api.loadDetails(item) { newItem in
|
||||||
|
if let index = self.history.firstIndex(where: { $0.id == item.id }) {
|
||||||
|
self.history[index] = newItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let item = Defaults[.lastPlayed] {
|
||||||
|
accounts.api.loadDetails(item) { [weak self] newItem in
|
||||||
|
self?.playNow(newItem.video, at: newItem.playbackTime?.seconds)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
autoPlayItems = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func presentPlayer() {
|
func presentPlayer() {
|
||||||
@ -75,7 +106,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var live: Bool {
|
var live: Bool {
|
||||||
currentItem?.video.live ?? false
|
currentItem?.video?.live ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
var playerItemDuration: CMTime? {
|
var playerItemDuration: CMTime? {
|
||||||
@ -91,7 +122,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func play() {
|
func play() {
|
||||||
guard !isPlaying else {
|
guard player.timeControlStatus != .playing else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,34 +130,20 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func pause() {
|
func pause() {
|
||||||
guard isPlaying else {
|
guard player.timeControlStatus != .paused else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
player.pause()
|
player.pause()
|
||||||
}
|
}
|
||||||
|
|
||||||
func playVideo(_ video: Video, time: CMTime? = nil) {
|
|
||||||
savedTime = time
|
|
||||||
shouldResumePlaying = true
|
|
||||||
|
|
||||||
loadAvailableStreams(video) { streams in
|
|
||||||
guard let stream = streams.first else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
self.streamSelection = stream
|
|
||||||
self.playStream(stream, of: video, preservingTime: !time.isNil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func upgradeToStream(_ stream: Stream) {
|
func upgradeToStream(_ stream: Stream) {
|
||||||
if !self.stream.isNil, self.stream != stream {
|
if !self.stream.isNil, self.stream != stream {
|
||||||
playStream(stream, of: currentVideo!, preservingTime: true)
|
playStream(stream, of: currentVideo!, preservingTime: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func playStream(
|
func playStream(
|
||||||
_ stream: Stream,
|
_ stream: Stream,
|
||||||
of video: Video,
|
of video: Video,
|
||||||
preservingTime: Bool = false
|
preservingTime: Bool = false
|
||||||
@ -164,11 +181,33 @@ final class PlayerModel: ObservableObject {
|
|||||||
|
|
||||||
attachMetadata(to: playerItem!, video: video, for: stream)
|
attachMetadata(to: playerItem!, video: video, for: stream)
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
self.stream = stream
|
self.stream = stream
|
||||||
self.composition = AVMutableComposition()
|
self.composition = AVMutableComposition()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let startPlaying = {
|
||||||
|
#if !os(macOS)
|
||||||
|
try? AVAudioSession.sharedInstance().setActive(true)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
||||||
|
guard let self = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard self.autoPlayItems else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let replaceItemAndSeek = {
|
let replaceItemAndSeek = {
|
||||||
self.player.replaceCurrentItem(with: playerItem)
|
self.player.replaceCurrentItem(with: playerItem)
|
||||||
self.seekToSavedTime { finished in
|
self.seekToSavedTime { finished in
|
||||||
@ -176,19 +215,8 @@ final class PlayerModel: ObservableObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
self.savedTime = nil
|
self.savedTime = nil
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
||||||
self.play()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let startPlaying = {
|
startPlaying()
|
||||||
#if !os(macOS)
|
|
||||||
try? AVAudioSession.sharedInstance().setActive(true)
|
|
||||||
#endif
|
|
||||||
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
||||||
self.play()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -224,7 +252,11 @@ final class PlayerModel: ObservableObject {
|
|||||||
insertPlayerItem(stream, for: video, preservingTime: preservingTime)
|
insertPlayerItem(stream, for: video, preservingTime: preservingTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadCompositionAsset(_ asset: AVURLAsset, type: AVMediaType, of video: Video) async {
|
private func loadCompositionAsset(
|
||||||
|
_ asset: AVURLAsset,
|
||||||
|
type: AVMediaType,
|
||||||
|
of video: Video
|
||||||
|
) async {
|
||||||
async let assetTracks = asset.loadTracks(withMediaType: type)
|
async let assetTracks = asset.loadTracks(withMediaType: type)
|
||||||
|
|
||||||
logger.info("loading \(type.rawValue) track")
|
logger.info("loading \(type.rawValue) track")
|
||||||
@ -242,7 +274,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try! compositionTrack.insertTimeRange(
|
try! compositionTrack.insertTimeRange(
|
||||||
CMTimeRange(start: .zero, duration: CMTime(seconds: video.length, preferredTimescale: 1_000_000)),
|
CMTimeRange(start: .zero, duration: CMTime.secondsInDefaultTimescale(video.length)),
|
||||||
of: assetTrack,
|
of: assetTrack,
|
||||||
at: .zero
|
at: .zero
|
||||||
)
|
)
|
||||||
@ -279,10 +311,14 @@ final class PlayerModel: ObservableObject {
|
|||||||
item.preferredForwardBufferDuration = 5
|
item.preferredForwardBufferDuration = 5
|
||||||
|
|
||||||
statusObservation?.invalidate()
|
statusObservation?.invalidate()
|
||||||
statusObservation = item.observe(\.status, options: [.old, .new]) { playerItem, _ in
|
statusObservation = item.observe(\.status, options: [.old, .new]) { [weak self] playerItem, _ in
|
||||||
|
guard let self = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
switch playerItem.status {
|
switch playerItem.status {
|
||||||
case .readyToPlay:
|
case .readyToPlay:
|
||||||
if self.isAutoplaying(playerItem), self.shouldResumePlaying {
|
if self.isAutoplaying(playerItem) {
|
||||||
self.play()
|
self.play()
|
||||||
}
|
}
|
||||||
case .failed:
|
case .failed:
|
||||||
@ -321,12 +357,14 @@ final class PlayerModel: ObservableObject {
|
|||||||
try? AVAudioSession.sharedInstance().setActive(false)
|
try? AVAudioSession.sharedInstance().setActive(false)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
currentItem.playbackTime = playerItemDuration
|
||||||
|
|
||||||
if queue.isEmpty {
|
if queue.isEmpty {
|
||||||
addCurrentItemToHistory()
|
addCurrentItemToHistory()
|
||||||
resetQueue()
|
resetQueue()
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
avPlayerViewController!.dismiss(animated: true) {
|
avPlayerViewController!.dismiss(animated: true) { [weak self] in
|
||||||
self.controller!.dismiss(animated: true)
|
self?.controller!.dismiss(animated: true)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
presentingPlayer = false
|
presentingPlayer = false
|
||||||
@ -342,8 +380,8 @@ final class PlayerModel: ObservableObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async { [weak self] in
|
||||||
self.savedTime = currentTime
|
self?.savedTime = currentTime
|
||||||
completionHandler()
|
completionHandler()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -355,43 +393,74 @@ final class PlayerModel: ObservableObject {
|
|||||||
|
|
||||||
player.seek(
|
player.seek(
|
||||||
to: time,
|
to: time,
|
||||||
toleranceBefore: .init(seconds: 1, preferredTimescale: 1_000_000),
|
toleranceBefore: .secondsInDefaultTimescale(1),
|
||||||
toleranceAfter: .zero,
|
toleranceAfter: .zero,
|
||||||
completionHandler: completionHandler
|
completionHandler: completionHandler
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addTimeObserver() {
|
private func addFrequentTimeObserver() {
|
||||||
let interval = CMTime(seconds: 0.5, preferredTimescale: 1_000_000)
|
let interval = CMTime.secondsInDefaultTimescale(0.5)
|
||||||
|
|
||||||
|
frequentTimeObserver = player.addPeriodicTimeObserver(
|
||||||
|
forInterval: interval,
|
||||||
|
queue: .main
|
||||||
|
) { [weak self] _ in
|
||||||
|
guard let self = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
timeObserver = player.addPeriodicTimeObserver(forInterval: interval, queue: .main) { _ in
|
|
||||||
self.currentRate = self.player.rate
|
self.currentRate = self.player.rate
|
||||||
|
|
||||||
guard !self.currentItem.isNil else {
|
guard !self.currentItem.isNil else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let time = self.player.currentTime()
|
self.handleSegments(at: self.player.currentTime())
|
||||||
|
|
||||||
self.currentItem!.playbackTime = time
|
|
||||||
self.currentItem!.videoDuration = self.player.currentItem?.asset.duration.seconds
|
|
||||||
|
|
||||||
self.handleSegments(at: time)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(macOS)
|
private func addInfrequentTimeObserver() {
|
||||||
private func addPlayerTimeControlStatusObserver() {
|
let interval = CMTime.secondsInDefaultTimescale(5)
|
||||||
playerTimeControlStatusObserver = player.observe(\.timeControlStatus) { player, _ in
|
|
||||||
guard self.player == player else {
|
infrequentTimeObserver = player.addPeriodicTimeObserver(
|
||||||
return
|
forInterval: interval,
|
||||||
}
|
queue: .main
|
||||||
|
) { [weak self] _ in
|
||||||
|
guard let self = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !self.currentItem.isNil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.updateCurrentItemIntervals()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func addPlayerTimeControlStatusObserver() {
|
||||||
|
playerTimeControlStatusObserver = player.observe(\.timeControlStatus) { [weak self] player, _ in
|
||||||
|
guard let self = self,
|
||||||
|
self.player == player
|
||||||
|
else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
if player.timeControlStatus == .playing {
|
if player.timeControlStatus == .playing {
|
||||||
ScreenSaverManager.shared.disable(reason: "Yattee is playing video")
|
ScreenSaverManager.shared.disable(reason: "Yattee is playing video")
|
||||||
} else {
|
} else {
|
||||||
ScreenSaverManager.shared.enable()
|
ScreenSaverManager.shared.enable()
|
||||||
}
|
}
|
||||||
}
|
#endif
|
||||||
|
|
||||||
|
self.updateCurrentItemIntervals()
|
||||||
}
|
}
|
||||||
#endif
|
}
|
||||||
|
|
||||||
|
private func updateCurrentItemIntervals() {
|
||||||
|
currentItem?.playbackTime = player.currentTime()
|
||||||
|
currentItem?.videoDuration = player.currentItem?.asset.duration.seconds
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,7 +39,7 @@ extension PlayerModel {
|
|||||||
currentItem = item
|
currentItem = item
|
||||||
|
|
||||||
if !time.isNil {
|
if !time.isNil {
|
||||||
currentItem.playbackTime = CMTime(seconds: time!, preferredTimescale: 1_000_000)
|
currentItem.playbackTime = .secondsInDefaultTimescale(time!)
|
||||||
} else if currentItem.playbackTime.isNil {
|
} else if currentItem.playbackTime.isNil {
|
||||||
currentItem.playbackTime = .zero
|
currentItem.playbackTime = .zero
|
||||||
}
|
}
|
||||||
@ -48,7 +48,20 @@ extension PlayerModel {
|
|||||||
currentItem.video = video!
|
currentItem.video = video!
|
||||||
}
|
}
|
||||||
|
|
||||||
playVideo(currentVideo!, time: currentItem.playbackTime)
|
savedTime = currentItem.playbackTime
|
||||||
|
|
||||||
|
loadAvailableStreams(currentVideo!) { streams in
|
||||||
|
guard let stream = streams.first else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.streamSelection = stream
|
||||||
|
self.playStream(
|
||||||
|
stream,
|
||||||
|
of: self.currentVideo!,
|
||||||
|
preservingTime: !self.currentItem.playbackTime.isNil
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func advanceToNextItem() {
|
func advanceToNextItem() {
|
||||||
@ -62,9 +75,10 @@ extension PlayerModel {
|
|||||||
func advanceToItem(_ newItem: PlayerQueueItem, at time: TimeInterval? = nil) {
|
func advanceToItem(_ newItem: PlayerQueueItem, at time: TimeInterval? = nil) {
|
||||||
addCurrentItemToHistory()
|
addCurrentItemToHistory()
|
||||||
|
|
||||||
let item = remove(newItem)!
|
remove(newItem)
|
||||||
loadDetails(newItem.video) { video in
|
|
||||||
self.playItem(item, video: video, at: time)
|
accounts.api.loadDetails(newItem) { newItem in
|
||||||
|
self.playItem(newItem, video: newItem.video, at: time)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,18 +91,30 @@ extension PlayerModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func resetQueue() {
|
func resetQueue() {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
guard let self = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
self.currentItem = nil
|
self.currentItem = nil
|
||||||
self.stream = nil
|
self.stream = nil
|
||||||
self.removeQueueItems()
|
self.removeQueueItems()
|
||||||
self.timeObserver = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
player.replaceCurrentItem(with: nil)
|
player.replaceCurrentItem(with: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func isAutoplaying(_ item: AVPlayerItem) -> Bool {
|
func isAutoplaying(_ item: AVPlayerItem) -> Bool {
|
||||||
player.currentItem == item
|
guard player.currentItem == item else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !autoPlayItems {
|
||||||
|
autoPlayItems = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult func enqueueVideo(
|
@discardableResult func enqueueVideo(
|
||||||
@ -102,36 +128,24 @@ extension PlayerModel {
|
|||||||
|
|
||||||
queue.insert(item, at: prepending ? 0 : queue.endIndex)
|
queue.insert(item, at: prepending ? 0 : queue.endIndex)
|
||||||
|
|
||||||
loadDetails(video) { video in
|
accounts.api.loadDetails(item) { newItem in
|
||||||
videoDetailsLoadHandler(video, item)
|
videoDetailsLoadHandler(newItem.video, newItem)
|
||||||
|
|
||||||
if play {
|
if play {
|
||||||
self.playItem(item, video: video)
|
self.playItem(newItem, video: video)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadDetails(_ video: Video?, onSuccess: @escaping (Video) -> Void) {
|
|
||||||
guard video != nil else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
accounts.api.video(video!.videoID).load().onSuccess { response in
|
|
||||||
if let video: Video = response.typedContent() {
|
|
||||||
onSuccess(video)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func addCurrentItemToHistory() {
|
func addCurrentItemToHistory() {
|
||||||
if let item = currentItem {
|
if let item = currentItem {
|
||||||
if let index = history.firstIndex(where: { $0.video.videoID == item.video.videoID }) {
|
if let index = history.firstIndex(where: { $0.video.videoID == item.video?.videoID }) {
|
||||||
history.remove(at: index)
|
history.remove(at: index)
|
||||||
}
|
}
|
||||||
|
|
||||||
history.insert(item, at: 0)
|
history.insert(currentItem, at: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,14 +1,19 @@
|
|||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
import Defaults
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
struct PlayerQueueItem: Hashable, Identifiable {
|
struct PlayerQueueItem: Hashable, Identifiable, Defaults.Serializable {
|
||||||
|
static let bridge = PlayerQueueItemBridge()
|
||||||
|
|
||||||
var id = UUID()
|
var id = UUID()
|
||||||
var video: Video
|
var video: Video!
|
||||||
|
var videoID: Video.ID
|
||||||
var playbackTime: CMTime?
|
var playbackTime: CMTime?
|
||||||
var videoDuration: TimeInterval?
|
var videoDuration: TimeInterval?
|
||||||
|
|
||||||
init(_ video: Video, playbackTime: CMTime? = nil, videoDuration: TimeInterval? = nil) {
|
init(_ video: Video? = nil, videoID: Video.ID? = nil, playbackTime: CMTime? = nil, videoDuration: TimeInterval? = nil) {
|
||||||
self.video = video
|
self.video = video
|
||||||
|
self.videoID = videoID ?? video!.videoID
|
||||||
self.playbackTime = playbackTime
|
self.playbackTime = playbackTime
|
||||||
self.videoDuration = videoDuration
|
self.videoDuration = videoDuration
|
||||||
}
|
}
|
||||||
|
67
Model/Player/PlayerQueueItemBridge.swift
Normal file
67
Model/Player/PlayerQueueItemBridge.swift
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import CoreMedia
|
||||||
|
import Defaults
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct PlayerQueueItemBridge: Defaults.Bridge {
|
||||||
|
typealias Value = PlayerQueueItem
|
||||||
|
typealias Serializable = [String: String]
|
||||||
|
|
||||||
|
func serialize(_ value: Value?) -> Serializable? {
|
||||||
|
guard let value = value else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let videoID = value.videoID.isEmpty ? value.video!.videoID : value.videoID
|
||||||
|
|
||||||
|
var playbackTime = ""
|
||||||
|
if let time = value.playbackTime {
|
||||||
|
if time.seconds.isFinite {
|
||||||
|
playbackTime = String(time.seconds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var videoDuration = ""
|
||||||
|
if let duration = value.videoDuration {
|
||||||
|
if duration.isFinite {
|
||||||
|
videoDuration = String(duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
"videoID": videoID,
|
||||||
|
"playbackTime": playbackTime,
|
||||||
|
"videoDuration": videoDuration
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
func deserialize(_ object: Serializable?) -> Value? {
|
||||||
|
guard
|
||||||
|
let object = object,
|
||||||
|
let videoID = object["videoID"]
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var playbackTime: CMTime?
|
||||||
|
var videoDuration: TimeInterval?
|
||||||
|
|
||||||
|
if let time = object["playbackTime"],
|
||||||
|
!time.isEmpty,
|
||||||
|
let seconds = TimeInterval(time)
|
||||||
|
{
|
||||||
|
playbackTime = .secondsInDefaultTimescale(seconds)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let duration = object["videoDuration"],
|
||||||
|
!duration.isEmpty
|
||||||
|
{
|
||||||
|
videoDuration = TimeInterval(duration)
|
||||||
|
}
|
||||||
|
|
||||||
|
return PlayerQueueItem(
|
||||||
|
videoID: videoID,
|
||||||
|
playbackTime: playbackTime,
|
||||||
|
videoDuration: videoDuration
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,7 @@ import Foundation
|
|||||||
extension PlayerModel {
|
extension PlayerModel {
|
||||||
func handleSegments(at time: CMTime) {
|
func handleSegments(at time: CMTime) {
|
||||||
if let segment = lastSkipped {
|
if let segment = lastSkipped {
|
||||||
if time > CMTime(seconds: segment.end + 10, preferredTimescale: 1_000_000) {
|
if time > .secondsInDefaultTimescale(segment.end + 10) {
|
||||||
resetLastSegment()
|
resetLastSegment()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -18,7 +18,7 @@ extension PlayerModel {
|
|||||||
var nextSegments = [firstSegment]
|
var nextSegments = [firstSegment]
|
||||||
|
|
||||||
while let segment = sponsorBlock.segments.first(where: {
|
while let segment = sponsorBlock.segments.first(where: {
|
||||||
$0.timeInSegment(CMTime(seconds: nextSegments.last!.end + 2, preferredTimescale: 1_000_000))
|
$0.timeInSegment(.secondsInDefaultTimescale(nextSegments.last!.end + 2))
|
||||||
}) {
|
}) {
|
||||||
nextSegments.append(segment)
|
nextSegments.append(segment)
|
||||||
}
|
}
|
@ -129,9 +129,9 @@
|
|||||||
374C053B2724614F009BDDBE /* PlayerTVMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053A2724614F009BDDBE /* PlayerTVMenu.swift */; };
|
374C053B2724614F009BDDBE /* PlayerTVMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053A2724614F009BDDBE /* PlayerTVMenu.swift */; };
|
||||||
374C053C2724614F009BDDBE /* PlayerTVMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053A2724614F009BDDBE /* PlayerTVMenu.swift */; };
|
374C053C2724614F009BDDBE /* PlayerTVMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053A2724614F009BDDBE /* PlayerTVMenu.swift */; };
|
||||||
374C053D2724614F009BDDBE /* PlayerTVMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053A2724614F009BDDBE /* PlayerTVMenu.swift */; };
|
374C053D2724614F009BDDBE /* PlayerTVMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053A2724614F009BDDBE /* PlayerTVMenu.swift */; };
|
||||||
374C053F272472C0009BDDBE /* PlayerSegments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053E272472C0009BDDBE /* PlayerSegments.swift */; };
|
374C053F272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053E272472C0009BDDBE /* PlayerSponsorBlock.swift */; };
|
||||||
374C0540272472C0009BDDBE /* PlayerSegments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053E272472C0009BDDBE /* PlayerSegments.swift */; };
|
374C0540272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053E272472C0009BDDBE /* PlayerSponsorBlock.swift */; };
|
||||||
374C0541272472C0009BDDBE /* PlayerSegments.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053E272472C0009BDDBE /* PlayerSegments.swift */; };
|
374C0541272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C053E272472C0009BDDBE /* PlayerSponsorBlock.swift */; };
|
||||||
374C0543272496E4009BDDBE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C0542272496E4009BDDBE /* AppDelegate.swift */; };
|
374C0543272496E4009BDDBE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374C0542272496E4009BDDBE /* AppDelegate.swift */; };
|
||||||
375168D62700FAFF008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; };
|
375168D62700FAFF008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; };
|
||||||
375168D72700FDB8008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; };
|
375168D72700FDB8008F96A6 /* Debounce.swift in Sources */ = {isa = PBXBuildFile; fileRef = 375168D52700FAFF008F96A6 /* Debounce.swift */; };
|
||||||
@ -302,6 +302,12 @@
|
|||||||
37BE0BDA26A214630092E2DB /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BD926A214630092E2DB /* PlayerViewController.swift */; };
|
37BE0BDA26A214630092E2DB /* PlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BD926A214630092E2DB /* PlayerViewController.swift */; };
|
||||||
37BE0BDC26A2367F0092E2DB /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BDB26A2367F0092E2DB /* Player.swift */; };
|
37BE0BDC26A2367F0092E2DB /* Player.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BDB26A2367F0092E2DB /* Player.swift */; };
|
||||||
37C069782725962F00F7F6CB /* ScreenSaverManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C069772725962F00F7F6CB /* ScreenSaverManager.swift */; };
|
37C069782725962F00F7F6CB /* ScreenSaverManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C069772725962F00F7F6CB /* ScreenSaverManager.swift */; };
|
||||||
|
37C0697A2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C069792725C09E00F7F6CB /* PlayerQueueItemBridge.swift */; };
|
||||||
|
37C0697B2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C069792725C09E00F7F6CB /* PlayerQueueItemBridge.swift */; };
|
||||||
|
37C0697C2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C069792725C09E00F7F6CB /* PlayerQueueItemBridge.swift */; };
|
||||||
|
37C0697E2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C0697D2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift */; };
|
||||||
|
37C0697F2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C0697D2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift */; };
|
||||||
|
37C069802725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C0697D2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift */; };
|
||||||
37C194C726F6A9C8005D3B96 /* RecentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* RecentsModel.swift */; };
|
37C194C726F6A9C8005D3B96 /* RecentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* RecentsModel.swift */; };
|
||||||
37C194C826F6A9C8005D3B96 /* RecentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* RecentsModel.swift */; };
|
37C194C826F6A9C8005D3B96 /* RecentsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C194C626F6A9C8005D3B96 /* RecentsModel.swift */; };
|
||||||
37C3A241272359900087A57A /* Double+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C3A240272359900087A57A /* Double+Format.swift */; };
|
37C3A241272359900087A57A /* Double+Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37C3A240272359900087A57A /* Double+Format.swift */; };
|
||||||
@ -509,7 +515,7 @@
|
|||||||
37484C3026FCB8F900287258 /* AccountValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountValidator.swift; sourceTree = "<group>"; };
|
37484C3026FCB8F900287258 /* AccountValidator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountValidator.swift; sourceTree = "<group>"; };
|
||||||
374C053427242D9F009BDDBE /* ServicesSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServicesSettings.swift; sourceTree = "<group>"; };
|
374C053427242D9F009BDDBE /* ServicesSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServicesSettings.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 /* PlayerSegments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerSegments.swift; sourceTree = "<group>"; };
|
374C053E272472C0009BDDBE /* PlayerSponsorBlock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerSponsorBlock.swift; sourceTree = "<group>"; };
|
||||||
374C0542272496E4009BDDBE /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = macOS/AppDelegate.swift; sourceTree = SOURCE_ROOT; };
|
374C0542272496E4009BDDBE /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AppDelegate.swift; path = macOS/AppDelegate.swift; sourceTree = SOURCE_ROOT; };
|
||||||
374C0544272496FD009BDDBE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
374C0544272496FD009BDDBE /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
375168D52700FAFF008F96A6 /* Debounce.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debounce.swift; sourceTree = "<group>"; };
|
375168D52700FAFF008F96A6 /* Debounce.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Debounce.swift; sourceTree = "<group>"; };
|
||||||
@ -577,6 +583,8 @@
|
|||||||
37BE0BD926A214630092E2DB /* PlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = "<group>"; };
|
37BE0BD926A214630092E2DB /* PlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerViewController.swift; sourceTree = "<group>"; };
|
||||||
37BE0BDB26A2367F0092E2DB /* Player.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = "<group>"; };
|
37BE0BDB26A2367F0092E2DB /* Player.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Player.swift; sourceTree = "<group>"; };
|
||||||
37C069772725962F00F7F6CB /* ScreenSaverManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenSaverManager.swift; sourceTree = "<group>"; };
|
37C069772725962F00F7F6CB /* ScreenSaverManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenSaverManager.swift; sourceTree = "<group>"; };
|
||||||
|
37C069792725C09E00F7F6CB /* PlayerQueueItemBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueueItemBridge.swift; sourceTree = "<group>"; };
|
||||||
|
37C0697D2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CMTime+DefaultTimescale.swift"; sourceTree = "<group>"; };
|
||||||
37C194C626F6A9C8005D3B96 /* RecentsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentsModel.swift; sourceTree = "<group>"; };
|
37C194C626F6A9C8005D3B96 /* RecentsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentsModel.swift; sourceTree = "<group>"; };
|
||||||
37C3A240272359900087A57A /* Double+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Format.swift"; sourceTree = "<group>"; };
|
37C3A240272359900087A57A /* Double+Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Format.swift"; sourceTree = "<group>"; };
|
||||||
37C3A24427235DA70087A57A /* ChannelPlaylist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelPlaylist.swift; sourceTree = "<group>"; };
|
37C3A24427235DA70087A57A /* ChannelPlaylist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelPlaylist.swift; sourceTree = "<group>"; };
|
||||||
@ -814,7 +822,8 @@
|
|||||||
37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */,
|
37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */,
|
||||||
37319F0427103F94004ECCD0 /* PlayerQueue.swift */,
|
37319F0427103F94004ECCD0 /* PlayerQueue.swift */,
|
||||||
37CC3F44270CE30600608308 /* PlayerQueueItem.swift */,
|
37CC3F44270CE30600608308 /* PlayerQueueItem.swift */,
|
||||||
374C053E272472C0009BDDBE /* PlayerSegments.swift */,
|
37C069792725C09E00F7F6CB /* PlayerQueueItemBridge.swift */,
|
||||||
|
374C053E272472C0009BDDBE /* PlayerSponsorBlock.swift */,
|
||||||
37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */,
|
37DD87C6271C9CFE0027CBF9 /* PlayerStreams.swift */,
|
||||||
374C053A2724614F009BDDBE /* PlayerTVMenu.swift */,
|
374C053A2724614F009BDDBE /* PlayerTVMenu.swift */,
|
||||||
37C069772725962F00F7F6CB /* ScreenSaverManager.swift */,
|
37C069772725962F00F7F6CB /* ScreenSaverManager.swift */,
|
||||||
@ -954,6 +963,7 @@
|
|||||||
children = (
|
children = (
|
||||||
379775922689365600DD52A8 /* Array+Next.swift */,
|
379775922689365600DD52A8 /* Array+Next.swift */,
|
||||||
376578842685429C00D4EA09 /* CaseIterable+Next.swift */,
|
376578842685429C00D4EA09 /* CaseIterable+Next.swift */,
|
||||||
|
37C0697D2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift */,
|
||||||
37C3A240272359900087A57A /* Double+Format.swift */,
|
37C3A240272359900087A57A /* Double+Format.swift */,
|
||||||
37BA794E26DC3E0E002A0235 /* Int+Format.swift */,
|
37BA794E26DC3E0E002A0235 /* Int+Format.swift */,
|
||||||
377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */,
|
377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */,
|
||||||
@ -1643,7 +1653,7 @@
|
|||||||
377FC7E5267A084E00A6BBAF /* SearchView.swift in Sources */,
|
377FC7E5267A084E00A6BBAF /* SearchView.swift in Sources */,
|
||||||
376578912685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
376578912685490700D4EA09 /* PlaylistsView.swift in Sources */,
|
||||||
37C3A24527235DA70087A57A /* ChannelPlaylist.swift in Sources */,
|
37C3A24527235DA70087A57A /* ChannelPlaylist.swift in Sources */,
|
||||||
374C053F272472C0009BDDBE /* PlayerSegments.swift in Sources */,
|
374C053F272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */,
|
||||||
37FB28412721B22200A57617 /* ContentItem.swift in Sources */,
|
37FB28412721B22200A57617 /* ContentItem.swift in Sources */,
|
||||||
37C3A24D272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */,
|
37C3A24D272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */,
|
||||||
37484C2D26FC844700287258 /* AccountsSettings.swift in Sources */,
|
37484C2D26FC844700287258 /* AccountsSettings.swift in Sources */,
|
||||||
@ -1651,6 +1661,7 @@
|
|||||||
37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */,
|
37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */,
|
||||||
37319F0527103F94004ECCD0 /* PlayerQueue.swift in Sources */,
|
37319F0527103F94004ECCD0 /* PlayerQueue.swift in Sources */,
|
||||||
376B2E0726F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
376B2E0726F920D600B1D64D /* SignInRequiredView.swift in Sources */,
|
||||||
|
37C0697E2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */,
|
||||||
3743CA4E270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
|
3743CA4E270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
|
||||||
37BD672426F13D65004BE0C1 /* AppSidebarPlaylists.swift in Sources */,
|
37BD672426F13D65004BE0C1 /* AppSidebarPlaylists.swift in Sources */,
|
||||||
37B17DA2268A1F8A006AEE9B /* VideoContextMenuView.swift in Sources */,
|
37B17DA2268A1F8A006AEE9B /* VideoContextMenuView.swift in Sources */,
|
||||||
@ -1688,6 +1699,7 @@
|
|||||||
37E70923271CD43000D34DDE /* WelcomeScreen.swift in Sources */,
|
37E70923271CD43000D34DDE /* WelcomeScreen.swift in Sources */,
|
||||||
37484C1D26FC83A400287258 /* InstancesSettings.swift in Sources */,
|
37484C1D26FC83A400287258 /* InstancesSettings.swift in Sources */,
|
||||||
37BD07BB2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */,
|
37BD07BB2698AB60003EBB87 /* AppSidebarNavigation.swift in Sources */,
|
||||||
|
37C0697A2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */,
|
||||||
37D4B0E42671614900C925CA /* PearvidiousApp.swift in Sources */,
|
37D4B0E42671614900C925CA /* PearvidiousApp.swift in Sources */,
|
||||||
37C3A241272359900087A57A /* Double+Format.swift in Sources */,
|
37C3A241272359900087A57A /* Double+Format.swift in Sources */,
|
||||||
37FB285E272225E800A57617 /* ContentItemView.swift in Sources */,
|
37FB285E272225E800A57617 /* ContentItemView.swift in Sources */,
|
||||||
@ -1706,7 +1718,7 @@
|
|||||||
37C3A24627235DA70087A57A /* ChannelPlaylist.swift in Sources */,
|
37C3A24627235DA70087A57A /* ChannelPlaylist.swift in Sources */,
|
||||||
3788AC2C26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */,
|
3788AC2C26F6842D00F6BAA9 /* WatchNowSectionBody.swift in Sources */,
|
||||||
37CEE4BE2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
|
37CEE4BE2677B670005A1EFE /* SingleAssetStream.swift in Sources */,
|
||||||
374C0540272472C0009BDDBE /* PlayerSegments.swift in Sources */,
|
374C0540272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */,
|
||||||
374C053627242D9F009BDDBE /* ServicesSettings.swift in Sources */,
|
374C053627242D9F009BDDBE /* ServicesSettings.swift in Sources */,
|
||||||
37BA794826DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */,
|
37BA794826DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */,
|
||||||
37E70928271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */,
|
37E70928271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */,
|
||||||
@ -1767,6 +1779,7 @@
|
|||||||
37F4AE7326828F0900BD60EA /* VerticalCells.swift in Sources */,
|
37F4AE7326828F0900BD60EA /* VerticalCells.swift in Sources */,
|
||||||
37001560271B12DD0049C794 /* SiestaConfiguration.swift in Sources */,
|
37001560271B12DD0049C794 /* SiestaConfiguration.swift in Sources */,
|
||||||
37B81AFD26D2C9C900675966 /* VideoDetailsPaddingModifier.swift in Sources */,
|
37B81AFD26D2C9C900675966 /* VideoDetailsPaddingModifier.swift in Sources */,
|
||||||
|
37C0697F2725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */,
|
||||||
37A9965B26D6F8CA006E3224 /* HorizontalCells.swift in Sources */,
|
37A9965B26D6F8CA006E3224 /* HorizontalCells.swift in Sources */,
|
||||||
3761AC1026F0F9A600AA496F /* UnsubscribeAlertModifier.swift in Sources */,
|
3761AC1026F0F9A600AA496F /* UnsubscribeAlertModifier.swift in Sources */,
|
||||||
37732FF52703D32400F04329 /* Sidebar.swift in Sources */,
|
37732FF52703D32400F04329 /* Sidebar.swift in Sources */,
|
||||||
@ -1804,6 +1817,7 @@
|
|||||||
3748186B26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
|
3748186B26A764FB0084E870 /* Thumbnail+Fixtures.swift in Sources */,
|
||||||
37C3A252272366440087A57A /* ChannelPlaylistView.swift in Sources */,
|
37C3A252272366440087A57A /* ChannelPlaylistView.swift in Sources */,
|
||||||
373CFADC269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
373CFADC269663F1003CB2C6 /* Thumbnail.swift in Sources */,
|
||||||
|
37C0697B2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */,
|
||||||
37C3A24A27235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */,
|
37C3A24A27235FAA0087A57A /* ChannelPlaylistCell.swift in Sources */,
|
||||||
373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
||||||
3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
|
3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
|
||||||
@ -1861,6 +1875,7 @@
|
|||||||
37DD87C9271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */,
|
37DD87C9271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */,
|
||||||
375168D82700FDB9008F96A6 /* Debounce.swift in Sources */,
|
375168D82700FDB9008F96A6 /* Debounce.swift in Sources */,
|
||||||
37BA794126DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
37BA794126DB8F97002A0235 /* ChannelVideosView.swift in Sources */,
|
||||||
|
37C0697C2725C09E00F7F6CB /* PlayerQueueItemBridge.swift in Sources */,
|
||||||
37C3A243272359900087A57A /* Double+Format.swift in Sources */,
|
37C3A243272359900087A57A /* Double+Format.swift in Sources */,
|
||||||
37AAF29226740715007FC770 /* Channel.swift in Sources */,
|
37AAF29226740715007FC770 /* Channel.swift in Sources */,
|
||||||
37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
37EAD86D267B9C5600D9E01B /* SponsorBlockAPI.swift in Sources */,
|
||||||
@ -1882,7 +1897,7 @@
|
|||||||
3743CA50270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
|
3743CA50270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
|
||||||
37A9966026D6F9B9006E3224 /* WatchNowView.swift in Sources */,
|
37A9966026D6F9B9006E3224 /* WatchNowView.swift in Sources */,
|
||||||
37001565271B1F250049C794 /* AccountsModel.swift in Sources */,
|
37001565271B1F250049C794 /* AccountsModel.swift in Sources */,
|
||||||
374C0541272472C0009BDDBE /* PlayerSegments.swift in Sources */,
|
374C0541272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */,
|
||||||
37E70929271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */,
|
37E70929271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */,
|
||||||
376A33E62720CB35000C1D6B /* Account.swift in Sources */,
|
376A33E62720CB35000C1D6B /* Account.swift in Sources */,
|
||||||
37484C1F26FC83A400287258 /* InstancesSettings.swift in Sources */,
|
37484C1F26FC83A400287258 /* InstancesSettings.swift in Sources */,
|
||||||
@ -1907,6 +1922,7 @@
|
|||||||
37484C2B26FC83FF00287258 /* AccountForm.swift in Sources */,
|
37484C2B26FC83FF00287258 /* AccountForm.swift in Sources */,
|
||||||
37FB2860272225E800A57617 /* ContentItemView.swift in Sources */,
|
37FB2860272225E800A57617 /* ContentItemView.swift in Sources */,
|
||||||
374C053727242D9F009BDDBE /* ServicesSettings.swift in Sources */,
|
374C053727242D9F009BDDBE /* ServicesSettings.swift in Sources */,
|
||||||
|
37C069802725C8D400F7F6CB /* CMTime+DefaultTimescale.swift in Sources */,
|
||||||
37BA793D26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
37BA793D26DB8EE4002A0235 /* PlaylistVideosView.swift in Sources */,
|
||||||
3711404126B206A6005B3555 /* SearchModel.swift in Sources */,
|
3711404126B206A6005B3555 /* SearchModel.swift in Sources */,
|
||||||
37FD43F02704A9C00073EE42 /* RecentsModel.swift in Sources */,
|
37FD43F02704A9C00073EE42 /* RecentsModel.swift in Sources */,
|
||||||
|
@ -29,6 +29,10 @@ extension Defaults.Keys {
|
|||||||
|
|
||||||
static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: [])
|
static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: [])
|
||||||
|
|
||||||
|
static let queue = Key<[PlayerQueueItem]>("queue", default: [])
|
||||||
|
static let history = Key<[PlayerQueueItem]>("history", default: [])
|
||||||
|
static let lastPlayed = Key<PlayerQueueItem?>("lastPlayed")
|
||||||
|
|
||||||
static let trendingCategory = Key<TrendingCategory>("trendingCategory", default: .default)
|
static let trendingCategory = Key<TrendingCategory>("trendingCategory", default: .default)
|
||||||
static let trendingCountry = Key<Country>("trendingCountry", default: .us)
|
static let trendingCountry = Key<Country>("trendingCountry", default: .us)
|
||||||
}
|
}
|
||||||
|
@ -111,6 +111,10 @@ struct ContentView: View {
|
|||||||
playlists.accounts = accounts
|
playlists.accounts = accounts
|
||||||
search.accounts = accounts
|
search.accounts = accounts
|
||||||
subscriptions.accounts = accounts
|
subscriptions.accounts = accounts
|
||||||
|
|
||||||
|
if !accounts.current.isNil {
|
||||||
|
player.loadHistoryDetails()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func openWelcomeScreenIfAccountEmpty() {
|
func openWelcomeScreenIfAccountEmpty() {
|
||||||
@ -135,6 +139,7 @@ struct ContentView: View {
|
|||||||
|
|
||||||
accounts.api.video(id).load().onSuccess { response in
|
accounts.api.video(id).load().onSuccess { response in
|
||||||
if let video: Video = response.typedContent() {
|
if let video: Video = response.typedContent() {
|
||||||
|
self.player.autoPlayItems = true
|
||||||
self.player.playNow(video, at: parser.time)
|
self.player.playNow(video, at: parser.time)
|
||||||
self.player.presentPlayer()
|
self.player.presentPlayer()
|
||||||
}
|
}
|
||||||
|
@ -24,7 +24,7 @@ struct PlaybackBar: View {
|
|||||||
if !player.lastSkipped.isNil {
|
if !player.lastSkipped.isNil {
|
||||||
restoreLastSkippedSegmentButton
|
restoreLastSkippedSegmentButton
|
||||||
}
|
}
|
||||||
if player.currentVideo!.live {
|
if player.live {
|
||||||
Image(systemName: "dot.radiowaves.left.and.right")
|
Image(systemName: "dot.radiowaves.left.and.right")
|
||||||
} else if player.isLoadingAvailableStreams || player.isLoadingStream {
|
} else if player.isLoadingAvailableStreams || player.isLoadingStream {
|
||||||
Image(systemName: "bolt.horizontal.fill")
|
Image(systemName: "bolt.horizontal.fill")
|
||||||
@ -78,7 +78,7 @@ struct PlaybackBar: View {
|
|||||||
return "LIVE"
|
return "LIVE"
|
||||||
}
|
}
|
||||||
|
|
||||||
guard player.time != nil, player.time!.isValid else {
|
guard player.time != nil, player.time!.isValid, !player.currentVideo.isNil else {
|
||||||
return "loading..."
|
return "loading..."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,11 +4,11 @@ import SDWebImageSwiftUI
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct VideoBanner: View {
|
struct VideoBanner: View {
|
||||||
let video: Video
|
let video: Video?
|
||||||
var playbackTime: CMTime?
|
var playbackTime: CMTime?
|
||||||
var videoDuration: TimeInterval?
|
var videoDuration: TimeInterval?
|
||||||
|
|
||||||
init(video: Video, playbackTime: CMTime? = nil, videoDuration: TimeInterval? = nil) {
|
init(video: Video? = nil, playbackTime: CMTime? = nil, videoDuration: TimeInterval? = nil) {
|
||||||
self.video = video
|
self.video = video
|
||||||
self.playbackTime = playbackTime
|
self.playbackTime = playbackTime
|
||||||
self.videoDuration = videoDuration
|
self.videoDuration = videoDuration
|
||||||
@ -24,14 +24,14 @@ struct VideoBanner: View {
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Text(video.title)
|
Text(video?.title ?? "Unknown title")
|
||||||
.truncationMode(.middle)
|
.truncationMode(.middle)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
.font(.headline)
|
.font(.headline)
|
||||||
.frame(alignment: .leading)
|
.frame(alignment: .leading)
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Text(video.author)
|
Text(video?.author ?? "Unknown author")
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
@ -40,7 +40,7 @@ struct VideoBanner: View {
|
|||||||
progressView
|
progressView
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if let time = (videoDuration ?? video.length).formattedAsPlaybackTime() {
|
if let time = (videoDuration ?? video?.length ?? 0).formattedAsPlaybackTime() {
|
||||||
Text(time)
|
Text(time)
|
||||||
.fontWeight(.light)
|
.fontWeight(.light)
|
||||||
}
|
}
|
||||||
@ -71,7 +71,7 @@ struct VideoBanner: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var smallThumbnail: some View {
|
private var smallThumbnail: some View {
|
||||||
WebImage(url: video.thumbnailURL(quality: .medium))
|
WebImage(url: video?.thumbnailURL(quality: .medium))
|
||||||
.resizable()
|
.resizable()
|
||||||
.placeholder {
|
.placeholder {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
@ -109,7 +109,7 @@ struct VideoBanner: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var progressViewTotal: Double {
|
private var progressViewTotal: Double {
|
||||||
videoDuration ?? video.length
|
videoDuration ?? video?.length ?? 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -31,12 +31,12 @@ struct PlayerControlsView<Content: View>: View {
|
|||||||
}) {
|
}) {
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading, spacing: 3) {
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
Text(model.currentItem?.video.title ?? "Not playing")
|
Text(model.currentItem?.video?.title ?? "Not playing")
|
||||||
.font(.system(size: 14).bold())
|
.font(.system(size: 14).bold())
|
||||||
.foregroundColor(model.currentItem.isNil ? .secondary : .accentColor)
|
.foregroundColor(model.currentItem.isNil ? .secondary : .accentColor)
|
||||||
.lineLimit(1)
|
.lineLimit(1)
|
||||||
|
|
||||||
Text(model.currentItem?.video.author ?? "Yattee v0.1")
|
Text(model.currentItem?.video?.author ?? "Yattee v0.1")
|
||||||
.fontWeight(model.currentItem.isNil ? .light : .bold)
|
.fontWeight(model.currentItem.isNil ? .light : .bold)
|
||||||
.font(.system(size: 10))
|
.font(.system(size: 10))
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
|
Loading…
Reference in New Issue
Block a user