From e2213ab7da15e79c8f446471647b78c527eadfbc Mon Sep 17 00:00:00 2001 From: Nguyen Duc Tuan Minh Date: Sat, 9 Nov 2024 16:29:19 +0700 Subject: [PATCH] Local Playlist Shuffle --- .../maxrave/simpmusic/data/db/Converters.kt | 11 ++ .../maxrave/simpmusic/data/db/DatabaseDao.kt | 3 + .../simpmusic/data/db/LocalDataSource.kt | 5 + .../data/manager/LocalPlaylistManager.kt | 5 + .../data/repository/MainRepository.kt | 8 ++ .../service/SimpleMediaServiceHandler.kt | 133 +++++++++++++----- .../ui/screen/player/NowPlayingScreen.kt | 8 +- .../viewModel/LocalPlaylistViewModel.kt | 59 ++++---- .../simpmusic/viewModel/base/BaseViewModel.kt | 4 + 9 files changed, 167 insertions(+), 69 deletions(-) diff --git a/app/src/main/java/com/maxrave/simpmusic/data/db/Converters.kt b/app/src/main/java/com/maxrave/simpmusic/data/db/Converters.kt index e8370f1e..d65d6b0d 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/db/Converters.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/db/Converters.kt @@ -25,6 +25,17 @@ class Converters { return gson.toJson(list) } + // No use in database + fun fromListIntToString(list: List?): String? { + val gson = Gson() + return gson.toJson(list) + } + + fun fromStringToListInt(value: String?): List? { + val listType: Type = object : TypeToken?>() {}.type + return Gson().fromJson(value, listType) + } + @TypeConverter fun fromStringToListTrack(value: String?): List? { val listType: Type = object : TypeToken?>() {}.type diff --git a/app/src/main/java/com/maxrave/simpmusic/data/db/DatabaseDao.kt b/app/src/main/java/com/maxrave/simpmusic/data/db/DatabaseDao.kt index 4475c162..347608e8 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/db/DatabaseDao.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/db/DatabaseDao.kt @@ -420,6 +420,9 @@ interface DatabaseDao { @Query("SELECT * FROM pair_song_local_playlist WHERE playlistId = :playlistId") suspend fun getPlaylistPairSong(playlistId: Long): List? + @Query("SELECT * FROM pair_song_local_playlist WHERE playlistId = :playlistId AND position in (:positionList)") + suspend fun getPlaylistPairSongByListPosition(playlistId: Long, positionList: List): List? + @Query( "SELECT * FROM pair_song_local_playlist WHERE playlistId = :playlistId ORDER BY position " + "ASC LIMIT 50 OFFSET :offset", diff --git a/app/src/main/java/com/maxrave/simpmusic/data/db/LocalDataSource.kt b/app/src/main/java/com/maxrave/simpmusic/data/db/LocalDataSource.kt index 364c56c3..dbaf54e1 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/db/LocalDataSource.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/db/LocalDataSource.kt @@ -245,6 +245,11 @@ class LocalDataSource( suspend fun getPlaylistPairSong(playlistId: Long) = databaseDao.getPlaylistPairSong(playlistId) + suspend fun getPlaylistPairSongByListPosition( + playlistId: Long, + listPosition: List, + ) = databaseDao.getPlaylistPairSongByListPosition(playlistId, listPosition) + suspend fun getPlaylistPairSongByOffset( playlistId: Long, offset: Int, diff --git a/app/src/main/java/com/maxrave/simpmusic/data/manager/LocalPlaylistManager.kt b/app/src/main/java/com/maxrave/simpmusic/data/manager/LocalPlaylistManager.kt index 452582ec..20c264bd 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/manager/LocalPlaylistManager.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/manager/LocalPlaylistManager.kt @@ -100,6 +100,11 @@ class LocalPlaylistManager( return tracks } + suspend fun getListTrackVideoId(id: Long): List { + val playlist = localDataSource.getLocalPlaylist(id) + return playlist.tracks ?: emptyList() + } + suspend fun insertLocalPlaylist(localPlaylist: LocalPlaylistEntity): Flow> = wrapMessageResource( successMessage = getString(R.string.added_local_playlist), diff --git a/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt b/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt index b00d438c..2f74b3a3 100644 --- a/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt +++ b/app/src/main/java/com/maxrave/simpmusic/data/repository/MainRepository.kt @@ -457,6 +457,14 @@ class MainRepository( emit(localDataSource.getPlaylistPairSong(playlistId)) }.flowOn(Dispatchers.IO) + suspend fun getPlaylistPairSongByListPosition( + playlistId: Long, + listPosition: List, + ): Flow?> = + flow { + emit(localDataSource.getPlaylistPairSongByListPosition(playlistId, listPosition)) + }.flowOn(Dispatchers.IO) + suspend fun getPlaylistPairSongByOffset( playlistId: Long, offset: Int, diff --git a/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaServiceHandler.kt b/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaServiceHandler.kt index a67b9378..75f685f3 100644 --- a/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaServiceHandler.kt +++ b/app/src/main/java/com/maxrave/simpmusic/service/SimpleMediaServiceHandler.kt @@ -36,6 +36,7 @@ import com.maxrave.simpmusic.common.MEDIA_CUSTOM_COMMAND import com.maxrave.simpmusic.common.SPONSOR_BLOCK import com.maxrave.simpmusic.data.dataStore.DataStoreManager import com.maxrave.simpmusic.data.dataStore.DataStoreManager.Settings.TRUE +import com.maxrave.simpmusic.data.db.Converters import com.maxrave.simpmusic.data.db.entities.NewFormatEntity import com.maxrave.simpmusic.data.db.entities.SongEntity import com.maxrave.simpmusic.data.model.browse.album.Track @@ -91,6 +92,7 @@ class SimpleMediaServiceHandler( @Suppress("ktlint:standard:property-naming") private val TAG = "SimpleMediaServiceHandler" + private val converter = Converters() private var loudnessEnhancer: LoudnessEnhancer? = null @@ -860,6 +862,42 @@ class SimpleMediaServiceHandler( } } + fun shufflePlaylist(randomTrackIndex: Int = 0) { + val playlistId = _queueData.value?.playlistId ?: return + val firstPlayedTrack = _queueData.value?.firstPlayedTrack ?: return + coroutineScope.launch { + if (playlistId.startsWith(LOCAL_PLAYLIST_ID)) { + _stateFlow.value = StateSource.STATE_INITIALIZING + mainRepository.insertSong(firstPlayedTrack.toSongEntity()).collect { + Log.w(TAG, "Inserted song: ${firstPlayedTrack.title}") + } + clearMediaItems() + firstPlayedTrack.durationSeconds?.let { + mainRepository.updateDurationSeconds(it, firstPlayedTrack.videoId) + } + addMediaItem(firstPlayedTrack.toMediaItem(), playWhenReady = true) + val longId = playlistId.replace(LOCAL_PLAYLIST_ID, "").toLong() + val localPlaylist = mainRepository.getLocalPlaylist(longId).singleOrNull() + if (localPlaylist != null) { + Log.w(TAG, "shufflePlaylist: Local playlist track size ${localPlaylist.tracks?.size}") + val trackCount = localPlaylist.tracks?.size ?: return@launch + val listPosition = (0 until trackCount).toMutableList().apply { + remove(randomTrackIndex) + } + if (listPosition.size <= 0) return@launch + listPosition.shuffle() + _queueData.update { + it?.copy( + //After shuffle prefix is offset and list position + continuation = "SHUFFLE0_${converter.fromListIntToString(listPosition)}", + ) + } + loadMore() + } + } + } + } + fun loadMore() { // Separate local and remote data // Local Add Prefix to PlaylistID to differentiate between local and remote @@ -874,42 +912,69 @@ class SimpleMediaServiceHandler( _stateFlow.value = StateSource.STATE_INITIALIZING val longId = playlistId.replace(LOCAL_PLAYLIST_ID, "").toLong() Log.w("Check loadMore", longId.toString()) - val filter = if (continuation.startsWith(ASC)) FilterState.OlderFirst else FilterState.NewerFirst - val offset = - if (filter == FilterState.OlderFirst) { - continuation - .removePrefix( - ASC, - ).toInt() - } else { - continuation.removePrefix(DESC).toInt() - } - val total = - mainRepository - .getLocalPlaylist(longId) - .firstOrNull() - ?.tracks - ?.size ?: 0 - mainRepository - .getPlaylistPairSongByOffset( + if (continuation.startsWith("SHUFFLE")) { + val regex = Regex("(?<=SHUFFLE)\\d+(?=_)") + var offset = regex.find(continuation)?.value?.toInt() ?: return@launch + val posString = continuation.removePrefix("SHUFFLE${offset}_") + val listPosition = converter.fromStringToListInt(posString) ?: return@launch + val theLastLoad = 50*(offset + 1) >= listPosition.size + mainRepository.getPlaylistPairSongByListPosition( longId, - offset, - filter, - total, - ).singleOrNull() - ?.let { pair -> + listPosition.subList(50*offset, if (theLastLoad) listPosition.size - 1 else 50*(offset + 1)), + ).singleOrNull()?.let { pair -> Log.w("Check loadMore response", pair.size.toString()) mainRepository.getSongsByListVideoId(pair.map { it.songId }).single().let { songs -> if (songs.isNotEmpty()) { delay(300) loadMoreCatalog(songs.toArrayListTrack()) -// Queue.setContinuation( -// playlistId, -// if (filter == FilterState.OlderFirst) ASC + (offset + 1) else DESC + (offset + 1).toString(), -// ) - _queueData.value = - _queueData.value?.copy( - continuation = + offset++ + _queueData.update { + if (!theLastLoad){ + it?.copy( + continuation = "SHUFFLE${offset}_$posString", + ) + } else { + it?.copy( + continuation = null + ) + } + } + } + } + } + } else { + val filter = if (continuation.startsWith(ASC)) FilterState.OlderFirst else FilterState.NewerFirst + val offset = + if (filter == FilterState.OlderFirst) { + continuation + .removePrefix( + ASC, + ).toInt() + } else { + continuation.removePrefix(DESC).toInt() + } + val total = + mainRepository + .getLocalPlaylist(longId) + .firstOrNull() + ?.tracks + ?.size ?: 0 + mainRepository + .getPlaylistPairSongByOffset( + longId, + offset, + filter, + total, + ).singleOrNull() + ?.let { pair -> + Log.w("Check loadMore response", pair.size.toString()) + mainRepository.getSongsByListVideoId(pair.map { it.songId }).single().let { songs -> + if (songs.isNotEmpty()) { + delay(300) + loadMoreCatalog(songs.toArrayListTrack()) + _queueData.value = + _queueData.value?.copy( + continuation = if (filter == FilterState.OlderFirst ) { @@ -917,11 +982,13 @@ class SimpleMediaServiceHandler( } else { DESC + (offset + 1).toString() }, - ) + ) + } else { + _stateFlow.value = StateSource.STATE_INITIALIZED + } } -// loadingMore.value = false } - } + } } } else { coroutineScope.launch { diff --git a/app/src/main/java/com/maxrave/simpmusic/ui/screen/player/NowPlayingScreen.kt b/app/src/main/java/com/maxrave/simpmusic/ui/screen/player/NowPlayingScreen.kt index db6fbb93..b75223e7 100644 --- a/app/src/main/java/com/maxrave/simpmusic/ui/screen/player/NowPlayingScreen.kt +++ b/app/src/main/java/com/maxrave/simpmusic/ui/screen/player/NowPlayingScreen.kt @@ -755,7 +755,13 @@ fun NowPlayingScreen( .fillMaxWidth() .wrapContentHeight(align = Alignment.CenterVertically) .basicMarquee(animationMode = MarqueeAnimationMode.Immediately) - .focusable(), + .focusable() + .clickable { + navController.navigateSafe( + R.id.action_global_artistFragment, + bundleOf("channelId" to screenDataState.songInfoData?.authorId), + ) + }, ) } Spacer(modifier = Modifier.size(10.dp)) diff --git a/app/src/main/java/com/maxrave/simpmusic/viewModel/LocalPlaylistViewModel.kt b/app/src/main/java/com/maxrave/simpmusic/viewModel/LocalPlaylistViewModel.kt index c05d592f..11b41aea 100644 --- a/app/src/main/java/com/maxrave/simpmusic/viewModel/LocalPlaylistViewModel.kt +++ b/app/src/main/java/com/maxrave/simpmusic/viewModel/LocalPlaylistViewModel.kt @@ -830,45 +830,34 @@ class LocalPlaylistViewModel( ) } is LocalPlaylistUIEvent.ShuffleClick -> { - val loadedList = lazyTrackPagingItems.value?.itemSnapshotList?.toList().let { - if (it.isNullOrEmpty()) { + viewModelScope.launch { + val listVideoId = localPlaylistManager.getListTrackVideoId(uiState.value.id) + log("ShuffleClick: uiState id ${uiState.value.id}", Log.DEBUG) + log("ShuffleClick: $listVideoId", Log.DEBUG) + if (listVideoId.isEmpty()) { makeToast(getString(R.string.playlist_is_empty)) - return - } else { - it.filterNotNull().toArrayListTrack() + return@launch } - } - val random = loadedList.random() - setQueueData( - QueueData( - listTracks = loadedList, - firstPlayedTrack = random, - playlistId = LOCAL_PLAYLIST_ID + uiState.value.id, - playlistName = "${ - getString( - R.string.playlist, - ) - } \"${uiState.value.title}\"", - playlistType = PlaylistType.LOCAL_PLAYLIST, - continuation = - if (offset.value > 0) { - if (uiState.value.filterState == FilterState.OlderFirst) { - (ASC + offset.toString()) - } else { - (DESC + offset) - } - } else { - null - }, + val random = listVideoId.random() + val randomIndex = listVideoId.indexOf(random) + val firstPlayedTrack = mainRepository.getSongById(random).singleOrNull()?.toTrack() ?: return@launch + setQueueData( + QueueData( + listTracks = arrayListOf(firstPlayedTrack), + firstPlayedTrack = firstPlayedTrack, + playlistId = LOCAL_PLAYLIST_ID + uiState.value.id, + playlistName = "${ + getString( + R.string.playlist, + ) + } \"${uiState.value.title}\"", + playlistType = PlaylistType.LOCAL_PLAYLIST, + continuation = "" + ) ) - ) - loadMediaItem( - random, - Config.PLAYLIST_CLICK, - loadedList.indexOf(random) - ) + shufflePlaylist(randomIndex) + } } - } } diff --git a/app/src/main/java/com/maxrave/simpmusic/viewModel/base/BaseViewModel.kt b/app/src/main/java/com/maxrave/simpmusic/viewModel/base/BaseViewModel.kt index 30b4df75..9fd83f72 100644 --- a/app/src/main/java/com/maxrave/simpmusic/viewModel/base/BaseViewModel.kt +++ b/app/src/main/java/com/maxrave/simpmusic/viewModel/base/BaseViewModel.kt @@ -128,5 +128,9 @@ abstract class BaseViewModel( } } + fun shufflePlaylist(firstPlayIndex: Int = 0) { + simpleMediaServiceHandler.shufflePlaylist(firstPlayIndex) + } + fun getQueueData() = simpleMediaServiceHandler.queueData.value } \ No newline at end of file