mirror of
https://github.com/yattee/yattee.git
synced 2026-05-13 19:05:03 +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:
12
Yattee/Core/AppGroup.swift
Normal file
12
Yattee/Core/AppGroup.swift
Normal file
@@ -0,0 +1,12 @@
|
||||
import Foundation
|
||||
|
||||
enum AppGroup {
|
||||
static let identifier = "group.stream.yattee.app.shared"
|
||||
|
||||
/// UserDefaults key holding an ordered [String] of enabled TopShelfSection raw values.
|
||||
static let enabledSectionsKey = "topShelf.enabledSections"
|
||||
|
||||
static var defaults: UserDefaults {
|
||||
UserDefaults(suiteName: identifier) ?? .standard
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,9 @@ enum SettingsKey: String, CaseIterable {
|
||||
case homeSectionItemsLimit
|
||||
case homeSectionLayout
|
||||
|
||||
// Top Shelf (tvOS)
|
||||
case topShelfSections
|
||||
|
||||
// Tab Bar (compact size class)
|
||||
case tabBarItemOrder
|
||||
case tabBarItemVisibility
|
||||
@@ -126,6 +129,8 @@ enum SettingsKey: String, CaseIterable {
|
||||
// Home layout — different UI paradigms per platform
|
||||
.homeShortcutOrder, .homeShortcutVisibility, .homeShortcutLayout,
|
||||
.homeSectionOrder, .homeSectionVisibility, .homeSectionItemsLimit, .homeSectionLayout,
|
||||
// Top Shelf — tvOS only
|
||||
.topShelfSections,
|
||||
// Tab bar (compact size class) layout
|
||||
.tabBarItemOrder, .tabBarItemVisibility, .tabBarStartupTab,
|
||||
// Sidebar layout/selection
|
||||
|
||||
43
Yattee/Core/Settings/SettingsManager+TopShelf.swift
Normal file
43
Yattee/Core/Settings/SettingsManager+TopShelf.swift
Normal file
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// SettingsManager+TopShelf.swift
|
||||
// Yattee
|
||||
//
|
||||
// tvOS Top Shelf settings.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension SettingsManager {
|
||||
/// Ordered list of sections visible in the tvOS Top Shelf.
|
||||
/// Inclusion means the section is shown; absence hides it.
|
||||
var topShelfSections: [TopShelfSection] {
|
||||
get {
|
||||
if let cached = _topShelfSections { return cached }
|
||||
guard let data = data(for: .topShelfSections),
|
||||
let saved = try? JSONDecoder().decode([TopShelfSection].self, from: data) else {
|
||||
return TopShelfSection.defaultOrder
|
||||
}
|
||||
return saved
|
||||
}
|
||||
set {
|
||||
_topShelfSections = newValue
|
||||
if let data = try? JSONEncoder().encode(newValue) {
|
||||
set(data, for: .topShelfSections)
|
||||
}
|
||||
let tsKey = modifiedAtKey(for: .topShelfSections)
|
||||
let now = Date().timeIntervalSince1970
|
||||
localDefaults.set(now, forKey: tsKey)
|
||||
if iCloudSyncEnabled && syncSettings && !isInitialSyncPending {
|
||||
ubiquitousStore.set(now, forKey: tsKey)
|
||||
}
|
||||
mirrorEnabledSectionsToAppGroup(newValue)
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirrors the enabled-sections list to the App Group UserDefaults suite
|
||||
/// so the tvOS Top Shelf extension can read the user's selection.
|
||||
func mirrorEnabledSectionsToAppGroup(_ sections: [TopShelfSection]) {
|
||||
let rawValues = sections.map(\.rawValue)
|
||||
AppGroup.defaults.set(rawValues, forKey: AppGroup.enabledSectionsKey)
|
||||
}
|
||||
}
|
||||
30
Yattee/Core/Settings/TopShelfSection.swift
Normal file
30
Yattee/Core/Settings/TopShelfSection.swift
Normal file
@@ -0,0 +1,30 @@
|
||||
import Foundation
|
||||
|
||||
/// Sections that can appear in the tvOS Top Shelf.
|
||||
/// Stored ordered in `SettingsKey.topShelfSections` — inclusion = visible.
|
||||
enum TopShelfSection: String, Codable, CaseIterable, Identifiable, Sendable {
|
||||
case continueWatching
|
||||
case recentFeed
|
||||
case recentBookmarks
|
||||
|
||||
var id: String { rawValue }
|
||||
|
||||
var localizedTitle: String {
|
||||
switch self {
|
||||
case .continueWatching: return String(localized: "home.section.continueWatching")
|
||||
case .recentFeed: return String(localized: "home.section.feed")
|
||||
case .recentBookmarks: return String(localized: "home.section.bookmarks")
|
||||
}
|
||||
}
|
||||
|
||||
/// UserDefaults key (under the app-group suite) holding the JSON snapshot for this section.
|
||||
var snapshotKey: String {
|
||||
switch self {
|
||||
case .continueWatching: return "topShelf.continueWatching"
|
||||
case .recentFeed: return "topShelf.recentFeed"
|
||||
case .recentBookmarks: return "topShelf.recentBookmarks"
|
||||
}
|
||||
}
|
||||
|
||||
static let defaultOrder: [TopShelfSection] = [.continueWatching, .recentFeed, .recentBookmarks]
|
||||
}
|
||||
@@ -109,6 +109,9 @@ final class SettingsManager {
|
||||
var _homeSectionItemsLimit: Int?
|
||||
var _homeSectionLayout: HomeSectionLayout?
|
||||
|
||||
// Top Shelf (tvOS)
|
||||
var _topShelfSections: [TopShelfSection]?
|
||||
|
||||
// Tab bar settings (compact size class only - iOS)
|
||||
var _tabBarItemOrder: [TabBarItem]?
|
||||
var _tabBarItemVisibility: [TabBarItem: Bool]?
|
||||
@@ -453,6 +456,7 @@ final class SettingsManager {
|
||||
_homeSectionVisibility = nil
|
||||
_homeSectionItemsLimit = nil
|
||||
_homeSectionLayout = nil
|
||||
_topShelfSections = nil
|
||||
_tabBarItemOrder = nil
|
||||
_tabBarItemVisibility = nil
|
||||
_sidebarMainItemOrder = nil
|
||||
|
||||
Reference in New Issue
Block a user