mirror of
https://github.com/yattee/yattee.git
synced 2026-05-14 03:15:03 +00:00
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.
47 lines
1.7 KiB
Swift
47 lines
1.7 KiB
Swift
//
|
|
// TopShelfSnapshot.swift
|
|
// Yattee
|
|
//
|
|
// Shared data contract between the main app (writer) and YatteeTopShelf extension (reader).
|
|
// Items are persisted as JSON in the App Group UserDefaults suite under per-section keys.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
/// A compact representation of a video suitable for a tvOS Top Shelf row.
|
|
/// Kept deliberately small — the extension has a tight memory/work budget.
|
|
struct TopShelfItem: Codable, Hashable, Sendable {
|
|
let videoID: String
|
|
let title: String
|
|
let authorName: String
|
|
let duration: TimeInterval
|
|
let thumbnailURL: String?
|
|
/// Pre-built `yattee://video/...` URL the extension uses for `displayURL`.
|
|
let deepLinkURL: String
|
|
/// Seconds watched — only set for continue-watching items.
|
|
let progressSeconds: TimeInterval?
|
|
}
|
|
|
|
/// Max items retained per section snapshot. The extension only needs a handful.
|
|
enum TopShelfSnapshot {
|
|
static let maxItems = 10
|
|
|
|
static func read(section: TopShelfSection, from defaults: UserDefaults = AppGroup.defaults) -> [TopShelfItem] {
|
|
guard let data = defaults.data(forKey: section.snapshotKey),
|
|
let items = try? JSONDecoder().decode([TopShelfItem].self, from: data) else {
|
|
return []
|
|
}
|
|
return items
|
|
}
|
|
|
|
static func write(_ items: [TopShelfItem], section: TopShelfSection, to defaults: UserDefaults = AppGroup.defaults) {
|
|
let capped = Array(items.prefix(maxItems))
|
|
if capped.isEmpty {
|
|
defaults.removeObject(forKey: section.snapshotKey)
|
|
return
|
|
}
|
|
guard let data = try? JSONEncoder().encode(capped) else { return }
|
|
defaults.set(data, forKey: section.snapshotKey)
|
|
}
|
|
}
|