mirror of
https://github.com/jellyfin/jellyfin-android.git
synced 2026-10-26 16:45:26 +00:00
Add multi-select deletion to downloads
This commit is contained in:
1 parent
2ace128505
commit
a98457b0a0
4 files changed
+218
-44
No files matched your search
@@ -15,6 +15,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.jellyfin.mobile.R
|
||||
@@ -22,7 +23,7 @@ import org.jellyfin.mobile.data.entity.DownloadEntity
|
||||
|
||||
@Composable
|
||||
fun DownloadRemoveDialog(
|
||||
download: DownloadEntity,
|
||||
downloads: List<DownloadEntity>,
|
||||
onConfirm: (Boolean) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
@@ -31,13 +32,25 @@ fun DownloadRemoveDialog(
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(text = stringResource(R.string.download_remove)) },
|
||||
title = { Text(text = pluralStringResource(R.plurals.download_remove_title, downloads.size)) },
|
||||
text = {
|
||||
Column {
|
||||
val name = remember(download, context) {
|
||||
download.getDisplayName(context).orEmpty()
|
||||
val name = remember(downloads, context) {
|
||||
downloads
|
||||
.firstOrNull()
|
||||
.takeIf { downloads.size == 1 }
|
||||
?.getDisplayName(context)
|
||||
.orEmpty()
|
||||
}
|
||||
Text(text = stringResource(R.string.download_remove_description, name))
|
||||
Text(
|
||||
text = pluralStringResource(
|
||||
R.plurals.download_remove_description,
|
||||
downloads.size,
|
||||
downloads.size,
|
||||
name,
|
||||
)
|
||||
)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
package org.jellyfin.mobile.ui.screens.downloads
|
||||
|
||||
import android.text.format.Formatter
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.expandHorizontally
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkHorizontally
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.Checkbox
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.LinearProgressIndicator
|
||||
import androidx.compose.material.ListItem
|
||||
@@ -14,6 +22,7 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
@@ -38,10 +47,13 @@ fun DownloadsList(
|
||||
downloads: List<DownloadFiles>,
|
||||
onOpen: (DownloadEntity) -> Unit,
|
||||
onDownload: (DownloadEntity) -> Unit,
|
||||
onRemove: (DownloadEntity) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
contentPadding: PaddingValues = PaddingValues.Zero,
|
||||
selection: Set<Long> = emptySet(),
|
||||
onToggleSelection: (DownloadEntity) -> Unit = {},
|
||||
) {
|
||||
val selectionMode = selection.isNotEmpty()
|
||||
|
||||
LazyColumn(
|
||||
modifier = modifier,
|
||||
contentPadding = contentPadding,
|
||||
@@ -54,7 +66,9 @@ fun DownloadsList(
|
||||
downloadFiles = downloadFiles,
|
||||
onOpen = { onOpen(downloadFiles.download) },
|
||||
onDownload = { onDownload(downloadFiles.download) },
|
||||
onRemove = { onRemove(downloadFiles.download) },
|
||||
onToggleSelection = { onToggleSelection(downloadFiles.download) },
|
||||
isSelected = selection.contains(downloadFiles.download.id),
|
||||
selectionMode = selectionMode,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -66,8 +80,10 @@ fun DownloadItem(
|
||||
downloadFiles: DownloadFiles,
|
||||
onOpen: () -> Unit,
|
||||
onDownload: () -> Unit,
|
||||
onRemove: () -> Unit,
|
||||
onToggleSelection: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
isSelected: Boolean = false,
|
||||
selectionMode: Boolean = false,
|
||||
) {
|
||||
val (download, files) = downloadFiles
|
||||
val context = LocalContext.current
|
||||
@@ -83,13 +99,13 @@ fun DownloadItem(
|
||||
modifier = modifier
|
||||
.combinedClickable(
|
||||
onClick = {
|
||||
if (!isVerified) {
|
||||
onDownload()
|
||||
} else {
|
||||
onOpen()
|
||||
when {
|
||||
selectionMode -> onToggleSelection()
|
||||
!isVerified -> onDownload()
|
||||
else -> onOpen()
|
||||
}
|
||||
},
|
||||
onLongClick = { onRemove() },
|
||||
onLongClick = { onToggleSelection() },
|
||||
),
|
||||
text = {
|
||||
val name = remember(download, context) { download.getDisplayName(context).orEmpty() }
|
||||
@@ -100,21 +116,35 @@ fun DownloadItem(
|
||||
)
|
||||
},
|
||||
icon = {
|
||||
val uri = remember(files) {
|
||||
files.find { it.type == DownloadFileType.IMAGE_PRIMARY }?.uri
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
AnimatedVisibility(
|
||||
visible = selectionMode,
|
||||
enter = fadeIn() + expandHorizontally(),
|
||||
exit = fadeOut() + shrinkHorizontally(),
|
||||
) {
|
||||
Checkbox(
|
||||
checked = isSelected,
|
||||
onCheckedChange = null,
|
||||
modifier = Modifier.padding(end = 8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
AsyncImage(
|
||||
model = uri,
|
||||
placeholder = painterResource(R.drawable.ic_local_movies_white_64),
|
||||
fallback = painterResource(R.drawable.ic_local_movies_white_64),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.size(
|
||||
width = 64.dp,
|
||||
height = 64.dp,
|
||||
val uri = remember(files) {
|
||||
files.find { it.type == DownloadFileType.IMAGE_PRIMARY }?.uri
|
||||
}
|
||||
|
||||
AsyncImage(
|
||||
model = uri,
|
||||
placeholder = painterResource(R.drawable.ic_local_movies_white_64),
|
||||
fallback = painterResource(R.drawable.ic_local_movies_white_64),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Fit,
|
||||
modifier = Modifier.size(
|
||||
width = 64.dp,
|
||||
height = 64.dp,
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
secondaryText = {
|
||||
if (download.status == DownloadStatus.DOWNLOADING || download.status == DownloadStatus.QUEUED) {
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
package org.jellyfin.mobile.ui.screens.downloads
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.systemBarsPadding
|
||||
import androidx.compose.material.DropdownMenu
|
||||
import androidx.compose.material.DropdownMenuItem
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
@@ -16,10 +28,14 @@ import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.MoreVert
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.mutableStateSetOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
@@ -30,7 +46,6 @@ import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import org.jellyfin.mobile.R
|
||||
import org.jellyfin.mobile.data.entity.DownloadEntity
|
||||
import org.jellyfin.mobile.downloads.DownloadsViewModel
|
||||
|
||||
@Composable
|
||||
@@ -39,16 +54,32 @@ fun DownloadsScreen(
|
||||
onBackPressed: () -> Unit = {},
|
||||
) {
|
||||
val downloads by viewModel.downloads.collectAsState()
|
||||
var downloadToRemove by remember { mutableStateOf<DownloadEntity?>(null) }
|
||||
val selection = remember { mutableStateSetOf<Long>() }
|
||||
var showDeleteConfirm by remember { mutableStateOf(false) }
|
||||
var showMenu by remember { mutableStateOf(false) }
|
||||
|
||||
if (downloadToRemove != null) {
|
||||
val selectionMode = selection.isNotEmpty()
|
||||
|
||||
BackHandler(enabled = selectionMode) {
|
||||
selection.clear()
|
||||
}
|
||||
|
||||
if (showDeleteConfirm) {
|
||||
val downloadsToRemove = remember(selection, downloads) {
|
||||
downloads
|
||||
.map { it.download }
|
||||
.filter { it.id in selection }
|
||||
}
|
||||
DownloadRemoveDialog(
|
||||
download = downloadToRemove!!,
|
||||
downloads = downloadsToRemove,
|
||||
onConfirm = { keepLocalFiles ->
|
||||
viewModel.removeDownload(downloadToRemove!!, deleteFiles = !keepLocalFiles)
|
||||
downloadToRemove = null
|
||||
downloadsToRemove.forEach { download ->
|
||||
viewModel.removeDownload(download, deleteFiles = !keepLocalFiles)
|
||||
}
|
||||
selection.clear()
|
||||
showDeleteConfirm = false
|
||||
},
|
||||
onDismiss = { downloadToRemove = null },
|
||||
onDismiss = { showDeleteConfirm = false },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -59,16 +90,103 @@ fun DownloadsScreen(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(text = stringResource(R.string.downloads))
|
||||
AnimatedContent(
|
||||
targetState = selectionMode,
|
||||
transitionSpec = {
|
||||
if (targetState) {
|
||||
(slideInVertically { height -> height } + fadeIn()) togetherWith
|
||||
(slideOutVertically { height -> -height } + fadeOut())
|
||||
} else {
|
||||
(slideInVertically { height -> -height } + fadeIn()) togetherWith
|
||||
(slideOutVertically { height -> height } + fadeOut())
|
||||
}
|
||||
},
|
||||
label = "TitleAnimation",
|
||||
) { isSelectionMode ->
|
||||
if (isSelectionMode) {
|
||||
Text(text = stringResource(R.string.selected_count, selection.size))
|
||||
} else {
|
||||
Text(text = stringResource(R.string.downloads))
|
||||
}
|
||||
}
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(
|
||||
onClick = onBackPressed,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
|
||||
contentDescription = null,
|
||||
)
|
||||
Crossfade(
|
||||
targetState = selectionMode,
|
||||
label = "NavIconAnimation",
|
||||
) { isSelectionMode ->
|
||||
if (isSelectionMode) {
|
||||
IconButton(
|
||||
onClick = { selection.clear() },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Close,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
IconButton(
|
||||
onClick = { onBackPressed() },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
Row {
|
||||
AnimatedVisibility(
|
||||
visible = selectionMode,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
Row {
|
||||
IconButton(onClick = { showDeleteConfirm = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Delete,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
||||
Box {
|
||||
IconButton(onClick = { showMenu = true }) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.MoreVert,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showMenu,
|
||||
onDismissRequest = { showMenu = false },
|
||||
) {
|
||||
if (selection.size < downloads.size) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
selection.addAll(downloads.map { it.download.id })
|
||||
showMenu = false
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(R.string.select_all))
|
||||
}
|
||||
}
|
||||
|
||||
if (selection.isNotEmpty()) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
selection.clear()
|
||||
showMenu = false
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(R.string.deselect_all))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
backgroundColor = Color.Transparent,
|
||||
@@ -84,7 +202,11 @@ fun DownloadsScreen(
|
||||
downloads = downloads,
|
||||
onOpen = { viewModel.openDownload(it) },
|
||||
onDownload = { viewModel.download(it) },
|
||||
onRemove = { downloadToRemove = it },
|
||||
selection = selection,
|
||||
onToggleSelection = { download ->
|
||||
if (selection.contains(download.id)) selection.remove(download.id)
|
||||
else selection.add(download.id)
|
||||
},
|
||||
contentPadding = innerPadding,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -160,11 +160,20 @@
|
||||
<string name="download_cancel">Cancel</string>
|
||||
<string name="download_completed">Download completed</string>
|
||||
<string name="download_progress">%1$d%%</string>
|
||||
<string name="download_remove">Remove download</string>
|
||||
<plurals name="download_remove_title">
|
||||
<item quantity="one">Remove download</item>
|
||||
<item quantity="other">Remove downloads</item>
|
||||
</plurals>
|
||||
<string name="download_remove_keep_local">Keep local file(s)</string>
|
||||
<string name="download_remove_description">Do you want to remove "%1$s" from your downloads?</string>
|
||||
<plurals name="download_remove_description">
|
||||
<item quantity="one">Do you want to remove \"%2$s\" from your downloads?</item>
|
||||
<item quantity="other">Do you want to remove these %1$d downloads?</item>
|
||||
</plurals>
|
||||
<string name="download_incomplete">Download incomplete</string>
|
||||
<string name="download_settings_dialog_title">Choose download location</string>
|
||||
<string name="download_settings_dialog_message">Please choose where downloaded media should be saved. Downloads use WiFi only by default.</string>
|
||||
<string name="select_folder">Select folder</string>
|
||||
<string name="select_all">Select all</string>
|
||||
<string name="deselect_all">Deselect all</string>
|
||||
<string name="selected_count">%1$d selected</string>
|
||||
</resources>
|
||||
Reference in new issue
Block a user