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
+206
View File
@@ -8,6 +8,8 @@
/* Begin PBXBuildFile section */
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 */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
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 */; };
A7596F780E93AF0BE3838A26 /* ConnectivityProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3CCA7E80F3A99D8759F2E59F /* ConnectivityProvider.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 */; };
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 */; };
E79A4474D308631AFA59CAE7 /* DeviceInfoPlusPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E1F41676FEF1AB57076F8B9 /* DeviceInfoPlusPlugin.swift */; };
/* 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 */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
@@ -39,11 +53,25 @@
name = "Embed Frameworks";
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 */
/* 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>"; };
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>"; };
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>"; };
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>"; };
@@ -56,6 +84,8 @@
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>"; };
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>"; };
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; };
@@ -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>"; };
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>"; };
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>"; };
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>"; };
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>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
43E2EE359822E384AD7E8A2E /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
1C6B234E223CDFFE5BF3FC0B /* TVServices.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -114,6 +154,17 @@
path = connectivity_plus;
sourceTree = "<group>";
};
47C2D3F50A23DE33DE92F592 /* TopShelfExtension */ = {
isa = PBXGroup;
children = (
F2F829B3F190657106F66379 /* TopShelfProvider.swift */,
BBCB49C8AE9E90DEF97A87CA /* Info.plist */,
9165AF55B967D8845D042FE7 /* TopShelfExtension.entitlements */,
);
name = TopShelfExtension;
path = TopShelfExtension;
sourceTree = "<group>";
};
53CDD29681161166962B9FA5 /* Pods */ = {
isa = PBXGroup;
children = (
@@ -128,6 +179,7 @@
isa = PBXGroup;
children = (
824D2C68D1F1206932E118C2 /* Pods_Runner.framework */,
25B7925EFD8A2C1C7EB667D5 /* TVServices.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -160,6 +212,7 @@
97C146EF1CF9000F007C117D /* Products */,
53CDD29681161166962B9FA5 /* Pods */,
5833EC5B503BBB4E370BA1B7 /* Frameworks */,
47C2D3F50A23DE33DE92F592 /* TopShelfExtension */,
);
sourceTree = "<group>";
};
@@ -167,6 +220,7 @@
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
035F0D5A5E54BE7AD9AFA23C /* TopShelfExtension.appex */,
);
name = Products;
sourceTree = "<group>";
@@ -182,6 +236,8 @@
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
BF0B7DFE378943E9D7865D18 /* MpvPlayer */,
D58B788B11920D95BC7D750C /* Plugins */,
0760C00EDC55A4D718BCB406 /* SystemShelfPlugin.swift */,
937C0D45D6114EF1E957F5F6 /* Runner.entitlements */,
);
path = Runner;
sourceTree = "<group>";
@@ -224,6 +280,23 @@
/* End PBXGroup 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 */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
@@ -234,12 +307,14 @@
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
AE0235548DB8213618795857 /* Embed App Extensions */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
7C33FC680D67B3859BF9AE6C /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
dependencies = (
F0399FC35D3A6ED67E74810D /* PBXTargetDependency */,
);
name = Runner;
packageProductDependencies = (
@@ -281,11 +356,19 @@
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
63C63C245D7F38BFF091CEC3 /* TopShelfExtension */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
64CEA7DE6B9394CE88783302 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -388,11 +471,29 @@
81F08E404EBEB19B00FF148D /* ConnectivityPlusPlugin.swift in Sources */,
A7596F780E93AF0BE3838A26 /* ConnectivityProvider.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;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
F0399FC35D3A6ED67E74810D /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
name = TopShelfExtension;
target = 63C63C245D7F38BFF091CEC3 /* TopShelfExtension */;
targetProxy = 3E4E59C5E29F44BA64BEB559 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
@@ -470,6 +571,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@@ -500,6 +602,98 @@
};
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 */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -614,6 +808,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@@ -651,6 +846,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
@@ -704,6 +900,16 @@
defaultConfigurationIsVisible = 0;
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 */
/* Begin XCRemoteSwiftPackageReference section */
+18
View File
@@ -128,6 +128,10 @@ import wakelock_plus
application.beginReceivingRemoteControlEvents()
if let url = launchOptions?[UIApplication.LaunchOptionsKey.url] as? URL {
_ = SystemShelfPlugin.handleOpenURL(url)
}
if let r = self.registrar(forPlugin: "SharedPreferencesPlugin") {
SharedPreferencesPlugin.register(with: r)
}
@@ -155,7 +159,21 @@ import wakelock_plus
if let r = self.registrar(forPlugin: "WakelockPlusPlugin") {
WakelockPlusPlugin.register(with: r)
}
if let r = self.registrar(forPlugin: "SystemShelfPlugin") {
SystemShelfPlugin.register(with: r)
}
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>
<key>CFBundleVersion</key>
<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>
<array>
<dict>
@@ -39,6 +50,11 @@
<true/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<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
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:-}"
if [[ -z "$app_path" ]]; then
if [[ -n "${PROJECT_DIR:-}" ]]; then
@@ -98,6 +104,13 @@ SyncRunnerVersion() {
echo " └─Syncing Runner version $FLUTTER_BUILD_NAME ($FLUTTER_BUILD_NUMBER)"
SetPlistString "$plist" CFBundleShortVersionString "$FLUTTER_BUILD_NAME"
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() {