mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 05:23:41 +00:00
Watch next view
This commit is contained in:
parent
fcf527fa87
commit
eca685ae29
@ -37,6 +37,47 @@ extension Video {
|
|||||||
likes: 37333,
|
likes: 37333,
|
||||||
dislikes: 30,
|
dislikes: 30,
|
||||||
keywords: ["very", "cool", "video", "msfs 2020", "757", "747", "A380", "737-900", "MOD", "Zibo", "MD80", "MD11", "Rotate", "Laminar", "787", "A350", "MSFS", "MS2020", "Microsoft Flight Simulator", "Microsoft", "Flight", "Simulator", "SIM", "World", "Ortho", "Flying", "Boeing MAX"],
|
keywords: ["very", "cool", "video", "msfs 2020", "757", "747", "A380", "737-900", "MOD", "Zibo", "MD80", "MD11", "Rotate", "Laminar", "787", "A350", "MSFS", "MS2020", "Microsoft Flight Simulator", "Microsoft", "Flight", "Simulator", "SIM", "World", "Ortho", "Flying", "Boeing MAX"],
|
||||||
|
related: [.otherFixture],
|
||||||
|
chapters: [
|
||||||
|
.init(title: "A good chapter name", image: chapterImageURL, start: 20),
|
||||||
|
.init(title: "Other fine but incredibly too long chapter name, I don't know what else to say", image: chapterImageURL, start: 30),
|
||||||
|
.init(title: "Short", image: chapterImageURL, start: 60)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static var otherFixture: Video {
|
||||||
|
let bannerURL = "https://yt3.ggpht.com/SQiRareBDrV2Z6A30HSD0iUABOGysanmKLtaJq7lJ_ME-MtoLb3O61QdlJfH2KhSOA0eKPr_=w2560-fcrop64=1,00005a57ffffa5a8-k-c0xffffffff-no-nd-rj"
|
||||||
|
let thumbnailURL = "https://yt3.ggpht.com/ytc/AKedOLR-pT_JEsz_hcaA4Gjx8DHcqJ8mS42aTRqcVy6P7w=s88-c-k-c0x00ffffff-no-rj-mo"
|
||||||
|
let chapterImageURL = URL(string: "https://pipedproxy.kavin.rocks/vi/rr2XfL_df3o/hqdefault_29633.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg%3D%3D&rs=AOn4CLDFDm9D5SvsIA7D3v5n5KZahLs_UA&host=i.ytimg.com")!
|
||||||
|
|
||||||
|
return Video(
|
||||||
|
app: .invidious,
|
||||||
|
videoID: fixtureID + fixtureID,
|
||||||
|
title: "Relaxing Piano Music to feel good",
|
||||||
|
author: "Fancy Videotuber",
|
||||||
|
length: 582,
|
||||||
|
published: "7 years ago",
|
||||||
|
views: 21534,
|
||||||
|
description: "Some relaxing live piano music",
|
||||||
|
genre: "Music",
|
||||||
|
channel: Channel(
|
||||||
|
app: .invidious,
|
||||||
|
id: fixtureChannelID + fixtureChannelID,
|
||||||
|
name: "The Channel",
|
||||||
|
bannerURL: URL(string: bannerURL)!,
|
||||||
|
thumbnailURL: URL(string: thumbnailURL)!,
|
||||||
|
subscriptionsCount: 2300,
|
||||||
|
totalViews: 3_260_378_817,
|
||||||
|
videos: []
|
||||||
|
),
|
||||||
|
thumbnails: [],
|
||||||
|
live: false,
|
||||||
|
upcoming: false,
|
||||||
|
publishedAt: Date(),
|
||||||
|
likes: 37333,
|
||||||
|
dislikes: 30,
|
||||||
|
keywords: ["very", "cool", "video", "msfs 2020", "757", "747", "A380", "737-900", "MOD", "Zibo", "MD80", "MD11", "Rotate", "Laminar", "787", "A350", "MSFS", "MS2020", "Microsoft Flight Simulator", "Microsoft", "Flight", "Simulator", "SIM", "World", "Ortho", "Flying", "Boeing MAX"],
|
||||||
chapters: [
|
chapters: [
|
||||||
.init(title: "A good chapter name", image: chapterImageURL, start: 20),
|
.init(title: "A good chapter name", image: chapterImageURL, start: 20),
|
||||||
.init(title: "Other fine but incredibly too long chapter name, I don't know what else to say", image: chapterImageURL, start: 30),
|
.init(title: "Other fine but incredibly too long chapter name, I don't know what else to say", image: chapterImageURL, start: 30),
|
||||||
|
@ -107,6 +107,8 @@ struct OpenVideosModel {
|
|||||||
prepending: playbackMode == .playNow || playbackMode == .playNext
|
prepending: playbackMode == .playNow || playbackMode == .playNext
|
||||||
)
|
)
|
||||||
|
|
||||||
|
WatchNextViewModel.shared.presentingOutro = false
|
||||||
|
|
||||||
if playbackMode == .playNow || playbackMode == .shuffleAll {
|
if playbackMode == .playNow || playbackMode == .shuffleAll {
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
if player.presentingPlayer {
|
if player.presentingPlayer {
|
||||||
|
@ -94,34 +94,37 @@ extension PlayerBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func eofPlaybackModeAction() {
|
func eofPlaybackModeAction() {
|
||||||
switch model.playbackMode {
|
let timer = Delay.by(5) {
|
||||||
case .queue, .shuffle:
|
switch model.playbackMode {
|
||||||
if Defaults[.closeLastItemOnPlaybackEnd] {
|
case .queue, .shuffle:
|
||||||
model.prepareCurrentItemForHistory(finished: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if model.queue.isEmpty {
|
|
||||||
if Defaults[.closeLastItemOnPlaybackEnd] {
|
if Defaults[.closeLastItemOnPlaybackEnd] {
|
||||||
#if os(tvOS)
|
model.prepareCurrentItemForHistory(finished: true)
|
||||||
if model.activeBackend == .appleAVPlayer {
|
|
||||||
model.avPlayerBackend.controller?.dismiss(animated: false)
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
model.resetQueue()
|
|
||||||
model.hide()
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
model.advanceToNextItem()
|
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()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
model.advanceToNextItem()
|
||||||
|
}
|
||||||
|
case .loopOne:
|
||||||
|
model.backend.seek(to: .zero, seekType: .loopRestart) { _ in
|
||||||
|
self.model.play()
|
||||||
|
}
|
||||||
|
case .related:
|
||||||
|
guard let item = model.autoplayItem else { return }
|
||||||
|
model.resetAutoplay()
|
||||||
|
model.advanceToItem(item)
|
||||||
}
|
}
|
||||||
case .loopOne:
|
|
||||||
model.backend.seek(to: .zero, seekType: .loopRestart) { _ in
|
|
||||||
self.model.play()
|
|
||||||
}
|
|
||||||
case .related:
|
|
||||||
guard let item = model.autoplayItem else { return }
|
|
||||||
model.resetAutoplay()
|
|
||||||
model.advanceToItem(item)
|
|
||||||
}
|
}
|
||||||
|
WatchNextViewModel.shared.prepareForNextItem(model.currentItem, timer: timer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateControls(completionHandler: (() -> Void)? = nil) {
|
func updateControls(completionHandler: (() -> Void)? = nil) {
|
||||||
|
@ -322,6 +322,8 @@ final class PlayerModel: ObservableObject {
|
|||||||
func play(_ video: Video, at time: CMTime? = nil, showingPlayer: Bool = true) {
|
func play(_ video: Video, at time: CMTime? = nil, showingPlayer: Bool = true) {
|
||||||
pause()
|
pause()
|
||||||
|
|
||||||
|
WatchNextViewModel.shared.presentingOutro = false
|
||||||
|
|
||||||
var changeBackendHandler: (() -> Void)?
|
var changeBackendHandler: (() -> Void)?
|
||||||
|
|
||||||
if let backend = (live && forceAVPlayerForLiveStreams) ? PlayerBackendType.appleAVPlayer :
|
if let backend = (live && forceAVPlayerForLiveStreams) ? PlayerBackendType.appleAVPlayer :
|
||||||
@ -569,7 +571,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func closeCurrentItem(finished: Bool = false) {
|
func closeCurrentItem(finished: Bool = false) {
|
||||||
controls.hide()
|
controls.presentingControls = false
|
||||||
pause()
|
pause()
|
||||||
closePiP()
|
closePiP()
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ extension PlayerModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func play(_ videos: [Video], shuffling: Bool = false) {
|
func play(_ videos: [Video], shuffling: Bool = false) {
|
||||||
|
WatchNextViewModel.shared.presentingOutro = false
|
||||||
playbackMode = shuffling ? .shuffle : .queue
|
playbackMode = shuffling ? .shuffle : .queue
|
||||||
|
|
||||||
videos.forEach { enqueueVideo($0, loadDetails: false) }
|
videos.forEach { enqueueVideo($0, loadDetails: false) }
|
||||||
@ -48,7 +49,10 @@ extension PlayerModel {
|
|||||||
|
|
||||||
comments.reset()
|
comments.reset()
|
||||||
stream = nil
|
stream = nil
|
||||||
|
WatchNextViewModel.shared.close()
|
||||||
|
|
||||||
withAnimation {
|
withAnimation {
|
||||||
|
aspectRatio = VideoPlayerView.defaultAspectRatio
|
||||||
currentItem = item
|
currentItem = item
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,6 +167,7 @@ extension PlayerModel {
|
|||||||
|
|
||||||
remove(newItem)
|
remove(newItem)
|
||||||
|
|
||||||
|
WatchNextViewModel.shared.close()
|
||||||
currentItem = newItem
|
currentItem = newItem
|
||||||
currentItem.playbackTime = time
|
currentItem.playbackTime = time
|
||||||
|
|
||||||
@ -207,6 +212,8 @@ extension PlayerModel {
|
|||||||
|
|
||||||
if play {
|
if play {
|
||||||
withAnimation {
|
withAnimation {
|
||||||
|
aspectRatio = VideoPlayerView.defaultAspectRatio
|
||||||
|
WatchNextViewModel.shared.close()
|
||||||
currentItem = item
|
currentItem = item
|
||||||
}
|
}
|
||||||
videoBeingOpened = video
|
videoBeingOpened = video
|
||||||
|
47
Model/WatchNextViewModel.swift
Normal file
47
Model/WatchNextViewModel.swift
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
final class WatchNextViewModel: ObservableObject {
|
||||||
|
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?
|
||||||
|
|
||||||
|
func prepareForEmptyPlayerPlaceholder(_ item: PlayerQueueItem? = nil) {
|
||||||
|
self.item = item
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareForNextItem(_ item: PlayerQueueItem? = nil, timer: Timer? = nil) {
|
||||||
|
self.item = item
|
||||||
|
self.timer?.invalidate()
|
||||||
|
self.timer = timer
|
||||||
|
isAutoplaying = true
|
||||||
|
withAnimation(Self.animation) {
|
||||||
|
presentingOutro = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cancelAutoplay() {
|
||||||
|
timer?.invalidate()
|
||||||
|
isAutoplaying = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func open() {
|
||||||
|
withAnimation(Self.animation) {
|
||||||
|
presentingOutro = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func close() {
|
||||||
|
withAnimation(Self.animation) {
|
||||||
|
presentingOutro = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resetItem() {
|
||||||
|
item = nil
|
||||||
|
}
|
||||||
|
}
|
@ -33,11 +33,6 @@ struct HistoryView: View {
|
|||||||
visibleWatches
|
visibleWatches
|
||||||
.forEach(player.loadHistoryVideoDetails)
|
.forEach(player.loadHistoryVideoDetails)
|
||||||
}
|
}
|
||||||
#if os(tvOS)
|
|
||||||
.padding(.horizontal, 40)
|
|
||||||
#else
|
|
||||||
.padding(.horizontal, 15)
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var visibleWatches: [Watch] {
|
private var visibleWatches: [Watch] {
|
||||||
|
@ -160,6 +160,11 @@ struct HomeView: View {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
HistoryView(limit: homeHistoryItems)
|
HistoryView(limit: homeHistoryItems)
|
||||||
|
#if os(tvOS)
|
||||||
|
.padding(.horizontal, 40)
|
||||||
|
#else
|
||||||
|
.padding(.horizontal, 15)
|
||||||
|
#endif
|
||||||
.id(historyID)
|
.id(historyID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -316,6 +316,9 @@ struct PlayerControls: View {
|
|||||||
|
|
||||||
private var closeVideoButton: some View {
|
private var closeVideoButton: some View {
|
||||||
button("Close", systemImage: "xmark") {
|
button("Close", systemImage: "xmark") {
|
||||||
|
// TODO: Setting
|
||||||
|
// WatchNextViewModel.shared.prepareForEmptyPlayerPlaceholder(player.currentItem)
|
||||||
|
// WatchNextViewModel.shared.open()
|
||||||
player.closeCurrentItem()
|
player.closeCurrentItem()
|
||||||
}
|
}
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
|
@ -4,10 +4,10 @@ import SwiftUI
|
|||||||
struct VideoDetailsOverlay: View {
|
struct VideoDetailsOverlay: View {
|
||||||
@ObservedObject private var controls = PlayerControlsModel.shared
|
@ObservedObject private var controls = PlayerControlsModel.shared
|
||||||
|
|
||||||
@State private var detailsPage = VideoDetails.DetailsPage.queue
|
@State private var detailsPage = VideoDetails.DetailsPage.info
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VideoDetails(page: $detailsPage, sidebarQueue: .constant(false), fullScreen: fullScreenBinding)
|
VideoDetails(video: PlayerModel.shared.currentVideo, page: $detailsPage, sidebarQueue: .constant(false), fullScreen: fullScreenBinding)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 4))
|
.clipShape(RoundedRectangle(cornerRadius: 4))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,6 +63,10 @@ struct VideoActions: View {
|
|||||||
if player.currentItem != nil {
|
if player.currentItem != nil {
|
||||||
Spacer()
|
Spacer()
|
||||||
actionButton("Close", systemImage: "xmark") {
|
actionButton("Close", systemImage: "xmark") {
|
||||||
|
// TODO: setting
|
||||||
|
// player.pause()
|
||||||
|
// WatchNextViewModel.shared.prepareForEmptyPlayerPlaceholder(player.currentItem)
|
||||||
|
// WatchNextViewModel.shared.open()
|
||||||
player.closeCurrentItem()
|
player.closeCurrentItem()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,8 @@ struct VideoDetails: View {
|
|||||||
case info, inspector, chapters, comments, related, queue
|
case info, inspector, chapters, comments, related, queue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var video: Video?
|
||||||
|
|
||||||
@Binding var page: DetailsPage
|
@Binding var page: DetailsPage
|
||||||
@Binding var sidebarQueue: Bool
|
@Binding var sidebarQueue: Bool
|
||||||
@Binding var fullScreen: Bool
|
@Binding var fullScreen: Bool
|
||||||
@ -25,16 +27,12 @@ struct VideoDetails: View {
|
|||||||
|
|
||||||
@ObservedObject private var accounts = AccountsModel.shared
|
@ObservedObject private var accounts = AccountsModel.shared
|
||||||
let comments = CommentsModel.shared
|
let comments = CommentsModel.shared
|
||||||
@ObservedObject private var player = PlayerModel.shared
|
var player = PlayerModel.shared
|
||||||
|
|
||||||
@Default(.enableReturnYouTubeDislike) private var enableReturnYouTubeDislike
|
@Default(.enableReturnYouTubeDislike) private var enableReturnYouTubeDislike
|
||||||
@Default(.detailsToolbarPosition) private var detailsToolbarPosition
|
@Default(.detailsToolbarPosition) private var detailsToolbarPosition
|
||||||
@Default(.playerSidebar) private var playerSidebar
|
@Default(.playerSidebar) private var playerSidebar
|
||||||
|
|
||||||
var video: Video? {
|
|
||||||
player.currentVideo
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
ControlsBar(
|
ControlsBar(
|
||||||
@ -46,13 +44,15 @@ struct VideoDetails: View {
|
|||||||
detailsTogglePlayer: false,
|
detailsTogglePlayer: false,
|
||||||
detailsToggleFullScreen: true
|
detailsToggleFullScreen: true
|
||||||
)
|
)
|
||||||
|
.animation(nil, value: player.currentItem)
|
||||||
|
|
||||||
VideoActions(video: video)
|
VideoActions(video: video)
|
||||||
|
.animation(nil, value: player.currentItem)
|
||||||
|
|
||||||
ZStack(alignment: .bottom) {
|
ZStack(alignment: .bottom) {
|
||||||
currentPage
|
currentPage
|
||||||
.frame(maxWidth: detailsSize.width)
|
.frame(maxWidth: detailsSize.width)
|
||||||
.transition(.fade)
|
.animation(nil, value: player.currentItem)
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
if detailsToolbarPosition.needsLeftSpacer { Spacer() }
|
if detailsToolbarPosition.needsLeftSpacer { Spacer() }
|
||||||
@ -68,26 +68,16 @@ struct VideoDetails: View {
|
|||||||
.offset(y: bottomPadding ? -SafeArea.insets.bottom : 0)
|
.offset(y: bottomPadding ? -SafeArea.insets.bottom : 0)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
.onChange(of: player.currentItem) { newItem in
|
.onChange(of: player.currentItem) { _ in
|
||||||
Delay.by(0.2) {
|
page = .info
|
||||||
guard let newItem else {
|
|
||||||
page = sidebarQueue ? .inspector : .queue
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if let video = newItem.video {
|
|
||||||
page = video.isLocal ? .inspector : .info
|
|
||||||
} else {
|
|
||||||
page = sidebarQueue ? .inspector : .queue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if video.isNil ||
|
if video.isNil ||
|
||||||
!VideoDetailsTool.find(for: page)!.isAvailable(for: video!, sidebarQueue: sidebarQueue)
|
!VideoDetailsTool.find(for: page)!.isAvailable(for: video!, sidebarQueue: sidebarQueue)
|
||||||
{
|
{
|
||||||
page = video == nil ? (sidebarQueue ? .inspector : .queue) : (video!.isLocal ? .inspector : .info)
|
guard let video, video.isLocal else { return }
|
||||||
|
page = .info
|
||||||
}
|
}
|
||||||
|
|
||||||
guard video != nil, accounts.app.supportsSubscriptions else {
|
guard video != nil, accounts.app.supportsSubscriptions else {
|
||||||
@ -146,14 +136,14 @@ struct VideoDetails: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
.frame(maxHeight: .infinity)
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
}
|
}
|
||||||
|
|
||||||
@State private var detailsSize = CGSize.zero
|
@State private var detailsSize = CGSize.zero
|
||||||
|
|
||||||
var detailsPage: some View {
|
var detailsPage: some View {
|
||||||
ScrollView(.vertical, showsIndicators: false) {
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
if let video {
|
if let video, player.videoBeingOpened == nil {
|
||||||
VStack(alignment: .leading, spacing: 10) {
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
videoProperties
|
videoProperties
|
||||||
|
|
||||||
@ -248,7 +238,7 @@ struct VideoDetails: View {
|
|||||||
|
|
||||||
struct VideoDetails_Previews: PreviewProvider {
|
struct VideoDetails_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VideoDetails(page: .constant(.info), sidebarQueue: .constant(true), fullScreen: .constant(false))
|
VideoDetails(video: .fixture, page: .constant(.info), sidebarQueue: .constant(true), fullScreen: .constant(false))
|
||||||
.injectFixtureEnvironmentObjects()
|
.injectFixtureEnvironmentObjects()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -30,7 +30,7 @@ struct VideoDetailsTool: Identifiable {
|
|||||||
}
|
}
|
||||||
switch page {
|
switch page {
|
||||||
case .info:
|
case .info:
|
||||||
return video != nil && !video!.isLocal
|
return true
|
||||||
case .inspector:
|
case .inspector:
|
||||||
return video == nil || Defaults[.showInspector] == .always || video!.isLocal
|
return video == nil || Defaults[.showInspector] == .always || video!.isLocal
|
||||||
case .chapters:
|
case .chapters:
|
||||||
|
@ -142,7 +142,7 @@ struct VideoDetailsToolbar: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var activeToolID: VideoDetailsTool.ID {
|
var activeToolID: VideoDetailsTool.ID {
|
||||||
activeTool?.id ?? "queue"
|
activeTool?.id ?? "info"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -79,6 +79,8 @@ struct VideoPlayerView: View {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
overlay
|
overlay
|
||||||
|
|
||||||
|
WatchNextView()
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if player.musicMode {
|
if player.musicMode {
|
||||||
@ -490,6 +492,8 @@ struct VideoPlayerView: View {
|
|||||||
struct VideoPlayerView_Previews: PreviewProvider {
|
struct VideoPlayerView_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
VideoPlayerView()
|
VideoPlayerView()
|
||||||
.injectFixtureEnvironmentObjects()
|
.onAppear {
|
||||||
|
OutroViewModel.shared.prepareForEmptyPlayerPlaceholder(.init(.fixture))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
172
Shared/Player/WatchNextView.swift
Normal file
172
Shared/Player/WatchNextView.swift
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
import Defaults
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct WatchNextView: View {
|
||||||
|
@ObservedObject private var model = WatchNextViewModel.shared
|
||||||
|
@ObservedObject private var player = PlayerModel.shared
|
||||||
|
|
||||||
|
@Default(.saveHistory) private var saveHistory
|
||||||
|
|
||||||
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
#if os(iOS)
|
||||||
|
NavigationView {
|
||||||
|
watchNext
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
VStack {
|
||||||
|
HStack {
|
||||||
|
closeButton
|
||||||
|
Spacer()
|
||||||
|
reopenButton
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
watchNext
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
#if os(tvOS)
|
||||||
|
.background(Color.background(scheme: colorScheme))
|
||||||
|
#else
|
||||||
|
.background(Color.background)
|
||||||
|
#endif
|
||||||
|
.opacity(model.presentingOutro ? 1 : 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
var watchNext: some View {
|
||||||
|
ScrollView {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
if model.isAutoplaying,
|
||||||
|
let item = nextFromTheQueue
|
||||||
|
{
|
||||||
|
HStack {
|
||||||
|
Text("Playing Next in 5...")
|
||||||
|
.font(.headline)
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button {
|
||||||
|
model.cancelAutoplay()
|
||||||
|
} label: {
|
||||||
|
Label("Cancel", systemImage: "xmark")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PlayerQueueRow(item: item)
|
||||||
|
.padding(.bottom, 10)
|
||||||
|
}
|
||||||
|
moreVideos
|
||||||
|
}
|
||||||
|
.padding(.horizontal)
|
||||||
|
}
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.navigationTitle("Watch Next")
|
||||||
|
#if !os(macOS)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .cancellationAction) {
|
||||||
|
closeButton
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolbarItem(placement: .primaryAction) {
|
||||||
|
reopenButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
var closeButton: some View {
|
||||||
|
Button {
|
||||||
|
player.closeCurrentItem()
|
||||||
|
player.hide(animate: true)
|
||||||
|
Delay.by(0.8) {
|
||||||
|
model.presentingOutro = false
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label("Close", systemImage: "xmark")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder var reopenButton: some View {
|
||||||
|
if player.currentItem != nil, model.item != nil {
|
||||||
|
Button {
|
||||||
|
model.close()
|
||||||
|
} label: {
|
||||||
|
Label("Back to last video", systemImage: "arrow.counterclockwise")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ViewBuilder var moreVideos: some View {
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
let queueForMoreVideos = player.queue.isEmpty ? [] : player.queue.suffix(from: model.isAutoplaying ? 1 : 0)
|
||||||
|
if !queueForMoreVideos.isEmpty {
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
Text("Next in Queue")
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.font(.headline)
|
||||||
|
|
||||||
|
ForEach(queueForMoreVideos) { item in
|
||||||
|
ContentItemView(item: .init(video: item.video))
|
||||||
|
.environment(\.listingStyle, .list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let item = model.item {
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
Text("Related videos")
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.font(.headline)
|
||||||
|
|
||||||
|
ForEach(item.video.related) { video in
|
||||||
|
ContentItemView(item: .init(video: video))
|
||||||
|
.environment(\.listingStyle, .list)
|
||||||
|
}
|
||||||
|
.padding(.bottom, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if saveHistory {
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
Text("History")
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.font(.headline)
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("Playing Next in 5...")
|
||||||
|
.font(.headline)
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Button {
|
||||||
|
model.cancelAutoplay()
|
||||||
|
} label: {
|
||||||
|
Label("Cancel", systemImage: "pause.fill")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HistoryView(limit: 15)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var nextFromTheQueue: PlayerQueueItem? {
|
||||||
|
if player.playbackMode == .related {
|
||||||
|
return player.autoplayItem
|
||||||
|
} else if player.playbackMode == .queue {
|
||||||
|
return player.queue.first
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OutroView_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
WatchNextView()
|
||||||
|
.onAppear {
|
||||||
|
WatchNextViewModel.shared.prepareForNextItem(.init(.fixture))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -223,6 +223,12 @@
|
|||||||
371F2F1A269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; };
|
371F2F1A269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; };
|
||||||
371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; };
|
371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; };
|
||||||
371F2F1C269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; };
|
371F2F1C269B43D300E4A7AB /* NavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371F2F19269B43D300E4A7AB /* NavigationModel.swift */; };
|
||||||
|
37220560294BE2C700E0D176 /* WatchNextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3722055F294BE2C700E0D176 /* WatchNextView.swift */; };
|
||||||
|
37220561294BE2C700E0D176 /* WatchNextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3722055F294BE2C700E0D176 /* WatchNextView.swift */; };
|
||||||
|
37220562294BE2C700E0D176 /* WatchNextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3722055F294BE2C700E0D176 /* WatchNextView.swift */; };
|
||||||
|
37220564294BEB2800E0D176 /* WatchNextViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37220563294BEB2800E0D176 /* WatchNextViewModel.swift */; };
|
||||||
|
37220565294BEB2800E0D176 /* WatchNextViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37220563294BEB2800E0D176 /* WatchNextViewModel.swift */; };
|
||||||
|
37220566294BEB2800E0D176 /* WatchNextViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37220563294BEB2800E0D176 /* WatchNextViewModel.swift */; };
|
||||||
3722AEBC274DA396005EA4D6 /* Badge+Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3722AEBB274DA396005EA4D6 /* Badge+Backport.swift */; };
|
3722AEBC274DA396005EA4D6 /* Badge+Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3722AEBB274DA396005EA4D6 /* Badge+Backport.swift */; };
|
||||||
3722AEBE274DA401005EA4D6 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3722AEBD274DA401005EA4D6 /* Backport.swift */; };
|
3722AEBE274DA401005EA4D6 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3722AEBD274DA401005EA4D6 /* Backport.swift */; };
|
||||||
3726386E2948A4B80043702D /* Badge+Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3722AEBB274DA396005EA4D6 /* Badge+Backport.swift */; };
|
3726386E2948A4B80043702D /* Badge+Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3722AEBB274DA396005EA4D6 /* Badge+Backport.swift */; };
|
||||||
@ -1175,6 +1181,8 @@
|
|||||||
371CC76F29468BDC00979C1A /* SettingsButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsButtons.swift; sourceTree = "<group>"; };
|
371CC76F29468BDC00979C1A /* SettingsButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsButtons.swift; sourceTree = "<group>"; };
|
||||||
371CC7732946963000979C1A /* ListingStyleButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListingStyleButtons.swift; sourceTree = "<group>"; };
|
371CC7732946963000979C1A /* ListingStyleButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListingStyleButtons.swift; sourceTree = "<group>"; };
|
||||||
371F2F19269B43D300E4A7AB /* NavigationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModel.swift; sourceTree = "<group>"; };
|
371F2F19269B43D300E4A7AB /* NavigationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModel.swift; sourceTree = "<group>"; };
|
||||||
|
3722055F294BE2C700E0D176 /* WatchNextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchNextView.swift; sourceTree = "<group>"; };
|
||||||
|
37220563294BEB2800E0D176 /* WatchNextViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchNextViewModel.swift; sourceTree = "<group>"; };
|
||||||
3722AEBB274DA396005EA4D6 /* Badge+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Badge+Backport.swift"; sourceTree = "<group>"; };
|
3722AEBB274DA396005EA4D6 /* Badge+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Badge+Backport.swift"; sourceTree = "<group>"; };
|
||||||
3722AEBD274DA401005EA4D6 /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
|
3722AEBD274DA401005EA4D6 /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
|
||||||
3722AEBF274DAEB8005EA4D6 /* Tint+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tint+Backport.swift"; sourceTree = "<group>"; };
|
3722AEBF274DAEB8005EA4D6 /* Tint+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tint+Backport.swift"; sourceTree = "<group>"; };
|
||||||
@ -1796,6 +1804,7 @@
|
|||||||
374924E629215FB60017D862 /* TapRecognizerViewModifier.swift */,
|
374924E629215FB60017D862 /* TapRecognizerViewModifier.swift */,
|
||||||
37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */,
|
37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */,
|
||||||
37BE0BCE26A0E2D50092E2DB /* VideoPlayerView.swift */,
|
37BE0BCE26A0E2D50092E2DB /* VideoPlayerView.swift */,
|
||||||
|
3722055F294BE2C700E0D176 /* WatchNextView.swift */,
|
||||||
);
|
);
|
||||||
path = Player;
|
path = Player;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -2388,6 +2397,7 @@
|
|||||||
37F5E8B5291BE9D0006C15F5 /* URLBookmarkModel.swift */,
|
37F5E8B5291BE9D0006C15F5 /* URLBookmarkModel.swift */,
|
||||||
37D4B19626717E1500C925CA /* Video.swift */,
|
37D4B19626717E1500C925CA /* Video.swift */,
|
||||||
3784CDDE27772EE40055BBF2 /* Watch.swift */,
|
3784CDDE27772EE40055BBF2 /* Watch.swift */,
|
||||||
|
37220563294BEB2800E0D176 /* WatchNextViewModel.swift */,
|
||||||
37130A59277657090033018A /* Yattee.xcdatamodeld */,
|
37130A59277657090033018A /* Yattee.xcdatamodeld */,
|
||||||
);
|
);
|
||||||
path = Model;
|
path = Model;
|
||||||
@ -3026,6 +3036,7 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
37220564294BEB2800E0D176 /* WatchNextViewModel.swift in Sources */,
|
||||||
37E6D79C2944AE1A00550C3D /* FeedModel.swift in Sources */,
|
37E6D79C2944AE1A00550C3D /* FeedModel.swift in Sources */,
|
||||||
374710052755291C00CE0F87 /* SearchTextField.swift in Sources */,
|
374710052755291C00CE0F87 /* SearchTextField.swift in Sources */,
|
||||||
37494EA529200B14000DF176 /* DocumentsView.swift in Sources */,
|
37494EA529200B14000DF176 /* DocumentsView.swift in Sources */,
|
||||||
@ -3088,6 +3099,7 @@
|
|||||||
37484C1926FC837400287258 /* PlayerSettings.swift in Sources */,
|
37484C1926FC837400287258 /* PlayerSettings.swift in Sources */,
|
||||||
3711403F26B206A6005B3555 /* SearchModel.swift in Sources */,
|
3711403F26B206A6005B3555 /* SearchModel.swift in Sources */,
|
||||||
3729037E2739E47400EA99F6 /* MenuCommands.swift in Sources */,
|
3729037E2739E47400EA99F6 /* MenuCommands.swift in Sources */,
|
||||||
|
37220560294BE2C700E0D176 /* WatchNextView.swift in Sources */,
|
||||||
37F64FE426FE70A60081B69E /* RedrawOnModifier.swift in Sources */,
|
37F64FE426FE70A60081B69E /* RedrawOnModifier.swift in Sources */,
|
||||||
37EBD8C427AF0DA800F1C24B /* PlayerBackend.swift in Sources */,
|
37EBD8C427AF0DA800F1C24B /* PlayerBackend.swift in Sources */,
|
||||||
376A33E02720CAD6000C1D6B /* VideosApp.swift in Sources */,
|
376A33E02720CAD6000C1D6B /* VideosApp.swift in Sources */,
|
||||||
@ -3455,6 +3467,7 @@
|
|||||||
376578862685429C00D4EA09 /* CaseIterable+Next.swift in Sources */,
|
376578862685429C00D4EA09 /* CaseIterable+Next.swift in Sources */,
|
||||||
37A9965F26D6F9B9006E3224 /* HomeView.swift in Sources */,
|
37A9965F26D6F9B9006E3224 /* HomeView.swift in Sources */,
|
||||||
37F4AE7326828F0900BD60EA /* VerticalCells.swift in Sources */,
|
37F4AE7326828F0900BD60EA /* VerticalCells.swift in Sources */,
|
||||||
|
37220561294BE2C700E0D176 /* WatchNextView.swift in Sources */,
|
||||||
37001560271B12DD0049C794 /* SiestaConfiguration.swift in Sources */,
|
37001560271B12DD0049C794 /* SiestaConfiguration.swift in Sources */,
|
||||||
372D85DE283841B800FF3C7D /* PiPDelegate.swift in Sources */,
|
372D85DE283841B800FF3C7D /* PiPDelegate.swift in Sources */,
|
||||||
37F13B63285E43C000B137E4 /* ControlsOverlay.swift in Sources */,
|
37F13B63285E43C000B137E4 /* ControlsOverlay.swift in Sources */,
|
||||||
@ -3546,6 +3559,7 @@
|
|||||||
373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */,
|
||||||
3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
|
3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */,
|
||||||
370015AA28BBAE7F000149FD /* ProgressBar.swift in Sources */,
|
370015AA28BBAE7F000149FD /* ProgressBar.swift in Sources */,
|
||||||
|
37220565294BEB2800E0D176 /* WatchNextViewModel.swift in Sources */,
|
||||||
371B7E672759786B00D21217 /* Comment+Fixtures.swift in Sources */,
|
371B7E672759786B00D21217 /* Comment+Fixtures.swift in Sources */,
|
||||||
3769C02F2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */,
|
3769C02F2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */,
|
||||||
37BA794426DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
37BA794426DBA973002A0235 /* PlaylistsModel.swift in Sources */,
|
||||||
@ -3642,6 +3656,7 @@
|
|||||||
37F4AE7426828F0900BD60EA /* VerticalCells.swift in Sources */,
|
37F4AE7426828F0900BD60EA /* VerticalCells.swift in Sources */,
|
||||||
376578872685429C00D4EA09 /* CaseIterable+Next.swift in Sources */,
|
376578872685429C00D4EA09 /* CaseIterable+Next.swift in Sources */,
|
||||||
37BDFF1D29487C5A000C6404 /* ChannelListItem.swift in Sources */,
|
37BDFF1D29487C5A000C6404 /* ChannelListItem.swift in Sources */,
|
||||||
|
37220562294BE2C700E0D176 /* WatchNextView.swift in Sources */,
|
||||||
37D4B1802671650A00C925CA /* YatteeApp.swift in Sources */,
|
37D4B1802671650A00C925CA /* YatteeApp.swift in Sources */,
|
||||||
3748187026A769D60084E870 /* DetailBadge.swift in Sources */,
|
3748187026A769D60084E870 /* DetailBadge.swift in Sources */,
|
||||||
3741A32C27E7EFFD00D266D1 /* PlayerControls.swift in Sources */,
|
3741A32C27E7EFFD00D266D1 /* PlayerControls.swift in Sources */,
|
||||||
@ -3845,6 +3860,7 @@
|
|||||||
37484C1B26FC837400287258 /* PlayerSettings.swift in Sources */,
|
37484C1B26FC837400287258 /* PlayerSettings.swift in Sources */,
|
||||||
372915E82687E3B900F5A35B /* Defaults.swift in Sources */,
|
372915E82687E3B900F5A35B /* Defaults.swift in Sources */,
|
||||||
37BAB54C269B39FD00E75ED1 /* TVNavigationView.swift in Sources */,
|
37BAB54C269B39FD00E75ED1 /* TVNavigationView.swift in Sources */,
|
||||||
|
37220566294BEB2800E0D176 /* WatchNextViewModel.swift in Sources */,
|
||||||
3718B9A62921A9BE0003DB2E /* PreferenceKeys.swift in Sources */,
|
3718B9A62921A9BE0003DB2E /* PreferenceKeys.swift in Sources */,
|
||||||
3797758D2689345500DD52A8 /* Store.swift in Sources */,
|
3797758D2689345500DD52A8 /* Store.swift in Sources */,
|
||||||
37484C2F26FC844700287258 /* InstanceSettings.swift in Sources */,
|
37484C2F26FC844700287258 /* InstanceSettings.swift in Sources */,
|
||||||
|
Loading…
Reference in New Issue
Block a user