From a8e0b4e5dd5e0de2b3b382a0e8f0fc5c42f2c40f Mon Sep 17 00:00:00 2001 From: sosauce2 <98750531+sosauce@users.noreply.github.com> Date: Sun, 1 Dec 2024 14:33:23 +0100 Subject: [PATCH] v2.3.1 --- app/.DS_Store | Bin 10244 -> 10244 bytes app/build.gradle.kts | 6 +- app/release/baselineProfiles/0/app-release.dm | Bin 7355 -> 7378 bytes app/release/baselineProfiles/1/app-release.dm | Bin 7320 -> 7349 bytes app/release/output-metadata.json | 4 +- .../cutemusic/data/actions/MetadataActions.kt | 8 +- .../com/sosauce/cutemusic/di/AppModule.kt | 2 +- .../domain/repository/MediaStoreHelper.kt | 3 + .../domain/repository/MediaStoreHelperImpl.kt | 15 +- .../domain/repository/MediaStoreObserver.kt | 15 - .../cutemusic/ui/navigation/Navigation.kt | 55 +-- .../cutemusic/ui/screens/album/AlbumScreen.kt | 45 ++- .../ui/screens/artist/ArtistsScreen.kt | 44 ++- .../screens/blacklisted/BlacklistedScreen.kt | 1 - .../cutemusic/ui/screens/lyrics/LyricsView.kt | 2 +- .../cutemusic/ui/screens/main/MainScreen.kt | 331 +++++++++--------- .../ui/screens/metadata/MetadataEditor.kt | 87 +++-- .../ui/screens/metadata/MetadataState.kt | 16 +- .../ui/screens/metadata/MetadataViewModel.kt | 149 ++++++-- .../ui/screens/settings/SettingsScreen.kt | 4 +- .../cutemusic/ui/shared_components/AppBar.kt | 16 - .../ui/shared_components/MusicViewModel.kt | 57 ++- .../ui/shared_components/PostViewModel.kt | 120 ++----- .../java/com/sosauce/cutemusic/utils/Enums.kt | 11 - .../com/sosauce/cutemusic/utils/Extensions.kt | 77 ++++ .../com/sosauce/cutemusic/utils/ImageUtils.kt | 3 + gradle/libs.versions.toml | 4 +- 27 files changed, 568 insertions(+), 507 deletions(-) delete mode 100644 app/src/main/java/com/sosauce/cutemusic/domain/repository/MediaStoreObserver.kt diff --git a/app/.DS_Store b/app/.DS_Store index fc824dcd7eb54f2619411a5eee7389a849127aed..398426152e500ff5fe2be85d58d84b6d4ae3cff1 100644 GIT binary patch delta 55 zcmV-70LcG@P=rvhCJ_P1lfM^$3Kx5OGBYtVEFd|Pu@D^rn3K>E9|4(@0TmUq1rh=d Nvj!mj1hWJc0R!}O5Fh{m delta 30 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 PJAaq^->Jy-&+fkf%y`5x literal 7355 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 diff --git a/app/release/baselineProfiles/1/app-release.dm b/app/release/baselineProfiles/1/app-release.dm index 33f76fd51e188424518531f360824719b5df2666..0bd015108a35951e6cebd509f24c5896c9d09381 100644 GIT binary patch literal 7349 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 C(4))% literal 7320 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 W)K fun fetchMusics(): List + fun fetchLatestMusics(): Flow> fun fetchAlbums(): List + fun fetchLatestAlbums(): Flow> 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 123f73f..aecf820 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 @@ -2,6 +2,7 @@ package com.sosauce.cutemusic.domain.repository +import android.annotation.SuppressLint import android.content.ContentUris import android.content.ContentValues import android.content.Context @@ -19,21 +20,27 @@ import com.sosauce.cutemusic.data.datastore.getBlacklistedFolder 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.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map import kotlinx.coroutines.runBlocking +@SuppressLint("UnsafeOptInUsageError") class MediaStoreHelperImpl( private val context: Context ) : MediaStoreHelper { - private fun getBlacklistedFoldersAsync(): Set = - runBlocking { getBlacklistedFolder(context) } + 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 selection = blacklistedFolders.joinToString(" AND ") { "${MediaStore.Audio.Media.DATA} NOT LIKE ?" } private val selectionArgs = blacklistedFolders.map { "$it%" }.toTypedArray() + + override fun fetchLatestMusics(): Flow> = context.contentResolver.observe(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI).map { fetchMusics() } + override fun fetchLatestAlbums(): Flow> = context.contentResolver.observe(MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI).map { fetchAlbums() } + @UnstableApi override fun fetchMusics(): List { diff --git a/app/src/main/java/com/sosauce/cutemusic/domain/repository/MediaStoreObserver.kt b/app/src/main/java/com/sosauce/cutemusic/domain/repository/MediaStoreObserver.kt deleted file mode 100644 index 814314d..0000000 --- a/app/src/main/java/com/sosauce/cutemusic/domain/repository/MediaStoreObserver.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.sosauce.cutemusic.domain.repository - -import android.database.ContentObserver -import android.os.Handler -import android.os.Looper - -class MediaStoreObserver( - private val onMediaStoreChanged: () -> Unit -) : ContentObserver(Handler(Looper.getMainLooper())) { - - override fun onChange(selfChange: Boolean) { - super.onChange(selfChange) - onMediaStoreChanged() - } -} \ 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 3a61f7d..2122a1d 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 @@ -26,7 +26,6 @@ import com.sosauce.cutemusic.ui.screens.playing.NowPlayingScreen import com.sosauce.cutemusic.ui.screens.settings.SettingsScreen import com.sosauce.cutemusic.ui.shared_components.MusicViewModel 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 @@ -39,9 +38,9 @@ fun Nav() { val viewModel = koinViewModel() val postViewModel = koinViewModel() val metadataViewModel = koinViewModel() - val musics = postViewModel.musics + val musics by postViewModel.musics.collectAsStateWithLifecycle() val musicState by viewModel.musicState.collectAsStateWithLifecycle() - val folders = postViewModel.folders + val albums by postViewModel.albums.collectAsStateWithLifecycle() SharedTransitionLayout { @@ -65,7 +64,6 @@ fun Nav() { }, animatedVisibilityScope = this, onLoadMetadata = { path, uri -> - metadataViewModel.onHandleMetadataActions(MetadataActions.ClearState) metadataViewModel.onHandleMetadataActions( MetadataActions.LoadSong( path, @@ -82,43 +80,20 @@ fun Nav() { intentSender ) }, - onHandleSorting = { sortingType -> - postViewModel.handleFiltering( - listToHandle = ListToHandle.TRACKS, - sortingType = sortingType - ) - }, - onHandleSearching = { query -> - postViewModel.handleSearch( - listToHandle = ListToHandle.TRACKS, - query = query - ) - }, onChargeAlbumSongs = postViewModel::albumSongs, onChargeArtistLists = { postViewModel.artistSongs(it) postViewModel.artistAlbums(it) - }, - musicState = musicState + } ) } + composable { + AlbumsScreen( - albums = postViewModel.albums, + albums = albums, animatedVisibilityScope = this, - onHandleSorting = { sortingType -> - postViewModel.handleFiltering( - listToHandle = ListToHandle.ALBUMS, - sortingType = sortingType - ) - }, - onHandleSearching = { query -> - postViewModel.handleSearch( - listToHandle = ListToHandle.ALBUMS, - query = query - ) - }, currentlyPlaying = musicState.currentlyPlaying, chargePVMAlbumSongs = postViewModel::albumSongs, isPlayerReady = musicState.isPlayerReady, @@ -132,7 +107,6 @@ fun Nav() { } }, selectedIndex = viewModel.selectedItem, - musicState = musicState ) } composable { @@ -154,20 +128,7 @@ fun Nav() { onHandlePlayerActions = viewModel::handlePlayerActions, isPlaying = musicState.isCurrentlyPlaying, animatedVisibilityScope = this, - isPlayerReady = musicState.isPlayerReady, - onHandleSorting = { sortingType -> - postViewModel.handleFiltering( - listToHandle = ListToHandle.ARTISTS, - sortingType = sortingType - ) - }, - onHandleSearching = { query -> - postViewModel.handleSearch( - listToHandle = ListToHandle.ARTISTS, - query = query - ) - }, - musicState = musicState + isPlayerReady = musicState.isPlayerReady ) } @@ -192,7 +153,7 @@ fun Nav() { } composable { val index = it.toRoute() - postViewModel.albums.find { album -> album.id == index.id }?.let { album -> + albums.find { album -> album.id == index.id }?.let { album -> AlbumDetailsScreen( album = album, viewModel = viewModel, 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 39491ee..c3a1dd2 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 @@ -33,6 +33,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -48,7 +49,6 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp 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 @@ -58,7 +58,6 @@ import com.sosauce.cutemusic.ui.shared_components.CuteText import com.sosauce.cutemusic.ui.shared_components.NavigationItem import com.sosauce.cutemusic.ui.shared_components.ScreenSelection import com.sosauce.cutemusic.utils.ImageUtils -import com.sosauce.cutemusic.utils.SortingType import com.sosauce.cutemusic.utils.rememberSearchbarAlignment import com.sosauce.cutemusic.utils.rememberSearchbarMaxFloatValue import com.sosauce.cutemusic.utils.rememberSearchbarRightPadding @@ -68,8 +67,6 @@ import com.sosauce.cutemusic.utils.thenIf fun SharedTransitionScope.AlbumsScreen( albums: List, animatedVisibilityScope: AnimatedVisibilityScope, - onHandleSorting: (SortingType) -> Unit, - onHandleSearching: (String) -> Unit, currentlyPlaying: String, chargePVMAlbumSongs: (String) -> Unit, onNavigate: (Screen) -> Unit, @@ -78,7 +75,6 @@ fun SharedTransitionScope.AlbumsScreen( onHandlePlayerActions: (PlayerActions) -> Unit, isPlayerReady: Boolean, onNavigationItemClicked: (Int, NavigationItem) -> Unit, - musicState: MusicState ) { val isLandscape = rememberIsLandscape() var query by remember { mutableStateOf("") } @@ -92,8 +88,25 @@ fun SharedTransitionScope.AlbumsScreen( if (isLandscape) 4 else 2 } + val displayAlbums by remember(isSortedByASC, albums, query) { + derivedStateOf { + if (query.isNotEmpty()) { + albums.filter { + it.name.contains( + other = query, + ignoreCase = true + ) == true + } + } else { + if (isSortedByASC) albums + else albums.sortedByDescending { it.name } + } + + } + } + Box { - if (albums.isEmpty()) { + if (displayAlbums.isEmpty()) { Column( modifier = Modifier .fillMaxSize() @@ -114,7 +127,7 @@ fun SharedTransitionScope.AlbumsScreen( .fillMaxSize() ) { itemsIndexed( - items = albums, + items = displayAlbums, key = { _, album -> album.id } ) { index, album -> AlbumCard( @@ -138,10 +151,7 @@ fun SharedTransitionScope.AlbumsScreen( } CuteSearchbar( query = query, - onQueryChange = { - query = it - onHandleSearching(query) - }, + onQueryChange = { query = it }, modifier = Modifier .navigationBarsPadding() .fillMaxWidth(rememberSearchbarMaxFloatValue()) @@ -182,18 +192,7 @@ fun SharedTransitionScope.AlbumsScreen( trailingIcon = { Row { IconButton( - onClick = { - isSortedByASC = !isSortedByASC - when (isSortedByASC) { - true -> { - onHandleSorting(SortingType.ASCENDING) - } - - false -> { - onHandleSorting(SortingType.DESCENDING) - } - } - } + onClick = { isSortedByASC = !isSortedByASC } ) { Icon( imageVector = Icons.Rounded.ArrowUpward, 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 e858f33..c6cc289 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 @@ -30,6 +30,7 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -44,7 +45,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.sosauce.cutemusic.R -import com.sosauce.cutemusic.data.MusicState import com.sosauce.cutemusic.data.actions.PlayerActions import com.sosauce.cutemusic.domain.model.Artist import com.sosauce.cutemusic.ui.navigation.Screen @@ -52,7 +52,6 @@ import com.sosauce.cutemusic.ui.shared_components.CuteSearchbar import com.sosauce.cutemusic.ui.shared_components.CuteText import com.sosauce.cutemusic.ui.shared_components.NavigationItem import com.sosauce.cutemusic.ui.shared_components.ScreenSelection -import com.sosauce.cutemusic.utils.SortingType import com.sosauce.cutemusic.utils.rememberSearchbarAlignment import com.sosauce.cutemusic.utils.rememberSearchbarMaxFloatValue import com.sosauce.cutemusic.utils.rememberSearchbarRightPadding @@ -61,8 +60,6 @@ import com.sosauce.cutemusic.utils.rememberSearchbarRightPadding fun SharedTransitionScope.ArtistsScreen( artist: List, animatedVisibilityScope: AnimatedVisibilityScope, - onHandleSorting: (SortingType) -> Unit, - onHandleSearching: (String) -> Unit, currentlyPlaying: String, onChargeArtistLists: (String) -> Unit, onNavigate: (Screen) -> Unit, @@ -71,7 +68,6 @@ fun SharedTransitionScope.ArtistsScreen( onHandlePlayerActions: (PlayerActions) -> Unit, isPlayerReady: Boolean, onNavigationItemClicked: (Int, NavigationItem) -> Unit, - musicState: MusicState ) { var query by remember { mutableStateOf("") } @@ -81,10 +77,26 @@ fun SharedTransitionScope.ArtistsScreen( targetValue = if (isSortedByASC) 45f else 135f, label = "Arrow Icon Animation" ) + val displayArtists by remember(isSortedByASC, artist, query) { + derivedStateOf { + if (query.isNotEmpty()) { + artist.filter { + it.name.contains( + other = query, + ignoreCase = true + ) == true + } + } else { + if (isSortedByASC) artist + else artist.sortedByDescending { it.name } + } + + } + } Scaffold { values -> Box { - if (artist.isEmpty()) { + if (displayArtists.isEmpty()) { Column( modifier = Modifier .fillMaxSize() @@ -106,7 +118,7 @@ fun SharedTransitionScope.ArtistsScreen( .padding(values), ) { items( - items = artist, + items = displayArtists, key = { it.id } ) { Column( @@ -127,10 +139,7 @@ fun SharedTransitionScope.ArtistsScreen( } CuteSearchbar( query = query, - onQueryChange = { - query = it - onHandleSearching(query) - }, + onQueryChange = { query = it }, modifier = Modifier .navigationBarsPadding() .fillMaxWidth(rememberSearchbarMaxFloatValue()) @@ -170,18 +179,7 @@ fun SharedTransitionScope.ArtistsScreen( trailingIcon = { Row { IconButton( - onClick = { - isSortedByASC = !isSortedByASC - when (isSortedByASC) { - true -> { - onHandleSorting(SortingType.ASCENDING) - } - - false -> { - onHandleSorting(SortingType.DESCENDING) - } - } - } + onClick = { isSortedByASC = !isSortedByASC } ) { Icon( imageVector = Icons.Rounded.ArrowUpward, 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 3f48254..e5d2e55 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 @@ -111,7 +111,6 @@ private fun BlacklistedScreenContent( AppBar( title = stringResource(id = R.string.blacklisted_folders), showBackArrow = true, - showMenuIcon = false, onPopBackStack = { onPopBackStack() } ) }, 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 c37fa8c..374af3c 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 @@ -101,7 +101,7 @@ fun LyricsView( if (musicState.currentLyrics.isEmpty()) { item { CuteText( - text = viewModel.loadEmbeddedLyrics(musicState.currentPath).toString(), + text = viewModel.loadEmbeddedLyrics(), ) } } else { 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 da1f7bf..bc0967b 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 @@ -74,9 +74,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.media3.common.MediaItem +import androidx.media3.common.util.UnstableApi 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.rememberHasSeenTip import com.sosauce.cutemusic.ui.navigation.Screen @@ -87,7 +87,6 @@ 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 -import com.sosauce.cutemusic.utils.SortingType import com.sosauce.cutemusic.utils.rememberSearchbarAlignment import com.sosauce.cutemusic.utils.rememberSearchbarMaxFloatValue import com.sosauce.cutemusic.utils.rememberSearchbarRightPadding @@ -108,11 +107,8 @@ fun SharedTransitionScope.MainScreen( currentMusicUri: String, onHandlePlayerAction: (PlayerActions) -> Unit, onDeleteMusic: (List, ActivityResultLauncher) -> Unit, - onHandleSorting: (SortingType) -> Unit, - onHandleSearching: (String) -> Unit, onChargeAlbumSongs: (String) -> Unit, onChargeArtistLists: (String) -> Unit, - musicState: MusicState ) { var query by remember { mutableStateOf("") } val state = rememberLazyListState() @@ -139,171 +135,172 @@ fun SharedTransitionScope.MainScreen( } } + val displayMusics by remember(isSortedByASC, musics, query) { + derivedStateOf { + if (query.isNotEmpty()) { + musics.filter { + it.mediaMetadata.title?.contains( + other = query, + ignoreCase = true + ) == true + } + } else { + if (isSortedByASC) musics + else musics.sortedByDescending { it.mediaMetadata.title.toString() } + } - Scaffold { _ -> - Box(Modifier.fillMaxSize()) { - LazyColumn( - state = state - ) { - if (musics.isEmpty()) { - item { - CuteText( - text = stringResource(id = R.string.no_musics_found), + } + } + + + Box(Modifier.fillMaxSize()) { + LazyColumn( + state = state + ) { + if (displayMusics.isEmpty()) { + item { + CuteText( + text = stringResource(id = R.string.no_musics_found), + modifier = Modifier + .statusBarsPadding() + .padding(16.dp) + .fillMaxWidth(), + textAlign = TextAlign.Center + ) + } + } else { + itemsIndexed( + items = displayMusics, + 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 = { onNavigate(it) }, + currentMusicUri = currentMusicUri, + onLoadMetadata = onLoadMetadata, + showBottomSheet = true, + onDeleteMusic = onDeleteMusic, + onChargeAlbumSongs = onChargeAlbumSongs, + onChargeArtistLists = onChargeArtistLists, modifier = Modifier - .statusBarsPadding() - .padding(16.dp) - .fillMaxWidth(), - textAlign = TextAlign.Center + .thenIf( + index == 0, + Modifier.statusBarsPadding() + ), + isPlayerReady = isPlayerReady ) } - } 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 = { onNavigate(it) }, - currentMusicUri = currentMusicUri, - onLoadMetadata = onLoadMetadata, - showBottomSheet = true, - onDeleteMusic = onDeleteMusic, - onChargeAlbumSongs = onChargeAlbumSongs, - onChargeArtistLists = onChargeArtistLists, - modifier = Modifier - .thenIf( - index == 0, - Modifier.statusBarsPadding() - ), - isPlayerReady = isPlayerReady - ) - } - } } } + } - // TODO : How do you make it NOT scroll to the first item when sorting changes !!!!! - Crossfade( - targetState = showCuteSearchbar, - 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() + // TODO : How do you make it NOT scroll to the first item when sorting changes !!!!! + Crossfade( + targetState = showCuteSearchbar, + 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() + CuteSearchbar( + query = query, + onQueryChange = { query = it }, + modifier = Modifier + .navigationBarsPadding() + .fillMaxWidth(rememberSearchbarMaxFloatValue()) + .padding( + bottom = 5.dp, + end = rememberSearchbarRightPadding() + ), + placeholder = { + CuteText( + text = stringResource(id = R.string.search) + " " + stringResource( + id = R.string.music ), - placeholder = { - CuteText( - text = stringResource(id = R.string.search) + " " + stringResource( - id = R.string.music - ), - color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f), + color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.5f), - ) - }, - leadingIcon = { - IconButton( - onClick = { - screenSelectionExpanded = true - if (!hasSeenTip) { - hasSeenTip = true - } + ) + }, + leadingIcon = { + IconButton( + onClick = { + screenSelectionExpanded = true + if (!hasSeenTip) { + hasSeenTip = true } + } + ) { + Icon( + painter = painterResource(R.drawable.music_note_rounded), + contentDescription = null, + tint = if (!hasSeenTip) color else LocalContentColor.current + ) + } + + + DropdownMenu( + expanded = screenSelectionExpanded, + onDismissRequest = { screenSelectionExpanded = false }, + modifier = Modifier + .width(180.dp) + .background(color = MaterialTheme.colorScheme.surface), + shape = RoundedCornerShape(24.dp) + ) { + ScreenSelection( + onNavigationItemClicked = onNavigationItemClicked, + selectedIndex = selectedIndex + ) + } + }, + trailingIcon = { + Row { + IconButton( + onClick = { isSortedByASC = !isSortedByASC } ) { Icon( - painter = painterResource(R.drawable.music_note_rounded), + imageVector = Icons.Rounded.ArrowUpward, contentDescription = null, - tint = if (!hasSeenTip) color else LocalContentColor.current + modifier = Modifier.rotate(float) ) } - - - DropdownMenu( - expanded = screenSelectionExpanded, - onDismissRequest = { screenSelectionExpanded = false }, - modifier = Modifier - .width(180.dp) - .background(color = MaterialTheme.colorScheme.surface), - shape = RoundedCornerShape(24.dp) + IconButton( + onClick = { onNavigate(Screen.Settings) } ) { - ScreenSelection( - onNavigationItemClicked = onNavigationItemClicked, - selectedIndex = selectedIndex + Icon( + imageVector = Icons.Rounded.Settings, + contentDescription = null ) } - }, - 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 = { onNavigate(Screen.Settings) } - ) { - Icon( - imageVector = Icons.Rounded.Settings, - contentDescription = null - ) - } - } - }, - currentlyPlaying = currentlyPlaying, - onHandlePlayerActions = onHandlePlayerAction, - isPlaying = isCurrentlyPlaying, - animatedVisibilityScope = animatedVisibilityScope, - isPlayerReady = isPlayerReady, - onNavigate = { onNavigate(Screen.NowPlaying) }, - onClickFAB = { onHandlePlayerAction(PlayerActions.PlayRandom) } - ) - } + } + }, + currentlyPlaying = currentlyPlaying, + onHandlePlayerActions = onHandlePlayerAction, + isPlaying = isCurrentlyPlaying, + animatedVisibilityScope = animatedVisibilityScope, + isPlayerReady = isPlayerReady, + onNavigate = { onNavigate(Screen.NowPlaying) }, + onClickFAB = { onHandlePlayerAction(PlayerActions.PlayRandom) } + ) } } } @@ -449,22 +446,22 @@ 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 + 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 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 b9132d5..1e19daf 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 @@ -2,11 +2,11 @@ package com.sosauce.cutemusic.ui.screens.metadata import android.app.Activity import android.net.Uri -import android.util.Log import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.IntentSenderRequest +import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.clickable @@ -65,7 +65,6 @@ fun MetadataEditor( onNavigate = onNavigate, metadataState = metadataState, onMetadataAction = { metadataViewModel.onHandleMetadataActions(it) }, - //vm = metadataViewModel, onEditMusic = onEditMusic ) } @@ -78,23 +77,15 @@ fun MetadataEditorContent( metadataState: MetadataState, onMetadataAction: (MetadataActions) -> Unit, onEditMusic: (List, ActivityResultLauncher) -> Unit - //vm: MetadataViewModel ) { val context = LocalContext.current val uri = Uri.parse(music.mediaMetadata.extras?.getString("uri")) -// var selectedImageUri by remember { mutableStateOf(null) } -// -// val photoPickerLauncher = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { -// selectedImageUri = it -// vm.changeImage(getFilePathFromUri(context, selectedImageUri!!)) -// } + val photoPickerLauncher = rememberLauncherForActivityResult(ActivityResultContracts.PickVisualMedia()) { onMetadataAction(MetadataActions.UpdateAudioArt(it ?: Uri.EMPTY)) } val editSongLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartIntentSenderForResult() ) { - Log.d("resulta", it.resultCode.toString()) - if (it.resultCode == Activity.RESULT_OK) { onMetadataAction(MetadataActions.SaveChanges) Toast.makeText( @@ -102,7 +93,6 @@ fun MetadataEditorContent( context.getString(R.string.success), Toast.LENGTH_SHORT ).show() - //onPopBackStack() } else { Toast.makeText( context, @@ -133,85 +123,86 @@ fun MetadataEditorContent( AppBar( title = stringResource(R.string.editor), showBackArrow = true, - showMenuIcon = false, onPopBackStack = { onPopBackStack() }, - onNavigate = { onNavigate(it) } ) } - ) { value -> + ) { pv -> Column( modifier = Modifier - .padding(value) + .padding(pv) .fillMaxSize() ) { - Row( + Column( modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally ) { +// IconButton( +// onClick = { onMetadataAction(MetadataActions.RemoveArtwork) }, +// modifier = Modifier.align(Alignment.End) +// ) { +// Icon( +// imageVector = Icons.Rounded.Close, +// contentDescription = null +// ) +// } AsyncImage( model = ImageUtils.imageRequester( - img = music.mediaMetadata.artworkUri, + img = metadataState.art?.data, context = context ), contentDescription = stringResource(id = R.string.artwork), modifier = Modifier .size(200.dp) - .padding(10.dp) + .padding(bottom = 10.dp) .clip(RoundedCornerShape(5)) .clickable { -// photoPickerLauncher.launch( -// PickVisualMediaRequest( -// ActivityResultContracts.PickVisualMedia.ImageOnly -// )) - Toast - .makeText( - context, - "Image editing will be available in the future!", - Toast.LENGTH_SHORT - ) - .show() + photoPickerLauncher.launch( + PickVisualMediaRequest( + ActivityResultContracts.PickVisualMedia.ImageOnly + )) }, contentScale = ContentScale.Crop ) } + Column { EditTextField( - value = metadataState.mutablePropertiesMap[0], + value = metadataState.mutablePropertiesMap["TITLE"], label = { CuteText( text = stringResource(R.string.title) ) } ) { title -> - metadataState.mutablePropertiesMap[0] = title + metadataState.mutablePropertiesMap["TITLE"] = title } EditTextField( - value = metadataState.mutablePropertiesMap[1], + value = metadataState.mutablePropertiesMap["ARTIST"], label = { CuteText( text = stringResource(R.string.artists).removeSuffix("s") // I'm too lazy to do plurals ) } ) { artist -> - metadataState.mutablePropertiesMap[1] = artist + metadataState.mutablePropertiesMap["ARTIST"] = artist } EditTextField( - value = metadataState.mutablePropertiesMap[2], + value = metadataState.mutablePropertiesMap["ALBUM"], label = { CuteText( text = stringResource(R.string.albums).removeSuffix("s") // I'm too lazy to do plurals ) } ) { album -> - metadataState.mutablePropertiesMap[2] = album + metadataState.mutablePropertiesMap["ALBUM"] = album } Spacer(Modifier.height(25.dp)) Row { EditTextField( - value = metadataState.mutablePropertiesMap[3], + value = metadataState.mutablePropertiesMap["DATE"], label = { CuteText( text = stringResource(R.string.year) @@ -220,10 +211,10 @@ fun MetadataEditorContent( modifier = Modifier.weight(1f), keyboardType = KeyboardType.Number ) { year -> - metadataState.mutablePropertiesMap[3] = year + metadataState.mutablePropertiesMap["DATE"] = year } EditTextField( - value = metadataState.mutablePropertiesMap[4], + value = metadataState.mutablePropertiesMap["GENRE"], label = { CuteText( text = stringResource(R.string.genre) @@ -231,12 +222,12 @@ fun MetadataEditorContent( }, modifier = Modifier.weight(1f) ) { genre -> - metadataState.mutablePropertiesMap[4] = genre + metadataState.mutablePropertiesMap["GENRE"] = genre } } Row { EditTextField( - value = metadataState.mutablePropertiesMap[5], + value = metadataState.mutablePropertiesMap["TRACKNUMBER"], label = { CuteText( text = stringResource(R.string.track_nb), @@ -246,10 +237,10 @@ fun MetadataEditorContent( modifier = Modifier.weight(1f), keyboardType = KeyboardType.Number ) { track -> - metadataState.mutablePropertiesMap[5] = track + metadataState.mutablePropertiesMap["TRACKNUMBER"] = track } EditTextField( - value = metadataState.mutablePropertiesMap[6], + value = metadataState.mutablePropertiesMap["DISCNUMBER"], label = { CuteText( text = stringResource(R.string.disc_nb), @@ -259,11 +250,11 @@ fun MetadataEditorContent( modifier = Modifier.weight(1f), keyboardType = KeyboardType.Number ) { disc -> - metadataState.mutablePropertiesMap[6] = disc + metadataState.mutablePropertiesMap["DISCNUMBER"] = disc } } EditTextField( - value = metadataState.mutablePropertiesMap[7], + value = metadataState.mutablePropertiesMap["LYRICS"], label = { CuteText( text = "Lyrics", @@ -271,7 +262,7 @@ fun MetadataEditorContent( ) } ) { lyrics -> - metadataState.mutablePropertiesMap[7] = lyrics + metadataState.mutablePropertiesMap["LYRICS"] = lyrics } } } diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/metadata/MetadataState.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/metadata/MetadataState.kt index af95e10..dde5dee 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/metadata/MetadataState.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/metadata/MetadataState.kt @@ -1,12 +1,18 @@ package com.sosauce.cutemusic.ui.screens.metadata import android.net.Uri -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.snapshots.SnapshotStateMap +import com.kyant.taglib.AudioProperties +import com.kyant.taglib.Metadata +import com.kyant.taglib.Picture data class MetadataState( - val mutablePropertiesMap: SnapshotStateList = mutableStateListOf(), + val mutablePropertiesMap: SnapshotStateMap = mutableStateMapOf(), val songPath: String = "", - val songUri: Uri = Uri.EMPTY - //var art: Artwork? = null + val songUri: Uri = Uri.EMPTY, + val metadata: Metadata? = null, + val audioProperties: AudioProperties? = null, + val art: Picture? = null, + val newArtUri: Uri = Uri.EMPTY ) \ No newline at end of file diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/screens/metadata/MetadataViewModel.kt b/app/src/main/java/com/sosauce/cutemusic/ui/screens/metadata/MetadataViewModel.kt index 8a83888..e0713e8 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/screens/metadata/MetadataViewModel.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/screens/metadata/MetadataViewModel.kt @@ -3,18 +3,35 @@ package com.sosauce.cutemusic.ui.screens.metadata import android.annotation.SuppressLint import android.app.Application import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.media.MediaScannerConnection import android.net.Uri import android.os.ParcelFileDescriptor import android.provider.MediaStore import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope +import com.kyant.taglib.AudioProperties +import com.kyant.taglib.AudioPropertiesReadStyle +import com.kyant.taglib.Metadata +import com.kyant.taglib.Picture +import com.kyant.taglib.TagLib import com.sosauce.cutemusic.data.actions.MetadataActions +import com.sosauce.cutemusic.utils.toAudioFileMetadata +import com.sosauce.cutemusic.utils.toModifiableMap +import com.sosauce.cutemusic.utils.toPropertyMap +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.ByteArrayOutputStream +import java.io.File import java.io.FileNotFoundException +// Inspired by Metadator and TagLib ! + class MetadataViewModel( private val application: Application ) : AndroidViewModel(application) { @@ -28,39 +45,110 @@ class MetadataViewModel( _metadata.value.mutablePropertiesMap.clear() } - private fun loadMetadataJAudio(path: String) { - -// val audioFile = AudioFileIO -// .read(File(path)) -// -// audioFile.tag.apply { -// val tagList = listOf( -// getFirst(FieldKey.TITLE), -// getFirst(FieldKey.ARTIST), -// getFirst(FieldKey.ALBUM), -// getFirst(FieldKey.YEAR), -// getFirst(FieldKey.GENRE), -// getFirst(FieldKey.TRACK), -// getFirst(FieldKey.DISC_NO), -// getFirst(FieldKey.LYRICS), -// ) -// -// -// tagList.forEach { -// _metadata.value.mutablePropertiesMap.add(it) -// } -// //_metadata.value.art = firstArtwork ?: null -// } + suspend fun loadMetadata() { + runCatching { + getFileDescriptorFromPath(application, metadataState.value.songPath)?.use { fd -> + val metadata = loadAudioMetadata(fd) + val audioProperties = loadAudioProperties(fd) + val audioArt = loadAudioArt(fd) + + _metadata.value = _metadata.value.copy( + metadata = metadata, + audioProperties = audioProperties, + art = audioArt + ) + + } + }.onSuccess { + metadataState.value.metadata?.propertyMap?.toModifiableMap()?.forEach { + metadataState.value.mutablePropertiesMap[it.key] = it.value ?: "" + } + } + } + + + private suspend fun loadAudioMetadata(songFd: ParcelFileDescriptor): Metadata? { + val fd = songFd.dup()?.detachFd() ?: throw NullPointerException() + + return withContext(Dispatchers.IO) { + TagLib.getMetadata(fd) + } + } + + private suspend fun loadAudioProperties( + songFd: ParcelFileDescriptor, + readStyle: AudioPropertiesReadStyle = AudioPropertiesReadStyle.Fast + ): AudioProperties? { + val fd = songFd.dup()?.detachFd() ?: throw NullPointerException() + + return withContext(Dispatchers.IO) { + TagLib.getAudioProperties(fd, readStyle) + } + } + + private suspend fun loadAudioArt(songFd: ParcelFileDescriptor): Picture? { + val fd = songFd.dup()?.detachFd() ?: throw NullPointerException() + + return withContext(Dispatchers.IO) { + TagLib.getFrontCover(fd) + } } + private fun saveAllChanges() { + try { + val fd = getFileDescriptorFromPath(application, metadataState.value.songPath, "w") + + + fd?.dup()?.detachFd()?.let { + TagLib.savePropertyMap(it, metadataState.value.mutablePropertiesMap.toAudioFileMetadata().toPropertyMap()) + } + + fd?.dup()?.detachFd()?.let { + if (metadataState.value.art != null) { + TagLib.savePictures(it, arrayOf(metadataState.value.art!!)) + } + } + + MediaScannerConnection.scanFile( + application.applicationContext, + arrayOf(metadataState.value.songPath), + null, + null + ) + } catch (e: Exception) { + Log.d("hello", "some error occured") + e.printStackTrace() + } } + private fun saveNewAudioArt(uri: Uri) { + // App will crash if it tries to open an input stream on an empty uri ! + if (uri == Uri.EMPTY) return - private fun clearState() { - _metadata.value.mutablePropertiesMap.clear() - //_metadata.value.art + val mimeType = application.contentResolver.getType(uri) + val byteArray = application.contentResolver.openInputStream(uri)?.use { inputStream -> + + val baos = ByteArrayOutputStream() + BitmapFactory.decodeStream(inputStream).apply { + compress(Bitmap.CompressFormat.JPEG, 100, baos) + } + + baos.toByteArray() + } + + + val picture = Picture( + data = byteArray ?: byteArrayOf(), + description = "", + pictureType = "Front Cover", + mimeType = mimeType ?: "image/jpeg" + ) + + _metadata.value = _metadata.value.copy( + art = picture + ) } @SuppressLint("Range") @@ -106,12 +194,15 @@ class MetadataViewModel( songPath = action.path, songUri = action.uri ) - loadMetadataJAudio(metadataState.value.songPath) + loadMetadata() } } + is MetadataActions.UpdateAudioArt -> { saveNewAudioArt(action.newArtUri) } - is MetadataActions.ClearState -> { - clearState() + is MetadataActions.RemoveArtwork -> { + _metadata.value = _metadata.value.copy( + art = null + ) } } } 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 9ae277c..440d28f 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 @@ -36,9 +36,7 @@ fun SettingsScreen( AppBar( title = stringResource(id = R.string.settings), showBackArrow = true, - showMenuIcon = false, - onPopBackStack = { onPopBackStack() }, - onNavigate = { onNavigate(it) } + onPopBackStack = { onPopBackStack() } ) }, modifier = Modifier diff --git a/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/AppBar.kt b/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/AppBar.kt index 6f618ad..1eccc5d 100644 --- a/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/AppBar.kt +++ b/app/src/main/java/com/sosauce/cutemusic/ui/shared_components/AppBar.kt @@ -2,14 +2,11 @@ package com.sosauce.cutemusic.ui.shared_components import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack -import androidx.compose.material.icons.rounded.Settings import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable -import com.sosauce.cutemusic.ui.navigation.Screen @OptIn(ExperimentalMaterial3Api::class) @@ -17,9 +14,7 @@ import com.sosauce.cutemusic.ui.navigation.Screen fun AppBar( title: String, showBackArrow: Boolean, - showMenuIcon: Boolean, onPopBackStack: (() -> Unit)? = null, - onNavigate: ((Screen) -> Unit)? = null ) { TopAppBar( title = { @@ -37,17 +32,6 @@ fun AppBar( ) } } - }, - actions = { - if (showMenuIcon) { - IconButton(onClick = { onNavigate?.invoke(Screen.Settings) }) { - Icon( - imageVector = Icons.Rounded.Settings, - contentDescription = "More", - tint = MaterialTheme.colorScheme.onBackground - ) - } - } } ) } 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 11918b1..8da77c2 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,7 +1,13 @@ package com.sosauce.cutemusic.ui.shared_components +import android.annotation.SuppressLint import android.app.Application import android.content.ComponentName +import android.content.Context +import android.net.Uri +import android.os.ParcelFileDescriptor +import android.provider.MediaStore +import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf @@ -16,6 +22,7 @@ import androidx.media3.common.util.UnstableApi import androidx.media3.session.MediaController import androidx.media3.session.SessionToken import com.google.common.util.concurrent.MoreExecutors +import com.kyant.taglib.TagLib import com.sosauce.cutemusic.data.MusicState import com.sosauce.cutemusic.data.actions.PlayerActions import com.sosauce.cutemusic.domain.model.Lyrics @@ -32,12 +39,11 @@ 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 +import java.io.FileNotFoundException class MusicViewModel( - application: Application, + private val application: Application, private val mediaStoreHelper: MediaStoreHelper ) : AndroidViewModel(application) { @@ -205,20 +211,45 @@ class MusicViewModel( } } - fun loadEmbeddedLyrics( - path: String - ): String { - val file = AudioFileIO.read(File(path)) + fun loadEmbeddedLyrics(): String { - file.tag.apply { - val embeddedLyrics = getFirst(FieldKey.LYRICS) + val fd = getFileDescriptorFromPath(application, musicState.value.currentPath) + return fd?.dup()?.detachFd()?.let { + TagLib.getMetadata(it)?.propertyMap["LYRICS"]?.getOrNull(0) ?: "No Lyrics Found !" + } ?: "No Lyrics Found !" - return if (embeddedLyrics != "") { - embeddedLyrics - } else { - "No lyrics found !" + } + + @SuppressLint("Range") + private fun getFileDescriptorFromPath( + context: Context, + filePath: String, + mode: String = "r" + ): ParcelFileDescriptor? { + val resolver = context.contentResolver + val uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + + val projection = arrayOf(MediaStore.Files.FileColumns._ID) + val selection = "${MediaStore.Files.FileColumns.DATA}=?" + val selectionArgs = arrayOf(filePath) + + resolver.query(uri, projection, selection, selectionArgs, null)?.use { cursor -> + if (cursor.moveToFirst()) { + val fileId = cursor.getInt(cursor.getColumnIndex(MediaStore.Files.FileColumns._ID)) + if (fileId == -1) { + return null + } else { + val fileUri = Uri.withAppendedPath(uri, fileId.toString()) + try { + return resolver.openFileDescriptor(fileUri, mode) + } catch (e: FileNotFoundException) { + Log.e("MediaStoreReceiver", "File not found: ${e.message}") + } + } } } + + return null } override fun onCleared() { 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 9cd783f..76e8f5c 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,36 +1,40 @@ package com.sosauce.cutemusic.ui.shared_components -import android.app.Application import android.net.Uri -import android.provider.MediaStore import android.util.Log import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.IntentSenderRequest import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.ViewModel 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.MediaStoreObserver -import com.sosauce.cutemusic.utils.ListToHandle -import com.sosauce.cutemusic.utils.SortingType +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 application: Application -) : AndroidViewModel(application) { + private val mediaStoreHelper: MediaStoreHelper +) : ViewModel() { - var musics by mutableStateOf( + + + var musics = mediaStoreHelper.fetchLatestMusics().stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5000), mediaStoreHelper.musics ) - var albums by mutableStateOf( - mediaStoreHelper.albums + var albums = mediaStoreHelper.fetchLatestAlbums().stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5000), + emptyList() ) var artists by mutableStateOf( @@ -41,35 +45,23 @@ class PostViewModel( mediaStoreHelper.folders ) - private val observer = MediaStoreObserver { - musics = mediaStoreHelper.fetchMusics() - } - - init { - application.contentResolver.registerContentObserver( - MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, - true, - observer - ) - } - companion object { + private companion object { const val CUTE_ERROR = "CuteError" } - override fun onCleared() { - super.onCleared() - application.contentResolver.unregisterContentObserver(observer) - } - var albumSongs by mutableStateOf(listOf()) var artistSongs by mutableStateOf(listOf()) var artistAlbums by mutableStateOf(listOf()) fun albumSongs(album: String) { try { - albumSongs = musics.filter { it.mediaMetadata.albumTitle.toString() == album } + viewModelScope.launch { + musics.collectLatest { + albumSongs = it.filter { it.mediaMetadata.albumTitle.toString() == album } + } + } } catch (e: Exception) { Log.e(CUTE_ERROR, e.message, e) } @@ -77,7 +69,11 @@ class PostViewModel( fun artistSongs(artistName: String) { try { - artistSongs = musics.filter { it.mediaMetadata.artist == artistName } + viewModelScope.launch { + musics.collectLatest { + artistSongs = it.filter { it.mediaMetadata.artist == artistName } + } + } } catch (e: Exception) { Log.e(CUTE_ERROR, e.message, e) } @@ -86,7 +82,7 @@ class PostViewModel( fun artistAlbums(artistName: String) { try { - artistAlbums = albums.filter { it.artist == artistName } + artistAlbums = albums.value.filter { it.artist == artistName } } catch (e: Exception) { Log.e(CUTE_ERROR, e.message, e) } @@ -117,66 +113,6 @@ class PostViewModel( } } - fun handleFiltering( - listToHandle: ListToHandle, - sortingType: SortingType, - ) { - 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 } - else - artists.sortedByDescending { it.name } - } - } - } - - fun handleSearch( - listToHandle: ListToHandle, - query: String = "" - ) { - when (listToHandle) { - ListToHandle.TRACKS -> { - musics = mediaStoreHelper.musics.filter { - it.mediaMetadata.title?.contains( - other = query, - ignoreCase = true - ) == true - } - } - ListToHandle.ALBUMS -> { - albums = mediaStoreHelper.albums.filter { - it.name.contains( - other = query, - ignoreCase = true - ) - } - } - - ListToHandle.ARTISTS -> { - artists = mediaStoreHelper.artists.filter { - it.name.contains( - other = query, - ignoreCase = true - ) - } - } - } - } } diff --git a/app/src/main/java/com/sosauce/cutemusic/utils/Enums.kt b/app/src/main/java/com/sosauce/cutemusic/utils/Enums.kt index 9e6bc28..e69edd1 100644 --- a/app/src/main/java/com/sosauce/cutemusic/utils/Enums.kt +++ b/app/src/main/java/com/sosauce/cutemusic/utils/Enums.kt @@ -1,12 +1 @@ package com.sosauce.cutemusic.utils - -enum class ListToHandle { - TRACKS, - ALBUMS, - ARTISTS -} - -enum class SortingType { - ASCENDING, - DESCENDING -} \ 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 3de9a1c..c6df397 100644 --- a/app/src/main/java/com/sosauce/cutemusic/utils/Extensions.kt +++ b/app/src/main/java/com/sosauce/cutemusic/utils/Extensions.kt @@ -1,10 +1,13 @@ package com.sosauce.cutemusic.utils +import android.content.ContentResolver import android.content.Context import android.content.Intent +import android.database.ContentObserver import android.media.MediaMetadataRetriever import android.net.Uri import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -13,7 +16,10 @@ import androidx.compose.ui.unit.dp import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackParameters import androidx.media3.common.Player +import com.kyant.taglib.PropertyMap import com.sosauce.cutemusic.data.datastore.rememberIsLandscape +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow import java.util.Locale fun Modifier.thenIf( @@ -158,6 +164,77 @@ fun Long.formatToReadableTime(): String { return String.format(Locale.getDefault(), "%d:%02d", minutes, seconds) } +fun PropertyMap.toModifiableMap(separator: String = ", "): MutableMap { + return mutableMapOf( + "TITLE" to this["TITLE"]?.getOrNull(0), + "ARTIST" to this["ARTIST"]?.joinToString(separator), + "ALBUM" to this["ALBUM"]?.getOrNull(0), + "TRACKNUMBER" to this["TRACKNUMBER"]?.getOrNull(0), + "DISCNUMBER" to this["DISCNUMBER"]?.getOrNull(0), + "DATE" to this["DATE"]?.getOrNull(0), + "GENRE" to this["GENRE"]?.joinToString(separator), + "LYRICS" to this["LYRICS"]?.getOrNull(0), + "DATE" to this["DATE"]?.getOrNull(0), + ) +} + +fun String?.formatForField(separator: String = ","): Array { + return this?.split(separator)?.map { it.trim() }?.toTypedArray() ?: arrayOf(this ?: "") +} + + + +@Stable +data class AudioFileMetadata( + val title: String?, + val artist: String?, + val album: String?, + val trackNumber: String?, + val discNumber: String?, + val date: String?, + val genre: String?, + val lyrics: String? +) + +fun Map.toAudioFileMetadata(): AudioFileMetadata { + return AudioFileMetadata( + title = this["TITLE"], + artist = this["ARTIST"], + album = this["ALBUM"], + trackNumber = this["TRACKNUMBER"], + discNumber = this["DISCNUMBER"], + date = this["DATE"], + genre = this["GENRE"], + lyrics = this["LYRICS"] + ) +} + +fun AudioFileMetadata.toPropertyMap(): PropertyMap { + return hashMapOf( + "TITLE" to arrayOf(title ?: ""), + "ARTIST" to artist.formatForField(), + "ALBUM" to arrayOf(album ?: ""), + "TRACKNUMBER" to arrayOf(trackNumber ?: ""), + "DISCNUMBER" to arrayOf(discNumber ?: ""), + "DATE" to arrayOf(date ?: ""), + "GENRE" to genre.formatForField(), + "LYRICS" to arrayOf(lyrics ?: "") + ) +} + +fun ContentResolver.observe(uri: Uri) = callbackFlow { + val observer = object : ContentObserver(null) { + override fun onChange(selfChange: Boolean) { + trySend(selfChange) + } + } + registerContentObserver(uri, true, observer) + trySend(false) + awaitClose { + unregisterContentObserver(observer) + } +} + @Composable fun rememberSearchbarAlignment( ): Alignment { 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 4e3bc77..973f713 100644 --- a/app/src/main/java/com/sosauce/cutemusic/utils/ImageUtils.kt +++ b/app/src/main/java/com/sosauce/cutemusic/utils/ImageUtils.kt @@ -3,6 +3,7 @@ package com.sosauce.cutemusic.utils import android.content.ContentUris import android.content.Context import androidx.core.net.toUri +import coil3.request.CachePolicy import coil3.request.crossfade import coil3.request.transformations import coil3.transform.RoundedCornersTransformation @@ -17,6 +18,8 @@ object ImageUtils { .transformations( RoundedCornersTransformation(15f) ) + .diskCacheKey(img.toString()) + .memoryCacheKey(img.toString()) .build() return request diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5775c8b..8f5ef37 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,5 @@ [versions] agp = "8.7.2" -jaudiotagger = "3.0.1" koinAndroid = "4.0.0" koinAndroidxCompose = "4.0.0" koinAndroidxStartup = "4.0.0" @@ -20,6 +19,7 @@ media3Session = "1.5.0" navigationCompose = "2.8.4" squigglyslider = "1.0.0" serialization = "2.0.0" +taglib = "1.0.0-alpha25" [libraries] androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } @@ -39,12 +39,12 @@ androidx-navigation-compose = { module = "androidx.navigation:navigation-compose androidx-ui = { module = "androidx.compose.ui:ui" } androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling" } coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coilCompose" } -jaudiotagger = { module = "net.jthink:jaudiotagger", version.ref = "jaudiotagger" } koin-android = { module = "io.insert-koin:koin-android", version.ref = "koinAndroid" } koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koinAndroidxCompose" } koin-androidx-startup = { module = "io.insert-koin:koin-androidx-startup", version.ref = "koinAndroidxStartup" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" } squigglyslider = { module = "me.saket.squigglyslider:squigglyslider", version.ref = "squigglyslider" } +taglib = { module = "com.github.Kyant0:taglib", version.ref = "taglib" } [plugins] androidApplication = { id = "com.android.application", version.ref = "agp" }