Skip to content

Commit

Permalink
feat(library song): multi-select
Browse files Browse the repository at this point in the history
  • Loading branch information
z-huang committed Aug 30, 2024
1 parent 3a83597 commit a206c27
Show file tree
Hide file tree
Showing 7 changed files with 173 additions and 42 deletions.
12 changes: 8 additions & 4 deletions app/src/main/java/com/zionhuang/music/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
Expand Down Expand Up @@ -283,6 +284,7 @@ class MainActivity : ComponentActivity() {

val navController = rememberNavController()
val navBackStackEntry by navController.currentBackStackEntryAsState()
val inSelectMode = navBackStackEntry?.savedStateHandle?.getStateFlow("inSelectMode", false)?.collectAsState()

val navigationItems = remember { Screens.MainScreens }
val defaultOpenTab = remember {
Expand Down Expand Up @@ -340,9 +342,11 @@ class MainActivity : ComponentActivity() {
mutableStateOf(intent?.action == ACTION_SEARCH)
}

val shouldShowSearchBar = remember(active, navBackStackEntry) {
active || navigationItems.fastAny { it.route == navBackStackEntry?.destination?.route } ||
navBackStackEntry?.destination?.route?.startsWith("search/") == true
val shouldShowSearchBar = remember(active, navBackStackEntry, inSelectMode?.value) {
(active ||
navigationItems.fastAny { it.route == navBackStackEntry?.destination?.route } ||
navBackStackEntry?.destination?.route?.startsWith("search/") == true) &&
inSelectMode?.value != true
}
val shouldShowNavigationBar = remember(navBackStackEntry, active) {
navBackStackEntry?.destination?.route == null ||
Expand Down Expand Up @@ -724,7 +728,7 @@ class MainActivity : ComponentActivity() {
},
onClick = {
if (navBackStackEntry?.destination?.hierarchy?.any { it.route == screen.route } == true) {
navController.currentBackStackEntry?.savedStateHandle?.set("scrollToTop", true)
navBackStackEntry?.savedStateHandle?.set("scrollToTop", true)
coroutineScope.launch {
searchBarScrollBehavior.state.resetHeightOffset()
}
Expand Down
32 changes: 19 additions & 13 deletions app/src/main/java/com/zionhuang/music/ui/menu/SongSelectionMenu.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,23 +49,26 @@ fun SongSelectionMenu(
val playerConnection = LocalPlayerConnection.current ?: return

val allInLibrary by remember(selection) {
mutableStateOf(selection.all { it.song.inLibrary != null })
mutableStateOf(selection.isNotEmpty() && selection.all { it.song.inLibrary != null })
}
val allLiked by remember(selection) {
mutableStateOf(selection.all { it.song.liked })
mutableStateOf(selection.isNotEmpty() && selection.all { it.song.liked })
}

var downloadState by remember {
mutableIntStateOf(Download.STATE_STOPPED)
}

LaunchedEffect(selection) {
if (selection.isEmpty()) return@LaunchedEffect
downloadUtil.downloads.collect { downloads ->
downloadState = when {
selection.all { downloads[it.id]?.state == STATE_COMPLETED } -> STATE_COMPLETED
selection.all { downloads[it.id]?.state in listOf(STATE_QUEUED, STATE_DOWNLOADING, STATE_COMPLETED) } -> STATE_DOWNLOADING
else -> Download.STATE_STOPPED
if (selection.isEmpty()) {
onDismiss()
} else {
downloadUtil.downloads.collect { downloads ->
downloadState = when {
selection.all { downloads[it.id]?.state == STATE_COMPLETED } -> STATE_COMPLETED
selection.all { downloads[it.id]?.state in listOf(STATE_QUEUED, STATE_DOWNLOADING, STATE_COMPLETED) } -> STATE_DOWNLOADING
else -> Download.STATE_STOPPED
}
}
}
}
Expand All @@ -80,10 +83,6 @@ fun SongSelectionMenu(
onDismiss = { showChoosePlaylistDialog = false },
)

var showRemoveDownloadDialog by remember {
mutableStateOf(false)
}

GridMenu(
contentPadding =
PaddingValues(
Expand Down Expand Up @@ -154,7 +153,14 @@ fun SongSelectionMenu(
}
},
onRemoveDownload = {
showRemoveDownloadDialog = true
selection.forEach { song ->
DownloadService.sendRemoveDownload(
context,
ExoDownloadService::class.java,
song.song.id,
false
)
}
},
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,7 @@ fun AlbumScreen(
}
)
IconButton(
enabled = selection.isNotEmpty(),
onClick = {
menuState.show {
SongSelectionMenu(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.zionhuang.music.ui.screens.library

import androidx.activity.compose.BackHandler
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
Expand All @@ -12,22 +13,33 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.listSaver
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachReversed
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import androidx.navigation.compose.currentBackStackEntryAsState
Expand All @@ -51,19 +63,22 @@ import com.zionhuang.music.ui.component.LocalMenuState
import com.zionhuang.music.ui.component.SongListItem
import com.zionhuang.music.ui.component.SortHeader
import com.zionhuang.music.ui.menu.SongMenu
import com.zionhuang.music.ui.menu.SongSelectionMenu
import com.zionhuang.music.utils.rememberEnumPreference
import com.zionhuang.music.utils.rememberPreference
import com.zionhuang.music.viewmodels.LibrarySongsViewModel

@OptIn(ExperimentalFoundationApi::class)
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun LibrarySongsScreen(
navController: NavController,
viewModel: LibrarySongsViewModel = hiltViewModel(),
) {
val haptic = LocalHapticFeedback.current
val context = LocalContext.current
val menuState = LocalMenuState.current
val playerConnection = LocalPlayerConnection.current ?: return

val isPlaying by playerConnection.isPlaying.collectAsState()
val mediaMetadata by playerConnection.mediaMetadata.collectAsState()

Expand All @@ -77,13 +92,40 @@ fun LibrarySongsScreen(
val backStackEntry by navController.currentBackStackEntryAsState()
val scrollToTop = backStackEntry?.savedStateHandle?.getStateFlow("scrollToTop", false)?.collectAsState()

var inSelectMode by rememberSaveable { mutableStateOf(false) }
val selection = rememberSaveable(
saver = listSaver<MutableList<String>, String>(
save = { it.toList() },
restore = { it.toMutableStateList() }
)
) { mutableStateListOf() }
val onExitSelectionMode = {
inSelectMode = false
selection.clear()
}
if (inSelectMode) {
BackHandler(onBack = onExitSelectionMode)
}

LaunchedEffect(scrollToTop?.value) {
if (scrollToTop?.value == true) {
lazyListState.animateScrollToItem(0)
backStackEntry?.savedStateHandle?.set("scrollToTop", false)
}
}

LaunchedEffect(inSelectMode) {
backStackEntry?.savedStateHandle?.set("inSelectMode", inSelectMode)
}

LaunchedEffect(songs) {
selection.fastForEachReversed { songId ->
if (songs?.find { it.id == songId } == null) {
selection.remove(songId)
}
}
}

Box(
modifier = Modifier.fillMaxSize()
) {
Expand Down Expand Up @@ -157,43 +199,69 @@ fun LibrarySongsScreen(
key = { _, item -> item.id },
contentType = { _, _ -> CONTENT_TYPE_SONG }
) { index, song ->
val onCheckedChange: (Boolean) -> Unit = {
if (it) {
selection.add(song.id)
} else {
selection.remove(song.id)
}
}

SongListItem(
song = song,
isActive = song.id == mediaMetadata?.id,
isPlaying = isPlaying,
trailingContent = {
IconButton(
onClick = {
menuState.show {
SongMenu(
originalSong = song,
navController = navController,
onDismiss = menuState::dismiss
)
if (inSelectMode) {
Checkbox(
checked = song.id in selection,
onCheckedChange = onCheckedChange
)
} else {
IconButton(
onClick = {
menuState.show {
SongMenu(
originalSong = song,
navController = navController,
onDismiss = menuState::dismiss
)
}
}
) {
Icon(
painter = painterResource(R.drawable.more_vert),
contentDescription = null
)
}
) {
Icon(
painter = painterResource(R.drawable.more_vert),
contentDescription = null
)
}
},
modifier = Modifier
.fillMaxWidth()
.combinedClickable {
if (song.id == mediaMetadata?.id) {
playerConnection.player.togglePlayPause()
} else {
playerConnection.playQueue(
ListQueue(
title = context.getString(R.string.queue_all_songs),
items = songs.map { it.toMediaItem() },
startIndex = index
.combinedClickable(
onClick = {
if (inSelectMode) {
onCheckedChange(song.id !in selection)
} else if (song.id == mediaMetadata?.id) {
playerConnection.player.togglePlayPause()
} else {
playerConnection.playQueue(
ListQueue(
title = context.getString(R.string.queue_all_songs),
items = songs.map { it.toMediaItem() },
startIndex = index
)
)
)
}
},
onLongClick = {
if (!inSelectMode) {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
inSelectMode = true
onCheckedChange(true)
}
}
}
)
.animateItem()
)
}
Expand All @@ -214,4 +282,52 @@ fun LibrarySongsScreen(
}
)
}

if (inSelectMode) {
TopAppBar(
title = {
Text(pluralStringResource(R.plurals.n_selected, selection.size, selection.size))
},
navigationIcon = {
IconButton(onClick = onExitSelectionMode) {
Icon(
painter = painterResource(R.drawable.close),
contentDescription = null,
)
}
},
actions = {
Checkbox(
checked = selection.size == songs?.size,
onCheckedChange = {
if (selection.size == songs?.size) {
selection.clear()
} else {
selection.clear()
selection.addAll(songs?.map { it.id }.orEmpty())
}
}
)
IconButton(
enabled = selection.isNotEmpty(),
onClick = {
menuState.show {
SongSelectionMenu(
selection = selection.mapNotNull { songId ->
songs?.find { it.id == songId }
},
onDismiss = menuState::dismiss,
onExitSelectionMode = onExitSelectionMode
)
}
}
) {
Icon(
painterResource(R.drawable.more_vert),
contentDescription = null
)
}
}
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,7 @@ fun LocalPlaylistScreen(
}
)
IconButton(
enabled = selection.isNotEmpty(),
onClick = {
menuState.show {
SongSelectionMenu(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,7 @@ fun OnlinePlaylistScreen(
}
)
IconButton(
enabled = selection.isNotEmpty(),
onClick = {
menuState.show {
YouTubeSongSelectionMenu(
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values-zh-rTW/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,9 @@
<string name="play_next">接著播放</string>
<string name="add_to_queue">加入待播清單</string>
<string name="add_to_library">加入媒體庫</string>
<string name="add_all_to_library">全部加入媒體庫</string>
<string name="remove_from_library">從音樂庫中移除</string>
<string name="remove_all_from_library">全部從音樂庫中移除</string>
<string name="download">下載</string>
<string name="downloading">下載中</string>
<string name="remove_download">刪除下載</string>
Expand Down

0 comments on commit a206c27

Please sign in to comment.