fix(tvos): top shelf sync
This commit is contained in:
@@ -148,6 +148,8 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
|||||||
LibrariesProvider? _librariesProvider;
|
LibrariesProvider? _librariesProvider;
|
||||||
Set<String> _lastSeenHiddenKeys = {};
|
Set<String> _lastSeenHiddenKeys = {};
|
||||||
List<String> _lastSeenLibraryOrderKeys = const [];
|
List<String> _lastSeenLibraryOrderKeys = const [];
|
||||||
|
Future<void>? _systemShelfSyncFuture;
|
||||||
|
List<MediaItem>? _pendingSystemShelfItems;
|
||||||
|
|
||||||
// WatchStateAware: watch on-deck items and their parent shows/seasons
|
// WatchStateAware: watch on-deck items and their parent shows/seasons
|
||||||
@override
|
@override
|
||||||
@@ -548,6 +550,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
|||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
_autoScrollTimer?.cancel();
|
_autoScrollTimer?.cancel();
|
||||||
_indicatorTimer?.cancel();
|
_indicatorTimer?.cancel();
|
||||||
|
_pendingSystemShelfItems = null;
|
||||||
_indicatorProgress.dispose();
|
_indicatorProgress.dispose();
|
||||||
_heroController.dispose();
|
_heroController.dispose();
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
@@ -902,14 +905,36 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
|||||||
|
|
||||||
/// Sync Continue Watching items to the platform launcher shelf.
|
/// Sync Continue Watching items to the platform launcher shelf.
|
||||||
Future<void> _syncSystemShelf(List<MediaItem> onDeck) async {
|
Future<void> _syncSystemShelf(List<MediaItem> onDeck) async {
|
||||||
|
_pendingSystemShelfItems = List<MediaItem>.unmodifiable(onDeck);
|
||||||
|
if (_systemShelfSyncFuture != null) {
|
||||||
|
await _systemShelfSyncFuture;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final syncFuture = _drainSystemShelfSyncQueue();
|
||||||
|
_systemShelfSyncFuture = syncFuture;
|
||||||
|
await syncFuture;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _drainSystemShelfSyncQueue() async {
|
||||||
try {
|
try {
|
||||||
await SystemShelfService().syncFromContinueWatching(
|
while (_pendingSystemShelfItems != null) {
|
||||||
onDeck,
|
final onDeck = _pendingSystemShelfItems!;
|
||||||
(serverId) => context.getMediaClientWithFallback(serverId),
|
_pendingSystemShelfItems = null;
|
||||||
hideSpoilers: context.settingsRead(SettingsService.hideSpoilers),
|
if (!mounted) return;
|
||||||
);
|
|
||||||
} catch (e) {
|
try {
|
||||||
appLogger.w('Failed to sync system shelf', error: e);
|
await SystemShelfService().syncFromContinueWatching(
|
||||||
|
onDeck,
|
||||||
|
(serverId) => context.getMediaClientWithFallback(serverId),
|
||||||
|
hideSpoilers: context.settingsRead(SettingsService.hideSpoilers),
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
appLogger.w('Failed to sync system shelf', error: e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_systemShelfSyncFuture = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import 'settings_service.dart' show EpisodePosterMode;
|
|||||||
class SystemShelfService {
|
class SystemShelfService {
|
||||||
static const MethodChannel _androidChannel = MethodChannel('com.plezy/watch_next');
|
static const MethodChannel _androidChannel = MethodChannel('com.plezy/watch_next');
|
||||||
static const MethodChannel _tvosChannel = MethodChannel('com.plezy/system_shelf');
|
static const MethodChannel _tvosChannel = MethodChannel('com.plezy/system_shelf');
|
||||||
|
static const bool _tvosBuild = bool.fromEnvironment('TVOS_BUILD');
|
||||||
|
|
||||||
static final SystemShelfService _instance = SystemShelfService._internal();
|
static final SystemShelfService _instance = SystemShelfService._internal();
|
||||||
factory SystemShelfService() => _instance;
|
factory SystemShelfService() => _instance;
|
||||||
@@ -32,7 +33,7 @@ class SystemShelfService {
|
|||||||
|
|
||||||
MethodChannel? get _channel {
|
MethodChannel? get _channel {
|
||||||
if (Platform.isAndroid) return _androidChannel;
|
if (Platform.isAndroid) return _androidChannel;
|
||||||
if (Platform.isIOS && PlatformDetector.isAppleTV()) return _tvosChannel;
|
if (Platform.isIOS && (_tvosBuild || PlatformDetector.isAppleTV())) return _tvosChannel;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,6 +53,9 @@ class SystemShelfService {
|
|||||||
if (channel == null) return null;
|
if (channel == null) return null;
|
||||||
try {
|
try {
|
||||||
return await channel.invokeMethod<String>('getInitialDeepLink');
|
return await channel.invokeMethod<String>('getInitialDeepLink');
|
||||||
|
} on MissingPluginException catch (e) {
|
||||||
|
appLogger.w('System shelf initial deep link failed: native channel missing', error: e);
|
||||||
|
return null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
appLogger.w('Failed to get system shelf initial deep link', error: e);
|
appLogger.w('Failed to get system shelf initial deep link', error: e);
|
||||||
return null;
|
return null;
|
||||||
@@ -64,7 +68,14 @@ class SystemShelfService {
|
|||||||
if (channel == null) return false;
|
if (channel == null) return false;
|
||||||
try {
|
try {
|
||||||
return await channel.invokeMethod<bool>('isSupported') ?? false;
|
return await channel.invokeMethod<bool>('isSupported') ?? false;
|
||||||
} catch (_) {
|
} on MissingPluginException catch (e) {
|
||||||
|
appLogger.w('System shelf unsupported: native channel missing', error: e);
|
||||||
|
return false;
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
appLogger.w('System shelf unsupported: native platform error', error: e);
|
||||||
|
return false;
|
||||||
|
} catch (e) {
|
||||||
|
appLogger.w('System shelf unsupported: native support check failed', error: e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,14 +90,20 @@ class SystemShelfService {
|
|||||||
if (channel == null) return false;
|
if (channel == null) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final supported = await isSupported();
|
|
||||||
if (!supported) return false;
|
|
||||||
|
|
||||||
final items = continueWatchingItems.map((item) {
|
final items = continueWatchingItems.map((item) {
|
||||||
return _convertToShelfItem(item, getClientForServerId, hideSpoilers: hideSpoilers);
|
return _convertToShelfItem(item, getClientForServerId, hideSpoilers: hideSpoilers);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
|
final supported = await isSupported();
|
||||||
|
if (!supported) return false;
|
||||||
|
|
||||||
return await channel.invokeMethod<bool>('sync', {'items': items}) ?? false;
|
return await channel.invokeMethod<bool>('sync', {'items': items}) ?? false;
|
||||||
|
} on MissingPluginException catch (e) {
|
||||||
|
appLogger.e('Failed to sync system shelf: native channel missing', error: e);
|
||||||
|
return false;
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
appLogger.e('Failed to sync system shelf: native platform error', error: e);
|
||||||
|
return false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
appLogger.e('Failed to sync system shelf', error: e);
|
appLogger.e('Failed to sync system shelf', error: e);
|
||||||
return false;
|
return false;
|
||||||
@@ -99,6 +116,12 @@ class SystemShelfService {
|
|||||||
if (channel == null) return false;
|
if (channel == null) return false;
|
||||||
try {
|
try {
|
||||||
return await channel.invokeMethod<bool>('clear') ?? false;
|
return await channel.invokeMethod<bool>('clear') ?? false;
|
||||||
|
} on MissingPluginException catch (e) {
|
||||||
|
appLogger.e('Failed to clear system shelf: native channel missing', error: e);
|
||||||
|
return false;
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
appLogger.e('Failed to clear system shelf: native platform error', error: e);
|
||||||
|
return false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
appLogger.e('Failed to clear system shelf', error: e);
|
appLogger.e('Failed to clear system shelf', error: e);
|
||||||
return false;
|
return false;
|
||||||
@@ -112,6 +135,12 @@ class SystemShelfService {
|
|||||||
try {
|
try {
|
||||||
final contentId = _buildContentId(serverId, ratingKey);
|
final contentId = _buildContentId(serverId, ratingKey);
|
||||||
return await channel.invokeMethod<bool>('remove', {'contentId': contentId}) ?? false;
|
return await channel.invokeMethod<bool>('remove', {'contentId': contentId}) ?? false;
|
||||||
|
} on MissingPluginException catch (e) {
|
||||||
|
appLogger.e('Failed to remove system shelf item: native channel missing', error: e);
|
||||||
|
return false;
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
appLogger.e('Failed to remove system shelf item: native platform error', error: e);
|
||||||
|
return false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
appLogger.e('Failed to remove system shelf item', error: e);
|
appLogger.e('Failed to remove system shelf item', error: e);
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import TVServices
|
|||||||
final class SystemShelfPlugin: NSObject, FlutterPlugin {
|
final class SystemShelfPlugin: NSObject, FlutterPlugin {
|
||||||
private static let channelName = "com.plezy/system_shelf"
|
private static let channelName = "com.plezy/system_shelf"
|
||||||
private static let appGroupIdentifier = "group.com.edde746.plezy"
|
private static let appGroupIdentifier = "group.com.edde746.plezy"
|
||||||
private static let cacheFileName = "PlezySystemShelfCache.json"
|
private static let cacheDataKey = "PlezySystemShelfCacheData"
|
||||||
private static var pendingDeepLink: String?
|
private static var pendingDeepLink: String?
|
||||||
private static var methodChannel: FlutterMethodChannel?
|
private static var methodChannel: FlutterMethodChannel?
|
||||||
|
|
||||||
@@ -36,11 +36,7 @@ import TVServices
|
|||||||
func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
switch call.method {
|
switch call.method {
|
||||||
case "isSupported":
|
case "isSupported":
|
||||||
let supported = Self.cacheURL != nil
|
result(Self.sharedDefaults != nil)
|
||||||
if !supported {
|
|
||||||
Self.log("App Group container unavailable")
|
|
||||||
}
|
|
||||||
result(supported)
|
|
||||||
case "sync":
|
case "sync":
|
||||||
guard let args = call.arguments as? [String: Any], let rawItems = args["items"] as? [[String: Any]] else {
|
guard let args = call.arguments as? [String: Any], let rawItems = args["items"] as? [[String: Any]] else {
|
||||||
result(FlutterError(code: "INVALID_ARGS", message: "Missing items", details: nil))
|
result(FlutterError(code: "INVALID_ARGS", message: "Missing items", details: nil))
|
||||||
@@ -64,16 +60,8 @@ import TVServices
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static var appGroupContainerURL: URL? {
|
private static var sharedDefaults: UserDefaults? {
|
||||||
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)
|
UserDefaults(suiteName: appGroupIdentifier)
|
||||||
}
|
|
||||||
|
|
||||||
private static var cacheURL: URL? {
|
|
||||||
appGroupContainerURL?.appendingPathComponent(cacheFileName, isDirectory: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func log(_ message: String) {
|
|
||||||
NSLog("PlezySystemShelf: %@", message)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func normalizedItem(_ item: [String: Any]) -> [String: Any] {
|
private static func normalizedItem(_ item: [String: Any]) -> [String: Any] {
|
||||||
@@ -99,61 +87,84 @@ import TVServices
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static func writePayload(_ payload: [String: Any]) -> Bool {
|
private static func writePayload(_ payload: [String: Any]) -> Bool {
|
||||||
guard let url = cacheURL else {
|
guard let defaults = sharedDefaults else {
|
||||||
log("Cannot write cache because App Group container is unavailable")
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
guard JSONSerialization.isValidJSONObject(payload),
|
let sanitizedPayload = sanitizedJSONObject(payload)
|
||||||
let data = try? JSONSerialization.data(withJSONObject: payload)
|
guard JSONSerialization.isValidJSONObject(sanitizedPayload) else {
|
||||||
else {
|
|
||||||
log("Cannot write cache because payload is not valid JSON")
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try data.write(to: url, options: [.atomic])
|
let data = try JSONSerialization.data(withJSONObject: sanitizedPayload)
|
||||||
|
defaults.set(data, forKey: cacheDataKey)
|
||||||
|
defaults.synchronize()
|
||||||
} catch {
|
} catch {
|
||||||
log("Failed to write cache: \(error)")
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
let itemCount =
|
|
||||||
(payload["sections"] as? [[String: Any]])?.reduce(0) { count, section in
|
|
||||||
count + ((section["items"] as? [[String: Any]])?.count ?? 0)
|
|
||||||
} ?? 0
|
|
||||||
log("Wrote cache with \(itemCount) items")
|
|
||||||
TVTopShelfContentProvider.topShelfContentDidChange()
|
TVTopShelfContentProvider.topShelfContentDidChange()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func clearCache() -> Bool {
|
private static func sanitizedJSONObject(_ object: [String: Any]) -> [String: Any] {
|
||||||
guard let url = cacheURL else {
|
object.reduce(into: [String: Any]()) { result, entry in
|
||||||
log("Cannot clear cache because App Group container is unavailable")
|
if let value = sanitizedJSONValue(entry.value) {
|
||||||
return false
|
result[entry.key] = value
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
if FileManager.default.fileExists(atPath: url.path) {
|
|
||||||
try FileManager.default.removeItem(at: url)
|
|
||||||
}
|
}
|
||||||
} catch {
|
}
|
||||||
log("Failed to clear cache: \(error)")
|
}
|
||||||
|
|
||||||
|
private static func sanitizedJSONValue(_ value: Any) -> Any? {
|
||||||
|
if value is NSNull { return nil }
|
||||||
|
|
||||||
|
if let value = value as? String { return value }
|
||||||
|
if let value = value as? NSNumber {
|
||||||
|
if CFGetTypeID(value) == CFBooleanGetTypeID() { return value.boolValue }
|
||||||
|
return value.doubleValue.isFinite ? value : nil
|
||||||
|
}
|
||||||
|
if let value = value as? Bool { return value }
|
||||||
|
if let value = value as? Int { return value }
|
||||||
|
if let value = value as? Int64 { return value }
|
||||||
|
if let value = value as? Double { return value.isFinite ? value : nil }
|
||||||
|
if let value = value as? Float { return value.isFinite ? Double(value) : nil }
|
||||||
|
|
||||||
|
if let value = value as? [String: Any] {
|
||||||
|
return value.reduce(into: [String: Any]()) { result, entry in
|
||||||
|
if let nestedValue = sanitizedJSONValue(entry.value) {
|
||||||
|
result[entry.key] = nestedValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let value = value as? [Any] {
|
||||||
|
return value.compactMap { nestedValue in
|
||||||
|
sanitizedJSONValue(nestedValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func clearCache() -> Bool {
|
||||||
|
guard let defaults = sharedDefaults else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
log("Cleared cache")
|
defaults.removeObject(forKey: cacheDataKey)
|
||||||
|
defaults.synchronize()
|
||||||
|
|
||||||
TVTopShelfContentProvider.topShelfContentDidChange()
|
TVTopShelfContentProvider.topShelfContentDidChange()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func removeItem(contentId: String) -> Bool {
|
private static func removeItem(contentId: String) -> Bool {
|
||||||
guard let url = cacheURL,
|
guard let defaults = sharedDefaults,
|
||||||
let data = try? Data(contentsOf: url),
|
let data = defaults.data(forKey: cacheDataKey),
|
||||||
var payload = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
var payload = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||||
let sections = payload["sections"] as? [[String: Any]]
|
let sections = payload["sections"] as? [[String: Any]]
|
||||||
else {
|
else {
|
||||||
log("Cannot remove cache item because cache is unavailable or invalid")
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +179,9 @@ import TVServices
|
|||||||
return nextSection
|
return nextSection
|
||||||
}
|
}
|
||||||
|
|
||||||
if !removed { return false }
|
if !removed {
|
||||||
|
return false
|
||||||
|
}
|
||||||
payload["updatedAt"] = Date().timeIntervalSince1970
|
payload["updatedAt"] = Date().timeIntervalSince1970
|
||||||
payload["sections"] = filteredSections
|
payload["sections"] = filteredSections
|
||||||
return writePayload(payload)
|
return writePayload(payload)
|
||||||
|
|||||||
@@ -3,16 +3,10 @@ import TVServices
|
|||||||
|
|
||||||
private enum TopShelfShared {
|
private enum TopShelfShared {
|
||||||
static let appGroupIdentifier = "group.com.edde746.plezy"
|
static let appGroupIdentifier = "group.com.edde746.plezy"
|
||||||
static let cacheFileName = "PlezySystemShelfCache.json"
|
static let cacheDataKey = "PlezySystemShelfCacheData"
|
||||||
|
|
||||||
static var cacheURL: URL? {
|
static var sharedDefaults: UserDefaults? {
|
||||||
FileManager.default
|
UserDefaults(suiteName: appGroupIdentifier)
|
||||||
.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)?
|
|
||||||
.appendingPathComponent(cacheFileName, isDirectory: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
static func log(_ message: String) {
|
|
||||||
NSLog("PlezyTopShelf: %@", message)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,33 +28,72 @@ private struct TopShelfCachePayload: Decodable {
|
|||||||
let lastPlaybackPosition: Double?
|
let lastPlaybackPosition: Double?
|
||||||
let seasonNumber: Int?
|
let seasonNumber: Int?
|
||||||
let episodeNumber: Int?
|
let episodeNumber: Int?
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case contentId
|
||||||
|
case title
|
||||||
|
case episodeTitle
|
||||||
|
case description
|
||||||
|
case posterUri
|
||||||
|
case type
|
||||||
|
case duration
|
||||||
|
case lastPlaybackPosition
|
||||||
|
case seasonNumber
|
||||||
|
case episodeNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
contentId = try container.decode(String.self, forKey: .contentId)
|
||||||
|
title = try container.decode(String.self, forKey: .title)
|
||||||
|
episodeTitle = try container.decodeIfPresent(String.self, forKey: .episodeTitle)
|
||||||
|
description = try container.decodeIfPresent(String.self, forKey: .description)
|
||||||
|
posterUri = try container.decodeIfPresent(String.self, forKey: .posterUri)
|
||||||
|
type = try container.decodeIfPresent(String.self, forKey: .type)
|
||||||
|
duration = container.decodeFlexibleDoubleIfPresent(.duration)
|
||||||
|
lastPlaybackPosition = container.decodeFlexibleDoubleIfPresent(.lastPlaybackPosition)
|
||||||
|
seasonNumber = container.decodeFlexibleIntIfPresent(.seasonNumber)
|
||||||
|
episodeNumber = container.decodeFlexibleIntIfPresent(.episodeNumber)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let sections: [Section]
|
let sections: [Section]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private extension KeyedDecodingContainer {
|
||||||
|
func decodeFlexibleDoubleIfPresent(_ key: Key) -> Double? {
|
||||||
|
if let value = try? decodeIfPresent(Double.self, forKey: key) { return value }
|
||||||
|
if let value = try? decodeIfPresent(Int.self, forKey: key) { return Double(value) }
|
||||||
|
if let value = try? decodeIfPresent(String.self, forKey: key) { return Double(value) }
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeFlexibleIntIfPresent(_ key: Key) -> Int? {
|
||||||
|
if let value = try? decodeIfPresent(Int.self, forKey: key) { return value }
|
||||||
|
if let value = try? decodeIfPresent(Double.self, forKey: key) { return Int(value) }
|
||||||
|
if let value = try? decodeIfPresent(String.self, forKey: key) { return Int(value) }
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final class TopShelfProvider: TVTopShelfContentProvider {
|
final class TopShelfProvider: TVTopShelfContentProvider {
|
||||||
override func loadTopShelfContent() async -> (any TVTopShelfContent)? {
|
override func loadTopShelfContent() async -> (any TVTopShelfContent)? {
|
||||||
buildContent()
|
return buildContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func buildContent() -> TVTopShelfContent? {
|
private func buildContent() -> TVTopShelfContent? {
|
||||||
guard let url = TopShelfShared.cacheURL else {
|
guard let defaults = TopShelfShared.sharedDefaults else {
|
||||||
TopShelfShared.log("App Group container unavailable")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
guard FileManager.default.fileExists(atPath: url.path) else {
|
guard let data = defaults.data(forKey: TopShelfShared.cacheDataKey) else {
|
||||||
TopShelfShared.log("Cache file missing")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let payload: TopShelfCachePayload
|
let payload: TopShelfCachePayload
|
||||||
do {
|
do {
|
||||||
let data = try Data(contentsOf: url)
|
|
||||||
payload = try JSONDecoder().decode(TopShelfCachePayload.self, from: data)
|
payload = try JSONDecoder().decode(TopShelfCachePayload.self, from: data)
|
||||||
} catch {
|
} catch {
|
||||||
TopShelfShared.log("Failed to read cache: \(error)")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,12 +107,9 @@ final class TopShelfProvider: TVTopShelfContentProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
guard !sections.isEmpty else {
|
guard !sections.isEmpty else {
|
||||||
TopShelfShared.log("Cache has no displayable items")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let itemCount = sections.reduce(0) { $0 + $1.items.count }
|
|
||||||
TopShelfShared.log("Loaded \(itemCount) items")
|
|
||||||
return TVTopShelfSectionedContent(sections: sections)
|
return TVTopShelfSectionedContent(sections: sections)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user