mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 02:51:10 +00:00
l10n: drive loading and install stages from step IDs, not their text
The loading screen's checklist, the session's package lines and the Decky plugin button branched on the English wording of progress lines that are now translated. They now carry what the line is about beside it: the installer's Step, a LoadingState topic and readable flag (the session script's English "== STEP" lines are still classified by their text), and DeckyManager's downloadingBinary. English behaviour is unchanged. Refs #212
This commit is contained in:
1 parent
51e3423f58
commit
aece5b9bcb
6 files changed
+114
-47
No files matched your search
@@ -19,6 +19,8 @@ internal class DeckyMenu(private val activity: android.app.Activity, private val
|
||||
var deckyChecking by mutableStateOf(false)
|
||||
var deckyStage by mutableStateOf<String?>(null)
|
||||
var deckyPercent by mutableIntStateOf(-1)
|
||||
/** True while the stage is the plugin's binary download (DeckyManager.downloadingBinary). */
|
||||
var deckyDownloadingBinary by mutableStateOf(false)
|
||||
var deckySupervisor by mutableStateOf(false)
|
||||
var deckyReleaseRequest = 0
|
||||
|
||||
@@ -59,11 +61,12 @@ internal class DeckyMenu(private val activity: android.app.Activity, private val
|
||||
Thread({
|
||||
val problem = runCatching {
|
||||
DeckyManager.installPluginZip(activity, archive) { label, value ->
|
||||
ui.post { deckyStage = label; deckyPercent = value }
|
||||
val binary = DeckyManager.downloadingBinary
|
||||
ui.post { deckyStage = label; deckyPercent = value; deckyDownloadingBinary = binary }
|
||||
}
|
||||
}.getOrElse { error -> activity.getString(R.string.decky_plugin_failed, error.message ?: error.javaClass.simpleName) }
|
||||
ui.post {
|
||||
deckyStage = null; deckyPercent = -1
|
||||
deckyStage = null; deckyPercent = -1; deckyDownloadingBinary = false
|
||||
val message = problem ?: activity.getString(R.string.decky_plugin_installed)
|
||||
android.widget.Toast.makeText(activity, message, android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
@@ -1178,6 +1178,7 @@ class MainActivity : ComponentActivity() {
|
||||
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,
|
||||
deckyDownloadingBinary = decky.deckyDownloadingBinary,
|
||||
deckyEnabled = decky.deckySupervisor, deckySessionRunning = SessionState.running,
|
||||
),
|
||||
ModeSettingsActions(
|
||||
|
||||
@@ -436,6 +436,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
if (hud.text.isNotEmpty()) HudText(hud.text)
|
||||
if (loading.visible) LoadingOverlay(
|
||||
loading.step, loading.percent, loading.elapsed, loading.hint, loading.ended,
|
||||
topic = loading.topic, readable = loading.readable,
|
||||
title = loadingTitle(), steam = loadingMode() == SessionService.MODE_STEAM,
|
||||
// As the drawer's Stop does: the service stops, and a start still installing is cancelled.
|
||||
onCancel = { SessionService.stop(this@SessionActivity); finish() },
|
||||
@@ -796,7 +797,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
"${SessionState.installing}.installing",
|
||||
mapOf("component" to SessionState.installing),
|
||||
)
|
||||
loading.step = getString((if (runtime) RUNTIME_LINES else if (desktop) DESKTOP_LINES else PROTON_LINES).downloading)
|
||||
val first = if (runtime) RUNTIME_LINES else if (desktop) DESKTOP_LINES else PROTON_LINES
|
||||
loading.say(getString(first.downloading), first.topic, readable = true)
|
||||
loading.percent = -1
|
||||
Thread({
|
||||
var failedComponent: String? = null
|
||||
@@ -837,7 +839,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
return@post
|
||||
}
|
||||
loading.percent = -1
|
||||
loading.step = getString(R.string.session_starting_session)
|
||||
loading.say(getString(R.string.session_starting_session), LoadingState.Topic.SESSION, readable = false)
|
||||
// The surface may have come and gone while the download ran; start on the live one.
|
||||
if (surfaceView.holder.surface?.isValid == true) surfaceCreated(surfaceView.holder)
|
||||
}
|
||||
@@ -853,17 +855,32 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}, "session-failure-artifacts").start()
|
||||
}
|
||||
|
||||
/** The loading screen's lines for one package: downloading it (with and without its size), checking it, unpacking it. */
|
||||
private class PackageLines(val downloading: Int, val downloadingMb: Int, val checking: Int, val unpacking: Int)
|
||||
/**
|
||||
* The loading screen's lines for one package: downloading it (with and without its size), checking it,
|
||||
* unpacking it; the checklist [topic] they belong to, and whether the checking line is shown as it is.
|
||||
*/
|
||||
private class PackageLines(
|
||||
val downloading: Int, val downloadingMb: Int, val checking: Int, val unpacking: Int,
|
||||
val topic: LoadingState.Topic, val checkingReadable: Boolean,
|
||||
)
|
||||
|
||||
/** Reports one package's download on the loading screen: "<what> · 332 of 791 MB", checking, unpacking. */
|
||||
private fun progressFor(what: PackageLines, mb: Long) = com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.ProgressListener { stage, p ->
|
||||
uiHandler.post {
|
||||
loading.percent = p
|
||||
loading.step = when {
|
||||
stage.startsWith("Downloading") -> if (p >= 0 && mb > 0) getString(what.downloadingMb, p * mb / 100, mb) else getString(what.downloading)
|
||||
stage.startsWith("Verifying") -> getString(what.checking)
|
||||
else -> getString(what.unpacking)
|
||||
private fun progressFor(what: PackageLines, mb: Long) = object : com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.ProgressListener {
|
||||
override fun onProgress(stage: String, percent: Int) =
|
||||
onProgress(com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.Step.OTHER, stage, percent)
|
||||
|
||||
override fun onProgress(step: com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.Step, stage: String, percent: Int) {
|
||||
uiHandler.post {
|
||||
loading.percent = percent
|
||||
when (step) {
|
||||
com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.Step.DOWNLOADING -> loading.say(
|
||||
if (percent >= 0 && mb > 0) getString(what.downloadingMb, percent * mb / 100, mb) else getString(what.downloading),
|
||||
what.topic, readable = true,
|
||||
)
|
||||
com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.Step.VERIFYING ->
|
||||
loading.say(getString(what.checking), what.topic, what.checkingReadable)
|
||||
else -> loading.say(getString(what.unpacking), what.topic, readable = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -879,7 +896,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
|
||||
/** Null when the desktop package is in, else the loading screen's closing line. */
|
||||
private fun installDesktop(): String? {
|
||||
uiHandler.post { loading.percent = -1; loading.step = getString(R.string.session_desktop_downloading) }
|
||||
uiHandler.post { loading.percent = -1; loading.say(getString(DESKTOP_LINES.downloading), DESKTOP_LINES.topic, readable = true) }
|
||||
val entry = com.droiddeck.launcher.runtime.DesktopCatalog.fetch()?.firstOrNull { it.id == "desktop" }
|
||||
?: return getString(R.string.session_desktop_catalog_unreachable)
|
||||
val problem = com.droiddeck.launcher.runtime.DesktopCatalog.install(this, entry,
|
||||
@@ -889,7 +906,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
|
||||
/** Null when the ARM64 Proton is in, else why not; the session goes on either way. */
|
||||
private fun installProton(): String? {
|
||||
uiHandler.post { loading.percent = -1; loading.step = getString(R.string.session_proton_downloading) }
|
||||
uiHandler.post { loading.percent = -1; loading.say(getString(PROTON_LINES.downloading), PROTON_LINES.topic, readable = true) }
|
||||
val catalog = com.droiddeck.launcher.runtime.DesktopCatalog
|
||||
val entry = catalog.fetch(catalog.STEAM_SEED_URL)?.firstOrNull { it.id == catalog.PROTON_SEED_ID }
|
||||
?: return "catalog unreachable"
|
||||
@@ -2038,14 +2055,17 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
private val RUNTIME_LINES = PackageLines(
|
||||
R.string.session_runtime_downloading, R.string.session_runtime_downloading_mb,
|
||||
R.string.session_runtime_checking, R.string.session_runtime_unpacking,
|
||||
LoadingState.Topic.RUNTIME, checkingReadable = true,
|
||||
)
|
||||
private val DESKTOP_LINES = PackageLines(
|
||||
R.string.session_desktop_downloading, R.string.session_desktop_downloading_mb,
|
||||
R.string.session_desktop_checking, R.string.session_desktop_unpacking,
|
||||
LoadingState.Topic.DESKTOP, checkingReadable = false,
|
||||
)
|
||||
private val PROTON_LINES = PackageLines(
|
||||
R.string.session_proton_downloading, R.string.session_proton_downloading_mb,
|
||||
R.string.session_proton_checking, R.string.session_proton_unpacking,
|
||||
LoadingState.Topic.STEAM, checkingReadable = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -17,9 +17,16 @@ import java.nio.charset.StandardCharsets
|
||||
* progress, and lifting that out is the difference between "starting the Steam client" for three
|
||||
* minutes and a percentage that moves.
|
||||
*/
|
||||
class LoadingState(context: Context, steam: Boolean = true) {
|
||||
class LoadingState(context: Context, private val steam: Boolean = true) {
|
||||
var visible by mutableStateOf(true)
|
||||
var step by mutableStateOf(context.getString(com.droiddeck.launcher.R.string.loadstate_starting))
|
||||
private set
|
||||
/** What [step] is about, so the screen picks its checklist stage without reading the (translated) words. */
|
||||
var topic by mutableStateOf(Topic.SESSION)
|
||||
private set
|
||||
/** [step] is a download or install line worth showing as it is, not one of the script's own notes. */
|
||||
var readable by mutableStateOf(false)
|
||||
private set
|
||||
var percent by mutableIntStateOf(-1)
|
||||
var elapsed by mutableStateOf("")
|
||||
var hint by mutableStateOf("")
|
||||
@@ -41,6 +48,13 @@ class LoadingState(context: Context, steam: Boolean = true) {
|
||||
hint = hints[((seconds / 8) % hints.size).toInt()]
|
||||
}
|
||||
|
||||
/** A line the app itself writes (an install's progress, the session starting). */
|
||||
fun say(line: String, topic: Topic, readable: Boolean) {
|
||||
step = line
|
||||
this.topic = topic
|
||||
this.readable = readable
|
||||
}
|
||||
|
||||
fun showEnded(message: String, detail: String? = null) {
|
||||
visible = true
|
||||
ended = true
|
||||
@@ -51,12 +65,16 @@ class LoadingState(context: Context, steam: Boolean = true) {
|
||||
/** Re-reads the end of the log and updates the line and the bar. */
|
||||
fun update(context: Context, log: File?) {
|
||||
val state = read(context, log) ?: return
|
||||
step = state.first
|
||||
percent = state.second
|
||||
step = state.line
|
||||
topic = state.topic
|
||||
readable = state.readable
|
||||
percent = state.percent
|
||||
}
|
||||
|
||||
private class Line(val line: String, val percent: Int, val topic: Topic, val readable: Boolean)
|
||||
|
||||
/** Only the tail is read: the client alone writes megabytes an hour. */
|
||||
private fun read(context: Context, log: File?): Pair<String, Int>? {
|
||||
private fun read(context: Context, log: File?): Line? {
|
||||
if (log == null || !log.isFile) return null
|
||||
val text = try {
|
||||
RandomAccessFile(log, "r").use { file ->
|
||||
@@ -101,16 +119,45 @@ class LoadingState(context: Context, steam: Boolean = true) {
|
||||
}
|
||||
}
|
||||
return when {
|
||||
downloadAt > stepAt && downloadPercent >= 0 ->
|
||||
Pair(context.getString(com.droiddeck.launcher.R.string.loadstate_steam_update, downloadPercent), downloadPercent)
|
||||
clientDownloadAt == stepAt && clientDownload != null ->
|
||||
Pair(context.getString(com.droiddeck.launcher.R.string.loadstate_steam_client, clientDownload), clientPercent)
|
||||
stepText != null -> Pair(stepText, -1)
|
||||
downloadAt > stepAt && downloadPercent >= 0 -> Line(
|
||||
context.getString(com.droiddeck.launcher.R.string.loadstate_steam_update, downloadPercent), downloadPercent,
|
||||
Topic.STEAM, readable = true,
|
||||
)
|
||||
clientDownloadAt == stepAt && clientDownload != null -> Line(
|
||||
context.getString(com.droiddeck.launcher.R.string.loadstate_steam_client, clientDownload), clientPercent,
|
||||
Topic.STEAM, readable = true,
|
||||
)
|
||||
stepText != null -> Line(stepText, -1, topicOf(stepText, steam), readableScriptStep(stepText))
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/** The checklist stages a loading line can move the screen to. */
|
||||
enum class Topic { RUNTIME, SESSION, DESKTOP, STEAM, STEAM_STARTING, OTHER }
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* A session script "== STEP" line's topic in a Steam ([steam]) or desktop session; those
|
||||
* lines are English whatever the app's language.
|
||||
*/
|
||||
internal fun topicOf(scriptStep: String, steam: Boolean): Topic {
|
||||
val t = scriptStep.lowercase()
|
||||
return when {
|
||||
"linux runtime" in t -> Topic.RUNTIME
|
||||
"starting the session" in t -> Topic.SESSION
|
||||
!steam && "desktop" in t -> Topic.DESKTOP
|
||||
steam && "starting the steam client" in t -> Topic.STEAM_STARTING
|
||||
steam && ("steam" in t || "client" in t || "proton" in t || "library" in t || "compatibility" in t) -> Topic.STEAM
|
||||
else -> Topic.OTHER
|
||||
}
|
||||
}
|
||||
|
||||
/** The script's install lines are worth reading as they are; its other notes are not. */
|
||||
internal fun readableScriptStep(scriptStep: String): Boolean {
|
||||
val t = scriptStep.lowercase()
|
||||
return t.startsWith("download") || t.startsWith("checking the linux") || t.startsWith("unpacking") || t.startsWith("installing")
|
||||
}
|
||||
|
||||
private const val TAIL_BYTES = 48L * 1024
|
||||
private val UPDATE_PROGRESS = Regex("""Downloading update \((\d+) of (\d+) KB\)""")
|
||||
private val INSTALL_COUNT = Regex("""downloading Steam: (\S+) \((\d+)/(\d+)\)""")
|
||||
|
||||
@@ -105,6 +105,8 @@ class ModeSettings(
|
||||
val deckyChecking: Boolean = false,
|
||||
val deckyStage: String? = null,
|
||||
val deckyPercent: Int = -1,
|
||||
/** The stage is the Decky plugin's binary download. */
|
||||
val deckyDownloadingBinary: Boolean = false,
|
||||
val deckyEnabled: Boolean = false,
|
||||
val deckySessionRunning: Boolean = false,
|
||||
)
|
||||
@@ -339,7 +341,7 @@ fun ModeSettingsPage(s: ModeSettings, a: ModeSettingsActions) {
|
||||
stringResource(R.string.mode_decky_plugins_hint),
|
||||
) {
|
||||
SecondaryButton(
|
||||
if (s.deckyStage?.startsWith("Downloading plugin binary") == true) stringResource(R.string.mode_decky_plugin_downloading) else stringResource(R.string.mode_decky_plugin_install_zip),
|
||||
if (s.deckyStage != null && s.deckyDownloadingBinary) stringResource(R.string.mode_decky_plugin_downloading) else stringResource(R.string.mode_decky_plugin_install_zip),
|
||||
enabled = s.deckyInstalled != null && s.deckyStage == null && !s.deckySessionRunning,
|
||||
onClick = a.onPickDeckyPluginZip,
|
||||
)
|
||||
|
||||
@@ -17,6 +17,7 @@ import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import com.droiddeck.launcher.frontend.Library
|
||||
import com.droiddeck.launcher.session.LoadingState
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||
@@ -105,31 +106,24 @@ private fun loadStages(steam: Boolean): List<LoadStage> = if (steam) listOf(
|
||||
)
|
||||
|
||||
/**
|
||||
* The checklist stage a loading line belongs to, -1 when it says nothing about the stage. The lines
|
||||
* are the session script's "== STEP" milestones and the installer's own progress; they arrive in
|
||||
* order, and the screen never steps back.
|
||||
* The checklist stage a loading line's [topic] belongs to, -1 when it says nothing about the stage.
|
||||
* The lines are the session script's "== STEP" milestones and the installer's own progress; they
|
||||
* arrive in order, and the screen never steps back.
|
||||
*/
|
||||
private fun stageOf(step: String, steam: Boolean): Int {
|
||||
val t = step.lowercase()
|
||||
return when {
|
||||
"linux runtime" in t -> 0
|
||||
"starting the session" in t -> 1
|
||||
!steam && "desktop" in t -> 1
|
||||
steam && "starting the steam client" in t -> 3
|
||||
steam && ("steam" in t || "client" in t || "proton" in t || "library" in t || "compatibility" in t) -> 2
|
||||
else -> -1
|
||||
}
|
||||
}
|
||||
|
||||
/** Download and install lines are worth reading as they are; the script's own notes are not. */
|
||||
private fun readableStep(step: String): Boolean {
|
||||
val t = step.lowercase()
|
||||
return t.startsWith("download") || t.startsWith("checking the linux") || t.startsWith("unpacking") || t.startsWith("installing")
|
||||
private fun stageOf(topic: LoadingState.Topic, steam: Boolean): Int = when (topic) {
|
||||
LoadingState.Topic.RUNTIME -> 0
|
||||
LoadingState.Topic.SESSION -> 1
|
||||
LoadingState.Topic.DESKTOP -> if (steam) -1 else 1
|
||||
LoadingState.Topic.STEAM_STARTING -> if (steam) 3 else -1
|
||||
LoadingState.Topic.STEAM -> if (steam) 2 else -1
|
||||
LoadingState.Topic.OTHER -> -1
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LoadingOverlay(
|
||||
step: String, percent: Int, elapsed: String, hint: String, ended: Boolean,
|
||||
/** What [step] is about, and whether it is a download or install line worth showing as it is (see [LoadingState]). */
|
||||
topic: LoadingState.Topic, readable: Boolean,
|
||||
title: String = stringResource(R.string.load_starting_steam), steam: Boolean = true, onCancel: (() -> Unit)? = null,
|
||||
/** An ended session's exit status and log path, shown small under the advice. */
|
||||
endedDetail: String? = null,
|
||||
@@ -144,12 +138,12 @@ fun LoadingOverlay(
|
||||
var ball by remember { mutableStateOf<Pair<Offset, Float>?>(null) }
|
||||
val stages = remember(steam) { loadStages(steam) }
|
||||
var reached by remember { mutableStateOf(0) }
|
||||
LaunchedEffect(step, steam) {
|
||||
val at = stageOf(step, steam)
|
||||
LaunchedEffect(step, topic, steam) {
|
||||
val at = stageOf(topic, steam)
|
||||
if (at > reached) reached = at
|
||||
}
|
||||
val active = reached.coerceAtMost(stages.lastIndex)
|
||||
val detail = if (percent >= 0 || readableStep(step)) step.replaceFirstChar { it.uppercase() } else stringResource(stages[active].detail)
|
||||
val detail = if (percent >= 0 || readable) step.replaceFirstChar { it.uppercase() } else stringResource(stages[active].detail)
|
||||
val context = LocalContext.current
|
||||
// The Steam tab's wall behind it, dimmed and slowed: starting Steam reads as the same place settling in.
|
||||
val games by produceState(emptyList<Library.SteamGame>()) {
|
||||
|
||||
Reference in new issue
Block a user