Add local Live Activity and Dynamic Island track display
This commit is contained in:
@@ -86,6 +86,13 @@ jobs:
|
|||||||
xcrun simctl launch "$SIMULATOR_ID" ru.yukinoki.musicbridge --preview-player --preview-large-text
|
xcrun simctl launch "$SIMULATOR_ID" ru.yukinoki.musicbridge --preview-player --preview-large-text
|
||||||
sleep 2
|
sleep 2
|
||||||
xcrun simctl io "$SIMULATOR_ID" screenshot build/player-large-text-screen.png
|
xcrun simctl io "$SIMULATOR_ID" screenshot build/player-large-text-screen.png
|
||||||
|
xcrun simctl terminate "$SIMULATOR_ID" ru.yukinoki.musicbridge
|
||||||
|
xcrun simctl launch "$SIMULATOR_ID" ru.yukinoki.musicbridge --preview-player --preview-live-card
|
||||||
|
sleep 2
|
||||||
|
xcrun simctl io "$SIMULATOR_ID" screenshot build/live-card-dark-screen.png
|
||||||
|
xcrun simctl ui "$SIMULATOR_ID" appearance light
|
||||||
|
sleep 1
|
||||||
|
xcrun simctl io "$SIMULATOR_ID" screenshot build/live-card-light-screen.png
|
||||||
RUNTIME=$(python3 -c 'import json,os; d=json.load(open("/tmp/musicbridge-simulators.json")); print(next(k for k,v in d["devices"].items() if any(x["udid"]==os.environ["SIMULATOR_ID"] for x in v)))')
|
RUNTIME=$(python3 -c 'import json,os; d=json.load(open("/tmp/musicbridge-simulators.json")); print(next(k for k,v in d["devices"].items() if any(x["udid"]==os.environ["SIMULATOR_ID"] for x in v)))')
|
||||||
COMPACT_ID=$(xcrun simctl create MusicBridge-Compact com.apple.CoreSimulator.SimDeviceType.iPhone-SE-3rd-generation "$RUNTIME")
|
COMPACT_ID=$(xcrun simctl create MusicBridge-Compact com.apple.CoreSimulator.SimDeviceType.iPhone-SE-3rd-generation "$RUNTIME")
|
||||||
xcrun simctl bootstatus "$COMPACT_ID" -b
|
xcrun simctl bootstatus "$COMPACT_ID" -b
|
||||||
|
|||||||
@@ -55,6 +55,16 @@
|
|||||||
"isa" = "PBXBuildFile";
|
"isa" = "PBXBuildFile";
|
||||||
"fileRef" = "76FB6BFFC478599567424755";
|
"fileRef" = "76FB6BFFC478599567424755";
|
||||||
};
|
};
|
||||||
|
"EE2357BD5CA3755AFEB3171A" = {
|
||||||
|
"isa" = "PBXFileReference";
|
||||||
|
"lastKnownFileType" = "sourcecode.swift";
|
||||||
|
"path" = "LiveActivityManager.swift";
|
||||||
|
"sourceTree" = "<group>";
|
||||||
|
};
|
||||||
|
"181CACDC331104A1E16CFC20" = {
|
||||||
|
"isa" = "PBXBuildFile";
|
||||||
|
"fileRef" = "EE2357BD5CA3755AFEB3171A";
|
||||||
|
};
|
||||||
"124CF3F5AC075D1D6042DF64" = {
|
"124CF3F5AC075D1D6042DF64" = {
|
||||||
"isa" = "PBXFileReference";
|
"isa" = "PBXFileReference";
|
||||||
"lastKnownFileType" = "sourcecode.swift";
|
"lastKnownFileType" = "sourcecode.swift";
|
||||||
@@ -109,7 +119,57 @@
|
|||||||
"isa" = "PBXGroup";
|
"isa" = "PBXGroup";
|
||||||
"path" = "MusicBridge";
|
"path" = "MusicBridge";
|
||||||
"sourceTree" = "<group>";
|
"sourceTree" = "<group>";
|
||||||
"children" = ("065A9D559DC80AAF876DA757", "763FA6A137E5E94274AC56A3", "971C0AE92F167DE7AB145086", "5F6F408829AB46D3B85FB911", "76FB6BFFC478599567424755", "124CF3F5AC075D1D6042DF64", "ADF0CD56882201BD3AC1581B", "9C432A0DA0BEC173839EBF86", "6B0F2027221CDD63E8FD4C59", "7148B52E915554F67E875C26", "8BF729FFE074CAEE622C0292",);
|
"children" = ("065A9D559DC80AAF876DA757", "763FA6A137E5E94274AC56A3", "971C0AE92F167DE7AB145086", "5F6F408829AB46D3B85FB911", "76FB6BFFC478599567424755", "EE2357BD5CA3755AFEB3171A", "124CF3F5AC075D1D6042DF64", "ADF0CD56882201BD3AC1581B", "9C432A0DA0BEC173839EBF86", "6B0F2027221CDD63E8FD4C59", "7148B52E915554F67E875C26", "8BF729FFE074CAEE622C0292",);
|
||||||
|
};
|
||||||
|
"7C5E653CCD928B143AB68F09" = {
|
||||||
|
"isa" = "PBXFileReference";
|
||||||
|
"lastKnownFileType" = "sourcecode.swift";
|
||||||
|
"path" = "MusicActivityAttributes.swift";
|
||||||
|
"sourceTree" = "<group>";
|
||||||
|
};
|
||||||
|
"C7506B09240305F44C61EB60" = {
|
||||||
|
"isa" = "PBXBuildFile";
|
||||||
|
"fileRef" = "7C5E653CCD928B143AB68F09";
|
||||||
|
};
|
||||||
|
"7C0FE298B442263CA281ABB7" = {
|
||||||
|
"isa" = "PBXFileReference";
|
||||||
|
"lastKnownFileType" = "sourcecode.swift";
|
||||||
|
"path" = "MusicActivityViews.swift";
|
||||||
|
"sourceTree" = "<group>";
|
||||||
|
};
|
||||||
|
"4FD4638045EDA8D7CCFD00AF" = {
|
||||||
|
"isa" = "PBXBuildFile";
|
||||||
|
"fileRef" = "7C0FE298B442263CA281ABB7";
|
||||||
|
};
|
||||||
|
"E3C4B39D6D5013477C98CD58" = {
|
||||||
|
"isa" = "PBXGroup";
|
||||||
|
"path" = "Shared";
|
||||||
|
"sourceTree" = "<group>";
|
||||||
|
"children" = ("7C5E653CCD928B143AB68F09", "7C0FE298B442263CA281ABB7",);
|
||||||
|
};
|
||||||
|
"ADFE24674E0FB119E649EC54" = {
|
||||||
|
"isa" = "PBXFileReference";
|
||||||
|
"lastKnownFileType" = "sourcecode.swift";
|
||||||
|
"path" = "MusicBridgeWidgets.swift";
|
||||||
|
"sourceTree" = "<group>";
|
||||||
|
};
|
||||||
|
"F942C4AC2D1ABF86575E779A" = {
|
||||||
|
"isa" = "PBXBuildFile";
|
||||||
|
"fileRef" = "ADFE24674E0FB119E649EC54";
|
||||||
|
};
|
||||||
|
"F932ED2EB70210651D1121CB" = {
|
||||||
|
"isa" = "PBXGroup";
|
||||||
|
"path" = "MusicBridgeWidgets";
|
||||||
|
"sourceTree" = "<group>";
|
||||||
|
"children" = ("ADFE24674E0FB119E649EC54",);
|
||||||
|
};
|
||||||
|
"D739DF07A523D6FA1D29EECD" = {
|
||||||
|
"isa" = "PBXBuildFile";
|
||||||
|
"fileRef" = "7C5E653CCD928B143AB68F09";
|
||||||
|
};
|
||||||
|
"E41BBFD90B0D341CC92C100E" = {
|
||||||
|
"isa" = "PBXBuildFile";
|
||||||
|
"fileRef" = "7C0FE298B442263CA281ABB7";
|
||||||
};
|
};
|
||||||
"8BF729FFE074CAEE622C0292" = {
|
"8BF729FFE074CAEE622C0292" = {
|
||||||
"isa" = "PBXFileReference";
|
"isa" = "PBXFileReference";
|
||||||
@@ -131,6 +191,16 @@
|
|||||||
"isa" = "PBXBuildFile";
|
"isa" = "PBXBuildFile";
|
||||||
"fileRef" = "C9F705D432CA45B6056BC572";
|
"fileRef" = "C9F705D432CA45B6056BC572";
|
||||||
};
|
};
|
||||||
|
"9819FF7CC21CAAD1BDB8D1B8" = {
|
||||||
|
"isa" = "PBXFileReference";
|
||||||
|
"lastKnownFileType" = "sourcecode.swift";
|
||||||
|
"path" = "LiveActivityTests.swift";
|
||||||
|
"sourceTree" = "<group>";
|
||||||
|
};
|
||||||
|
"BD0B3FAC7F1259D249134283" = {
|
||||||
|
"isa" = "PBXBuildFile";
|
||||||
|
"fileRef" = "9819FF7CC21CAAD1BDB8D1B8";
|
||||||
|
};
|
||||||
"ACD0393DCDDDF044F2966340" = {
|
"ACD0393DCDDDF044F2966340" = {
|
||||||
"isa" = "PBXFileReference";
|
"isa" = "PBXFileReference";
|
||||||
"lastKnownFileType" = "sourcecode.swift";
|
"lastKnownFileType" = "sourcecode.swift";
|
||||||
@@ -165,7 +235,7 @@
|
|||||||
"isa" = "PBXGroup";
|
"isa" = "PBXGroup";
|
||||||
"path" = "MusicBridgeTests";
|
"path" = "MusicBridgeTests";
|
||||||
"sourceTree" = "<group>";
|
"sourceTree" = "<group>";
|
||||||
"children" = ("C9F705D432CA45B6056BC572", "ACD0393DCDDDF044F2966340", "464F6C65BC2FAB6BCCC77F46", "8BED860F1BD59C500D215E8D", "F16D05EC6B29248D2C61ADB1", "4731785CAB9BB914B8834AE6",);
|
"children" = ("C9F705D432CA45B6056BC572", "9819FF7CC21CAAD1BDB8D1B8", "ACD0393DCDDDF044F2966340", "464F6C65BC2FAB6BCCC77F46", "8BED860F1BD59C500D215E8D", "F16D05EC6B29248D2C61ADB1", "4731785CAB9BB914B8834AE6",);
|
||||||
};
|
};
|
||||||
"F16D05EC6B29248D2C61ADB1" = {
|
"F16D05EC6B29248D2C61ADB1" = {
|
||||||
"isa" = "PBXFileReference";
|
"isa" = "PBXFileReference";
|
||||||
@@ -197,7 +267,7 @@
|
|||||||
"72B74C8A2143F64FD53D71E5" = {
|
"72B74C8A2143F64FD53D71E5" = {
|
||||||
"isa" = "PBXSourcesBuildPhase";
|
"isa" = "PBXSourcesBuildPhase";
|
||||||
"buildActionMask" = 2147483647;
|
"buildActionMask" = 2147483647;
|
||||||
"files" = ("82F1C33CE4F8B8F641FF0461", "40D2920E2C996D62B7190BA4", "16EA630DE1FE9F0A16580C9D", "842E87CD23DCB5BBCA4340F6", "DA06B3B12E52FB69A6A34887", "DEDCCD95A140162B6D654871", "48E2FDCA11A6694953C86E6D", "47693934E8A974162CF08A38", "287044ADBD143F929035BC61", "BE808A0D13A16FE67D32B7FB",);
|
"files" = ("82F1C33CE4F8B8F641FF0461", "40D2920E2C996D62B7190BA4", "16EA630DE1FE9F0A16580C9D", "842E87CD23DCB5BBCA4340F6", "DA06B3B12E52FB69A6A34887", "181CACDC331104A1E16CFC20", "DEDCCD95A140162B6D654871", "48E2FDCA11A6694953C86E6D", "47693934E8A974162CF08A38", "287044ADBD143F929035BC61", "BE808A0D13A16FE67D32B7FB", "C7506B09240305F44C61EB60", "4FD4638045EDA8D7CCFD00AF",);
|
||||||
"runOnlyForDeploymentPostprocessing" = 0;
|
"runOnlyForDeploymentPostprocessing" = 0;
|
||||||
};
|
};
|
||||||
"734C4650C7A5604092763C32" = {
|
"734C4650C7A5604092763C32" = {
|
||||||
@@ -212,6 +282,34 @@
|
|||||||
"files" = ("5522B3B3B2EACC1C8F9BEAAC",);
|
"files" = ("5522B3B3B2EACC1C8F9BEAAC",);
|
||||||
"runOnlyForDeploymentPostprocessing" = 0;
|
"runOnlyForDeploymentPostprocessing" = 0;
|
||||||
};
|
};
|
||||||
|
"310BC1A83962BC738E0B4980" = {
|
||||||
|
"isa" = "PBXBuildFile";
|
||||||
|
"fileRef" = "CE9225C1B8E80F534681689F";
|
||||||
|
"settings" = {
|
||||||
|
"ATTRIBUTES" = ("RemoveHeadersOnCopy",);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
"AFC32963E7959E7A34264826" = {
|
||||||
|
"isa" = "PBXCopyFilesBuildPhase";
|
||||||
|
"buildActionMask" = 2147483647;
|
||||||
|
"dstPath" = "";
|
||||||
|
"dstSubfolderSpec" = 13;
|
||||||
|
"files" = ("310BC1A83962BC738E0B4980",);
|
||||||
|
"name" = "Embed App Extensions";
|
||||||
|
"runOnlyForDeploymentPostprocessing" = 0;
|
||||||
|
};
|
||||||
|
"01E77404E10F254C43E64C55" = {
|
||||||
|
"isa" = "PBXContainerItemProxy";
|
||||||
|
"containerPortal" = "244210E48437B6556980A702";
|
||||||
|
"proxyType" = 1;
|
||||||
|
"remoteGlobalIDString" = "BFE9A9DC8C194372006C3ACC";
|
||||||
|
"remoteInfo" = "MusicBridgeWidgets";
|
||||||
|
};
|
||||||
|
"A4C2D386AB0259F33DCDC427" = {
|
||||||
|
"isa" = "PBXTargetDependency";
|
||||||
|
"target" = "BFE9A9DC8C194372006C3ACC";
|
||||||
|
"targetProxy" = "01E77404E10F254C43E64C55";
|
||||||
|
};
|
||||||
"E3322BA79DBD9A84650B2CAE" = {
|
"E3322BA79DBD9A84650B2CAE" = {
|
||||||
"isa" = "XCBuildConfiguration";
|
"isa" = "XCBuildConfiguration";
|
||||||
"name" = "Debug";
|
"name" = "Debug";
|
||||||
@@ -226,8 +324,8 @@
|
|||||||
"CODE_SIGN_STYLE" = "Automatic";
|
"CODE_SIGN_STYLE" = "Automatic";
|
||||||
"GENERATE_INFOPLIST_FILE" = "NO";
|
"GENERATE_INFOPLIST_FILE" = "NO";
|
||||||
"INFOPLIST_FILE" = "MusicBridge/Info.plist";
|
"INFOPLIST_FILE" = "MusicBridge/Info.plist";
|
||||||
"CURRENT_PROJECT_VERSION" = "5";
|
"CURRENT_PROJECT_VERSION" = "6";
|
||||||
"MARKETING_VERSION" = "0.4.0";
|
"MARKETING_VERSION" = "0.5.0";
|
||||||
"ASSETCATALOG_COMPILER_APPICON_NAME" = "AppIcon";
|
"ASSETCATALOG_COMPILER_APPICON_NAME" = "AppIcon";
|
||||||
"SWIFT_OPTIMIZATION_LEVEL" = "-Onone";
|
"SWIFT_OPTIMIZATION_LEVEL" = "-Onone";
|
||||||
"ENABLE_TESTABILITY" = "YES";
|
"ENABLE_TESTABILITY" = "YES";
|
||||||
@@ -248,8 +346,8 @@
|
|||||||
"CODE_SIGN_STYLE" = "Automatic";
|
"CODE_SIGN_STYLE" = "Automatic";
|
||||||
"GENERATE_INFOPLIST_FILE" = "NO";
|
"GENERATE_INFOPLIST_FILE" = "NO";
|
||||||
"INFOPLIST_FILE" = "MusicBridge/Info.plist";
|
"INFOPLIST_FILE" = "MusicBridge/Info.plist";
|
||||||
"CURRENT_PROJECT_VERSION" = "5";
|
"CURRENT_PROJECT_VERSION" = "6";
|
||||||
"MARKETING_VERSION" = "0.4.0";
|
"MARKETING_VERSION" = "0.5.0";
|
||||||
"ASSETCATALOG_COMPILER_APPICON_NAME" = "AppIcon";
|
"ASSETCATALOG_COMPILER_APPICON_NAME" = "AppIcon";
|
||||||
"SWIFT_OPTIMIZATION_LEVEL" = "-O";
|
"SWIFT_OPTIMIZATION_LEVEL" = "-O";
|
||||||
};
|
};
|
||||||
@@ -267,9 +365,9 @@
|
|||||||
"productReference" = "7C35A0276A762371ABAE5596";
|
"productReference" = "7C35A0276A762371ABAE5596";
|
||||||
"productType" = "com.apple.product-type.application";
|
"productType" = "com.apple.product-type.application";
|
||||||
"buildConfigurationList" = "C6336ED5908EABEBF60AE675";
|
"buildConfigurationList" = "C6336ED5908EABEBF60AE675";
|
||||||
"buildPhases" = ("72B74C8A2143F64FD53D71E5", "734C4650C7A5604092763C32", "69816CB820E8173702C54C37",);
|
"buildPhases" = ("72B74C8A2143F64FD53D71E5", "734C4650C7A5604092763C32", "69816CB820E8173702C54C37", "AFC32963E7959E7A34264826",);
|
||||||
"buildRules" = ();
|
"buildRules" = ();
|
||||||
"dependencies" = ();
|
"dependencies" = ("A4C2D386AB0259F33DCDC427",);
|
||||||
};
|
};
|
||||||
"5E4BE508157A7B2CB7B4C2F2" = {
|
"5E4BE508157A7B2CB7B4C2F2" = {
|
||||||
"isa" = "PBXFileReference";
|
"isa" = "PBXFileReference";
|
||||||
@@ -281,7 +379,7 @@
|
|||||||
"AB453A3F0E1490D48374A69A" = {
|
"AB453A3F0E1490D48374A69A" = {
|
||||||
"isa" = "PBXSourcesBuildPhase";
|
"isa" = "PBXSourcesBuildPhase";
|
||||||
"buildActionMask" = 2147483647;
|
"buildActionMask" = 2147483647;
|
||||||
"files" = ("1DD7BB0437E9F53586831F22", "17E248E58CB43C680B48DD51", "EFDB280E0B23F7E2A246BC89", "DC1A5B0B94F4C98F1CA36676",);
|
"files" = ("1DD7BB0437E9F53586831F22", "BD0B3FAC7F1259D249134283", "17E248E58CB43C680B48DD51", "EFDB280E0B23F7E2A246BC89", "DC1A5B0B94F4C98F1CA36676",);
|
||||||
"runOnlyForDeploymentPostprocessing" = 0;
|
"runOnlyForDeploymentPostprocessing" = 0;
|
||||||
};
|
};
|
||||||
"9AA2A4C344B5C474AF5D336C" = {
|
"9AA2A4C344B5C474AF5D336C" = {
|
||||||
@@ -363,16 +461,104 @@
|
|||||||
"buildRules" = ();
|
"buildRules" = ();
|
||||||
"dependencies" = ("F9A94CEF8CF7D85CE881E703",);
|
"dependencies" = ("F9A94CEF8CF7D85CE881E703",);
|
||||||
};
|
};
|
||||||
|
"CE9225C1B8E80F534681689F" = {
|
||||||
|
"isa" = "PBXFileReference";
|
||||||
|
"explicitFileType" = "wrapper.app-extension";
|
||||||
|
"includeInIndex" = 0;
|
||||||
|
"path" = "MusicBridgeWidgets.appex";
|
||||||
|
"sourceTree" = "BUILT_PRODUCTS_DIR";
|
||||||
|
};
|
||||||
|
"FF6BDEA70DDE9A3C7EEA9DCF" = {
|
||||||
|
"isa" = "PBXSourcesBuildPhase";
|
||||||
|
"buildActionMask" = 2147483647;
|
||||||
|
"files" = ("F942C4AC2D1ABF86575E779A", "D739DF07A523D6FA1D29EECD", "E41BBFD90B0D341CC92C100E",);
|
||||||
|
"runOnlyForDeploymentPostprocessing" = 0;
|
||||||
|
};
|
||||||
|
"49C806A88B3E70B1004AB10D" = {
|
||||||
|
"isa" = "PBXFrameworksBuildPhase";
|
||||||
|
"buildActionMask" = 2147483647;
|
||||||
|
"files" = ();
|
||||||
|
"runOnlyForDeploymentPostprocessing" = 0;
|
||||||
|
};
|
||||||
|
"64D7ED5162C7963DE736D0DF" = {
|
||||||
|
"isa" = "PBXResourcesBuildPhase";
|
||||||
|
"buildActionMask" = 2147483647;
|
||||||
|
"files" = ();
|
||||||
|
"runOnlyForDeploymentPostprocessing" = 0;
|
||||||
|
};
|
||||||
|
"D30B018A8B4A6A434860DC58" = {
|
||||||
|
"isa" = "XCBuildConfiguration";
|
||||||
|
"name" = "Debug";
|
||||||
|
"buildSettings" = {
|
||||||
|
"PRODUCT_NAME" = "$(TARGET_NAME)";
|
||||||
|
"PRODUCT_BUNDLE_IDENTIFIER" = "ru.yukinoki.musicbridge.widgets";
|
||||||
|
"SWIFT_VERSION" = "5.0";
|
||||||
|
"IPHONEOS_DEPLOYMENT_TARGET" = "17.0";
|
||||||
|
"SDKROOT" = "iphoneos";
|
||||||
|
"SUPPORTED_PLATFORMS" = "iphoneos iphonesimulator";
|
||||||
|
"TARGETED_DEVICE_FAMILY" = "1";
|
||||||
|
"CODE_SIGN_STYLE" = "Automatic";
|
||||||
|
"GENERATE_INFOPLIST_FILE" = "NO";
|
||||||
|
"INFOPLIST_FILE" = "MusicBridgeWidgets/Info.plist";
|
||||||
|
"CURRENT_PROJECT_VERSION" = "6";
|
||||||
|
"MARKETING_VERSION" = "0.5.0";
|
||||||
|
"APPLICATION_EXTENSION_API_ONLY" = "YES";
|
||||||
|
"SKIP_INSTALL" = "YES";
|
||||||
|
"LD_RUNPATH_SEARCH_PATHS" = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||||
|
"SWIFT_OPTIMIZATION_LEVEL" = "-Onone";
|
||||||
|
"ENABLE_TESTABILITY" = "YES";
|
||||||
|
"SWIFT_ACTIVE_COMPILATION_CONDITIONS" = "DEBUG";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
"6A281BFCCFF96276806F5934" = {
|
||||||
|
"isa" = "XCBuildConfiguration";
|
||||||
|
"name" = "Release";
|
||||||
|
"buildSettings" = {
|
||||||
|
"PRODUCT_NAME" = "$(TARGET_NAME)";
|
||||||
|
"PRODUCT_BUNDLE_IDENTIFIER" = "ru.yukinoki.musicbridge.widgets";
|
||||||
|
"SWIFT_VERSION" = "5.0";
|
||||||
|
"IPHONEOS_DEPLOYMENT_TARGET" = "17.0";
|
||||||
|
"SDKROOT" = "iphoneos";
|
||||||
|
"SUPPORTED_PLATFORMS" = "iphoneos iphonesimulator";
|
||||||
|
"TARGETED_DEVICE_FAMILY" = "1";
|
||||||
|
"CODE_SIGN_STYLE" = "Automatic";
|
||||||
|
"GENERATE_INFOPLIST_FILE" = "NO";
|
||||||
|
"INFOPLIST_FILE" = "MusicBridgeWidgets/Info.plist";
|
||||||
|
"CURRENT_PROJECT_VERSION" = "6";
|
||||||
|
"MARKETING_VERSION" = "0.5.0";
|
||||||
|
"APPLICATION_EXTENSION_API_ONLY" = "YES";
|
||||||
|
"SKIP_INSTALL" = "YES";
|
||||||
|
"LD_RUNPATH_SEARCH_PATHS" = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks";
|
||||||
|
"SWIFT_OPTIMIZATION_LEVEL" = "-O";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
"5DCAC255CDC0CA72231320F4" = {
|
||||||
|
"isa" = "XCConfigurationList";
|
||||||
|
"buildConfigurations" = ("D30B018A8B4A6A434860DC58", "6A281BFCCFF96276806F5934",);
|
||||||
|
"defaultConfigurationIsVisible" = 0;
|
||||||
|
"defaultConfigurationName" = "Release";
|
||||||
|
};
|
||||||
|
"BFE9A9DC8C194372006C3ACC" = {
|
||||||
|
"isa" = "PBXNativeTarget";
|
||||||
|
"name" = "MusicBridgeWidgets";
|
||||||
|
"productName" = "MusicBridgeWidgets";
|
||||||
|
"productReference" = "CE9225C1B8E80F534681689F";
|
||||||
|
"productType" = "com.apple.product-type.app-extension";
|
||||||
|
"buildConfigurationList" = "5DCAC255CDC0CA72231320F4";
|
||||||
|
"buildPhases" = ("FF6BDEA70DDE9A3C7EEA9DCF", "49C806A88B3E70B1004AB10D", "64D7ED5162C7963DE736D0DF",);
|
||||||
|
"buildRules" = ();
|
||||||
|
"dependencies" = ();
|
||||||
|
};
|
||||||
"0A3E27B8CA818264D75C8D81" = {
|
"0A3E27B8CA818264D75C8D81" = {
|
||||||
"isa" = "PBXGroup";
|
"isa" = "PBXGroup";
|
||||||
"name" = "Products";
|
"name" = "Products";
|
||||||
"sourceTree" = "<group>";
|
"sourceTree" = "<group>";
|
||||||
"children" = ("7C35A0276A762371ABAE5596", "5E4BE508157A7B2CB7B4C2F2",);
|
"children" = ("7C35A0276A762371ABAE5596", "5E4BE508157A7B2CB7B4C2F2", "CE9225C1B8E80F534681689F",);
|
||||||
};
|
};
|
||||||
"0D6E4079E36703EBD37C0072" = {
|
"0D6E4079E36703EBD37C0072" = {
|
||||||
"isa" = "PBXGroup";
|
"isa" = "PBXGroup";
|
||||||
"sourceTree" = "<group>";
|
"sourceTree" = "<group>";
|
||||||
"children" = ("026F39792BBA0E5032536CED", "E1AD768E105BF42350625991", "0A3E27B8CA818264D75C8D81",);
|
"children" = ("026F39792BBA0E5032536CED", "E3C4B39D6D5013477C98CD58", "F932ED2EB70210651D1121CB", "E1AD768E105BF42350625991", "0A3E27B8CA818264D75C8D81",);
|
||||||
};
|
};
|
||||||
"73EBF6644258C120893E685D" = {
|
"73EBF6644258C120893E685D" = {
|
||||||
"isa" = "XCBuildConfiguration";
|
"isa" = "XCBuildConfiguration";
|
||||||
@@ -414,7 +600,7 @@
|
|||||||
"productRefGroup" = "0A3E27B8CA818264D75C8D81";
|
"productRefGroup" = "0A3E27B8CA818264D75C8D81";
|
||||||
"projectDirPath" = "";
|
"projectDirPath" = "";
|
||||||
"projectRoot" = "";
|
"projectRoot" = "";
|
||||||
"targets" = ("F380385F14BF54F0FA81EEAE", "C2A8D97FC5416EA6103FBF9A",);
|
"targets" = ("F380385F14BF54F0FA81EEAE", "C2A8D97FC5416EA6103FBF9A", "BFE9A9DC8C194372006C3ACC",);
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
"rootObject" = "244210E48437B6556980A702";
|
"rootObject" = "244210E48437B6556980A702";
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import SwiftUI
|
|||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
@ObservedObject var store: PlayerStore
|
@ObservedObject var store: PlayerStore
|
||||||
|
@ObservedObject var liveActivity: LiveActivityManager
|
||||||
@State private var showForget = false
|
@State private var showForget = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -19,6 +20,18 @@ struct ContentView: View {
|
|||||||
} else {
|
} else {
|
||||||
PlayerView(store: store, coverSize: min(280, max(120, min(viewport.size.width - 40, viewport.size.height * 0.30))))
|
PlayerView(store: store, coverSize: min(280, max(120, min(viewport.size.width - 40, viewport.size.height * 0.30))))
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
Button {
|
||||||
|
if liveActivity.running { liveActivity.end() }
|
||||||
|
else if let state = store.state { liveActivity.start(state) }
|
||||||
|
} label: {
|
||||||
|
Label(liveActivity.running ? "Убрать с экрана блокировки" : "Показать на экране блокировки", systemImage: "waveform.rectangle")
|
||||||
|
}.disabled(!liveActivity.running && (!store.connected || store.state?.hasSession != true))
|
||||||
|
Text("Live Activity и Dynamic Island показывают последний трек. В фоне данные могут устареть; нажмите карточку, чтобы открыть плеер.")
|
||||||
|
.font(.caption).foregroundStyle(.secondary)
|
||||||
|
if let error = liveActivity.error { Text(error).font(.caption).foregroundStyle(.red) }
|
||||||
|
}.frame(maxWidth: .infinity, alignment: .leading).bridgeCard()
|
||||||
|
|
||||||
if !store.connected && !store.needsPairing {
|
if !store.connected && !store.needsPairing {
|
||||||
Button("Подключиться снова") { store.resume() }
|
Button("Подключиться снова") { store.resume() }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@
|
|||||||
<true/>
|
<true/>
|
||||||
<key>UILaunchScreen</key>
|
<key>UILaunchScreen</key>
|
||||||
<dict/>
|
<dict/>
|
||||||
|
<key>NSSupportsLiveActivities</key>
|
||||||
|
<true/>
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import ActivityKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class LiveActivityManager: ObservableObject {
|
||||||
|
@Published private(set) var running = false
|
||||||
|
@Published private(set) var error: String?
|
||||||
|
private var activity: Activity<MusicActivityAttributes>?
|
||||||
|
private var updates: Task<Void, Never>?
|
||||||
|
private var observer: Task<Void, Never>?
|
||||||
|
private var last: MusicActivityAttributes.ContentState?
|
||||||
|
|
||||||
|
init() {
|
||||||
|
let existing = Activity<MusicActivityAttributes>.activities
|
||||||
|
.filter { $0.activityState == .active || $0.activityState == .stale }
|
||||||
|
if let first = existing.first {
|
||||||
|
activity = first
|
||||||
|
last = first.content.state
|
||||||
|
running = true
|
||||||
|
observe(first)
|
||||||
|
}
|
||||||
|
for extra in existing.dropFirst() {
|
||||||
|
Task { await extra.end(nil, dismissalPolicy: .immediate) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func content(_ state: MediaState, now: Date = Date()) -> MusicActivityAttributes.ContentState {
|
||||||
|
.init(title: MusicActivityAttributes.ContentState.bounded(state.title.isEmpty ? "Без названия" : state.title),
|
||||||
|
artist: MusicActivityAttributes.ContentState.bounded(state.artist),
|
||||||
|
playing: state.playbackStatus == "Playing",
|
||||||
|
position: max(0, state.position(elapsed: 0) - state.startSeconds),
|
||||||
|
duration: max(0, state.endSeconds - state.startSeconds), updatedAt: now, connected: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func start(_ state: MediaState) {
|
||||||
|
guard !running, state.hasSession, UIApplication.shared.applicationState == .active else { return }
|
||||||
|
guard ActivityAuthorizationInfo().areActivitiesEnabled else {
|
||||||
|
error = "Разрешите Live Activities в настройках MusicBridge на iPhone."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let value = Self.content(state)
|
||||||
|
let new = try Activity.request(attributes: MusicActivityAttributes(),
|
||||||
|
content: ActivityContent(state: value, staleDate: Date().addingTimeInterval(15)),
|
||||||
|
pushType: nil)
|
||||||
|
activity = new
|
||||||
|
last = value
|
||||||
|
running = true
|
||||||
|
error = nil
|
||||||
|
observe(new)
|
||||||
|
} catch { self.error = "Не удалось включить Live Activity: " + error.localizedDescription }
|
||||||
|
}
|
||||||
|
|
||||||
|
func receive(_ state: MediaState) {
|
||||||
|
guard state.hasSession else { end(); return }
|
||||||
|
guard let activity, running else { return }
|
||||||
|
let value = Self.content(state)
|
||||||
|
// Coalesce ordinary position ticks, but publish track/playback changes immediately.
|
||||||
|
if let last, last.connected, last.title == value.title, last.artist == value.artist,
|
||||||
|
last.playing == value.playing, last.duration == value.duration,
|
||||||
|
abs(last.position - value.position) < 5, value.updatedAt.timeIntervalSince(last.updatedAt) < 5 { return }
|
||||||
|
last = value
|
||||||
|
enqueue { await activity.update(ActivityContent(state: value, staleDate: value.updatedAt.addingTimeInterval(15))) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func unavailable() {
|
||||||
|
guard let activity, var value = last, value.connected else { return }
|
||||||
|
value.connected = false
|
||||||
|
last = value
|
||||||
|
enqueue { await activity.update(ActivityContent(state: value, staleDate: Date())) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func end() {
|
||||||
|
guard let activity else { return }
|
||||||
|
self.activity = nil
|
||||||
|
running = false
|
||||||
|
last = nil
|
||||||
|
observer?.cancel()
|
||||||
|
enqueue { await activity.end(nil, dismissalPolicy: .immediate) }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func enqueue(_ operation: @escaping @MainActor () async -> Void) {
|
||||||
|
let previous = updates
|
||||||
|
updates = Task { await previous?.value; await operation() }
|
||||||
|
}
|
||||||
|
|
||||||
|
private func observe(_ activity: Activity<MusicActivityAttributes>) {
|
||||||
|
observer?.cancel()
|
||||||
|
observer = Task { [weak self] in
|
||||||
|
for await state in activity.activityStateUpdates {
|
||||||
|
guard !Task.isCancelled, let self, self.activity?.id == activity.id else { return }
|
||||||
|
if state == .dismissed || state == .ended {
|
||||||
|
self.activity = nil; self.running = false; self.last = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,14 +3,32 @@ import SwiftUI
|
|||||||
@main
|
@main
|
||||||
struct MusicBridgeApp: App {
|
struct MusicBridgeApp: App {
|
||||||
@StateObject private var store = PreviewSupport.makeStore()
|
@StateObject private var store = PreviewSupport.makeStore()
|
||||||
|
@StateObject private var liveActivity = LiveActivityManager()
|
||||||
@Environment(\.scenePhase) private var phase
|
@Environment(\.scenePhase) private var phase
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
ContentView(store: store)
|
Group {
|
||||||
|
if PreviewSupport.liveCard, let state = store.state {
|
||||||
|
ScrollView {
|
||||||
|
VStack(spacing: 24) {
|
||||||
|
Text("Live Activity · предпросмотр").font(.headline)
|
||||||
|
MusicActivityCard(state: LiveActivityManager.content(state), stale: false)
|
||||||
|
.background(Color(.secondarySystemBackground), in: RoundedRectangle(cornerRadius: 24))
|
||||||
|
MusicActivityCard(state: LiveActivityManager.content(state), stale: true)
|
||||||
|
.background(Color(.secondarySystemBackground), in: RoundedRectangle(cornerRadius: 24))
|
||||||
|
}.padding()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ContentView(store: store, liveActivity: liveActivity)
|
||||||
|
}
|
||||||
|
}
|
||||||
.dynamicTypeSize(PreviewSupport.largeText ? DynamicTypeSize.accessibility2...DynamicTypeSize.accessibility2 : DynamicTypeSize.xSmall...DynamicTypeSize.accessibility5)
|
.dynamicTypeSize(PreviewSupport.largeText ? DynamicTypeSize.accessibility2...DynamicTypeSize.accessibility2 : DynamicTypeSize.xSmall...DynamicTypeSize.accessibility5)
|
||||||
.onOpenURL { store.importInvitation($0.absoluteString) }
|
.onOpenURL { if $0.absoluteString != "musicbridge://player" { store.importInvitation($0.absoluteString) } }
|
||||||
.task { if !PreviewSupport.enabled { store.setActive(phase == .active) } }
|
.task {
|
||||||
|
store.liveActivity = liveActivity
|
||||||
|
if !PreviewSupport.enabled { store.setActive(phase == .active) }
|
||||||
|
}
|
||||||
.onChange(of: phase) { _, value in if !PreviewSupport.enabled { store.setActive(value == .active) } }
|
.onChange(of: phase) { _, value in if !PreviewSupport.enabled { store.setActive(value == .active) } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ final class PlayerStore: ObservableObject {
|
|||||||
@Published var artworkError: String?
|
@Published var artworkError: String?
|
||||||
@Published var incomingInvitation: String?
|
@Published var incomingInvitation: String?
|
||||||
let discovery = DiscoveryStore()
|
let discovery = DiscoveryStore()
|
||||||
|
var liveActivity: LiveActivityManager?
|
||||||
private(set) var receivedAt = Date()
|
private(set) var receivedAt = Date()
|
||||||
private var client: BridgeClient?
|
private var client: BridgeClient?
|
||||||
private var runner: Task<Void, Never>?
|
private var runner: Task<Void, Never>?
|
||||||
@@ -143,6 +144,7 @@ final class PlayerStore: ObservableObject {
|
|||||||
client.close()
|
client.close()
|
||||||
guard !Task.isCancelled, self.generation == current else { return }
|
guard !Task.isCancelled, self.generation == current else { return }
|
||||||
self.connected = false
|
self.connected = false
|
||||||
|
self.liveActivity?.unavailable()
|
||||||
self.state = nil
|
self.state = nil
|
||||||
self.artworkTask?.cancel()
|
self.artworkTask?.cancel()
|
||||||
self.artwork = nil
|
self.artwork = nil
|
||||||
@@ -167,6 +169,7 @@ final class PlayerStore: ObservableObject {
|
|||||||
receivedAt = Date()
|
receivedAt = Date()
|
||||||
connected = true
|
connected = true
|
||||||
status = "Подключено"
|
status = "Подключено"
|
||||||
|
liveActivity?.receive(snapshot)
|
||||||
guard artworkID != snapshot.artworkId else { return }
|
guard artworkID != snapshot.artworkId else { return }
|
||||||
artworkID = snapshot.artworkId
|
artworkID = snapshot.artworkId
|
||||||
artworkTask?.cancel()
|
artworkTask?.cancel()
|
||||||
@@ -244,6 +247,7 @@ final class PlayerStore: ObservableObject {
|
|||||||
func forget() {
|
func forget() {
|
||||||
do { try storage.delete() }
|
do { try storage.delete() }
|
||||||
catch { self.error = error.localizedDescription; return }
|
catch { self.error = error.localizedDescription; return }
|
||||||
|
liveActivity?.end()
|
||||||
stop()
|
stop()
|
||||||
cancelPairing()
|
cancelPairing()
|
||||||
needsPairing = false
|
needsPairing = false
|
||||||
@@ -253,6 +257,7 @@ final class PlayerStore: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func stop() {
|
private func stop() {
|
||||||
|
liveActivity?.unavailable()
|
||||||
generation = UUID()
|
generation = UUID()
|
||||||
runner?.cancel()
|
runner?.cancel()
|
||||||
runner = nil
|
runner = nil
|
||||||
@@ -268,6 +273,7 @@ final class PlayerStore: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func revokeAccess() {
|
private func revokeAccess() {
|
||||||
|
liveActivity?.end()
|
||||||
stop()
|
stop()
|
||||||
needsPairing = true
|
needsPairing = true
|
||||||
status = "Нужно повторное сопряжение"
|
status = "Нужно повторное сопряжение"
|
||||||
|
|||||||
@@ -17,6 +17,13 @@ enum PreviewSupport {
|
|||||||
false
|
false
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
static var liveCard: Bool {
|
||||||
|
#if DEBUG
|
||||||
|
enabled && ProcessInfo.processInfo.arguments.contains("--preview-live-card")
|
||||||
|
#else
|
||||||
|
false
|
||||||
|
#endif
|
||||||
|
}
|
||||||
static func makeStore() -> PlayerStore {
|
static func makeStore() -> PlayerStore {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
if enabled {
|
if enabled {
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import MusicBridge
|
||||||
|
|
||||||
|
final class LiveActivityTests: XCTestCase {
|
||||||
|
@MainActor
|
||||||
|
func testSnapshotUsesTrackRelativePosition() throws {
|
||||||
|
let state = try fixture(position: 130, start: 100, end: 300)
|
||||||
|
let content = LiveActivityManager.content(state, now: Date(timeIntervalSince1970: 1000))
|
||||||
|
XCTAssertEqual(content.position, 30)
|
||||||
|
XCTAssertEqual(content.duration, 200)
|
||||||
|
XCTAssertEqual(content.progress, 0.15, accuracy: 0.001)
|
||||||
|
XCTAssertEqual(content.updatedAt, Date(timeIntervalSince1970: 1000))
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testOversizedUnicodeMetadataStaysBelowActivityKitBudget() throws {
|
||||||
|
let malicious = String(repeating: "🎵\u{301}", count: 5000)
|
||||||
|
let state = try fixture(title: malicious, artist: malicious)
|
||||||
|
let content = LiveActivityManager.content(state)
|
||||||
|
XCTAssertLessThanOrEqual(content.title.utf8.count, 256)
|
||||||
|
XCTAssertLessThanOrEqual(content.artist.utf8.count, 256)
|
||||||
|
let data = try JSONEncoder().encode(content)
|
||||||
|
XCTAssertLessThan(data.count, 2048)
|
||||||
|
XCTAssertEqual(try JSONDecoder().decode(MusicActivityAttributes.ContentState.self, from: data), content)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testUnknownDurationAndPausedState() throws {
|
||||||
|
let state = try fixture(title: "", playback: "Paused", position: 42, start: 0, end: 0)
|
||||||
|
let content = LiveActivityManager.content(state)
|
||||||
|
XCTAssertEqual(content.title, "Без названия")
|
||||||
|
XCTAssertFalse(content.playing)
|
||||||
|
XCTAssertEqual(content.progress, 0)
|
||||||
|
XCTAssertTrue(content.connected)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fixture(title: String = "Track", artist: String = "Artist", playback: String = "Playing",
|
||||||
|
position: Double = 42, start: Double = 0, end: Double = 200) throws -> MediaState {
|
||||||
|
let url = Bundle(for: Self.self).url(forResource: "state-v1", withExtension: "json")!
|
||||||
|
var json = try JSONSerialization.jsonObject(with: Data(contentsOf: url)) as! [String: Any]
|
||||||
|
json["title"] = title; json["artist"] = artist; json["playbackStatus"] = playback
|
||||||
|
json["positionSeconds"] = position; json["startSeconds"] = start; json["endSeconds"] = end
|
||||||
|
return try JSONDecoder().decode(MediaState.self, from: JSONSerialization.data(withJSONObject: json))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
<?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>NSExtension</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
<string>com.apple.widgetkit-extension</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import ActivityKit
|
||||||
|
import SwiftUI
|
||||||
|
import WidgetKit
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct MusicBridgeWidgets: WidgetBundle {
|
||||||
|
var body: some Widget { MusicBridgeLiveActivity() }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MusicBridgeLiveActivity: Widget {
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
ActivityConfiguration(for: MusicActivityAttributes.self) { context in
|
||||||
|
MusicActivityCard(state: context.state, stale: context.isStale)
|
||||||
|
.activityBackgroundTint(Color(.secondarySystemBackground))
|
||||||
|
.activitySystemActionForegroundColor(.indigo)
|
||||||
|
.widgetURL(URL(string: "musicbridge://player"))
|
||||||
|
} dynamicIsland: { context in
|
||||||
|
DynamicIsland {
|
||||||
|
DynamicIslandExpandedRegion(.leading) {
|
||||||
|
Image(systemName: "waveform").foregroundStyle(.cyan).padding(.leading, 4)
|
||||||
|
}
|
||||||
|
DynamicIslandExpandedRegion(.trailing) {
|
||||||
|
Image(systemName: context.isStale || !context.state.connected ? "arrow.clockwise" : context.state.playing ? "play.fill" : "pause.fill")
|
||||||
|
}
|
||||||
|
DynamicIslandExpandedRegion(.bottom) {
|
||||||
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
|
TrackLabels(state: context.state)
|
||||||
|
ProgressView(value: context.state.progress).tint(.cyan)
|
||||||
|
ActivityStatus(state: context.state, stale: context.isStale)
|
||||||
|
Link("Открыть плеер", destination: URL(string: "musicbridge://player")!)
|
||||||
|
.font(.caption.weight(.semibold)).foregroundStyle(.cyan)
|
||||||
|
}.padding(.bottom, 4)
|
||||||
|
}
|
||||||
|
} compactLeading: {
|
||||||
|
Image(systemName: "waveform").foregroundStyle(.cyan)
|
||||||
|
} compactTrailing: {
|
||||||
|
Image(systemName: context.isStale || !context.state.connected ? "arrow.clockwise" : context.state.playing ? "play.fill" : "pause.fill")
|
||||||
|
.accessibilityLabel(context.isStale || !context.state.connected ? "Откройте MusicBridge для обновления" : "Музыка на ПК")
|
||||||
|
} minimal: {
|
||||||
|
Image(systemName: context.isStale || !context.state.connected ? "arrow.clockwise" : "waveform").foregroundStyle(.cyan)
|
||||||
|
}
|
||||||
|
.widgetURL(URL(string: "musicbridge://player"))
|
||||||
|
.keylineTint(.cyan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import ActivityKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct MusicActivityAttributes: ActivityAttributes {
|
||||||
|
struct ContentState: Codable, Hashable {
|
||||||
|
var title: String
|
||||||
|
var artist: String
|
||||||
|
var playing: Bool
|
||||||
|
var position: Double
|
||||||
|
var duration: Double
|
||||||
|
var updatedAt: Date
|
||||||
|
var connected: Bool
|
||||||
|
|
||||||
|
// Bound UTF-8, including pathological combining characters, below ActivityKit's 4 KB limit.
|
||||||
|
static func bounded(_ text: String) -> String {
|
||||||
|
var result = ""
|
||||||
|
for scalar in text.unicodeScalars {
|
||||||
|
guard result.utf8.count + scalar.utf8.count <= 256 else { break }
|
||||||
|
result.unicodeScalars.append(scalar)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
var progress: Double { duration > 0 ? min(1, max(0, position / duration)) : 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct MusicActivityCard: View {
|
||||||
|
let state: MusicActivityAttributes.ContentState
|
||||||
|
let stale: Bool
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 10) {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Image(systemName: "waveform").font(.title).foregroundStyle(.indigo)
|
||||||
|
TrackLabels(state: state)
|
||||||
|
Spacer(minLength: 0)
|
||||||
|
Image(systemName: state.playing ? "play.fill" : "pause.fill")
|
||||||
|
.accessibilityLabel("Последнее состояние: " + (state.playing ? "воспроизведение" : "пауза"))
|
||||||
|
}
|
||||||
|
ProgressView(value: state.progress).tint(.indigo)
|
||||||
|
ActivityStatus(state: state, stale: stale)
|
||||||
|
Label("Открыть плеер", systemImage: "arrow.up.forward.app").font(.caption.weight(.semibold))
|
||||||
|
}.padding(16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TrackLabels: View {
|
||||||
|
let state: MusicActivityAttributes.ContentState
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 3) {
|
||||||
|
Text(state.title).font(.headline).lineLimit(1)
|
||||||
|
Text(state.artist.isEmpty ? "MusicBridge · музыка на ПК" : state.artist)
|
||||||
|
.font(.caption).foregroundStyle(.secondary).lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ActivityStatus: View {
|
||||||
|
let state: MusicActivityAttributes.ContentState
|
||||||
|
let stale: Bool
|
||||||
|
var body: some View {
|
||||||
|
if stale || !state.connected {
|
||||||
|
Text("Данные устарели · откройте плеер").font(.caption).foregroundStyle(.secondary)
|
||||||
|
} else {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Text("Снимок с ПК в")
|
||||||
|
Text(state.updatedAt, style: .time)
|
||||||
|
}.font(.caption).foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,6 +35,11 @@ def source_group(folder):
|
|||||||
return add(folder, "PBXGroup", path=folder, sourceTree="<group>", children=refs), builds
|
return add(folder, "PBXGroup", path=folder, sourceTree="<group>", children=refs), builds
|
||||||
|
|
||||||
app_group, app_sources = source_group("MusicBridge")
|
app_group, app_sources = source_group("MusicBridge")
|
||||||
|
shared_group, shared_sources = source_group("Shared")
|
||||||
|
widget_group, widget_sources = source_group("MusicBridgeWidgets")
|
||||||
|
app_sources += shared_sources
|
||||||
|
widget_sources += [add("widget-shared/" + str(index), "PBXBuildFile", fileRef=objects[build]["fileRef"])
|
||||||
|
for index, build in enumerate(shared_sources)]
|
||||||
assets = add("assets", "PBXFileReference", lastKnownFileType="folder.assetcatalog", path="Assets.xcassets", sourceTree="<group>")
|
assets = add("assets", "PBXFileReference", lastKnownFileType="folder.assetcatalog", path="Assets.xcassets", sourceTree="<group>")
|
||||||
objects[app_group]["children"].append(assets)
|
objects[app_group]["children"].append(assets)
|
||||||
assets_build = add("assets-build", "PBXBuildFile", fileRef=assets)
|
assets_build = add("assets-build", "PBXBuildFile", fileRef=assets)
|
||||||
@@ -47,14 +52,15 @@ objects[test_group]["children"].append(tls_fixture)
|
|||||||
tls_fixture_build = add("tls-fixture-build", "PBXBuildFile", fileRef=tls_fixture)
|
tls_fixture_build = add("tls-fixture-build", "PBXBuildFile", fileRef=tls_fixture)
|
||||||
products = []
|
products = []
|
||||||
targets = []
|
targets = []
|
||||||
for name, sources, is_test in [("MusicBridge", app_sources, False), ("MusicBridgeTests", test_sources, True)]:
|
for name, sources, is_test in [("MusicBridge", app_sources, False), ("MusicBridgeTests", test_sources, True), ("MusicBridgeWidgets", widget_sources, False)]:
|
||||||
product = add(name + "product", "PBXFileReference", explicitFileType="wrapper.cfbundle" if is_test else "wrapper.application",
|
is_widget = name == "MusicBridgeWidgets"
|
||||||
includeInIndex=0, path=name + (".xctest" if is_test else ".app"), sourceTree="BUILT_PRODUCTS_DIR")
|
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")
|
||||||
products.append(product)
|
products.append(product)
|
||||||
phases = [add(name + "sources", "PBXSourcesBuildPhase", buildActionMask=2147483647, files=sources, runOnlyForDeploymentPostprocessing=0),
|
phases = [add(name + "sources", "PBXSourcesBuildPhase", buildActionMask=2147483647, files=sources, runOnlyForDeploymentPostprocessing=0),
|
||||||
add(name + "frameworks", "PBXFrameworksBuildPhase", buildActionMask=2147483647, files=[], runOnlyForDeploymentPostprocessing=0),
|
add(name + "frameworks", "PBXFrameworksBuildPhase", buildActionMask=2147483647, files=[], runOnlyForDeploymentPostprocessing=0),
|
||||||
add(name + "resources", "PBXResourcesBuildPhase", buildActionMask=2147483647, files=[fixture_build, tls_fixture_build] if is_test else [assets_build], 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" + (".tests" if is_test else ""),
|
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",
|
SWIFT_VERSION="5.0", IPHONEOS_DEPLOYMENT_TARGET="17.0", SDKROOT="iphoneos",
|
||||||
SUPPORTED_PLATFORMS="iphoneos iphonesimulator", TARGETED_DEVICE_FAMILY="1", CODE_SIGN_STYLE="Automatic",
|
SUPPORTED_PLATFORMS="iphoneos iphonesimulator", TARGETED_DEVICE_FAMILY="1", CODE_SIGN_STYLE="Automatic",
|
||||||
GENERATE_INFOPLIST_FILE="YES" if is_test else "NO")
|
GENERATE_INFOPLIST_FILE="YES" if is_test else "NO")
|
||||||
@@ -63,14 +69,24 @@ 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)")
|
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")
|
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)]
|
deps = [add("test-dependency", "PBXTargetDependency", target=uid("MusicBridgetarget"), targetProxy=proxy)]
|
||||||
|
elif is_widget:
|
||||||
|
settings.update(INFOPLIST_FILE="MusicBridgeWidgets/Info.plist", CURRENT_PROJECT_VERSION="6", MARKETING_VERSION="0.5.0",
|
||||||
|
APPLICATION_EXTENSION_API_ONLY="YES", SKIP_INSTALL="YES",
|
||||||
|
LD_RUNPATH_SEARCH_PATHS="$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks")
|
||||||
else:
|
else:
|
||||||
settings.update(INFOPLIST_FILE="MusicBridge/Info.plist", CURRENT_PROJECT_VERSION="5", MARKETING_VERSION="0.4.0", ASSETCATALOG_COMPILER_APPICON_NAME="AppIcon")
|
settings.update(INFOPLIST_FILE="MusicBridge/Info.plist", CURRENT_PROJECT_VERSION="6", MARKETING_VERSION="0.5.0", ASSETCATALOG_COMPILER_APPICON_NAME="AppIcon")
|
||||||
|
embed = add("embed-widget", "PBXBuildFile", fileRef=uid("MusicBridgeWidgetsproduct"), settings=dict(ATTRIBUTES=["RemoveHeadersOnCopy"]))
|
||||||
|
phases.append(add("embed-extensions", "PBXCopyFilesBuildPhase", buildActionMask=2147483647,
|
||||||
|
dstPath="", dstSubfolderSpec=13, files=[embed], name="Embed App 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)]
|
||||||
targets.append(add(name + "target", "PBXNativeTarget", name=name, productName=name, productReference=product,
|
targets.append(add(name + "target", "PBXNativeTarget", name=name, productName=name, productReference=product,
|
||||||
productType="com.apple.product-type.bundle.unit-test" if is_test else "com.apple.product-type.application",
|
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",
|
||||||
buildConfigurationList=configurations(name, settings), buildPhases=phases, buildRules=[], dependencies=deps))
|
buildConfigurationList=configurations(name, settings), buildPhases=phases, buildRules=[], dependencies=deps))
|
||||||
|
|
||||||
product_group = add("products", "PBXGroup", name="Products", sourceTree="<group>", children=products)
|
product_group = add("products", "PBXGroup", name="Products", sourceTree="<group>", children=products)
|
||||||
main_group = add("main", "PBXGroup", sourceTree="<group>", children=[app_group, test_group, product_group])
|
main_group = add("main", "PBXGroup", sourceTree="<group>", children=[app_group, shared_group, widget_group, test_group, product_group])
|
||||||
project = add("project", "PBXProject", attributes=dict(LastUpgradeCheck="1600", BuildIndependentTargetsInParallel="YES"),
|
project = add("project", "PBXProject", attributes=dict(LastUpgradeCheck="1600", BuildIndependentTargetsInParallel="YES"),
|
||||||
buildConfigurationList=configurations("project", dict(CLANG_ENABLE_MODULES="YES", CLANG_ENABLE_OBJC_ARC="YES")),
|
buildConfigurationList=configurations("project", dict(CLANG_ENABLE_MODULES="YES", CLANG_ENABLE_OBJC_ARC="YES")),
|
||||||
compatibilityVersion="Xcode 14.0", developmentRegion="ru", knownRegions=["ru", "en", "Base"],
|
compatibilityVersion="Xcode 14.0", developmentRegion="ru", knownRegions=["ru", "en", "Base"],
|
||||||
@@ -106,7 +122,7 @@ test_ref = reference("MusicBridgeTests", "MusicBridgeTests.xctest")
|
|||||||
info = dict(CFBundleDevelopmentRegion="ru", CFBundleDisplayName="MusicBridge", CFBundleExecutable="$(EXECUTABLE_NAME)",
|
info = dict(CFBundleDevelopmentRegion="ru", CFBundleDisplayName="MusicBridge", CFBundleExecutable="$(EXECUTABLE_NAME)",
|
||||||
CFBundleIdentifier="$(PRODUCT_BUNDLE_IDENTIFIER)", CFBundleInfoDictionaryVersion="6.0", CFBundleName="$(PRODUCT_NAME)",
|
CFBundleIdentifier="$(PRODUCT_BUNDLE_IDENTIFIER)", CFBundleInfoDictionaryVersion="6.0", CFBundleName="$(PRODUCT_NAME)",
|
||||||
CFBundlePackageType="APPL", CFBundleShortVersionString="$(MARKETING_VERSION)", CFBundleVersion="$(CURRENT_PROJECT_VERSION)",
|
CFBundlePackageType="APPL", CFBundleShortVersionString="$(MARKETING_VERSION)", CFBundleVersion="$(CURRENT_PROJECT_VERSION)",
|
||||||
LSRequiresIPhoneOS=True, UILaunchScreen={},
|
LSRequiresIPhoneOS=True, UILaunchScreen={}, NSSupportsLiveActivities=True,
|
||||||
UISupportedInterfaceOrientations=["UIInterfaceOrientationPortrait"],
|
UISupportedInterfaceOrientations=["UIInterfaceOrientationPortrait"],
|
||||||
NSLocalNetworkUsageDescription="MusicBridge подключается к вашему ПК, чтобы показывать музыку и управлять воспроизведением.",
|
NSLocalNetworkUsageDescription="MusicBridge подключается к вашему ПК, чтобы показывать музыку и управлять воспроизведением.",
|
||||||
NSCameraUsageDescription="Камера сканирует QR в MusicBridge на ПК для безопасного сопряжения.",
|
NSCameraUsageDescription="Камера сканирует QR в MusicBridge на ПК для безопасного сопряжения.",
|
||||||
@@ -120,4 +136,10 @@ info = dict(CFBundleDevelopmentRegion="ru", CFBundleDisplayName="MusicBridge", C
|
|||||||
for network in ("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")
|
for network in ("10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16")
|
||||||
}))
|
}))
|
||||||
(ROOT / "MusicBridge" / "Info.plist").write_bytes(plistlib.dumps(info, sort_keys=False))
|
(ROOT / "MusicBridge" / "Info.plist").write_bytes(plistlib.dumps(info, sort_keys=False))
|
||||||
|
widget_info = dict(CFBundleDevelopmentRegion="ru", CFBundleDisplayName="MusicBridge",
|
||||||
|
CFBundleExecutable="$(EXECUTABLE_NAME)", CFBundleIdentifier="$(PRODUCT_BUNDLE_IDENTIFIER)",
|
||||||
|
CFBundleInfoDictionaryVersion="6.0", CFBundleName="$(PRODUCT_NAME)", CFBundlePackageType="XPC!",
|
||||||
|
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))
|
||||||
print("Generated Xcode project, shared scheme and Info.plist.")
|
print("Generated Xcode project, shared scheme and Info.plist.")
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ import xml.etree.ElementTree as ET
|
|||||||
root = pathlib.Path(__file__).resolve().parents[1]
|
root = pathlib.Path(__file__).resolve().parents[1]
|
||||||
generated = [root / "MusicBridge.xcodeproj/project.pbxproj",
|
generated = [root / "MusicBridge.xcodeproj/project.pbxproj",
|
||||||
root / "MusicBridge.xcodeproj/xcshareddata/xcschemes/MusicBridge.xcscheme",
|
root / "MusicBridge.xcodeproj/xcshareddata/xcschemes/MusicBridge.xcscheme",
|
||||||
root / "MusicBridge/Info.plist"]
|
root / "MusicBridge/Info.plist", root / "MusicBridgeWidgets/Info.plist"]
|
||||||
before = [p.read_bytes() for p in generated]
|
before = [p.read_bytes() for p in generated]
|
||||||
subprocess.run([sys.executable, str(root / "tools/generate_project.py")], check=True)
|
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."
|
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")
|
project = generated[0].read_text(encoding="utf-8")
|
||||||
for folder in ("MusicBridge", "MusicBridgeTests"):
|
for folder in ("MusicBridge", "MusicBridgeTests", "Shared", "MusicBridgeWidgets"):
|
||||||
for source in (root / folder).glob("*.swift"):
|
for source in (root / folder).glob("*.swift"):
|
||||||
key = hashlib.sha256(source.relative_to(root).as_posix().encode()).hexdigest()[:24].upper()
|
key = hashlib.sha256(source.relative_to(root).as_posix().encode()).hexdigest()[:24].upper()
|
||||||
assert key in project, f"Missing source: {source.name}"
|
assert key in project, f"Missing source: {source.name}"
|
||||||
@@ -26,6 +26,8 @@ assert defined == referenced, "Project has dangling object references."
|
|||||||
info = plistlib.loads(generated[2].read_bytes())
|
info = plistlib.loads(generated[2].read_bytes())
|
||||||
assert info["NSLocalNetworkUsageDescription"]
|
assert info["NSLocalNetworkUsageDescription"]
|
||||||
assert info["NSCameraUsageDescription"]
|
assert info["NSCameraUsageDescription"]
|
||||||
|
assert info["NSSupportsLiveActivities"] is True
|
||||||
|
assert plistlib.loads(generated[3].read_bytes())["NSExtension"]["NSExtensionPointIdentifier"] == "com.apple.widgetkit-extension"
|
||||||
assert info["NSBonjourServices"] == ["_musicbridge._tcp"]
|
assert info["NSBonjourServices"] == ["_musicbridge._tcp"]
|
||||||
assert info["CFBundleURLTypes"][0]["CFBundleURLSchemes"] == ["musicbridge"]
|
assert info["CFBundleURLTypes"][0]["CFBundleURLSchemes"] == ["musicbridge"]
|
||||||
assert info["NSAppTransportSecurity"] == {
|
assert info["NSAppTransportSecurity"] == {
|
||||||
|
|||||||
@@ -29,5 +29,24 @@ with zipfile.ZipFile(path) as archive:
|
|||||||
platforms.append(struct.unpack_from("<I", binary, offset + 8)[0])
|
platforms.append(struct.unpack_from("<I", binary, offset + 8)[0])
|
||||||
offset += size
|
offset += size
|
||||||
assert platforms == [2], "Mach-O is not built for iOS devices"
|
assert platforms == [2], "Mach-O is not built for iOS devices"
|
||||||
|
assert info["NSSupportsLiveActivities"] is True
|
||||||
|
extension = prefix + "PlugIns/MusicBridgeWidgets.appex/"
|
||||||
|
widget = plistlib.loads(archive.read(extension + "Info.plist"))
|
||||||
|
assert widget["CFBundleIdentifier"] == info["CFBundleIdentifier"] + ".widgets"
|
||||||
|
assert widget["CFBundleShortVersionString"] == info["CFBundleShortVersionString"]
|
||||||
|
assert widget["CFBundleVersion"] == info["CFBundleVersion"]
|
||||||
|
assert widget["NSExtension"]["NSExtensionPointIdentifier"] == "com.apple.widgetkit-extension"
|
||||||
|
assert widget["CFBundleSupportedPlatforms"] == ["iPhoneOS"]
|
||||||
|
binary = archive.read(extension + widget["CFBundleExecutable"])
|
||||||
|
assert struct.unpack_from("<II", binary) == (0xFEEDFACF, 0x0100000C)
|
||||||
|
offset, platforms = 32, []
|
||||||
|
for _ in range(struct.unpack_from("<I", binary, 16)[0]):
|
||||||
|
command, size = struct.unpack_from("<II", binary, offset)
|
||||||
|
assert size >= 8 and offset + size <= len(binary)
|
||||||
|
if command == 0x32:
|
||||||
|
platforms.append(struct.unpack_from("<I", binary, offset + 8)[0])
|
||||||
|
offset += size
|
||||||
|
assert platforms == [2], "Widget is not built for iOS devices"
|
||||||
print(f"PASS: {path.name}: valid IPA, iPhoneOS ARM64, iOS 17+, ru.yukinoki.musicbridge.")
|
print(f"PASS: {path.name}: valid IPA, iPhoneOS ARM64, iOS 17+, ru.yukinoki.musicbridge.")
|
||||||
|
print("PASS: embedded WidgetKit extension, matching version, ARM64 iPhoneOS and Live Activities support.")
|
||||||
print("Requires local signing with Sideloadly before installation on an iPhone.")
|
print("Requires local signing with Sideloadly before installation on an iPhone.")
|
||||||
|
|||||||
Reference in New Issue
Block a user