diff --git a/Yattee/ContentView.swift b/Yattee/ContentView.swift index 0255a678..b37f1645 100644 --- a/Yattee/ContentView.swift +++ b/Yattee/ContentView.swift @@ -24,6 +24,9 @@ struct ContentView: View { ProgressView(String(localized: "common.loading")) } } + // Shared sheets for the main window. The expanded player hosts its own, + // and takes global requests while it is up. + .appSheetHost(isTopLevel: appEnvironment?.navigationCoordinator.isPlayerExpanded != true) .toastOverlay() } @@ -101,20 +104,6 @@ struct ContentView: View { appEnvironment.settingsManager.macPlayerSeparateWindow else { return } ExpandedPlayerWindowManager.shared.updateWindowLevel(floating: floating) } - .sheet(isPresented: Binding( - get: { appEnvironment.navigationCoordinator.isMiniPlayerQueueSheetPresented }, - set: { appEnvironment.navigationCoordinator.isMiniPlayerQueueSheetPresented = $0 } - )) { - QueueManagementSheet() - } - .sheet(isPresented: Binding( - get: { appEnvironment.navigationCoordinator.isMiniPlayerPlaylistSheetPresented }, - set: { appEnvironment.navigationCoordinator.isMiniPlayerPlaylistSheetPresented = $0 } - )) { - if let video = appEnvironment.playerService.state.currentVideo { - PlaylistSelectorSheet(video: video) - } - } #elseif os(tvOS) .fullScreenCover(isPresented: Binding( get: { appEnvironment.navigationCoordinator.isPlayerExpanded }, @@ -178,8 +167,6 @@ struct AdaptiveTabView: View { let appEnvironment: AppEnvironment - @State private var showingMiniPlayerQueueSheet = false - @State private var showingMiniPlayerPlaylistSheet = false /// Whether to show the black overlay that covers scaled content when player is expanded private var shouldShowExpandedOverlay: Bool { @@ -243,26 +230,6 @@ struct AdaptiveTabView: View { ExpandedPlayerWindowManager.shared.hide() } } - .onChange(of: appEnvironment.navigationCoordinator.isMiniPlayerQueueSheetPresented, initial: true) { _, newValue in - showingMiniPlayerQueueSheet = newValue - } - .onChange(of: appEnvironment.navigationCoordinator.isMiniPlayerPlaylistSheetPresented, initial: true) { _, newValue in - showingMiniPlayerPlaylistSheet = newValue - } - .sheet(isPresented: $showingMiniPlayerQueueSheet, onDismiss: { - appEnvironment.navigationCoordinator.isMiniPlayerQueueSheetPresented = false - }) { - QueueManagementSheet() - .presentationDetents([.medium, .large]) - } - .sheet(isPresented: $showingMiniPlayerPlaylistSheet, onDismiss: { - appEnvironment.navigationCoordinator.isMiniPlayerPlaylistSheetPresented = false - }) { - if let video = appEnvironment.playerService.state.currentVideo { - PlaylistSelectorSheet(video: video) - .presentationDetents([.medium, .large]) - } - } } private var usesSidebar: Bool { diff --git a/Yattee/Services/Navigation/NavigationCoordinator.swift b/Yattee/Services/Navigation/NavigationCoordinator.swift index 2bfd099d..129b32d4 100644 --- a/Yattee/Services/Navigation/NavigationCoordinator.swift +++ b/Yattee/Services/Navigation/NavigationCoordinator.swift @@ -14,6 +14,9 @@ final class NavigationCoordinator { /// Selected section and per-section stacks, shared by every layout. let router = NavigationRouter() + /// Shared sheets requested from outside the view tree. + let presenter = PresentationRouter() + init() { router.onSelectionChange = { [weak self] _ in self?.updateHandoffForCurrentTab() @@ -60,9 +63,6 @@ final class NavigationCoordinator { /// (remote control state) reads the real layout instead of screen bounds. var isWideScreenLayoutActive = false - /// Whether the mini player queue sheet is showing. - var isMiniPlayerQueueSheetPresented = false - /// Video to present in the queue action sheet (e.g. a video link tapped inside /// a description). Set by the deep-link handler; cleared when the sheet dismisses. /// Presented by YatteeApp when the player is collapsed and by ExpandedPlayerSheet @@ -79,9 +79,6 @@ final class NavigationCoordinator { /// match) awaiting user confirmation before yt-dlp extraction. Dual-hosted. var ambiguousExternalLinkPrompt: URL? - /// Whether the mini player playlist sheet is showing. - var isMiniPlayerPlaylistSheetPresented = false - /// Counter that increments each time the player should expand. /// Used to trigger presentation even when isPlayerExpanded is already true. var playerExpandTrigger = 0 diff --git a/Yattee/Services/Navigation/PresentationRouter.swift b/Yattee/Services/Navigation/PresentationRouter.swift new file mode 100644 index 00000000..049303aa --- /dev/null +++ b/Yattee/Services/Navigation/PresentationRouter.swift @@ -0,0 +1,165 @@ +// +// PresentationRouter.swift +// Yattee +// +// Shared sheets, declared once per host instead of once per screen or row. +// +// Views present through `@Environment(\.presentAppSheet)`, which reaches the +// nearest `.appSheetHost()` above them: the app root, the expanded player, +// or a sheet that is itself presented by a host (each presented sheet hosts +// the next level). Code outside the view tree uses `PresentationRouter.request`, +// which the top-most host (the player when expanded, the root otherwise) +// picks up. +// + +import SwiftUI + +/// Sheets presented through `.appSheetHost()`. +enum AppSheet: Identifiable { + /// Queue of the current player. `halfHeight` opens at the medium detent. + case queue(halfHeight: Bool = false) + /// Add a video to a playlist. `halfHeight` opens at the medium detent. + case addToPlaylist(Video, halfHeight: Bool = false) + #if !os(tvOS) + /// Download quality picker. Streams and captions already loaded by the + /// caller (the player) skip the sheet's own fetch. + case downloadOptions(Video, streams: [Stream] = [], captions: [Caption] = [], dislikeCount: Int? = nil) + #endif + + var id: String { + switch self { + case .queue: + return "queue" + case .addToPlaylist(let video, _): + return "addToPlaylist-\(video.id.id)" + #if !os(tvOS) + case .downloadOptions(let video, _, _, _): + return "downloadOptions-\(video.id.id)" + #endif + } + } +} + +@Observable +@MainActor +final class PresentationRouter { + /// A sheet requested from outside the view tree. The top-most host moves + /// it into its own state and clears this. + var request: AppSheet? + + func present(_ sheet: AppSheet) { + request = sheet + } +} + +// MARK: - Environment + +/// Presents an `AppSheet` from the nearest host. +struct PresentAppSheetAction { + fileprivate let handler: @MainActor (AppSheet) -> Void + + @MainActor + func callAsFunction(_ sheet: AppSheet) { + handler(sheet) + } +} + +extension EnvironmentValues { + /// Outside any host this falls back to the global request, which the + /// top-most host presents. + @Entry var presentAppSheet = PresentAppSheetAction { sheet in + AppEnvironment.shared.navigationCoordinator.presenter.present(sheet) + } +} + +// MARK: - Host + +private struct AppSheetHost: ViewModifier { + @Environment(\.appEnvironment) private var appEnvironment + + /// Whether this host presents global requests (`PresentationRouter.request`). + let isTopLevel: Bool + + @State private var sheet: AppSheet? + + func body(content: Content) -> some View { + content + .environment(\.presentAppSheet, PresentAppSheetAction { sheet = $0 }) + #if os(tvOS) + .fullScreenCover(item: $sheet) { sheet in + sheetContent(for: sheet) + .tvFullScreenCoverBackground() + } + #else + .sheet(item: $sheet) { sheet in + sheetContent(for: sheet) + } + #endif + .onChange(of: pendingRequestID, initial: true) { _, _ in + takePendingRequest() + } + } + + private var pendingRequestID: String? { + isTopLevel ? appEnvironment?.navigationCoordinator.presenter.request?.id : nil + } + + private func takePendingRequest() { + guard isTopLevel, let presenter = appEnvironment?.navigationCoordinator.presenter, + let request = presenter.request else { return } + presenter.request = nil + sheet = request + } + + @ViewBuilder + private func sheetContent(for sheet: AppSheet) -> some View { + Group { + switch sheet { + case .queue(let halfHeight): + QueueManagementSheet() + .halfHeightDetents(halfHeight) + case .addToPlaylist(let video, let halfHeight): + PlaylistSelectorSheet(video: video) + .halfHeightDetents(halfHeight) + #if !os(tvOS) + case .downloadOptions(let video, let streams, let captions, let dislikeCount): + DownloadQualitySheet(video: video, streams: streams, captions: captions, dislikeCount: dislikeCount) + #endif + } + } + // The next level: sheets opened from inside this one. + .appSheetHost(isTopLevel: false) + .optionalAppEnvironment(appEnvironment) + } +} + +private extension View { + @ViewBuilder + func halfHeightDetents(_ halfHeight: Bool) -> some View { + if halfHeight { + presentationDetents([.medium, .large]) + } else { + self + } + } + + @ViewBuilder + func optionalAppEnvironment(_ appEnvironment: AppEnvironment?) -> some View { + if let appEnvironment { + self.appEnvironment(appEnvironment) + } else { + self + } + } +} + +extension View { + /// Hosts `AppSheet`s for the content below it. + /// + /// - Parameter isTopLevel: true for the hosts that present global requests: + /// the app root while the player is collapsed, the expanded player while + /// it is up. + func appSheetHost(isTopLevel: Bool) -> some View { + modifier(AppSheetHost(isTopLevel: isTopLevel)) + } +} diff --git a/Yattee/Views/Components/PlayerPillButtonView.swift b/Yattee/Views/Components/PlayerPillButtonView.swift index b5227297..0725e688 100644 --- a/Yattee/Views/Components/PlayerPillButtonView.swift +++ b/Yattee/Views/Components/PlayerPillButtonView.swift @@ -11,6 +11,7 @@ import SwiftUI struct PlayerPillButtonView: View { @Environment(\.appEnvironment) private var appEnvironment + @Environment(\.presentAppSheet) private var presentAppSheet let configuration: ControlButtonConfiguration let isPlaying: Bool @@ -24,7 +25,6 @@ struct PlayerPillButtonView: View { // Context for special buttons let video: Video? let playbackRate: PlaybackRate - @Binding var showingPlaylistSheet: Bool // Callbacks let onAction: () -> Void @@ -263,7 +263,9 @@ struct PlayerPillButtonView: View { private var addToPlaylistButton: some View { Button { - showingPlaylistSheet = true + if let video { + presentAppSheet(.addToPlaylist(video)) + } } label: { genericButtonLabel } @@ -326,7 +328,6 @@ struct PlayerPillButtonView: View { isOrientationLocked: false, video: nil, playbackRate: .x1, - showingPlaylistSheet: .constant(false), onAction: {}, onRateChanged: { _ in }, onTogglePiP: {} @@ -348,7 +349,6 @@ struct PlayerPillButtonView: View { isOrientationLocked: false, video: nil, playbackRate: .x1, - showingPlaylistSheet: .constant(false), onAction: {}, onRateChanged: { _ in }, onTogglePiP: {} @@ -364,7 +364,6 @@ struct PlayerPillButtonView: View { isOrientationLocked: false, video: nil, playbackRate: .x1, - showingPlaylistSheet: .constant(false), onAction: {}, onRateChanged: { _ in }, onTogglePiP: {} diff --git a/Yattee/Views/Components/PlayerPillView.swift b/Yattee/Views/Components/PlayerPillView.swift index e4c88a40..3f3a19c6 100644 --- a/Yattee/Views/Components/PlayerPillView.swift +++ b/Yattee/Views/Components/PlayerPillView.swift @@ -31,7 +31,6 @@ struct PlayerPillView: View { // Video context for special buttons let video: Video? let playbackRate: PlaybackRate - @Binding var showingPlaylistSheet: Bool // Action callbacks let onQueueTap: () -> Void @@ -62,7 +61,6 @@ struct PlayerPillView: View { isOrientationLocked: Bool = false, video: Video? = nil, playbackRate: PlaybackRate = .x1, - showingPlaylistSheet: Binding = .constant(false), onQueueTap: @escaping () -> Void = {}, onPrevious: @escaping () -> Void = {}, onPlayPause: @escaping () -> Void = {}, @@ -86,7 +84,6 @@ struct PlayerPillView: View { self.isOrientationLocked = isOrientationLocked self.video = video self.playbackRate = playbackRate - self._showingPlaylistSheet = showingPlaylistSheet self.onQueueTap = onQueueTap self.onPrevious = onPrevious self.onPlayPause = onPlayPause @@ -142,7 +139,6 @@ struct PlayerPillView: View { isOrientationLocked: isOrientationLocked, video: video, playbackRate: playbackRate, - showingPlaylistSheet: $showingPlaylistSheet, onAction: { handleAction(for: config) }, onRateChanged: onRateChanged, onTogglePiP: onPiP diff --git a/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift b/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift index 5f9401d4..a757843e 100644 --- a/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift +++ b/Yattee/Views/Player/ExpandedPlayerSheet+Layouts.swift @@ -121,7 +121,7 @@ extension ExpandedPlayerSheet { /// Starts a download either automatically or by showing the quality sheet. private func startDownload(for video: Video) { guard let appEnvironment else { - showingDownloadSheet = true + presentDownloadOptions() return } @@ -175,7 +175,7 @@ extension ExpandedPlayerSheet { } } } else { - showingDownloadSheet = true + presentDownloadOptions() } } #else @@ -984,7 +984,7 @@ extension ExpandedPlayerSheet { await playerService.playPrevious() }, onShowQueue: { [self] in - showingQueueSheet = true + presentSheet(.queue()) }, onVolumeChanged: { [weak appEnvironment] volume in playerService.currentBackend?.volume = volume @@ -1084,10 +1084,10 @@ extension ExpandedPlayerSheet { showingQualitySheet = true }, onShowQueue: { [self] in - showingQueueSheet = true + presentSheet(.queue()) }, onShowPlaylistSelector: { [self] in - showingPlaylistSheet = true + presentAddToPlaylist() }, onRateChanged: { rate in playerState.rate = rate @@ -1268,7 +1268,7 @@ extension ExpandedPlayerSheet { await playerService.playPrevious() }, onShowQueue: { [self] in - showingQueueSheet = true + presentSheet(.queue()) }, forceInitialHidden: forceHidden, videoAreaTop: videoAreaTop, @@ -1373,10 +1373,10 @@ extension ExpandedPlayerSheet { showingQualitySheet = true }, onShowQueue: { [self] in - showingQueueSheet = true + presentSheet(.queue()) }, onShowPlaylistSelector: { [self] in - showingPlaylistSheet = true + presentAddToPlaylist() }, onRateChanged: { rate in playerState.rate = rate @@ -1704,7 +1704,7 @@ extension ExpandedPlayerSheet { await playerService.playPrevious() }, onShowQueue: { [self] in - showingQueueSheet = true + presentSheet(.queue()) }, videoAreaTop: (availableHeight - videoHeight) / 2, videoAreaHeight: videoHeight, @@ -1855,10 +1855,10 @@ extension ExpandedPlayerSheet { showingQualitySheet = true }, onShowQueue: { [self] in - showingQueueSheet = true + presentSheet(.queue()) }, onShowPlaylistSelector: { [self] in - showingPlaylistSheet = true + presentAddToPlaylist() }, onTitleTap: { onTogglePanel() diff --git a/Yattee/Views/Player/ExpandedPlayerSheet.swift b/Yattee/Views/Player/ExpandedPlayerSheet.swift index 3faa9a33..0f7660e9 100644 --- a/Yattee/Views/Player/ExpandedPlayerSheet.swift +++ b/Yattee/Views/Player/ExpandedPlayerSheet.swift @@ -18,10 +18,7 @@ struct ExpandedPlayerSheet: View { // MARK: - Sheet State @State var showingQualitySheet = false - @State var showingPlaylistSheet = false - @State var showingDownloadSheet = false @State var showingDeleteDownloadAlert = false - @State var showingQueueSheet = false @State var showingErrorSheet = false @State var isCommentsExpanded: Bool = false @State var commentsDismissOffset: CGFloat = 0 @@ -178,14 +175,38 @@ struct ExpandedPlayerSheet: View { ) } + // MARK: - Shared Sheets + + /// Presents through the player's sheet host. The host sits inside this + /// view's body, so this view's own environment can't reach it; the global + /// request does, because the player host takes requests while it is up. + func presentSheet(_ sheet: AppSheet) { + appEnvironment?.navigationCoordinator.presenter.present(sheet) + } + + func presentAddToPlaylist() { + guard let video = playerState?.currentVideo else { return } + presentSheet(.addToPlaylist(video)) + } + + #if !os(tvOS) + func presentDownloadOptions() { + guard let video = playerState?.currentVideo, let playerService = appEnvironment?.playerService else { return } + presentSheet(.downloadOptions( + video, + streams: playerService.availableStreams, + captions: playerService.availableCaptions, + dislikeCount: playerState?.dislikeCount + )) + } + #endif + // MARK: - Body var body: some View { playerContentWithOverlays .applyPlayerSheets( showingQualitySheet: $showingQualitySheet, - showingPlaylistSheet: $showingPlaylistSheet, - showingDownloadSheet: $showingDownloadSheet, showingDeleteDownloadAlert: $showingDeleteDownloadAlert, onStreamSelected: { stream, audioStream in switchToStream(stream, audioStream: audioStream) @@ -469,9 +490,6 @@ struct ExpandedPlayerSheet: View { } } .animation(.spring(response: 0.3, dampingFraction: 0.7), value: commentsState == .loaded && !comments.isEmpty) - .sheet(isPresented: $showingQueueSheet) { - QueueManagementSheet() - } .sheet(isPresented: $showingErrorSheet) { ErrorDetailsSheet(errorMessage: playerState?.errorMessage ?? "Unknown error") } @@ -495,6 +513,9 @@ struct ExpandedPlayerSheet: View { shouldHost: (appEnvironment?.navigationCoordinator.isPlayerExpanded == true), appEnvironment: appEnvironment ) + // Shared sheets opened from the player, and global requests while it + // is up, so they appear above the player window. + .appSheetHost(isTopLevel: appEnvironment?.navigationCoordinator.isPlayerExpanded == true) #if os(iOS) .toolbar(.hidden, for: .navigationBar) // Corner layout with panels visible keeps the system status bar and @@ -516,15 +537,11 @@ private extension View { @ViewBuilder func applyPlayerSheets( showingQualitySheet: Binding, - showingPlaylistSheet: Binding, - showingDownloadSheet: Binding, showingDeleteDownloadAlert: Binding, onStreamSelected: @escaping (Stream, Stream?) -> Void ) -> some View { modifier(PlayerSheetsModifier( showingQualitySheet: showingQualitySheet, - showingPlaylistSheet: showingPlaylistSheet, - showingDownloadSheet: showingDownloadSheet, showingDeleteDownloadAlert: showingDeleteDownloadAlert, onStreamSelected: onStreamSelected )) @@ -535,8 +552,6 @@ private extension View { private struct PlayerSheetsModifier: ViewModifier { @Environment(\.appEnvironment) var appEnvironment @Binding var showingQualitySheet: Bool - @Binding var showingPlaylistSheet: Bool - @Binding var showingDownloadSheet: Bool @Binding var showingDeleteDownloadAlert: Bool let onStreamSelected: (Stream, Stream?) -> Void @@ -549,15 +564,7 @@ private struct PlayerSheetsModifier: ViewModifier { .sheet(isPresented: $showingQualitySheet) { qualitySelectorSheet } - .sheet(isPresented: $showingPlaylistSheet) { - if let video = playerState?.currentVideo { - PlaylistSelectorSheet(video: video) - } - } #if !os(tvOS) - .sheet(isPresented: $showingDownloadSheet) { - downloadSheet - } .alert( String(localized: "player.deleteDownload.title"), isPresented: $showingDeleteDownloadAlert @@ -648,19 +655,6 @@ private struct PlayerSheetsModifier: ViewModifier { } } - #if !os(tvOS) - @ViewBuilder - private var downloadSheet: some View { - if let video = playerState?.currentVideo, let playerService { - DownloadQualitySheet( - video: video, - streams: playerService.availableStreams, - captions: playerService.availableCaptions, - dislikeCount: playerState?.dislikeCount - ) - } - } - #endif } // MARK: - Player Event Handlers Modifier diff --git a/Yattee/Views/Player/FloatingDetailsPanel.swift b/Yattee/Views/Player/FloatingDetailsPanel.swift index 068247cc..fd9582e4 100644 --- a/Yattee/Views/Player/FloatingDetailsPanel.swift +++ b/Yattee/Views/Player/FloatingDetailsPanel.swift @@ -40,8 +40,7 @@ struct FloatingDetailsPanel: View { @State private var scrollOffset: CGFloat = 0 @State private var panelHeight: CGFloat = 0 @State private var scrollToTopTrigger: Bool = false - @State private var showQueueSheet: Bool = false - @State private var showPlaylistSheet: Bool = false + @Environment(\.presentAppSheet) private var presentAppSheet @State private var isDragHandleActive: Bool = false private var settingsManager: SettingsManager? { appEnvironment?.settingsManager } @@ -173,14 +172,6 @@ struct FloatingDetailsPanel: View { // Re-clamp width when available width changes (e.g., rotation) panelWidth = clampWidth(panelWidth) } - .sheet(isPresented: $showQueueSheet) { - QueueManagementSheet() - } - .sheet(isPresented: $showPlaylistSheet) { - if let currentVideo = playerState?.currentVideo { - PlaylistSelectorSheet(video: currentVideo) - } - } } @ViewBuilder @@ -379,8 +370,7 @@ struct FloatingDetailsPanel: View { }(), video: video, playbackRate: playerState?.rate ?? .x1, - showingPlaylistSheet: $showPlaylistSheet, - onQueueTap: { showQueueSheet = true }, + onQueueTap: { presentAppSheet(.queue()) }, onPrevious: { Task { await playerService?.playPrevious() } }, onPlayPause: { playerService?.togglePlayPause() }, onNext: { Task { await playerService?.playNext() } }, diff --git a/Yattee/Views/Player/MiniPlayerView.swift b/Yattee/Views/Player/MiniPlayerView.swift index 631b2d6a..38c402c8 100644 --- a/Yattee/Views/Player/MiniPlayerView.swift +++ b/Yattee/Views/Player/MiniPlayerView.swift @@ -11,6 +11,7 @@ import NukeUI #if os(iOS) || os(macOS) struct MiniPlayerView: View { @Environment(\.appEnvironment) private var appEnvironment + @Environment(\.presentAppSheet) private var presentAppSheet /// Whether this is displayed as a tab bottom accessory (affects styling) var isTabAccessory: Bool = false @@ -509,7 +510,7 @@ struct MiniPlayerView: View { private func queueButton(config: ControlButtonConfiguration) -> some View { Button { incrementTapCount(for: config) - navigationCoordinator?.isMiniPlayerQueueSheetPresented = true + presentAppSheet(.queue(halfHeight: true)) } label: { ZStack(alignment: .bottom) { Image(systemName: "list.bullet") @@ -550,7 +551,9 @@ struct MiniPlayerView: View { private func addToPlaylistButton(config: ControlButtonConfiguration) -> some View { Button { incrementTapCount(for: config) - navigationCoordinator?.isMiniPlayerPlaylistSheetPresented = true + if let video = appEnvironment?.playerService.state.currentVideo { + presentAppSheet(.addToPlaylist(video, halfHeight: true)) + } } label: { Image(systemName: "text.badge.plus") .font(isTabAccessory ? .title3 : .title2) diff --git a/Yattee/Views/Player/PlayerControlsView.swift b/Yattee/Views/Player/PlayerControlsView.swift index dbe3bf1b..b17b3732 100644 --- a/Yattee/Views/Player/PlayerControlsView.swift +++ b/Yattee/Views/Player/PlayerControlsView.swift @@ -120,7 +120,6 @@ struct PlayerControlsView: View { // MARK: - Sheet State - @State private var showingPlaylistSheet = false @State private var showingCaptionsSheet = false @State private var showingVideoTrackSheet = false @State private var showingAudioTrackSheet = false @@ -305,11 +304,6 @@ struct PlayerControlsView: View { .onReceive(NotificationCenter.default.publisher(for: UIScreen.brightnessDidChangeNotification)) { _ in currentBrightness = UIScreen.main.brightness } - .sheet(isPresented: $showingPlaylistSheet) { - if let video = currentVideo { - PlaylistSelectorSheet(video: video) - } - } .sheet(isPresented: $showingCaptionsSheet) { captionsSheet } @@ -491,7 +485,12 @@ struct PlayerControlsView: View { }, onRateChanged: onRateChanged, onCaptionSelected: onCaptionSelected, - onShowPlaylistSelector: { [self] in showingPlaylistSheet = true }, + onShowPlaylistSelector: { [self] in + // Through the player's sheet host, which takes requests while the player is up + if let video = currentVideo { + appEnvironment?.navigationCoordinator.presenter.present(.addToPlaylist(video)) + } + }, onShowQueue: onShowQueue, onShowCaptionsSelector: { [self] in showingCaptionsSheet = true }, onShowChaptersSelector: { [self] in showingChaptersSheet = true }, diff --git a/Yattee/Views/Player/PortraitDetailsPanel.swift b/Yattee/Views/Player/PortraitDetailsPanel.swift index b618bf62..5393817c 100644 --- a/Yattee/Views/Player/PortraitDetailsPanel.swift +++ b/Yattee/Views/Player/PortraitDetailsPanel.swift @@ -42,8 +42,7 @@ struct PortraitDetailsPanel: View { @State private var selectedPage: PanelPageType = .info @State private var scrollOffset: CGFloat = 0 @State private var scrollToTopTrigger: Bool = false - @State private var showQueueSheet: Bool = false - @State private var showPlaylistSheet: Bool = false + @Environment(\.presentAppSheet) private var presentAppSheet private var settingsManager: SettingsManager? { appEnvironment?.settingsManager } private var accentColor: Color { settingsManager?.resolvedAccentColor ?? .accentColor } @@ -261,14 +260,6 @@ struct PortraitDetailsPanel: View { .onChange(of: isCommentsExpanded) { _, newValue in appEnvironment?.navigationCoordinator.isCommentsExpanded = newValue } - .sheet(isPresented: $showQueueSheet) { - QueueManagementSheet() - } - .sheet(isPresented: $showPlaylistSheet) { - if let currentVideo = playerState?.currentVideo { - PlaylistSelectorSheet(video: currentVideo) - } - } } } @@ -431,8 +422,7 @@ struct PortraitDetailsPanel: View { isOrientationLocked: appEnvironment?.settingsManager.inAppOrientationLock ?? false, video: video, playbackRate: playerState?.rate ?? .x1, - showingPlaylistSheet: $showPlaylistSheet, - onQueueTap: { showQueueSheet = true }, + onQueueTap: { presentAppSheet(.queue()) }, onPrevious: { Task { await playerService?.playPrevious() } }, onPlayPause: { playerService?.togglePlayPause() }, onNext: { Task { await playerService?.playNext() } }, diff --git a/Yattee/Views/Player/QueueManagementSheet.swift b/Yattee/Views/Player/QueueManagementSheet.swift index c260b668..0e1fea10 100644 --- a/Yattee/Views/Player/QueueManagementSheet.swift +++ b/Yattee/Views/Player/QueueManagementSheet.swift @@ -166,7 +166,6 @@ struct QueueManagementSheet: View { tvOSQueueListView #else QueueListView(onPlay: { - navigationCoordinator?.isMiniPlayerQueueSheetPresented = false performDismiss() }) #endif @@ -177,7 +176,6 @@ struct QueueManagementSheet: View { TVQueueListView( autoFocusOnAppear: true, onDidSelectVideo: { - navigationCoordinator?.isMiniPlayerQueueSheetPresented = false performDismiss() } ) diff --git a/Yattee/Views/Video/VideoInfoView.swift b/Yattee/Views/Video/VideoInfoView.swift index 60fdeb52..2573581f 100644 --- a/Yattee/Views/Video/VideoInfoView.swift +++ b/Yattee/Views/Video/VideoInfoView.swift @@ -19,6 +19,7 @@ enum VideoInfoInitMode: Sendable { struct VideoInfoView: View { @Environment(\.appEnvironment) private var appEnvironment + @Environment(\.presentAppSheet) private var presentAppSheet @Environment(\.videoQueueContext) private var videoQueueContext private let initMode: VideoInfoInitMode @@ -45,7 +46,6 @@ struct VideoInfoView: View { @State private var isBookmarked = false @State private var showingErrorDetailsSheet = false - @State private var showingPlaylistSheet = false @State private var showingCommentsSheet = false @State private var showingRemoveBookmarkAlert = false @State private var currentBookmark: Bookmark? @@ -102,7 +102,6 @@ struct VideoInfoView: View { // Download state @State private var download: Download? - @State private var showingDownloadSheet = false @State private var isEnqueuingDownload = false @State private var showingRemoveDownloadConfirmation = false @@ -394,8 +393,6 @@ struct VideoInfoView: View { // Reset state when navigating to different video showingCommentsSheet = false - showingPlaylistSheet = false - showingDownloadSheet = false showingRemoveBookmarkAlert = false bookmarkSaveTask?.cancel() comments = [] @@ -441,11 +438,6 @@ struct VideoInfoView: View { await loadVideoDetails() } } - .sheet(isPresented: $showingPlaylistSheet) { - if let video = displayedVideo { - PlaylistSelectorSheet(video: video) - } - } #if !os(tvOS) .sheet(isPresented: $showingErrorDetailsSheet) { ErrorDetailsSheet(errorMessage: videoDetailsLoadError ?? initialVideoLoadError ?? "Unknown error") @@ -469,11 +461,6 @@ struct VideoInfoView: View { ) } #if !os(tvOS) - .sheet(isPresented: $showingDownloadSheet) { - if let video = displayedVideo { - DownloadQualitySheet(video: video) - } - } .onChange(of: downloadManager?.completedDownloads) { _, newValue in // Update download state when completedDownloads changes if let video = displayedVideo { @@ -726,7 +713,7 @@ struct VideoInfoView: View { .focused($isPlayFocused) Button { - showingPlaylistSheet = true + presentAddToPlaylist() } label: { Label(String(localized: "video.context.addToPlaylist"), systemImage: "text.badge.plus") .frame(maxWidth: .infinity, minHeight: 50) @@ -1326,7 +1313,7 @@ struct VideoInfoView: View { // Add to Playlist and Bookmark HStack(spacing: 12) { Button { - showingPlaylistSheet = true + presentAddToPlaylist() } label: { Label(String(localized: "video.context.addToPlaylist"), systemImage: "text.badge.plus") .frame(maxWidth: .infinity) @@ -1409,7 +1396,7 @@ struct VideoInfoView: View { } case .addToPlaylist: Button { - showingPlaylistSheet = true + presentAddToPlaylist() } label: { Label(String(localized: "video.context.addToPlaylist"), systemImage: "text.badge.plus") } @@ -1565,7 +1552,7 @@ struct VideoInfoView: View { /// Starts a download either automatically or by showing the quality sheet. private func startDownload(for video: Video) { guard let appEnvironment else { - showingDownloadSheet = true + presentAppSheet(.downloadOptions(video)) return } @@ -1627,7 +1614,7 @@ struct VideoInfoView: View { } } } else { - showingDownloadSheet = true + presentAppSheet(.downloadOptions(video)) } } @@ -2264,6 +2251,11 @@ struct VideoInfoView: View { // MARK: - Actions + private func presentAddToPlaylist() { + guard let video = displayedVideo else { return } + presentAppSheet(.addToPlaylist(video)) + } + private func toggleBookmark() { guard let dataManager, let video = displayedVideo else { return }