From dc721fcd0d742de79eec11da23b3209e91518b57 Mon Sep 17 00:00:00 2001 From: Gunny Date: Tue, 7 Jul 2026 18:47:02 +0530 Subject: [PATCH] fix(playlist): enable Save when all playlists deselected for single song MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #2553 The Save FAB in PlaylistBottomSheet was gated on isAnyPlaylistSelected, which meant that when a user opened the sheet for a song already in a playlist and unchecked all playlists, the button went grey and was non-functional — making it impossible to remove a song from its last playlist via this UI. Fix: replace isAnyPlaylistSelected with hasSelectionChanged, which captures the initial membership snapshot (initialSelection) once on sheet open and enables Save whenever the current state differs from it. Passing an empty list to addOrRemoveSongFromPlaylists is already handled correctly by the repository layer — it removes the song from every playlist that contains it. This also fixes a secondary issue: the Save button was incorrectly enabled after checking then unchecking a box (back to original state), because the old logic only checked whether anything was ticked, not whether the state had actually changed. --- .../components/PlaylistBottomSheet.kt | 97 ++++++++++--------- 1 file changed, 50 insertions(+), 47 deletions(-) diff --git a/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistBottomSheet.kt b/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistBottomSheet.kt index 6a3dc19f3..c09a3c41a 100644 --- a/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistBottomSheet.kt +++ b/app/src/main/java/com/theveloper/pixelplay/presentation/components/PlaylistBottomSheet.kt @@ -62,9 +62,12 @@ fun PlaylistBottomSheet( playlistViewModel: PlaylistViewModel = hiltViewModel(), currentPlaylistId: String? = null ) { - val playlistCreatedAndSongsAddedMessage = stringResource(R.string.playlist_sheet_created_and_songs_added) - val setAiProviderApiKeyFirstMessage = stringResource(R.string.library_toast_set_ai_provider_api_key_first) - val songAddedToPlaylistsMessage = stringResource(R.string.playlist_sheet_song_added_to_playlists) + val playlistCreatedAndSongsAddedMessage = + stringResource(R.string.playlist_sheet_created_and_songs_added) + val setAiProviderApiKeyFirstMessage = + stringResource(R.string.library_toast_set_ai_provider_api_key_first) + val songAddedToPlaylistsMessage = + stringResource(R.string.playlist_sheet_song_added_to_playlists) val commonSavedMessage = stringResource(R.string.common_saved) val saveActionText = stringResource(R.string.common_save) val internalStorageText = stringResource(R.string.playlist_sheet_internal_storage) @@ -77,33 +80,40 @@ fun PlaylistBottomSheet( ) var searchQuery by remember { mutableStateOf("") } + val filteredPlaylists = remember(searchQuery, playlistUiState.playlists) { if (searchQuery.isBlank()) playlistUiState.playlists else playlistUiState.playlists.filter { it.name.contains(searchQuery, true) } } + val hasActiveAiProviderApiKey by playerViewModel.hasActiveAiProviderApiKey.collectAsStateWithLifecycle() + // Capture the initial membership snapshot once when the sheet opens. + // For a single song: true = song is already in that playlist. + // For multiple songs: always false (additive-only mode). + val initialSelection = remember { + if (songs.size == 1) { + val songId = songs.first().id + filteredPlaylists.associate { it.id to it.songIds.contains(songId) } + } else { + filteredPlaylists.associate { it.id to false } + } + } + val selectedPlaylists = remember { mutableStateMapOf().apply { - if (songs.size == 1) { - // Single song: pre-select playlists containing it - val songId = songs.first().id - filteredPlaylists.forEach { - put(it.id, it.songIds.contains(songId)) - } - } else { - // Multiple songs: start empty (additive only) - filteredPlaylists.forEach { - put(it.id, false) - } - } + putAll(initialSelection) } } - val isAnyPlaylistSelected = selectedPlaylists.values.any { it } + // Save is enabled whenever the current selection differs from the initial state, + // including the case where all playlists are deselected (remove from all). + val hasSelectionChanged = selectedPlaylists.any { (id, checked) -> + initialSelection[id] != checked + } val alpha by animateFloatAsState( - targetValue = if (isAnyPlaylistSelected) 1f else 0.4f, + targetValue = if (hasSelectionChanged) 1f else 0.4f, label = "fab_alpha" ) @@ -113,7 +123,6 @@ fun PlaylistBottomSheet( contentWindowInsets = { BottomSheetDefaults.modalWindowInsets } // Manejo de insets como el teclado ) { Box(modifier = Modifier.fillMaxSize()) { - Column { Row( modifier = Modifier @@ -122,7 +131,7 @@ fun PlaylistBottomSheet( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { - Text( + Text( if (songs.size > 1) { stringResource(R.string.playlist_sheet_add_songs_title, songs.size) } else { @@ -132,6 +141,7 @@ fun PlaylistBottomSheet( fontFamily = GoogleSansRounded ) } + OutlinedTextField( value = searchQuery, colors = TextFieldDefaults.colors( @@ -152,25 +162,19 @@ fun PlaylistBottomSheet( shape = CircleShape, singleLine = true, trailingIcon = { - if (searchQuery.isNotEmpty()) IconButton(onClick = { - searchQuery = "" - }) { Icon(Icons.Filled.Clear, null) } + if (searchQuery.isNotEmpty()) IconButton(onClick = { searchQuery = "" }) { + Icon(Icons.Filled.Clear, null) + } } ) - - - LibraryActionRow( modifier = Modifier.padding( top = 10.dp, start = 10.dp, end = 10.dp ), - //currentPage = pagerState.currentPage, - onMainActionClick = { - showCreatePlaylistDialog = true - }, + onMainActionClick = { showCreatePlaylistDialog = true }, iconRotation = 0f, showSortButton = false, showImportButton = false, @@ -194,7 +198,7 @@ fun PlaylistBottomSheet( navController = null, playerViewModel = playerViewModel, isAddingToPlaylist = true, - currentSong = songs.firstOrNull() ?: Song.emptySong(), // Fallback safe + currentSong = songs.firstOrNull() ?: Song.emptySong(), filteredPlaylists = filteredPlaylists, selectedPlaylists = selectedPlaylists ) @@ -203,10 +207,9 @@ fun PlaylistBottomSheet( CreatePlaylistDialogRedesigned( onDismiss = { showCreatePlaylistDialog = false }, onCreate = { name -> - // Pass all selected songs to the new playlist playlistViewModel.createPlaylist(name, songIds = songs.map { it.id }) showCreatePlaylistDialog = false - onDismiss() // Close sheet after creation + add + onDismiss() playerViewModel.sendToast(playlistCreatedAndSongsAddedMessage) }, onGenerateClick = { @@ -225,31 +228,31 @@ fun PlaylistBottomSheet( modifier = Modifier .align(Alignment.BottomEnd) .padding(bottom = 18.dp, end = 8.dp) - .graphicsLayer { - this.alpha = alpha - }, + .graphicsLayer { this.alpha = alpha }, shape = CircleShape, onClick = { - if (!isAnyPlaylistSelected) return@MediumExtendedFloatingActionButton - + if (!hasSelectionChanged) return@MediumExtendedFloatingActionButton if (songs.size == 1) { - playlistViewModel.addOrRemoveSongFromPlaylists( + playlistViewModel.addOrRemoveSongFromPlaylists( songs.first().id, selectedPlaylists.filter { it.value }.keys.toList(), currentPlaylistId ) } else { - // Batch add - val selectedPlaylistIds = selectedPlaylists.filter { it.value }.keys.toList() - if (selectedPlaylistIds.isNotEmpty()) { - playlistViewModel.addSongsToPlaylists( - songs.map { it.id }, - selectedPlaylistIds - ) - } + // Batch add: multi-song mode is additive only + val selectedPlaylistIds = + selectedPlaylists.filter { it.value }.keys.toList() + if (selectedPlaylistIds.isNotEmpty()) { + playlistViewModel.addSongsToPlaylists( + songs.map { it.id }, + selectedPlaylistIds + ) + } } onDismiss() - playerViewModel.sendToast(if (songs.size > 1) songAddedToPlaylistsMessage else commonSavedMessage) + playerViewModel.sendToast( + if (songs.size > 1) songAddedToPlaylistsMessage else commonSavedMessage + ) playerViewModel.multiSelectionStateHolder.clearSelection() }, icon = { Icon(Icons.Rounded.Save, saveActionText) },