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 9d41e42..135c032 100644 Binary files a/app/release/baselineProfiles/0/app-release.dm and b/app/release/baselineProfiles/0/app-release.dm differ diff --git a/app/release/baselineProfiles/1/app-release.dm b/app/release/baselineProfiles/1/app-release.dm index 332f30c..83c4505 100644 Binary files a/app/release/baselineProfiles/1/app-release.dm and b/app/release/baselineProfiles/1/app-release.dm differ 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