mirror of
https://github.com/yattee/yattee.git
synced 2024-12-22 13:33:42 +00:00
Layout and PiP improvements, new settings
- player is now a separate window on macOS - add setting to disable pause when player is closed (fixes #40) - add PiP settings: * Close PiP when starting playing other video * Close PiP when player is opened * Close PiP and open player when application enters foreground (iOS/tvOS) (fixes #37) - new player placeholder when in PiP, context menu with exit option
This commit is contained in:
parent
cef0b2594a
commit
61a4951831
@ -49,7 +49,11 @@ final class NavigationModel: ObservableObject {
|
|||||||
navigationStyle: NavigationStyle
|
navigationStyle: NavigationStyle
|
||||||
) {
|
) {
|
||||||
let recent = RecentItem(from: channel)
|
let recent = RecentItem(from: channel)
|
||||||
player.presentingPlayer = false
|
#if os(macOS)
|
||||||
|
OpenWindow.main.open()
|
||||||
|
#else
|
||||||
|
player.hide()
|
||||||
|
#endif
|
||||||
|
|
||||||
let openRecent = {
|
let openRecent = {
|
||||||
recents.add(recent)
|
recents.add(recent)
|
||||||
|
@ -15,18 +15,15 @@ final class PlayerModel: ObservableObject {
|
|||||||
let logger = Logger(label: "stream.yattee.app")
|
let logger = Logger(label: "stream.yattee.app")
|
||||||
|
|
||||||
private(set) var player = AVPlayer()
|
private(set) var player = AVPlayer()
|
||||||
private(set) var playerView = Player()
|
var playerView = Player()
|
||||||
var controller: PlayerViewController? { didSet { playerView.controller = controller } }
|
var controller: PlayerViewController?
|
||||||
#if os(tvOS)
|
|
||||||
var avPlayerViewController: AVPlayerViewController?
|
|
||||||
#endif
|
|
||||||
|
|
||||||
@Published var presentingPlayer = false { didSet { pauseOnPlayerDismiss() } }
|
@Published var presentingPlayer = false { didSet { handlePresentationChange() } }
|
||||||
|
|
||||||
@Published var stream: Stream?
|
@Published var stream: Stream?
|
||||||
@Published var currentRate: Float = 1.0 { didSet { player.rate = currentRate } }
|
@Published var currentRate: Float = 1.0 { didSet { player.rate = currentRate } }
|
||||||
|
|
||||||
@Published var availableStreams = [Stream]() { didSet { handleAvailableStreamsChange() }}
|
@Published var availableStreams = [Stream]() { didSet { handleAvailableStreamsChange() } }
|
||||||
@Published var streamSelection: Stream? { didSet { rebuildTVMenu() } }
|
@Published var streamSelection: Stream? { didSet { rebuildTVMenu() } }
|
||||||
|
|
||||||
@Published var queue = [PlayerQueueItem]() { didSet { Defaults[.queue] = queue } }
|
@Published var queue = [PlayerQueueItem]() { didSet { Defaults[.queue] = queue } }
|
||||||
@ -35,7 +32,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
|
|
||||||
@Published var preservedTime: CMTime?
|
@Published var preservedTime: CMTime?
|
||||||
|
|
||||||
@Published var playerNavigationLinkActive = false { didSet { pauseOnChannelPlayerDismiss() } }
|
@Published var playerNavigationLinkActive = false { didSet { handleNavigationViewPlayerPresentationChange() } }
|
||||||
|
|
||||||
@Published var sponsorBlock = SponsorBlockAPI()
|
@Published var sponsorBlock = SponsorBlockAPI()
|
||||||
@Published var segmentRestorationTime: CMTime?
|
@Published var segmentRestorationTime: CMTime?
|
||||||
@ -70,6 +67,14 @@ final class PlayerModel: ObservableObject {
|
|||||||
#endif
|
#endif
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
@Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer
|
||||||
|
@Default(.closePiPOnNavigation) var closePiPOnNavigation
|
||||||
|
@Default(.closePiPOnOpeningPlayer) var closePiPOnOpeningPlayer
|
||||||
|
|
||||||
|
#if !os(macOS)
|
||||||
|
@Default(.closePiPAndOpenPlayerOnEnteringForeground) var closePiPAndOpenPlayerOnEnteringForeground
|
||||||
|
#endif
|
||||||
|
|
||||||
init(accounts: AccountsModel? = nil, comments: CommentsModel? = nil) {
|
init(accounts: AccountsModel? = nil, comments: CommentsModel? = nil) {
|
||||||
self.accounts = accounts ?? AccountsModel()
|
self.accounts = accounts ?? AccountsModel()
|
||||||
self.comments = comments ?? CommentsModel()
|
self.comments = comments ?? CommentsModel()
|
||||||
@ -80,12 +85,41 @@ final class PlayerModel: ObservableObject {
|
|||||||
addPlayerTimeControlStatusObserver()
|
addPlayerTimeControlStatusObserver()
|
||||||
}
|
}
|
||||||
|
|
||||||
func presentPlayer() {
|
func show() {
|
||||||
|
guard !presentingPlayer else {
|
||||||
|
#if os(macOS)
|
||||||
|
OpenWindow.player.focus()
|
||||||
|
#endif
|
||||||
|
return
|
||||||
|
}
|
||||||
|
#if os(macOS)
|
||||||
|
OpenWindow.player.open()
|
||||||
|
OpenWindow.player.focus()
|
||||||
|
#endif
|
||||||
presentingPlayer = true
|
presentingPlayer = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hide() {
|
||||||
|
guard presentingPlayer else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
presentingPlayer = false
|
||||||
|
}
|
||||||
|
|
||||||
func togglePlayer() {
|
func togglePlayer() {
|
||||||
presentingPlayer.toggle()
|
#if os(macOS)
|
||||||
|
if !presentingPlayer {
|
||||||
|
OpenWindow.player.open()
|
||||||
|
}
|
||||||
|
OpenWindow.player.focus()
|
||||||
|
#else
|
||||||
|
if presentingPlayer {
|
||||||
|
hide()
|
||||||
|
} else {
|
||||||
|
show()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
var isPlaying: Bool {
|
var isPlaying: Bool {
|
||||||
@ -189,12 +223,29 @@ final class PlayerModel: ObservableObject {
|
|||||||
preservingTime: !currentItem.playbackTime.isNil
|
preservingTime: !currentItem.playbackTime.isNil
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func handlePresentationChange() {
|
||||||
|
if presentingPlayer, closePiPOnOpeningPlayer, playingInPictureInPicture {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||||
|
self?.closePiP()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !presentingPlayer, pauseOnHidingPlayer, !playingInPictureInPicture {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||||
|
self?.pause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !presentingPlayer, !pauseOnHidingPlayer, isPlaying {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
|
||||||
|
self?.play()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func pauseOnChannelPlayerDismiss() {
|
private func handleNavigationViewPlayerPresentationChange() {
|
||||||
if !playingInPictureInPicture, !playerNavigationLinkActive {
|
if pauseOnHidingPlayer, !playingInPictureInPicture, !playerNavigationLinkActive {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
self.pause()
|
self.pause()
|
||||||
}
|
}
|
||||||
@ -371,6 +422,10 @@ final class PlayerModel: ObservableObject {
|
|||||||
|
|
||||||
item.preferredForwardBufferDuration = 5
|
item.preferredForwardBufferDuration = 5
|
||||||
|
|
||||||
|
observePlayerItemStatus(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func observePlayerItemStatus(_ item: AVPlayerItem) {
|
||||||
statusObservation?.invalidate()
|
statusObservation?.invalidate()
|
||||||
statusObservation = item.observe(\.status, options: [.old, .new]) { [weak self] playerItem, _ in
|
statusObservation = item.observe(\.status, options: [.old, .new]) { [weak self] playerItem, _ in
|
||||||
guard let self = self else {
|
guard let self = self else {
|
||||||
@ -422,11 +477,9 @@ final class PlayerModel: ObservableObject {
|
|||||||
addCurrentItemToHistory()
|
addCurrentItemToHistory()
|
||||||
resetQueue()
|
resetQueue()
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
avPlayerViewController!.dismiss(animated: true) { [weak self] in
|
controller?.dismiss(animated: true)
|
||||||
self?.controller!.dismiss(animated: true)
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
presentingPlayer = false
|
hide()
|
||||||
} else {
|
} else {
|
||||||
advanceToNextItem()
|
advanceToNextItem()
|
||||||
}
|
}
|
||||||
@ -621,4 +674,70 @@ final class PlayerModel: ObservableObject {
|
|||||||
currentItem = nil
|
currentItem = nil
|
||||||
player.replaceCurrentItem(with: nil)
|
player.replaceCurrentItem(with: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func closePiP() {
|
||||||
|
guard playingInPictureInPicture else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let wasPlaying = isPlaying
|
||||||
|
pause()
|
||||||
|
|
||||||
|
#if os(tvOS)
|
||||||
|
show()
|
||||||
|
closePipByReplacingItem(wasPlaying: wasPlaying)
|
||||||
|
#else
|
||||||
|
closePiPByNilingPlayer(wasPlaying: wasPlaying)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private func closePipByReplacingItem(wasPlaying: Bool) {
|
||||||
|
let item = player.currentItem
|
||||||
|
let time = player.currentTime()
|
||||||
|
|
||||||
|
self.player.replaceCurrentItem(with: nil)
|
||||||
|
|
||||||
|
guard !item.isNil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.player.seek(to: time)
|
||||||
|
self.player.replaceCurrentItem(with: item)
|
||||||
|
|
||||||
|
guard wasPlaying else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in
|
||||||
|
self?.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func closePiPByNilingPlayer(wasPlaying: Bool) {
|
||||||
|
controller?.playerView.player = nil
|
||||||
|
controller?.playerView.player = player
|
||||||
|
|
||||||
|
guard wasPlaying else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
||||||
|
self?.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if os(macOS)
|
||||||
|
var windowTitle: String {
|
||||||
|
currentVideo.isNil ? "Not playing" : "\(currentVideo!.title) - \(currentVideo!.author)"
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
func handleEnterForeground() {
|
||||||
|
guard closePiPAndOpenPlayerOnEnteringForeground, playingInPictureInPicture else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
show()
|
||||||
|
closePiP()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import AVFoundation
|
import AVKit
|
||||||
import Defaults
|
import Defaults
|
||||||
import Foundation
|
import Foundation
|
||||||
import Siesta
|
import Siesta
|
||||||
@ -29,7 +29,10 @@ extension PlayerModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func playNow(_ video: Video, at time: TimeInterval? = nil) {
|
func playNow(_ video: Video, at time: TimeInterval? = nil) {
|
||||||
player.replaceCurrentItem(with: nil)
|
if !playingInPictureInPicture || closePiPOnNavigation {
|
||||||
|
closePiP()
|
||||||
|
}
|
||||||
|
|
||||||
addCurrentItemToHistory()
|
addCurrentItemToHistory()
|
||||||
|
|
||||||
enqueueVideo(video, prepending: true) { _, item in
|
enqueueVideo(video, prepending: true) { _, item in
|
||||||
@ -38,7 +41,12 @@ extension PlayerModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func playItem(_ item: PlayerQueueItem, video: Video? = nil, at time: TimeInterval? = nil) {
|
func playItem(_ item: PlayerQueueItem, video: Video? = nil, at time: TimeInterval? = nil) {
|
||||||
|
if !playingInPictureInPicture {
|
||||||
|
player.replaceCurrentItem(with: nil)
|
||||||
|
}
|
||||||
|
|
||||||
comments.reset()
|
comments.reset()
|
||||||
|
stream = nil
|
||||||
currentItem = item
|
currentItem = item
|
||||||
|
|
||||||
if !time.isNil {
|
if !time.isNil {
|
||||||
@ -83,7 +91,6 @@ extension PlayerModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func advanceToItem(_ newItem: PlayerQueueItem, at time: TimeInterval? = nil) {
|
func advanceToItem(_ newItem: PlayerQueueItem, at time: TimeInterval? = nil) {
|
||||||
player.replaceCurrentItem(with: nil)
|
|
||||||
addCurrentItemToHistory()
|
addCurrentItemToHistory()
|
||||||
|
|
||||||
remove(newItem)
|
remove(newItem)
|
||||||
@ -116,7 +123,7 @@ extension PlayerModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func isAutoplaying(_ item: AVPlayerItem) -> Bool {
|
func isAutoplaying(_ item: AVPlayerItem) -> Bool {
|
||||||
player.currentItem == item && (presentingPlayer || playerNavigationLinkActive || playingInPictureInPicture)
|
player.currentItem == item
|
||||||
}
|
}
|
||||||
|
|
||||||
@discardableResult func enqueueVideo(
|
@discardableResult func enqueueVideo(
|
||||||
|
@ -66,7 +66,7 @@ extension PlayerModel {
|
|||||||
|
|
||||||
func rebuildTVMenu() {
|
func rebuildTVMenu() {
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
avPlayerViewController?.transportBarCustomMenuItems = [
|
controller?.playerView.transportBarCustomMenuItems = [
|
||||||
restoreLastSkippedSegmentAction,
|
restoreLastSkippedSegmentAction,
|
||||||
rateMenu,
|
rateMenu,
|
||||||
streamsMenu
|
streamsMenu
|
||||||
|
@ -38,6 +38,13 @@ extension Defaults.Keys {
|
|||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
static let commentsPlacement = Key<CommentsPlacement>("commentsPlacement", default: .separate)
|
static let commentsPlacement = Key<CommentsPlacement>("commentsPlacement", default: .separate)
|
||||||
#endif
|
#endif
|
||||||
|
static let pauseOnHidingPlayer = Key<Bool>("pauseOnHidingPlayer", default: true)
|
||||||
|
|
||||||
|
static let closePiPOnNavigation = Key<Bool>("closePiPOnNavigation", default: false)
|
||||||
|
static let closePiPOnOpeningPlayer = Key<Bool>("closePiPOnOpeningPlayer", default: false)
|
||||||
|
#if !os(macOS)
|
||||||
|
static let closePiPAndOpenPlayerOnEnteringForeground = Key<Bool>("closePiPAndOpenPlayerOnEnteringForeground", default: false)
|
||||||
|
#endif
|
||||||
|
|
||||||
static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: [])
|
static let recentlyOpened = Key<[RecentItem]>("recentlyOpened", default: [])
|
||||||
|
|
||||||
|
@ -62,10 +62,18 @@ struct MenuCommands: Commands {
|
|||||||
.disabled(model.player?.queue.isEmpty ?? true)
|
.disabled(model.player?.queue.isEmpty ?? true)
|
||||||
.keyboardShortcut("s")
|
.keyboardShortcut("s")
|
||||||
|
|
||||||
Button((model.player?.presentingPlayer ?? true) ? "Hide Player" : "Show Player") {
|
Button(togglePlayerLabel) {
|
||||||
model.player?.togglePlayer()
|
model.player?.togglePlayer()
|
||||||
}
|
}
|
||||||
.keyboardShortcut("o")
|
.keyboardShortcut("o")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var togglePlayerLabel: String {
|
||||||
|
#if os(macOS)
|
||||||
|
"Show Player"
|
||||||
|
#else
|
||||||
|
(model.player?.presentingPlayer ?? true) ? "Hide Player" : "Show Player"
|
||||||
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,6 +62,7 @@ struct AppSidebarNavigation: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.environment(\.navigationStyle, .sidebar)
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
.background(
|
.background(
|
||||||
EmptyView().fullScreenCover(isPresented: $player.presentingPlayer) {
|
EmptyView().fullScreenCover(isPresented: $player.presentingPlayer) {
|
||||||
@ -69,16 +70,7 @@ struct AppSidebarNavigation: View {
|
|||||||
.environment(\.navigationStyle, .sidebar)
|
.environment(\.navigationStyle, .sidebar)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
#elseif os(macOS)
|
|
||||||
.background(
|
|
||||||
EmptyView().sheet(isPresented: $player.presentingPlayer) {
|
|
||||||
videoPlayer
|
|
||||||
.frame(minWidth: 1000, minHeight: 750)
|
|
||||||
.environment(\.navigationStyle, .sidebar)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
#endif
|
#endif
|
||||||
.environment(\.navigationStyle, .sidebar)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private var videoPlayer: some View {
|
private var videoPlayer: some View {
|
||||||
|
@ -7,16 +7,16 @@ import Siesta
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@StateObject private var accounts = AccountsModel()
|
@EnvironmentObject<AccountsModel> private var accounts
|
||||||
@StateObject private var comments = CommentsModel()
|
@EnvironmentObject<CommentsModel> private var comments
|
||||||
@StateObject private var instances = InstancesModel()
|
@EnvironmentObject<InstancesModel> private var instances
|
||||||
@StateObject private var navigation = NavigationModel()
|
@EnvironmentObject<NavigationModel> private var navigation
|
||||||
@StateObject private var player = PlayerModel()
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
@StateObject private var playlists = PlaylistsModel()
|
@EnvironmentObject<PlaylistsModel> private var playlists
|
||||||
@StateObject private var recents = RecentsModel()
|
@EnvironmentObject<RecentsModel> private var recents
|
||||||
@StateObject private var search = SearchModel()
|
@EnvironmentObject<SearchModel> private var search
|
||||||
@StateObject private var subscriptions = SubscriptionsModel()
|
@EnvironmentObject<SubscriptionsModel> private var subscriptions
|
||||||
@StateObject private var thumbnailsModel = ThumbnailsModel()
|
@EnvironmentObject<ThumbnailsModel> private var thumbnailsModel
|
||||||
|
|
||||||
@EnvironmentObject<MenuModel> private var menu
|
@EnvironmentObject<MenuModel> private var menu
|
||||||
|
|
||||||
@ -61,7 +61,6 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
.handlesExternalEvents(preferring: Set(["*"]), allowing: Set(["*"]))
|
|
||||||
.onOpenURL(perform: handleOpenedURL)
|
.onOpenURL(perform: handleOpenedURL)
|
||||||
.background(
|
.background(
|
||||||
EmptyView().sheet(isPresented: $navigation.presentingAddToPlaylist) {
|
EmptyView().sheet(isPresented: $navigation.presentingAddToPlaylist) {
|
||||||
@ -162,7 +161,7 @@ struct ContentView: View {
|
|||||||
if let video: Video = response.typedContent() {
|
if let video: Video = response.typedContent() {
|
||||||
player.addCurrentItemToHistory()
|
player.addCurrentItemToHistory()
|
||||||
self.player.playNow(video, at: parser.time)
|
self.player.playNow(video, at: parser.time)
|
||||||
self.player.presentPlayer()
|
self.player.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,9 @@ struct PlaybackBar: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack {
|
HStack {
|
||||||
|
#if !os(macOS)
|
||||||
closeButton
|
closeButton
|
||||||
|
#endif
|
||||||
|
|
||||||
if player.currentItem != nil {
|
if player.currentItem != nil {
|
||||||
HStack {
|
HStack {
|
||||||
@ -20,6 +22,9 @@ struct PlaybackBar: View {
|
|||||||
rateMenu
|
rateMenu
|
||||||
}
|
}
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
|
#if os(macOS)
|
||||||
|
.padding(.leading, 4)
|
||||||
|
#endif
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
@ -68,7 +73,7 @@ struct PlaybackBar: View {
|
|||||||
message: Text(player.playerError?.localizedDescription ?? "")
|
message: Text(player.playerError?.localizedDescription ?? "")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.frame(minWidth: 0, maxWidth: .infinity)
|
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 20)
|
||||||
.padding(4)
|
.padding(4)
|
||||||
.background(colorScheme == .dark ? Color.black : Color.white)
|
.background(colorScheme == .dark ? Color.black : Color.white)
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import Defaults
|
||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
@ -8,6 +9,8 @@ struct PlayerQueueRow: View {
|
|||||||
|
|
||||||
@EnvironmentObject<PlayerModel> private var player
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
|
|
||||||
|
@Default(.closePiPOnNavigation) var closePiPOnNavigation
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
Button {
|
Button {
|
||||||
@ -24,6 +27,10 @@ struct PlayerQueueRow: View {
|
|||||||
fullScreen = false
|
fullScreen = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if closePiPOnNavigation, player.playingInPictureInPicture {
|
||||||
|
player.closePiP()
|
||||||
|
}
|
||||||
} label: {
|
} label: {
|
||||||
VideoBanner(video: item.video, playbackTime: item.playbackTime, videoDuration: item.videoDuration)
|
VideoBanner(video: item.video, playbackTime: item.playbackTime, videoDuration: item.videoDuration)
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import AVKit
|
import AVKit
|
||||||
|
import Defaults
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class PlayerViewController: UIViewController {
|
final class PlayerViewController: UIViewController {
|
||||||
@ -7,11 +8,11 @@ final class PlayerViewController: UIViewController {
|
|||||||
var navigationModel: NavigationModel!
|
var navigationModel: NavigationModel!
|
||||||
var playerModel: PlayerModel!
|
var playerModel: PlayerModel!
|
||||||
var subscriptionsModel: SubscriptionsModel!
|
var subscriptionsModel: SubscriptionsModel!
|
||||||
var playerViewController = AVPlayerViewController()
|
var playerView = AVPlayerViewController()
|
||||||
|
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
var aspectRatio: Double? {
|
var aspectRatio: Double? {
|
||||||
let ratio = Double(playerViewController.videoBounds.width) / Double(playerViewController.videoBounds.height)
|
let ratio = Double(playerView.videoBounds.width) / Double(playerView.videoBounds.height)
|
||||||
|
|
||||||
guard ratio.isFinite else {
|
guard ratio.isFinite else {
|
||||||
return VideoPlayerView.defaultAspectRatio // swiftlint:disable:this implicit_return
|
return VideoPlayerView.defaultAspectRatio // swiftlint:disable:this implicit_return
|
||||||
@ -27,24 +28,35 @@ final class PlayerViewController: UIViewController {
|
|||||||
loadPlayer()
|
loadPlayer()
|
||||||
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
if !playerViewController.isBeingPresented, !playerViewController.isBeingDismissed {
|
if !playerView.isBeingPresented, !playerView.isBeingDismissed {
|
||||||
present(playerViewController, animated: false)
|
present(playerView, animated: false)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if os(tvOS)
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
func loadPlayer() {
|
func loadPlayer() {
|
||||||
guard !playerLoaded else {
|
guard !playerLoaded else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
playerModel.controller = self
|
playerModel.controller = self
|
||||||
playerViewController.player = playerModel.player
|
playerView.player = playerModel.player
|
||||||
playerViewController.allowsPictureInPicturePlayback = true
|
playerView.allowsPictureInPicturePlayback = true
|
||||||
playerViewController.delegate = self
|
playerView.delegate = self
|
||||||
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
playerModel.avPlayerViewController = playerViewController
|
|
||||||
var infoViewControllers = [UIHostingController<AnyView>]()
|
var infoViewControllers = [UIHostingController<AnyView>]()
|
||||||
if CommentsModel.enabled {
|
if CommentsModel.enabled {
|
||||||
infoViewControllers.append(infoViewController([.comments], title: "Comments"))
|
infoViewControllers.append(infoViewController([.comments], title: "Comments"))
|
||||||
@ -54,7 +66,7 @@ final class PlayerViewController: UIViewController {
|
|||||||
infoViewController([.playingNext, .playedPreviously], title: "Playing Next")
|
infoViewController([.playingNext, .playedPreviously], title: "Playing Next")
|
||||||
])
|
])
|
||||||
|
|
||||||
playerViewController.customInfoViewControllers = infoViewControllers
|
playerView.customInfoViewControllers = infoViewControllers
|
||||||
#else
|
#else
|
||||||
embedViewController()
|
embedViewController()
|
||||||
#endif
|
#endif
|
||||||
@ -81,12 +93,12 @@ final class PlayerViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
func embedViewController() {
|
func embedViewController() {
|
||||||
playerViewController.view.frame = view.bounds
|
playerView.view.frame = view.bounds
|
||||||
|
|
||||||
addChild(playerViewController)
|
addChild(playerView)
|
||||||
view.addSubview(playerViewController.view)
|
view.addSubview(playerView.view)
|
||||||
|
|
||||||
playerViewController.didMove(toParent: self)
|
playerView.didMove(toParent: self)
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@ -127,19 +139,19 @@ extension PlayerViewController: AVPlayerViewControllerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func playerViewController(
|
func playerViewController(
|
||||||
_ playerViewController: AVPlayerViewController,
|
_: AVPlayerViewController,
|
||||||
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
|
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
|
||||||
) {
|
) {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
if self.navigationModel.presentingChannel {
|
if self.navigationModel.presentingChannel {
|
||||||
self.playerModel.playerNavigationLinkActive = true
|
self.playerModel.playerNavigationLinkActive = true
|
||||||
} else {
|
} else {
|
||||||
self.playerModel.presentPlayer()
|
self.playerModel.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
if self.playerModel.playingInPictureInPicture {
|
if self.playerModel.playingInPictureInPicture {
|
||||||
self.present(playerViewController, animated: false) {
|
self.present(self.playerView, animated: false) {
|
||||||
completionHandler(true)
|
completionHandler(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,7 +31,8 @@ struct VideoPlayerView: View {
|
|||||||
HSplitView {
|
HSplitView {
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
.frame(idealWidth: 1000, maxWidth: 1100, minHeight: 700)
|
.onOpenURL(perform: handleOpenedURL)
|
||||||
|
.frame(minWidth: 950, minHeight: 700)
|
||||||
#else
|
#else
|
||||||
GeometryReader { geometry in
|
GeometryReader { geometry in
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
@ -66,15 +67,16 @@ struct VideoPlayerView: View {
|
|||||||
|
|
||||||
if player.currentItem.isNil {
|
if player.currentItem.isNil {
|
||||||
playerPlaceholder(geometry: geometry)
|
playerPlaceholder(geometry: geometry)
|
||||||
|
} else if player.playingInPictureInPicture {
|
||||||
|
pictureInPicturePlaceholder(geometry: geometry)
|
||||||
} else {
|
} else {
|
||||||
#if os(macOS)
|
|
||||||
Player()
|
|
||||||
.modifier(VideoPlayerSizeModifier(geometry: geometry, aspectRatio: player.controller?.aspectRatio))
|
|
||||||
|
|
||||||
#else
|
|
||||||
player.playerView
|
player.playerView
|
||||||
.modifier(VideoPlayerSizeModifier(geometry: geometry, aspectRatio: player.controller?.aspectRatio))
|
.modifier(
|
||||||
#endif
|
VideoPlayerSizeModifier(
|
||||||
|
geometry: geometry,
|
||||||
|
aspectRatio: player.controller?.aspectRatio
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
@ -143,6 +145,35 @@ struct VideoPlayerView: View {
|
|||||||
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: geometry.size.width / VideoPlayerView.defaultAspectRatio)
|
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: geometry.size.width / VideoPlayerView.defaultAspectRatio)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func pictureInPicturePlaceholder(geometry: GeometryProxy) -> some View {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
VStack(spacing: 10) {
|
||||||
|
#if !os(tvOS)
|
||||||
|
Image(systemName: "pip")
|
||||||
|
.font(.system(size: 120))
|
||||||
|
#endif
|
||||||
|
|
||||||
|
Text("Playing in Picture in Picture")
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.contextMenu {
|
||||||
|
Button {
|
||||||
|
player.closePiP()
|
||||||
|
} label: {
|
||||||
|
Label("Exit Picture in Picture", systemImage: "pip.exit")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: geometry.size.width / VideoPlayerView.defaultAspectRatio)
|
||||||
|
}
|
||||||
|
|
||||||
var sidebarQueue: Bool {
|
var sidebarQueue: Bool {
|
||||||
switch Defaults[.playerSidebar] {
|
switch Defaults[.playerSidebar] {
|
||||||
case .never:
|
case .never:
|
||||||
@ -160,6 +191,27 @@ struct VideoPlayerView: View {
|
|||||||
set: { _ in }
|
set: { _ in }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if !os(tvOS)
|
||||||
|
func handleOpenedURL(_ url: URL) {
|
||||||
|
guard !player.accounts.current.isNil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let parser = VideoURLParser(url: url)
|
||||||
|
|
||||||
|
guard let id = parser.id else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
player.accounts.api.video(id).load().onSuccess { response in
|
||||||
|
if let video: Video = response.typedContent() {
|
||||||
|
self.player.playNow(video, at: parser.time)
|
||||||
|
self.player.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
struct VideoPlayerView_Previews: PreviewProvider {
|
struct VideoPlayerView_Previews: PreviewProvider {
|
||||||
|
@ -144,7 +144,7 @@ struct PlaylistsView: View {
|
|||||||
|
|
||||||
Button {
|
Button {
|
||||||
player.playAll(items.compactMap(\.video))
|
player.playAll(items.compactMap(\.video))
|
||||||
player.presentPlayer()
|
player.show()
|
||||||
} label: {
|
} label: {
|
||||||
HStack(spacing: 15) {
|
HStack(spacing: 15) {
|
||||||
Image(systemName: "play.fill")
|
Image(systemName: "play.fill")
|
||||||
|
@ -9,6 +9,12 @@ struct PlaybackSettings: View {
|
|||||||
@Default(.showKeywords) private var showKeywords
|
@Default(.showKeywords) private var showKeywords
|
||||||
@Default(.showChannelSubscribers) private var channelSubscribers
|
@Default(.showChannelSubscribers) private var channelSubscribers
|
||||||
@Default(.saveHistory) private var saveHistory
|
@Default(.saveHistory) private var saveHistory
|
||||||
|
@Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer
|
||||||
|
@Default(.closePiPOnNavigation) private var closePiPOnNavigation
|
||||||
|
@Default(.closePiPOnOpeningPlayer) private var closePiPOnOpeningPlayer
|
||||||
|
#if !os(macOS)
|
||||||
|
@Default(.closePiPAndOpenPlayerOnEnteringForeground) private var closePiPAndOpenPlayerOnEnteringForeground
|
||||||
|
#endif
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
private var idiom: UIUserInterfaceIdiom {
|
private var idiom: UIUserInterfaceIdiom {
|
||||||
@ -29,6 +35,13 @@ struct PlaybackSettings: View {
|
|||||||
|
|
||||||
keywordsToggle
|
keywordsToggle
|
||||||
channelSubscribersToggle
|
channelSubscribersToggle
|
||||||
|
pauseOnHidingPlayerToggle
|
||||||
|
}
|
||||||
|
|
||||||
|
Section(header: SettingsHeader(text: "Picture in Picture")) {
|
||||||
|
closePiPOnNavigationToggle
|
||||||
|
closePiPOnOpeningPlayerToggle
|
||||||
|
closePiPAndOpenPlayerOnEnteringForegroundToggle
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
Section(header: SettingsHeader(text: "Source")) {
|
Section(header: SettingsHeader(text: "Source")) {
|
||||||
@ -47,6 +60,15 @@ struct PlaybackSettings: View {
|
|||||||
|
|
||||||
keywordsToggle
|
keywordsToggle
|
||||||
channelSubscribersToggle
|
channelSubscribersToggle
|
||||||
|
pauseOnHidingPlayerToggle
|
||||||
|
|
||||||
|
Section(header: SettingsHeader(text: "Picture in Picture")) {
|
||||||
|
closePiPOnNavigationToggle
|
||||||
|
closePiPOnOpeningPlayerToggle
|
||||||
|
#if !os(macOS)
|
||||||
|
closePiPAndOpenPlayerOnEnteringForegroundToggle
|
||||||
|
#endif
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,6 +136,24 @@ struct PlaybackSettings: View {
|
|||||||
private var channelSubscribersToggle: some View {
|
private var channelSubscribersToggle: some View {
|
||||||
Toggle("Show channel subscribers count", isOn: $channelSubscribers)
|
Toggle("Show channel subscribers count", isOn: $channelSubscribers)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var pauseOnHidingPlayerToggle: some View {
|
||||||
|
Toggle("Pause when player is closed", isOn: $pauseOnHidingPlayer)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var closePiPOnNavigationToggle: some View {
|
||||||
|
Toggle("Close PiP when starting playing other video", isOn: $closePiPOnNavigation)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var closePiPOnOpeningPlayerToggle: some View {
|
||||||
|
Toggle("Close PiP when player is opened", isOn: $closePiPOnOpeningPlayer)
|
||||||
|
}
|
||||||
|
|
||||||
|
#if !os(macOS)
|
||||||
|
private var closePiPAndOpenPlayerOnEnteringForegroundToggle: some View {
|
||||||
|
Toggle("Close PiP and open player when application enters foreground", isOn: $closePiPAndOpenPlayerOnEnteringForeground)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
struct PlaybackSettings_Previews: PreviewProvider {
|
struct PlaybackSettings_Previews: PreviewProvider {
|
||||||
|
@ -31,7 +31,7 @@ struct VideoCell: View {
|
|||||||
if inNavigationView {
|
if inNavigationView {
|
||||||
player.playerNavigationLinkActive = true
|
player.playerNavigationLinkActive = true
|
||||||
} else {
|
} else {
|
||||||
player.presentPlayer()
|
player.show()
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
content
|
content
|
||||||
|
@ -27,7 +27,7 @@ struct PlayerControlsView<Content: View>: View {
|
|||||||
private var controls: some View {
|
private var controls: some View {
|
||||||
let controls = HStack {
|
let controls = HStack {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
model.presentingPlayer.toggle()
|
model.togglePlayer()
|
||||||
}) {
|
}) {
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading, spacing: 3) {
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
@ -96,7 +96,7 @@ struct PlayerControlsView<Content: View>: View {
|
|||||||
.borderBottom(height: navigationStyle == .sidebar ? 0 : 0.4, color: Color("ControlsBorderColor"))
|
.borderBottom(height: navigationStyle == .sidebar ? 0 : 0.4, color: Color("ControlsBorderColor"))
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
.onSwipeGesture(up: {
|
.onSwipeGesture(up: {
|
||||||
model.presentingPlayer = true
|
model.show()
|
||||||
})
|
})
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@ -64,7 +64,7 @@ struct VideoContextMenuView: View {
|
|||||||
if inNavigationView {
|
if inNavigationView {
|
||||||
playerNavigationLinkActive = true
|
playerNavigationLinkActive = true
|
||||||
} else {
|
} else {
|
||||||
player.presentPlayer()
|
player.show()
|
||||||
}
|
}
|
||||||
} label: {
|
} label: {
|
||||||
Label("Play Now", systemImage: "play")
|
Label("Play Now", systemImage: "play")
|
||||||
|
@ -8,15 +8,45 @@ struct YatteeApp: App {
|
|||||||
@StateObject private var updater = UpdaterModel()
|
@StateObject private var updater = UpdaterModel()
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
@StateObject private var accounts = AccountsModel()
|
||||||
|
@StateObject private var comments = CommentsModel()
|
||||||
|
@StateObject private var instances = InstancesModel()
|
||||||
@StateObject private var menu = MenuModel()
|
@StateObject private var menu = MenuModel()
|
||||||
|
@StateObject private var navigation = NavigationModel()
|
||||||
|
@StateObject private var player = PlayerModel()
|
||||||
|
@StateObject private var playlists = PlaylistsModel()
|
||||||
|
@StateObject private var recents = RecentsModel()
|
||||||
|
@StateObject private var search = SearchModel()
|
||||||
|
@StateObject private var subscriptions = SubscriptionsModel()
|
||||||
|
@StateObject private var thumbnails = ThumbnailsModel()
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView()
|
ContentView()
|
||||||
|
.environmentObject(accounts)
|
||||||
|
.environmentObject(comments)
|
||||||
|
.environmentObject(instances)
|
||||||
|
.environmentObject(navigation)
|
||||||
|
.environmentObject(player)
|
||||||
|
.environmentObject(playlists)
|
||||||
|
.environmentObject(recents)
|
||||||
|
.environmentObject(subscriptions)
|
||||||
|
.environmentObject(thumbnails)
|
||||||
.environmentObject(menu)
|
.environmentObject(menu)
|
||||||
|
.environmentObject(search)
|
||||||
|
#if !os(macOS)
|
||||||
|
.onReceive(
|
||||||
|
NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)
|
||||||
|
) { _ in
|
||||||
|
player.handleEnterForeground()
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
#if !os(tvOS)
|
||||||
|
.handlesExternalEvents(preferring: Set(["watch"]), allowing: Set(["watch"]))
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
#if !os(tvOS)
|
#if !os(tvOS)
|
||||||
.handlesExternalEvents(matching: Set(["*"]))
|
.handlesExternalEvents(matching: Set(arrayLiteral: "watch"))
|
||||||
.commands {
|
.commands {
|
||||||
SidebarCommands()
|
SidebarCommands()
|
||||||
|
|
||||||
@ -34,6 +64,24 @@ struct YatteeApp: App {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if os(macOS)
|
#if os(macOS)
|
||||||
|
WindowGroup(player.windowTitle) {
|
||||||
|
VideoPlayerView()
|
||||||
|
.onAppear { player.presentingPlayer = true }
|
||||||
|
.onDisappear { player.presentingPlayer = false }
|
||||||
|
.environment(\.navigationStyle, .sidebar)
|
||||||
|
.environmentObject(accounts)
|
||||||
|
.environmentObject(comments)
|
||||||
|
.environmentObject(instances)
|
||||||
|
.environmentObject(navigation)
|
||||||
|
.environmentObject(player)
|
||||||
|
.environmentObject(playlists)
|
||||||
|
.environmentObject(recents)
|
||||||
|
.environmentObject(subscriptions)
|
||||||
|
.environmentObject(thumbnails)
|
||||||
|
.handlesExternalEvents(preferring: Set(["player"]), allowing: Set(["player"]))
|
||||||
|
}
|
||||||
|
.handlesExternalEvents(matching: Set(["player"]))
|
||||||
|
|
||||||
Settings {
|
Settings {
|
||||||
SettingsView()
|
SettingsView()
|
||||||
.environmentObject(AccountsModel())
|
.environmentObject(AccountsModel())
|
||||||
|
@ -223,6 +223,7 @@
|
|||||||
37732FF22703A26300F04329 /* AccountValidationStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37732FEF2703A26300F04329 /* AccountValidationStatus.swift */; };
|
37732FF22703A26300F04329 /* AccountValidationStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37732FEF2703A26300F04329 /* AccountValidationStatus.swift */; };
|
||||||
37732FF42703D32400F04329 /* Sidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37732FF32703D32400F04329 /* Sidebar.swift */; };
|
37732FF42703D32400F04329 /* Sidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37732FF32703D32400F04329 /* Sidebar.swift */; };
|
||||||
37732FF52703D32400F04329 /* Sidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37732FF32703D32400F04329 /* Sidebar.swift */; };
|
37732FF52703D32400F04329 /* Sidebar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37732FF32703D32400F04329 /* Sidebar.swift */; };
|
||||||
|
37737786276F9858000521C1 /* OpenWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37737785276F9858000521C1 /* OpenWindow.swift */; };
|
||||||
3774122A27387B6C00423605 /* InstancesModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3774122927387B6C00423605 /* InstancesModelTests.swift */; };
|
3774122A27387B6C00423605 /* InstancesModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3774122927387B6C00423605 /* InstancesModelTests.swift */; };
|
||||||
3774122F27387C7600423605 /* VideosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376A33DF2720CAD6000C1D6B /* VideosApp.swift */; };
|
3774122F27387C7600423605 /* VideosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 376A33DF2720CAD6000C1D6B /* VideosApp.swift */; };
|
||||||
3774123327387CB000423605 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; };
|
3774123327387CB000423605 /* Defaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 372915E52687E3B900F5A35B /* Defaults.swift */; };
|
||||||
@ -642,6 +643,7 @@
|
|||||||
376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Instance+Fixtures.swift"; sourceTree = "<group>"; };
|
376CD21526FBE18D001E1AC1 /* Instance+Fixtures.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Instance+Fixtures.swift"; sourceTree = "<group>"; };
|
||||||
37732FEF2703A26300F04329 /* AccountValidationStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountValidationStatus.swift; sourceTree = "<group>"; };
|
37732FEF2703A26300F04329 /* AccountValidationStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountValidationStatus.swift; sourceTree = "<group>"; };
|
||||||
37732FF32703D32400F04329 /* Sidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar.swift; sourceTree = "<group>"; };
|
37732FF32703D32400F04329 /* Sidebar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sidebar.swift; sourceTree = "<group>"; };
|
||||||
|
37737785276F9858000521C1 /* OpenWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenWindow.swift; sourceTree = "<group>"; };
|
||||||
3774122927387B6C00423605 /* InstancesModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesModelTests.swift; sourceTree = "<group>"; };
|
3774122927387B6C00423605 /* InstancesModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstancesModelTests.swift; sourceTree = "<group>"; };
|
||||||
377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedContentAccessors.swift; sourceTree = "<group>"; };
|
377A20A82693C9A2002842B8 /* TypedContentAccessors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedContentAccessors.swift; sourceTree = "<group>"; };
|
||||||
3782B94E27553A6700990149 /* SearchSuggestions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSuggestions.swift; sourceTree = "<group>"; };
|
3782B94E27553A6700990149 /* SearchSuggestions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSuggestions.swift; sourceTree = "<group>"; };
|
||||||
@ -1112,6 +1114,7 @@
|
|||||||
37BE7AF227601DBF00DBECED /* Updates */,
|
37BE7AF227601DBF00DBECED /* Updates */,
|
||||||
374C0542272496E4009BDDBE /* AppDelegate.swift */,
|
374C0542272496E4009BDDBE /* AppDelegate.swift */,
|
||||||
37FD43DB270470B70073EE42 /* InstancesSettings.swift */,
|
37FD43DB270470B70073EE42 /* InstancesSettings.swift */,
|
||||||
|
37737785276F9858000521C1 /* OpenWindow.swift */,
|
||||||
374108D0272B11B2006C5CC8 /* PictureInPictureDelegate.swift */,
|
374108D0272B11B2006C5CC8 /* PictureInPictureDelegate.swift */,
|
||||||
37BE0BDB26A2367F0092E2DB /* Player.swift */,
|
37BE0BDB26A2367F0092E2DB /* Player.swift */,
|
||||||
37BE0BD926A214630092E2DB /* PlayerViewController.swift */,
|
37BE0BD926A214630092E2DB /* PlayerViewController.swift */,
|
||||||
@ -1924,6 +1927,7 @@
|
|||||||
374710062755291C00CE0F87 /* SearchField.swift in Sources */,
|
374710062755291C00CE0F87 /* SearchField.swift in Sources */,
|
||||||
378AE93F274EDFB5006A4EE1 /* Tint+Backport.swift in Sources */,
|
378AE93F274EDFB5006A4EE1 /* Tint+Backport.swift in Sources */,
|
||||||
37C194C826F6A9C8005D3B96 /* RecentsModel.swift in Sources */,
|
37C194C826F6A9C8005D3B96 /* RecentsModel.swift in Sources */,
|
||||||
|
37737786276F9858000521C1 /* OpenWindow.swift in Sources */,
|
||||||
37BE0BDC26A2367F0092E2DB /* Player.swift in Sources */,
|
37BE0BDC26A2367F0092E2DB /* Player.swift in Sources */,
|
||||||
3743CA53270F284F00E4D32B /* View+Borders.swift in Sources */,
|
3743CA53270F284F00E4D32B /* View+Borders.swift in Sources */,
|
||||||
37599F39272B4D740087F250 /* FavoriteButton.swift in Sources */,
|
37599F39272B4D740087F250 /* FavoriteButton.swift in Sources */,
|
||||||
@ -2598,9 +2602,9 @@
|
|||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_FILE = iOS/Info.plist;
|
INFOPLIST_FILE = iOS/Info.plist;
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
|
INFOPLIST_KEY_UIRequiresFullScreen = NO;
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
@ -15,6 +15,11 @@
|
|||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
|
<key>UIApplicationSceneManifest</key>
|
||||||
|
<dict>
|
||||||
|
<key>UIApplicationSupportsMultipleScenes</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIBackgroundModes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>audio</string>
|
<string>audio</string>
|
||||||
|
@ -94,7 +94,7 @@ struct InstancesSettings: View {
|
|||||||
}
|
}
|
||||||
.labelsHidden()
|
.labelsHidden()
|
||||||
|
|
||||||
Text("Used to create links from videos, channels and playlist")
|
Text("Used to create links from videos, channels and playlists")
|
||||||
.font(.caption)
|
.font(.caption)
|
||||||
.foregroundColor(.secondary)
|
.foregroundColor(.secondary)
|
||||||
}
|
}
|
||||||
|
40
macOS/OpenWindow.swift
Normal file
40
macOS/OpenWindow.swift
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import AppKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum OpenWindow: String, CaseIterable {
|
||||||
|
case player, main
|
||||||
|
|
||||||
|
var window: NSWindow? {
|
||||||
|
// this is not solid but works as long as there is only two windows in the app
|
||||||
|
// needs to be changed in case we ever have more windows to handle
|
||||||
|
|
||||||
|
switch self {
|
||||||
|
case .player:
|
||||||
|
return NSApplication.shared.windows.last
|
||||||
|
case .main:
|
||||||
|
return NSApplication.shared.windows.first
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func focus() {
|
||||||
|
window?.makeKeyAndOrderFront(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
var location: String {
|
||||||
|
switch self {
|
||||||
|
case .player:
|
||||||
|
return rawValue
|
||||||
|
case .main:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func open() {
|
||||||
|
switch self {
|
||||||
|
case .player:
|
||||||
|
NSWorkspace.shared.open(URL(string: "yattee://player")!)
|
||||||
|
case .main:
|
||||||
|
Self.main.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -11,14 +11,14 @@ final class PictureInPictureDelegate: NSObject, AVPlayerViewPictureInPictureDele
|
|||||||
func playerViewWillStartPicture(inPicture _: AVPlayerView) {
|
func playerViewWillStartPicture(inPicture _: AVPlayerView) {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
||||||
self?.playerModel.playingInPictureInPicture = true
|
self?.playerModel.playingInPictureInPicture = true
|
||||||
self?.playerModel.presentingPlayer = false
|
self?.playerModel.hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func playerViewWillStopPicture(inPicture _: AVPlayerView) {
|
func playerViewWillStopPicture(inPicture _: AVPlayerView) {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
||||||
self?.playerModel.playingInPictureInPicture = false
|
self?.playerModel.playingInPictureInPicture = false
|
||||||
self?.playerModel.presentPlayer()
|
self?.playerModel.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -27,7 +27,7 @@ final class PictureInPictureDelegate: NSObject, AVPlayerViewPictureInPictureDele
|
|||||||
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: (Bool) -> Void
|
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: (Bool) -> Void
|
||||||
) {
|
) {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
||||||
self?.playerModel.presentingPlayer = true
|
self?.playerModel.show()
|
||||||
}
|
}
|
||||||
completionHandler(true)
|
completionHandler(true)
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import SwiftUI
|
|||||||
struct Player: NSViewControllerRepresentable {
|
struct Player: NSViewControllerRepresentable {
|
||||||
@EnvironmentObject<PlayerModel> private var player
|
@EnvironmentObject<PlayerModel> private var player
|
||||||
|
|
||||||
var controller: PlayerViewController?
|
@State private var controller: PlayerViewController?
|
||||||
|
|
||||||
init(controller: PlayerViewController? = nil) {
|
init(controller: PlayerViewController? = nil) {
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
|
@ -33,7 +33,7 @@ struct NowPlayingView: View {
|
|||||||
if sections.contains(.nowPlaying), let item = player.currentItem {
|
if sections.contains(.nowPlaying), let item = player.currentItem {
|
||||||
Section(header: Text("Now Playing")) {
|
Section(header: Text("Now Playing")) {
|
||||||
Button {
|
Button {
|
||||||
player.presentPlayer()
|
player.show()
|
||||||
} label: {
|
} label: {
|
||||||
VideoBanner(video: item.video)
|
VideoBanner(video: item.video)
|
||||||
}
|
}
|
||||||
@ -59,7 +59,7 @@ struct NowPlayingView: View {
|
|||||||
ForEach(player.queue) { item in
|
ForEach(player.queue) { item in
|
||||||
Button {
|
Button {
|
||||||
player.advanceToItem(item)
|
player.advanceToItem(item)
|
||||||
player.presentPlayer()
|
player.show()
|
||||||
} label: {
|
} label: {
|
||||||
VideoBanner(video: item.video)
|
VideoBanner(video: item.video)
|
||||||
}
|
}
|
||||||
@ -77,7 +77,7 @@ struct NowPlayingView: View {
|
|||||||
ForEach(player.currentVideo!.related) { video in
|
ForEach(player.currentVideo!.related) { video in
|
||||||
Button {
|
Button {
|
||||||
player.playNow(video)
|
player.playNow(video)
|
||||||
player.presentPlayer()
|
player.show()
|
||||||
} label: {
|
} label: {
|
||||||
VideoBanner(video: video)
|
VideoBanner(video: video)
|
||||||
}
|
}
|
||||||
@ -99,7 +99,7 @@ struct NowPlayingView: View {
|
|||||||
ForEach(player.history) { item in
|
ForEach(player.history) { item in
|
||||||
Button {
|
Button {
|
||||||
player.playHistory(item)
|
player.playHistory(item)
|
||||||
player.presentPlayer()
|
player.show()
|
||||||
} label: {
|
} label: {
|
||||||
VideoBanner(video: item.video, playbackTime: item.playbackTime, videoDuration: item.videoDuration)
|
VideoBanner(video: item.video, playbackTime: item.playbackTime, videoDuration: item.videoDuration)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user