mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 21:43:41 +00:00
PiP and UI improvements
This commit is contained in:
parent
c387454d9a
commit
24f7c566bf
@ -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) {
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
import Defaults
|
||||
import Foundation
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
@ -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 */,
|
||||
|
@ -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 }
|
||||
|
@ -104,6 +104,7 @@ struct AppTabNavigation: View {
|
||||
if let channel = recents.presentedChannel {
|
||||
NavigationView {
|
||||
ChannelVideosView(channel: channel)
|
||||
.environment(\.inChannelView, true)
|
||||
.environment(\.inNavigationView, true)
|
||||
.background(playerNavigationLink)
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -21,6 +21,10 @@ struct VideoCell: View {
|
||||
Button(action: {
|
||||
player.playNow(video)
|
||||
|
||||
guard !player.playingInPictureInPicture else {
|
||||
return
|
||||
}
|
||||
|
||||
if inNavigationView {
|
||||
player.playerNavigationLinkActive = true
|
||||
} else {
|
||||
|
@ -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 {
|
||||
|
@ -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())
|
||||
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
28
macOS/PictureInPictureDelegate.swift
Normal file
28
macOS/PictureInPictureDelegate.swift
Normal 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)
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user