diff --git a/app/src/main/java/com/zionhuang/music/ui/menu/SelectionSongsMenu.kt b/app/src/main/java/com/zionhuang/music/ui/menu/SelectionSongsMenu.kt index 79217eaea..6708a6474 100644 --- a/app/src/main/java/com/zionhuang/music/ui/menu/SelectionSongsMenu.kt +++ b/app/src/main/java/com/zionhuang/music/ui/menu/SelectionSongsMenu.kt @@ -20,6 +20,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.core.net.toUri +import androidx.media3.common.Timeline import androidx.media3.exoplayer.offline.Download import androidx.media3.exoplayer.offline.DownloadRequest import androidx.media3.exoplayer.offline.DownloadService @@ -30,6 +31,7 @@ import com.zionhuang.music.R import com.zionhuang.music.db.entities.PlaylistSongMap import com.zionhuang.music.db.entities.Song import com.zionhuang.music.extensions.toMediaItem +import com.zionhuang.music.models.MediaMetadata import com.zionhuang.music.playback.ExoDownloadService import com.zionhuang.music.playback.queues.ListQueue import com.zionhuang.music.ui.component.DefaultDialog @@ -222,4 +224,189 @@ fun SelectionSongMenu( } } } -} \ No newline at end of file +} + +@Composable +fun SelectionMediaMetadataMenu( + songSelection: List, + currentItems: List, + onDismiss: () -> Unit, + clearAction: () -> Unit, +){ + val context = LocalContext.current + val database = LocalDatabase.current + val downloadUtil = LocalDownloadUtil.current + val playerConnection = LocalPlayerConnection.current ?: return + + var downloadState by remember { + mutableStateOf(Download.STATE_STOPPED) + } + + LaunchedEffect(songSelection) { + if (songSelection.isEmpty()) return@LaunchedEffect + downloadUtil.downloads.collect { downloads -> + downloadState = + if (songSelection.all { downloads[it.id]?.state == Download.STATE_COMPLETED }) + Download.STATE_COMPLETED + else if (songSelection.all { + downloads[it.id]?.state == Download.STATE_QUEUED + || downloads[it.id]?.state == Download.STATE_DOWNLOADING + || downloads[it.id]?.state == Download.STATE_COMPLETED + }) + Download.STATE_DOWNLOADING + else + Download.STATE_STOPPED + } + } + + var showChoosePlaylistDialog by rememberSaveable { + mutableStateOf(false) + } + + AddToPlaylistDialog( + isVisible = showChoosePlaylistDialog, + onAdd = { playlist -> + database.query { + songSelection.forEach { song -> + insert( + PlaylistSongMap( + songId = song.id, + playlistId = playlist.id, + position = playlist.songCount + ) + ) + } + } + }, + onDismiss = { showChoosePlaylistDialog = false } + ) + + var showRemoveDownloadDialog by remember { + mutableStateOf(false) + } + + if (showRemoveDownloadDialog) { + DefaultDialog( + onDismiss = { showRemoveDownloadDialog = false }, + content = { + Text( + text = stringResource(R.string.remove_download_playlist_confirm, "selection"), + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(horizontal = 18.dp) + ) + }, + buttons = { + TextButton( + onClick = { + showRemoveDownloadDialog = false + } + ) { + Text(text = stringResource(android.R.string.cancel)) + } + + TextButton( + onClick = { + showRemoveDownloadDialog = false + songSelection.forEach { song -> + DownloadService.sendRemoveDownload( + context, + ExoDownloadService::class.java, + song.id, + false + ) + } + } + ) { + Text(text = stringResource(android.R.string.ok)) + } + } + ) + } + + GridMenu ( + contentPadding = PaddingValues( + start = 8.dp, + top = 8.dp, + end = 8.dp, + bottom = 8.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding() + ) + ){ + GridMenuItem( + icon = R.drawable.delete, + title = R.string.delete + ) { + onDismiss() + var i = 0 + currentItems.forEach { cur -> + playerConnection.player.removeMediaItem(cur.firstPeriodIndex - i) + i++ + } + clearAction() + } + + GridMenuItem( + icon = R.drawable.play, + title = R.string.play + ) { + onDismiss() + playerConnection.playQueue( + ListQueue( + title = "Selection", + items = songSelection.map { it.toMediaItem() } + ) + ) + clearAction() + } + + GridMenuItem( + icon = R.drawable.shuffle, + title = R.string.shuffle + ) { + onDismiss() + playerConnection.playQueue( + ListQueue( + title = "Selection", + items = songSelection.shuffled().map { it.toMediaItem() } + ) + ) + clearAction() + } + + GridMenuItem( + icon = R.drawable.queue_music, + title = R.string.add_to_queue + ) { + onDismiss() + playerConnection.addToQueue(songSelection.map { it.toMediaItem() }) + clearAction() + } + + GridMenuItem( + icon = R.drawable.playlist_add, + title = R.string.add_to_playlist + ) { + showChoosePlaylistDialog = true + } + + DownloadGridMenu( + state = downloadState, + onDownload = { + songSelection.forEach { song -> + val downloadRequest = DownloadRequest.Builder(song.id, song.id.toUri()) + .setCustomCacheKey(song.id) + .setData(song.title.toByteArray()) + .build() + DownloadService.sendAddDownload( + context, + ExoDownloadService::class.java, + downloadRequest, + false + ) + } + }, + onRemoveDownload = { + showRemoveDownloadDialog = true + } + ) + } +} diff --git a/app/src/main/java/com/zionhuang/music/ui/player/Queue.kt b/app/src/main/java/com/zionhuang/music/ui/player/Queue.kt index 04c67fd14..95ee35c7a 100644 --- a/app/src/main/java/com/zionhuang/music/ui/player/Queue.kt +++ b/app/src/main/java/com/zionhuang/music/ui/player/Queue.kt @@ -1,5 +1,6 @@ package com.zionhuang.music.ui.player +import android.annotation.SuppressLint import android.text.format.Formatter import android.widget.Toast import androidx.compose.foundation.ExperimentalFoundationApi @@ -28,13 +29,13 @@ import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.DismissValue import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.NavigationBarDefaults import androidx.compose.material3.SwipeToDismiss @@ -77,20 +78,22 @@ import com.zionhuang.music.extensions.metadata import com.zionhuang.music.extensions.move import com.zionhuang.music.extensions.togglePlayPause import com.zionhuang.music.extensions.toggleRepeatMode +import com.zionhuang.music.models.MediaMetadata import com.zionhuang.music.ui.component.BottomSheet import com.zionhuang.music.ui.component.BottomSheetState import com.zionhuang.music.ui.component.LocalMenuState import com.zionhuang.music.ui.component.MediaMetadataListItem import com.zionhuang.music.ui.component.ResizableIconButton import com.zionhuang.music.ui.menu.PlayerMenu +import com.zionhuang.music.ui.menu.SelectionMediaMetadataMenu import com.zionhuang.music.utils.makeTimeString import kotlinx.coroutines.launch import org.burnoutcrew.reorderable.ReorderableItem import org.burnoutcrew.reorderable.detectReorder -import org.burnoutcrew.reorderable.detectReorderAfterLongPress import org.burnoutcrew.reorderable.rememberReorderableLazyListState import org.burnoutcrew.reorderable.reorderable +@SuppressLint("UnrememberedMutableState") @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable fun Queue( @@ -112,6 +115,9 @@ fun Queue( val currentFormat by playerConnection.currentFormat.collectAsState(initial = null) + val selectedSongs: MutableList = mutableStateListOf() + val selectedItems: MutableList = mutableStateListOf() + var showDetailsDialog by rememberSaveable { mutableStateOf(false) } @@ -276,50 +282,86 @@ fun Queue( state = dismissState, background = {}, dismissContent = { - MediaMetadataListItem( - mediaMetadata = window.mediaItem.metadata!!, - isActive = index == currentWindowIndex, - isPlaying = isPlaying, - trailingContent = { - IconButton( - onClick = { }, - modifier = Modifier - .detectReorder(reorderableState) - ) { - Icon( - painter = painterResource(R.drawable.drag_handle), - contentDescription = null - ) - } - }, - modifier = Modifier - .fillMaxWidth() - .combinedClickable( - onClick = { - if (index == currentWindowIndex) { - playerConnection.player.togglePlayPause() - } else { - playerConnection.player.seekToDefaultPosition(window.firstPeriodIndex) - playerConnection.player.playWhenReady = true - } - }, - onLongClick = { - menuState.show { - PlayerMenu( - mediaMetadata = window.mediaItem.metadata!!, - navController = navController, - playerBottomSheetState = playerBottomSheetState, - isQueueTrigger = true, - onShowDetailsDialog = { - showDetailsDialog = true - }, - onDismiss = menuState::dismiss - ) - } + Row( + horizontalArrangement = Arrangement.Center + ) { + IconButton( + modifier = Modifier + .align(Alignment.CenterVertically), + onClick = { + println(window.mediaItem.metadata!!.title) + if (window.mediaItem.metadata!! in selectedSongs){ + selectedSongs.remove(window.mediaItem.metadata!!) + selectedItems.remove(currentItem) + } else { + selectedSongs.add(window.mediaItem.metadata!!) + selectedItems.add(currentItem) } + } + ) { + Icon( + painter = painterResource(if (window.mediaItem.metadata!! in selectedSongs) R.drawable.check_box else R.drawable.uncheck_box), + contentDescription = null, + tint = LocalContentColor.current ) - //.detectReorderAfterLongPress(reorderableState) - ) + } + MediaMetadataListItem( + mediaMetadata = window.mediaItem.metadata!!, + isActive = index == currentWindowIndex, + isPlaying = isPlaying, + trailingContent = { + IconButton( + onClick = { }, + modifier = Modifier + .detectReorder(reorderableState) + ) { + Icon( + painter = painterResource(R.drawable.drag_handle), + contentDescription = null + ) + } + }, + modifier = Modifier + .fillMaxWidth() + .combinedClickable( + onClick = { + if (selectedSongs.isEmpty()) { + if (index == currentWindowIndex) { + playerConnection.player.togglePlayPause() + } else { + playerConnection.player.seekToDefaultPosition( + window.firstPeriodIndex + ) + playerConnection.player.playWhenReady = true + } + } else { + if (window.mediaItem.metadata!! in selectedSongs){ + selectedSongs.remove(window.mediaItem.metadata!!) + selectedItems.remove(currentItem) + } else { + selectedSongs.add(window.mediaItem.metadata!!) + selectedItems.add(currentItem) + } + } + }, + onLongClick = { + menuState.show { + PlayerMenu( + mediaMetadata = window.mediaItem.metadata!!, + navController = navController, + playerBottomSheetState = playerBottomSheetState, + isQueueTrigger = true, + onShowDetailsDialog = { + showDetailsDialog = true + }, + onDismiss = menuState::dismiss + ) + } + } + ) + //.detectReorderAfterLongPress(reorderableState) + ) + } } ) } @@ -352,6 +394,30 @@ fun Queue( modifier = Modifier.weight(1f) ) + if (selectedSongs.isNotEmpty()) { + IconButton( + onClick = { + menuState.show { + SelectionMediaMetadataMenu( + songSelection = selectedSongs, + onDismiss = menuState::dismiss, + clearAction = { + selectedSongs.clear() + selectedItems.clear() + }, + currentItems = selectedItems + ) + } + } + ) { + Icon( + painter = painterResource(R.drawable.more_vert), + contentDescription = null, + tint = LocalContentColor.current + ) + } + } + Column( verticalArrangement = Arrangement.spacedBy(4.dp), horizontalAlignment = Alignment.End diff --git a/app/src/main/res/drawable/check_box.xml b/app/src/main/res/drawable/check_box.xml new file mode 100644 index 000000000..0db63174e --- /dev/null +++ b/app/src/main/res/drawable/check_box.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/uncheck_box.xml b/app/src/main/res/drawable/uncheck_box.xml new file mode 100644 index 000000000..f705ba4ef --- /dev/null +++ b/app/src/main/res/drawable/uncheck_box.xml @@ -0,0 +1,9 @@ + + +