Live Activities fix

This commit is contained in:
2026-04-19 16:57:57 +02:00
parent 053c743c41
commit c41b58d837
24 changed files with 1079 additions and 7 deletions
@@ -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 */;
}
@@ -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 400700 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(
+8
View File
@@ -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
}
}
+16
View File
@@ -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")
}
+16
View File
@@ -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() {}
}