feat(tvos): add top shelf support

This commit is contained in:
edde746
2026-06-04 15:38:12 +02:00
parent 145881318e
commit 43012da2b8
13 changed files with 857 additions and 69 deletions
+7 -13
View File
@@ -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);
} }
} }
+18 -18
View File
@@ -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;
+206
View File
@@ -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 */
+18
View File
@@ -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)
}
} }
+16
View File
@@ -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>
+10
View File
@@ -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>
+177
View File
@@ -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
+40
View File
@@ -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
}
}
+145
View File
@@ -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'
+13
View File
@@ -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() {