From ac9309f66e6a8676e2d10b32875973a8686e9eef Mon Sep 17 00:00:00 2001 From: Ayaan Hussain Date: Fri, 15 May 2026 02:45:10 +0530 Subject: [PATCH 1/2] feat: Added tab switcher in CreatePlaylistScreen & SongPickerBottomSheet to filter Local, Cloud songs fix:favorite Filter chip wasn't working --- .../pixelplay/data/database/MusicDao.kt | 8 +- .../components/SongPickerBottomSheet.kt | 199 +++++++++++++----- .../screens/CreatePlaylistScreen.kt | 145 ++++++++++++- .../presentation/viewmodel/PlayerViewModel.kt | 52 ++++- app/src/main/res/drawable/ic_phone.xml | 24 +++ app/src/main/res/drawable/ic_phonef.xml | 24 +++ .../values/strings_presentation_batch_c.xml | 6 +- 7 files changed, 385 insertions(+), 73 deletions(-) create mode 100644 app/src/main/res/drawable/ic_phone.xml create mode 100644 app/src/main/res/drawable/ic_phonef.xml diff --git a/app/src/main/java/com/theveloper/pixelplay/data/database/MusicDao.kt b/app/src/main/java/com/theveloper/pixelplay/data/database/MusicDao.kt index 74dc6b479..1ea18c03b 100644 --- a/app/src/main/java/com/theveloper/pixelplay/data/database/MusicDao.kt +++ b/app/src/main/java/com/theveloper/pixelplay/data/database/MusicDao.kt @@ -669,7 +669,7 @@ interface MusicDao { */ @Query(""" SELECT * FROM songs - WHERE (:applyDirectoryFilter = 0 OR parent_directory_path IN (:allowedParentDirs)) + WHERE (:applyDirectoryFilter = 0 OR id < 0 OR parent_directory_path IN (:allowedParentDirs)) AND ( :filterMode = 0 OR ( @@ -753,7 +753,7 @@ interface MusicDao { @Query(""" SELECT songs.* FROM songs INNER JOIN favorites ON songs.id = favorites.songId AND favorites.isFavorite = 1 - WHERE (:applyDirectoryFilter = 0 OR songs.parent_directory_path IN (:allowedParentDirs)) + WHERE (:applyDirectoryFilter = 0 OR songs.id < 0 OR songs.parent_directory_path IN (:allowedParentDirs)) AND ( :filterMode = 0 OR ( @@ -790,7 +790,7 @@ interface MusicDao { @Query(""" SELECT songs.* FROM songs INNER JOIN favorites ON songs.id = favorites.songId AND favorites.isFavorite = 1 - WHERE (:applyDirectoryFilter = 0 OR songs.parent_directory_path IN (:allowedParentDirs)) + WHERE (:applyDirectoryFilter = 0 OR songs.id < 0 OR songs.parent_directory_path IN (:allowedParentDirs)) AND ( :filterMode = 0 OR ( @@ -854,7 +854,7 @@ interface MusicDao { @Query(""" SELECT COUNT(*) FROM songs INNER JOIN favorites ON songs.id = favorites.songId AND favorites.isFavorite = 1 - WHERE (:applyDirectoryFilter = 0 OR songs.parent_directory_path IN (:allowedParentDirs)) + WHERE (:applyDirectoryFilter = 0 OR songs.id < 0 OR songs.parent_directory_path IN (:allowedParentDirs)) AND ( :filterMode = 0 OR ( diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/SongPickerBottomSheet.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/SongPickerBottomSheet.kt index f99023401..1fbe46b1d 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/SongPickerBottomSheet.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/SongPickerBottomSheet.kt @@ -1,5 +1,6 @@ package com.theveloper.pixelplay.presentation.components +import androidx.annotation.OptIn import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -17,10 +18,20 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.rounded.Check +import androidx.compose.material.icons.rounded.AudioFile +import androidx.compose.material.icons.rounded.Cloud import androidx.compose.material.icons.rounded.Favorite import androidx.compose.material.icons.rounded.Search import androidx.compose.material3.Button @@ -29,15 +40,20 @@ import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExtendedFloatingActionButton +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.FilledIconButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.PrimaryTabRow import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextFieldDefaults +import com.theveloper.pixelplay.data.model.StorageFilter import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -50,12 +66,16 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.media3.common.util.UnstableApi import androidx.paging.LoadState import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems @@ -63,12 +83,14 @@ import androidx.paging.compose.itemContentType import coil.size.Size import com.theveloper.pixelplay.R import com.theveloper.pixelplay.data.model.Song +import com.theveloper.pixelplay.presentation.screens.TabAnimation import com.theveloper.pixelplay.presentation.viewmodel.PlayerViewModel import com.theveloper.pixelplay.ui.theme.GoogleSansRounded import kotlinx.coroutines.flow.map import racra.compose.smooth_corner_rect_library.AbsoluteSmoothCornerShape -@OptIn(ExperimentalMaterial3Api::class) +@OptIn(UnstableApi::class) +@ExperimentalMaterial3Api @Composable fun SongPickerBottomSheet( initiallySelectedSongIds: Set, @@ -97,65 +119,122 @@ fun SongPickerBottomSheet( } } +@OptIn(UnstableApi::class) @Composable fun SongPickerContent( selectedSongIds: MutableMap, onConfirm: (Set) -> Unit, playerViewModel: PlayerViewModel = hiltViewModel() ) { - Box( - modifier = Modifier.fillMaxSize() - ) { - Scaffold( - topBar = { - Row( + Scaffold( + containerColor = Color.Transparent, + topBar = { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 26.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + stringResource(R.string.song_picker_title), + style = MaterialTheme.typography.displaySmall, + fontFamily = GoogleSansRounded + ) + } + }, + bottomBar = { + Row( + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding() + .padding(horizontal = 16.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + val storageFilter by playerViewModel.playlistPickerStorageFilter.collectAsStateWithLifecycle() + val tabs = listOf( + StorageFilter.OFFLINE to R.string.library_storage_filter_offline, + StorageFilter.ONLINE to R.string.library_storage_filter_online + ) + val selectedTabIndex = tabs.indexOfFirst { it.first == storageFilter }.coerceAtLeast(0) + + PrimaryTabRow( + selectedTabIndex = selectedTabIndex, modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 26.dp, vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween + .weight(1f) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainerHigh) + .padding(5.dp), + containerColor = Color.Transparent, + divider = {}, + indicator = {} ) { - Text( - stringResource(R.string.song_picker_title), - style = MaterialTheme.typography.displaySmall, - fontFamily = GoogleSansRounded - ) + tabs.forEachIndexed { index, (filter, labelRes) -> + TabAnimation( + index = index, + title = stringResource(labelRes), + selectedIndex = selectedTabIndex, + onClick = { playerViewModel.setPlaylistPickerStorageFilter(filter) }, + transformOrigin = if (index == 0) TransformOrigin(0f, 0.5f) else TransformOrigin(1f, 0.5f) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + if (filter == StorageFilter.OFFLINE) { + Icon( + painter = painterResource(R.drawable.ic_phonef), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } else { + Icon( + imageVector = Icons.Rounded.Cloud, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } + Spacer(Modifier.width(8.dp)) + Text( + text = stringResource(labelRes), + fontFamily = GoogleSansRounded, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(end = 4.dp) + ) + } + } + } } - }, - floatingActionButton = { - ExtendedFloatingActionButton( - modifier = Modifier.padding(bottom = 18.dp, end = 8.dp), - shape = CircleShape, + + FilledIconButton( onClick = { onConfirm(selectedSongIds.filterValues { it }.keys) }, - icon = { Icon(Icons.Rounded.Check, stringResource(R.string.cd_confirm_add_songs)) }, - text = { Text(stringResource(R.string.song_picker_action_add)) }, - ) + modifier = Modifier.size(56.dp), + shape = RoundedCornerShape(16.dp), + colors = IconButtonDefaults.filledIconButtonColors( + containerColor = MaterialTheme.colorScheme.tertiaryContainer, + contentColor = MaterialTheme.colorScheme.onTertiaryContainer + ) + ) { + Icon( + Icons.Rounded.Check, + contentDescription = stringResource(R.string.cd_confirm_add_songs), + modifier = Modifier.size(28.dp) + ) + } } - ) { innerPadding -> - SongPickerSelectionPane( - selectedSongIds = selectedSongIds, - modifier = Modifier.padding(innerPadding), - contentPadding = PaddingValues(bottom = 100.dp, top = 8.dp), - playerViewModel = playerViewModel - ) } - Box( - modifier = Modifier - .align(Alignment.BottomCenter) - .fillMaxWidth() - .height(30.dp) - .background( - brush = Brush.verticalGradient( - listOf( - Color.Transparent, - MaterialTheme.colorScheme.surface - ) - ) - ) + ) { innerPadding -> + SongPickerSelectionPane( + selectedSongIds = selectedSongIds, + modifier = Modifier.fillMaxSize().padding(top = innerPadding.calculateTopPadding()), + contentPadding = PaddingValues(bottom = 120.dp, top = 8.dp), + playerViewModel = playerViewModel ) } } +@OptIn(UnstableApi::class) @Composable fun SongPickerSelectionPane( selectedSongIds: MutableMap, @@ -165,13 +244,32 @@ fun SongPickerSelectionPane( ) { var searchQuery by remember { mutableStateOf("") } var favoritesOnly by remember { mutableStateOf(false) } + val storageFilter by playerViewModel.playlistPickerStorageFilter.collectAsStateWithLifecycle() + val pagedSongs = playerViewModel.playlistPickerSongs.collectAsLazyPagingItems() + val pagedFavoriteSongs = playerViewModel.playlistPickerFavoriteSongs.collectAsLazyPagingItems() + val favoriteIds by playerViewModel.favoriteSongIds.collectAsStateWithLifecycle() val searchResultsInitialValue: List? = remember(searchQuery) { if (searchQuery.isBlank()) emptyList() else null } - val searchResults by remember(searchQuery, playerViewModel) { + val searchResults by remember(searchQuery, playerViewModel, storageFilter) { playerViewModel.searchSongs(searchQuery) + .map { songs -> + when (storageFilter) { + StorageFilter.OFFLINE -> songs.filter { s -> + s.telegramFileId == null && s.neteaseId == null && s.gdriveFileId == null && + s.qqMusicMid == null && s.navidromeId == null && s.jellyfinId == null + } + + StorageFilter.ONLINE -> songs.filter { s -> + s.telegramFileId != null || s.neteaseId != null || s.gdriveFileId != null || + s.qqMusicMid != null || s.navidromeId != null || s.jellyfinId != null + } + + else -> songs + } + } .map, List?> { it } }.collectAsStateWithLifecycle(initialValue = searchResultsInitialValue) @@ -208,6 +306,7 @@ fun SongPickerSelectionPane( selected = favoritesOnly, onClick = { favoritesOnly = !favoritesOnly }, label = { Text(stringResource(R.string.song_picker_filter_favorites)) }, + shape = CircleShape, leadingIcon = { Icon( imageVector = Icons.Rounded.Favorite, @@ -233,12 +332,8 @@ fun SongPickerSelectionPane( ) } favoritesOnly -> { - val favoriteSongs = remember(pagedSongs.itemSnapshotList.items, favoriteIds) { - pagedSongs.itemSnapshotList.items.filter { it.id in favoriteIds } - } - SongPickerList( - filteredSongs = favoriteSongs, - isLoading = pagedSongs.loadState.refresh is LoadState.Loading && pagedSongs.itemCount == 0, + SongPickerPagingList( + pagedSongs = pagedFavoriteSongs, selectedSongIds = selectedSongIds, albumShape = albumShape, modifier = Modifier.weight(1f), diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/CreatePlaylistScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/CreatePlaylistScreen.kt index 4b89ad755..b1fa37677 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/CreatePlaylistScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/CreatePlaylistScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith +import androidx.annotation.OptIn import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -46,8 +47,10 @@ import androidx.compose.material.icons.automirrored.rounded.ArrowForward import androidx.compose.material.icons.filled.AddPhotoAlternate import androidx.compose.material.icons.rounded.AddPhotoAlternate import androidx.compose.material.icons.rounded.Album +import androidx.compose.material.icons.rounded.AudioFile import androidx.compose.material.icons.rounded.AutoAwesome import androidx.compose.material.icons.rounded.Check +import androidx.compose.material.icons.rounded.Cloud import androidx.compose.material.icons.rounded.Close import androidx.compose.material.icons.rounded.Favorite import androidx.compose.material.icons.rounded.GridView @@ -95,6 +98,7 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @@ -126,6 +130,7 @@ import androidx.compose.ui.graphics.Matrix import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.lazy.LazyRow import androidx.compose.material.icons.automirrored.rounded.QueueMusic import androidx.compose.material.icons.rounded.Clear @@ -134,6 +139,13 @@ import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.PrimaryTabRow +import androidx.compose.material3.Surface +import androidx.compose.ui.graphics.TransformOrigin +import com.theveloper.pixelplay.data.model.StorageFilter +import com.theveloper.pixelplay.presentation.viewmodel.PlayerViewModel +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.media3.common.util.UnstableApi import androidx.compose.material3.MediumExtendedFloatingActionButton import androidx.compose.material3.SliderDefaults @@ -146,7 +158,9 @@ import androidx.compose.ui.text.style.TextGeometricTransform import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.sp import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.theveloper.pixelplay.R +import com.theveloper.pixelplay.presentation.screens.TabAnimation data class Quadruple(val first: A, val second: B, val third: C, val fourth: D) @@ -263,12 +277,13 @@ fun EditPlaylistDialog( } } -@OptIn(ExperimentalMaterial3Api::class) -@Composable +@OptIn(UnstableApi::class) +@androidx.compose.runtime.Composable private fun CreatePlaylistContent( onDismiss: () -> Unit, onGenerateClick: () -> Unit, - onCreate: (String, String?, Int?, String?, List, Float, Float, Float, String?, Float?, Float?, Float?, Float?, String?) -> Unit + onCreate: (String, String?, Int?, String?, List, Float, Float, Float, String?, Float?, Float?, Float?, Float?, String?) -> Unit, + playerViewModel: PlayerViewModel = hiltViewModel() ) { val context = LocalContext.current @@ -396,7 +411,7 @@ private fun CreatePlaylistContent( ) }, floatingActionButton = { - if (!showCropUi) { + if (!showCropUi && !(currentStep == 1 && creationMode == PlaylistCreationMode.MANUAL)) { MediumExtendedFloatingActionButton( text = { Text( @@ -498,6 +513,120 @@ private fun CreatePlaylistContent( ) } }, + bottomBar = { + if (currentStep == 1 && creationMode == PlaylistCreationMode.MANUAL) { + Row( + modifier = Modifier + .fillMaxWidth() + .navigationBarsPadding() + .padding(horizontal = 16.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + val storageFilter by playerViewModel.playlistPickerStorageFilter.collectAsStateWithLifecycle() + val tabs = listOf( + StorageFilter.OFFLINE to R.string.library_storage_filter_offline, + StorageFilter.ONLINE to R.string.library_storage_filter_online + ) + val selectedTabIndex = tabs.indexOfFirst { it.first == storageFilter }.coerceAtLeast(0) + + PrimaryTabRow( + selectedTabIndex = selectedTabIndex, + modifier = Modifier + .weight(1f) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surfaceContainerHigh) + .padding(5.dp), + containerColor = Color.Transparent, + divider = {}, + indicator = {} + ) { + tabs.forEachIndexed { index, (filter, labelRes) -> + TabAnimation( + index = index, + title = stringResource(labelRes), + selectedIndex = selectedTabIndex, + onClick = { playerViewModel.setPlaylistPickerStorageFilter(filter) }, + transformOrigin = if (index == 0) TransformOrigin(0f, 0.5f) else TransformOrigin(1f, 0.5f) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + if (filter == StorageFilter.OFFLINE) { + Icon( + painter = painterResource(id = R.drawable.ic_phonef), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } else { + Icon( + imageVector = Icons.Rounded.Cloud, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } + Spacer(Modifier.width(8.dp)) + Text( + text = stringResource(labelRes), + fontFamily = GoogleSansRounded, + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(end = 4.dp) + ) + } + } + } + } + + FilledIconButton( + onClick = { + val imageUriString = if(selectedTab == 1) selectedImageUri?.toString() else null + val color = if(selectedTab == 2) selectedColor else null + val icon = if(selectedTab == 2) selectedIconName else null + + val scale = if(selectedTab == 1) cropScale else 1f + val panX = if(selectedTab == 1) cropOffset.x else 0f + val panY = if(selectedTab == 1) cropOffset.y else 0f + + val shapeTypeForSave = if (selectedTab == 2) selectedShapeType.name else null + val (d1, d2, d3, d4) = if (selectedTab == 2) { + when (selectedShapeType) { + PlaylistShapeType.SmoothRect -> Quadruple(smoothRectCornerRadius, smoothRectSmoothness, 0f, 0f) + PlaylistShapeType.Star -> Quadruple(starCurve.toFloat(), starRotation, starScale, starSides.toFloat()) + else -> Quadruple(0f, 0f, 0f, 0f) + } + } else Quadruple(null, null, null, null) + + onCreate( + playlistName, + imageUriString, + color, + icon, + selectedSongIds.filterValues { it }.keys.toList(), + scale, + panX, + panY, + shapeTypeForSave, + d1, d2, d3, d4, + null + ) + }, + modifier = Modifier.size(56.dp), + shape = RoundedCornerShape(16.dp), + colors = IconButtonDefaults.filledIconButtonColors( + containerColor = MaterialTheme.colorScheme.tertiaryContainer, + contentColor = MaterialTheme.colorScheme.onTertiaryContainer + ) + ) { + Icon( + Icons.Rounded.Check, + contentDescription = stringResource(R.string.presentation_batch_f_create), + modifier = Modifier.size(28.dp) + ) + } + } + } + }, containerColor = MaterialTheme.colorScheme.surface ) { paddingValues -> AnimatedContent( @@ -559,7 +688,8 @@ private fun CreatePlaylistContent( modifier = Modifier .fillMaxSize() .imePadding(), - contentPadding = PaddingValues(bottom = 100.dp, top = 8.dp) + contentPadding = PaddingValues(bottom = 120.dp, top = 8.dp), + playerViewModel = playerViewModel ) } } @@ -994,7 +1124,7 @@ private fun PlaylistFormContent( contentAlignment = Alignment.Center ) { if (selectedIconName != null) { - val icon = getIconByName(selectedIconName!!) ?: Icons.Rounded.MusicNote + val icon = getIconByName(selectedIconName) ?: Icons.Rounded.MusicNote Icon( imageVector = icon, contentDescription = null, @@ -1110,7 +1240,8 @@ private fun PlaylistFormContent( FilterChip( selected = selectedSmartRule == rule, onClick = { onSmartRuleChange(rule) }, - label = { Text(smartPlaylistRuleTitle(rule)) } + label = { Text(smartPlaylistRuleTitle(rule)) }, + shape = CircleShape ) } } diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt index 01fde6676..b4aae7ccf 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/viewmodel/PlayerViewModel.kt @@ -312,6 +312,9 @@ class PlayerViewModel @Inject constructor( + private val _playlistPickerStorageFilter = MutableStateFlow(com.theveloper.pixelplay.data.model.StorageFilter.OFFLINE) + val playlistPickerStorageFilter: StateFlow = _playlistPickerStorageFilter.asStateFlow() + /** * Paginated songs for efficient display in LibraryScreen. * Uses Paging 3 for memory-efficient loading of large libraries. @@ -321,11 +324,31 @@ class PlayerViewModel @Inject constructor( .cachedIn(viewModelScope) @OptIn(ExperimentalCoroutinesApi::class) - val playlistPickerSongs: Flow> = libraryStateHolder.currentSongSortOption - .flatMapLatest { sortOption -> + val playlistPickerFavoriteSongs: Flow> = combine( + libraryStateHolder.currentSongSortOption, + _playlistPickerStorageFilter + ) { sortOption, storageFilter -> + sortOption to storageFilter + } + .flatMapLatest { (sortOption, storageFilter) -> + musicRepository.getPaginatedFavoriteSongs( + sortOption = sortOption, + storageFilter = storageFilter + ) + } + .cachedIn(viewModelScope) + + @OptIn(ExperimentalCoroutinesApi::class) + val playlistPickerSongs: Flow> = combine( + libraryStateHolder.currentSongSortOption, + _playlistPickerStorageFilter + ) { sortOption, storageFilter -> + sortOption to storageFilter + } + .flatMapLatest { (sortOption, storageFilter) -> musicRepository.getPaginatedSongs( sortOption = sortOption, - storageFilter = com.theveloper.pixelplay.data.model.StorageFilter.ALL + storageFilter = storageFilter ) } .cachedIn(viewModelScope) @@ -2057,6 +2080,10 @@ class PlayerViewModel @Inject constructor( libraryStateHolder.setStorageFilter(filter) } + fun setPlaylistPickerStorageFilter(filter: com.theveloper.pixelplay.data.model.StorageFilter) { + _playlistPickerStorageFilter.value = filter + } + fun setHideLocalMedia(hide: Boolean) { viewModelScope.launch { userPreferencesRepository.setHideLocalMedia(hide) @@ -2078,7 +2105,8 @@ class PlayerViewModel @Inject constructor( contextSongs: List, queueName: String = "Current Context", isVoluntaryPlay: Boolean = true, - cancelPendingQueueBuild: Boolean = true + cancelPendingQueueBuild: Boolean = true, + playlistId: String? = null ) { if (cancelPendingQueueBuild) { cancelPendingFullQueuePlayback() @@ -2146,7 +2174,12 @@ class PlayerViewModel @Inject constructor( } } - if (isVoluntaryPlay) incrementSongScore(song) + if (isVoluntaryPlay) { + incrementSongScore(song) + if (playlistId != null && queueName != "None") { + appShortcutManager.updateLastPlaylistShortcut(playlistId, queueName) + } + } return } // Local playback logic mediaController?.let { controller -> @@ -2161,10 +2194,15 @@ class PlayerViewModel @Inject constructor( controller.seekTo(songIndexInQueue, 0L) controller.play() } - if (isVoluntaryPlay) incrementSongScore(song) + if (isVoluntaryPlay) { + incrementSongScore(song) + if (playlistId != null && queueName != "None") { + appShortcutManager.updateLastPlaylistShortcut(playlistId, queueName) + } + } } else { if (isVoluntaryPlay) incrementSongScore(song) - playSongs(playbackContext, song, queueName, null) + playSongs(playbackContext, song, queueName, playlistId) } } resetPredictiveBackState() diff --git a/app/src/main/res/drawable/ic_phone.xml b/app/src/main/res/drawable/ic_phone.xml new file mode 100644 index 000000000..7034d0862 --- /dev/null +++ b/app/src/main/res/drawable/ic_phone.xml @@ -0,0 +1,24 @@ + + + + diff --git a/app/src/main/res/drawable/ic_phonef.xml b/app/src/main/res/drawable/ic_phonef.xml new file mode 100644 index 000000000..c542459fa --- /dev/null +++ b/app/src/main/res/drawable/ic_phonef.xml @@ -0,0 +1,24 @@ + + + + diff --git a/app/src/main/res/values/strings_presentation_batch_c.xml b/app/src/main/res/values/strings_presentation_batch_c.xml index dfe59de00..3f71cd6df 100644 --- a/app/src/main/res/values/strings_presentation_batch_c.xml +++ b/app/src/main/res/values/strings_presentation_batch_c.xml @@ -17,8 +17,8 @@ Import M3U playlist Locate current song All songs - Online - Offline + CLOUD + LOCAL Sort options @@ -30,7 +30,7 @@ Add selected songs Add Search or filter songs… - Favorites only + Liked Failed to load songs Load more From ee1893fc2b8c63c60cc8e5748ecc4257bd007cd8 Mon Sep 17 00:00:00 2001 From: Ayaan Hussain Date: Fri, 15 May 2026 03:12:38 +0530 Subject: [PATCH 2/2] fix: Changed the icon to match liked state --- .../presentation/components/SongPickerBottomSheet.kt | 2 +- .../pixelplay/presentation/screens/LibraryEmptyState.kt | 6 +++--- .../pixelplay/presentation/screens/LibraryScreen.kt | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/SongPickerBottomSheet.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/SongPickerBottomSheet.kt index 1fbe46b1d..4d78b6c68 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/SongPickerBottomSheet.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/SongPickerBottomSheet.kt @@ -309,7 +309,7 @@ fun SongPickerSelectionPane( shape = CircleShape, leadingIcon = { Icon( - imageVector = Icons.Rounded.Favorite, + painter = painterResource(R.drawable.round_favorite_24), contentDescription = null, modifier = Modifier.size(FilterChipDefaults.IconSize) ) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryEmptyState.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryEmptyState.kt index ddf2355a3..c7e510afb 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryEmptyState.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryEmptyState.kt @@ -95,17 +95,17 @@ private fun libraryEmptySpec( LibraryTabId.LIKED -> when (storageFilter) { StorageFilter.ALL -> LibraryEmptySpec( - iconRes = R.drawable.rounded_favorite_24, + iconRes = R.drawable.round_favorite_24, titleRes = R.string.lib_empty_liked_all_title, subtitleRes = R.string.lib_empty_liked_all_subtitle ) StorageFilter.OFFLINE -> LibraryEmptySpec( - iconRes = R.drawable.rounded_favorite_24, + iconRes = R.drawable.round_favorite_24, titleRes = R.string.lib_empty_liked_offline_title, subtitleRes = R.string.lib_empty_liked_offline_subtitle ) StorageFilter.ONLINE -> LibraryEmptySpec( - iconRes = R.drawable.rounded_favorite_24, + iconRes = R.drawable.round_favorite_24, titleRes = R.string.lib_empty_liked_online_title, subtitleRes = R.string.lib_empty_liked_online_subtitle ) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt index 762161d8c..d9a3ba6a7 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/screens/LibraryScreen.kt @@ -2779,7 +2779,7 @@ private fun LibraryTabId.iconRes(): Int = when (this) { LibraryTabId.ARTISTS -> R.drawable.rounded_artist_24 LibraryTabId.PLAYLISTS -> R.drawable.rounded_playlist_play_24 LibraryTabId.FOLDERS -> R.drawable.rounded_folder_24 - LibraryTabId.LIKED -> R.drawable.rounded_favorite_24 + LibraryTabId.LIKED -> R.drawable.round_favorite_24 } private fun LibraryTabId.displayTitle(): String =