diff --git a/Yattee/Services/Navigation/NavigationCoordinator.swift b/Yattee/Services/Navigation/NavigationCoordinator.swift index 129b32d4..89f7e23b 100644 --- a/Yattee/Services/Navigation/NavigationCoordinator.swift +++ b/Yattee/Services/Navigation/NavigationCoordinator.swift @@ -63,14 +63,8 @@ final class NavigationCoordinator { /// (remote control state) reads the real layout instead of screen bounds. var isWideScreenLayoutActive = 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 - /// when the player is expanded, so the sheet always sits above the visible layer. - var descriptionLinkQueueSheetVideo: Video? - /// Resolved short-link URL awaiting user confirmation ("Try in Yattee" vs - /// "Open in Browser"). Dual-hosted like `descriptionLinkQueueSheetVideo` + /// "Open in Browser"). Dual-hosted (root and expanded player) /// so the dialog is visible whether or not the expanded player is covering /// the main app view. var resolvedShortLinkPrompt: URL? diff --git a/Yattee/Services/Navigation/PresentationRouter.swift b/Yattee/Services/Navigation/PresentationRouter.swift index 049303aa..4fa23c88 100644 --- a/Yattee/Services/Navigation/PresentationRouter.swift +++ b/Yattee/Services/Navigation/PresentationRouter.swift @@ -20,6 +20,10 @@ enum AppSheet: Identifiable { case queue(halfHeight: Bool = false) /// Add a video to a playlist. `halfHeight` opens at the medium detent. case addToPlaylist(Video, halfHeight: Bool = false) + /// Play now / queue choices for a tapped video while something is playing. + case queueAction(Video, queueSource: QueueSource? = nil) + /// Continue / start over for a partially watched video. + case resumeAction(Video, resumeTime: TimeInterval, onContinue: () -> Void, onStartOver: () -> Void) #if !os(tvOS) /// Download quality picker. Streams and captions already loaded by the /// caller (the player) skip the sheet's own fetch. @@ -32,6 +36,10 @@ enum AppSheet: Identifiable { return "queue" case .addToPlaylist(let video, _): return "addToPlaylist-\(video.id.id)" + case .queueAction(let video, _): + return "queueAction-\(video.id.id)" + case .resumeAction(let video, _, _, _): + return "resumeAction-\(video.id.id)" #if !os(tvOS) case .downloadOptions(let video, _, _, _): return "downloadOptions-\(video.id.id)" @@ -85,16 +93,9 @@ private struct AppSheetHost: ViewModifier { 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() } @@ -121,6 +122,10 @@ private struct AppSheetHost: ViewModifier { case .addToPlaylist(let video, let halfHeight): PlaylistSelectorSheet(video: video) .halfHeightDetents(halfHeight) + case .queueAction(let video, let queueSource): + QueueActionSheet(video: video, queueSource: queueSource) + case .resumeAction(let video, let resumeTime, let onContinue, let onStartOver): + ResumeActionSheet(video: video, resumeTime: resumeTime, onContinue: onContinue, onStartOver: onStartOver) #if !os(tvOS) case .downloadOptions(let video, let streams, let captions, let dislikeCount): DownloadQualitySheet(video: video, streams: streams, captions: captions, dislikeCount: dislikeCount) diff --git a/Yattee/Views/Components/ResolvedLinkPromptsModifier.swift b/Yattee/Views/Components/ResolvedLinkPromptsModifier.swift index 0048c2fc..b5460d39 100644 --- a/Yattee/Views/Components/ResolvedLinkPromptsModifier.swift +++ b/Yattee/Views/Components/ResolvedLinkPromptsModifier.swift @@ -13,8 +13,7 @@ // The state lives on `NavigationCoordinator` so both the root app view and // `ExpandedPlayerSheet` can host the dialogs. Only whichever host is currently // on top presents the dialog (`shouldHost`), so the dialog is visible whether -// the expanded player is covering the main view or not — matching the -// `descriptionLinkQueueSheetVideo` pattern. +// the expanded player is covering the main view or not. // import SwiftUI diff --git a/Yattee/Views/Components/TappableVideoModifier.swift b/Yattee/Views/Components/TappableVideoModifier.swift index 45e0d846..f7761b1b 100644 --- a/Yattee/Views/Components/TappableVideoModifier.swift +++ b/Yattee/Views/Components/TappableVideoModifier.swift @@ -7,37 +7,12 @@ import SwiftUI -/// Data for the resume action sheet, used with sheet(item:) to ensure data availability. -struct ResumeSheetData: Identifiable { - let id = UUID() - let video: Video - let resumeTime: TimeInterval -} - /// View modifier that wraps content in a button that plays a video when tapped. /// When queue is enabled and not empty, shows a sheet with queue options. /// When queue is empty or disabled, plays the video directly and queues subsequent videos. -// MARK: - Presentation Color Scheme - -/// The real system color scheme, published by hosts that override -/// `colorScheme` for their own content (the unpinned floating details panel -/// renders dark over video). Sheets presented from inside such a host use it -/// so they don't get dark text colors on a light system sheet background. -private struct PresentationColorSchemeKey: EnvironmentKey { - static let defaultValue: ColorScheme? = nil -} - -extension EnvironmentValues { - var presentationColorScheme: ColorScheme? { - get { self[PresentationColorSchemeKey.self] } - set { self[PresentationColorSchemeKey.self] = newValue } - } -} - struct TappableVideoModifier: ViewModifier { @Environment(\.appEnvironment) private var appEnvironment - @Environment(\.colorScheme) private var colorScheme - @Environment(\.presentationColorScheme) private var presentationColorScheme + @Environment(\.presentAppSheet) private var presentAppSheet let video: Video var startTime: Double? = nil @@ -56,11 +31,6 @@ struct TappableVideoModifier: ViewModifier { /// Callback to load more videos via continuation nonisolated(unsafe) var loadMoreVideos: LoadMoreVideosCallback? = nil - @State private var showingQueueSheet = false - - // Resume action sheet state - using item-based sheet to ensure data is available when presented - @State private var resumeSheetData: ResumeSheetData? = nil - // Password alert state (for WebDAV sources) @State private var showingPasswordAlert = false @State private var sourceNeedingPassword: MediaSource? @@ -102,19 +72,6 @@ struct TappableVideoModifier: ViewModifier { startTime: startTime ) } - .sheet(isPresented: $showingQueueSheet) { - QueueActionSheet(video: video, queueSource: queueSource) - .environment(\.colorScheme, presentationColorScheme ?? colorScheme) - } - .sheet(item: $resumeSheetData) { data in - ResumeActionSheet( - video: data.video, - resumeTime: data.resumeTime, - onContinue: { playVideoWithStartTime(data.resumeTime) }, - onStartOver: { playVideoWithStartTime(0) } - ) - .environment(\.colorScheme, presentationColorScheme ?? colorScheme) - } .alert(String(localized: "common.authenticationRequired"), isPresented: $showingPasswordAlert) { SecureField(String(localized: "common.password"), text: $passwordInput) Button(String(localized: "common.cancel"), role: .cancel) { @@ -165,7 +122,7 @@ struct TappableVideoModifier: ViewModifier { sourceNeedingPassword = source showingPasswordAlert = true } else if shouldShowQueueSheet { - showingQueueSheet = true + presentAppSheet(.queueAction(video, queueSource: queueSource)) } else { playVideoAndQueueRest() } @@ -180,7 +137,7 @@ struct TappableVideoModifier: ViewModifier { // Now check if we should show queue sheet or play directly if shouldShowQueueSheet { - showingQueueSheet = true + presentAppSheet(.queueAction(video, queueSource: queueSource)) } else { playVideoAndQueueRest() } @@ -229,7 +186,12 @@ struct TappableVideoModifier: ViewModifier { playVideoWithStartTime(0) case .ask: // Show the resume action sheet with data bundled together - resumeSheetData = ResumeSheetData(video: video, resumeTime: savedProgress) + presentAppSheet(.resumeAction( + video, + resumeTime: savedProgress, + onContinue: { playVideoWithStartTime(savedProgress) }, + onStartOver: { playVideoWithStartTime(0) } + )) } } else { // No saved progress or video was completed - play from beginning diff --git a/Yattee/Views/Components/VideoContextMenu.swift b/Yattee/Views/Components/VideoContextMenu.swift index 54984a6f..2454465c 100644 --- a/Yattee/Views/Components/VideoContextMenu.swift +++ b/Yattee/Views/Components/VideoContextMenu.swift @@ -49,8 +49,6 @@ struct VideoContextMenuModifier: ViewModifier { var startTime: Double? = nil var watchProgress: Double? = nil - @State private var showingPlaylistSheet = false - @State private var showingDownloadSheet = false @State private var showingDeleteDownloadConfirmation = false @State private var downloadToDelete: Download? @State private var menuRefreshID = UUID() @@ -64,8 +62,6 @@ struct VideoContextMenuModifier: ViewModifier { customActions: customActions, context: context, startTime: startTime, - showingPlaylistSheet: $showingPlaylistSheet, - showingDownloadSheet: $showingDownloadSheet, showingDeleteDownloadConfirmation: $showingDeleteDownloadConfirmation, downloadToDelete: $downloadToDelete, appEnvironment: appEnvironment @@ -79,8 +75,6 @@ struct VideoContextMenuModifier: ViewModifier { customActions: customActions, context: context, startTime: startTime, - showingPlaylistSheet: $showingPlaylistSheet, - showingDownloadSheet: $showingDownloadSheet, showingDeleteDownloadConfirmation: $showingDeleteDownloadConfirmation, downloadToDelete: $downloadToDelete, appEnvironment: appEnvironment @@ -98,13 +92,7 @@ struct VideoContextMenuModifier: ViewModifier { .environment(\.appEnvironment, appEnvironment) }) #endif - .sheet(isPresented: $showingPlaylistSheet) { - PlaylistSelectorSheet(video: video) - } #if !os(tvOS) - .sheet(isPresented: $showingDownloadSheet) { - DownloadQualitySheet(video: video) - } .alert(String(localized: "videoInfo.download.remove.title"), isPresented: $showingDeleteDownloadConfirmation) { Button(String(localized: "common.cancel"), role: .cancel) {} Button(String(localized: "videoInfo.download.remove.confirm"), role: .destructive) { @@ -130,13 +118,12 @@ struct VideoContextMenuModifier: ViewModifier { /// All observable values are snapshotted at init time to prevent redraws during playback. struct VideoContextMenuContent: View { @Environment(\.appEnvironment) private var appEnvironment + @Environment(\.presentAppSheet) private var presentAppSheet let video: Video var customActions: [VideoContextAction] = [] var context: VideoContextMenuContext = .default var startTime: Double? = nil - @Binding var showingPlaylistSheet: Bool - @Binding var showingDownloadSheet: Bool @Binding var showingDeleteDownloadConfirmation: Bool @Binding var downloadToDelete: Download? @@ -168,8 +155,6 @@ struct VideoContextMenuContent: View { customActions: [VideoContextAction] = [], context: VideoContextMenuContext = .default, startTime: Double? = nil, - showingPlaylistSheet: Binding, - showingDownloadSheet: Binding, showingDeleteDownloadConfirmation: Binding = .constant(false), downloadToDelete: Binding = .constant(nil), appEnvironment: AppEnvironment? = nil @@ -178,8 +163,6 @@ struct VideoContextMenuContent: View { self.customActions = customActions self.context = context self.startTime = startTime - self._showingPlaylistSheet = showingPlaylistSheet - self._showingDownloadSheet = showingDownloadSheet self._showingDeleteDownloadConfirmation = showingDeleteDownloadConfirmation self._downloadToDelete = downloadToDelete @@ -427,7 +410,7 @@ struct VideoContextMenuContent: View { // Add to playlist if showAddToPlaylist { Button { - showingPlaylistSheet = true + presentAppSheet(.addToPlaylist(video)) } label: { Label(String(localized: "video.context.addToPlaylist"), systemImage: "text.badge.plus") } @@ -440,7 +423,7 @@ struct VideoContextMenuContent: 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 } @@ -494,7 +477,7 @@ struct VideoContextMenuContent: View { } } } else { - showingDownloadSheet = true + presentAppSheet(.downloadOptions(video)) } } #endif @@ -613,8 +596,6 @@ struct VideoContextMenuView: View { var buttonBackgroundStyle: ButtonBackgroundStyle = .none var theme: ControlsTheme = .dark - @State private var showingPlaylistSheet = false - @State private var showingDownloadSheet = false @State private var showingDeleteDownloadConfirmation = false @State private var downloadToDelete: Download? @State private var refreshID = UUID() @@ -628,8 +609,6 @@ struct VideoContextMenuView: View { VideoContextMenuContent( video: video, context: .player, - showingPlaylistSheet: $showingPlaylistSheet, - showingDownloadSheet: $showingDownloadSheet, showingDeleteDownloadConfirmation: $showingDeleteDownloadConfirmation, downloadToDelete: $downloadToDelete, appEnvironment: appEnvironment @@ -645,12 +624,6 @@ struct VideoContextMenuView: View { .menuStyle(.borderlessButton) .fixedSize() #endif - .sheet(isPresented: $showingPlaylistSheet) { - PlaylistSelectorSheet(video: video) - } - .sheet(isPresented: $showingDownloadSheet) { - DownloadQualitySheet(video: video) - } .alert(String(localized: "videoInfo.download.remove.title"), isPresented: $showingDeleteDownloadConfirmation) { Button(String(localized: "common.cancel"), role: .cancel) {} Button(String(localized: "videoInfo.download.remove.confirm"), role: .destructive) { diff --git a/Yattee/Views/Components/VideoSwipeActionsModifier.swift b/Yattee/Views/Components/VideoSwipeActionsModifier.swift index f47a6fdc..2af22f3c 100644 --- a/Yattee/Views/Components/VideoSwipeActionsModifier.swift +++ b/Yattee/Views/Components/VideoSwipeActionsModifier.swift @@ -12,22 +12,14 @@ import SwiftUI struct VideoSwipeActionsModifier: ViewModifier { @Environment(\.appEnvironment) private var appEnvironment @Environment(\.videoQueueContext) private var queueContext + @Environment(\.presentAppSheet) private var presentAppSheet let video: Video var fixedActions: [SwipeAction] = [] - @State private var showingPlaylistSheet = false - @State private var showingDownloadSheet = false - func body(content: Content) -> some View { content .swipeActions(actionsArray: allActions()) - .sheet(isPresented: $showingPlaylistSheet) { - PlaylistSelectorSheet(video: video) - } - .sheet(isPresented: $showingDownloadSheet) { - DownloadQualitySheet(video: video) - } } private var visibleActions: [VideoSwipeAction] { @@ -104,7 +96,7 @@ struct VideoSwipeActionsModifier: ViewModifier { tint: action.tint, background: action.backgroundColor ) { reset in - showingPlaylistSheet = true + presentAppSheet(.addToPlaylist(video)) reset() } @@ -223,7 +215,7 @@ struct VideoSwipeActionsModifier: ViewModifier { private func startDownload() { guard let appEnvironment else { - showingDownloadSheet = true + presentAppSheet(.downloadOptions(video)) return } @@ -277,7 +269,7 @@ struct VideoSwipeActionsModifier: ViewModifier { } } } else { - showingDownloadSheet = true + presentAppSheet(.downloadOptions(video)) } } } diff --git a/Yattee/Views/Player/ExpandedPlayerSheet.swift b/Yattee/Views/Player/ExpandedPlayerSheet.swift index 0f7660e9..cb429523 100644 --- a/Yattee/Views/Player/ExpandedPlayerSheet.swift +++ b/Yattee/Views/Player/ExpandedPlayerSheet.swift @@ -493,19 +493,6 @@ struct ExpandedPlayerSheet: View { .sheet(isPresented: $showingErrorSheet) { ErrorDetailsSheet(errorMessage: playerState?.errorMessage ?? "Unknown error") } - .sheet(item: Binding( - get: { appEnvironment?.navigationCoordinator.descriptionLinkQueueSheetVideo }, - set: { newValue in - if newValue == nil { - appEnvironment?.navigationCoordinator.descriptionLinkQueueSheetVideo = nil - } - } - )) { video in - if let appEnvironment { - QueueActionSheet(video: video) - .appEnvironment(appEnvironment) - } - } // Host the resolved/ambiguous link confirmation dialogs while the // expanded player is up so they appear above this sheet rather than // being buried underneath it on the root app view. diff --git a/Yattee/Views/Player/FloatingDetailsPanel.swift b/Yattee/Views/Player/FloatingDetailsPanel.swift index fd9582e4..44408ada 100644 --- a/Yattee/Views/Player/FloatingDetailsPanel.swift +++ b/Yattee/Views/Player/FloatingDetailsPanel.swift @@ -154,7 +154,6 @@ struct FloatingDetailsPanel: View { } } .environment(\.colorScheme, isPinned ? systemColorScheme : .dark) - .environment(\.presentationColorScheme, systemColorScheme) .animation(.easeInOut(duration: 0.25), value: isPinned) .onChange(of: video.id) { _, _ in // Comments state is managed by parent - just reset local state diff --git a/Yattee/Views/Video/VideoInfoView.swift b/Yattee/Views/Video/VideoInfoView.swift index 2573581f..666d554a 100644 --- a/Yattee/Views/Video/VideoInfoView.swift +++ b/Yattee/Views/Video/VideoInfoView.swift @@ -106,7 +106,6 @@ struct VideoInfoView: View { @State private var showingRemoveDownloadConfirmation = false // Resume action sheet state - @State private var resumeSheetData: ResumeSheetData? // Video details cache - stores full video details loaded from API @State private var loadedVideoDetails: [String: Video] = [:] @@ -452,14 +451,6 @@ struct VideoInfoView: View { commentsSheetContent } #endif - .sheet(item: $resumeSheetData) { data in - ResumeActionSheet( - video: data.video, - resumeTime: data.resumeTime, - onContinue: { playVideoWithStartTime(data.resumeTime) }, - onStartOver: { playVideoWithStartTime(0) } - ) - } #if !os(tvOS) .onChange(of: downloadManager?.completedDownloads) { _, newValue in // Update download state when completedDownloads changes @@ -2571,7 +2562,12 @@ struct VideoInfoView: View { playVideoWithStartTime(0) case .ask: // Show the resume action sheet - resumeSheetData = ResumeSheetData(video: video, resumeTime: savedProgress) + presentAppSheet(.resumeAction( + video, + resumeTime: savedProgress, + onContinue: { playVideoWithStartTime(savedProgress) }, + onStartOver: { playVideoWithStartTime(0) } + )) } } else { // No saved progress or video was completed - play from beginning diff --git a/Yattee/YatteeApp.swift b/Yattee/YatteeApp.swift index 56045d7b..07dcd267 100644 --- a/Yattee/YatteeApp.swift +++ b/Yattee/YatteeApp.swift @@ -123,22 +123,6 @@ struct YatteeApp: App { OpenLinkSheet(prefilledURL: url) .appEnvironment(appEnvironment) } - .sheet(item: Binding( - get: { - // Only host the sheet here when the player is not expanded. - // When expanded, ExpandedPlayerSheet hosts it so it appears above the player window. - guard !appEnvironment.navigationCoordinator.isPlayerExpanded else { return nil } - return appEnvironment.navigationCoordinator.descriptionLinkQueueSheetVideo - }, - set: { newValue in - if newValue == nil { - appEnvironment.navigationCoordinator.descriptionLinkQueueSheetVideo = nil - } - } - )) { video in - QueueActionSheet(video: video) - .appEnvironment(appEnvironment) - } #if !os(tvOS) .sheet(isPresented: $showingDeepLinkDownloadSheet) { if let video = deepLinkVideo { @@ -598,7 +582,8 @@ struct YatteeApp: App { instance: instance ) appEnvironment.toastManager.dismiss(id: toastID) - appEnvironment.navigationCoordinator.descriptionLinkQueueSheetVideo = video + // The top-most sheet host (the player when expanded) presents it + appEnvironment.navigationCoordinator.presenter.present(.queueAction(video)) } catch { appEnvironment.toastManager.dismiss(id: toastID) appEnvironment.navigationCoordinator.navigate(to: .video(.id(videoID)))