mirror of
https://github.com/yattee/yattee.git
synced 2026-10-23 13:12:53 +00:00
Present error details and Open Link through the shared sheet host
Error details were declared in the video info view and the expanded player, and Open Link three times: at the app root for deep links and for the File menu command, and on Home. All of them are now AppSheets; the deep link and menu requests go through the global request, so a deep link that arrives while the player is expanded no longer opens the sheet behind it. The four sign-in modals that were a full-screen cover on tvOS and a sheet elsewhere, each written out twice, use a platformSheet helper.
This commit is contained in:
23
Yattee/Extensions/View+PlatformSheet.swift
Normal file
23
Yattee/Extensions/View+PlatformSheet.swift
Normal file
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// View+PlatformSheet.swift
|
||||
// Yattee
|
||||
//
|
||||
// A modal that is a full-screen cover on tvOS and a sheet elsewhere, for
|
||||
// forms (sign-in) that need the whole screen on tvOS.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
func platformSheet<Item: Identifiable, Content: View>(
|
||||
item: Binding<Item?>,
|
||||
onDismiss: (() -> Void)? = nil,
|
||||
@ViewBuilder content: @escaping (Item) -> Content
|
||||
) -> some View {
|
||||
#if os(tvOS)
|
||||
fullScreenCover(item: item, onDismiss: onDismiss, content: content)
|
||||
#else
|
||||
sheet(item: item, onDismiss: onDismiss, content: content)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,10 @@ enum AppSheet: Identifiable {
|
||||
case addToPlaylist(Video, halfHeight: Bool = false)
|
||||
/// Play now / queue choices for a tapped video while something is playing.
|
||||
case queueAction(Video, queueSource: QueueSource? = nil)
|
||||
/// Details of a playback or loading error.
|
||||
case errorDetails(String)
|
||||
/// Open a link, optionally prefilled.
|
||||
case openLink(URL?)
|
||||
/// Continue / start over for a partially watched video.
|
||||
case resumeAction(Video, resumeTime: TimeInterval, onContinue: () -> Void, onStartOver: () -> Void)
|
||||
#if !os(tvOS)
|
||||
@@ -36,6 +40,10 @@ enum AppSheet: Identifiable {
|
||||
return "queue"
|
||||
case .addToPlaylist(let video, _):
|
||||
return "addToPlaylist-\(video.id.id)"
|
||||
case .errorDetails:
|
||||
return "errorDetails"
|
||||
case .openLink:
|
||||
return "openLink"
|
||||
case .queueAction(let video, _):
|
||||
return "queueAction-\(video.id.id)"
|
||||
case .resumeAction(let video, _, _, _):
|
||||
@@ -122,6 +130,10 @@ private struct AppSheetHost: ViewModifier {
|
||||
case .addToPlaylist(let video, let halfHeight):
|
||||
PlaylistSelectorSheet(video: video)
|
||||
.halfHeightDetents(halfHeight)
|
||||
case .errorDetails(let message):
|
||||
ErrorDetailsSheet(errorMessage: message)
|
||||
case .openLink(let url):
|
||||
OpenLinkSheet(prefilledURL: url)
|
||||
case .queueAction(let video, let queueSource):
|
||||
QueueActionSheet(video: video, queueSource: queueSource)
|
||||
case .resumeAction(let video, let resumeTime, let onContinue, let onStartOver):
|
||||
|
||||
@@ -9,6 +9,7 @@ import SwiftUI
|
||||
|
||||
struct HomeView: View {
|
||||
@Environment(\.appEnvironment) private var appEnvironment
|
||||
@Environment(\.presentAppSheet) private var presentAppSheet
|
||||
#if os(iOS)
|
||||
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
|
||||
#endif
|
||||
@@ -37,7 +38,6 @@ struct HomeView: View {
|
||||
#if !os(tvOS) && !os(macOS)
|
||||
@State private var showingSettings = false
|
||||
#endif
|
||||
@State private var showingOpenLink = false
|
||||
@State private var showingRemoteControl = false
|
||||
@State private var showingCustomizeHome = false
|
||||
@State private var channelsCount: Int = 0
|
||||
@@ -201,9 +201,6 @@ struct HomeView: View {
|
||||
showingSettings = false
|
||||
}
|
||||
#endif
|
||||
.sheet(isPresented: $showingOpenLink) {
|
||||
OpenLinkSheet()
|
||||
}
|
||||
.sheet(isPresented: $showingRemoteControl) {
|
||||
RemoteDevicesSheet()
|
||||
}
|
||||
@@ -712,7 +709,7 @@ struct HomeView: View {
|
||||
|
||||
private var openURLShortcutCard: some View {
|
||||
Button {
|
||||
showingOpenLink = true
|
||||
presentAppSheet(.openLink(nil))
|
||||
} label: {
|
||||
HomeShortcutCardView(
|
||||
icon: "link",
|
||||
@@ -974,7 +971,7 @@ struct HomeView: View {
|
||||
|
||||
private var openURLShortcutRow: some View {
|
||||
Button {
|
||||
showingOpenLink = true
|
||||
presentAppSheet(.openLink(nil))
|
||||
} label: {
|
||||
HomeShortcutRowView(
|
||||
icon: "link",
|
||||
|
||||
@@ -576,19 +576,11 @@ struct InstanceBrowseView: View {
|
||||
Text(accounts)
|
||||
}
|
||||
}
|
||||
#if os(tvOS)
|
||||
.fullScreenCover(item: $loginTarget) { target in
|
||||
.platformSheet(item: $loginTarget) { target in
|
||||
InstanceLoginView(instance: instance, initialUsername: storedUsername(for: target)) { credential, username in
|
||||
storeLogin(credential: credential, username: username, target: target)
|
||||
}
|
||||
}
|
||||
#else
|
||||
.sheet(item: $loginTarget) { target in
|
||||
InstanceLoginView(instance: instance, initialUsername: storedUsername(for: target)) { credential, username in
|
||||
storeLogin(credential: credential, username: username, target: target)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
.confirmationDialog(
|
||||
String(localized: "accounts.remove.confirm.title \(pendingSignOut.map(accountDisplayName) ?? instance.displayName)"),
|
||||
isPresented: Binding(
|
||||
|
||||
@@ -109,7 +109,7 @@ extension ExpandedPlayerSheet {
|
||||
VStack(spacing: 16) {
|
||||
// Error details button
|
||||
Button {
|
||||
showingErrorSheet = true
|
||||
presentSheet(.errorDetails(playerState?.errorMessage ?? "Unknown error"))
|
||||
} label: {
|
||||
Label(String(localized: "player.error.button"), systemImage: "info.circle")
|
||||
.font(.subheadline.weight(.medium))
|
||||
|
||||
@@ -19,7 +19,6 @@ struct ExpandedPlayerSheet: View {
|
||||
|
||||
@State var showingQualitySheet = false
|
||||
@State var showingDeleteDownloadAlert = false
|
||||
@State var showingErrorSheet = false
|
||||
@State var isCommentsExpanded: Bool = false
|
||||
@State var commentsDismissOffset: CGFloat = 0
|
||||
/// Direct scroll position control - use scrollTo(y:) to scroll to top
|
||||
@@ -490,9 +489,6 @@ struct ExpandedPlayerSheet: View {
|
||||
}
|
||||
}
|
||||
.animation(.spring(response: 0.3, dampingFraction: 0.7), value: commentsState == .loaded && !comments.isEmpty)
|
||||
.sheet(isPresented: $showingErrorSheet) {
|
||||
ErrorDetailsSheet(errorMessage: playerState?.errorMessage ?? "Unknown error")
|
||||
}
|
||||
// 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.
|
||||
|
||||
@@ -79,15 +79,9 @@ struct AccountsListView: View {
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
#if os(tvOS)
|
||||
.fullScreenCover(item: $login) { login in
|
||||
.platformSheet(item: $login) { login in
|
||||
loginSheet(login)
|
||||
}
|
||||
#else
|
||||
.sheet(item: $login) { login in
|
||||
loginSheet(login)
|
||||
}
|
||||
#endif
|
||||
.confirmationDialog(
|
||||
pendingRemoval.map { String(localized: "accounts.remove.confirm.title \($0.name)") } ?? "",
|
||||
isPresented: Binding(
|
||||
|
||||
@@ -353,19 +353,11 @@ private struct EditRemoteServerContent: View {
|
||||
}
|
||||
}
|
||||
.presentationCompactAdaptation(.sheet)
|
||||
#if os(tvOS)
|
||||
.fullScreenCover(item: $loginTarget) { target in
|
||||
.platformSheet(item: $loginTarget) { target in
|
||||
InstanceLoginView(instance: instance, initialUsername: storedUsername(for: target)) { credential, username in
|
||||
storeLogin(credential: credential, username: username, target: target)
|
||||
}
|
||||
}
|
||||
#else
|
||||
.sheet(item: $loginTarget) { target in
|
||||
InstanceLoginView(instance: instance, initialUsername: storedUsername(for: target)) { credential, username in
|
||||
storeLogin(credential: credential, username: username, target: target)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
// Signing out happens in the accounts section below, which cannot reach
|
||||
// this state directly. Without this the authenticated import actions
|
||||
// stay on screen after the last account is removed, and then fail.
|
||||
|
||||
@@ -45,7 +45,6 @@ struct VideoInfoView: View {
|
||||
@State private var loadMoreError: String?
|
||||
|
||||
@State private var isBookmarked = false
|
||||
@State private var showingErrorDetailsSheet = false
|
||||
@State private var showingCommentsSheet = false
|
||||
@State private var showingRemoveBookmarkAlert = false
|
||||
@State private var currentBookmark: Bookmark?
|
||||
@@ -437,11 +436,6 @@ struct VideoInfoView: View {
|
||||
await loadVideoDetails()
|
||||
}
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.sheet(isPresented: $showingErrorDetailsSheet) {
|
||||
ErrorDetailsSheet(errorMessage: videoDetailsLoadError ?? initialVideoLoadError ?? "Unknown error")
|
||||
}
|
||||
#endif
|
||||
#if os(tvOS)
|
||||
.fullScreenCover(isPresented: $showingCommentsSheet) {
|
||||
commentsSheetContent
|
||||
@@ -878,7 +872,7 @@ struct VideoInfoView: View {
|
||||
|
||||
#if !os(tvOS)
|
||||
Button {
|
||||
showingErrorDetailsSheet = true
|
||||
presentAppSheet(.errorDetails(videoDetailsLoadError ?? initialVideoLoadError ?? "Unknown error"))
|
||||
} label: {
|
||||
Label(String(localized: "player.error.button"), systemImage: "info.circle")
|
||||
}
|
||||
@@ -1977,7 +1971,7 @@ struct VideoInfoView: View {
|
||||
|
||||
#if !os(tvOS)
|
||||
Button {
|
||||
showingErrorDetailsSheet = true
|
||||
presentAppSheet(.errorDetails(videoDetailsLoadError ?? initialVideoLoadError ?? "Unknown error"))
|
||||
} label: {
|
||||
Label(String(localized: "player.error.button"), systemImage: "info.circle")
|
||||
}
|
||||
|
||||
@@ -38,7 +38,6 @@ struct YatteeApp: App {
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
|
||||
// Deep link handling state
|
||||
@State private var prefilledLinkURL: URL?
|
||||
#if !os(tvOS)
|
||||
@State private var deepLinkVideo: Video?
|
||||
@State private var deepLinkStreams: [Stream] = []
|
||||
@@ -51,7 +50,6 @@ struct YatteeApp: App {
|
||||
@State private var showingICloudProgress = false
|
||||
@State private var showingLegacyAccountsAlert = false
|
||||
@State private var showingLegacyAccountsImport = false
|
||||
@State private var showingOpenLinkSheet = false
|
||||
|
||||
// Ambiguous-link prompts are stored on NavigationCoordinator so they can
|
||||
// be dual-hosted by the root app view (here) and the expanded player sheet.
|
||||
@@ -119,10 +117,6 @@ struct YatteeApp: App {
|
||||
Text(String(localized: "alert.openVideo.message \(url.host ?? "clipboard")"))
|
||||
}
|
||||
}
|
||||
.sheet(item: $prefilledLinkURL) { url in
|
||||
OpenLinkSheet(prefilledURL: url)
|
||||
.appEnvironment(appEnvironment)
|
||||
}
|
||||
#if !os(tvOS)
|
||||
.sheet(isPresented: $showingDeepLinkDownloadSheet) {
|
||||
if let video = deepLinkVideo {
|
||||
@@ -202,15 +196,9 @@ struct YatteeApp: App {
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
#if !os(tvOS)
|
||||
.sheet(isPresented: $showingOpenLinkSheet) {
|
||||
OpenLinkSheet()
|
||||
.appEnvironment(appEnvironment)
|
||||
}
|
||||
#endif
|
||||
.onReceive(NotificationCenter.default.publisher(for: .showOpenLinkSheet)) { _ in
|
||||
appEnvironment.navigationCoordinator.isPlayerExpanded = false
|
||||
showingOpenLinkSheet = true
|
||||
appEnvironment.navigationCoordinator.presenter.present(.openLink(nil))
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .openDescriptionLink)) { notification in
|
||||
if let url = notification.object as? URL {
|
||||
@@ -645,7 +633,8 @@ struct YatteeApp: App {
|
||||
|
||||
/// Show OpenLinkSheet with a pre-filled URL.
|
||||
private func showOpenLinkSheetWithURL(_ url: URL) {
|
||||
prefilledLinkURL = url
|
||||
// The top-most sheet host (the player when expanded) presents it
|
||||
appEnvironment.navigationCoordinator.presenter.present(.openLink(url))
|
||||
}
|
||||
|
||||
/// Play a video from a deep link.
|
||||
|
||||
Reference in New Issue
Block a user