downloads: address review on the cancel/progress rows

Name the cancel and delete buttons after the row so screen readers can
tell them apart, show an indeterminate bar when a running track has no
known total, and drop the no-op media refresh on cancel. Trim the dead
size alias and a redundant guard, and cover a queued row with no engine
id plus a failed track in the progress denominator.
This commit is contained in:
austinried
2026-10-04 00:30:07 +00:00
parent 196733f9a7
commit 4ddbefe7cd
6 changed files with 43 additions and 16 deletions
@@ -107,7 +107,7 @@ interface DownloadDao {
@Query(
"SELECT d.sourceId AS sourceId, d.songId AS songId, s.title AS title, s.albumId AS albumId, al.name AS albumName, " +
"al.artistId AS artistId, ar.name AS artistName, d.status AS status, " +
"d.bytes AS bytes, d.total AS total, d.total AS size " +
"d.bytes AS bytes, d.total AS total " +
"FROM song_downloads d " +
"JOIN songs s ON s.sourceId = d.sourceId AND s.id = d.songId " +
"LEFT JOIN albums al ON al.sourceId = s.sourceId AND al.id = s.albumId " +
@@ -262,11 +262,11 @@ private fun NodeRow(
trailingContent = {
if (progress != null) {
IconButton(onClick = onCancel) {
Icon(Icons.Rounded.Close, contentDescription = "Cancel download")
Icon(Icons.Rounded.Close, contentDescription = "Cancel download of $name")
}
} else {
IconButton(onClick = onDelete) {
Icon(Icons.Rounded.Delete, contentDescription = "Delete downloads")
Icon(Icons.Rounded.Delete, contentDescription = "Delete downloads of $name")
}
}
},
@@ -280,7 +280,7 @@ private fun activeDownloads(songs: List<DownloadedSong>): List<DownloadedSong> =
internal fun nodeProgress(songs: List<DownloadedSong>): Float? {
val active = activeDownloads(songs)
if (active.isEmpty() || songs.isEmpty()) return null
if (active.isEmpty()) return null
val completed = songs.count { it.status == DownloadStatus.Completed }
val partial = active.sumOf { (it.progress ?: 0f).toDouble() }
return ((completed + partial) / songs.size).toFloat().coerceIn(0f, 1f)
@@ -298,27 +298,30 @@ private fun SongRow(
encoding = if (song.status == DownloadStatus.Completed) loadEncoding(song) else null
}
val active = song.status == DownloadStatus.Queued || song.status == DownloadStatus.Running
val barModifier = Modifier.fillMaxWidth().padding(top = 4.dp)
ListItem(
headlineContent = { Text(song.title, maxLines = 1, overflow = TextOverflow.Ellipsis) },
supportingContent = {
Column {
Text(songSubtitle(song, encoding), maxLines = 1, overflow = TextOverflow.Ellipsis)
if (active) {
LinearProgressIndicator(
progress = { song.progress ?: 0f },
modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
)
val progress = song.progress
if (progress == null) {
LinearProgressIndicator(modifier = barModifier)
} else {
LinearProgressIndicator(progress = { progress }, modifier = barModifier)
}
}
}
},
trailingContent = {
if (active) {
IconButton(onClick = onCancel) {
Icon(Icons.Rounded.Close, contentDescription = "Cancel download")
Icon(Icons.Rounded.Close, contentDescription = "Cancel download of ${song.title}")
}
} else {
IconButton(onClick = onDelete) {
Icon(Icons.Rounded.Delete, contentDescription = "Delete download")
Icon(Icons.Rounded.Delete, contentDescription = "Delete download of ${song.title}")
}
}
},
@@ -82,7 +82,6 @@ class DownloadsViewModel(
fun cancelSongs(songIds: List<String>) {
viewModelScope.launch {
downloadRepository.cancel(sourceId.first(), songIds)
playbackController.refreshMediaItems()
}
}