Live Activities fix
This commit is contained in:
@@ -10,18 +10,66 @@
|
|||||||
2616AF4E2F876BEF00CB210E /* ServicesMAStoreManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2616AF4D2F876BEF00CB210E /* ServicesMAStoreManager.swift */; };
|
2616AF4E2F876BEF00CB210E /* ServicesMAStoreManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2616AF4D2F876BEF00CB210E /* ServicesMAStoreManager.swift */; };
|
||||||
2616AF502F87782600CB210E /* Donations.storekit in Resources */ = {isa = PBXBuildFile; fileRef = 2616AF4F2F87782600CB210E /* Donations.storekit */; };
|
2616AF502F87782600CB210E /* Donations.storekit in Resources */ = {isa = PBXBuildFile; fileRef = 2616AF4F2F87782600CB210E /* Donations.storekit */; };
|
||||||
2681ED6F2F8393AC002FB204 /* ViewsPlayerQueueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2681ED6E2F8393AC002FB204 /* ViewsPlayerQueueView.swift */; };
|
2681ED6F2F8393AC002FB204 /* ViewsPlayerQueueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2681ED6E2F8393AC002FB204 /* ViewsPlayerQueueView.swift */; };
|
||||||
|
269ECE562F92A07000444B14 /* MobileMAShared in Frameworks */ = {isa = PBXBuildFile; productRef = 269ECE552F92A07000444B14 /* MobileMAShared */; };
|
||||||
|
269ECE582F92A08300444B14 /* MobileMAShared in Frameworks */ = {isa = PBXBuildFile; productRef = 269ECE572F92A08300444B14 /* MobileMAShared */; };
|
||||||
|
269ECE5A2F92A24900444B14 /* MobileMALiveActivityExtension.appex in CopyFiles */ = {isa = PBXBuildFile; fileRef = 26DA6F7C2F928B2100849EC7 /* MobileMALiveActivityExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||||
26A83F532F825B3C003834A9 /* ViewsPlayerNowPlayingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26A83F522F825B3C003834A9 /* ViewsPlayerNowPlayingView.swift */; };
|
26A83F532F825B3C003834A9 /* ViewsPlayerNowPlayingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26A83F522F825B3C003834A9 /* ViewsPlayerNowPlayingView.swift */; };
|
||||||
|
26DA6F7F2F928B2100849EC7 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26DA6F7E2F928B2100849EC7 /* WidgetKit.framework */; };
|
||||||
|
26DA6F812F928B2100849EC7 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 26DA6F802F928B2100849EC7 /* SwiftUI.framework */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
269ECE592F92A21100444B14 /* CopyFiles */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "";
|
||||||
|
dstSubfolderSpec = 13;
|
||||||
|
files = (
|
||||||
|
269ECE5A2F92A24900444B14 /* MobileMALiveActivityExtension.appex in CopyFiles */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
2616AF4D2F876BEF00CB210E /* ServicesMAStoreManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServicesMAStoreManager.swift; sourceTree = "<group>"; };
|
2616AF4D2F876BEF00CB210E /* ServicesMAStoreManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServicesMAStoreManager.swift; sourceTree = "<group>"; };
|
||||||
2616AF4F2F87782600CB210E /* Donations.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Donations.storekit; sourceTree = "<group>"; };
|
2616AF4F2F87782600CB210E /* Donations.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = Donations.storekit; sourceTree = "<group>"; };
|
||||||
2681ED6E2F8393AC002FB204 /* ViewsPlayerQueueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewsPlayerQueueView.swift; sourceTree = "<group>"; };
|
2681ED6E2F8393AC002FB204 /* ViewsPlayerQueueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewsPlayerQueueView.swift; sourceTree = "<group>"; };
|
||||||
|
269ECE4D2F929A9800444B14 /* MobileMAShared */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = "MobileMAShared"; sourceTree = "<group>"; };
|
||||||
26A83F522F825B3C003834A9 /* ViewsPlayerNowPlayingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewsPlayerNowPlayingView.swift; sourceTree = "<group>"; };
|
26A83F522F825B3C003834A9 /* ViewsPlayerNowPlayingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewsPlayerNowPlayingView.swift; sourceTree = "<group>"; };
|
||||||
|
26DA6F7C2F928B2100849EC7 /* MobileMALiveActivityExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = MobileMALiveActivityExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
26DA6F7E2F928B2100849EC7 /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; };
|
||||||
|
26DA6F802F928B2100849EC7 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; };
|
||||||
26ED92612F759EEA0025419D /* Mobile Music Assistant.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Mobile Music Assistant.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
26ED92612F759EEA0025419D /* Mobile Music Assistant.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Mobile Music Assistant.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
269ECE4C2F9295BB00444B14 /* Exceptions for "MobileMALiveActivity" folder in "Mobile Music Assistant" target */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||||
|
membershipExceptions = (
|
||||||
|
MALiveActivityAttributes.swift,
|
||||||
|
);
|
||||||
|
target = 26ED92602F759EEA0025419D /* Mobile Music Assistant */;
|
||||||
|
};
|
||||||
|
26DA6F902F928B2200849EC7 /* Exceptions for "MobileMALiveActivity" folder in "MobileMALiveActivityExtension" target */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||||
|
membershipExceptions = (
|
||||||
|
Info.plist,
|
||||||
|
);
|
||||||
|
target = 26DA6F7B2F928B2100849EC7 /* MobileMALiveActivityExtension */;
|
||||||
|
};
|
||||||
|
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||||
|
|
||||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
26DA6F822F928B2100849EC7 /* MobileMALiveActivity */ = {
|
||||||
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
|
exceptions = (
|
||||||
|
269ECE4C2F9295BB00444B14 /* Exceptions for "MobileMALiveActivity" folder in "Mobile Music Assistant" target */,
|
||||||
|
26DA6F902F928B2200849EC7 /* Exceptions for "MobileMALiveActivity" folder in "MobileMALiveActivityExtension" target */,
|
||||||
|
);
|
||||||
|
path = MobileMALiveActivity;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
26ED92632F759EEA0025419D /* Mobile Music Assistant */ = {
|
26ED92632F759EEA0025419D /* Mobile Music Assistant */ = {
|
||||||
isa = PBXFileSystemSynchronizedRootGroup;
|
isa = PBXFileSystemSynchronizedRootGroup;
|
||||||
path = "Mobile Music Assistant";
|
path = "Mobile Music Assistant";
|
||||||
@@ -30,21 +78,44 @@
|
|||||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
26DA6F792F928B2100849EC7 /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
26DA6F812F928B2100849EC7 /* SwiftUI.framework in Frameworks */,
|
||||||
|
269ECE582F92A08300444B14 /* MobileMAShared in Frameworks */,
|
||||||
|
26DA6F7F2F928B2100849EC7 /* WidgetKit.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
26ED925E2F759EEA0025419D /* Frameworks */ = {
|
26ED925E2F759EEA0025419D /* Frameworks */ = {
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
269ECE562F92A07000444B14 /* MobileMAShared in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
26DA6F7D2F928B2100849EC7 /* Frameworks */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
26DA6F7E2F928B2100849EC7 /* WidgetKit.framework */,
|
||||||
|
26DA6F802F928B2100849EC7 /* SwiftUI.framework */,
|
||||||
|
);
|
||||||
|
name = Frameworks;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
26ED92582F759EEA0025419D = {
|
26ED92582F759EEA0025419D = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
269ECE4D2F929A9800444B14 /* MobileMAShared */,
|
||||||
2616AF4F2F87782600CB210E /* Donations.storekit */,
|
2616AF4F2F87782600CB210E /* Donations.storekit */,
|
||||||
26ED92632F759EEA0025419D /* Mobile Music Assistant */,
|
26ED92632F759EEA0025419D /* Mobile Music Assistant */,
|
||||||
|
26DA6F822F928B2100849EC7 /* MobileMALiveActivity */,
|
||||||
|
26DA6F7D2F928B2100849EC7 /* Frameworks */,
|
||||||
26ED92622F759EEA0025419D /* Products */,
|
26ED92622F759EEA0025419D /* Products */,
|
||||||
26A83F522F825B3C003834A9 /* ViewsPlayerNowPlayingView.swift */,
|
26A83F522F825B3C003834A9 /* ViewsPlayerNowPlayingView.swift */,
|
||||||
2681ED6E2F8393AC002FB204 /* ViewsPlayerQueueView.swift */,
|
2681ED6E2F8393AC002FB204 /* ViewsPlayerQueueView.swift */,
|
||||||
@@ -56,6 +127,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
26ED92612F759EEA0025419D /* Mobile Music Assistant.app */,
|
26ED92612F759EEA0025419D /* Mobile Music Assistant.app */,
|
||||||
|
26DA6F7C2F928B2100849EC7 /* MobileMALiveActivityExtension.appex */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -63,6 +135,29 @@
|
|||||||
/* End PBXGroup section */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
|
26DA6F7B2F928B2100849EC7 /* MobileMALiveActivityExtension */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 26DA6F912F928B2200849EC7 /* Build configuration list for PBXNativeTarget "MobileMALiveActivityExtension" */;
|
||||||
|
buildPhases = (
|
||||||
|
26DA6F782F928B2100849EC7 /* Sources */,
|
||||||
|
26DA6F792F928B2100849EC7 /* Frameworks */,
|
||||||
|
26DA6F7A2F928B2100849EC7 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
fileSystemSynchronizedGroups = (
|
||||||
|
26DA6F822F928B2100849EC7 /* MobileMALiveActivity */,
|
||||||
|
);
|
||||||
|
name = MobileMALiveActivityExtension;
|
||||||
|
packageProductDependencies = (
|
||||||
|
269ECE572F92A08300444B14 /* MobileMAShared */,
|
||||||
|
);
|
||||||
|
productName = MobileMALiveActivityExtension;
|
||||||
|
productReference = 26DA6F7C2F928B2100849EC7 /* MobileMALiveActivityExtension.appex */;
|
||||||
|
productType = "com.apple.product-type.app-extension";
|
||||||
|
};
|
||||||
26ED92602F759EEA0025419D /* Mobile Music Assistant */ = {
|
26ED92602F759EEA0025419D /* Mobile Music Assistant */ = {
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 26ED926C2F759EEB0025419D /* Build configuration list for PBXNativeTarget "Mobile Music Assistant" */;
|
buildConfigurationList = 26ED926C2F759EEB0025419D /* Build configuration list for PBXNativeTarget "Mobile Music Assistant" */;
|
||||||
@@ -70,6 +165,7 @@
|
|||||||
26ED925D2F759EEA0025419D /* Sources */,
|
26ED925D2F759EEA0025419D /* Sources */,
|
||||||
26ED925E2F759EEA0025419D /* Frameworks */,
|
26ED925E2F759EEA0025419D /* Frameworks */,
|
||||||
26ED925F2F759EEA0025419D /* Resources */,
|
26ED925F2F759EEA0025419D /* Resources */,
|
||||||
|
269ECE592F92A21100444B14 /* CopyFiles */,
|
||||||
);
|
);
|
||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
@@ -80,6 +176,7 @@
|
|||||||
);
|
);
|
||||||
name = "Mobile Music Assistant";
|
name = "Mobile Music Assistant";
|
||||||
packageProductDependencies = (
|
packageProductDependencies = (
|
||||||
|
269ECE552F92A07000444B14 /* MobileMAShared */,
|
||||||
);
|
);
|
||||||
productName = "Mobile Music Assistant";
|
productName = "Mobile Music Assistant";
|
||||||
productReference = 26ED92612F759EEA0025419D /* Mobile Music Assistant.app */;
|
productReference = 26ED92612F759EEA0025419D /* Mobile Music Assistant.app */;
|
||||||
@@ -95,6 +192,9 @@
|
|||||||
LastSwiftUpdateCheck = 2640;
|
LastSwiftUpdateCheck = 2640;
|
||||||
LastUpgradeCheck = 2640;
|
LastUpgradeCheck = 2640;
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
|
26DA6F7B2F928B2100849EC7 = {
|
||||||
|
CreatedOnToolsVersion = 26.4;
|
||||||
|
};
|
||||||
26ED92602F759EEA0025419D = {
|
26ED92602F759EEA0025419D = {
|
||||||
CreatedOnToolsVersion = 26.4;
|
CreatedOnToolsVersion = 26.4;
|
||||||
};
|
};
|
||||||
@@ -112,17 +212,28 @@
|
|||||||
);
|
);
|
||||||
mainGroup = 26ED92582F759EEA0025419D;
|
mainGroup = 26ED92582F759EEA0025419D;
|
||||||
minimizedProjectReferenceProxies = 1;
|
minimizedProjectReferenceProxies = 1;
|
||||||
|
packageReferences = (
|
||||||
|
269ECE4E2F929FE100444B14 /* XCLocalSwiftPackageReference "MobileMAShared" */,
|
||||||
|
);
|
||||||
preferredProjectObjectVersion = 77;
|
preferredProjectObjectVersion = 77;
|
||||||
productRefGroup = 26ED92622F759EEA0025419D /* Products */;
|
productRefGroup = 26ED92622F759EEA0025419D /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
26ED92602F759EEA0025419D /* Mobile Music Assistant */,
|
26ED92602F759EEA0025419D /* Mobile Music Assistant */,
|
||||||
|
26DA6F7B2F928B2100849EC7 /* MobileMALiveActivityExtension */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
|
|
||||||
/* Begin PBXResourcesBuildPhase section */
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
26DA6F7A2F928B2100849EC7 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
26ED925F2F759EEA0025419D /* Resources */ = {
|
26ED925F2F759EEA0025419D /* Resources */ = {
|
||||||
isa = PBXResourcesBuildPhase;
|
isa = PBXResourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -134,6 +245,13 @@
|
|||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXSourcesBuildPhase section */
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
26DA6F782F928B2100849EC7 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
26ED925D2F759EEA0025419D /* Sources */ = {
|
26ED925D2F759EEA0025419D /* Sources */ = {
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
@@ -147,6 +265,66 @@
|
|||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
|
26DA6F8E2F928B2200849EC7 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = EKFHUHT63T;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = MobileMALiveActivity/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = MobileMALiveActivity;
|
||||||
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "Team.Mobile-Music-Assistant.MobileMALiveActivity";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
26DA6F8F2F928B2200849EC7 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
|
ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEVELOPMENT_TEAM = EKFHUHT63T;
|
||||||
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
|
INFOPLIST_FILE = MobileMALiveActivity/Info.plist;
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = MobileMALiveActivity;
|
||||||
|
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@executable_path/../../Frameworks",
|
||||||
|
);
|
||||||
|
MARKETING_VERSION = 1.0;
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = "Team.Mobile-Music-Assistant.MobileMALiveActivity";
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SKIP_INSTALL = YES;
|
||||||
|
STRING_CATALOG_GENERATE_SYMBOLS = YES;
|
||||||
|
SWIFT_APPROACHABLE_CONCURRENCY = YES;
|
||||||
|
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||||
|
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
26ED926A2F759EEB0025419D /* Debug */ = {
|
26ED926A2F759EEB0025419D /* Debug */ = {
|
||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
@@ -200,7 +378,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 26.4;
|
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
@@ -258,7 +436,7 @@
|
|||||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 26.4;
|
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
|
||||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||||
MTL_ENABLE_DEBUG_INFO = NO;
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
MTL_FAST_MATH = YES;
|
MTL_FAST_MATH = YES;
|
||||||
@@ -278,7 +456,10 @@
|
|||||||
DEVELOPMENT_TEAM = EKFHUHT63T;
|
DEVELOPMENT_TEAM = EKFHUHT63T;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Mobile MA";
|
INFOPLIST_FILE = "Mobile-Music-Assistant-Info.plist";
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = "";
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "";
|
||||||
|
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
@@ -315,7 +496,10 @@
|
|||||||
DEVELOPMENT_TEAM = EKFHUHT63T;
|
DEVELOPMENT_TEAM = EKFHUHT63T;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
INFOPLIST_KEY_CFBundleDisplayName = "Mobile MA";
|
INFOPLIST_FILE = "Mobile-Music-Assistant-Info.plist";
|
||||||
|
INFOPLIST_KEY_CFBundleDisplayName = "";
|
||||||
|
INFOPLIST_KEY_LSApplicationCategoryType = "";
|
||||||
|
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||||
@@ -345,6 +529,15 @@
|
|||||||
/* End XCBuildConfiguration section */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
|
26DA6F912F928B2200849EC7 /* Build configuration list for PBXNativeTarget "MobileMALiveActivityExtension" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
26DA6F8E2F928B2200849EC7 /* Debug */,
|
||||||
|
26DA6F8F2F928B2200849EC7 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
26ED925C2F759EEA0025419D /* Build configuration list for PBXProject "Mobile Music Assistant" */ = {
|
26ED925C2F759EEA0025419D /* Build configuration list for PBXProject "Mobile Music Assistant" */ = {
|
||||||
isa = XCConfigurationList;
|
isa = XCConfigurationList;
|
||||||
buildConfigurations = (
|
buildConfigurations = (
|
||||||
@@ -364,6 +557,26 @@
|
|||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
|
/* Begin XCLocalSwiftPackageReference section */
|
||||||
|
269ECE4E2F929FE100444B14 /* XCLocalSwiftPackageReference "MobileMAShared" */ = {
|
||||||
|
isa = XCLocalSwiftPackageReference;
|
||||||
|
relativePath = MobileMAShared;
|
||||||
|
};
|
||||||
|
/* End XCLocalSwiftPackageReference section */
|
||||||
|
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
269ECE552F92A07000444B14 /* MobileMAShared */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 269ECE4E2F929FE100444B14 /* XCLocalSwiftPackageReference "MobileMAShared" */;
|
||||||
|
productName = MobileMAShared;
|
||||||
|
};
|
||||||
|
269ECE572F92A08300444B14 /* MobileMAShared */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = 269ECE4E2F929FE100444B14 /* XCLocalSwiftPackageReference "MobileMAShared" */;
|
||||||
|
productName = MobileMAShared;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = 26ED92592F759EEA0025419D /* Project object */;
|
rootObject = 26ED92592F759EEA0025419D /* Project object */;
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -9,6 +9,11 @@
|
|||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>0</integer>
|
<integer>0</integer>
|
||||||
</dict>
|
</dict>
|
||||||
|
<key>MobileMALiveActivityExtension.xcscheme_^#shared#^_</key>
|
||||||
|
<dict>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<key>SuppressBuildableAutocreation</key>
|
<key>SuppressBuildableAutocreation</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ enum MANavigationDestination: Hashable {
|
|||||||
case album(MAAlbum)
|
case album(MAAlbum)
|
||||||
case playlist(MAPlaylist)
|
case playlist(MAPlaylist)
|
||||||
case podcast(MAPodcast)
|
case podcast(MAPodcast)
|
||||||
|
case genre(MAGenre)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ViewModifier to apply all navigation destinations consistently
|
/// ViewModifier to apply all navigation destinations consistently
|
||||||
@@ -31,6 +32,9 @@ struct MANavigationDestinations: ViewModifier {
|
|||||||
.navigationDestination(for: MAPodcast.self) { podcast in
|
.navigationDestination(for: MAPodcast.self) { podcast in
|
||||||
PodcastDetailView(podcast: podcast)
|
PodcastDetailView(podcast: podcast)
|
||||||
}
|
}
|
||||||
|
.navigationDestination(for: MAGenre.self) { genre in
|
||||||
|
GenreDetailView(genre: genre)
|
||||||
|
}
|
||||||
.navigationDestination(for: MANavigationDestination.self) { destination in
|
.navigationDestination(for: MANavigationDestination.self) { destination in
|
||||||
switch destination {
|
switch destination {
|
||||||
case .artist(let artist):
|
case .artist(let artist):
|
||||||
@@ -41,6 +45,8 @@ struct MANavigationDestinations: ViewModifier {
|
|||||||
PlaylistDetailView(playlist: playlist)
|
PlaylistDetailView(playlist: playlist)
|
||||||
case .podcast(let podcast):
|
case .podcast(let podcast):
|
||||||
PodcastDetailView(podcast: podcast)
|
PodcastDetailView(podcast: podcast)
|
||||||
|
case .genre(let genre):
|
||||||
|
GenreDetailView(genre: genre)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1317,6 +1317,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Genres" : {
|
||||||
|
"comment" : "Title of the genres tab in the library view.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Group \"%@\" with \"%@\"?" : {
|
"Group \"%@\" with \"%@\"?" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -1771,6 +1775,14 @@
|
|||||||
"comment" : "A title for a view that shows when a user has no favorite songs.",
|
"comment" : "A title for a view that shows when a user has no favorite songs.",
|
||||||
"isCommentAutoGenerated" : true
|
"isCommentAutoGenerated" : true
|
||||||
},
|
},
|
||||||
|
"No Genres" : {
|
||||||
|
"comment" : "A title for a view that shows when a user has no genres in their library.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
|
"No Items" : {
|
||||||
|
"comment" : "A label displayed when a genre has no items.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"No Players Found" : {
|
"No Players Found" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -1969,6 +1981,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Nothing found for this genre" : {
|
||||||
|
"comment" : "A description displayed when a genre has no items.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Now Playing" : {
|
"Now Playing" : {
|
||||||
"extractionState" : "stale",
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -2014,6 +2030,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Other" : {
|
||||||
|
"comment" : "A section for items that don't fit into the \"Artists\" or \"Albums\" section.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Play" : {
|
"Play" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
@@ -2969,6 +2989,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"Your library doesn't contain any genres yet" : {
|
||||||
|
"comment" : "A description of the content of the \"No Genres\" view.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"Your library doesn't contain any playlists yet" : {
|
"Your library doesn't contain any playlists yet" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"de" : {
|
"de" : {
|
||||||
|
|||||||
@@ -469,6 +469,35 @@ struct MAPodcast: Codable, Identifiable, Hashable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Genre
|
||||||
|
|
||||||
|
struct MAGenre: Codable, Identifiable, Hashable {
|
||||||
|
let uri: String
|
||||||
|
let name: String
|
||||||
|
let metadata: MediaItemMetadata?
|
||||||
|
|
||||||
|
var id: String { uri }
|
||||||
|
var imageUrl: String? { metadata?.thumbImage?.path }
|
||||||
|
var imageProvider: String? { metadata?.thumbImage?.provider }
|
||||||
|
|
||||||
|
enum CodingKeys: String, CodingKey {
|
||||||
|
case uri, name, metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
init(uri: String, name: String, metadata: MediaItemMetadata? = nil) {
|
||||||
|
self.uri = uri
|
||||||
|
self.name = name
|
||||||
|
self.metadata = metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
init(from decoder: Decoder) throws {
|
||||||
|
let c = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
uri = try c.decode(String.self, forKey: .uri)
|
||||||
|
name = try c.decode(String.self, forKey: .name)
|
||||||
|
metadata = try? c.decodeIfPresent(MediaItemMetadata.self, forKey: .metadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Repeat Mode
|
// MARK: - Repeat Mode
|
||||||
|
|
||||||
enum RepeatMode: String, Codable, CaseIterable {
|
enum RepeatMode: String, Codable, CaseIterable {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ final class MALibraryManager {
|
|||||||
private(set) var albums: [MAAlbum] = []
|
private(set) var albums: [MAAlbum] = []
|
||||||
private(set) var playlists: [MAPlaylist] = []
|
private(set) var playlists: [MAPlaylist] = []
|
||||||
private(set) var podcasts: [MAPodcast] = []
|
private(set) var podcasts: [MAPodcast] = []
|
||||||
|
private(set) var genres: [MAGenre] = []
|
||||||
|
|
||||||
// Pagination
|
// Pagination
|
||||||
private var artistsOffset = 0
|
private var artistsOffset = 0
|
||||||
@@ -39,6 +40,7 @@ final class MALibraryManager {
|
|||||||
private(set) var isLoadingAlbums = false
|
private(set) var isLoadingAlbums = false
|
||||||
private(set) var isLoadingPlaylists = false
|
private(set) var isLoadingPlaylists = false
|
||||||
private(set) var isLoadingPodcasts = false
|
private(set) var isLoadingPodcasts = false
|
||||||
|
private(set) var isLoadingGenres = false
|
||||||
|
|
||||||
/// URIs currently marked as favorites — source of truth for UI.
|
/// URIs currently marked as favorites — source of truth for UI.
|
||||||
/// Populated from decoded model data, then mutated optimistically on toggle.
|
/// Populated from decoded model data, then mutated optimistically on toggle.
|
||||||
@@ -152,6 +154,7 @@ final class MALibraryManager {
|
|||||||
albums = []
|
albums = []
|
||||||
playlists = []
|
playlists = []
|
||||||
podcasts = []
|
podcasts = []
|
||||||
|
genres = []
|
||||||
favoriteURIs = []
|
favoriteURIs = []
|
||||||
artistsOffset = 0
|
artistsOffset = 0
|
||||||
albumArtistsOffset = 0
|
albumArtistsOffset = 0
|
||||||
@@ -397,6 +400,27 @@ final class MALibraryManager {
|
|||||||
logger.info("Loaded \(loaded.count) podcasts")
|
logger.info("Loaded \(loaded.count) podcasts")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Genres
|
||||||
|
|
||||||
|
func loadGenres(refresh: Bool = false) async throws {
|
||||||
|
guard !isLoadingGenres else { return }
|
||||||
|
guard genres.isEmpty || refresh else { return }
|
||||||
|
guard let service else { throw MAWebSocketClient.ClientError.notConnected }
|
||||||
|
|
||||||
|
isLoadingGenres = true
|
||||||
|
defer { isLoadingGenres = false }
|
||||||
|
|
||||||
|
logger.info("Loading genres")
|
||||||
|
let loaded = try await service.getGenres()
|
||||||
|
genres = loaded.sorted { $0.name < $1.name }
|
||||||
|
logger.info("Loaded \(loaded.count) genres")
|
||||||
|
}
|
||||||
|
|
||||||
|
func browseGenre(genreUri: String) async throws -> [MAMediaItem] {
|
||||||
|
guard let service else { throw MAWebSocketClient.ClientError.notConnected }
|
||||||
|
return try await service.browseGenre(genreUri: genreUri)
|
||||||
|
}
|
||||||
|
|
||||||
func getPodcastEpisodes(podcastUri: String) async throws -> [MAMediaItem] {
|
func getPodcastEpisodes(podcastUri: String) async throws -> [MAMediaItem] {
|
||||||
guard let service else { throw MAWebSocketClient.ClientError.notConnected }
|
guard let service else { throw MAWebSocketClient.ClientError.notConnected }
|
||||||
logger.info("Loading episodes for podcast \(podcastUri)")
|
logger.info("Loading episodes for podcast \(podcastUri)")
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
//
|
||||||
|
// ServicesMALiveActivityManager.swift
|
||||||
|
// Mobile Music Assistant
|
||||||
|
//
|
||||||
|
|
||||||
|
import ActivityKit
|
||||||
|
import Foundation
|
||||||
|
import MobileMAShared
|
||||||
|
import OSLog
|
||||||
|
|
||||||
|
private let logger = Logger(subsystem: "com.musicassistant.mobile", category: "LiveActivity")
|
||||||
|
|
||||||
|
/// Manages the Now Playing Live Activity lifecycle.
|
||||||
|
@Observable
|
||||||
|
final class MALiveActivityManager {
|
||||||
|
|
||||||
|
private var currentActivity: Activity<MusicActivityAttributes>?
|
||||||
|
|
||||||
|
init() {
|
||||||
|
// End any orphaned activities left over from previous sessions or format changes.
|
||||||
|
Task {
|
||||||
|
for orphan in Activity<MusicActivityAttributes>.activities {
|
||||||
|
await orphan.end(dismissalPolicy: .immediate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Public Interface
|
||||||
|
|
||||||
|
/// Start or update the Live Activity with current playback state.
|
||||||
|
func update(trackTitle: String, artistName: String, artworkData: Data?, isPlaying: Bool, playerName: String) {
|
||||||
|
let state = MusicActivityAttributes.ContentState(
|
||||||
|
trackTitle: trackTitle,
|
||||||
|
artistName: artistName,
|
||||||
|
artworkData: artworkData,
|
||||||
|
isPlaying: isPlaying,
|
||||||
|
playerName: playerName
|
||||||
|
)
|
||||||
|
|
||||||
|
if let activity = currentActivity {
|
||||||
|
Task {
|
||||||
|
await activity.update(ActivityContent(state: state, staleDate: nil))
|
||||||
|
logger.debug("Updated live activity: \(trackTitle)")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
start(state: state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// End the Live Activity immediately.
|
||||||
|
func end() {
|
||||||
|
guard let activity = currentActivity else { return }
|
||||||
|
currentActivity = nil
|
||||||
|
Task {
|
||||||
|
await activity.end(dismissalPolicy: .immediate)
|
||||||
|
logger.debug("Ended live activity")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
private func start(state: MusicActivityAttributes.ContentState) {
|
||||||
|
guard ActivityAuthorizationInfo().areActivitiesEnabled else {
|
||||||
|
logger.info("Live Activities not enabled on this device")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let activity = try Activity.request(
|
||||||
|
attributes: MusicActivityAttributes(),
|
||||||
|
content: ActivityContent(state: state, staleDate: nil)
|
||||||
|
)
|
||||||
|
currentActivity = activity
|
||||||
|
logger.info("Started live activity: \(state.trackTitle)")
|
||||||
|
} catch {
|
||||||
|
logger.error("Failed to start live activity: \(error.localizedDescription)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import OSLog
|
import OSLog
|
||||||
|
import UIKit
|
||||||
|
|
||||||
private let logger = Logger(subsystem: "com.musicassistant.mobile", category: "PlayerManager")
|
private let logger = Logger(subsystem: "com.musicassistant.mobile", category: "PlayerManager")
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ final class MAPlayerManager {
|
|||||||
|
|
||||||
private weak var service: MAService?
|
private weak var service: MAService?
|
||||||
private var eventTask: Task<Void, Never>?
|
private var eventTask: Task<Void, Never>?
|
||||||
|
let liveActivityManager = MALiveActivityManager()
|
||||||
|
|
||||||
// MARK: - Initialization
|
// MARK: - Initialization
|
||||||
|
|
||||||
@@ -84,6 +86,7 @@ final class MAPlayerManager {
|
|||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
players[player.playerId] = player
|
players[player.playerId] = player
|
||||||
logger.debug("Updated player: \(player.name) state=\(player.state.rawValue) item=\(player.currentItem?.name ?? "nil")")
|
logger.debug("Updated player: \(player.name) state=\(player.state.rawValue) item=\(player.currentItem?.name ?? "nil")")
|
||||||
|
updateLiveActivity()
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
logger.error("Failed to decode player_updated event: \(error)")
|
logger.error("Failed to decode player_updated event: \(error)")
|
||||||
@@ -98,6 +101,7 @@ final class MAPlayerManager {
|
|||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
playerQueues[queue.queueId] = queue
|
playerQueues[queue.queueId] = queue
|
||||||
logger.debug("Updated queue state for player \(queue.queueId), current: \(queue.currentItem?.name ?? "nil")")
|
logger.debug("Updated queue state for player \(queue.queueId), current: \(queue.currentItem?.name ?? "nil")")
|
||||||
|
updateLiveActivity()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -140,6 +144,119 @@ final class MAPlayerManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Live Activity
|
||||||
|
|
||||||
|
/// Finds the best currently-playing player and pushes its state to the Live Activity.
|
||||||
|
/// Spawns a Task to fetch artwork with auth before updating.
|
||||||
|
private func updateLiveActivity() {
|
||||||
|
let playing = players.values
|
||||||
|
.filter { $0.state == .playing }
|
||||||
|
.first { $0.currentItem != nil || playerQueues[$0.playerId]?.currentItem != nil }
|
||||||
|
?? players.values.first { $0.state == .playing }
|
||||||
|
|
||||||
|
guard let player = playing else {
|
||||||
|
liveActivityManager.end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let item = playerQueues[player.playerId]?.currentItem ?? player.currentItem else {
|
||||||
|
liveActivityManager.end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let media = item.mediaItem
|
||||||
|
let trackTitle = item.name.isEmpty ? (media?.name ?? "Unknown Track") : item.name
|
||||||
|
let artistName = media?.artists?.first?.name ?? ""
|
||||||
|
let isPlaying = player.state == .playing
|
||||||
|
let playerName = player.name
|
||||||
|
let imagePath = media?.imageUrl
|
||||||
|
let imageProvider = media?.imageProvider
|
||||||
|
|
||||||
|
logger.debug("updateLiveActivity: track='\(trackTitle)' imagePath=\(imagePath ?? "nil")")
|
||||||
|
|
||||||
|
// Update immediately so the live activity appears without waiting for artwork.
|
||||||
|
liveActivityManager.update(
|
||||||
|
trackTitle: trackTitle,
|
||||||
|
artistName: artistName,
|
||||||
|
artworkData: nil,
|
||||||
|
isPlaying: isPlaying,
|
||||||
|
playerName: playerName
|
||||||
|
)
|
||||||
|
|
||||||
|
// Then fetch artwork in background and refresh.
|
||||||
|
let capturedService = service
|
||||||
|
Task {
|
||||||
|
let artworkData = await Self.fetchArtworkData(path: imagePath, provider: imageProvider, service: capturedService)
|
||||||
|
logger.debug("fetchArtworkData result: \(artworkData != nil ? "\(artworkData!.count) bytes" : "nil")")
|
||||||
|
guard let artworkData else { return }
|
||||||
|
liveActivityManager.update(
|
||||||
|
trackTitle: trackTitle,
|
||||||
|
artistName: artistName,
|
||||||
|
artworkData: artworkData,
|
||||||
|
isPlaying: isPlaying,
|
||||||
|
playerName: playerName
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches artwork as small JPEG data for the Live Activity.
|
||||||
|
/// Checks the app's ImageCache at sizes the app normally loads (512, 64) before
|
||||||
|
/// falling back to a fresh network download at size 128 with auth.
|
||||||
|
private static func fetchArtworkData(path: String?, provider: String?, service: MAService?) async -> Data? {
|
||||||
|
guard let path, !path.isEmpty else {
|
||||||
|
logger.debug("fetchArtworkData: no image path")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let service else {
|
||||||
|
logger.debug("fetchArtworkData: service is nil")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check cache at sizes the app commonly loads
|
||||||
|
for size in [512, 64] {
|
||||||
|
guard let url = service.imageProxyURL(path: path, provider: provider, size: size) else { continue }
|
||||||
|
let key = ImageCache.shared.cacheKey(for: url)
|
||||||
|
if let img = ImageCache.shared.memoryImage(for: key) {
|
||||||
|
logger.debug("fetchArtworkData: memory cache hit at size \(size)")
|
||||||
|
return resizeAndEncode(img)
|
||||||
|
}
|
||||||
|
if let img = await Task.detached(priority: .userInitiated) { ImageCache.shared.diskImage(for: key) }.value {
|
||||||
|
logger.debug("fetchArtworkData: disk cache hit at size \(size)")
|
||||||
|
return resizeAndEncode(img)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not cached — download fresh at compact size with auth header
|
||||||
|
guard let downloadURL = service.imageProxyURL(path: path, provider: provider, size: 128) else { return nil }
|
||||||
|
logger.debug("fetchArtworkData: downloading from \(downloadURL)")
|
||||||
|
var request = URLRequest(url: downloadURL)
|
||||||
|
if let token = service.authManager.currentToken {
|
||||||
|
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
} else {
|
||||||
|
logger.warning("fetchArtworkData: no auth token available")
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let (data, response) = try await URLSession.shared.data(for: request)
|
||||||
|
let status = (response as? HTTPURLResponse)?.statusCode ?? -1
|
||||||
|
logger.debug("fetchArtworkData: HTTP \(status), \(data.count) bytes")
|
||||||
|
guard status == 200, let img = UIImage(data: data) else { return nil }
|
||||||
|
ImageCache.shared.store(img, data: data, for: ImageCache.shared.cacheKey(for: downloadURL))
|
||||||
|
return resizeAndEncode(img)
|
||||||
|
} catch {
|
||||||
|
logger.error("fetchArtworkData: network error: \(error.localizedDescription)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func resizeAndEncode(_ image: UIImage) -> Data? {
|
||||||
|
// ActivityKit ContentState limit is 4 KB total (Data fields are base64 in the payload).
|
||||||
|
// 40×40 JPEG at 0.3 quality ≈ 400–700 bytes, well within limits.
|
||||||
|
let size = CGSize(width: 40, height: 40)
|
||||||
|
let renderer = UIGraphicsImageRenderer(size: size)
|
||||||
|
let scaled = renderer.image { _ in image.draw(in: CGRect(origin: .zero, size: size)) }
|
||||||
|
return scaled.jpegData(compressionQuality: 0.3)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Data Loading
|
// MARK: - Data Loading
|
||||||
|
|
||||||
/// Load all players and their queue states
|
/// Load all players and their queue states
|
||||||
@@ -175,6 +292,7 @@ final class MAPlayerManager {
|
|||||||
playerQueues[pid] = queue
|
playerQueues[pid] = queue
|
||||||
}
|
}
|
||||||
logger.info("Loaded queue states for \(queueResults.count) players")
|
logger.info("Loaded queue states for \(queueResults.count) players")
|
||||||
|
updateLiveActivity()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -351,6 +351,50 @@ final class MAService {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get genres
|
||||||
|
func getGenres() async throws -> [MAGenre] {
|
||||||
|
logger.debug("Fetching genres")
|
||||||
|
return try await webSocketClient.sendCommand(
|
||||||
|
"music/genres/library_items",
|
||||||
|
resultType: [MAGenre].self
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Browse items under a genre URI.
|
||||||
|
/// MA returns provider sub-folders at the first level, so we auto-expand
|
||||||
|
/// them with a second browse pass to surface actual artists/albums.
|
||||||
|
func browseGenre(genreUri: String) async throws -> [MAMediaItem] {
|
||||||
|
logger.debug("Browsing genre \(genreUri)")
|
||||||
|
let firstLevel = try await webSocketClient.sendCommand(
|
||||||
|
"music/browse",
|
||||||
|
args: ["uri": genreUri],
|
||||||
|
resultType: [MAMediaItem].self
|
||||||
|
)
|
||||||
|
|
||||||
|
// If first level already contains real media items, return them.
|
||||||
|
let realItems = firstLevel.filter {
|
||||||
|
guard let t = $0.mediaType else { return false }
|
||||||
|
return t != .unknown
|
||||||
|
}
|
||||||
|
if !realItems.isEmpty { return realItems }
|
||||||
|
|
||||||
|
// Otherwise these are sub-folders (providers) — browse each one.
|
||||||
|
var allItems: [MAMediaItem] = []
|
||||||
|
var seen = Set<String>()
|
||||||
|
for folder in firstLevel {
|
||||||
|
let items = (try? await webSocketClient.sendCommand(
|
||||||
|
"music/browse",
|
||||||
|
args: ["uri": folder.uri],
|
||||||
|
resultType: [MAMediaItem].self
|
||||||
|
)) ?? []
|
||||||
|
for item in items where seen.insert(item.uri).inserted {
|
||||||
|
allItems.append(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.debug("Genre browse returned \(allItems.count) items after expanding \(firstLevel.count) folders")
|
||||||
|
return allItems
|
||||||
|
}
|
||||||
|
|
||||||
/// Get radio stations
|
/// Get radio stations
|
||||||
func getRadios() async throws -> [MAMediaItem] {
|
func getRadios() async throws -> [MAMediaItem] {
|
||||||
logger.debug("Fetching radios")
|
logger.debug("Fetching radios")
|
||||||
|
|||||||
@@ -0,0 +1,201 @@
|
|||||||
|
//
|
||||||
|
// GenresView.swift
|
||||||
|
// Mobile Music Assistant
|
||||||
|
//
|
||||||
|
// Created by Sven Hanold on 17.04.26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
// MARK: - Genres List
|
||||||
|
|
||||||
|
struct GenresView: View {
|
||||||
|
@Environment(MAService.self) private var service
|
||||||
|
@State private var errorMessage: String?
|
||||||
|
@State private var showError = false
|
||||||
|
|
||||||
|
private var genres: [MAGenre] { service.libraryManager.genres }
|
||||||
|
private var isLoading: Bool { service.libraryManager.isLoadingGenres }
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List(genres) { genre in
|
||||||
|
NavigationLink(value: genre) {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
Image(systemName: "guitars")
|
||||||
|
.font(.title3)
|
||||||
|
.foregroundStyle(.tint)
|
||||||
|
.frame(width: 36, height: 36)
|
||||||
|
.background(.tint.opacity(0.12), in: RoundedRectangle(cornerRadius: 8))
|
||||||
|
Text(genre.name.capitalized)
|
||||||
|
.font(.body)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listStyle(.plain)
|
||||||
|
.overlay {
|
||||||
|
if genres.isEmpty && isLoading {
|
||||||
|
ProgressView()
|
||||||
|
} else if genres.isEmpty && !isLoading {
|
||||||
|
ContentUnavailableView(
|
||||||
|
"No Genres",
|
||||||
|
systemImage: "guitars",
|
||||||
|
description: Text("Your library doesn't contain any genres yet")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.refreshable {
|
||||||
|
await loadGenres(refresh: true)
|
||||||
|
}
|
||||||
|
.task {
|
||||||
|
if genres.isEmpty {
|
||||||
|
await loadGenres(refresh: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.alert("Error", isPresented: $showError) {
|
||||||
|
Button("OK", role: .cancel) { }
|
||||||
|
} message: {
|
||||||
|
if let errorMessage { Text(errorMessage) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadGenres(refresh: Bool) async {
|
||||||
|
do {
|
||||||
|
try await service.libraryManager.loadGenres(refresh: refresh)
|
||||||
|
} catch {
|
||||||
|
errorMessage = error.localizedDescription
|
||||||
|
showError = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Genre Detail
|
||||||
|
|
||||||
|
struct GenreDetailView: View {
|
||||||
|
@Environment(MAService.self) private var service
|
||||||
|
let genre: MAGenre
|
||||||
|
|
||||||
|
@State private var items: [MAMediaItem] = []
|
||||||
|
@State private var isLoading = false
|
||||||
|
@State private var errorMessage: String?
|
||||||
|
@State private var showError = false
|
||||||
|
|
||||||
|
private var artists: [MAMediaItem] { items.filter { $0.mediaType == .artist }.sorted { $0.name < $1.name } }
|
||||||
|
private var albums: [MAMediaItem] { items.filter { $0.mediaType == .album }.sorted { $0.name < $1.name } }
|
||||||
|
private var others: [MAMediaItem] { items.filter { $0.mediaType != .artist && $0.mediaType != .album } }
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
List {
|
||||||
|
if !artists.isEmpty {
|
||||||
|
Section("Artists") {
|
||||||
|
ForEach(artists) { item in
|
||||||
|
let artist = MAArtist(uri: item.uri, name: item.name,
|
||||||
|
imageUrl: item.imageUrl, imageProvider: item.imageProvider)
|
||||||
|
NavigationLink(value: artist) {
|
||||||
|
GenreItemRow(item: item, icon: "music.mic")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !albums.isEmpty {
|
||||||
|
Section("Albums") {
|
||||||
|
ForEach(albums) { item in
|
||||||
|
let album = MAAlbum(uri: item.uri, name: item.name,
|
||||||
|
artists: item.artists,
|
||||||
|
imageUrl: item.imageUrl, imageProvider: item.imageProvider)
|
||||||
|
NavigationLink(value: album) {
|
||||||
|
GenreItemRow(item: item, icon: "square.stack")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !others.isEmpty {
|
||||||
|
Section("Other") {
|
||||||
|
ForEach(others) { item in
|
||||||
|
GenreItemRow(item: item, icon: "music.note")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle(genre.name.capitalized)
|
||||||
|
.navigationBarTitleDisplayMode(.large)
|
||||||
|
.overlay {
|
||||||
|
if isLoading {
|
||||||
|
ProgressView()
|
||||||
|
} else if items.isEmpty && !isLoading {
|
||||||
|
ContentUnavailableView(
|
||||||
|
"No Items",
|
||||||
|
systemImage: "guitars",
|
||||||
|
description: Text("Nothing found for this genre")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.task { await loadItems() }
|
||||||
|
.refreshable { await loadItems() }
|
||||||
|
.alert("Error", isPresented: $showError) {
|
||||||
|
Button("OK", role: .cancel) { }
|
||||||
|
} message: {
|
||||||
|
if let errorMessage { Text(errorMessage) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func loadItems() async {
|
||||||
|
isLoading = true
|
||||||
|
do {
|
||||||
|
items = try await service.libraryManager.browseGenre(genreUri: genre.uri)
|
||||||
|
} catch {
|
||||||
|
errorMessage = error.localizedDescription
|
||||||
|
showError = true
|
||||||
|
}
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Genre Item Row
|
||||||
|
|
||||||
|
private struct GenreItemRow: View {
|
||||||
|
@Environment(MAService.self) private var service
|
||||||
|
let item: MAMediaItem
|
||||||
|
let icon: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
CachedAsyncImage(url: service.imageProxyURL(
|
||||||
|
path: item.imageUrl,
|
||||||
|
provider: item.imageProvider,
|
||||||
|
size: 64
|
||||||
|
)) { image in
|
||||||
|
image.resizable().aspectRatio(contentMode: .fill)
|
||||||
|
} placeholder: {
|
||||||
|
RoundedRectangle(cornerRadius: 6)
|
||||||
|
.fill(Color.gray.opacity(0.2))
|
||||||
|
.overlay {
|
||||||
|
Image(systemName: icon)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: 44, height: 44)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 6))
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(item.name)
|
||||||
|
.font(.body)
|
||||||
|
.lineLimit(1)
|
||||||
|
if let artists = item.artists, !artists.isEmpty {
|
||||||
|
Text(artists.map(\.name).joined(separator: ", "))
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
NavigationStack {
|
||||||
|
GenresView()
|
||||||
|
.environment(MAService())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,7 +9,7 @@ import SwiftUI
|
|||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
enum LibraryTab: CaseIterable {
|
enum LibraryTab: CaseIterable {
|
||||||
case albumArtists, artists, albums, playlists, podcasts, radio
|
case albumArtists, artists, albums, playlists, genres, podcasts, radio
|
||||||
|
|
||||||
var title: LocalizedStringKey {
|
var title: LocalizedStringKey {
|
||||||
switch self {
|
switch self {
|
||||||
@@ -17,6 +17,7 @@ enum LibraryTab: CaseIterable {
|
|||||||
case .artists: return "Artists"
|
case .artists: return "Artists"
|
||||||
case .albums: return "Albums"
|
case .albums: return "Albums"
|
||||||
case .playlists: return "Playlists"
|
case .playlists: return "Playlists"
|
||||||
|
case .genres: return "Genres"
|
||||||
case .podcasts: return "Podcasts"
|
case .podcasts: return "Podcasts"
|
||||||
case .radio: return "Radio"
|
case .radio: return "Radio"
|
||||||
}
|
}
|
||||||
@@ -42,6 +43,7 @@ struct LibraryView: View {
|
|||||||
case .artists: ArtistsView()
|
case .artists: ArtistsView()
|
||||||
case .albums: AlbumsView()
|
case .albums: AlbumsView()
|
||||||
case .playlists: PlaylistsView()
|
case .playlists: PlaylistsView()
|
||||||
|
case .genres: GenresView()
|
||||||
case .podcasts: PodcastsView()
|
case .podcasts: PodcastsView()
|
||||||
case .radio: RadiosView()
|
case .radio: RadiosView()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -300,7 +300,7 @@ struct PodcastDetailView: View {
|
|||||||
private func loadEpisodes() async {
|
private func loadEpisodes() async {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
do {
|
do {
|
||||||
episodes = try await service.libraryManager.getPodcastEpisodes(podcastUri: podcast.uri)
|
episodes = try await service.libraryManager.getPodcastEpisodes(podcastUri: podcast.uri).reversed()
|
||||||
isLoading = false
|
isLoading = false
|
||||||
} catch is CancellationError {
|
} catch is CancellationError {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ private let syncLogger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "Mobi
|
|||||||
|
|
||||||
struct MainTabView: View {
|
struct MainTabView: View {
|
||||||
@Environment(MAService.self) private var service
|
@Environment(MAService.self) private var service
|
||||||
|
@Environment(\.scenePhase) private var scenePhase
|
||||||
@State private var selectedTab: String = "library"
|
@State private var selectedTab: String = "library"
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
@@ -49,6 +50,11 @@ struct MainTabView: View {
|
|||||||
.onDisappear {
|
.onDisappear {
|
||||||
service.playerManager.stopListening()
|
service.playerManager.stopListening()
|
||||||
}
|
}
|
||||||
|
.onChange(of: scenePhase) { _, newPhase in
|
||||||
|
if newPhase == .active {
|
||||||
|
Task { try? await service.playerManager.loadPlayers() }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,7 +99,7 @@ struct PlayerListView: View {
|
|||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
NavigationStack {
|
||||||
Group {
|
Group {
|
||||||
if isLoading {
|
if isLoading && !hasContent {
|
||||||
ProgressView()
|
ProgressView()
|
||||||
} else if let errorMessage {
|
} else if let errorMessage {
|
||||||
ContentUnavailableView(
|
ContentUnavailableView(
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>NSSupportsLiveActivities</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "dark"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"appearances" : [
|
||||||
|
{
|
||||||
|
"appearance" : "luminosity",
|
||||||
|
"value" : "tinted"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"idiom" : "universal",
|
||||||
|
"platform" : "ios",
|
||||||
|
"size" : "1024x1024"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"colors" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?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>NSExtension</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSExtensionPointIdentifier</key>
|
||||||
|
<string>com.apple.widgetkit-extension</string>
|
||||||
|
</dict>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
//
|
||||||
|
// MALiveActivityAttributes.swift
|
||||||
|
// MobileMALiveActivity
|
||||||
|
//
|
||||||
|
// Re-exports MusicActivityAttributes from the shared package.
|
||||||
|
//
|
||||||
|
|
||||||
|
@_exported import MobileMAShared
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
//
|
||||||
|
// MobileMALiveActivityBundle.swift
|
||||||
|
// MobileMALiveActivity
|
||||||
|
//
|
||||||
|
// Created by Sven Hanold on 17.04.26.
|
||||||
|
//
|
||||||
|
|
||||||
|
import WidgetKit
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct MobileMALiveActivityBundle: WidgetBundle {
|
||||||
|
var body: some Widget {
|
||||||
|
MobileMALiveActivityLiveActivity()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
//
|
||||||
|
// MobileMALiveActivityLiveActivity.swift
|
||||||
|
// MobileMALiveActivity
|
||||||
|
//
|
||||||
|
|
||||||
|
import ActivityKit
|
||||||
|
import MobileMAShared
|
||||||
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
import WidgetKit
|
||||||
|
|
||||||
|
private let activityTeal = Color(red: 0.0, green: 0.82, blue: 0.75)
|
||||||
|
|
||||||
|
// MARK: - Artwork View
|
||||||
|
|
||||||
|
private struct ArtworkView: View {
|
||||||
|
let artworkData: Data?
|
||||||
|
let size: CGFloat
|
||||||
|
let cornerRadius: CGFloat
|
||||||
|
let isPlaying: Bool
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Group {
|
||||||
|
if let artworkData, let uiImage = UIImage(data: artworkData) {
|
||||||
|
Image(uiImage: uiImage)
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fill)
|
||||||
|
} else {
|
||||||
|
fallbackIcon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(width: size, height: size)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: cornerRadius))
|
||||||
|
}
|
||||||
|
|
||||||
|
private var fallbackIcon: some View {
|
||||||
|
ZStack {
|
||||||
|
activityTeal.opacity(0.2)
|
||||||
|
Image(systemName: "speaker.wave.3.fill")
|
||||||
|
.symbolEffect(
|
||||||
|
.variableColor.iterative.dimInactiveLayers.reversing,
|
||||||
|
isActive: isPlaying
|
||||||
|
)
|
||||||
|
.font(.system(size: size * 0.4, weight: .semibold))
|
||||||
|
.foregroundStyle(activityTeal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Widget
|
||||||
|
|
||||||
|
struct MobileMALiveActivityLiveActivity: Widget {
|
||||||
|
var body: some WidgetConfiguration {
|
||||||
|
ActivityConfiguration(for: MusicActivityAttributes.self) { context in
|
||||||
|
LockScreenView(state: context.state)
|
||||||
|
.activityBackgroundTint(activityTeal.opacity(0.2))
|
||||||
|
} dynamicIsland: { context in
|
||||||
|
DynamicIsland {
|
||||||
|
DynamicIslandExpandedRegion(.leading) {
|
||||||
|
ArtworkView(
|
||||||
|
artworkData: context.state.artworkData,
|
||||||
|
size: 50,
|
||||||
|
cornerRadius: 10,
|
||||||
|
isPlaying: context.state.isPlaying
|
||||||
|
)
|
||||||
|
.padding(.leading, 4)
|
||||||
|
}
|
||||||
|
DynamicIslandExpandedRegion(.trailing) {
|
||||||
|
Image(systemName: context.state.isPlaying ? "pause.circle.fill" : "play.circle.fill")
|
||||||
|
.font(.title)
|
||||||
|
.foregroundStyle(activityTeal)
|
||||||
|
.padding(.trailing, 4)
|
||||||
|
}
|
||||||
|
DynamicIslandExpandedRegion(.center) {
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(context.state.trackTitle)
|
||||||
|
.font(.headline)
|
||||||
|
.lineLimit(1)
|
||||||
|
Text(context.state.artistName)
|
||||||
|
.font(.subheadline)
|
||||||
|
.lineLimit(1)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
DynamicIslandExpandedRegion(.bottom) {
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image(systemName: "hifispeaker.fill")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(activityTeal)
|
||||||
|
Text(context.state.playerName)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
}
|
||||||
|
} compactLeading: {
|
||||||
|
ArtworkView(
|
||||||
|
artworkData: context.state.artworkData,
|
||||||
|
size: 28,
|
||||||
|
cornerRadius: 6,
|
||||||
|
isPlaying: context.state.isPlaying
|
||||||
|
)
|
||||||
|
.padding(.leading, 2)
|
||||||
|
} compactTrailing: {
|
||||||
|
Image(systemName: context.state.isPlaying ? "pause.circle.fill" : "play.circle.fill")
|
||||||
|
.font(.body)
|
||||||
|
.foregroundStyle(activityTeal)
|
||||||
|
.padding(.trailing, 2)
|
||||||
|
} minimal: {
|
||||||
|
ArtworkView(
|
||||||
|
artworkData: context.state.artworkData,
|
||||||
|
size: 24,
|
||||||
|
cornerRadius: 12,
|
||||||
|
isPlaying: context.state.isPlaying
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.keylineTint(activityTeal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Lock Screen View
|
||||||
|
|
||||||
|
private struct LockScreenView: View {
|
||||||
|
let state: MusicActivityAttributes.ContentState
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 12) {
|
||||||
|
ArtworkView(artworkData: state.artworkData, size: 54, cornerRadius: 10, isPlaying: state.isPlaying)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
|
Text(state.trackTitle.isEmpty ? "Now Playing" : state.trackTitle)
|
||||||
|
.font(.headline)
|
||||||
|
.lineLimit(1)
|
||||||
|
if !state.artistName.isEmpty {
|
||||||
|
Text(state.artistName)
|
||||||
|
.font(.subheadline)
|
||||||
|
.lineLimit(1)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image(systemName: "hifispeaker.fill")
|
||||||
|
.font(.caption2)
|
||||||
|
.foregroundStyle(activityTeal)
|
||||||
|
Text(state.playerName)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image(systemName: state.isPlaying ? "pause.circle.fill" : "play.circle.fill")
|
||||||
|
.font(.system(size: 32))
|
||||||
|
.foregroundStyle(activityTeal)
|
||||||
|
}
|
||||||
|
.padding(16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Preview
|
||||||
|
|
||||||
|
#Preview("Notification", as: .content, using: MusicActivityAttributes()) {
|
||||||
|
MobileMALiveActivityLiveActivity()
|
||||||
|
} contentStates: {
|
||||||
|
MusicActivityAttributes.ContentState(
|
||||||
|
trackTitle: "Bohemian Rhapsody", artistName: "Queen",
|
||||||
|
artworkData: nil, isPlaying: true, playerName: "Living Room")
|
||||||
|
MusicActivityAttributes.ContentState(
|
||||||
|
trackTitle: "Bohemian Rhapsody", artistName: "Queen",
|
||||||
|
artworkData: nil, isPlaying: false, playerName: "Living Room")
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
// swift-tools-version: 5.9
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "MobileMAShared",
|
||||||
|
platforms: [.iOS(.v18)],
|
||||||
|
products: [
|
||||||
|
.library(name: "MobileMAShared", targets: ["MobileMAShared"])
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
.target(
|
||||||
|
name: "MobileMAShared",
|
||||||
|
path: "Sources/MobileMAShared"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import ActivityKit
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct MusicActivityAttributes: ActivityAttributes {
|
||||||
|
public struct ContentState: Codable, Hashable, Sendable {
|
||||||
|
public var trackTitle: String
|
||||||
|
public var artistName: String
|
||||||
|
public var artworkData: Data? // Small JPEG pre-fetched by the main app
|
||||||
|
public var isPlaying: Bool
|
||||||
|
public var playerName: String
|
||||||
|
|
||||||
|
public init(trackTitle: String, artistName: String, artworkData: Data?, isPlaying: Bool, playerName: String) {
|
||||||
|
self.trackTitle = trackTitle
|
||||||
|
self.artistName = artistName
|
||||||
|
self.artworkData = artworkData
|
||||||
|
self.isPlaying = isPlaying
|
||||||
|
self.playerName = playerName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public init() {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user