mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 13:12:53 +00:00
Present shared sheets through one host per window
The add-to-playlist, download options and queue sheets were declared in every view that could open them: the root view (twice, mirrored from coordinator flags), the expanded player, its portrait and floating panels, the player controls and the video info view. AppSheet lists the shared sheets, and .appSheetHost() declares them once. The app root and the expanded player each host them, and every presented sheet hosts the next level. Views present through the presentAppSheet environment action, which reaches the nearest host; code without a view (and the expanded player itself, whose host sits in its own body) uses PresentationRouter.request, taken by the top-most host: the player while it is up, the root otherwise. The mini player's queue and playlist sheets now go through the host, replacing the isMiniPlayer*SheetPresented flags.
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
165
Yattee/Services/Navigation/PresentationRouter.swift
Normal file
165
Yattee/Services/Navigation/PresentationRouter.swift
Normal file
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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: {}
|
||||
|
||||
@@ -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<Bool> = .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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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<Bool>,
|
||||
showingPlaylistSheet: Binding<Bool>,
|
||||
showingDownloadSheet: Binding<Bool>,
|
||||
showingDeleteDownloadAlert: Binding<Bool>,
|
||||
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
|
||||
|
||||
@@ -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() } },
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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() } },
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user