diff --git a/app/.DS_Store b/app/.DS_Store index 693cf22..b435c59 100644 Binary files a/app/.DS_Store and b/app/.DS_Store differ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ce6b446..fc7ad66 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -15,8 +15,8 @@ android { applicationId = "com.sosauce.cutemusic" minSdk = 26 targetSdk = 35 - versionCode = 20 - versionName = "2.3.3" + versionCode = 21 + versionName = "2.3.4" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" vectorDrawables { useSupportLibrary = true @@ -54,14 +54,14 @@ android { } -// splits { -// abi { -// isEnable = true -// reset() -// include("armeabi-v7a", "arm64-v8a") -// isUniversalApk = false -// } -// } + splits { + abi { + isEnable = true + reset() + include("armeabi-v7a", "arm64-v8a") + isUniversalApk = true + } + } } dependencies { diff --git a/app/release/baselineProfiles/0/app-release.dm b/app/release/baselineProfiles/0/app-release.dm index 0742d85..a47f464 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 05d5a95..c46f920 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 cb11106..a2adf5f 100644 --- a/app/release/output-metadata.json +++ b/app/release/output-metadata.json @@ -11,8 +11,8 @@ "type": "SINGLE", "filters": [], "attributes": [], - "versionCode": 19, - "versionName": "2.3.2", + "versionCode": 20, + "versionName": "2.3.3", "outputFile": "app-release.apk" } ], diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index ea3033c..5248757 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,6 +19,7 @@ + + + + + + + + \ No newline at end of file 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 7c6de4a..41b6306 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 @@ -1,8 +1,10 @@ package com.sosauce.cutemusic.data.datastore import android.content.Context +import android.util.Log import androidx.compose.runtime.Composable import androidx.datastore.core.DataStore +import androidx.datastore.core.IOException import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.stringSetPreferencesKey @@ -11,6 +13,7 @@ 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.SAF_TRACKS import com.sosauce.cutemusic.data.datastore.PreferencesKeys.SHOW_ALBUMS_TAB import com.sosauce.cutemusic.data.datastore.PreferencesKeys.SHOW_ARTISTS_TAB import com.sosauce.cutemusic.data.datastore.PreferencesKeys.SHOW_FOLDERS_TAB @@ -21,7 +24,10 @@ 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 +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map private const val PREFERENCES_NAME = "settings" @@ -43,6 +49,7 @@ private data object PreferencesKeys { val SHOW_ALBUMS_TAB = booleanPreferencesKey("show_albums_tab") val SHOW_ARTISTS_TAB = booleanPreferencesKey("show_artists_tab") val SHOW_FOLDERS_TAB = booleanPreferencesKey("show_folders_tab") + val SAF_TRACKS = stringSetPreferencesKey("saf_tracks") } @Composable @@ -103,9 +110,25 @@ fun rememberShowArtistsTab() = fun rememberShowFoldersTab() = rememberPreference(key = SHOW_FOLDERS_TAB, defaultValue = true) +@Composable +fun rememberAllSafTracks() = + rememberPreference(key = SAF_TRACKS, defaultValue = emptySet()) + suspend fun getBlacklistedFolder(context: Context): Set { val preferences = context.dataStore.data.first() return preferences[BLACKLISTED_FOLDERS] ?: emptySet() } +fun getSafTracks(context: Context): Flow> = + + context.dataStore.data + .catch { exception -> + if (exception is IOException) { + Log.d("CuteError", "getSafTracks: ${exception.message}") + } else throw exception + } + .map { preference -> + preference[SAF_TRACKS] ?: emptySet() + } + 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 4656c8d..f398c81 100644 --- a/app/src/main/java/com/sosauce/cutemusic/di/AppModule.kt +++ b/app/src/main/java/com/sosauce/cutemusic/di/AppModule.kt @@ -2,6 +2,7 @@ package com.sosauce.cutemusic.di import com.sosauce.cutemusic.domain.repository.MediaStoreHelper import com.sosauce.cutemusic.domain.repository.MediaStoreHelperImpl +import com.sosauce.cutemusic.domain.repository.SafManager import com.sosauce.cutemusic.ui.screens.metadata.MetadataViewModel import com.sosauce.cutemusic.ui.shared_components.MusicViewModel import com.sosauce.cutemusic.ui.shared_components.PostViewModel @@ -14,11 +15,16 @@ val appModule = module { single { MediaStoreHelperImpl(androidContext()) } + + single { + SafManager(androidContext()) + } + viewModel { - PostViewModel(get()) + PostViewModel(get(), get()) } viewModel { - MusicViewModel(androidApplication(), get()) + MusicViewModel(androidApplication(), get(), 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 dbf2943..b5ad9d5 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 @@ -7,7 +7,7 @@ import androidx.media3.common.MediaItem import com.sosauce.cutemusic.domain.model.Album import com.sosauce.cutemusic.domain.model.Artist import com.sosauce.cutemusic.domain.model.Folder -import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow interface MediaStoreHelper { @@ -17,10 +17,10 @@ interface MediaStoreHelper { val folders: List fun fetchMusics(): List - fun fetchLatestMusics(): Flow> + fun fetchLatestMusics(): StateFlow> fun fetchAlbums(): List - fun fetchLatestAlbums(): Flow> + fun fetchLatestAlbums(): StateFlow> fun fetchArtists(): 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 eae9bfb..d634638 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 @@ -21,9 +21,13 @@ import com.sosauce.cutemusic.domain.model.Album import com.sosauce.cutemusic.domain.model.Artist import com.sosauce.cutemusic.domain.model.Folder import com.sosauce.cutemusic.utils.observe +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.runBlocking @SuppressLint("UnsafeOptInUsageError") @@ -31,26 +35,41 @@ class MediaStoreHelperImpl( private val context: Context ) : MediaStoreHelper { + private fun getBlacklistedFoldersAsync(): Set = runBlocking { getBlacklistedFolder(context) } + private val blacklistedFolders = getBlacklistedFoldersAsync() private val selection = blacklistedFolders.joinToString(" AND ") { "${MediaStore.Audio.Media.DATA} NOT LIKE ?" } private val selectionArgs = blacklistedFolders.map { "$it%" }.toTypedArray() - override fun fetchLatestMusics(): Flow> = + override fun fetchLatestMusics(): StateFlow> = context.contentResolver.observe(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI) - .map { fetchMusics() } + .map { + fetchMusics() + } + .stateIn( + CoroutineScope(Dispatchers.IO), + SharingStarted.WhileSubscribed(5000), + listOf() + ) - override fun fetchLatestAlbums(): Flow> = + override fun fetchLatestAlbums(): StateFlow> = context.contentResolver.observe(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI) .map { fetchAlbums() } + .stateIn( + CoroutineScope(Dispatchers.IO), + SharingStarted.WhileSubscribed(5000), + listOf() + ) @UnstableApi override fun fetchMusics(): List { + val musics = mutableListOf() val projection = arrayOf( @@ -62,7 +81,8 @@ class MediaStoreHelperImpl( MediaStore.Audio.Media.ALBUM_ID, MediaStore.Audio.Media.DATA, MediaStore.Audio.Media.SIZE, - MediaStore.Audio.Media.DURATION + MediaStore.Audio.Media.DURATION, + MediaStore.Audio.Media.TRACK, ) @@ -82,6 +102,7 @@ class MediaStoreHelperImpl( val folderColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DATA) val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE) val durationColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DURATION) + val trackNbColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.TRACK) //val isFavColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.IS_FAVORITE) while (cursor.moveToNext()) { @@ -95,6 +116,7 @@ class MediaStoreHelperImpl( val folder = filePath.substring(0, filePath.lastIndexOf('/')) val size = cursor.getLong(sizeColumn) val duration = cursor.getLong(durationColumn) + val trackNumber = cursor.getInt(trackNbColumn) //val isFavorite = cursor.getInt(isFavColumn) // 1 = is favorite, 0 = no val uri = ContentUris.withAppendedId( MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, @@ -119,6 +141,7 @@ class MediaStoreHelperImpl( .setAlbumTitle(album) .setArtworkUri(artUri) .setDurationMs(duration) + .setTrackNumber(trackNumber) .setExtras( Bundle() .apply { @@ -128,6 +151,7 @@ class MediaStoreHelperImpl( putString("uri", uri.toString()) putLong("album_id", albumId) putLong("artist_id", artistId) + putBoolean("is_saf", false) // putInt("isFavorite", isFavorite) }).build() ) diff --git a/app/src/main/java/com/sosauce/cutemusic/domain/repository/SafManager.kt b/app/src/main/java/com/sosauce/cutemusic/domain/repository/SafManager.kt new file mode 100644 index 0000000..2de943f --- /dev/null +++ b/app/src/main/java/com/sosauce/cutemusic/domain/repository/SafManager.kt @@ -0,0 +1,124 @@ +package com.sosauce.cutemusic.domain.repository + +import android.content.Context +import android.media.MediaMetadataRetriever +import android.net.Uri +import android.os.Bundle +import android.util.Log +import androidx.core.net.toUri +import androidx.media3.common.MediaItem +import androidx.media3.common.MediaMetadata +import androidx.media3.common.util.UnstableApi +import com.sosauce.cutemusic.data.datastore.getSafTracks +import com.sosauce.cutemusic.utils.getUriFromByteArray +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext +import java.util.UUID + +class SafManager( + private val context: Context +) { + + + @UnstableApi + fun fetchLatestSafTracks(): StateFlow> = getSafTracks(context) + .map { tracks -> + tracks.map { uri -> + uriToMediaItem(uri.toUri()) + } + } + .stateIn( + CoroutineScope(Dispatchers.IO), + SharingStarted.WhileSubscribed(5000), + listOf() + ) + + + @UnstableApi + private suspend fun uriToMediaItem(uri: Uri): MediaItem = withContext(Dispatchers.IO) { + + val retriever = MediaMetadataRetriever() + + try { + retriever.setDataSource(context, uri) + + val id = uri.hashCode() + val title = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE) + val artist = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ARTIST) + val album = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_ALBUM) + val size = + context.contentResolver.openAssetFileDescriptor(uri, "r")?.use { it.length } ?: 0 + val duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) + val artUri = retriever.embeddedPicture?.getUriFromByteArray(context) + + return@withContext MediaItem + .Builder() + .setUri(uri) + .setMediaId(id.toString()) + .setMediaMetadata( + MediaMetadata + .Builder() + .setIsBrowsable(false) + .setIsPlayable(true) + .setTitle(title) + .setArtist(artist) + .setAlbumTitle(album) + .setArtworkUri(artUri) + .setDurationMs(duration?.toLong() ?: 0) + .setExtras( + Bundle() + .apply { + putString("folder", "SAF") + putLong("size", size) + putString("path", "${uri.path}") + putString("uri", uri.toString()) + putLong("album_id", 0) + putLong("artist_id", 0) + putBoolean("is_saf", true) + // putInt("isFavorite", isFavorite) + }).build() + ) + .build() + + } catch (e: Exception) { + Log.d("FAILED_SAF", "uriToMediaItem: ${e.stackTrace} ${e.message}") + } finally { + retriever.release() + } + + return@withContext MediaItem + .Builder() + .setUri(uri) + .setMediaId(UUID.randomUUID().toString()) + .setMediaMetadata( + MediaMetadata + .Builder() + .setIsBrowsable(false) + .setIsPlayable(true) + .setTitle("No title") + .setArtist("No artist") + .setAlbumTitle("No album") + .setArtworkUri(Uri.EMPTY) + .setDurationMs(0) + .setExtras( + Bundle() + .apply { + putString("folder", "SAF") + putLong("size", 0) + putString("path", "${uri.path}") + putString("uri", uri.toString()) + putLong("album_id", 0) + putLong("artist_id", 0) + putBoolean("is_saf", true) + // putInt("isFavorite", isFavorite) + }).build() + ) + .build() + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/main/AutoPlaybackService.kt b/app/src/main/java/com/sosauce/cutemusic/main/AutoPlaybackService.kt new file mode 100644 index 0000000..1ce2d97 --- /dev/null +++ b/app/src/main/java/com/sosauce/cutemusic/main/AutoPlaybackService.kt @@ -0,0 +1,70 @@ +package com.sosauce.cutemusic.main + +import android.content.Intent +import android.media.MediaDescription +import android.media.browse.MediaBrowser +import android.net.Uri +import android.os.Bundle +import android.service.media.MediaBrowserService +import androidx.media3.common.util.UnstableApi +import com.sosauce.cutemusic.domain.repository.MediaStoreHelperImpl +import com.sosauce.cutemusic.utils.ROOT_ID +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +@UnstableApi +class AutoPlaybackService : MediaBrowserService() { + + + val mediaStoreHelper by lazy { MediaStoreHelperImpl(this) } + private val job = SupervisorJob() + private val scope = CoroutineScope(Dispatchers.IO + job) + + + override fun onGetRoot( + clientPackageName: String, + clientUid: Int, + rootHints: Bundle? + ): BrowserRoot? = BrowserRoot(ROOT_ID, null) + + override fun onLoadChildren( + parentId: String, + result: Result?> + ) { + + val mediaItems: MutableList = mutableListOf() + + if (ROOT_ID == parentId) { + scope.launch { + mediaStoreHelper.fetchLatestMusics().collectLatest { list -> + list.forEach { mediaItem -> + mediaItems.add( + MediaBrowser.MediaItem( + MediaDescription.Builder() + .setMediaId(mediaItem.mediaId) + .setTitle(mediaItem.mediaMetadata.title ?: "No title") + .setIconUri(mediaItem.mediaMetadata.artworkUri ?: Uri.EMPTY) + .build(), + MediaBrowser.MediaItem.FLAG_PLAYABLE + ) + ) + } + } + result.sendResult(mediaItems) + } + } else result.sendResult(listOf()) + } + + override fun onDestroy() { + super.onDestroy() + job.cancel() + } + + override fun onTaskRemoved(rootIntent: Intent?) { + super.onTaskRemoved(rootIntent) + job.cancel() + } +} \ No newline at end of file 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 574dd61..1c68172 100644 --- a/app/src/main/java/com/sosauce/cutemusic/main/PlaybackService.kt +++ b/app/src/main/java/com/sosauce/cutemusic/main/PlaybackService.kt @@ -13,6 +13,7 @@ import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaLibraryService.MediaLibrarySession import androidx.media3.session.MediaSession import com.sosauce.cutemusic.R +import com.sosauce.cutemusic.utils.CUTE_MUSIC_ID class PlaybackService : MediaLibraryService(), @@ -45,6 +46,7 @@ class PlaybackService : MediaLibraryService(), .build() mediaLibrarySession = MediaLibrarySession .Builder(this, player, this) + .setId(CUTE_MUSIC_ID) .setShowPlayButtonIfPlaybackIsSuppressed(false) // .setBitmapLoader(object : BitmapLoader { 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 2122a1d..7a74dc8 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 @@ -23,6 +23,7 @@ import com.sosauce.cutemusic.ui.screens.main.MainScreen import com.sosauce.cutemusic.ui.screens.metadata.MetadataEditor import com.sosauce.cutemusic.ui.screens.metadata.MetadataViewModel import com.sosauce.cutemusic.ui.screens.playing.NowPlayingScreen +import com.sosauce.cutemusic.ui.screens.saf.SafScreen import com.sosauce.cutemusic.ui.screens.settings.SettingsScreen import com.sosauce.cutemusic.ui.shared_components.MusicViewModel import com.sosauce.cutemusic.ui.shared_components.PostViewModel @@ -222,6 +223,19 @@ fun Nav() { animatedVisibilityScope = this, ) } + + composable { + val latestSafTracks by postViewModel.safTracks.collectAsStateWithLifecycle() + + SafScreen( + onNavigateUp = navController::navigateUp, + latestSafTracks = latestSafTracks, + onNavigate = { navController.navigate(it) }, + onShortClick = { viewModel.handlePlayerActions(PlayerActions.StartPlayback(it)) }, + isPlayerReady = musicState.isPlayerReady, + currentMusicUri = musicState.currentMusicUri, + ) + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/navigation/Screen.kt b/app/src/main/java/com/sosauce/cutemusic/ui/navigation/Screen.kt index a4c083d..597466e 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/navigation/Screen.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/navigation/Screen.kt @@ -25,6 +25,9 @@ sealed class Screen { @Serializable data object AllFolders : Screen() + @Serializable + data object Saf : Screen() + @Serializable data class AlbumsDetails( val id: Long 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 b999915..604e2e7 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 @@ -144,7 +144,11 @@ private fun SharedTransitionScope.AlbumDetailsContent( contentDescription = stringResource(R.string.artwork), modifier = Modifier .size(150.dp) - .clip(RoundedCornerShape(12.dp)), + .sharedElement( + state = rememberSharedContentState(key = album.id), + animatedVisibilityScope = animatedVisibilityScope, + ) + .clip(RoundedCornerShape(24.dp)), contentScale = ContentScale.Crop ) Spacer(Modifier.width(10.dp)) @@ -154,13 +158,23 @@ private fun SharedTransitionScope.AlbumDetailsContent( CuteText( text = album.name, fontSize = 22.sp, - modifier = Modifier.basicMarquee() + modifier = Modifier + .sharedElement( + state = rememberSharedContentState(key = album.name + album.id), + animatedVisibilityScope = animatedVisibilityScope, + ) + .basicMarquee() ) CuteText( text = album.artist, fontSize = 22.sp, color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.85f), - modifier = Modifier.basicMarquee() + modifier = Modifier + .sharedElement( + state = rememberSharedContentState(key = album.artist + album.id), + animatedVisibilityScope = animatedVisibilityScope, + ) + .basicMarquee() ) Spacer(Modifier.height(60.dp)) CuteText( @@ -172,21 +186,26 @@ private fun SharedTransitionScope.AlbumDetailsContent( } Spacer(Modifier.height(10.dp)) Column { - albumSongs.forEach { music -> - MusicListItem( - music = music, - onShortClick = { - viewModel.handlePlayerActions( - PlayerActions.StartAlbumPlayback( - albumName = music.mediaMetadata.albumTitle.toString(), - mediaId = it + albumSongs.sortedWith(compareBy( + { it.mediaMetadata.trackNumber == null || it.mediaMetadata.trackNumber == 0 }, + { it.mediaMetadata.trackNumber } + )) + .forEach { music -> + MusicListItem( + music = music, + onShortClick = { + viewModel.handlePlayerActions( + PlayerActions.StartAlbumPlayback( + albumName = music.mediaMetadata.albumTitle.toString(), + mediaId = it + ) ) - ) - }, - currentMusicUri = musicState.currentMusicUri, - isPlayerReady = musicState.isPlayerReady - ) - } + }, + currentMusicUri = musicState.currentMusicUri, + isPlayerReady = musicState.isPlayerReady, + showTrackNumber = true + ) + } } } } 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 c3a1dd2..e3b8dcb 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 @@ -142,9 +142,11 @@ fun SharedTransitionScope.AlbumsScreen( .thenIf( if (isLandscape) index == 0 || index == 1 || index == 2 || index == 3 - else index == 0 || index == 1, + else index == 0 || index == 1 + ) { Modifier.statusBarsPadding() - ) + }, + animatedVisibilityScope = animatedVisibilityScope ) } } @@ -223,9 +225,10 @@ fun SharedTransitionScope.AlbumsScreen( @Composable -fun AlbumCard( +fun SharedTransitionScope.AlbumCard( album: Album, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, + animatedVisibilityScope: AnimatedVisibilityScope, ) { val context = LocalContext.current @@ -241,6 +244,10 @@ fun AlbumCard( contentDescription = stringResource(id = R.string.artwork), modifier = Modifier .size(160.dp) + .sharedElement( + state = rememberSharedContentState(key = album.id), + animatedVisibilityScope = animatedVisibilityScope, + ) .clip(RoundedCornerShape(24.dp)), contentScale = ContentScale.Crop ) @@ -249,12 +256,22 @@ fun AlbumCard( CuteText( text = album.name, maxLines = 1, - modifier = Modifier.basicMarquee() + modifier = Modifier + .sharedElement( + state = rememberSharedContentState(key = album.name + album.id), + animatedVisibilityScope = animatedVisibilityScope, + ) + .basicMarquee() ) CuteText( text = album.artist, color = MaterialTheme.colorScheme.onBackground.copy(0.85f), - modifier = Modifier.basicMarquee() + modifier = Modifier + .sharedElement( + state = rememberSharedContentState(key = album.artist + album.id), + animatedVisibilityScope = animatedVisibilityScope, + ) + .basicMarquee() ) } } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/all_folders/AllFoldersScreen.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/all_folders/AllFoldersScreen.kt index 5208adb..49054bb 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/all_folders/AllFoldersScreen.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/all_folders/AllFoldersScreen.kt @@ -16,7 +16,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth 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.layout.width import androidx.compose.foundation.lazy.LazyColumn @@ -44,9 +43,8 @@ import androidx.compose.ui.unit.dp import androidx.media3.common.MediaItem import com.sosauce.cutemusic.R import com.sosauce.cutemusic.data.actions.PlayerActions -import com.sosauce.cutemusic.domain.model.Folder import com.sosauce.cutemusic.ui.navigation.Screen -import com.sosauce.cutemusic.ui.screens.blacklisted.components.FolderItem +import com.sosauce.cutemusic.ui.screens.blacklisted.FolderItem import com.sosauce.cutemusic.ui.screens.main.MusicListItem import com.sosauce.cutemusic.ui.shared_components.CuteSearchbar import com.sosauce.cutemusic.ui.shared_components.CuteText @@ -118,22 +116,22 @@ fun SharedTransitionScope.AllFoldersScreen( ) FolderItem( - folder = Folder( - name = folder?.substring(folder.lastIndexOf('/') + 1) ?: "No Name", - path = folder.toString() - ), - onClick = { areMusicsVisible[folder ?: "No Name"] = !isExpanded }, + folder = folder ?: " No name", topDp = topDp, bottomDp = bottomDp, - icon = { - Icon( - imageVector = Icons.Rounded.ArrowBackIosNew, - contentDescription = null, - modifier = Modifier - .size(30.dp) - .rotate(rotation), - tint = MaterialTheme.colorScheme.onBackground - ) + modifier = Modifier.animateItem(), + actionButton = { + IconButton( + onClick = { + areMusicsVisible[folder ?: "No name"] = !isExpanded + } + ) { + Icon( + imageVector = Icons.Rounded.ArrowBackIosNew, + contentDescription = null, + modifier = Modifier.rotate(rotation) + ) + } } ) } 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 01893ad..f8e0702 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 @@ -21,7 +21,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.rounded.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -79,7 +79,8 @@ fun SharedTransitionScope.ArtistDetails( chargePVMAlbumSongs = { postViewModel.albumSongs(it) }, artist = artist, currentMusicUri = musicState.currentMusicUri, - isPlayerReady = musicState.isPlayerReady + isPlayerReady = musicState.isPlayerReady, + animatedVisibilityScope = animatedVisibilityScope ) } else { Scaffold( @@ -96,7 +97,6 @@ fun SharedTransitionScope.ArtistDetails( ) CuteText( text = "${artistSongs.size} ${if (artistSongs.size <= 1) "song" else "songs"}", - fontSize = 20.sp ) } @@ -106,7 +106,7 @@ fun SharedTransitionScope.ArtistDetails( onClick = navController::navigateUp ) { Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, + imageVector = Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "Back arrow" ) } @@ -137,7 +137,8 @@ fun SharedTransitionScope.ArtistDetails( .clickable { postViewModel.albumSongs(album.name) onNavigate(Screen.AlbumsDetails(album.id)) - } + }, + animatedVisibilityScope = animatedVisibilityScope ) } } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistDetailsLandscape.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistDetailsLandscape.kt index 2e83c40..1e8e05c 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistDetailsLandscape.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/artist/ArtistDetailsLandscape.kt @@ -1,5 +1,10 @@ +@file:OptIn(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.Column import androidx.compose.foundation.layout.Row @@ -32,7 +37,7 @@ import com.sosauce.cutemusic.ui.screens.main.MusicListItem import com.sosauce.cutemusic.ui.shared_components.CuteText @Composable -fun ArtistDetailsLandscape( +fun SharedTransitionScope.ArtistDetailsLandscape( onNavigateUp: () -> Unit, artistAlbums: List, artistSongs: List, @@ -41,7 +46,8 @@ fun ArtistDetailsLandscape( chargePVMAlbumSongs: (String) -> Unit, artist: Artist, currentMusicUri: String, - isPlayerReady: Boolean + isPlayerReady: Boolean, + animatedVisibilityScope: AnimatedVisibilityScope, ) { Column( modifier = Modifier @@ -87,7 +93,8 @@ fun ArtistDetailsLandscape( chargePVMAlbumSongs(album.name) onNavigate(Screen.AlbumsDetails(album.id)) } - .size(230.dp) + .size(230.dp), + animatedVisibilityScope = animatedVisibilityScope ) } } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/blacklisted/BlacklistedScreen.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/blacklisted/BlacklistedScreen.kt index e5d2e55..6d50340 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/blacklisted/BlacklistedScreen.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/blacklisted/BlacklistedScreen.kt @@ -2,15 +2,12 @@ package com.sosauce.cutemusic.ui.screens.blacklisted -import android.widget.Toast import androidx.compose.animation.core.animateDpAsState -import androidx.compose.animation.core.tween import androidx.compose.foundation.Image 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.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -19,27 +16,21 @@ import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.FolderOpen +import androidx.compose.material.icons.rounded.Add import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults 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 -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Scaffold 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.graphics.ColorFilter -import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -48,7 +39,6 @@ import androidx.navigation.NavController import com.sosauce.cutemusic.R import com.sosauce.cutemusic.data.datastore.rememberAllBlacklistedFolders import com.sosauce.cutemusic.domain.model.Folder -import com.sosauce.cutemusic.ui.screens.blacklisted.components.AllFoldersBottomSheet import com.sosauce.cutemusic.ui.shared_components.AppBar import com.sosauce.cutemusic.ui.shared_components.CuteText import java.io.File @@ -58,51 +48,6 @@ fun BlacklistedScreen( navController: NavController, folders: List, ) { - var isSheetOpen by remember { mutableStateOf(false) } - val context = LocalContext.current - var blacklistedFolders by rememberAllBlacklistedFolders() - - if (isSheetOpen) { - ModalBottomSheet( - onDismissRequest = { isSheetOpen = false }, - modifier = Modifier.fillMaxHeight() - ) { - AllFoldersBottomSheet( - folders = folders, - onClick = { path -> - if (path in blacklistedFolders) { - Toast.makeText( - context, - context.resources.getText(R.string.alrdy_blacklisted), - Toast.LENGTH_SHORT - ).show() - } else { - blacklistedFolders = blacklistedFolders.toMutableSet().apply { - add(path) - } - isSheetOpen = false - Toast.makeText( - context, - context.resources.getText(R.string.pls_restart), - Toast.LENGTH_SHORT - ).show() - } - } - ) - } - } - - BlacklistedScreenContent( - onAddFolder = { isSheetOpen = true }, - onPopBackStack = navController::navigateUp - ) -} - -@Composable -private fun BlacklistedScreenContent( - onAddFolder: () -> Unit, - onPopBackStack: () -> Unit, -) { var blacklistedFolders by rememberAllBlacklistedFolders() @@ -111,18 +56,8 @@ private fun BlacklistedScreenContent( AppBar( title = stringResource(id = R.string.blacklisted_folders), showBackArrow = true, - onPopBackStack = { onPopBackStack() } + onPopBackStack = navController::navigateUp ) - }, - floatingActionButton = { - FloatingActionButton( - onClick = { onAddFolder() } - ) { - Icon( - imageVector = Icons.Default.Add, - contentDescription = null - ) - } } ) { values -> LazyColumn( @@ -130,54 +65,97 @@ private fun BlacklistedScreenContent( .fillMaxSize() .padding(values) ) { - itemsIndexed( - items = blacklistedFolders.toList(), - key = { _, folder -> folder } - ) { index, folder -> + folders.sortedBy { it.name } + .groupBy { it.path in blacklistedFolders } + .toSortedMap(compareByDescending { it }) + .forEach { (isBlacklisted, allFolders) -> + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + horizontal = 34.dp, + vertical = 8.dp + ) + ) { + CuteText( + text = if (isBlacklisted) stringResource(R.string.blacklisted) else stringResource( + R.string.not_blacklisted + ), + color = MaterialTheme.colorScheme.primary + ) + } + } - val topDp by animateDpAsState( - targetValue = if (index == 0) 24.dp else 4.dp, - label = "Top Dp", - animationSpec = tween(500) - ) - val bottomDp by animateDpAsState( - targetValue = if (index == blacklistedFolders.size - 1) 24.dp else 4.dp, - label = "Bottom Dp", - animationSpec = tween(500) - ) + itemsIndexed( + items = allFolders, + key = { _, folder -> folder.path } + ) { index, folder -> + val topDp by animateDpAsState( + targetValue = if (index == 0) 24.dp else 4.dp, + label = "Top Dp" + ) + val bottomDp by animateDpAsState( + targetValue = if (index == allFolders.size - 1) 24.dp else 4.dp, + label = "Bottom Dp" + ) - BlackFolderItem( - folder = folder, - onClick = { - blacklistedFolders = blacklistedFolders.toMutableSet().apply { - remove(folder) - } - }, - topDp = topDp, - bottomDp = bottomDp, - modifier = Modifier.animateItem() - ) - } + FolderItem( + folder = folder.path, + topDp = topDp, + bottomDp = bottomDp, + modifier = Modifier.animateItem(), + actionButton = { + if (isBlacklisted) { + IconButton( + onClick = { + blacklistedFolders = + blacklistedFolders.toMutableSet().apply { + remove(folder.path) + } + } + ) { + Icon( + painter = painterResource(R.drawable.trash_rounded_filled), + contentDescription = null, + tint = MaterialTheme.colorScheme.error + ) + } + } else { + IconButton( + onClick = { + blacklistedFolders = + blacklistedFolders.toMutableSet().apply { + add(folder.path) + } + } + ) { + Icon( + imageVector = Icons.Rounded.Add, + contentDescription = null + ) + } + } + } + ) + } + } } } } @Composable -private fun BlackFolderItem( +fun FolderItem( modifier: Modifier = Modifier, folder: String, - onClick: () -> Unit, topDp: Dp, bottomDp: Dp, + actionButton: @Composable () -> Unit ) { Card( modifier = modifier - .padding( - start = 13.dp, - end = 13.dp, - bottom = 8.dp - ), + .padding(horizontal = 16.dp, vertical = 2.dp), colors = CardDefaults.cardColors( containerColor = MaterialTheme.colorScheme.surfaceContainer ), @@ -196,7 +174,7 @@ private fun BlackFolderItem( horizontalArrangement = Arrangement.SpaceBetween ) { Image( - imageVector = Icons.Default.FolderOpen, + painter = painterResource(R.drawable.folder_rounded), contentDescription = null, modifier = Modifier.size(33.dp), colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground) @@ -208,7 +186,7 @@ private fun BlackFolderItem( horizontalAlignment = Alignment.Start ) { CuteText( - text = getFileName(folder), + text = File(folder).name, fontSize = 18.sp ) CuteText( @@ -218,20 +196,7 @@ private fun BlackFolderItem( modifier = Modifier.basicMarquee() ) } - IconButton( - onClick = onClick - ) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = null, - tint = MaterialTheme.colorScheme.error - ) - } + actionButton() } } -} - -private fun getFileName(filePath: String): String { - val file = File(filePath) - return file.name } \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/blacklisted/components/AllFolderBottomSheet.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/blacklisted/components/AllFolderBottomSheet.kt deleted file mode 100644 index 8b4704d..0000000 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/blacklisted/components/AllFolderBottomSheet.kt +++ /dev/null @@ -1,105 +0,0 @@ -package com.sosauce.cutemusic.ui.screens.blacklisted.components - -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.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Folder -import androidx.compose.material3.Card -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import com.sosauce.cutemusic.domain.model.Folder -import com.sosauce.cutemusic.ui.shared_components.CuteText - -@Composable -fun AllFoldersBottomSheet( - folders: List, - onClick: (path: String) -> Unit, -) { - - LazyColumn { - itemsIndexed( - items = folders, - key = { _, folder -> folder.path } - ) { index, folder -> - FolderItem( - folder = folder, - onClick = { path -> - onClick(path) - }, - topDp = if (index == 0) 24.dp else 4.dp, - bottomDp = if (index == folders.size - 1) 24.dp else 4.dp, - icon = { - Icon( - imageVector = Icons.Rounded.Folder, - contentDescription = null, - modifier = Modifier.size(33.dp), - tint = MaterialTheme.colorScheme.onBackground - ) - } - ) - } - } -} - - -@Composable -fun FolderItem( - folder: Folder, - onClick: (path: String) -> Unit, - topDp: Dp, - bottomDp: Dp, - icon: @Composable () -> Unit -) { - Card( - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerHighest.copy( - alpha = 0.5f - ) - ), - modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp), - shape = RoundedCornerShape( - topStart = topDp, - topEnd = topDp, - bottomStart = bottomDp, - bottomEnd = bottomDp - ), - onClick = { onClick(folder.path) } - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(10.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - icon() - Column( - verticalArrangement = Arrangement.Center, - modifier = Modifier - .fillMaxWidth() - .padding(start = 10.dp) - ) { - CuteText( - text = folder.name - ) - CuteText( - text = folder.path, - color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f) - ) - } - } - } -} \ No newline at end of file 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 2bf51b1..49d250d 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 @@ -7,6 +7,7 @@ package com.sosauce.cutemusic.ui.screens.main import android.app.Activity import android.content.Intent +import android.graphics.Paint import android.net.Uri import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult @@ -65,7 +66,11 @@ 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.drawWithContent import androidx.compose.ui.draw.rotate +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -190,10 +195,9 @@ fun SharedTransitionScope.MainScreen( onChargeAlbumSongs = onChargeAlbumSongs, onChargeArtistLists = onChargeArtistLists, modifier = Modifier - .thenIf( - index == 0, + .thenIf(index == 0) { Modifier.statusBarsPadding() - ), + }, isPlayerReady = isPlayerReady ) } @@ -317,7 +321,9 @@ fun MusicListItem( onDeleteMusic: (List, ActivityResultLauncher) -> Unit = { _, _ -> }, onChargeAlbumSongs: (String) -> Unit = {}, onChargeArtistLists: (String) -> Unit = {}, - isPlayerReady: Boolean + isPlayerReady: Boolean, + onDeleteSafTrack: () -> Unit = {}, + showTrackNumber: Boolean = false ) { val context = LocalContext.current @@ -336,6 +342,8 @@ fun MusicListItem( label = "Background Color", animationSpec = tween(500) ) + val materialSurfaceContainer = MaterialTheme.colorScheme.surfaceContainer + val materialOnSurface = MaterialTheme.colorScheme.onSurface val deleteSongLauncher = rememberLauncherForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { if (it.resultCode == Activity.RESULT_OK) { @@ -397,7 +405,51 @@ fun MusicListItem( stringResource(R.string.artwork), modifier = Modifier .padding(start = 10.dp) - .size(45.dp), + .size(45.dp) + .drawWithContent { + drawContent() + if (showTrackNumber && music.mediaMetadata.trackNumber != null && music.mediaMetadata.trackNumber != 0) { + val circleCenter = Offset(size.width, size.height / 12) + drawCircle( + color = materialSurfaceContainer, + center = circleCenter, + radius = 25f + ) + val text = Paint().apply { + color = materialOnSurface.toArgb() + textSize = 30f + textAlign = Paint.Align.CENTER + } + drawContext.canvas.nativeCanvas.drawText( + music.mediaMetadata.trackNumber.toString(), + circleCenter.x, + circleCenter.y - (text.ascent() + text.descent()) / 2, + text + ) + } + }, +// .thenIf(showTrackNumber && music.mediaMetadata.trackNumber != null && music.mediaMetadata.trackNumber != 0) { +// Modifier.drawWithContent { +// val circleCenter = Offset(size.width, size.height / 12) +// drawContent() +// drawCircle( +// color = materialSurfaceContainer, +// center = circleCenter, +// radius = 25f +// ) +// val text = Paint().apply { +// color = materialOnSurface.toArgb() +// textSize = 30f +// textAlign = Paint.Align.CENTER +// } +// drawContext.canvas.nativeCanvas.drawText( +// music.mediaMetadata.trackNumber.toString(), +// circleCenter.x, +// circleCenter.y - (text.ascent() + text.descent()) / 2, +// text +// ) +// } +// }, contentScale = ContentScale.Crop, ) @@ -445,62 +497,64 @@ fun MusicListItem( ) } ) - DropdownMenuItem( - onClick = { - isDropDownExpanded = false - onLoadMetadata(path ?: "", uri) - onNavigate(Screen.MetadataEditor(music.mediaId)) - }, - text = { - CuteText(stringResource(R.string.edit)) - }, - leadingIcon = { - Icon( - painter = painterResource(R.drawable.edit_rounded), - contentDescription = null - ) - } - ) - DropdownMenuItem( - onClick = { - isDropDownExpanded = false - onChargeAlbumSongs(music.mediaMetadata.albumTitle.toString()) - onNavigate( - Screen.AlbumsDetails( - music.mediaMetadata.extras?.getLong("album_id") ?: 0 + if (music.mediaMetadata.extras?.getBoolean("is_saf") == false) { + DropdownMenuItem( + onClick = { + isDropDownExpanded = false + onLoadMetadata(path ?: "", uri) + onNavigate(Screen.MetadataEditor(music.mediaId)) + }, + text = { + CuteText(stringResource(R.string.edit)) + }, + leadingIcon = { + Icon( + painter = painterResource(R.drawable.edit_rounded), + contentDescription = null ) - ) - }, - text = { - CuteText(stringResource(R.string.go_to) + music.mediaMetadata.albumTitle) - }, - leadingIcon = { - Icon( - painter = painterResource(androidx.media3.session.R.drawable.media3_icon_album), - contentDescription = null - ) - } - ) - DropdownMenuItem( - onClick = { - isDropDownExpanded = false - onChargeArtistLists(music.mediaMetadata.artist.toString()) - onNavigate( - Screen.ArtistsDetails( - music.mediaMetadata.extras?.getLong("artist_id") ?: 0 + } + ) + DropdownMenuItem( + onClick = { + isDropDownExpanded = false + onChargeAlbumSongs(music.mediaMetadata.albumTitle.toString()) + onNavigate( + Screen.AlbumsDetails( + music.mediaMetadata.extras?.getLong("album_id") ?: 0 + ) ) - ) - }, - text = { - CuteText(stringResource(R.string.go_to) + music.mediaMetadata.artist) - }, - leadingIcon = { - Icon( - painter = painterResource(R.drawable.artist_rounded), - contentDescription = null - ) - } - ) + }, + text = { + CuteText("${stringResource(R.string.go_to)} ${music.mediaMetadata.albumTitle}") + }, + leadingIcon = { + Icon( + painter = painterResource(androidx.media3.session.R.drawable.media3_icon_album), + contentDescription = null + ) + } + ) + DropdownMenuItem( + onClick = { + isDropDownExpanded = false + onChargeArtistLists(music.mediaMetadata.artist.toString()) + onNavigate( + Screen.ArtistsDetails( + music.mediaMetadata.extras?.getLong("artist_id") ?: 0 + ) + ) + }, + text = { + CuteText("${stringResource(R.string.go_to)} ${music.mediaMetadata.artist}") + }, + leadingIcon = { + Icon( + painter = painterResource(R.drawable.artist_rounded), + contentDescription = null + ) + } + ) + } DropdownMenuItem( onClick = { val shareIntent = Intent().apply { @@ -530,7 +584,13 @@ fun MusicListItem( } ) DropdownMenuItem( - onClick = { onDeleteMusic(listOf(uri), deleteSongLauncher) }, + onClick = { + if (music.mediaMetadata.extras?.getBoolean("is_saf") == false) { + onDeleteMusic(listOf(uri), deleteSongLauncher) + } else { + onDeleteSafTrack() + } + }, text = { CuteText( text = stringResource(R.string.delete), 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 dca8e60..367d81d 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 @@ -59,30 +59,15 @@ fun MetadataEditor( val metadataState by metadataViewModel.metadataState.collectAsStateWithLifecycle() - MetadataEditorContent( - music = music, - onPopBackStack = onPopBackStack, - onNavigate = onNavigate, - metadataState = metadataState, - onMetadataAction = { metadataViewModel.onHandleMetadataActions(it) }, - onEditMusic = onEditMusic - ) -} - -@Composable -fun MetadataEditorContent( - music: MediaItem, - onPopBackStack: () -> Unit, - onNavigate: (Screen) -> Unit, - metadataState: MetadataState, - onMetadataAction: (MetadataActions) -> Unit, - onEditMusic: (List, ActivityResultLauncher) -> Unit -) { val context = LocalContext.current val uri = Uri.parse(music.mediaMetadata.extras?.getString("uri")) val photoPickerLauncher = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { - onMetadataAction(MetadataActions.UpdateAudioArt(it ?: Uri.EMPTY)) + metadataViewModel.onHandleMetadataActions( + MetadataActions.UpdateAudioArt( + it ?: Uri.EMPTY + ) + ) } val editSongLauncher = @@ -90,7 +75,7 @@ fun MetadataEditorContent( contract = ActivityResultContracts.StartIntentSenderForResult() ) { if (it.resultCode == Activity.RESULT_OK) { - onMetadataAction(MetadataActions.SaveChanges) + metadataViewModel.onHandleMetadataActions(MetadataActions.SaveChanges) Toast.makeText( context, context.getString(R.string.success), @@ -273,6 +258,7 @@ fun MetadataEditorContent( } } + @Composable private fun EditTextField( modifier: Modifier = Modifier, 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 36d12a1..2753e06 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 @@ -178,7 +178,7 @@ fun SharedTransitionScope.NowPlayingLandscape( onChargeAlbumSongs = onChargeAlbumSongs, onShowSpeedCard = { showSpeedCard = true }, onChargeArtistLists = onChargeArtistLists, - onHandlePlayerActions = onEvent + onHandlePlayerActions = onEvent, ) } } 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 439ea99..4b14079 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 @@ -6,7 +6,6 @@ package com.sosauce.cutemusic.ui.screens.playing import androidx.compose.animation.AnimatedVisibilityScope import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.SharedTransitionScope -import androidx.compose.animation.core.tween import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -153,7 +152,7 @@ private fun SharedTransitionScope.NowPlayingContent( horizontalArrangement = Arrangement.Start ) { IconButton( - onClick = onNavigateUp + onClick = onNavigateUp, ) { Icon( imageVector = Icons.Rounded.KeyboardArrowDown, @@ -193,14 +192,12 @@ private fun SharedTransitionScope.NowPlayingContent( color = MaterialTheme.colorScheme.onBackground, fontSize = 20.sp, modifier = Modifier - .sharedElement( - state = rememberSharedContentState(key = "currentlyPlaying"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(durationMillis = 500) - } - ) .basicMarquee() + .sharedBounds( + sharedContentState = rememberSharedContentState(key = "currentlyPlaying"), + animatedVisibilityScope = animatedVisibilityScope + ) + ) //Spacer(modifier = Modifier.height(5.dp)) CuteText( 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 015fb3f..9888703 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 @@ -219,10 +219,7 @@ fun SharedTransitionScope.ActionsButtonsRow( } .sharedElement( state = rememberSharedContentState(key = "skipPreviousButton"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(durationMillis = 500) - } + animatedVisibilityScope = animatedVisibilityScope ) ) } else { @@ -269,10 +266,7 @@ fun SharedTransitionScope.ActionsButtonsRow( contentDescription = "pause/play button", modifier = Modifier.sharedElement( state = rememberSharedContentState(key = "playPauseIcon"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(durationMillis = 500) - } + animatedVisibilityScope = animatedVisibilityScope ) ) } @@ -349,10 +343,7 @@ fun SharedTransitionScope.ActionsButtonsRow( } .sharedElement( state = rememberSharedContentState(key = "skipNextButton"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(durationMillis = 500) - } + animatedVisibilityScope = animatedVisibilityScope ) ) } 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 index 383d952..260db19 100644 --- 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 @@ -1,7 +1,13 @@ package com.sosauce.cutemusic.ui.screens.playing.components +import android.content.Context import android.content.Intent +import android.media.audiofx.AudioEffect import android.net.Uri +import android.util.Log +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContract +import androidx.activity.result.launch import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -12,11 +18,13 @@ import androidx.compose.foundation.layout.size 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.automirrored.rounded.OpenInNew import androidx.compose.material.icons.rounded.ErrorOutline import androidx.compose.material.icons.rounded.MoreVert import androidx.compose.material.icons.rounded.Speed import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -39,6 +47,7 @@ import com.sosauce.cutemusic.data.actions.PlayerActions import com.sosauce.cutemusic.ui.navigation.Screen import com.sosauce.cutemusic.ui.shared_components.CuteText import com.sosauce.cutemusic.ui.shared_components.MusicStateDetailsDialog +import com.sosauce.cutemusic.utils.CUTE_MUSIC_ID @Composable fun QuickActionsRow( @@ -48,7 +57,7 @@ fun QuickActionsRow( onChargeAlbumSongs: (String) -> Unit, onNavigate: (Screen) -> Unit, onChargeArtistLists: (String) -> Unit, - onHandlePlayerActions: (PlayerActions) -> Unit + onHandlePlayerActions: (PlayerActions) -> Unit, ) { val context = LocalContext.current var isDropDownExpanded by remember { mutableStateOf(false) } @@ -56,6 +65,7 @@ fun QuickActionsRow( val uri = remember { Uri.parse(musicState.currentMusicUri) } var showTimePicker by remember { mutableStateOf(false) } val onBackground = MaterialTheme.colorScheme.onBackground + val eqIntent = rememberLauncherForActivityResult(equalizerActivityContract()) { } if (showDetailsDialog) { MusicStateDetailsDialog( @@ -131,6 +141,32 @@ fun QuickActionsRow( onDismissRequest = { isDropDownExpanded = false }, shape = RoundedCornerShape(24.dp) ) { + DropdownMenuItem( + onClick = { + try { + eqIntent.launch() + } catch (e: Exception) { + Log.d( + "CuteError", + "Couldn't open EQ: ${e.stackTrace}, ${e.message}" + ) + } + }, + text = { + CuteText(stringResource(R.string.open_eq)) + }, + leadingIcon = { + Icon( + imageVector = Icons.AutoMirrored.Rounded.OpenInNew, + contentDescription = null + ) + } + ) + HorizontalDivider( + modifier = Modifier + .fillMaxWidth(0.9f) + .align(Alignment.CenterHorizontally) + ) DropdownMenuItem( onClick = { showDetailsDialog = true }, text = { @@ -151,7 +187,7 @@ fun QuickActionsRow( onNavigate(Screen.AlbumsDetails(musicState.currentAlbumId)) }, text = { - CuteText(stringResource(R.string.go_to) + musicState.currentAlbum) + CuteText("${stringResource(R.string.go_to)} ${musicState.currentAlbum}") }, leadingIcon = { Icon( @@ -167,7 +203,7 @@ fun QuickActionsRow( onNavigate(Screen.ArtistsDetails(musicState.currentArtistId)) }, text = { - CuteText(stringResource(R.string.go_to) + musicState.currentArtist) + CuteText("${stringResource(R.string.go_to)} ${musicState.currentArtist}") }, leadingIcon = { Icon( @@ -205,4 +241,21 @@ fun QuickActionsRow( } } } +} + +private fun equalizerActivityContract() = object : ActivityResultContract() { + override fun createIntent( + context: Context, + input: Unit, + ) = Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL).apply { + putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName) + putExtra(AudioEffect.EXTRA_AUDIO_SESSION, CUTE_MUSIC_ID) + putExtra(AudioEffect.EXTRA_CONTENT_TYPE, AudioEffect.CONTENT_TYPE_MUSIC) + } + + override fun parseResult( + resultCode: Int, + intent: Intent?, + ) { + } } \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/saf/SafScreen.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/saf/SafScreen.kt new file mode 100644 index 0000000..27f4f37 --- /dev/null +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/saf/SafScreen.kt @@ -0,0 +1,152 @@ +package com.sosauce.cutemusic.ui.screens.saf + +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContract +import androidx.activity.result.launch +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.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.rounded.OpenInNew +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +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.media3.common.MediaItem +import com.sosauce.cutemusic.R +import com.sosauce.cutemusic.data.datastore.rememberAllSafTracks +import com.sosauce.cutemusic.ui.navigation.Screen +import com.sosauce.cutemusic.ui.screens.main.MusicListItem +import com.sosauce.cutemusic.ui.shared_components.AppBar +import com.sosauce.cutemusic.ui.shared_components.CuteText + +@Composable +fun SafScreen( + onNavigateUp: () -> Unit, + latestSafTracks: List, + onNavigate: (Screen) -> Unit, + onShortClick: (String) -> Unit, + isPlayerReady: Boolean, + currentMusicUri: String, +) { + + val context = LocalContext.current + var safTracks by rememberAllSafTracks() + + val safAudioPicker = rememberLauncherForActivityResult(safActivityContract()) { + safTracks = safTracks.toMutableSet().apply { + add(it.toString()) + } + + context.contentResolver.takePersistableUriPermission( + it ?: Uri.EMPTY, + Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + } + + Scaffold( + topBar = { + AppBar( + title = stringResource(R.string.saf_manager), + showBackArrow = true, + onPopBackStack = onNavigateUp + ) + } + ) { values -> + + Column( + modifier = Modifier + .fillMaxSize() + .padding(values) + ) { + Card( + colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceContainer), + modifier = Modifier.align(Alignment.CenterHorizontally), + onClick = { + safAudioPicker.launch() + } + ) { + Row( + modifier = Modifier.padding(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.AutoMirrored.Rounded.OpenInNew, + contentDescription = null + ) + Spacer(Modifier.width(10.dp)) + CuteText(stringResource(R.string.open_saf)) + } + } + Spacer(Modifier.height(10.dp)) + LazyColumn { + items( + items = latestSafTracks.toList(), + key = { it.mediaId } + ) { safTrack -> + + Column( + modifier = Modifier + .animateItem() + .padding( + vertical = 2.dp, + horizontal = 4.dp + ) + ) { + MusicListItem( + onShortClick = { onShortClick(safTrack.mediaId) }, + music = safTrack, + onNavigate = { onNavigate(it) }, + currentMusicUri = currentMusicUri, + showBottomSheet = true, + isPlayerReady = isPlayerReady, + onDeleteSafTrack = { + safTracks = safTracks.toMutableSet().apply { + remove(safTrack.mediaMetadata.extras?.getString("uri")) + } + } + ) + } + + } + } + } + + + } + +} + +private fun safActivityContract() = object : ActivityResultContract() { + override fun createIntent( + context: Context, + input: Unit, + ) = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "audio/*" + } + + override fun parseResult(resultCode: Int, intent: Intent?): Uri? { + return intent?.data + } + +} \ No newline at end of file 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 e871579..8c77399 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 @@ -27,15 +27,15 @@ import androidx.compose.ui.unit.sp import com.sosauce.cutemusic.ui.shared_components.CuteText @Composable -inline fun SettingsCards( +fun SettingsCards( hasInfoDialog: Boolean = false, checked: Boolean, topDp: Dp, bottomDp: Dp, text: String, - crossinline onCheckedChange: () -> Unit, - crossinline onClick: () -> Unit = {}, - crossinline optionalDescription: @Composable () -> Unit = {} + onCheckedChange: () -> Unit, + onClick: () -> Unit = {}, + optionalDescription: @Composable () -> Unit = {} ) { Card( colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceContainer), 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 0edf924..8e95dde 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 @@ -16,6 +16,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.sosauce.cutemusic.R @@ -36,6 +37,7 @@ fun Misc( onNavigate: (Screen) -> Unit ) { //var killService by remember { rememberKillService(context) } + val context = LocalContext.current Column { CuteText( @@ -56,6 +58,19 @@ fun Misc( topDp = 24.dp, bottomDp = 24.dp ) +// TextSettingsCards( +// text = stringResource(id = R.string.saf_manager), +// onClick = { onNavigate(Screen.Saf) }, +// modifier = Modifier +// .padding( +// top = 25.dp, +// start = 15.dp, +// bottom = 25.dp +// ) +// .fillMaxWidth(), +// topDp = 4.dp, +// bottomDp = 24.dp +// ) // SettingsCards( // checked = killService, // onCheckedChange = { killService = !killService }, @@ -87,7 +102,8 @@ fun ThemeManagement() { text = stringResource(id = R.string.follow_sys) ) AnimatedContent( - targetState = !followSys, label = "", + targetState = !followSys, + label = "", transitionSpec = { (slideInHorizontally() + fadeIn()).togetherWith(slideOutHorizontally() + fadeOut()) } 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 index 678c6d3..00e0073 100644 --- 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 @@ -117,6 +117,20 @@ fun MusicDetailsDialog( text = "${stringResource(id = R.string.duration)}: ${music.mediaMetadata.durationMs?.formatToReadableTime() ?: 0}", modifier = Modifier.padding(bottom = 5.dp) ) + if (music.mediaMetadata.extras?.getBoolean("is_saf") == true) { + Spacer(Modifier.height(5.dp)) + Card( + colors = CardDefaults.cardColors(MaterialTheme.colorScheme.surfaceContainerHighest), + modifier = Modifier.align(Alignment.CenterHorizontally) + ) { + Row( + modifier = Modifier.padding(10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + CuteText(stringResource(R.string.from_saf)) + } + } + } } } ) 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 39caa7b..7b2235a 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 @@ -29,6 +29,7 @@ import com.sosauce.cutemusic.data.MusicState import com.sosauce.cutemusic.data.actions.PlayerActions import com.sosauce.cutemusic.domain.model.Lyrics import com.sosauce.cutemusic.domain.repository.MediaStoreHelper +import com.sosauce.cutemusic.domain.repository.SafManager import com.sosauce.cutemusic.main.PlaybackService import com.sosauce.cutemusic.utils.applyLoop import com.sosauce.cutemusic.utils.applyPlaybackSpeed @@ -37,16 +38,24 @@ 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.FlowPreview import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import java.io.File import java.io.FileNotFoundException +@OptIn(FlowPreview::class) +@SuppressLint("UnsafeOptInUsageError") class MusicViewModel( private val application: Application, - private val mediaStoreHelper: MediaStoreHelper + private val mediaStoreHelper: MediaStoreHelper, + private val safManager: SafManager ) : AndroidViewModel(application) { private var mediaController: MediaController? by mutableStateOf(null) @@ -58,7 +67,8 @@ class MusicViewModel( var sleepCountdownTimer: CountDownTimer? = null - private val playerListener = object : Player.Listener { + private val playerListener = @UnstableApi + object : Player.Listener { @UnstableApi override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) { super.onMediaMetadataChanged(mediaMetadata) @@ -67,8 +77,8 @@ class MusicViewModel( 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!", + currentPath = mediaMetadata.extras?.getString("path") ?: "No path found!", + currentMusicUri = mediaMetadata.extras?.getString("uri") ?: "No uri found!", currentLrcFile = getLrcFile(), currentAlbum = mediaMetadata.albumTitle.toString(), currentAlbumId = mediaMetadata.extras?.getLong("album_id") ?: 0, @@ -81,46 +91,58 @@ class MusicViewModel( override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) { super.onPlaybackParametersChanged(playbackParameters) - _musicState.value = _musicState.value.copy( - playbackParameters = playbackParameters - ) + _musicState.update { + it.copy( + playbackParameters = playbackParameters + ) + } } override fun onIsPlayingChanged(isPlaying: Boolean) { super.onIsPlayingChanged(isPlaying) - _musicState.value = _musicState.value.copy( - isCurrentlyPlaying = isPlaying - ) + _musicState.update { + it.copy( + isCurrentlyPlaying = isPlaying + ) + } } override fun onRepeatModeChanged(repeatMode: Int) { super.onRepeatModeChanged(repeatMode) when (repeatMode) { Player.REPEAT_MODE_ONE -> { - _musicState.value = _musicState.value.copy( - isLooping = true - ) + _musicState.update { + it.copy( + isLooping = true + ) + } } Player.REPEAT_MODE_OFF -> { - _musicState.value = _musicState.value.copy( - isLooping = false - ) + _musicState.update { + it.copy( + isLooping = false + ) + } } else -> { - _musicState.value = _musicState.value.copy( - isLooping = false - ) + _musicState.update { + it.copy( + isLooping = false + ) + } } } } override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) { super.onShuffleModeEnabledChanged(shuffleModeEnabled) - _musicState.value = _musicState.value.copy( - isShuffling = shuffleModeEnabled - ) + _musicState.update { + it.copy( + isShuffling = shuffleModeEnabled + ) + } } @@ -128,10 +150,12 @@ class MusicViewModel( super.onEvents(player, events) viewModelScope.launch { while (player.isPlaying) { - _musicState.value = _musicState.value.copy( - currentMusicDuration = player.duration, - currentPosition = player.currentPosition - ) + _musicState.update { + it.copy( + currentMusicDuration = player.duration, + currentPosition = player.currentPosition + ) + } delay(500) } } @@ -141,27 +165,55 @@ class MusicViewModel( super.onPlaybackStateChanged(playbackState) when (playbackState) { Player.STATE_IDLE -> { - _musicState.value = _musicState.value.copy( - isPlayerReady = false - ) + _musicState.update { + it.copy( + isPlayerReady = false + ) + } } Player.STATE_READY -> { - _musicState.value = _musicState.value.copy( - isPlayerReady = true - ) + _musicState.update { + it.copy( + isPlayerReady = true + ) + } } else -> { - _musicState.value = _musicState.value.copy( - isPlayerReady = true - ) + _musicState.update { + it.copy( + isPlayerReady = true + ) + } } } } } init { +// if (uiModeManager.currentModeType == Configuration.UI_MODE_TYPE_CAR) { +// MediaController +// .Builder( +// application, +// SessionToken( +// application, +// ComponentName(application, AutoPlaybackService::class.java) +// ) +// ) +// .buildAsync() +// .apply { +// addListener( +// { +// mediaController = get() +// mediaController!!.addListener(playerListener) +// }, +// MoreExecutors.directExecutor() +// ) +// +// } +// } else { +// } MediaController .Builder( application, @@ -176,7 +228,24 @@ class MusicViewModel( { mediaController = get() mediaController!!.addListener(playerListener) - mediaController!!.setMediaItems(mediaStoreHelper.musics) + viewModelScope.launch { + combine( + mediaStoreHelper.fetchLatestMusics(), + safManager.fetchLatestSafTracks() + ) { musics, safTracks -> + val combinedList = musics + safTracks + combinedList + } + .debounce(500) + .collectLatest { combinedList -> + mediaController!!.replaceMediaItems( + 0, + combinedList.size - 1, + combinedList + ) + } + } + }, MoreExecutors.directExecutor() ) @@ -219,7 +288,8 @@ class MusicViewModel( val fd = getFileDescriptorFromPath(application, musicState.value.currentPath) return fd?.dup()?.detachFd()?.let { - TagLib.getMetadata(it)?.propertyMap["LYRICS"]?.getOrNull(0) ?: application.getString(R.string.no_lyrics_note) + TagLib.getMetadata(it)?.propertyMap["LYRICS"]?.getOrNull(0) + ?: application.getString(R.string.no_lyrics_note) } ?: application.getString(R.string.no_lyrics_note) } 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 c912c09..def2896 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 @@ -1,5 +1,6 @@ package com.sosauce.cutemusic.ui.shared_components +import android.annotation.SuppressLint import android.net.Uri import android.util.Log import androidx.activity.result.ActivityResultLauncher @@ -12,24 +13,40 @@ import androidx.lifecycle.viewModelScope import androidx.media3.common.MediaItem import com.sosauce.cutemusic.domain.model.Album import com.sosauce.cutemusic.domain.repository.MediaStoreHelper +import com.sosauce.cutemusic.domain.repository.SafManager +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlin.collections.filter class PostViewModel( - private val mediaStoreHelper: MediaStoreHelper + private val mediaStoreHelper: MediaStoreHelper, + private val safManager: SafManager ) : ViewModel() { + @SuppressLint("UnsafeOptInUsageError") + val safTracks = safManager.fetchLatestSafTracks() + +// @SuppressLint("UnsafeOptInUsageError") +// var musics = combine(safTracks, mediaStoreHelper.fetchLatestMusics()) { safList, trackList -> +// safList + trackList +// }.stateIn( +// CoroutineScope(Dispatchers.IO), +// SharingStarted.WhileSubscribed(5000), +// mediaStoreHelper.musics +// ) + var musics = mediaStoreHelper.fetchLatestMusics().stateIn( - viewModelScope, + CoroutineScope(Dispatchers.IO), SharingStarted.WhileSubscribed(5000), mediaStoreHelper.musics ) + var albums = mediaStoreHelper.fetchLatestAlbums().stateIn( viewModelScope, SharingStarted.WhileSubscribed(5000), @@ -56,9 +73,7 @@ class PostViewModel( fun albumSongs(album: String) { try { viewModelScope.launch { - musics.collectLatest { - albumSongs = it.filter { it.mediaMetadata.albumTitle.toString() == album } - } + albumSongs = musics.value.filter { it.mediaMetadata.albumTitle.toString() == album } } } catch (e: Exception) { Log.e(CUTE_ERROR, e.message, e) @@ -68,9 +83,7 @@ class PostViewModel( fun artistSongs(artistName: String) { try { viewModelScope.launch { - musics.collectLatest { - artistSongs = it.filter { it.mediaMetadata.artist == artistName } - } + artistSongs = musics.value.filter { it.mediaMetadata.artist == artistName } } } catch (e: Exception) { Log.e(CUTE_ERROR, e.message, e) 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 f7ef891..aa66759 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 @@ -93,7 +93,11 @@ fun SharedTransitionScope.CuteSearchbar( minWidth = 45.dp, minHeight = 45.dp ) - .align(Alignment.End), + .align(Alignment.End) + .sharedBounds( + sharedContentState = rememberSharedContentState(key = "fab"), + animatedVisibilityScope = animatedVisibilityScope + ), shape = RoundedCornerShape(14.dp) ) { Icon( @@ -111,12 +115,11 @@ fun SharedTransitionScope.CuteSearchbar( color = MaterialTheme.colorScheme.surfaceContainer, shape = RoundedCornerShape(roundedShape) ) - .thenIf( - isPlayerReady, + .thenIf(isPlayerReady) { Modifier.clickable { onNavigate() } - ) + } ) { AnimatedVisibility( visible = isPlayerReady, @@ -149,15 +152,11 @@ fun SharedTransitionScope.CuteSearchbar( text = currentlyPlaying, modifier = Modifier .padding(start = 5.dp) - .sharedElement( - state = rememberSharedContentState(key = "currentlyPlaying"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(500) - } - ) .basicMarquee() - + .sharedBounds( + sharedContentState = rememberSharedContentState(key = "currentlyPlaying"), + animatedVisibilityScope = animatedVisibilityScope + ) ) } Row { @@ -188,10 +187,7 @@ fun SharedTransitionScope.CuteSearchbar( } .sharedElement( state = rememberSharedContentState(key = "skipPreviousButton"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(500) - } + animatedVisibilityScope = animatedVisibilityScope ) ) } @@ -203,10 +199,7 @@ fun SharedTransitionScope.CuteSearchbar( contentDescription = null, modifier = Modifier.sharedElement( state = rememberSharedContentState(key = "playPauseIcon"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(500) - } + animatedVisibilityScope = animatedVisibilityScope ) ) } @@ -237,10 +230,7 @@ fun SharedTransitionScope.CuteSearchbar( } .sharedElement( state = rememberSharedContentState(key = "skipNextButton"), - animatedVisibilityScope = animatedVisibilityScope, - boundsTransform = { _, _ -> - tween(500) - } + animatedVisibilityScope = animatedVisibilityScope ) ) } diff --git a/app/src/main/java/com/sosauce/cutemusic/utils/Constants.kt b/app/src/main/java/com/sosauce/cutemusic/utils/Constants.kt new file mode 100644 index 0000000..339a2cb --- /dev/null +++ b/app/src/main/java/com/sosauce/cutemusic/utils/Constants.kt @@ -0,0 +1,4 @@ +package com.sosauce.cutemusic.utils + +const val CUTE_MUSIC_ID = "CUTE_MUSIC_ID" +const val ROOT_ID = "cute_music_root" \ No newline at end of file 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 7f56fa0..0c1c5ff 100644 --- a/app/src/main/java/com/sosauce/cutemusic/utils/Extensions.kt +++ b/app/src/main/java/com/sosauce/cutemusic/utils/Extensions.kt @@ -19,17 +19,17 @@ import com.kyant.taglib.PropertyMap import com.sosauce.cutemusic.data.datastore.rememberIsLandscape import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.callbackFlow +import java.io.File +import java.io.FileOutputStream import java.util.Locale fun Modifier.thenIf( condition: Boolean, - modifier: Modifier + modifier: Modifier.() -> Modifier ): Modifier { - return this.then( - if (condition) { - modifier - } else Modifier - ) + return if (condition) { + this.then(modifier()) + } else this } fun Long.formatBinarySize(): String { @@ -140,6 +140,18 @@ fun Player.applyPlaybackSpeed( } +fun ByteArray.getUriFromByteArray(context: Context): Uri { + val albumArtFile = File(context.cacheDir, "album_art_${this.hashCode()}.jpg") + try { + FileOutputStream(albumArtFile).use { os -> + os.write(this) + } + return Uri.fromFile(albumArtFile) + } catch (e: Exception) { + return Uri.EMPTY + } +} + fun Uri.getBitrate(context: Context): String { val retriever = MediaMetadataRetriever() return try { @@ -221,6 +233,7 @@ fun AudioFileMetadata.toPropertyMap(): PropertyMap { fun ContentResolver.observe(uri: Uri) = callbackFlow { val observer = object : ContentObserver(null) { override fun onChange(selfChange: Boolean) { + trySend(selfChange) } } diff --git a/app/src/main/res/drawable/trash_rounded_filled.xml b/app/src/main/res/drawable/trash_rounded_filled.xml new file mode 100644 index 0000000..c14cfe1 --- /dev/null +++ b/app/src/main/res/drawable/trash_rounded_filled.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e71948b..9e74932 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -75,4 +75,10 @@ Set sleep timer Go to: No lyrics found ! + Equalizer + This track comes from the S.A.F + Open S.A.F to add tracks + Blacklisted + Not blacklisted + S.A.F manager \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 759f737..b71e133 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,8 +6,8 @@ koinAndroidxStartup = "4.0.0" kotlin = "2.0.21" activityCompose = "1.9.3" coilCompose = "3.0.3" -composeBom = "2024.11.00" -composeAnimation = "1.7.5" +composeBom = "2024.12.01" +composeAnimation = "1.7.6" coreKtx = "1.15.0" coreSplashscreen = "1.0.1" datastorePreferences = "1.1.1" @@ -16,7 +16,7 @@ lifecycleViewmodelCompose = "2.8.7" media3Common = "1.5.0" media3Exoplayer = "1.5.0" media3Session = "1.5.0" -navigationCompose = "2.8.4" +navigationCompose = "2.8.5" squigglyslider = "1.0.0" serialization = "2.0.0" taglib = "1.0.0-alpha25"