From 3b6fb1341b3d4a6b2a57189a700349a28b6c877a Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Sun, 27 Sep 2026 13:08:24 +0000 Subject: [PATCH] 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. --- .../ui/components/HeroDetailScaffold.kt | 43 ++++++++++++++----- .../ui/components/BulkDownloadActionTest.kt | 22 ++++++++++ .../ui/components/HeroDownloadTest.kt | 23 ++++++++++ 3 files changed, 78 insertions(+), 10 deletions(-) diff --git a/app/src/main/kotlin/com/subtracks/ui/components/HeroDetailScaffold.kt b/app/src/main/kotlin/com/subtracks/ui/components/HeroDetailScaffold.kt index d0100768..626a1015 100644 --- a/app/src/main/kotlin/com/subtracks/ui/components/HeroDetailScaffold.kt +++ b/app/src/main/kotlin/com/subtracks/ui/components/HeroDetailScaffold.kt @@ -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 = 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()) }, - enabled = hasSongs, - modifier = Modifier.padding(horizontal = 8.dp), + Box( + modifier = + Modifier + .size(48.dp) + .combinedClickable( + enabled = hasSongs, + 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)) } } } diff --git a/app/src/test/kotlin/com/subtracks/ui/components/BulkDownloadActionTest.kt b/app/src/test/kotlin/com/subtracks/ui/components/BulkDownloadActionTest.kt index dd74bfb6..2e7915c9 100644 --- a/app/src/test/kotlin/com/subtracks/ui/components/BulkDownloadActionTest.kt +++ b/app/src/test/kotlin/com/subtracks/ui/components/BulkDownloadActionTest.kt @@ -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()) + } } diff --git a/app/src/test/kotlin/com/subtracks/ui/components/HeroDownloadTest.kt b/app/src/test/kotlin/com/subtracks/ui/components/HeroDownloadTest.kt index 99685676..d46242ea 100644 --- a/app/src/test/kotlin/com/subtracks/ui/components/HeroDownloadTest.kt +++ b/app/src/test/kotlin/com/subtracks/ui/components/HeroDownloadTest.kt @@ -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() + render(ListDownloadStatus(total = 3, downloaded = 1)) { actions += it } + + composeRule.onNodeWithContentDescription("Download").performTouchInput { longClick() } + + assertEquals(listOf(BulkDownloadAction.Delete), actions) + } + + @Test + fun longPressingAnUndownloadedListDoesNothing() { + val actions = mutableListOf() + render(ListDownloadStatus(total = 3)) { actions += it } + + composeRule.onNodeWithContentDescription("Download").performTouchInput { longClick() } + + assertTrue(actions.isEmpty()) + } + @Test fun theButtonIsDisabledWithoutSongs() { render(ListDownloadStatus(), hasSongs = false) {}