mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-13 03:51:10 +00:00
Driver selection as a full page with refresh and delete for every driver
Runtime driver and Display driver open a full page instead of a drop-down, built from the settings widgets (as previewed): INSTALLED lists every driver as a tappable row with a radio mark and a tag (BUNDLED / DOWNLOADED / IMPORTED); AVAILABLE TO DOWNLOAD lists what the last check of Banners-Turnip and WinNative found, with a Download button and progress; Import stays at the bottom. The refresh button sits beside the title (SettingsPage gains an action slot) and is the only thing that goes online; it spins while checking. Every driver gets a trash can with a confirmation, the bundled ones too: deleting a bundled display driver removes its unpacked copy and hides it (TurnipDriver.hideBundled), with 'Restore built-in drivers' to bring them back. It stays in the apk and Auto still unpacks it on a GPU that needs it, so a deletion can never leave a device without a driver. Auto and Runtime default are settings, not drivers, and have no trash can.
This commit is contained in:
1 parent
97d74e3748
commit
2cd88855f4
6 files changed
+277
-87
No files matched your search
@@ -109,8 +109,9 @@ class MainActivity : ComponentActivity() {
|
||||
/** The latest Banners-Turnip release as each driver menu offers it (see [refreshReleaseRows]). */
|
||||
private var linuxDownloads by mutableStateOf<List<com.steamdeck.launcher.ui.DownloadRow>>(emptyList())
|
||||
private var androidDownloads by mutableStateOf<List<com.steamdeck.launcher.ui.DownloadRow>>(emptyList())
|
||||
private var releaseStatus by mutableStateOf("Check for new drivers (Banners-Turnip, WinNative)")
|
||||
private var releaseChecking = false
|
||||
private var releaseStatus by mutableStateOf("Not checked yet - tap refresh to look for new drivers")
|
||||
private var releaseChecking by mutableStateOf(false)
|
||||
private var canRestoreBundled by mutableStateOf(false)
|
||||
/** Asset name -> download percent, while it downloads. */
|
||||
private val releaseProgress = HashMap<String, Int>()
|
||||
private var linuxSteam by mutableStateOf("")
|
||||
@@ -526,6 +527,7 @@ class MainActivity : ComponentActivity() {
|
||||
addedGames = if (mode == SessionService.MODE_STEAM) addedGames else emptyList(),
|
||||
addedGamesArt = addedGamesArt,
|
||||
linuxDownloads = linuxDownloads, androidDownloads = androidDownloads, releaseStatus = releaseStatus,
|
||||
releaseChecking = releaseChecking, canRestoreBundled = canRestoreBundled,
|
||||
),
|
||||
ModeSettingsActions(
|
||||
onResolution = { cap -> SessionPrefs.setResolutionCap(this, mode, cap); resolutionCap = cap },
|
||||
@@ -537,6 +539,7 @@ class MainActivity : ComponentActivity() {
|
||||
onRemoveLinux = { id -> deleteDriver(id, linux = true) },
|
||||
onRefreshReleases = { checkLatestTurnip() },
|
||||
onDownloadDriver = { name -> downloadReleaseDriver(name) },
|
||||
onRestoreBundled = { TurnipDriver(this).restoreBundled(); refreshDrivers() },
|
||||
onSelectAndroid = { id -> SessionPrefs.setAndroidDriver(this, id); refreshDrivers() },
|
||||
onImportAndroid = { pickAndroidDriver.launch(InAppFilePicker.buildIntent(this, ZIP_EXT, "Choose a display driver (AdrenoTools zip)")) },
|
||||
onRemoveAndroid = { id -> deleteDriver(id, linux = false) },
|
||||
@@ -662,6 +665,7 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
private fun refreshDrivers() {
|
||||
val lm = LinuxVulkanDriverManager(this)
|
||||
fun origin(id: String) = if (TurnipReleases.isDownloaded(this, id)) DriverRow.DOWNLOADED else DriverRow.IMPORTED
|
||||
linuxRows = LinuxVulkanDriver.optionValues(this).map { id ->
|
||||
if (id.isEmpty()) DriverRow("", "Runtime default", "the Turnip built into the runtime", false)
|
||||
else DriverRow(
|
||||
@@ -669,8 +673,8 @@ class MainActivity : ComponentActivity() {
|
||||
listOfNotNull(
|
||||
lm.getDriverVersion(id).takeIf { it.isNotEmpty() },
|
||||
lm.getMinGlibc(id).takeIf { it.isNotEmpty() }?.let { "glibc $it+" },
|
||||
).joinToString(" · ").ifEmpty { "imported" },
|
||||
true,
|
||||
).joinToString(" · "),
|
||||
true, origin(id),
|
||||
)
|
||||
}
|
||||
linuxSteam = SessionPrefs.linuxDriver(this, SessionService.MODE_STEAM)
|
||||
@@ -683,9 +687,10 @@ class MainActivity : ComponentActivity() {
|
||||
if (auto == "system") "system Vulkan: no bundled build for this GPU" else "${td.displayName(auto)} (bundled)",
|
||||
false,
|
||||
))
|
||||
for (id in TurnipDriver.BUNDLED) add(DriverRow(id, td.displayName(id), "bundled" + td.driverVersion(id).let { if (it.isEmpty()) "" else " · $it" }, false))
|
||||
for (id in td.enumerateImported()) add(DriverRow(id, td.displayName(id), "imported" + td.driverVersion(id).let { if (it.isEmpty()) "" else " · $it" }, true))
|
||||
for (id in td.visibleBundled()) add(DriverRow(id, td.displayName(id), td.driverVersion(id), true, DriverRow.BUNDLED))
|
||||
for (id in td.enumerateImported()) add(DriverRow(id, td.displayName(id), td.driverVersion(id), true, origin(id)))
|
||||
}
|
||||
canRestoreBundled = td.hiddenBundled().isNotEmpty()
|
||||
androidSelected = SessionPrefs.androidDriver(this)
|
||||
refreshReleaseRows()
|
||||
}
|
||||
@@ -729,7 +734,8 @@ class MainActivity : ComponentActivity() {
|
||||
if (SessionPrefs.linuxDriver(this, mode) == id) SessionPrefs.setLinuxDriver(this, mode, "")
|
||||
}
|
||||
} else {
|
||||
TurnipDriver(this).remove(id)
|
||||
val td = TurnipDriver(this)
|
||||
if (id in TurnipDriver.BUNDLED) td.hideBundled(id) else td.remove(id)
|
||||
if (SessionPrefs.androidDriver(this) == id) SessionPrefs.setAndroidDriver(this, TurnipDriver.AUTO)
|
||||
}
|
||||
TurnipReleases.forget(this, id)
|
||||
@@ -747,18 +753,15 @@ class MainActivity : ComponentActivity() {
|
||||
.filter { a -> TurnipReleases.installedId(this, a) { id -> if (linux) lm.isInstalled(id) else td.isInstalled(id) } == null }
|
||||
.map { a ->
|
||||
val mb = "%.1f MB".format(a.size / 1_048_576.0)
|
||||
com.steamdeck.launcher.ui.DownloadRow(
|
||||
a.name, "Download ${a.source} ${a.tag}",
|
||||
releaseProgress[a.name]?.let { "${a.label} · downloading $it%" } ?: "${a.label} · $mb",
|
||||
)
|
||||
com.steamdeck.launcher.ui.DownloadRow(a.name, "${a.source} ${a.tag}", "${a.label} · $mb", releaseProgress[a.name])
|
||||
}
|
||||
linuxDownloads = rows(linux = true)
|
||||
androidDownloads = rows(linux = false)
|
||||
if (!releaseChecking) releaseStatus = when (check) {
|
||||
null -> "Check for new drivers (Banners-Turnip, WinNative)"
|
||||
null -> "Not checked yet - tap refresh to look for new drivers"
|
||||
else -> "Latest: " + check.latest.joinToString(" · ") { "${it.first} ${it.second}" } +
|
||||
(if (check.failed.isEmpty()) "" else " · ${check.failed.joinToString()} unreachable") +
|
||||
" · checked ${ago(check.checkedAt)} · tap to refresh"
|
||||
" · checked ${ago(check.checkedAt)}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -784,7 +787,7 @@ class MainActivity : ComponentActivity() {
|
||||
ui.post {
|
||||
releaseChecking = false
|
||||
refreshReleaseRows()
|
||||
if (problem != null) releaseStatus = "Couldn't check: $problem · tap to retry"
|
||||
if (problem != null) releaseStatus = "Couldn't check: $problem"
|
||||
}
|
||||
}, "turnip-release-check").start()
|
||||
}
|
||||
|
||||
@@ -52,6 +52,9 @@ public final class TurnipDriver {
|
||||
public static final List<String> BUNDLED = Collections.unmodifiableList(Arrays.asList(DRIVER_A7XX, DRIVER_A8XX));
|
||||
/** Overrides the pick, for a device we cannot reach: a7xx, a8xx or system. */
|
||||
private static final String OVERRIDE_FILE = "Download/steamdeck-driver";
|
||||
/** Bundled drivers the user deleted: hidden from the list, their unpacked copy removed. */
|
||||
private static final String PREFS = "graphics_driver";
|
||||
private static final String KEY_HIDDEN = "hiddenBundled";
|
||||
/** Stored choice meaning "pick by GPU" - the default, and what a removed import falls back to. */
|
||||
public static final String AUTO = "";
|
||||
|
||||
@@ -111,6 +114,37 @@ public final class TurnipDriver {
|
||||
return library != null && !library.isEmpty() && new File(new File(contentDir, driverId), library).isFile();
|
||||
}
|
||||
|
||||
/**
|
||||
* The bundled drivers the list shows: {@link #BUNDLED} minus the ones the user deleted. A deleted
|
||||
* one still lives in the apk and Auto still unpacks it on a GPU that needs it, so deleting can
|
||||
* never leave a device without a driver; it only leaves the list.
|
||||
*/
|
||||
public List<String> visibleBundled() {
|
||||
java.util.Set<String> hidden = hiddenBundled();
|
||||
ArrayList<String> out = new ArrayList<>();
|
||||
for (String id : BUNDLED) if (!hidden.contains(id)) out.add(id);
|
||||
return out;
|
||||
}
|
||||
|
||||
public java.util.Set<String> hiddenBundled() {
|
||||
return new java.util.HashSet<>(context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
.getStringSet(KEY_HIDDEN, java.util.Collections.<String>emptySet()));
|
||||
}
|
||||
|
||||
/** "Delete" for a bundled driver: remove its unpacked copy and hide it from the list. */
|
||||
public void hideBundled(String driverId) {
|
||||
if (!BUNDLED.contains(driverId)) return;
|
||||
FileUtils.delete(new File(contentDir, driverId));
|
||||
java.util.Set<String> hidden = hiddenBundled();
|
||||
hidden.add(driverId);
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().putStringSet(KEY_HIDDEN, hidden).apply();
|
||||
Log.i(TAG, "bundled driver " + driverId + " hidden by the user");
|
||||
}
|
||||
|
||||
public void restoreBundled() {
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit().remove(KEY_HIDDEN).apply();
|
||||
}
|
||||
|
||||
/** Imported drivers only - the bundled two are listed by {@link #BUNDLED}. */
|
||||
public List<String> enumerateImported() {
|
||||
ArrayList<String> ids = new ArrayList<>();
|
||||
|
||||
@@ -146,6 +146,12 @@ object TurnipReleases {
|
||||
fun installedId(context: Context, asset: Asset, isInstalled: (String) -> Boolean): String? =
|
||||
downloads(context).optString(asset.name, "").takeIf { it.isNotEmpty() && isInstalled(it) }
|
||||
|
||||
/** True when [id] came from a release download rather than a zip picked by hand. */
|
||||
fun isDownloaded(context: Context, id: String): Boolean {
|
||||
val d = downloads(context)
|
||||
return d.keys().asSequence().any { d.optString(it) == id }
|
||||
}
|
||||
|
||||
fun recordDownload(context: Context, asset: Asset, id: String) {
|
||||
prefs(context).edit().putString(KEY_DOWNLOADS, downloads(context).put(asset.name, id).toString()).apply()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
package com.steamdeck.launcher.ui
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
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.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.Refresh
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
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.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.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
|
||||
|
||||
/**
|
||||
* One driver list as a full page - the Runtime driver or the Display driver - opened from the
|
||||
* session's settings page. Installed drivers are picked by tapping the row and each carries a trash
|
||||
* can (the "Auto"/"Runtime default" row is a setting, not a driver, so it has none); what the last
|
||||
* check of the release repos found is offered below; the refresh button beside the title is the
|
||||
* only thing that goes online.
|
||||
*/
|
||||
@Composable
|
||||
fun DriverPage(
|
||||
title: String,
|
||||
hint: String,
|
||||
rows: List<DriverRow>,
|
||||
selected: String,
|
||||
downloads: List<DownloadRow>,
|
||||
status: String,
|
||||
checking: Boolean,
|
||||
importLabel: String,
|
||||
canRestore: Boolean,
|
||||
onSelect: (String) -> Unit,
|
||||
onDelete: (String) -> Unit,
|
||||
onRefresh: () -> Unit,
|
||||
onDownload: (String) -> Unit,
|
||||
onImport: () -> Unit,
|
||||
onRestore: () -> Unit,
|
||||
onBack: () -> Unit,
|
||||
) {
|
||||
val host = rememberMenuHost()
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
var confirm by remember { mutableStateOf<DriverRow?>(null) }
|
||||
SettingsPage(
|
||||
host, title = title, onBack = onBack, lede = "$hint\n$status",
|
||||
action = {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.size(40.dp).clip(RoundedCornerShape(10.dp))
|
||||
.border(1.dp, pal.line, RoundedCornerShape(10.dp))
|
||||
.clickable(enabled = !checking, onClick = onRefresh),
|
||||
) {
|
||||
if (checking) CircularProgressIndicator(strokeWidth = 2.dp, modifier = Modifier.size(18.dp))
|
||||
else Icon(Icons.Outlined.Refresh, contentDescription = "Check for new drivers", tint = colors.onBackground, modifier = Modifier.size(20.dp))
|
||||
}
|
||||
},
|
||||
) {
|
||||
SettingsGroup("Installed") {
|
||||
for (row in rows) InstalledRow(row, row.id == selected, onSelect = { onSelect(row.id) }, onDelete = { confirm = row })
|
||||
}
|
||||
SettingsGroup("Available to download") {
|
||||
if (downloads.isEmpty()) Text(
|
||||
"Nothing new from the last check. Tap the refresh button to look for newer releases.",
|
||||
fontSize = 12.5.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(horizontal = 14.dp, vertical = 14.dp),
|
||||
) else for (d in downloads) {
|
||||
SettingsRow(d.label, d.detail) {
|
||||
val p = d.progress
|
||||
if (p == null) SecondaryButton("Download") { onDownload(d.key) }
|
||||
else Column(horizontalAlignment = Alignment.End, modifier = Modifier.width(120.dp)) {
|
||||
Text("$p%", fontSize = 12.sp, color = colors.onSurfaceVariant)
|
||||
Spacer(Modifier.height(4.dp))
|
||||
LinearProgressIndicator(progress = { p / 100f }, modifier = Modifier.fillMaxWidth())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(top = 14.dp)) {
|
||||
SecondaryButton(importLabel, onClick = onImport)
|
||||
if (canRestore) Text(
|
||||
"Restore built-in drivers", fontSize = 13.sp, fontWeight = FontWeight.SemiBold, color = pal.signal,
|
||||
modifier = Modifier.clip(RoundedCornerShape(8.dp)).clickable(onClick = onRestore).padding(8.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
confirm?.let { row ->
|
||||
AlertDialog(
|
||||
onDismissRequest = { confirm = null },
|
||||
title = { Text("Delete ${row.name}?") },
|
||||
text = {
|
||||
Text(
|
||||
if (row.tag == DriverRow.BUNDLED) "It is built into the app, so this removes its unpacked files and hides it. \"Restore built-in drivers\" brings it back, and Auto still uses it on a GPU that needs it."
|
||||
else "Its files are removed from the app." + if (row.id == selected) " It is the driver in use, so the default takes its place." else "",
|
||||
fontSize = 13.sp,
|
||||
)
|
||||
},
|
||||
confirmButton = { TextButton(onClick = { confirm = null; onDelete(row.id) }) { Text("Delete") } },
|
||||
dismissButton = { TextButton(onClick = { confirm = null }) { Text("Cancel") } },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun InstalledRow(row: DriverRow, selected: Boolean, onSelect: () -> Unit, onDelete: () -> Unit) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth().background(if (selected) pal.signal.copy(alpha = 0.08f) else Color.Transparent)
|
||||
.clickable(onClick = onSelect).padding(horizontal = 14.dp, vertical = 11.dp),
|
||||
) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.size(20.dp).border(2.dp, if (selected) pal.signal else colors.onSurfaceVariant, CircleShape),
|
||||
) { if (selected) Box(Modifier.size(10.dp).background(pal.signal, CircleShape)) }
|
||||
Spacer(Modifier.width(14.dp))
|
||||
Column(Modifier.weight(1f)) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(
|
||||
row.name, fontSize = 15.sp, fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis,
|
||||
color = if (selected) pal.signal else colors.onBackground, modifier = Modifier.weight(1f, fill = false),
|
||||
)
|
||||
if (row.tag.isNotEmpty()) Text(
|
||||
row.tag, fontSize = 9.5.sp, fontWeight = FontWeight.Bold, letterSpacing = 1.sp, color = colors.onSurfaceVariant,
|
||||
modifier = Modifier.padding(start = 8.dp).clip(RoundedCornerShape(5.dp)).background(Color.White.copy(alpha = 0.07f))
|
||||
.padding(horizontal = 6.dp, vertical = 2.dp),
|
||||
)
|
||||
}
|
||||
if (row.detail.isNotEmpty()) Text(row.detail, fontSize = 11.5.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(top = 2.dp))
|
||||
}
|
||||
if (row.removable) Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.size(40.dp).clip(RoundedCornerShape(10.dp)).clickable(onClick = onDelete),
|
||||
) { Icon(Icons.Outlined.Delete, contentDescription = "Delete ${row.name}", tint = colors.onSurfaceVariant, modifier = Modifier.size(20.dp)) }
|
||||
}
|
||||
Box(Modifier.fillMaxWidth().height(1.dp).background(pal.line))
|
||||
}
|
||||
@@ -3,10 +3,6 @@ package com.steamdeck.launcher.ui
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.Download
|
||||
import androidx.compose.material.icons.outlined.Refresh
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -21,10 +17,17 @@ import com.steamdeck.launcher.core.FexPreset
|
||||
import com.steamdeck.launcher.session.SessionPrefs
|
||||
import com.steamdeck.launcher.session.SessionService
|
||||
|
||||
class DriverRow(val id: String, val name: String, val detail: String, val removable: Boolean)
|
||||
/** [tag] is shown beside the name: [BUNDLED], [DOWNLOADED], [IMPORTED], or "" for Auto / Runtime default. */
|
||||
class DriverRow(val id: String, val name: String, val detail: String, val removable: Boolean, val tag: String = "") {
|
||||
companion object {
|
||||
const val BUNDLED = "BUNDLED"
|
||||
const val DOWNLOADED = "DOWNLOADED"
|
||||
const val IMPORTED = "IMPORTED"
|
||||
}
|
||||
}
|
||||
|
||||
/** A release driver (Banners-Turnip, WinNative) that is not installed yet; [key] is its asset name. */
|
||||
class DownloadRow(val key: String, val label: String, val detail: String)
|
||||
class DownloadRow(val key: String, val label: String, val detail: String, val progress: Int? = null)
|
||||
|
||||
class ModeSettings(
|
||||
val mode: String,
|
||||
@@ -59,7 +62,10 @@ class ModeSettings(
|
||||
/** Latest Banners-Turnip release: what each driver menu offers to download, and the refresh line. */
|
||||
val linuxDownloads: List<DownloadRow> = emptyList(),
|
||||
val androidDownloads: List<DownloadRow> = emptyList(),
|
||||
val releaseStatus: String = "Check for new drivers (Banners-Turnip, WinNative)",
|
||||
val releaseStatus: String = "Not checked yet - tap refresh to look for new drivers",
|
||||
val releaseChecking: Boolean = false,
|
||||
/** A bundled display driver was deleted: the page offers to restore it. */
|
||||
val canRestoreBundled: Boolean = false,
|
||||
)
|
||||
|
||||
/** One added game as the settings page shows it: its folder, the chosen .exe, the other .exe files it could be. */
|
||||
@@ -77,6 +83,7 @@ class ModeSettingsActions(
|
||||
val onRefreshReleases: () -> Unit = {},
|
||||
/** Asset name of the release driver to download. */
|
||||
val onDownloadDriver: (String) -> Unit = {},
|
||||
val onRestoreBundled: () -> Unit = {},
|
||||
val onSelectAndroid: (String) -> Unit,
|
||||
val onImportAndroid: () -> Unit,
|
||||
val onRemoveAndroid: (String) -> Unit,
|
||||
@@ -103,6 +110,34 @@ class ModeSettingsActions(
|
||||
fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
val steam = s.mode == SessionService.MODE_STEAM
|
||||
val host = rememberMenuHost()
|
||||
// The two driver lists open as full pages over this one ("rt" = runtime, "panel" = display).
|
||||
var driverPage by remember { mutableStateOf<String?>(null) }
|
||||
when (driverPage) {
|
||||
"rt" -> {
|
||||
DriverPage(
|
||||
title = "Runtime driver",
|
||||
hint = (if (steam) "Used by Steam and games." else "Used by desktop apps.") + " Applies next session.",
|
||||
rows = s.linuxRows, selected = s.linuxSelected, downloads = s.linuxDownloads,
|
||||
status = s.releaseStatus, checking = s.releaseChecking, importLabel = "Import Turnip zip…", canRestore = false,
|
||||
onSelect = a.onSelectLinux, onDelete = a.onRemoveLinux, onRefresh = a.onRefreshReleases,
|
||||
onDownload = a.onDownloadDriver, onImport = a.onImportLinux, onRestore = {}, onBack = { driverPage = null },
|
||||
)
|
||||
return
|
||||
}
|
||||
"panel" -> {
|
||||
DriverPage(
|
||||
title = "Display driver",
|
||||
hint = "Used by the compositor in both modes. Restart the app to apply.",
|
||||
rows = s.androidRows, selected = s.androidSelected, downloads = s.androidDownloads,
|
||||
status = s.releaseStatus, checking = s.releaseChecking, importLabel = "Import an AdrenoTools zip…",
|
||||
canRestore = s.canRestoreBundled,
|
||||
onSelect = a.onSelectAndroid, onDelete = a.onRemoveAndroid, onRefresh = a.onRefreshReleases,
|
||||
onDownload = a.onDownloadDriver, onImport = a.onImportAndroid, onRestore = a.onRestoreBundled,
|
||||
onBack = { driverPage = null },
|
||||
)
|
||||
return
|
||||
}
|
||||
}
|
||||
SettingsPage(
|
||||
host,
|
||||
title = if (steam) "Steam session" else "Desktop session",
|
||||
@@ -140,22 +175,12 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
)
|
||||
}
|
||||
SettingsGroup("Drivers") {
|
||||
DriverRowMenu(
|
||||
host, "rt", "Runtime driver",
|
||||
(if (steam) "Used by Steam and games." else "Used by desktop apps.") + " Applies next session.",
|
||||
s.linuxRows, s.linuxSelected, importLabel = "Import Turnip zip…",
|
||||
onSelect = a.onSelectLinux, onRemove = a.onRemoveLinux, onImport = a.onImportLinux,
|
||||
downloads = s.linuxDownloads, releaseStatus = s.releaseStatus,
|
||||
onRefresh = a.onRefreshReleases, onDownload = a.onDownloadDriver,
|
||||
)
|
||||
DriverRowMenu(
|
||||
host, "panel", "Display driver",
|
||||
"Used by the compositor in both modes. Restart the app to apply.",
|
||||
s.androidRows, s.androidSelected, importLabel = "Import an AdrenoTools zip…",
|
||||
onSelect = a.onSelectAndroid, onRemove = a.onRemoveAndroid, onImport = a.onImportAndroid,
|
||||
downloads = s.androidDownloads, releaseStatus = s.releaseStatus,
|
||||
onRefresh = a.onRefreshReleases, onDownload = a.onDownloadDriver,
|
||||
)
|
||||
SettingsRow("Runtime driver", (if (steam) "Used by Steam and games." else "Used by desktop apps.") + " Applies next session.") {
|
||||
ValueChip(s.linuxRows.firstOrNull { it.id == s.linuxSelected }?.name ?: "Runtime default", open = false) { driverPage = "rt" }
|
||||
}
|
||||
SettingsRow("Display driver", "Used by the compositor in both modes. Restart the app to apply.") {
|
||||
ValueChip(s.androidRows.firstOrNull { it.id == s.androidSelected }?.name ?: "Auto - picked by GPU", open = false) { driverPage = "panel" }
|
||||
}
|
||||
}
|
||||
SettingsGroup(if (steam) "Touch & controls" else "Touch") {
|
||||
ChoiceRow(
|
||||
@@ -277,55 +302,6 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DriverRowMenu(
|
||||
host: MenuHost, key: String, label: String, hint: String, rows: List<DriverRow>, selected: String, importLabel: String,
|
||||
onSelect: (String) -> Unit, onRemove: (String) -> Unit, onImport: () -> Unit,
|
||||
downloads: List<DownloadRow> = emptyList(), releaseStatus: String? = null,
|
||||
onRefresh: () -> Unit = {}, onDownload: (String) -> Unit = {},
|
||||
) {
|
||||
val open = host.open == key
|
||||
val colors = MaterialTheme.colorScheme
|
||||
var confirmDelete by remember { mutableStateOf<DriverRow?>(null) }
|
||||
SettingsRow(label, hint, highlighted = open) {
|
||||
androidx.compose.foundation.layout.Box {
|
||||
ValueChip(rows.firstOrNull { it.id == selected }?.name ?: rows.firstOrNull()?.name ?: "-", open) { host.open = if (open) null else key }
|
||||
AnchoredMenu(open, onDismiss = { if (host.open == key) host.open = null }, title = label) {
|
||||
for (row in rows) MenuItem(
|
||||
row.name, checked = row.id == selected, detail = row.detail.ifEmpty { null },
|
||||
trailing = if (row.removable) ({
|
||||
androidx.compose.material3.Icon(
|
||||
Icons.Outlined.Delete, contentDescription = "Delete ${row.name}",
|
||||
tint = colors.onSurfaceVariant,
|
||||
modifier = Modifier.size(32.dp).clickable { confirmDelete = row }.padding(6.dp),
|
||||
)
|
||||
}) else null,
|
||||
) { onSelect(row.id); host.open = null }
|
||||
// The latest Banners-Turnip release: what is not installed yet, then the refresh line.
|
||||
// Tapping either keeps the menu open, so the progress and the result show in place.
|
||||
for (d in downloads) MenuItem(
|
||||
d.label, checked = false, detail = d.detail,
|
||||
leading = { androidx.compose.material3.Icon(Icons.Outlined.Download, null, tint = colors.onSurfaceVariant, modifier = Modifier.size(18.dp)) },
|
||||
) { onDownload(d.key) }
|
||||
if (releaseStatus != null) MenuItem(
|
||||
releaseStatus, checked = false,
|
||||
leading = { androidx.compose.material3.Icon(Icons.Outlined.Refresh, null, tint = colors.onSurfaceVariant, modifier = Modifier.size(18.dp)) },
|
||||
) { onRefresh() }
|
||||
MenuItem(importLabel, checked = false) { host.open = null; onImport() }
|
||||
}
|
||||
}
|
||||
}
|
||||
confirmDelete?.let { row ->
|
||||
androidx.compose.material3.AlertDialog(
|
||||
onDismissRequest = { confirmDelete = null },
|
||||
title = { Text("Delete ${row.name}?") },
|
||||
text = { Text("Its files are removed from the app. If it is the driver in use, the default takes its place.", fontSize = 13.sp) },
|
||||
confirmButton = { androidx.compose.material3.TextButton(onClick = { confirmDelete = null; onRemove(row.id) }) { Text("Delete") } },
|
||||
dismissButton = { androidx.compose.material3.TextButton(onClick = { confirmDelete = null }) { Text("Cancel") } },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** The Resolution menu's "Custom…" entry. */
|
||||
private const val CUSTOM = -1
|
||||
|
||||
|
||||
@@ -280,6 +280,8 @@ fun SettingsPage(
|
||||
onBack: () -> Unit,
|
||||
eyebrow: String? = null,
|
||||
lede: String? = null,
|
||||
/** A small control at the right of the title (the driver pages' refresh button). */
|
||||
action: (@Composable () -> Unit)? = null,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
@@ -297,7 +299,12 @@ fun SettingsPage(
|
||||
}
|
||||
}
|
||||
}
|
||||
Rise(1) { Title(title) }
|
||||
Rise(1) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(Modifier.weight(1f)) { Title(title) }
|
||||
if (action != null) action()
|
||||
}
|
||||
}
|
||||
if (lede != null) Rise(2) { Lede(lede) }
|
||||
Rise(3, Modifier.weight(1f).fillMaxWidth()) {
|
||||
Column(modifier = Modifier.fillMaxSize().graphicsLayer { alpha = dim }.verticalScroll(rememberScrollState()).padding(bottom = 24.dp)) { content() }
|
||||
|
||||
Reference in new issue
Block a user