Add iOS 27 remote media session and system player extension
This commit is contained in:
@@ -125,11 +125,93 @@
|
||||
"isa" = "PBXBuildFile";
|
||||
"fileRef" = "7148B52E915554F67E875C26";
|
||||
};
|
||||
"088AC623B345717D980B3156" = {
|
||||
"isa" = "PBXFileReference";
|
||||
"lastKnownFileType" = "sourcecode.swift";
|
||||
"path" = "SystemPlayerManager.swift";
|
||||
"sourceTree" = "<group>";
|
||||
};
|
||||
"27227986C8A227728D784957" = {
|
||||
"isa" = "PBXBuildFile";
|
||||
"fileRef" = "088AC623B345717D980B3156";
|
||||
};
|
||||
"026F39792BBA0E5032536CED" = {
|
||||
"isa" = "PBXGroup";
|
||||
"path" = "MusicBridge";
|
||||
"sourceTree" = "<group>";
|
||||
"children" = ("065A9D559DC80AAF876DA757", "763FA6A137E5E94274AC56A3", "971C0AE92F167DE7AB145086", "5F6F408829AB46D3B85FB911", "76FB6BFFC478599567424755", "581AD2E9199DAED730DC0F6E", "EE2357BD5CA3755AFEB3171A", "124CF3F5AC075D1D6042DF64", "ADF0CD56882201BD3AC1581B", "9C432A0DA0BEC173839EBF86", "6B0F2027221CDD63E8FD4C59", "7148B52E915554F67E875C26", "8BF729FFE074CAEE622C0292",);
|
||||
"children" = ("065A9D559DC80AAF876DA757", "763FA6A137E5E94274AC56A3", "971C0AE92F167DE7AB145086", "5F6F408829AB46D3B85FB911", "76FB6BFFC478599567424755", "581AD2E9199DAED730DC0F6E", "EE2357BD5CA3755AFEB3171A", "124CF3F5AC075D1D6042DF64", "ADF0CD56882201BD3AC1581B", "9C432A0DA0BEC173839EBF86", "6B0F2027221CDD63E8FD4C59", "7148B52E915554F67E875C26", "088AC623B345717D980B3156", "8BF729FFE074CAEE622C0292",);
|
||||
};
|
||||
"1C6CBD84758D03C1B40D19C7" = {
|
||||
"isa" = "PBXFileReference";
|
||||
"lastKnownFileType" = "sourcecode.swift";
|
||||
"path" = "SystemPlayerAttributes.swift";
|
||||
"sourceTree" = "<group>";
|
||||
};
|
||||
"0D17ED02E5B8770D785508A3" = {
|
||||
"isa" = "PBXBuildFile";
|
||||
"fileRef" = "1C6CBD84758D03C1B40D19C7";
|
||||
};
|
||||
"77372299BB61F0ED174508F0" = {
|
||||
"isa" = "PBXFileReference";
|
||||
"lastKnownFileType" = "sourcecode.swift";
|
||||
"path" = "SystemPlayerCredentials.swift";
|
||||
"sourceTree" = "<group>";
|
||||
};
|
||||
"2599D4B5F13D672B723A4BD3" = {
|
||||
"isa" = "PBXBuildFile";
|
||||
"fileRef" = "77372299BB61F0ED174508F0";
|
||||
};
|
||||
"897E8908B2E9AD60A31E6897" = {
|
||||
"isa" = "PBXFileReference";
|
||||
"lastKnownFileType" = "sourcecode.swift";
|
||||
"path" = "SystemPlayerTransport.swift";
|
||||
"sourceTree" = "<group>";
|
||||
};
|
||||
"804FFB386BC1AF4252EDA177" = {
|
||||
"isa" = "PBXBuildFile";
|
||||
"fileRef" = "897E8908B2E9AD60A31E6897";
|
||||
};
|
||||
"D2DF40467DEC980974060FD5" = {
|
||||
"isa" = "PBXGroup";
|
||||
"path" = "RemoteShared";
|
||||
"sourceTree" = "<group>";
|
||||
"children" = ("1C6CBD84758D03C1B40D19C7", "77372299BB61F0ED174508F0", "897E8908B2E9AD60A31E6897",);
|
||||
};
|
||||
"54D4A2CB4EFD85CA136E4B32" = {
|
||||
"isa" = "PBXFileReference";
|
||||
"lastKnownFileType" = "sourcecode.swift";
|
||||
"path" = "MusicBridgeRemote.swift";
|
||||
"sourceTree" = "<group>";
|
||||
};
|
||||
"ED075C376D7772FEBD3EC117" = {
|
||||
"isa" = "PBXBuildFile";
|
||||
"fileRef" = "54D4A2CB4EFD85CA136E4B32";
|
||||
};
|
||||
"E2D7CC5D9CE02F4A7B8243D8" = {
|
||||
"isa" = "PBXGroup";
|
||||
"path" = "MusicBridgeRemote";
|
||||
"sourceTree" = "<group>";
|
||||
"children" = ("54D4A2CB4EFD85CA136E4B32",);
|
||||
};
|
||||
"511889FA962E9B3AC1BC92DE" = {
|
||||
"isa" = "PBXBuildFile";
|
||||
"fileRef" = "1C6CBD84758D03C1B40D19C7";
|
||||
};
|
||||
"A3C91023F16219D79F80723C" = {
|
||||
"isa" = "PBXBuildFile";
|
||||
"fileRef" = "77372299BB61F0ED174508F0";
|
||||
};
|
||||
"823EB4050AFBFC9AE72A3C03" = {
|
||||
"isa" = "PBXBuildFile";
|
||||
"fileRef" = "897E8908B2E9AD60A31E6897";
|
||||
};
|
||||
"8EE58DCF843B86D38CF45CC0" = {
|
||||
"isa" = "PBXBuildFile";
|
||||
"fileRef" = "124CF3F5AC075D1D6042DF64";
|
||||
};
|
||||
"4AAE356B6279CE0B686A3B9D" = {
|
||||
"isa" = "PBXBuildFile";
|
||||
"fileRef" = "065A9D559DC80AAF876DA757";
|
||||
};
|
||||
"7C5E653CCD928B143AB68F09" = {
|
||||
"isa" = "PBXFileReference";
|
||||
@@ -301,7 +383,7 @@
|
||||
"72B74C8A2143F64FD53D71E5" = {
|
||||
"isa" = "PBXSourcesBuildPhase";
|
||||
"buildActionMask" = 2147483647;
|
||||
"files" = ("82F1C33CE4F8B8F641FF0461", "40D2920E2C996D62B7190BA4", "16EA630DE1FE9F0A16580C9D", "842E87CD23DCB5BBCA4340F6", "DA06B3B12E52FB69A6A34887", "D6D68067600EEBE36972550E", "181CACDC331104A1E16CFC20", "DEDCCD95A140162B6D654871", "48E2FDCA11A6694953C86E6D", "47693934E8A974162CF08A38", "287044ADBD143F929035BC61", "BE808A0D13A16FE67D32B7FB", "C7506B09240305F44C61EB60", "4FD4638045EDA8D7CCFD00AF", "962BED0D8E17961AD72EC24F",);
|
||||
"files" = ("82F1C33CE4F8B8F641FF0461", "40D2920E2C996D62B7190BA4", "16EA630DE1FE9F0A16580C9D", "842E87CD23DCB5BBCA4340F6", "DA06B3B12E52FB69A6A34887", "D6D68067600EEBE36972550E", "181CACDC331104A1E16CFC20", "DEDCCD95A140162B6D654871", "48E2FDCA11A6694953C86E6D", "47693934E8A974162CF08A38", "287044ADBD143F929035BC61", "BE808A0D13A16FE67D32B7FB", "27227986C8A227728D784957", "0D17ED02E5B8770D785508A3", "2599D4B5F13D672B723A4BD3", "804FFB386BC1AF4252EDA177", "C7506B09240305F44C61EB60", "4FD4638045EDA8D7CCFD00AF", "962BED0D8E17961AD72EC24F",);
|
||||
"runOnlyForDeploymentPostprocessing" = 0;
|
||||
};
|
||||
"734C4650C7A5604092763C32" = {
|
||||
@@ -323,6 +405,13 @@
|
||||
"ATTRIBUTES" = ("RemoveHeadersOnCopy",);
|
||||
};
|
||||
};
|
||||
"013486AD4616563D96D6E9AC" = {
|
||||
"isa" = "PBXBuildFile";
|
||||
"fileRef" = "F1BB1F45195BEB71A550539C";
|
||||
"settings" = {
|
||||
"ATTRIBUTES" = ("RemoveHeadersOnCopy",);
|
||||
};
|
||||
};
|
||||
"AFC32963E7959E7A34264826" = {
|
||||
"isa" = "PBXCopyFilesBuildPhase";
|
||||
"buildActionMask" = 2147483647;
|
||||
@@ -332,6 +421,15 @@
|
||||
"name" = "Embed App Extensions";
|
||||
"runOnlyForDeploymentPostprocessing" = 0;
|
||||
};
|
||||
"B2EC54BE7DEB4C17571E7035" = {
|
||||
"isa" = "PBXCopyFilesBuildPhase";
|
||||
"buildActionMask" = 2147483647;
|
||||
"dstPath" = "$(EXTENSIONS_FOLDER_PATH)";
|
||||
"dstSubfolderSpec" = 16;
|
||||
"files" = ("013486AD4616563D96D6E9AC",);
|
||||
"name" = "Embed ExtensionKit Extensions";
|
||||
"runOnlyForDeploymentPostprocessing" = 0;
|
||||
};
|
||||
"01E77404E10F254C43E64C55" = {
|
||||
"isa" = "PBXContainerItemProxy";
|
||||
"containerPortal" = "244210E48437B6556980A702";
|
||||
@@ -344,6 +442,18 @@
|
||||
"target" = "BFE9A9DC8C194372006C3ACC";
|
||||
"targetProxy" = "01E77404E10F254C43E64C55";
|
||||
};
|
||||
"E73C0469E1928499B9BE1AC7" = {
|
||||
"isa" = "PBXContainerItemProxy";
|
||||
"containerPortal" = "244210E48437B6556980A702";
|
||||
"proxyType" = 1;
|
||||
"remoteGlobalIDString" = "714DB4A269580A6F5127F5CC";
|
||||
"remoteInfo" = "MusicBridgeRemote";
|
||||
};
|
||||
"F76CBC167D5B68BDC0376A94" = {
|
||||
"isa" = "PBXTargetDependency";
|
||||
"target" = "714DB4A269580A6F5127F5CC";
|
||||
"targetProxy" = "E73C0469E1928499B9BE1AC7";
|
||||
};
|
||||
"E3322BA79DBD9A84650B2CAE" = {
|
||||
"isa" = "XCBuildConfiguration";
|
||||
"name" = "Debug";
|
||||
@@ -358,9 +468,10 @@
|
||||
"CODE_SIGN_STYLE" = "Automatic";
|
||||
"GENERATE_INFOPLIST_FILE" = "NO";
|
||||
"INFOPLIST_FILE" = "MusicBridge/Info.plist";
|
||||
"CURRENT_PROJECT_VERSION" = "7";
|
||||
"MARKETING_VERSION" = "0.6.0";
|
||||
"CURRENT_PROJECT_VERSION" = "8";
|
||||
"MARKETING_VERSION" = "0.7.0";
|
||||
"ASSETCATALOG_COMPILER_APPICON_NAME" = "AppIcon";
|
||||
"CODE_SIGN_ENTITLEMENTS" = "MusicBridge/App.entitlements";
|
||||
"SWIFT_OPTIMIZATION_LEVEL" = "-Onone";
|
||||
"ENABLE_TESTABILITY" = "YES";
|
||||
"SWIFT_ACTIVE_COMPILATION_CONDITIONS" = "DEBUG";
|
||||
@@ -380,9 +491,10 @@
|
||||
"CODE_SIGN_STYLE" = "Automatic";
|
||||
"GENERATE_INFOPLIST_FILE" = "NO";
|
||||
"INFOPLIST_FILE" = "MusicBridge/Info.plist";
|
||||
"CURRENT_PROJECT_VERSION" = "7";
|
||||
"MARKETING_VERSION" = "0.6.0";
|
||||
"CURRENT_PROJECT_VERSION" = "8";
|
||||
"MARKETING_VERSION" = "0.7.0";
|
||||
"ASSETCATALOG_COMPILER_APPICON_NAME" = "AppIcon";
|
||||
"CODE_SIGN_ENTITLEMENTS" = "MusicBridge/App.entitlements";
|
||||
"SWIFT_OPTIMIZATION_LEVEL" = "-O";
|
||||
};
|
||||
};
|
||||
@@ -399,9 +511,9 @@
|
||||
"productReference" = "7C35A0276A762371ABAE5596";
|
||||
"productType" = "com.apple.product-type.application";
|
||||
"buildConfigurationList" = "C6336ED5908EABEBF60AE675";
|
||||
"buildPhases" = ("72B74C8A2143F64FD53D71E5", "734C4650C7A5604092763C32", "69816CB820E8173702C54C37", "AFC32963E7959E7A34264826",);
|
||||
"buildPhases" = ("72B74C8A2143F64FD53D71E5", "734C4650C7A5604092763C32", "69816CB820E8173702C54C37", "AFC32963E7959E7A34264826", "B2EC54BE7DEB4C17571E7035",);
|
||||
"buildRules" = ();
|
||||
"dependencies" = ("A4C2D386AB0259F33DCDC427",);
|
||||
"dependencies" = ("A4C2D386AB0259F33DCDC427", "F76CBC167D5B68BDC0376A94",);
|
||||
};
|
||||
"5E4BE508157A7B2CB7B4C2F2" = {
|
||||
"isa" = "PBXFileReference";
|
||||
@@ -534,8 +646,8 @@
|
||||
"CODE_SIGN_STYLE" = "Automatic";
|
||||
"GENERATE_INFOPLIST_FILE" = "NO";
|
||||
"INFOPLIST_FILE" = "MusicBridgeWidgets/Info.plist";
|
||||
"CURRENT_PROJECT_VERSION" = "7";
|
||||
"MARKETING_VERSION" = "0.6.0";
|
||||
"CURRENT_PROJECT_VERSION" = "8";
|
||||
"MARKETING_VERSION" = "0.7.0";
|
||||
"APPLICATION_EXTENSION_API_ONLY" = "YES";
|
||||
"SKIP_INSTALL" = "YES";
|
||||
"LD_RUNPATH_SEARCH_PATHS" = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
@@ -558,8 +670,8 @@
|
||||
"CODE_SIGN_STYLE" = "Automatic";
|
||||
"GENERATE_INFOPLIST_FILE" = "NO";
|
||||
"INFOPLIST_FILE" = "MusicBridgeWidgets/Info.plist";
|
||||
"CURRENT_PROJECT_VERSION" = "7";
|
||||
"MARKETING_VERSION" = "0.6.0";
|
||||
"CURRENT_PROJECT_VERSION" = "8";
|
||||
"MARKETING_VERSION" = "0.7.0";
|
||||
"APPLICATION_EXTENSION_API_ONLY" = "YES";
|
||||
"SKIP_INSTALL" = "YES";
|
||||
"LD_RUNPATH_SEARCH_PATHS" = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
@@ -584,16 +696,106 @@
|
||||
"buildRules" = ();
|
||||
"dependencies" = ();
|
||||
};
|
||||
"F1BB1F45195BEB71A550539C" = {
|
||||
"isa" = "PBXFileReference";
|
||||
"explicitFileType" = "wrapper.extensionkit-extension";
|
||||
"includeInIndex" = 0;
|
||||
"path" = "MusicBridgeRemote.appex";
|
||||
"sourceTree" = "BUILT_PRODUCTS_DIR";
|
||||
};
|
||||
"C2BA95929A255B0F1FC04A79" = {
|
||||
"isa" = "PBXSourcesBuildPhase";
|
||||
"buildActionMask" = 2147483647;
|
||||
"files" = ("ED075C376D7772FEBD3EC117", "511889FA962E9B3AC1BC92DE", "A3C91023F16219D79F80723C", "823EB4050AFBFC9AE72A3C03", "8EE58DCF843B86D38CF45CC0", "4AAE356B6279CE0B686A3B9D",);
|
||||
"runOnlyForDeploymentPostprocessing" = 0;
|
||||
};
|
||||
"D3E3B87195B5FF1DA14795C1" = {
|
||||
"isa" = "PBXFrameworksBuildPhase";
|
||||
"buildActionMask" = 2147483647;
|
||||
"files" = ();
|
||||
"runOnlyForDeploymentPostprocessing" = 0;
|
||||
};
|
||||
"9C3BF80554FD9B39BDF1988D" = {
|
||||
"isa" = "PBXResourcesBuildPhase";
|
||||
"buildActionMask" = 2147483647;
|
||||
"files" = ();
|
||||
"runOnlyForDeploymentPostprocessing" = 0;
|
||||
};
|
||||
"809F689056832D3F2A4D91B6" = {
|
||||
"isa" = "XCBuildConfiguration";
|
||||
"name" = "Debug";
|
||||
"buildSettings" = {
|
||||
"PRODUCT_NAME" = "$(TARGET_NAME)";
|
||||
"PRODUCT_BUNDLE_IDENTIFIER" = "ru.yukinoki.musicbridge.remote";
|
||||
"SWIFT_VERSION" = "5.0";
|
||||
"IPHONEOS_DEPLOYMENT_TARGET" = "27.0";
|
||||
"SDKROOT" = "iphoneos";
|
||||
"SUPPORTED_PLATFORMS" = "iphoneos iphonesimulator";
|
||||
"TARGETED_DEVICE_FAMILY" = "1";
|
||||
"CODE_SIGN_STYLE" = "Automatic";
|
||||
"GENERATE_INFOPLIST_FILE" = "NO";
|
||||
"INFOPLIST_FILE" = "MusicBridgeRemote/Info.plist";
|
||||
"CURRENT_PROJECT_VERSION" = "8";
|
||||
"MARKETING_VERSION" = "0.7.0";
|
||||
"APPLICATION_EXTENSION_API_ONLY" = "YES";
|
||||
"SKIP_INSTALL" = "YES";
|
||||
"LD_RUNPATH_SEARCH_PATHS" = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
"CODE_SIGN_ENTITLEMENTS" = "MusicBridgeRemote/Remote.entitlements";
|
||||
"SWIFT_OPTIMIZATION_LEVEL" = "-Onone";
|
||||
"ENABLE_TESTABILITY" = "YES";
|
||||
"SWIFT_ACTIVE_COMPILATION_CONDITIONS" = "DEBUG";
|
||||
};
|
||||
};
|
||||
"5E42ACAF0DE32186ED14E82F" = {
|
||||
"isa" = "XCBuildConfiguration";
|
||||
"name" = "Release";
|
||||
"buildSettings" = {
|
||||
"PRODUCT_NAME" = "$(TARGET_NAME)";
|
||||
"PRODUCT_BUNDLE_IDENTIFIER" = "ru.yukinoki.musicbridge.remote";
|
||||
"SWIFT_VERSION" = "5.0";
|
||||
"IPHONEOS_DEPLOYMENT_TARGET" = "27.0";
|
||||
"SDKROOT" = "iphoneos";
|
||||
"SUPPORTED_PLATFORMS" = "iphoneos iphonesimulator";
|
||||
"TARGETED_DEVICE_FAMILY" = "1";
|
||||
"CODE_SIGN_STYLE" = "Automatic";
|
||||
"GENERATE_INFOPLIST_FILE" = "NO";
|
||||
"INFOPLIST_FILE" = "MusicBridgeRemote/Info.plist";
|
||||
"CURRENT_PROJECT_VERSION" = "8";
|
||||
"MARKETING_VERSION" = "0.7.0";
|
||||
"APPLICATION_EXTENSION_API_ONLY" = "YES";
|
||||
"SKIP_INSTALL" = "YES";
|
||||
"LD_RUNPATH_SEARCH_PATHS" = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||
"CODE_SIGN_ENTITLEMENTS" = "MusicBridgeRemote/Remote.entitlements";
|
||||
"SWIFT_OPTIMIZATION_LEVEL" = "-O";
|
||||
};
|
||||
};
|
||||
"AD0B31AD2D7D0B6DCF3ACF58" = {
|
||||
"isa" = "XCConfigurationList";
|
||||
"buildConfigurations" = ("809F689056832D3F2A4D91B6", "5E42ACAF0DE32186ED14E82F",);
|
||||
"defaultConfigurationIsVisible" = 0;
|
||||
"defaultConfigurationName" = "Release";
|
||||
};
|
||||
"714DB4A269580A6F5127F5CC" = {
|
||||
"isa" = "PBXNativeTarget";
|
||||
"name" = "MusicBridgeRemote";
|
||||
"productName" = "MusicBridgeRemote";
|
||||
"productReference" = "F1BB1F45195BEB71A550539C";
|
||||
"productType" = "com.apple.product-type.extensionkit-extension";
|
||||
"buildConfigurationList" = "AD0B31AD2D7D0B6DCF3ACF58";
|
||||
"buildPhases" = ("C2BA95929A255B0F1FC04A79", "D3E3B87195B5FF1DA14795C1", "9C3BF80554FD9B39BDF1988D",);
|
||||
"buildRules" = ();
|
||||
"dependencies" = ();
|
||||
};
|
||||
"0A3E27B8CA818264D75C8D81" = {
|
||||
"isa" = "PBXGroup";
|
||||
"name" = "Products";
|
||||
"sourceTree" = "<group>";
|
||||
"children" = ("7C35A0276A762371ABAE5596", "5E4BE508157A7B2CB7B4C2F2", "CE9225C1B8E80F534681689F",);
|
||||
"children" = ("7C35A0276A762371ABAE5596", "5E4BE508157A7B2CB7B4C2F2", "CE9225C1B8E80F534681689F", "F1BB1F45195BEB71A550539C",);
|
||||
};
|
||||
"0D6E4079E36703EBD37C0072" = {
|
||||
"isa" = "PBXGroup";
|
||||
"sourceTree" = "<group>";
|
||||
"children" = ("026F39792BBA0E5032536CED", "E3C4B39D6D5013477C98CD58", "F932ED2EB70210651D1121CB", "E1AD768E105BF42350625991", "0A3E27B8CA818264D75C8D81",);
|
||||
"children" = ("026F39792BBA0E5032536CED", "E3C4B39D6D5013477C98CD58", "F932ED2EB70210651D1121CB", "D2DF40467DEC980974060FD5", "E2D7CC5D9CE02F4A7B8243D8", "E1AD768E105BF42350625991", "0A3E27B8CA818264D75C8D81",);
|
||||
};
|
||||
"73EBF6644258C120893E685D" = {
|
||||
"isa" = "XCBuildConfiguration";
|
||||
@@ -635,7 +837,7 @@
|
||||
"productRefGroup" = "0A3E27B8CA818264D75C8D81";
|
||||
"projectDirPath" = "";
|
||||
"projectRoot" = "";
|
||||
"targets" = ("F380385F14BF54F0FA81EEAE", "C2A8D97FC5416EA6103FBF9A", "BFE9A9DC8C194372006C3ACC",);
|
||||
"targets" = ("F380385F14BF54F0FA81EEAE", "C2A8D97FC5416EA6103FBF9A", "BFE9A9DC8C194372006C3ACC", "714DB4A269580A6F5127F5CC",);
|
||||
};
|
||||
};
|
||||
"rootObject" = "244210E48437B6556980A702";
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?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>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<string>$(AppIdentifierPrefix)ru.yukinoki.musicbridge.remote-control</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -3,6 +3,7 @@ import SwiftUI
|
||||
struct ContentView: View {
|
||||
@ObservedObject var store: PlayerStore
|
||||
@ObservedObject var liveActivity: LiveActivityManager
|
||||
@ObservedObject var systemPlayer: SystemPlayerManager
|
||||
@State private var showForget = false
|
||||
|
||||
var body: some View {
|
||||
@@ -20,6 +21,23 @@ struct ContentView: View {
|
||||
} else {
|
||||
PlayerView(store: store, coverSize: min(280, max(120, min(viewport.size.width - 40, viewport.size.height * 0.30))))
|
||||
|
||||
if #available(iOS 27.0, *) {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Button {
|
||||
if systemPlayer.running { systemPlayer.end() }
|
||||
else if let state = store.state, let connection = store.connection {
|
||||
Task { await systemPlayer.start(state: state, connection: connection) }
|
||||
}
|
||||
} label: {
|
||||
Label(systemPlayer.running ? "Выключить системный пульт" : "Включить системный пульт iOS", systemImage: "play.rectangle")
|
||||
}.disabled(systemPlayer.busy || (!systemPlayer.running && (!store.connected || store.state?.hasSession != true)))
|
||||
if systemPlayer.busy { ProgressView() }
|
||||
Text("Музыка ПК в Пункте управления. Звук остаётся на компьютере.")
|
||||
.font(.caption).foregroundStyle(.secondary)
|
||||
if let error = systemPlayer.error { Text(error).font(.caption).foregroundStyle(.red).textSelection(.enabled) }
|
||||
}.frame(maxWidth: .infinity, alignment: .leading).bridgeCard()
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Button {
|
||||
if liveActivity.running { liveActivity.end() }
|
||||
|
||||
@@ -72,5 +72,7 @@
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>MusicBridgeSharedKeychainGroup</key>
|
||||
<string>$(AppIdentifierPrefix)ru.yukinoki.musicbridge.remote-control</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -119,7 +119,7 @@ struct Invitation: Decodable {
|
||||
}
|
||||
}
|
||||
|
||||
struct Capabilities: Decodable {
|
||||
struct Capabilities: Codable {
|
||||
let play, pause, toggle, next, previous, seek: Bool
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import SwiftUI
|
||||
struct MusicBridgeApp: App {
|
||||
@StateObject private var store = PreviewSupport.makeStore()
|
||||
@StateObject private var liveActivity = LiveActivityManager()
|
||||
@StateObject private var systemPlayer = SystemPlayerManager()
|
||||
@Environment(\.scenePhase) private var phase
|
||||
|
||||
var body: some Scene {
|
||||
@@ -20,13 +21,15 @@ struct MusicBridgeApp: App {
|
||||
}.padding()
|
||||
}
|
||||
} else {
|
||||
ContentView(store: store, liveActivity: liveActivity)
|
||||
ContentView(store: store, liveActivity: liveActivity, systemPlayer: systemPlayer)
|
||||
}
|
||||
}
|
||||
.dynamicTypeSize(PreviewSupport.largeText ? DynamicTypeSize.accessibility2...DynamicTypeSize.accessibility2 : DynamicTypeSize.xSmall...DynamicTypeSize.accessibility5)
|
||||
.onOpenURL { if $0.absoluteString != "musicbridge://player" { store.importInvitation($0.absoluteString) } }
|
||||
.task {
|
||||
store.liveActivity = liveActivity
|
||||
store.systemPlayer = systemPlayer
|
||||
if !PreviewSupport.enabled { await systemPlayer.restore(connection: store.connection) }
|
||||
if !PreviewSupport.enabled { store.setActive(phase == .active) }
|
||||
}
|
||||
.onChange(of: phase) { _, value in if !PreviewSupport.enabled { store.setActive(value == .active) } }
|
||||
|
||||
@@ -15,6 +15,7 @@ final class PlayerStore: ObservableObject {
|
||||
@Published var incomingInvitation: String?
|
||||
let discovery = DiscoveryStore()
|
||||
var liveActivity: LiveActivityManager?
|
||||
var systemPlayer: SystemPlayerManager?
|
||||
private(set) var receivedAt = Date()
|
||||
private var client: BridgeClient?
|
||||
private var runner: Task<Void, Never>?
|
||||
@@ -170,6 +171,7 @@ final class PlayerStore: ObservableObject {
|
||||
connected = true
|
||||
status = "Подключено"
|
||||
liveActivity?.receive(snapshot)
|
||||
if let connection { systemPlayer?.receive(snapshot, connection: connection) }
|
||||
guard artworkID != snapshot.artworkId else { return }
|
||||
artworkID = snapshot.artworkId
|
||||
artworkTask?.cancel()
|
||||
@@ -248,6 +250,7 @@ final class PlayerStore: ObservableObject {
|
||||
do { try storage.delete() }
|
||||
catch { self.error = error.localizedDescription; return }
|
||||
liveActivity?.end()
|
||||
systemPlayer?.end()
|
||||
stop()
|
||||
cancelPairing()
|
||||
needsPairing = false
|
||||
@@ -273,6 +276,7 @@ final class PlayerStore: ObservableObject {
|
||||
|
||||
private func revokeAccess() {
|
||||
liveActivity?.end()
|
||||
systemPlayer?.end()
|
||||
stop()
|
||||
needsPairing = true
|
||||
status = "Нужно повторное сопряжение"
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import SwiftUI
|
||||
import NowPlaying
|
||||
|
||||
@MainActor
|
||||
final class SystemPlayerManager: ObservableObject {
|
||||
@Published private(set) var running = false
|
||||
@Published private(set) var busy = false
|
||||
@Published private(set) var error: String?
|
||||
private var storage: Any?
|
||||
private var id: String?
|
||||
private var generation = UUID()
|
||||
private var updates: Task<Void, Never>?
|
||||
private var lastUpdate = Date.distantPast
|
||||
private var lastTrack = ""
|
||||
private var lastPlaying = false
|
||||
private let savedIDKey = "MusicBridge.systemPlayerSession"
|
||||
|
||||
func start(state: MediaState, connection: Connection) async {
|
||||
guard !busy, !running, state.hasSession, connection.token != nil,
|
||||
UIApplication.shared.applicationState == .active else { return }
|
||||
guard #available(iOS 27.0, *) else { error = "Системный пульт требует iOS 27."; return }
|
||||
busy = true; error = nil
|
||||
let operation = generation
|
||||
let newID = UUID().uuidString
|
||||
id = newID
|
||||
defer { if generation == operation { busy = false } }
|
||||
do {
|
||||
try SystemPlayerCredentials.save(connection, id: newID)
|
||||
_ = try SystemPlayerCredentials.load(newID)
|
||||
let session = try await RemoteMediaSession.start(attributes: SystemPlayerAttributes(id: newID, state: state))
|
||||
guard generation == operation else { try? await session.end(); return }
|
||||
storage = session
|
||||
try await session.requestToBecomeSystemPrimary()
|
||||
guard generation == operation else { try? await session.end(); return }
|
||||
running = true
|
||||
UserDefaults.standard.set(newID, forKey: savedIDKey)
|
||||
} catch {
|
||||
guard generation == operation else { return }
|
||||
self.error = "Не удалось включить системный пульт: " + BridgeError.describe(error)
|
||||
end()
|
||||
}
|
||||
}
|
||||
|
||||
func restore(connection: Connection?) async {
|
||||
guard #available(iOS 27.0, *), id == nil, !busy,
|
||||
let savedID = UserDefaults.standard.string(forKey: savedIDKey) else { return }
|
||||
do {
|
||||
let saved = try SystemPlayerCredentials.load(savedID)
|
||||
guard let connection, saved.fingerprint == connection.fingerprint, saved.token == connection.token else {
|
||||
try SystemPlayerCredentials.delete(savedID)
|
||||
UserDefaults.standard.removeObject(forKey: savedIDKey)
|
||||
for session in try await RemoteMediaSession<SystemPlayerAttributes>.sessions() where session.id == savedID { try await session.end() }
|
||||
return
|
||||
}
|
||||
if let session = try await RemoteMediaSession<SystemPlayerAttributes>.sessions().first(where: { $0.id == savedID }) {
|
||||
storage = session; id = savedID; running = true
|
||||
} else {
|
||||
try SystemPlayerCredentials.delete(savedID)
|
||||
UserDefaults.standard.removeObject(forKey: savedIDKey)
|
||||
}
|
||||
} catch { self.error = BridgeError.describe(error) }
|
||||
}
|
||||
|
||||
func receive(_ state: MediaState, connection: Connection) {
|
||||
guard #available(iOS 27.0, *), running, let id,
|
||||
let session = storage as? RemoteMediaSession<SystemPlayerAttributes> else { return }
|
||||
guard state.hasSession else { end(); return }
|
||||
let track = state.title + "\n" + state.artist
|
||||
let playing = state.playbackStatus == "Playing"
|
||||
guard track != lastTrack || playing != lastPlaying || Date().timeIntervalSince(lastUpdate) >= 5 else { return }
|
||||
lastUpdate = Date(); lastTrack = track; lastPlaying = playing
|
||||
let value = SystemPlayerAttributes(id: id, state: state)
|
||||
let operation = generation
|
||||
enqueue { [weak self] in
|
||||
guard let self, self.generation == operation else { return }
|
||||
do {
|
||||
try SystemPlayerCredentials.save(connection, id: id)
|
||||
try await session.update(value)
|
||||
} catch { self.error = BridgeError.describe(error) }
|
||||
}
|
||||
}
|
||||
|
||||
func end() {
|
||||
generation = UUID()
|
||||
busy = false; running = false
|
||||
if let id {
|
||||
do { try SystemPlayerCredentials.delete(id) }
|
||||
catch { self.error = BridgeError.describe(error) }
|
||||
}
|
||||
UserDefaults.standard.removeObject(forKey: savedIDKey)
|
||||
id = nil
|
||||
if #available(iOS 27.0, *), let session = storage as? RemoteMediaSession<SystemPlayerAttributes> {
|
||||
enqueue { [weak self] in
|
||||
do { try await session.end() }
|
||||
catch { self?.error = BridgeError.describe(error) }
|
||||
}
|
||||
}
|
||||
storage = nil
|
||||
}
|
||||
|
||||
private func enqueue(_ action: @escaping @MainActor () async -> Void) {
|
||||
let previous = updates
|
||||
updates = Task { await previous?.value; await action() }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
<?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>ru</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>MusicBridge</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>EXAppExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>EXExtensionPointIdentifier</key>
|
||||
<string>com.apple.nowplaying.remote-media</string>
|
||||
</dict>
|
||||
<key>MusicBridgeSharedKeychainGroup</key>
|
||||
<string>$(AppIdentifierPrefix)ru.yukinoki.musicbridge.remote-control</string>
|
||||
<key>NSLocalNetworkUsageDescription</key>
|
||||
<string>MusicBridge подключается к вашему ПК, чтобы показывать музыку и управлять воспроизведением.</string>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
<key>NSExceptionDomains</key>
|
||||
<dict>
|
||||
<key>10.0.0.0/8</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>172.16.0.0/12</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>192.168.0.0/16</key>
|
||||
<dict>
|
||||
<key>NSExceptionAllowsInsecureHTTPLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,68 @@
|
||||
import ExtensionFoundation
|
||||
import NowPlaying
|
||||
import Observation
|
||||
import Foundation
|
||||
|
||||
@main
|
||||
struct MusicBridgeRemote: RemoteMediaSessionExtension {
|
||||
var configuration: RemoteMediaSessionExtensionConfiguration<Self> {
|
||||
RemoteMediaSessionExtensionConfiguration(extension: self)
|
||||
}
|
||||
func session(_ attributes: SystemPlayerAttributes) async throws -> PCMediaSession {
|
||||
_ = try SystemPlayerCredentials.load(attributes.id)
|
||||
return PCMediaSession(attributes)
|
||||
}
|
||||
}
|
||||
|
||||
@MainActor @Observable
|
||||
final class PCMediaSession: RemoteMediaSessionRepresentable {
|
||||
let id: String
|
||||
var attributes: SystemPlayerAttributes
|
||||
private let transport = SystemPlayerTransport()
|
||||
init(_ attributes: SystemPlayerAttributes) { id = attributes.id; self.attributes = attributes }
|
||||
|
||||
func update(_ attributes: SystemPlayerAttributes) { self.attributes = attributes }
|
||||
|
||||
var content: (any MediaContentRepresentable)? {
|
||||
guard attributes.hasSession else { return nil }
|
||||
let artwork = attributes.artworkID.map { artworkID in
|
||||
Artwork(id: artworkID) { _ in
|
||||
let saved = try SystemPlayerCredentials.load(self.id)
|
||||
let client = BridgeClient(saved, timeout: 5)
|
||||
defer { client.close() }
|
||||
return try ArtworkRepresentation(data: await client.artwork(artworkID))
|
||||
}
|
||||
}
|
||||
return MusicContent(id: attributes.source + "/" + attributes.title + "/" + attributes.artist,
|
||||
songTitle: attributes.title.isEmpty ? "Без названия" : attributes.title,
|
||||
artistName: attributes.artist, albumName: "", type: .audio,
|
||||
duration: attributes.duration > 0 ? .finite(attributes.duration) : nil, artwork: artwork)
|
||||
}
|
||||
|
||||
var playbackSnapshot: MediaPlaybackSnapshot? {
|
||||
MediaPlaybackSnapshot(state: attributes.playing ? .playing(rate: attributes.rate) : .paused,
|
||||
elapsedTime: attributes.position, timestamp: attributes.timestamp)
|
||||
}
|
||||
|
||||
var commands: [MediaCommand] {
|
||||
[
|
||||
.play { try await self.send(.play) }.enabled(attributes.capabilities.play),
|
||||
.pause { try await self.send(.pause) }.enabled(attributes.capabilities.pause),
|
||||
.next { try await self.send(.next) }.enabled(attributes.capabilities.next),
|
||||
.previous { try await self.send(.previous) }.enabled(attributes.capabilities.previous),
|
||||
.seekToPosition { try await self.send(.seek, value: $0) }.enabled(attributes.capabilities.seek)
|
||||
]
|
||||
}
|
||||
|
||||
var devices: [MediaDevice] {
|
||||
let capabilities: [MediaDevice.Capability] = attributes.volume.map { volume in
|
||||
[.absoluteVolume(Float(volume)) { try await self.send(.volume, value: Double($0)) }]
|
||||
} ?? []
|
||||
return [MediaDevice(id: id, name: "MusicBridge · ПК", type: .speaker, capabilities: capabilities)]
|
||||
}
|
||||
|
||||
private func send(_ action: SystemPlayerAction, value: Double? = nil) async throws {
|
||||
let state = try await transport.send(action, value: value, sessionID: id)
|
||||
attributes = SystemPlayerAttributes(id: id, state: state)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?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>keychain-access-groups</key>
|
||||
<array>
|
||||
<string>$(AppIdentifierPrefix)$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<string>$(AppIdentifierPrefix)ru.yukinoki.musicbridge.remote-control</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,29 @@
|
||||
import Foundation
|
||||
import NowPlaying
|
||||
|
||||
@available(iOS 27.0, *)
|
||||
struct SystemPlayerAttributes: RemoteMediaSessionAttributes {
|
||||
let id: String
|
||||
var title: String
|
||||
var artist: String
|
||||
var source: String
|
||||
var artworkID: String?
|
||||
var playing: Bool
|
||||
var rate: Double
|
||||
var position: Double
|
||||
var duration: Double
|
||||
var timestamp: Date
|
||||
var capabilities: Capabilities
|
||||
var volume: Double?
|
||||
var hasSession: Bool
|
||||
|
||||
init(id: String, state: MediaState, now: Date = Date()) {
|
||||
self.id = id
|
||||
title = String(state.title.prefix(256)); artist = String(state.artist.prefix(256))
|
||||
source = String(state.source.prefix(128)); artworkID = state.artworkId
|
||||
playing = state.playbackStatus == "Playing"; rate = state.playbackRate
|
||||
position = max(0, state.position(elapsed: 0) - state.startSeconds)
|
||||
duration = max(0, state.endSeconds - state.startSeconds)
|
||||
timestamp = now; capabilities = state.capabilities; volume = state.volume; hasSession = state.hasSession
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import Foundation
|
||||
import Security
|
||||
|
||||
enum SystemPlayerCredentials {
|
||||
private static func query(_ id: String) throws -> [String: Any] {
|
||||
guard let group = Bundle.main.object(forInfoDictionaryKey: "MusicBridgeSharedKeychainGroup") as? String,
|
||||
!group.isEmpty, !group.contains("$(") else {
|
||||
throw BridgeError.message("Подпись приложения не настроила общий Keychain для системного пульта.")
|
||||
}
|
||||
return [kSecClass as String: kSecClassGenericPassword, kSecAttrService as String: "MusicBridge.system-player.v1",
|
||||
kSecAttrAccount as String: id, kSecAttrAccessGroup as String: group]
|
||||
}
|
||||
|
||||
static func save(_ connection: Connection, id: String) throws {
|
||||
let query = try query(id)
|
||||
let values: [String: Any] = [kSecValueData as String: try JSONEncoder().encode(connection),
|
||||
kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly]
|
||||
var status = SecItemUpdate(query as CFDictionary, values as CFDictionary)
|
||||
if status == errSecItemNotFound { status = SecItemAdd(query.merging(values) { _, v in v } as CFDictionary, nil) }
|
||||
guard status == errSecSuccess else { throw failure(status) }
|
||||
}
|
||||
|
||||
static func load(_ id: String) throws -> Connection {
|
||||
var query = try query(id)
|
||||
query[kSecReturnData as String] = true; query[kSecMatchLimit as String] = kSecMatchLimitOne
|
||||
var item: CFTypeRef?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
||||
guard status == errSecSuccess, let data = item as? Data else { throw failure(status) }
|
||||
let saved = try JSONDecoder().decode(Connection.self, from: data)
|
||||
guard let token = saved.token, Connection.isFingerprint(token) else { throw BridgeError.unauthorized }
|
||||
return try Connection(endpoint: saved.endpoint.absoluteString, fingerprint: saved.fingerprint, token: token)
|
||||
}
|
||||
|
||||
static func delete(_ id: String) throws {
|
||||
let status = SecItemDelete(try query(id) as CFDictionary)
|
||||
guard status == errSecSuccess || status == errSecItemNotFound else { throw failure(status) }
|
||||
}
|
||||
|
||||
private static func failure(_ status: OSStatus) -> BridgeError {
|
||||
.message("Системный пульт не получил доступ к Keychain (\(status)). Проверьте подпись приложения и расширения; обычный плеер остаётся доступен.")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import Foundation
|
||||
|
||||
enum SystemPlayerAction { case play, pause, next, previous, seek, volume }
|
||||
|
||||
@MainActor
|
||||
final class SystemPlayerTransport {
|
||||
private var busy = false
|
||||
|
||||
func send(_ action: SystemPlayerAction, value: Double? = nil, sessionID: String) async throws -> MediaState {
|
||||
guard !busy else { throw BridgeError.message("Предыдущая команда ещё выполняется.") }
|
||||
busy = true
|
||||
defer { busy = false }
|
||||
let saved = try SystemPlayerCredentials.load(sessionID)
|
||||
let client = BridgeClient(saved, timeout: 5)
|
||||
defer { client.close() }
|
||||
return try await Self.execute(action, value: value, client: client) {
|
||||
let current = try SystemPlayerCredentials.load(sessionID)
|
||||
guard current.endpoint == saved.endpoint, current.fingerprint == saved.fingerprint, current.token == saved.token else {
|
||||
throw BridgeError.unauthorized
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func execute(_ action: SystemPlayerAction, value: Double? = nil, client: BridgeClient,
|
||||
validate: () throws -> Void = {}) async throws -> MediaState {
|
||||
let state = try await client.state()
|
||||
try validate()
|
||||
let command = try command(action, value: value, state: state)
|
||||
do { try await client.command(command) }
|
||||
catch {
|
||||
if error is URLError { throw BridgeError.message("Результат команды неизвестен. Откройте плеер перед повтором.") }
|
||||
throw error
|
||||
}
|
||||
do { return try await client.state() }
|
||||
catch {
|
||||
if case BridgeError.unauthorized = error { throw error }
|
||||
throw BridgeError.message("Команда выполнена, но новый трек не получен. Откройте приложение для обновления.")
|
||||
}
|
||||
}
|
||||
|
||||
static func command(_ action: SystemPlayerAction, value: Double?, state: MediaState) throws -> Command {
|
||||
switch action {
|
||||
case .play where state.hasSession && state.capabilities.play: return Command(type: "play", value: nil)
|
||||
case .pause where state.hasSession && state.capabilities.pause: return Command(type: "pause", value: nil)
|
||||
case .next where state.hasSession && state.capabilities.next: return Command(type: "next", value: nil)
|
||||
case .previous where state.hasSession && state.capabilities.previous: return Command(type: "previous", value: nil)
|
||||
case .seek where state.hasSession && state.capabilities.seek && state.maxSeekSeconds > state.minSeekSeconds:
|
||||
guard let value, value.isFinite else { throw BridgeError.message("Некорректная позиция трека.") }
|
||||
return Command(type: "seek", value: min(state.maxSeekSeconds, max(state.minSeekSeconds, state.startSeconds + value)))
|
||||
case .volume where state.volume != nil:
|
||||
guard let value, value.isFinite else { throw BridgeError.message("Некорректная громкость.") }
|
||||
return Command(type: "volume", value: min(1, max(0, value)))
|
||||
default: throw BridgeError.message("Плеер на ПК не поддерживает эту команду.")
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
-11
@@ -37,6 +37,13 @@ def source_group(folder):
|
||||
return add(folder, "PBXGroup", path=folder, sourceTree="<group>", children=refs), builds
|
||||
|
||||
app_group, app_sources = source_group("MusicBridge")
|
||||
remote_shared_group, remote_shared_sources = source_group("RemoteShared")
|
||||
remote_group, remote_sources = source_group("MusicBridgeRemote")
|
||||
remote_sources += [add("remote-shared/" + str(index), "PBXBuildFile", fileRef=objects[build]["fileRef"])
|
||||
for index, build in enumerate(remote_shared_sources)]
|
||||
for filename in ("Models.swift", "BridgeClient.swift"):
|
||||
remote_sources.append(add("remote-core/" + filename, "PBXBuildFile", fileRef=uid("MusicBridge/" + filename)))
|
||||
app_sources += remote_shared_sources
|
||||
shared_group, shared_sources = source_group("Shared")
|
||||
widget_group, widget_sources = source_group("MusicBridgeWidgets")
|
||||
app_sources += shared_sources
|
||||
@@ -54,16 +61,18 @@ objects[test_group]["children"].append(tls_fixture)
|
||||
tls_fixture_build = add("tls-fixture-build", "PBXBuildFile", fileRef=tls_fixture)
|
||||
products = []
|
||||
targets = []
|
||||
for name, sources, is_test in [("MusicBridge", app_sources, False), ("MusicBridgeTests", test_sources, True), ("MusicBridgeWidgets", widget_sources, False)]:
|
||||
for name, sources, is_test in [("MusicBridge", app_sources, False), ("MusicBridgeTests", test_sources, True), ("MusicBridgeWidgets", widget_sources, False), ("MusicBridgeRemote", remote_sources, False)]:
|
||||
is_widget = name == "MusicBridgeWidgets"
|
||||
product = add(name + "product", "PBXFileReference", explicitFileType="wrapper.app-extension" if is_widget else "wrapper.cfbundle" if is_test else "wrapper.application",
|
||||
includeInIndex=0, path=name + (".appex" if is_widget else ".xctest" if is_test else ".app"), sourceTree="BUILT_PRODUCTS_DIR")
|
||||
is_remote = name == "MusicBridgeRemote"
|
||||
is_extension = is_widget or is_remote
|
||||
product = add(name + "product", "PBXFileReference", explicitFileType="wrapper.extensionkit-extension" if is_remote else "wrapper.app-extension" if is_extension else "wrapper.cfbundle" if is_test else "wrapper.application",
|
||||
includeInIndex=0, path=name + (".appex" if is_extension else ".xctest" if is_test else ".app"), sourceTree="BUILT_PRODUCTS_DIR")
|
||||
products.append(product)
|
||||
phases = [add(name + "sources", "PBXSourcesBuildPhase", buildActionMask=2147483647, files=sources, runOnlyForDeploymentPostprocessing=0),
|
||||
add(name + "frameworks", "PBXFrameworksBuildPhase", buildActionMask=2147483647, files=[], runOnlyForDeploymentPostprocessing=0),
|
||||
add(name + "resources", "PBXResourcesBuildPhase", buildActionMask=2147483647, files=[] if is_widget else [fixture_build, tls_fixture_build] if is_test else [assets_build], runOnlyForDeploymentPostprocessing=0)]
|
||||
settings = dict(PRODUCT_NAME="$(TARGET_NAME)", PRODUCT_BUNDLE_IDENTIFIER="ru.yukinoki.musicbridge" + (".widgets" if is_widget else ".tests" if is_test else ""),
|
||||
SWIFT_VERSION="5.0", IPHONEOS_DEPLOYMENT_TARGET="17.0", SDKROOT="iphoneos",
|
||||
add(name + "resources", "PBXResourcesBuildPhase", buildActionMask=2147483647, files=[] if is_extension else [fixture_build, tls_fixture_build] if is_test else [assets_build], runOnlyForDeploymentPostprocessing=0)]
|
||||
settings = dict(PRODUCT_NAME="$(TARGET_NAME)", PRODUCT_BUNDLE_IDENTIFIER="ru.yukinoki.musicbridge" + (".remote" if is_remote else ".widgets" if is_widget else ".tests" if is_test else ""),
|
||||
SWIFT_VERSION="5.0", IPHONEOS_DEPLOYMENT_TARGET="27.0" if is_remote else "17.0", SDKROOT="iphoneos",
|
||||
SUPPORTED_PLATFORMS="iphoneos iphonesimulator", TARGETED_DEVICE_FAMILY="1", CODE_SIGN_STYLE="Automatic",
|
||||
GENERATE_INFOPLIST_FILE="YES" if is_test else "NO")
|
||||
deps = []
|
||||
@@ -71,24 +80,32 @@ for name, sources, is_test in [("MusicBridge", app_sources, False), ("MusicBridg
|
||||
settings.update(TEST_HOST="$(BUILT_PRODUCTS_DIR)/MusicBridge.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/MusicBridge", BUNDLE_LOADER="$(TEST_HOST)")
|
||||
proxy = add("test-proxy", "PBXContainerItemProxy", containerPortal=uid("project"), proxyType=1, remoteGlobalIDString=uid("MusicBridgetarget"), remoteInfo="MusicBridge")
|
||||
deps = [add("test-dependency", "PBXTargetDependency", target=uid("MusicBridgetarget"), targetProxy=proxy)]
|
||||
elif is_widget:
|
||||
settings.update(INFOPLIST_FILE="MusicBridgeWidgets/Info.plist", CURRENT_PROJECT_VERSION="7", MARKETING_VERSION="0.6.0",
|
||||
elif is_extension:
|
||||
settings.update(INFOPLIST_FILE=name + "/Info.plist", CURRENT_PROJECT_VERSION="8", MARKETING_VERSION="0.7.0",
|
||||
APPLICATION_EXTENSION_API_ONLY="YES", SKIP_INSTALL="YES",
|
||||
LD_RUNPATH_SEARCH_PATHS="$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks")
|
||||
if is_remote:
|
||||
settings.update(CODE_SIGN_ENTITLEMENTS="MusicBridgeRemote/Remote.entitlements")
|
||||
else:
|
||||
settings.update(INFOPLIST_FILE="MusicBridge/Info.plist", CURRENT_PROJECT_VERSION="7", MARKETING_VERSION="0.6.0", ASSETCATALOG_COMPILER_APPICON_NAME="AppIcon")
|
||||
settings.update(INFOPLIST_FILE="MusicBridge/Info.plist", CURRENT_PROJECT_VERSION="8", MARKETING_VERSION="0.7.0", ASSETCATALOG_COMPILER_APPICON_NAME="AppIcon", CODE_SIGN_ENTITLEMENTS="MusicBridge/App.entitlements")
|
||||
embed = add("embed-widget", "PBXBuildFile", fileRef=uid("MusicBridgeWidgetsproduct"), settings=dict(ATTRIBUTES=["RemoveHeadersOnCopy"]))
|
||||
embed_remote = add("embed-remote", "PBXBuildFile", fileRef=uid("MusicBridgeRemoteproduct"), settings=dict(ATTRIBUTES=["RemoveHeadersOnCopy"]))
|
||||
phases.append(add("embed-extensions", "PBXCopyFilesBuildPhase", buildActionMask=2147483647,
|
||||
dstPath="", dstSubfolderSpec=13, files=[embed], name="Embed App Extensions", runOnlyForDeploymentPostprocessing=0))
|
||||
phases.append(add("embed-remote-extensions", "PBXCopyFilesBuildPhase", buildActionMask=2147483647,
|
||||
dstPath="$(EXTENSIONS_FOLDER_PATH)", dstSubfolderSpec=16, files=[embed_remote], name="Embed ExtensionKit Extensions", runOnlyForDeploymentPostprocessing=0))
|
||||
proxy = add("widget-proxy", "PBXContainerItemProxy", containerPortal=uid("project"), proxyType=1,
|
||||
remoteGlobalIDString=uid("MusicBridgeWidgetstarget"), remoteInfo="MusicBridgeWidgets")
|
||||
deps = [add("widget-dependency", "PBXTargetDependency", target=uid("MusicBridgeWidgetstarget"), targetProxy=proxy)]
|
||||
remote_proxy = add("remote-proxy", "PBXContainerItemProxy", containerPortal=uid("project"), proxyType=1,
|
||||
remoteGlobalIDString=uid("MusicBridgeRemotetarget"), remoteInfo="MusicBridgeRemote")
|
||||
deps.append(add("remote-dependency", "PBXTargetDependency", target=uid("MusicBridgeRemotetarget"), targetProxy=remote_proxy))
|
||||
targets.append(add(name + "target", "PBXNativeTarget", name=name, productName=name, productReference=product,
|
||||
productType="com.apple.product-type.app-extension" if is_widget else "com.apple.product-type.bundle.unit-test" if is_test else "com.apple.product-type.application",
|
||||
productType="com.apple.product-type.extensionkit-extension" if is_remote else "com.apple.product-type.app-extension" if is_widget else "com.apple.product-type.bundle.unit-test" if is_test else "com.apple.product-type.application",
|
||||
buildConfigurationList=configurations(name, settings), buildPhases=phases, buildRules=[], dependencies=deps))
|
||||
|
||||
product_group = add("products", "PBXGroup", name="Products", sourceTree="<group>", children=products)
|
||||
main_group = add("main", "PBXGroup", sourceTree="<group>", children=[app_group, shared_group, widget_group, test_group, product_group])
|
||||
main_group = add("main", "PBXGroup", sourceTree="<group>", children=[app_group, shared_group, widget_group, remote_shared_group, remote_group, test_group, product_group])
|
||||
project = add("project", "PBXProject", attributes=dict(LastUpgradeCheck="1600", BuildIndependentTargetsInParallel="YES"),
|
||||
buildConfigurationList=configurations("project", dict(CLANG_ENABLE_MODULES="YES", CLANG_ENABLE_OBJC_ARC="YES")),
|
||||
compatibilityVersion="Xcode 14.0", developmentRegion="ru", knownRegions=["ru", "en", "Base"],
|
||||
@@ -144,4 +161,16 @@ widget_info = dict(CFBundleDevelopmentRegion="ru", CFBundleDisplayName="MusicBri
|
||||
CFBundleShortVersionString="$(MARKETING_VERSION)", CFBundleVersion="$(CURRENT_PROJECT_VERSION)",
|
||||
NSExtension=dict(NSExtensionPointIdentifier="com.apple.widgetkit-extension"))
|
||||
(ROOT / "MusicBridgeWidgets" / "Info.plist").write_bytes(plistlib.dumps(widget_info, sort_keys=False))
|
||||
shared_group_name = "$(AppIdentifierPrefix)ru.yukinoki.musicbridge.remote-control"
|
||||
info["MusicBridgeSharedKeychainGroup"] = shared_group_name
|
||||
(ROOT / "MusicBridge" / "Info.plist").write_bytes(plistlib.dumps(info, sort_keys=False))
|
||||
remote_info = {k: v for k, v in widget_info.items() if k != "NSExtension"}
|
||||
remote_info.update(EXAppExtensionAttributes=dict(EXExtensionPointIdentifier="com.apple.nowplaying.remote-media"),
|
||||
MusicBridgeSharedKeychainGroup=shared_group_name,
|
||||
NSLocalNetworkUsageDescription=info["NSLocalNetworkUsageDescription"],
|
||||
NSAppTransportSecurity=info["NSAppTransportSecurity"])
|
||||
(ROOT / "MusicBridgeRemote" / "Info.plist").write_bytes(plistlib.dumps(remote_info, sort_keys=False))
|
||||
for folder, filename in (("MusicBridge", "App.entitlements"), ("MusicBridgeRemote", "Remote.entitlements")):
|
||||
groups = ["$(AppIdentifierPrefix)$(PRODUCT_BUNDLE_IDENTIFIER)", shared_group_name]
|
||||
(ROOT / folder / filename).write_bytes(plistlib.dumps({"keychain-access-groups": groups}))
|
||||
print("Generated Xcode project, shared scheme and Info.plist.")
|
||||
|
||||
@@ -11,12 +11,13 @@ import xml.etree.ElementTree as ET
|
||||
root = pathlib.Path(__file__).resolve().parents[1]
|
||||
generated = [root / "MusicBridge.xcodeproj/project.pbxproj",
|
||||
root / "MusicBridge.xcodeproj/xcshareddata/xcschemes/MusicBridge.xcscheme",
|
||||
root / "MusicBridge/Info.plist", root / "MusicBridgeWidgets/Info.plist"]
|
||||
root / "MusicBridge/Info.plist", root / "MusicBridgeWidgets/Info.plist",
|
||||
root / "MusicBridgeRemote/Info.plist", root / "MusicBridge/App.entitlements", root / "MusicBridgeRemote/Remote.entitlements"]
|
||||
before = [p.read_bytes() for p in generated]
|
||||
subprocess.run([sys.executable, str(root / "tools/generate_project.py")], check=True)
|
||||
assert before == [p.read_bytes() for p in generated], "Generated project is stale; review and commit regeneration."
|
||||
project = generated[0].read_text(encoding="utf-8")
|
||||
for folder in ("MusicBridge", "MusicBridgeTests", "Shared", "MusicBridgeWidgets"):
|
||||
for folder in ("MusicBridge", "MusicBridgeTests", "Shared", "MusicBridgeWidgets", "RemoteShared", "MusicBridgeRemote"):
|
||||
for source in (root / folder).glob("*.swift"):
|
||||
key = hashlib.sha256(source.relative_to(root).as_posix().encode()).hexdigest()[:24].upper()
|
||||
assert key in project, f"Missing source: {source.name}"
|
||||
|
||||
Reference in New Issue
Block a user