Files
yattee/YatteeTopShelf/TopShelfContentProvider.swift
Arkadiusz Fal 9f86ff0667 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.
2026-04-18 20:38:02 +02:00

39 lines
1.7 KiB
Swift

import TVServices
class TopShelfContentProvider: TVTopShelfContentProvider {
override func loadTopShelfContent(completionHandler: @escaping (any TVTopShelfContent) -> Void) {
let defaults = AppGroup.defaults
let enabled = TopShelfSnapshot.enabledSections(from: defaults)
let collections: [TVTopShelfItemCollection<TVTopShelfSectionedItem>] = enabled.compactMap { section in
let items = TopShelfSnapshot.read(section: section, from: defaults)
guard !items.isEmpty else { return nil }
let sectioned = items.map { makeItem(from: $0, in: section) }
let collection = TVTopShelfItemCollection(items: sectioned)
collection.title = section.localizedTitle
return collection
}
completionHandler(TVTopShelfSectionedContent(sections: collections))
}
private func makeItem(from item: TopShelfItem, in section: TopShelfSection) -> TVTopShelfSectionedItem {
let sectioned = TVTopShelfSectionedItem(identifier: "\(section.rawValue).\(item.videoID)")
sectioned.title = item.title
sectioned.imageShape = .hdtv
if let url = item.thumbnailURL.flatMap(URL.init(string:)) {
sectioned.setImageURL(url, for: .screenScale1x)
sectioned.setImageURL(url, for: .screenScale2x)
}
if let deepLink = URL(string: item.deepLinkURL) {
sectioned.displayAction = TVTopShelfAction(url: deepLink)
sectioned.playAction = TVTopShelfAction(url: deepLink)
}
if section == .continueWatching,
let progress = item.progressSeconds, item.duration > 0 {
sectioned.playbackProgress = max(0, min(1, progress / item.duration))
}
return sectioned
}
}