mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 07:29:14 +01:00
downloads: show progress and cancel on album and artist rows too
A node with any active download now shows the aggregate progress bar and cancels all its active downloads instead of offering delete; a node with only finished downloads keeps the trash.
This commit is contained in:
@@ -146,11 +146,13 @@ fun DownloadsScreen(
|
|||||||
tree.artists.forEach { artist ->
|
tree.artists.forEach { artist ->
|
||||||
val artistKey = "artist:${artist.id}:${artist.name}"
|
val artistKey = "artist:${artist.id}:${artist.name}"
|
||||||
item(key = artistKey) {
|
item(key = artistKey) {
|
||||||
|
val active = activeDownloads(artist.albums.flatMap { it.songs })
|
||||||
NodeRow(
|
NodeRow(
|
||||||
name = artist.name,
|
name = artist.name,
|
||||||
subtitle = "${artist.albums.size} albums · ${size(artist.bytes)}",
|
subtitle = "${artist.albums.size} albums · ${size(artist.bytes)}",
|
||||||
expanded = artistKey in expanded,
|
expanded = artistKey in expanded,
|
||||||
indent = 0,
|
indent = 0,
|
||||||
|
progress = active.progress(),
|
||||||
onToggle = { expanded = expanded.toggle(artistKey) },
|
onToggle = { expanded = expanded.toggle(artistKey) },
|
||||||
onDelete = {
|
onDelete = {
|
||||||
onDelete(
|
onDelete(
|
||||||
@@ -161,17 +163,20 @@ fun DownloadsScreen(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
onCancel = { onCancel(active.map { it.songId }) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (artistKey in expanded) {
|
if (artistKey in expanded) {
|
||||||
artist.albums.forEach { album ->
|
artist.albums.forEach { album ->
|
||||||
val albumKey = "album:$artistKey:${album.id}:${album.name}"
|
val albumKey = "album:$artistKey:${album.id}:${album.name}"
|
||||||
item(key = albumKey) {
|
item(key = albumKey) {
|
||||||
|
val active = activeDownloads(album.songs)
|
||||||
NodeRow(
|
NodeRow(
|
||||||
name = album.name,
|
name = album.name,
|
||||||
subtitle = "${album.songs.size} songs · ${size(album.bytes)}",
|
subtitle = "${album.songs.size} songs · ${size(album.bytes)}",
|
||||||
expanded = albumKey in expanded,
|
expanded = albumKey in expanded,
|
||||||
indent = 1,
|
indent = 1,
|
||||||
|
progress = active.progress(),
|
||||||
onToggle = { expanded = expanded.toggle(albumKey) },
|
onToggle = { expanded = expanded.toggle(albumKey) },
|
||||||
onDelete = {
|
onDelete = {
|
||||||
onDelete(
|
onDelete(
|
||||||
@@ -182,6 +187,7 @@ fun DownloadsScreen(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
onCancel = { onCancel(active.map { it.songId }) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (albumKey in expanded) {
|
if (albumKey in expanded) {
|
||||||
@@ -226,12 +232,24 @@ private fun NodeRow(
|
|||||||
subtitle: String,
|
subtitle: String,
|
||||||
expanded: Boolean,
|
expanded: Boolean,
|
||||||
indent: Int,
|
indent: Int,
|
||||||
|
progress: Float?,
|
||||||
onToggle: () -> Unit,
|
onToggle: () -> Unit,
|
||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
|
onCancel: () -> Unit,
|
||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(name, maxLines = 1, overflow = TextOverflow.Ellipsis) },
|
headlineContent = { Text(name, maxLines = 1, overflow = TextOverflow.Ellipsis) },
|
||||||
supportingContent = { Text(subtitle, maxLines = 1, overflow = TextOverflow.Ellipsis) },
|
supportingContent = {
|
||||||
|
Column {
|
||||||
|
Text(subtitle, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||||
|
if (progress != null) {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = { progress },
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
leadingContent = {
|
leadingContent = {
|
||||||
IconButton(onClick = onToggle) {
|
IconButton(onClick = onToggle) {
|
||||||
Icon(
|
Icon(
|
||||||
@@ -241,15 +259,30 @@ private fun NodeRow(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
trailingContent = {
|
trailingContent = {
|
||||||
|
if (progress != null) {
|
||||||
|
IconButton(onClick = onCancel) {
|
||||||
|
Icon(Icons.Rounded.Close, contentDescription = "Cancel download")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
IconButton(onClick = onDelete) {
|
IconButton(onClick = onDelete) {
|
||||||
Icon(Icons.Rounded.Delete, contentDescription = "Delete downloads")
|
Icon(Icons.Rounded.Delete, contentDescription = "Delete downloads")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier.clickable(onClick = onToggle).padding(start = 16.dp * indent),
|
modifier = Modifier.clickable(onClick = onToggle).padding(start = 16.dp * indent),
|
||||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun activeDownloads(songs: List<DownloadedSong>): List<DownloadedSong> =
|
||||||
|
songs.filter { it.status == DownloadStatus.Queued || it.status == DownloadStatus.Running }
|
||||||
|
|
||||||
|
private fun List<DownloadedSong>.progress(): Float? {
|
||||||
|
if (isEmpty()) return null
|
||||||
|
val total = sumOf { it.total }
|
||||||
|
return if (total > 0) (sumOf { it.bytes }.toFloat() / total).coerceIn(0f, 1f) else 0f
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SongRow(
|
private fun SongRow(
|
||||||
song: DownloadedSong,
|
song: DownloadedSong,
|
||||||
|
|||||||
@@ -39,43 +39,56 @@ class DownloadsScreenTest {
|
|||||||
private val done = active.copy(songId = "s2", title = "Done", status = DownloadStatus.Completed)
|
private val done = active.copy(songId = "s2", title = "Done", status = DownloadStatus.Completed)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun anInProgressDownloadShowsCancelAndACompletedOneShowsDelete() {
|
fun anInProgressNodeShowsCancelAndACompletedNodeShowsDelete() {
|
||||||
var cancelled: List<String>? = null
|
var cancelled: List<String>? = null
|
||||||
render(onCancel = { cancelled = it })
|
render(
|
||||||
|
tree =
|
||||||
expandArtistAndAlbum()
|
DownloadTree(
|
||||||
|
artists =
|
||||||
|
listOf(
|
||||||
|
artist("ar1", "Active", listOf(active)),
|
||||||
|
artist("ar2", "Done", listOf(done)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onCancel = { cancelled = it },
|
||||||
|
)
|
||||||
|
|
||||||
composeRule.onAllNodesWithContentDescription("Cancel download").assertCountEquals(1)
|
composeRule.onAllNodesWithContentDescription("Cancel download").assertCountEquals(1)
|
||||||
composeRule.onAllNodesWithContentDescription("Delete download").assertCountEquals(1)
|
composeRule.onAllNodesWithContentDescription("Delete downloads").assertCountEquals(1)
|
||||||
|
|
||||||
composeRule.onNodeWithContentDescription("Cancel download").performClick()
|
composeRule.onNodeWithContentDescription("Cancel download").performClick()
|
||||||
assertEquals(listOf("s1"), cancelled)
|
assertEquals(listOf("s1"), cancelled)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun render(onCancel: (List<String>) -> Unit) {
|
@Test
|
||||||
|
fun songsUnderAnInProgressNodeAlsoShowCancel() {
|
||||||
|
render(tree = DownloadTree(artists = listOf(artist("ar1", "Artist", listOf(active, done)))), onCancel = {})
|
||||||
|
|
||||||
|
expandArtistAndAlbum()
|
||||||
|
|
||||||
|
composeRule.onAllNodesWithContentDescription("Cancel download").assertCountEquals(3)
|
||||||
|
composeRule.onAllNodesWithContentDescription("Delete download").assertCountEquals(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun artist(
|
||||||
|
id: String,
|
||||||
|
name: String,
|
||||||
|
songs: List<DownloadedSong>,
|
||||||
|
) = DownloadArtistNode(
|
||||||
|
id = id,
|
||||||
|
name = name,
|
||||||
|
bytes = 0,
|
||||||
|
albums = listOf(DownloadAlbumNode(id = "al-$id", name = "Album", bytes = 0, songs = songs)),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun render(
|
||||||
|
tree: DownloadTree,
|
||||||
|
onCancel: (List<String>) -> Unit,
|
||||||
|
) {
|
||||||
composeRule.setContent {
|
composeRule.setContent {
|
||||||
SubtracksTheme {
|
SubtracksTheme {
|
||||||
DownloadsScreen(
|
DownloadsScreen(
|
||||||
tree =
|
tree = tree,
|
||||||
DownloadTree(
|
|
||||||
artists =
|
|
||||||
listOf(
|
|
||||||
DownloadArtistNode(
|
|
||||||
id = "ar1",
|
|
||||||
name = "Artist",
|
|
||||||
bytes = 0,
|
|
||||||
albums =
|
|
||||||
listOf(
|
|
||||||
DownloadAlbumNode(
|
|
||||||
id = "al1",
|
|
||||||
name = "Album",
|
|
||||||
bytes = 0,
|
|
||||||
songs = listOf(active, done),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
loadEncoding = { null },
|
loadEncoding = { null },
|
||||||
onBack = {},
|
onBack = {},
|
||||||
onDelete = {},
|
onDelete = {},
|
||||||
|
|||||||
Reference in New Issue
Block a user