ui: share bulk download deletion confirmation

This commit is contained in:
austinried
2026-11-04 12:16:06 +00:00
parent 69f41b3459
commit 11abd4afbd
6 changed files with 219 additions and 91 deletions
@@ -6,10 +6,21 @@ import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import com.subtracks.R import com.subtracks.R
import com.subtracks.data.model.BulkDownloadAction
import com.subtracks.data.model.DownloadList import com.subtracks.data.model.DownloadList
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
data class PendingDownloadDelete( data class PendingDownloadDelete(
val name: String, val name: String,
@@ -18,6 +29,70 @@ data class PendingDownloadDelete(
val bytes: Long, val bytes: Long,
) )
class PendingDownloadDeleteController internal constructor(
private val scope: CoroutineScope,
private val downloadedBytes: State<suspend (DownloadList, String) -> Long>,
) {
var pending by mutableStateOf<PendingDownloadDelete?>(null)
private set
fun onAction(
target: MenuTarget,
action: BulkDownloadAction,
onAction: (DownloadList, String, BulkDownloadAction) -> Unit,
) {
val (list, refId) = target.bulkRef() ?: return
onAction(target.title, list, refId, action, onAction)
}
fun onAction(
name: String,
list: DownloadList,
refId: String,
action: BulkDownloadAction,
onAction: (DownloadList, String, BulkDownloadAction) -> Unit,
) {
if (action == BulkDownloadAction.Delete) {
scope.launch {
pending = PendingDownloadDelete(name, list, refId, downloadedBytes.value(list, refId))
}
} else {
onAction(list, refId, action)
}
}
fun dismiss() {
pending = null
}
}
@Composable
fun rememberPendingDownloadDelete(
dismissals: Flow<Unit>,
downloadedBytes: suspend (DownloadList, String) -> Long,
): PendingDownloadDeleteController {
val scope = rememberCoroutineScope()
val currentDownloadedBytes = rememberUpdatedState(downloadedBytes)
val controller = remember(scope) { PendingDownloadDeleteController(scope, currentDownloadedBytes) }
DismissOnRequest(dismissals, controller::dismiss)
return controller
}
@Composable
fun PendingDownloadDeleteConfirmation(
controller: PendingDownloadDeleteController,
onConfirm: (PendingDownloadDelete) -> Unit,
) {
controller.pending?.let { pending ->
DeleteDownloadsDialog(
name = pending.name,
bytes = pending.bytes,
onConfirm = { onConfirm(pending) },
onDismiss = controller::dismiss,
)
}
}
@Composable @Composable
fun DeleteDownloadsDialog( fun DeleteDownloadsDialog(
name: String, name: String,
@@ -16,7 +16,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@@ -30,6 +29,7 @@ import com.subtracks.data.model.Album
import com.subtracks.data.model.BulkDownloadAction import com.subtracks.data.model.BulkDownloadAction
import com.subtracks.data.model.CoverArtRef import com.subtracks.data.model.CoverArtRef
import com.subtracks.data.model.Disc import com.subtracks.data.model.Disc
import com.subtracks.data.model.DownloadList
import com.subtracks.data.model.ListDownloadStatus import com.subtracks.data.model.ListDownloadStatus
import com.subtracks.data.model.QueueKind import com.subtracks.data.model.QueueKind
import com.subtracks.data.model.Song import com.subtracks.data.model.Song
@@ -39,16 +39,16 @@ import com.subtracks.playback.PlaybackController
import com.subtracks.ui.components.ArtistChooserSheet import com.subtracks.ui.components.ArtistChooserSheet
import com.subtracks.ui.components.CloseWhenDownloadsGone import com.subtracks.ui.components.CloseWhenDownloadsGone
import com.subtracks.ui.components.ContextMenuHost import com.subtracks.ui.components.ContextMenuHost
import com.subtracks.ui.components.DeleteDownloadsDialog
import com.subtracks.ui.components.DismissOnRequest import com.subtracks.ui.components.DismissOnRequest
import com.subtracks.ui.components.HeroDetailScaffold import com.subtracks.ui.components.HeroDetailScaffold
import com.subtracks.ui.components.HeroHeader import com.subtracks.ui.components.HeroHeader
import com.subtracks.ui.components.ItemActions import com.subtracks.ui.components.ItemActions
import com.subtracks.ui.components.MenuTarget import com.subtracks.ui.components.MenuTarget
import com.subtracks.ui.components.PendingDownloadDeleteConfirmation
import com.subtracks.ui.components.rememberPendingDownloadDelete
import com.subtracks.ui.theme.ArtworkColors import com.subtracks.ui.theme.ArtworkColors
import com.subtracks.ui.theme.rememberArtworkColors import com.subtracks.ui.theme.rememberArtworkColors
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.launch
import org.koin.compose.koinInject import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
@@ -74,21 +74,16 @@ fun AlbumDetailRoute(
val downloadStatus by viewModel.downloadStatus.collectAsStateWithLifecycle() val downloadStatus by viewModel.downloadStatus.collectAsStateWithLifecycle()
val offline by viewModel.offline.collectAsStateWithLifecycle() val offline by viewModel.offline.collectAsStateWithLifecycle()
CloseWhenDownloadsGone(downloadStatus.downloaded, offline, onBack) CloseWhenDownloadsGone(downloadStatus.downloaded, offline, onBack)
val scope = rememberCoroutineScope() val dismissals = contextMenuHost?.dismissals ?: emptyFlow()
var pendingDelete by remember { mutableStateOf<Long?>(null) } val pendingDelete = rememberPendingDownloadDelete(dismissals) { _, _ -> viewModel.downloadedBytes() }
var showArtistChooser by remember { mutableStateOf(false) } var showArtistChooser by remember { mutableStateOf(false) }
DismissOnRequest(contextMenuHost?.dismissals ?: emptyFlow()) { DismissOnRequest(dismissals) { showArtistChooser = false }
pendingDelete = null
showArtistChooser = false
}
val playback by playbackController.state.collectAsStateWithLifecycle() val playback by playbackController.state.collectAsStateWithLifecycle()
val context = playback.context val context = playback.context
val shortcutArt = remember(coverArtId) { coverArtId?.let { viewModel.coverArt(it, true) } } val shortcutArt = remember(coverArtId) { coverArtId?.let { viewModel.coverArt(it, true) } }
val requestDownloadAction: (BulkDownloadAction) -> Unit = { action -> val requestDownloadAction: (BulkDownloadAction) -> Unit = { action ->
if (action == BulkDownloadAction.Delete) { pendingDelete.onAction(album?.name.orEmpty(), DownloadList.Album, albumId, action) { _, _, requested ->
scope.launch { pendingDelete = viewModel.downloadedBytes() } viewModel.onDownloadAction(requested)
} else {
viewModel.onDownloadAction(action)
} }
} }
val actions = val actions =
@@ -141,13 +136,8 @@ fun AlbumDetailRoute(
playingSongId = playback.item?.id.takeIf { context?.kind == QueueKind.Album && context.refId == albumId }, playingSongId = playback.item?.id.takeIf { context?.kind == QueueKind.Album && context.refId == albumId },
) )
pendingDelete?.let { bytes -> PendingDownloadDeleteConfirmation(pendingDelete) {
DeleteDownloadsDialog( viewModel.onDownloadAction(BulkDownloadAction.Delete)
name = album?.name.orEmpty(),
bytes = bytes,
onConfirm = { viewModel.onDownloadAction(BulkDownloadAction.Delete) },
onDismiss = { pendingDelete = null },
)
} }
if (showArtistChooser) { if (showArtistChooser) {
@@ -59,7 +59,6 @@ import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
@@ -111,13 +110,12 @@ import com.subtracks.ui.components.ArtistHeroArt
import com.subtracks.ui.components.CloseWhenDownloadsGone import com.subtracks.ui.components.CloseWhenDownloadsGone
import com.subtracks.ui.components.ContextMenuHost import com.subtracks.ui.components.ContextMenuHost
import com.subtracks.ui.components.CoverArt import com.subtracks.ui.components.CoverArt
import com.subtracks.ui.components.DeleteDownloadsDialog
import com.subtracks.ui.components.DismissOnRequest import com.subtracks.ui.components.DismissOnRequest
import com.subtracks.ui.components.ItemActions import com.subtracks.ui.components.ItemActions
import com.subtracks.ui.components.ListDownloadIndicator import com.subtracks.ui.components.ListDownloadIndicator
import com.subtracks.ui.components.MenuTarget import com.subtracks.ui.components.MenuTarget
import com.subtracks.ui.components.PendingDownloadDelete import com.subtracks.ui.components.PendingDownloadDeleteConfirmation
import com.subtracks.ui.components.bulkRef import com.subtracks.ui.components.rememberPendingDownloadDelete
import com.subtracks.ui.components.rememberViewportFill import com.subtracks.ui.components.rememberViewportFill
import com.subtracks.ui.components.statusBarScrim import com.subtracks.ui.components.statusBarScrim
import com.subtracks.ui.theme.ArtworkColors import com.subtracks.ui.theme.ArtworkColors
@@ -130,7 +128,6 @@ import com.subtracks.ui.theme.rememberArtworkColors
import com.subtracks.ui.theme.rememberOverlaidNameBusy import com.subtracks.ui.theme.rememberOverlaidNameBusy
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.koin.compose.koinInject import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
@@ -217,26 +214,13 @@ fun ArtistDetailRoute(
val offline by viewModel.offline.collectAsStateWithLifecycle() val offline by viewModel.offline.collectAsStateWithLifecycle()
CloseWhenDownloadsGone(downloadStatus.downloaded, offline, onBack) CloseWhenDownloadsGone(downloadStatus.downloaded, offline, onBack)
val albumDownloads by viewModel.albumDownloads.collectAsStateWithLifecycle() val albumDownloads by viewModel.albumDownloads.collectAsStateWithLifecycle()
val scope = rememberCoroutineScope() val pendingDelete = rememberPendingDownloadDelete(contextMenuHost?.dismissals ?: emptyFlow(), viewModel::downloadedBytes)
var pendingDelete by remember { mutableStateOf<PendingDownloadDelete?>(null) }
DismissOnRequest(contextMenuHost?.dismissals ?: emptyFlow()) { pendingDelete = null }
val shortcutArt = remember(coverArtId) { coverArtId?.let { viewModel.coverArt(it, true) } } val shortcutArt = remember(coverArtId) { coverArtId?.let { viewModel.coverArt(it, true) } }
val actions = val actions =
ItemActions( ItemActions(
playNext = { playbackController.playNext(it.sourceId, it.kind, it.refId) }, playNext = { playbackController.playNext(it.sourceId, it.kind, it.refId) },
addToQueue = { playbackController.addToQueue(it.sourceId, it.kind, it.refId) }, addToQueue = { playbackController.addToQueue(it.sourceId, it.kind, it.refId) },
bulkDownload = { target, action -> bulkDownload = { target, action -> pendingDelete.onAction(target, action, viewModel::onDownloadAction) },
target.bulkRef()?.let { (list, refId) ->
if (action == BulkDownloadAction.Delete) {
scope.launch {
pendingDelete =
PendingDownloadDelete(target.title, list, refId, viewModel.downloadedBytes(list, refId))
}
} else {
viewModel.onDownloadAction(list, refId, action)
}
}
},
setStar = setStar, setStar = setStar,
viewAlbum = onViewAlbum, viewAlbum = onViewAlbum,
) )
@@ -267,13 +251,8 @@ fun ArtistDetailRoute(
onShuffle = viewModel::shuffle, onShuffle = viewModel::shuffle,
) )
pendingDelete?.let { pending -> PendingDownloadDeleteConfirmation(pendingDelete) { pending ->
DeleteDownloadsDialog( viewModel.onDownloadAction(pending.list, pending.refId, BulkDownloadAction.Delete)
name = pending.name,
bytes = pending.bytes,
onConfirm = { viewModel.onDownloadAction(pending.list, pending.refId, BulkDownloadAction.Delete) },
onDismiss = { pendingDelete = null },
)
} }
} }
@@ -68,7 +68,6 @@ import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.snapshotFlow
@@ -126,13 +125,12 @@ import com.subtracks.data.source.StarType
import com.subtracks.playback.PlaybackController import com.subtracks.playback.PlaybackController
import com.subtracks.ui.components.ArtistArt import com.subtracks.ui.components.ArtistArt
import com.subtracks.ui.components.ContextMenuHost import com.subtracks.ui.components.ContextMenuHost
import com.subtracks.ui.components.DeleteDownloadsDialog
import com.subtracks.ui.components.DismissOnRequest import com.subtracks.ui.components.DismissOnRequest
import com.subtracks.ui.components.ItemActions import com.subtracks.ui.components.ItemActions
import com.subtracks.ui.components.MenuTarget import com.subtracks.ui.components.MenuTarget
import com.subtracks.ui.components.PendingDownloadDelete import com.subtracks.ui.components.PendingDownloadDeleteConfirmation
import com.subtracks.ui.components.SearchField import com.subtracks.ui.components.SearchField
import com.subtracks.ui.components.bulkRef import com.subtracks.ui.components.rememberPendingDownloadDelete
import com.subtracks.ui.components.statusBarScrim import com.subtracks.ui.components.statusBarScrim
import com.subtracks.ui.home.HomeRoute import com.subtracks.ui.home.HomeRoute
import com.subtracks.ui.home.HomeSection import com.subtracks.ui.home.HomeSection
@@ -143,7 +141,6 @@ import com.subtracks.ui.theme.playerSurfaceColor
import com.subtracks.ui.theme.rememberArtworkColors import com.subtracks.ui.theme.rememberArtworkColors
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.launch
import org.koin.compose.koinInject import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel import org.koin.compose.viewmodel.koinViewModel
import kotlin.math.abs import kotlin.math.abs
@@ -278,26 +275,13 @@ fun LibraryRoute(
val artwork = rememberArtworkColors(playbackController.coverArt(playback.item, thumbnail = true), fallbackName = playback.item?.title) val artwork = rememberArtworkColors(playbackController.coverArt(playback.item, thumbnail = true), fallbackName = playback.item?.title)
val downloadsState = listTab?.let { viewModel.downloads(it).collectAsStateWithLifecycle() } val downloadsState = listTab?.let { viewModel.downloads(it).collectAsStateWithLifecycle() }
val downloads = downloadsState?.value ?: emptyMap() val downloads = downloadsState?.value ?: emptyMap()
val scope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
var pendingDelete by remember { mutableStateOf<PendingDownloadDelete?>(null) } val pendingDelete = rememberPendingDownloadDelete(contextMenuHost?.dismissals ?: emptyFlow(), viewModel::downloadedBytes)
DismissOnRequest(contextMenuHost?.dismissals ?: emptyFlow()) { pendingDelete = null }
val itemActions = val itemActions =
ItemActions( ItemActions(
playNext = { playbackController.playNext(it.sourceId, it.kind, it.refId) }, playNext = { playbackController.playNext(it.sourceId, it.kind, it.refId) },
addToQueue = { playbackController.addToQueue(it.sourceId, it.kind, it.refId) }, addToQueue = { playbackController.addToQueue(it.sourceId, it.kind, it.refId) },
bulkDownload = { target, action -> bulkDownload = { target, action -> pendingDelete.onAction(target, action, viewModel::onDownloadAction) },
target.bulkRef()?.let { (list, refId) ->
if (action == BulkDownloadAction.Delete) {
scope.launch {
pendingDelete =
PendingDownloadDelete(target.title, list, refId, viewModel.downloadedBytes(list, refId))
}
} else {
viewModel.onDownloadAction(list, refId, action)
}
}
},
setStar = setStar, setStar = setStar,
viewAlbum = onViewAlbum, viewAlbum = onViewAlbum,
viewArtist = onViewArtist, viewArtist = onViewArtist,
@@ -377,13 +361,8 @@ fun LibraryRoute(
}, },
) )
pendingDelete?.let { pending -> PendingDownloadDeleteConfirmation(pendingDelete) { pending ->
DeleteDownloadsDialog( viewModel.onDownloadAction(pending.list, pending.refId, BulkDownloadAction.Delete)
name = pending.name,
bytes = pending.bytes,
onConfirm = { viewModel.onDownloadAction(pending.list, pending.refId, BulkDownloadAction.Delete) },
onDismiss = { pendingDelete = null },
)
} }
} }
@@ -7,7 +7,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -18,6 +17,7 @@ import androidx.paging.compose.itemKey
import com.subtracks.R import com.subtracks.R
import com.subtracks.data.model.BulkDownloadAction import com.subtracks.data.model.BulkDownloadAction
import com.subtracks.data.model.CoverArtRef import com.subtracks.data.model.CoverArtRef
import com.subtracks.data.model.DownloadList
import com.subtracks.data.model.ListDownloadStatus import com.subtracks.data.model.ListDownloadStatus
import com.subtracks.data.model.Playlist import com.subtracks.data.model.Playlist
import com.subtracks.data.model.PlaylistSongItem import com.subtracks.data.model.PlaylistSongItem
@@ -27,16 +27,16 @@ import com.subtracks.data.source.StarType
import com.subtracks.playback.PlaybackController import com.subtracks.playback.PlaybackController
import com.subtracks.ui.components.CloseWhenDownloadsGone import com.subtracks.ui.components.CloseWhenDownloadsGone
import com.subtracks.ui.components.ContextMenuHost import com.subtracks.ui.components.ContextMenuHost
import com.subtracks.ui.components.DeleteDownloadsDialog
import com.subtracks.ui.components.DismissOnRequest import com.subtracks.ui.components.DismissOnRequest
import com.subtracks.ui.components.HeroDetailScaffold import com.subtracks.ui.components.HeroDetailScaffold
import com.subtracks.ui.components.HeroHeader import com.subtracks.ui.components.HeroHeader
import com.subtracks.ui.components.ItemActions import com.subtracks.ui.components.ItemActions
import com.subtracks.ui.components.MenuTarget import com.subtracks.ui.components.MenuTarget
import com.subtracks.ui.components.PendingDownloadDeleteConfirmation
import com.subtracks.ui.components.rememberPendingDownloadDelete
import com.subtracks.ui.theme.ArtworkColors import com.subtracks.ui.theme.ArtworkColors
import com.subtracks.ui.theme.rememberArtworkColors import com.subtracks.ui.theme.rememberArtworkColors
import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.launch
import org.koin.compose.koinInject import org.koin.compose.koinInject
import org.koin.compose.viewmodel.koinViewModel import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.parameter.parametersOf import org.koin.core.parameter.parametersOf
@@ -60,16 +60,12 @@ fun PlaylistDetailRoute(
val downloadStatus by viewModel.downloadStatus.collectAsStateWithLifecycle() val downloadStatus by viewModel.downloadStatus.collectAsStateWithLifecycle()
val offline by viewModel.offline.collectAsStateWithLifecycle() val offline by viewModel.offline.collectAsStateWithLifecycle()
CloseWhenDownloadsGone(downloadStatus.downloaded, offline, onBack) CloseWhenDownloadsGone(downloadStatus.downloaded, offline, onBack)
val scope = rememberCoroutineScope() val pendingDelete = rememberPendingDownloadDelete(contextMenuHost?.dismissals ?: emptyFlow()) { _, _ -> viewModel.downloadedBytes() }
var pendingDelete by remember { mutableStateOf<Long?>(null) }
DismissOnRequest(contextMenuHost?.dismissals ?: emptyFlow()) { pendingDelete = null }
val playback by playbackController.state.collectAsStateWithLifecycle() val playback by playbackController.state.collectAsStateWithLifecycle()
val context = playback.context val context = playback.context
val requestDownloadAction: (BulkDownloadAction) -> Unit = { action -> val requestDownloadAction: (BulkDownloadAction) -> Unit = { action ->
if (action == BulkDownloadAction.Delete) { pendingDelete.onAction(playlist?.name.orEmpty(), DownloadList.Playlist, playlistId, action) { _, _, requested ->
scope.launch { pendingDelete = viewModel.downloadedBytes() } viewModel.onDownloadAction(requested)
} else {
viewModel.onDownloadAction(action)
} }
} }
val actions = val actions =
@@ -104,13 +100,8 @@ fun PlaylistDetailRoute(
playingSongId = playback.item?.id.takeIf { context?.kind == QueueKind.Playlist && context.refId == playlistId }, playingSongId = playback.item?.id.takeIf { context?.kind == QueueKind.Playlist && context.refId == playlistId },
) )
pendingDelete?.let { bytes -> PendingDownloadDeleteConfirmation(pendingDelete) {
DeleteDownloadsDialog( viewModel.onDownloadAction(BulkDownloadAction.Delete)
name = playlist?.name.orEmpty(),
bytes = bytes,
onConfirm = { viewModel.onDownloadAction(BulkDownloadAction.Delete) },
onDismiss = { pendingDelete = null },
)
} }
} }
@@ -6,8 +6,13 @@ import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick import androidx.compose.ui.test.performClick
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.DownloadList
import com.subtracks.ui.theme.SubtracksTheme import com.subtracks.ui.theme.SubtracksTheme
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.emptyFlow
import org.junit.Assert.assertEquals import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
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
@@ -55,6 +60,115 @@ class DeleteDownloadsDialogTest {
composeRule.onNodeWithText("512 B", substring = true).assertIsDisplayed() composeRule.onNodeWithText("512 B", substring = true).assertIsDisplayed()
} }
@Test
fun sharedWorkflowLooksUpBytesAndConfirmsTheRequestedTargetOnce() {
val actions = mutableListOf<Triple<DownloadList, String, BulkDownloadAction>>()
lateinit var controller: PendingDownloadDeleteController
renderWorkflow(
downloadedBytes = { list, refId ->
assertEquals(DownloadList.Album, list)
assertEquals("al1", refId)
1_500_000
},
onAction = { list, refId, action -> actions += Triple(list, refId, action) },
onController = { controller = it },
)
composeRule.runOnIdle {
controller.onAction("Kid A", DownloadList.Album, "al1", BulkDownloadAction.Delete) { _, _, _ -> }
}
composeRule.waitUntil { controller.pending != null }
composeRule.onNodeWithText("Kid A", substring = true).assertIsDisplayed()
composeRule.onNodeWithText("MB", substring = true).assertIsDisplayed()
composeRule.onNodeWithText("Delete").performClick()
composeRule.runOnIdle {
assertEquals(listOf(Triple(DownloadList.Album, "al1", BulkDownloadAction.Delete)), actions)
assertNull(controller.pending)
}
}
@Test
fun sharedWorkflowCancelDoesNotConfirm() {
val actions = mutableListOf<BulkDownloadAction>()
lateinit var controller: PendingDownloadDeleteController
renderWorkflow(
onAction = { _, _, action -> actions += action },
onController = { controller = it },
)
composeRule.runOnIdle {
controller.onAction("Kid A", DownloadList.Album, "al1", BulkDownloadAction.Delete) { _, _, _ -> }
}
composeRule.waitUntil { controller.pending != null }
composeRule.onNodeWithText("Cancel").performClick()
composeRule.runOnIdle {
assertEquals(emptyList<BulkDownloadAction>(), actions)
assertNull(controller.pending)
}
}
@Test
fun sharedWorkflowDismissesWithTheContextMenu() {
val dismissals = MutableSharedFlow<Unit>(replay = 1)
lateinit var controller: PendingDownloadDeleteController
renderWorkflow(dismissals = dismissals, onController = { controller = it })
composeRule.runOnIdle {
controller.onAction("Kid A", DownloadList.Album, "al1", BulkDownloadAction.Delete) { _, _, _ -> }
}
composeRule.waitUntil { controller.pending != null }
composeRule.runOnIdle { dismissals.tryEmit(Unit) }
composeRule.waitUntil { controller.pending == null }
composeRule.onNodeWithText("Delete").assertDoesNotExist()
}
@Test
fun sharedWorkflowDispatchesNonDeleteActionsImmediately() {
val actions = mutableListOf<Triple<DownloadList, String, BulkDownloadAction>>()
var lookups = 0
lateinit var controller: PendingDownloadDeleteController
renderWorkflow(
downloadedBytes = { _, _ ->
lookups++
0
},
onAction = { list, refId, action -> actions += Triple(list, refId, action) },
onController = { controller = it },
)
composeRule.runOnIdle {
controller.onAction("Kid A", DownloadList.Album, "al1", BulkDownloadAction.Download) { list, refId, action ->
actions += Triple(list, refId, action)
}
}
composeRule.runOnIdle {
assertEquals(listOf(Triple(DownloadList.Album, "al1", BulkDownloadAction.Download)), actions)
assertEquals(0, lookups)
assertNull(controller.pending)
}
}
private fun renderWorkflow(
dismissals: kotlinx.coroutines.flow.Flow<Unit> = emptyFlow(),
downloadedBytes: suspend (DownloadList, String) -> Long = { _, _ -> 0 },
onAction: (DownloadList, String, BulkDownloadAction) -> Unit = { _, _, _ -> },
onController: (PendingDownloadDeleteController) -> Unit,
) {
composeRule.setContent {
SubtracksTheme {
val controller = rememberPendingDownloadDelete(dismissals, downloadedBytes)
onController(controller)
PendingDownloadDeleteConfirmation(controller) { pending ->
onAction(pending.list, pending.refId, BulkDownloadAction.Delete)
}
}
}
}
private fun render( private fun render(
bytes: Long = 0, bytes: Long = 0,
onConfirm: () -> Unit = {}, onConfirm: () -> Unit = {},