From ba588456448c71d9b30e665174fc87ac638db732 Mon Sep 17 00:00:00 2001 From: sosauce2 <98750531+sosauce@users.noreply.github.com> Date: Fri, 18 Oct 2024 17:19:52 +0200 Subject: [PATCH] v2.2.3 --- app/build.gradle.kts | 4 +- app/release/baselineProfiles/0/app-release.dm | Bin 7331 -> 7250 bytes app/release/baselineProfiles/1/app-release.dm | Bin 7301 -> 7224 bytes app/release/output-metadata.json | 4 +- app/src/main/AndroidManifest.xml | 3 + .../com/sosauce/cutemusic/data/MusicState.kt | 24 + .../cutemusic/data/actions/PlayerActions.kt | 18 + .../cutemusic/data/datastore/DataStore.kt | 15 +- .../cutemusic/data/datastore/SettingsExt.kt | 36 -- .../com/sosauce/cutemusic/di/AppModule.kt | 15 +- .../domain/repository/MediaStoreHelper.kt | 12 +- .../domain/repository/MediaStoreHelperImpl.kt | 76 ++- .../sosauce/cutemusic/main/AppContainer.kt | 12 - .../sosauce/cutemusic/main/MainActivity.kt | 2 +- .../sosauce/cutemusic/main/PlaybackService.kt | 81 ++- .../main/quickplay/QuickPlayActivity.kt | 17 +- .../cutemusic/ui/customs/CustomExtention.kt | 50 -- .../cutemusic/ui/navigation/Navigation.kt | 62 +- .../ui/screens/album/AlbumDetailsLandscape.kt | 15 +- .../ui/screens/album/AlbumDetailsScreen.kt | 175 ++++-- .../cutemusic/ui/screens/album/AlbumScreen.kt | 31 +- .../ui/screens/artist/ArtistDetails.kt | 75 ++- .../ui/screens/artist/ArtistsScreen.kt | 37 +- .../cutemusic/ui/screens/lyrics/LyricsView.kt | 31 +- .../cutemusic/ui/screens/main/MainScreen.kt | 554 +++++++++++------- .../main/components/BottomSheetContent.kt | 243 -------- .../ui/screens/metadata/MetadataEditor.kt | 10 +- .../ui/screens/playing/NowPlayingLandscape.kt | 217 +++---- .../ui/screens/playing/NowPlayingScreen.kt | 250 ++++---- .../ui/screens/playing/NowPlayingState.kt | 13 - .../ui/screens/playing/components/Buttons.kt | 59 +- .../playing/components/QuickActionsRow.kt | 136 +++++ .../ui/screens/playing/components/Slider.kt | 74 ++- .../screens/playing/components/SpeedCard.kt | 22 +- .../ui/screens/settings/SettingsScreen.kt | 3 +- .../settings/compenents/SettingsCards.kt | 8 +- .../screens/settings/compenents/Switches.kt | 34 +- .../shared_components/MusicDetailsDialog.kt | 201 +++++++ .../ui/shared_components/MusicViewModel.kt | 248 ++++---- .../ui/shared_components/PostViewModel.kt | 26 +- .../ui/shared_components/Searchbar.kt | 120 ++-- .../com/sosauce/cutemusic/ui/theme/Color.kt | 6 +- .../ui/theme/ColorEngineViewModel.kt | 2 +- .../com/sosauce/cutemusic/ui/theme/Theme.kt | 2 - .../com/sosauce/cutemusic/utils/Customs.kt | 3 - .../com/sosauce/cutemusic/utils/Extensions.kt | 137 ++++- .../com/sosauce/cutemusic/utils/ImageUtils.kt | 4 +- .../main/res/drawable/round_music_note_24.xml | 6 + app/src/main/res/values/strings.xml | 2 + app/src/main/res/xml/automotive_app_desc.xml | 4 + .../baselineProfiles/baseline-prof.txt | 10 +- .../baselineProfiles/startup-prof.txt | 10 +- gradle/libs.versions.toml | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 54 files changed, 1853 insertions(+), 1350 deletions(-) create mode 100644 app/src/main/java/com/sosauce/cutemusic/data/MusicState.kt delete mode 100644 app/src/main/java/com/sosauce/cutemusic/main/AppContainer.kt delete mode 100644 app/src/main/java/com/sosauce/cutemusic/ui/customs/CustomExtention.kt delete mode 100644 app/src/main/java/com/sosauce/cutemusic/ui/screens/main/components/BottomSheetContent.kt delete mode 100644 app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/NowPlayingState.kt create mode 100644 app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/QuickActionsRow.kt create mode 100644 app/src/main/java/com/sosauce/cutemusic/ui/shared_components/MusicDetailsDialog.kt create mode 100644 app/src/main/res/drawable/round_music_note_24.xml create mode 100644 app/src/main/res/xml/automotive_app_desc.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b1e3805..76943c1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -13,8 +13,8 @@ android { applicationId = "com.sosauce.cutemusic" minSdk = 26 targetSdk = 35 - versionCode = 13 - versionName = "2.2.2" + versionCode = 14 + versionName = "2.2.3" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true diff --git a/app/release/baselineProfiles/0/app-release.dm b/app/release/baselineProfiles/0/app-release.dm index 9d41e422326d9f0174610abdab273511d2461578..135c0325c5a30611fc0ad61ad55c62774b2e39cd 100644 GIT binary patch literal 7250 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 VPm7%5-{%{DkMG~g0{Hj#zW{-hlz;#L diff --git a/app/release/baselineProfiles/1/app-release.dm b/app/release/baselineProfiles/1/app-release.dm index 332f30c99564c43668e587847095421bfbe417a2..83c45052989bc0cc67841f4e928a92d7e3bb163e 100644 GIT binary patch literal 7224 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 literal 7301 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 diff --git a/app/release/output-metadata.json b/app/release/output-metadata.json index 27051fc..5b69d58 100644 --- a/app/release/output-metadata.json +++ b/app/release/output-metadata.json @@ -11,8 +11,8 @@ "type": "SINGLE", "filters": [], "attributes": [], - "versionCode": 12, - "versionName": "2.2.1", + "versionCode": 13, + "versionName": "2.2.2", "outputFile": "app-release.apk" } ], diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 60ff93a..06a4946 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -63,6 +63,9 @@ + + \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/data/MusicState.kt b/app/src/main/java/com/sosauce/cutemusic/data/MusicState.kt new file mode 100644 index 0000000..cff4672 --- /dev/null +++ b/app/src/main/java/com/sosauce/cutemusic/data/MusicState.kt @@ -0,0 +1,24 @@ +package com.sosauce.cutemusic.data + +import android.net.Uri +import com.sosauce.cutemusic.domain.model.Lyrics +import java.io.File + +data class MusicState( + var currentlyPlaying: String = "", + var currentArtist: String = "", + val currentArtistId: Long = 0, + var currentArt: Uri? = null, + var isCurrentlyPlaying: Boolean = false, + var currentPosition: Long = 0L, + var currentMusicDuration: Long = 0L, + var currentMusicUri: String = "", + var currentLyrics: List = listOf(), + var isLooping: Boolean = false, + var isShuffling: Boolean = false, + var currentPath: String = "", + val currentAlbum: String = "", + val currentAlbumId: Long = 0, + val currentSize: Long = 0, + val currentLrcFile: File? = null +) diff --git a/app/src/main/java/com/sosauce/cutemusic/data/actions/PlayerActions.kt b/app/src/main/java/com/sosauce/cutemusic/data/actions/PlayerActions.kt index 4698540..c8265ee 100644 --- a/app/src/main/java/com/sosauce/cutemusic/data/actions/PlayerActions.kt +++ b/app/src/main/java/com/sosauce/cutemusic/data/actions/PlayerActions.kt @@ -5,7 +5,25 @@ sealed interface PlayerActions { data object SeekToNextMusic : PlayerActions data object SeekToPreviousMusic : PlayerActions data object RestartSong : PlayerActions + data object PlayRandom : PlayerActions + data object ApplyLoop : PlayerActions + data object ApplyShuffle : PlayerActions data class SeekTo(val position: Long) : PlayerActions data class SeekToSlider(val position: Long) : PlayerActions data class RewindTo(val position: Long) : PlayerActions + data class StartPlayback(val mediaId: String) : PlayerActions + data class StartAlbumPlayback( + val albumName: String, + val mediaId: String? + ) : PlayerActions + + data class StartArtistPlayback( + val artistName: String, + val mediaId: String? + ) : PlayerActions + + data class ApplyPlaybackSpeed( + val speed: Float, + val pitch: Float + ) : PlayerActions } diff --git a/app/src/main/java/com/sosauce/cutemusic/data/datastore/DataStore.kt b/app/src/main/java/com/sosauce/cutemusic/data/datastore/DataStore.kt index 1d45414..bbaa39b 100644 --- a/app/src/main/java/com/sosauce/cutemusic/data/datastore/DataStore.kt +++ b/app/src/main/java/com/sosauce/cutemusic/data/datastore/DataStore.kt @@ -7,16 +7,17 @@ import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.stringSetPreferencesKey import androidx.datastore.preferences.preferencesDataStore +import com.sosauce.cutemusic.data.datastore.PreferencesKeys.APPLY_LOOP import com.sosauce.cutemusic.data.datastore.PreferencesKeys.BLACKLISTED_FOLDERS import com.sosauce.cutemusic.data.datastore.PreferencesKeys.FOLLOW_SYS import com.sosauce.cutemusic.data.datastore.PreferencesKeys.HAS_SEEN_TIP -import com.sosauce.cutemusic.data.datastore.PreferencesKeys.KILL_SERVICE import com.sosauce.cutemusic.data.datastore.PreferencesKeys.SNAP_SPEED_N_PITCH import com.sosauce.cutemusic.data.datastore.PreferencesKeys.SORT_ORDER import com.sosauce.cutemusic.data.datastore.PreferencesKeys.SORT_ORDER_ALBUMS import com.sosauce.cutemusic.data.datastore.PreferencesKeys.SORT_ORDER_ARTISTS import com.sosauce.cutemusic.data.datastore.PreferencesKeys.USE_AMOLED_MODE import com.sosauce.cutemusic.data.datastore.PreferencesKeys.USE_ART_THEME +import com.sosauce.cutemusic.data.datastore.PreferencesKeys.USE_CLASSIC_SLIDER import com.sosauce.cutemusic.data.datastore.PreferencesKeys.USE_DARK_MODE import com.sosauce.cutemusic.data.datastore.PreferencesKeys.USE_SYSTEM_FONT @@ -37,6 +38,8 @@ private data object PreferencesKeys { val SNAP_SPEED_N_PITCH = booleanPreferencesKey("snap_peed_n_pitch") val KILL_SERVICE = booleanPreferencesKey("kill_service") val USE_ART_THEME = booleanPreferencesKey("use_art_theme") + val APPLY_LOOP = booleanPreferencesKey("apply_loop") + val USE_CLASSIC_SLIDER = booleanPreferencesKey("use_classic_slider") } @Composable @@ -83,6 +86,12 @@ fun rememberSnapSpeedAndPitch() = fun rememberUseArtTheme() = rememberPreference(key = USE_ART_THEME, defaultValue = false) -fun rememberKillService(context: Context) = - rememberNonComposablePreference(key = KILL_SERVICE, defaultValue = true, context = context) +//fun rememberKillService(context: Context) = +// rememberNonComposablePreference(key = KILL_SERVICE, defaultValue = true, context = context) +@Composable +fun rememberShouldApplyLoop() = + rememberPreference(key = APPLY_LOOP, defaultValue = false) +@Composable +fun rememberUseClassicSlider() = + rememberPreference(key = USE_CLASSIC_SLIDER, defaultValue = false) diff --git a/app/src/main/java/com/sosauce/cutemusic/data/datastore/SettingsExt.kt b/app/src/main/java/com/sosauce/cutemusic/data/datastore/SettingsExt.kt index fac26ac..866ab4b 100644 --- a/app/src/main/java/com/sosauce/cutemusic/data/datastore/SettingsExt.kt +++ b/app/src/main/java/com/sosauce/cutemusic/data/datastore/SettingsExt.kt @@ -1,21 +1,16 @@ package com.sosauce.cutemusic.data.datastore -import android.content.Context import android.content.res.Configuration import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit import androidx.lifecycle.compose.collectAsStateWithLifecycle -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -49,37 +44,6 @@ fun rememberPreference( } } -fun rememberNonComposablePreference( - key: Preferences.Key, - defaultValue: T, - context: Context -): MutableState { - val coroutineScope = CoroutineScope(Dispatchers.Main) - var state by mutableStateOf(defaultValue) - - coroutineScope.launch { - context.dataStore.data - .map { preferences -> preferences[key] ?: defaultValue } - .collect { newValue -> - state = newValue - } - } - - return object : MutableState { - override var value: T - get() = state - set(value) { - coroutineScope.launch { - context.dataStore.edit { - it[key] = value - } - } - } - - override fun component1() = value - override fun component2(): (T) -> Unit = { value = it } - } -} @Composable fun rememberIsLandscape(): Boolean { diff --git a/app/src/main/java/com/sosauce/cutemusic/di/AppModule.kt b/app/src/main/java/com/sosauce/cutemusic/di/AppModule.kt index 2c6931a..8a87562 100644 --- a/app/src/main/java/com/sosauce/cutemusic/di/AppModule.kt +++ b/app/src/main/java/com/sosauce/cutemusic/di/AppModule.kt @@ -1,11 +1,7 @@ package com.sosauce.cutemusic.di -import android.content.ComponentName -import androidx.media3.session.MediaController -import androidx.media3.session.SessionToken import com.sosauce.cutemusic.domain.repository.MediaStoreHelper import com.sosauce.cutemusic.domain.repository.MediaStoreHelperImpl -import com.sosauce.cutemusic.main.PlaybackService import com.sosauce.cutemusic.ui.screens.metadata.MetadataViewModel import com.sosauce.cutemusic.ui.shared_components.MusicViewModel import com.sosauce.cutemusic.ui.shared_components.PostViewModel @@ -18,20 +14,11 @@ val appModule = module { single { MediaStoreHelperImpl(androidContext()) } - single { - MediaController.Builder( - androidContext(), - SessionToken( - androidContext(), - ComponentName(androidContext(), PlaybackService::class.java) - ) - ).buildAsync() - } viewModel { PostViewModel(get(), androidApplication()) } viewModel { - MusicViewModel(get(), androidApplication()) + MusicViewModel(androidApplication(), get()) } viewModel { MetadataViewModel(androidApplication()) diff --git a/app/src/main/java/com/sosauce/cutemusic/domain/repository/MediaStoreHelper.kt b/app/src/main/java/com/sosauce/cutemusic/domain/repository/MediaStoreHelper.kt index f8f0921..478cf79 100644 --- a/app/src/main/java/com/sosauce/cutemusic/domain/repository/MediaStoreHelper.kt +++ b/app/src/main/java/com/sosauce/cutemusic/domain/repository/MediaStoreHelper.kt @@ -10,13 +10,17 @@ import com.sosauce.cutemusic.domain.model.Folder interface MediaStoreHelper { - fun getMusics() : List + val musics: List + val albums: List + val artists: List - fun getAlbums(): List + fun fetchMusics(): List - fun getArtists(): List + fun fetchAlbums(): List - fun getFoldersWithMusics(): List + fun fetchArtists(): List + + fun fetchFoldersWithMusics(): List suspend fun deleteMusics( uris: List, diff --git a/app/src/main/java/com/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl.kt b/app/src/main/java/com/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl.kt index 8d2d1f7..710ff8f 100644 --- a/app/src/main/java/com/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl.kt +++ b/app/src/main/java/com/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl.kt @@ -18,16 +18,18 @@ import com.sosauce.cutemusic.domain.model.Folder class MediaStoreHelperImpl( private val context: Context -): MediaStoreHelper { +) : MediaStoreHelper { - override fun getMusics(): List { + override fun fetchMusics(): List { val musics = mutableListOf() val projection = arrayOf( MediaStore.Audio.Media._ID, MediaStore.Audio.Media.TITLE, MediaStore.Audio.Media.ARTIST, + MediaStore.Audio.Media.ARTIST_ID, MediaStore.Audio.Media.ALBUM, + MediaStore.Audio.Media.ALBUM_ID, MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.SIZE, //MediaStore.Audio.Media.IS_FAVORITE, @@ -45,7 +47,9 @@ class MediaStoreHelperImpl( val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID) val titleColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TITLE) val artistColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST) + val artistIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ARTIST_ID) val albumColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM) + val albumIdColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.ALBUM_ID) val folderColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA) val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE) //val isFavColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.IS_FAVORITE) @@ -54,7 +58,9 @@ class MediaStoreHelperImpl( val id = cursor.getLong(idColumn) val title = cursor.getString(titleColumn) val artist = cursor.getString(artistColumn) + val artistId = cursor.getLong(artistIdColumn) val album = cursor.getString(albumColumn) + val albumId = cursor.getLong(albumIdColumn) val filePath = cursor.getString(folderColumn) val folder = filePath.substring(0, filePath.lastIndexOf('/')) val size = cursor.getLong(sizeColumn) @@ -63,36 +69,37 @@ class MediaStoreHelperImpl( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id ) - val artUri = ContentUris.appendId( - MediaStore.Audio.Media.EXTERNAL_CONTENT_URI.buildUpon(), id - ).appendPath("albumart").build() + + val artUri = Uri.parse("$uri/albumart") musics.add( - MediaItem - .Builder() - .setUri(uri) - .setMediaId(id.toString()) - .setMediaMetadata( - MediaMetadata - .Builder() - .setIsBrowsable(false) - .setIsPlayable(true) - .setTitle(title) - .setArtist(artist) - .setAlbumTitle(album) - .setArtworkUri(artUri) - .setExtras( - Bundle() - .apply { - putString("folder", folder) - putLong("size", size) - putString("path", filePath) - putString("uri", uri.toString()) - // putInt("isFavorite", isFavorite) - }).build() - ) - .build() + MediaItem + .Builder() + .setUri(uri) + .setMediaId(id.toString()) + .setMediaMetadata( + MediaMetadata + .Builder() + .setIsBrowsable(false) + .setIsPlayable(true) + .setTitle(title) + .setArtist(artist) + .setAlbumTitle(album) + .setArtworkUri(artUri) + .setExtras( + Bundle() + .apply { + putString("folder", folder) + putLong("size", size) + putString("path", filePath) + putString("uri", uri.toString()) + putLong("album_id", albumId) + putLong("artist_id", artistId) + // putInt("isFavorite", isFavorite) + }).build() + ) + .build() ) } } @@ -101,7 +108,7 @@ class MediaStoreHelperImpl( } - override fun getAlbums(): List { + override fun fetchAlbums(): List { val albums = mutableListOf() val projection = arrayOf( @@ -136,7 +143,7 @@ class MediaStoreHelperImpl( return albums } - override fun getArtists(): List { + override fun fetchArtists(): List { val artists = mutableListOf() val projection = arrayOf( @@ -171,7 +178,7 @@ class MediaStoreHelperImpl( // Only gets folder with musics in them - override fun getFoldersWithMusics(): List { + override fun fetchFoldersWithMusics(): List { val folders = mutableListOf() @@ -245,4 +252,9 @@ class MediaStoreHelperImpl( intentSenderLauncher.launch(IntentSenderRequest.Builder(intentSender).build()) } } + + // Caching music to not re-query them in Music and Post ViewModels + override val musics: List = fetchMusics() + override val albums: List = fetchAlbums() + override val artists: List = fetchArtists() } diff --git a/app/src/main/java/com/sosauce/cutemusic/main/AppContainer.kt b/app/src/main/java/com/sosauce/cutemusic/main/AppContainer.kt deleted file mode 100644 index 38df531..0000000 --- a/app/src/main/java/com/sosauce/cutemusic/main/AppContainer.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.sosauce.cutemusic.main - -import androidx.media3.session.MediaController -import com.google.common.util.concurrent.ListenableFuture - -interface AppContainer { - val controllerFuture: ListenableFuture -} - -class DefaultAppContainer( - override val controllerFuture: ListenableFuture -) : AppContainer \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/main/MainActivity.kt b/app/src/main/java/com/sosauce/cutemusic/main/MainActivity.kt index 6cfae01..65aacae 100644 --- a/app/src/main/java/com/sosauce/cutemusic/main/MainActivity.kt +++ b/app/src/main/java/com/sosauce/cutemusic/main/MainActivity.kt @@ -39,7 +39,7 @@ class MainActivity : ComponentActivity() { modifier = Modifier .fillMaxSize() ) { _ -> - Nav() + Nav() } } } diff --git a/app/src/main/java/com/sosauce/cutemusic/main/PlaybackService.kt b/app/src/main/java/com/sosauce/cutemusic/main/PlaybackService.kt index 80574eb..352ec28 100644 --- a/app/src/main/java/com/sosauce/cutemusic/main/PlaybackService.kt +++ b/app/src/main/java/com/sosauce/cutemusic/main/PlaybackService.kt @@ -2,46 +2,88 @@ package com.sosauce.cutemusic.main import android.app.PendingIntent import android.content.Intent -import androidx.compose.runtime.getValue import androidx.media3.common.AudioAttributes import androidx.media3.common.C import androidx.media3.common.MediaMetadata import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.session.DefaultMediaNotificationProvider import androidx.media3.session.MediaLibraryService +import androidx.media3.session.MediaLibraryService.MediaLibrarySession import androidx.media3.session.MediaSession -import com.sosauce.cutemusic.data.datastore.rememberKillService +import com.sosauce.cutemusic.R -class PlaybackService : MediaLibraryService() { + +class PlaybackService : MediaLibraryService(), + MediaLibrarySession.Callback, + Player.Listener { private var mediaLibrarySession: MediaLibrarySession? = null - private var callback: MediaLibrarySession.Callback = object : MediaLibrarySession.Callback {} private val audioAttributes = AudioAttributes .Builder() .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC) .setUsage(C.USAGE_MEDIA) .build() - private val listener = object : Player.Listener { - override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) { - super.onMediaMetadataChanged(mediaMetadata) - sendMusicBroadcast(mediaMetadata.title.toString()) - } - } - + override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) { + super.onMediaMetadataChanged(mediaMetadata) + sendMusicBroadcast(mediaMetadata.title.toString()) + } override fun onGetSession(controllerInfo: MediaSession.ControllerInfo): MediaLibrarySession? = mediaLibrarySession + @UnstableApi override fun onCreate() { super.onCreate() + val player = ExoPlayer.Builder(applicationContext) .setAudioAttributes(audioAttributes, true) .setHandleAudioBecomingNoisy(true) .build() mediaLibrarySession = MediaLibrarySession - .Builder(this, player, callback) + .Builder(this, player, this) + .setShowPlayButtonIfPlaybackIsSuppressed(false) + +// .setBitmapLoader(object : BitmapLoader { +// +// override fun supportsMimeType(mimeType: String): Boolean = true +// +// override fun decodeBitmap(data: ByteArray): ListenableFuture = +// throw UnsupportedOperationException() +// +// override fun loadBitmap(uri: Uri): ListenableFuture = throw UnsupportedOperationException() +// +// override fun loadBitmapFromMetadata(metadata: MediaMetadata): ListenableFuture? { +// val completer = SettableFuture.create() +// val request = ImageRequest.Builder(this@PlaybackService) +// .data( +// if (metadata.artworkUri == Uri.parse("content://media/external/audio/media/1000000397/albumart")) { +// R.drawable.artist +// } else { +// metadata.artworkUri +// } +// ) +// .target( +// onSuccess = { result -> +// completer.set((result as BitmapImage).bitmap) +// }, +// onError = { _ -> +// completer.setException(Exception("Error")) +// } +// ) +// .build() +// println("Art URI: ${metadata.artworkUri}") +// ImageLoader(this@PlaybackService).enqueue(request) +// +// return completer +// } +// +// } + +//) .setSessionActivity( PendingIntent.getActivity( this, @@ -51,8 +93,13 @@ class PlaybackService : MediaLibraryService() { ) ) .build() + setMediaNotificationProvider( + DefaultMediaNotificationProvider.Builder(this).build().apply { + setSmallIcon(R.drawable.round_music_note_24) + } + ) - player.addListener(listener) + player.addListener(this) } @@ -73,6 +120,7 @@ class PlaybackService : MediaLibraryService() { stopSelf() } + companion object { private const val CURRENTLY_PLAYING_CHANGED = "CM_CUR_PLAY_CHANGED" } @@ -80,10 +128,9 @@ class PlaybackService : MediaLibraryService() { private fun sendMusicBroadcast( currentlyPlaying: String ) { - val intent = Intent(CURRENTLY_PLAYING_CHANGED).apply { + Intent(CURRENTLY_PLAYING_CHANGED).apply { putExtra("currentlyPlaying", currentlyPlaying) + sendBroadcast(this) } - sendBroadcast(intent) } - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/sosauce/cutemusic/main/quickplay/QuickPlayActivity.kt b/app/src/main/java/com/sosauce/cutemusic/main/quickplay/QuickPlayActivity.kt index 1444998..333ace8 100644 --- a/app/src/main/java/com/sosauce/cutemusic/main/quickplay/QuickPlayActivity.kt +++ b/app/src/main/java/com/sosauce/cutemusic/main/quickplay/QuickPlayActivity.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.sosauce.cutemusic.data.actions.PlayerActions import com.sosauce.cutemusic.ui.screens.playing.components.MusicSlider import com.sosauce.cutemusic.ui.shared_components.CuteText @@ -56,7 +57,8 @@ class QuickPlayActivity : ComponentActivity() { ) { _ -> MaterialTheme { var uri by remember { mutableStateOf(null) } - val vm = koinViewModel() + val viewModel = koinViewModel() + val musicState by viewModel.musicState.collectAsStateWithLifecycle() when { intent?.action == Intent.ACTION_SEND -> { @@ -82,7 +84,7 @@ class QuickPlayActivity : ComponentActivity() { horizontalAlignment = Alignment.CenterHorizontally ) { CuteText( - text = vm.currentlyPlaying, + text = musicState.currentlyPlaying, color = MaterialTheme.colorScheme.onBackground, fontSize = 20.sp, modifier = Modifier.basicMarquee() @@ -91,13 +93,16 @@ class QuickPlayActivity : ComponentActivity() { ) Spacer(modifier = Modifier.height(5.dp)) CuteText( - text = vm.currentArtist, + text = musicState.currentArtist, color = MaterialTheme.colorScheme.onBackground.copy(0.85f), fontSize = 14.sp, modifier = Modifier.basicMarquee() ) - MusicSlider(vm) + MusicSlider( + viewModel = viewModel, + musicState = musicState + ) Spacer(modifier = Modifier.height(7.dp)) Row( modifier = Modifier.fillMaxWidth(), @@ -106,13 +111,13 @@ class QuickPlayActivity : ComponentActivity() { ) { FloatingActionButton( onClick = { - if (!vm.isPlaylistEmptyAndDataNotNull()) vm.quickPlay(uri) else vm.handlePlayerActions( + if (!viewModel.isPlayerReady()) viewModel.quickPlay(uri) else viewModel.handlePlayerActions( PlayerActions.PlayOrPause ) } ) { Icon( - imageVector = if (vm.isCurrentlyPlaying) Icons.Rounded.Pause else Icons.Rounded.PlayArrow, + imageVector = if (musicState.isCurrentlyPlaying) Icons.Rounded.Pause else Icons.Rounded.PlayArrow, contentDescription = "pause/play button" ) } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/customs/CustomExtention.kt b/app/src/main/java/com/sosauce/cutemusic/ui/customs/CustomExtention.kt deleted file mode 100644 index 751fc12..0000000 --- a/app/src/main/java/com/sosauce/cutemusic/ui/customs/CustomExtention.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.sosauce.cutemusic.ui.customs - -import android.content.Context -import android.content.Intent -import androidx.media3.common.Player -import java.util.Locale - -fun Long.formatBinarySize(): String { - val kiloByteAsByte = 1.0 * 1024.0 - val megaByteAsByte = 1.0 * 1024.0 * 1024.0 - val gigaByteAsByte = 1.0 * 1024.0 * 1024.0 * 1024.0 - return when { - this < kiloByteAsByte -> "${this.toDouble()} B" - this >= kiloByteAsByte && this < megaByteAsByte -> "${ - String.format( - Locale.getDefault(), - "%.2f", - (this / kiloByteAsByte) - ) - } KB" - - this >= megaByteAsByte && this < gigaByteAsByte -> "${ - String.format( - Locale.getDefault(), - "%.2f", - (this / megaByteAsByte) - ) - } MB" - - else -> "Too Big!" - } -} - -fun Context.restart() { - val intent = packageManager.getLaunchIntentForPackage(packageName)!! - val componentName = intent.component!! - val restartIntent = Intent.makeRestartActivityTask(componentName) - startActivity(restartIntent) - Runtime.getRuntime().exit(0) -} - -fun Player.playAtIndex( - mediaId: String -) { - val index = (0 until mediaItemCount).indexOfFirst { getMediaItemAt(it).mediaId == mediaId } - index.takeIf { it != -1 }?.let { - seekTo(it, 0) - play() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/navigation/Navigation.kt b/app/src/main/java/com/sosauce/cutemusic/ui/navigation/Navigation.kt index cbc9ba5..4a87145 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/navigation/Navigation.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/navigation/Navigation.kt @@ -2,20 +2,17 @@ package com.sosauce.cutemusic.ui.navigation -import android.annotation.SuppressLint -import android.content.Intent -import android.util.Log import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionLayout import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.ui.platform.LocalContext +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.toRoute import com.sosauce.cutemusic.data.actions.MetadataActions +import com.sosauce.cutemusic.data.actions.PlayerActions import com.sosauce.cutemusic.data.datastore.rememberAllBlacklistedFolders import com.sosauce.cutemusic.ui.screens.album.AlbumDetailsScreen import com.sosauce.cutemusic.ui.screens.album.AlbumsScreen @@ -32,6 +29,9 @@ import com.sosauce.cutemusic.ui.shared_components.PostViewModel import com.sosauce.cutemusic.utils.ListToHandle import org.koin.androidx.compose.koinViewModel +// https://stackoverflow.com/a/78771053 + + @Composable fun Nav() { @@ -42,7 +42,7 @@ fun Nav() { val blacklistedFolders by rememberAllBlacklistedFolders() val musics = postViewModel.musics .filter { it.mediaMetadata.extras?.getString("folder") !in blacklistedFolders } - + val musicState by viewModel.musicState.collectAsStateWithLifecycle() SharedTransitionLayout { @@ -55,11 +55,9 @@ fun Nav() { musics = musics, selectedIndex = viewModel.selectedItem, onNavigateTo = { navController.navigate(it) }, - currentlyPlaying = viewModel.currentlyPlaying, - isCurrentlyPlaying = viewModel.isCurrentlyPlaying, - onShortClick = { - viewModel.itemClicked(it, musics) - }, + currentlyPlaying = musicState.currentlyPlaying, + isCurrentlyPlaying = musicState.isCurrentlyPlaying, + onShortClick = { viewModel.handlePlayerActions(PlayerActions.StartPlayback(it)) }, onNavigationItemClicked = { index, item -> navController.navigate(item.navigateTo) { viewModel.selectedItem = index @@ -75,8 +73,8 @@ fun Nav() { ) ) }, - isPlaylistEmpty = viewModel.isPlaylistEmptyAndDataNotNull(), - currentMusicUri = viewModel.currentMusicUri, + isPlayerReady = viewModel.isPlayerReady(), + currentMusicUri = musicState.currentMusicUri, onHandlePlayerAction = { viewModel.handlePlayerActions(it) }, onDeleteMusic = { uris, intentSender -> postViewModel.deleteMusic( @@ -95,7 +93,12 @@ fun Nav() { listToHandle = ListToHandle.TRACKS, query = query ) - } + }, + onChargeAlbumSongs = postViewModel::albumSongs, + onChargeArtistLists = { + postViewModel.artistSongs(it) + postViewModel.artistAlbums(it) + }, ) } @@ -115,10 +118,10 @@ fun Nav() { query = query ) }, - currentlyPlaying = viewModel.currentlyPlaying, + currentlyPlaying = musicState.currentlyPlaying, chargePVMAlbumSongs = postViewModel::albumSongs, - isPlaylistEmpty = viewModel.isPlaylistEmptyAndDataNotNull(), - isPlaying = viewModel.isCurrentlyPlaying, + isPlayerReady = viewModel.isPlayerReady(), + isPlaying = musicState.isCurrentlyPlaying, onHandlePlayerActions = viewModel::handlePlayerActions, onNavigate = { navController.navigate(it) }, onNavigationItemClicked = { index, item -> @@ -142,15 +145,15 @@ fun Nav() { } }, selectedIndex = viewModel.selectedItem, - chargePVMLists = { + onChargeArtistLists = { postViewModel.artistSongs(it) postViewModel.artistAlbums(it) }, - currentlyPlaying = viewModel.currentlyPlaying, + currentlyPlaying = musicState.currentlyPlaying, onHandlePlayerActions = viewModel::handlePlayerActions, - isPlaying = viewModel.isCurrentlyPlaying, + isPlaying = musicState.isCurrentlyPlaying, animatedVisibilityScope = this, - isPlaylistEmpty = viewModel.isPlaylistEmptyAndDataNotNull(), + isPlayerReady = viewModel.isPlayerReady(), onHandleSorting = { sortingType -> postViewModel.handleFiltering( listToHandle = ListToHandle.ARTISTS, @@ -170,7 +173,13 @@ fun Nav() { NowPlayingScreen( navController = navController, viewModel = viewModel, - animatedVisibilityScope = this + animatedVisibilityScope = this, + musicState = musicState, + onChargeAlbumSongs = postViewModel::albumSongs, + onChargeArtistLists = { + postViewModel.artistSongs(it) + postViewModel.artistAlbums(it) + } ) } composable { @@ -186,7 +195,10 @@ fun Nav() { album = album, viewModel = viewModel, onPopBackStack = navController::navigateUp, - postViewModel = postViewModel + postViewModel = postViewModel, + musicState = musicState, + animatedVisibilityScope = this, + onNavigate = { screen -> navController.navigate(screen) }, ) } @@ -199,7 +211,9 @@ fun Nav() { navController = navController, viewModel = viewModel, postViewModel = postViewModel, - onNavigate = { screen -> navController.navigate(screen) } + onNavigate = { screen -> navController.navigate(screen) }, + musicState = musicState, + animatedVisibilityScope = this ) } } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumDetailsLandscape.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumDetailsLandscape.kt index 94b87f2..71c1ccd 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumDetailsLandscape.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumDetailsLandscape.kt @@ -29,6 +29,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage import com.sosauce.cutemusic.R +import com.sosauce.cutemusic.data.MusicState +import com.sosauce.cutemusic.data.actions.PlayerActions import com.sosauce.cutemusic.domain.model.Album import com.sosauce.cutemusic.ui.screens.main.MusicListItem import com.sosauce.cutemusic.ui.shared_components.CuteText @@ -41,7 +43,8 @@ fun AlbumDetailsLandscape( album: Album, onNavigateUp: () -> Unit, postViewModel: PostViewModel, - viewModel: MusicViewModel + viewModel: MusicViewModel, + musicState: MusicState ) { val albumSongs by remember { mutableStateOf(postViewModel.albumSongs) } @@ -100,8 +103,14 @@ fun AlbumDetailsLandscape( items(albumSongs, key = { it.mediaId }) { music -> MusicListItem( music = music, - currentMusicUri = viewModel.currentMusicUri, - onShortClick = { viewModel.itemClicked(it, listOf()) } + currentMusicUri = musicState.currentMusicUri, + onShortClick = { + viewModel.handlePlayerActions( + PlayerActions.StartPlayback( + it + ) + ) + } ) } } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumDetailsScreen.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumDetailsScreen.kt index 30b2756..0eb3f5c 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumDetailsScreen.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumDetailsScreen.kt @@ -1,7 +1,10 @@ -@file:OptIn(ExperimentalMaterial3Api::class) +@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class) package com.sosauce.cutemusic.ui.screens.album +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -11,15 +14,17 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.rounded.Shuffle import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -41,20 +46,30 @@ import androidx.compose.ui.unit.sp import androidx.media3.common.MediaItem import coil3.compose.AsyncImage import com.sosauce.cutemusic.R +import com.sosauce.cutemusic.data.MusicState +import com.sosauce.cutemusic.data.actions.PlayerActions import com.sosauce.cutemusic.data.datastore.rememberIsLandscape import com.sosauce.cutemusic.domain.model.Album +import com.sosauce.cutemusic.ui.navigation.Screen import com.sosauce.cutemusic.ui.screens.main.MusicListItem +import com.sosauce.cutemusic.ui.shared_components.CuteSearchbar import com.sosauce.cutemusic.ui.shared_components.CuteText import com.sosauce.cutemusic.ui.shared_components.MusicViewModel import com.sosauce.cutemusic.ui.shared_components.PostViewModel import com.sosauce.cutemusic.utils.ImageUtils +import com.sosauce.cutemusic.utils.rememberSearchbarAlignment +import com.sosauce.cutemusic.utils.rememberSearchbarMaxFloatValue +import com.sosauce.cutemusic.utils.rememberSearchbarRightPadding @Composable -fun AlbumDetailsScreen( +fun SharedTransitionScope.AlbumDetailsScreen( album: Album, viewModel: MusicViewModel, postViewModel: PostViewModel, onPopBackStack: () -> Unit, + musicState: MusicState, + animatedVisibilityScope: AnimatedVisibilityScope, + onNavigate: (Screen) -> Unit, ) { val albumSongs by remember { mutableStateOf(postViewModel.albumSongs) } @@ -63,26 +78,33 @@ fun AlbumDetailsScreen( album = album, onNavigateUp = { onPopBackStack() }, postViewModel = postViewModel, - viewModel = viewModel + viewModel = viewModel, + musicState = musicState ) } else { AlbumDetailsContent( album = album, viewModel = viewModel, onPopBackStack = { onPopBackStack() }, - albumSongs = albumSongs + albumSongs = albumSongs, + musicState = musicState, + animatedVisibilityScope = animatedVisibilityScope, + onNavigate = onNavigate, ) } } @Composable -private fun AlbumDetailsContent( +private fun SharedTransitionScope.AlbumDetailsContent( album: Album, viewModel: MusicViewModel, onPopBackStack: () -> Unit, - albumSongs: List - ) { + albumSongs: List, + musicState: MusicState, + animatedVisibilityScope: AnimatedVisibilityScope, + onNavigate: (Screen) -> Unit, +) { val context = LocalContext.current Scaffold( topBar = { @@ -97,6 +119,25 @@ private fun AlbumDetailsContent( } } ) + }, + floatingActionButton = { + FloatingActionButton( + onClick = { + viewModel.handlePlayerActions( + PlayerActions.StartAlbumPlayback( + albumName = album.name, + mediaId = null + ) + ) + }, + modifier = Modifier + .padding(bottom = 55.dp) + ) { + Icon( + imageVector = Icons.Rounded.Shuffle, + contentDescription = null + ) + } } ) { values -> Box( @@ -108,59 +149,83 @@ private fun AlbumDetailsContent( top = values.calculateTopPadding(), bottom = values.calculateBottomPadding() ) - .verticalScroll(rememberScrollState()) ) { - Column { - Row( - horizontalArrangement = Arrangement.Start, - verticalAlignment = Alignment.Top, - modifier = Modifier.fillMaxWidth() - ) { - AsyncImage( - model = ImageUtils.imageRequester( - img = ImageUtils.getAlbumArt(album.id), - context = context - ), - contentDescription = stringResource(R.string.artwork), - modifier = Modifier - .size(150.dp) - .clip(RoundedCornerShape(12.dp)), - contentScale = ContentScale.Crop - ) - Spacer(Modifier.width(10.dp)) - Column( - horizontalAlignment = Alignment.Start + LazyColumn { + item { + Row( + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.Top, + modifier = Modifier.fillMaxWidth() ) { - CuteText( - text = album.name, - fontSize = 22.sp, - modifier = Modifier.basicMarquee() - ) - CuteText( - text = album.artist, - fontSize = 22.sp, - color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.85f), - modifier = Modifier.basicMarquee() - ) - Spacer(Modifier.height(60.dp)) - CuteText( - text = "${albumSongs.size} ${if (albumSongs.size <= 1) "song" else "songs"}", - fontSize = 22.sp, - modifier = Modifier.basicMarquee() + AsyncImage( + model = ImageUtils.imageRequester( + img = ImageUtils.getAlbumArt(album.id), + context = context + ), + contentDescription = stringResource(R.string.artwork), + modifier = Modifier + .size(150.dp) + .clip(RoundedCornerShape(12.dp)), + contentScale = ContentScale.Crop ) + Spacer(Modifier.width(10.dp)) + Column( + horizontalAlignment = Alignment.Start + ) { + CuteText( + text = album.name, + fontSize = 22.sp, + modifier = Modifier.basicMarquee() + ) + CuteText( + text = album.artist, + fontSize = 22.sp, + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.85f), + modifier = Modifier.basicMarquee() + ) + Spacer(Modifier.height(60.dp)) + CuteText( + text = "${albumSongs.size} ${if (albumSongs.size <= 1) "song" else "songs"}", + fontSize = 22.sp, + modifier = Modifier.basicMarquee() + ) + } } - } - Spacer(Modifier.height(10.dp)) - Column { - albumSongs.forEach { music -> - MusicListItem( - music = music, - onShortClick = { viewModel.itemClicked(it, listOf()) }, - currentMusicUri = viewModel.currentMusicUri - ) + Spacer(Modifier.height(10.dp)) + Column { + albumSongs.forEach { music -> + MusicListItem( + music = music, + onShortClick = { + viewModel.handlePlayerActions( + PlayerActions.StartAlbumPlayback( + albumName = music.mediaMetadata.albumTitle.toString(), + mediaId = it + ) + ) + }, + currentMusicUri = musicState.currentMusicUri + ) + } } } } + + CuteSearchbar( + currentlyPlaying = musicState.currentlyPlaying, + isPlayerReady = viewModel.isPlayerReady(), + isPlaying = musicState.isCurrentlyPlaying, + onHandlePlayerActions = viewModel::handlePlayerActions, + animatedVisibilityScope = animatedVisibilityScope, + modifier = Modifier + .navigationBarsPadding() + .fillMaxWidth(rememberSearchbarMaxFloatValue()) + .padding(end = rememberSearchbarRightPadding()) + .align(rememberSearchbarAlignment()), + showSearchField = false, + onNavigate = { onNavigate(Screen.NowPlaying) } + ) + } } } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumScreen.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumScreen.kt index 29deafd..9f9f868 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumScreen.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/album/AlbumScreen.kt @@ -6,7 +6,6 @@ import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.clickable @@ -39,7 +38,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate @@ -48,7 +46,6 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import coil3.compose.AsyncImage import com.sosauce.cutemusic.R import com.sosauce.cutemusic.data.actions.PlayerActions @@ -57,9 +54,7 @@ import com.sosauce.cutemusic.domain.model.Album import com.sosauce.cutemusic.ui.navigation.Screen import com.sosauce.cutemusic.ui.shared_components.CuteSearchbar import com.sosauce.cutemusic.ui.shared_components.CuteText -import com.sosauce.cutemusic.ui.shared_components.MusicViewModel import com.sosauce.cutemusic.ui.shared_components.NavigationItem -import com.sosauce.cutemusic.ui.shared_components.PostViewModel import com.sosauce.cutemusic.ui.shared_components.ScreenSelection import com.sosauce.cutemusic.utils.ImageUtils import com.sosauce.cutemusic.utils.SortingType @@ -80,7 +75,7 @@ fun SharedTransitionScope.AlbumsScreen( selectedIndex: Int, isPlaying: Boolean, onHandlePlayerActions: (PlayerActions) -> Unit, - isPlaylistEmpty: Boolean, + isPlayerReady: Boolean, onNavigationItemClicked: (Int, NavigationItem) -> Unit ) { val isLandscape = rememberIsLandscape() @@ -130,10 +125,12 @@ fun SharedTransitionScope.AlbumsScreen( chargePVMAlbumSongs(album.name) onNavigate(Screen.AlbumsDetails(album.id)) } -// .thenIf( -// index == 0 || index == 1 || index == 2 || index == 3, // booo bad -// Modifier.statusBarsPadding() -// ) + .thenIf( + if (isLandscape) + index == 0 || index == 1 || index == 2 || index == 3 + else index == 0 || index == 1, + Modifier.statusBarsPadding() + ) ) } } @@ -151,14 +148,7 @@ fun SharedTransitionScope.AlbumsScreen( bottom = 5.dp, end = rememberSearchbarRightPadding() ) - .align(rememberSearchbarAlignment()) - .sharedElement( - state = rememberSharedContentState(key = "searchbar"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(durationMillis = 500) - } - ), + .align(rememberSearchbarAlignment()), placeholder = { CuteText( text = stringResource(id = R.string.search) + " " + stringResource(R.string.albums), @@ -179,7 +169,8 @@ fun SharedTransitionScope.AlbumsScreen( onDismissRequest = { screenSelectionExpanded = false }, modifier = Modifier .width(180.dp) - .background(color = MaterialTheme.colorScheme.surface) + .background(color = MaterialTheme.colorScheme.surface), + shape = RoundedCornerShape(24.dp) ) { ScreenSelection( onNavigationItemClicked = onNavigationItemClicked, @@ -223,7 +214,7 @@ fun SharedTransitionScope.AlbumsScreen( onHandlePlayerActions = onHandlePlayerActions, isPlaying = isPlaying, animatedVisibilityScope = animatedVisibilityScope, - isPlaylistEmpty = isPlaylistEmpty, + isPlayerReady = isPlayerReady, onNavigate = { onNavigate(Screen.NowPlaying) } ) } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistDetails.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistDetails.kt index 3218ff5..b1d2783 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistDetails.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistDetails.kt @@ -1,7 +1,10 @@ -@file:OptIn(ExperimentalMaterial3Api::class) +@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalSharedTransitionApi::class) package com.sosauce.cutemusic.ui.screens.artist +import androidx.compose.animation.AnimatedVisibilityScope +import androidx.compose.animation.ExperimentalSharedTransitionApi +import androidx.compose.animation.SharedTransitionScope import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -10,6 +13,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow @@ -17,7 +21,9 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.rounded.Shuffle import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -29,25 +35,34 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController +import com.sosauce.cutemusic.data.MusicState +import com.sosauce.cutemusic.data.actions.PlayerActions import com.sosauce.cutemusic.data.datastore.rememberIsLandscape import com.sosauce.cutemusic.domain.model.Artist import com.sosauce.cutemusic.ui.navigation.Screen import com.sosauce.cutemusic.ui.screens.album.AlbumCard import com.sosauce.cutemusic.ui.screens.main.MusicListItem +import com.sosauce.cutemusic.ui.shared_components.CuteSearchbar import com.sosauce.cutemusic.ui.shared_components.CuteText import com.sosauce.cutemusic.ui.shared_components.MusicViewModel import com.sosauce.cutemusic.ui.shared_components.PostViewModel +import com.sosauce.cutemusic.utils.rememberSearchbarAlignment +import com.sosauce.cutemusic.utils.rememberSearchbarMaxFloatValue +import com.sosauce.cutemusic.utils.rememberSearchbarRightPadding @Composable -fun ArtistDetails( +fun SharedTransitionScope.ArtistDetails( artist: Artist, navController: NavController, viewModel: MusicViewModel, postViewModel: PostViewModel, onNavigate: (Screen) -> Unit, + musicState: MusicState, + animatedVisibilityScope: AnimatedVisibilityScope ) { val artistSongs by remember { mutableStateOf(postViewModel.artistSongs) } @@ -58,11 +73,11 @@ fun ArtistDetails( onNavigateUp = navController::navigateUp, artistAlbums = artistAlbums, artistSongs = artistSongs, - onClickPlay = { viewModel.itemClicked(it, listOf()) }, + onClickPlay = { viewModel.handlePlayerActions(PlayerActions.StartPlayback(it)) }, onNavigate = { navController.navigate(it) }, chargePVMAlbumSongs = { postViewModel.albumSongs(it) }, artist = artist, - currentMusicUri = viewModel.currentMusicUri + currentMusicUri = musicState.currentMusicUri ) } else { Scaffold( @@ -94,12 +109,36 @@ fun ArtistDetails( } } ) + }, + floatingActionButton = { + FloatingActionButton( + onClick = { + viewModel.handlePlayerActions( + PlayerActions.StartArtistPlayback( + artistName = artist.name, + mediaId = null + ) + ) + }, + modifier = Modifier + .padding(bottom = 55.dp) + ) { + Icon( + imageVector = Icons.Rounded.Shuffle, + contentDescription = null + ) + } } ) { values -> Box( modifier = Modifier .fillMaxSize() - .padding(values) + .padding( + start = values.calculateLeftPadding(LayoutDirection.Ltr) + 10.dp, + end = values.calculateRightPadding(LayoutDirection.Rtl) + 10.dp, + top = values.calculateTopPadding(), + bottom = values.calculateBottomPadding() + ) ) { Column { LazyRow { @@ -122,13 +161,33 @@ fun ArtistDetails( items(artistSongs) { music -> MusicListItem( music = music, - onShortClick = { viewModel.itemClicked(it, listOf()) }, - currentMusicUri = viewModel.currentMusicUri + onShortClick = { + viewModel.handlePlayerActions( + PlayerActions.StartArtistPlayback( + artistName = artist.name, + mediaId = it + ) + ) + }, + currentMusicUri = musicState.currentMusicUri ) } } - } + CuteSearchbar( + currentlyPlaying = musicState.currentlyPlaying, + isPlayerReady = viewModel.isPlayerReady(), + isPlaying = musicState.isCurrentlyPlaying, + onHandlePlayerActions = viewModel::handlePlayerActions, + animatedVisibilityScope = animatedVisibilityScope, + modifier = Modifier + .navigationBarsPadding() + .fillMaxWidth(rememberSearchbarMaxFloatValue()) + .padding(end = rememberSearchbarRightPadding()) + .align(rememberSearchbarAlignment()), + showSearchField = false, + onNavigate = { onNavigate(Screen.NowPlaying) } + ) } } } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistsScreen.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistsScreen.kt index a70f590..e4dfdc1 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistsScreen.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistsScreen.kt @@ -6,7 +6,6 @@ import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.clickable @@ -21,6 +20,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.ArrowUpward import androidx.compose.material.icons.rounded.Person @@ -43,18 +43,14 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.navigation.NavController import coil3.compose.AsyncImage import com.sosauce.cutemusic.R import com.sosauce.cutemusic.data.actions.PlayerActions -import com.sosauce.cutemusic.data.datastore.rememberIsLandscape import com.sosauce.cutemusic.domain.model.Artist import com.sosauce.cutemusic.ui.navigation.Screen import com.sosauce.cutemusic.ui.shared_components.CuteSearchbar import com.sosauce.cutemusic.ui.shared_components.CuteText -import com.sosauce.cutemusic.ui.shared_components.MusicViewModel import com.sosauce.cutemusic.ui.shared_components.NavigationItem -import com.sosauce.cutemusic.ui.shared_components.PostViewModel import com.sosauce.cutemusic.ui.shared_components.ScreenSelection import com.sosauce.cutemusic.utils.ImageUtils import com.sosauce.cutemusic.utils.SortingType @@ -69,12 +65,12 @@ fun SharedTransitionScope.ArtistsScreen( onHandleSorting: (SortingType) -> Unit, onHandleSearching: (String) -> Unit, currentlyPlaying: String, - chargePVMLists: (String) -> Unit, + onChargeArtistLists: (String) -> Unit, onNavigate: (Screen) -> Unit, selectedIndex: Int, isPlaying: Boolean, onHandlePlayerActions: (PlayerActions) -> Unit, - isPlaylistEmpty: Boolean, + isPlayerReady: Boolean, onNavigationItemClicked: (Int, NavigationItem) -> Unit ) { @@ -122,7 +118,7 @@ fun SharedTransitionScope.ArtistsScreen( ) ) { ArtistInfoList(it) { - chargePVMLists(it.name) + onChargeArtistLists(it.name) onNavigate(Screen.ArtistsDetails(it.id)) } } @@ -142,14 +138,7 @@ fun SharedTransitionScope.ArtistsScreen( bottom = 5.dp, end = rememberSearchbarRightPadding() ) - .align(rememberSearchbarAlignment()) - .sharedElement( - state = rememberSharedContentState(key = "searchbar"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(durationMillis = 500) - } - ), + .align(rememberSearchbarAlignment()), placeholder = { CuteText( text = stringResource(id = R.string.search) + " " + stringResource(R.string.artists), @@ -169,7 +158,8 @@ fun SharedTransitionScope.ArtistsScreen( onDismissRequest = { screenSelectionExpanded = false }, modifier = Modifier .width(180.dp) - .background(color = MaterialTheme.colorScheme.surface) + .background(color = MaterialTheme.colorScheme.surface), + shape = RoundedCornerShape(24.dp) ) { ScreenSelection( onNavigationItemClicked = onNavigationItemClicked, @@ -182,9 +172,14 @@ fun SharedTransitionScope.ArtistsScreen( IconButton( onClick = { isSortedByASC = !isSortedByASC - when(isSortedByASC) { - true -> { onHandleSorting(SortingType.ASCENDING) } - false -> { onHandleSorting(SortingType.DESCENDING) } + when (isSortedByASC) { + true -> { + onHandleSorting(SortingType.ASCENDING) + } + + false -> { + onHandleSorting(SortingType.DESCENDING) + } } } ) { @@ -208,7 +203,7 @@ fun SharedTransitionScope.ArtistsScreen( onHandlePlayerActions = onHandlePlayerActions, isPlaying = isPlaying, animatedVisibilityScope = animatedVisibilityScope, - isPlaylistEmpty = isPlaylistEmpty, + isPlayerReady = isPlayerReady, onNavigate = { onNavigate(Screen.NowPlaying) } ) } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/lyrics/LyricsView.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/lyrics/LyricsView.kt index 254afbd..74c9df7 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/lyrics/LyricsView.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/lyrics/LyricsView.kt @@ -2,7 +2,6 @@ package com.sosauce.cutemusic.ui.screens.lyrics -import android.util.Log import android.view.WindowManager import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.ExperimentalFoundationApi @@ -40,30 +39,28 @@ import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.lifecycle.viewmodel.compose.viewModel +import com.sosauce.cutemusic.data.MusicState import com.sosauce.cutemusic.data.actions.PlayerActions import com.sosauce.cutemusic.domain.model.Lyrics import com.sosauce.cutemusic.main.MainActivity import com.sosauce.cutemusic.ui.shared_components.CuteText import com.sosauce.cutemusic.ui.shared_components.MusicViewModel -import org.koin.androidx.compose.koinViewModel @Composable fun LyricsView( viewModel: MusicViewModel, onHideLyrics: () -> Unit, - path: String, - isLandscape: Boolean = false + isLandscape: Boolean = false, + musicState: MusicState ) { var currentLyric by remember { mutableStateOf(Lyrics()) } val clipboardManager = LocalClipboardManager.current - val indexToScrollTo = viewModel.currentLyrics.indexOfFirst { lyric -> - viewModel.currentPosition in lyric.timestamp until (viewModel.currentLyrics.getOrNull( - viewModel.currentLyrics.indexOf(lyric) + 1 + val indexToScrollTo = musicState.currentLyrics.indexOfFirst { lyric -> + musicState.currentPosition in lyric.timestamp until (musicState.currentLyrics.getOrNull( + musicState.currentLyrics.indexOf(lyric) + 1 )?.timestamp ?: 0) } val lazyListState = rememberLazyListState( @@ -94,29 +91,29 @@ fun LyricsView( state = lazyListState ) { - if (viewModel.currentLyrics.isEmpty()) { + if (musicState.currentLyrics.isEmpty()) { item { CuteText( - text = viewModel.loadEmbeddedLyrics(path), + text = viewModel.loadEmbeddedLyrics(musicState.currentPath), ) } } else { itemsIndexed( - items = viewModel.currentLyrics, + items = musicState.currentLyrics, key = { _, item -> item.timestamp } ) { index, lyric -> - val nextTimestamp = remember(index, viewModel.currentLyrics) { - if (index < viewModel.currentLyrics.size - 1) { - viewModel.currentLyrics[index + 1].timestamp + val nextTimestamp = remember(index, musicState.currentLyrics) { + if (index < musicState.currentLyrics.size - 1) { + musicState.currentLyrics[index + 1].timestamp } else { 0 } } - val isCurrentLyric = remember(viewModel.currentPosition, nextTimestamp) { - viewModel.currentPosition in lyric.timestamp until nextTimestamp + val isCurrentLyric = remember(musicState.currentPosition, nextTimestamp) { + musicState.currentPosition in lyric.timestamp until nextTimestamp } val color by animateColorAsState( diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/main/MainScreen.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/main/MainScreen.kt index 7f79fc7..123cbf6 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/main/MainScreen.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/main/MainScreen.kt @@ -5,9 +5,14 @@ package com.sosauce.cutemusic.ui.screens.main +import android.app.Activity import android.net.Uri +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.IntentSenderRequest +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.Crossfade import androidx.compose.animation.ExperimentalSharedTransitionApi @@ -19,6 +24,7 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween +import androidx.compose.animation.scaleOut import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.basicMarquee @@ -27,7 +33,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBarsPadding @@ -40,18 +45,25 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Album import androidx.compose.material.icons.rounded.ArrowUpward +import androidx.compose.material.icons.rounded.Delete +import androidx.compose.material.icons.rounded.Edit +import androidx.compose.material.icons.rounded.Info import androidx.compose.material.icons.rounded.MoreVert import androidx.compose.material.icons.rounded.MusicNote +import androidx.compose.material.icons.rounded.Person import androidx.compose.material.icons.rounded.Settings +import androidx.compose.material.icons.rounded.Shuffle import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -61,6 +73,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.TransformOrigin import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -72,9 +85,9 @@ import com.sosauce.cutemusic.R import com.sosauce.cutemusic.data.actions.PlayerActions import com.sosauce.cutemusic.data.datastore.rememberHasSeenTip import com.sosauce.cutemusic.ui.navigation.Screen -import com.sosauce.cutemusic.ui.screens.main.components.BottomSheetContent import com.sosauce.cutemusic.ui.shared_components.CuteSearchbar import com.sosauce.cutemusic.ui.shared_components.CuteText +import com.sosauce.cutemusic.ui.shared_components.MusicDetailsDialog import com.sosauce.cutemusic.ui.shared_components.NavigationItem import com.sosauce.cutemusic.ui.shared_components.ScreenSelection import com.sosauce.cutemusic.utils.ImageUtils @@ -94,13 +107,15 @@ fun SharedTransitionScope.MainScreen( onNavigationItemClicked: (Int, NavigationItem) -> Unit, selectedIndex: Int, animatedVisibilityScope: AnimatedVisibilityScope, - onLoadMetadata: ((String) -> Unit)? = null, - isPlaylistEmpty: Boolean, + onLoadMetadata: (String) -> Unit = {}, + isPlayerReady: Boolean, currentMusicUri: String, onHandlePlayerAction: (PlayerActions) -> Unit, onDeleteMusic: (List, ActivityResultLauncher) -> Unit, onHandleSorting: (SortingType) -> Unit, - onHandleSearching: (String) -> Unit + onHandleSearching: (String) -> Unit, + onChargeAlbumSongs: (String) -> Unit, + onChargeArtistLists: (String) -> Unit ) { var query by remember { mutableStateOf("") } val state = rememberLazyListState() @@ -112,166 +127,192 @@ fun SharedTransitionScope.MainScreen( ) - - Box(Modifier.fillMaxSize()) { - LazyColumn( - state = state - ) { - if (musics.isEmpty()) { - item { - CuteText( - text = stringResource(id = R.string.no_musics_found), - modifier = Modifier - .padding(16.dp) - .fillMaxWidth(), - textAlign = TextAlign.Center - ) - } - } else { - itemsIndexed( - items = musics, - key = { _, music -> music.mediaId } - ) { index, music -> - Column( - modifier = Modifier - .animateItem() - .padding( - vertical = 2.dp, - horizontal = 4.dp - ) - ) { - MusicListItem( - onShortClick = { onShortClick(music.mediaId) }, - music = music, - onNavigate = { onNavigateTo(it) }, - currentMusicUri = currentMusicUri, - onLoadMetadata = onLoadMetadata, - showBottomSheet = true, - modifier = Modifier.thenIf( - index == 0, - Modifier.statusBarsPadding() - ), - onDeleteMusic = onDeleteMusic - ) - } + Scaffold( + floatingActionButton = { + AnimatedVisibility( + visible = !isPlayerReady, + exit = scaleOut( + // 2 times faster than the searchbar so it doesn't look too weird + animationSpec = tween(250), + transformOrigin = TransformOrigin(0.5f, 0.25f) + ) + ) { + FloatingActionButton( + onClick = { onHandlePlayerAction(PlayerActions.PlayRandom) }, + modifier = Modifier + .padding(bottom = 70.dp) + ) { + Icon( + imageVector = Icons.Rounded.Shuffle, + contentDescription = null + ) } } } - - - Crossfade( - targetState = state.canScrollForward || musics.size <= 15, - label = "", - modifier = Modifier.align(rememberSearchbarAlignment()) - ) { visible -> - if (visible) { - val transition = rememberInfiniteTransition(label = "Infinite Color Change") - val color by transition.animateColor( - initialValue = LocalContentColor.current, - targetValue = MaterialTheme.colorScheme.errorContainer, - animationSpec = infiniteRepeatable( - tween(500), - repeatMode = RepeatMode.Reverse - ), - label = "" - ) - var hasSeenTip by rememberHasSeenTip() - - CuteSearchbar( - query = query, - onQueryChange = { - query = it - onHandleSearching(query) - }, - modifier = Modifier - .navigationBarsPadding() - .fillMaxWidth(rememberSearchbarMaxFloatValue()) - .padding( - bottom = 5.dp, - end = rememberSearchbarRightPadding() - ) - .sharedElement( - state = rememberSharedContentState(key = "searchbar"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(durationMillis = 500) - } - ), - placeholder = { + ) { _ -> + Box(Modifier.fillMaxSize()) { + LazyColumn( + state = state + ) { + if (musics.isEmpty()) { + item { CuteText( - text = stringResource(id = R.string.search) + " " + stringResource(id = R.string.music), - color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f), - - ) - }, - leadingIcon = { - IconButton( - onClick = { - screenSelectionExpanded = true - // Let's prevent writing to datastore everytime the user clicks ;) - if (!hasSeenTip) { - hasSeenTip = true - } - } + text = stringResource(id = R.string.no_musics_found), + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + textAlign = TextAlign.Center + ) + } + } else { + itemsIndexed( + items = musics, + key = { _, music -> music.mediaId } + ) { index, music -> + Column( + modifier = Modifier + .animateItem() + .padding( + vertical = 2.dp, + horizontal = 4.dp + ) ) { - Icon( - imageVector = Icons.Rounded.MusicNote, - contentDescription = null, - tint = if (!hasSeenTip) color else LocalContentColor.current + MusicListItem( + onShortClick = { onShortClick(music.mediaId) }, + music = music, + onNavigate = { onNavigateTo(it) }, + currentMusicUri = currentMusicUri, + onLoadMetadata = onLoadMetadata, + showBottomSheet = true, + modifier = Modifier.thenIf( + index == 0, + Modifier.statusBarsPadding() + ), + onDeleteMusic = onDeleteMusic, + onChargeAlbumSongs = onChargeAlbumSongs, + onChargeArtistLists = onChargeArtistLists ) } + } + } + } + // TODO : How do you make it NOT scroll to the first item when sorting changes !!!!! + Crossfade( + targetState = true, + //targetState = state.canScrollForward || musics.size <= 15, + label = "", + modifier = Modifier.align(rememberSearchbarAlignment()) + ) { visible -> + if (visible) { + val transition = rememberInfiniteTransition(label = "Infinite Color Change") + val color by transition.animateColor( + initialValue = LocalContentColor.current, + targetValue = MaterialTheme.colorScheme.errorContainer, + animationSpec = infiniteRepeatable( + tween(500), + repeatMode = RepeatMode.Reverse + ), + label = "" + ) + var hasSeenTip by rememberHasSeenTip() - DropdownMenu( - expanded = screenSelectionExpanded, - onDismissRequest = { screenSelectionExpanded = false }, - modifier = Modifier - .width(180.dp) - .background(color = MaterialTheme.colorScheme.surface) - ) { - ScreenSelection( - onNavigationItemClicked = onNavigationItemClicked, - selectedIndex = selectedIndex - ) - } - }, - trailingIcon = { - Row { + CuteSearchbar( + query = query, + onQueryChange = { + query = it + onHandleSearching(query) + }, + modifier = Modifier + .navigationBarsPadding() + .fillMaxWidth(rememberSearchbarMaxFloatValue()) + .padding( + bottom = 5.dp, + end = rememberSearchbarRightPadding() + ), + placeholder = { + CuteText( + text = stringResource(id = R.string.search) + " " + stringResource( + id = R.string.music + ), + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f), + + ) + }, + leadingIcon = { IconButton( onClick = { - isSortedByASC = !isSortedByASC - when(isSortedByASC) { - true -> { onHandleSorting(SortingType.ASCENDING) } - false -> { onHandleSorting(SortingType.DESCENDING) } + screenSelectionExpanded = true + // Let's prevent writing to datastore everytime the user clicks ;) + if (!hasSeenTip) { + hasSeenTip = true } } ) { Icon( - imageVector = Icons.Rounded.ArrowUpward, + imageVector = Icons.Rounded.MusicNote, contentDescription = null, - modifier = Modifier.rotate(float) + tint = if (!hasSeenTip) color else LocalContentColor.current ) } - IconButton( - onClick = { onNavigateTo(Screen.Settings) } + + + DropdownMenu( + expanded = screenSelectionExpanded, + onDismissRequest = { screenSelectionExpanded = false }, + modifier = Modifier + .width(180.dp) + .background(color = MaterialTheme.colorScheme.surface), + shape = RoundedCornerShape(24.dp) ) { - Icon( - imageVector = Icons.Rounded.Settings, - contentDescription = null + ScreenSelection( + onNavigationItemClicked = onNavigationItemClicked, + selectedIndex = selectedIndex ) } - } - }, - currentlyPlaying = currentlyPlaying, - onHandlePlayerActions = onHandlePlayerAction, - isPlaying = isCurrentlyPlaying, - animatedVisibilityScope = animatedVisibilityScope, - isPlaylistEmpty = isPlaylistEmpty, - onNavigate = { onNavigateTo(Screen.NowPlaying) } - ) + }, + trailingIcon = { + Row { + IconButton( + onClick = { + isSortedByASC = !isSortedByASC + when (isSortedByASC) { + true -> { + onHandleSorting(SortingType.ASCENDING) + } + + false -> { + onHandleSorting(SortingType.DESCENDING) + } + } + } + ) { + Icon( + imageVector = Icons.Rounded.ArrowUpward, + contentDescription = null, + modifier = Modifier.rotate(float) + ) + } + IconButton( + onClick = { onNavigateTo(Screen.Settings) } + ) { + Icon( + imageVector = Icons.Rounded.Settings, + contentDescription = null + ) + } + } + }, + currentlyPlaying = currentlyPlaying, + onHandlePlayerActions = onHandlePlayerAction, + isPlaying = isCurrentlyPlaying, + animatedVisibilityScope = animatedVisibilityScope, + isPlayerReady = isPlayerReady, + onNavigate = { onNavigateTo(Screen.NowPlaying) } + ) + } } } - } } @@ -279,17 +320,21 @@ fun SharedTransitionScope.MainScreen( fun MusicListItem( modifier: Modifier = Modifier, music: MediaItem, - onShortClick: (String) -> Unit, + onShortClick: (albumName: String) -> Unit, onNavigate: (Screen) -> Unit = {}, currentMusicUri: String, - onLoadMetadata: ((String) -> Unit)? = null, + onLoadMetadata: (String) -> Unit = {}, showBottomSheet: Boolean = false, - onDeleteMusic: ((List, ActivityResultLauncher) -> Unit)? = null + onDeleteMusic: (List, ActivityResultLauncher) -> Unit = { _, _ -> }, + onChargeAlbumSongs: (String) -> Unit = {}, + onChargeArtistLists: (String) -> Unit = {}, ) { - val sheetState = rememberModalBottomSheetState() val context = LocalContext.current - var isSheetOpen by remember { mutableStateOf(false) } + var isDropDownExpanded by remember { mutableStateOf(false) } + var showDetailsDialog by remember { mutableStateOf(false) } + val uri = remember { Uri.parse(music.mediaMetadata.extras?.getString("uri")) } + val path = remember { music.mediaMetadata.extras?.getString("path") } val isPlaying = currentMusicUri == music.mediaMetadata.extras?.getString("uri") val bgColor by animateColorAsState( targetValue = if (isPlaying) { @@ -300,84 +345,183 @@ fun MusicListItem( label = "Background Color", animationSpec = tween(500) ) + val deleteSongLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { + if (it.resultCode == Activity.RESULT_OK) { + Toast.makeText( + context, + context.resources.getText(R.string.deleting_song_OK), + Toast.LENGTH_SHORT + ).show() - - if (isSheetOpen) { - ModalBottomSheet( - modifier = Modifier.fillMaxHeight(), - sheetState = sheetState, - onDismissRequest = { isSheetOpen = false }, - ) { - BottomSheetContent( - music = music, - onNavigate = { onNavigate(it) }, - onDismiss = { isSheetOpen = false }, - onLoadMetadata = onLoadMetadata, - onDeleteMusic = onDeleteMusic!! - ) + } else { + Toast.makeText( + context, + context.resources.getText(R.string.error_deleting_song), + Toast.LENGTH_SHORT + ).show() + } } + + if (showDetailsDialog) { + MusicDetailsDialog( + music = music, + onDismissRequest = { showDetailsDialog = false } + ) } + + Row( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(24.dp)) + .combinedClickable( + onClick = { onShortClick(music.mediaId) } + ) + .background( + color = bgColor, + shape = RoundedCornerShape(24.dp) + ), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Row( - modifier = modifier - .fillMaxWidth() - .clip(RoundedCornerShape(24.dp)) - .combinedClickable( - onClick = { onShortClick(music.mediaId) } - ) - .background( - color = bgColor, - shape = RoundedCornerShape(24.dp) - ), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween + modifier = Modifier.weight(1f) ) { + AsyncImage( + model = ImageUtils.imageRequester( + img = music.mediaMetadata.artworkUri, + context = context + ), + stringResource(R.string.artwork), + modifier = Modifier + .padding(start = 10.dp) + .size(45.dp), + contentScale = ContentScale.Crop, + ) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.weight(1f) + Column( + modifier = Modifier.padding(15.dp) ) { - AsyncImage( - model = ImageUtils.imageRequester( - img = music.mediaMetadata.artworkUri, - context = context - ), - stringResource(R.string.artwork), - modifier = Modifier - .padding(start = 10.dp) - .size(45.dp), - contentScale = ContentScale.Crop, + CuteText( + text = music.mediaMetadata.title.toString(), + maxLines = 1, + modifier = Modifier.basicMarquee() + ) + CuteText( + text = music.mediaMetadata.artist.toString(), + maxLines = 1, + color = MaterialTheme.colorScheme.onBackground.copy(0.85f) ) - - Column( - modifier = Modifier.padding(15.dp) - ) { - CuteText( - text = music.mediaMetadata.title.toString(), - maxLines = 1, - modifier = Modifier.basicMarquee() - ) - CuteText( - text = music.mediaMetadata.artist.toString(), - - maxLines = 1, - color = MaterialTheme.colorScheme.onBackground.copy(0.85f) - ) - } } + } - if (showBottomSheet) { + if (showBottomSheet) { + Row { IconButton( - onClick = { isSheetOpen = true } + onClick = { isDropDownExpanded = true } ) { Icon( imageVector = Icons.Rounded.MoreVert, contentDescription = null ) } + DropdownMenu( + expanded = isDropDownExpanded, + onDismissRequest = { isDropDownExpanded = false }, + shape = RoundedCornerShape(24.dp) + ) { + DropdownMenuItem( + onClick = { showDetailsDialog = true }, + text = { + CuteText(stringResource(R.string.details)) + }, + leadingIcon = { + Icon( + imageVector = Icons.Rounded.Info, + contentDescription = null + ) + } + ) + DropdownMenuItem( + onClick = { + isDropDownExpanded = false + onLoadMetadata(path ?: "") + onNavigate(Screen.MetadataEditor(music.mediaId)) + }, + text = { + CuteText(stringResource(R.string.edit)) + }, + leadingIcon = { + Icon( + imageVector = Icons.Rounded.Edit, + contentDescription = null + ) + } + ) + DropdownMenuItem( + onClick = { + isDropDownExpanded = false + onChargeAlbumSongs(music.mediaMetadata.albumTitle.toString()) + onNavigate( + Screen.AlbumsDetails( + music.mediaMetadata.extras?.getLong("album_id") ?: 0 + ) + ) + }, + text = { + CuteText("Go to: ${music.mediaMetadata.albumTitle}") + }, + leadingIcon = { + Icon( + imageVector = Icons.Rounded.Album, + contentDescription = null + ) + } + ) + DropdownMenuItem( + onClick = { + isDropDownExpanded = false + onChargeArtistLists(music.mediaMetadata.artist.toString()) + onNavigate( + Screen.ArtistsDetails( + music.mediaMetadata.extras?.getLong("artist_id") ?: 0 + ) + ) + }, + text = { + CuteText("Go to: ${music.mediaMetadata.artist}") + }, + leadingIcon = { + Icon( + imageVector = Icons.Rounded.Person, + contentDescription = null + ) + } + ) + DropdownMenuItem( + onClick = { onDeleteMusic(listOf(uri), deleteSongLauncher) }, + text = { + CuteText( + text = stringResource(R.string.delete), + color = MaterialTheme.colorScheme.error + ) + }, + leadingIcon = { + Icon( + imageVector = Icons.Rounded.Delete, + contentDescription = null, + tint = MaterialTheme.colorScheme.error + ) + } + ) + } } } + } } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/main/components/BottomSheetContent.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/main/components/BottomSheetContent.kt deleted file mode 100644 index 92418fc..0000000 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/main/components/BottomSheetContent.kt +++ /dev/null @@ -1,243 +0,0 @@ -package com.sosauce.cutemusic.ui.screens.main.components - -import android.app.Activity -import android.content.Context -import android.media.MediaMetadataRetriever -import android.net.Uri -import android.widget.Toast -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.ActivityResultLauncher -import androidx.activity.result.IntentSenderRequest -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.basicMarquee -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.media3.common.MediaItem -import coil3.compose.AsyncImage -import com.sosauce.cutemusic.R -import com.sosauce.cutemusic.ui.customs.formatBinarySize -import com.sosauce.cutemusic.ui.navigation.Screen -import com.sosauce.cutemusic.ui.shared_components.CuteText -import com.sosauce.cutemusic.utils.ImageUtils - -@Composable -fun BottomSheetContent( - music: MediaItem, - onNavigate: (Screen) -> Unit, - onDismiss: () -> Unit, - onLoadMetadata: ((String) -> Unit)? = null, - onDeleteMusic: (List, ActivityResultLauncher) -> Unit -) { - val context = LocalContext.current - val fileBitrate = - getFileBitrate(context, Uri.parse(music.mediaMetadata.extras?.getString("uri"))) - val fileType = - context.contentResolver.getType(Uri.parse(music.mediaMetadata.extras?.getString("uri"))) - val uri = Uri.parse(music.mediaMetadata.extras?.getString("uri")) - val path = music.mediaMetadata.extras?.getString("path") - - val deleteSongLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { - if (it.resultCode == Activity.RESULT_OK) { - Toast.makeText( - context, - context.resources.getText(R.string.deleting_song_OK), - Toast.LENGTH_SHORT - ).show() - - } else { - Toast.makeText( - context, - context.resources.getText(R.string.error_deleting_song), - Toast.LENGTH_SHORT - ).show() - } - } - - Column { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 2.dp), - shape = RoundedCornerShape( - topStart = 24.dp, - topEnd = 24.dp, - bottomStart = 4.dp, - bottomEnd = 4.dp - ), - colors = CardDefaults.cardColors( - MaterialTheme.colorScheme.surfaceContainerHighest.copy( - alpha = 0.5f - ) - ) - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - AsyncImage( - model = ImageUtils.imageRequester( - img = music.mediaMetadata.artworkUri, - context = context - ), - contentDescription = null, - modifier = Modifier - .size(100.dp) - .padding(15.dp) - .clip(RoundedCornerShape(15)), - contentScale = ContentScale.Crop - - ) - Column { - CuteText( - text = music.mediaMetadata.title.toString(), - modifier = Modifier - .basicMarquee() - ) - CuteText( - text = music.mediaMetadata.artist.toString(), - color = MaterialTheme.colorScheme.onBackground.copy(0.85f), - modifier = Modifier.basicMarquee() - ) - } - } - } - - Row( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 2.dp) - ) { - Card( - modifier = Modifier - .padding(bottom = 5.dp) - .weight(1f) - .clip( - RoundedCornerShape( - topStart = 4.dp, - topEnd = 4.dp, - bottomStart = 24.dp, - bottomEnd = 4.dp - ) - ) - .clickable { - onLoadMetadata?.let { it(path.toString()) } - onDismiss() - onNavigate(Screen.MetadataEditor(music.mediaId)) - }, - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerHighest.copy( - alpha = 0.5f - ) - ), - shape = RoundedCornerShape( - topStart = 4.dp, - topEnd = 4.dp, - bottomStart = 24.dp, - bottomEnd = 4.dp - ), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - CuteText( - text = stringResource(R.string.edit), - modifier = Modifier.padding(10.dp) - ) - } - } - Card( - modifier = Modifier - .padding(start = 5.dp) - .weight(1f) - .clip( - RoundedCornerShape( - topStart = 4.dp, - topEnd = 4.dp, - bottomStart = 4.dp, - bottomEnd = 24.dp - ) - ) - .clickable { onDeleteMusic( - listOf(uri), - deleteSongLauncher - ) }, - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerHighest.copy( - alpha = 0.5f - ) - ), - shape = RoundedCornerShape( - topStart = 4.dp, - topEnd = 4.dp, - bottomStart = 4.dp, - bottomEnd = 24.dp - ), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - CuteText( - text = stringResource(R.string.delete), - color = MaterialTheme.colorScheme.error, - modifier = Modifier.padding(10.dp) - ) - } - } - } - - Column( - modifier = Modifier.padding(start = 16.dp, top = 16.dp) - ) { - CuteText( - text = "${stringResource(id = R.string.size)}: ${ - music.mediaMetadata.extras?.getLong( - "size" - )?.formatBinarySize() - }", - modifier = Modifier.padding(bottom = 5.dp) - ) - CuteText( - text = "${stringResource(id = R.string.bitrate)}: $fileBitrate", - - modifier = Modifier.padding(bottom = 5.dp) - ) - CuteText( - text = "${stringResource(id = R.string.type)}: $fileType", - - modifier = Modifier.padding(bottom = 5.dp) - ) - } - - } -} - -private fun getFileBitrate(context: Context, uri: Uri): String { - val retriever = MediaMetadataRetriever() - return try { - retriever.setDataSource(context, uri) - val bitrate = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE) - bitrate?.toInt()?.div(1000)?.toString()?.plus(" kbps") ?: "Unknown" - } catch (e: Exception) { - "Unknown" - } finally { - retriever.release() - } -} - diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/metadata/MetadataEditor.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/metadata/MetadataEditor.kt index 93657dc..5b1f719 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/metadata/MetadataEditor.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/metadata/MetadataEditor.kt @@ -110,10 +110,12 @@ fun MetadataEditorContent( Scaffold( floatingActionButton = { FloatingActionButton( - onClick = { onEditMusic( - listOf(uri), - editSongLauncher - ) } + onClick = { + onEditMusic( + listOf(uri), + editSongLauncher + ) + } ) { Icon( imageVector = Icons.Default.Done, diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/NowPlayingLandscape.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/NowPlayingLandscape.kt index 1c52055..e6983af 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/NowPlayingLandscape.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/NowPlayingLandscape.kt @@ -6,8 +6,6 @@ import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.core.animateIntAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -22,16 +20,7 @@ import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.rounded.Article import androidx.compose.material.icons.filled.KeyboardArrowDown -import androidx.compose.material.icons.rounded.FastForward -import androidx.compose.material.icons.rounded.FastRewind -import androidx.compose.material.icons.rounded.Pause -import androidx.compose.material.icons.rounded.PlayArrow -import androidx.compose.material.icons.rounded.SkipNext -import androidx.compose.material.icons.rounded.SkipPrevious -import androidx.compose.material.icons.rounded.Speed -import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -47,44 +36,32 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.navigation.NavController import coil3.compose.AsyncImage import com.sosauce.cutemusic.R +import com.sosauce.cutemusic.data.MusicState import com.sosauce.cutemusic.data.actions.PlayerActions import com.sosauce.cutemusic.data.datastore.rememberSnapSpeedAndPitch +import com.sosauce.cutemusic.ui.navigation.Screen import com.sosauce.cutemusic.ui.screens.lyrics.LyricsView import com.sosauce.cutemusic.ui.screens.playing.components.ActionsButtonsRow -import com.sosauce.cutemusic.ui.screens.playing.components.LoopButton import com.sosauce.cutemusic.ui.screens.playing.components.MusicSlider -import com.sosauce.cutemusic.ui.screens.playing.components.ShuffleButton +import com.sosauce.cutemusic.ui.screens.playing.components.QuickActionsRow import com.sosauce.cutemusic.ui.screens.playing.components.SpeedCard import com.sosauce.cutemusic.ui.shared_components.CuteText import com.sosauce.cutemusic.ui.shared_components.MusicViewModel @Composable fun SharedTransitionScope.NowPlayingLandscape( - viewModel: MusicViewModel, - navController: NavController, - animatedVisibilityScope: AnimatedVisibilityScope, -) { - NPLContent( - viewModel = viewModel, - onEvent = viewModel::handlePlayerActions, - onNavigateUp = navController::navigateUp, - onClickLoop = { viewModel.setLoop(it) }, - onClickShuffle = { viewModel.setShuffle(it) }, - animatedVisibilityScope = animatedVisibilityScope, - ) -} - -@Composable -private fun SharedTransitionScope.NPLContent( viewModel: MusicViewModel, onNavigateUp: () -> Unit, onEvent: (PlayerActions) -> Unit, - onClickLoop: (Boolean) -> Unit, - onClickShuffle: (Boolean) -> Unit, + onClickLoop: () -> Unit, + onClickShuffle: () -> Unit, animatedVisibilityScope: AnimatedVisibilityScope, + musicState: MusicState, + onChargeAlbumSongs: (String) -> Unit, + onNavigate: (Screen) -> Unit, + onChargeArtistLists: (String) -> Unit ) { var showSpeedCard by remember { mutableStateOf(false) } var showLyrics by remember { mutableStateOf(false) } @@ -106,112 +83,104 @@ private fun SharedTransitionScope.NPLContent( ) Box( + modifier = Modifier + .fillMaxSize() + .statusBarsPadding() + .navigationBarsPadding() + .padding(start = 30.dp, end = 30.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxSize() - .statusBarsPadding() - .navigationBarsPadding() - .padding(start = 30.dp, end = 30.dp) ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxSize() - ) { - Column { - AsyncImage( - model = viewModel.currentArt, - stringResource(R.string.artwork), - modifier = Modifier - .size(imgSize.dp) - .clip(RoundedCornerShape(10)), - contentScale = ContentScale.Crop - ) - if (showLyrics) { - Spacer(Modifier.height(10.dp)) - CuteText( - text = viewModel.currentlyPlaying - ) - CuteText( - text = viewModel.currentArtist - ) - } - } - Spacer(modifier = Modifier.width(10.dp)) + Column { + AsyncImage( + model = musicState.currentArt, + stringResource(R.string.artwork), + modifier = Modifier + .size(imgSize.dp) + .clip(RoundedCornerShape(10)), + contentScale = ContentScale.Crop + ) if (showLyrics) { - LyricsView( - viewModel = viewModel, - onHideLyrics = { showLyrics = false }, - path = viewModel.currentPath, - isLandscape = true + Spacer(Modifier.height(10.dp)) + CuteText( + text = musicState.currentlyPlaying + ) + CuteText( + text = musicState.currentArtist ) - } else { - Column( - modifier = Modifier + } + } + Spacer(modifier = Modifier.width(10.dp)) + if (showLyrics) { + LyricsView( + viewModel = viewModel, + onHideLyrics = { showLyrics = false }, + isLandscape = true, + musicState = musicState + ) + } else { + Column( + modifier = Modifier + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Row( + Modifier .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally + verticalAlignment = Alignment.CenterVertically ) { - Row( - Modifier - .fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically + IconButton( + onClick = { onNavigateUp() } ) { - IconButton( - onClick = { onNavigateUp() } - ) { - Icon( - imageVector = Icons.Default.KeyboardArrowDown, - contentDescription = null, - modifier = Modifier.size(28.dp) - ) - } - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxWidth(0.9f) + Icon( + imageVector = Icons.Default.KeyboardArrowDown, + contentDescription = null, + modifier = Modifier.size(28.dp) ) - { - CuteText( - text = viewModel.currentlyPlaying, - color = MaterialTheme.colorScheme.onBackground, - fontSize = 30.sp - ) - } } - CuteText( - text = viewModel.currentArtist, - color = MaterialTheme.colorScheme.onBackground.copy(0.85f), - fontSize = 16.sp + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth(0.9f) ) - MusicSlider(viewModel = viewModel) - ActionsButtonsRow( - onClickLoop = onClickLoop, - onClickShuffle = onClickShuffle, - viewModel = viewModel, - onEvent = onEvent, - animatedVisibilityScope = animatedVisibilityScope - ) - Spacer(modifier = Modifier.weight(1f)) - Row( - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - ) { - IconButton(onClick = { showSpeedCard = true }) { - Icon( - imageVector = Icons.Rounded.Speed, - contentDescription = "change speed" - ) - } - IconButton(onClick = { showLyrics = true }) { - Icon( - imageVector = Icons.AutoMirrored.Rounded.Article, - contentDescription = "show lyrics" - ) - } + { + CuteText( + text = musicState.currentlyPlaying, + color = MaterialTheme.colorScheme.onBackground, + fontSize = 30.sp + ) } } + CuteText( + text = musicState.currentArtist, + color = MaterialTheme.colorScheme.onBackground.copy(0.85f), + fontSize = 16.sp + ) + MusicSlider( + viewModel = viewModel, + musicState = musicState + ) + ActionsButtonsRow( + onClickLoop = onClickLoop, + onClickShuffle = onClickShuffle, + onEvent = onEvent, + animatedVisibilityScope = animatedVisibilityScope, + musicState = musicState + ) + Spacer(modifier = Modifier.weight(1f)) + QuickActionsRow( + musicState = musicState, + onNavigate = onNavigate, + onShowLyrics = { showLyrics = true }, + onChargeAlbumSongs = onChargeAlbumSongs, + onShowSpeedCard = { showSpeedCard = true }, + onChargeArtistLists = onChargeArtistLists + ) } - } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/NowPlayingScreen.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/NowPlayingScreen.kt index 71e7b71..519497b 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/NowPlayingScreen.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/NowPlayingScreen.kt @@ -3,42 +3,27 @@ package com.sosauce.cutemusic.ui.screens.playing -import androidx.annotation.OptIn import androidx.compose.animation.AnimatedVisibilityScope -import androidx.compose.animation.Crossfade import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope -import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.animateIntAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.rounded.Article -import androidx.compose.material.icons.rounded.FastForward -import androidx.compose.material.icons.rounded.FastRewind import androidx.compose.material.icons.rounded.KeyboardArrowDown -import androidx.compose.material.icons.rounded.Pause -import androidx.compose.material.icons.rounded.PlayArrow -import androidx.compose.material.icons.rounded.RestartAlt -import androidx.compose.material.icons.rounded.SkipNext -import androidx.compose.material.icons.rounded.SkipPrevious -import androidx.compose.material.icons.rounded.Speed -import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -52,38 +37,47 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.media3.common.util.UnstableApi import androidx.navigation.NavController import coil3.compose.AsyncImage import com.sosauce.cutemusic.R +import com.sosauce.cutemusic.data.MusicState import com.sosauce.cutemusic.data.actions.PlayerActions import com.sosauce.cutemusic.data.datastore.rememberIsLandscape import com.sosauce.cutemusic.data.datastore.rememberSnapSpeedAndPitch +import com.sosauce.cutemusic.ui.navigation.Screen import com.sosauce.cutemusic.ui.screens.lyrics.LyricsView import com.sosauce.cutemusic.ui.screens.playing.components.ActionsButtonsRow -import com.sosauce.cutemusic.ui.screens.playing.components.LoopButton import com.sosauce.cutemusic.ui.screens.playing.components.MusicSlider -import com.sosauce.cutemusic.ui.screens.playing.components.ShuffleButton +import com.sosauce.cutemusic.ui.screens.playing.components.QuickActionsRow import com.sosauce.cutemusic.ui.screens.playing.components.SpeedCard import com.sosauce.cutemusic.ui.shared_components.CuteText import com.sosauce.cutemusic.ui.shared_components.MusicViewModel import com.sosauce.cutemusic.utils.ImageUtils -@OptIn(UnstableApi::class) @Composable fun SharedTransitionScope.NowPlayingScreen( navController: NavController, viewModel: MusicViewModel, animatedVisibilityScope: AnimatedVisibilityScope, + musicState: MusicState, + onChargeAlbumSongs: (String) -> Unit, + onChargeArtistLists: (String) -> Unit ) { var showFullLyrics by remember { mutableStateOf(false) } if (rememberIsLandscape()) { NowPlayingLandscape( viewModel = viewModel, - navController = navController, + onNavigateUp = navController::navigateUp, + onEvent = { viewModel.handlePlayerActions(it) }, + onClickLoop = { viewModel.handlePlayerActions(PlayerActions.ApplyLoop) }, + onClickShuffle = { viewModel.handlePlayerActions(PlayerActions.ApplyShuffle) }, animatedVisibilityScope = animatedVisibilityScope, + musicState = musicState, + onChargeAlbumSongs = onChargeAlbumSongs, + onNavigate = { navController.navigate(it) }, + onChargeArtistLists = onChargeArtistLists ) } else { when (showFullLyrics) { @@ -91,7 +85,7 @@ fun SharedTransitionScope.NowPlayingScreen( LyricsView( viewModel = viewModel, onHideLyrics = { showFullLyrics = false }, - path = viewModel.currentPath + musicState = musicState ) } @@ -100,10 +94,14 @@ fun SharedTransitionScope.NowPlayingScreen( viewModel = viewModel, onEvent = viewModel::handlePlayerActions, onNavigateUp = navController::navigateUp, - onClickLoop = { viewModel.setLoop(it) }, - onClickShuffle = { viewModel.setShuffle(it) }, + onClickLoop = { viewModel.handlePlayerActions(PlayerActions.ApplyLoop) }, + onClickShuffle = { viewModel.handlePlayerActions(PlayerActions.ApplyShuffle) }, animatedVisibilityScope = animatedVisibilityScope, onShowLyrics = { showFullLyrics = true }, + musicState = musicState, + onChargeAlbumSongs = onChargeAlbumSongs, + onNavigate = { navController.navigate(it) }, + onChargeArtistLists = onChargeArtistLists ) } } @@ -116,10 +114,14 @@ private fun SharedTransitionScope.NowPlayingContent( viewModel: MusicViewModel, onEvent: (PlayerActions) -> Unit, onNavigateUp: () -> Unit, - onClickLoop: (Boolean) -> Unit, - onClickShuffle: (Boolean) -> Unit, + onClickLoop: () -> Unit, + onClickShuffle: () -> Unit, animatedVisibilityScope: AnimatedVisibilityScope, onShowLyrics: () -> Unit, + musicState: MusicState, + onChargeAlbumSongs: (String) -> Unit, + onNavigate: (Screen) -> Unit, + onChargeArtistLists: (String) -> Unit ) { val context = LocalContext.current var showSpeedCard by remember { mutableStateOf(false) } @@ -135,121 +137,109 @@ private fun SharedTransitionScope.NowPlayingContent( onChangeSnap = { snap = !snap } ) } - Scaffold( - modifier = Modifier.padding(15.dp) - ) { _ -> - Column( + Column( + modifier = Modifier + .fillMaxSize() + .padding(15.dp) + .statusBarsPadding(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Row( modifier = Modifier .fillMaxWidth() - .statusBarsPadding(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + .padding(15.dp), + horizontalArrangement = Arrangement.Start ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(15.dp), - horizontalArrangement = Arrangement.Start - ) { - IconButton( - onClick = onNavigateUp - ) { - Icon( - imageVector = Icons.Rounded.KeyboardArrowDown, - contentDescription = null, - modifier = Modifier - .sharedElement( - state = rememberSharedContentState(key = "arrow"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(durationMillis = 500) - } - ) - .size(28.dp) - ) - } - } - AsyncImage( - model = ImageUtils.imageRequester( - img = viewModel.currentArt, - context = context - ), - contentDescription = stringResource(R.string.artwork), - modifier = Modifier - .size(340.dp) - .clip(RoundedCornerShape(5)), - contentScale = ContentScale.Crop - ) - - Spacer(modifier = Modifier.height(20.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center + IconButton( + onClick = onNavigateUp ) { - Column( + Icon( + imageVector = Icons.Rounded.KeyboardArrowDown, + contentDescription = null, modifier = Modifier - .padding(horizontal = 15.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - - CuteText( - text = viewModel.currentlyPlaying, - color = MaterialTheme.colorScheme.onBackground, - fontSize = 20.sp, - modifier = Modifier - .sharedElement( - state = rememberSharedContentState(key = "currentlyPlaying"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(durationMillis = 500) - } - ) - .basicMarquee() - ) - //Spacer(modifier = Modifier.height(5.dp)) - CuteText( - text = viewModel.currentArtist, - color = MaterialTheme.colorScheme.onBackground.copy(0.85f), - fontSize = 14.sp, - modifier = Modifier.basicMarquee() - ) - } + .sharedElement( + state = rememberSharedContentState(key = "arrow"), + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = { _, _ -> + tween(durationMillis = 500) + } + ) + .size(28.dp) + ) } - Spacer(modifier = Modifier.height(10.dp)) + } + AsyncImage( + model = ImageUtils.imageRequester( + img = musicState.currentArt, + context = context + ), + contentDescription = stringResource(R.string.artwork), + modifier = Modifier + .size(340.dp) + .clip(RoundedCornerShape(5)), + contentScale = ContentScale.Crop + ) + + Spacer(modifier = Modifier.height(20.dp)) - MusicSlider(viewModel = viewModel) - Spacer(modifier = Modifier.height(7.dp)) - ActionsButtonsRow( - onClickLoop = onClickLoop, - onClickShuffle = onClickShuffle, - viewModel = viewModel, - onEvent = onEvent, - animatedVisibilityScope = animatedVisibilityScope - ) - Spacer(modifier = Modifier.weight(1f)) - Row( - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Column( modifier = Modifier - .fillMaxWidth() - .navigationBarsPadding() + .padding(horizontal = 15.dp), + horizontalAlignment = Alignment.CenterHorizontally, ) { - IconButton(onClick = onShowLyrics) { - Icon( - imageVector = Icons.AutoMirrored.Rounded.Article, - contentDescription = "show lyrics" - ) - } - IconButton(onClick = { showSpeedCard = true }) { - Icon( - imageVector = Icons.Rounded.Speed, - contentDescription = "change speed" - ) - } + + CuteText( + text = musicState.currentlyPlaying, + color = MaterialTheme.colorScheme.onBackground, + fontSize = 20.sp, + modifier = Modifier + .sharedElement( + state = rememberSharedContentState(key = "currentlyPlaying"), + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = { _, _ -> + tween(durationMillis = 500) + } + ) + .basicMarquee() + ) + //Spacer(modifier = Modifier.height(5.dp)) + CuteText( + text = musicState.currentArtist, + color = MaterialTheme.colorScheme.onBackground.copy(0.85f), + fontSize = 14.sp, + modifier = Modifier.basicMarquee() + ) } } + Spacer(modifier = Modifier.height(10.dp)) + + MusicSlider( + viewModel = viewModel, + musicState = musicState + ) + Spacer(modifier = Modifier.height(7.dp)) + ActionsButtonsRow( + onClickLoop = onClickLoop, + onClickShuffle = onClickShuffle, + onEvent = onEvent, + animatedVisibilityScope = animatedVisibilityScope, + musicState = musicState + ) + Spacer(modifier = Modifier.weight(1f)) + QuickActionsRow( + musicState = musicState, + onNavigate = onNavigate, + onShowLyrics = onShowLyrics, + onChargeAlbumSongs = onChargeAlbumSongs, + onShowSpeedCard = { showSpeedCard = true }, + onChargeArtistLists = onChargeArtistLists + ) } } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/NowPlayingState.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/NowPlayingState.kt deleted file mode 100644 index f7c82b0..0000000 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/NowPlayingState.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.sosauce.cutemusic.ui.screens.playing - -import androidx.media3.common.MediaItem - -data class NowPlayingState( - var currentlyPlaying: String = "", - var currentlyArtist: String = "", - var currentMusicDuration: Long = 0L, - var currentPosition: Long = 0L, - var isPlaying: Boolean = false, - var artwork: ByteArray? = null, - val musics: List? = emptyList() -) \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/Buttons.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/Buttons.kt index fbdf085..bed1b14 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/Buttons.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/Buttons.kt @@ -2,8 +2,7 @@ package com.sosauce.cutemusic.ui.screens.playing.components -import android.renderscript.RenderScript -import androidx.compose.animation.AnimatedVisibility +import android.util.Log import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.Crossfade import androidx.compose.animation.ExperimentalSharedTransitionApi @@ -12,21 +11,13 @@ import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.animateIntAsState import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -import androidx.compose.animation.slideInVertically -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.FastForward @@ -38,7 +29,6 @@ import androidx.compose.material.icons.rounded.RestartAlt import androidx.compose.material.icons.rounded.Shuffle import androidx.compose.material.icons.rounded.SkipNext import androidx.compose.material.icons.rounded.SkipPrevious -import androidx.compose.material3.DropdownMenu import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -51,34 +41,31 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.blur -import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Popup -import androidx.compose.ui.window.PopupProperties +import com.sosauce.cutemusic.data.MusicState import com.sosauce.cutemusic.data.actions.PlayerActions +import com.sosauce.cutemusic.data.datastore.rememberShouldApplyLoop import com.sosauce.cutemusic.ui.shared_components.CuteText -import com.sosauce.cutemusic.ui.shared_components.MusicViewModel import com.sosauce.cutemusic.utils.CuteIconButton -import com.sosauce.cutemusic.utils.thenIf import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @Composable fun LoopButton( - onClick: (Boolean) -> Unit, + onClick: () -> Unit, isLooping: Boolean ) { + val rotation = remember { Animatable(0f) } val scope = rememberCoroutineScope() + var shouldApplyLoop by rememberShouldApplyLoop() IconButton( onClick = { - onClick(!isLooping) + shouldApplyLoop = !isLooping + onClick() scope.launch(Dispatchers.IO) { rotation.animateTo( targetValue = -360f, @@ -90,6 +77,8 @@ fun LoopButton( animationSpec = tween(0) ) } + Log.d("Looping2", shouldApplyLoop.toString()) + } ) { Icon( @@ -103,15 +92,13 @@ fun LoopButton( @Composable fun ShuffleButton( - onClick: (Boolean) -> Unit, + onClick: () -> Unit, isShuffling: Boolean ) { IconButton( - onClick = { - onClick(!isShuffling) - } + onClick = onClick ) { Icon( imageVector = Icons.Rounded.Shuffle, @@ -123,11 +110,11 @@ fun ShuffleButton( @Composable fun SharedTransitionScope.ActionsButtonsRow( - onClickLoop: (Boolean) -> Unit, - onClickShuffle: (Boolean) -> Unit, - viewModel: MusicViewModel, + onClickLoop: () -> Unit, + onClickShuffle: () -> Unit, onEvent: (PlayerActions) -> Unit, - animatedVisibilityScope: AnimatedVisibilityScope + animatedVisibilityScope: AnimatedVisibilityScope, + musicState: MusicState ) { @@ -148,7 +135,7 @@ fun SharedTransitionScope.ActionsButtonsRow( ) val roundedFAB by animateIntAsState( - targetValue = if (viewModel.isCurrentlyPlaying) 30 else 50, + targetValue = if (musicState.isCurrentlyPlaying) 30 else 50, label = "FAB Shape" ) @@ -181,7 +168,7 @@ fun SharedTransitionScope.ActionsButtonsRow( } else { ShuffleButton( onClick = onClickShuffle, - isShuffling = viewModel.isShuffling + isShuffling = musicState.isShuffling ) } } @@ -200,7 +187,7 @@ fun SharedTransitionScope.ActionsButtonsRow( } else { IconButton( onClick = { - if (viewModel.currentPosition >= 10000) { + if (musicState.currentPosition >= 10000) { onEvent(PlayerActions.RestartSong) } else { onEvent(PlayerActions.SeekToPreviousMusic) @@ -218,7 +205,7 @@ fun SharedTransitionScope.ActionsButtonsRow( } ) { Crossfade( - targetState = viewModel.currentPosition >= 10000, + targetState = musicState.currentPosition >= 10000, label = "" ) { if (!it) { @@ -263,7 +250,7 @@ fun SharedTransitionScope.ActionsButtonsRow( } ) { CuteText("-10") } } else { - CuteIconButton ( + CuteIconButton( onClick = { onEvent(PlayerActions.RewindTo(5000)) }, onLongClick = { showLongPressMenuMinus = true } ) { @@ -280,7 +267,7 @@ fun SharedTransitionScope.ActionsButtonsRow( shape = RoundedCornerShape(roundedFAB) ) { Icon( - imageVector = if (viewModel.isCurrentlyPlaying) Icons.Rounded.Pause else Icons.Rounded.PlayArrow, + imageVector = if (musicState.isCurrentlyPlaying) Icons.Rounded.Pause else Icons.Rounded.PlayArrow, contentDescription = "pause/play button", modifier = Modifier.sharedElement( state = rememberSharedContentState(key = "playPauseIcon"), @@ -388,7 +375,7 @@ fun SharedTransitionScope.ActionsButtonsRow( } else { LoopButton( onClick = onClickLoop, - isLooping = viewModel.isLooping + isLooping = musicState.isLooping ) } } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/QuickActionsRow.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/QuickActionsRow.kt new file mode 100644 index 0000000..8a79021 --- /dev/null +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/QuickActionsRow.kt @@ -0,0 +1,136 @@ +package com.sosauce.cutemusic.ui.screens.playing.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.Article +import androidx.compose.material.icons.rounded.Album +import androidx.compose.material.icons.rounded.Info +import androidx.compose.material.icons.rounded.MoreVert +import androidx.compose.material.icons.rounded.Person +import androidx.compose.material.icons.rounded.Speed +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.sosauce.cutemusic.R +import com.sosauce.cutemusic.data.MusicState +import com.sosauce.cutemusic.ui.navigation.Screen +import com.sosauce.cutemusic.ui.shared_components.CuteText +import com.sosauce.cutemusic.ui.shared_components.MusicStateDetailsDialog + +@Composable +fun QuickActionsRow( + musicState: MusicState, + onShowLyrics: () -> Unit, + onShowSpeedCard: () -> Unit, + onChargeAlbumSongs: (String) -> Unit, + onNavigate: (Screen) -> Unit, + onChargeArtistLists: (String) -> Unit +) { + + var isDropDownExpanded by remember { mutableStateOf(false) } + var showDetailsDialog by remember { mutableStateOf(false) } + + if (showDetailsDialog) { + MusicStateDetailsDialog( + musicState = musicState, + onDismissRequest = { showDetailsDialog = false } + ) + } + + + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding() + ) { + + IconButton(onClick = onShowLyrics) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.Article, + contentDescription = "show lyrics" + ) + } + IconButton(onClick = onShowSpeedCard) { + Icon( + imageVector = Icons.Rounded.Speed, + contentDescription = "change speed" + ) + } + Row { + IconButton(onClick = { isDropDownExpanded = true }) { + Icon( + imageVector = Icons.Rounded.MoreVert, + contentDescription = "more" + ) + } + + + DropdownMenu( + expanded = isDropDownExpanded, + onDismissRequest = { isDropDownExpanded = false }, + shape = RoundedCornerShape(24.dp) + ) { + DropdownMenuItem( + onClick = { showDetailsDialog = true }, + text = { + CuteText(stringResource(R.string.details)) + }, + leadingIcon = { + Icon( + imageVector = Icons.Rounded.Info, + contentDescription = null + ) + } + ) + DropdownMenuItem( + onClick = { + isDropDownExpanded = false + onChargeAlbumSongs(musicState.currentAlbum) + onNavigate(Screen.AlbumsDetails(musicState.currentAlbumId)) + }, + text = { + CuteText("Go to: ${musicState.currentAlbum}") + }, + leadingIcon = { + Icon( + imageVector = Icons.Rounded.Album, + contentDescription = null + ) + } + ) + DropdownMenuItem( + onClick = { + isDropDownExpanded = false + onChargeArtistLists(musicState.currentArtist) + onNavigate(Screen.ArtistsDetails(musicState.currentArtistId)) + }, + text = { + CuteText("Go to: ${musicState.currentArtist}") + }, + leadingIcon = { + Icon( + imageVector = Icons.Rounded.Person, + contentDescription = null + ) + } + ) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/Slider.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/Slider.kt index b1681d1..a02cba1 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/Slider.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/Slider.kt @@ -1,4 +1,4 @@ -@file:OptIn(ExperimentalMaterial3Api::class) +@file:OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3Api::class) package com.sosauce.cutemusic.ui.screens.playing.components @@ -9,27 +9,38 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp +import com.sosauce.cutemusic.data.MusicState import com.sosauce.cutemusic.data.actions.PlayerActions +import com.sosauce.cutemusic.data.datastore.rememberUseClassicSlider import com.sosauce.cutemusic.ui.shared_components.CuteText import com.sosauce.cutemusic.ui.shared_components.MusicViewModel import me.saket.squiggles.SquigglySlider import java.util.Locale @Composable -fun MusicSlider(viewModel: MusicViewModel) { +fun MusicSlider( + viewModel: MusicViewModel, + musicState: MusicState +) { - val sliderPosition = rememberUpdatedState(viewModel.currentPosition) + val sliderPosition = rememberUpdatedState(musicState.currentPosition) + val useClassicSlider by rememberUseClassicSlider() + val interactionSource = remember { MutableInteractionSource() } Column { Row( @@ -46,7 +57,7 @@ fun MusicSlider(viewModel: MusicViewModel) { .padding(horizontal = 8.dp, vertical = 4.dp) ) { CuteText( - text = totalDuration(viewModel.currentMusicDuration), + text = totalDuration(musicState.currentMusicDuration), ) } Box( @@ -58,22 +69,51 @@ fun MusicSlider(viewModel: MusicViewModel) { .padding(horizontal = 8.dp, vertical = 4.dp) ) { CuteText( - text = timeLeft(viewModel.currentPosition) + text = timeLeft(musicState.currentPosition) ) } } - SquigglySlider( - value = sliderPosition.value.toFloat(), - onValueChange = { - viewModel.currentPosition = it.toLong() - viewModel.handlePlayerActions(PlayerActions.SeekToSlider(viewModel.currentPosition)) - }, - valueRange = 0f..viewModel.currentMusicDuration.toFloat(), - onValueChangeFinished = { - viewModel.handlePlayerActions(PlayerActions.SeekToSlider(viewModel.currentPosition)) - }, - modifier = Modifier.fillMaxWidth() - ) + if (useClassicSlider) { + Slider( + value = sliderPosition.value.toFloat(), + onValueChange = { + musicState.currentPosition = it.toLong() + viewModel.handlePlayerActions(PlayerActions.SeekToSlider(musicState.currentPosition)) + }, + valueRange = 0f..musicState.currentMusicDuration.toFloat(), + onValueChangeFinished = { + viewModel.handlePlayerActions(PlayerActions.SeekToSlider(musicState.currentPosition)) + }, + modifier = Modifier.fillMaxWidth(), + track = { sliderState -> + SliderDefaults.Track( + sliderState = sliderState, + drawStopIndicator = null, + thumbTrackGapSize = 0.dp, + modifier = Modifier.height(4.dp) + ) + }, + thumb = { + SliderDefaults.Thumb( + interactionSource = interactionSource, + thumbSize = DpSize(width = 20.dp, height = 20.dp) + ) + } + ) + } else { + SquigglySlider( + value = sliderPosition.value.toFloat(), + onValueChange = { + musicState.currentPosition = it.toLong() + viewModel.handlePlayerActions(PlayerActions.SeekToSlider(musicState.currentPosition)) + }, + valueRange = 0f..musicState.currentMusicDuration.toFloat(), + onValueChangeFinished = { + viewModel.handlePlayerActions(PlayerActions.SeekToSlider(musicState.currentPosition)) + }, + modifier = Modifier.fillMaxWidth() + ) + } } } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/SpeedCard.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/SpeedCard.kt index ef48384..fc5dea3 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/SpeedCard.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/playing/components/SpeedCard.kt @@ -32,6 +32,7 @@ import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.sosauce.cutemusic.R +import com.sosauce.cutemusic.data.actions.PlayerActions import com.sosauce.cutemusic.ui.shared_components.CuteText import com.sosauce.cutemusic.ui.shared_components.MusicViewModel @@ -87,7 +88,12 @@ fun SpeedCard( value = speed, onValueChange = { speed = it - viewModel.setPlaybackSpeed(speed, pitch) + viewModel.handlePlayerActions( + PlayerActions.ApplyPlaybackSpeed( + speed = speed, + pitch = pitch + ) + ) }, valueRange = 0.5f..2f, track = { sliderState -> @@ -125,7 +131,12 @@ fun SpeedCard( value = pitch, onValueChange = { pitch = it - viewModel.setPlaybackSpeed(speed, pitch) + viewModel.handlePlayerActions( + PlayerActions.ApplyPlaybackSpeed( + speed = speed, + pitch = pitch + ) + ) }, valueRange = 0.5f..2f, track = { sliderState -> @@ -153,7 +164,12 @@ fun SpeedCard( onValueChange = { speed = it pitch = it - viewModel.setPlaybackSpeed(speed, pitch) + viewModel.handlePlayerActions( + PlayerActions.ApplyPlaybackSpeed( + speed = speed, + pitch = pitch + ) + ) }, valueRange = 0.5f..2f, track = { sliderState -> diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/SettingsScreen.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/SettingsScreen.kt index 394e82f..bcda213 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/SettingsScreen.kt @@ -16,6 +16,7 @@ import com.sosauce.cutemusic.ui.navigation.Screen import com.sosauce.cutemusic.ui.screens.settings.compenents.AboutCard import com.sosauce.cutemusic.ui.screens.settings.compenents.Misc import com.sosauce.cutemusic.ui.screens.settings.compenents.ThemeManagement +import com.sosauce.cutemusic.ui.screens.settings.compenents.UISettings import com.sosauce.cutemusic.ui.shared_components.AppBar @@ -49,7 +50,7 @@ fun SettingsScreen( ) { AboutCard() ThemeManagement() - //UISettings() + UISettings() Misc(onNavigateTo = onNavigate) } } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/compenents/SettingsCards.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/compenents/SettingsCards.kt index f61f909..9328df8 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/compenents/SettingsCards.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/compenents/SettingsCards.kt @@ -75,10 +75,10 @@ inline fun SettingsCards( } } } - Switch( - checked = checked, - onCheckedChange = { onCheckedChange() } - ) + Switch( + checked = checked, + onCheckedChange = { onCheckedChange() } + ) } } } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/compenents/Switches.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/compenents/Switches.kt index 386b46d..494f998 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/compenents/Switches.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/settings/compenents/Switches.kt @@ -17,16 +17,16 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.sosauce.cutemusic.R import com.sosauce.cutemusic.data.datastore.rememberFollowSys import com.sosauce.cutemusic.data.datastore.rememberUseAmoledMode import com.sosauce.cutemusic.data.datastore.rememberUseArtTheme +import com.sosauce.cutemusic.data.datastore.rememberUseClassicSlider import com.sosauce.cutemusic.data.datastore.rememberUseDarkMode import com.sosauce.cutemusic.data.datastore.rememberUseSystemFont -import com.sosauce.cutemusic.ui.customs.restart import com.sosauce.cutemusic.ui.navigation.Screen import com.sosauce.cutemusic.ui.shared_components.CuteText +import com.sosauce.cutemusic.utils.restart @Composable fun Misc( @@ -131,6 +131,7 @@ fun ThemeManagement() { @Composable fun UISettings() { var useArtTheme by rememberUseArtTheme() + var useClassicSlider by rememberUseClassicSlider() Column { CuteText( @@ -138,20 +139,27 @@ fun UISettings() { color = MaterialTheme.colorScheme.primary, modifier = Modifier.padding(horizontal = 34.dp, vertical = 8.dp) ) +// SettingsCards( +// checked = useArtTheme, +// onCheckedChange = { useArtTheme = !useArtTheme }, +// topDp = 24.dp, +// bottomDp = 24.dp, +// text = stringResource(id = R.string.use_art), +// optionalDescription = { +// CuteText( +// text = "App's theme will follow the currently playing music's art", +// color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.85f), +// fontSize = 13.sp +// +// ) +// } +// ) SettingsCards( - checked = useArtTheme, - onCheckedChange = { useArtTheme = !useArtTheme }, + checked = useClassicSlider, + onCheckedChange = { useClassicSlider = !useClassicSlider }, topDp = 24.dp, bottomDp = 24.dp, - text = stringResource(id = R.string.use_art), - optionalDescription = { - CuteText( - text = "App's theme will follow the currently playing music's art", - color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.85f), - fontSize = 13.sp - - ) - } + text = stringResource(id = R.string.classic_slider), ) } } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/MusicDetailsDialog.kt b/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/MusicDetailsDialog.kt new file mode 100644 index 0000000..62c4ff4 --- /dev/null +++ b/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/MusicDetailsDialog.kt @@ -0,0 +1,201 @@ +package com.sosauce.cutemusic.ui.shared_components + +import android.net.Uri +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.media3.common.MediaItem +import coil3.compose.AsyncImage +import com.sosauce.cutemusic.R +import com.sosauce.cutemusic.data.MusicState +import com.sosauce.cutemusic.utils.ImageUtils +import com.sosauce.cutemusic.utils.formatBinarySize +import com.sosauce.cutemusic.utils.getBitrate + +@Composable +fun MusicDetailsDialog( + music: MediaItem, + onDismissRequest: () -> Unit +) { + val context = LocalContext.current + val uri = remember { Uri.parse(music.mediaMetadata.extras?.getString("uri")) } + val fileBitrate = uri.getBitrate(context) + val fileType = + context.contentResolver.getType(uri) + + AlertDialog( + onDismissRequest = onDismissRequest, + confirmButton = { + TextButton( + onClick = onDismissRequest + ) { + CuteText(stringResource(R.string.okay)) + } + }, + title = { + CuteText(stringResource(R.string.details)) + }, + text = { + Column { + Card( + modifier = Modifier + .fillMaxWidth(), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + MaterialTheme.colorScheme.surfaceContainerHighest + ) + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + AsyncImage( + model = ImageUtils.imageRequester( + img = music.mediaMetadata.artworkUri, + context = context + ), + contentDescription = null, + modifier = Modifier + .size(100.dp) + .padding(15.dp) + .clip(RoundedCornerShape(15)), + contentScale = ContentScale.Crop + + ) + Column { + CuteText( + text = music.mediaMetadata.title.toString(), + modifier = Modifier + .basicMarquee() + ) + CuteText( + text = music.mediaMetadata.artist.toString(), + color = MaterialTheme.colorScheme.onBackground.copy(0.85f), + modifier = Modifier.basicMarquee() + ) + } + } + } + Spacer(Modifier.height(10.dp)) + CuteText( + text = "${stringResource(id = R.string.size)}: ${ + music.mediaMetadata.extras?.getLong( + "size" + )?.formatBinarySize() + }", + modifier = Modifier.padding(bottom = 5.dp) + ) + CuteText( + text = "${stringResource(id = R.string.bitrate)}: $fileBitrate", + + modifier = Modifier.padding(bottom = 5.dp) + ) + CuteText( + text = "${stringResource(id = R.string.type)}: $fileType", + + modifier = Modifier.padding(bottom = 5.dp) + ) + } + } + ) +} + +@Composable +fun MusicStateDetailsDialog( + musicState: MusicState, + onDismissRequest: () -> Unit +) { + val context = LocalContext.current + val uri = remember { Uri.parse(musicState.currentMusicUri) } + val fileBitrate = uri.getBitrate(context) + val fileType = + context.contentResolver.getType(uri) + + AlertDialog( + onDismissRequest = onDismissRequest, + confirmButton = { + TextButton( + onClick = onDismissRequest + ) { + CuteText(stringResource(R.string.okay)) + } + }, + title = { + CuteText(stringResource(R.string.details)) + }, + text = { + Column { + Card( + modifier = Modifier + .fillMaxWidth(), + shape = RoundedCornerShape(24.dp), + colors = CardDefaults.cardColors( + MaterialTheme.colorScheme.surfaceContainerHighest + ) + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + AsyncImage( + model = ImageUtils.imageRequester( + img = musicState.currentArt, + context = context + ), + contentDescription = null, + modifier = Modifier + .size(100.dp) + .padding(15.dp) + .clip(RoundedCornerShape(15)), + contentScale = ContentScale.Crop + + ) + Column { + CuteText( + text = musicState.currentlyPlaying, + modifier = Modifier + .basicMarquee() + ) + CuteText( + text = musicState.currentArtist, + color = MaterialTheme.colorScheme.onBackground.copy(0.85f), + modifier = Modifier.basicMarquee() + ) + } + } + } + Spacer(Modifier.height(10.dp)) + CuteText( + text = "${stringResource(id = R.string.size)}: ${ + musicState.currentSize.formatBinarySize() + }", + modifier = Modifier.padding(bottom = 5.dp) + ) + CuteText( + text = "${stringResource(id = R.string.bitrate)}: $fileBitrate", + + modifier = Modifier.padding(bottom = 5.dp) + ) + CuteText( + text = "${stringResource(id = R.string.type)}: $fileType", + + modifier = Modifier.padding(bottom = 5.dp) + ) + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/MusicViewModel.kt b/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/MusicViewModel.kt index c76f71f..adc6179 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/MusicViewModel.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/MusicViewModel.kt @@ -1,76 +1,107 @@ package com.sosauce.cutemusic.ui.shared_components import android.app.Application +import android.content.ComponentName import android.net.Uri -import android.os.Handler -import android.os.Looper -import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import androidx.media3.common.MediaItem import androidx.media3.common.MediaMetadata -import androidx.media3.common.PlaybackParameters import androidx.media3.common.Player import androidx.media3.session.MediaController -import com.google.common.util.concurrent.ListenableFuture +import androidx.media3.session.SessionToken import com.google.common.util.concurrent.MoreExecutors +import com.sosauce.cutemusic.data.MusicState import com.sosauce.cutemusic.data.actions.PlayerActions -import com.sosauce.cutemusic.data.datastore.rememberKillService import com.sosauce.cutemusic.domain.model.Lyrics -import com.sosauce.cutemusic.ui.customs.playAtIndex +import com.sosauce.cutemusic.domain.repository.MediaStoreHelper +import com.sosauce.cutemusic.main.PlaybackService +import com.sosauce.cutemusic.utils.applyLoop +import com.sosauce.cutemusic.utils.applyPlaybackSpeed +import com.sosauce.cutemusic.utils.applyShuffle +import com.sosauce.cutemusic.utils.playAtIndex +import com.sosauce.cutemusic.utils.playFromAlbum +import com.sosauce.cutemusic.utils.playFromArtist +import com.sosauce.cutemusic.utils.playRandom import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import org.jaudiotagger.audio.AudioFileIO import org.jaudiotagger.tag.FieldKey import java.io.File -class MusicViewModel ( - private val controllerFuture: ListenableFuture, - application: Application +class MusicViewModel( + application: Application, + private val mediaStoreHelper: MediaStoreHelper ) : AndroidViewModel(application) { private var mediaController: MediaController? by mutableStateOf(null) - private val shouldKill by rememberKillService(application) - - var selectedItem by mutableIntStateOf(0) + private val _musicState = MutableStateFlow(MusicState()) + val musicState = _musicState.asStateFlow() - var currentlyPlaying by mutableStateOf("") - var currentArtist by mutableStateOf("") - var currentArt: Uri? by mutableStateOf(null) - var isCurrentlyPlaying by mutableStateOf(false) - var currentPosition by mutableLongStateOf(0L) - var currentMusicDuration by mutableLongStateOf(0L) - var currentMusicUri by mutableStateOf("") - var currentLyrics by mutableStateOf(listOf()) - var isLooping by mutableStateOf(false) - var isShuffling by mutableStateOf(false) - var currentPath by mutableStateOf("") - - private var currentLrcFile by mutableStateOf(null) - private val playerListener = object : Player.Listener { override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) { super.onMediaMetadataChanged(mediaMetadata) - currentlyPlaying = (mediaMetadata.title ?: currentlyPlaying).toString() - currentArtist = (mediaMetadata.artist ?: currentArtist).toString() - currentArt = mediaMetadata.artworkUri ?: currentArt - currentPath = (mediaMetadata.extras?.getString("path") ?: currentPath) - currentMusicUri = mediaMetadata.extras?.getString("uri") ?: currentMusicUri - currentLrcFile = loadLrcFile(currentPath) - currentLyrics = parseLrcFile(currentLrcFile) + _musicState.value = _musicState.value.copy( + currentlyPlaying = mediaMetadata.title.toString(), + currentArtist = mediaMetadata.artist.toString(), + currentArtistId = mediaMetadata.extras?.getLong("artist_id") ?: 0, + currentArt = mediaMetadata.artworkUri, + currentPath = mediaMetadata.extras?.getString("path") ?: "No Path Found!", + currentMusicUri = mediaMetadata.extras?.getString("uri") ?: "No Uri Found!", + currentLrcFile = loadLrcFile(musicState.value.currentPath), + currentLyrics = parseLrcFile(musicState.value.currentLrcFile), + currentAlbum = mediaMetadata.albumTitle.toString(), + currentAlbumId = mediaMetadata.extras?.getLong("album_id") ?: 0, + currentSize = mediaMetadata.extras?.getLong("size") ?: 0 + ) } + override fun onIsPlayingChanged(isPlaying: Boolean) { super.onIsPlayingChanged(isPlaying) - isCurrentlyPlaying = isPlaying + _musicState.value = _musicState.value.copy( + isCurrentlyPlaying = isPlaying + ) + } + + override fun onRepeatModeChanged(repeatMode: Int) { + super.onRepeatModeChanged(repeatMode) + when (repeatMode) { + Player.REPEAT_MODE_ONE -> { + _musicState.value = _musicState.value.copy( + isLooping = true + ) + } + + Player.REPEAT_MODE_OFF -> { + _musicState.value = _musicState.value.copy( + isLooping = false + ) + } + + + else -> { + _musicState.value = _musicState.value.copy( + isLooping = false + ) + } + } + } + + override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { + super.onShuffleModeEnabledChanged(shuffleModeEnabled) + _musicState.value = _musicState.value.copy( + isShuffling = shuffleModeEnabled + ) } @@ -78,31 +109,40 @@ class MusicViewModel ( super.onEvents(player, events) viewModelScope.launch { while (player.isPlaying) { - currentMusicDuration = player.duration - currentPosition = player.currentPosition - delay(550) + _musicState.value = _musicState.value.copy( + currentMusicDuration = player.duration, + currentPosition = player.currentPosition + ) + delay(500) } } } } - companion object { - const val CUTE_ERROR = "CuteError" - } - init { - controllerFuture.addListener( - { - Handler(Looper.getMainLooper()).post { - mediaController = controllerFuture.get() - mediaController!!.addListener(playerListener) - } - }, - MoreExecutors.directExecutor() - ) + MediaController + .Builder( + application, + SessionToken( + application, + ComponentName(application, PlaybackService::class.java) + ) + ) + .buildAsync() + .apply { + addListener( + { + mediaController = get() + mediaController!!.addListener(playerListener) + mediaController!!.setMediaItems(mediaStoreHelper.musics) + }, + MoreExecutors.directExecutor() + ) + } } + private fun loadLrcFile(path: String): File? { val lrcFilePath = path.replaceAfterLast('.', "lrc") val lrcFile = File(lrcFilePath) @@ -111,6 +151,8 @@ class MusicViewModel ( private fun parseLrcFile(file: File?): List { val lyrics = mutableListOf() + val regex = Regex("""\[(\d{2}):(\d{2})\.(\d{2})]""") + if (file == null) { return emptyList() } @@ -118,9 +160,7 @@ class MusicViewModel ( viewModelScope.launch { file.bufferedReader().useLines { lines -> lines.forEach { line -> - val regex = Regex("""\[(\d{2}):(\d{2})\.(\d{2})]""") val matchResult = regex.find(line) - if (matchResult != null) { val (minutes, seconds, hundredths) = matchResult.destructured val timeInMillis = @@ -155,77 +195,23 @@ class MusicViewModel ( } } } + override fun onCleared() { super.onCleared() mediaController!!.removeListener(playerListener) mediaController!!.release() } - - - fun getPlaybackSpeed() = mediaController!!.playbackParameters - private fun playAtIndex(mediaId: String) { - try { - mediaController!!.playAtIndex( - mediaId = mediaId - ) - } catch (e: Exception) { - Log.d(CUTE_ERROR, e.message.toString()) - } - } - - fun itemClicked( - mediaId: String, - musics1: List - ) { - - if (mediaController!!.mediaItemCount == 0) { - musics1.forEach { - mediaController!!.addMediaItem(it) + fun isPlayerReady(): Boolean { + return if (mediaController == null) false else + when (mediaController!!.playbackState) { + Player.STATE_IDLE -> false + Player.STATE_READY -> true + else -> true } - mediaController!!.prepare() - } - - playAtIndex(mediaId) - - } - - - fun isPlaylistEmptyAndDataNotNull(): Boolean { - return if (mediaController == null) false else mediaController!!.mediaItemCount != 0 - } - - - fun setPlaybackSpeed( - speed: Float = 1f, - pitch: Float = 1f - ) { - mediaController!!.playbackParameters = PlaybackParameters( - speed, - pitch - ) - } - - - - fun setLoop( - shouldLoop: Boolean - ) { - if (shouldLoop) { - mediaController!!.repeatMode = Player.REPEAT_MODE_ONE - isLooping = true - } else { - mediaController!!.repeatMode = Player.REPEAT_MODE_OFF - isLooping = false - } - } - - fun setShuffle(shouldShuffle: Boolean) { - mediaController!!.shuffleModeEnabled = shouldShuffle - isShuffling = mediaController!!.shuffleModeEnabled } fun quickPlay(uri: Uri?) { @@ -242,14 +228,42 @@ class MusicViewModel ( fun handlePlayerActions(action: PlayerActions) { when (action) { + is PlayerActions.RestartSong -> mediaController!!.seekTo(0) + is PlayerActions.PlayRandom -> mediaController!!.playRandom() + is PlayerActions.ApplyLoop -> mediaController!!.applyLoop() + is PlayerActions.ApplyShuffle -> mediaController!!.applyShuffle() is PlayerActions.PlayOrPause -> if (mediaController!!.isPlaying) mediaController!!.pause() else mediaController!!.play() is PlayerActions.SeekToNextMusic -> mediaController!!.seekToNextMediaItem() is PlayerActions.SeekToPreviousMusic -> mediaController!!.seekToPreviousMediaItem() is PlayerActions.SeekTo -> mediaController!!.seekTo(mediaController!!.currentPosition + action.position) is PlayerActions.SeekToSlider -> mediaController!!.seekTo(action.position) is PlayerActions.RewindTo -> mediaController!!.seekTo(mediaController!!.currentPosition - action.position) - is PlayerActions.RestartSong -> mediaController!!.seekTo(0) + is PlayerActions.ApplyPlaybackSpeed -> mediaController!!.applyPlaybackSpeed( + action.speed, + action.pitch + ) + + is PlayerActions.StartAlbumPlayback -> mediaController!!.playFromAlbum( + action.albumName, + action.mediaId, + mediaStoreHelper.musics + ) + + is PlayerActions.StartArtistPlayback -> mediaController!!.playFromArtist( + action.artistName, + action.mediaId, + mediaStoreHelper.musics + ) + + is PlayerActions.StartPlayback -> { + // If a user started album/artist playback, we need to make sure all songs are re-fed to Exoplayer when they want to play from anywhere else + if (mediaController!!.mediaItemCount != mediaStoreHelper.musics.size) { + mediaController!!.setMediaItems(mediaStoreHelper.musics) + mediaController!!.playAtIndex(action.mediaId) + } else { + mediaController!!.playAtIndex(action.mediaId) + } + } } } } - diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/PostViewModel.kt b/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/PostViewModel.kt index 65daeea..9bac6f2 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/PostViewModel.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/PostViewModel.kt @@ -26,23 +26,23 @@ class PostViewModel( ) : AndroidViewModel(application) { var musics by mutableStateOf( - mediaStoreHelper.getMusics() + mediaStoreHelper.musics ) var albums by mutableStateOf( - mediaStoreHelper.getAlbums() + mediaStoreHelper.albums ) var artists by mutableStateOf( - mediaStoreHelper.getArtists() + mediaStoreHelper.artists ) var folders by mutableStateOf( - mediaStoreHelper.getFoldersWithMusics() + mediaStoreHelper.fetchFoldersWithMusics() ) private val observer = MediaStoreObserver { - musics = mediaStoreHelper.getMusics() + musics = mediaStoreHelper.fetchMusics() } @@ -121,19 +121,21 @@ class PostViewModel( listToHandle: ListToHandle, sortingType: SortingType, ) { - when(listToHandle) { + when (listToHandle) { ListToHandle.TRACKS -> { musics = if (sortingType == SortingType.ASCENDING) musics.sortedBy { it.mediaMetadata.title.toString() } else musics.sortedByDescending { it.mediaMetadata.title.toString() } } + ListToHandle.ALBUMS -> { albums = if (sortingType == SortingType.ASCENDING) albums.sortedBy { it.name } else albums.sortedByDescending { it.name } } + ListToHandle.ARTISTS -> { artists = if (sortingType == SortingType.ASCENDING) artists.sortedBy { it.name } @@ -147,25 +149,27 @@ class PostViewModel( listToHandle: ListToHandle, query: String = "" ) { - when(listToHandle) { + when (listToHandle) { ListToHandle.TRACKS -> { - musics = mediaStoreHelper.getMusics().filter { + musics = mediaStoreHelper.musics.filter { it.mediaMetadata.title?.contains( other = query, ignoreCase = true ) == true } } + ListToHandle.ALBUMS -> { - albums = mediaStoreHelper.getAlbums().filter { + albums = mediaStoreHelper.albums.filter { it.name.contains( other = query, ignoreCase = true ) } } + ListToHandle.ARTISTS -> { - artists = mediaStoreHelper.getArtists().filter { + artists = mediaStoreHelper.artists.filter { it.name.contains( other = query, ignoreCase = true @@ -174,5 +178,5 @@ class PostViewModel( } } } - } +} diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/Searchbar.kt b/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/Searchbar.kt index 841328e..efb175d 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/Searchbar.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/Searchbar.kt @@ -2,14 +2,11 @@ package com.sosauce.cutemusic.ui.shared_components -import android.graphics.drawable.Animatable2 import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope import androidx.compose.animation.core.Animatable -import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.animateIntAsState import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.slideInVertically @@ -36,7 +33,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TextField import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment @@ -54,7 +50,7 @@ import kotlinx.coroutines.launch @Composable fun SharedTransitionScope.CuteSearchbar( modifier: Modifier = Modifier, - query: String, + query: String = "", onQueryChange: (String) -> Unit = {}, leadingIcon: @Composable (() -> Unit)? = null, trailingIcon: @Composable (() -> Unit)? = null, @@ -63,38 +59,9 @@ fun SharedTransitionScope.CuteSearchbar( onHandlePlayerActions: (PlayerActions) -> Unit, isPlaying: Boolean, animatedVisibilityScope: AnimatedVisibilityScope, - isPlaylistEmpty: Boolean, - onNavigate: () -> Unit -) = CustomSearchbar( - query = query, - onQueryChange = onQueryChange, - leadingIcon = leadingIcon, - trailingIcon = trailingIcon, - placeholder = placeholder, - modifier = modifier, - currentlyPlaying = currentlyPlaying, - onHandlePlayerActions = onHandlePlayerActions, - isPlaying = isPlaying, - animatedVisibilityScope = animatedVisibilityScope, - isPlaylistEmpty = isPlaylistEmpty, - onNavigate = onNavigate -) - - -@Composable -private fun SharedTransitionScope.CustomSearchbar( - query: String, - onQueryChange: (String) -> Unit, - leadingIcon: @Composable (() -> Unit)? = null, - trailingIcon: @Composable (() -> Unit)? = null, - placeholder: @Composable (() -> Unit)? = null, - modifier: Modifier, - currentlyPlaying: String, - onHandlePlayerActions: (PlayerActions) -> Unit, - isPlaying: Boolean, - animatedVisibilityScope: AnimatedVisibilityScope, - isPlaylistEmpty: Boolean, - onNavigate: () -> Unit + isPlayerReady: Boolean, + onNavigate: () -> Unit = {}, + showSearchField: Boolean = true, ) { val focusManager = LocalFocusManager.current val roundedShape = 24.dp @@ -112,21 +79,24 @@ private fun SharedTransitionScope.CustomSearchbar( shape = RoundedCornerShape(roundedShape) ) .thenIf( - isPlaylistEmpty, + isPlayerReady, Modifier.clickable { onNavigate() } ) ) { AnimatedVisibility( - visible = isPlaylistEmpty, - enter = fadeIn() + slideInVertically(initialOffsetY = { it }) + visible = isPlayerReady, + enter = fadeIn() + slideInVertically( + animationSpec = tween(500), + initialOffsetY = { it } + ) ) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .padding(start = 15.dp) - .fillMaxWidth() + .fillMaxWidth(), ) { Row( modifier = Modifier.weight(1f) @@ -139,7 +109,7 @@ private fun SharedTransitionScope.CustomSearchbar( state = rememberSharedContentState(key = "arrow"), animatedVisibilityScope = animatedVisibilityScope, boundsTransform = { _, _ -> - tween(durationMillis = 500) + tween(500) } ) ) @@ -151,7 +121,7 @@ private fun SharedTransitionScope.CustomSearchbar( state = rememberSharedContentState(key = "currentlyPlaying"), animatedVisibilityScope = animatedVisibilityScope, boundsTransform = { _, _ -> - tween(durationMillis = 500) + tween(500) } ) .basicMarquee() @@ -188,7 +158,7 @@ private fun SharedTransitionScope.CustomSearchbar( state = rememberSharedContentState(key = "skipPreviousButton"), animatedVisibilityScope = animatedVisibilityScope, boundsTransform = { _, _ -> - tween(durationMillis = 500) + tween(500) } ) ) @@ -203,7 +173,7 @@ private fun SharedTransitionScope.CustomSearchbar( state = rememberSharedContentState(key = "playPauseIcon"), animatedVisibilityScope = animatedVisibilityScope, boundsTransform = { _, _ -> - tween(durationMillis = 500) + tween(500) } ) ) @@ -234,41 +204,43 @@ private fun SharedTransitionScope.CustomSearchbar( ) } .sharedElement( - state = rememberSharedContentState(key = "skipNextButton"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(durationMillis = 500) - } - ) + state = rememberSharedContentState(key = "skipNextButton"), + animatedVisibilityScope = animatedVisibilityScope, + boundsTransform = { _, _ -> + tween(500) + } + ) ) } } } } - TextField( - value = query, - onValueChange = onQueryChange, - colors = TextFieldDefaults.colors( - unfocusedContainerColor = MaterialTheme.colorScheme.secondaryContainer.copy(0.5f), - focusedContainerColor = MaterialTheme.colorScheme.secondaryContainer.copy(0.5f), - disabledIndicatorColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent, - errorIndicatorColor = Color.Transparent - ), - shape = RoundedCornerShape(50.dp), - leadingIcon = leadingIcon, - trailingIcon = trailingIcon, - placeholder = placeholder, - singleLine = true, - modifier = Modifier - .fillMaxWidth() - .padding(6.dp), - keyboardActions = KeyboardActions( - onDone = { focusManager.clearFocus() } - ) + if (showSearchField) { + TextField( + value = query, + onValueChange = onQueryChange, + colors = TextFieldDefaults.colors( + unfocusedContainerColor = MaterialTheme.colorScheme.secondaryContainer.copy(0.5f), + focusedContainerColor = MaterialTheme.colorScheme.secondaryContainer.copy(0.5f), + disabledIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + errorIndicatorColor = Color.Transparent + ), + shape = RoundedCornerShape(50.dp), + leadingIcon = leadingIcon, + trailingIcon = trailingIcon, + placeholder = placeholder, + singleLine = true, + modifier = Modifier + .fillMaxWidth() + .padding(6.dp), + keyboardActions = KeyboardActions( + onDone = { focusManager.clearFocus() } + ) - ) + ) + } } } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/theme/Color.kt b/app/src/main/java/com/sosauce/cutemusic/ui/theme/Color.kt index 2644850..27ef94f 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/theme/Color.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/theme/Color.kt @@ -58,10 +58,6 @@ val md_theme_dark_outline = Color(0xFF9C8D93) val md_theme_dark_inverseOnSurface = Color(0xFF1F1A1C) val md_theme_dark_inverseSurface = Color(0xFFEBE0E2) val md_theme_dark_inversePrimary = Color(0xFFB0137F) -val md_theme_dark_shadow = Color(0xFF000000) val md_theme_dark_surfaceTint = Color(0xFFFFAFD7) val md_theme_dark_outlineVariant = Color(0xFF504349) -val md_theme_dark_scrim = Color(0xFF000000) - - -val seed = Color(0xFFF555B8) \ No newline at end of file +val md_theme_dark_scrim = Color(0xFF000000) \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/theme/ColorEngineViewModel.kt b/app/src/main/java/com/sosauce/cutemusic/ui/theme/ColorEngineViewModel.kt index 92937f5..7ab3d03 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/theme/ColorEngineViewModel.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/theme/ColorEngineViewModel.kt @@ -18,7 +18,7 @@ class ColorEngineViewModel : ViewModel() { fun generateNewPalette( uri: Uri, context: Context - ) : ImageBitmap?{ + ): ImageBitmap? { var imageBitmap by mutableStateOf(null) viewModelScope.launch(Dispatchers.IO) { diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/theme/Theme.kt b/app/src/main/java/com/sosauce/cutemusic/ui/theme/Theme.kt index e6be16c..757ea1a 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/theme/Theme.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/theme/Theme.kt @@ -1,5 +1,3 @@ -@file:Suppress("PrivatePropertyName") - package com.sosauce.cutemusic.ui.theme import android.os.Build diff --git a/app/src/main/java/com/sosauce/cutemusic/utils/Customs.kt b/app/src/main/java/com/sosauce/cutemusic/utils/Customs.kt index a5fd840..8710c9b 100644 --- a/app/src/main/java/com/sosauce/cutemusic/utils/Customs.kt +++ b/app/src/main/java/com/sosauce/cutemusic/utils/Customs.kt @@ -5,9 +5,6 @@ import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material3.IconButtonColors -import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment diff --git a/app/src/main/java/com/sosauce/cutemusic/utils/Extensions.kt b/app/src/main/java/com/sosauce/cutemusic/utils/Extensions.kt index 1f93ae8..7c244b7 100644 --- a/app/src/main/java/com/sosauce/cutemusic/utils/Extensions.kt +++ b/app/src/main/java/com/sosauce/cutemusic/utils/Extensions.kt @@ -1,12 +1,20 @@ package com.sosauce.cutemusic.utils +import android.content.Context +import android.content.Intent +import android.media.MediaMetadataRetriever +import android.net.Uri import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.media3.common.MediaItem +import androidx.media3.common.PlaybackParameters +import androidx.media3.common.Player import com.sosauce.cutemusic.data.datastore.rememberIsLandscape +import java.util.Locale fun Modifier.thenIf( condition: Boolean, @@ -19,11 +27,132 @@ fun Modifier.thenIf( ) } -// How bad of a programmer am I for all the below functions +fun Long.formatBinarySize(): String { + val kiloByteAsByte = 1.0 * 1024.0 + val megaByteAsByte = 1.0 * 1024.0 * 1024.0 + val gigaByteAsByte = 1.0 * 1024.0 * 1024.0 * 1024.0 + return when { + this < kiloByteAsByte -> "${this.toDouble()} B" + this >= kiloByteAsByte && this < megaByteAsByte -> "${ + String.format( + Locale.getDefault(), + "%.2f", + (this / kiloByteAsByte) + ) + } KB" + + this >= megaByteAsByte && this < gigaByteAsByte -> "${ + String.format( + Locale.getDefault(), + "%.2f", + (this / megaByteAsByte) + ) + } MB" + + else -> "Too Big!" + } +} + +fun Context.restart() { + val intent = packageManager.getLaunchIntentForPackage(packageName)!! + val componentName = intent.component!! + val restartIntent = Intent.makeRestartActivityTask(componentName) + startActivity(restartIntent) + Runtime.getRuntime().exit(0) +} + +fun Player.playAtIndex( + mediaId: String +) { + val index = (0 until mediaItemCount).indexOfFirst { getMediaItemAt(it).mediaId == mediaId } + index.takeIf { it != -1 }?.let { + seekTo(it, 0) + play() + } +} + +fun Player.playRandom() { + val range = 0..mediaItemCount + seekTo(range.random(), 0) + play() +} + +fun Player.playFromAlbum( + albumName: String, + mediaId: String? = null, + musics: List +) { + clearMediaItems() + musics.forEach { + if (it.mediaMetadata.albumTitle.toString() == albumName) { + addMediaItem(it) + } + } + + if (mediaId == null) { + playRandom() + } else { + playAtIndex(mediaId) + } +} + +fun Player.playFromArtist( + artistsName: String, + mediaId: String? = null, + musics: List +) { + clearMediaItems() + musics.forEach { + if (it.mediaMetadata.artist.toString() == artistsName) { + addMediaItem(it) + } + } + + if (mediaId == null) { + playRandom() + } else { + playAtIndex(mediaId) + } +} + +fun Player.applyLoop() { + repeatMode = when (repeatMode) { + Player.REPEAT_MODE_OFF -> Player.REPEAT_MODE_ONE + Player.REPEAT_MODE_ONE -> Player.REPEAT_MODE_OFF + else -> Player.REPEAT_MODE_OFF + } +} + +fun Player.applyShuffle() { + shuffleModeEnabled = !shuffleModeEnabled +} + +fun Player.applyPlaybackSpeed( + speed: Float = 1f, + pitch: Float = 1f, +) { + playbackParameters = PlaybackParameters( + speed, + pitch + ) +} + +fun Uri.getBitrate(context: Context): String { + val retriever = MediaMetadataRetriever() + return try { + retriever.setDataSource(context, this) + val bitrate = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_BITRATE) + bitrate?.toInt()?.div(1000)?.toString()?.plus(" kbps") ?: "Unknown" + } catch (e: Exception) { + "Error parsing bitrate!" + } finally { + retriever.release() + } +} @Composable fun rememberSearchbarAlignment( -) : Alignment { +): Alignment { val isLandscape = rememberIsLandscape() @@ -38,7 +167,7 @@ fun rememberSearchbarAlignment( @Composable fun rememberSearchbarMaxFloatValue( -) : Float { +): Float { val isLandscape = rememberIsLandscape() @@ -53,7 +182,7 @@ fun rememberSearchbarMaxFloatValue( @Composable fun rememberSearchbarRightPadding( -) : Dp { +): Dp { val isLandscape = rememberIsLandscape() diff --git a/app/src/main/java/com/sosauce/cutemusic/utils/ImageUtils.kt b/app/src/main/java/com/sosauce/cutemusic/utils/ImageUtils.kt index 270634b..aa8f34b 100644 --- a/app/src/main/java/com/sosauce/cutemusic/utils/ImageUtils.kt +++ b/app/src/main/java/com/sosauce/cutemusic/utils/ImageUtils.kt @@ -11,13 +11,15 @@ import java.io.FileNotFoundException object ImageUtils { fun imageRequester(img: Any?, context: Context): ImageRequest { - return ImageRequest.Builder(context) + val request = ImageRequest.Builder(context) .data(img) .crossfade(true) .transformations( RoundedCornersTransformation(15f) ) .build() + + return request } fun getAlbumArt(albumId: Long): Any? { diff --git a/app/src/main/res/drawable/round_music_note_24.xml b/app/src/main/res/drawable/round_music_note_24.xml new file mode 100644 index 0000000..c1fd5db --- /dev/null +++ b/app/src/main/res/drawable/round_music_note_24.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5b29d33..800e81f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -58,4 +58,6 @@ Success Error while saving changes. Editor + Details + Use Classic Slider \ No newline at end of file diff --git a/app/src/main/res/xml/automotive_app_desc.xml b/app/src/main/res/xml/automotive_app_desc.xml new file mode 100644 index 0000000..0a6a3c9 --- /dev/null +++ b/app/src/main/res/xml/automotive_app_desc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/release/generated/baselineProfiles/baseline-prof.txt b/app/src/release/generated/baselineProfiles/baseline-prof.txt index 32a07ce..a6ea94d 100644 --- a/app/src/release/generated/baselineProfiles/baseline-prof.txt +++ b/app/src/release/generated/baselineProfiles/baseline-prof.txt @@ -16552,10 +16552,10 @@ HSPLcom/sosauce/cutemusic/domain/blacklist/BlacklistedDatabase_Impl$1;->onOpen(L Lcom/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl; HSPLcom/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl;->()V HSPLcom/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl;->(Landroid/content/Context;)V -HSPLcom/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl;->getAlbums()Ljava/util/List; -HSPLcom/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl;->getArtists()Ljava/util/List; -HSPLcom/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl;->getFoldersWithMusics()Ljava/util/List; -HSPLcom/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl;->getMusics()Ljava/util/List; +HSPLcom/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl;->fetchAlbums()Ljava/util/List; +HSPLcom/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl;->fetchArtists()Ljava/util/List; +HSPLcom/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl;->fetchFoldersWithMusics()Ljava/util/List; +HSPLcom/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl;->fetchMusics()Ljava/util/List; Lcom/sosauce/cutemusic/main/App; HSPLcom/sosauce/cutemusic/main/App;->$r8$lambda$xrutaltFOQbSx_O2rxWYEJHPxKQ(Lcom/sosauce/cutemusic/main/App;Lorg/koin/core/KoinApplication;)Lkotlin/Unit; HSPLcom/sosauce/cutemusic/main/App;->()V @@ -16878,7 +16878,7 @@ Lcom/sosauce/cutemusic/ui/shared_components/PostViewModel; HSPLcom/sosauce/cutemusic/ui/shared_components/PostViewModel;->()V HSPLcom/sosauce/cutemusic/ui/shared_components/PostViewModel;->(Lcom/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl;Lcom/sosauce/cutemusic/domain/blacklist/BlackDao;)V HSPLcom/sosauce/cutemusic/ui/shared_components/PostViewModel;->access$getMediaStoreHelperImpl$p(Lcom/sosauce/cutemusic/ui/shared_components/PostViewModel;)Lcom/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl; -HSPLcom/sosauce/cutemusic/ui/shared_components/PostViewModel;->getMusics()Ljava/util/List; +HSPLcom/sosauce/cutemusic/ui/shared_components/PostViewModel;->fetchMusics()Ljava/util/List; HSPLcom/sosauce/cutemusic/ui/shared_components/PostViewModel;->getState()Lkotlinx/coroutines/flow/StateFlow; HSPLcom/sosauce/cutemusic/ui/shared_components/PostViewModel;->setMusics(Ljava/util/List;)V Lcom/sosauce/cutemusic/ui/shared_components/PostViewModel$1; diff --git a/app/src/release/generated/baselineProfiles/startup-prof.txt b/app/src/release/generated/baselineProfiles/startup-prof.txt index 32a07ce..a6ea94d 100644 --- a/app/src/release/generated/baselineProfiles/startup-prof.txt +++ b/app/src/release/generated/baselineProfiles/startup-prof.txt @@ -16552,10 +16552,10 @@ HSPLcom/sosauce/cutemusic/domain/blacklist/BlacklistedDatabase_Impl$1;->onOpen(L Lcom/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl; HSPLcom/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl;->()V HSPLcom/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl;->(Landroid/content/Context;)V -HSPLcom/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl;->getAlbums()Ljava/util/List; -HSPLcom/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl;->getArtists()Ljava/util/List; -HSPLcom/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl;->getFoldersWithMusics()Ljava/util/List; -HSPLcom/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl;->getMusics()Ljava/util/List; +HSPLcom/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl;->fetchAlbums()Ljava/util/List; +HSPLcom/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl;->fetchArtists()Ljava/util/List; +HSPLcom/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl;->fetchFoldersWithMusics()Ljava/util/List; +HSPLcom/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl;->fetchMusics()Ljava/util/List; Lcom/sosauce/cutemusic/main/App; HSPLcom/sosauce/cutemusic/main/App;->$r8$lambda$xrutaltFOQbSx_O2rxWYEJHPxKQ(Lcom/sosauce/cutemusic/main/App;Lorg/koin/core/KoinApplication;)Lkotlin/Unit; HSPLcom/sosauce/cutemusic/main/App;->()V @@ -16878,7 +16878,7 @@ Lcom/sosauce/cutemusic/ui/shared_components/PostViewModel; HSPLcom/sosauce/cutemusic/ui/shared_components/PostViewModel;->()V HSPLcom/sosauce/cutemusic/ui/shared_components/PostViewModel;->(Lcom/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl;Lcom/sosauce/cutemusic/domain/blacklist/BlackDao;)V HSPLcom/sosauce/cutemusic/ui/shared_components/PostViewModel;->access$getMediaStoreHelperImpl$p(Lcom/sosauce/cutemusic/ui/shared_components/PostViewModel;)Lcom/sosauce/cutemusic/domain/repository/MediaStoreHelperImpl; -HSPLcom/sosauce/cutemusic/ui/shared_components/PostViewModel;->getMusics()Ljava/util/List; +HSPLcom/sosauce/cutemusic/ui/shared_components/PostViewModel;->fetchMusics()Ljava/util/List; HSPLcom/sosauce/cutemusic/ui/shared_components/PostViewModel;->getState()Lkotlinx/coroutines/flow/StateFlow; HSPLcom/sosauce/cutemusic/ui/shared_components/PostViewModel;->setMusics(Ljava/util/List;)V Lcom/sosauce/cutemusic/ui/shared_components/PostViewModel$1; diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 67fa09d..0484d7b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -agp = "8.6.1" +agp = "8.7.1" jaudiotagger = "3.0.1" koinAndroid = "4.0.0" koinAndroidxCompose = "4.0.0" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 0f8a7a1..856c82a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Sat Jan 13 19:30:48 CET 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists