From f607e6e27600a7eb14afcf1f2127a5adca6c0e1c Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Fri, 2 Sep 2022 01:05:31 +0200 Subject: [PATCH] Model improvements --- Fixtures/View+Fixtures.swift | 2 +- Model/NetworkStateModel.swift | 12 +- Model/Player/Backends/AVPlayerBackend.swift | 16 +-- Model/Player/Backends/MPVBackend.swift | 22 +--- Model/Player/Backends/PlayerBackend.swift | 15 +-- Model/Player/ControlsOverlayModel.swift | 21 +++ Model/Player/PlayerControlsModel.swift | 26 ++-- Model/Player/PlayerModel.swift | 57 +------- Model/SeekModel.swift | 2 +- Shared/Player/Controls/OSD/Buffering.swift | 10 +- Shared/Player/Controls/OSD/NetworkState.swift | 7 +- Shared/Player/Controls/OSD/Seek.swift | 122 +++++++++--------- Shared/Player/Controls/PlayerControls.swift | 29 ++--- Shared/Player/Controls/TVControls.swift | 6 +- Shared/Player/Controls/TimelineView.swift | 10 +- Shared/Player/PlayerBackendView.swift | 18 +-- Shared/Player/PlayerDragGesture.swift | 4 +- Shared/Player/PlayerGestures.swift | 3 + Shared/Player/VideoDetails.swift | 10 +- Shared/Player/VideoPlayerView.swift | 51 ++++---- Shared/Views/ControlsBar.swift | 4 +- Shared/YatteeApp.swift | 9 +- Yattee.xcodeproj/project.pbxproj | 8 ++ 23 files changed, 194 insertions(+), 270 deletions(-) create mode 100644 Model/Player/ControlsOverlayModel.swift diff --git a/Fixtures/View+Fixtures.swift b/Fixtures/View+Fixtures.swift index fe593f5b..6bc15d0e 100644 --- a/Fixtures/View+Fixtures.swift +++ b/Fixtures/View+Fixtures.swift @@ -78,7 +78,7 @@ struct FixtureEnvironmentObjectsModifier: ViewModifier { } private var playerControls: PlayerControlsModel { - PlayerControlsModel(presentingControls: true, presentingControlsOverlay: false, player: player) + PlayerControlsModel(presentingControls: true) } private var subscriptions: SubscriptionsModel { diff --git a/Model/NetworkStateModel.swift b/Model/NetworkStateModel.swift index cc06af26..e5d87d9e 100644 --- a/Model/NetworkStateModel.swift +++ b/Model/NetworkStateModel.swift @@ -1,11 +1,19 @@ import Foundation final class NetworkStateModel: ObservableObject { + static var shared = NetworkStateModel() + @Published var pausedForCache = false @Published var cacheDuration = 0.0 @Published var bufferingState = 0.0 - var player: PlayerModel! + private var player: PlayerModel! { .shared } + private let controlsOverlayModel = ControlOverlaysModel.shared + + var osdVisible: Bool { + guard let player = player else { return false } + return player.isPlaying && ((player.activeBackend == .mpv && pausedForCache) || player.isSeeking) + } var fullStateText: String? { guard let bufferingStateText = bufferingStateText, @@ -34,7 +42,7 @@ final class NetworkStateModel: ObservableObject { var needsUpdates: Bool { if let player = player { - return !player.currentItem.isNil && (pausedForCache || player.isSeeking || player.isLoadingVideo || player.controls.presentingControlsOverlay) + return !player.currentItem.isNil && (pausedForCache || player.isSeeking || player.isLoadingVideo || controlsOverlayModel.presenting) } return false diff --git a/Model/Player/Backends/AVPlayerBackend.swift b/Model/Player/Backends/AVPlayerBackend.swift index 0fca77a2..c388eaad 100644 --- a/Model/Player/Backends/AVPlayerBackend.swift +++ b/Model/Player/Backends/AVPlayerBackend.swift @@ -12,11 +12,11 @@ final class AVPlayerBackend: PlayerBackend { private var logger = Logger(label: "avplayer-backend") - var model: PlayerModel! - var controls: PlayerControlsModel! - var playerTime: PlayerTimeModel! - var networkState: NetworkStateModel! - var seek: SeekModel! + var model: PlayerModel! { .shared } + var controls: PlayerControlsModel! { .shared } + var playerTime: PlayerTimeModel! { .shared } + var networkState: NetworkStateModel! { .shared } + var seek: SeekModel! { .shared } var stream: Stream? var video: Video? @@ -76,11 +76,7 @@ final class AVPlayerBackend: PlayerBackend { internal var controlsUpdates = false - init(model: PlayerModel, controls: PlayerControlsModel?, playerTime: PlayerTimeModel?) { - self.model = model - self.controls = controls - self.playerTime = playerTime ?? PlayerTimeModel.shared - + init() { addFrequentTimeObserver() addInfrequentTimeObserver() addPlayerTimeControlStatusObserver() diff --git a/Model/Player/Backends/MPVBackend.swift b/Model/Player/Backends/MPVBackend.swift index 71ab8e7b..520b007c 100644 --- a/Model/Player/Backends/MPVBackend.swift +++ b/Model/Player/Backends/MPVBackend.swift @@ -13,11 +13,11 @@ final class MPVBackend: PlayerBackend { private var logger = Logger(label: "mpv-backend") - var model: PlayerModel! - var controls: PlayerControlsModel! - var playerTime: PlayerTimeModel! - var networkState: NetworkStateModel! - var seek: SeekModel! + var model: PlayerModel! { .shared } + var controls: PlayerControlsModel! { .shared } + var playerTime: PlayerTimeModel! { .shared } + var networkState: NetworkStateModel! { .shared } + var seek: SeekModel! { .shared } var stream: Stream? var video: Video? @@ -120,17 +120,7 @@ final class MPVBackend: PlayerBackend { client?.cacheDuration ?? 0 } - init( - model: PlayerModel, - controls: PlayerControlsModel? = nil, - playerTime: PlayerTimeModel? = nil, - networkState: NetworkStateModel? = nil - ) { - self.model = model - self.controls = controls - self.playerTime = playerTime ?? PlayerTimeModel.shared - self.networkState = networkState - + init() { clientTimer = .init(interval: .seconds(Self.timeUpdateInterval), mode: .infinite) { [weak self] _ in self?.getTimeUpdates() } diff --git a/Model/Player/Backends/PlayerBackend.swift b/Model/Player/Backends/PlayerBackend.swift index e04eba2f..1f34b6d8 100644 --- a/Model/Player/Backends/PlayerBackend.swift +++ b/Model/Player/Backends/PlayerBackend.swift @@ -6,11 +6,10 @@ import Foundation #endif protocol PlayerBackend { - var model: PlayerModel! { get set } - var controls: PlayerControlsModel! { get set } - var playerTime: PlayerTimeModel! { get set } - var seek: SeekModel! { get set } - var networkState: NetworkStateModel! { get set } + var model: PlayerModel! { get } + var controls: PlayerControlsModel! { get } + var playerTime: PlayerTimeModel! { get } + var networkState: NetworkStateModel! { get } var stream: Stream? { get set } var video: Video? { get set } @@ -69,20 +68,20 @@ protocol PlayerBackend { extension PlayerBackend { func seek(to time: CMTime, seekType: SeekType, completionHandler: ((Bool) -> Void)? = nil) { - seek.registerSeek(at: time, type: seekType, restore: currentTime) + model.seek.registerSeek(at: time, type: seekType, restore: currentTime) seek(to: time, seekType: seekType, completionHandler: completionHandler) } func seek(to seconds: Double, seekType: SeekType, completionHandler: ((Bool) -> Void)? = nil) { let seconds = CMTime.secondsInDefaultTimescale(seconds) - seek.registerSeek(at: seconds, type: seekType, restore: currentTime) + model.seek.registerSeek(at: seconds, type: seekType, restore: currentTime) seek(to: seconds, seekType: seekType, completionHandler: completionHandler) } func seek(relative time: CMTime, seekType: SeekType, completionHandler: ((Bool) -> Void)? = nil) { if let currentTime = currentTime, let duration = playerItemDuration { let seekTime = min(max(0, currentTime.seconds + time.seconds), duration.seconds) - seek.registerSeek(at: .secondsInDefaultTimescale(seekTime), type: seekType, restore: currentTime) + model.seek.registerSeek(at: .secondsInDefaultTimescale(seekTime), type: seekType, restore: currentTime) seek(to: seekTime, seekType: seekType, completionHandler: completionHandler) } } diff --git a/Model/Player/ControlsOverlayModel.swift b/Model/Player/ControlsOverlayModel.swift new file mode 100644 index 00000000..dc5dc4f5 --- /dev/null +++ b/Model/Player/ControlsOverlayModel.swift @@ -0,0 +1,21 @@ +import Defaults +import Foundation +import SwiftUI + +final class ControlOverlaysModel: ObservableObject { + static let shared = ControlOverlaysModel() + @Published var presenting = false { didSet { handlePresentationChange() } } + + private lazy var controls = PlayerControlsModel.shared + private lazy var player: PlayerModel! = PlayerModel.shared + + func toggle() { + presenting.toggle() + controls.objectWillChange.send() + } + + private func handlePresentationChange() { + guard let player = player else { return } + player.backend.setNeedsNetworkStateUpdates(presenting && Defaults[.showMPVPlaybackStats]) + } +} diff --git a/Model/Player/PlayerControlsModel.swift b/Model/Player/PlayerControlsModel.swift index f23a4267..3720dfc5 100644 --- a/Model/Player/PlayerControlsModel.swift +++ b/Model/Player/PlayerControlsModel.swift @@ -10,7 +10,6 @@ final class PlayerControlsModel: ObservableObject { @Published var isLoadingVideo = false @Published var isPlaying = true @Published var presentingControls = false { didSet { handlePresentationChange() } } - @Published var presentingControlsOverlay = false { didSet { handleSettingsOverlayPresentationChange() } } @Published var presentingDetailsOverlay = false { didSet { handleDetailsOverlayPresentationChange() } } var timer: Timer? @@ -18,24 +17,21 @@ final class PlayerControlsModel: ObservableObject { private(set) var reporter = PassthroughSubject() #endif - var player: PlayerModel! + private var player: PlayerModel! { .shared } + private var controlsOverlayModel = ControlOverlaysModel.shared init( isLoadingVideo: Bool = false, isPlaying: Bool = true, presentingControls: Bool = false, - presentingControlsOverlay: Bool = false, presentingDetailsOverlay: Bool = false, - timer: Timer? = nil, - player: PlayerModel? = nil + timer: Timer? = nil ) { self.isLoadingVideo = isLoadingVideo self.isPlaying = isPlaying self.presentingControls = presentingControls - self.presentingControlsOverlay = presentingControlsOverlay self.presentingDetailsOverlay = presentingDetailsOverlay self.timer = timer - self.player = player ?? .shared } func handlePresentationChange() { @@ -60,26 +56,22 @@ final class PlayerControlsModel: ObservableObject { } func handleSettingsOverlayPresentationChange() { - player?.backend.setNeedsNetworkStateUpdates(presentingControlsOverlay && Defaults[.showMPVPlaybackStats]) + player?.backend.setNeedsNetworkStateUpdates(controlsOverlayModel.presenting && Defaults[.showMPVPlaybackStats]) } func handleDetailsOverlayPresentationChange() {} var presentingOverlays: Bool { - presentingDetailsOverlay || presentingControlsOverlay + presentingDetailsOverlay || controlsOverlayModel.presenting } func hideOverlays() { presentingDetailsOverlay = false - presentingControlsOverlay = false + controlsOverlayModel.presenting = false } func show() { - guard !(player?.currentItem.isNil ?? true) else { - return - } - - guard !presentingControls else { + guard !player.currentItem.isNil, !presentingControls else { return } @@ -132,4 +124,8 @@ final class PlayerControlsModel: ObservableObject { timer?.invalidate() timer = nil } + + func update() { + player?.backend.updateControls() + } } diff --git a/Model/Player/PlayerModel.swift b/Model/Player/PlayerModel.swift index 2d22098f..3b36584c 100644 --- a/Model/Player/PlayerModel.swift +++ b/Model/Player/PlayerModel.swift @@ -58,8 +58,8 @@ final class PlayerModel: ObservableObject { @Published var presentingPlayer = false { didSet { handlePresentationChange() } } @Published var activeBackend = PlayerBackendType.mpv - var avPlayerBackend: AVPlayerBackend! - var mpvBackend: MPVBackend! + var avPlayerBackend = AVPlayerBackend() + var mpvBackend = MPVBackend() #if !os(macOS) var mpvController = MPVViewController() #endif @@ -124,34 +124,10 @@ final class PlayerModel: ObservableObject { var accounts: AccountsModel var comments: CommentsModel - var controls: PlayerControlsModel { didSet { - backends.forEach { backend in - var backend = backend - backend.controls = controls - backend.controls.player = self - } - }} - var playerTime: PlayerTimeModel { didSet { - backends.forEach { backend in - var backend = backend - backend.playerTime = playerTime - backend.playerTime.player = self - } - }} - var networkState: NetworkStateModel { didSet { - backends.forEach { backend in - var backend = backend - backend.networkState = networkState - backend.networkState.player = self - } - }} - var seek: SeekModel { didSet { - backends.forEach { backend in - var backend = backend - backend.seek = seek - backend.seek.player = self - } - }} + var controls: PlayerControlsModel { .shared } + var playerTime: PlayerTimeModel { .shared } + var networkState: NetworkStateModel { .shared } + var seek: SeekModel { .shared } var navigation: NavigationModel var context: NSManagedObjectContext = PersistenceController.shared.container.viewContext @@ -194,30 +170,11 @@ final class PlayerModel: ObservableObject { init( accounts: AccountsModel = AccountsModel(), comments: CommentsModel = CommentsModel(), - controls: PlayerControlsModel = PlayerControlsModel(), - navigation: NavigationModel = NavigationModel(), - playerTime: PlayerTimeModel = PlayerTimeModel(), - networkState: NetworkStateModel = NetworkStateModel(), - seek: SeekModel = SeekModel() + navigation: NavigationModel = NavigationModel() ) { self.accounts = accounts self.comments = comments - self.controls = controls self.navigation = navigation - self.playerTime = playerTime - self.networkState = networkState - self.seek = seek - - self.avPlayerBackend = AVPlayerBackend( - model: self, - controls: controls, - playerTime: playerTime - ) - self.mpvBackend = MPVBackend( - model: self, - playerTime: playerTime, - networkState: networkState - ) #if !os(macOS) mpvBackend.controller = mpvController diff --git a/Model/SeekModel.swift b/Model/SeekModel.swift index 52b225b6..44b7e817 100644 --- a/Model/SeekModel.swift +++ b/Model/SeekModel.swift @@ -17,7 +17,7 @@ final class SeekModel: ObservableObject { @Published var presentingOSD = false - var player: PlayerModel! + var player: PlayerModel! { .shared } var dismissTimer: Timer? diff --git a/Shared/Player/Controls/OSD/Buffering.swift b/Shared/Player/Controls/OSD/Buffering.swift index 50fa07d2..6a816466 100644 --- a/Shared/Player/Controls/OSD/Buffering.swift +++ b/Shared/Player/Controls/OSD/Buffering.swift @@ -16,15 +16,7 @@ struct Buffering: View { @Default(.fullScreenPlayerControlsLayout) private var fullScreenPlayerControlsLayout var playerControlsLayout: PlayerControlsLayout { - fullScreenLayout ? fullScreenPlayerControlsLayout : regularPlayerControlsLayout - } - - var fullScreenLayout: Bool { - #if os(iOS) - player.playingFullScreen || verticalSizeClass == .compact - #else - player.playingFullScreen - #endif + player.playingFullScreen ? fullScreenPlayerControlsLayout : regularPlayerControlsLayout } var body: some View { diff --git a/Shared/Player/Controls/OSD/NetworkState.swift b/Shared/Player/Controls/OSD/NetworkState.swift index b73991af..24c400e8 100644 --- a/Shared/Player/Controls/OSD/NetworkState.swift +++ b/Shared/Player/Controls/OSD/NetworkState.swift @@ -1,16 +1,11 @@ import SwiftUI struct NetworkState: View { - @EnvironmentObject private var player @EnvironmentObject private var model var body: some View { Buffering(state: model.fullStateText) - .opacity(visible ? 1 : 0) - } - - var visible: Bool { - player.isPlaying && ((player.activeBackend == .mpv && model.pausedForCache) || player.isSeeking) + .opacity(model.osdVisible ? 1 : 0) } } diff --git a/Shared/Player/Controls/OSD/Seek.swift b/Shared/Player/Controls/OSD/Seek.swift index 67333496..17bab5b2 100644 --- a/Shared/Player/Controls/OSD/Seek.swift +++ b/Shared/Player/Controls/OSD/Seek.swift @@ -15,68 +15,77 @@ struct Seek: View { @Default(.fullScreenPlayerControlsLayout) private var fullScreenPlayerControlsLayout var body: some View { - Button(action: model.restoreTime) { - VStack(spacing: playerControlsLayout.osdSpacing) { - ProgressBar(value: model.progress) - .frame(maxHeight: playerControlsLayout.osdProgressBarHeight) + Group { + #if os(tvOS) + content + .shadow(radius: 3) + #else + Button(action: model.restoreTime) { content } + .buttonStyle(.plain) + #endif + } + .opacity(visible || YatteeApp.isForPreviews ? 1 : 0) + } - timeline + var content: some View { + VStack(spacing: playerControlsLayout.osdSpacing) { + ProgressBar(value: model.progress) + .frame(maxHeight: playerControlsLayout.osdProgressBarHeight) - if model.isSeeking { + timeline + + if model.isSeeking { + Divider() + gestureSeekTime + .foregroundColor(.secondary) + .font(.system(size: playerControlsLayout.chapterFontSize).monospacedDigit()) + .frame(height: playerControlsLayout.chapterFontSize + 5) + + if let chapter = projectedChapter { Divider() - gestureSeekTime - .foregroundColor(.secondary) - .font(.system(size: playerControlsLayout.chapterFontSize).monospacedDigit()) - .frame(height: playerControlsLayout.chapterFontSize + 5) - - if let chapter = projectedChapter { + Text(chapter.title) + .multilineTextAlignment(.center) + .font(.system(size: playerControlsLayout.chapterFontSize)) + .fixedSize(horizontal: false, vertical: true) + } + if let segment = projectedSegment { + Text(SponsorBlockAPI.categoryDescription(segment.category) ?? "Sponsor") + .font(.system(size: playerControlsLayout.segmentFontSize)) + .foregroundColor(Color("AppRedColor")) + } + } else { + #if !os(tvOS) + if !model.restoreSeekTime.isNil { Divider() - Text(chapter.title) - .multilineTextAlignment(.center) - .font(.system(size: playerControlsLayout.chapterFontSize)) - .fixedSize(horizontal: false, vertical: true) + Label(model.restoreSeekPlaybackTime, systemImage: "arrow.counterclockwise") + .foregroundColor(.secondary) + .font(.system(size: playerControlsLayout.chapterFontSize).monospacedDigit()) + .frame(height: playerControlsLayout.chapterFontSize + 5) } - if let segment = projectedSegment { - Text(SponsorBlockAPI.categoryDescription(segment.category) ?? "Sponsor") + #endif + Group { + switch model.lastSeekType { + case let .segmentSkip(category): + Divider() + Text(SponsorBlockAPI.categoryDescription(category) ?? "Sponsor") .font(.system(size: playerControlsLayout.segmentFontSize)) .foregroundColor(Color("AppRedColor")) - } - } else { - #if !os(tvOS) - if !model.restoreSeekTime.isNil { - Divider() - Label(model.restoreSeekPlaybackTime, systemImage: "arrow.counterclockwise") - .foregroundColor(.secondary) - .font(.system(size: playerControlsLayout.chapterFontSize).monospacedDigit()) - .frame(height: playerControlsLayout.chapterFontSize + 5) - } - #endif - Group { - switch model.lastSeekType { - case let .segmentSkip(category): - Divider() - Text(SponsorBlockAPI.categoryDescription(category) ?? "Sponsor") - .font(.system(size: playerControlsLayout.segmentFontSize)) - .foregroundColor(Color("AppRedColor")) - default: - EmptyView() - } + default: + EmptyView() } } } - .frame(maxWidth: playerControlsLayout.seekOSDWidth) - #if os(tvOS) - .padding(30) - #else - .padding(2) - .modifier(ControlBackgroundModifier()) - .clipShape(RoundedRectangle(cornerRadius: 3)) - #endif - - .foregroundColor(.primary) } - .buttonStyle(.plain) - .opacity(visible || YatteeApp.isForPreviews ? 1 : 0) + .frame(maxWidth: playerControlsLayout.seekOSDWidth) + #if os(tvOS) + .padding(30) + #else + .padding(2) + .modifier(ControlBackgroundModifier()) + .clipShape(RoundedRectangle(cornerRadius: 3)) + #endif + + .foregroundColor(.primary) } var timeline: some View { @@ -121,16 +130,7 @@ struct Seek: View { } var playerControlsLayout: PlayerControlsLayout { - fullScreenLayout ? fullScreenPlayerControlsLayout : regularPlayerControlsLayout - } - - var fullScreenLayout: Bool { - guard let player = model.player else { return false } - #if os(iOS) - return player.playingFullScreen || verticalSizeClass == .compact - #else - return player.playingFullScreen - #endif + (model.player?.playingFullScreen ?? false) ? fullScreenPlayerControlsLayout : regularPlayerControlsLayout } } diff --git a/Shared/Player/Controls/PlayerControls.swift b/Shared/Player/Controls/PlayerControls.swift index 6eb70d8f..cc682c75 100644 --- a/Shared/Player/Controls/PlayerControls.swift +++ b/Shared/Player/Controls/PlayerControls.swift @@ -10,7 +10,7 @@ struct PlayerControls: View { private var player: PlayerModel! private var thumbnails: ThumbnailsModel! - @EnvironmentObject private var model + @ObservedObject private var model = PlayerControlsModel.shared #if os(iOS) @Environment(\.verticalSizeClass) private var verticalSizeClass @@ -34,8 +34,10 @@ struct PlayerControls: View { @Default(.playerControlsLayout) private var regularPlayerControlsLayout @Default(.fullScreenPlayerControlsLayout) private var fullScreenPlayerControlsLayout + private let controlsOverlayModel = ControlOverlaysModel.shared + var playerControlsLayout: PlayerControlsLayout { - fullScreenLayout ? fullScreenPlayerControlsLayout : regularPlayerControlsLayout + player.playingFullScreen ? fullScreenPlayerControlsLayout : regularPlayerControlsLayout } init(player: PlayerModel, thumbnails: ThumbnailsModel) { @@ -90,7 +92,7 @@ struct PlayerControls: View { buttonsBar HStack { - if !player.currentVideo.isNil, fullScreenLayout { + if !player.currentVideo.isNil, player.playingFullScreen { Button { withAnimation(Self.animation) { model.presentingDetailsOverlay = true @@ -160,7 +162,8 @@ struct PlayerControls: View { .offset(y: -playerControlsLayout.timelineHeight - 5) #endif } - }.opacity(model.presentingControls && !model.presentingOverlays ? 1 : 0) + } + .opacity(model.presentingControls ? 1 : 0) } } .frame(maxWidth: .infinity) @@ -193,7 +196,7 @@ struct PlayerControls: View { guard player.presentingPlayer else { return } if value == "swipe down", !model.presentingControls, !model.presentingOverlays { withAnimation(Self.animation) { - model.presentingControlsOverlay = true + controlsOverlayModel.presenting = false } } else { model.show() @@ -302,19 +305,19 @@ struct PlayerControls: View { var fullscreenButton: some View { button( "Fullscreen", - systemImage: fullScreenLayout ? "arrow.down.right.and.arrow.up.left" : "arrow.up.left.and.arrow.down.right" + systemImage: player.playingFullScreen ? "arrow.down.right.and.arrow.up.left" : "arrow.up.left.and.arrow.down.right" ) { - player.toggleFullscreen(fullScreenLayout) + player.toggleFullscreen(player.playingFullScreen) } #if !os(tvOS) - .keyboardShortcut(fullScreenLayout ? .cancelAction : .defaultAction) + .keyboardShortcut(player.playingFullScreen ? .cancelAction : .defaultAction) #endif } private var settingsButton: some View { button("settings", systemImage: "gearshape") { withAnimation(Self.animation) { - model.presentingControlsOverlay.toggle() + controlsOverlayModel.toggle() } } #if os(tvOS) @@ -492,14 +495,6 @@ struct PlayerControls: View { .modifier(ControlBackgroundModifier(enabled: useBackground)) .clipShape(RoundedRectangle(cornerRadius: cornerRadius)) } - - var fullScreenLayout: Bool { - #if os(iOS) - player.playingFullScreen || verticalSizeClass == .compact - #else - player.playingFullScreen - #endif - } } struct PlayerControls_Previews: PreviewProvider { diff --git a/Shared/Player/Controls/TVControls.swift b/Shared/Player/Controls/TVControls.swift index d648ef6f..3c1631e7 100644 --- a/Shared/Player/Controls/TVControls.swift +++ b/Shared/Player/Controls/TVControls.swift @@ -47,16 +47,16 @@ struct TVControls: UIViewRepresentable { func updateUIView(_: UIView, context _: Context) {} func makeCoordinator() -> TVControls.Coordinator { - Coordinator(controlsArea, model: model) + Coordinator(controlsArea) } final class Coordinator: NSObject { private let view: UIView private let model: PlayerControlsModel - init(_ view: UIView, model: PlayerControlsModel) { + init(_ view: UIView) { self.view = view - self.model = model + model = .shared super.init() } diff --git a/Shared/Player/Controls/TimelineView.swift b/Shared/Player/Controls/TimelineView.swift index b613bd30..788748ea 100644 --- a/Shared/Player/Controls/TimelineView.swift +++ b/Shared/Player/Controls/TimelineView.swift @@ -53,15 +53,7 @@ struct TimelineView: View { @Default(.fullScreenPlayerControlsLayout) private var fullScreenPlayerControlsLayout var playerControlsLayout: PlayerControlsLayout { - fullScreenLayout ? fullScreenPlayerControlsLayout : regularPlayerControlsLayout - } - - var fullScreenLayout: Bool { - #if os(iOS) - player.playingFullScreen || verticalSizeClass == .compact - #else - player.playingFullScreen - #endif + player.playingFullScreen ? fullScreenPlayerControlsLayout : regularPlayerControlsLayout } var chapters: [Chapter] { diff --git a/Shared/Player/PlayerBackendView.swift b/Shared/Player/PlayerBackendView.swift index b13e3082..65945775 100644 --- a/Shared/Player/PlayerBackendView.swift +++ b/Shared/Player/PlayerBackendView.swift @@ -39,21 +39,13 @@ struct PlayerBackendView: View { #endif } #if os(iOS) - .statusBarHidden(fullScreenLayout) - #endif - } - - var fullScreenLayout: Bool { - #if os(iOS) - player.playingFullScreen || verticalSizeClass == .compact - #else - player.playingFullScreen + .statusBarHidden(player.playingFullScreen) #endif } #if os(iOS) var controlsTopPadding: Double { - guard fullScreenLayout else { return 0 } + guard player.playingFullScreen else { return 0 } if UIDevice.current.userInterfaceIdiom != .pad { return verticalSizeClass == .compact ? SafeArea.insets.top : 0 @@ -63,12 +55,12 @@ struct PlayerBackendView: View { } var controlsBottomPadding: Double { - guard fullScreenLayout else { return 0 } + guard player.playingFullScreen else { return 0 } if UIDevice.current.userInterfaceIdiom != .pad { - return fullScreenLayout && verticalSizeClass == .compact ? SafeArea.insets.bottom : 0 + return player.playingFullScreen && verticalSizeClass == .compact ? SafeArea.insets.bottom : 0 } else { - return fullScreenLayout ? SafeArea.insets.bottom : 0 + return player.playingFullScreen ? SafeArea.insets.bottom : 0 } } #endif diff --git a/Shared/Player/PlayerDragGesture.swift b/Shared/Player/PlayerDragGesture.swift index a613807c..ad6c5556 100644 --- a/Shared/Player/PlayerDragGesture.swift +++ b/Shared/Player/PlayerDragGesture.swift @@ -17,7 +17,7 @@ extension VideoPlayerView { } .onChanged { value in guard player.presentingPlayer, - !player.controls.presentingControlsOverlay else { return } + !controlsOverlayModel.presenting else { return } if player.controls.presentingControls, !player.musicMode { player.controls.presentingControls = false @@ -83,7 +83,7 @@ extension VideoPlayerView { isVerticalDrag = false guard player.presentingPlayer, - !player.controls.presentingControlsOverlay else { return } + !controlsOverlayModel.presenting else { return } if viewDragOffset > 100 { withAnimation(Constants.overlayAnimation) { diff --git a/Shared/Player/PlayerGestures.swift b/Shared/Player/PlayerGestures.swift index 24e09135..d575ced3 100644 --- a/Shared/Player/PlayerGestures.swift +++ b/Shared/Player/PlayerGestures.swift @@ -12,6 +12,9 @@ struct PlayerGestures: View { singleTapAction: { singleTapAction() }, doubleTapAction: { player.backend.seek(relative: .secondsInDefaultTimescale(-10), seekType: .userInteracted) + }, + anyTapAction: { + model.update() } ) diff --git a/Shared/Player/VideoDetails.swift b/Shared/Player/VideoDetails.swift index 0f328e4b..ed78e520 100644 --- a/Shared/Player/VideoDetails.swift +++ b/Shared/Player/VideoDetails.swift @@ -231,7 +231,7 @@ struct VideoDetails: View { } else if video.description != nil, !video.description!.isEmpty { VideoDescription(video: video, detailsSize: detailsSize) #if os(iOS) - .padding(.bottom, fullScreenLayout ? 10 : SafeArea.insets.bottom) + .padding(.bottom, player.playingFullScreen ? 10 : SafeArea.insets.bottom) #endif } else { Text("No description") @@ -243,14 +243,6 @@ struct VideoDetails: View { .padding(.horizontal) } - var fullScreenLayout: Bool { - #if os(iOS) - return player.playingFullScreen || verticalSizeClass == .compact - #else - return player.playingFullScreen - #endif - } - @ViewBuilder var videoProperties: some View { HStack(spacing: 2) { publishedDateSection diff --git a/Shared/Player/VideoPlayerView.swift b/Shared/Player/VideoPlayerView.swift index 5a6a2608..ca66ba5f 100644 --- a/Shared/Player/VideoPlayerView.swift +++ b/Shared/Player/VideoPlayerView.swift @@ -70,17 +70,18 @@ struct VideoPlayerView: View { @Default(.seekGestureSpeed) var seekGestureSpeed @Default(.seekGestureSensitivity) var seekGestureSensitivity + @ObservedObject internal var controlsOverlayModel = ControlOverlaysModel.shared + var body: some View { ZStack(alignment: overlayAlignment) { videoPlayer .zIndex(-1) #if os(iOS) - .gesture(player.controls.presentingControlsOverlay ? videoPlayerCloseControlsOverlayGesture : nil) + .gesture(controlsOverlayModel.presenting ? videoPlayerCloseControlsOverlayGesture : nil) #endif overlay } - .animation(nil, value: player.playerSize) .onAppear { if player.musicMode { player.backend.startControlsUpdates() @@ -184,20 +185,20 @@ struct VideoPlayerView: View { .offset(y: playerOffset) .animation(dragGestureState ? .interactiveSpring(response: 0.05) : .easeOut(duration: 0.2), value: playerOffset) .backport - .persistentSystemOverlays(!fullScreenLayout) + .persistentSystemOverlays(!player.playingFullScreen) #endif #endif } var overlay: some View { VStack { - if player.controls.presentingControlsOverlay { + if controlsOverlayModel.presenting { HStack { HStack { ControlsOverlay() #if os(tvOS) .onExitCommand { - withAnimation(Player.controls.animation) { + withAnimation(PlayerControls.animation) { player.controls.hideOverlays() } } @@ -210,11 +211,11 @@ struct VideoPlayerView: View { .clipShape(RoundedRectangle(cornerRadius: 4)) } #if !os(tvOS) - .frame(maxWidth: fullScreenLayout ? .infinity : player.playerSize.width) + .frame(maxWidth: player.playingFullScreen ? .infinity : player.playerSize.width) #endif #if !os(tvOS) - if !fullScreenLayout && sidebarQueue { + if !player.playingFullScreen && sidebarQueue { Spacer() } #endif @@ -255,12 +256,12 @@ struct VideoPlayerView: View { } var playerWidth: Double? { - fullScreenLayout ? (UIScreen.main.bounds.size.width - SafeArea.insets.left - SafeArea.insets.right) : nil + player.playingFullScreen ? (UIScreen.main.bounds.size.width - SafeArea.insets.left - SafeArea.insets.right) : nil } var playerHeight: Double? { let lockedPortrait = player.lockedOrientation?.contains(.portrait) ?? false - return fullScreenLayout ? UIScreen.main.bounds.size.height - (OrientationTracker.shared.currentInterfaceOrientation.isPortrait || lockedPortrait ? (SafeArea.insets.top + SafeArea.insets.bottom) : 0) : nil + return player.playingFullScreen ? UIScreen.main.bounds.size.height - (OrientationTracker.shared.currentInterfaceOrientation.isPortrait || lockedPortrait ? (SafeArea.insets.top + SafeArea.insets.bottom) : 0) : nil } var playerEdgesIgnoringSafeArea: Edge.Set { @@ -268,7 +269,7 @@ struct VideoPlayerView: View { return [] } - if fullScreenLayout, UIDevice.current.orientation.isLandscape { + if player.playingFullScreen, UIDevice.current.orientation.isLandscape { return [.vertical] } @@ -296,12 +297,12 @@ struct VideoPlayerView: View { VideoPlayerSizeModifier( geometry: geometry, aspectRatio: player.aspectRatio, - fullScreen: fullScreenLayout + fullScreen: player.playingFullScreen ) ) .overlay(playerPlaceholder) #endif - .frame(maxWidth: fullScreenLayout ? .infinity : nil, maxHeight: fullScreenLayout ? .infinity : nil) + .frame(maxWidth: player.playingFullScreen ? .infinity : nil, maxHeight: player.playingFullScreen ? .infinity : nil) .onHover { hovering in hoveringPlayer = hovering hovering ? player.controls.show() : player.controls.hide() @@ -326,7 +327,7 @@ struct VideoPlayerView: View { .background(Color.black) #if !os(tvOS) - if !fullScreenLayout { + if !player.playingFullScreen { VideoDetails(sidebarQueue: sidebarQueue, fullScreen: $fullScreenDetails) #if os(iOS) .ignoresSafeArea(.all, edges: .bottom) @@ -346,7 +347,7 @@ struct VideoPlayerView: View { } #endif } - .background(((colorScheme == .dark || fullScreenLayout) ? Color.black : Color.white).edgesIgnoringSafeArea(.all)) + .background(((colorScheme == .dark || player.playingFullScreen) ? Color.black : Color.white).edgesIgnoringSafeArea(.all)) #if os(macOS) .frame(minWidth: 650) #endif @@ -354,9 +355,9 @@ struct VideoPlayerView: View { .onMoveCommand { direction in if direction == .up { player.controls.show() - } else if direction == .down, !player.controls.presentingControlsOverlay, !player.controls.presentingControls { - withAnimation(Player.controls.animation) { - player.controls.presentingControlsOverlay = true + } else if direction == .down, !controlsOverlayModel.presenting, !player.controls.presentingControls { + withAnimation(PlayerControls.animation) { + controlsOverlayModel.presenting = true } } @@ -385,7 +386,7 @@ struct VideoPlayerView: View { } } #endif - if !fullScreenLayout { + if !player.playingFullScreen { #if os(iOS) if sidebarQueue { PlayerQueueView(sidebarQueue: true, fullScreen: $fullScreenDetails) @@ -402,19 +403,11 @@ struct VideoPlayerView: View { #endif } } - .onChange(of: fullScreenLayout) { newValue in + .onChange(of: player.playingFullScreen) { newValue in if !newValue { player.controls.hideOverlays() } } #if os(iOS) - .statusBar(hidden: fullScreenLayout) - #endif - } - - var fullScreenLayout: Bool { - #if os(iOS) - return player.playingFullScreen || verticalSizeClass == .compact - #else - return player.playingFullScreen + .statusBar(hidden: player.playingFullScreen) #endif } @@ -459,7 +452,7 @@ struct VideoPlayerView: View { #if os(tvOS) var tvControls: some View { - TVControls(model: playerControls, player: player, thumbnails: thumbnails) + TVControls(player: player, thumbnails: thumbnails) } #endif } diff --git a/Shared/Views/ControlsBar.swift b/Shared/Views/ControlsBar.swift index 04d2ec8d..bf9e2b63 100644 --- a/Shared/Views/ControlsBar.swift +++ b/Shared/Views/ControlsBar.swift @@ -26,6 +26,8 @@ struct ControlsBar: View { var detailsToggleFullScreen = false var titleLineLimit = 2 + private let controlsOverlayModel = ControlOverlaysModel.shared + var body: some View { HStack(spacing: 0) { detailsButton @@ -63,7 +65,7 @@ struct ControlsBar: View { } } else if detailsToggleFullScreen { Button { - model.controls.presentingControlsOverlay = false + controlsOverlayModel.presenting = false model.controls.presentingControls = false withAnimation { fullScreen.toggle() diff --git a/Shared/YatteeApp.swift b/Shared/YatteeApp.swift index fb9f69e1..2a199f07 100644 --- a/Shared/YatteeApp.swift +++ b/Shared/YatteeApp.swift @@ -137,7 +137,6 @@ struct YatteeApp: App { .environmentObject(playlists) .environmentObject(recents) .environmentObject(search) - .environmentObject(seek) .environmentObject(subscriptions) .environmentObject(thumbnails) .handlesExternalEvents(preferring: Set(["player", "*"]), allowing: Set(["player", "*"])) @@ -184,8 +183,6 @@ struct YatteeApp: App { InstancesManifest.shared.setPublicAccount(countryOfPublicInstances!, accounts: accounts, asCurrent: accounts.current.isNil) } - PlayerModel.shared = player - playlists.accounts = accounts search.accounts = accounts subscriptions.accounts = accounts @@ -196,15 +193,11 @@ struct YatteeApp: App { menu.navigation = navigation menu.player = player - playerControls.player = player - player.accounts = accounts player.comments = comments - player.controls = playerControls player.navigation = navigation - player.networkState = networkState - player.seek = .shared + PlayerModel.shared = player PlayerTimeModel.shared.player = player if !accounts.current.isNil { diff --git a/Yattee.xcodeproj/project.pbxproj b/Yattee.xcodeproj/project.pbxproj index 1ace9233..d3f38e1d 100644 --- a/Yattee.xcodeproj/project.pbxproj +++ b/Yattee.xcodeproj/project.pbxproj @@ -810,6 +810,9 @@ 37EF9A77275BEB8E0043B585 /* CommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF9A75275BEB8E0043B585 /* CommentView.swift */; }; 37EF9A78275BEB8E0043B585 /* CommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF9A75275BEB8E0043B585 /* CommentView.swift */; }; 37EF9A79275BEB8E0043B585 /* CommentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EF9A75275BEB8E0043B585 /* CommentView.swift */; }; + 37EFAC0828C138CD00ED9B89 /* ControlsOverlayModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EFAC0728C138CD00ED9B89 /* ControlsOverlayModel.swift */; }; + 37EFAC0928C138CD00ED9B89 /* ControlsOverlayModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EFAC0728C138CD00ED9B89 /* ControlsOverlayModel.swift */; }; + 37EFAC0A28C138CD00ED9B89 /* ControlsOverlayModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37EFAC0728C138CD00ED9B89 /* ControlsOverlayModel.swift */; }; 37F0F4EA286F397E00C06C2E /* SettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F0F4E9286F397E00C06C2E /* SettingsModel.swift */; }; 37F0F4EB286F397E00C06C2E /* SettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F0F4E9286F397E00C06C2E /* SettingsModel.swift */; }; 37F0F4EC286F397E00C06C2E /* SettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37F0F4E9286F397E00C06C2E /* SettingsModel.swift */; }; @@ -1284,6 +1287,7 @@ 37ECED55289FE166002BC2C9 /* SafeArea.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeArea.swift; sourceTree = ""; }; 37EF5C212739D37B00B03725 /* MenuModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuModel.swift; sourceTree = ""; }; 37EF9A75275BEB8E0043B585 /* CommentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentView.swift; sourceTree = ""; }; + 37EFAC0728C138CD00ED9B89 /* ControlsOverlayModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlsOverlayModel.swift; sourceTree = ""; }; 37F0F4E9286F397E00C06C2E /* SettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModel.swift; sourceTree = ""; }; 37F0F4ED286F734400C06C2E /* AdvancedSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedSettings.swift; sourceTree = ""; }; 37F13B61285E43C000B137E4 /* ControlsOverlay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControlsOverlay.swift; sourceTree = ""; }; @@ -1723,6 +1727,7 @@ isa = PBXGroup; children = ( 37EBD8C227AF0D7C00F1C24B /* Backends */, + 37EFAC0728C138CD00ED9B89 /* ControlsOverlayModel.swift */, 373031F428383A89000CFD59 /* PiPDelegate.swift */, 37B767DA2677C3CA0098BAA8 /* PlayerModel.swift */, 37319F0427103F94004ECCD0 /* PlayerQueue.swift */, @@ -2885,6 +2890,7 @@ 376BE50927347B5F009AD608 /* SettingsHeader.swift in Sources */, 376527BB285F60F700102284 /* PlayerTimeModel.swift in Sources */, 3722AEBE274DA401005EA4D6 /* Backport.swift in Sources */, + 37EFAC0828C138CD00ED9B89 /* ControlsOverlayModel.swift in Sources */, 37F4AD2628613B81004D0F66 /* Color+Debug.swift in Sources */, 3700155F271B12DD0049C794 /* SiestaConfiguration.swift in Sources */, 37F13B62285E43C000B137E4 /* ControlsOverlay.swift in Sources */, @@ -3142,6 +3148,7 @@ 372915E72687E3B900F5A35B /* Defaults.swift in Sources */, 37C3A242272359900087A57A /* Double+Format.swift in Sources */, 37B795912771DAE0001CF27B /* OpenURLHandler.swift in Sources */, + 37EFAC0928C138CD00ED9B89 /* ControlsOverlayModel.swift in Sources */, 37DD87C8271C9CFE0027CBF9 /* PlayerStreams.swift in Sources */, 376578922685490700D4EA09 /* PlaylistsView.swift in Sources */, 37F9619C27BD89E000058149 /* TapRecognizerViewModifier.swift in Sources */, @@ -3307,6 +3314,7 @@ 37DD9DBC2785D60300539416 /* FramePreferenceKey.swift in Sources */, 375EC974289F2ABF00751258 /* MultiselectRow.swift in Sources */, 37EBD8C827AF26B300F1C24B /* AVPlayerBackend.swift in Sources */, + 37EFAC0A28C138CD00ED9B89 /* ControlsOverlayModel.swift in Sources */, 37CC3F52270D010D00608308 /* VideoBanner.swift in Sources */, 37F49BA526CAA59B00304AC0 /* Playlist+Fixtures.swift in Sources */, 376CD21826FBE18D001E1AC1 /* Instance+Fixtures.swift in Sources */,