diff --git a/mupl.xcodeproj/project.pbxproj b/mupl.xcodeproj/project.pbxproj index 933aa56..95e12af 100644 --- a/mupl.xcodeproj/project.pbxproj +++ b/mupl.xcodeproj/project.pbxproj @@ -40,7 +40,8 @@ 084E07CB2B8E200F00E14695 /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 084E07CA2B8E200F00E14695 /* Bundle.swift */; }; 084E07CD2B8E2FB900E14695 /* AppInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 084E07CC2B8E2FB900E14695 /* AppInfoView.swift */; }; 084E07CF2B8E32AD00E14695 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 084E07CE2B8E32AD00E14695 /* AppDelegate.swift */; }; - 0856A4B52B46E0D90073ACE7 /* MusicAuthenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0856A4B42B46E0D90073ACE7 /* MusicAuthenticator.swift */; }; + 084E07D22B8F752D00E14695 /* MusicAuthorizationPrompt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 084E07D12B8F752D00E14695 /* MusicAuthorizationPrompt.swift */; }; + 0856A4B52B46E0D90073ACE7 /* MusicManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0856A4B42B46E0D90073ACE7 /* MusicManager.swift */; }; 0856A4BA2B46FF0E0073ACE7 /* MusicCatalog.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0856A4B92B46FF0E0073ACE7 /* MusicCatalog.swift */; }; 0856A4BD2B470B4D0073ACE7 /* MusicCatalogPersonal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0856A4BC2B470B4D0073ACE7 /* MusicCatalogPersonal.swift */; }; 0856A4BF2B470BF30073ACE7 /* MusicCatalogCharts.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0856A4BE2B470BF30073ACE7 /* MusicCatalogCharts.swift */; }; @@ -73,6 +74,8 @@ 086FDB9C2B7E63A600A4102E /* ArtistDetailsAlbumsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086FDB9B2B7E63A600A4102E /* ArtistDetailsAlbumsSection.swift */; }; 086FDBA02B7E645200A4102E /* ArtistDetailsPlaylistsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086FDB9F2B7E645200A4102E /* ArtistDetailsPlaylistsSection.swift */; }; 086FDBA32B7E653C00A4102E /* ArtistDetailsInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086FDBA22B7E653C00A4102E /* ArtistDetailsInfoView.swift */; }; + 0878EED42B9DA09D009EDAD2 /* MusicAuthorizationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0878EED32B9DA09D009EDAD2 /* MusicAuthorizationManager.swift */; }; + 0878EED62B9DA0C7009EDAD2 /* MusicSubscriptionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0878EED52B9DA0C7009EDAD2 /* MusicSubscriptionManager.swift */; }; 08B4552F2B67F47C006007A9 /* LoadableValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B4552E2B67F47C006007A9 /* LoadableValue.swift */; }; 08B455322B6938F0006007A9 /* AlbumDetailsInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B455312B6938F0006007A9 /* AlbumDetailsInfoView.swift */; }; 08B455342B693938006007A9 /* AlbumDetailsTrackList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B455332B693938006007A9 /* AlbumDetailsTrackList.swift */; }; @@ -81,6 +84,7 @@ 08B5F7042B55AF02006248A7 /* SongItemStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B5F7032B55AF02006248A7 /* SongItemStyle.swift */; }; 08B5F7062B55AF47006248A7 /* PlainSongItemStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B5F7052B55AF47006248A7 /* PlainSongItemStyle.swift */; }; 08B5F7082B55BD6D006248A7 /* GroupedSongItemStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B5F7072B55BD6D006248A7 /* GroupedSongItemStyle.swift */; }; + 08B6DC882B9C88AF006FE677 /* MusicSubscriptionPrompt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B6DC872B9C88AF006FE677 /* MusicSubscriptionPrompt.swift */; }; 08B77F452B20F9D2004B6D8D /* muplApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B77F442B20F9D2004B6D8D /* muplApp.swift */; }; 08B77F472B20F9D3004B6D8D /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B77F462B20F9D3004B6D8D /* ContentView.swift */; }; 08B77F492B20F9D3004B6D8D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 08B77F482B20F9D3004B6D8D /* Assets.xcassets */; }; @@ -156,8 +160,9 @@ 084E07CA2B8E200F00E14695 /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; 084E07CC2B8E2FB900E14695 /* AppInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppInfoView.swift; sourceTree = ""; }; 084E07CE2B8E32AD00E14695 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 084E07D12B8F752D00E14695 /* MusicAuthorizationPrompt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicAuthorizationPrompt.swift; sourceTree = ""; }; 0856A4B32B46D7A60073ACE7 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 0856A4B42B46E0D90073ACE7 /* MusicAuthenticator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicAuthenticator.swift; sourceTree = ""; }; + 0856A4B42B46E0D90073ACE7 /* MusicManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicManager.swift; sourceTree = ""; }; 0856A4B92B46FF0E0073ACE7 /* MusicCatalog.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicCatalog.swift; sourceTree = ""; }; 0856A4BC2B470B4D0073ACE7 /* MusicCatalogPersonal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicCatalogPersonal.swift; sourceTree = ""; }; 0856A4BE2B470BF30073ACE7 /* MusicCatalogCharts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicCatalogCharts.swift; sourceTree = ""; }; @@ -189,6 +194,8 @@ 086FDB9B2B7E63A600A4102E /* ArtistDetailsAlbumsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArtistDetailsAlbumsSection.swift; sourceTree = ""; }; 086FDB9F2B7E645200A4102E /* ArtistDetailsPlaylistsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArtistDetailsPlaylistsSection.swift; sourceTree = ""; }; 086FDBA22B7E653C00A4102E /* ArtistDetailsInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArtistDetailsInfoView.swift; sourceTree = ""; }; + 0878EED32B9DA09D009EDAD2 /* MusicAuthorizationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicAuthorizationManager.swift; sourceTree = ""; }; + 0878EED52B9DA0C7009EDAD2 /* MusicSubscriptionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicSubscriptionManager.swift; sourceTree = ""; }; 08B4552E2B67F47C006007A9 /* LoadableValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadableValue.swift; sourceTree = ""; }; 08B455312B6938F0006007A9 /* AlbumDetailsInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumDetailsInfoView.swift; sourceTree = ""; }; 08B455332B693938006007A9 /* AlbumDetailsTrackList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlbumDetailsTrackList.swift; sourceTree = ""; }; @@ -197,6 +204,7 @@ 08B5F7032B55AF02006248A7 /* SongItemStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SongItemStyle.swift; sourceTree = ""; }; 08B5F7052B55AF47006248A7 /* PlainSongItemStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlainSongItemStyle.swift; sourceTree = ""; }; 08B5F7072B55BD6D006248A7 /* GroupedSongItemStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupedSongItemStyle.swift; sourceTree = ""; }; + 08B6DC872B9C88AF006FE677 /* MusicSubscriptionPrompt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MusicSubscriptionPrompt.swift; sourceTree = ""; }; 08B77F412B20F9D2004B6D8D /* mupl.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = mupl.app; sourceTree = BUILT_PRODUCTS_DIR; }; 08B77F442B20F9D2004B6D8D /* muplApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = muplApp.swift; sourceTree = ""; }; 08B77F462B20F9D3004B6D8D /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; @@ -330,6 +338,8 @@ 0856A4C32B4710420073ACE7 /* TrackCollectionItem */, 0856A4F22B49C6DD0073ACE7 /* SongItem */, 0856A4FA2B4B32500073ACE7 /* MusicArtworkImage */, + 084E07D02B8F750800E14695 /* MusicAuthorizationPrompt */, + 08B6DC862B9C889F006FE677 /* MusicSubscriptionPrompt */, 084E07BB2B8CBA2F00E14695 /* Chips */, 08415D112B40BA5700764CD6 /* QueueBar */, 08415CF92B3DC1F400764CD6 /* Slider */, @@ -517,6 +527,14 @@ path = AppInfo; sourceTree = ""; }; + 084E07D02B8F750800E14695 /* MusicAuthorizationPrompt */ = { + isa = PBXGroup; + children = ( + 084E07D12B8F752D00E14695 /* MusicAuthorizationPrompt.swift */, + ); + path = MusicAuthorizationPrompt; + sourceTree = ""; + }; 0856A4B12B46D5870073ACE7 /* Music */ = { isa = PBXGroup; children = ( @@ -570,19 +588,20 @@ isa = PBXGroup; children = ( 084E07C42B8DC22200E14695 /* Models */, - 0856A4D32B472C260073ACE7 /* Authenticator */, + 0856A4D32B472C260073ACE7 /* Manager */, 082683E52B83A190007108E9 /* Player */, 0856A4BB2B470B370073ACE7 /* Catalog */, ); path = Base; sourceTree = ""; }; - 0856A4D32B472C260073ACE7 /* Authenticator */ = { + 0856A4D32B472C260073ACE7 /* Manager */ = { isa = PBXGroup; children = ( - 0856A4B42B46E0D90073ACE7 /* MusicAuthenticator.swift */, + 0856A4B42B46E0D90073ACE7 /* MusicManager.swift */, + 0878EED22B9DA06D009EDAD2 /* Submanagers */, ); - path = Authenticator; + path = Manager; sourceTree = ""; }; 0856A4D42B4738080073ACE7 /* Styles */ = { @@ -755,6 +774,15 @@ path = Components; sourceTree = ""; }; + 0878EED22B9DA06D009EDAD2 /* Submanagers */ = { + isa = PBXGroup; + children = ( + 0878EED32B9DA09D009EDAD2 /* MusicAuthorizationManager.swift */, + 0878EED52B9DA0C7009EDAD2 /* MusicSubscriptionManager.swift */, + ); + path = Submanagers; + sourceTree = ""; + }; 08B4552D2B67F46A006007A9 /* LoadableValue */ = { isa = PBXGroup; children = ( @@ -775,8 +803,8 @@ 08B5F6FC2B53EBAA006248A7 /* AlbumDetails */ = { isa = PBXGroup; children = ( - 08B455302B6938BE006007A9 /* Components */, 08B5F6FD2B53EC8E006248A7 /* View */, + 08B455302B6938BE006007A9 /* Components */, ); path = AlbumDetails; sourceTree = ""; @@ -799,6 +827,14 @@ path = Styles; sourceTree = ""; }; + 08B6DC862B9C889F006FE677 /* MusicSubscriptionPrompt */ = { + isa = PBXGroup; + children = ( + 08B6DC872B9C88AF006FE677 /* MusicSubscriptionPrompt.swift */, + ); + path = MusicSubscriptionPrompt; + sourceTree = ""; + }; 08B77F382B20F9D2004B6D8D = { isa = PBXGroup; children = ( @@ -925,8 +961,8 @@ 08D1B6042B7656F5005FB6FA /* PlaylistDetails */ = { isa = PBXGroup; children = ( - 08D1B6082B765762005FB6FA /* Components */, 08D1B6052B7656F9005FB6FA /* View */, + 08D1B6082B765762005FB6FA /* Components */, ); path = PlaylistDetails; sourceTree = ""; @@ -1144,6 +1180,7 @@ 08415D072B3E00B300764CD6 /* View + IfCondition.swift in Sources */, 08415CD02B321C9800764CD6 /* CGFloat + LayoutSize.swift in Sources */, 0856A4F42B49C6FB0073ACE7 /* SongItem.swift in Sources */, + 0878EED62B9DA0C7009EDAD2 /* MusicSubscriptionManager.swift in Sources */, 084E07BF2B8CBF2900E14695 /* LibraryTrackCollectionList.swift in Sources */, 0856A4F12B49C54C0073ACE7 /* HomeChartsSection.swift in Sources */, 086FDBA32B7E653C00A4102E /* ArtistDetailsInfoView.swift in Sources */, @@ -1157,7 +1194,7 @@ 0856A4DE2B47564D0073ACE7 /* LoadingState.swift in Sources */, 086FDB972B7E60BE00A4102E /* ArtistDetailsLatestReleaseSection.swift in Sources */, 08C4A30E2B7793B800734378 /* SearchOverviewGenresSection.swift in Sources */, - 0856A4B52B46E0D90073ACE7 /* MusicAuthenticator.swift in Sources */, + 0856A4B52B46E0D90073ACE7 /* MusicManager.swift in Sources */, 08F4231D2B45B27B005158F2 /* Data.swift in Sources */, 08415CFB2B3DC1FA00764CD6 /* Slider.swift in Sources */, 086FDBA02B7E645200A4102E /* ArtistDetailsPlaylistsSection.swift in Sources */, @@ -1178,6 +1215,7 @@ 08B4552F2B67F47C006007A9 /* LoadableValue.swift in Sources */, 0856A4BF2B470BF30073ACE7 /* MusicCatalogCharts.swift in Sources */, 08B815C92B74F77900B9F438 /* AppButtonStyle.swift in Sources */, + 08B6DC882B9C88AF006FE677 /* MusicSubscriptionPrompt.swift in Sources */, 0856A4E62B48935E0073ACE7 /* ProvidableSection.swift in Sources */, 08D1B6072B765700005FB6FA /* PlaylistDetailsView.swift in Sources */, 08C4A3332B7806DB00734378 /* MusicSearchResults.swift in Sources */, @@ -1210,6 +1248,7 @@ 084E07CB2B8E200F00E14695 /* Bundle.swift in Sources */, 086FDB9A2B7E633200A4102E /* ArtistDetailsFeaturedAlbumsSection.swift in Sources */, 08B5F6FF2B53EC99006248A7 /* AlbumDetailsView.swift in Sources */, + 0878EED42B9DA09D009EDAD2 /* MusicAuthorizationManager.swift in Sources */, 0856A4BD2B470B4D0073ACE7 /* MusicCatalogPersonal.swift in Sources */, 08F4231F2B45B5A0005158F2 /* NetworkSession.swift in Sources */, 08B77F452B20F9D2004B6D8D /* muplApp.swift in Sources */, @@ -1221,6 +1260,7 @@ 08C4A31A2B77CC7000734378 /* MusicCatalogSearch.swift in Sources */, 086FDB832B7E340F00A4102E /* ChartSectionProvider.swift in Sources */, 08415CF42B3DBCB600764CD6 /* Playbar.swift in Sources */, + 084E07D22B8F752D00E14695 /* MusicAuthorizationPrompt.swift in Sources */, 086FDB952B7E601F00A4102E /* ArtistDetailsTopSongsSection.swift in Sources */, 08C4A3292B77CFD300734378 /* SearchResultsAlbumsSection.swift in Sources */, 08F423362B45EC2C005158F2 /* NetworkUploadPayload.swift in Sources */, diff --git a/mupl/Common/Components/MusicAuthorizationPrompt/MusicAuthorizationPrompt.swift b/mupl/Common/Components/MusicAuthorizationPrompt/MusicAuthorizationPrompt.swift new file mode 100644 index 0000000..6b4641c --- /dev/null +++ b/mupl/Common/Components/MusicAuthorizationPrompt/MusicAuthorizationPrompt.swift @@ -0,0 +1,65 @@ +// +// MusicAuthorizationPrompt.swift +// mupl +// +// Created by Tamerlan Satualdypov on 28.02.2024. +// + +import SwiftUI + +struct MusicAuthorizationPrompt: View { + @EnvironmentObject private var musicManager: MusicManager + + var body: some View { + VStack(alignment: .leading, spacing: 16.0) { + Image("Common/Logo") + .resizable() + .scaledToFit() + .frame(width: 40.0, height: 40.0) + .clipShape(.rect(cornerRadius: 12.0)) + + VStack(spacing: 16.0) { + VStack(alignment: .leading, spacing: 8.0) { + Text("Apple Music access required") + .font(.system(size: 16.0, weight: .bold)) + .foregroundStyle(Color.primaryText) + + Text("Granting permission allows us to enhance your experience by accessing your music library. We prioritize your privacy and will only utilize this access for its intended purpose.") + .font(.system(size: 14.0)) + .multilineTextAlignment(.leading) + .foregroundStyle(Color.secondaryText) + } + + Button { + if self.musicManager.authorization.status == .notDetermined { + self.requestAccess() + } + + if self.musicManager.authorization.status == .denied { + self.openSettings() + } + } label: { + Text("Allow access") + .frame(maxWidth: .infinity) + } + .buttonStyle(.app(.primary)) + } + } + .padding(.all, 24.0) + .frame(maxWidth: 512.0) + .background(.ultraThinMaterial) + .clipShape(.rect(cornerRadius: 16.0)) + .border(style: .quinaryFill, cornerRadius: 16.0) + } + + private func requestAccess() { + Task { + await self.musicManager.authorization.requestIfNeeded() + } + } + + private func openSettings() { + guard let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Media") else { return } + NSWorkspace.shared.open(url) + } +} diff --git a/mupl/Common/Components/MusicSubscriptionPrompt/MusicSubscriptionPrompt.swift b/mupl/Common/Components/MusicSubscriptionPrompt/MusicSubscriptionPrompt.swift new file mode 100644 index 0000000..fb2c0f4 --- /dev/null +++ b/mupl/Common/Components/MusicSubscriptionPrompt/MusicSubscriptionPrompt.swift @@ -0,0 +1,59 @@ +// +// MusicSubscriptionPrompt.swift +// mupl +// +// Created by Tamerlan Satualdypov on 09.03.2024. +// + +import SwiftUI + +struct MusicSubscriptionPrompt: View { + @State private var isShowingOffer: Bool = false + + @EnvironmentObject private var musicManager: MusicManager + + var body: some View { + VStack(alignment: .leading, spacing: 16.0) { + HStack { + Image("Common/AppleMusic") + .resizable() + .scaledToFit() + .frame(width: 40.0, height: 40.0) + + Spacer() + + Image(systemName: "xmark.circle.fill") + .font(.system(size: 16.0)) + .foregroundStyle(.white) + .tappable { + self.musicManager.subscription.isOffering = false + } + } + + VStack(alignment: .leading, spacing: 8.0) { + Text("Join Apple Music") + .font(.system(size: 16.0, weight: .bold)) + .foregroundStyle(Color.primaryText) + + Text("Listen to over 100 million songs, ad-free with zero commercials. Plus get unlimited downloads to your library, and listen anywhere without Wi-Fi or using data. There’s no commitment, you can cancel anytime.") + .font(.system(size: 14.0)) + .multilineTextAlignment(.leading) + .foregroundStyle(Color.secondaryText) + } + + Button { + self.isShowingOffer = true + } label: { + Text("Join") + .frame(maxWidth: .infinity) + } + .buttonStyle(.app(.primary)) + } + .padding(.all, 24.0) + .frame(maxWidth: 512.0) + .background(.ultraThinMaterial) + .clipShape(.rect(cornerRadius: 16.0)) + .border(style: .quinaryFill, cornerRadius: 16.0) + .musicSubscriptionOffer(isPresented: self.$isShowingOffer) + } +} diff --git a/mupl/Common/Components/Playbar/PlaybarSongControls.swift b/mupl/Common/Components/Playbar/PlaybarSongControls.swift index a0b008a..cc46a64 100644 --- a/mupl/Common/Components/Playbar/PlaybarSongControls.swift +++ b/mupl/Common/Components/Playbar/PlaybarSongControls.swift @@ -32,10 +32,12 @@ struct PlaybarSongControls: View { Image(systemName: self.musicPlayer.playbackStatus == .playing ? "pause.fill" : "play.fill") .foregroundStyle(Color.secondaryText) .tappable { - if self.musicPlayer.playbackStatus == .playing { - self.musicPlayer.pause() - } else { - self.musicPlayer.play() + Task { + if self.musicPlayer.playbackStatus == .playing { + self.musicPlayer.pause() + } else { + await self.musicPlayer.play() + } } } @@ -66,7 +68,10 @@ struct PlaybarSongControls: View { self.playbackTimePercentage = requestedPercentage self.musicPlayer.seek(to: requestedPercentage * duration) - self.musicPlayer.play() + + Task { + await self.musicPlayer.play() + } } } .onChange(of: self.musicPlayer.playbackTime) { _, value in diff --git a/mupl/Common/Components/QueueBar/QueueBar.swift b/mupl/Common/Components/QueueBar/QueueBar.swift index 81ffb29..985f3bd 100644 --- a/mupl/Common/Components/QueueBar/QueueBar.swift +++ b/mupl/Common/Components/QueueBar/QueueBar.swift @@ -157,7 +157,9 @@ extension QueueBar { self.isHovered = hovering } .onTapGesture { - self.musicPlayer.skip(to: self.song) + Task { + await self.musicPlayer.skip(to: self.song) + } } } } diff --git a/mupl/Common/Components/SongItem/SongItem.swift b/mupl/Common/Components/SongItem/SongItem.swift index 4704014..8b9b5a2 100644 --- a/mupl/Common/Components/SongItem/SongItem.swift +++ b/mupl/Common/Components/SongItem/SongItem.swift @@ -46,12 +46,14 @@ struct SongItem: View { self.isHovered = hovering } .onTapGesture { - if self.isCurrentlyPlaying { - self.musicPlayer.pause() - } else if self.isCurrent { - self.musicPlayer.play() - } else { - self.musicPlayer.play(item: self.song) + Task { + if self.isCurrentlyPlaying { + self.musicPlayer.pause() + } else if self.isCurrent { + await self.musicPlayer.play() + } else { + await self.musicPlayer.play(item: self.song) + } } } } diff --git a/mupl/Common/Components/TrackCollectionItem/TrackCollectionItem.swift b/mupl/Common/Components/TrackCollectionItem/TrackCollectionItem.swift index 680697c..2ca55ed 100644 --- a/mupl/Common/Components/TrackCollectionItem/TrackCollectionItem.swift +++ b/mupl/Common/Components/TrackCollectionItem/TrackCollectionItem.swift @@ -49,7 +49,9 @@ struct TrackCollectionItem: View { .zIndex(1) Button { - self.musicPlayer.play(item: self.item) + Task { + await self.musicPlayer.play(item: self.item) + } } label: { Image(systemName: "play.circle.fill") .font(.system(size: 24.0)) diff --git a/mupl/Common/Models/LoadableValue/LoadableValue.swift b/mupl/Common/Models/LoadableValue/LoadableValue.swift index 756d944..d3f577e 100644 --- a/mupl/Common/Models/LoadableValue/LoadableValue.swift +++ b/mupl/Common/Models/LoadableValue/LoadableValue.swift @@ -56,6 +56,7 @@ class LoadableValue { } func load(_ task: @Sendable @escaping () async throws -> T) { + self.task?.cancel() self.status = .loading self.task = Task { diff --git a/mupl/Common/Models/Services/Music/Base/Authenticator/MusicAuthenticator.swift b/mupl/Common/Models/Services/Music/Base/Authenticator/MusicAuthenticator.swift deleted file mode 100644 index 2f0f705..0000000 --- a/mupl/Common/Models/Services/Music/Base/Authenticator/MusicAuthenticator.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// MusicAuthenticator.swift -// mupl -// -// Created by Tamerlan Satualdypov on 04.01.2024. -// - -import SwiftUI -import MusicKit - -@MainActor -final class MusicAuthenticator: ObservableObject { - @AppStorage("music.auth.status") var status: MusicAuthorization.Status = .notDetermined - - func requestIfNeeded() async { - guard self.status != .authorized else { return } - self.status = await MusicAuthorization.request() - } -} diff --git a/mupl/Common/Models/Services/Music/Base/Catalog/MusicCatalogCharts.swift b/mupl/Common/Models/Services/Music/Base/Catalog/MusicCatalogCharts.swift index ce1b564..3f5067c 100644 --- a/mupl/Common/Models/Services/Music/Base/Catalog/MusicCatalogCharts.swift +++ b/mupl/Common/Models/Services/Music/Base/Catalog/MusicCatalogCharts.swift @@ -10,8 +10,8 @@ import MusicKit extension MusicCatalog { struct Charts { - func compilation(for genre: Genre? = nil) async -> MusicChartsCompilation { - let charts = await self.charts(for: genre, kinds: [.mostPlayed, .dailyGlobalTop, .cityTop], types: [Song.self, Album.self, Playlist.self]) + func compilation(for genre: Genre? = nil) async throws -> MusicChartsCompilation { + let charts = try await self.charts(for: genre, kinds: [.mostPlayed, .dailyGlobalTop, .cityTop], types: [Song.self, Album.self, Playlist.self]) let albumCharts = Dictionary(uniqueKeysWithValues: charts?.albumCharts.map { ($0.kind, $0) } ?? []) let playlistCharts = Dictionary(uniqueKeysWithValues: charts?.playlistCharts.map { ($0.kind, $0) } ?? []) @@ -24,24 +24,24 @@ extension MusicCatalog { ) } - func albumChart(for genre: Genre? = nil, _ kind: MusicCatalogChartKind) async -> MusicCatalogChart? { - let charts = await self.charts(for: genre, kinds: [kind], types: [Album.self]) + func albumChart(for genre: Genre? = nil, _ kind: MusicCatalogChartKind) async throws -> MusicCatalogChart? { + let charts = try await self.charts(for: genre, kinds: [kind], types: [Album.self]) return charts?.albumCharts.first(where: { $0.kind == kind }) } - func playlistChart(for genre: Genre? = nil, _ kind: MusicCatalogChartKind) async -> MusicCatalogChart? { - let charts = await self.charts(for: genre, kinds: [kind], types: [Playlist.self]) + func playlistChart(for genre: Genre? = nil, _ kind: MusicCatalogChartKind) async throws -> MusicCatalogChart? { + let charts = try await self.charts(for: genre, kinds: [kind], types: [Playlist.self]) return charts?.playlistCharts.first(where: { $0.kind == kind }) } - func songChart(for genre: Genre? = nil, _ kind: MusicCatalogChartKind) async -> MusicCatalogChart? { - let charts = await self.charts(for: genre, kinds: [kind], types: [Song.self]) + func songChart(for genre: Genre? = nil, _ kind: MusicCatalogChartKind) async throws -> MusicCatalogChart? { + let charts = try await self.charts(for: genre, kinds: [kind], types: [Song.self]) return charts?.songCharts.first(where: { $0.kind == kind }) } - private func charts(for genre: Genre?, kinds: [MusicCatalogChartKind], types: [MusicCatalogChartRequestable.Type]) async -> MusicCatalogChartsResponse? { + private func charts(for genre: Genre?, kinds: [MusicCatalogChartKind], types: [MusicCatalogChartRequestable.Type]) async throws -> MusicCatalogChartsResponse? { let request = MusicCatalogChartsRequest(genre: genre, kinds: kinds, types: types) - return try? await request.response() + return try await request.response() } } } diff --git a/mupl/Common/Models/Services/Music/Base/Catalog/MusicCatalogPersonal.swift b/mupl/Common/Models/Services/Music/Base/Catalog/MusicCatalogPersonal.swift index c19443b..59b610e 100644 --- a/mupl/Common/Models/Services/Music/Base/Catalog/MusicCatalogPersonal.swift +++ b/mupl/Common/Models/Services/Music/Base/Catalog/MusicCatalogPersonal.swift @@ -28,24 +28,20 @@ extension MusicCatalog { } var recommendations: [MusicPersonalRecommendationItem] { - get async { + get async throws { let request = MusicPersonalRecommendationsRequest() - let response = try? await request.response() + let response = try await request.response() - if let recommendations = response?.recommendations { - return recommendations - .compactMap { recommendation in - guard !recommendation.items.isEmpty else { return nil } - - return .init( - type: .init(id: recommendation.id.rawValue), - title: recommendation.title ?? "Recommended for You", - items: recommendation.items - ) - } - } - - return [] + return response.recommendations + .compactMap { recommendation in + guard !recommendation.items.isEmpty else { return nil } + + return .init( + type: .init(id: recommendation.id.rawValue), + title: recommendation.title ?? "Recommended for You", + items: recommendation.items + ) + } } } diff --git a/mupl/Common/Models/Services/Music/Base/Manager/MusicManager.swift b/mupl/Common/Models/Services/Music/Base/Manager/MusicManager.swift new file mode 100644 index 0000000..f099d65 --- /dev/null +++ b/mupl/Common/Models/Services/Music/Base/Manager/MusicManager.swift @@ -0,0 +1,34 @@ +// +// MusicAuthenticator.swift +// mupl +// +// Created by Tamerlan Satualdypov on 04.01.2024. +// + +import SwiftUI +import MusicKit +import Combine + +@MainActor +final class MusicManager: ObservableObject { + static let shared: MusicManager = .init() + + @Published var authorization: Authorization = .init() + @Published var subscription: Subscription = .init() + + private var cancellables: Set = .init() + + private init() { + self.authorization.objectWillChange + .sink { _ in + self.objectWillChange.send() + } + .store(in: &self.cancellables) + + self.subscription.objectWillChange + .sink { _ in + self.objectWillChange.send() + } + .store(in: &self.cancellables) + } +} diff --git a/mupl/Common/Models/Services/Music/Base/Manager/Submanagers/MusicAuthorizationManager.swift b/mupl/Common/Models/Services/Music/Base/Manager/Submanagers/MusicAuthorizationManager.swift new file mode 100644 index 0000000..395633d --- /dev/null +++ b/mupl/Common/Models/Services/Music/Base/Manager/Submanagers/MusicAuthorizationManager.swift @@ -0,0 +1,25 @@ +// +// MusicAuthorizationManager.swift +// mupl +// +// Created by Tamerlan Satualdypov on 10.03.2024. +// + +import SwiftUI +import MusicKit + +extension MusicManager { + @MainActor + final class Authorization: ObservableObject { + @Published var status: MusicAuthorization.Status + + init() { + self.status = MusicAuthorization.currentStatus + } + + func requestIfNeeded() async { + guard self.status != .authorized else { return } + self.status = await MusicAuthorization.request() + } + } +} diff --git a/mupl/Common/Models/Services/Music/Base/Manager/Submanagers/MusicSubscriptionManager.swift b/mupl/Common/Models/Services/Music/Base/Manager/Submanagers/MusicSubscriptionManager.swift new file mode 100644 index 0000000..00161a5 --- /dev/null +++ b/mupl/Common/Models/Services/Music/Base/Manager/Submanagers/MusicSubscriptionManager.swift @@ -0,0 +1,38 @@ +// +// MusicSubscriptionManager.swift +// mupl +// +// Created by Tamerlan Satualdypov on 10.03.2024. +// + +import SwiftUI +import MusicKit + +extension MusicManager { + @MainActor + final class Subscription: ObservableObject { + @Published var status: MusicSubscription? + @Published var isOffering: Bool = false + + var canOffer: Bool { + return self.status?.canBecomeSubscriber ?? false + } + + init() { + self.startObservation() + } + + func offer() { + guard self.canOffer else { return } + self.isOffering = true + } + + private func startObservation() { + Task { + for await subscription in MusicSubscription.subscriptionUpdates { + self.status = subscription + } + } + } + } +} diff --git a/mupl/Common/Models/Services/Music/Base/Player/MusicPlayer.swift b/mupl/Common/Models/Services/Music/Base/Player/MusicPlayer.swift index 251c4ec..424a00e 100644 --- a/mupl/Common/Models/Services/Music/Base/Player/MusicPlayer.swift +++ b/mupl/Common/Models/Services/Music/Base/Player/MusicPlayer.swift @@ -10,6 +10,7 @@ import Combine import MusicKit import AVFoundation +@MainActor final class MusicPlayer: ObservableObject { typealias PlaybackStatus = MusicKit.MusicPlayer.PlaybackStatus typealias ShuffleMode = MusicKit.MusicPlayer.ShuffleMode @@ -20,6 +21,7 @@ final class MusicPlayer: ObservableObject { case backward } + private let manager: MusicManager = .shared private let player: ApplicationMusicPlayer = .shared private let audio: Audio = .init() @@ -82,39 +84,39 @@ final class MusicPlayer: ObservableObject { } } - func play(shuffleMode: ShuffleMode? = nil) { - Task { - if let shuffleMode = shuffleMode { - await MainActor.run { - self.shuffleMode = shuffleMode - } - } - - try await self.player.prepareToPlay() - try await self.player.play() + func play(shuffleMode: ShuffleMode? = nil) async { + guard !self.manager.subscription.canOffer else { + return self.manager.subscription.offer() } + + if let shuffleMode = shuffleMode { + self.shuffleMode = shuffleMode + } + + try? await self.player.prepareToPlay() + try? await self.player.play() } - func play(item: PlayableMusicItem, shuffleMode: ShuffleMode? = nil) { + func play(item: PlayableMusicItem, shuffleMode: ShuffleMode? = nil) async { self.player.queue = [item] - self.play(shuffleMode: shuffleMode) + await self.play(shuffleMode: shuffleMode) } - func play(song: Song) { + func play(song: Song) async { self.player.queue = [song] - self.play() + await self.play() } - func play(songs: [Song], shuffleMode: ShuffleMode? = nil) { + func play(songs: [Song], shuffleMode: ShuffleMode? = nil) async { self.player.queue = .init(for: songs) - self.play(shuffleMode: shuffleMode) + await self.play(shuffleMode: shuffleMode) } - func skip(to song: Song) { + func skip(to song: Song) async { guard self.queue.contains(where: { $0.id == song.id }) else { return } self.player.queue = .init(self.player.queue.entries, startingAt: .init(song)) - self.play() + await self.play() } func skip(_ direction: ActionDirection = .forward) { @@ -156,9 +158,7 @@ final class MusicPlayer: ObservableObject { case .song(let song) = self.player.queue.entries.last?.item, song.id == self.currentSong?.id { - await MainActor.run { - self.player.queue.entries = [] - } + self.player.queue.entries = [] } } } diff --git a/mupl/Modules/AlbumDetails/Components/AlbumDetailsInfoView.swift b/mupl/Modules/AlbumDetails/Components/AlbumDetailsInfoView.swift index 7e198f9..78baec4 100644 --- a/mupl/Modules/AlbumDetails/Components/AlbumDetailsInfoView.swift +++ b/mupl/Modules/AlbumDetails/Components/AlbumDetailsInfoView.swift @@ -38,7 +38,7 @@ extension AlbumDetailsView { self.info } - self.buttons + self.playButtons self.copyright } .frame(width: 256.0) @@ -60,16 +60,6 @@ extension AlbumDetailsView { } Spacer() - - Button { - - } label: { - Image(systemName: "ellipsis.circle.fill") - .font(.system(size: 24.0)) - .symbolRenderingMode(.hierarchical) - .foregroundStyle(Color.pinkAccent) - } - .buttonStyle(.plain) } .zIndex(1) @@ -86,10 +76,12 @@ extension AlbumDetailsView { } } - private var buttons: some View { + private var playButtons: some View { HStack(spacing: 4.0) { Button { - self.musicPlayer.play(item: self.album) + Task { + await self.musicPlayer.play(item: self.album) + } } label: { HStack(spacing: 4.0) { Image(systemName: "play.fill") @@ -100,7 +92,9 @@ extension AlbumDetailsView { .buttonStyle(.app(.primary)) Button { - self.musicPlayer.play(item: self.album, shuffleMode: .songs) + Task { + await self.musicPlayer.play(item: self.album, shuffleMode: .songs) + } } label: { HStack(spacing: 4.0) { Image(systemName: "shuffle") diff --git a/mupl/Modules/ArtistDetails/Components/ArtistDetailsInfoView.swift b/mupl/Modules/ArtistDetails/Components/ArtistDetailsInfoView.swift index 08a431b..53a003f 100644 --- a/mupl/Modules/ArtistDetails/Components/ArtistDetailsInfoView.swift +++ b/mupl/Modules/ArtistDetails/Components/ArtistDetailsInfoView.swift @@ -41,8 +41,10 @@ extension ArtistDetailsView { HStack(spacing: 12.0) { Button { - if let topSongs = self.artist.topSongs { - self.musicPlayer.play(songs: .init(topSongs), shuffleMode: .songs) + Task { + if let topSongs = self.artist.topSongs { + await self.musicPlayer.play(songs: .init(topSongs), shuffleMode: .songs) + } } } label: { Image(systemName: "play.circle.fill") diff --git a/mupl/Modules/ArtistDetails/SectionProvider/Sections/ArtistDetailsLatestReleaseSection.swift b/mupl/Modules/ArtistDetails/SectionProvider/Sections/ArtistDetailsLatestReleaseSection.swift index ef38a58..af821d5 100644 --- a/mupl/Modules/ArtistDetails/SectionProvider/Sections/ArtistDetailsLatestReleaseSection.swift +++ b/mupl/Modules/ArtistDetails/SectionProvider/Sections/ArtistDetailsLatestReleaseSection.swift @@ -39,33 +39,16 @@ extension ArtistDetailsSectionProvider { .foregroundStyle(Color.secondaryText) } } - } - } - .buttonStyle(.plain) - - Spacer() - - HStack(spacing: 8.0) { - Button { - } label: { - Image(systemName: "play.circle.fill") - .font(.system(size: 24.0)) - .symbolRenderingMode(.multicolor) - .foregroundStyle(Color.pinkAccent) - } - .buttonStyle(.plain) - - Button { + Spacer() - } label: { - Image(systemName: "ellipsis.circle.fill") - .font(.system(size: 24.0)) - .symbolRenderingMode(.hierarchical) - .foregroundStyle(Color.pinkAccent) + Image(systemName: "chevron.forward") + .font(.system(size: 14.0)) + .foregroundStyle(Color.secondaryText) } - .buttonStyle(.plain) + .contentShape(Rectangle()) } + .buttonStyle(.plain) } .padding(.vertical, 12.0) .padding(.horizontal, 16.0) diff --git a/mupl/Modules/Chart/View/ChartView.swift b/mupl/Modules/Chart/View/ChartView.swift index d6a977e..68c797f 100644 --- a/mupl/Modules/Chart/View/ChartView.swift +++ b/mupl/Modules/Chart/View/ChartView.swift @@ -45,7 +45,7 @@ struct ChartView: View { .scrollDisabled(!self.charts.status.isLoaded) .task { self.charts.load { - await self.musicCatalog.charts.compilation(for: self.genre) + try await self.musicCatalog.charts.compilation(for: self.genre) } } } diff --git a/mupl/Modules/Home/View/HomeView.swift b/mupl/Modules/Home/View/HomeView.swift index cb78438..063e943 100644 --- a/mupl/Modules/Home/View/HomeView.swift +++ b/mupl/Modules/Home/View/HomeView.swift @@ -9,14 +9,18 @@ import SwiftUI import MusicKit struct HomeView: View { + private struct Value: Hashable { + let recommendations: [MusicPersonalRecommendationItem] + let charts: MusicChartsCompilation? + } + private let sectionProvider: HomeSectionProvider = .init() + @EnvironmentObject private var musicManager: MusicManager @EnvironmentObject private var musicCatalog: MusicCatalog @EnvironmentObject private var router: Router - @State private var recommendations: [MusicPersonalRecommendationItem] = [] - @State private var charts: MusicChartsCompilation? - @State private var loadingState: LoadingState = .idle + @State private var value: LoadableValue = .init() var body: some View { NavigationStack(path: self.router.bindablePath(for: .home)) { @@ -27,27 +31,19 @@ struct HomeView: View { .foregroundStyle(Color.primaryText) Group { - switch self.loadingState { - case .idle, .loading: + switch self.value.status { + case .idle, .loading, .error: self.skeleton - case .loaded: - self.content + case .loaded(let content): + self.content(content) } } .transition(.opacity) } .padding(.all, 24.0) - .animation(.easeIn, value: self.loadingState) - } - .scrollDisabled(self.loadingState != .loaded) - .task { - self.loadingState = .loading - - self.recommendations = await self.musicCatalog.personal.recommendations - self.charts = await self.musicCatalog.charts.compilation() - - self.loadingState = .loaded + .animation(.easeIn, value: self.value.status) } + .scrollDisabled(!self.value.status.isLoaded) .navigationDestination(for: Artist.self) { artist in ArtistDetailsView(artist: artist) } @@ -58,15 +54,19 @@ struct HomeView: View { PlaylistDetailsView(playlist: playlist) } } + .onAppear(perform: self.loadValue) + .onChange(of: self.musicManager.authorization.status) { _, _ in + self.loadValue() + } } // MARK: - Content - private var content: some View { + private func content(_ value: Value) -> some View { Group { - self.sectionProvider.section(for: \.recommendations, value: self.recommendations) + self.sectionProvider.section(for: \.recommendations, value: value.recommendations) - if let charts = self.charts { + if let charts = value.charts { self.sectionProvider.section(for: \.charts, value: charts) } } @@ -78,4 +78,15 @@ struct HomeView: View { self.sectionProvider.skeleton(for: \.charts) } } + + // MARK: - Utility + + private func loadValue() { + self.value.load { + return .init( + recommendations: try await self.musicCatalog.personal.recommendations, + charts: try await self.musicCatalog.charts.compilation() + ) + } + } } diff --git a/mupl/Modules/PlaylistDetails/Components/PlaylistDetailsInfoView.swift b/mupl/Modules/PlaylistDetails/Components/PlaylistDetailsInfoView.swift index 023b6a1..7cee982 100644 --- a/mupl/Modules/PlaylistDetails/Components/PlaylistDetailsInfoView.swift +++ b/mupl/Modules/PlaylistDetails/Components/PlaylistDetailsInfoView.swift @@ -13,6 +13,7 @@ extension PlaylistDetailsView { struct InfoView: View { private let playlist: Playlist + @EnvironmentObject private var musicManager: MusicManager @EnvironmentObject private var musicPlayer: MusicPlayer init(playlist: Playlist) { @@ -36,6 +37,7 @@ extension PlaylistDetailsView { self.info self.buttons } + .frame(maxWidth: .infinity, alignment: .leading) } .frame(height: 256.0) } @@ -76,7 +78,9 @@ extension PlaylistDetailsView { HStack { HStack(spacing: 4.0) { Button { - self.musicPlayer.play(item: self.playlist) + Task { + await self.musicPlayer.play(item: self.playlist) + } } label: { HStack(spacing: 4.0) { Image(systemName: "play.fill") @@ -87,7 +91,9 @@ extension PlaylistDetailsView { .buttonStyle(.app(.primary)) Button { - self.musicPlayer.play(item: self.playlist, shuffleMode: .songs) + Task { + await self.musicPlayer.play(item: self.playlist, shuffleMode: .songs) + } } label: { HStack(spacing: 4.0) { Image(systemName: "shuffle") @@ -99,16 +105,6 @@ extension PlaylistDetailsView { } Spacer() - - Button { - - } label: { - Image(systemName: "ellipsis.circle.fill") - .font(.system(size: 24.0)) - .symbolRenderingMode(.hierarchical) - .foregroundStyle(Color.pinkAccent) - } - .buttonStyle(.plain) } } } diff --git a/mupl/Supporting Files/App/muplApp.swift b/mupl/Supporting Files/App/muplApp.swift index 0dc19ce..2eaeb0a 100644 --- a/mupl/Supporting Files/App/muplApp.swift +++ b/mupl/Supporting Files/App/muplApp.swift @@ -11,19 +11,35 @@ import SwiftUI struct muplApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate - @StateObject private var musicAuthenticator: MusicAuthenticator = .init() + @StateObject private var musicManager: MusicManager = .shared @StateObject private var musicCatalog: MusicCatalog = .init() @StateObject private var musicPlayer: MusicPlayer = .init() @StateObject private var router: Router = .init() var body: some Scene { WindowGroup { - ContentView() - .task { - await self.musicAuthenticator.requestIfNeeded() + ZStack { + ContentView() + .zIndex(0) + + Group { + if self.musicManager.authorization.status != .authorized { + self.prompt { + MusicAuthorizationPrompt() + } + } else if self.musicManager.subscription.isOffering { + self.prompt { + MusicSubscriptionPrompt() + } + } } + .zIndex(1) + } + .transition(.opacity) + .animation(.easeIn(duration: 0.2), value: self.musicManager.authorization.status) + .animation(.easeIn(duration: 0.2), value: self.musicManager.subscription.isOffering) } - .environmentObject(self.musicAuthenticator) + .environmentObject(self.musicManager) .environmentObject(self.musicCatalog) .environmentObject(self.musicPlayer) .environmentObject(self.router) @@ -40,10 +56,22 @@ struct muplApp: App { Button("Quit \(Bundle.main.appName)") { NSApplication.shared.terminate(nil) } + .keyboardShortcut("Q", modifiers: [.command]) } } } .windowToolbarStyle(.unified(showsTitle: false)) .windowStyle(.hiddenTitleBar) } + + private func prompt(@ViewBuilder _ content: () -> Content) -> some View { + ZStack { + Color.black + .opacity(0.8) + .ignoresSafeArea() + + content() + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } } diff --git a/mupl/Supporting Files/Resources/Assets.xcassets/Common/AppleMusic.imageset/AppleMusic.pdf b/mupl/Supporting Files/Resources/Assets.xcassets/Common/AppleMusic.imageset/AppleMusic.pdf new file mode 100644 index 0000000..b933e15 Binary files /dev/null and b/mupl/Supporting Files/Resources/Assets.xcassets/Common/AppleMusic.imageset/AppleMusic.pdf differ diff --git a/mupl/Supporting Files/Resources/Assets.xcassets/Common/AppleMusic.imageset/Contents.json b/mupl/Supporting Files/Resources/Assets.xcassets/Common/AppleMusic.imageset/Contents.json new file mode 100644 index 0000000..4e013a4 --- /dev/null +++ b/mupl/Supporting Files/Resources/Assets.xcassets/Common/AppleMusic.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "AppleMusic.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/mupl/Supporting Files/Resources/Assets.xcassets/Common/LogoAlpha.imageset/Contents.json b/mupl/Supporting Files/Resources/Assets.xcassets/Common/LogoAlpha.imageset/Contents.json deleted file mode 100644 index 22f7668..0000000 --- a/mupl/Supporting Files/Resources/Assets.xcassets/Common/LogoAlpha.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "filename" : "common.logo.alpha.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "common.logo.alpha@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "common.logo.alpha@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/mupl/Supporting Files/Resources/Assets.xcassets/Common/LogoAlpha.imageset/common.logo.alpha.png b/mupl/Supporting Files/Resources/Assets.xcassets/Common/LogoAlpha.imageset/common.logo.alpha.png deleted file mode 100644 index 752f85e..0000000 Binary files a/mupl/Supporting Files/Resources/Assets.xcassets/Common/LogoAlpha.imageset/common.logo.alpha.png and /dev/null differ diff --git a/mupl/Supporting Files/Resources/Assets.xcassets/Common/LogoAlpha.imageset/common.logo.alpha@2x.png b/mupl/Supporting Files/Resources/Assets.xcassets/Common/LogoAlpha.imageset/common.logo.alpha@2x.png deleted file mode 100644 index 3aa53ed..0000000 Binary files a/mupl/Supporting Files/Resources/Assets.xcassets/Common/LogoAlpha.imageset/common.logo.alpha@2x.png and /dev/null differ diff --git a/mupl/Supporting Files/Resources/Assets.xcassets/Common/LogoAlpha.imageset/common.logo.alpha@3x.png b/mupl/Supporting Files/Resources/Assets.xcassets/Common/LogoAlpha.imageset/common.logo.alpha@3x.png deleted file mode 100644 index cdb8912..0000000 Binary files a/mupl/Supporting Files/Resources/Assets.xcassets/Common/LogoAlpha.imageset/common.logo.alpha@3x.png and /dev/null differ