From 5390f99b8296aaec851f48de363bdaaacaeec6e0 Mon Sep 17 00:00:00 2001 From: Arkadiusz Fal Date: Fri, 16 Oct 2026 23:55:14 +0200 Subject: [PATCH] Give tvOS playlist not-found and empty states a focusable button The not-found state had nothing focusable, and an empty remote playlist this account does not own showed no left-column buttons either, so the remote was stuck. Not found offers Retry (remote) or Open Playlists (local); the empty remote playlist offers Refresh. --- Yattee/Localizable.xcstrings | 11 +++++ .../Playlist/UnifiedPlaylistDetailView.swift | 40 ++++++++++++++++--- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/Yattee/Localizable.xcstrings b/Yattee/Localizable.xcstrings index 4e803ee4..3959c34b 100644 --- a/Yattee/Localizable.xcstrings +++ b/Yattee/Localizable.xcstrings @@ -9391,6 +9391,17 @@ } } }, + "playlist.notFound.openPlaylists" : { + "comment" : "tvOS button on the playlist-not-found screen that opens the Playlists list", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open Playlists" + } + } + } + }, "playlist.play" : { "comment" : "Play button in playlist toolbar menu", "localizations" : { diff --git a/Yattee/Views/Playlist/UnifiedPlaylistDetailView.swift b/Yattee/Views/Playlist/UnifiedPlaylistDetailView.swift index dd0fd7a2..9b25f4cb 100644 --- a/Yattee/Views/Playlist/UnifiedPlaylistDetailView.swift +++ b/Yattee/Views/Playlist/UnifiedPlaylistDetailView.swift @@ -252,12 +252,7 @@ struct UnifiedPlaylistDetailView: View { } else if let error = errorMessage { errorView(error) } else { - ContentUnavailableView( - String(localized: "playlist.notFound"), - systemImage: "list.bullet.rectangle", - description: Text(String(localized: "playlist.notFound.description")) - ) - .frame(maxWidth: .infinity, maxHeight: .infinity) + notFoundView } } #if !os(tvOS) @@ -819,11 +814,44 @@ struct UnifiedPlaylistDetailView: View { Label(String(localized: "playlist.empty"), systemImage: "music.note.list") } description: { Text(String(localized: "playlist.empty.description")) + } actions: { + #if os(tvOS) + // The left column has no buttons for an empty remote playlist this + // account does not own; without one the remote has nowhere to go. + if case .remote = source { + Button(String(localized: "common.refresh")) { + Task { await loadPlaylist() } + } + } + #endif } .frame(maxWidth: .infinity, maxHeight: .infinity) .padding(.top, 40) } + private var notFoundView: some View { + ContentUnavailableView { + Label(String(localized: "playlist.notFound"), systemImage: "list.bullet.rectangle") + } description: { + Text(String(localized: "playlist.notFound.description")) + } actions: { + #if os(tvOS) + // Nothing else on this screen is focusable, and as a sidebar + // playlist it is a tab root that Menu cannot pop. + if case .remote = source { + Button(String(localized: "common.retry")) { + Task { await loadPlaylist() } + } + } else { + Button(String(localized: "playlist.notFound.openPlaylists")) { + appEnvironment?.navigationCoordinator.router.select(.playlistsList) + } + } + #endif + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + private var videoList: some View { #if os(tvOS) let indexColumnWidth: CGFloat = 60