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.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -209,11 +210,21 @@ private fun HeroSubtitle(
) )
} }
fun ListDownloadStatus.action(): BulkDownloadAction = fun ListDownloadStatus.actions(): List<BulkDownloadAction> =
when { when {
complete -> BulkDownloadAction.Delete complete -> listOf(BulkDownloadAction.Delete)
downloading > 0 -> BulkDownloadAction.Cancel downloading > 0 -> listOf(BulkDownloadAction.Cancel)
else -> BulkDownloadAction.Download 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 @Composable
@@ -284,17 +295,29 @@ fun HeroHeader(
modifier = controlsModifier.fillMaxWidth(), modifier = controlsModifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
IconButton( Box(
onClick = { onDownloadAction(downloadStatus.action()) }, modifier =
Modifier
.size(48.dp)
.combinedClickable(
enabled = hasSongs, 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()) { when (downloadStatus.action()) {
BulkDownloadAction.Delete -> { BulkDownloadAction.Delete -> {
Icon( Icon(
imageVector = Icons.Rounded.DownloadDone, imageVector = Icons.Rounded.DownloadDone,
contentDescription = "Delete download", 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 -> { 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() { fun aFullyDownloadedListOffersDelete() {
assertEquals(BulkDownloadAction.Delete, ListDownloadStatus(total = 3, downloaded = 3).action()) 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.activity.ComponentActivity
import androidx.compose.ui.test.assertIsNotEnabled import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule 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.onNodeWithContentDescription
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTouchInput
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import com.subtracks.data.model.BulkDownloadAction import com.subtracks.data.model.BulkDownloadAction
import com.subtracks.data.model.ListDownloadStatus import com.subtracks.data.model.ListDownloadStatus
import com.subtracks.ui.theme.SubtracksTheme import com.subtracks.ui.theme.SubtracksTheme
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Rule import org.junit.Rule
import org.junit.Test import org.junit.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
@@ -42,6 +45,26 @@ class HeroDownloadTest {
assertEquals(listOf(BulkDownloadAction.Delete), actions) 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 @Test
fun theButtonIsDisabledWithoutSongs() { fun theButtonIsDisabledWithoutSongs() {
render(ListDownloadStatus(), hasSongs = false) {} render(ListDownloadStatus(), hasSongs = false) {}