Live Activities fix
This commit is contained in:
@@ -10,18 +10,66 @@
|
||||
2616AF4E2F876BEF00CB210E /* ServicesMAStoreManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2616AF4D2F876BEF00CB210E /* ServicesMAStoreManager.swift */; };
|
||||
2616AF502F87782600CB210E /* Donations.storekit in Resources */ = {isa = PBXBuildFile; fileRef = 2616AF4F2F87782600CB210E /* Donations.storekit */; };
|
||||
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 */; };
|
||||
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 */
|
||||
|
||||
/* 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 */
|
||||
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>"; };
|
||||
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>"; };
|
||||
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; };
|
||||
/* 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 */
|
||||
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 */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
path = "Mobile Music Assistant";
|
||||
@@ -30,21 +78,44 @@
|
||||
/* End PBXFileSystemSynchronizedRootGroup 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 */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
269ECE562F92A07000444B14 /* MobileMAShared in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
26DA6F7D2F928B2100849EC7 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
26DA6F7E2F928B2100849EC7 /* WidgetKit.framework */,
|
||||
26DA6F802F928B2100849EC7 /* SwiftUI.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
26ED92582F759EEA0025419D = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
269ECE4D2F929A9800444B14 /* MobileMAShared */,
|
||||
2616AF4F2F87782600CB210E /* Donations.storekit */,
|
||||
26ED92632F759EEA0025419D /* Mobile Music Assistant */,
|
||||
26DA6F822F928B2100849EC7 /* MobileMALiveActivity */,
|
||||
26DA6F7D2F928B2100849EC7 /* Frameworks */,
|
||||
26ED92622F759EEA0025419D /* Products */,
|
||||
26A83F522F825B3C003834A9 /* ViewsPlayerNowPlayingView.swift */,
|
||||
2681ED6E2F8393AC002FB204 /* ViewsPlayerQueueView.swift */,
|
||||
@@ -56,6 +127,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
26ED92612F759EEA0025419D /* Mobile Music Assistant.app */,
|
||||
26DA6F7C2F928B2100849EC7 /* MobileMALiveActivityExtension.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -63,6 +135,29 @@
|
||||
/* End PBXGroup 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 */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 26ED926C2F759EEB0025419D /* Build configuration list for PBXNativeTarget "Mobile Music Assistant" */;
|
||||
@@ -70,6 +165,7 @@
|
||||
26ED925D2F759EEA0025419D /* Sources */,
|
||||
26ED925E2F759EEA0025419D /* Frameworks */,
|
||||
26ED925F2F759EEA0025419D /* Resources */,
|
||||
269ECE592F92A21100444B14 /* CopyFiles */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -80,6 +176,7 @@
|
||||
);
|
||||
name = "Mobile Music Assistant";
|
||||
packageProductDependencies = (
|
||||
269ECE552F92A07000444B14 /* MobileMAShared */,
|
||||
);
|
||||
productName = "Mobile Music Assistant";
|
||||
productReference = 26ED92612F759EEA0025419D /* Mobile Music Assistant.app */;
|
||||
@@ -95,6 +192,9 @@
|
||||
LastSwiftUpdateCheck = 2640;
|
||||
LastUpgradeCheck = 2640;
|
||||
TargetAttributes = {
|
||||
26DA6F7B2F928B2100849EC7 = {
|
||||
CreatedOnToolsVersion = 26.4;
|
||||
};
|
||||
26ED92602F759EEA0025419D = {
|
||||
CreatedOnToolsVersion = 26.4;
|
||||
};
|
||||
@@ -112,17 +212,28 @@
|
||||
);
|
||||
mainGroup = 26ED92582F759EEA0025419D;
|
||||
minimizedProjectReferenceProxies = 1;
|
||||
packageReferences = (
|
||||
269ECE4E2F929FE100444B14 /* XCLocalSwiftPackageReference "MobileMAShared" */,
|
||||
);
|
||||
preferredProjectObjectVersion = 77;
|
||||
productRefGroup = 26ED92622F759EEA0025419D /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
26ED92602F759EEA0025419D /* Mobile Music Assistant */,
|
||||
26DA6F7B2F928B2100849EC7 /* MobileMALiveActivityExtension */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
26DA6F7A2F928B2100849EC7 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
26ED925F2F759EEA0025419D /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -134,6 +245,13 @@
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
26DA6F782F928B2100849EC7 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
26ED925D2F759EEA0025419D /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -147,6 +265,66 @@
|
||||
/* End PBXSourcesBuildPhase 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 */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
@@ -200,7 +378,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 26.4;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
@@ -258,7 +436,7 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 26.4;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.6;
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
@@ -278,7 +456,10 @@
|
||||
DEVELOPMENT_TEAM = EKFHUHT63T;
|
||||
ENABLE_PREVIEWS = 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_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
@@ -315,7 +496,10 @@
|
||||
DEVELOPMENT_TEAM = EKFHUHT63T;
|
||||
ENABLE_PREVIEWS = 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_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
@@ -345,6 +529,15 @@
|
||||
/* End XCBuildConfiguration 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" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
@@ -364,6 +557,26 @@
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* 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 */;
|
||||
}
|
||||
|
||||
+5
@@ -9,6 +9,11 @@
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
<key>MobileMALiveActivityExtension.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>SuppressBuildableAutocreation</key>
|
||||
<dict>
|
||||
|
||||
@@ -13,6 +13,7 @@ enum MANavigationDestination: Hashable {
|
||||
case album(MAAlbum)
|
||||
case playlist(MAPlaylist)
|
||||
case podcast(MAPodcast)
|
||||
case genre(MAGenre)
|
||||
}
|
||||
|
||||
/// ViewModifier to apply all navigation destinations consistently
|
||||
@@ -31,6 +32,9 @@ struct MANavigationDestinations: ViewModifier {
|
||||
.navigationDestination(for: MAPodcast.self) { podcast in
|
||||
PodcastDetailView(podcast: podcast)
|
||||
}
|
||||
.navigationDestination(for: MAGenre.self) { genre in
|
||||
GenreDetailView(genre: genre)
|
||||
}
|
||||
.navigationDestination(for: MANavigationDestination.self) { destination in
|
||||
switch destination {
|
||||
case .artist(let artist):
|
||||
@@ -41,6 +45,8 @@ struct MANavigationDestinations: ViewModifier {
|
||||
PlaylistDetailView(playlist: playlist)
|
||||
case .podcast(let 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 \"%@\"?" : {
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
@@ -1771,6 +1775,14 @@
|
||||
"comment" : "A title for a view that shows when a user has no favorite songs.",
|
||||
"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" : {
|
||||
"localizations" : {
|
||||
"de" : {
|
||||
@@ -1969,6 +1981,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Nothing found for this genre" : {
|
||||
"comment" : "A description displayed when a genre has no items.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Now Playing" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
@@ -2014,6 +2030,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Other" : {
|
||||
"comment" : "A section for items that don't fit into the \"Artists\" or \"Albums\" section.",
|
||||
"isCommentAutoGenerated" : true
|
||||
},
|
||||
"Play" : {
|
||||
"localizations" : {
|
||||
"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" : {
|
||||
"localizations" : {
|
||||
"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
|
||||
|
||||
enum RepeatMode: String, Codable, CaseIterable {
|
||||
|
||||
@@ -23,6 +23,7 @@ final class MALibraryManager {
|
||||
private(set) var albums: [MAAlbum] = []
|
||||
private(set) var playlists: [MAPlaylist] = []
|
||||
private(set) var podcasts: [MAPodcast] = []
|
||||
private(set) var genres: [MAGenre] = []
|
||||
|
||||
// Pagination
|
||||
private var artistsOffset = 0
|
||||
@@ -39,6 +40,7 @@ final class MALibraryManager {
|
||||
private(set) var isLoadingAlbums = false
|
||||
private(set) var isLoadingPlaylists = false
|
||||
private(set) var isLoadingPodcasts = false
|
||||
private(set) var isLoadingGenres = false
|
||||
|
||||
/// URIs currently marked as favorites — source of truth for UI.
|
||||
/// Populated from decoded model data, then mutated optimistically on toggle.
|
||||
@@ -152,6 +154,7 @@ final class MALibraryManager {
|
||||
albums = []
|
||||
playlists = []
|
||||
podcasts = []
|
||||
genres = []
|
||||
favoriteURIs = []
|
||||
artistsOffset = 0
|
||||
albumArtistsOffset = 0
|
||||
@@ -397,6 +400,27 @@ final class MALibraryManager {
|
||||
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] {
|
||||
guard let service else { throw MAWebSocketClient.ClientError.notConnected }
|
||||
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 OSLog
|
||||
import UIKit
|
||||
|
||||
private let logger = Logger(subsystem: "com.musicassistant.mobile", category: "PlayerManager")
|
||||
|
||||
@@ -21,6 +22,7 @@ final class MAPlayerManager {
|
||||
|
||||
private weak var service: MAService?
|
||||
private var eventTask: Task<Void, Never>?
|
||||
let liveActivityManager = MALiveActivityManager()
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
@@ -84,6 +86,7 @@ final class MAPlayerManager {
|
||||
await MainActor.run {
|
||||
players[player.playerId] = player
|
||||
logger.debug("Updated player: \(player.name) state=\(player.state.rawValue) item=\(player.currentItem?.name ?? "nil")")
|
||||
updateLiveActivity()
|
||||
}
|
||||
} catch {
|
||||
logger.error("Failed to decode player_updated event: \(error)")
|
||||
@@ -98,6 +101,7 @@ final class MAPlayerManager {
|
||||
await MainActor.run {
|
||||
playerQueues[queue.queueId] = queue
|
||||
logger.debug("Updated queue state for player \(queue.queueId), current: \(queue.currentItem?.name ?? "nil")")
|
||||
updateLiveActivity()
|
||||
}
|
||||
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
|
||||
|
||||
/// Load all players and their queue states
|
||||
@@ -175,6 +292,7 @@ final class MAPlayerManager {
|
||||
playerQueues[pid] = queue
|
||||
}
|
||||
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
|
||||
func getRadios() async throws -> [MAMediaItem] {
|
||||
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
|
||||
|
||||
enum LibraryTab: CaseIterable {
|
||||
case albumArtists, artists, albums, playlists, podcasts, radio
|
||||
case albumArtists, artists, albums, playlists, genres, podcasts, radio
|
||||
|
||||
var title: LocalizedStringKey {
|
||||
switch self {
|
||||
@@ -17,6 +17,7 @@ enum LibraryTab: CaseIterable {
|
||||
case .artists: return "Artists"
|
||||
case .albums: return "Albums"
|
||||
case .playlists: return "Playlists"
|
||||
case .genres: return "Genres"
|
||||
case .podcasts: return "Podcasts"
|
||||
case .radio: return "Radio"
|
||||
}
|
||||
@@ -42,6 +43,7 @@ struct LibraryView: View {
|
||||
case .artists: ArtistsView()
|
||||
case .albums: AlbumsView()
|
||||
case .playlists: PlaylistsView()
|
||||
case .genres: GenresView()
|
||||
case .podcasts: PodcastsView()
|
||||
case .radio: RadiosView()
|
||||
}
|
||||
|
||||
@@ -300,7 +300,7 @@ struct PodcastDetailView: View {
|
||||
private func loadEpisodes() async {
|
||||
isLoading = true
|
||||
do {
|
||||
episodes = try await service.libraryManager.getPodcastEpisodes(podcastUri: podcast.uri)
|
||||
episodes = try await service.libraryManager.getPodcastEpisodes(podcastUri: podcast.uri).reversed()
|
||||
isLoading = false
|
||||
} catch is CancellationError {
|
||||
return
|
||||
|
||||
@@ -13,6 +13,7 @@ private let syncLogger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "Mobi
|
||||
|
||||
struct MainTabView: View {
|
||||
@Environment(MAService.self) private var service
|
||||
@Environment(\.scenePhase) private var scenePhase
|
||||
@State private var selectedTab: String = "library"
|
||||
|
||||
var body: some View {
|
||||
@@ -49,6 +50,11 @@ struct MainTabView: View {
|
||||
.onDisappear {
|
||||
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 {
|
||||
NavigationStack {
|
||||
Group {
|
||||
if isLoading {
|
||||
if isLoading && !hasContent {
|
||||
ProgressView()
|
||||
} else if let errorMessage {
|
||||
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