mirror of
https://github.com/yattee/yattee.git
synced 2026-07-06 05:42:02 +00:00
Add tvOS Top Shelf extension
Surfaces Continue Watching, Recent Feed, and Recent Bookmarks in the
Apple TV Home top shelf when Yattee is focused. Tapping a tile opens
the video via the existing yattee://video/{id} deep link.
- New YatteeTopShelf app extension target (tvOS only). LD_ENTRY_POINT is
overridden to _NSExtensionMain; the tv-app-extension product type
defaults to _TVExtensionMain which is for the pre-tvOS-13 legacy API
and crashes modern TVTopShelfContentProvider subclasses at launch.
- Main app writes per-section JSON snapshots (capped at 10 items each)
to a shared App Group UserDefaults suite after bookmark, watch-history,
and feed-cache changes, plus an initial write on launch.
- Enabled-sections list is mirrored to the same App Group so the
extension can respect the user's selection without touching SwiftData.
- Settings → Top Shelf (tvOS only) lets the user toggle sections.
- Deep link playback shows a loading toast while video details are
fetched, and an error toast if no source is configured.
This commit is contained in:
@@ -76,6 +76,13 @@ struct YatteeApp: App {
|
||||
}
|
||||
.onAppear {
|
||||
registerBackgroundTasksIfNeeded()
|
||||
#if os(tvOS)
|
||||
TopShelfSnapshotWriter.startObserving(dataManager: appEnvironment.dataManager)
|
||||
TopShelfSnapshotWriter.writeAll(
|
||||
dataManager: appEnvironment.dataManager,
|
||||
settingsManager: appEnvironment.settingsManager
|
||||
)
|
||||
#endif
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .continueUserActivity)) { notification in
|
||||
if let activity = notification.object as? NSUserActivity {
|
||||
@@ -373,15 +380,51 @@ struct YatteeApp: App {
|
||||
|
||||
/// Play a video from a deep link.
|
||||
private func playVideoFromDeepLink(videoID: VideoID) async {
|
||||
guard let instance = appEnvironment.instancesManager.instance(for: videoID.source) else { return }
|
||||
LoggingService.shared.info(
|
||||
"Deep link play: videoID=\(videoID.videoID) source=\(videoID.source)",
|
||||
category: .general
|
||||
)
|
||||
guard let instance = appEnvironment.instancesManager.instance(for: videoID.source) else {
|
||||
LoggingService.shared.warning(
|
||||
"Deep link play: no instance configured for source \(videoID.source) — aborting",
|
||||
category: .general
|
||||
)
|
||||
appEnvironment.toastManager.showError(
|
||||
String(localized: "deepLink.noInstance.title", defaultValue: "Can't open video"),
|
||||
subtitle: String(
|
||||
localized: "deepLink.noInstance.subtitle",
|
||||
defaultValue: "No source is configured for this video."
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
LoggingService.shared.info(
|
||||
"Deep link play: using instance \(instance.url.absoluteString)",
|
||||
category: .general
|
||||
)
|
||||
|
||||
let toastID = appEnvironment.toastManager.show(
|
||||
scopes: [.main, .player],
|
||||
category: .loading,
|
||||
title: String(localized: "deepLink.loading.title", defaultValue: "Loading video…"),
|
||||
subtitle: instance.name,
|
||||
autoDismissDelay: 30.0
|
||||
)
|
||||
|
||||
do {
|
||||
let video = try await appEnvironment.contentService.video(
|
||||
id: videoID.videoID,
|
||||
instance: instance
|
||||
)
|
||||
LoggingService.shared.info("Deep link play: fetched video, opening player", category: .general)
|
||||
appEnvironment.toastManager.dismiss(id: toastID)
|
||||
appEnvironment.playerService.openVideo(video)
|
||||
} catch {
|
||||
LoggingService.shared.error(
|
||||
"Deep link play: video fetch failed (\(error.localizedDescription)), falling back to info view",
|
||||
category: .general
|
||||
)
|
||||
appEnvironment.toastManager.dismiss(id: toastID)
|
||||
// If video fetch fails, fall back to navigating to the video info view
|
||||
appEnvironment.navigationCoordinator.navigate(to: .video(.id(videoID)))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user