feat(tvos): add top shelf support
This commit is contained in:
@@ -61,7 +61,7 @@ import '../utils/video_player_navigation.dart';
|
|||||||
import '../utils/layout_constants.dart';
|
import '../utils/layout_constants.dart';
|
||||||
import '../utils/platform_detector.dart';
|
import '../utils/platform_detector.dart';
|
||||||
import '../theme/mono_tokens.dart';
|
import '../theme/mono_tokens.dart';
|
||||||
import '../services/watch_next_service.dart';
|
import '../services/system_shelf_service.dart';
|
||||||
import 'auth_screen.dart';
|
import 'auth_screen.dart';
|
||||||
import 'libraries/content_state_builder.dart';
|
import 'libraries/content_state_builder.dart';
|
||||||
import 'main_screen.dart';
|
import 'main_screen.dart';
|
||||||
@@ -769,10 +769,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
|||||||
_heroFocusNode.requestFocus();
|
_heroFocusNode.requestFocus();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync to Android TV Watch Next row
|
unawaited(_syncSystemShelf(onDeck));
|
||||||
if (Platform.isAndroid) {
|
|
||||||
unawaited(_syncWatchNext(onDeck));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sync PageController to first page after OnDeck loads
|
// Sync PageController to first page after OnDeck loads
|
||||||
if (_heroController.hasClients && onDeck.isNotEmpty) {
|
if (_heroController.hasClients && onDeck.isNotEmpty) {
|
||||||
@@ -875,10 +872,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sync to Android TV Watch Next row
|
unawaited(_syncSystemShelf(onDeck));
|
||||||
if (Platform.isAndroid) {
|
|
||||||
unawaited(_syncWatchNext(onDeck));
|
|
||||||
}
|
|
||||||
|
|
||||||
appLogger.d('Continue Watching refreshed successfully');
|
appLogger.d('Continue Watching refreshed successfully');
|
||||||
}
|
}
|
||||||
@@ -901,16 +895,16 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sync On Deck items to Android TV Watch Next row.
|
/// Sync Continue Watching items to the platform launcher shelf.
|
||||||
Future<void> _syncWatchNext(List<MediaItem> onDeck) async {
|
Future<void> _syncSystemShelf(List<MediaItem> onDeck) async {
|
||||||
try {
|
try {
|
||||||
await WatchNextService().syncFromOnDeck(
|
await SystemShelfService().syncFromContinueWatching(
|
||||||
onDeck,
|
onDeck,
|
||||||
(serverId) => context.getMediaClientWithFallback(serverId),
|
(serverId) => context.getMediaClientWithFallback(serverId),
|
||||||
hideSpoilers: context.settingsRead(SettingsService.hideSpoilers),
|
hideSpoilers: context.settingsRead(SettingsService.hideSpoilers),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
appLogger.w('Failed to sync Watch Next', error: e);
|
appLogger.w('Failed to sync system shelf', error: e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ import 'search_screen.dart';
|
|||||||
import 'downloads/downloads_screen.dart';
|
import 'downloads/downloads_screen.dart';
|
||||||
import 'settings/settings_screen.dart';
|
import 'settings/settings_screen.dart';
|
||||||
import 'profile/profile_switch_screen.dart';
|
import 'profile/profile_switch_screen.dart';
|
||||||
import '../services/watch_next_service.dart';
|
import '../services/system_shelf_service.dart';
|
||||||
import '../watch_together/watch_together.dart';
|
import '../watch_together/watch_together.dart';
|
||||||
|
|
||||||
/// Provides access to the main screen's focus control.
|
/// Provides access to the main screen's focus control.
|
||||||
@@ -334,7 +334,7 @@ class _MainScreenState extends State<MainScreen>
|
|||||||
// Set up Watch Together callbacks immediately (must be synchronous to catch early messages)
|
// Set up Watch Together callbacks immediately (must be synchronous to catch early messages)
|
||||||
if (!_isOffline) {
|
if (!_isOffline) {
|
||||||
_setupWatchTogetherCallback();
|
_setupWatchTogetherCallback();
|
||||||
_setupWatchNextDeepLink();
|
_setupSystemShelfDeepLink();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wire profile binder + tracker bootstrap (skip in offline mode)
|
// Wire profile binder + tracker bootstrap (skip in offline mode)
|
||||||
@@ -646,35 +646,35 @@ class _MainScreenState extends State<MainScreen>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set up Watch Next deep link handling for Android TV launcher taps
|
/// Set up launcher shelf deep link handling for Android TV and tvOS taps.
|
||||||
void _setupWatchNextDeepLink() {
|
void _setupSystemShelfDeepLink() {
|
||||||
if (!Platform.isAndroid) return;
|
if (!Platform.isAndroid && !PlatformDetector.isAppleTV()) return;
|
||||||
|
|
||||||
final watchNext = WatchNextService();
|
final systemShelf = SystemShelfService();
|
||||||
|
|
||||||
// Listen for deep links when app is already running (warm start)
|
// Listen for deep links when app is already running (warm start)
|
||||||
watchNext.onWatchNextTap = (contentId) {
|
systemShelf.onShelfItemTap = (contentId) {
|
||||||
appLogger.d('Watch Next tap: $contentId');
|
appLogger.d('System shelf tap: $contentId');
|
||||||
_handleWatchNextContentId(contentId);
|
_handleShelfContentId(contentId);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check for pending deep link from cold start
|
// Check for pending deep link from cold start
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||||
final contentId = await watchNext.getInitialDeepLink();
|
final contentId = await systemShelf.getInitialDeepLink();
|
||||||
if (contentId != null && mounted) {
|
if (contentId != null && mounted) {
|
||||||
appLogger.d('Watch Next initial deep link: $contentId');
|
appLogger.d('System shelf initial deep link: $contentId');
|
||||||
unawaited(_handleWatchNextContentId(contentId));
|
unawaited(_handleShelfContentId(contentId));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle a Watch Next content ID by fetching metadata and starting playback
|
/// Handle a launcher shelf content ID by fetching metadata and starting playback.
|
||||||
Future<void> _handleWatchNextContentId(String contentId) async {
|
Future<void> _handleShelfContentId(String contentId) async {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
final parsed = WatchNextService.parseContentId(contentId);
|
final parsed = SystemShelfService.parseContentId(contentId);
|
||||||
if (parsed == null) {
|
if (parsed == null) {
|
||||||
appLogger.w('Watch Next: invalid content ID: $contentId');
|
appLogger.w('System shelf: invalid content ID: $contentId');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -685,7 +685,7 @@ class _MainScreenState extends State<MainScreen>
|
|||||||
final client = multiServer.getClientForServer(serverId);
|
final client = multiServer.getClientForServer(serverId);
|
||||||
|
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
appLogger.w('Watch Next: server $serverId not available');
|
appLogger.w('System shelf: server $serverId not available');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -695,7 +695,7 @@ class _MainScreenState extends State<MainScreen>
|
|||||||
|
|
||||||
unawaited(navigateToVideoPlayer(context, metadata: metadata));
|
unawaited(navigateToVideoPlayer(context, metadata: metadata));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
appLogger.e('Watch Next: failed to navigate to media', error: e);
|
appLogger.e('System shelf: failed to navigate to media', error: e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,101 +1,119 @@
|
|||||||
import 'dart:io' show Platform;
|
import 'dart:io' show Platform;
|
||||||
import '../media/ids.dart';
|
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import '../media/ids.dart';
|
||||||
import '../media/media_item.dart';
|
import '../media/media_item.dart';
|
||||||
import '../media/media_item_types.dart';
|
import '../media/media_item_types.dart';
|
||||||
import '../media/media_kind.dart';
|
import '../media/media_kind.dart';
|
||||||
import '../media/media_server_client.dart';
|
import '../media/media_server_client.dart';
|
||||||
import '../utils/app_logger.dart';
|
import '../utils/app_logger.dart';
|
||||||
|
import '../utils/platform_detector.dart';
|
||||||
import 'settings_service.dart' show EpisodePosterMode;
|
import 'settings_service.dart' show EpisodePosterMode;
|
||||||
|
|
||||||
/// Service for syncing On Deck / Continue Watching content to Android TV's Watch Next row.
|
/// Syncs Continue Watching content to platform launcher surfaces.
|
||||||
class WatchNextService {
|
///
|
||||||
static const MethodChannel _channel = MethodChannel('com.plezy/watch_next');
|
/// Android uses the Watch Next row. tvOS uses the app's Top Shelf extension.
|
||||||
|
class SystemShelfService {
|
||||||
|
static const MethodChannel _androidChannel = MethodChannel('com.plezy/watch_next');
|
||||||
|
static const MethodChannel _tvosChannel = MethodChannel('com.plezy/system_shelf');
|
||||||
|
|
||||||
static final WatchNextService _instance = WatchNextService._internal();
|
static final SystemShelfService _instance = SystemShelfService._internal();
|
||||||
factory WatchNextService() => _instance;
|
factory SystemShelfService() => _instance;
|
||||||
|
|
||||||
WatchNextService._internal() {
|
SystemShelfService._internal() {
|
||||||
_channel.setMethodCallHandler(_handleMethodCall);
|
_androidChannel.setMethodCallHandler(_handleMethodCall);
|
||||||
|
_tvosChannel.setMethodCallHandler(_handleMethodCall);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Callback for when a Watch Next item is tapped (warm start deep link).
|
/// Callback for warm-start launcher surface taps.
|
||||||
ValueChanged<String>? onWatchNextTap;
|
ValueChanged<String>? onShelfItemTap;
|
||||||
|
|
||||||
|
MethodChannel? get _channel {
|
||||||
|
if (Platform.isAndroid) return _androidChannel;
|
||||||
|
if (Platform.isIOS && PlatformDetector.isAppleTV()) return _tvosChannel;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
Future<dynamic> _handleMethodCall(MethodCall call) async {
|
Future<dynamic> _handleMethodCall(MethodCall call) async {
|
||||||
if (call.method == 'onWatchNextTap') {
|
if (call.method == 'onWatchNextTap' || call.method == 'onShelfItemTap') {
|
||||||
final contentId = call.arguments['contentId'] as String?;
|
final args = call.arguments;
|
||||||
|
final contentId = args is Map ? args['contentId'] as String? : null;
|
||||||
if (contentId != null) {
|
if (contentId != null) {
|
||||||
onWatchNextTap?.call(contentId);
|
onShelfItemTap?.call(contentId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a pending deep link from cold start (consumed on first call).
|
/// Get a pending deep link from cold start (consumed on first call).
|
||||||
Future<String?> getInitialDeepLink() async {
|
Future<String?> getInitialDeepLink() async {
|
||||||
if (!Platform.isAndroid) return null;
|
final channel = _channel;
|
||||||
|
if (channel == null) return null;
|
||||||
try {
|
try {
|
||||||
return await _channel.invokeMethod<String>('getInitialDeepLink');
|
return await channel.invokeMethod<String>('getInitialDeepLink');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
appLogger.w('Failed to get initial deep link', error: e);
|
appLogger.w('Failed to get system shelf initial deep link', error: e);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if Watch Next is supported (Android TV only).
|
/// Check whether the current platform has a launcher shelf integration.
|
||||||
Future<bool> isSupported() async {
|
Future<bool> isSupported() async {
|
||||||
if (!Platform.isAndroid) return false;
|
final channel = _channel;
|
||||||
|
if (channel == null) return false;
|
||||||
try {
|
try {
|
||||||
return await _channel.invokeMethod<bool>('isSupported') ?? false;
|
return await channel.invokeMethod<bool>('isSupported') ?? false;
|
||||||
} catch (e) {
|
} catch (_) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sync On Deck items to Watch Next row.
|
/// Sync Continue Watching items to the current platform's launcher shelf.
|
||||||
Future<bool> syncFromOnDeck(
|
Future<bool> syncFromContinueWatching(
|
||||||
List<MediaItem> onDeckItems,
|
List<MediaItem> continueWatchingItems,
|
||||||
MediaServerClient Function(ServerId serverId) getClientForServerId, {
|
MediaServerClient Function(ServerId serverId) getClientForServerId, {
|
||||||
bool hideSpoilers = false,
|
bool hideSpoilers = false,
|
||||||
}) async {
|
}) async {
|
||||||
if (!Platform.isAndroid) return false;
|
final channel = _channel;
|
||||||
|
if (channel == null) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final supported = await isSupported();
|
final supported = await isSupported();
|
||||||
if (!supported) return false;
|
if (!supported) return false;
|
||||||
|
|
||||||
final items = onDeckItems.map((item) {
|
final items = continueWatchingItems.map((item) {
|
||||||
return _convertToWatchNextItem(item, getClientForServerId, hideSpoilers: hideSpoilers);
|
return _convertToShelfItem(item, getClientForServerId, hideSpoilers: hideSpoilers);
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
return await _channel.invokeMethod<bool>('sync', {'items': items}) ?? false;
|
return await channel.invokeMethod<bool>('sync', {'items': items}) ?? false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
appLogger.e('Failed to sync Watch Next', error: e);
|
appLogger.e('Failed to sync system shelf', error: e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear all Watch Next entries.
|
/// Clear all launcher shelf entries owned by the app.
|
||||||
Future<bool> clear() async {
|
Future<bool> clear() async {
|
||||||
if (!Platform.isAndroid) return false;
|
final channel = _channel;
|
||||||
|
if (channel == null) return false;
|
||||||
try {
|
try {
|
||||||
return await _channel.invokeMethod<bool>('clear') ?? false;
|
return await channel.invokeMethod<bool>('clear') ?? false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
appLogger.e('Failed to clear Watch Next', error: e);
|
appLogger.e('Failed to clear system shelf', error: e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove a single item from Watch Next.
|
/// Remove a single launcher shelf item.
|
||||||
Future<bool> removeItem(ServerId serverId, String ratingKey) async {
|
Future<bool> removeItem(ServerId serverId, String ratingKey) async {
|
||||||
if (!Platform.isAndroid) return false;
|
final channel = _channel;
|
||||||
|
if (channel == null) return false;
|
||||||
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;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
appLogger.e('Failed to remove Watch Next item', error: e);
|
appLogger.e('Failed to remove system shelf item', error: e);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -113,7 +131,7 @@ class WatchNextService {
|
|||||||
return (ServerId(parts.first), parts.sublist(1).join('_'));
|
return (ServerId(parts.first), parts.sublist(1).join('_'));
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> _convertToWatchNextItem(
|
Map<String, dynamic> _convertToShelfItem(
|
||||||
MediaItem item,
|
MediaItem item,
|
||||||
MediaServerClient Function(ServerId serverId) getClientForServerId, {
|
MediaServerClient Function(ServerId serverId) getClientForServerId, {
|
||||||
bool hideSpoilers = false,
|
bool hideSpoilers = false,
|
||||||
@@ -134,7 +152,7 @@ class WatchNextService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
appLogger.w('Failed to get poster URL for Watch Next: ${item.title}', error: e);
|
appLogger.w('Failed to get shelf poster URL for ${item.title}', error: e);
|
||||||
}
|
}
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
@@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
055E465F9095D0B6E9B91D46 /* PathProviderPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D41AA251EF365516E2AC5287 /* PathProviderPlugin.swift */; };
|
055E465F9095D0B6E9B91D46 /* PathProviderPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D41AA251EF365516E2AC5287 /* PathProviderPlugin.swift */; };
|
||||||
|
1C6B234E223CDFFE5BF3FC0B /* TVServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 25B7925EFD8A2C1C7EB667D5 /* TVServices.framework */; };
|
||||||
|
28DB4404B17342F46BC2B0A1 /* TopShelfProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F2F829B3F190657106F66379 /* TopShelfProvider.swift */; };
|
||||||
35DB0C8FEF635A3BCA0B722A /* PackageInfoPlusPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9426EFA282CDA8E0E98EEE9 /* PackageInfoPlusPlugin.swift */; };
|
35DB0C8FEF635A3BCA0B722A /* PackageInfoPlusPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9426EFA282CDA8E0E98EEE9 /* PackageInfoPlusPlugin.swift */; };
|
||||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||||
5C71F5F7B33075F2B007825B /* MpvPlayerPluginShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73645904F226A24585A092CE /* MpvPlayerPluginShared.swift */; };
|
5C71F5F7B33075F2B007825B /* MpvPlayerPluginShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73645904F226A24585A092CE /* MpvPlayerPluginShared.swift */; };
|
||||||
@@ -22,12 +24,24 @@
|
|||||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||||
A7596F780E93AF0BE3838A26 /* ConnectivityProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCA7E80F3A99D8759F2E59F /* ConnectivityProvider.swift */; };
|
A7596F780E93AF0BE3838A26 /* ConnectivityProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCA7E80F3A99D8759F2E59F /* ConnectivityProvider.swift */; };
|
||||||
AA34E3E5872B3792D6959EAA /* MpvPipController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2635E12EB9322B151EE5127 /* MpvPipController.swift */; };
|
AA34E3E5872B3792D6959EAA /* MpvPipController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2635E12EB9322B151EE5127 /* MpvPipController.swift */; };
|
||||||
|
BDD87DA7F5C435F9DBB8F9BE /* TopShelfExtension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 035F0D5A5E54BE7AD9AFA23C /* TopShelfExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
C6A15611158B29B0FF43A960 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 824D2C68D1F1206932E118C2 /* Pods_Runner.framework */; };
|
C6A15611158B29B0FF43A960 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 824D2C68D1F1206932E118C2 /* Pods_Runner.framework */; };
|
||||||
CD1C0534948840272E58248E /* PathMonitorConnectivityProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AEF79DFBD1D80F00AB39CDA /* PathMonitorConnectivityProvider.swift */; };
|
CD1C0534948840272E58248E /* PathMonitorConnectivityProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6AEF79DFBD1D80F00AB39CDA /* PathMonitorConnectivityProvider.swift */; };
|
||||||
|
D2A548D9DE1A0F319B30B74C /* SystemShelfPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0760C00EDC55A4D718BCB406 /* SystemShelfPlugin.swift */; };
|
||||||
E3DEAD2AAFC347A2E55AC0F7 /* SharedPreferencesPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0455EBA0EF4A61D3B71D2D7 /* SharedPreferencesPlugin.swift */; };
|
E3DEAD2AAFC347A2E55AC0F7 /* SharedPreferencesPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0455EBA0EF4A61D3B71D2D7 /* SharedPreferencesPlugin.swift */; };
|
||||||
E79A4474D308631AFA59CAE7 /* DeviceInfoPlusPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E1F41676FEF1AB57076F8B9 /* DeviceInfoPlusPlugin.swift */; };
|
E79A4474D308631AFA59CAE7 /* DeviceInfoPlusPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E1F41676FEF1AB57076F8B9 /* DeviceInfoPlusPlugin.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
3E4E59C5E29F44BA64BEB559 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 63C63C245D7F38BFF091CEC3;
|
||||||
|
remoteInfo = TopShelfExtension;
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
/* Begin PBXCopyFilesBuildPhase section */
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||||
isa = PBXCopyFilesBuildPhase;
|
isa = PBXCopyFilesBuildPhase;
|
||||||
@@ -39,11 +53,25 @@
|
|||||||
name = "Embed Frameworks";
|
name = "Embed Frameworks";
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
AE0235548DB8213618795857 /* Embed App Extensions */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "";
|
||||||
|
dstSubfolderSpec = 13;
|
||||||
|
files = (
|
||||||
|
BDD87DA7F5C435F9DBB8F9BE /* TopShelfExtension.appex in Embed App Extensions */,
|
||||||
|
);
|
||||||
|
name = "Embed App Extensions";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
035F0D5A5E54BE7AD9AFA23C /* TopShelfExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; name = TopShelfExtension.appex; path = TopShelfExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
04DD35536DEE7C27FFA53862 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
04DD35536DEE7C27FFA53862 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
0760C00EDC55A4D718BCB406 /* SystemShelfPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SystemShelfPlugin.swift; sourceTree = "<group>"; };
|
||||||
0C25F4F2367A30B47E945AD6 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
0C25F4F2367A30B47E945AD6 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||||
|
25B7925EFD8A2C1C7EB667D5 /* TVServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = TVServices.framework; path = System/Library/Frameworks/TVServices.framework; sourceTree = SDKROOT; };
|
||||||
34CD411CCD84E381C4BF4C1B /* messages.g.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = messages.g.swift; sourceTree = "<group>"; };
|
34CD411CCD84E381C4BF4C1B /* messages.g.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = messages.g.swift; sourceTree = "<group>"; };
|
||||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||||
3CCA7E80F3A99D8759F2E59F /* ConnectivityProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConnectivityProvider.swift; sourceTree = "<group>"; };
|
3CCA7E80F3A99D8759F2E59F /* ConnectivityProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConnectivityProvider.swift; sourceTree = "<group>"; };
|
||||||
@@ -56,6 +84,8 @@
|
|||||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
824D2C68D1F1206932E118C2 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
824D2C68D1F1206932E118C2 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
9165AF55B967D8845D042FE7 /* TopShelfExtension.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = TopShelfExtension.entitlements; sourceTree = "<group>"; };
|
||||||
|
937C0D45D6114EF1E957F5F6 /* Runner.entitlements */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
||||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||||
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
@@ -66,13 +96,23 @@
|
|||||||
9D7A998830EDC8F77BF521D1 /* MpvPlayerPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MpvPlayerPlugin.swift; path = ../ios/Runner/MpvPlayer/MpvPlayerPlugin.swift; sourceTree = "<source_root>"; };
|
9D7A998830EDC8F77BF521D1 /* MpvPlayerPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MpvPlayerPlugin.swift; path = ../ios/Runner/MpvPlayer/MpvPlayerPlugin.swift; sourceTree = "<source_root>"; };
|
||||||
A12B8610AE5D580077264851 /* MpvPlayerCore.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MpvPlayerCore.swift; path = ../ios/Runner/MpvPlayer/MpvPlayerCore.swift; sourceTree = "<source_root>"; };
|
A12B8610AE5D580077264851 /* MpvPlayerCore.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MpvPlayerCore.swift; path = ../ios/Runner/MpvPlayer/MpvPlayerCore.swift; sourceTree = "<source_root>"; };
|
||||||
A2635E12EB9322B151EE5127 /* MpvPipController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MpvPipController.swift; path = ../ios/Runner/MpvPlayer/MpvPipController.swift; sourceTree = "<source_root>"; };
|
A2635E12EB9322B151EE5127 /* MpvPipController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MpvPipController.swift; path = ../ios/Runner/MpvPlayer/MpvPipController.swift; sourceTree = "<source_root>"; };
|
||||||
|
BBCB49C8AE9E90DEF97A87CA /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
C0455EBA0EF4A61D3B71D2D7 /* SharedPreferencesPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SharedPreferencesPlugin.swift; sourceTree = "<group>"; };
|
C0455EBA0EF4A61D3B71D2D7 /* SharedPreferencesPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SharedPreferencesPlugin.swift; sourceTree = "<group>"; };
|
||||||
D41AA251EF365516E2AC5287 /* PathProviderPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PathProviderPlugin.swift; sourceTree = "<group>"; };
|
D41AA251EF365516E2AC5287 /* PathProviderPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PathProviderPlugin.swift; sourceTree = "<group>"; };
|
||||||
D52A3BDA46E79969EA1DF3AC /* MpvPlayerCoreBase.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MpvPlayerCoreBase.swift; path = ../shared/apple/MpvPlayer/MpvPlayerCoreBase.swift; sourceTree = "<source_root>"; };
|
D52A3BDA46E79969EA1DF3AC /* MpvPlayerCoreBase.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MpvPlayerCoreBase.swift; path = ../shared/apple/MpvPlayer/MpvPlayerCoreBase.swift; sourceTree = "<source_root>"; };
|
||||||
|
F2F829B3F190657106F66379 /* TopShelfProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TopShelfProvider.swift; sourceTree = "<group>"; };
|
||||||
F9426EFA282CDA8E0E98EEE9 /* PackageInfoPlusPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PackageInfoPlusPlugin.swift; sourceTree = "<group>"; };
|
F9426EFA282CDA8E0E98EEE9 /* PackageInfoPlusPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PackageInfoPlusPlugin.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
43E2EE359822E384AD7E8A2E /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
1C6B234E223CDFFE5BF3FC0B /* TVServices.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -114,6 +154,17 @@
|
|||||||
path = connectivity_plus;
|
path = connectivity_plus;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
47C2D3F50A23DE33DE92F592 /* TopShelfExtension */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
F2F829B3F190657106F66379 /* TopShelfProvider.swift */,
|
||||||
|
BBCB49C8AE9E90DEF97A87CA /* Info.plist */,
|
||||||
|
9165AF55B967D8845D042FE7 /* TopShelfExtension.entitlements */,
|
||||||
|
);
|
||||||
|
name = TopShelfExtension;
|
||||||
|
path = TopShelfExtension;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
53CDD29681161166962B9FA5 /* Pods */ = {
|
53CDD29681161166962B9FA5 /* Pods */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -128,6 +179,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
824D2C68D1F1206932E118C2 /* Pods_Runner.framework */,
|
824D2C68D1F1206932E118C2 /* Pods_Runner.framework */,
|
||||||
|
25B7925EFD8A2C1C7EB667D5 /* TVServices.framework */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -160,6 +212,7 @@
|
|||||||
97C146EF1CF9000F007C117D /* Products */,
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
53CDD29681161166962B9FA5 /* Pods */,
|
53CDD29681161166962B9FA5 /* Pods */,
|
||||||
5833EC5B503BBB4E370BA1B7 /* Frameworks */,
|
5833EC5B503BBB4E370BA1B7 /* Frameworks */,
|
||||||
|
47C2D3F50A23DE33DE92F592 /* TopShelfExtension */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -167,6 +220,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
97C146EE1CF9000F007C117D /* Runner.app */,
|
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||||
|
035F0D5A5E54BE7AD9AFA23C /* TopShelfExtension.appex */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -182,6 +236,8 @@
|
|||||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||||
BF0B7DFE378943E9D7865D18 /* MpvPlayer */,
|
BF0B7DFE378943E9D7865D18 /* MpvPlayer */,
|
||||||
D58B788B11920D95BC7D750C /* Plugins */,
|
D58B788B11920D95BC7D750C /* Plugins */,
|
||||||
|
0760C00EDC55A4D718BCB406 /* SystemShelfPlugin.swift */,
|
||||||
|
937C0D45D6114EF1E957F5F6 /* Runner.entitlements */,
|
||||||
);
|
);
|
||||||
path = Runner;
|
path = Runner;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -224,6 +280,23 @@
|
|||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
|
63C63C245D7F38BFF091CEC3 /* TopShelfExtension */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = C995F7A5B711B6DB7E97B000 /* Build configuration list for PBXNativeTarget "TopShelfExtension" */;
|
||||||
|
buildPhases = (
|
||||||
|
B1C48839AB6C24EF099DDFA3 /* Sources */,
|
||||||
|
43E2EE359822E384AD7E8A2E /* Frameworks */,
|
||||||
|
64CEA7DE6B9394CE88783302 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = TopShelfExtension;
|
||||||
|
productName = TopShelfExtension;
|
||||||
|
productReference = 035F0D5A5E54BE7AD9AFA23C /* TopShelfExtension.appex */;
|
||||||
|
productType = "com.apple.product-type.app-extension";
|
||||||
|
};
|
||||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
@@ -234,12 +307,14 @@
|
|||||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
97C146EC1CF9000F007C117D /* Resources */,
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
|
AE0235548DB8213618795857 /* Embed App Extensions */,
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
7C33FC680D67B3859BF9AE6C /* [CP] Embed Pods Frameworks */,
|
7C33FC680D67B3859BF9AE6C /* [CP] Embed Pods Frameworks */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
|
F0399FC35D3A6ED67E74810D /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
name = Runner;
|
name = Runner;
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
@@ -281,11 +356,19 @@
|
|||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
97C146ED1CF9000F007C117D /* Runner */,
|
97C146ED1CF9000F007C117D /* Runner */,
|
||||||
|
63C63C245D7F38BFF091CEC3 /* TopShelfExtension */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
64CEA7DE6B9394CE88783302 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
97C146EC1CF9000F007C117D /* Resources */ = {
|
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -388,11 +471,29 @@
|
|||||||
81F08E404EBEB19B00FF148D /* ConnectivityPlusPlugin.swift in Sources */,
|
81F08E404EBEB19B00FF148D /* ConnectivityPlusPlugin.swift in Sources */,
|
||||||
A7596F780E93AF0BE3838A26 /* ConnectivityProvider.swift in Sources */,
|
A7596F780E93AF0BE3838A26 /* ConnectivityProvider.swift in Sources */,
|
||||||
CD1C0534948840272E58248E /* PathMonitorConnectivityProvider.swift in Sources */,
|
CD1C0534948840272E58248E /* PathMonitorConnectivityProvider.swift in Sources */,
|
||||||
|
D2A548D9DE1A0F319B30B74C /* SystemShelfPlugin.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
B1C48839AB6C24EF099DDFA3 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
28DB4404B17342F46BC2B0A1 /* TopShelfProvider.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
F0399FC35D3A6ED67E74810D /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
name = TopShelfExtension;
|
||||||
|
target = 63C63C245D7F38BFF091CEC3 /* TopShelfExtension */;
|
||||||
|
targetProxy = 3E4E59C5E29F44BA64BEB559 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin PBXVariantGroup section */
|
/* Begin PBXVariantGroup section */
|
||||||
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||||
isa = PBXVariantGroup;
|
isa = PBXVariantGroup;
|
||||||
@@ -470,6 +571,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
@@ -500,6 +602,98 @@
|
|||||||
};
|
};
|
||||||
name = Profile;
|
name = Profile;
|
||||||
};
|
};
|
||||||
|
37D721B4A1E3E0EDC16D1456 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 9740EEB31CF90195004384FC /* Generated.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = TopShelfExtension/TopShelfExtension.entitlements;
|
||||||
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = G88U5B5783;
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = TopShelfExtension/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.edde746.plezy.TopShelfExtension;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = appletvos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SUPPORTED_PLATFORMS = "appletvos appletvsimulator";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 3;
|
||||||
|
TVOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
4A02CC70113C79647D6EEB12 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 9740EEB31CF90195004384FC /* Generated.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = TopShelfExtension/TopShelfExtension.entitlements;
|
||||||
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = G88U5B5783;
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = TopShelfExtension/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.edde746.plezy.TopShelfExtension;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = appletvos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SUPPORTED_PLATFORMS = "appletvos appletvsimulator";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 3;
|
||||||
|
TVOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
65F6020FE0DF48006BADE49C /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
baseConfigurationReference = 9740EEB31CF90195004384FC /* Generated.xcconfig */;
|
||||||
|
buildSettings = {
|
||||||
|
APPLICATION_EXTENSION_API_ONLY = YES;
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = TopShelfExtension/TopShelfExtension.entitlements;
|
||||||
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
|
DEVELOPMENT_TEAM = G88U5B5783;
|
||||||
|
ENABLE_BITCODE = NO;
|
||||||
|
INFOPLIST_FILE = TopShelfExtension/Info.plist;
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = "$(FLUTTER_BUILD_NAME)";
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.edde746.plezy.TopShelfExtension;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = appletvos;
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
SUPPORTED_PLATFORMS = "appletvos appletvsimulator";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 3;
|
||||||
|
TVOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
97C147031CF9000F007C117D /* Debug */ = {
|
97C147031CF9000F007C117D /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
@@ -614,6 +808,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
@@ -651,6 +846,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||||
@@ -704,6 +900,16 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
|
C995F7A5B711B6DB7E97B000 /* Build configuration list for PBXNativeTarget "TopShelfExtension" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
65F6020FE0DF48006BADE49C /* Release */,
|
||||||
|
37D721B4A1E3E0EDC16D1456 /* Debug */,
|
||||||
|
4A02CC70113C79647D6EEB12 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
/* Begin XCRemoteSwiftPackageReference section */
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
|||||||
@@ -128,6 +128,10 @@ import wakelock_plus
|
|||||||
|
|
||||||
application.beginReceivingRemoteControlEvents()
|
application.beginReceivingRemoteControlEvents()
|
||||||
|
|
||||||
|
if let url = launchOptions?[UIApplication.LaunchOptionsKey.url] as? URL {
|
||||||
|
_ = SystemShelfPlugin.handleOpenURL(url)
|
||||||
|
}
|
||||||
|
|
||||||
if let r = self.registrar(forPlugin: "SharedPreferencesPlugin") {
|
if let r = self.registrar(forPlugin: "SharedPreferencesPlugin") {
|
||||||
SharedPreferencesPlugin.register(with: r)
|
SharedPreferencesPlugin.register(with: r)
|
||||||
}
|
}
|
||||||
@@ -155,7 +159,21 @@ import wakelock_plus
|
|||||||
if let r = self.registrar(forPlugin: "WakelockPlusPlugin") {
|
if let r = self.registrar(forPlugin: "WakelockPlusPlugin") {
|
||||||
WakelockPlusPlugin.register(with: r)
|
WakelockPlusPlugin.register(with: r)
|
||||||
}
|
}
|
||||||
|
if let r = self.registrar(forPlugin: "SystemShelfPlugin") {
|
||||||
|
SystemShelfPlugin.register(with: r)
|
||||||
|
}
|
||||||
|
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func application(
|
||||||
|
_ application: UIApplication,
|
||||||
|
open url: URL,
|
||||||
|
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
|
||||||
|
) -> Bool {
|
||||||
|
if SystemShelfPlugin.handleOpenURL(url) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return super.application(application, open: url, options: options)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,6 +22,17 @@
|
|||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>com.edde746.plezy.play</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>plezy</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
<key>GCSupportedGameControllers</key>
|
<key>GCSupportedGameControllers</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
<dict>
|
||||||
@@ -39,6 +50,11 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<false/>
|
<false/>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>LaunchScreen</string>
|
<string>LaunchScreen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.com.edde746.plezy</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import Foundation
|
||||||
|
import TVServices
|
||||||
|
|
||||||
|
#if os(tvOS)
|
||||||
|
import Flutter
|
||||||
|
|
||||||
|
final class SystemShelfPlugin: NSObject, FlutterPlugin {
|
||||||
|
private static let channelName = "com.plezy/system_shelf"
|
||||||
|
private static let appGroupIdentifier = "group.com.edde746.plezy"
|
||||||
|
private static let cacheFileName = "PlezySystemShelfCache.json"
|
||||||
|
private static var pendingDeepLink: String?
|
||||||
|
private static var methodChannel: FlutterMethodChannel?
|
||||||
|
|
||||||
|
static func register(with registrar: FlutterPluginRegistrar) {
|
||||||
|
let channel = FlutterMethodChannel(
|
||||||
|
name: channelName,
|
||||||
|
binaryMessenger: registrar.messenger()
|
||||||
|
)
|
||||||
|
methodChannel = channel
|
||||||
|
registrar.addMethodCallDelegate(SystemShelfPlugin(), channel: channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func handleOpenURL(_ url: URL) -> Bool {
|
||||||
|
guard let contentId = contentId(from: url) else { return false }
|
||||||
|
pendingDeepLink = contentId
|
||||||
|
methodChannel?.invokeMethod("onShelfItemTap", arguments: ["contentId": contentId])
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func contentId(from url: URL) -> String? {
|
||||||
|
guard url.scheme == "plezy", url.host == "play" else { return nil }
|
||||||
|
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
|
||||||
|
return components?.queryItems?.first { $0.name == "content_id" }?.value
|
||||||
|
}
|
||||||
|
|
||||||
|
func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||||
|
switch call.method {
|
||||||
|
case "isSupported":
|
||||||
|
let supported = Self.cacheURL != nil
|
||||||
|
if !supported {
|
||||||
|
Self.log("App Group container unavailable")
|
||||||
|
}
|
||||||
|
result(supported)
|
||||||
|
case "sync":
|
||||||
|
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))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result(Self.writeItems(rawItems.map(Self.normalizedItem)))
|
||||||
|
case "clear":
|
||||||
|
result(Self.clearCache())
|
||||||
|
case "remove":
|
||||||
|
guard let args = call.arguments as? [String: Any], let contentId = args["contentId"] as? String else {
|
||||||
|
result(FlutterError(code: "INVALID_ARGS", message: "Missing contentId", details: nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result(Self.removeItem(contentId: contentId))
|
||||||
|
case "getInitialDeepLink":
|
||||||
|
let contentId = Self.pendingDeepLink
|
||||||
|
Self.pendingDeepLink = nil
|
||||||
|
result(contentId)
|
||||||
|
default:
|
||||||
|
result(FlutterMethodNotImplemented)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static var appGroupContainerURL: URL? {
|
||||||
|
FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: 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] {
|
||||||
|
item.reduce(into: [String: Any]()) { result, entry in
|
||||||
|
if entry.value is NSNull { return }
|
||||||
|
result[entry.key] = entry.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func writeItems(_ items: [[String: Any]]) -> Bool {
|
||||||
|
let payload: [String: Any] = [
|
||||||
|
"updatedAt": Date().timeIntervalSince1970,
|
||||||
|
"sections": [
|
||||||
|
[
|
||||||
|
"id": "continue_watching",
|
||||||
|
"title": "Continue Watching",
|
||||||
|
"items": items,
|
||||||
|
]
|
||||||
|
],
|
||||||
|
]
|
||||||
|
|
||||||
|
return writePayload(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func writePayload(_ payload: [String: Any]) -> Bool {
|
||||||
|
guard let url = cacheURL else {
|
||||||
|
log("Cannot write cache because App Group container is unavailable")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
guard JSONSerialization.isValidJSONObject(payload),
|
||||||
|
let data = try? JSONSerialization.data(withJSONObject: payload)
|
||||||
|
else {
|
||||||
|
log("Cannot write cache because payload is not valid JSON")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
try data.write(to: url, options: [.atomic])
|
||||||
|
} catch {
|
||||||
|
log("Failed to write cache: \(error)")
|
||||||
|
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()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func clearCache() -> Bool {
|
||||||
|
guard let url = cacheURL else {
|
||||||
|
log("Cannot clear cache because App Group container is unavailable")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
if FileManager.default.fileExists(atPath: url.path) {
|
||||||
|
try FileManager.default.removeItem(at: url)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
log("Failed to clear cache: \(error)")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
log("Cleared cache")
|
||||||
|
TVTopShelfContentProvider.topShelfContentDidChange()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func removeItem(contentId: String) -> Bool {
|
||||||
|
guard let url = cacheURL,
|
||||||
|
let data = try? Data(contentsOf: url),
|
||||||
|
var payload = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||||
|
let sections = payload["sections"] as? [[String: Any]]
|
||||||
|
else {
|
||||||
|
log("Cannot remove cache item because cache is unavailable or invalid")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var removed = false
|
||||||
|
let filteredSections = sections.map { section -> [String: Any] in
|
||||||
|
var nextSection = section
|
||||||
|
if let items = section["items"] as? [[String: Any]] {
|
||||||
|
let filteredItems = items.filter { $0["contentId"] as? String != contentId }
|
||||||
|
removed = removed || filteredItems.count != items.count
|
||||||
|
nextSection["items"] = filteredItems
|
||||||
|
}
|
||||||
|
return nextSection
|
||||||
|
}
|
||||||
|
|
||||||
|
if !removed { return false }
|
||||||
|
payload["updatedAt"] = Date().timeIntervalSince1970
|
||||||
|
payload["sections"] = filteredSections
|
||||||
|
return writePayload(payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Plezy Top Shelf</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(MARKETING_VERSION)</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
<key>NSExtension</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
<string>com.apple.tv-top-shelf</string>
|
||||||
|
<key>NSExtensionPrincipalClass</key>
|
||||||
|
<string>$(PRODUCT_MODULE_NAME).TopShelfProvider</string>
|
||||||
|
</dict>
|
||||||
|
<key>UIRequiredDeviceCapabilities</key>
|
||||||
|
<array>
|
||||||
|
<string>arm64</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.com.edde746.plezy</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import Foundation
|
||||||
|
import TVServices
|
||||||
|
|
||||||
|
private enum TopShelfShared {
|
||||||
|
static let appGroupIdentifier = "group.com.edde746.plezy"
|
||||||
|
static let cacheFileName = "PlezySystemShelfCache.json"
|
||||||
|
|
||||||
|
static var cacheURL: URL? {
|
||||||
|
FileManager.default
|
||||||
|
.containerURL(forSecurityApplicationGroupIdentifier: appGroupIdentifier)?
|
||||||
|
.appendingPathComponent(cacheFileName, isDirectory: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func log(_ message: String) {
|
||||||
|
NSLog("PlezyTopShelf: %@", message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private struct TopShelfCachePayload: Decodable {
|
||||||
|
struct Section: Decodable {
|
||||||
|
let id: String
|
||||||
|
let title: String
|
||||||
|
let items: [Item]
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Item: Decodable {
|
||||||
|
let contentId: String
|
||||||
|
let title: String
|
||||||
|
let episodeTitle: String?
|
||||||
|
let description: String?
|
||||||
|
let posterUri: String?
|
||||||
|
let type: String?
|
||||||
|
let duration: Double?
|
||||||
|
let lastPlaybackPosition: Double?
|
||||||
|
let seasonNumber: Int?
|
||||||
|
let episodeNumber: Int?
|
||||||
|
}
|
||||||
|
|
||||||
|
let sections: [Section]
|
||||||
|
}
|
||||||
|
|
||||||
|
final class TopShelfProvider: TVTopShelfContentProvider {
|
||||||
|
override func loadTopShelfContent() async -> (any TVTopShelfContent)? {
|
||||||
|
buildContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func buildContent() -> TVTopShelfContent? {
|
||||||
|
guard let url = TopShelfShared.cacheURL else {
|
||||||
|
TopShelfShared.log("App Group container unavailable")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard FileManager.default.fileExists(atPath: url.path) else {
|
||||||
|
TopShelfShared.log("Cache file missing")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let payload: TopShelfCachePayload
|
||||||
|
do {
|
||||||
|
let data = try Data(contentsOf: url)
|
||||||
|
payload = try JSONDecoder().decode(TopShelfCachePayload.self, from: data)
|
||||||
|
} catch {
|
||||||
|
TopShelfShared.log("Failed to read cache: \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let sections = payload.sections.compactMap { section -> TVTopShelfItemCollection<TVTopShelfSectionedItem>? in
|
||||||
|
let items = section.items.compactMap(makeTopShelfItem)
|
||||||
|
guard !items.isEmpty else { return nil }
|
||||||
|
|
||||||
|
let collection = TVTopShelfItemCollection(items: items)
|
||||||
|
collection.title = section.title
|
||||||
|
return collection
|
||||||
|
}
|
||||||
|
|
||||||
|
guard !sections.isEmpty else {
|
||||||
|
TopShelfShared.log("Cache has no displayable items")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let itemCount = sections.reduce(0) { $0 + $1.items.count }
|
||||||
|
TopShelfShared.log("Loaded \(itemCount) items")
|
||||||
|
return TVTopShelfSectionedContent(sections: sections)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeTopShelfItem(_ cacheItem: TopShelfCachePayload.Item) -> TVTopShelfSectionedItem? {
|
||||||
|
guard !cacheItem.contentId.isEmpty else { return nil }
|
||||||
|
|
||||||
|
let item = TVTopShelfSectionedItem(identifier: cacheItem.contentId)
|
||||||
|
item.title = displayTitle(for: cacheItem)
|
||||||
|
item.imageShape = .hdtv
|
||||||
|
|
||||||
|
if let duration = cacheItem.duration, duration > 0,
|
||||||
|
let position = cacheItem.lastPlaybackPosition, position > 0
|
||||||
|
{
|
||||||
|
item.playbackProgress = min(max(position / duration, 0), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let url = deepLinkURL(contentId: cacheItem.contentId) {
|
||||||
|
let action = TVTopShelfAction(url: url)
|
||||||
|
item.displayAction = action
|
||||||
|
item.playAction = action
|
||||||
|
}
|
||||||
|
|
||||||
|
if let posterUri = cacheItem.posterUri, let imageURL = URL(string: posterUri) {
|
||||||
|
item.setImageURL(imageURL, for: .screenScale1x)
|
||||||
|
item.setImageURL(imageURL, for: .screenScale2x)
|
||||||
|
}
|
||||||
|
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
private func displayTitle(for item: TopShelfCachePayload.Item) -> String {
|
||||||
|
guard let episodeTitle = item.episodeTitle, !episodeTitle.isEmpty else {
|
||||||
|
return item.title
|
||||||
|
}
|
||||||
|
|
||||||
|
let episodePrefix: String? = {
|
||||||
|
if let seasonNumber = item.seasonNumber, let episodeNumber = item.episodeNumber {
|
||||||
|
return "S\(seasonNumber) E\(episodeNumber)"
|
||||||
|
}
|
||||||
|
if let episodeNumber = item.episodeNumber {
|
||||||
|
return "E\(episodeNumber)"
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
if let episodePrefix {
|
||||||
|
return "\(item.title) - \(episodePrefix) - \(episodeTitle)"
|
||||||
|
}
|
||||||
|
return "\(item.title) - \(episodeTitle)"
|
||||||
|
}
|
||||||
|
|
||||||
|
private func deepLinkURL(contentId: String) -> URL? {
|
||||||
|
var components = URLComponents()
|
||||||
|
components.scheme = "plezy"
|
||||||
|
components.host = "play"
|
||||||
|
components.queryItems = [URLQueryItem(name: "content_id", value: contentId)]
|
||||||
|
return components.url
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,145 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
# Adds Plezy's tvOS Top Shelf extension target and Runner-side bridge source.
|
||||||
|
|
||||||
|
require 'xcodeproj'
|
||||||
|
|
||||||
|
PROJECT_PATH = File.expand_path('../Runner.xcodeproj', __dir__)
|
||||||
|
project = Xcodeproj::Project.open(PROJECT_PATH)
|
||||||
|
runner = project.targets.find { |t| t.name == 'Runner' }
|
||||||
|
raise 'Runner target not found' unless runner
|
||||||
|
|
||||||
|
main_group = project.main_group
|
||||||
|
runner_group = main_group['Runner']
|
||||||
|
raise 'Runner group not found' unless runner_group
|
||||||
|
products_group = main_group['Products'] || main_group.new_group('Products')
|
||||||
|
frameworks_group = main_group['Frameworks'] || main_group.new_group('Frameworks')
|
||||||
|
generated_config_ref = project.files.find { |file| file.path == 'Flutter/Generated.xcconfig' }
|
||||||
|
raise 'Generated.xcconfig not found' unless generated_config_ref
|
||||||
|
|
||||||
|
def ensure_file(group, path, name: nil, source_tree: '<group>')
|
||||||
|
existing = group.files.find { |f| f.path == path || f.display_name == (name || File.basename(path)) }
|
||||||
|
return existing if existing
|
||||||
|
|
||||||
|
ref = group.new_file(path)
|
||||||
|
ref.name = name if name
|
||||||
|
ref.source_tree = source_tree
|
||||||
|
ref
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_source(target, file_ref)
|
||||||
|
phase = target.source_build_phase
|
||||||
|
return if phase.files_references.include?(file_ref)
|
||||||
|
|
||||||
|
phase.add_file_reference(file_ref, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_copy_file(project, phase, file_ref)
|
||||||
|
existing = phase.files.find { |build_file| build_file.file_ref == file_ref }
|
||||||
|
return existing if existing
|
||||||
|
|
||||||
|
build_file = project.new(Xcodeproj::Project::Object::PBXBuildFile)
|
||||||
|
build_file.file_ref = file_ref
|
||||||
|
build_file.settings = { 'ATTRIBUTES' => ['RemoveHeadersOnCopy'] }
|
||||||
|
phase.files << build_file
|
||||||
|
build_file
|
||||||
|
end
|
||||||
|
|
||||||
|
def ensure_framework(target, file_ref)
|
||||||
|
phase = target.frameworks_build_phase
|
||||||
|
return if phase.files_references.include?(file_ref)
|
||||||
|
|
||||||
|
phase.add_file_reference(file_ref, true)
|
||||||
|
end
|
||||||
|
|
||||||
|
system_shelf_ref = ensure_file(runner_group, 'SystemShelfPlugin.swift')
|
||||||
|
ensure_source(runner, system_shelf_ref)
|
||||||
|
ensure_file(runner_group, 'Runner.entitlements')
|
||||||
|
|
||||||
|
extension_group = main_group['TopShelfExtension'] || main_group.new_group('TopShelfExtension', 'TopShelfExtension')
|
||||||
|
top_shelf_ref = ensure_file(extension_group, 'TopShelfProvider.swift')
|
||||||
|
ensure_file(extension_group, 'Info.plist')
|
||||||
|
ensure_file(extension_group, 'TopShelfExtension.entitlements')
|
||||||
|
|
||||||
|
extension_target = project.targets.find { |t| t.name == 'TopShelfExtension' }
|
||||||
|
unless extension_target
|
||||||
|
extension_target = project.new_target(:app_extension, 'TopShelfExtension', :tvos, '14.0')
|
||||||
|
end
|
||||||
|
extension_target.product_type = 'com.apple.product-type.app-extension'
|
||||||
|
|
||||||
|
ensure_source(extension_target, top_shelf_ref)
|
||||||
|
|
||||||
|
removed_framework_refs = []
|
||||||
|
extension_target.frameworks_build_phase.files.delete_if do |build_file|
|
||||||
|
next false unless build_file.file_ref&.display_name == 'Foundation.framework'
|
||||||
|
|
||||||
|
removed_framework_refs << build_file.file_ref
|
||||||
|
true
|
||||||
|
end
|
||||||
|
removed_framework_refs.compact.uniq.each do |file_ref|
|
||||||
|
still_used = project.targets.any? do |target|
|
||||||
|
target.frameworks_build_phase.files_references.include?(file_ref)
|
||||||
|
end
|
||||||
|
file_ref.remove_from_project unless still_used
|
||||||
|
end
|
||||||
|
|
||||||
|
tv_services_ref = ensure_file(
|
||||||
|
frameworks_group,
|
||||||
|
'System/Library/Frameworks/TVServices.framework',
|
||||||
|
name: 'TVServices.framework',
|
||||||
|
source_tree: 'SDKROOT'
|
||||||
|
)
|
||||||
|
ensure_framework(extension_target, tv_services_ref)
|
||||||
|
|
||||||
|
extension_product = extension_target.product_reference
|
||||||
|
extension_product.name = 'TopShelfExtension.appex'
|
||||||
|
extension_product.path = 'TopShelfExtension.appex'
|
||||||
|
extension_product.explicit_file_type = 'wrapper.app-extension'
|
||||||
|
products_group.children << extension_product unless products_group.children.include?(extension_product)
|
||||||
|
|
||||||
|
runner.add_dependency(extension_target) unless runner.dependencies.any? { |d| d.target == extension_target }
|
||||||
|
|
||||||
|
embed_phase = runner.copy_files_build_phases.find { |phase| phase.name == 'Embed App Extensions' }
|
||||||
|
unless embed_phase
|
||||||
|
embed_phase = project.new(Xcodeproj::Project::Object::PBXCopyFilesBuildPhase)
|
||||||
|
embed_phase.name = 'Embed App Extensions'
|
||||||
|
embed_phase.dst_subfolder_spec = '13'
|
||||||
|
embed_phase.dst_path = ''
|
||||||
|
runner.build_phases.insert(-3, embed_phase)
|
||||||
|
end
|
||||||
|
ensure_copy_file(project, embed_phase, extension_product)
|
||||||
|
|
||||||
|
runner.build_configurations.each do |config|
|
||||||
|
config.build_settings['CODE_SIGN_ENTITLEMENTS'] = 'Runner/Runner.entitlements'
|
||||||
|
end
|
||||||
|
|
||||||
|
extension_target.build_configurations.each do |config|
|
||||||
|
config.base_configuration_reference = generated_config_ref
|
||||||
|
|
||||||
|
settings = config.build_settings
|
||||||
|
settings['APPLICATION_EXTENSION_API_ONLY'] = 'YES'
|
||||||
|
settings['CLANG_ENABLE_MODULES'] = 'YES'
|
||||||
|
settings['CODE_SIGN_ENTITLEMENTS'] = 'TopShelfExtension/TopShelfExtension.entitlements'
|
||||||
|
settings['CODE_SIGN_IDENTITY'] = 'Apple Development'
|
||||||
|
settings['CODE_SIGN_STYLE'] = 'Automatic'
|
||||||
|
settings['CURRENT_PROJECT_VERSION'] = '$(FLUTTER_BUILD_NUMBER)'
|
||||||
|
settings['DEVELOPMENT_TEAM'] = 'G88U5B5783'
|
||||||
|
settings['ENABLE_BITCODE'] = 'NO'
|
||||||
|
settings['INFOPLIST_FILE'] = 'TopShelfExtension/Info.plist'
|
||||||
|
settings['LD_RUNPATH_SEARCH_PATHS'] = [
|
||||||
|
'$(inherited)',
|
||||||
|
'@executable_path/Frameworks',
|
||||||
|
'@executable_path/../../Frameworks',
|
||||||
|
]
|
||||||
|
settings['MARKETING_VERSION'] = '$(FLUTTER_BUILD_NAME)'
|
||||||
|
settings['PRODUCT_BUNDLE_IDENTIFIER'] = 'com.edde746.plezy.TopShelfExtension'
|
||||||
|
settings['PRODUCT_NAME'] = '$(TARGET_NAME)'
|
||||||
|
settings['SDKROOT'] = 'appletvos'
|
||||||
|
settings['SKIP_INSTALL'] = 'YES'
|
||||||
|
settings['SUPPORTED_PLATFORMS'] = 'appletvos appletvsimulator'
|
||||||
|
settings['SWIFT_VERSION'] = '5.0'
|
||||||
|
settings['TARGETED_DEVICE_FAMILY'] = '3'
|
||||||
|
settings['TVOS_DEPLOYMENT_TARGET'] = '14.0'
|
||||||
|
end
|
||||||
|
|
||||||
|
project.save
|
||||||
|
puts 'Saved Top Shelf wiring'
|
||||||
@@ -21,6 +21,12 @@ else
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
ReadPubspecVersion() {
|
ReadPubspecVersion() {
|
||||||
|
if [[ -n "${FLUTTER_BUILD_NAME:-}" && -n "${FLUTTER_BUILD_NUMBER:-}" ]]; then
|
||||||
|
export FLUTTER_BUILD_NAME
|
||||||
|
export FLUTTER_BUILD_NUMBER
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
local app_path="${FLUTTER_APPLICATION_PATH:-}"
|
local app_path="${FLUTTER_APPLICATION_PATH:-}"
|
||||||
if [[ -z "$app_path" ]]; then
|
if [[ -z "$app_path" ]]; then
|
||||||
if [[ -n "${PROJECT_DIR:-}" ]]; then
|
if [[ -n "${PROJECT_DIR:-}" ]]; then
|
||||||
@@ -98,6 +104,13 @@ SyncRunnerVersion() {
|
|||||||
echo " └─Syncing Runner version $FLUTTER_BUILD_NAME ($FLUTTER_BUILD_NUMBER)"
|
echo " └─Syncing Runner version $FLUTTER_BUILD_NAME ($FLUTTER_BUILD_NUMBER)"
|
||||||
SetPlistString "$plist" CFBundleShortVersionString "$FLUTTER_BUILD_NAME"
|
SetPlistString "$plist" CFBundleShortVersionString "$FLUTTER_BUILD_NAME"
|
||||||
SetPlistString "$plist" CFBundleVersion "$FLUTTER_BUILD_NUMBER"
|
SetPlistString "$plist" CFBundleVersion "$FLUTTER_BUILD_NUMBER"
|
||||||
|
|
||||||
|
local top_shelf_plist="$TARGET_BUILD_DIR/$WRAPPER_NAME/PlugIns/TopShelfExtension.appex/Info.plist"
|
||||||
|
if [[ -f "$top_shelf_plist" ]]; then
|
||||||
|
echo " └─Syncing TopShelfExtension version $FLUTTER_BUILD_NAME ($FLUTTER_BUILD_NUMBER)"
|
||||||
|
SetPlistString "$top_shelf_plist" CFBundleShortVersionString "$FLUTTER_BUILD_NAME"
|
||||||
|
SetPlistString "$top_shelf_plist" CFBundleVersion "$FLUTTER_BUILD_NUMBER"
|
||||||
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
EngineOutputExists() {
|
EngineOutputExists() {
|
||||||
|
|||||||
Reference in New Issue
Block a user