long press the hero to delete a partially downloaded list

The hero's control now carries the whole rule: a tap does what the state offers
(Download, Cancel, Delete) and a long press asks to delete when there is anything
downloaded to delete, so a partially downloaded album can be emptied without
starting the rest first. Nothing downloaded means the long press does nothing.

The rule itself returns the actions rather than one of them, so a partially
downloaded list offers both Download and Delete; the tap takes the first. That
list is what the library tabs' context menus will read.
This commit is contained in:
austinried
2026-10-04 00:30:03 +00:00
parent 37ccfd0b17
commit 3b6fb1341b
3 changed files with 78 additions and 10 deletions
@@ -2,6 +2,7 @@ package com.subtracks.ui.components
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -209,11 +210,21 @@ private fun HeroSubtitle(
)
}
fun ListDownloadStatus.action(): BulkDownloadAction =
fun ListDownloadStatus.actions(): List<BulkDownloadAction> =
when {
complete -> BulkDownloadAction.Delete
downloading > 0 -> BulkDownloadAction.Cancel
else -> BulkDownloadAction.Download
complete -> listOf(BulkDownloadAction.Delete)
downloading > 0 -> listOf(BulkDownloadAction.Cancel)
downloaded > 0 -> listOf(BulkDownloadAction.Download, BulkDownloadAction.Delete)
else -> listOf(BulkDownloadAction.Download)
}
fun ListDownloadStatus.action(): BulkDownloadAction = actions().first()
private fun downloadActionLabel(action: BulkDownloadAction): String =
when (action) {
BulkDownloadAction.Download -> "Download"
BulkDownloadAction.Cancel -> "Cancel download"
BulkDownloadAction.Delete -> "Delete download"
}
@Composable
@@ -284,17 +295,29 @@ fun HeroHeader(
modifier = controlsModifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
IconButton(
onClick = { onDownloadAction(downloadStatus.action()) },
Box(
modifier =
Modifier
.size(48.dp)
.combinedClickable(
enabled = hasSongs,
modifier = Modifier.padding(horizontal = 8.dp),
onClick = { onDownloadAction(downloadStatus.action()) },
onClickLabel = downloadActionLabel(downloadStatus.action()),
onLongClick = {
if (BulkDownloadAction.Delete in downloadStatus.actions()) {
onDownloadAction(BulkDownloadAction.Delete)
}
},
onLongClickLabel = "Delete download",
),
contentAlignment = Alignment.Center,
) {
when (downloadStatus.action()) {
BulkDownloadAction.Delete -> {
Icon(
imageVector = Icons.Rounded.DownloadDone,
contentDescription = "Delete download",
modifier = Modifier.size(28.dp).offset(y = 1.5.dp),
modifier = Modifier.size(28.dp),
)
}
@@ -310,7 +333,7 @@ fun HeroHeader(
}
BulkDownloadAction.Download -> {
Icon(Icons.Rounded.Download, contentDescription = "Download", modifier = Modifier.offset(y = 1.5.dp))
Icon(Icons.Rounded.Download, contentDescription = "Download", modifier = Modifier.size(24.dp))
}
}
}
@@ -26,4 +26,26 @@ class BulkDownloadActionTest {
fun aFullyDownloadedListOffersDelete() {
assertEquals(BulkDownloadAction.Delete, ListDownloadStatus(total = 3, downloaded = 3).action())
}
@Test
fun aPartiallyDownloadedListOffersBoth() {
assertEquals(
listOf(BulkDownloadAction.Download, BulkDownloadAction.Delete),
ListDownloadStatus(total = 3, downloaded = 1).actions(),
)
}
@Test
fun anInFlightListOffersOnlyCancel() {
assertEquals(
listOf(BulkDownloadAction.Cancel),
ListDownloadStatus(total = 3, downloaded = 1, downloading = 1).actions(),
)
}
@Test
fun anUndownloadedListOffersOnlyDownload() {
assertEquals(listOf(BulkDownloadAction.Download), ListDownloadStatus(total = 3).actions())
assertEquals(listOf(BulkDownloadAction.Download), ListDownloadStatus().actions())
}
}
@@ -3,14 +3,17 @@ package com.subtracks.ui.components
import androidx.activity.ComponentActivity
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
import androidx.compose.ui.test.longClick
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.subtracks.data.model.BulkDownloadAction
import com.subtracks.data.model.ListDownloadStatus
import com.subtracks.ui.theme.SubtracksTheme
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -42,6 +45,26 @@ class HeroDownloadTest {
assertEquals(listOf(BulkDownloadAction.Delete), actions)
}
@Test
fun longPressingAPartiallyDownloadedListAsksToDelete() {
val actions = mutableListOf<BulkDownloadAction>()
render(ListDownloadStatus(total = 3, downloaded = 1)) { actions += it }
composeRule.onNodeWithContentDescription("Download").performTouchInput { longClick() }
assertEquals(listOf(BulkDownloadAction.Delete), actions)
}
@Test
fun longPressingAnUndownloadedListDoesNothing() {
val actions = mutableListOf<BulkDownloadAction>()
render(ListDownloadStatus(total = 3)) { actions += it }
composeRule.onNodeWithContentDescription("Download").performTouchInput { longClick() }
assertTrue(actions.isEmpty())
}
@Test
fun theButtonIsDisabledWithoutSongs() {
render(ListDownloadStatus(), hasSongs = false) {}