mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 19:41:10 +00:00
Merge branch 'wip/l10n-g1' into feat/language-selection
This commit is contained in:
commit
fba2f378be
8 files changed
+416
-158
No files matched your search
@@ -63,7 +63,7 @@ internal class ComponentsMenu(private val activity: android.app.Activity, privat
|
||||
if (compBusy != null) return
|
||||
compBusy = label
|
||||
Thread({
|
||||
val message = runCatching(work).getOrElse { e -> "$label failed: ${e.message ?: e.javaClass.simpleName}" }
|
||||
val message = runCatching(work).getOrElse { e -> activity.getString(R.string.main_action_failed, label, e.message ?: e.javaClass.simpleName) }
|
||||
ui.post {
|
||||
compBusy = null
|
||||
android.widget.Toast.makeText(activity, message, android.widget.Toast.LENGTH_LONG).show()
|
||||
@@ -79,7 +79,7 @@ internal class ComponentsMenu(private val activity: android.app.Activity, privat
|
||||
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()
|
||||
if (cat == null || cat.items.isEmpty()) android.widget.Toast.makeText(activity, activity.getString(R.string.comp_nightlies_unreachable), android.widget.Toast.LENGTH_LONG).show()
|
||||
else { compCatalog = cat.items; compCatalogAt = cat.fetchedAt }
|
||||
}
|
||||
}, "components-catalog").start()
|
||||
@@ -91,8 +91,8 @@ internal class ComponentsMenu(private val activity: android.app.Activity, privat
|
||||
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}" }
|
||||
activity.getString(R.string.comp_stored, pkg.version)
|
||||
}.getOrElse { e -> activity.getString(R.string.comp_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()
|
||||
|
||||
@@ -41,11 +41,11 @@ internal class DeckyMenu(private val activity: android.app.Activity, private val
|
||||
|
||||
fun installDecky(release: DeckyManager.Release) {
|
||||
if (deckyStage != null || SessionState.running) return
|
||||
deckyStage = "Starting…"; deckyPercent = -1
|
||||
deckyStage = activity.getString(R.string.store_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}" }
|
||||
}.getOrElse { error -> activity.getString(R.string.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()
|
||||
@@ -55,16 +55,16 @@ internal class DeckyMenu(private val activity: android.app.Activity, private val
|
||||
|
||||
fun importPluginZip(archive: File) {
|
||||
if (deckyStage != null || SessionState.running || deckyInstalled == null) return
|
||||
deckyStage = "Starting plugin import…"; deckyPercent = -1
|
||||
deckyStage = activity.getString(R.string.decky_plugin_starting); deckyPercent = -1
|
||||
Thread({
|
||||
val problem = runCatching {
|
||||
DeckyManager.installPluginZip(activity, archive) { label, value ->
|
||||
ui.post { deckyStage = label; deckyPercent = value }
|
||||
}
|
||||
}.getOrElse { error -> "Plugin import failed: ${error.message ?: error.javaClass.simpleName}" }
|
||||
}.getOrElse { error -> activity.getString(R.string.decky_plugin_failed, error.message ?: error.javaClass.simpleName) }
|
||||
ui.post {
|
||||
deckyStage = null; deckyPercent = -1
|
||||
val message = problem ?: "Plugin installed. Restart Steam to load it."
|
||||
val message = problem ?: activity.getString(R.string.decky_plugin_installed)
|
||||
android.widget.Toast.makeText(activity, message, android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}, "import-decky-plugin").start()
|
||||
|
||||
@@ -37,7 +37,8 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
/** The latest Banners-Turnip release as each driver menu offers it (see [refreshReleaseRows]). */
|
||||
var linuxDownloads by mutableStateOf<List<com.droiddeck.launcher.ui.DownloadRow>>(emptyList())
|
||||
var androidDownloads by mutableStateOf<List<com.droiddeck.launcher.ui.DownloadRow>>(emptyList())
|
||||
var releaseStatus by mutableStateOf("Not checked yet - tap refresh to look for new drivers")
|
||||
// Empty until the first refresh: the activity has no context yet while its fields are made.
|
||||
var releaseStatus by mutableStateOf("")
|
||||
var releaseChecking by mutableStateOf(false)
|
||||
var canRestoreBundled by mutableStateOf(false)
|
||||
/** Asset name -> download percent, while it downloads. */
|
||||
@@ -60,7 +61,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
gpuName = gpu.name, gpuFamily = gpu.family.label, soc = gpu.soc, supportText = gpu.supportText,
|
||||
supported = gpu.support == GpuInfo.Support.TESTED, unsupported = gpu.support == GpuInfo.Support.UNSUPPORTED,
|
||||
auto = mode == SessionPrefs.GPU_DRIVERS_AUTO, pairs = pairRows, busy = pairBusy, percent = pairPercent,
|
||||
autoStatus = autoStatus, releaseStatus = releaseStatus, checking = releaseChecking,
|
||||
autoStatus = autoStatus, releaseStatus = releaseStatus.ifEmpty { activity.getString(R.string.drivers_not_checked) }, checking = releaseChecking,
|
||||
linuxRows = linuxRows, linuxSelected = linuxSelected, androidRows = androidRows, androidSelected = androidSelected,
|
||||
linuxDownloads = linuxDownloads, androidDownloads = androidDownloads, canRestoreBundled = canRestoreBundled,
|
||||
activeBundle = activeBundle,
|
||||
@@ -73,10 +74,10 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
fun origin(id: String) = if (TurnipReleases.isDownloaded(activity, id)) DriverRow.DOWNLOADED else DriverRow.IMPORTED
|
||||
val bundles = DriverBundle.all(activity)
|
||||
fun bundleRow(id: String, linux: Boolean) = bundles.firstOrNull { (if (linux) it.linuxId else it.androidId) == id }?.let {
|
||||
DriverRow(id, it.label, "Android + Linux: sets the ${if (linux) "display" else "runtime"} driver with it", true, DriverRow.BUNDLE)
|
||||
DriverRow(id, it.label, activity.getString(if (linux) R.string.drivers_bundle_sets_display else R.string.drivers_bundle_sets_runtime), true, DriverRow.BUNDLE)
|
||||
}
|
||||
linuxRows = LinuxVulkanDriver.optionValues(activity).map { id ->
|
||||
if (id.isEmpty()) DriverRow("", "Runtime default", "the Turnip built into the runtime", false)
|
||||
if (id.isEmpty()) DriverRow("", activity.getString(R.string.gpu_runtime_default), activity.getString(R.string.drivers_runtime_builtin), false)
|
||||
else bundleRow(id, linux = true) ?: DriverRow(
|
||||
id, lm.getDriverName(id),
|
||||
listOfNotNull(
|
||||
@@ -92,8 +93,8 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
val auto = td.autoId()
|
||||
androidRows = buildList {
|
||||
add(DriverRow(
|
||||
TurnipDriver.AUTO, "Auto - picked by GPU",
|
||||
if (auto == "system") "system Vulkan: no bundled build for this GPU" else "${td.displayName(auto)} (bundled)",
|
||||
TurnipDriver.AUTO, activity.getString(R.string.gpu_auto_by_gpu),
|
||||
if (auto == "system") activity.getString(R.string.drivers_system_vulkan) else activity.getString(R.string.drivers_bundled, td.displayName(auto)),
|
||||
false,
|
||||
))
|
||||
for (id in td.visibleBundled()) add(DriverRow(id, td.displayName(id), td.driverVersion(id), true, DriverRow.BUNDLED))
|
||||
@@ -109,7 +110,8 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
/** One line for the settings row that opens this: "Auto · WinNative · Balanced". */
|
||||
fun summary(): String {
|
||||
val active = pairRows.firstOrNull { it.active }?.name ?: activeBundle
|
||||
return (if (mode == SessionPrefs.GPU_DRIVERS_AUTO) "Auto" else "Manual") + (active?.let { " · $it" } ?: "")
|
||||
val label = activity.getString(if (mode == SessionPrefs.GPU_DRIVERS_AUTO) R.string.common_auto else R.string.drivers_manual)
|
||||
return active?.let { activity.getString(R.string.drivers_summary, label, it) } ?: label
|
||||
}
|
||||
|
||||
/** The pairs the last check found, with what is installed and in use; this GPU's first. */
|
||||
@@ -125,7 +127,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
val mb = p.assets.sumOf { it.size } / 1_048_576.0
|
||||
PairRow(
|
||||
key = p.key, name = p.name, version = p.version,
|
||||
detail = if (!p.complete) "Only one half is published right now" else "%.0f MB for both".format(mb),
|
||||
detail = if (!p.complete) activity.getString(R.string.drivers_half_published) else activity.getString(R.string.drivers_pair_size, mb),
|
||||
recommended = p.key == recommended, suits = p.suits(gpu), complete = p.complete,
|
||||
installed = displayId != null && linuxId != null,
|
||||
active = displayId != null && linuxId != null &&
|
||||
@@ -153,11 +155,11 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
if (!force && autoCheckedThisProcess) return
|
||||
autoCheckedThisProcess = true
|
||||
if (DriverPairs.recommendedKey(gpu, emptyList()) == null) {
|
||||
autoStatus = "No drivers to set: ${gpu.supportText.lowercase()}"
|
||||
autoStatus = activity.getString(R.string.drivers_none_to_set, gpu.supportText.lowercase())
|
||||
return
|
||||
}
|
||||
releaseChecking = true
|
||||
autoStatus = "Checking for the latest drivers…"
|
||||
autoStatus = activity.getString(R.string.drivers_checking_latest)
|
||||
Thread({
|
||||
val cached = TurnipReleases.cached(activity)
|
||||
val stale = cached == null || System.currentTimeMillis() - cached.checkedAt > 24 * 3_600_000L ||
|
||||
@@ -172,9 +174,10 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
val pair = pairs.firstOrNull { it.key == key }
|
||||
val row = pairRows.firstOrNull { it.key == key }
|
||||
when {
|
||||
row?.active == true -> autoStatus = "Up to date" + (problem?.let { " (couldn't check: $it)" } ?: "")
|
||||
row?.active == true -> autoStatus = problem?.let { activity.getString(R.string.drivers_up_to_date_check_failed, it) }
|
||||
?: activity.getString(R.string.upd_current_status)
|
||||
pair == null || !pair.complete ->
|
||||
autoStatus = problem?.let { "Couldn't check: $it" } ?: "The recommended drivers aren't published right now"
|
||||
autoStatus = problem?.let { activity.getString(R.string.upd_couldnt_check, it) } ?: activity.getString(R.string.drivers_recommended_unpublished)
|
||||
else -> installPair(pair, auto = true)
|
||||
}
|
||||
}
|
||||
@@ -197,7 +200,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
if (!pair.complete) return
|
||||
pairBusy = pair.key
|
||||
pairPercent = 0
|
||||
if (auto) autoStatus = "Downloading ${pair.name} ${pair.version}…"
|
||||
if (auto) autoStatus = activity.getString(R.string.drivers_downloading_pair, pair.name, pair.version)
|
||||
Thread({
|
||||
val lm = LinuxVulkanDriverManager(activity)
|
||||
val td = TurnipDriver(activity)
|
||||
@@ -217,7 +220,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
val displayChanged = SessionPrefs.androidDriver(activity) != displayId
|
||||
SessionPrefs.setAndroidDriver(activity, displayId)
|
||||
SessionPrefs.setLinuxDriver(activity, linuxId)
|
||||
val restart = if (displayChanged && CompositorHost.isStarted) " The display driver applies after DroidDeck restarts." else ""
|
||||
val restart = displayChanged && CompositorHost.isStarted
|
||||
if (auto) {
|
||||
val previous = SessionPrefs.gpuAutoInstalled(activity)
|
||||
for (id in previous - setOf(displayId, linuxId)) {
|
||||
@@ -230,14 +233,18 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
TurnipReleases.forget(activity, id)
|
||||
}
|
||||
SessionPrefs.setGpuAutoInstalled(activity, setOf(displayId, linuxId))
|
||||
autoStatus = (if (downloaded) "Updated to" else "Switched to") + " ${pair.name} ${pair.version}.$restart"
|
||||
autoStatus = activity.getString(
|
||||
if (downloaded) { if (restart) R.string.drivers_updated_to_restart else R.string.drivers_updated_to }
|
||||
else { if (restart) R.string.drivers_switched_to_restart else R.string.drivers_switched_to },
|
||||
pair.name, pair.version,
|
||||
)
|
||||
} else {
|
||||
android.widget.Toast.makeText(activity, "Using ${pair.name} ${pair.version}.$restart", android.widget.Toast.LENGTH_LONG).show()
|
||||
android.widget.Toast.makeText(activity, activity.getString(if (restart) R.string.drivers_using_restart else R.string.drivers_using, pair.name, pair.version), android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}.onFailure { e ->
|
||||
Log.w(TAG, "driver pair ${pair.key}", e)
|
||||
val why = if (e is IllegalArgumentException) e.message else "Download failed: ${e.message}"
|
||||
if (auto) autoStatus = why ?: "Download failed"
|
||||
val why = if (e is IllegalArgumentException) e.message else activity.getString(R.string.comp_download_failed, e.message)
|
||||
if (auto) autoStatus = why ?: activity.getString(R.string.user_apps_download_failed)
|
||||
else android.widget.Toast.makeText(activity, why, android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
refreshDrivers()
|
||||
@@ -298,7 +305,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
try {
|
||||
LinuxVulkanDriverManager(activity).installDriver(uri, name)
|
||||
} catch (runtime: IllegalArgumentException) {
|
||||
throw IllegalArgumentException("Not a driver zip: neither an AdrenoTools driver, a -Linux Turnip nor an Android + Linux bundle")
|
||||
throw IllegalArgumentException(activity.getString(R.string.drivers_not_a_driver))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -307,10 +314,11 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
e.message
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "driver import", e)
|
||||
"Import failed: ${e.message}"
|
||||
activity.getString(R.string.drivers_import_failed, e.message)
|
||||
}
|
||||
ui.post {
|
||||
val done = bundle?.let { useBundle(it) } ?: "Imported ${name ?: "driver"}"
|
||||
val done = bundle?.let { useBundle(it) }
|
||||
?: name?.let { activity.getString(R.string.comp_imported, it) } ?: activity.getString(R.string.drivers_imported_unnamed)
|
||||
android.widget.Toast.makeText(
|
||||
activity, problem ?: done,
|
||||
if (problem != null || bundle != null) android.widget.Toast.LENGTH_LONG else android.widget.Toast.LENGTH_SHORT,
|
||||
@@ -340,8 +348,10 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
val displayChanged = SessionPrefs.androidDriver(activity) != bundle.androidId
|
||||
DriverBundle.select(activity, bundle)
|
||||
if (mode == SessionPrefs.GPU_DRIVERS_AUTO) setMode(false)
|
||||
return "Using ${bundle.label} as both the runtime and display driver." +
|
||||
if (displayChanged && CompositorHost.isStarted) " The display driver applies after DroidDeck restarts." else ""
|
||||
return activity.getString(
|
||||
if (displayChanged && CompositorHost.isStarted) R.string.drivers_using_bundle_restart else R.string.drivers_using_bundle,
|
||||
bundle.label,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -354,7 +364,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
if (bundle != null) {
|
||||
DriverBundle.remove(activity, bundle)
|
||||
TurnipReleases.forget(activity, bundle.id)
|
||||
android.widget.Toast.makeText(activity, "Deleted ${bundle.label} (both drivers)", android.widget.Toast.LENGTH_SHORT).show()
|
||||
android.widget.Toast.makeText(activity, activity.getString(R.string.drivers_deleted_bundle, bundle.label), android.widget.Toast.LENGTH_SHORT).show()
|
||||
refreshDrivers()
|
||||
return
|
||||
}
|
||||
@@ -367,7 +377,7 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
if (SessionPrefs.androidDriver(activity) == id) SessionPrefs.setAndroidDriver(activity, TurnipDriver.AUTO)
|
||||
}
|
||||
TurnipReleases.forget(activity, id)
|
||||
android.widget.Toast.makeText(activity, "Deleted ${id}", android.widget.Toast.LENGTH_SHORT).show()
|
||||
android.widget.Toast.makeText(activity, activity.getString(R.string.drivers_deleted, id), android.widget.Toast.LENGTH_SHORT).show()
|
||||
refreshDrivers()
|
||||
}
|
||||
|
||||
@@ -378,26 +388,28 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
.filter { it.bundle || it.linux == linux }
|
||||
.filter { a -> installedIdOf(a) == null }
|
||||
.map { a ->
|
||||
val mb = "%.1f MB".format(a.size / 1_048_576.0)
|
||||
val mb = activity.getString(R.string.common_size_mb, a.size / 1_048_576.0)
|
||||
com.droiddeck.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 -> "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)}"
|
||||
null -> activity.getString(R.string.drivers_not_checked)
|
||||
else -> {
|
||||
val latest = check.latest.joinToString(" · ") { "${it.first} ${it.second}" }
|
||||
if (check.failed.isEmpty()) activity.getString(R.string.drivers_release_status, latest, ago(check.checkedAt))
|
||||
else activity.getString(R.string.drivers_release_status_failed, latest, check.failed.joinToString(), ago(check.checkedAt))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ago(t: Long): String {
|
||||
val m = ((System.currentTimeMillis() - t) / 60_000).coerceAtLeast(0)
|
||||
return when {
|
||||
m < 1 -> "just now"
|
||||
m < 60 -> "$m min ago"
|
||||
m < 48 * 60 -> "${m / 60} h ago"
|
||||
else -> "${m / (24 * 60)} days ago"
|
||||
m < 1 -> activity.getString(R.string.upd_just_now)
|
||||
m < 60 -> activity.resources.getQuantityString(R.plurals.drivers_ago_minutes, m.toInt(), m)
|
||||
m < 48 * 60 -> activity.resources.getQuantityString(R.plurals.drivers_ago_hours, (m / 60).toInt(), m / 60)
|
||||
else -> activity.resources.getQuantityString(R.plurals.drivers_ago_days, (m / (24 * 60)).toInt(), m / (24 * 60))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -405,16 +417,16 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
fun checkLatestTurnip() {
|
||||
if (releaseChecking) return
|
||||
releaseChecking = true
|
||||
releaseStatus = "Checking Banners-Turnip, WinNative and DroidDeck…"
|
||||
releaseStatus = activity.getString(R.string.drivers_checking_sources)
|
||||
Thread({
|
||||
val problem = try { TurnipReleases.refresh(activity); null } catch (e: Exception) {
|
||||
Log.w(TAG, "latest Turnip check", e); e.message ?: "check failed"
|
||||
Log.w(TAG, "latest Turnip check", e); e.message ?: activity.getString(R.string.drivers_check_failed)
|
||||
}
|
||||
ui.post {
|
||||
releaseChecking = false
|
||||
refreshReleaseRows()
|
||||
refreshPairs()
|
||||
if (problem != null) releaseStatus = "Couldn't check: $problem"
|
||||
if (problem != null) releaseStatus = activity.getString(R.string.upd_couldnt_check, problem)
|
||||
}
|
||||
}, "turnip-release-check").start()
|
||||
}
|
||||
@@ -433,13 +445,16 @@ internal class DriverMenus(private val activity: Activity, private val ui: Handl
|
||||
e.message
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "release driver download", e)
|
||||
"Download failed: ${e.message}"
|
||||
activity.getString(R.string.comp_download_failed, e.message)
|
||||
}
|
||||
ui.post {
|
||||
releaseProgress.remove(assetName)
|
||||
val pick = if (asset.bundle) "pick it in either list to set both drivers" else "pick it in the menu"
|
||||
val installed = activity.getString(
|
||||
if (asset.bundle) R.string.drivers_installed_pick_either else R.string.drivers_installed_pick,
|
||||
asset.name.removeSuffix(".zip"),
|
||||
)
|
||||
android.widget.Toast.makeText(
|
||||
activity, problem ?: "Installed ${asset.name.removeSuffix(".zip")} - $pick",
|
||||
activity, problem ?: installed,
|
||||
android.widget.Toast.LENGTH_LONG,
|
||||
).show()
|
||||
refreshDrivers()
|
||||
|
||||
@@ -109,7 +109,7 @@ class MainActivity : ComponentActivity() {
|
||||
private var stage by mutableStateOf("")
|
||||
private var percent by mutableIntStateOf(-1)
|
||||
private var failed by mutableStateOf(false)
|
||||
private var frameGenLabel by mutableStateOf("Off")
|
||||
private var frameGenLabel by mutableStateOf("")
|
||||
private var lossless by mutableStateOf(Lossless.State.NONE)
|
||||
private var showRemove by mutableStateOf(false)
|
||||
private var showNonAdreno by mutableStateOf<LinuxRuntimeInstaller.Release?>(null)
|
||||
@@ -256,29 +256,32 @@ class MainActivity : ComponentActivity() {
|
||||
/** Import a save zip into [game]: pick it in the app's file picker, then back up and unzip off the main thread. */
|
||||
private fun importSaves(name: String, game: () -> GameSaves.Game) {
|
||||
if (SessionState.running) {
|
||||
android.widget.Toast.makeText(this, "Close the Steam session first, so the game can't save over the import", android.widget.Toast.LENGTH_LONG).show()
|
||||
android.widget.Toast.makeText(this, R.string.main_saves_close_session, android.widget.Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
onSavePicked = { zip ->
|
||||
saveAction("Importing into $name") {
|
||||
saveAction(getString(R.string.main_saves_importing, name)) {
|
||||
val (written, backup) = GameSaves.import(game(), zip)
|
||||
val kind = GameSaves.layoutOf(zip)?.label ?: "zip"
|
||||
"Imported $written files from the $kind into $name" + (backup?.let { ". Old saves backed up to Download/DroidDeck/Saves/backups" } ?: "")
|
||||
resources.getQuantityString(
|
||||
if (backup != null) R.plurals.main_saves_imported_backup else R.plurals.main_saves_imported,
|
||||
written, written, kind, name,
|
||||
)
|
||||
}
|
||||
}
|
||||
pickSaveZip.launch(InAppFilePicker.buildIntent(this, listOf("zip"), "Choose a save zip for $name", GameSaves.savesDir().parentFile?.parentFile?.path))
|
||||
pickSaveZip.launch(InAppFilePicker.buildIntent(this, listOf("zip"), getString(R.string.main_saves_pick_zip, name), GameSaves.savesDir().parentFile?.parentFile?.path))
|
||||
}
|
||||
|
||||
/** Export [game]'s saves in [layout] to a folder picked in the app's file picker. */
|
||||
private fun exportSaves(name: String, layout: GameSaves.Layout, game: () -> GameSaves.Game) {
|
||||
onSavePicked = { dir ->
|
||||
saveAction("Exporting $name") {
|
||||
saveAction(getString(R.string.main_saves_exporting, name)) {
|
||||
val (zip, count) = GameSaves.export(game(), layout, dir)
|
||||
"Exported $count files as a ${layout.label}: ${zip.path.removePrefix("/storage/emulated/0/")}"
|
||||
resources.getQuantityString(R.plurals.main_saves_exported, count, count, layout.label, zip.path.removePrefix("/storage/emulated/0/"))
|
||||
}
|
||||
}
|
||||
GameSaves.savesDir().mkdirs()
|
||||
pickSaveDir.launch(InAppFilePicker.buildDirIntent(this, "Choose where to save $name (${layout.label})", GameSaves.savesDir().path))
|
||||
pickSaveDir.launch(InAppFilePicker.buildDirIntent(this, getString(R.string.main_saves_pick_folder, name, layout.label), GameSaves.savesDir().path))
|
||||
}
|
||||
private val pickAddedGamesDir = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { r ->
|
||||
if (r.resultCode == RESULT_OK) InAppFilePicker.pickedPath(r.data)?.let { path ->
|
||||
@@ -361,15 +364,15 @@ class MainActivity : ComponentActivity() {
|
||||
if (intent?.component?.className == SessionActivity::class.java.name) {
|
||||
when {
|
||||
busy || LinuxRuntimeInstaller.isBusy() -> {
|
||||
android.widget.Toast.makeText(this, "Wait for the runtime operation to finish", android.widget.Toast.LENGTH_SHORT).show()
|
||||
android.widget.Toast.makeText(this, R.string.main_wait_runtime, android.widget.Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
protons.protonBusyId != null || ProtonExtras.installInProgress -> {
|
||||
android.widget.Toast.makeText(this, "Wait for the compatibility tool install to finish", android.widget.Toast.LENGTH_SHORT).show()
|
||||
android.widget.Toast.makeText(this, R.string.main_wait_proton_install, android.widget.Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
pkgStage != null -> {
|
||||
android.widget.Toast.makeText(this, "Wait for the desktop app install to finish", android.widget.Toast.LENGTH_SHORT).show()
|
||||
android.widget.Toast.makeText(this, R.string.main_wait_desktop_install, android.widget.Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -440,7 +443,7 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
private fun launchGame(game: Library.SteamGame): Boolean {
|
||||
if (protons.protonBusyId != null || ProtonExtras.installInProgress || pkgStage != null) {
|
||||
android.widget.Toast.makeText(this, "Wait for the install to finish before launching a game", android.widget.Toast.LENGTH_SHORT).show()
|
||||
android.widget.Toast.makeText(this, R.string.main_wait_install_game, android.widget.Toast.LENGTH_SHORT).show()
|
||||
return false
|
||||
}
|
||||
if (SessionState.running) {
|
||||
@@ -487,6 +490,7 @@ class MainActivity : ComponentActivity() {
|
||||
displayManager = getSystemService(DISPLAY_SERVICE) as DisplayManager
|
||||
refreshPhantomStatus()
|
||||
theme = SessionPrefs.theme(this)
|
||||
frameGenLabel = getString(R.string.frame_gen_off)
|
||||
// Last start's game list, so the Steam wall is up on the first frame; refresh() replaces it.
|
||||
steamGames = if (shortcutPicker) emptyList() else com.droiddeck.launcher.frontend.LibraryCache.load(this)
|
||||
backActionsInverted = SessionPrefs.backActionsInverted(this)
|
||||
@@ -632,7 +636,7 @@ class MainActivity : ComponentActivity() {
|
||||
Thread({
|
||||
val zip = runCatching { SessionLogShare.zipLatest(this) }.getOrNull()
|
||||
ui.post {
|
||||
if (zip == null) android.widget.Toast.makeText(this, "No session logs yet: run a session first.", android.widget.Toast.LENGTH_LONG).show()
|
||||
if (zip == null) android.widget.Toast.makeText(this, R.string.main_no_logs_yet, android.widget.Toast.LENGTH_LONG).show()
|
||||
else startActivity(SessionLogShare.shareIntent(this, zip))
|
||||
}
|
||||
}, "share-logs").start()
|
||||
@@ -642,7 +646,7 @@ class MainActivity : ComponentActivity() {
|
||||
val cleared = runCatching { SessionArtifacts.clearAll(this) }.getOrDefault(0)
|
||||
ui.post {
|
||||
android.widget.Toast.makeText(this,
|
||||
if (cleared == 0) "No session logs to clear." else "Cleared $cleared session log folder${if (cleared == 1) "" else "s"}.",
|
||||
if (cleared == 0) getString(R.string.main_no_logs_to_clear) else resources.getQuantityString(R.plurals.main_logs_cleared, cleared, cleared),
|
||||
android.widget.Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}, "clear-logs").start()
|
||||
@@ -688,7 +692,7 @@ class MainActivity : ComponentActivity() {
|
||||
val result = runCatching {
|
||||
kotlinx.coroutines.runBlocking { WirelessAdbFix.pair(this@MainActivity, host, port, code) }
|
||||
}
|
||||
val error = result.exceptionOrNull()?.let { it.localizedMessage ?: "Wireless debugging pairing failed" }
|
||||
val error = result.exceptionOrNull()?.let { it.localizedMessage ?: getString(R.string.main_wadb_pair_failed) }
|
||||
ui.post { complete(error) }
|
||||
}, "wireless-adb-pair").start()
|
||||
},
|
||||
@@ -701,7 +705,7 @@ class MainActivity : ComponentActivity() {
|
||||
onWirelessAdbApply = { host, port, enabled, complete ->
|
||||
Thread({
|
||||
val result = runCatching { WirelessAdbFix.setChildProcessLimit(this@MainActivity, host, port, enabled) }
|
||||
val error = result.exceptionOrNull()?.let { it.localizedMessage ?: "Wireless debugging command failed" }
|
||||
val error = result.exceptionOrNull()?.let { it.localizedMessage ?: getString(R.string.main_wadb_command_failed) }
|
||||
ui.post {
|
||||
if (error == null) refreshPhantomStatus()
|
||||
complete(error)
|
||||
@@ -711,7 +715,7 @@ class MainActivity : ComponentActivity() {
|
||||
onSetPhantomProcessLimit = { enabled, complete ->
|
||||
Thread({
|
||||
val result = runCatching { WirelessAdbFix.setUsingSavedPairing(this@MainActivity, enabled) }
|
||||
val error = result.exceptionOrNull()?.let { it.localizedMessage ?: "Wireless debugging is unavailable" }
|
||||
val error = result.exceptionOrNull()?.let { it.localizedMessage ?: getString(R.string.main_wadb_unavailable) }
|
||||
ui.post {
|
||||
if (error == null) refreshPhantomStatus()
|
||||
complete(error)
|
||||
@@ -722,7 +726,7 @@ class MainActivity : ComponentActivity() {
|
||||
(getSystemService(CLIPBOARD_SERVICE) as ClipboardManager).setPrimaryClip(
|
||||
ClipData.newPlainText("DroidDeck child-process setting", PhantomProcessLimit.adbCommand(enabled)),
|
||||
)
|
||||
android.widget.Toast.makeText(this, "ADB command copied", android.widget.Toast.LENGTH_SHORT).show()
|
||||
android.widget.Toast.makeText(this, R.string.main_adb_copied, android.widget.Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
onDismissPhantomGate = { showPhantomGate = false },
|
||||
onStartWirelessAdbPairing = { WirelessAdbPairingService.start(this) },
|
||||
@@ -756,24 +760,24 @@ class MainActivity : ComponentActivity() {
|
||||
path = romsDir,
|
||||
onChoose = {
|
||||
showRoms = false
|
||||
pickRomsDir.launch(InAppFilePicker.buildDirIntent(this, "Choose the ROMs folder", romsDir))
|
||||
pickRomsDir.launch(InAppFilePicker.buildDirIntent(this, getString(R.string.main_pick_roms), romsDir))
|
||||
},
|
||||
onClear = { SessionPrefs.setRomsDir(this, ""); romsDir = null; showRoms = false },
|
||||
onDismiss = { showRoms = false },
|
||||
)
|
||||
showNonAdreno?.let { release ->
|
||||
ConfirmDialog(
|
||||
title = "Not an Adreno GPU",
|
||||
text = "Turnip supports Adreno GPUs. On ${com.droiddeck.launcher.core.DeviceSupport.gpuName()}, Steam may show a black screen. Download: ${"%.0f".format(release.size / 1e6)} MB.",
|
||||
confirm = "Install anyway",
|
||||
title = androidx.compose.ui.res.stringResource(R.string.main_non_adreno_title),
|
||||
text = androidx.compose.ui.res.stringResource(R.string.main_non_adreno_text, com.droiddeck.launcher.core.DeviceSupport.gpuName(), release.size / 1e6),
|
||||
confirm = androidx.compose.ui.res.stringResource(R.string.main_install_anyway),
|
||||
onConfirm = { showNonAdreno = null; install(release) },
|
||||
onDismiss = { showNonAdreno = null },
|
||||
)
|
||||
}
|
||||
if (showRemove) ConfirmDialog(
|
||||
title = "Remove Linux runtime",
|
||||
text = "This deletes the runtime, the Steam client inside it, and every game installed there.",
|
||||
confirm = "Remove",
|
||||
title = androidx.compose.ui.res.stringResource(R.string.main_remove_runtime_title),
|
||||
text = androidx.compose.ui.res.stringResource(R.string.main_remove_runtime_text),
|
||||
confirm = androidx.compose.ui.res.stringResource(R.string.store_remove),
|
||||
onConfirm = { showRemove = false; removeRuntime() },
|
||||
onDismiss = { showRemove = false },
|
||||
)
|
||||
@@ -883,7 +887,7 @@ class MainActivity : ComponentActivity() {
|
||||
if (!SessionState.running) Thread({
|
||||
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()
|
||||
android.widget.Toast.makeText(this, getString(R.string.main_applied, applied.joinToString(", ")), android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
ui.post { if (showComponents) components.refreshComponents() }
|
||||
}, "components-queue").start()
|
||||
@@ -942,9 +946,13 @@ class MainActivity : ComponentActivity() {
|
||||
try {
|
||||
HomeApp.launch(this, app, displayId)
|
||||
} catch (_: Exception) {
|
||||
val target = if (displayId == null || displayId == Display.DEFAULT_DISPLAY) "the primary screen"
|
||||
else secondScreenDisplays.firstOrNull { it.id == displayId }?.label ?: "display $displayId"
|
||||
android.widget.Toast.makeText(this, "Could not open ${app.label} on $target", android.widget.Toast.LENGTH_SHORT).show()
|
||||
val label = secondScreenDisplays.firstOrNull { it.id == displayId }?.label
|
||||
val message = when {
|
||||
displayId == null || displayId == Display.DEFAULT_DISPLAY -> getString(R.string.android_app_open_failed_primary, app.label)
|
||||
label != null -> getString(R.string.android_app_open_failed_on, app.label, label)
|
||||
else -> getString(R.string.android_app_open_failed_display, app.label, displayId)
|
||||
}
|
||||
android.widget.Toast.makeText(this, message, android.widget.Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -975,7 +983,7 @@ class MainActivity : ComponentActivity() {
|
||||
if (busy || LinuxRuntimeInstaller.isBusy() || saveBusy != null) return
|
||||
saveBusy = label
|
||||
Thread({
|
||||
val message = runCatching(work).getOrElse { e -> "$label failed: ${e.message ?: e.javaClass.simpleName}" }
|
||||
val message = runCatching(work).getOrElse { e -> getString(R.string.main_action_failed, label, e.message ?: e.javaClass.simpleName) }
|
||||
ui.post {
|
||||
saveBusy = null
|
||||
android.widget.Toast.makeText(this, message, android.widget.Toast.LENGTH_LONG).show()
|
||||
@@ -1021,10 +1029,10 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
private fun importComponent(uri: Uri) {
|
||||
val name = displayNameOf(uri) ?: "imported.wcp"
|
||||
components.componentAction("Importing") {
|
||||
components.componentAction(getString(R.string.comp_busy_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() }
|
||||
contentResolver.openInputStream(uri)?.use { input -> tmp.outputStream().use { input.copyTo(it) } } ?: error(getString(R.string.comp_cannot_read))
|
||||
try { getString(R.string.comp_imported, ComponentsManager.importPackage(this, tmp, name).version) } finally { tmp.delete() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1042,14 +1050,14 @@ class MainActivity : ComponentActivity() {
|
||||
requestInitialFocus = focusComponentsContent,
|
||||
onProton = { components.chooseProton(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) } } },
|
||||
onSwap = { file -> components.compProton?.let { pid -> components.componentAction(getString(R.string.comp_busy_swapping)) { ComponentsManager.swap(this, pid, file) } } },
|
||||
onRestore = { version -> components.compProton?.let { pid -> components.componentAction(getString(R.string.comp_busy_restoring)) { ComponentsManager.restore(this, pid, components.compComp, version) } } },
|
||||
onCancelQueued = { components.compProton?.let { pid -> components.componentAction(getString(R.string.comp_busy_cancelling)) { ComponentsManager.cancelQueued(this, pid, components.compComp); getString(R.string.comp_swap_cancelled) } } },
|
||||
onDeletePackage = { file -> components.componentAction(getString(R.string.comp_busy_deleting)) { ComponentsManager.deletePackage(this, file) } },
|
||||
onDeleteOriginal = { version -> components.compProton?.let { pid -> components.componentAction(getString(R.string.comp_busy_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)")) },
|
||||
onImport = { pickComponent.launch(InAppFilePicker.buildIntent(this, WCP_EXT, getString(R.string.comp_pick_wcp))) },
|
||||
onBack = { showComponents = false },
|
||||
gpu = drivers.state(),
|
||||
gpuActions = com.droiddeck.launcher.ui.GpuDriversActions(
|
||||
@@ -1062,9 +1070,9 @@ class MainActivity : ComponentActivity() {
|
||||
onRemoveLinux = { id -> drivers.deleteDriver(id, linux = true) },
|
||||
onRemoveAndroid = { id -> drivers.deleteDriver(id, linux = false) },
|
||||
onDownloadDriver = { name -> drivers.downloadReleaseDriver(name) },
|
||||
onImportLinux = { pickLinuxDriver.launch(InAppFilePicker.buildIntent(this, ZIP_EXT, "Choose a Linux runtime driver (-Linux zip or Android + Linux bundle)")) },
|
||||
onImportAndroid = { pickAndroidDriver.launch(InAppFilePicker.buildIntent(this, ZIP_EXT, "Choose a display driver (AdrenoTools zip or Android + Linux bundle)")) },
|
||||
onImportZip = { pickAnyDriver.launch(InAppFilePicker.buildIntent(this, ZIP_EXT, "Choose a driver zip or an Android + Linux bundle")) },
|
||||
onImportLinux = { pickLinuxDriver.launch(InAppFilePicker.buildIntent(this, ZIP_EXT, getString(R.string.drivers_pick_linux))) },
|
||||
onImportAndroid = { pickAndroidDriver.launch(InAppFilePicker.buildIntent(this, ZIP_EXT, getString(R.string.drivers_pick_android))) },
|
||||
onImportZip = { pickAnyDriver.launch(InAppFilePicker.buildIntent(this, ZIP_EXT, getString(R.string.drivers_pick_any))) },
|
||||
onRestoreBundled = { TurnipDriver(this).restoreBundled(); drivers.refreshDrivers() },
|
||||
),
|
||||
)
|
||||
@@ -1100,14 +1108,14 @@ class MainActivity : ComponentActivity() {
|
||||
private fun installPackage(id: String) {
|
||||
val entry = catalog?.firstOrNull { it.id == id } ?: return
|
||||
if (busy || LinuxRuntimeInstaller.isBusy() || pkgStage != null || SessionState.running) return
|
||||
pkgId = id; pkgStage = "Starting…"; pkgPercent = -1
|
||||
pkgId = id; pkgStage = getString(R.string.store_starting); pkgPercent = -1
|
||||
Thread({
|
||||
val problem = DesktopCatalog.install(this, entry) { stage, percent ->
|
||||
ui.post { pkgStage = stage; pkgPercent = percent }
|
||||
}
|
||||
ui.post {
|
||||
pkgStage = null; pkgId = null
|
||||
if (problem != null) android.widget.Toast.makeText(this, "${entry.name}: $problem", android.widget.Toast.LENGTH_LONG).show()
|
||||
if (problem != null) android.widget.Toast.makeText(this, getString(R.string.main_pkg_problem, entry.name, problem), android.widget.Toast.LENGTH_LONG).show()
|
||||
refreshPackages()
|
||||
refresh()
|
||||
}
|
||||
@@ -1117,7 +1125,7 @@ class MainActivity : ComponentActivity() {
|
||||
private fun removePackage(id: String) {
|
||||
val entry = catalog?.firstOrNull { it.id == id } ?: return
|
||||
if (busy || LinuxRuntimeInstaller.isBusy() || pkgStage != null || SessionState.running) return
|
||||
pkgId = id; pkgStage = if (entry.kind == "appimage") "Removing ${entry.name}…" else "Forgetting ${entry.name}…"; pkgPercent = -1
|
||||
pkgId = id; pkgStage = getString(if (entry.kind == "appimage") R.string.main_removing_named else R.string.main_forgetting_named, entry.name); pkgPercent = -1
|
||||
Thread({
|
||||
DesktopCatalog.remove(this, entry)
|
||||
ui.post {
|
||||
@@ -1220,7 +1228,7 @@ class MainActivity : ComponentActivity() {
|
||||
onRenderer = { r -> SessionPrefs.setDesktopRenderer(this, r); renderer = r },
|
||||
onGameStorage = { path, label -> setGameStorage(path, label) },
|
||||
onPickGameStorageFolder = {
|
||||
pickGameStorage.launch(InAppFilePicker.buildDirIntent(this, "Choose the game storage folder", gameStorage.ifEmpty { null }))
|
||||
pickGameStorage.launch(InAppFilePicker.buildDirIntent(this, getString(R.string.main_pick_game_storage), gameStorage.ifEmpty { null }))
|
||||
},
|
||||
onStorageDiagnostics = { on ->
|
||||
SessionPrefs.setStorageDiagnosticsEnabled(this, on)
|
||||
@@ -1246,13 +1254,13 @@ class MainActivity : ComponentActivity() {
|
||||
SessionPrefs.setRunSteamAtStartup(this, on)
|
||||
runSteamAtStartup = on
|
||||
},
|
||||
onPickAddedGamesDir = { pickAddedGamesDir.launch(InAppFilePicker.buildDirIntent(this, "Choose a folder of your own games", addedGamesDirs.lastOrNull())) },
|
||||
onPickAddedGamesDir = { pickAddedGamesDir.launch(InAppFilePicker.buildDirIntent(this, getString(R.string.main_pick_added_games), addedGamesDirs.lastOrNull())) },
|
||||
onAddedGamesArt = { on -> SessionPrefs.setAddedGamesArt(this, on); addedGamesArt = on; if (on) refreshAddedGames() },
|
||||
onForgetAddedGamesDir = { dir -> SessionPrefs.setAddedGamesDirs(this, addedGamesDirs - dir); addedGamesDirs = SessionPrefs.addedGamesDirs(this); refreshAddedGames(); refresh() },
|
||||
onAddedGameExe = { folder, path -> SessionPrefs.setAddedGameExe(this, folder, path); refreshAddedGames(); refresh() },
|
||||
onPickAddedGameExe = { folder ->
|
||||
pendingAddedGame = folder
|
||||
pickAddedGameExe.launch(InAppFilePicker.buildIntent(this, listOf("exe"), "Choose the game's .exe", folder))
|
||||
pickAddedGameExe.launch(InAppFilePicker.buildIntent(this, listOf("exe"), getString(R.string.main_pick_game_exe), folder))
|
||||
},
|
||||
onDeckyInstall = { release -> decky.installDecky(release) },
|
||||
onDeckyCheck = { decky.refreshDecky() },
|
||||
@@ -1263,7 +1271,7 @@ class MainActivity : ComponentActivity() {
|
||||
decky.deckySupervisor = false
|
||||
},
|
||||
onPickDeckyPluginZip = {
|
||||
pickDeckyPluginZip.launch(InAppFilePicker.buildIntent(this, ZIP_EXT, "Choose a Decky plugin ZIP"))
|
||||
pickDeckyPluginZip.launch(InAppFilePicker.buildIntent(this, ZIP_EXT, getString(R.string.main_pick_decky_zip)))
|
||||
},
|
||||
onDismiss = { settingsMode = null },
|
||||
),
|
||||
@@ -1405,7 +1413,7 @@ class MainActivity : ComponentActivity() {
|
||||
if (path.isNotEmpty() && path != SessionPrefs.GAME_STORAGE_OFF) {
|
||||
val problem = GameStorage.prepare(path)
|
||||
if (problem != null) {
|
||||
android.widget.Toast.makeText(this, "Not usable: $problem", android.widget.Toast.LENGTH_LONG).show()
|
||||
android.widget.Toast.makeText(this, getString(R.string.main_storage_not_usable, problem), android.widget.Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1447,9 +1455,9 @@ class MainActivity : ComponentActivity() {
|
||||
romsDir = SessionPrefs.romsDir(this).takeIf { it.isNotEmpty() }
|
||||
logsEnabled = SessionPrefs.logsEnabled(this)
|
||||
runningLabel = if (SessionState.running) when (SessionState.mode) {
|
||||
SessionService.MODE_DESKTOP -> "Desktop"
|
||||
SessionService.MODE_DESKTOP -> getString(R.string.rail_desktop)
|
||||
SessionService.MODE_RUN -> Library.nameForProgram(SessionState.program)
|
||||
?: SessionState.program?.substringAfterLast('/')?.substringBefore('.') ?: "Program"
|
||||
?: SessionState.program?.substringAfterLast('/')?.substringBefore('.') ?: getString(R.string.main_running_program)
|
||||
else -> "Steam"
|
||||
} else null
|
||||
// The libraries, off the main thread: manifests and a folder scan.
|
||||
@@ -1532,10 +1540,10 @@ class MainActivity : ComponentActivity() {
|
||||
} catch (error: Exception) {
|
||||
if (displayId != null) {
|
||||
runCatching { startActivity(intent) }
|
||||
android.widget.Toast.makeText(this, "Could not open Settings on the bottom screen; opened it on the main screen.", android.widget.Toast.LENGTH_LONG).show()
|
||||
android.widget.Toast.makeText(this, R.string.main_settings_main_screen, android.widget.Toast.LENGTH_LONG).show()
|
||||
} else {
|
||||
startActivity(Intent(android.provider.Settings.ACTION_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
android.widget.Toast.makeText(this, "Open Developer options in Settings", android.widget.Toast.LENGTH_LONG).show()
|
||||
android.widget.Toast.makeText(this, R.string.main_open_dev_options, android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1568,7 +1576,7 @@ class MainActivity : ComponentActivity() {
|
||||
private fun followRuntimeOperation(run: (LinuxRuntimeInstaller.ProgressListener) -> Boolean?) {
|
||||
busy = true
|
||||
failed = false
|
||||
stage = if (LinuxRuntimeInstaller.isRemoving()) "Removing Linux runtime" else "Starting…"
|
||||
stage = getString(if (LinuxRuntimeInstaller.isRemoving()) R.string.main_removing_runtime else R.string.store_starting)
|
||||
percent = -1
|
||||
Thread({
|
||||
val ok = run(LinuxRuntimeInstaller.ProgressListener { s, p ->
|
||||
|
||||
@@ -24,7 +24,7 @@ internal class ProtonMenu(private val activity: android.app.Activity, private va
|
||||
if (protonBusyId != null || SessionState.running) return
|
||||
ProtonExtras.unqueue(activity, tool)
|
||||
protonBusyId = id
|
||||
protonStage = "Starting…"
|
||||
protonStage = activity.getString(R.string.store_starting)
|
||||
protonPercent = -1
|
||||
Thread({
|
||||
val problem = ProtonExtras.install(activity, tool) { label, value ->
|
||||
@@ -44,7 +44,7 @@ internal class ProtonMenu(private val activity: android.app.Activity, private va
|
||||
val tool = ProtonExtras.tools.firstOrNull { it.id == id } ?: return
|
||||
if (protonBusyId != null || SessionState.running) return
|
||||
protonBusyId = id
|
||||
protonStage = "Removing ${tool.name}…"
|
||||
protonStage = activity.getString(R.string.main_removing_named, tool.name)
|
||||
protonPercent = -1
|
||||
Thread({
|
||||
ProtonExtras.remove(activity, tool)
|
||||
|
||||
@@ -287,7 +287,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.isRemoving()) {
|
||||
runtimeRemovalBlocked = true
|
||||
android.widget.Toast.makeText(this, "Wait for runtime removal to finish", android.widget.Toast.LENGTH_SHORT).show()
|
||||
// Best effort: the activity finishes either way, even where its resources cannot be read.
|
||||
runCatching { android.widget.Toast.makeText(this, R.string.session_wait_removal, android.widget.Toast.LENGTH_SHORT).show() }
|
||||
finish()
|
||||
return
|
||||
}
|
||||
@@ -471,7 +472,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
SessionDrawer(drawerOpen, drawerPage, drawerControllerActive, onPageChange = { drawerPage = it }, a = DrawerActions(
|
||||
steam = SessionState.mode == SessionService.MODE_STEAM,
|
||||
title = if (SessionState.mode == SessionService.MODE_RUN)
|
||||
com.droiddeck.launcher.frontend.Library.nameForProgram(SessionState.program) ?: "Game" else null,
|
||||
com.droiddeck.launcher.frontend.Library.nameForProgram(SessionState.program) ?: getString(R.string.session_title_game) else null,
|
||||
isHomeApp = isHomeApp,
|
||||
androidApps = androidApps,
|
||||
hudOn = hudOn,
|
||||
@@ -481,7 +482,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
frameGen = frameGen,
|
||||
lossless = lossless,
|
||||
oscMode = oscMode, suspendPolicy = suspendPolicy, touchMode = touchMode,
|
||||
touchAuto = if (usingTouchpad()) "touchpad" else "direct",
|
||||
touchAuto = getString(if (usingTouchpad()) R.string.session_touch_auto_touchpad else R.string.session_touch_auto_direct),
|
||||
shapeMode = shapeMode, fexPreset = fexPreset,
|
||||
secondScreenMode = secondScreenMode,
|
||||
secondScreenDisplays = secondScreenDisplays,
|
||||
@@ -543,9 +544,13 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
try {
|
||||
HomeApp.launch(this@SessionActivity, app, displayId)
|
||||
} catch (_: Exception) {
|
||||
val target = if (displayId == null || displayId == Display.DEFAULT_DISPLAY) "the primary screen"
|
||||
else secondScreenDisplays.firstOrNull { it.id == displayId }?.label ?: "display $displayId"
|
||||
Toast.makeText(this@SessionActivity, "Could not open ${app.label} on $target", Toast.LENGTH_SHORT).show()
|
||||
val label = secondScreenDisplays.firstOrNull { it.id == displayId }?.label
|
||||
val message = when {
|
||||
displayId == null || displayId == Display.DEFAULT_DISPLAY -> getString(R.string.android_app_open_failed_primary, app.label)
|
||||
label != null -> getString(R.string.android_app_open_failed_on, app.label, label)
|
||||
else -> getString(R.string.android_app_open_failed_display, app.label, displayId)
|
||||
}
|
||||
Toast.makeText(this@SessionActivity, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
},
|
||||
pipSupported = com.droiddeck.launcher.session.SessionPipController.supported(this@SessionActivity),
|
||||
@@ -663,7 +668,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
val snap = runCatching { ComponentsManager.snapshot(this) }.getOrNull()
|
||||
uiHandler.post {
|
||||
drawerComponents = snap
|
||||
if (applied.isNotEmpty()) android.widget.Toast.makeText(this, "Applied: " + applied.joinToString(", "), android.widget.Toast.LENGTH_LONG).show()
|
||||
if (applied.isNotEmpty()) android.widget.Toast.makeText(this, getString(R.string.main_applied, applied.joinToString(", ")), android.widget.Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}, "drawer-components").start()
|
||||
}
|
||||
@@ -678,10 +683,10 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
val current = active ?: "orig:$build"
|
||||
if (queued != null && value == current) {
|
||||
// Picking what is already in place again cancels the swap waiting for the game.
|
||||
ComponentsManager.cancelQueued(this, protonId, comp); "The waiting swap was cancelled."
|
||||
ComponentsManager.cancelQueued(this, protonId, comp); getString(R.string.comp_swap_cancelled)
|
||||
} else if (value.startsWith("orig:")) ComponentsManager.restore(this, protonId, comp, value.removePrefix("orig:"))
|
||||
else ComponentsManager.swap(this, protonId, value)
|
||||
}.getOrElse { e -> "Swap failed: ${e.message ?: e.javaClass.simpleName}" }
|
||||
}.getOrElse { e -> getString(R.string.session_swap_failed, e.message ?: e.javaClass.simpleName) }
|
||||
uiHandler.post {
|
||||
android.widget.Toast.makeText(this, message, android.widget.Toast.LENGTH_LONG).show()
|
||||
refreshDrawerComponents()
|
||||
@@ -695,20 +700,22 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
else intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM
|
||||
|
||||
private fun pausedTitle(): String = when (SessionState.mode) {
|
||||
SessionService.MODE_DESKTOP -> "The desktop is paused"
|
||||
SessionService.MODE_RUN -> (com.droiddeck.launcher.frontend.Library.nameForProgram(SessionState.program) ?: "The program") + " is paused"
|
||||
else -> "Steam is paused"
|
||||
SessionService.MODE_DESKTOP -> getString(R.string.session_desktop_paused)
|
||||
SessionService.MODE_RUN -> com.droiddeck.launcher.frontend.Library.nameForProgram(SessionState.program)
|
||||
?.let { getString(R.string.session_named_paused, it) } ?: getString(R.string.session_program_paused)
|
||||
else -> getString(R.string.load_steam_paused)
|
||||
}
|
||||
|
||||
private fun loadingTitle(): String = when (loadingMode()) {
|
||||
SessionService.MODE_DESKTOP ->
|
||||
if (!SessionState.running && intent.getStringExtra(SessionService.EXTRA_STEAM_UI) != null) "Starting Steam on the desktop"
|
||||
else "Starting the desktop"
|
||||
if (!SessionState.running && intent.getStringExtra(SessionService.EXTRA_STEAM_UI) != null) getString(R.string.session_starting_steam_desktop)
|
||||
else getString(R.string.session_starting_desktop)
|
||||
SessionService.MODE_RUN -> {
|
||||
val program = if (SessionState.running) SessionState.program else intent.getStringExtra(SessionService.EXTRA_PROGRAM)
|
||||
"Starting " + (com.droiddeck.launcher.frontend.Library.nameForProgram(program) ?: "the program")
|
||||
com.droiddeck.launcher.frontend.Library.nameForProgram(program)?.let { getString(R.string.session_starting_named, it) }
|
||||
?: getString(R.string.session_starting_program)
|
||||
}
|
||||
else -> "Starting Steam"
|
||||
else -> getString(R.string.load_starting_steam)
|
||||
}
|
||||
|
||||
private fun shareCurrentSessionLogs() {
|
||||
@@ -716,7 +723,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
// ended screen's Share logs is for that one.
|
||||
val folder = SessionPaths.current() ?: SessionPaths.lastEnded()
|
||||
if (folder == null || !folder.isDirectory) {
|
||||
Toast.makeText(this, "No logs for this session.", Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(this, R.string.session_no_logs, Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
Thread({
|
||||
@@ -725,12 +732,12 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
.getOrNull()
|
||||
uiHandler.post {
|
||||
if (zip == null) {
|
||||
Toast.makeText(this, "Could not create the session log archive.", Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(this, R.string.session_logs_archive_failed, Toast.LENGTH_LONG).show()
|
||||
} else {
|
||||
runCatching { startActivity(SessionLogShare.shareIntent(this, zip)) }
|
||||
.onFailure {
|
||||
Log.w(TAG, "could not share current session logs", it)
|
||||
Toast.makeText(this, "Could not share the session logs.", Toast.LENGTH_LONG).show()
|
||||
Toast.makeText(this, R.string.session_logs_share_failed, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -790,8 +797,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
"${SessionState.installing}.installing",
|
||||
mapOf("component" to SessionState.installing),
|
||||
)
|
||||
loading.step = if (runtime) "downloading the Linux runtime" else if (desktop) "downloading the desktop"
|
||||
else "downloading Proton Experimental (ARM64)"
|
||||
loading.step = getString((if (runtime) RUNTIME_LINES else if (desktop) DESKTOP_LINES else PROTON_LINES).downloading)
|
||||
loading.percent = -1
|
||||
Thread({
|
||||
var failedComponent: String? = null
|
||||
@@ -832,7 +838,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
return@post
|
||||
}
|
||||
loading.percent = -1
|
||||
loading.step = "Starting the session…"
|
||||
loading.step = getString(R.string.session_starting_session)
|
||||
// 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)
|
||||
}
|
||||
@@ -848,14 +854,17 @@ 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)
|
||||
|
||||
/** Reports one package's download on the loading screen: "<what> · 332 of 791 MB", checking, unpacking. */
|
||||
private fun progressFor(what: String, mb: Long) = com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.ProgressListener { stage, p ->
|
||||
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) "downloading $what · ${p * mb / 100} of $mb MB" else "downloading $what"
|
||||
stage.startsWith("Verifying") -> "checking $what"
|
||||
else -> "unpacking $what"
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -863,29 +872,29 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
/** Null when the runtime is in, else the loading screen's closing line. */
|
||||
private fun installRuntime(): String? {
|
||||
val release = com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.fetchRelease()
|
||||
?: return "Could not reach the runtime catalog. Check the connection and press Play again."
|
||||
?: return getString(R.string.session_runtime_catalog_unreachable)
|
||||
val ok = com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.install(this, release,
|
||||
progressFor("the Linux runtime", release.size / 1_000_000))
|
||||
return if (ok) null else "The Linux runtime did not install. Check the runtime in Setup and press Play again."
|
||||
progressFor(RUNTIME_LINES, release.size / 1_000_000))
|
||||
return if (ok) null else getString(R.string.session_runtime_install_failed)
|
||||
}
|
||||
|
||||
/** 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 = "downloading the desktop" }
|
||||
uiHandler.post { loading.percent = -1; loading.step = getString(R.string.session_desktop_downloading) }
|
||||
val entry = com.droiddeck.launcher.runtime.DesktopCatalog.fetch()?.firstOrNull { it.id == "desktop" }
|
||||
?: return "Could not reach the desktop catalog. Check the connection and press Desktop again."
|
||||
?: return getString(R.string.session_desktop_catalog_unreachable)
|
||||
val problem = com.droiddeck.launcher.runtime.DesktopCatalog.install(this, entry,
|
||||
progressFor("the desktop", entry.size / 1_000_000))
|
||||
return problem?.let { "The desktop did not install ($it). Check the connection and press Desktop again." }
|
||||
progressFor(DESKTOP_LINES, entry.size / 1_000_000))
|
||||
return problem?.let { getString(R.string.session_desktop_install_failed, it) }
|
||||
}
|
||||
|
||||
/** 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 = "downloading Proton Experimental (ARM64)" }
|
||||
uiHandler.post { loading.percent = -1; loading.step = getString(R.string.session_proton_downloading) }
|
||||
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"
|
||||
return catalog.install(this, entry, progressFor("Proton Experimental (ARM64)", entry.size / 1_000_000))
|
||||
return catalog.install(this, entry, progressFor(PROTON_LINES, entry.size / 1_000_000))
|
||||
}
|
||||
|
||||
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||
@@ -1168,14 +1177,15 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
return
|
||||
}
|
||||
// It stays until the user leaves: a failure read against a timer is a failure not read.
|
||||
val what = when (SessionState.mode) {
|
||||
SessionService.MODE_RUN -> com.droiddeck.launcher.frontend.Library.nameForProgram(SessionState.program) ?: "The program"
|
||||
SessionService.MODE_DESKTOP -> "The desktop"
|
||||
else -> "Steam"
|
||||
val stopped = when (SessionState.mode) {
|
||||
SessionService.MODE_RUN -> com.droiddeck.launcher.frontend.Library.nameForProgram(SessionState.program)
|
||||
?.let { getString(R.string.session_ended_named, it) } ?: getString(R.string.session_ended_program)
|
||||
SessionService.MODE_DESKTOP -> getString(R.string.session_ended_desktop)
|
||||
else -> getString(R.string.session_ended_named, "Steam")
|
||||
}
|
||||
loading.showEnded(
|
||||
hint?.text ?: "$what stopped unexpectedly. Share the logs with a bug report, or try again.",
|
||||
"Exit status $status · ${SessionState.logFile?.path ?: "no log"}",
|
||||
hint?.text ?: stopped,
|
||||
getString(R.string.session_ended_detail, status, SessionState.logFile?.path ?: getString(R.string.session_ended_no_log)),
|
||||
)
|
||||
focusEndedScreen()
|
||||
}
|
||||
@@ -1225,8 +1235,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
val what = com.droiddeck.launcher.frontend.Library.nameForProgram(SessionState.program) ?: getString(R.string.session_end_program)
|
||||
EndHint(getString(R.string.session_end_fex_missing, what), evenOnSuccess = true)
|
||||
} else if (enosys >= 8) {
|
||||
EndHint("The log shows $enosys \"Function not implemented\" errors: proot's seccomp acceleration is failing a helper on this device. " +
|
||||
"Try Performance \u2192 \"Run proot without seccomp\" (or \"Skip Steam's xalia helper\") and start again.")
|
||||
EndHint(resources.getQuantityString(R.plurals.session_hint_enosys, enosys, enosys,
|
||||
getString(R.string.perf_title), getString(R.string.perf_seccomp), getString(R.string.perf_xalia)))
|
||||
} else null
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
@@ -2026,5 +2036,17 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
/** Compositor scale modes (Container.FULLSCREEN_* values): 1 = fit with bars, centred. */
|
||||
private const val SCALE_FIT = 1
|
||||
private const val ALIGN_CENTER = 0
|
||||
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,
|
||||
)
|
||||
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,
|
||||
)
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl
|
||||
onAllowInstalls = {
|
||||
askPermission = false
|
||||
runCatching { activity.startActivity(SelfInstaller.permissionIntent(activity)) }
|
||||
.onFailure { error = "Couldn't open Android's \"Install unknown apps\" setting"; pending = null }
|
||||
.onFailure { error = activity.getString(R.string.upd_unknown_apps_failed); pending = null }
|
||||
},
|
||||
onDismissPermission = { askPermission = false; pending = null },
|
||||
)
|
||||
@@ -77,7 +77,7 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl
|
||||
catalog = it
|
||||
follow = AppUpdates.follow(activity, it)
|
||||
error = null
|
||||
}.onFailure { if (force) error = "Couldn't check: ${it.message ?: it.javaClass.simpleName}" }
|
||||
}.onFailure { if (force) error = activity.getString(R.string.upd_couldnt_check, it.message ?: it.javaClass.simpleName) }
|
||||
}
|
||||
}, "app-updates").start()
|
||||
}
|
||||
@@ -90,20 +90,20 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl
|
||||
|
||||
private fun install(r: AppUpdates.Release) {
|
||||
if (stage != null) return
|
||||
val apk = r.apk ?: run { error = "This build has no download for this copy of DroidDeck"; return }
|
||||
val apk = r.apk ?: run { error = activity.getString(R.string.upd_no_download); return }
|
||||
AppUpdates.installBlock(r)?.let { error = it; return }
|
||||
if (SessionState.running) { error = "Stop the running session first, then update"; return }
|
||||
if (SessionState.running) { error = activity.getString(R.string.upd_stop_first); return }
|
||||
if (!SelfInstaller.canInstall(activity)) { pending = r; askPermission = true; return }
|
||||
error = null
|
||||
stage = "Downloading"
|
||||
stage = activity.getString(R.string.upd_stage_downloading)
|
||||
percent = 0
|
||||
Thread({
|
||||
val result = runCatching {
|
||||
val file = SelfInstaller.download(activity, apk) { pct -> ui.post { percent = pct } }
|
||||
ui.post { stage = "Installing"; percent = -1 }
|
||||
ui.post { stage = activity.getString(R.string.upd_stage_installing); percent = -1 }
|
||||
SelfInstaller.install(activity, file)
|
||||
}
|
||||
result.onFailure { e -> ui.post { stage = null; error = e.message ?: "The update failed" } }
|
||||
result.onFailure { e -> ui.post { stage = null; error = e.message ?: activity.getString(R.string.upd_failed) } }
|
||||
}, "app-update-install").start()
|
||||
}
|
||||
|
||||
@@ -114,8 +114,8 @@ internal class UpdatesMenu(private val activity: Activity, private val ui: Handl
|
||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
val confirm = if (Build.VERSION.SDK_INT >= 33) intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java)
|
||||
else @Suppress("DEPRECATION") intent.getParcelableExtra(Intent.EXTRA_INTENT)
|
||||
if (confirm == null) { stage = null; error = "Android didn't offer to install the update"; return }
|
||||
stage = "Waiting for you to confirm"
|
||||
if (confirm == null) { stage = null; error = activity.getString(R.string.upd_not_offered); return }
|
||||
stage = activity.getString(R.string.upd_stage_confirm)
|
||||
activity.startActivity(confirm.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK))
|
||||
}
|
||||
PackageInstaller.STATUS_SUCCESS -> stage = null
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Launcher (MainActivity) -->
|
||||
<string name="main_wait_runtime">Wait for the runtime operation to finish</string>
|
||||
<string name="main_wait_proton_install">Wait for the compatibility tool install to finish</string>
|
||||
<string name="main_wait_desktop_install">Wait for the desktop app install to finish</string>
|
||||
<string name="main_wait_install_game">Wait for the install to finish before launching a game</string>
|
||||
<string name="main_saves_close_session">Close the Steam session first, so the game can\'t save over the import</string>
|
||||
<!-- Busy line while saves are imported; %1$s is the game's name -->
|
||||
<string name="main_saves_importing">Importing into %1$s</string>
|
||||
<!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the game's name -->
|
||||
<plurals name="main_saves_imported">
|
||||
<item quantity="one">Imported %1$d file from the %2$s into %3$s</item>
|
||||
<item quantity="other">Imported %1$d files from the %2$s into %3$s</item>
|
||||
</plurals>
|
||||
<plurals name="main_saves_imported_backup">
|
||||
<item quantity="one">Imported %1$d file from the %2$s into %3$s. Old saves backed up to Download/DroidDeck/Saves/backups</item>
|
||||
<item quantity="other">Imported %1$d files from the %2$s into %3$s. Old saves backed up to Download/DroidDeck/Saves/backups</item>
|
||||
</plurals>
|
||||
<string name="main_saves_pick_zip">Choose a save zip for %1$s</string>
|
||||
<!-- Busy line while saves are exported; %1$s is the game's name -->
|
||||
<string name="main_saves_exporting">Exporting %1$s</string>
|
||||
<!-- %1$d files, %2$s the save layout (e.g. GameHub zip), %3$s the zip's path -->
|
||||
<plurals name="main_saves_exported">
|
||||
<item quantity="one">Exported %1$d file as a %2$s: %3$s</item>
|
||||
<item quantity="other">Exported %1$d files as a %2$s: %3$s</item>
|
||||
</plurals>
|
||||
<!-- %1$s is the game's name, %2$s the save layout -->
|
||||
<string name="main_saves_pick_folder">Choose where to save %1$s (%2$s)</string>
|
||||
<!-- %1$s is what was being done (e.g. Importing into Portal 2, Swapping), %2$s the error -->
|
||||
<string name="main_action_failed">%1$s failed: %2$s</string>
|
||||
<string name="main_no_logs_yet">No session logs yet: run a session first.</string>
|
||||
<string name="main_no_logs_to_clear">No session logs to clear.</string>
|
||||
<plurals name="main_logs_cleared">
|
||||
<item quantity="one">Cleared %1$d session log folder.</item>
|
||||
<item quantity="other">Cleared %1$d session log folders.</item>
|
||||
</plurals>
|
||||
<string name="main_wadb_pair_failed">Wireless debugging pairing failed</string>
|
||||
<string name="main_wadb_command_failed">Wireless debugging command failed</string>
|
||||
<string name="main_wadb_unavailable">Wireless debugging is unavailable</string>
|
||||
<string name="main_adb_copied">ADB command copied</string>
|
||||
<string name="main_pick_roms">Choose the ROMs folder</string>
|
||||
<string name="main_non_adreno_title">Not an Adreno GPU</string>
|
||||
<!-- %1$s is the GPU's name, %2$.0f the download size in MB -->
|
||||
<string name="main_non_adreno_text">Turnip supports Adreno GPUs. On %1$s, Steam may show a black screen. Download: %2$.0f MB.</string>
|
||||
<string name="main_install_anyway">Install anyway</string>
|
||||
<string name="main_remove_runtime_title">Remove Linux runtime</string>
|
||||
<string name="main_remove_runtime_text">This deletes the runtime, the Steam client inside it, and every game installed there.</string>
|
||||
<!-- Toast; %1$s is a list of components, separated by commas -->
|
||||
<string name="main_applied">Applied: %1$s</string>
|
||||
<string name="android_app_open_failed_primary">Could not open %1$s on the primary screen</string>
|
||||
<!-- %1$s is the app, %2$s the name of the display -->
|
||||
<string name="android_app_open_failed_on">Could not open %1$s on %2$s</string>
|
||||
<!-- %1$s is the app, %2$d the display's number -->
|
||||
<string name="android_app_open_failed_display">Could not open %1$s on display %2$d</string>
|
||||
<!-- Components page busy lines, shown with … after them -->
|
||||
<string name="comp_busy_importing">Importing</string>
|
||||
<string name="comp_busy_swapping">Swapping</string>
|
||||
<string name="comp_busy_restoring">Restoring</string>
|
||||
<string name="comp_busy_cancelling">Cancelling</string>
|
||||
<string name="comp_busy_deleting">Deleting</string>
|
||||
<string name="comp_cannot_read">cannot read the file</string>
|
||||
<!-- %1$s is a version or a name -->
|
||||
<string name="comp_imported">Imported %1$s</string>
|
||||
<string name="comp_swap_cancelled">The waiting swap was cancelled.</string>
|
||||
<string name="comp_pick_wcp">Choose a component package (-linux .wcp)</string>
|
||||
<string name="comp_nightlies_unreachable">The Nightlies could not be reached</string>
|
||||
<!-- %1$s is the package version -->
|
||||
<string name="comp_stored">Stored %1$s</string>
|
||||
<string name="comp_download_failed">Download failed: %1$s</string>
|
||||
<string name="drivers_pick_linux">Choose a Linux runtime driver (-Linux zip or Android + Linux bundle)</string>
|
||||
<string name="drivers_pick_android">Choose a display driver (AdrenoTools zip or Android + Linux bundle)</string>
|
||||
<string name="drivers_pick_any">Choose a driver zip or an Android + Linux bundle</string>
|
||||
<!-- %1$s is a desktop app's name, %2$s what went wrong -->
|
||||
<string name="main_pkg_problem">%1$s: %2$s</string>
|
||||
<string name="main_removing_named">Removing %1$s…</string>
|
||||
<string name="main_forgetting_named">Forgetting %1$s…</string>
|
||||
<string name="main_pick_game_storage">Choose the game storage folder</string>
|
||||
<string name="main_pick_added_games">Choose a folder of your own games</string>
|
||||
<string name="main_pick_game_exe">Choose the game\'s .exe</string>
|
||||
<string name="main_pick_decky_zip">Choose a Decky plugin ZIP</string>
|
||||
<string name="main_storage_not_usable">Not usable: %1$s</string>
|
||||
<!-- What is running, when it is a program without a known name -->
|
||||
<string name="main_running_program">Program</string>
|
||||
<string name="main_removing_runtime">Removing Linux runtime</string>
|
||||
<string name="main_settings_main_screen">Could not open Settings on the bottom screen; opened it on the main screen.</string>
|
||||
<string name="main_open_dev_options">Open Developer options in Settings</string>
|
||||
|
||||
<!-- Updates -->
|
||||
<string name="upd_unknown_apps_failed">Couldn\'t open Android\'s \"Install unknown apps\" setting</string>
|
||||
<string name="upd_couldnt_check">Couldn\'t check: %1$s</string>
|
||||
<string name="upd_no_download">This build has no download for this copy of DroidDeck</string>
|
||||
<string name="upd_stop_first">Stop the running session first, then update</string>
|
||||
<!-- Update stages, shown with … after them -->
|
||||
<string name="upd_stage_downloading">Downloading</string>
|
||||
<string name="upd_stage_installing">Installing</string>
|
||||
<string name="upd_stage_confirm">Waiting for you to confirm</string>
|
||||
<string name="upd_failed">The update failed</string>
|
||||
<string name="upd_not_offered">Android didn\'t offer to install the update</string>
|
||||
|
||||
<!-- Decky -->
|
||||
<string name="decky_install_failed">Decky install failed: %1$s</string>
|
||||
<string name="decky_plugin_starting">Starting plugin import…</string>
|
||||
<string name="decky_plugin_failed">Plugin import failed: %1$s</string>
|
||||
<string name="decky_plugin_installed">Plugin installed. Restart Steam to load it.</string>
|
||||
|
||||
<!-- GPU drivers -->
|
||||
<string name="drivers_not_checked">Not checked yet - tap refresh to look for new drivers</string>
|
||||
<string name="drivers_bundle_sets_display">Android + Linux: sets the display driver with it</string>
|
||||
<string name="drivers_bundle_sets_runtime">Android + Linux: sets the runtime driver with it</string>
|
||||
<string name="drivers_runtime_builtin">the Turnip built into the runtime</string>
|
||||
<string name="drivers_system_vulkan">system Vulkan: no bundled build for this GPU</string>
|
||||
<string name="drivers_bundled">%1$s (bundled)</string>
|
||||
<string name="drivers_manual">Manual</string>
|
||||
<!-- Driver mode, then the driver pair in use -->
|
||||
<string name="drivers_summary">%1$s · %2$s</string>
|
||||
<string name="drivers_half_published">Only one half is published right now</string>
|
||||
<string name="drivers_pair_size">%1$.0f MB for both</string>
|
||||
<!-- %1$s says why (the GPU's support status) -->
|
||||
<string name="drivers_none_to_set">No drivers to set: %1$s</string>
|
||||
<string name="drivers_checking_latest">Checking for the latest drivers…</string>
|
||||
<string name="drivers_up_to_date_check_failed">Up to date (couldn\'t check: %1$s)</string>
|
||||
<string name="drivers_recommended_unpublished">The recommended drivers aren\'t published right now</string>
|
||||
<!-- %1$s is the driver pair's name, %2$s its version -->
|
||||
<string name="drivers_downloading_pair">Downloading %1$s %2$s…</string>
|
||||
<string name="drivers_updated_to">Updated to %1$s %2$s.</string>
|
||||
<string name="drivers_updated_to_restart">Updated to %1$s %2$s. The display driver applies after DroidDeck restarts.</string>
|
||||
<string name="drivers_switched_to">Switched to %1$s %2$s.</string>
|
||||
<string name="drivers_switched_to_restart">Switched to %1$s %2$s. The display driver applies after DroidDeck restarts.</string>
|
||||
<string name="drivers_using">Using %1$s %2$s.</string>
|
||||
<string name="drivers_using_restart">Using %1$s %2$s. The display driver applies after DroidDeck restarts.</string>
|
||||
<string name="drivers_not_a_driver">Not a driver zip: neither an AdrenoTools driver, a -Linux Turnip nor an Android + Linux bundle</string>
|
||||
<string name="drivers_import_failed">Import failed: %1$s</string>
|
||||
<string name="drivers_imported_unnamed">Imported driver</string>
|
||||
<string name="drivers_using_bundle">Using %1$s as both the runtime and display driver.</string>
|
||||
<string name="drivers_using_bundle_restart">Using %1$s as both the runtime and display driver. The display driver applies after DroidDeck restarts.</string>
|
||||
<string name="drivers_deleted_bundle">Deleted %1$s (both drivers)</string>
|
||||
<string name="drivers_deleted">Deleted %1$s</string>
|
||||
<!-- %1$s lists the latest releases, %2$s when they were checked (e.g. 5 min ago) -->
|
||||
<string name="drivers_release_status">Latest: %1$s · checked %2$s</string>
|
||||
<!-- %1$s lists the latest releases, %2$s the sources that could not be reached, %3$s when they were checked -->
|
||||
<string name="drivers_release_status_failed">Latest: %1$s · %2$s unreachable · checked %3$s</string>
|
||||
<plurals name="drivers_ago_minutes">
|
||||
<item quantity="one">%1$d min ago</item>
|
||||
<item quantity="other">%1$d min ago</item>
|
||||
</plurals>
|
||||
<plurals name="drivers_ago_hours">
|
||||
<item quantity="one">%1$d h ago</item>
|
||||
<item quantity="other">%1$d h ago</item>
|
||||
</plurals>
|
||||
<plurals name="drivers_ago_days">
|
||||
<item quantity="one">%1$d day ago</item>
|
||||
<item quantity="other">%1$d days ago</item>
|
||||
</plurals>
|
||||
<string name="drivers_checking_sources">Checking Banners-Turnip, WinNative and DroidDeck…</string>
|
||||
<string name="drivers_check_failed">check failed</string>
|
||||
<!-- %1$s is the driver's name -->
|
||||
<string name="drivers_installed_pick_either">Installed %1$s - pick it in either list to set both drivers</string>
|
||||
<string name="drivers_installed_pick">Installed %1$s - pick it in the menu</string>
|
||||
|
||||
<!-- Session (SessionActivity) -->
|
||||
<string name="session_wait_removal">Wait for runtime removal to finish</string>
|
||||
<!-- Drawer title when the running program has no known name -->
|
||||
<string name="session_title_game">Game</string>
|
||||
<!-- Shown as "Auto (touchpad)" in the drawer's touch setting -->
|
||||
<string name="session_touch_auto_touchpad">touchpad</string>
|
||||
<!-- Shown as "Auto (direct)" in the drawer's touch setting -->
|
||||
<string name="session_touch_auto_direct">direct</string>
|
||||
<string name="session_swap_failed">Swap failed: %1$s</string>
|
||||
<string name="session_desktop_paused">The desktop is paused</string>
|
||||
<!-- %1$s is the program's name -->
|
||||
<string name="session_named_paused">%1$s is paused</string>
|
||||
<string name="session_program_paused">The program is paused</string>
|
||||
<string name="session_starting_steam_desktop">Starting Steam on the desktop</string>
|
||||
<string name="session_starting_desktop">Starting the desktop</string>
|
||||
<!-- %1$s is the program's name -->
|
||||
<string name="session_starting_named">Starting %1$s</string>
|
||||
<string name="session_starting_program">Starting the program</string>
|
||||
<string name="session_no_logs">No logs for this session.</string>
|
||||
<string name="session_logs_archive_failed">Could not create the session log archive.</string>
|
||||
<string name="session_logs_share_failed">Could not share the session logs.</string>
|
||||
<string name="session_starting_session">Starting the session…</string>
|
||||
<!-- Loading screen lines while packages download; %1$d and %2$d are megabytes -->
|
||||
<string name="session_runtime_downloading">downloading the Linux runtime</string>
|
||||
<string name="session_runtime_downloading_mb">downloading the Linux runtime · %1$d of %2$d MB</string>
|
||||
<string name="session_runtime_checking">checking the Linux runtime</string>
|
||||
<string name="session_runtime_unpacking">unpacking the Linux runtime</string>
|
||||
<string name="session_desktop_downloading">downloading the desktop</string>
|
||||
<string name="session_desktop_downloading_mb">downloading the desktop · %1$d of %2$d MB</string>
|
||||
<string name="session_desktop_checking">checking the desktop</string>
|
||||
<string name="session_desktop_unpacking">unpacking the desktop</string>
|
||||
<string name="session_proton_downloading">downloading Proton Experimental (ARM64)</string>
|
||||
<string name="session_proton_downloading_mb">downloading Proton Experimental (ARM64) · %1$d of %2$d MB</string>
|
||||
<string name="session_proton_checking">checking Proton Experimental (ARM64)</string>
|
||||
<string name="session_proton_unpacking">unpacking Proton Experimental (ARM64)</string>
|
||||
<string name="session_runtime_catalog_unreachable">Could not reach the runtime catalog. Check the connection and press Play again.</string>
|
||||
<string name="session_runtime_install_failed">The Linux runtime did not install. Check the runtime in Setup and press Play again.</string>
|
||||
<string name="session_desktop_catalog_unreachable">Could not reach the desktop catalog. Check the connection and press Desktop again.</string>
|
||||
<!-- %1$s says what went wrong -->
|
||||
<string name="session_desktop_install_failed">The desktop did not install (%1$s). Check the connection and press Desktop again.</string>
|
||||
<!-- %1$s is the program's name, or Steam -->
|
||||
<string name="session_ended_named">%1$s stopped unexpectedly. Share the logs with a bug report, or try again.</string>
|
||||
<string name="session_ended_program">The program stopped unexpectedly. Share the logs with a bug report, or try again.</string>
|
||||
<string name="session_ended_desktop">The desktop stopped unexpectedly. Share the logs with a bug report, or try again.</string>
|
||||
<!-- %1$d is the exit status, %2$s the log file's path -->
|
||||
<string name="session_ended_detail">Exit status %1$d · %2$s</string>
|
||||
<string name="session_ended_no_log">no log</string>
|
||||
<!-- %1$d errors; %2$s, %3$s and %4$s are the Performance page and its two settings -->
|
||||
<plurals name="session_hint_enosys">
|
||||
<item quantity="one">The log shows %1$d \"Function not implemented\" error: proot\'s seccomp acceleration is failing a helper on this device. Try %2$s → \"%3$s\" (or \"%4$s\") and start again.</item>
|
||||
<item quantity="other">The log shows %1$d \"Function not implemented\" errors: proot\'s seccomp acceleration is failing a helper on this device. Try %2$s → \"%3$s\" (or \"%4$s\") and start again.</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
Reference in new issue
Block a user