mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
Stores: Downloads rows act with icons; finished rows below one Clear
The row actions are round icon buttons with their labels as descriptions: pause, resume, play, retry, and a cancel that asks with a red delete and an undo. Downloads under way stay on top; finished ones sit below a line whose X clears them all - rows only, a failed download's kept files stay for its game page's Clear - with installed games as a compact row with Play. Records the pre-PR device checks.
This commit is contained in:
1 parent
d62b96ac15
commit
62502877fc
4 files changed
+104
-16
No files matched your search
@@ -248,6 +248,14 @@ object DownloadQueue {
|
||||
StoreDownloadService.finish(context.applicationContext, key)
|
||||
}
|
||||
|
||||
/** Drops every finished, failed or cancelled row; files a failed one kept stay (its game page's Clear removes them). */
|
||||
fun dismissFinished() {
|
||||
synchronized(lock) {
|
||||
items.entries.removeAll { !it.value.entry.isActive }
|
||||
publishLocked()
|
||||
}
|
||||
}
|
||||
|
||||
/** Drops a finished, failed or cancelled row from the list. */
|
||||
fun dismiss(key: String) {
|
||||
synchronized(lock) {
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
package com.droiddeck.launcher.ui
|
||||
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.hoverable
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.material.icons.automirrored.outlined.Undo
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.Close
|
||||
import androidx.compose.material.icons.filled.Refresh
|
||||
import androidx.compose.material.icons.filled.Pause
|
||||
import coil.compose.AsyncImage
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
@@ -75,9 +84,77 @@ private fun DownloadList(s: FrontEndState, a: FrontEndActions) {
|
||||
}
|
||||
return
|
||||
}
|
||||
// Under way on top; finished below a line whose X clears them all (rows only - a failed
|
||||
// download's kept files stay until its game page's Clear).
|
||||
val (active, finished) = entries.partition { it.isActive }
|
||||
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
// The list changes only when rows come, go or change state; each row reads its own progress.
|
||||
entries.forEachIndexed { i, row -> key(row.key) { Rise(1 + i.coerceAtMost(5)) { DownloadCard(StoresState.download(row.key) ?: row, s, a) } } }
|
||||
active.forEachIndexed { i, row -> key(row.key) { Rise(1 + i.coerceAtMost(5)) { DownloadCard(StoresState.download(row.key) ?: row, s, a) } } }
|
||||
if (finished.isNotEmpty()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth().padding(top = if (active.isEmpty()) 0.dp else 4.dp)) {
|
||||
Box(Modifier.weight(1f).height(1.dp).background(LocalPalette.current.line))
|
||||
RoundAction(Icons.Outlined.Close, stringResource(R.string.stores_dl_clear_all), size = 30.dp) { DownloadQueue.dismissFinished() }
|
||||
}
|
||||
finished.forEach { row ->
|
||||
key(row.key) {
|
||||
val d = StoresState.download(row.key) ?: row
|
||||
if (d.state == DownloadState.FAILED) DownloadCard(d, s, a) else FinishedRow(d, s, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A finished download: its name and store, and Play when it installed. */
|
||||
@Composable
|
||||
private fun FinishedRow(d: DownloadEntry, s: FrontEndState, a: FrontEndActions) {
|
||||
val ctx = LocalContext.current
|
||||
val colors = MaterialTheme.colorScheme
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
modifier = Modifier.fillMaxWidth().clip(Shape12).background(colors.surface).padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
) {
|
||||
Text(d.name, fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f, fill = false))
|
||||
SourceChip(d.store.id, small = true)
|
||||
Text(stringResource(if (d.state == DownloadState.INSTALLED) R.string.stores_installed_chip else R.string.stores_dl_cancelled), fontSize = 11.sp, color = colors.onSurfaceVariant, maxLines = 1, modifier = Modifier.weight(1f))
|
||||
if (d.state == DownloadState.INSTALLED) RoundAction(Icons.Filled.PlayArrow, stringResource(R.string.stores_play), primary = true) { launchStoreGame(ctx, d.store, d.id, s, a) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A row's action as a round icon button - the label is its description - sized like the old
|
||||
* compact buttons and focusable by pad. [primary] fills it with the accent, [danger] in red.
|
||||
*/
|
||||
@Composable
|
||||
internal fun RoundAction(
|
||||
icon: androidx.compose.ui.graphics.vector.ImageVector, description: String, primary: Boolean = false, danger: Boolean = false,
|
||||
enabled: Boolean = true, size: androidx.compose.ui.unit.Dp = 36.dp, onClick: () -> Unit,
|
||||
) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
val src = remember { androidx.compose.foundation.interaction.MutableInteractionSource() }
|
||||
val hot = rememberHot(src)
|
||||
val fill = when { danger -> Color(0xFFD9443B); primary -> pal.signal; else -> colors.surface.copy(alpha = 0.85f) }
|
||||
val tint = if (primary || danger) Color.White else if (hot) pal.signal else colors.onBackground
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.size(size).clip(androidx.compose.foundation.shape.CircleShape).background(fill)
|
||||
.glideBorder(hot, androidx.compose.foundation.shape.CircleShape, if (primary || danger) colors.onBackground else pal.signal, pal.line)
|
||||
.alpha(if (enabled) 1f else 0.4f)
|
||||
.hoverable(src).clickable(interactionSource = src, indication = androidx.compose.foundation.LocalIndication.current, enabled = enabled, role = androidx.compose.ui.semantics.Role.Button, onClick = onClick)
|
||||
.controllerConfirm(enabled = enabled, onClick = onClick),
|
||||
) { androidx.compose.material3.Icon(icon, description, tint = tint, modifier = Modifier.size(size * 0.5f)) }
|
||||
}
|
||||
|
||||
/** Cancel in two taps: the stop icon, then a red delete and a keep that undoes it (back by itself after a few seconds). */
|
||||
@Composable
|
||||
private fun CancelAction(onDelete: () -> Unit) {
|
||||
var armed by remember { mutableStateOf(false) }
|
||||
androidx.compose.runtime.LaunchedEffect(armed) { if (armed) { kotlinx.coroutines.delay(4000); armed = false } }
|
||||
if (!armed) RoundAction(Icons.Outlined.Close, stringResource(R.string.stores_dl_cancel)) { armed = true }
|
||||
else {
|
||||
RoundAction(Icons.Outlined.Delete, stringResource(R.string.stores_dl_cancel_confirm), danger = true) { armed = false; onDelete() }
|
||||
RoundAction(Icons.AutoMirrored.Outlined.Undo, stringResource(R.string.stores_notification_keep)) { armed = false }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,26 +204,20 @@ private fun DownloadCard(d: DownloadEntry, s: FrontEndState, a: FrontEndActions)
|
||||
}
|
||||
}
|
||||
}
|
||||
// The row's actions on the right, over the art.
|
||||
Column(verticalArrangement = Arrangement.spacedBy(6.dp), horizontalAlignment = Alignment.End) {
|
||||
// The row's actions on the right, over the art: icons, their labels as descriptions.
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
when (d.state) {
|
||||
DownloadState.RUNNING, DownloadState.QUEUED -> {
|
||||
SecondaryButton(stringResource(R.string.stores_dl_pause), compact = true) { DownloadQueue.pause(d.key) }
|
||||
ConfirmButton(stringResource(R.string.stores_dl_cancel), stringResource(R.string.stores_dl_cancel_confirm), compact = true) { DownloadQueue.cancel(ctx, d.key) }
|
||||
RoundAction(Icons.Filled.Pause, stringResource(R.string.stores_dl_pause)) { DownloadQueue.pause(d.key) }
|
||||
CancelAction { DownloadQueue.cancel(ctx, d.key) }
|
||||
}
|
||||
DownloadState.PAUSED -> {
|
||||
PrimaryButton(stringResource(R.string.stores_dl_resume), compact = true) { DownloadQueue.resume(ctx, d.key) }
|
||||
ConfirmButton(stringResource(R.string.stores_dl_cancel), stringResource(R.string.stores_dl_cancel_confirm), compact = true) { DownloadQueue.cancel(ctx, d.key) }
|
||||
RoundAction(Icons.Filled.PlayArrow, stringResource(R.string.stores_dl_resume), primary = true) { DownloadQueue.resume(ctx, d.key) }
|
||||
CancelAction { DownloadQueue.cancel(ctx, d.key) }
|
||||
}
|
||||
DownloadState.FAILED -> {
|
||||
PrimaryButton(stringResource(R.string.stores_dl_resume), compact = true) { DownloadQueue.retry(ctx, d.key) }
|
||||
ConfirmButton(stringResource(R.string.stores_dl_clear), stringResource(R.string.stores_dl_cancel_confirm), compact = true) { DownloadQueue.clear(ctx, d.key) }
|
||||
}
|
||||
DownloadState.INSTALLED -> {
|
||||
PrimaryButton(stringResource(R.string.stores_play), compact = true, icon = Icons.Filled.PlayArrow) { launchStoreGame(ctx, d.store, d.id, s, a) }
|
||||
SecondaryButton(stringResource(R.string.stores_dl_clear), compact = true) { DownloadQueue.dismiss(d.key) }
|
||||
}
|
||||
DownloadState.CANCELLED -> SecondaryButton(stringResource(R.string.stores_dl_clear), compact = true) { DownloadQueue.dismiss(d.key) }
|
||||
DownloadState.FAILED -> RoundAction(Icons.Filled.Refresh, stringResource(R.string.stores_dl_resume), primary = true) { DownloadQueue.retry(ctx, d.key) }
|
||||
DownloadState.INSTALLED -> RoundAction(Icons.Filled.PlayArrow, stringResource(R.string.stores_play), primary = true) { launchStoreGame(ctx, d.store, d.id, s, a) }
|
||||
DownloadState.CANCELLED -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2070,6 +2070,7 @@
|
||||
<string name="stores_dl_cancel">Cancel</string>
|
||||
<string name="stores_dl_retry">Retry</string>
|
||||
<string name="stores_dl_clear">Clear</string>
|
||||
<string name="stores_dl_clear_all">Clear finished</string>
|
||||
<string name="stores_stage_manifest">Manifest</string>
|
||||
<string name="stores_stage_download">Download</string>
|
||||
<string name="stores_stage_verify">Verify</string>
|
||||
|
||||
@@ -153,6 +153,14 @@ without it and Setup says "Stores engine not built").
|
||||
- **Cloud saves device-proven (AYANEO Pocket FIT, `6a175f1`, ELDERBORN on GOG):** the launch logged
|
||||
`cloud gog 1732383191 down result=ok files=5 bytes=237499 reason=downloaded` (template found) and
|
||||
the in-game slot shows the cloud save (Catacombs: Tutorial part 1, 00:08:40).
|
||||
- **Pre-PR device checks pass (AYANEO Pocket FIT):** GOG install, launch and cloud saves down and up
|
||||
(the session-end upload included); Epic with EOS sign-in; Amazon (Dread Templar); an SD-card install
|
||||
(DOOM I Enhanced) launched from `/mnt/droiddeck-sd`.
|
||||
- **Downloads page, icons and one Clear** (maintainer): the row actions are round icon buttons -
|
||||
pause, resume, play, retry, and a cancel that asks with a red delete and an undo - their labels as
|
||||
descriptions. Downloads under way sit on top; finished ones below a line whose X clears them all
|
||||
(rows only; a failed download's kept files stay for its game page's Clear), installed ones as a
|
||||
compact row with Play.
|
||||
- **Saves menu without explanations:** Import / Export rows (Steam, Custom, GOG and Epic alike) lose
|
||||
their descriptive subtitles; only live values remain (last sync, "…", conflict count, the save
|
||||
folder and its size). The three strings are gone from every locale.
|
||||
|
||||
Reference in new issue
Block a user