From e54d3b811ed7d5d8b3847a35f9d570ab172a8840 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Sat, 20 Aug 2022 23:05:40 +0200 Subject: [PATCH] Bring AVPlayer back to tvOS --- Fixtures/View+Fixtures.swift | 14 +- Model/Player/Backends/MPVClient.swift | 4 +- Model/Player/PlayerTVMenu.swift | 12 +- Model/Stream.swift | 4 +- Shared/Defaults.swift | 2 +- Shared/Player/AppleAVPlayerView.swift | 46 +++++-- .../Player/AppleAVPlayerViewController.swift | 120 +++++++----------- Shared/Player/ChapterView.swift | 63 +++++++++ Shared/Player/ChaptersView.swift | 51 +------- Shared/Player/Controls/ControlsOverlay.swift | 27 ++-- Shared/Player/Controls/PlayerControls.swift | 15 ++- Shared/Player/StreamControl.swift | 8 +- Shared/Player/VideoPlayerView.swift | 4 +- Shared/Settings/QualityProfileForm.swift | 7 +- Yattee.xcodeproj/project.pbxproj | 26 ++-- tvOS/NowPlayingView.swift | 39 +++--- 16 files changed, 245 insertions(+), 197 deletions(-) create mode 100644 Shared/Player/ChapterView.swift diff --git a/Fixtures/View+Fixtures.swift b/Fixtures/View+Fixtures.swift index 8f717ef0..d9d0b26f 100644 --- a/Fixtures/View+Fixtures.swift +++ b/Fixtures/View+Fixtures.swift @@ -55,7 +55,19 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier { channel: .init(id: "", name: "Channel Name"), likes: 2332, dislikes: 30, - keywords: ["Video", "Computer", "Long Long Keyword"] + keywords: ["Video", "Computer", "Long Long Keyword"], + chapters: [ + .init( + title: "Abc", + image: URL(string: "https://pipedproxy.kavin.rocks/vi/rr2XfL_df3o/hqdefault_29633.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg%3D%3D&rs=AOn4CLDFDm9D5SvsIA7D3v5n5KZahLs_UA&host=i.ytimg.com")!, + start: 3 + ), + .init( + title: "Def", + image: URL(string: "https://pipedproxy.kavin.rocks/vi/rr2XfL_df3o/hqdefault_98900.jpg?sqp=-oaymwEcCNACELwBSFXyq4qpAw4IARUAAIhCGAFwAcABBg%3D%3D&rs=AOn4CLCfjXJBJb2O2q0jT0RHIi7hARVahw&host=i.ytimg.com")!, + start: 33 + ) + ] ) ) #if os(iOS) diff --git a/Model/Player/Backends/MPVClient.swift b/Model/Player/Backends/MPVClient.swift index f81bf305..1c75fd98 100644 --- a/Model/Player/Backends/MPVClient.swift +++ b/Model/Player/Backends/MPVClient.swift @@ -142,7 +142,9 @@ final class MPVClient: ObservableObject { options.append("force-seekable=yes") - args.append(options.joined(separator: ",")) + if !options.isEmpty { + args.append(options.joined(separator: ",")) + } command("loadfile", args: args, returnValueCallback: completionHandler) } diff --git a/Model/Player/PlayerTVMenu.swift b/Model/Player/PlayerTVMenu.swift index 97535b34..ce2ac2e4 100644 --- a/Model/Player/PlayerTVMenu.swift +++ b/Model/Player/PlayerTVMenu.swift @@ -20,7 +20,7 @@ extension PlayerModel { ] } - return availableStreamsSorted.map { stream in + return availableStreamsSorted.filter { backend.canPlay($0) }.map { stream in let state = stream == streamSelection ? UIAction.State.on : .off return UIAction(title: stream.description, state: state) { _ in @@ -43,6 +43,13 @@ extension PlayerModel { } } + var switchToMPVAction: UIAction? { + UIAction(title: "Switch to MPV", image: UIImage(systemName: "m.circle")) { _ in + self.avPlayerBackend.controller?.dismiss(animated: false) + self.changeActiveBackend(from: .appleAVPlayer, to: .mpv) + } + } + private var rateMenu: UIMenu { UIMenu(title: "Playback rate", image: UIImage(systemName: rateMenuSystemImage), children: rateMenuActions) } @@ -69,7 +76,8 @@ extension PlayerModel { avPlayerBackend.controller?.playerView.transportBarCustomMenuItems = [ restoreLastSkippedSegmentAction, rateMenu, - streamsMenu + streamsMenu, + switchToMPVAction ].compactMap { $0 } #endif } diff --git a/Model/Stream.swift b/Model/Stream.swift index c15df422..507e9b36 100644 --- a/Model/Stream.swift +++ b/Model/Stream.swift @@ -136,8 +136,8 @@ class Stream: Equatable, Hashable, Identifiable { var kind: Kind! var format: Format! - var encoding: String! - var videoFormat: String! + var encoding: String? + var videoFormat: String? init( instance: Instance? = nil, diff --git a/Shared/Defaults.swift b/Shared/Defaults.swift index 62f35c26..746e8042 100644 --- a/Shared/Defaults.swift +++ b/Shared/Defaults.swift @@ -65,7 +65,7 @@ extension Defaults.Keys { static let qualityProfilesDefault = [ hd2160pMPVProfile, hd1080pMPVProfile, - hd720pMPVProfile + hd720pAVPlayerProfile ] static let batteryCellularProfileDefault = hd1080pMPVProfile.id static let batteryNonCellularProfileDefault = hd1080pMPVProfile.id diff --git a/Shared/Player/AppleAVPlayerView.swift b/Shared/Player/AppleAVPlayerView.swift index 8a112bc8..a9519479 100644 --- a/Shared/Player/AppleAVPlayerView.swift +++ b/Shared/Player/AppleAVPlayerView.swift @@ -2,14 +2,44 @@ import AVKit import Defaults import SwiftUI -struct AppleAVPlayerView: UIViewRepresentable { - @EnvironmentObject private var player +#if os(iOS) + struct AppleAVPlayerView: UIViewRepresentable { + @EnvironmentObject private var player - func makeUIView(context _: Context) -> some UIView { - let playerLayerView = PlayerLayerView(frame: .zero) - playerLayerView.player = player - return playerLayerView + func makeUIView(context _: Context) -> some UIView { + let playerLayerView = PlayerLayerView(frame: .zero) + playerLayerView.player = player + return playerLayerView + } + + func updateUIView(_: UIViewType, context _: Context) {} } +#else + struct AppleAVPlayerView: UIViewControllerRepresentable { + @EnvironmentObject private var accounts + @EnvironmentObject private var comments + @EnvironmentObject private var navigation + @EnvironmentObject private var player + @EnvironmentObject private var playlists + @EnvironmentObject private var subscriptions - func updateUIView(_: UIViewType, context _: Context) {} -} + func makeUIViewController(context _: Context) -> AppleAVPlayerViewController { + let controller = AppleAVPlayerViewController() + + controller.accountsModel = accounts + controller.commentsModel = comments + controller.navigationModel = navigation + controller.playerModel = player + controller.playlistsModel = playlists + controller.subscriptionsModel = subscriptions + + player.avPlayerBackend.controller = controller + + return controller + } + + func updateUIViewController(_: AppleAVPlayerViewController, context _: Context) { + player.rebuildTVMenu() + } + } +#endif diff --git a/Shared/Player/AppleAVPlayerViewController.swift b/Shared/Player/AppleAVPlayerViewController.swift index b528d3db..37c20e8c 100644 --- a/Shared/Player/AppleAVPlayerViewController.swift +++ b/Shared/Player/AppleAVPlayerViewController.swift @@ -4,125 +4,93 @@ import SwiftUI final class AppleAVPlayerViewController: UIViewController { var playerLoaded = false + var accountsModel: AccountsModel! var commentsModel: CommentsModel! var navigationModel: NavigationModel! var playerModel: PlayerModel! + var playlistsModel: PlaylistsModel! var subscriptionsModel: SubscriptionsModel! var playerView = AVPlayerViewController() let persistenceController = PersistenceController.shared - #if os(iOS) - var aspectRatio: Double? { - let ratio = Double(playerView.videoBounds.width) / Double(playerView.videoBounds.height) - - guard ratio.isFinite else { - return VideoPlayerView.defaultAspectRatio // swiftlint:disable:this implicit_return - } - - return [ratio, 1.0].max()! - } - #endif - override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) loadPlayer() - #if os(tvOS) - if !playerView.isBeingPresented, !playerView.isBeingDismissed { - present(playerView, animated: false) - } - #endif + if playerModel.presentingPlayer, !playerView.isBeingPresented, !playerView.isBeingDismissed { + present(playerView, animated: false) + } } - #if os(tvOS) - override func viewDidDisappear(_ animated: Bool) { - super.viewDidDisappear(animated) + override func viewDidDisappear(_ animated: Bool) { + super.viewDidDisappear(animated) - if !playerModel.presentingPlayer, !Defaults[.pauseOnHidingPlayer], !playerModel.isPlaying { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in - self?.playerModel.play() - } + if !playerModel.presentingPlayer, !Defaults[.pauseOnHidingPlayer], !playerModel.isPlaying { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in + self?.playerModel.play() } } - #endif + } func loadPlayer() { guard !playerLoaded else { return } - playerModel.avPlayerBackend.controller = self playerView.player = playerModel.avPlayerBackend.avPlayer playerView.allowsPictureInPicturePlayback = true - playerView.showsPlaybackControls = false - #if os(iOS) - if #available(iOS 14.2, *) { - playerView.canStartPictureInPictureAutomaticallyFromInline = true - } - #endif + playerView.showsPlaybackControls = true playerView.delegate = self - #if os(tvOS) - var infoViewControllers = [UIHostingController]() - infoViewControllers.append(infoViewController([.comments], title: "Comments")) + var infoViewControllers = [UIHostingController]() + infoViewControllers.append(infoViewController([.chapters], title: "Chapters")) + infoViewControllers.append(infoViewController([.comments], title: "Comments")) - var queueSections = [NowPlayingView.ViewSection.playingNext] - if Defaults[.showHistoryInPlayer] { - queueSections.append(.playedPreviously) - } + var queueSections = [NowPlayingView.ViewSection.playingNext] + if Defaults[.showHistoryInPlayer] { + queueSections.append(.playedPreviously) + } - infoViewControllers.append(contentsOf: [ - infoViewController([.related], title: "Related"), - infoViewController(queueSections, title: "Queue") - ]) + infoViewControllers.append(contentsOf: [ + infoViewController([.related], title: "Related"), + infoViewController(queueSections, title: "Queue") + ]) - playerView.customInfoViewControllers = infoViewControllers - #else - embedViewController() - #endif + playerView.customInfoViewControllers = infoViewControllers } - #if os(tvOS) - func infoViewController( - _ sections: [NowPlayingView.ViewSection], - title: String - ) -> UIHostingController { - let controller = UIHostingController(rootView: - AnyView( - NowPlayingView(sections: sections, inInfoViewController: true) - .frame(maxHeight: 600) - .environmentObject(commentsModel) - .environmentObject(playerModel) - .environmentObject(subscriptionsModel) - .environment(\.managedObjectContext, persistenceController.container.viewContext) - ) + func infoViewController( + _ sections: [NowPlayingView.ViewSection], + title: String + ) -> UIHostingController { + let controller = UIHostingController(rootView: + AnyView( + NowPlayingView(sections: sections, inInfoViewController: true) + .frame(maxHeight: 600) + .environmentObject(accountsModel) + .environmentObject(commentsModel) + .environmentObject(playerModel) + .environmentObject(playlistsModel) + .environmentObject(subscriptionsModel) + .environment(\.managedObjectContext, persistenceController.container.viewContext) ) + ) - controller.title = title + controller.title = title - return controller - } - #else - func embedViewController() { - playerView.view.frame = view.bounds - - addChild(playerView) - view.addSubview(playerView.view) - - playerView.didMove(toParent: self) - } - #endif + return controller + } } extension AppleAVPlayerViewController: AVPlayerViewControllerDelegate { func playerViewControllerShouldDismiss(_: AVPlayerViewController) -> Bool { - false + true } func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_: AVPlayerViewController) -> Bool { - false + true } func playerViewControllerWillBeginDismissalTransition(_: AVPlayerViewController) { diff --git a/Shared/Player/ChapterView.swift b/Shared/Player/ChapterView.swift new file mode 100644 index 00000000..9e49a54c --- /dev/null +++ b/Shared/Player/ChapterView.swift @@ -0,0 +1,63 @@ +import Foundation +import SDWebImageSwiftUI +import SwiftUI + +struct ChapterView: View { + var chapter: Chapter + + @EnvironmentObject private var player + + var body: some View { + Button { + player.backend.seek(to: chapter.start) + } label: { + HStack(spacing: 12) { + if !chapter.image.isNil { + smallImage(chapter) + } + + VStack(alignment: .leading, spacing: 4) { + Text(chapter.title) + .font(.headline) + Text(chapter.start.formattedAsPlaybackTime(allowZero: true) ?? "") + .font(.system(.subheadline).monospacedDigit()) + .foregroundColor(.secondary) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + + @ViewBuilder func smallImage(_ chapter: Chapter) -> some View { + WebImage(url: chapter.image) + .resizable() + .placeholder { + ProgressView() + } + .indicator(.activity) + #if os(tvOS) + .frame(width: thumbnailWidth, height: 140) + .mask(RoundedRectangle(cornerRadius: 12)) + #else + .frame(width: thumbnailWidth, height: 60) + .mask(RoundedRectangle(cornerRadius: 6)) + #endif + } + + private var thumbnailWidth: Double { + #if os(tvOS) + 250 + #else + 100 + #endif + } +} + +struct ChapterView_Preview: PreviewProvider { + static var previews: some View { + ChapterView(chapter: .init(title: "Chapter", start: 30)) + .injectFixtureEnvironmentObjects() + } +} diff --git a/Shared/Player/ChaptersView.swift b/Shared/Player/ChaptersView.swift index e0d01a0d..d7fbe067 100644 --- a/Shared/Player/ChaptersView.swift +++ b/Shared/Player/ChaptersView.swift @@ -10,12 +10,7 @@ struct ChaptersView: View { List { Section(header: Text("Chapters")) { ForEach(chapters) { chapter in - Button { - player.backend.seek(to: chapter.start) - } label: { - chapterButtonLabel(chapter) - } - .buttonStyle(.plain) + ChapterView(chapter: chapter) } } .listRowBackground(Color.clear) @@ -33,51 +28,9 @@ struct ChaptersView: View { NoCommentsView(text: "No chapters information available", systemImage: "xmark.circle.fill") } } - - @ViewBuilder func chapterButtonLabel(_ chapter: Chapter) -> some View { - HStack(spacing: 12) { - if !chapter.image.isNil { - smallImage(chapter) - } - - VStack(alignment: .leading, spacing: 4) { - Text(chapter.title) - .font(.headline) - Text(chapter.start.formattedAsPlaybackTime(allowZero: true) ?? "") - .font(.system(.subheadline).monospacedDigit()) - .foregroundColor(.secondary) - } - } - .frame(maxWidth: .infinity, alignment: .leading) - .contentShape(Rectangle()) - } - - @ViewBuilder func smallImage(_ chapter: Chapter) -> some View { - WebImage(url: chapter.image) - .resizable() - .placeholder { - ProgressView() - } - .indicator(.activity) - #if os(tvOS) - .frame(width: thumbnailWidth, height: 140) - .mask(RoundedRectangle(cornerRadius: 12)) - #else - .frame(width: thumbnailWidth, height: 60) - .mask(RoundedRectangle(cornerRadius: 6)) - #endif - } - - private var thumbnailWidth: Double { - #if os(tvOS) - 250 - #else - 100 - #endif - } } -struct ChaptersView_Preview: PreviewProvider { +struct ChaptersView_Previews: PreviewProvider { static var previews: some View { ChaptersView() .injectFixtureEnvironmentObjects() diff --git a/Shared/Player/Controls/ControlsOverlay.swift b/Shared/Player/Controls/ControlsOverlay.swift index e565ae02..e1428f61 100644 --- a/Shared/Player/Controls/ControlsOverlay.swift +++ b/Shared/Player/Controls/ControlsOverlay.swift @@ -58,23 +58,15 @@ struct ControlsOverlay: View { #endif } - #if os(tvOS) - let streamAndPlayerHeaderText = "Stream" - #else - let streamAndPlayerHeaderText = "Stream & Player" - #endif - - Section(header: controlsHeader(streamAndPlayerHeaderText)) { + Section(header: controlsHeader("Stream & Player")) { qualityButton #if os(tvOS) .focused($focusedField, equals: .stream) #endif - #if !os(tvOS) - HStack { - backendButtons - } - #endif + HStack(spacing: 8) { + backendButtons + } } if player.activeBackend == .mpv, @@ -129,11 +121,13 @@ struct ControlsOverlay: View { private var backendButtons: some View { ForEach(PlayerBackendType.allCases, id: \.self) { backend in backendButton(backend) + #if !os(tvOS) .frame(height: 40) + #endif #if os(iOS) - .frame(maxWidth: 115) - .modifier(ControlBackgroundModifier()) - .clipShape(RoundedRectangle(cornerRadius: 4)) + .frame(maxWidth: 115) + .modifier(ControlBackgroundModifier()) + .clipShape(RoundedRectangle(cornerRadius: 4)) #endif } } @@ -150,9 +144,6 @@ struct ControlsOverlay: View { } #if os(macOS) .buttonStyle(.bordered) - #elseif os(tvOS) - .modifier(ControlBackgroundModifier()) - .clipShape(RoundedRectangle(cornerRadius: 4)) #endif } diff --git a/Shared/Player/Controls/PlayerControls.swift b/Shared/Player/Controls/PlayerControls.swift index 3baedcf5..710b17b3 100644 --- a/Shared/Player/Controls/PlayerControls.swift +++ b/Shared/Player/Controls/PlayerControls.swift @@ -89,14 +89,15 @@ struct PlayerControls: View { } .frame(maxHeight: .infinity) } + .frame(maxWidth: .infinity, maxHeight: .infinity) #if os(tvOS) - .onChange(of: model.presentingControls) { newValue in - if newValue { focusedField = .play } - } - .onChange(of: focusedField) { _ in model.resetTimer() } + .onChange(of: model.presentingControls) { newValue in + if newValue { focusedField = .play } + } + .onChange(of: focusedField) { _ in model.resetTimer() } #else - .background(PlayerGestures()) - .background(controlsBackground) + .background(PlayerGestures()) + .background(controlsBackground) #endif if model.presentingDetailsOverlay { @@ -176,6 +177,7 @@ struct PlayerControls: View { } .retryOnAppear(true) .indicator(.activity) + .frame(maxWidth: .infinity, maxHeight: .infinity) } } @@ -273,7 +275,6 @@ struct PlayerControls: View { private var musicModeButton: some View { button("Music Mode", systemImage: "music.note", background: false, active: player.musicMode, action: player.toggleMusicMode) - .disabled(player.activeBackend == .appleAVPlayer) } private var pipButton: some View { diff --git a/Shared/Player/StreamControl.swift b/Shared/Player/StreamControl.swift index d2ae9025..c62c7e0d 100644 --- a/Shared/Player/StreamControl.swift +++ b/Shared/Player/StreamControl.swift @@ -60,7 +60,7 @@ struct StreamControl: View { .frame(maxWidth: 320) } .contextMenu { - ForEach(player.availableStreamsSorted) { stream in + ForEach(streams) { stream in Button(stream.description) { player.streamSelection = stream } } @@ -79,10 +79,14 @@ struct StreamControl: View { } private func availableStreamsForInstance(_ instance: Instance) -> [Stream.Kind: [Stream]] { - let streams = player.availableStreamsSorted.filter { $0.instance == instance }.filter { player.backend.canPlay($0) } + let streams = streams.filter { $0.instance == instance }.filter { player.backend.canPlay($0) } return Dictionary(grouping: streams, by: \.kind!) } + + var streams: [Stream] { + player.availableStreamsSorted.filter { player.backend.canPlay($0) } + } } struct StreamControl_Previews: PreviewProvider { diff --git a/Shared/Player/VideoPlayerView.swift b/Shared/Player/VideoPlayerView.swift index f6e46c6b..954120e7 100644 --- a/Shared/Player/VideoPlayerView.swift +++ b/Shared/Player/VideoPlayerView.swift @@ -252,7 +252,9 @@ struct VideoPlayerView: View { ZStack { player.playerBackendView - tvControls + if player.activeBackend == .mpv { + tvControls + } } .ignoresSafeArea() #else diff --git a/Shared/Settings/QualityProfileForm.swift b/Shared/Settings/QualityProfileForm.swift index 3e675538..d4224307 100644 --- a/Shared/Settings/QualityProfileForm.swift +++ b/Shared/Settings/QualityProfileForm.swift @@ -99,6 +99,9 @@ struct QualityProfileForm: View { Section(header: Text("Resolution")) { qualityButton } + Section(header: Text("Backend")) { + backendPicker + } #else backendPicker qualityPicker @@ -147,7 +150,7 @@ struct QualityProfileForm: View { } #else - return picker + picker #endif } @@ -191,7 +194,7 @@ struct QualityProfileForm: View { } #else - return picker + picker #endif } diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index c9f48084..4fa45262 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -162,6 +162,9 @@ 37169AA72729E2CC0011DE61 /* AccountsBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */; }; 37169AA82729E2CC0011DE61 /* AccountsBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */; }; 37192D5528B0D5D60012EEDD /* PlayerLayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373031F22838388A000CFD59 /* PlayerLayerView.swift */; }; + 37192D5728B179D60012EEDD /* ChaptersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37192D5628B179D60012EEDD /* ChaptersView.swift */; }; + 37192D5828B179D60012EEDD /* ChaptersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37192D5628B179D60012EEDD /* ChaptersView.swift */; }; + 37192D5928B179D60012EEDD /* ChaptersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37192D5628B179D60012EEDD /* ChaptersView.swift */; }; 371B7E5C27596B8400D21217 /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E5B27596B8400D21217 /* Comment.swift */; }; 371B7E5D27596B8400D21217 /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E5B27596B8400D21217 /* Comment.swift */; }; 371B7E5E27596B8400D21217 /* Comment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 371B7E5B27596B8400D21217 /* Comment.swift */; }; @@ -312,9 +315,9 @@ 37520699285E8DD300CA655F /* Chapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37520698285E8DD300CA655F /* Chapter.swift */; }; 3752069A285E8DD300CA655F /* Chapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37520698285E8DD300CA655F /* Chapter.swift */; }; 3752069B285E8DD300CA655F /* Chapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37520698285E8DD300CA655F /* Chapter.swift */; }; - 3752069D285E910600CA655F /* ChaptersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3752069C285E910600CA655F /* ChaptersView.swift */; }; - 3752069E285E910600CA655F /* ChaptersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3752069C285E910600CA655F /* ChaptersView.swift */; }; - 3752069F285E910600CA655F /* ChaptersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3752069C285E910600CA655F /* ChaptersView.swift */; }; + 3752069D285E910600CA655F /* ChapterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3752069C285E910600CA655F /* ChapterView.swift */; }; + 3752069E285E910600CA655F /* ChapterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3752069C285E910600CA655F /* ChapterView.swift */; }; + 3752069F285E910600CA655F /* ChapterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3752069C285E910600CA655F /* ChapterView.swift */; }; 3756C2A62861131100E4B059 /* NetworkState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3756C2A52861131100E4B059 /* NetworkState.swift */; }; 3756C2A72861131100E4B059 /* NetworkState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3756C2A52861131100E4B059 /* NetworkState.swift */; }; 3756C2A82861131100E4B059 /* NetworkState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3756C2A52861131100E4B059 /* NetworkState.swift */; }; @@ -622,7 +625,6 @@ 37BE0BD126A0E2D50092E2DB /* VideoPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BCE26A0E2D50092E2DB /* VideoPlayerView.swift */; }; 37BE0BD326A1D4780092E2DB /* AppleAVPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BD226A1D4780092E2DB /* AppleAVPlayerView.swift */; }; 37BE0BD426A1D47D0092E2DB /* AppleAVPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BD226A1D4780092E2DB /* AppleAVPlayerView.swift */; }; - 37BE0BD626A1D4A90092E2DB /* AppleAVPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BD526A1D4A90092E2DB /* AppleAVPlayerViewController.swift */; }; 37BE0BD726A1D4A90092E2DB /* AppleAVPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BD526A1D4A90092E2DB /* AppleAVPlayerViewController.swift */; }; 37BE0BDC26A2367F0092E2DB /* AppleAVPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BE0BDB26A2367F0092E2DB /* AppleAVPlayerView.swift */; }; 37BF661C27308859008CCFB0 /* DropFavorite.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37BF661B27308859008CCFB0 /* DropFavorite.swift */; }; @@ -987,6 +989,7 @@ 37152EE926EFEB95004FB96D /* LazyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LazyView.swift; sourceTree = ""; }; 37169AA12729D98A0011DE61 /* InstancesBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesBridge.swift; sourceTree = ""; }; 37169AA52729E2CC0011DE61 /* AccountsBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsBridge.swift; sourceTree = ""; }; + 37192D5628B179D60012EEDD /* ChaptersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChaptersView.swift; sourceTree = ""; }; 371B7E5B27596B8400D21217 /* Comment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Comment.swift; sourceTree = ""; }; 371B7E602759706A00D21217 /* CommentsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentsView.swift; sourceTree = ""; }; 371B7E652759786B00D21217 /* Comment+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Comment+Fixtures.swift"; sourceTree = ""; }; @@ -1053,7 +1056,7 @@ 3751BA7F27E64244007B1A60 /* VideoLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoLayer.swift; sourceTree = ""; }; 3751BA8227E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReturnYouTubeDislikeAPI.swift; sourceTree = ""; }; 37520698285E8DD300CA655F /* Chapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Chapter.swift; sourceTree = ""; }; - 3752069C285E910600CA655F /* ChaptersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChaptersView.swift; sourceTree = ""; }; + 3752069C285E910600CA655F /* ChapterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChapterView.swift; sourceTree = ""; }; 3756C2A52861131100E4B059 /* NetworkState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkState.swift; sourceTree = ""; }; 3756C2A92861151C00E4B059 /* NetworkStateModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkStateModel.swift; sourceTree = ""; }; 37579D5C27864F5F00FD0B98 /* Help.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Help.swift; sourceTree = ""; }; @@ -1531,7 +1534,8 @@ 375E45F327B1973400BA7902 /* MPV */, 37BE0BD226A1D4780092E2DB /* AppleAVPlayerView.swift */, 37BE0BD526A1D4A90092E2DB /* AppleAVPlayerViewController.swift */, - 3752069C285E910600CA655F /* ChaptersView.swift */, + 3752069C285E910600CA655F /* ChapterView.swift */, + 37192D5628B179D60012EEDD /* ChaptersView.swift */, 371B7E602759706A00D21217 /* CommentsView.swift */, 37EF9A75275BEB8E0043B585 /* CommentView.swift */, 37DD9DA22785BBC900539416 /* NoCommentsView.swift */, @@ -2760,7 +2764,6 @@ 37977583268922F600DD52A8 /* InvidiousAPI.swift in Sources */, 37130A5F277657300033018A /* PersistenceController.swift in Sources */, 37FD43E32704847C0073EE42 /* View+Fixtures.swift in Sources */, - 37BE0BD626A1D4A90092E2DB /* AppleAVPlayerViewController.swift in Sources */, 3776ADD6287381240078EBC4 /* Captions.swift in Sources */, 37BA793F26DB8F97002A0235 /* ChannelVideosView.swift in Sources */, 37C194C726F6A9C8005D3B96 /* RecentsModel.swift in Sources */, @@ -2770,6 +2773,7 @@ 37F64FE426FE70A60081B69E /* RedrawOnModifier.swift in Sources */, 37EBD8C427AF0DA800F1C24B /* PlayerBackend.swift in Sources */, 376A33E02720CAD6000C1D6B /* VideosApp.swift in Sources */, + 37192D5728B179D60012EEDD /* ChaptersView.swift in Sources */, 37B81AF926D2C9A700675966 /* VideoPlayerSizeModifier.swift in Sources */, 37C0698227260B2100F7F6CB /* ThumbnailsModel.swift in Sources */, 37BC50A82778A84700510953 /* HistorySettings.swift in Sources */, @@ -2792,7 +2796,7 @@ 37030FF727B0347C00ECDDAA /* MPVPlayerView.swift in Sources */, 37BA794726DC2E56002A0235 /* AppSidebarSubscriptions.swift in Sources */, 377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */, - 3752069D285E910600CA655F /* ChaptersView.swift in Sources */, + 3752069D285E910600CA655F /* ChapterView.swift in Sources */, 375EC96A289F232600751258 /* QualityProfilesModel.swift in Sources */, 3751B4B227836902000B7DF4 /* SearchPage.swift in Sources */, 37CC3F4C270CFE1700608308 /* PlayerQueueView.swift in Sources */, @@ -2981,6 +2985,7 @@ 3743CA4F270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */, 374C053C2724614F009BDDBE /* PlayerTVMenu.swift in Sources */, 377FC7DD267A081A00A6BBAF /* PopularView.swift in Sources */, + 37192D5828B179D60012EEDD /* ChaptersView.swift in Sources */, 3784CDE327772EE40055BBF2 /* Watch.swift in Sources */, 37E80F3D287B107F00561799 /* VideoDetailsOverlay.swift in Sources */, 37DD9DBB2785D60300539416 /* FramePreferenceKey.swift in Sources */, @@ -3025,7 +3030,7 @@ 37CC3F51270D010D00608308 /* VideoBanner.swift in Sources */, 37F961A027BD90BB00058149 /* PlayerBackendType.swift in Sources */, 37BC50AD2778BCBA00510953 /* HistoryModel.swift in Sources */, - 3752069E285E910600CA655F /* ChaptersView.swift in Sources */, + 3752069E285E910600CA655F /* ChapterView.swift in Sources */, 37030FF827B0347C00ECDDAA /* MPVPlayerView.swift in Sources */, 378E50FC26FE8B9F00F49626 /* Instance.swift in Sources */, 37169AA32729D98A0011DE61 /* InstancesBridge.swift in Sources */, @@ -3278,6 +3283,7 @@ 37E80F44287B7AB400561799 /* VideoDetails.swift in Sources */, 3751BA8527E6914F007B1A60 /* ReturnYouTubeDislikeAPI.swift in Sources */, 37030FFD27B0398000ECDDAA /* MPVClient.swift in Sources */, + 37192D5928B179D60012EEDD /* ChaptersView.swift in Sources */, 37B767DD2677C3CA0098BAA8 /* PlayerModel.swift in Sources */, 373CFAF12697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */, 3784CDE427772EE40055BBF2 /* Watch.swift in Sources */, @@ -3341,7 +3347,7 @@ 37FEF11527EFD8580033912F /* PlaceholderCell.swift in Sources */, 37FD43F02704A9C00073EE42 /* RecentsModel.swift in Sources */, 379775952689365600DD52A8 /* Array+Next.swift in Sources */, - 3752069F285E910600CA655F /* ChaptersView.swift in Sources */, + 3752069F285E910600CA655F /* ChapterView.swift in Sources */, 37F4AD1D28612B23004D0F66 /* OpeningStream.swift in Sources */, 3705B180267B4DFB00704544 /* TrendingCountry.swift in Sources */, 373CFACD26966264003CB2C6 /* SearchQuery.swift in Sources */, diff --git a/tvOS/NowPlayingView.swift b/tvOS/NowPlayingView.swift index f1ee777b..ca0881d8 100644 --- a/tvOS/NowPlayingView.swift +++ b/tvOS/NowPlayingView.swift @@ -4,7 +4,7 @@ import SwiftUI struct NowPlayingView: View { enum ViewSection: CaseIterable { - case nowPlaying, playingNext, playedPreviously, related, comments + case nowPlaying, playingNext, playedPreviously, related, comments, chapters } var sections = [ViewSection.nowPlaying, .playingNext, .playedPreviously, .related] @@ -84,23 +84,16 @@ struct NowPlayingView: View { } } - if sections.contains(.related), !player.currentVideo.isNil, !player.currentVideo!.related.isEmpty { - Section(header: inInfoViewController ? AnyView(EmptyView()) : AnyView(Text("Related"))) { - ForEach(player.currentVideo!.related) { video in + if sections.contains(.related), let video = player.currentVideo, !video.related.isEmpty { + Section(header: Text("Related")) { + ForEach(video.related) { video in Button { - player.playNow(video) - player.show() + player.play(video) } label: { VideoBanner(video: video) } .contextMenu { - Button("Play Next") { - player.playNext(video) - } - Button("Play Last") { - player.enqueueVideo(video) - } - Button("Cancel", role: .cancel) {} + VideoContextMenuView(video: video) } } } @@ -125,9 +118,7 @@ struct NowPlayingView: View { player.loadHistoryVideoDetails(watch.videoID) } .contextMenu { - Button("Remove", role: .destructive) { - player.removeWatch(watch) - } + VideoContextMenuView(video: watch.video) } } } @@ -161,6 +152,20 @@ struct NowPlayingView: View { } } } + + if sections.contains(.chapters) { + if let video = player.currentVideo { + if video.chapters.isEmpty { + NoCommentsView(text: "No chapters information available", systemImage: "xmark.circle.fill") + } else { + Section(header: Text("Chapters")) { + ForEach(video.chapters) { chapter in + ChapterView(chapter: chapter) + } + } + } + } + } } .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 20)) .padding(.vertical, 20) @@ -177,7 +182,7 @@ struct NowPlayingView: View { struct NowPlayingView_Previews: PreviewProvider { static var previews: some View { - NowPlayingView() + NowPlayingView(sections: [.chapters]) .injectFixtureEnvironmentObjects() NowPlayingView(inInfoViewController: true)