Watch Next behavior and settings

This commit is contained in:
Arkadiusz Fal
2022-12-18 19:39:03 +01:00
parent b90c856e21
commit 39fc23c5dc
23 changed files with 487 additions and 451 deletions

View File

@@ -107,7 +107,7 @@ struct OpenVideosModel {
prepending: playbackMode == .playNow || playbackMode == .playNext
)
WatchNextViewModel.shared.presentingOutro = false
WatchNextViewModel.shared.hide()
if playbackMode == .playNow || playbackMode == .shuffleAll {
#if os(iOS)

View File

@@ -531,10 +531,6 @@ final class AVPlayerBackend: PlayerBackend {
}
@objc func itemDidPlayToEndTime() {
if Defaults[.closeLastItemOnPlaybackEnd] {
model.prepareCurrentItemForHistory(finished: true)
}
eofPlaybackModeAction()
}

View File

@@ -94,37 +94,59 @@ extension PlayerBackend {
}
func eofPlaybackModeAction() {
let timer = Delay.by(5) {
let loopAction = {
model.backend.seek(to: .zero, seekType: .loopRestart) { _ in
self.model.play()
}
}
guard model.playbackMode != .loopOne else {
loopAction()
return
}
let action = {
switch model.playbackMode {
case .queue, .shuffle:
if Defaults[.closeLastItemOnPlaybackEnd] {
model.prepareCurrentItemForHistory(finished: true)
}
model.prepareCurrentItemForHistory(finished: true)
if model.queue.isEmpty {
if Defaults[.closeLastItemOnPlaybackEnd] {
#if os(tvOS)
if model.activeBackend == .appleAVPlayer {
model.avPlayerBackend.controller?.dismiss(animated: false)
}
#endif
model.resetQueue()
model.hide()
}
#if os(tvOS)
if model.activeBackend == .appleAVPlayer {
model.avPlayerBackend.controller?.dismiss(animated: false)
}
#endif
model.resetQueue()
model.hide()
} else {
model.advanceToNextItem()
}
case .loopOne:
model.backend.seek(to: .zero, seekType: .loopRestart) { _ in
self.model.play()
}
loopAction()
case .related:
guard let item = model.autoplayItem else { return }
model.resetAutoplay()
model.advanceToItem(item)
}
}
WatchNextViewModel.shared.prepareForNextItem(model.currentItem, timer: timer)
let actionAndHideWatchNext: (Bool) -> Void = { delay in
WatchNextViewModel.shared.hide()
if delay {
Delay.by(0.3) {
action()
}
} else {
action()
}
}
if Defaults[.openWatchNextOnFinishedWatching], model.presentingPlayer {
let timer = Delay.by(TimeInterval(Defaults[.openWatchNextOnFinishedWatchingDelay]) ?? 5.0) {
actionAndHideWatchNext(true)
}
WatchNextViewModel.shared.finishedWatching(model.currentItem, timer: timer)
} else {
actionAndHideWatchNext(false)
}
}
func updateControls(completionHandler: (() -> Void)? = nil) {

View File

@@ -80,6 +80,9 @@ final class PlayerModel: ObservableObject {
@Published var playerSize: CGSize = .zero { didSet {
#if !os(tvOS)
#if os(macOS)
guard videoForDisplay != nil else { return }
#endif
backend.setSize(playerSize.width, playerSize.height)
#endif
}}
@@ -162,7 +165,6 @@ final class PlayerModel: ObservableObject {
#if !os(macOS)
@Default(.closePiPAndOpenPlayerOnEnteringForeground) var closePiPAndOpenPlayerOnEnteringForeground
@Default(.closePlayerOnItemClose) private var closePlayerOnItemClose
#endif
private var currentArtwork: MPMediaItemArtwork?
@@ -324,7 +326,7 @@ final class PlayerModel: ObservableObject {
pause()
videoBeingOpened = video
WatchNextViewModel.shared.presentingOutro = false
WatchNextViewModel.shared.hide()
var changeBackendHandler: (() -> Void)?

View File

@@ -14,7 +14,7 @@ extension PlayerModel {
}
func play(_ videos: [Video], shuffling: Bool = false) {
WatchNextViewModel.shared.presentingOutro = false
WatchNextViewModel.shared.hide()
playbackMode = shuffling ? .shuffle : .queue
videos.forEach { enqueueVideo($0, loadDetails: false) }
@@ -55,7 +55,7 @@ extension PlayerModel {
comments.reset()
stream = nil
WatchNextViewModel.shared.close()
WatchNextViewModel.shared.hide()
withAnimation {
aspectRatio = VideoPlayerView.defaultAspectRatio
@@ -175,7 +175,7 @@ extension PlayerModel {
remove(newItem)
WatchNextViewModel.shared.close()
WatchNextViewModel.shared.hide()
currentItem = newItem
currentItem.playbackTime = time
@@ -221,7 +221,7 @@ extension PlayerModel {
if play {
withAnimation {
aspectRatio = VideoPlayerView.defaultAspectRatio
WatchNextViewModel.shared.close()
WatchNextViewModel.shared.hide()
currentItem = item
}
videoBeingOpened = video

View File

@@ -25,7 +25,7 @@ struct PlayerQueueItem: Hashable, Identifiable, Defaults.Serializable {
}
init(
_ video: Video? = nil,
_ video: Video? = .fixture,
videoID: Video.ID? = nil,
app: VideosApp? = nil,
instanceURL: URL? = nil,

View File

@@ -9,10 +9,12 @@ final class UnwatchedFeedCountModel: ObservableObject {
private var accounts = AccountsModel.shared
// swiftlint:disable empty_count
var unwatchedText: Text? {
if let account = accounts.current,
!account.anonymous,
let count = unwatched[account]
let count = unwatched[account],
count > 0
{
return Text(String(count))
}
@@ -23,7 +25,8 @@ final class UnwatchedFeedCountModel: ObservableObject {
func unwatchedByChannelText(_ channel: Channel) -> Text? {
if let account = accounts.current,
!account.anonymous,
let count = unwatchedByChannel[account]?[channel.id]
let count = unwatchedByChannel[account]?[channel.id],
count > 0
{
return Text(String(count))
}

View File

@@ -327,6 +327,10 @@ struct Video: Identifiable, Equatable, Hashable {
return path.contains(".") ? path.components(separatedBy: ".").last?.uppercased() : nil
}
var isShareable: Bool {
!isLocal || localStreamIsRemoteURL
}
private var localStreamURLComponents: URLComponents? {
guard let localStream else { return nil }
return URLComponents(url: localStream.localURL, resolvingAgainstBaseURL: false)

View File

@@ -1,47 +1,192 @@
import Combine
import Defaults
import Foundation
import SwiftUI
final class WatchNextViewModel: ObservableObject {
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
}
static let animation = Animation.easeIn(duration: 0.25)
static let shared = WatchNextViewModel()
@Published var item: PlayerQueueItem?
@Published var presentingOutro = true
@Published var isAutoplaying = true
var timer: Timer?
@Published private(set) var isPresenting = true
@Published var reason: PresentationReason?
@Published var page = Page.queue
func prepareForEmptyPlayerPlaceholder(_ item: PlayerQueueItem? = nil) {
self.item = item
@Published var countdown = 0.0
var countdownTimer: Timer?
private var player = PlayerModel.shared
var autoplayTimer: Timer?
var isAutoplaying: Bool {
reason == .finishedWatching
}
func prepareForNextItem(_ item: PlayerQueueItem? = nil, timer: Timer? = nil) {
self.item = item
self.timer?.invalidate()
self.timer = timer
isAutoplaying = true
withAnimation(Self.animation) {
presentingOutro = true
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 cancelAutoplay() {
timer?.invalidate()
isAutoplaying = false
func userInteractedOpen(_ item: PlayerQueueItem?) {
self.item = item
open(reason: .userInteracted)
}
func open() {
func finishedWatching(_ item: PlayerQueueItem?, timer: Timer? = nil) {
if canAutoplay {
countdown = TimeInterval(Defaults[.openWatchNextOnFinishedWatchingDelay]) ?? 5.0
resetCountdownTimer()
autoplayTimer?.invalidate()
autoplayTimer = timer
} else {
timer?.invalidate()
}
self.item = item
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)
}
}
func closed(_ item: PlayerQueueItem) {
self.item = item
open(reason: .closed)
}
func keepFromAutoplaying() {
userInteractedOpen(item)
cancelAutoplay()
}
func cancelAutoplay() {
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()
}
}
private func open(reason: PresentationReason) {
self.reason = reason
page = Page.allCases.first { isAvailable($0) } ?? .history
guard !isPresenting else { return }
withAnimation(Self.animation) {
presentingOutro = true
isPresenting = true
}
}
func close() {
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 }
withAnimation(Self.animation) {
presentingOutro = false
isPresenting = false
}
}
func resetItem() {
item = nil
}
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
}
}