From eca685ae290e5bea2a6f4dcd69faca216e6a0b19 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sun, 18 Dec 2022 00:08:30 +0100 Subject: [PATCH] Watch next view --- Fixtures/Video+Fixtures.swift | 41 +++++ Model/OpenVideosModel.swift | 2 + Model/Player/Backends/PlayerBackend.swift | 51 +++--- Model/Player/PlayerModel.swift | 4 +- Model/Player/PlayerQueue.swift | 7 + Model/WatchNextViewModel.swift | 47 +++++ Shared/Home/HistoryView.swift | 5 - Shared/Home/HomeView.swift | 5 + Shared/Player/Controls/PlayerControls.swift | 3 + .../Player/Controls/VideoDetailsOverlay.swift | 4 +- .../Player/Video Details/VideoActions.swift | 4 + .../Player/Video Details/VideoDetails.swift | 36 ++-- .../Video Details/VideoDetailsTool.swift | 2 +- .../Video Details/VideoDetailsToolbar.swift | 2 +- Shared/Player/VideoPlayerView.swift | 6 +- Shared/Player/WatchNextView.swift | 172 ++++++++++++++++++ Yattee.xcodeproj/project.pbxproj | 16 ++ 17 files changed, 349 insertions(+), 58 deletions(-) create mode 100644 Model/WatchNextViewModel.swift create mode 100644 Shared/Player/WatchNextView.swift diff --git a/Fixtures/Video+Fixtures.swift b/Fixtures/Video+Fixtures.swift index 101cfd94..8a122738 100644 --- a/Fixtures/Video+Fixtures.swift +++ b/Fixtures/Video+Fixtures.swift @@ -37,6 +37,47 @@ extension Video { 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"], + 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: [ .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), diff --git a/Model/OpenVideosModel.swift b/Model/OpenVideosModel.swift index 4a6289bf..fb4f8de4 100644 --- a/Model/OpenVideosModel.swift +++ b/Model/OpenVideosModel.swift @@ -107,6 +107,8 @@ struct OpenVideosModel { prepending: playbackMode == .playNow || playbackMode == .playNext ) + WatchNextViewModel.shared.presentingOutro = false + if playbackMode == .playNow || playbackMode == .shuffleAll { #if os(iOS) if player.presentingPlayer { diff --git a/Model/Player/Backends/PlayerBackend.swift b/Model/Player/Backends/PlayerBackend.swift index d8771770..64c40449 100644 --- a/Model/Player/Backends/PlayerBackend.swift +++ b/Model/Player/Backends/PlayerBackend.swift @@ -94,34 +94,37 @@ extension PlayerBackend { } func eofPlaybackModeAction() { - switch model.playbackMode { - case .queue, .shuffle: - if Defaults[.closeLastItemOnPlaybackEnd] { - model.prepareCurrentItemForHistory(finished: true) - } - - if model.queue.isEmpty { + let timer = Delay.by(5) { + switch model.playbackMode { + case .queue, .shuffle: if Defaults[.closeLastItemOnPlaybackEnd] { - #if os(tvOS) - if model.activeBackend == .appleAVPlayer { - model.avPlayerBackend.controller?.dismiss(animated: false) - } - #endif - model.resetQueue() - model.hide() + model.prepareCurrentItemForHistory(finished: true) } - } 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) { diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index 6ee2db54..6d4e5012 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -322,6 +322,8 @@ final class PlayerModel: ObservableObject { func play(_ video: Video, at time: CMTime? = nil, showingPlayer: Bool = true) { pause() + WatchNextViewModel.shared.presentingOutro = false + var changeBackendHandler: (() -> Void)? if let backend = (live && forceAVPlayerForLiveStreams) ? PlayerBackendType.appleAVPlayer : @@ -569,7 +571,7 @@ final class PlayerModel: ObservableObject { } func closeCurrentItem(finished: Bool = false) { - controls.hide() + controls.presentingControls = false pause() closePiP() diff --git a/Model/Player/PlayerQueue.swift b/Model/Player/PlayerQueue.swift index c3186186..a3683d26 100644 --- a/Model/Player/PlayerQueue.swift +++ b/Model/Player/PlayerQueue.swift @@ -10,6 +10,7 @@ extension PlayerModel { } func play(_ videos: [Video], shuffling: Bool = false) { + WatchNextViewModel.shared.presentingOutro = false playbackMode = shuffling ? .shuffle : .queue videos.forEach { enqueueVideo($0, loadDetails: false) } @@ -48,7 +49,10 @@ extension PlayerModel { comments.reset() stream = nil + WatchNextViewModel.shared.close() + withAnimation { + aspectRatio = VideoPlayerView.defaultAspectRatio currentItem = item } @@ -163,6 +167,7 @@ extension PlayerModel { remove(newItem) + WatchNextViewModel.shared.close() currentItem = newItem currentItem.playbackTime = time @@ -207,6 +212,8 @@ extension PlayerModel { if play { withAnimation { + aspectRatio = VideoPlayerView.defaultAspectRatio + WatchNextViewModel.shared.close() currentItem = item } videoBeingOpened = video diff --git a/Model/WatchNextViewModel.swift b/Model/WatchNextViewModel.swift new file mode 100644 index 00000000..f7e1441d --- /dev/null +++ b/Model/WatchNextViewModel.swift @@ -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 + } +} diff --git a/Shared/Home/HistoryView.swift b/Shared/Home/HistoryView.swift index d9f4fb1e..6343f0a5 100644 --- a/Shared/Home/HistoryView.swift +++ b/Shared/Home/HistoryView.swift @@ -33,11 +33,6 @@ struct HistoryView: View { visibleWatches .forEach(player.loadHistoryVideoDetails) } - #if os(tvOS) - .padding(.horizontal, 40) - #else - .padding(.horizontal, 15) - #endif } private var visibleWatches: [Watch] { diff --git a/Shared/Home/HomeView.swift b/Shared/Home/HomeView.swift index eba3cf85..c804087a 100644 --- a/Shared/Home/HomeView.swift +++ b/Shared/Home/HomeView.swift @@ -160,6 +160,11 @@ struct HomeView: View { #endif HistoryView(limit: homeHistoryItems) + #if os(tvOS) + .padding(.horizontal, 40) + #else + .padding(.horizontal, 15) + #endif .id(historyID) } } diff --git a/Shared/Player/Controls/PlayerControls.swift b/Shared/Player/Controls/PlayerControls.swift index 189c33fb..4fc6aabd 100644 --- a/Shared/Player/Controls/PlayerControls.swift +++ b/Shared/Player/Controls/PlayerControls.swift @@ -316,6 +316,9 @@ struct PlayerControls: View { private var closeVideoButton: some View { button("Close", systemImage: "xmark") { +// TODO: Setting +// WatchNextViewModel.shared.prepareForEmptyPlayerPlaceholder(player.currentItem) +// WatchNextViewModel.shared.open() player.closeCurrentItem() } #if os(tvOS) diff --git a/Shared/Player/Controls/VideoDetailsOverlay.swift b/Shared/Player/Controls/VideoDetailsOverlay.swift index ee7c8661..0243b3c8 100644 --- a/Shared/Player/Controls/VideoDetailsOverlay.swift +++ b/Shared/Player/Controls/VideoDetailsOverlay.swift @@ -4,10 +4,10 @@ import SwiftUI struct VideoDetailsOverlay: View { @ObservedObject private var controls = PlayerControlsModel.shared - @State private var detailsPage = VideoDetails.DetailsPage.queue + @State private var detailsPage = VideoDetails.DetailsPage.info 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)) } diff --git a/Shared/Player/Video Details/VideoActions.swift b/Shared/Player/Video Details/VideoActions.swift index caf21493..35bf4402 100644 --- a/Shared/Player/Video Details/VideoActions.swift +++ b/Shared/Player/Video Details/VideoActions.swift @@ -63,6 +63,10 @@ struct VideoActions: View { if player.currentItem != nil { Spacer() actionButton("Close", systemImage: "xmark") { +// TODO: setting +// player.pause() +// WatchNextViewModel.shared.prepareForEmptyPlayerPlaceholder(player.currentItem) +// WatchNextViewModel.shared.open() player.closeCurrentItem() } } diff --git a/Shared/Player/Video Details/VideoDetails.swift b/Shared/Player/Video Details/VideoDetails.swift index 05dc9e07..9b8f1c5d 100644 --- a/Shared/Player/Video Details/VideoDetails.swift +++ b/Shared/Player/Video Details/VideoDetails.swift @@ -8,6 +8,8 @@ struct VideoDetails: View { case info, inspector, chapters, comments, related, queue } + var video: Video? + @Binding var page: DetailsPage @Binding var sidebarQueue: Bool @Binding var fullScreen: Bool @@ -25,16 +27,12 @@ struct VideoDetails: View { @ObservedObject private var accounts = AccountsModel.shared let comments = CommentsModel.shared - @ObservedObject private var player = PlayerModel.shared + var player = PlayerModel.shared @Default(.enableReturnYouTubeDislike) private var enableReturnYouTubeDislike @Default(.detailsToolbarPosition) private var detailsToolbarPosition @Default(.playerSidebar) private var playerSidebar - var video: Video? { - player.currentVideo - } - var body: some View { VStack(alignment: .leading, spacing: 0) { ControlsBar( @@ -46,13 +44,15 @@ struct VideoDetails: View { detailsTogglePlayer: false, detailsToggleFullScreen: true ) + .animation(nil, value: player.currentItem) VideoActions(video: video) + .animation(nil, value: player.currentItem) ZStack(alignment: .bottom) { currentPage .frame(maxWidth: detailsSize.width) - .transition(.fade) + .animation(nil, value: player.currentItem) HStack { if detailsToolbarPosition.needsLeftSpacer { Spacer() } @@ -68,26 +68,16 @@ struct VideoDetails: View { .offset(y: bottomPadding ? -SafeArea.insets.bottom : 0) #endif } - .onChange(of: player.currentItem) { newItem in - Delay.by(0.2) { - guard let newItem else { - page = sidebarQueue ? .inspector : .queue - return - } - - if let video = newItem.video { - page = video.isLocal ? .inspector : .info - } else { - page = sidebarQueue ? .inspector : .queue - } - } + .onChange(of: player.currentItem) { _ in + page = .info } } .onAppear { if video.isNil || !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 { @@ -146,14 +136,14 @@ struct VideoDetails: View { } } .contentShape(Rectangle()) - .frame(maxHeight: .infinity) + .frame(maxWidth: .infinity, maxHeight: .infinity) } @State private var detailsSize = CGSize.zero var detailsPage: some View { ScrollView(.vertical, showsIndicators: false) { - if let video { + if let video, player.videoBeingOpened == nil { VStack(alignment: .leading, spacing: 10) { videoProperties @@ -248,7 +238,7 @@ struct VideoDetails: View { struct VideoDetails_Previews: PreviewProvider { 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() } } diff --git a/Shared/Player/Video Details/VideoDetailsTool.swift b/Shared/Player/Video Details/VideoDetailsTool.swift index 6ec045cc..0d8dd2c4 100644 --- a/Shared/Player/Video Details/VideoDetailsTool.swift +++ b/Shared/Player/Video Details/VideoDetailsTool.swift @@ -30,7 +30,7 @@ struct VideoDetailsTool: Identifiable { } switch page { case .info: - return video != nil && !video!.isLocal + return true case .inspector: return video == nil || Defaults[.showInspector] == .always || video!.isLocal case .chapters: diff --git a/Shared/Player/Video Details/VideoDetailsToolbar.swift b/Shared/Player/Video Details/VideoDetailsToolbar.swift index cef209e8..152942e2 100644 --- a/Shared/Player/Video Details/VideoDetailsToolbar.swift +++ b/Shared/Player/Video Details/VideoDetailsToolbar.swift @@ -142,7 +142,7 @@ struct VideoDetailsToolbar: View { } var activeToolID: VideoDetailsTool.ID { - activeTool?.id ?? "queue" + activeTool?.id ?? "info" } } diff --git a/Shared/Player/VideoPlayerView.swift b/Shared/Player/VideoPlayerView.swift index 3045d732..7039c8aa 100644 --- a/Shared/Player/VideoPlayerView.swift +++ b/Shared/Player/VideoPlayerView.swift @@ -79,6 +79,8 @@ struct VideoPlayerView: View { #endif overlay + + WatchNextView() } .onAppear { if player.musicMode { @@ -490,6 +492,8 @@ struct VideoPlayerView: View { struct VideoPlayerView_Previews: PreviewProvider { static var previews: some View { VideoPlayerView() - .injectFixtureEnvironmentObjects() + .onAppear { + OutroViewModel.shared.prepareForEmptyPlayerPlaceholder(.init(.fixture)) + } } } diff --git a/Shared/Player/WatchNextView.swift b/Shared/Player/WatchNextView.swift new file mode 100644 index 00000000..e9ce2f6c --- /dev/null +++ b/Shared/Player/WatchNextView.swift @@ -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)) + } + } +} diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index 470181f7..11c65f7f 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -223,6 +223,12 @@ 371F2F1A269B43D300E4A7AB /* 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 */; }; + 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 */; }; 3722AEBE274DA401005EA4D6 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3722AEBD274DA401005EA4D6 /* 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 = ""; }; 371CC7732946963000979C1A /* ListingStyleButtons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListingStyleButtons.swift; sourceTree = ""; }; 371F2F19269B43D300E4A7AB /* NavigationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationModel.swift; sourceTree = ""; }; + 3722055F294BE2C700E0D176 /* WatchNextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchNextView.swift; sourceTree = ""; }; + 37220563294BEB2800E0D176 /* WatchNextViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchNextViewModel.swift; sourceTree = ""; }; 3722AEBB274DA396005EA4D6 /* Badge+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Badge+Backport.swift"; sourceTree = ""; }; 3722AEBD274DA401005EA4D6 /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = ""; }; 3722AEBF274DAEB8005EA4D6 /* Tint+Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tint+Backport.swift"; sourceTree = ""; }; @@ -1796,6 +1804,7 @@ 374924E629215FB60017D862 /* TapRecognizerViewModifier.swift */, 37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */, 37BE0BCE26A0E2D50092E2DB /* VideoPlayerView.swift */, + 3722055F294BE2C700E0D176 /* WatchNextView.swift */, ); path = Player; sourceTree = ""; @@ -2388,6 +2397,7 @@ 37F5E8B5291BE9D0006C15F5 /* URLBookmarkModel.swift */, 37D4B19626717E1500C925CA /* Video.swift */, 3784CDDE27772EE40055BBF2 /* Watch.swift */, + 37220563294BEB2800E0D176 /* WatchNextViewModel.swift */, 37130A59277657090033018A /* Yattee.xcdatamodeld */, ); path = Model; @@ -3026,6 +3036,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 37220564294BEB2800E0D176 /* WatchNextViewModel.swift in Sources */, 37E6D79C2944AE1A00550C3D /* FeedModel.swift in Sources */, 374710052755291C00CE0F87 /* SearchTextField.swift in Sources */, 37494EA529200B14000DF176 /* DocumentsView.swift in Sources */, @@ -3088,6 +3099,7 @@ 37484C1926FC837400287258 /* PlayerSettings.swift in Sources */, 3711403F26B206A6005B3555 /* SearchModel.swift in Sources */, 3729037E2739E47400EA99F6 /* MenuCommands.swift in Sources */, + 37220560294BE2C700E0D176 /* WatchNextView.swift in Sources */, 37F64FE426FE70A60081B69E /* RedrawOnModifier.swift in Sources */, 37EBD8C427AF0DA800F1C24B /* PlayerBackend.swift in Sources */, 376A33E02720CAD6000C1D6B /* VideosApp.swift in Sources */, @@ -3455,6 +3467,7 @@ 376578862685429C00D4EA09 /* CaseIterable+Next.swift in Sources */, 37A9965F26D6F9B9006E3224 /* HomeView.swift in Sources */, 37F4AE7326828F0900BD60EA /* VerticalCells.swift in Sources */, + 37220561294BE2C700E0D176 /* WatchNextView.swift in Sources */, 37001560271B12DD0049C794 /* SiestaConfiguration.swift in Sources */, 372D85DE283841B800FF3C7D /* PiPDelegate.swift in Sources */, 37F13B63285E43C000B137E4 /* ControlsOverlay.swift in Sources */, @@ -3546,6 +3559,7 @@ 373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */, 3763495226DFF59D00B9A393 /* AppSidebarRecents.swift in Sources */, 370015AA28BBAE7F000149FD /* ProgressBar.swift in Sources */, + 37220565294BEB2800E0D176 /* WatchNextViewModel.swift in Sources */, 371B7E672759786B00D21217 /* Comment+Fixtures.swift in Sources */, 3769C02F2779F18600DDB3EA /* PlaceholderProgressView.swift in Sources */, 37BA794426DBA973002A0235 /* PlaylistsModel.swift in Sources */, @@ -3642,6 +3656,7 @@ 37F4AE7426828F0900BD60EA /* VerticalCells.swift in Sources */, 376578872685429C00D4EA09 /* CaseIterable+Next.swift in Sources */, 37BDFF1D29487C5A000C6404 /* ChannelListItem.swift in Sources */, + 37220562294BE2C700E0D176 /* WatchNextView.swift in Sources */, 37D4B1802671650A00C925CA /* YatteeApp.swift in Sources */, 3748187026A769D60084E870 /* DetailBadge.swift in Sources */, 3741A32C27E7EFFD00D266D1 /* PlayerControls.swift in Sources */, @@ -3845,6 +3860,7 @@ 37484C1B26FC837400287258 /* PlayerSettings.swift in Sources */, 372915E82687E3B900F5A35B /* Defaults.swift in Sources */, 37BAB54C269B39FD00E75ED1 /* TVNavigationView.swift in Sources */, + 37220566294BEB2800E0D176 /* WatchNextViewModel.swift in Sources */, 3718B9A62921A9BE0003DB2E /* PreferenceKeys.swift in Sources */, 3797758D2689345500DD52A8 /* Store.swift in Sources */, 37484C2F26FC844700287258 /* InstanceSettings.swift in Sources */,