PiP and UI improvements

This commit is contained in:
Arkadiusz Fal 2021-10-28 19:14:55 +02:00
parent c387454d9a
commit 24f7c566bf
18 changed files with 169 additions and 55 deletions

View File

@ -47,8 +47,12 @@ struct Instance: Defaults.Serializable, Hashable, Identifiable {
URLComponents(string: apiURL)!
}
var frontendHost: String {
URLComponents(string: frontendURL!)!.host!
var frontendHost: String? {
guard let url = app == .invidious ? apiURL : frontendURL else {
return nil
}
return URLComponents(string: url)?.host
}
func hash(into hasher: inout Hasher) {

View File

@ -1,4 +1,3 @@
import Defaults
import Foundation

View File

@ -26,7 +26,7 @@ protocol VideosAPI {
func channelPlaylist(_ id: String) -> Resource?
func loadDetails(_ item: PlayerQueueItem, completionHandler: @escaping (PlayerQueueItem) -> Void)
func shareURL(_ item: ContentItem) -> URL
func shareURL(_ item: ContentItem) -> URL?
}
extension VideosAPI {
@ -48,9 +48,13 @@ extension VideosAPI {
}
}
func shareURL(_ item: ContentItem) -> URL {
func shareURL(_ item: ContentItem) -> URL? {
guard let frontendHost = account.instance.frontendHost else {
return nil
}
var urlComponents = account.instance.urlComponents
urlComponents.host = account.instance.frontendHost
urlComponents.host = frontendHost
switch item.contentType {
case .video:

View File

@ -13,7 +13,8 @@ final class PlayerModel: ObservableObject {
let logger = Logger(label: "net.arekf.Pearvidious.ps")
private(set) var player = AVPlayer()
var controller: PlayerViewController?
private(set) var playerView = Player()
var controller: PlayerViewController? { didSet { playerView.controller = controller } }
#if os(tvOS)
var avPlayerViewController: AVPlayerViewController?
#endif
@ -52,6 +53,8 @@ final class PlayerModel: ObservableObject {
private var timeObserverThrottle = Throttle(interval: 2)
var playingInPictureInPicture = false
init(accounts: AccountsModel? = nil, instances: InstancesModel? = nil) {
self.accounts = accounts ?? AccountsModel()
self.instances = instances ?? InstancesModel()

View File

@ -96,6 +96,7 @@
373CFAEF2697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */; };
373CFAF02697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */; };
373CFAF12697A78B003CB2C6 /* AddToPlaylistView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */; };
374108D1272B11B2006C5CC8 /* PictureInPictureDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 374108D0272B11B2006C5CC8 /* PictureInPictureDelegate.swift */; };
3743B86927216D3600261544 /* ChannelCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3743B86727216D3600261544 /* ChannelCell.swift */; };
3743B86A27216D3600261544 /* ChannelCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3743B86727216D3600261544 /* ChannelCell.swift */; };
3743CA4E270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */; };
@ -518,6 +519,7 @@
373CFADA269663F1003CB2C6 /* Thumbnail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Thumbnail.swift; sourceTree = "<group>"; };
373CFAEA26975CBF003CB2C6 /* PlaylistFormView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlaylistFormView.swift; sourceTree = "<group>"; };
373CFAEE2697A78B003CB2C6 /* AddToPlaylistView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddToPlaylistView.swift; sourceTree = "<group>"; };
374108D0272B11B2006C5CC8 /* PictureInPictureDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PictureInPictureDelegate.swift; sourceTree = "<group>"; };
3743B86727216D3600261544 /* ChannelCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChannelCell.swift; sourceTree = "<group>"; };
3743CA4D270EFE3400E4D32B /* PlayerQueueRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayerQueueRow.swift; sourceTree = "<group>"; };
3743CA51270F284F00E4D32B /* View+Borders.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "View+Borders.swift"; sourceTree = "<group>"; };
@ -976,6 +978,7 @@
children = (
37FD43E1270472060073EE42 /* Settings */,
374C0542272496E4009BDDBE /* AppDelegate.swift */,
374108D0272B11B2006C5CC8 /* PictureInPictureDelegate.swift */,
37BE0BDB26A2367F0092E2DB /* Player.swift */,
37BE0BD926A214630092E2DB /* PlayerViewController.swift */,
374C0544272496FD009BDDBE /* Info.plist */,
@ -1825,6 +1828,7 @@
3784B23E2728B85300B09468 /* ShareButton.swift in Sources */,
37BE0BDA26A214630092E2DB /* PlayerViewController.swift in Sources */,
37E64DD226D597EB00C71877 /* SubscriptionsModel.swift in Sources */,
374108D1272B11B2006C5CC8 /* PictureInPictureDelegate.swift in Sources */,
37C7A1D6267BFD9D0010EAD6 /* SponsorBlockSegment.swift in Sources */,
37319F0627103F94004ECCD0 /* PlayerQueue.swift in Sources */,
37B767DC2677C3CA0098BAA8 /* PlayerModel.swift in Sources */,

View File

@ -5,6 +5,10 @@ private struct InNavigationViewKey: EnvironmentKey {
static let defaultValue = false
}
private struct InChannelViewKey: EnvironmentKey {
static let defaultValue = false
}
private struct HorizontalCellsKey: EnvironmentKey {
static let defaultValue = false
}
@ -27,6 +31,11 @@ extension EnvironmentValues {
set { self[InNavigationViewKey.self] = newValue }
}
var inChannelView: Bool {
get { self[InChannelViewKey.self] }
set { self[InChannelViewKey.self] = newValue }
}
var horizontalCells: Bool {
get { self[HorizontalCellsKey.self] }
set { self[HorizontalCellsKey.self] = newValue }

View File

@ -104,6 +104,7 @@ struct AppTabNavigation: View {
if let channel = recents.presentedChannel {
NavigationView {
ChannelVideosView(channel: channel)
.environment(\.inChannelView, true)
.environment(\.inNavigationView, true)
.background(playerNavigationLink)
}

View File

@ -2,6 +2,7 @@ import Defaults
import SwiftUI
struct Player: UIViewControllerRepresentable {
@EnvironmentObject<NavigationModel> private var navigation
@EnvironmentObject<PlayerModel> private var player
var controller: PlayerViewController?
@ -17,6 +18,7 @@ struct Player: UIViewControllerRepresentable {
let controller = PlayerViewController()
controller.navigationModel = navigation
controller.playerModel = player
player.controller = controller

View File

@ -4,6 +4,7 @@ import SwiftUI
final class PlayerViewController: UIViewController {
var playerLoaded = false
var navigationModel: NavigationModel!
var playerModel: PlayerModel!
var playerViewController = AVPlayerViewController()
@ -11,6 +12,12 @@ final class PlayerViewController: UIViewController {
super.viewWillAppear(animated)
loadPlayer()
#if os(tvOS)
if !playerViewController.isBeingPresented, !playerViewController.isBeingDismissed {
present(playerViewController, animated: false)
}
#endif
}
func loadPlayer() {
@ -26,12 +33,9 @@ final class PlayerViewController: UIViewController {
#if os(tvOS)
playerModel.avPlayerViewController = playerViewController
playerViewController.customInfoViewControllers = [playerQueueInfoViewController]
present(playerViewController, animated: false)
#else
embedViewController()
#endif
playerLoaded = true
}
#if os(tvOS)
@ -66,7 +70,7 @@ extension PlayerViewController: AVPlayerViewControllerDelegate {
}
func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_: AVPlayerViewController) -> Bool {
false
true
}
func playerViewControllerWillBeginDismissalTransition(_: AVPlayerViewController) {}
@ -95,7 +99,35 @@ extension PlayerViewController: AVPlayerViewControllerDelegate {
}
}
func playerViewControllerWillStartPictureInPicture(_: AVPlayerViewController) {}
func playerViewControllerWillStopPictureInPicture(_: AVPlayerViewController) {}
func playerViewController(
_ playerViewController: AVPlayerViewController,
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if self.navigationModel.presentingChannel {
self.playerModel.playerNavigationLinkActive = true
} else {
self.playerModel.presentPlayer()
}
#if os(tvOS)
if self.playerModel.playingInPictureInPicture {
self.present(playerViewController, animated: false) {
completionHandler(true)
}
}
#else
completionHandler(true)
#endif
}
}
func playerViewControllerWillStartPictureInPicture(_: AVPlayerViewController) {
playerModel.playingInPictureInPicture = true
playerModel.playerNavigationLinkActive = false
}
func playerViewControllerWillStopPictureInPicture(_: AVPlayerViewController) {
playerModel.playingInPictureInPicture = false
}
}

View File

@ -17,6 +17,7 @@ struct VideoDetails: View {
@State private var currentPage = Page.details
@Environment(\.dismiss) private var dismiss
@Environment(\.inNavigationView) private var inNavigationView
@EnvironmentObject<AccountsModel> private var accounts
@EnvironmentObject<PlayerModel> private var player
@ -89,6 +90,7 @@ struct VideoDetails: View {
.edgesIgnoringSafeArea(.horizontal)
}
}
.padding(.top, inNavigationView && fullScreen ? 10 : 0)
.onAppear {
#if !os(macOS)
if video.isNil {
@ -298,9 +300,9 @@ struct VideoDetails: View {
}
#if os(iOS)
.sheet(isPresented: $presentingShareSheet) {
ShareSheet(activityItems: [
accounts.api.shareURL(contentItem)
])
if let url = accounts.api.shareURL(contentItem) {
ShareSheet(activityItems: [url])
}
}
#endif
}

View File

@ -42,9 +42,9 @@ struct VideoPlayerView: View {
var content: some View {
Group {
VStack(alignment: .leading, spacing: 0) {
Group {
#if os(tvOS)
player()
player.playerView
#else
GeometryReader { geometry in
VStack(spacing: 0) {
@ -59,7 +59,8 @@ struct VideoPlayerView: View {
if player.currentItem.isNil {
playerPlaceholder(geometry: geometry)
} else {
player(geometry: geometry)
player.playerView
.modifier(VideoPlayerSizeModifier(geometry: geometry))
}
}
#if os(iOS)
@ -131,13 +132,6 @@ struct VideoPlayerView: View {
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: geometry.size.width / VideoPlayerView.defaultAspectRatio)
}
func player(geometry: GeometryProxy? = nil) -> some View {
Player()
#if !os(tvOS)
.modifier(VideoPlayerSizeModifier(geometry: geometry))
#endif
}
#if os(iOS)
var sidebarQueue: Bool {
horizontalSizeClass == .regular && playerSize.width > 750

View File

@ -21,6 +21,10 @@ struct VideoCell: View {
Button(action: {
player.playNow(video)
guard !player.playingInPictureInPicture else {
return
}
if inNavigationView {
player.playerNavigationLinkActive = true
} else {

View File

@ -48,9 +48,9 @@ struct ChannelPlaylistView: View {
}
#if os(iOS)
.sheet(isPresented: $presentingShareSheet) {
ShareSheet(activityItems: [
accounts.api.shareURL(contentItem)
])
if let url = accounts.api.shareURL(contentItem) {
ShareSheet(activityItems: [url])
}
}
#endif
.onAppear {

View File

@ -67,6 +67,7 @@ struct ChannelVideosView: View {
.prefersDefaultFocus(in: focusNamespace)
#endif
}
.environment(\.inChannelView, true)
#if !os(iOS)
.focusScope(focusNamespace)
#endif
@ -102,9 +103,9 @@ struct ChannelVideosView: View {
#endif
#if os(iOS)
.sheet(isPresented: $presentingShareSheet) {
ShareSheet(activityItems: [
accounts.api.shareURL(contentItem)
])
if let url = accounts.api.shareURL(contentItem) {
ShareSheet(activityItems: [url])
}
}
#endif
.modifier(UnsubscribeAlertModifier())

View File

@ -7,12 +7,14 @@ struct ShareButton: View {
@EnvironmentObject<AccountsModel> private var accounts
var body: some View {
Group {
if let url = shareURL {
Button {
#if os(iOS)
presentingShareSheet = true
#else
NSPasteboard.general.clearContents()
NSPasteboard.general.setString(shareURL, forType: .string)
NSPasteboard.general.setString(url, forType: .string)
#endif
} label: {
#if os(iOS)
@ -25,10 +27,14 @@ struct ShareButton: View {
.foregroundColor(.blue)
.buttonStyle(.plain)
.labelStyle(.iconOnly)
} else {
EmptyView()
}
}
}
private var shareURL: String {
accounts.api.shareURL(contentItem).absoluteString
private var shareURL: String? {
accounts.api.shareURL(contentItem)?.absoluteString
}
}

View File

@ -7,6 +7,7 @@ struct VideoContextMenuView: View {
@Binding var playerNavigationLinkActive: Bool
@Environment(\.inNavigationView) private var inNavigationView
@Environment(\.inChannelView) private var inChannelView
@Environment(\.navigationStyle) private var navigationStyle
@Environment(\.currentPlaylistID) private var playlistID
@ -27,6 +28,7 @@ struct VideoContextMenuView: View {
addToQueueButton
}
if !inChannelView {
Section {
openChannelButton
@ -34,6 +36,7 @@ struct VideoContextMenuView: View {
subscriptionButton
}
}
}
if accounts.app.supportsUserPlaylists {
Section {
@ -54,6 +57,10 @@ struct VideoContextMenuView: View {
Button {
player.playNow(video)
guard !player.playingInPictureInPicture else {
return
}
if inNavigationView {
playerNavigationLinkActive = true
} else {
@ -72,6 +79,14 @@ struct VideoContextMenuView: View {
}
}
private var isShowingChannelButton: Bool {
if case .channel = navigation.tabSelection {
return false
}
return !inChannelView
}
private var addToQueueButton: some View {
Button {
player.enqueueVideo(video)

View File

@ -0,0 +1,28 @@
import AVKit
import Foundation
final class PictureInPictureDelegate: NSObject, AVPlayerViewPictureInPictureDelegate {
var playerModel: PlayerModel!
func playerViewShouldAutomaticallyDismissAtPicture(inPictureStart _: AVPlayerView) -> Bool {
false
}
func playerViewWillStartPicture(inPicture _: AVPlayerView) {
playerModel.playingInPictureInPicture = true
playerModel.presentingPlayer = false
}
func playerViewWillStopPicture(inPicture _: AVPlayerView) {
playerModel.playingInPictureInPicture = false
playerModel.presentPlayer()
}
func playerView(
_: AVPlayerView,
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: (Bool) -> Void
) {
playerModel.presentingPlayer = true
completionHandler(true)
}
}

View File

@ -4,18 +4,24 @@ import SwiftUI
final class PlayerViewController: NSViewController {
var playerModel: PlayerModel!
var playerView = AVPlayerView()
var pictureInPictureDelegate = PictureInPictureDelegate()
override func viewDidDisappear() {
if !playerModel.playingInPictureInPicture {
playerModel.pause()
}
super.viewDidDisappear()
}
override func loadView() {
playerView.player = playerModel.player
pictureInPictureDelegate.playerModel = playerModel
playerView.allowsPictureInPicturePlayback = true
playerView.showsFullScreenToggleButton = true
playerView.pictureInPictureDelegate = pictureInPictureDelegate
view = playerView
}
}