2022-12-18 18:39:03 +00:00
|
|
|
import Combine
|
|
|
|
import Defaults
|
2022-12-17 23:08:30 +00:00
|
|
|
import Foundation
|
|
|
|
import SwiftUI
|
|
|
|
|
|
|
|
final class WatchNextViewModel: ObservableObject {
|
2022-12-18 18:39:03 +00:00
|
|
|
enum Page: String, CaseIterable {
|
|
|
|
case queue
|
|
|
|
case related
|
|
|
|
case history
|
|
|
|
|
|
|
|
var title: String {
|
|
|
|
rawValue.capitalized.localized()
|
|
|
|
}
|
|
|
|
|
|
|
|
var systemImageName: String {
|
|
|
|
switch self {
|
|
|
|
case .queue:
|
|
|
|
return "list.and.film"
|
|
|
|
case .related:
|
|
|
|
return "rectangle.stack.fill"
|
|
|
|
case .history:
|
|
|
|
return "clock"
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
enum PresentationReason {
|
|
|
|
case userInteracted
|
|
|
|
case finishedWatching
|
|
|
|
case closed
|
|
|
|
}
|
|
|
|
|
2022-12-17 23:08:30 +00:00
|
|
|
static let animation = Animation.easeIn(duration: 0.25)
|
|
|
|
static let shared = WatchNextViewModel()
|
|
|
|
|
|
|
|
@Published var item: PlayerQueueItem?
|
2022-12-18 18:39:03 +00:00
|
|
|
@Published private(set) var isPresenting = true
|
|
|
|
@Published var reason: PresentationReason?
|
|
|
|
@Published var page = Page.queue
|
|
|
|
|
|
|
|
@Published var countdown = 0.0
|
|
|
|
var countdownTimer: Timer?
|
|
|
|
|
|
|
|
private var player = PlayerModel.shared
|
|
|
|
|
|
|
|
var autoplayTimer: Timer?
|
|
|
|
|
|
|
|
var isAutoplaying: Bool {
|
|
|
|
reason == .finishedWatching
|
|
|
|
}
|
2022-12-17 23:08:30 +00:00
|
|
|
|
2022-12-18 18:39:03 +00:00
|
|
|
var isHideable: Bool {
|
|
|
|
reason == .userInteracted
|
|
|
|
}
|
|
|
|
|
|
|
|
var isRestartable: Bool {
|
|
|
|
player.currentItem != nil && reason != .userInteracted
|
|
|
|
}
|
|
|
|
|
|
|
|
var canAutoplay: Bool {
|
|
|
|
switch player.playbackMode {
|
|
|
|
case .shuffle:
|
|
|
|
return !player.queue.isEmpty
|
|
|
|
default:
|
|
|
|
return nextFromTheQueue != nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func userInteractedOpen(_ item: PlayerQueueItem?) {
|
2022-12-17 23:08:30 +00:00
|
|
|
self.item = item
|
2022-12-18 18:39:03 +00:00
|
|
|
open(reason: .userInteracted)
|
2022-12-17 23:08:30 +00:00
|
|
|
}
|
|
|
|
|
2022-12-18 18:39:03 +00:00
|
|
|
func finishedWatching(_ item: PlayerQueueItem?, timer: Timer? = nil) {
|
|
|
|
if canAutoplay {
|
|
|
|
countdown = TimeInterval(Defaults[.openWatchNextOnFinishedWatchingDelay]) ?? 5.0
|
|
|
|
resetCountdownTimer()
|
|
|
|
autoplayTimer?.invalidate()
|
|
|
|
autoplayTimer = timer
|
|
|
|
} else {
|
|
|
|
timer?.invalidate()
|
|
|
|
}
|
2022-12-17 23:08:30 +00:00
|
|
|
self.item = item
|
2022-12-18 18:39:03 +00:00
|
|
|
open(reason: .finishedWatching)
|
|
|
|
}
|
|
|
|
|
|
|
|
func resetCountdownTimer() {
|
|
|
|
countdownTimer?.invalidate()
|
|
|
|
countdownTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { timer in
|
|
|
|
guard self.countdown > 0 else {
|
|
|
|
timer.invalidate()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
self.countdown = max(0, self.countdown - 1)
|
2022-12-17 23:08:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-12-18 18:39:03 +00:00
|
|
|
func closed(_ item: PlayerQueueItem) {
|
|
|
|
self.item = item
|
|
|
|
open(reason: .closed)
|
|
|
|
}
|
|
|
|
|
|
|
|
func keepFromAutoplaying() {
|
|
|
|
userInteractedOpen(item)
|
|
|
|
cancelAutoplay()
|
|
|
|
}
|
|
|
|
|
2022-12-17 23:08:30 +00:00
|
|
|
func cancelAutoplay() {
|
2022-12-18 18:39:03 +00:00
|
|
|
autoplayTimer?.invalidate()
|
|
|
|
countdownTimer?.invalidate()
|
|
|
|
}
|
|
|
|
|
|
|
|
func restart() {
|
|
|
|
cancelAutoplay()
|
|
|
|
|
|
|
|
guard player.currentItem != nil else { return }
|
|
|
|
|
|
|
|
if reason == .closed {
|
|
|
|
hide()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
player.backend.seek(to: .zero, seekType: .loopRestart) { _ in
|
|
|
|
self.hide()
|
|
|
|
self.player.play()
|
|
|
|
}
|
2022-12-17 23:08:30 +00:00
|
|
|
}
|
|
|
|
|
2022-12-18 18:39:03 +00:00
|
|
|
private func open(reason: PresentationReason) {
|
|
|
|
self.reason = reason
|
|
|
|
page = Page.allCases.first { isAvailable($0) } ?? .history
|
|
|
|
|
|
|
|
guard !isPresenting else { return }
|
2022-12-17 23:08:30 +00:00
|
|
|
withAnimation(Self.animation) {
|
2022-12-18 18:39:03 +00:00
|
|
|
isPresenting = true
|
2022-12-17 23:08:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func close() {
|
2022-12-18 18:39:03 +00:00
|
|
|
let close = {
|
|
|
|
self.player.closeCurrentItem()
|
|
|
|
self.player.hide()
|
|
|
|
Delay.by(0.5) {
|
|
|
|
self.isPresenting = false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if reason == .closed {
|
|
|
|
close()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if canAutoplay {
|
|
|
|
cancelAutoplay()
|
|
|
|
hide()
|
|
|
|
} else {
|
|
|
|
close()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func hide() {
|
|
|
|
guard isPresenting else { return }
|
2022-12-17 23:08:30 +00:00
|
|
|
withAnimation(Self.animation) {
|
2022-12-18 18:39:03 +00:00
|
|
|
isPresenting = false
|
2022-12-17 23:08:30 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func resetItem() {
|
|
|
|
item = nil
|
|
|
|
}
|
2022-12-18 18:39:03 +00:00
|
|
|
|
|
|
|
func isAvailable(_ page: Page) -> Bool {
|
|
|
|
switch page {
|
|
|
|
case .queue:
|
|
|
|
return !player.queue.isEmpty
|
|
|
|
case .related:
|
|
|
|
guard let video = item?.video else { return false }
|
|
|
|
return !video.related.isEmpty
|
|
|
|
case .history:
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var nextFromTheQueue: PlayerQueueItem? {
|
|
|
|
if player.playbackMode == .related {
|
|
|
|
return player.autoplayItem
|
|
|
|
} else if player.playbackMode == .queue {
|
|
|
|
return player.queue.first
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
2022-12-17 23:08:30 +00:00
|
|
|
}
|