feat: tvos support

This commit is contained in:
edde746
2026-04-24 14:11:24 +02:00
parent 77401bef32
commit 2479e1faaa
98 changed files with 4464 additions and 56 deletions
+33
View File
@@ -0,0 +1,33 @@
*.mode1v3
*.mode2v3
*.moved-aside
*.pbxuser
*.perspectivev3
**/*sync/
.sconsign.dblite
.tags*
**/.vagrant/
**/DerivedData/
Icon?
**/Pods/
**/.symlinks/
profile
xcuserdata
**/.generated/
Flutter/App.framework
Flutter/Flutter.framework
Flutter/Flutter.podspec
Flutter/Generated.xcconfig
Flutter/app.flx
Flutter/app.zip
Flutter/flutter_assets/
Flutter/flutter_export_environment.sh
Flutter/GeneratedPluginRegistrant.*
ServiceDefinitions.json
Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!default.mode1v3
!default.mode2v3
!default.pbxuser
!default.perspectivev3
+26
View File
@@ -0,0 +1,26 @@
<?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>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>9.0</string>
</dict>
</plist>
+2
View File
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"
+2
View File
@@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig"
+12
View File
@@ -0,0 +1,12 @@
platform :tvos, '14.0'
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
target 'Runner' do
# No pods.
end
+749
View File
@@ -0,0 +1,749 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
0D69E2DEA6EA0538722BC7F0 /* ConnectivityProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D0E84BD711F2744BA0D136 /* ConnectivityProvider.swift */; };
12E72ED3EFA35A8715DE2A52 /* GamepadStreamHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D82813FF8CCC8320A8C32FC7 /* GamepadStreamHandler.swift */; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
30E568153D56A59EA5EADCBB /* PathProviderPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87984F20624EC6CA45E20790 /* PathProviderPlugin.swift */; };
32F0056AF154FC4EA76CBEFC /* GamepadPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD57BBEE1B72584C9C7D44B9 /* GamepadPlugin.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
477FB0F520224870902B9FD7 /* messages.g.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FF71CD2416844A1FC485D77 /* messages.g.swift */; };
4A6DE7BDEF20A6242A3C6365 /* GCControllerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F2071908D08983E6A6A1C1E /* GCControllerManager.swift */; };
5689B077C6EA57D53BBE8D0D /* OsMediaControlsPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B8663A3A20A0D6F446B6B37 /* OsMediaControlsPlugin.swift */; };
5C71F5F7B33075F2B007825B /* MpvPlayerPluginShared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73645904F226A24585A092CE /* MpvPlayerPluginShared.swift */; };
5CDBEB7E3FE968FC131E964A /* ConnectivityPlusPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = C667C1287EFBDF4DF45C5313 /* ConnectivityPlusPlugin.swift */; };
691577F0EB3F4EB1A5160280 /* MpvPlayerCoreBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = D52A3BDA46E79969EA1DF3AC /* MpvPlayerCoreBase.swift */; };
6A0510802F9B91CB0090B5FC /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = EA0F4263E7B912702C490108 /* MPVKit */; };
6F3C0DD6F2F8DA14E7E2F386 /* MpvPlayerCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = A12B8610AE5D580077264851 /* MpvPlayerCore.swift */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
8E5EED3DDAC9455D4DAA9776 /* MpvPlayerPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D7A998830EDC8F77BF521D1 /* MpvPlayerPlugin.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
A7ED1D92E7B15D3F7562F93E /* SharedPreferencesPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = E09D436FC3E6CD994DC1B488 /* SharedPreferencesPlugin.swift */; };
AA34E3E5872B3792D6959EAA /* MpvPipController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2635E12EB9322B151EE5127 /* MpvPipController.swift */; };
B494441CBB8FB05D04F55DD9 /* DeviceInfoPlusPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C4BADB53B8D59E031563AA9 /* DeviceInfoPlusPlugin.swift */; };
BF599A1C25382688DD7F4505 /* ButtonMapping.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FBF4BBA5D82316059225B32 /* ButtonMapping.swift */; };
C1E0D7252DE079EDFA234F90 /* PathMonitorConnectivityProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4716B74C5238614844BCA224 /* PathMonitorConnectivityProvider.swift */; };
C330A1087AA135CCFC0C6203 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 5F435CB4590A55958615A7AD /* libPods-Runner.a */; };
F36688C4E8DF44BC6BDD99A5 /* PackageInfoPlusPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 300783F8CE83D2530D2EDD97 /* PackageInfoPlusPlugin.swift */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 10;
files = (
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
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>"; };
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>"; };
0FF71CD2416844A1FC485D77 /* messages.g.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = messages.g.swift; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
20D0E84BD711F2744BA0D136 /* ConnectivityProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConnectivityProvider.swift; sourceTree = "<group>"; };
300783F8CE83D2530D2EDD97 /* PackageInfoPlusPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PackageInfoPlusPlugin.swift; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
420881FB6A648A2AFD39FFF2 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
4716B74C5238614844BCA224 /* PathMonitorConnectivityProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PathMonitorConnectivityProvider.swift; sourceTree = "<group>"; };
5C4BADB53B8D59E031563AA9 /* DeviceInfoPlusPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DeviceInfoPlusPlugin.swift; sourceTree = "<group>"; };
5F435CB4590A55958615A7AD /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; };
5FBF4BBA5D82316059225B32 /* ButtonMapping.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ButtonMapping.swift; sourceTree = "<group>"; };
73645904F226A24585A092CE /* MpvPlayerPluginShared.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MpvPlayerPluginShared.swift; path = ../shared/apple/MpvPlayer/MpvPlayerPluginShared.swift; sourceTree = "<source_root>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; 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>"; };
7B8663A3A20A0D6F446B6B37 /* OsMediaControlsPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = OsMediaControlsPlugin.swift; sourceTree = "<group>"; };
87984F20624EC6CA45E20790 /* PathProviderPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PathProviderPlugin.swift; sourceTree = "<group>"; };
8F2071908D08983E6A6A1C1E /* GCControllerManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GCControllerManager.swift; 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; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
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>"; };
C667C1287EFBDF4DF45C5313 /* ConnectivityPlusPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConnectivityPlusPlugin.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>"; };
D82813FF8CCC8320A8C32FC7 /* GamepadStreamHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GamepadStreamHandler.swift; sourceTree = "<group>"; };
DD57BBEE1B72584C9C7D44B9 /* GamepadPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = GamepadPlugin.swift; sourceTree = "<group>"; };
E09D436FC3E6CD994DC1B488 /* SharedPreferencesPlugin.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SharedPreferencesPlugin.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
6A0510802F9B91CB0090B5FC /* MPVKit in Frameworks */,
C330A1087AA135CCFC0C6203 /* libPods-Runner.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
0524C4A636652321237B0884 /* path_provider */ = {
isa = PBXGroup;
children = (
87984F20624EC6CA45E20790 /* PathProviderPlugin.swift */,
);
path = path_provider;
sourceTree = "<group>";
};
08F5845B7875697970629696 /* package_info_plus */ = {
isa = PBXGroup;
children = (
300783F8CE83D2530D2EDD97 /* PackageInfoPlusPlugin.swift */,
);
path = package_info_plus;
sourceTree = "<group>";
};
19DB3FBCF738894832DA07EF /* device_info_plus */ = {
isa = PBXGroup;
children = (
5C4BADB53B8D59E031563AA9 /* DeviceInfoPlusPlugin.swift */,
);
path = device_info_plus;
sourceTree = "<group>";
};
53CDD29681161166962B9FA5 /* Pods */ = {
isa = PBXGroup;
children = (
0C25F4F2367A30B47E945AD6 /* Pods-Runner.debug.xcconfig */,
04DD35536DEE7C27FFA53862 /* Pods-Runner.release.xcconfig */,
420881FB6A648A2AFD39FFF2 /* Pods-Runner.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
5815557FAD15751414018354 /* shared_preferences_foundation */ = {
isa = PBXGroup;
children = (
E09D436FC3E6CD994DC1B488 /* SharedPreferencesPlugin.swift */,
0FF71CD2416844A1FC485D77 /* messages.g.swift */,
);
path = shared_preferences_foundation;
sourceTree = "<group>";
};
5833EC5B503BBB4E370BA1B7 /* Frameworks */ = {
isa = PBXGroup;
children = (
5F435CB4590A55958615A7AD /* libPods-Runner.a */,
);
name = Frameworks;
sourceTree = "<group>";
};
737C2C621E266B83D4011221 /* connectivity_plus */ = {
isa = PBXGroup;
children = (
C667C1287EFBDF4DF45C5313 /* ConnectivityPlusPlugin.swift */,
20D0E84BD711F2744BA0D136 /* ConnectivityProvider.swift */,
4716B74C5238614844BCA224 /* PathMonitorConnectivityProvider.swift */,
);
path = connectivity_plus;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
53CDD29681161166962B9FA5 /* Pods */,
5833EC5B503BBB4E370BA1B7 /* Frameworks */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
);
name = Products;
sourceTree = "<group>";
};
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
97C147021CF9000F007C117D /* Info.plist */,
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
BF0B7DFE378943E9D7865D18 /* MpvPlayer */,
E6BAF734A5201F83A1B61DCC /* Plugins */,
);
path = Runner;
sourceTree = "<group>";
};
AD5C822C9001042A47B99FBD /* universal_gamepad */ = {
isa = PBXGroup;
children = (
DD57BBEE1B72584C9C7D44B9 /* GamepadPlugin.swift */,
D82813FF8CCC8320A8C32FC7 /* GamepadStreamHandler.swift */,
8F2071908D08983E6A6A1C1E /* GCControllerManager.swift */,
5FBF4BBA5D82316059225B32 /* ButtonMapping.swift */,
);
path = universal_gamepad;
sourceTree = "<group>";
};
BF0B7DFE378943E9D7865D18 /* MpvPlayer */ = {
isa = PBXGroup;
children = (
D52A3BDA46E79969EA1DF3AC /* MpvPlayerCoreBase.swift */,
73645904F226A24585A092CE /* MpvPlayerPluginShared.swift */,
A12B8610AE5D580077264851 /* MpvPlayerCore.swift */,
9D7A998830EDC8F77BF521D1 /* MpvPlayerPlugin.swift */,
A2635E12EB9322B151EE5127 /* MpvPipController.swift */,
);
name = MpvPlayer;
path = Runner/MpvPlayer;
sourceTree = "<group>";
};
C538704E1F003452B22FE0C7 /* os_media_controls */ = {
isa = PBXGroup;
children = (
7B8663A3A20A0D6F446B6B37 /* OsMediaControlsPlugin.swift */,
);
path = os_media_controls;
sourceTree = "<group>";
};
E6BAF734A5201F83A1B61DCC /* Plugins */ = {
isa = PBXGroup;
children = (
5815557FAD15751414018354 /* shared_preferences_foundation */,
08F5845B7875697970629696 /* package_info_plus */,
0524C4A636652321237B0884 /* path_provider */,
AD5C822C9001042A47B99FBD /* universal_gamepad */,
19DB3FBCF738894832DA07EF /* device_info_plus */,
737C2C621E266B83D4011221 /* connectivity_plus */,
C538704E1F003452B22FE0C7 /* os_media_controls */,
);
path = Plugins;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
570D41C54A87D72AB27F6F15 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
packageProductDependencies = (
EA0F4263E7B912702C490108 /* MPVKit */,
);
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1020;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
compatibilityVersion = "Xcode 9.3";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
Base,
);
mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
CEE842A5A25923620300AB90 /* XCRemoteSwiftPackageReference "MPVKit" */,
);
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = "";
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Thin Binary";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "echo no-op thin binary\n";
};
570D41C54A87D72AB27F6F15 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "Run Script";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "#/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n/bin/sh \"$SOURCE_ROOT/scripts/xcode_appletv.sh\" build\n";
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
691577F0EB3F4EB1A5160280 /* MpvPlayerCoreBase.swift in Sources */,
5C71F5F7B33075F2B007825B /* MpvPlayerPluginShared.swift in Sources */,
6F3C0DD6F2F8DA14E7E2F386 /* MpvPlayerCore.swift in Sources */,
8E5EED3DDAC9455D4DAA9776 /* MpvPlayerPlugin.swift in Sources */,
AA34E3E5872B3792D6959EAA /* MpvPipController.swift in Sources */,
A7ED1D92E7B15D3F7562F93E /* SharedPreferencesPlugin.swift in Sources */,
477FB0F520224870902B9FD7 /* messages.g.swift in Sources */,
F36688C4E8DF44BC6BDD99A5 /* PackageInfoPlusPlugin.swift in Sources */,
30E568153D56A59EA5EADCBB /* PathProviderPlugin.swift in Sources */,
32F0056AF154FC4EA76CBEFC /* GamepadPlugin.swift in Sources */,
12E72ED3EFA35A8715DE2A52 /* GamepadStreamHandler.swift in Sources */,
4A6DE7BDEF20A6242A3C6365 /* GCControllerManager.swift in Sources */,
BF599A1C25382688DD7F4505 /* ButtonMapping.swift in Sources */,
B494441CBB8FB05D04F55DD9 /* DeviceInfoPlusPlugin.swift in Sources */,
5CDBEB7E3FE968FC131E964A /* ConnectivityPlusPlugin.swift in Sources */,
0D69E2DEA6EA0538722BC7F0 /* ConnectivityProvider.swift in Sources */,
C1E0D7252DE079EDFA234F90 /* PathMonitorConnectivityProvider.swift in Sources */,
5689B077C6EA57D53BBE8D0D /* OsMediaControlsPlugin.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C146FB1CF9000F007C117D /* Base */,
);
name = Main.storyboard;
sourceTree = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MTL_ENABLE_DEBUG_INFO = NO;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = appletvos;
SUPPORTED_PLATFORMS = "appletvos appletvsimulator";
TARGETED_DEVICE_FAMILY = 3;
TVOS_DEPLOYMENT_TARGET = 13.0;
VALIDATE_PRODUCT = YES;
};
name = Profile;
};
249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = G88U5B5783;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Plezy;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.edde746.plezy;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
"$(inherited)",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = appletvos;
TARGETED_DEVICE_FAMILY = 3;
TVOS_DEPLOYMENT_TARGET = 13.0;
};
name = Debug;
};
97C147041CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MTL_ENABLE_DEBUG_INFO = NO;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = appletvos;
SUPPORTED_PLATFORMS = "appletvos appletvsimulator";
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
TARGETED_DEVICE_FAMILY = 3;
TVOS_DEPLOYMENT_TARGET = 13.0;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
97C147061CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = G88U5B5783;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Plezy;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.edde746.plezy;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Debug;
};
97C147071CF9000F007C117D /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = G88U5B5783;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = Plezy;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
LIBRARY_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.edde746.plezy;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147031CF9000F007C117D /* Debug */,
97C147041CF9000F007C117D /* Release */,
249021D3217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (
97C147061CF9000F007C117D /* Debug */,
97C147071CF9000F007C117D /* Release */,
249021D4217E4FDB00AE95B9 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
CEE842A5A25923620300AB90 /* XCRemoteSwiftPackageReference "MPVKit" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/edde746/MPVKit";
requirement = {
branch = main;
kind = branch;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
EA0F4263E7B912702C490108 /* MPVKit */ = {
isa = XCSwiftPackageProductDependency;
package = CEE842A5A25923620300AB90 /* XCRemoteSwiftPackageReference "MPVKit" */;
productName = MPVKit;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?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>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?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>PreviewsEnabled</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,15 @@
{
"originHash" : "c535d3cdb62bd4e11e67f3c9e68290fa7ee9306634c5f56d2b0396eb75ed41ee",
"pins" : [
{
"identity" : "mpvkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/edde746/MPVKit",
"state" : {
"branch" : "main",
"revision" : "29803ccbfa631997d496f34c2c1ed191cc8fd2c3"
}
}
],
"version" : 3
}
@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1020"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Release"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
@@ -0,0 +1,8 @@
<?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>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>
@@ -0,0 +1,8 @@
<?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>PreviewsEnabled</key>
<false/>
</dict>
</plist>
@@ -0,0 +1,15 @@
{
"originHash" : "c535d3cdb62bd4e11e67f3c9e68290fa7ee9306634c5f56d2b0396eb75ed41ee",
"pins" : [
{
"identity" : "mpvkit",
"kind" : "remoteSourceControl",
"location" : "https://github.com/edde746/MPVKit",
"state" : {
"branch" : "main",
"revision" : "29803ccbfa631997d496f34c2c1ed191cc8fd2c3"
}
}
],
"version" : 3
}
+67
View File
@@ -0,0 +1,67 @@
import Flutter
import UIKit
import AVFoundation
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
do {
let session = AVAudioSession.sharedInstance()
try session.setCategory(.playback, mode: .default)
try session.setActive(true)
} catch {
print("Failed to configure audio session: \(error)")
}
application.beginReceivingRemoteControlEvents()
NSLog("[tvos] AppDelegate didFinishLaunching — registering plugins")
if let spRegistrar = self.registrar(forPlugin: "SharedPreferencesPlugin") {
NSLog("[tvos] Registering SharedPreferencesPlugin (Swift-direct)")
SharedPreferencesPlugin.register(with: spRegistrar)
} else {
NSLog("[tvos] SharedPreferencesPlugin registrar nil")
}
if let mpvRegistrar = self.registrar(forPlugin: "MpvPlayerPlugin") {
NSLog("[tvos] Registering MpvPlayerPlugin")
MpvPlayerPlugin.register(with: mpvRegistrar)
}
if let r = self.registrar(forPlugin: "PackageInfoPlusPlugin") {
NSLog("[tvos] Registering PackageInfoPlusPlugin")
PackageInfoPlusPlugin.register(with: r)
}
if let r = self.registrar(forPlugin: "PathProviderPlugin") {
NSLog("[tvos] Registering PathProviderPlugin")
PathProviderPlugin.register(with: r)
}
if let r = self.registrar(forPlugin: "GamepadPlugin") {
NSLog("[tvos] Registering GamepadPlugin")
GamepadPlugin.register(with: r)
}
if let r = self.registrar(forPlugin: "DeviceInfoPlusPlugin") {
NSLog("[tvos] Registering DeviceInfoPlusPlugin")
DeviceInfoPlusPlugin.register(with: r)
}
if let r = self.registrar(forPlugin: "ConnectivityPlusPlugin") {
NSLog("[tvos] Registering ConnectivityPlusPlugin")
ConnectivityPlusPlugin.register(with: r)
}
if let r = self.registrar(forPlugin: "OsMediaControlsPlugin") {
NSLog("[tvos] Registering OsMediaControlsPlugin")
OsMediaControlsPlugin.register(with: r)
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
@@ -0,0 +1,6 @@
{
"images" : [
{ "filename" : "Back.png", "idiom" : "tv" }
],
"info" : { "author" : "xcode", "version" : 1 }
}
@@ -0,0 +1,3 @@
{
"info" : { "author" : "xcode", "version" : 1 }
}
@@ -0,0 +1,8 @@
{
"info" : { "author" : "xcode", "version" : 1 },
"layers" : [
{ "filename" : "Front.imagestacklayer" },
{ "filename" : "Middle.imagestacklayer" },
{ "filename" : "Back.imagestacklayer" }
]
}
@@ -0,0 +1,6 @@
{
"images" : [
{ "filename" : "Front.png", "idiom" : "tv" }
],
"info" : { "author" : "xcode", "version" : 1 }
}
@@ -0,0 +1,3 @@
{
"info" : { "author" : "xcode", "version" : 1 }
}
@@ -0,0 +1,6 @@
{
"images" : [
{ "filename" : "Middle.png", "idiom" : "tv" }
],
"info" : { "author" : "xcode", "version" : 1 }
}
@@ -0,0 +1,3 @@
{
"info" : { "author" : "xcode", "version" : 1 }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 KiB

@@ -0,0 +1,7 @@
{
"images" : [
{ "filename" : "Back.png", "idiom" : "tv", "scale" : "1x" },
{ "filename" : "Back@2x.png", "idiom" : "tv", "scale" : "2x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}
@@ -0,0 +1,3 @@
{
"info" : { "author" : "xcode", "version" : 1 }
}
@@ -0,0 +1,8 @@
{
"info" : { "author" : "xcode", "version" : 1 },
"layers" : [
{ "filename" : "Front.imagestacklayer" },
{ "filename" : "Middle.imagestacklayer" },
{ "filename" : "Back.imagestacklayer" }
]
}
@@ -0,0 +1,7 @@
{
"images" : [
{ "filename" : "Front.png", "idiom" : "tv", "scale" : "1x" },
{ "filename" : "Front@2x.png", "idiom" : "tv", "scale" : "2x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

@@ -0,0 +1,3 @@
{
"info" : { "author" : "xcode", "version" : 1 }
}
@@ -0,0 +1,7 @@
{
"images" : [
{ "filename" : "Middle.png", "idiom" : "tv", "scale" : "1x" },
{ "filename" : "Middle@2x.png", "idiom" : "tv", "scale" : "2x" }
],
"info" : { "author" : "xcode", "version" : 1 }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 333 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 367 B

@@ -0,0 +1,3 @@
{
"info" : { "author" : "xcode", "version" : 1 }
}
@@ -0,0 +1,9 @@
{
"assets" : [
{ "filename" : "App Icon.imagestack", "idiom" : "tv", "role" : "primary-app-icon", "size" : "400x240" },
{ "filename" : "App Icon - App Store.imagestack","idiom" : "tv", "role" : "primary-app-icon", "size" : "1280x768" },
{ "filename" : "Top Shelf Image.imageset", "idiom" : "tv", "role" : "top-shelf-image", "size" : "1920x720" },
{ "filename" : "Top Shelf Image Wide.imageset", "idiom" : "tv", "role" : "top-shelf-image-wide", "size" : "2320x720" }
],
"info" : { "author" : "xcode", "version" : 1 }
}
@@ -0,0 +1,6 @@
{
"images" : [
{ "filename" : "top-shelf-wide.png", "idiom" : "tv" }
],
"info" : { "author" : "xcode", "version" : 1 }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

@@ -0,0 +1,6 @@
{
"images" : [
{ "filename" : "top-shelf.png", "idiom" : "tv" }
],
"info" : { "author" : "xcode", "version" : 1 }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 983 KiB

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "LaunchImage.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"filename" : "LaunchImage@3x.png",
"scale" : "3x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

@@ -0,0 +1,5 @@
# Launch Screen Assets
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder.AppleTV.Storyboard" version="3.0" toolsVersion="16096" targetRuntime="AppleTV" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" ulaunchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="appleTV" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="16087"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="1920" height="1080"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
<rect key="frame" x="959.5" y="539.5" width="1" height="1"/>
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="1" height="1"/>
</resources>
</document>
+29
View File
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder.AppleTV.Storyboard" version="3.0" toolsVersion="15702" targetRuntime="AppleTV" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
<device id="appleTV" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15704"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="1920" height="1080"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="wu6-TO-1qx"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
+36
View File
@@ -0,0 +1,36 @@
<?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</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>Plezy</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>arm64</string>
</array>
</dict>
</plist>
@@ -0,0 +1,92 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source is governed by a BSD-style license that can
// be found in the LICENSE file.
import Flutter
public class ConnectivityPlusPlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
private let connectivityProvider: ConnectivityProvider
private var eventSink: FlutterEventSink?
init(connectivityProvider: ConnectivityProvider) {
self.connectivityProvider = connectivityProvider
super.init()
self.connectivityProvider.connectivityUpdateHandler = connectivityUpdateHandler
}
public static func register(with registrar: FlutterPluginRegistrar) {
let binaryMessenger = registrar.messenger()
let channel = FlutterMethodChannel(
name: "dev.fluttercommunity.plus/connectivity",
binaryMessenger: binaryMessenger)
let streamChannel = FlutterEventChannel(
name: "dev.fluttercommunity.plus/connectivity_status",
binaryMessenger: binaryMessenger)
let connectivityProvider = PathMonitorConnectivityProvider()
let instance = ConnectivityPlusPlugin(connectivityProvider: connectivityProvider)
streamChannel.setStreamHandler(instance)
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func detachFromEngine(for registrar: FlutterPluginRegistrar) {
eventSink = nil
connectivityProvider.stop()
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "check":
result(statusFrom(connectivityTypes: connectivityProvider.currentConnectivityTypes))
default:
result(FlutterMethodNotImplemented)
}
}
private func statusFrom(connectivityType: ConnectivityType) -> String {
switch connectivityType {
case .wifi:
return "wifi"
case .cellular:
return "mobile"
case .wiredEthernet:
return "ethernet"
case .other:
return "other"
case .none:
return "none"
}
}
private func statusFrom(connectivityTypes: [ConnectivityType]) -> [String] {
return connectivityTypes.map {
self.statusFrom(connectivityType: $0)
}
}
public func onListen(
withArguments _: Any?,
eventSink events: @escaping FlutterEventSink
) -> FlutterError? {
eventSink = events
connectivityProvider.start()
// Update this to handle a list
connectivityUpdateHandler(connectivityTypes: connectivityProvider.currentConnectivityTypes)
return nil
}
private func connectivityUpdateHandler(connectivityTypes: [ConnectivityType]) {
DispatchQueue.main.async {
self.eventSink?(self.statusFrom(connectivityTypes: connectivityTypes))
}
}
public func onCancel(withArguments _: Any?) -> FlutterError? {
connectivityProvider.stop()
eventSink = nil
return nil
}
}
@@ -0,0 +1,21 @@
import Foundation
public enum ConnectivityType {
case none
case wiredEthernet
case wifi
case cellular
case other
}
public protocol ConnectivityProvider: NSObjectProtocol {
typealias ConnectivityUpdateHandler = ([ConnectivityType]) -> Void
var currentConnectivityTypes: [ConnectivityType] { get }
var connectivityUpdateHandler: ConnectivityUpdateHandler? { get set }
func start()
func stop()
}
@@ -0,0 +1,69 @@
import Foundation
import Network
public class PathMonitorConnectivityProvider: NSObject, ConnectivityProvider {
// Use .utility, as it is intended for tasks that the user does not track actively.
// See: https://developer.apple.com/documentation/dispatch/dispatchqos
private let queue = DispatchQueue.global(qos: .utility)
private var pathMonitor: NWPathMonitor?
private func connectivityFrom(path: NWPath) -> [ConnectivityType] {
var types: [ConnectivityType] = []
// Check for connectivity and append to types array as necessary
if path.status == .satisfied {
if path.usesInterfaceType(.wifi) {
types.append(.wifi)
}
if path.usesInterfaceType(.cellular) {
types.append(.cellular)
}
if path.usesInterfaceType(.wiredEthernet) {
types.append(.wiredEthernet)
}
if path.usesInterfaceType(.other) {
types.append(.other)
}
}
return types.isEmpty ? [.none] : types
}
public var currentConnectivityTypes: [ConnectivityType] {
let path = ensurePathMonitor().currentPath
return connectivityFrom(path: path)
}
public var connectivityUpdateHandler: ConnectivityUpdateHandler?
override init() {
super.init()
_ = ensurePathMonitor()
}
public func start() {
_ = ensurePathMonitor()
}
public func stop() {
pathMonitor?.cancel()
pathMonitor = nil
}
@discardableResult
private func ensurePathMonitor() -> NWPathMonitor {
if (pathMonitor == nil) {
let pathMonitor = NWPathMonitor()
pathMonitor.start(queue: queue)
pathMonitor.pathUpdateHandler = pathUpdateHandler
self.pathMonitor = pathMonitor
}
return self.pathMonitor!
}
private func pathUpdateHandler(path: NWPath) {
connectivityUpdateHandler?(connectivityFrom(path: path))
}
}
@@ -0,0 +1,119 @@
// Pure-Swift tvOS port of fluttercommunity.plus/device_info. Mirrors the
// Objective-C FPPDeviceInfoPlusPlugin on iOS so the Dart IosDeviceInfo
// parser finds all the keys it expects.
import Foundation
import UIKit
#if os(iOS) || os(tvOS)
import Flutter
public class DeviceInfoPlusPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(
name: "dev.fluttercommunity.plus/device_info",
binaryMessenger: registrar.messenger()
)
let instance = DeviceInfoPlusPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
guard call.method == "getDeviceInfo" else {
result(FlutterMethodNotImplemented)
return
}
let device = UIDevice.current
var uts = utsname()
uname(&uts)
let processInfo = ProcessInfo.processInfo
let isSimulator: Bool = {
#if targetEnvironment(simulator)
return true
#else
return false
#endif
}()
let isPhysicalDevice = !isSimulator
let machine: String
if isPhysicalDevice {
machine = Self.utsnameString(&uts.machine)
} else {
machine = processInfo.environment["SIMULATOR_MODEL_IDENTIFIER"] ?? ""
}
let freeDisk: Int64
let totalDisk: Int64
if let attrs = try? FileManager.default.attributesOfFileSystem(
forPath: NSHomeDirectory())
{
freeDisk = (attrs[.systemFreeSize] as? NSNumber)?.int64Value ?? -1
totalDisk = (attrs[.systemSize] as? NSNumber)?.int64Value ?? -1
} else {
freeDisk = -1
totalDisk = -1
}
var isiOSAppOnMac = false
if #available(iOS 14.0, tvOS 14.0, *) {
isiOSAppOnMac = processInfo.isiOSAppOnMac
}
let physicalRam = Int64(processInfo.physicalMemory) / 1_048_576
let info: [String: Any] = [
"name": device.name,
"systemName": device.systemName,
"systemVersion": device.systemVersion,
"model": device.model,
"localizedModel": device.localizedModel,
"modelName": machine,
"identifierForVendor": device.identifierForVendor?.uuidString as Any,
"freeDiskSize": freeDisk,
"totalDiskSize": totalDisk,
"isPhysicalDevice": isPhysicalDevice,
"isiOSAppOnMac": isiOSAppOnMac,
"physicalRamSize": physicalRam,
"availableRamSize": Self.availableMemoryMB(),
"utsname": [
"sysname": Self.utsnameString(&uts.sysname),
"nodename": Self.utsnameString(&uts.nodename),
"release": Self.utsnameString(&uts.release),
"version": Self.utsnameString(&uts.version),
"machine": machine,
],
]
result(info)
}
private static func utsnameString<T>(_ tupled: inout T) -> String {
withUnsafePointer(to: &tupled) { ptr in
ptr.withMemoryRebound(to: CChar.self, capacity: MemoryLayout<T>.size) {
String(cString: $0)
}
}
}
private static func availableMemoryMB() -> Int {
var hostInfo = vm_statistics_data_t()
var count = mach_msg_type_number_t(
MemoryLayout<vm_statistics_data_t>.size / MemoryLayout<integer_t>.size)
let host = mach_host_self()
var pageSize: vm_size_t = 0
host_page_size(host, &pageSize)
let result: kern_return_t = withUnsafeMutablePointer(to: &hostInfo) { ptr in
ptr.withMemoryRebound(to: integer_t.self, capacity: Int(count)) { reboundPtr in
host_statistics(host, HOST_VM_INFO, reboundPtr, &count)
}
}
guard result == KERN_SUCCESS else { return -1 }
let memFree = UInt64(hostInfo.free_count) * UInt64(pageSize)
return Int(memFree / 1_048_576)
}
}
#endif
@@ -0,0 +1,370 @@
import Flutter
import UIKit
import MediaPlayer
import AVFoundation
public class OsMediaControlsPlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
private var eventSink: FlutterEventSink?
private let nowPlayingCenter = MPNowPlayingInfoCenter.default()
private let commandCenter = MPRemoteCommandCenter.shared()
private var currentMetadata: [String: Any] = [:]
public static func register(with registrar: FlutterPluginRegistrar) {
let methodChannel = FlutterMethodChannel(
name: "com.edde746.os_media_controls/methods",
binaryMessenger: registrar.messenger()
)
let eventChannel = FlutterEventChannel(
name: "com.edde746.os_media_controls/events",
binaryMessenger: registrar.messenger()
)
let instance = OsMediaControlsPlugin()
registrar.addMethodCallDelegate(instance, channel: methodChannel)
eventChannel.setStreamHandler(instance)
}
public override init() {
super.init()
// Ensure app receives remote control events for Now Playing controls
DispatchQueue.main.async {
UIApplication.shared.beginReceivingRemoteControlEvents()
}
setupRemoteCommandCenter()
}
private func setupRemoteCommandCenter() {
// Play command
commandCenter.playCommand.isEnabled = true
commandCenter.playCommand.addTarget { [weak self] event in
self?.sendEvent(["type": "play"])
return .success
}
// Pause command
commandCenter.pauseCommand.isEnabled = true
commandCenter.pauseCommand.addTarget { [weak self] event in
self?.sendEvent(["type": "pause"])
return .success
}
// Toggle play/pause command
commandCenter.togglePlayPauseCommand.isEnabled = true
commandCenter.togglePlayPauseCommand.addTarget { [weak self] event in
self?.sendEvent(["type": "togglePlayPause"])
return .success
}
// Next track command
commandCenter.nextTrackCommand.isEnabled = true
commandCenter.nextTrackCommand.addTarget { [weak self] event in
self?.sendEvent(["type": "next"])
return .success
}
// Previous track command
commandCenter.previousTrackCommand.isEnabled = true
commandCenter.previousTrackCommand.addTarget { [weak self] event in
self?.sendEvent(["type": "previous"])
return .success
}
// Change playback position command (seek)
commandCenter.changePlaybackPositionCommand.isEnabled = true
commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in
if let positionEvent = event as? MPChangePlaybackPositionCommandEvent {
self?.sendEvent([
"type": "seek",
"position": positionEvent.positionTime
])
}
return .success
}
// Skip forward command
commandCenter.skipForwardCommand.isEnabled = false // Disabled by default
commandCenter.skipForwardCommand.addTarget { [weak self] event in
if let skipEvent = event as? MPSkipIntervalCommandEvent {
self?.sendEvent([
"type": "skipForward",
"interval": skipEvent.interval
])
} else {
self?.sendEvent(["type": "skipForward"])
}
return .success
}
// Skip backward command
commandCenter.skipBackwardCommand.isEnabled = false // Disabled by default
commandCenter.skipBackwardCommand.addTarget { [weak self] event in
if let skipEvent = event as? MPSkipIntervalCommandEvent {
self?.sendEvent([
"type": "skipBackward",
"interval": skipEvent.interval
])
} else {
self?.sendEvent(["type": "skipBackward"])
}
return .success
}
// Change playback rate command
commandCenter.changePlaybackRateCommand.isEnabled = true
commandCenter.changePlaybackRateCommand.supportedPlaybackRates = [0.5, 1.0, 1.5, 2.0]
commandCenter.changePlaybackRateCommand.addTarget { [weak self] event in
if let rateEvent = event as? MPChangePlaybackRateCommandEvent {
self?.sendEvent([
"type": "setSpeed",
"speed": rateEvent.playbackRate
])
}
return .success
}
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "setMetadata":
setMetadata(arguments: call.arguments as? [String: Any])
result(nil)
case "setPlaybackState":
setPlaybackState(arguments: call.arguments as? [String: Any])
result(nil)
case "enableControls":
enableControls(arguments: call.arguments as? [String])
result(nil)
case "disableControls":
disableControls(arguments: call.arguments as? [String])
result(nil)
case "setSkipIntervals":
setSkipIntervals(arguments: call.arguments as? [String: Any])
result(nil)
case "setQueueInfo":
setQueueInfo(arguments: call.arguments as? [String: Any])
result(nil)
case "clear":
clear()
result(nil)
default:
result(FlutterMethodNotImplemented)
}
}
private func setMetadata(arguments: [String: Any]?) {
guard let args = arguments else { return }
// Reactivate audio session to ensure media controls work after being cleared
do {
try AVAudioSession.sharedInstance().setActive(true)
} catch {
print("Failed to reactivate audio session: \(error)")
}
// Store metadata for later use
for (key, value) in args {
if key != "artwork" {
currentMetadata[key] = value
}
}
var nowPlayingInfo = nowPlayingCenter.nowPlayingInfo ?? [:]
if let title = args["title"] as? String {
nowPlayingInfo[MPMediaItemPropertyTitle] = title
}
if let artist = args["artist"] as? String {
nowPlayingInfo[MPMediaItemPropertyArtist] = artist
}
if let album = args["album"] as? String {
nowPlayingInfo[MPMediaItemPropertyAlbumTitle] = album
}
if let albumArtist = args["albumArtist"] as? String {
nowPlayingInfo[MPMediaItemPropertyAlbumArtist] = albumArtist
}
if let duration = args["duration"] as? Double {
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration
}
// Handle artwork from bytes (takes precedence over URL)
if let artworkData = args["artwork"] as? FlutterStandardTypedData {
if let image = UIImage(data: artworkData.data) {
nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in image }
}
} else if let artworkUrlString = args["artworkUrl"] as? String, let artworkUrl = URL(string: artworkUrlString) {
// Handle artwork from URL - download asynchronously
URLSession.shared.dataTask(with: artworkUrl) { [weak self] data, response, error in
guard let self = self else { return }
guard error == nil, let data = data, let image = UIImage(data: data) else {
return
}
DispatchQueue.main.async {
var updatedInfo = self.nowPlayingCenter.nowPlayingInfo ?? [:]
updatedInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size) { _ in image }
self.nowPlayingCenter.nowPlayingInfo = updatedInfo
}
}.resume()
}
nowPlayingCenter.nowPlayingInfo = nowPlayingInfo
}
private func setPlaybackState(arguments: [String: Any]?) {
guard let args = arguments,
let stateString = args["state"] as? String,
let position = args["position"] as? Double,
let speed = args["speed"] as? Double else { return }
// Reactivate audio session to ensure media controls work after being cleared
do {
try AVAudioSession.sharedInstance().setActive(true)
} catch {
print("Failed to reactivate audio session: \(error)")
}
var nowPlayingInfo = nowPlayingCenter.nowPlayingInfo ?? [:]
nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = position
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] =
stateString == "playing" ? speed : 0.0
nowPlayingCenter.nowPlayingInfo = nowPlayingInfo
}
private func enableControls(arguments: [String]?) {
guard let controls = arguments else { return }
for control in controls {
switch control {
case "play":
commandCenter.playCommand.isEnabled = true
case "pause":
commandCenter.pauseCommand.isEnabled = true
case "stop":
// iOS doesn't have a dedicated stop command
break
case "next":
commandCenter.nextTrackCommand.isEnabled = true
case "previous":
commandCenter.previousTrackCommand.isEnabled = true
case "seek":
commandCenter.changePlaybackPositionCommand.isEnabled = true
case "skipForward":
commandCenter.skipForwardCommand.isEnabled = true
case "skipBackward":
commandCenter.skipBackwardCommand.isEnabled = true
case "changeSpeed":
commandCenter.changePlaybackRateCommand.isEnabled = true
default:
break
}
}
}
private func disableControls(arguments: [String]?) {
guard let controls = arguments else { return }
for control in controls {
switch control {
case "play":
commandCenter.playCommand.isEnabled = false
case "pause":
commandCenter.pauseCommand.isEnabled = false
case "next":
commandCenter.nextTrackCommand.isEnabled = false
case "previous":
commandCenter.previousTrackCommand.isEnabled = false
case "seek":
commandCenter.changePlaybackPositionCommand.isEnabled = false
case "skipForward":
commandCenter.skipForwardCommand.isEnabled = false
case "skipBackward":
commandCenter.skipBackwardCommand.isEnabled = false
case "changeSpeed":
commandCenter.changePlaybackRateCommand.isEnabled = false
default:
break
}
}
}
private func setSkipIntervals(arguments: [String: Any]?) {
guard let args = arguments else { return }
if let forward = args["forward"] as? Int {
commandCenter.skipForwardCommand.isEnabled = true
commandCenter.skipForwardCommand.preferredIntervals = [NSNumber(value: forward)]
}
if let backward = args["backward"] as? Int {
commandCenter.skipBackwardCommand.isEnabled = true
commandCenter.skipBackwardCommand.preferredIntervals = [NSNumber(value: backward)]
}
}
private func setQueueInfo(arguments: [String: Any]?) {
guard let args = arguments,
let currentIndex = args["currentIndex"] as? Int,
let queueLength = args["queueLength"] as? Int else { return }
var nowPlayingInfo = nowPlayingCenter.nowPlayingInfo ?? [:]
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackQueueIndex] = currentIndex
nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackQueueCount] = queueLength
nowPlayingCenter.nowPlayingInfo = nowPlayingInfo
}
private func clear() {
nowPlayingCenter.nowPlayingInfo = nil
currentMetadata.removeAll()
// Deactivate audio session to force iOS to remove controls from Control Center
do {
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
} catch {
// Audio session deactivation failed, but continue with cleanup
}
// Disable all command center buttons
let commandCenter = MPRemoteCommandCenter.shared()
commandCenter.playCommand.isEnabled = false
commandCenter.pauseCommand.isEnabled = false
commandCenter.togglePlayPauseCommand.isEnabled = false
commandCenter.nextTrackCommand.isEnabled = false
commandCenter.previousTrackCommand.isEnabled = false
commandCenter.changePlaybackPositionCommand.isEnabled = false
commandCenter.skipForwardCommand.isEnabled = false
commandCenter.skipBackwardCommand.isEnabled = false
commandCenter.changePlaybackRateCommand.isEnabled = false
}
private func sendEvent(_ event: [String: Any]) {
eventSink?(event)
}
// MARK: - FlutterStreamHandler
public func onListen(withArguments arguments: Any?,
eventSink events: @escaping FlutterEventSink) -> FlutterError? {
self.eventSink = events
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
self.eventSink = nil
return nil
}
}
@@ -0,0 +1,82 @@
// Pure-Swift tvOS port of fluttercommunity.plus/package_info.
// Matches the Objective-C FPPPackageInfoPlusPlugin on iOS exactly enough to
// satisfy PackageInfo.fromPlatform() in Dart.
import Foundation
#if os(iOS) || os(tvOS)
import Flutter
public class PackageInfoPlusPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(
name: "dev.fluttercommunity.plus/package_info",
binaryMessenger: registrar.messenger()
)
let instance = PackageInfoPlusPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
guard call.method == "getAll" else {
result(FlutterMethodNotImplemented)
return
}
let bundle = Bundle.main
let appStoreReceipt = bundle.appStoreReceiptURL?.path ?? ""
let installerStore: String
if appStoreReceipt.contains("CoreSimulator") {
installerStore = "com.apple.simulator"
} else if appStoreReceipt.contains("sandboxReceipt") {
installerStore = "com.apple.testflight"
} else {
installerStore = "com.apple"
}
let appName =
(bundle.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String)
?? (bundle.object(forInfoDictionaryKey: "CFBundleName") as? String)
?? ""
let packageName = bundle.bundleIdentifier ?? ""
let version =
(bundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String) ?? ""
let buildNumber = (bundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String) ?? ""
let installTime = Self.timeMillisString(from: Self.installDate())
let updateTime = Self.timeMillisString(from: Self.updateDate())
result([
"appName": appName,
"packageName": packageName,
"version": version,
"buildNumber": buildNumber,
"installerStore": installerStore,
"installTime": installTime as Any,
"updateTime": updateTime as Any,
])
}
private static func installDate() -> Date? {
guard
let docsURL = FileManager.default.urls(
for: .documentDirectory, in: .userDomainMask
).last
else {
return nil
}
let attrs = try? FileManager.default.attributesOfItem(atPath: docsURL.path)
return attrs?[.creationDate] as? Date
}
private static func updateDate() -> Date? {
let attrs = try? FileManager.default.attributesOfItem(atPath: Bundle.main.bundlePath)
return attrs?[.modificationDate] as? Date
}
private static func timeMillisString(from date: Date?) -> String? {
guard let date = date else { return nil }
return String(Int64(date.timeIntervalSince1970 * 1000))
}
}
#endif
@@ -0,0 +1,85 @@
// Pure-Swift tvOS implementation of the legacy plugins.flutter.io/path_provider
// channel. Replaces path_provider_foundation's FFI-based Dart impl, which
// requires the objective_c package's dylib and isn't linked on tvOS.
import Foundation
#if os(iOS) || os(tvOS)
import Flutter
public class PathProviderPlugin: NSObject, FlutterPlugin {
public static func register(with registrar: FlutterPluginRegistrar) {
let channel = FlutterMethodChannel(
name: "plugins.flutter.io/path_provider",
binaryMessenger: registrar.messenger()
)
let instance = PathProviderPlugin()
registrar.addMethodCallDelegate(instance, channel: channel)
}
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
// On tvOS apps have no persistent writable storage; only the Caches
// directory is writable (and can be evicted anytime). Docs, Support, and
// Library all return dummy paths that fail on write. Route them into
// Caches subdirectories so plugins expecting a writable path work.
#if os(tvOS)
let tvosCache = pathIn(.cachesDirectory)
func tvosSubdir(_ name: String) -> String? {
guard let base = tvosCache else { return nil }
let p = (base as NSString).appendingPathComponent(name)
try? FileManager.default.createDirectory(
atPath: p, withIntermediateDirectories: true)
return p
}
switch call.method {
case "getTemporaryDirectory":
result(tvosCache)
case "getApplicationSupportDirectory":
result(tvosSubdir("ApplicationSupport"))
case "getApplicationDocumentsDirectory":
result(tvosSubdir("Documents"))
case "getApplicationCacheDirectory":
result(tvosSubdir("AppCache"))
case "getLibraryDirectory":
result(tvosSubdir("Library"))
case "getDownloadsDirectory":
result(tvosSubdir("Downloads"))
default:
result(FlutterMethodNotImplemented)
}
#else
switch call.method {
case "getTemporaryDirectory":
result(pathIn(.cachesDirectory))
case "getApplicationSupportDirectory":
let path = pathIn(.applicationSupportDirectory)
if let p = path {
try? FileManager.default.createDirectory(
atPath: p, withIntermediateDirectories: true)
}
result(path)
case "getApplicationDocumentsDirectory":
result(pathIn(.documentDirectory))
case "getApplicationCacheDirectory":
let path = pathIn(.cachesDirectory)
if let p = path {
try? FileManager.default.createDirectory(
atPath: p, withIntermediateDirectories: true)
}
result(path)
case "getLibraryDirectory":
result(pathIn(.libraryDirectory))
case "getDownloadsDirectory":
result(pathIn(.downloadsDirectory))
default:
result(FlutterMethodNotImplemented)
}
#endif
}
private func pathIn(_ directory: FileManager.SearchPathDirectory) -> String? {
NSSearchPathForDirectoriesInDomains(directory, .userDomainMask, true).first
}
}
#endif
@@ -0,0 +1,194 @@
// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import Foundation
#if os(iOS) || os(tvOS)
import Flutter
#elseif os(macOS)
import FlutterMacOS
#endif
let argumentError: String = "Argument Error"
public class LegacySharedPreferencesPlugin: NSObject, FlutterPlugin, LegacyUserDefaultsApi {
public static func register(with registrar: FlutterPluginRegistrar) {
let instance = LegacySharedPreferencesPlugin()
// Workaround for https://github.com/flutter/flutter/issues/118103.
#if os(iOS) || os(tvOS)
let messenger = registrar.messenger()
#else
let messenger = registrar.messenger
#endif
LegacyUserDefaultsApiSetup.setUp(binaryMessenger: messenger, api: instance)
}
func getAll(prefix: String, allowList: [String]?) -> [String: Any] {
return getAllPrefs(prefix: prefix, allowList: allowList)
}
func setBool(key: String, value: Bool) {
UserDefaults.standard.set(value, forKey: key)
}
func setDouble(key: String, value: Double) {
UserDefaults.standard.set(value, forKey: key)
}
func setValue(key: String, value: Any) {
UserDefaults.standard.set(value, forKey: key)
}
func remove(key: String) {
UserDefaults.standard.removeObject(forKey: key)
}
func clear(prefix: String, allowList: [String]?) -> Bool {
let defaults = UserDefaults.standard
for (key, _) in getAllPrefs(prefix: prefix, allowList: allowList) {
defaults.removeObject(forKey: key)
}
return true
}
/// Returns all preferences stored with specified prefix.
/// If [allowList] is included, only items included will be returned.
func getAllPrefs(prefix: String, allowList: [String]?) -> [String: Any] {
var filteredPrefs: [String: Any] = [:]
let prefs = try! SharedPreferencesPlugin.getAllPrefs(
allowList: allowList, options: SharedPreferencesPigeonOptions())
for (key, value) in prefs where (key.hasPrefix(prefix)) {
filteredPrefs[key] = value
}
return filteredPrefs
}
}
public class SharedPreferencesPlugin: NSObject, FlutterPlugin, UserDefaultsApi {
public static func register(with registrar: FlutterPluginRegistrar) {
let instance = SharedPreferencesPlugin()
// Workaround for https://github.com/flutter/flutter/issues/118103.
#if os(iOS) || os(tvOS)
let messenger = registrar.messenger()
#else
let messenger = registrar.messenger
#endif
UserDefaultsApiSetup.setUp(binaryMessenger: messenger, api: instance)
LegacySharedPreferencesPlugin.register(with: registrar)
}
static private func getUserDefaults(options: SharedPreferencesPigeonOptions) throws
-> UserDefaults
{
#if os(iOS)
if !(options.suiteName?.starts(with: "group.") ?? true) {
throw FlutterError(
code: argumentError,
message:
"The provided Suite Name '\(options.suiteName!)' does not follow the predefined requirements",
details: "") as! Error
}
#endif
let prefs = UserDefaults(suiteName: options.suiteName)
if prefs == nil {
throw FlutterError(
code: argumentError,
message: "The provided Suite Name '\(options.suiteName!)' does not exist",
details: "") as! Error
}
return prefs!
}
func getKeys(allowList: [String]?, options: SharedPreferencesPigeonOptions) throws -> [String] {
return Array(try getAll(allowList: allowList, options: options).keys)
}
func getAll(allowList: [String]?, options: SharedPreferencesPigeonOptions) throws -> [String: Any]
{
return try SharedPreferencesPlugin.getAllPrefs(allowList: allowList, options: options)
}
func set(key: String, value: Any, options: SharedPreferencesPigeonOptions) throws {
try SharedPreferencesPlugin.getUserDefaults(options: options).set(value, forKey: key)
}
func getValue(key: String, options: SharedPreferencesPigeonOptions) throws -> Any? {
let preference = try SharedPreferencesPlugin.getUserDefaults(options: options).object(
forKey: key)
return SharedPreferencesPlugin.isTypeCompatible(value: preference as Any) ? preference : nil
}
func remove(key: String, options: SharedPreferencesPigeonOptions) throws {
try SharedPreferencesPlugin.getUserDefaults(options: options).removeObject(forKey: key)
}
func clear(allowList: [String]?, options: SharedPreferencesPigeonOptions) throws {
let defaults = try SharedPreferencesPlugin.getUserDefaults(options: options)
if let allowList = allowList {
for (key) in allowList {
defaults.removeObject(forKey: key)
}
} else {
for key in defaults.dictionaryRepresentation().keys {
defaults.removeObject(forKey: key)
}
}
}
/// Returns all preferences stored with specified prefix.
/// If [allowList] is included, only items included will be returned.
/// If no [allowList], returns supported types only.
static func getAllPrefs(allowList: [String]?, options: SharedPreferencesPigeonOptions) throws
-> [String: Any]
{
var filteredPrefs: [String: Any] = [:]
var compatiblePrefs: [String: Any] = [:]
let allowSet = allowList.map { Set($0) }
// Since `getUserDefaults` is initialized with the suite name, it seems redundant to call
// `persistentDomain` with the suite name again. However, it is necessary because
// `dictionaryRepresentation` returns keys from the global domain.
// Also, Apple's docs on `persistentDomain` are incorrect,
// see: https://github.com/feedback-assistant/reports/issues/165
if let appDomain = options.suiteName ?? Bundle.main.bundleIdentifier,
let prefs = try getUserDefaults(options: options).persistentDomain(forName: appDomain)
{
if let allowSet = allowSet {
filteredPrefs = prefs.filter { allowSet.contains($0.key) }
} else {
filteredPrefs = prefs
}
compatiblePrefs = filteredPrefs.filter { isTypeCompatible(value: $0.value) }
}
return compatiblePrefs
}
static func isTypeCompatible(value: Any) -> Bool {
switch value {
case is Bool:
return true
case is Double:
return true
case is String:
return true
case is Int:
return true
case is [Any]:
if let value = value as? [Any] {
return value.allSatisfy(isTypeCompatible)
}
default:
return false
}
return false
}
}
@@ -0,0 +1,452 @@
// Copyright 2013 The Flutter Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Autogenerated from Pigeon (v26.1.0), do not edit directly.
// See also: https://pub.dev/packages/pigeon
import Foundation
#if os(iOS) || os(tvOS)
import Flutter
#elseif os(macOS)
import FlutterMacOS
#else
#error("Unsupported platform.")
#endif
/// Error class for passing custom error details to Dart side.
final class PigeonError: Error {
let code: String
let message: String?
let details: Sendable?
init(code: String, message: String?, details: Sendable?) {
self.code = code
self.message = message
self.details = details
}
var localizedDescription: String {
return
"PigeonError(code: \(code), message: \(message ?? "<nil>"), details: \(details ?? "<nil>")"
}
}
private func wrapResult(_ result: Any?) -> [Any?] {
return [result]
}
private func wrapError(_ error: Any) -> [Any?] {
if let pigeonError = error as? PigeonError {
return [
pigeonError.code,
pigeonError.message,
pigeonError.details,
]
}
if let flutterError = error as? FlutterError {
return [
flutterError.code,
flutterError.message,
flutterError.details,
]
}
return [
"\(error)",
"\(type(of: error))",
"Stacktrace: \(Thread.callStackSymbols)",
]
}
private func isNullish(_ value: Any?) -> Bool {
return value is NSNull || value == nil
}
private func nilOrValue<T>(_ value: Any?) -> T? {
if value is NSNull { return nil }
return value as! T?
}
func deepEqualsmessages(_ lhs: Any?, _ rhs: Any?) -> Bool {
let cleanLhs = nilOrValue(lhs) as Any?
let cleanRhs = nilOrValue(rhs) as Any?
switch (cleanLhs, cleanRhs) {
case (nil, nil):
return true
case (nil, _), (_, nil):
return false
case is (Void, Void):
return true
case let (cleanLhsHashable, cleanRhsHashable) as (AnyHashable, AnyHashable):
return cleanLhsHashable == cleanRhsHashable
case let (cleanLhsArray, cleanRhsArray) as ([Any?], [Any?]):
guard cleanLhsArray.count == cleanRhsArray.count else { return false }
for (index, element) in cleanLhsArray.enumerated() {
if !deepEqualsmessages(element, cleanRhsArray[index]) {
return false
}
}
return true
case let (cleanLhsDictionary, cleanRhsDictionary) as ([AnyHashable: Any?], [AnyHashable: Any?]):
guard cleanLhsDictionary.count == cleanRhsDictionary.count else { return false }
for (key, cleanLhsValue) in cleanLhsDictionary {
guard cleanRhsDictionary.index(forKey: key) != nil else { return false }
if !deepEqualsmessages(cleanLhsValue, cleanRhsDictionary[key]!) {
return false
}
}
return true
default:
// Any other type shouldn't be able to be used with pigeon. File an issue if you find this to be untrue.
return false
}
}
func deepHashmessages(value: Any?, hasher: inout Hasher) {
if let valueList = value as? [AnyHashable] {
for item in valueList { deepHashmessages(value: item, hasher: &hasher) }
return
}
if let valueDict = value as? [AnyHashable: AnyHashable] {
for key in valueDict.keys {
hasher.combine(key)
deepHashmessages(value: valueDict[key]!, hasher: &hasher)
}
return
}
if let hashableValue = value as? AnyHashable {
hasher.combine(hashableValue.hashValue)
}
return hasher.combine(String(describing: value))
}
/// Generated class from Pigeon that represents data sent in messages.
struct SharedPreferencesPigeonOptions: Hashable {
var suiteName: String? = nil
// swift-format-ignore: AlwaysUseLowerCamelCase
static func fromList(_ pigeonVar_list: [Any?]) -> SharedPreferencesPigeonOptions? {
let suiteName: String? = nilOrValue(pigeonVar_list[0])
return SharedPreferencesPigeonOptions(
suiteName: suiteName
)
}
func toList() -> [Any?] {
return [
suiteName
]
}
static func == (lhs: SharedPreferencesPigeonOptions, rhs: SharedPreferencesPigeonOptions) -> Bool
{
return deepEqualsmessages(lhs.toList(), rhs.toList())
}
func hash(into hasher: inout Hasher) {
deepHashmessages(value: toList(), hasher: &hasher)
}
}
private class MessagesPigeonCodecReader: FlutterStandardReader {
override func readValue(ofType type: UInt8) -> Any? {
switch type {
case 129:
return SharedPreferencesPigeonOptions.fromList(self.readValue() as! [Any?])
default:
return super.readValue(ofType: type)
}
}
}
private class MessagesPigeonCodecWriter: FlutterStandardWriter {
override func writeValue(_ value: Any) {
if let value = value as? SharedPreferencesPigeonOptions {
super.writeByte(129)
super.writeValue(value.toList())
} else {
super.writeValue(value)
}
}
}
private class MessagesPigeonCodecReaderWriter: FlutterStandardReaderWriter {
override func reader(with data: Data) -> FlutterStandardReader {
return MessagesPigeonCodecReader(data: data)
}
override func writer(with data: NSMutableData) -> FlutterStandardWriter {
return MessagesPigeonCodecWriter(data: data)
}
}
class MessagesPigeonCodec: FlutterStandardMessageCodec, @unchecked Sendable {
static let shared = MessagesPigeonCodec(readerWriter: MessagesPigeonCodecReaderWriter())
}
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
protocol LegacyUserDefaultsApi {
func remove(key: String) throws
func setBool(key: String, value: Bool) throws
func setDouble(key: String, value: Double) throws
func setValue(key: String, value: Any) throws
func getAll(prefix: String, allowList: [String]?) throws -> [String: Any]
func clear(prefix: String, allowList: [String]?) throws -> Bool
}
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
class LegacyUserDefaultsApiSetup {
static var codec: FlutterStandardMessageCodec { MessagesPigeonCodec.shared }
/// Sets up an instance of `LegacyUserDefaultsApi` to handle messages through the `binaryMessenger`.
static func setUp(
binaryMessenger: FlutterBinaryMessenger, api: LegacyUserDefaultsApi?,
messageChannelSuffix: String = ""
) {
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
let removeChannel = FlutterBasicMessageChannel(
name:
"dev.flutter.pigeon.shared_preferences_foundation.LegacyUserDefaultsApi.remove\(channelSuffix)",
binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
removeChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let keyArg = args[0] as! String
do {
try api.remove(key: keyArg)
reply(wrapResult(nil))
} catch {
reply(wrapError(error))
}
}
} else {
removeChannel.setMessageHandler(nil)
}
let setBoolChannel = FlutterBasicMessageChannel(
name:
"dev.flutter.pigeon.shared_preferences_foundation.LegacyUserDefaultsApi.setBool\(channelSuffix)",
binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
setBoolChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let keyArg = args[0] as! String
let valueArg = args[1] as! Bool
do {
try api.setBool(key: keyArg, value: valueArg)
reply(wrapResult(nil))
} catch {
reply(wrapError(error))
}
}
} else {
setBoolChannel.setMessageHandler(nil)
}
let setDoubleChannel = FlutterBasicMessageChannel(
name:
"dev.flutter.pigeon.shared_preferences_foundation.LegacyUserDefaultsApi.setDouble\(channelSuffix)",
binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
setDoubleChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let keyArg = args[0] as! String
let valueArg = args[1] as! Double
do {
try api.setDouble(key: keyArg, value: valueArg)
reply(wrapResult(nil))
} catch {
reply(wrapError(error))
}
}
} else {
setDoubleChannel.setMessageHandler(nil)
}
let setValueChannel = FlutterBasicMessageChannel(
name:
"dev.flutter.pigeon.shared_preferences_foundation.LegacyUserDefaultsApi.setValue\(channelSuffix)",
binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
setValueChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let keyArg = args[0] as! String
let valueArg = args[1]!
do {
try api.setValue(key: keyArg, value: valueArg)
reply(wrapResult(nil))
} catch {
reply(wrapError(error))
}
}
} else {
setValueChannel.setMessageHandler(nil)
}
let getAllChannel = FlutterBasicMessageChannel(
name:
"dev.flutter.pigeon.shared_preferences_foundation.LegacyUserDefaultsApi.getAll\(channelSuffix)",
binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
getAllChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let prefixArg = args[0] as! String
let allowListArg: [String]? = nilOrValue(args[1])
do {
let result = try api.getAll(prefix: prefixArg, allowList: allowListArg)
reply(wrapResult(result))
} catch {
reply(wrapError(error))
}
}
} else {
getAllChannel.setMessageHandler(nil)
}
let clearChannel = FlutterBasicMessageChannel(
name:
"dev.flutter.pigeon.shared_preferences_foundation.LegacyUserDefaultsApi.clear\(channelSuffix)",
binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
clearChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let prefixArg = args[0] as! String
let allowListArg: [String]? = nilOrValue(args[1])
do {
let result = try api.clear(prefix: prefixArg, allowList: allowListArg)
reply(wrapResult(result))
} catch {
reply(wrapError(error))
}
}
} else {
clearChannel.setMessageHandler(nil)
}
}
}
/// Generated protocol from Pigeon that represents a handler of messages from Flutter.
protocol UserDefaultsApi {
/// Adds property to shared preferences data set of type String.
func set(key: String, value: Any, options: SharedPreferencesPigeonOptions) throws
/// Removes all properties from shared preferences data set with matching prefix.
func clear(allowList: [String]?, options: SharedPreferencesPigeonOptions) throws
/// Gets all properties from shared preferences data set with matching prefix.
func getAll(allowList: [String]?, options: SharedPreferencesPigeonOptions) throws -> [String: Any]
/// Gets individual value stored with [key], if any.
func getValue(key: String, options: SharedPreferencesPigeonOptions) throws -> Any?
/// Gets all properties from shared preferences data set with matching prefix.
func getKeys(allowList: [String]?, options: SharedPreferencesPigeonOptions) throws -> [String]
}
/// Generated setup class from Pigeon to handle messages through the `binaryMessenger`.
class UserDefaultsApiSetup {
static var codec: FlutterStandardMessageCodec { MessagesPigeonCodec.shared }
/// Sets up an instance of `UserDefaultsApi` to handle messages through the `binaryMessenger`.
static func setUp(
binaryMessenger: FlutterBinaryMessenger, api: UserDefaultsApi?,
messageChannelSuffix: String = ""
) {
let channelSuffix = messageChannelSuffix.count > 0 ? ".\(messageChannelSuffix)" : ""
/// Adds property to shared preferences data set of type String.
let setChannel = FlutterBasicMessageChannel(
name: "dev.flutter.pigeon.shared_preferences_foundation.UserDefaultsApi.set\(channelSuffix)",
binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
setChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let keyArg = args[0] as! String
let valueArg = args[1]!
let optionsArg = args[2] as! SharedPreferencesPigeonOptions
do {
try api.set(key: keyArg, value: valueArg, options: optionsArg)
reply(wrapResult(nil))
} catch {
reply(wrapError(error))
}
}
} else {
setChannel.setMessageHandler(nil)
}
/// Removes all properties from shared preferences data set with matching prefix.
let clearChannel = FlutterBasicMessageChannel(
name:
"dev.flutter.pigeon.shared_preferences_foundation.UserDefaultsApi.clear\(channelSuffix)",
binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
clearChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let allowListArg: [String]? = nilOrValue(args[0])
let optionsArg = args[1] as! SharedPreferencesPigeonOptions
do {
try api.clear(allowList: allowListArg, options: optionsArg)
reply(wrapResult(nil))
} catch {
reply(wrapError(error))
}
}
} else {
clearChannel.setMessageHandler(nil)
}
/// Gets all properties from shared preferences data set with matching prefix.
let getAllChannel = FlutterBasicMessageChannel(
name:
"dev.flutter.pigeon.shared_preferences_foundation.UserDefaultsApi.getAll\(channelSuffix)",
binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
getAllChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let allowListArg: [String]? = nilOrValue(args[0])
let optionsArg = args[1] as! SharedPreferencesPigeonOptions
do {
let result = try api.getAll(allowList: allowListArg, options: optionsArg)
reply(wrapResult(result))
} catch {
reply(wrapError(error))
}
}
} else {
getAllChannel.setMessageHandler(nil)
}
/// Gets individual value stored with [key], if any.
let getValueChannel = FlutterBasicMessageChannel(
name:
"dev.flutter.pigeon.shared_preferences_foundation.UserDefaultsApi.getValue\(channelSuffix)",
binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
getValueChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let keyArg = args[0] as! String
let optionsArg = args[1] as! SharedPreferencesPigeonOptions
do {
let result = try api.getValue(key: keyArg, options: optionsArg)
reply(wrapResult(result))
} catch {
reply(wrapError(error))
}
}
} else {
getValueChannel.setMessageHandler(nil)
}
/// Gets all properties from shared preferences data set with matching prefix.
let getKeysChannel = FlutterBasicMessageChannel(
name:
"dev.flutter.pigeon.shared_preferences_foundation.UserDefaultsApi.getKeys\(channelSuffix)",
binaryMessenger: binaryMessenger, codec: codec)
if let api = api {
getKeysChannel.setMessageHandler { message, reply in
let args = message as! [Any?]
let allowListArg: [String]? = nilOrValue(args[0])
let optionsArg = args[1] as! SharedPreferencesPigeonOptions
do {
let result = try api.getKeys(allowList: allowListArg, options: optionsArg)
reply(wrapResult(result))
} catch {
reply(wrapError(error))
}
}
} else {
getKeysChannel.setMessageHandler(nil)
}
}
}
@@ -0,0 +1,146 @@
import GameController
/// Maps GCController elements to W3C Standard Gamepad indices.
///
/// W3C Standard Gamepad button mapping:
/// 0 = A (South) 8 = Back/View
/// 1 = B (East) 9 = Start/Menu
/// 2 = X (West) 10 = Left Stick Button
/// 3 = Y (North) 11 = Right Stick Button
/// 4 = Left Shoulder 12 = D-pad Up
/// 5 = Right Shoulder 13 = D-pad Down
/// 6 = Left Trigger 14 = D-pad Left
/// 7 = Right Trigger 15 = D-pad Right
/// 16 = Guide/Home
///
/// W3C Standard Gamepad axis mapping:
/// 0 = Left Stick X (-1 left, +1 right)
/// 1 = Left Stick Y (-1 up, +1 down)
/// 2 = Right Stick X (-1 left, +1 right)
/// 3 = Right Stick Y (-1 up, +1 down)
///
/// Note: GCController reports Y-axis with up = +1, which is the opposite of
/// the W3C convention (up = -1). The axis mapping inverts Y values.
enum ButtonMapping {
/// Returns the W3C button index and the corresponding `GCControllerButtonInput`
/// if `element` matches a known button in `gamepad`.
static func buttonIndex(for element: GCControllerElement,
in gamepad: GCExtendedGamepad) -> (Int, GCControllerButtonInput)? {
// Face buttons
if element == gamepad.buttonA {
return (0, gamepad.buttonA)
}
if element == gamepad.buttonB {
return (1, gamepad.buttonB)
}
if element == gamepad.buttonX {
return (2, gamepad.buttonX)
}
if element == gamepad.buttonY {
return (3, gamepad.buttonY)
}
// Shoulders
if element == gamepad.leftShoulder {
return (4, gamepad.leftShoulder)
}
if element == gamepad.rightShoulder {
return (5, gamepad.rightShoulder)
}
// Triggers
if element == gamepad.leftTrigger {
return (6, gamepad.leftTrigger)
}
if element == gamepad.rightTrigger {
return (7, gamepad.rightTrigger)
}
// Menu buttons
if let buttonOptions = gamepad.buttonOptions, element == buttonOptions {
return (8, buttonOptions)
}
if element == gamepad.buttonMenu {
return (9, gamepad.buttonMenu)
}
// Thumbstick buttons
if let leftThumbstickButton = gamepad.leftThumbstickButton, element == leftThumbstickButton {
return (10, leftThumbstickButton)
}
if let rightThumbstickButton = gamepad.rightThumbstickButton, element == rightThumbstickButton {
return (11, rightThumbstickButton)
}
// D-pad individual buttons
if element == gamepad.dpad.up {
return (12, gamepad.dpad.up)
}
if element == gamepad.dpad.down {
return (13, gamepad.dpad.down)
}
if element == gamepad.dpad.left {
return (14, gamepad.dpad.left)
}
if element == gamepad.dpad.right {
return (15, gamepad.dpad.right)
}
// Guide / Home button
if let buttonHome = gamepad.buttonHome, element == buttonHome {
return (16, buttonHome)
}
return nil
}
/// Returns all W3C axis events for the given `element`.
///
/// The `valueChangedHandler` on `GCExtendedGamepad` may fire with the
/// entire `GCControllerDirectionPad` (thumbstick) as the element, or with
/// an individual axis sub-element. When the full thumbstick fires we
/// return both X and Y axis entries. When a single axis fires we return
/// one entry.
///
/// Y-axis values are inverted: GCController uses +1 for up, but the W3C
/// standard uses -1 for up.
static func axisIndices(for element: GCControllerElement,
in gamepad: GCExtendedGamepad) -> [(Int, Double)] {
// Left stick (full thumbstick element)
if element == gamepad.leftThumbstick {
return [
(0, Double(gamepad.leftThumbstick.xAxis.value)),
(1, Double(-gamepad.leftThumbstick.yAxis.value)),
]
}
// Left stick individual axes
if element == gamepad.leftThumbstick.xAxis {
return [(0, Double(gamepad.leftThumbstick.xAxis.value))]
}
if element == gamepad.leftThumbstick.yAxis {
// Invert Y: GC up is +1, W3C up is -1
return [(1, Double(-gamepad.leftThumbstick.yAxis.value))]
}
// Right stick (full thumbstick element)
if element == gamepad.rightThumbstick {
return [
(2, Double(gamepad.rightThumbstick.xAxis.value)),
(3, Double(-gamepad.rightThumbstick.yAxis.value)),
]
}
// Right stick individual axes
if element == gamepad.rightThumbstick.xAxis {
return [(2, Double(gamepad.rightThumbstick.xAxis.value))]
}
if element == gamepad.rightThumbstick.yAxis {
// Invert Y: GC up is +1, W3C up is -1
return [(3, Double(-gamepad.rightThumbstick.yAxis.value))]
}
return []
}
}
@@ -0,0 +1,244 @@
import GameController
/// Manages GCController connections and input, translating events into the
/// compact wire-format arrays expected by the Dart side.
class GCControllerManager {
// MARK: - Properties
private let streamHandler: GamepadStreamHandler
/// Maps a GCController instance to its stable int gamepad ID.
private var controllerIds: [ObjectIdentifier: Int] = [:]
/// Counter for generating unique IDs.
private var nextId: Int = 0
/// Notification observers so we can remove them on dispose.
private var connectObserver: NSObjectProtocol?
private var disconnectObserver: NSObjectProtocol?
// MARK: - Init
init(streamHandler: GamepadStreamHandler) {
self.streamHandler = streamHandler
}
// MARK: - Lifecycle
/// Begin listening for GCController connect/disconnect notifications and
/// register input handlers on any controllers that are already connected.
func startObserving() {
connectObserver = NotificationCenter.default.addObserver(
forName: .GCControllerDidConnect,
object: nil,
queue: .main
) { [weak self] notification in
guard let self = self,
let controller = notification.object as? GCController else { return }
self.controllerDidConnect(controller)
}
disconnectObserver = NotificationCenter.default.addObserver(
forName: .GCControllerDidDisconnect,
object: nil,
queue: .main
) { [weak self] notification in
guard let self = self,
let controller = notification.object as? GCController else { return }
self.controllerDidDisconnect(controller)
}
// Handle controllers that were already connected before we started
// observing (e.g. wired controllers).
for controller in GCController.controllers() {
controllerDidConnect(controller)
}
}
/// Remove all observers and clear input handlers.
func stopObserving() {
if let observer = connectObserver {
NotificationCenter.default.removeObserver(observer)
connectObserver = nil
}
if let observer = disconnectObserver {
NotificationCenter.default.removeObserver(observer)
disconnectObserver = nil
}
// Remove value changed handlers from all tracked controllers.
for controller in GCController.controllers() {
controller.extendedGamepad?.valueChangedHandler = nil
}
controllerIds.removeAll()
}
// MARK: - listGamepads
/// Returns an array of dictionaries describing currently connected
/// controllers, suitable for returning over a MethodChannel.
func listGamepads() -> [[String: Any]] {
var result: [[String: Any]] = []
for controller in GCController.controllers() {
let key = ObjectIdentifier(controller)
guard let gamepadId = controllerIds[key] else { continue }
var info: [String: Any] = [
"id": gamepadId,
"name": controllerName(controller),
]
if let vendorId = vendorId(for: controller) {
info["vendorId"] = vendorId
}
if let productId = productId(for: controller) {
info["productId"] = productId
}
result.append(info)
}
return result
}
// MARK: - Connection events
private func controllerDidConnect(_ controller: GCController) {
let gamepadId = assignId(for: controller)
// Register input handlers for the extended gamepad profile.
registerInputHandlers(for: controller, gamepadId: gamepadId)
// Wire format: [0, gamepadId, timestamp, connected, name, vendorId, productId]
let event: [Any] = [
0,
gamepadId,
currentTimestamp(),
true,
controllerName(controller),
vendorId(for: controller) as Any,
productId(for: controller) as Any,
]
streamHandler.send(event: event)
}
private func controllerDidDisconnect(_ controller: GCController) {
let key = ObjectIdentifier(controller)
guard let gamepadId = controllerIds.removeValue(forKey: key) else { return }
controller.extendedGamepad?.valueChangedHandler = nil
// Wire format: [0, gamepadId, timestamp, connected, name, vendorId, productId]
let event: [Any] = [
0,
gamepadId,
currentTimestamp(),
false,
controllerName(controller),
vendorId(for: controller) as Any,
productId(for: controller) as Any,
]
streamHandler.send(event: event)
}
// MARK: - Input handlers
private func registerInputHandlers(for controller: GCController, gamepadId: Int) {
guard let extendedGamepad = controller.extendedGamepad else { return }
extendedGamepad.valueChangedHandler = { [weak self] (gamepad, element) in
guard let self = self else { return }
self.handleValueChanged(gamepad: gamepad, element: element, gamepadId: gamepadId)
}
}
private func handleValueChanged(gamepad: GCExtendedGamepad,
element: GCControllerElement,
gamepadId: Int) {
let timestamp = currentTimestamp()
// D-pad fires as a single GCControllerDirectionPad element, not as
// individual direction buttons. Expand it into 4 button events.
if element === gamepad.dpad {
sendButtonEvent(gamepadId: gamepadId, index: 12,
pressed: gamepad.dpad.up.isPressed,
value: Double(gamepad.dpad.up.value), timestamp: timestamp)
sendButtonEvent(gamepadId: gamepadId, index: 13,
pressed: gamepad.dpad.down.isPressed,
value: Double(gamepad.dpad.down.value), timestamp: timestamp)
sendButtonEvent(gamepadId: gamepadId, index: 14,
pressed: gamepad.dpad.left.isPressed,
value: Double(gamepad.dpad.left.value), timestamp: timestamp)
sendButtonEvent(gamepadId: gamepadId, index: 15,
pressed: gamepad.dpad.right.isPressed,
value: Double(gamepad.dpad.right.value), timestamp: timestamp)
return
}
// Check buttons.
if let (index, button) = ButtonMapping.buttonIndex(for: element, in: gamepad) {
sendButtonEvent(gamepadId: gamepadId, index: index,
pressed: button.isPressed,
value: Double(button.value), timestamp: timestamp)
return
}
// Check axes (may produce multiple events when a full thumbstick fires).
let axisEvents = ButtonMapping.axisIndices(for: element, in: gamepad)
for (index, axisValue) in axisEvents {
// Wire format: [2, gamepadId, timestamp, axisIndex, value]
streamHandler.send(event: [2, gamepadId, timestamp, index, axisValue])
}
}
private func sendButtonEvent(gamepadId: Int, index: Int,
pressed: Bool, value: Double, timestamp: Int) {
// Wire format: [1, gamepadId, timestamp, buttonIndex, pressed, value]
streamHandler.send(event: [1, gamepadId, timestamp, index, pressed, value])
}
// MARK: - Helpers
private func assignId(for controller: GCController) -> Int {
let key = ObjectIdentifier(controller)
if let existing = controllerIds[key] {
return existing
}
let id = nextId
nextId += 1
controllerIds[key] = id
return id
}
private func controllerName(_ controller: GCController) -> String {
let category = controller.productCategory
if !category.isEmpty {
return category
}
return controller.vendorName ?? "Unknown Controller"
}
/// Returns the USB vendor ID for the controller, if available.
///
/// GCController does not directly expose numeric vendor/product IDs
/// through the public API. We attempt to obtain them from the
/// underlying physical device description on iOS 16+.
private func vendorId(for controller: GCController) -> Int? {
// GCController does not publicly expose numeric vendor IDs on iOS.
// Return nil; the Dart side treats this as optional.
return nil
}
/// Returns the USB product ID for the controller, if available.
private func productId(for controller: GCController) -> Int? {
// GCController does not publicly expose numeric product IDs on iOS.
// Return nil; the Dart side treats this as optional.
return nil
}
/// Returns the current timestamp in milliseconds since epoch.
private func currentTimestamp() -> Int {
return Int(Date().timeIntervalSince1970 * 1000)
}
}
@@ -0,0 +1,61 @@
import Flutter
import UIKit
/// Entry point for the gamepad iOS plugin.
///
/// Registers a MethodChannel (`dev.universal_gamepad/methods`) and an
/// EventChannel (`dev.universal_gamepad/events`) with the Flutter engine.
public class GamepadPlugin: NSObject, FlutterPlugin {
private let controllerManager: GCControllerManager
private let streamHandler: GamepadStreamHandler
init(controllerManager: GCControllerManager, streamHandler: GamepadStreamHandler) {
self.controllerManager = controllerManager
self.streamHandler = streamHandler
super.init()
}
// MARK: - FlutterPlugin
public static func register(with registrar: FlutterPluginRegistrar) {
let streamHandler = GamepadStreamHandler()
let controllerManager = GCControllerManager(streamHandler: streamHandler)
let methodChannel = FlutterMethodChannel(
name: "dev.universal_gamepad/methods",
binaryMessenger: registrar.messenger()
)
let eventChannel = FlutterEventChannel(
name: "dev.universal_gamepad/events",
binaryMessenger: registrar.messenger()
)
let instance = GamepadPlugin(
controllerManager: controllerManager,
streamHandler: streamHandler
)
registrar.addMethodCallDelegate(instance, channel: methodChannel)
eventChannel.setStreamHandler(streamHandler)
// Start observing controllers immediately so we capture connections
// that happen before the Dart side listens.
controllerManager.startObserving()
}
// MARK: - MethodChannel
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
switch call.method {
case "listGamepads":
result(controllerManager.listGamepads())
case "dispose":
controllerManager.stopObserving()
result(nil)
default:
result(FlutterMethodNotImplemented)
}
}
}
@@ -0,0 +1,62 @@
import Flutter
/// FlutterStreamHandler that forwards native gamepad events to Dart.
///
/// Events are queued if no sink is attached yet; once a listener subscribes
/// all queued events are flushed immediately.
public class GamepadStreamHandler: NSObject, FlutterStreamHandler {
/// The active event sink provided by Flutter's EventChannel.
private var eventSink: FlutterEventSink?
/// Events received before the Dart side starts listening.
private var pendingEvents: [[Any]] = []
/// Thread-safety lock for sink / queue access.
private let lock = NSLock()
// MARK: - FlutterStreamHandler
public func onListen(withArguments arguments: Any?,
eventSink events: @escaping FlutterEventSink) -> FlutterError? {
lock.lock()
eventSink = events
// Flush any events that arrived before the listener was attached.
for event in pendingEvents {
events(event)
}
pendingEvents.removeAll()
lock.unlock()
return nil
}
public func onCancel(withArguments arguments: Any?) -> FlutterError? {
lock.lock()
eventSink = nil
lock.unlock()
return nil
}
// MARK: - Internal API
/// Sends a gamepad event array to Dart.
///
/// If the sink is not yet available the event is queued.
func send(event: [Any]) {
lock.lock()
if let sink = eventSink {
lock.unlock()
// Dispatch on the main thread to satisfy Flutter platform channel
// requirements.
DispatchQueue.main.async {
sink(event)
}
} else {
pendingEvents.append(event)
lock.unlock()
}
}
}
+1
View File
@@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"
+1
View File
@@ -0,0 +1 @@
3.41.6
+26
View File
@@ -0,0 +1,26 @@
<?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>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>13.0</string>
</dict>
</plist>
+81
View File
@@ -0,0 +1,81 @@
#!/usr/bin/env bash
# Exit on error
set -e
#determine ios folder
if [[ -d ./_ios ]]; then
IOS_DIR=./_ios
if [[ -d ./ios ]]; then
TVOS_DIR=./ios
else
echo "Error: could not find './ios' folder with tvos content"
exit 1
fi
elif [[ -d ./ios ]]; then
IOS_DIR=./ios
if [[ -d ./tvos ]]; then
TVOS_DIR=./tvos
else
echo "Error: could not find './tvos' folder with tvos content"
exit 1
fi
else
echo "Error: could not find './ios' or './_ios' folder"
exit 1
fi
IOS_FLUTTER_ASSETS_DIR=$IOS_DIR/Flutter/App.framework/flutter_assets
TVOS_FLUTTER_ASSETS_DIR=$TVOS_DIR/tvos_flutter_assets/flutter_assets
if [[ -d $IOS_FLUTTER_ASSETS_DIR ]]; then
echo "Copying flutter_assets from IOS compiled target"
# better approuch would to copy complete assets folder and remove files that are generated by compilation step
# isolate_snapshot_data, isolate_snapshot_instr, kernel_blob, vm_snapshot_data
# note, this list might be different for debug (jit) and release (aot)??????
rm -rf "$TVOS_FLUTTER_ASSETS_DIR"
mkdir -p "$TVOS_FLUTTER_ASSETS_DIR"
echo " └─App.framework/flutter_assets/AssetManifest.json"
cp -v "$IOS_FLUTTER_ASSETS_DIR/AssetManifest.json" "$TVOS_FLUTTER_ASSETS_DIR/AssetManifest.json"
echo " └─App.framework/flutter_assets/FontManifest.json"
cp -v "$IOS_FLUTTER_ASSETS_DIR/FontManifest.json" "$TVOS_FLUTTER_ASSETS_DIR/FontManifest.json"
set +e
# the content has changed from flutter v 1.15.19 to 1.19.0-4.3pre
echo " └─App.framework/flutter_assets/LICENSE"
cp -v "$IOS_FLUTTER_ASSETS_DIR/LICENSE" "$TVOS_FLUTTER_ASSETS_DIR/LICENSE"
echo " └─App.framework/flutter_assets/NOTICES"
cp -v "$IOS_FLUTTER_ASSETS_DIR/NOTICES" "$TVOS_FLUTTER_ASSETS_DIR/NOTICES"
echo " └─App.framework/flutter_assets/assets/"
mkdir -p "$TVOS_FLUTTER_ASSETS_DIR/assets"
cp -v -R "$IOS_FLUTTER_ASSETS_DIR/assets" "$TVOS_FLUTTER_ASSETS_DIR"
echo " └─App.framework/flutter_assets/fonts/"
mkdir -p "$TVOS_FLUTTER_ASSETS_DIR/fonts"
cp -v -R "$IOS_FLUTTER_ASSETS_DIR/fonts" "$TVOS_FLUTTER_ASSETS_DIR"
echo " └─App.framework/flutter_assets/packages/"
mkdir -p "$TVOS_FLUTTER_ASSETS_DIR/packages"
cp -v -R "$IOS_FLUTTER_ASSETS_DIR/packages" "$TVOS_FLUTTER_ASSETS_DIR"
set -e
echo " └─Done"
else
echo "'./ios/Flutter/App.framework/flutter_assets' folder does not not exist or IOS target not compiled yet, build any ios buiild first!"
exit 1
fi
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# based on scripts in /engine/src/flutter/testing/scenario_app
# and xcode_backend.sh script for integration in xcode
# Exit on error
set -e
if [[ $(uname -m) == "arm64" ]]; then
TARGET_POSTFIX='_arm64'
CLANG_POSTFIX='_arm64'
else
TARGET_POSTFIX=''
CLANG_POSTFIX='_X86'
fi
if [ -z "$FLUTTER_LOCAL_ENGINE" ]; then
echo " └─ERROR: FLUTTER_LOCAL_ENGINE not set!"
return 1;
fi
if [ "$1" == "release" ]; then
echo "Coping Flutter.framework (release)..."
DEVICE_TOOLS=$FLUTTER_LOCAL_ENGINE/out/ios_release$TARGET_POSTFIX
elif [[ "$1" == "debug_sim" ]] ; then
echo "Coping Flutter.framework (debug-simulator)..."
DEVICE_TOOLS=$FLUTTER_LOCAL_ENGINE/out/ios_debug_sim_unopt$TARGET_POSTFIX
else
#debug
echo "Coping Flutter.framework (debug)..."
DEVICE_TOOLS=$FLUTTER_LOCAL_ENGINE/out/ios_debug_unopt$TARGET_POSTFIX
fi
OUTDIR=$PWD/Flutter
rm -rf "$OUTDIR/Flutter.framework"
cp -R "$DEVICE_TOOLS/Flutter.framework" "$OUTDIR"
+101
View File
@@ -0,0 +1,101 @@
#!/usr/bin/env bash
# Download the prebuilt flutter-tvos engine tarball from GitHub Releases,
# extract it into a shared cache, and write tvos/Flutter/Generated.xcconfig
# so Xcode picks it up via FLUTTER_LOCAL_ENGINE.
#
# Reads the engine version from tvos/engine.version. Re-runs are cheap —
# skips download if the cache already has the matching version.
#
# Usage:
# tvos/scripts/fetch_engine.sh
#
# Env overrides:
# FLUTTER_TVOS_ENGINE_CACHE — root dir for cached engines (default: ~/.cache/flutter-tvos-engine)
# FLUTTER_TVOS_RELEASES_URL — base URL for release tarballs (default: github.com/edde746/flutter-tvos)
set -euo pipefail
TVOS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
REPO_ROOT="$(cd "${TVOS_DIR}/.." && pwd)"
VERSION_FILE="${TVOS_DIR}/engine.version"
if [[ ! -f "$VERSION_FILE" ]]; then
echo "error: $VERSION_FILE missing" >&2
exit 1
fi
VERSION="$(tr -d '[:space:]' < "$VERSION_FILE")"
if [[ -z "$VERSION" ]]; then
echo "error: tvos/engine.version is empty" >&2
exit 1
fi
CACHE_ROOT="${FLUTTER_TVOS_ENGINE_CACHE:-$HOME/.cache/flutter-tvos-engine}"
RELEASES_URL="${FLUTTER_TVOS_RELEASES_URL:-https://github.com/edde746/flutter-tvos}"
ENGINE_DIR="${CACHE_ROOT}/v${VERSION}"
TARBALL_URL="${RELEASES_URL}/releases/download/v${VERSION}/flutter-tvos-${VERSION}.tar.gz"
STAMP="${ENGINE_DIR}/.installed-${VERSION}"
if [[ ! -f "$STAMP" ]]; then
echo "[fetch_engine] downloading ${TARBALL_URL}"
mkdir -p "$ENGINE_DIR"
TMP_TAR="$(mktemp -t flutter-tvos-engine.XXXXXX.tar.gz)"
trap 'rm -f "$TMP_TAR"' EXIT
curl -fL --progress-bar -o "$TMP_TAR" "$TARBALL_URL"
echo "[fetch_engine] extracting to $ENGINE_DIR"
tar -xzf "$TMP_TAR" -C "$ENGINE_DIR"
touch "$STAMP"
rm -f "$TMP_TAR"
trap - EXIT
else
echo "[fetch_engine] using cached engine at $ENGINE_DIR"
fi
# Locate a host Flutter SDK for flutter CLI invocation during the build.
if [[ -n "${FLUTTER_ROOT:-}" ]]; then
FLUTTER_ROOT_RESOLVED="$FLUTTER_ROOT"
elif command -v flutter >/dev/null 2>&1; then
FLUTTER_ROOT_RESOLVED="$(dirname "$(dirname "$(command -v flutter)")")"
else
echo "error: couldn't find Flutter SDK — set FLUTTER_ROOT or put flutter on PATH" >&2
exit 1
fi
# Read version name/number from pubspec so Generated.xcconfig matches the app.
PUBSPEC="${REPO_ROOT}/pubspec.yaml"
PUB_NAME=""
PUB_NUMBER=""
if [[ -f "$PUBSPEC" ]]; then
VER_LINE="$(awk '/^version:/ {print $2; exit}' "$PUBSPEC" | tr -d '"')"
if [[ -n "$VER_LINE" ]]; then
PUB_NAME="${VER_LINE%+*}"
if [[ "$VER_LINE" == *+* ]]; then
PUB_NUMBER="${VER_LINE#*+}"
fi
fi
fi
PUB_NAME="${PUB_NAME:-1.0.0}"
PUB_NUMBER="${PUB_NUMBER:-1}"
# tvOS deployment target is hard-coded at the Podfile / project level to 14.0.
# Write Generated.xcconfig so Xcode (and the Run Script phase) see the engine.
GEN_XC="${TVOS_DIR}/Flutter/Generated.xcconfig"
mkdir -p "$(dirname "$GEN_XC")"
cat > "$GEN_XC" <<EOF
FLUTTER_ROOT=${FLUTTER_ROOT_RESOLVED}
FLUTTER_APPLICATION_PATH=${REPO_ROOT}
FLUTTER_TARGET=lib/main.dart
FLUTTER_BUILD_NAME=${PUB_NAME}
FLUTTER_BUILD_NUMBER=${PUB_NUMBER}
TVOS_DEPLOYMENT_TARGET=14.0
FLUTTER_LOCAL_ENGINE=${ENGINE_DIR}
PODS_ROOT=${TVOS_DIR}/Pods
EOF
echo
echo "Engine ready. Wrote $GEN_XC with:"
echo " FLUTTER_LOCAL_ENGINE=$ENGINE_DIR"
echo " FLUTTER_BUILD_NAME=$PUB_NAME"
echo " FLUTTER_BUILD_NUMBER=$PUB_NUMBER"
echo
echo "Next: open tvos/Runner.xcworkspace in Xcode and build."
+58
View File
@@ -0,0 +1,58 @@
#!/bin/bash
set -e
RED='\033[0;31m'
NOCOLOR='\033[0m'
if [[ $(uname -m) == "arm64" ]]; then
echo "Host: arm64"
GN_SIM_ARGS="--simulator-cpu=arm64"
GN_ARGS="--mac-cpu=arm64"
GN_ARGS_HOST="--mac-cpu=arm64 "
OUTPUT_POSTFIX="_arm64"
fi
if [[ "$1" == "clean" ]]; then
echo "Clean build ..."
rm -irf ./out/ios_debug_sim_unopt$OUTPUT_POSTFIX
rm -rf ./out/ios_debug_unopt$OUTPUT_POSTFIX
rm -rf ./out/ios_release$OUTPUT_POSTFIX
rm -rf ./out/host_debug_unopt$OUTPUT_POSTFIX
rm -rf ./out/host_release$OUTPUT_POSTFIX
fi
if [[ $(uname -m) != "arm64" ]]; then
echo "Host: x64"
GN_ARGS_HOST="--mac-cpu=x64"
OUTPUT_POSTFIX=""
fi
if [[ "$1" == "clean" ]] || [[ ! -d ./out/host_release$OUTPUT_POSTFIX ]]; then
./flutter/tools/gn --no-goma --no-lto --runtime-mode=release --no-prebuilt-dart-sdk $GN_ARGS_HOST
fi
ninja -C out/host_release$OUTPUT_POSTFIX
if [[ "$1" == "clean" ]] || [[ ! -d ./out/host_debug_unopt$OUTPUT_POSTFIX ]]; then
./flutter/tools/gn --no-goma --unoptimized --no-prebuilt-dart-sdk $GN_ARGS_HOST
fi
ninja -C out/host_debug_unopt$OUTPUT_POSTFIX
if [[ $(uname -m) != "arm64" ]]; then
GN_SIM_ARGS="--simulator-cpu=x64"
GN_ARGS="--mac-cpu=x64"
fi
if [[ "$1" == "clean" ]] || [[ ! -d ./out/ios_debug_sim_unopt$OUTPUT_POSTFIX ]]; then
./flutter/tools/gn --ios --no-goma --simulator --unoptimized $GN_SIM_ARGS
fi
ninja -C out/ios_debug_sim_unopt$OUTPUT_POSTFIX
if [[ "$1" == "clean" ]] || [[ ! -d ./out/ios_debug_unopt$OUTPUT_POSTFIX ]]; then
./flutter/tools/gn --ios --no-goma --unoptimized $GN_ARGS
fi
ninja -C out/ios_debug_unopt$OUTPUT_POSTFIX
if [[ "$1" == "clean" ]] || [[ ! -d ./out/ios_release$OUTPUT_POSTFIX ]]; then
./flutter/tools/gn --ios --no-goma --runtime-mode=release $GN_ARGS
fi
ninja -C out/ios_release$OUTPUT_POSTFIX
+18
View File
@@ -0,0 +1,18 @@
#!/bin/bash
set -e
# Note: sed tool only works when going in to the actual folder of the package, specifying the direct full path to the project.pbxproj does not work!
cd Pods/Pods.xcodeproj
echo "pathching $PWD/project.pbxproj ..."
find . -name 'project.pbxproj' -print0 | xargs -0 sed -i '' -e 's/TARGETED_DEVICE_FAMILY[[:space:]]=[[:space:]]\"1,2\"/TARGETED_DEVICE_FAMILY = \"3\"/g'
find . -name 'project.pbxproj' -print0 | xargs -0 sed -i '' -e 's/SDKROOT[[:space:]]=[[:space:]]iphoneos/SDKROOT = appletvos/g'
find . -name 'project.pbxproj' -print0 | xargs -0 sed -i '' -e 's/SUPPORTED_PLATFORMS[[:space:]]=[[:space:]]iphoneos/SUPPORTED_PLATFORMS = appletvos/g'
find . -name 'project.pbxproj' -print0 | xargs -0 sed -i '' -e 's/IPHONEOS_DEPLOYMENT_TARGET[[:space:]]=[[:space:]]9.0/TVOS_DEPLOYMENT_TARGET = 13.0/g'
# TODO: 8.0 --> should work with any version!
cd ../..
+18
View File
@@ -0,0 +1,18 @@
#!/bin/bash
set -e
# Note: sed tool only works when going in to the actual folder of the package, specifying the direct full path to the project.pbxproj does not work!
cd Runner.xcodeproj
echo "pathching $PWD/project.pbxproj ..."
find . -name 'project.pbxproj' -print0 | xargs -0 sed -i '' -e 's/TARGETED_DEVICE_FAMILY[[:space:]]=[[:space:]]\"1,2\"/TARGETED_DEVICE_FAMILY = \"3\"/g'
find . -name 'project.pbxproj' -print0 | xargs -0 sed -i '' -e 's/SDKROOT[[:space:]]=[[:space:]]iphoneos/SDKROOT = appletvos/g'
find . -name 'project.pbxproj' -print0 | xargs -0 sed -i '' -e 's/SUPPORTED_PLATFORMS[[:space:]]=[[:space:]]iphoneos/SUPPORTED_PLATFORMS = appletvos/g'
find . -name 'project.pbxproj' -print0 | xargs -0 sed -i '' -e 's/IPHONEOS_DEPLOYMENT_TARGET[[:space:]]=[[:space:]][0-9].[0-9]/TVOS_DEPLOYMENT_TARGET = 13.0/g'
# TODO: 8.0 --> should work with any version!
cd ..
+29
View File
@@ -0,0 +1,29 @@
#!/bin/bash
set -e
if [ "$1" == "ios" ]; then
if [[ ! -d ./tvos ]] && [[ -d ./_ios ]] && [[ -d ./ios ]]; then
echo "Renaming 'ios' to 'tvos'"
mv ./ios ./tvos
mv ./_ios ./ios
else
echo "'tvos' already exists or '_ios'/'ios' does not exist"
exit 1
fi
elif [[ "$1" == "tvos" ]] ; then
if [[ ! -d ./_ios ]] && [[ -d ./tvos ]] && [[ -d ./ios ]]; then
echo "Renaming 'tvos' to 'ios'"
mv ./ios ./_ios
mv ./tvos ./ios
else
echo "'_ios' already exists or 'tvos'/'ios' does not exist"
exit 1
fi
else
echo "invalid arguments:"
echo " Usage: $0 ios or $0 tvos"
exit 1
fi
+70
View File
@@ -0,0 +1,70 @@
#!/usr/bin/env ruby
# Adds the Plezy MpvPlayer Swift sources and the MPVKit Swift Package
# dependency to tvos/Runner.xcodeproj so it matches the iOS project's
# linkage. Idempotent: re-running skips already-added entries.
require 'xcodeproj'
PROJECT_PATH = File.expand_path('../Runner.xcodeproj', __dir__)
project = Xcodeproj::Project.open(PROJECT_PATH)
runner_target = project.targets.find { |t| t.name == 'Runner' }
raise "Runner target not found" unless runner_target
# Find or create the MpvPlayer group under Runner.
main_group = project.main_group['Runner']
raise "Runner group not found" unless main_group
mpv_group = main_group['MpvPlayer'] || main_group.new_group('MpvPlayer', 'Runner/MpvPlayer')
# File references.
# path is relative to the group's path (Runner/MpvPlayer → ../..).
# For files elsewhere in the repo, use SOURCE_ROOT with the absolute-ish path.
sources = [
{ name: 'MpvPlayerCoreBase.swift', path: '../shared/apple/MpvPlayer/MpvPlayerCoreBase.swift', tree: '<source_root>' },
{ name: 'MpvPlayerPluginShared.swift', path: '../shared/apple/MpvPlayer/MpvPlayerPluginShared.swift', tree: '<source_root>' },
{ name: 'MpvPlayerCore.swift', path: '../ios/Runner/MpvPlayer/MpvPlayerCore.swift', tree: '<source_root>' },
{ name: 'MpvPlayerPlugin.swift', path: '../ios/Runner/MpvPlayer/MpvPlayerPlugin.swift', tree: '<source_root>' },
{ name: 'MpvPipController.swift', path: '../ios/Runner/MpvPlayer/MpvPipController.swift', tree: '<source_root>' },
]
sources_phase = runner_target.source_build_phase
sources.each do |src|
existing = mpv_group.files.find { |f| f.display_name == src[:name] }
if existing
puts "[skip] #{src[:name]} already present"
next
end
ref = mpv_group.new_file(src[:path])
ref.name = src[:name]
ref.source_tree = src[:tree]
sources_phase.add_file_reference(ref, true)
puts "[add ] #{src[:name]}"
end
# Swift Package: MPVKit.
pkg_url = 'https://github.com/edde746/MPVKit'
existing_pkg = project.root_object.package_references.find do |p|
p.repositoryURL == pkg_url rescue false
end
if existing_pkg
puts "[skip] MPVKit SPM package already present"
else
pkg = project.new(Xcodeproj::Project::Object::XCRemoteSwiftPackageReference)
pkg.repositoryURL = pkg_url
pkg.requirement = { 'kind' => 'branch', 'branch' => 'main' }
project.root_object.package_references << pkg
product = project.new(Xcodeproj::Project::Object::XCSwiftPackageProductDependency)
product.package = pkg
product.product_name = 'MPVKit'
runner_target.package_product_dependencies << product
frameworks_phase = runner_target.frameworks_build_phase
build_file = project.new(Xcodeproj::Project::Object::PBXBuildFile)
build_file.product_ref = product
frameworks_phase.files << build_file
puts "[add ] MPVKit SPM package + framework linkage"
end
project.save
puts "Saved #{PROJECT_PATH}"
+93
View File
@@ -0,0 +1,93 @@
#!/usr/bin/env ruby
# Adds pure-Swift Flutter plugin sources to Runner target (no pods needed).
# Cleans up stale plugin file references left from earlier failed attempts.
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
# Names we want to own (anything with these basenames that's NOT part of
# MpvPlayer is a stale duplicate from earlier failed runs).
PLUGIN_BASENAMES = %w[
SharedPreferencesPlugin.swift
messages.g.swift
PackageInfoPlusPlugin.swift
PathProviderPlugin.swift
GamepadPlugin.swift
GamepadStreamHandler.swift
GCControllerManager.swift
ButtonMapping.swift
DeviceInfoPlusPlugin.swift
ConnectivityPlusPlugin.swift
ConnectivityProvider.swift
PathMonitorConnectivityProvider.swift
OsMediaControlsPlugin.swift
]
# Remove any existing file refs for these basenames from the entire project.
project.files.select { |f| PLUGIN_BASENAMES.include?(f.display_name) }.each do |f|
f.remove_from_project
end
# Drop any build files whose file_ref is now nil.
project.targets.each do |t|
t.build_phases.each do |phase|
next unless phase.respond_to?(:files)
phase.files.delete_if { |bf| bf.file_ref.nil? }
end
end
# Remove any empty Plugins groups.
runner_group = project.main_group['Runner']
if (prev = runner_group['Plugins'])
prev.remove_from_project
end
# Fresh Plugins group, filesystem-aligned under tvos/Runner/Plugins.
plugins_group = runner_group.new_group('Plugins', 'Plugins')
plugins = {
'shared_preferences_foundation' => %w[
SharedPreferencesPlugin.swift
messages.g.swift
],
'package_info_plus' => %w[
PackageInfoPlusPlugin.swift
],
'path_provider' => %w[
PathProviderPlugin.swift
],
'universal_gamepad' => %w[
GamepadPlugin.swift
GamepadStreamHandler.swift
GCControllerManager.swift
ButtonMapping.swift
],
'device_info_plus' => %w[
DeviceInfoPlusPlugin.swift
],
'connectivity_plus' => %w[
ConnectivityPlusPlugin.swift
ConnectivityProvider.swift
PathMonitorConnectivityProvider.swift
],
'os_media_controls' => %w[
OsMediaControlsPlugin.swift
],
}
sources_phase = runner.source_build_phase
plugins.each do |plugin_name, files|
sub = plugins_group.new_group(plugin_name, plugin_name)
files.each do |fname|
ref = sub.new_file(fname)
sources_phase.add_file_reference(ref, true)
puts "[add ] Runner/Plugins/#{plugin_name}/#{fname}"
end
end
project.save
puts "Saved"
+364
View File
@@ -0,0 +1,364 @@
#!/usr/bin/env bash
# based on scripts in /engine/src/flutter/testing/scenario_app
# and xcode_backend.sh script for integration in xcode
# Exit on error
set -e
debug_sim=""
if [[ $(uname -m) == "arm64" ]]; then
TARGET_POSTFIX='_arm64'
CLANG_POSTFIX='_arm64'
SIM_ARCH='arm64'
else
TARGET_POSTFIX=''
CLANG_POSTFIX='_X86'
SIM_ARCH='x86_64'
fi
BuildAppDebug() {
# Host tools (frontend_server, patched SDK, dartaotruntime) ship in
# host_release for both debug and release consumers — the frontend_server
# compiles debug kernels regardless of the host build flavor.
HOST_TOOLS=$FLUTTER_LOCAL_ENGINE/out/host_release
if [[ "$debug_sim" == "true" ]]; then
DEVICE_TOOLS=$FLUTTER_LOCAL_ENGINE/out/tvos_debug_sim_unopt$TARGET_POSTFIX
else
# Device build is always arm64; gn outputs `tvos_debug_unopt` without suffix.
DEVICE_TOOLS=$FLUTTER_LOCAL_ENGINE/out/tvos_debug_unopt
fi
ROOTDIR=$(dirname "$PROJECT_DIR")
OUTDIR=$ROOTDIR/build/ios/Release-iphoneos
mkdir -p $OUTDIR
echo " └─OUTDIR: $OUTDIR"
echo " └─BUILT_PRODUCTS_DIR: $BUILT_PRODUCTS_DIR"
echo " └─Copying Flutter.framework"
rm -rf "$OUTDIR/Flutter.framework"
cp -R "$DEVICE_TOOLS/Flutter.framework" "$OUTDIR"
tvos_deployment_target="$TVOS_DEPLOYMENT_TARGET"
echo " └─Generate flutter_assets via flutter build bundle"
mkdir -p "$OUTDIR/App.framework/flutter_assets"
# Resolve the flutter CLI: prefer FLUTTER_ROOT from Generated.xcconfig, else
# fall back to the `flutter` on PATH.
FLUTTER_BIN=""
if [ -n "$FLUTTER_ROOT" ] && [ -x "$FLUTTER_ROOT/bin/flutter" ]; then
FLUTTER_BIN="$FLUTTER_ROOT/bin/flutter"
elif command -v flutter >/dev/null 2>&1; then
FLUTTER_BIN="$(command -v flutter)"
fi
if [ -z "$FLUTTER_BIN" ]; then
echo " └─ERROR: flutter CLI not found (set FLUTTER_ROOT or add flutter to PATH)"
return 1
fi
# flutter build bundle produces: AssetManifest, FontManifest, NOTICES,
# shaders, fonts, assets, packages, plus a kernel_blob.bin and
# isolate_snapshot_data compiled against the stock flutter engine. We
# overwrite kernel_blob.bin and both snapshot blobs below with versions
# from our tvOS engine so the tvOS VM can load them.
(
cd "$FLUTTER_APPLICATION_PATH" && \
"$FLUTTER_BIN" build bundle \
--asset-dir="$OUTDIR/App.framework/flutter_assets" \
--no-tree-shake-icons \
--suppress-analytics
) || {
echo " └─ERROR: flutter build bundle failed"
return 1
}
echo " └─Compiling tvOS kernel via local engine frontend_server"
FRONTEND_SERVER="$HOST_TOOLS/dart-sdk/bin/snapshots/frontend_server_aot.dart.snapshot"
if [ ! -f "$FRONTEND_SERVER" ]; then
FRONTEND_SERVER="$HOST_TOOLS/gen/frontend_server_aot.dart.snapshot"
fi
"$HOST_TOOLS/dart-sdk/bin/dartaotruntime" \
"$FRONTEND_SERVER" \
--sdk-root "$HOST_TOOLS/flutter_patched_sdk" \
--tfa --target=flutter \
-DTVOS_BUILD=true \
--output-dill "$OUTDIR/App.framework/flutter_assets/kernel_blob.bin" \
"$FLUTTER_APPLICATION_PATH/lib/main.dart"
echo " └─Copying tvOS VM + isolate snapshots (pure JIT, no gen_snapshot)"
cp "$DEVICE_TOOLS/gen/flutter/lib/snapshot/vm_isolate_snapshot.bin" \
"$OUTDIR/App.framework/flutter_assets/vm_snapshot_data"
cp "$DEVICE_TOOLS/gen/flutter/lib/snapshot/isolate_snapshot.bin" \
"$OUTDIR/App.framework/flutter_assets/isolate_snapshot_data"
if [[ "$debug_sim" == "true" ]]; then
SYSROOT=$(xcrun --sdk appletvsimulator --show-sdk-path)
else
SYSROOT=$(xcrun --sdk appletvos --show-sdk-path)
fi
echo " └─Creating stub App using $SYSROOT"
if [[ "$debug_sim" == "true" ]]; then
echo "static const int Moo = 88;" | xcrun clang -x c \
-arch $SIM_ARCH \
-L"$SYSROOT/usr/lib" \
-isysroot "$SYSROOT" \
-mappletvsimulator-version-min=$tvos_deployment_target \
-dynamiclib \
-Xlinker -rpath -Xlinker '@executable_path/Frameworks' \
-Xlinker -rpath -Xlinker '@loader_path/Frameworks' \
-install_name '@rpath/App.framework/App' \
-o "$OUTDIR/App.framework/App" -
else
echo "static const int Moo = 88;" | xcrun clang -x c \
-arch arm64 \
-isysroot "$SYSROOT" \
-mtvos-version-min=$tvos_deployment_target \
-dynamiclib \
-Xlinker -rpath -Xlinker '@executable_path/Frameworks' \
-Xlinker -rpath -Xlinker '@loader_path/Frameworks' \
-install_name '@rpath/App.framework/App' \
-o "$OUTDIR/App.framework/App" -
fi
strip "$OUTDIR/App.framework/App"
echo " └─copy frameworks"
cp "$PROJECT_DIR/scripts/Info.plist" "$OUTDIR/App.framework/Info.plist"
# For Archive builds BUILT_PRODUCTS_DIR differs from TARGET_BUILD_DIR:
# Swift compile looks up frameworks under BUILT_PRODUCTS_DIR, but the
# embedded copy needs to end up inside TARGET_BUILD_DIR/Runner.app.
cp -R "${OUTDIR}/"{App.framework,Flutter.framework} "$TARGET_BUILD_DIR"
if [ -n "${BUILT_PRODUCTS_DIR:-}" ] && [ "$BUILT_PRODUCTS_DIR" != "$TARGET_BUILD_DIR" ]; then
rm -rf "$BUILT_PRODUCTS_DIR/App.framework" "$BUILT_PRODUCTS_DIR/Flutter.framework"
cp -R "${OUTDIR}/"{App.framework,Flutter.framework} "$BUILT_PRODUCTS_DIR"
fi
# Also embed into Runner.app/Frameworks so the dylib @rpath resolves at launch.
APP_FRAMEWORKS_DIR="$TARGET_BUILD_DIR/$WRAPPER_NAME/Frameworks"
mkdir -p "$APP_FRAMEWORKS_DIR"
rm -rf "$APP_FRAMEWORKS_DIR/App.framework" "$APP_FRAMEWORKS_DIR/Flutter.framework"
cp -R "${OUTDIR}/"{App.framework,Flutter.framework} "$APP_FRAMEWORKS_DIR"
# Sign the binaries we moved. Both the flat copy (used by linker/embedder)
# and the embedded copy inside Runner.app/Frameworks need signatures.
# Skip when signing is disabled (CODE_SIGNING_ALLOWED=NO, unit-test builds,
# or sim builds where Xcode doesn't sign frameworks). Xcode's own CodeSign
# phase will sign the final app + frameworks in proper archive builds.
echo " └─Sign"
if [[ "$debug_sim" != "true" && -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" && "${CODE_SIGNING_ALLOWED:-YES}" != "NO" ]]; then
codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${TARGET_BUILD_DIR}/App.framework/App"
codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${TARGET_BUILD_DIR}/Flutter.framework/Flutter"
codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${APP_FRAMEWORKS_DIR}/App.framework/App"
codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${APP_FRAMEWORKS_DIR}/Flutter.framework/Flutter"
else
echo " (skipped — no code sign identity or signing disabled)"
fi
echo " └─Done"
return 0
}
BuildAppRelease() {
HOST_TOOLS=$FLUTTER_LOCAL_ENGINE/out/host_release
DEVICE_TOOLS=$FLUTTER_LOCAL_ENGINE/out/tvos_release
# Locate gen_snapshot. Use the cross-compile variant that targets iOS/tvOS
# arm64 (emits arm64-ios assembly). The plain host gen_snapshot targets
# macOS and its snapshots are rejected at runtime by the iOS/tvOS VM.
GEN_SNAPSHOT=""
for cand in \
"$DEVICE_TOOLS/artifacts_arm64/gen_snapshot_arm64" \
"$DEVICE_TOOLS/universal/gen_snapshot_arm64" \
"$DEVICE_TOOLS/artifacts_x64/gen_snapshot_arm64" \
"$DEVICE_TOOLS/gen_snapshot_arm64"; do
if [ -x "$cand" ]; then
GEN_SNAPSHOT="$cand"
break
fi
done
if [ -z "$GEN_SNAPSHOT" ]; then
echo " └─ERROR: gen_snapshot not found under $DEVICE_TOOLS or $HOST_TOOLS"
return 1
fi
ROOTDIR=$(dirname "$PROJECT_DIR")
OUTDIR=$ROOTDIR/build/ios/Release-iphoneos
mkdir -p "$OUTDIR"
echo " └─OUTDIR: $OUTDIR"
echo " └─gen_snapshot: $GEN_SNAPSHOT"
echo " └─Copying Flutter.framework"
rm -rf "$OUTDIR/Flutter.framework"
cp -R "$DEVICE_TOOLS/Flutter.framework" "$OUTDIR"
tvos_deployment_target="$TVOS_DEPLOYMENT_TARGET"
# Resolve flutter CLI.
FLUTTER_BIN=""
if [ -n "$FLUTTER_ROOT" ] && [ -x "$FLUTTER_ROOT/bin/flutter" ]; then
FLUTTER_BIN="$FLUTTER_ROOT/bin/flutter"
elif command -v flutter >/dev/null 2>&1; then
FLUTTER_BIN="$(command -v flutter)"
fi
if [ -z "$FLUTTER_BIN" ]; then
echo " └─ERROR: flutter CLI not found (set FLUTTER_ROOT or add flutter to PATH)"
return 1
fi
echo " └─Generate flutter_assets via flutter build bundle (release)"
mkdir -p "$OUTDIR/App.framework/flutter_assets"
(
cd "$FLUTTER_APPLICATION_PATH" && \
"$FLUTTER_BIN" build bundle \
--release \
--asset-dir="$OUTDIR/App.framework/flutter_assets" \
--no-tree-shake-icons \
--suppress-analytics
) || {
echo " └─ERROR: flutter build bundle failed"
return 1
}
# AOT builds don't need kernel_blob.bin in flutter_assets — the compiled
# arm64 code lives in App.framework/App itself.
rm -f "$OUTDIR/App.framework/flutter_assets/kernel_blob.bin"
echo " └─Compiling AOT kernel via local engine frontend_server"
# The snapshot under dart-sdk/bin/snapshots/ is the actual AOT-compiled one;
# the one under gen/ is a stale/placeholder kernel.
FRONTEND_SERVER="$HOST_TOOLS/dart-sdk/bin/snapshots/frontend_server_aot.dart.snapshot"
if [ ! -f "$FRONTEND_SERVER" ]; then
FRONTEND_SERVER="$HOST_TOOLS/gen/frontend_server_aot.dart.snapshot"
fi
"$HOST_TOOLS/dart-sdk/bin/dartaotruntime" \
"$FRONTEND_SERVER" \
--sdk-root "$HOST_TOOLS/flutter_patched_sdk" \
--aot --tfa --target=flutter \
-Ddart.vm.product=true \
-Ddart.vm.profile=false \
-DFLUTTER_BUILD_MODE=release \
-DTARGET_PLATFORM=TVOS \
-DTVOS_BUILD=true \
--output-dill "$OUTDIR/app.dill" \
"$FLUTTER_APPLICATION_PATH/lib/main.dart"
echo " └─Compiling AOT Assembly"
"$GEN_SNAPSHOT" \
--deterministic \
--snapshot_kind=app-aot-assembly \
--assembly="$OUTDIR/snapshot_assembly.S" \
--strip \
"$OUTDIR/app.dill"
echo " └─Compiling Assembly"
SYSROOT=$(xcrun --sdk appletvos --show-sdk-path)
cc -arch arm64 \
-isysroot "$SYSROOT" \
-mtvos-version-min=$tvos_deployment_target \
-c "$OUTDIR/snapshot_assembly.S" \
-o "$OUTDIR/snapshot_assembly.o"
echo " └─Linking app"
clang -arch arm64 \
-isysroot "$SYSROOT" \
-mtvos-version-min=$tvos_deployment_target \
-dynamiclib \
-Xlinker -rpath -Xlinker @executable_path/Frameworks \
-Xlinker -rpath -Xlinker @loader_path/Frameworks \
-install_name @rpath/App.framework/App \
-o "$OUTDIR/App.framework/App" \
"$OUTDIR/snapshot_assembly.o"
strip "$OUTDIR/App.framework/App"
cp "$PROJECT_DIR/scripts/Info.plist" "$OUTDIR/App.framework/Info.plist"
echo " └─copy frameworks"
# Archive builds point BUILT_PRODUCTS_DIR at a separate products path that
# Swift compile scans for frameworks — copy there too so downstream
# compilation in the same target can resolve Flutter.h.
cp -R "${OUTDIR}/"{App.framework,Flutter.framework} "$TARGET_BUILD_DIR"
if [ -n "${BUILT_PRODUCTS_DIR:-}" ] && [ "$BUILT_PRODUCTS_DIR" != "$TARGET_BUILD_DIR" ]; then
rm -rf "$BUILT_PRODUCTS_DIR/App.framework" "$BUILT_PRODUCTS_DIR/Flutter.framework"
cp -R "${OUTDIR}/"{App.framework,Flutter.framework} "$BUILT_PRODUCTS_DIR"
fi
APP_FRAMEWORKS_DIR="$TARGET_BUILD_DIR/$WRAPPER_NAME/Frameworks"
mkdir -p "$APP_FRAMEWORKS_DIR"
rm -rf "$APP_FRAMEWORKS_DIR/App.framework" "$APP_FRAMEWORKS_DIR/Flutter.framework"
cp -R "${OUTDIR}/"{App.framework,Flutter.framework} "$APP_FRAMEWORKS_DIR"
echo " └─Sign"
if [[ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" && "${CODE_SIGNING_ALLOWED:-YES}" != "NO" ]]; then
codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${TARGET_BUILD_DIR}/App.framework/App"
codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${TARGET_BUILD_DIR}/Flutter.framework/Flutter"
codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${APP_FRAMEWORKS_DIR}/App.framework/App"
codesign --force --verbose --sign "${EXPANDED_CODE_SIGN_IDENTITY}" -- "${APP_FRAMEWORKS_DIR}/Flutter.framework/Flutter"
else
echo " (skipped — no code sign identity or signing disabled)"
fi
echo " └─Done"
return 0
}
BuildApp() {
local build_mode="$(echo "${FLUTTER_BUILD_MODE:-${CONFIGURATION}}" | tr "[:upper:]" "[:lower:]")"
echo "Compiling Flutter/App.Framework"
if [ -z "$FLUTTER_LOCAL_ENGINE" ]; then
echo " └─ERROR: FLUTTER_LOCAL_ENGINE not set!"
return 1;
fi
echo " └─engine $FLUTTER_LOCAL_ENGINE"
if [[ "$PLATFORM_NAME" == "appletvsimulator" && "$build_mode" =~ "debug" ]]; then
debug_sim="true"
BuildAppDebug
elif [[ "$build_mode" =~ "debug" ]]; then
BuildAppDebug
elif [[ "$build_mode" =~ "release" ]]; then
# release/archive (archive: build mode == "release" && ${ACTION} == "install")
BuildAppRelease
else
echo " └─ERROR: unknown target: ${build_mode}"
return 1;
fi
return 0
}
# Main entry point.
if [[ $# == 0 ]]; then
# Backwards-compatibility: if no args are provided, build and embed.
BuildApp
EmbedFlutterFrameworks
else
case $1 in
"build")
BuildApp ;;
# "embed_and_thin")
# "Not needed, used from flutter xcode_backend.sh script"
esac
fi