refactor(launcher): move components, decky and proton state out of MainActivity

This commit is contained in:
xXJsonDeruloXx committed 2026-09-27 11:43:35 -04:00
1 parent 3b82b3e1f8
commit c92a423ba1
4 files changed
+247 -193

No files matched your search

@@ -0,0 +1,84 @@
package com.droiddeck.launcher
import android.os.Handler
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.droiddeck.launcher.session.ComponentsManager
/**
* The Components page's state and work: the Protons and their DXVK/VKD3D sets, the Nightlies
* catalog, downloads and the swap/restore actions. The launcher screen keeps one and shows it.
*/
internal class ComponentsMenu(private val activity: android.app.Activity, private val ui: android.os.Handler) {
var compSnapshot by mutableStateOf<ComponentsManager.Snapshot?>(null)
var compCatalog by mutableStateOf<List<ComponentsManager.CatalogItem>>(emptyList())
var compCatalogAt by mutableStateOf(0L)
var compProton by mutableStateOf<String?>(null)
var compComp by mutableStateOf("dxvk")
var compChecking by mutableStateOf(false)
var compBusy by mutableStateOf<String?>(null)
var compDownloads by mutableStateOf<Map<String, Int>>(emptyMap())
/** Reads the Protons (and on first open saves their originals) off the UI thread; the Nightlies list comes from its cache. */
fun refreshComponents(snapshotFirst: Boolean = false) {
Thread({
if (snapshotFirst) runCatching { ComponentsManager.snapshotAll(activity) }
runCatching { ComponentsManager.applyQueued(activity) }
val snap = runCatching { ComponentsManager.snapshot(activity) }.onFailure { Log.w(TAG, "components", it) }.getOrNull()
val cat = runCatching { ComponentsManager.catalog(activity, false) }.getOrNull()
ui.post {
compSnapshot = snap ?: ComponentsManager.Snapshot(emptyList(), emptyList())
if (cat != null && cat.fetchedAt > 0) { compCatalog = cat.items; compCatalogAt = cat.fetchedAt }
if (compProton == null || snap?.protons?.none { it.proton.id == compProton } == true) compProton = snap?.protons?.firstOrNull()?.proton?.id
}
}, "components").start()
}
fun componentAction(label: String, work: () -> String) {
if (compBusy != null) return
compBusy = label
Thread({
val message = runCatching(work).getOrElse { e -> "$label failed: ${e.message ?: e.javaClass.simpleName}" }
ui.post {
compBusy = null
android.widget.Toast.makeText(activity, message, android.widget.Toast.LENGTH_LONG).show()
refreshComponents()
}
}, "components-action").start()
}
fun refreshComponentCatalog() {
if (compChecking) return
compChecking = true
Thread({
val cat = runCatching { ComponentsManager.catalog(activity, true) }.getOrNull()
ui.post {
compChecking = false
if (cat == null || cat.items.isEmpty()) android.widget.Toast.makeText(activity, "The Nightlies could not be reached", android.widget.Toast.LENGTH_LONG).show()
else { compCatalog = cat.items; compCatalogAt = cat.fetchedAt }
}
}, "components-catalog").start()
}
fun downloadComponent(item: ComponentsManager.CatalogItem) {
if (compDownloads.containsKey(item.file)) return
compDownloads = compDownloads + (item.file to -1)
Thread({
val message = runCatching {
val pkg = ComponentsManager.download(activity, item) { pc -> ui.post { if (compDownloads.containsKey(item.file)) compDownloads = compDownloads + (item.file to pc) } }
"Stored ${pkg.version}"
}.getOrElse { e -> "Download failed: ${e.message ?: e.javaClass.simpleName}" }
ui.post {
compDownloads = compDownloads - item.file
android.widget.Toast.makeText(activity, message, android.widget.Toast.LENGTH_SHORT).show()
refreshComponents()
}
}, "components-download").start()
}
private companion object {
const val TAG = "MainActivity"
}
}
@@ -0,0 +1,54 @@
package com.droiddeck.launcher
import android.os.Handler
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.droiddeck.launcher.runtime.DeckyManager
import com.droiddeck.launcher.session.SessionState
/**
* Decky Loader's state in the launcher: what is installed, the releases on offer and an install
* in progress. The launcher screen keeps one and shows it.
*/
internal class DeckyMenu(private val activity: android.app.Activity, private val ui: android.os.Handler) {
var deckyInstalled by mutableStateOf<String?>(null)
var deckyReleases by mutableStateOf<List<DeckyManager.Release>>(emptyList())
var deckyChecking by mutableStateOf(false)
var deckyStage by mutableStateOf<String?>(null)
var deckyPercent by mutableIntStateOf(-1)
var deckySupervisor by mutableStateOf(false)
var deckyReleaseRequest = 0
fun refreshDecky() {
val request = ++deckyReleaseRequest
deckyChecking = true
Thread({
val channels = runCatching { DeckyManager.releaseChannels(activity) }
.getOrElse { DeckyManager.ReleaseChannels(emptyList(), emptyList()) }
ui.post {
if (request == deckyReleaseRequest) {
// Use the newest compatible stable build, or the newest compatible
// prerelease when the fork has not published a stable ARM64 asset.
deckyReleases = channels.stable.ifEmpty { channels.prerelease }
deckyChecking = false
}
}
}, "decky-releases").start()
}
fun installDecky(release: DeckyManager.Release) {
if (deckyStage != null || SessionState.running) return
deckyStage = "Starting…"; deckyPercent = -1
Thread({
val problem = runCatching {
DeckyManager.install(activity, release) { label, value -> ui.post { deckyStage = label; deckyPercent = value } }
}.getOrElse { error -> "Decky install failed: ${error.message ?: error.javaClass.simpleName}" }
ui.post {
deckyStage = null; deckyPercent = -1; deckyInstalled = DeckyManager.installed(activity)
if (problem != null) android.widget.Toast.makeText(activity, problem, android.widget.Toast.LENGTH_LONG).show()
}
}, "install-decky").start()
}
}
@@ -40,7 +40,6 @@ import com.droiddeck.launcher.ui.ComponentsPage
import com.droiddeck.launcher.session.SessionLogShare
import com.droiddeck.launcher.session.SessionPrefs
import com.droiddeck.launcher.ui.ProtonPage
import com.droiddeck.launcher.ui.ProtonRow
import com.droiddeck.launcher.core.CpuCores
import com.droiddeck.launcher.core.PhantomProcessLimit
import com.droiddeck.launcher.core.PhantomProcessStatus
@@ -78,6 +77,9 @@ import com.droiddeck.launcher.input.SecondScreenDisplays
class MainActivity : ComponentActivity() {
private val ui = Handler(Looper.getMainLooper())
private val drivers = DriverMenus(this, ui)
private val components = ComponentsMenu(this, ui)
private val decky = DeckyMenu(this, ui)
private val protons = ProtonMenu(this, ui)
// The screen's state. Compose redraws whatever reads these when they change.
private var installed by mutableStateOf<String?>(null)
@@ -98,21 +100,6 @@ class MainActivity : ComponentActivity() {
// Components page: FEX / DXVK / VKD3D-Proton per Proton (ComponentsManager).
private var showComponents by mutableStateOf(false)
private var focusComponentsContent by mutableStateOf(true)
private var compSnapshot by mutableStateOf<ComponentsManager.Snapshot?>(null)
private var compCatalog by mutableStateOf<List<ComponentsManager.CatalogItem>>(emptyList())
private var compCatalogAt by mutableStateOf(0L)
private var compProton by mutableStateOf<String?>(null)
private var compComp by mutableStateOf("dxvk")
private var compChecking by mutableStateOf(false)
private var compBusy by mutableStateOf<String?>(null)
private var compDownloads by mutableStateOf<Map<String, Int>>(emptyMap())
private var deckyInstalled by mutableStateOf<String?>(null)
private var deckyReleases by mutableStateOf<List<DeckyManager.Release>>(emptyList())
private var deckyChecking by mutableStateOf(false)
private var deckyStage by mutableStateOf<String?>(null)
private var deckyPercent by mutableIntStateOf(-1)
private var deckySupervisor by mutableStateOf(false)
private var deckyReleaseRequest = 0
private var showMapping by mutableStateOf(false)
private var controllerSettings by mutableStateOf<ControllerPrefs.Settings?>(null)
private var catalog by mutableStateOf<List<DesktopCatalog.Entry>?>(null)
@@ -124,10 +111,6 @@ class MainActivity : ComponentActivity() {
private var desktopInstalled by mutableStateOf(false)
private var offlineAccount by mutableStateOf<String?>(null)
private var offline by mutableStateOf(false)
private var protonRows by mutableStateOf<List<ProtonRow>>(emptyList())
private var protonBusyId by mutableStateOf<String?>(null)
private var protonStage by mutableStateOf<String?>(null)
private var protonPercent by mutableIntStateOf(-1)
private var showPerformance by mutableStateOf(false)
private var clientOverride by mutableStateOf(false)
private var clientCores by mutableStateOf<Set<Int>>(emptySet())
@@ -232,7 +215,7 @@ class MainActivity : ComponentActivity() {
override fun startActivity(intent: Intent?) {
if (intent?.component?.className == SessionActivity::class.java.name) {
when {
protonBusyId != null || ProtonExtras.installInProgress -> {
protons.protonBusyId != null || ProtonExtras.installInProgress -> {
android.widget.Toast.makeText(this, "Wait for the compatibility tool install to finish", android.widget.Toast.LENGTH_SHORT).show()
return
}
@@ -526,7 +509,7 @@ class MainActivity : ComponentActivity() {
if (event.action == KeyEvent.ACTION_DOWN && event.repeatCount == 0) {
val all = ComponentsManager.COMPONENTS
val step = if (event.keyCode == KeyEvent.KEYCODE_BUTTON_R1) 1 else all.size - 1
compComp = all[(all.indexOf(compComp).coerceAtLeast(0) + step) % all.size]
components.compComp = all[(all.indexOf(components.compComp).coerceAtLeast(0) + step) % all.size]
}
return true
}
@@ -542,7 +525,7 @@ class MainActivity : ComponentActivity() {
val applied = runCatching { ComponentsManager.applyQueued(this) }.getOrDefault(emptyList())
if (applied.isNotEmpty()) ui.post {
android.widget.Toast.makeText(this, "Applied: " + applied.joinToString(", "), android.widget.Toast.LENGTH_LONG).show()
if (showComponents) refreshComponents()
if (showComponents) components.refreshComponents()
}
}, "components-queue").start()
oscMode = SessionPrefs.oscMode(this)
@@ -550,9 +533,9 @@ class MainActivity : ComponentActivity() {
refreshHomeAppState()
refreshSecondScreenDisplays()
refresh()
deckyInstalled = DeckyManager.installed(this)
decky.deckyInstalled = DeckyManager.installed(this)
DeckyManager.syncCefMarker(this)
deckySupervisor = DeckyManager.supervisorEnabled(this)
decky.deckySupervisor = DeckyManager.supervisorEnabled(this)
// Added games' art (a store lookup for what the folders lack) starts here, not only when
// the cog opens.
refreshAddedGames()
@@ -621,38 +604,7 @@ class MainActivity : ComponentActivity() {
showComponents = false
showMapping = false
showProtons = true
refreshProtons()
}
private fun refreshDecky() {
val request = ++deckyReleaseRequest
deckyChecking = true
Thread({
val channels = runCatching { DeckyManager.releaseChannels(this) }
.getOrElse { DeckyManager.ReleaseChannels(emptyList(), emptyList()) }
ui.post {
if (request == deckyReleaseRequest) {
// Use the newest compatible stable build, or the newest compatible
// prerelease when the fork has not published a stable ARM64 asset.
deckyReleases = channels.stable.ifEmpty { channels.prerelease }
deckyChecking = false
}
}
}, "decky-releases").start()
}
private fun installDecky(release: DeckyManager.Release) {
if (deckyStage != null || SessionState.running) return
deckyStage = "Starting…"; deckyPercent = -1
Thread({
val problem = runCatching {
DeckyManager.install(this, release) { label, value -> ui.post { deckyStage = label; deckyPercent = value } }
}.getOrElse { error -> "Decky install failed: ${error.message ?: error.javaClass.simpleName}" }
ui.post {
deckyStage = null; deckyPercent = -1; deckyInstalled = DeckyManager.installed(this)
if (problem != null) android.widget.Toast.makeText(this, problem, android.widget.Toast.LENGTH_LONG).show()
}
}, "install-decky").start()
protons.refreshProtons()
}
private fun refreshController() {
@@ -677,69 +629,12 @@ class MainActivity : ComponentActivity() {
showProtons = false
showMapping = false
showComponents = true
refreshComponents(snapshotFirst = true)
}
/** Reads the Protons (and on first open saves their originals) off the UI thread; the Nightlies list comes from its cache. */
private fun refreshComponents(snapshotFirst: Boolean = false) {
Thread({
if (snapshotFirst) runCatching { ComponentsManager.snapshotAll(this) }
runCatching { ComponentsManager.applyQueued(this) }
val snap = runCatching { ComponentsManager.snapshot(this) }.onFailure { Log.w(TAG, "components", it) }.getOrNull()
val cat = runCatching { ComponentsManager.catalog(this, false) }.getOrNull()
ui.post {
compSnapshot = snap ?: ComponentsManager.Snapshot(emptyList(), emptyList())
if (cat != null && cat.fetchedAt > 0) { compCatalog = cat.items; compCatalogAt = cat.fetchedAt }
if (compProton == null || snap?.protons?.none { it.proton.id == compProton } == true) compProton = snap?.protons?.firstOrNull()?.proton?.id
}
}, "components").start()
}
private fun componentAction(label: String, work: () -> String) {
if (compBusy != null) return
compBusy = label
Thread({
val message = runCatching(work).getOrElse { e -> "$label failed: ${e.message ?: e.javaClass.simpleName}" }
ui.post {
compBusy = null
android.widget.Toast.makeText(this, message, android.widget.Toast.LENGTH_LONG).show()
refreshComponents()
}
}, "components-action").start()
}
private fun refreshComponentCatalog() {
if (compChecking) return
compChecking = true
Thread({
val cat = runCatching { ComponentsManager.catalog(this, true) }.getOrNull()
ui.post {
compChecking = false
if (cat == null || cat.items.isEmpty()) android.widget.Toast.makeText(this, "The Nightlies could not be reached", android.widget.Toast.LENGTH_LONG).show()
else { compCatalog = cat.items; compCatalogAt = cat.fetchedAt }
}
}, "components-catalog").start()
}
private fun downloadComponent(item: ComponentsManager.CatalogItem) {
if (compDownloads.containsKey(item.file)) return
compDownloads = compDownloads + (item.file to -1)
Thread({
val message = runCatching {
val pkg = ComponentsManager.download(this, item) { pc -> ui.post { if (compDownloads.containsKey(item.file)) compDownloads = compDownloads + (item.file to pc) } }
"Stored ${pkg.version}"
}.getOrElse { e -> "Download failed: ${e.message ?: e.javaClass.simpleName}" }
ui.post {
compDownloads = compDownloads - item.file
android.widget.Toast.makeText(this, message, android.widget.Toast.LENGTH_SHORT).show()
refreshComponents()
}
}, "components-download").start()
components.refreshComponents(snapshotFirst = true)
}
private fun importComponent(uri: Uri) {
val name = displayNameOf(uri) ?: "imported.wcp"
componentAction("Importing") {
components.componentAction("Importing") {
val tmp = File(cacheDir, "component-import.wcp")
contentResolver.openInputStream(uri)?.use { input -> tmp.outputStream().use { input.copyTo(it) } } ?: error("cannot read the file")
try { "Imported ${ComponentsManager.importPackage(this, tmp, name).version}" } finally { tmp.delete() }
@@ -749,24 +644,24 @@ class MainActivity : ComponentActivity() {
@Composable
private fun ComponentsHost() {
ComponentsPage(
snapshot = compSnapshot,
catalog = compCatalog,
catalogAt = compCatalogAt,
protonId = compProton,
comp = compComp,
checking = compChecking,
busy = compBusy,
downloads = compDownloads,
snapshot = components.compSnapshot,
catalog = components.compCatalog,
catalogAt = components.compCatalogAt,
protonId = components.compProton,
comp = components.compComp,
checking = components.compChecking,
busy = components.compBusy,
downloads = components.compDownloads,
requestInitialFocus = focusComponentsContent,
onProton = { compProton = it },
onComp = { compComp = it },
onSwap = { file -> compProton?.let { pid -> componentAction("Swapping") { ComponentsManager.swap(this, pid, file) } } },
onRestore = { version -> compProton?.let { pid -> componentAction("Restoring") { ComponentsManager.restore(this, pid, compComp, version) } } },
onCancelQueued = { compProton?.let { pid -> componentAction("Cancelling") { ComponentsManager.cancelQueued(this, pid, compComp); "The waiting swap was cancelled." } } },
onDeletePackage = { file -> componentAction("Deleting") { ComponentsManager.deletePackage(this, file) } },
onDeleteOriginal = { version -> compProton?.let { pid -> componentAction("Deleting") { ComponentsManager.deleteOriginal(this, pid, compComp, version) } } },
onDownload = { downloadComponent(it) },
onRefresh = { refreshComponentCatalog() },
onProton = { components.compProton = it },
onComp = { components.compComp = it },
onSwap = { file -> components.compProton?.let { pid -> components.componentAction("Swapping") { ComponentsManager.swap(this, pid, file) } } },
onRestore = { version -> components.compProton?.let { pid -> components.componentAction("Restoring") { ComponentsManager.restore(this, pid, components.compComp, version) } } },
onCancelQueued = { components.compProton?.let { pid -> components.componentAction("Cancelling") { ComponentsManager.cancelQueued(this, pid, components.compComp); "The waiting swap was cancelled." } } },
onDeletePackage = { file -> components.componentAction("Deleting") { ComponentsManager.deletePackage(this, file) } },
onDeleteOriginal = { version -> components.compProton?.let { pid -> components.componentAction("Deleting") { ComponentsManager.deleteOriginal(this, pid, components.compComp, version) } } },
onDownload = { components.downloadComponent(it) },
onRefresh = { components.refreshComponentCatalog() },
onImport = { pickComponent.launch(InAppFilePicker.buildIntent(this, WCP_EXT, "Choose a component package (-linux .wcp)")) },
onBack = { showComponents = false },
)
@@ -775,56 +670,19 @@ class MainActivity : ComponentActivity() {
@Composable
private fun ProtonHost() {
ProtonPage(
rows = protonRows,
busyId = protonBusyId,
stage = protonStage,
percent = protonPercent,
rows = protons.protonRows,
busyId = protons.protonBusyId,
stage = protons.protonStage,
percent = protons.protonPercent,
runtimeReady = ready,
sessionRunning = SessionState.running,
onInstall = { id -> installProton(id) },
onCancel = { id -> ProtonExtras.tools.firstOrNull { it.id == id }?.let { ProtonExtras.unqueue(this, it) }; refreshProtons() },
onRemove = { id -> removeProton(id) },
onInstall = { id -> protons.installProton(id) },
onCancel = { id -> ProtonExtras.tools.firstOrNull { it.id == id }?.let { ProtonExtras.unqueue(this, it) }; protons.refreshProtons() },
onRemove = { id -> protons.removeProton(id) },
onBack = { showProtons = false },
)
}
private fun installProton(id: String) {
val tool = ProtonExtras.tools.firstOrNull { it.id == id } ?: return
if (protonBusyId != null || SessionState.running) return
ProtonExtras.unqueue(this, tool)
protonBusyId = id
protonStage = "Starting…"
protonPercent = -1
Thread({
val problem = ProtonExtras.install(this, tool) { label, value ->
ui.post { protonStage = label; protonPercent = value }
}
ui.post {
protonBusyId = null
protonStage = null
protonPercent = -1
refreshProtons()
if (problem != null) android.widget.Toast.makeText(this, problem, android.widget.Toast.LENGTH_LONG).show()
}
}, "install-proton-$id").start()
}
private fun removeProton(id: String) {
val tool = ProtonExtras.tools.firstOrNull { it.id == id } ?: return
if (protonBusyId != null || SessionState.running) return
protonBusyId = id
protonStage = "Removing ${tool.name}…"
protonPercent = -1
Thread({
ProtonExtras.remove(this, tool)
ui.post {
protonBusyId = null
protonStage = null
refreshProtons()
}
}, "remove-proton-$id").start()
}
private fun refreshPackages() {
packageRows = catalog?.map { PackageRow(it.id, it.kind, it.notes) }
desktopInstalled = DesktopCatalog.desktopInstalled(this)
@@ -895,10 +753,10 @@ class MainActivity : ComponentActivity() {
addedGamesArt = addedGamesArt,
linuxDownloads = drivers.linuxDownloads, androidDownloads = drivers.androidDownloads, releaseStatus = drivers.releaseStatus,
releaseChecking = drivers.releaseChecking, canRestoreBundled = drivers.canRestoreBundled,
deckyInstalled = if (mode == SessionService.MODE_STEAM) deckyInstalled else null,
deckyLatestRelease = if (mode == SessionService.MODE_STEAM) deckyReleases.firstOrNull() else null,
deckyChecking = deckyChecking, deckyStage = deckyStage, deckyPercent = deckyPercent,
deckyEnabled = deckySupervisor, deckySessionRunning = SessionState.running,
deckyInstalled = if (mode == SessionService.MODE_STEAM) decky.deckyInstalled else null,
deckyLatestRelease = if (mode == SessionService.MODE_STEAM) decky.deckyReleases.firstOrNull() else null,
deckyChecking = decky.deckyChecking, deckyStage = decky.deckyStage, deckyPercent = decky.deckyPercent,
deckyEnabled = decky.deckySupervisor, deckySessionRunning = SessionState.running,
),
ModeSettingsActions(
onResolution = { cap -> SessionPrefs.setResolutionCap(this, mode, cap); resolutionCap = cap },
@@ -952,13 +810,13 @@ class MainActivity : ComponentActivity() {
pendingAddedGame = folder
pickAddedGameExe.launch(InAppFilePicker.buildIntent(this, listOf("exe"), "Choose the game's .exe", folder))
},
onDeckyInstall = { release -> installDecky(release) },
onDeckyCheck = { refreshDecky() },
onDeckyEnabled = { enabled -> DeckyManager.setSupervisorEnabled(this, enabled); deckySupervisor = enabled },
onDeckyInstall = { release -> decky.installDecky(release) },
onDeckyCheck = { decky.refreshDecky() },
onDeckyEnabled = { enabled -> DeckyManager.setSupervisorEnabled(this, enabled); decky.deckySupervisor = enabled },
onDeckyUninstall = {
DeckyManager.uninstall(this, wipeData = false)
deckyInstalled = null
deckySupervisor = false
decky.deckyInstalled = null
decky.deckySupervisor = false
},
onDismiss = { settingsMode = null },
),
@@ -1040,9 +898,9 @@ class MainActivity : ComponentActivity() {
gameStorage = SessionPrefs.gameStorage(this)
storageOptions = GameStorage.options(this).map { it.label to it.path }
if (mode == SessionService.MODE_STEAM) {
deckyInstalled = DeckyManager.installed(this)
deckySupervisor = DeckyManager.supervisorEnabled(this)
refreshDecky()
decky.deckyInstalled = DeckyManager.installed(this)
decky.deckySupervisor = DeckyManager.supervisorEnabled(this)
decky.refreshDecky()
}
settingsMode = mode
}
@@ -1075,10 +933,6 @@ class MainActivity : ComponentActivity() {
refreshPhantomStatus()
}
private fun refreshProtons() {
protonRows = ProtonExtras.tools.map { ProtonRow(it.id, it.name, ProtonExtras.installed(this, it), ProtonExtras.queued(this, it)) }
}
private fun refresh() {
if (!busy && LinuxRuntimeInstaller.isInstalling()) followInstall { LinuxRuntimeInstaller.attach(it) }
desktopInstalled = DesktopCatalog.desktopInstalled(this)
@@ -0,0 +1,62 @@
package com.droiddeck.launcher
import android.os.Handler
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.droiddeck.launcher.session.ProtonExtras
import com.droiddeck.launcher.ui.ProtonRow
import com.droiddeck.launcher.session.SessionState
/**
* The optional ARM64 Protons (GE-Proton, proton-cachyos): the rows the page lists and an install or
* removal in progress. The launcher screen keeps one and shows it.
*/
internal class ProtonMenu(private val activity: android.app.Activity, private val ui: android.os.Handler) {
var protonRows by mutableStateOf<List<ProtonRow>>(emptyList())
var protonBusyId by mutableStateOf<String?>(null)
var protonStage by mutableStateOf<String?>(null)
var protonPercent by mutableIntStateOf(-1)
fun installProton(id: String) {
val tool = ProtonExtras.tools.firstOrNull { it.id == id } ?: return
if (protonBusyId != null || SessionState.running) return
ProtonExtras.unqueue(activity, tool)
protonBusyId = id
protonStage = "Starting…"
protonPercent = -1
Thread({
val problem = ProtonExtras.install(activity, tool) { label, value ->
ui.post { protonStage = label; protonPercent = value }
}
ui.post {
protonBusyId = null
protonStage = null
protonPercent = -1
refreshProtons()
if (problem != null) android.widget.Toast.makeText(activity, problem, android.widget.Toast.LENGTH_LONG).show()
}
}, "install-proton-$id").start()
}
fun removeProton(id: String) {
val tool = ProtonExtras.tools.firstOrNull { it.id == id } ?: return
if (protonBusyId != null || SessionState.running) return
protonBusyId = id
protonStage = "Removing ${tool.name}…"
protonPercent = -1
Thread({
ProtonExtras.remove(activity, tool)
ui.post {
protonBusyId = null
protonStage = null
refreshProtons()
}
}, "remove-proton-$id").start()
}
fun refreshProtons() {
protonRows = ProtonExtras.tools.map { ProtonRow(it.id, it.name, ProtonExtras.installed(activity, it), ProtonExtras.queued(activity, it)) }
}
}