mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 11:31:08 +00:00
Desktop & apps is a page with a dropdown per tier, not a dialog
The package list opens in the front end's pane like the Settings pages. Each tier (Native ARM64, ARM64 experimental, source builds) is a dropdown row with the rail's caret and an installed/total pill; Native ARM64 starts open. Each package has its own Install/Remove button, and the one that is installing shows its own step line and bar in its row (its tier opens when the install starts). Leaving the page does not stop an install. The rail's Install emulators and Setup's Desktop & apps highlight while the page is open.
This commit is contained in:
1 parent
0ea4b584b4
commit
bde8dc0089
3 files changed
+186
-79
No files matched your search
@@ -28,7 +28,7 @@ import com.steamdeck.launcher.runtime.LinuxRuntime
|
||||
import com.steamdeck.launcher.runtime.DesktopCatalog
|
||||
import com.steamdeck.launcher.runtime.LinuxRuntimeInstaller
|
||||
import com.steamdeck.launcher.session.SessionService
|
||||
import com.steamdeck.launcher.ui.DesktopAppsDialog
|
||||
import com.steamdeck.launcher.ui.DesktopAppsPage
|
||||
import com.steamdeck.launcher.ui.PackageRow
|
||||
import com.steamdeck.launcher.session.OfflineMode
|
||||
import com.steamdeck.launcher.session.ProtonExtras
|
||||
@@ -84,6 +84,7 @@ class MainActivity : ComponentActivity() {
|
||||
private var showApps by mutableStateOf(false)
|
||||
private var catalog by mutableStateOf<List<DesktopCatalog.Entry>?>(emptyList())
|
||||
private var packageRows by mutableStateOf<List<PackageRow>?>(emptyList())
|
||||
private var pkgId by mutableStateOf<String?>(null)
|
||||
private var pkgStage by mutableStateOf<String?>(null)
|
||||
private var pkgPercent by mutableIntStateOf(-1)
|
||||
private var desktopInstalled by mutableStateOf(false)
|
||||
@@ -187,6 +188,7 @@ class MainActivity : ComponentActivity() {
|
||||
val page: (@Composable () -> Unit)? = when {
|
||||
sm != null -> { { ModeSettingsHost(sm) } }
|
||||
showPerformance -> { { PerformanceHost() } }
|
||||
showApps -> { { AppsHost() } }
|
||||
else -> null
|
||||
}
|
||||
FrontEndScreen(
|
||||
@@ -199,7 +201,7 @@ class MainActivity : ComponentActivity() {
|
||||
steamGames = steamGames, emulators = emulatorList, running = runningLabel,
|
||||
frameGenEngine = FrameGen.engine(this), frameGenMultiplier = FrameGen.multiplier(this),
|
||||
lsfgReady = LsfgNative.isInstalled(this),
|
||||
pageKey = sm?.let { "settings:$it" } ?: if (showPerformance) "performance" else null,
|
||||
pageKey = sm?.let { "settings:$it" } ?: if (showPerformance) "performance" else if (showApps) "apps" else null,
|
||||
theme = theme,
|
||||
),
|
||||
FrontEndActions(
|
||||
@@ -235,7 +237,7 @@ class MainActivity : ComponentActivity() {
|
||||
frameGenLabel = FrameGen.label(this)
|
||||
},
|
||||
onProtons = { refreshProtons(); showProtons = true },
|
||||
onPerformance = { refreshCores(); showPerformance = true },
|
||||
onPerformance = { refreshCores(); showApps = false; showPerformance = true },
|
||||
onRoms = { showRoms = true },
|
||||
onFiles = { startActivity(Intent(this, com.steamdeck.launcher.files.FileManagerActivity::class.java)) },
|
||||
onLogs = {
|
||||
@@ -257,7 +259,7 @@ class MainActivity : ComponentActivity() {
|
||||
},
|
||||
onEmulatorHelp = { showEmulatorHelp = true },
|
||||
onCredits = { showCredits = true },
|
||||
onPageBack = { settingsMode = null; showPerformance = false },
|
||||
onPageBack = { settingsMode = null; showPerformance = false; showApps = false },
|
||||
onTheme = { id -> SessionPrefs.setTheme(this, id); theme = id },
|
||||
),
|
||||
page = page,
|
||||
@@ -271,13 +273,6 @@ class MainActivity : ComponentActivity() {
|
||||
onClear = { SessionPrefs.setRomsDir(this, ""); romsDir = null; showRoms = false },
|
||||
onDismiss = { showRoms = false },
|
||||
)
|
||||
if (showApps) DesktopAppsDialog(
|
||||
rows = packageRows, busyStage = pkgStage, busyPercent = pkgPercent,
|
||||
onInstall = { id -> installPackage(id) },
|
||||
onRemove = { id -> catalog?.firstOrNull { it.id == id }?.let { DesktopCatalog.remove(this, it) }; refreshPackages() },
|
||||
onLaunch = { path -> showApps = false; launchProgram(path) },
|
||||
onDismiss = { showApps = false },
|
||||
)
|
||||
if (showProtons) ProtonDialog(
|
||||
rows = protonRows,
|
||||
onInstall = { id -> ProtonExtras.tools.first { it.id == id }.let { ProtonExtras.queue(this, it) }; refreshProtons() },
|
||||
@@ -334,7 +329,21 @@ class MainActivity : ComponentActivity() {
|
||||
if (!busy) Thread({ checkCatalog() }, "catalog").start()
|
||||
}
|
||||
|
||||
/** Desktop & apps, in the front end's pane. */
|
||||
@Composable
|
||||
private fun AppsHost() {
|
||||
DesktopAppsPage(
|
||||
rows = packageRows, busyId = pkgId, busyStage = pkgStage, busyPercent = pkgPercent,
|
||||
onInstall = { id -> installPackage(id) },
|
||||
onRemove = { id -> catalog?.firstOrNull { it.id == id }?.let { DesktopCatalog.remove(this, it) }; refreshPackages() },
|
||||
onLaunch = { path -> showApps = false; launchProgram(path) },
|
||||
onBack = { showApps = false },
|
||||
)
|
||||
}
|
||||
|
||||
private fun openApps() {
|
||||
settingsMode = null
|
||||
showPerformance = false
|
||||
showApps = true
|
||||
if (catalog.isNullOrEmpty()) Thread({
|
||||
val fetched = DesktopCatalog.fetch()
|
||||
@@ -385,13 +394,13 @@ class MainActivity : ComponentActivity() {
|
||||
private fun installPackage(id: String) {
|
||||
val entry = catalog?.firstOrNull { it.id == id } ?: return
|
||||
if (pkgStage != null) return
|
||||
pkgStage = "Starting…"; pkgPercent = -1
|
||||
pkgId = id; pkgStage = "Starting…"; pkgPercent = -1
|
||||
Thread({
|
||||
val problem = DesktopCatalog.install(this, entry) { stage, percent ->
|
||||
ui.post { pkgStage = stage; pkgPercent = percent }
|
||||
}
|
||||
ui.post {
|
||||
pkgStage = null
|
||||
pkgStage = null; pkgId = null
|
||||
if (problem != null) android.widget.Toast.makeText(this, "${entry.name}: $problem", android.widget.Toast.LENGTH_LONG).show()
|
||||
refreshPackages()
|
||||
}
|
||||
@@ -518,6 +527,7 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
|
||||
private fun openModeSettings(mode: String) {
|
||||
showApps = false
|
||||
refreshDrivers()
|
||||
resolutionCap = SessionPrefs.resolutionCap(this, mode)
|
||||
fexPreset = SessionPrefs.fexPreset(this)
|
||||
|
||||
@@ -1,23 +1,48 @@
|
||||
package com.steamdeck.launcher.ui
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.hoverable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
class PackageRow(
|
||||
val id: String, val name: String, val tier: Int, val version: String, val size: String,
|
||||
@@ -25,72 +50,144 @@ class PackageRow(
|
||||
val launchers: List<Pair<String, String>> = emptyList(),
|
||||
)
|
||||
|
||||
private val RowShape = RoundedCornerShape(10.dp)
|
||||
|
||||
private fun tierName(tier: Int) = when (tier) {
|
||||
1 -> "Native ARM64"
|
||||
2 -> "ARM64 · experimental"
|
||||
else -> "Source build · experimental"
|
||||
}
|
||||
|
||||
/**
|
||||
* Desktop & apps as a page in the front end's pane: one dropdown per tier, opened and closed like
|
||||
* the rail's own groups, and each package with its own Install/Remove and, while it installs, its
|
||||
* own bar. Leaving the page does not stop an install; coming back shows where it is.
|
||||
*/
|
||||
@Composable
|
||||
fun DesktopAppsDialog(
|
||||
fun DesktopAppsPage(
|
||||
rows: List<PackageRow>?,
|
||||
busyId: String?,
|
||||
busyStage: String?,
|
||||
busyPercent: Int,
|
||||
onInstall: (String) -> Unit,
|
||||
onRemove: (String) -> Unit,
|
||||
onLaunch: (path: String) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { if (busyStage == null) onDismiss() },
|
||||
title = { Text("Desktop & apps") },
|
||||
text = {
|
||||
Column(modifier = Modifier.heightIn(max = 420.dp).verticalScroll(rememberScrollState())) {
|
||||
if (busyStage != null) {
|
||||
Text(if (busyPercent >= 0) "$busyStage $busyPercent%" else busyStage, style = MaterialTheme.typography.bodySmall)
|
||||
if (busyPercent >= 0) LinearProgressIndicator(progress = { busyPercent / 100f }, modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp))
|
||||
else LinearProgressIndicator(modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp))
|
||||
Spacer(Modifier.height(8.dp))
|
||||
}
|
||||
when {
|
||||
rows == null -> Text("Could not reach the package catalog", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
rows.isEmpty() -> Text("Loading the catalog…", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
else -> Unit
|
||||
}
|
||||
var lastTier = 0
|
||||
for (row in rows ?: emptyList()) {
|
||||
if (row.tier != lastTier) {
|
||||
lastTier = row.tier
|
||||
Text(
|
||||
when (row.tier) {
|
||||
1 -> "Native ARM64"
|
||||
2 -> "ARM64 · experimental"
|
||||
else -> "Source build · experimental"
|
||||
},
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.padding(top = 8.dp, bottom = 2.dp),
|
||||
)
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(vertical = 3.dp)) {
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(row.name, style = MaterialTheme.typography.bodyMedium)
|
||||
Text(
|
||||
(if (row.installed != null) "Installed ${row.installed}" else "${row.version} · ${row.size}") +
|
||||
(if (row.notes.isNotEmpty()) " · ${row.notes}" else ""),
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
if (row.installed != null) TextButton(enabled = busyStage == null, onClick = { onRemove(row.id) }) { Text("Remove") }
|
||||
else TextButton(enabled = busyStage == null, onClick = { onInstall(row.id) }) { Text("Install") }
|
||||
}
|
||||
if (row.installed != null && row.launchers.isNotEmpty()) {
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(start = 12.dp, bottom = 2.dp)) {
|
||||
for ((label, path) in row.launchers) {
|
||||
TextButton(enabled = busyStage == null, onClick = { onLaunch(path) }) {
|
||||
Text(" $label", style = MaterialTheme.typography.labelMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val host = rememberMenuHost()
|
||||
val colors = MaterialTheme.colorScheme
|
||||
// The first tier starts open; the rest wait for a tap.
|
||||
var open by remember { mutableStateOf(setOf(1)) }
|
||||
// An install always shows: its tier opens when it starts.
|
||||
val busyTier = rows?.firstOrNull { it.id == busyId }?.tier
|
||||
LaunchedEffect(busyTier) { if (busyTier != null) open = open + busyTier }
|
||||
SettingsPage(
|
||||
host, title = "Desktop & apps", eyebrow = "Desktop",
|
||||
lede = "Packages for the Linux desktop. Each installs into the runtime and appears in the desktop's menu.",
|
||||
onBack = onBack,
|
||||
) {
|
||||
when {
|
||||
rows == null -> Text("Could not reach the package catalog.", fontSize = 13.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(top = 12.dp))
|
||||
rows.isEmpty() -> Text("Loading the catalog…", fontSize = 13.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(top = 12.dp))
|
||||
}
|
||||
for ((tier, group) in (rows ?: emptyList()).groupBy { it.tier }.toSortedMap()) {
|
||||
val expanded = tier in open
|
||||
TierHeader(tierName(tier), "${group.count { it.installed != null }}/${group.size}", expanded) {
|
||||
open = if (expanded) open - tier else open + tier
|
||||
}
|
||||
AnimatedVisibility(
|
||||
visible = expanded,
|
||||
enter = expandVertically(Motion.tw(280)) + fadeIn(Motion.tw(280)),
|
||||
exit = shrinkVertically(Motion.tw(200)) + fadeOut(Motion.tw(160)),
|
||||
) {
|
||||
val line = LocalPalette.current.line
|
||||
Column(
|
||||
modifier = Modifier.padding(start = 16.dp).fillMaxWidth()
|
||||
.drawWithContent { drawContent(); drawRect(line, size = size.copy(width = 1.dp.toPx())) }
|
||||
.padding(start = 8.dp, top = 2.dp, bottom = 6.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
) {
|
||||
for (row in group) PackageItem(
|
||||
row, busy = busyId == row.id, anyBusy = busyId != null,
|
||||
stage = busyStage, percent = busyPercent,
|
||||
onInstall = onInstall, onRemove = onRemove, onLaunch = onLaunch,
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = { TextButton(enabled = busyStage == null, onClick = onDismiss) { Text("Close") } },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** A tier's dropdown row: the rail's caret, label and count pill. */
|
||||
@Composable
|
||||
private fun TierHeader(label: String, count: String, expanded: Boolean, onClick: () -> Unit) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
val src = remember { MutableInteractionSource() }
|
||||
val focused by src.collectIsFocusedAsState()
|
||||
val hovered by src.collectIsHoveredAsState()
|
||||
val ring by animateColorAsState(if (focused) pal.signal else Color.Transparent, Motion.tw(180), label = "tierRing")
|
||||
val rot by animateFloatAsState(if (expanded) 90f else 0f, Motion.sp(0.6f), label = "tierCaret")
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp)
|
||||
.clip(RowShape)
|
||||
.background(if (hovered) Color.White.copy(alpha = 0.04f) else Color.Transparent)
|
||||
.border(1.5.dp, ring, RowShape)
|
||||
.hoverable(src).clickable(interactionSource = src, indication = LocalIndication.current, onClick = onClick)
|
||||
.padding(horizontal = 10.dp, vertical = 9.dp),
|
||||
) {
|
||||
Text("›", fontSize = 16.sp, color = colors.onSurfaceVariant, modifier = Modifier.width(14.dp).rotate(rot))
|
||||
Text(label, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, color = colors.onBackground, modifier = Modifier.weight(1f))
|
||||
Text(
|
||||
count, fontSize = 11.sp, color = colors.onSurfaceVariant,
|
||||
modifier = Modifier.clip(RoundedCornerShape(99.dp)).background(colors.background).padding(horizontal = 7.dp, vertical = 2.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** One package inside its tier: name, size or installed version, notes, the button, and its own bar. */
|
||||
@Composable
|
||||
private fun PackageItem(
|
||||
row: PackageRow, busy: Boolean, anyBusy: Boolean, stage: String?, percent: Int,
|
||||
onInstall: (String) -> Unit, onRemove: (String) -> Unit, onLaunch: (String) -> Unit,
|
||||
) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
val bg by animateColorAsState(if (busy) pal.signal.copy(alpha = 0.10f) else Color.Transparent, Motion.tw(200), label = "pkgBg")
|
||||
Column(modifier = Modifier.fillMaxWidth().clip(RowShape).background(bg).padding(horizontal = 10.dp, vertical = 8.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Column(modifier = Modifier.weight(1f).padding(end = 12.dp)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(row.name, fontSize = 14.sp, fontWeight = FontWeight.Medium, color = colors.onBackground,
|
||||
maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f, fill = false))
|
||||
Text(
|
||||
if (row.installed != null) " ● ${row.installed}" else " ${row.size}",
|
||||
fontSize = 11.sp, color = if (row.installed != null) pal.good else colors.onSurfaceVariant, maxLines = 1,
|
||||
)
|
||||
}
|
||||
if (row.notes.isNotEmpty()) Text(row.notes, fontSize = 11.5.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(top = 2.dp))
|
||||
}
|
||||
if (row.installed != null) SecondaryButton("Remove", enabled = !anyBusy) { onRemove(row.id) }
|
||||
else SecondaryButton(if (busy) "Installing…" else "Install", enabled = !anyBusy) { onInstall(row.id) }
|
||||
}
|
||||
if (busy) {
|
||||
Text(
|
||||
if (stage != null && percent >= 0) "$stage · $percent%" else stage ?: "Starting…",
|
||||
fontSize = 11.5.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(top = 6.dp, bottom = 4.dp),
|
||||
)
|
||||
if (percent >= 0) LinearProgressIndicator(progress = { percent / 100f }, modifier = Modifier.fillMaxWidth().height(4.dp))
|
||||
else LinearProgressIndicator(modifier = Modifier.fillMaxWidth().height(4.dp))
|
||||
}
|
||||
if (row.installed != null && row.launchers.isNotEmpty()) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(top = 6.dp)) {
|
||||
for ((label, path) in row.launchers) {
|
||||
Box(
|
||||
modifier = Modifier.clip(RoundedCornerShape(99.dp)).border(1.dp, pal.line2, RoundedCornerShape(99.dp))
|
||||
.clickable(enabled = !anyBusy) { onLaunch(path) }.padding(horizontal = 10.dp, vertical = 4.dp),
|
||||
) { Text("▶ $label", fontSize = 12.sp, color = colors.onBackground) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -331,7 +331,7 @@ private fun Rail(
|
||||
) { if (e.id != "retroarch") a.onRoms() }
|
||||
}
|
||||
}
|
||||
if (s.emulators.none { it.installed }) NavItem("Install emulators", "x", false, small = true, muted = true, register = register, unregister = unregister) { a.onApps() }
|
||||
if (s.emulators.none { it.installed }) NavItem("Install emulators", "apps", selected == "apps", small = true, muted = true, register = register, unregister = unregister) { a.onApps() }
|
||||
NavItem("Settings", "settings:lxqt", selected == "settings:lxqt", small = true, tiny = true, muted = true, i = s.emulators.count { it.installed }, register = register, unregister = unregister) { a.onDesktopSettings() }
|
||||
}
|
||||
Spacer(Modifier.height(6.dp))
|
||||
@@ -342,7 +342,7 @@ private fun Rail(
|
||||
NavItem("Setup", "x", false, caret = openSetup, count = 10, onClick = onToggleSetup)
|
||||
Sub(openSetup) {
|
||||
item("Files", null, 0, null, a.onFiles)
|
||||
item("Desktop & apps", null, 1, null, a.onApps)
|
||||
item("Desktop & apps", null, 1, "apps", a.onApps)
|
||||
item("Compatibility tools", null, 2, null, a.onProtons)
|
||||
Box {
|
||||
item("Frame generation", s.frameGenLabel, 3, null) { menus.open = "fg" }
|
||||
|
||||
Reference in new issue
Block a user