From 94bae6ea529624ac139337e551a09d5e72e1b98f Mon Sep 17 00:00:00 2001 From: maxjivi05 Date: Sun, 4 Oct 2026 22:33:23 -0400 Subject: [PATCH] l10n: move session, GPU and display UI text into string resources (#212) --- .../droiddeck/launcher/gpu/DriverBundle.kt | 7 +- .../com/droiddeck/launcher/gpu/GpuInfo.kt | 16 +- .../gpu/LinuxVulkanDriverManager.java | 10 +- .../droiddeck/launcher/gpu/ScreenEffects.kt | 30 ++-- .../droiddeck/launcher/gpu/TurnipDriver.java | 21 +-- .../droiddeck/launcher/gpu/TurnipReleases.kt | 15 +- .../launcher/session/ComponentsManager.kt | 55 ++++--- .../droiddeck/launcher/session/EsyncPacks.kt | 6 +- .../droiddeck/launcher/session/GameStorage.kt | 7 +- .../launcher/session/LoadingState.kt | 9 +- .../droiddeck/launcher/session/OfflineMode.kt | 3 +- .../launcher/session/ProtonExtras.kt | 47 +++--- .../launcher/session/SessionLogShare.kt | 3 +- .../launcher/session/SessionPrefs.kt | 38 +++++ .../droiddeck/launcher/wayland/HdrSupport.kt | 22 +-- app/src/main/res/values/strings_g4.xml | 144 ++++++++++++++++++ 16 files changed, 328 insertions(+), 105 deletions(-) create mode 100644 app/src/main/res/values/strings_g4.xml diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/DriverBundle.kt b/app/src/main/java/com/droiddeck/launcher/gpu/DriverBundle.kt index 169682101..2814e4bfb 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/DriverBundle.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/DriverBundle.kt @@ -3,6 +3,7 @@ package com.droiddeck.launcher.gpu import android.content.Context import android.net.Uri import android.util.Log +import com.droiddeck.launcher.R import com.droiddeck.launcher.core.FileUtils import com.droiddeck.launcher.session.SessionPrefs import org.json.JSONObject @@ -84,7 +85,7 @@ object DriverBundle { unzip(context, uri, work) val manifest = File(work, MANIFEST).takeIf { it.isFile } ?.let { runCatching { parseManifest(JSONObject(FileUtils.readString(it))) }.getOrNull() } - ?: throw IllegalArgumentException("Not a driver bundle: no manifest.json naming an android and a linux driver") + ?: throw IllegalArgumentException(context.getString(R.string.bundle_no_manifest)) val androidDir = File(work, manifest.androidPath) val linuxDir = File(work, manifest.linuxPath) @@ -135,7 +136,7 @@ object DriverBundle { for (entry in generateSequence { zip.nextEntry }) { if (entry.isDirectory) continue val out = File(root, entry.name).canonicalFile - if (!out.path.startsWith(root.path + File.separator)) throw IllegalArgumentException("Not a driver bundle: ${entry.name} points outside it") + if (!out.path.startsWith(root.path + File.separator)) throw IllegalArgumentException(context.getString(R.string.bundle_entry_outside, entry.name)) out.parentFile?.mkdirs() out.outputStream().use { sink -> val buf = ByteArray(1 shl 16) @@ -143,7 +144,7 @@ object DriverBundle { val r = zip.read(buf) if (r <= 0) break total += r - if (total > MAX_BYTES) throw IllegalArgumentException("Not a driver bundle: it unpacks to more than ${MAX_BYTES shr 20} MB") + if (total > MAX_BYTES) throw IllegalArgumentException(context.getString(R.string.bundle_too_large, MAX_BYTES shr 20)) sink.write(buf, 0, r) } } diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/GpuInfo.kt b/app/src/main/java/com/droiddeck/launcher/gpu/GpuInfo.kt index 3054f0733..4f90bdfb4 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/GpuInfo.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/GpuInfo.kt @@ -1,6 +1,8 @@ package com.droiddeck.launcher.gpu +import android.content.Context import android.os.Build +import com.droiddeck.launcher.R import com.droiddeck.launcher.core.FileUtils import java.io.File @@ -29,7 +31,10 @@ data class GpuInfo( A6XX("Adreno 6xx"), /** An Adreno whose model KGSL does not give: treated as the newest family it could be. */ ADRENO_UNKNOWN("Adreno"), - NOT_ADRENO("Not an Adreno GPU"), + NOT_ADRENO("Not an Adreno GPU"); + + /** [label] in the app's language: the Adreno families are names, the rest is words. */ + fun label(context: Context): String = if (this == NOT_ADRENO) context.getString(R.string.gpuinfo_not_adreno) else label } enum class Support { TESTED, UNTESTED, UNSUPPORTED } @@ -42,7 +47,7 @@ data class GpuInfo( else -> Support.UNTESTED } - /** One line for the device card and the system check. */ + /** One line for the device card and the system check, in English for the device report. */ val supportText: String get() = when (support) { Support.TESTED -> "Supported" @@ -51,6 +56,13 @@ data class GpuInfo( Support.UNSUPPORTED -> "Not supported: DroidDeck needs an Adreno (Snapdragon) GPU" } + /** [supportText] in the app's language. */ + fun supportText(context: Context): String = context.getString(when (support) { + Support.TESTED -> R.string.gpuinfo_supported + Support.UNTESTED -> if (family == Family.A7XX_LOW) R.string.gpuinfo_experimental else R.string.gpuinfo_below_tested + Support.UNSUPPORTED -> R.string.gpuinfo_unsupported + }) + companion object { fun detect(): GpuInfo { val adreno = File("/sys/class/kgsl/kgsl-3d0").exists() || File("/vendor/lib64/hw/vulkan.adreno.so").exists() diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriverManager.java b/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriverManager.java index c0cb48f08..2b7a5aa25 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriverManager.java +++ b/app/src/main/java/com/droiddeck/launcher/gpu/LinuxVulkanDriverManager.java @@ -4,6 +4,7 @@ import android.content.Context; import android.net.Uri; import android.util.Log; +import com.droiddeck.launcher.R; import com.droiddeck.launcher.core.FileUtils; import org.json.JSONObject; @@ -181,20 +182,17 @@ public class LinuxVulkanDriverManager { String adopt(File tmpDir, String soName, JSONObject zipMeta, String displayName) throws IOException { try { if (soName == null) { - throw new IllegalArgumentException("No libvulkan_freedreno*.so in this zip. An Android " - + "(AdrenoTools) or -Wayland Turnip zip is not a Linux runtime driver."); + throw new IllegalArgumentException(context.getString(R.string.lvd_no_library)); } File so = new File(tmpDir, LIB_NAME); if (!isAarch64Elf(so)) { - throw new IllegalArgumentException(soName + " is not a 64-bit AArch64 ELF shared library."); + throw new IllegalArgumentException(context.getString(R.string.lvd_not_aarch64, soName)); } // The one check that actually separates this from the bionic builds. Both sonames // are in the driver's .dynstr, so a byte scan is enough and needs no ELF parsing: // glibc's is versioned ("libc.so.6"), bionic's is not ("libc.so"). if (!containsAscii(so, "libc.so.6")) { - throw new IllegalArgumentException(soName + " is not a glibc driver - it links Android's libc. " - + "The Linux runtime needs a \"-Linux\" zip; a plain or \"-Wayland\" Turnip cannot be " - + "loaded by the Steam client at all."); + throw new IllegalArgumentException(context.getString(R.string.lvd_not_glibc, soName)); } String kind = zipMeta != null ? zipMeta.optString("kind", "") : ""; if (!kind.isEmpty() && !"linux-vulkan-icd".equals(kind)) { diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/ScreenEffects.kt b/app/src/main/java/com/droiddeck/launcher/gpu/ScreenEffects.kt index 28adc4f51..36d233f49 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/ScreenEffects.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/ScreenEffects.kt @@ -1,5 +1,7 @@ package com.droiddeck.launcher.gpu +import androidx.annotation.StringRes +import com.droiddeck.launcher.R import com.droiddeck.launcher.wayland.WaylandCompositor import org.json.JSONObject import kotlin.math.abs @@ -82,11 +84,15 @@ data class ScreenEffects( * SessionPrefs.upscalerChoices mode the Look switches to. */ data class Look( + /** The Look's id: what the row picks by and the compositor logs. */ val name: String, val effects: ScreenEffects, val scalingMode: Int? = null, /** One line under the row while this Look is picked. */ val desc: String, + /** [name] and [desc] in the app's language, for the menu. */ + @StringRes val label: Int, + @StringRes val descRes: Int, ) object ScreenEffectLooks { @@ -96,29 +102,29 @@ object ScreenEffectLooks { /** Index 0 is the neutral state a fresh session matches, so the row always names something. */ val LOOKS: List = listOf( - Look("Off", ScreenEffects.OFF, desc = "Nothing applied."), + Look("Off", ScreenEffects.OFF, desc = "Nothing applied.", label = R.string.frame_gen_off, descRes = R.string.look_off_desc), Look("Game Clarity", ScreenEffects(brightness = 2, contrast = 12, saturation = 108, cas = true, casLevel = 55), - desc = "Sharpening with a touch of contrast - the everyday one."), + desc = "Sharpening with a touch of contrast - the everyday one.", label = R.string.look_game_clarity, descRes = R.string.look_game_clarity_desc), Look("Vivid", ScreenEffects(brightness = 3, contrast = 10, gamma = 0.98f, saturation = 145, cas = true, casLevel = 30, deband = true), - desc = "Colour pushed hard. Deband on so skies don't stripe."), + desc = "Colour pushed hard. Deband on so skies don't stripe.", label = R.string.look_vivid, descRes = R.string.look_vivid_desc), Look("Cinematic", ScreenEffects(brightness = -4, contrast = 22, gamma = 1.10f, saturation = 88, cas = true, casLevel = 20, fxaa = true, deband = true), - desc = "Deeper contrast, cooler colour, edges smoothed."), + desc = "Deeper contrast, cooler colour, edges smoothed.", label = R.string.look_cinematic, descRes = R.string.look_cinematic_desc), Look("Competitive", ScreenEffects(brightness = 16, contrast = 18, gamma = 0.92f, saturation = 82, cas = true, casLevel = 75), - desc = "Shadows lifted and heavy sharpening - spot people first."), + desc = "Shadows lifted and heavy sharpening - spot people first.", label = R.string.look_competitive, descRes = R.string.look_competitive_desc), Look("Adaptive Sharpen", ScreenEffects(cas = true, casLevel = 85), - desc = "Sharpening only, colour untouched."), + desc = "Sharpening only, colour untouched.", label = R.string.look_adaptive_sharpen, descRes = R.string.look_adaptive_sharpen_desc), Look("Filmic", ScreenEffects(brightness = -2, contrast = 16, gamma = 1.14f, saturation = 92, cas = true, casLevel = 15, fxaa = true, deband = true), - desc = "Film-like curve, slightly muted."), + desc = "Film-like curve, slightly muted.", label = R.string.look_filmic, descRes = R.string.look_filmic_desc), Look("Arcade", ScreenEffects(brightness = 6, contrast = 20, gamma = 0.95f, saturation = 170, cas = true, casLevel = 45, deband = true), - desc = "Loud and punchy."), + desc = "Loud and punchy.", label = R.string.look_arcade, descRes = R.string.look_arcade_desc), Look("Retro CRT", ScreenEffects(brightness = 6, contrast = 14, gamma = 1.05f, saturation = 115, crt = true, ntsc = true), - desc = "The CRT tube and analogue signal passes together."), + desc = "The CRT tube and analogue signal passes together.", label = R.string.look_retro_crt, descRes = R.string.look_retro_crt_desc), Look("Upscale Sharp", ScreenEffects(contrast = 6, saturation = 104, cas = true, casLevel = 70), scalingMode = SCALING_FSR, - desc = "For running below panel resolution."), + desc = "For running below panel resolution.", label = R.string.look_upscale_sharp, descRes = R.string.look_upscale_sharp_desc), Look("Pixel Clean", ScreenEffects(contrast = 8, deband = true), scalingMode = SCALING_NEAREST, - desc = "Crisp pixels for 2D and old titles."), + desc = "Crisp pixels for 2D and old titles.", label = R.string.look_pixel_clean, descRes = R.string.look_pixel_clean_desc), Look("Anime Edge", ScreenEffects(brightness = 2, contrast = 10, saturation = 125, cas = true, casLevel = 50, toon = true, deband = true), - desc = "Toon outlines plus sharpening."), + desc = "Toon outlines plus sharpening.", label = R.string.look_anime_edge, descRes = R.string.look_anime_edge_desc), ) /** diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java b/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java index 52d8129ff..5cd414676 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java +++ b/app/src/main/java/com/droiddeck/launcher/gpu/TurnipDriver.java @@ -5,6 +5,7 @@ import android.net.Uri; import android.os.Environment; import android.util.Log; +import com.droiddeck.launcher.R; import com.droiddeck.launcher.core.FileUtils; import com.droiddeck.launcher.core.TarZst; import com.droiddeck.launcher.session.SessionPrefs; @@ -218,7 +219,7 @@ public final class TurnipDriver { */ String adopt(File tmpDir, String displayName) throws IOException { String rejected = rejectionReason(tmpDir); - if (rejected != null) throw new IllegalArgumentException("Not an AdrenoTools driver: " + rejected); + if (rejected != null) throw new IllegalArgumentException(context.getString(R.string.turnip_not_adrenotools, rejected)); JSONObject meta = readMetaFile(new File(tmpDir, "meta.json")); String name = meta != null ? meta.optString("name", "") : ""; @@ -245,31 +246,31 @@ public final class TurnipDriver { * Why a just-extracted zip is not something adrenotools can be pointed at, or null when it is. * The kinds refused by name are the other driver lists' zips, which carry a meta.json too. */ - private static String rejectionReason(File dir) { + private String rejectionReason(File dir) { File metaFile = new File(dir, "meta.json"); - if (!metaFile.isFile()) return "no meta.json"; + if (!metaFile.isFile()) return context.getString(R.string.turnip_reject_no_meta); String libraryName, kind; try { JSONObject meta = new JSONObject(FileUtils.readString(metaFile)); libraryName = meta.optString("libraryName", ""); kind = meta.optString("kind", ""); } catch (Exception e) { - return "meta.json is unreadable (" + e.getMessage() + ")"; + return context.getString(R.string.turnip_reject_meta_unreadable, e.getMessage()); } if ("linux-vulkan-icd".equals(kind)) - return "this is a Linux runtime driver (import it under \"Linux runtime driver\")"; + return context.getString(R.string.turnip_reject_linux_driver); if ("wayland-game-driver".equals(kind)) - return "this is a Wayland game driver, which this app has no use for"; + return context.getString(R.string.turnip_reject_wayland); if (libraryName.isEmpty()) - return "meta.json names no libraryName, so nothing could be handed to AdrenoTools"; + return context.getString(R.string.turnip_reject_no_library_name); File library = new File(dir, libraryName); if (!library.isFile()) - return "meta.json names " + libraryName + ", which is not in the zip"; + return context.getString(R.string.turnip_reject_library_missing, libraryName); if (!LinuxVulkanDriverManager.isAarch64Elf(library)) - return libraryName + " is not a 64-bit AArch64 ELF shared library"; + return context.getString(R.string.turnip_reject_not_aarch64, libraryName); // A glibc build that happens to carry a libraryName would load nothing in this process. if (LinuxVulkanDriverManager.containsAscii(library, "libc.so.6")) - return libraryName + " links glibc - a \"-Linux\" Turnip, which belongs under \"Linux runtime driver\""; + return context.getString(R.string.turnip_reject_glibc, libraryName); return null; } diff --git a/app/src/main/java/com/droiddeck/launcher/gpu/TurnipReleases.kt b/app/src/main/java/com/droiddeck/launcher/gpu/TurnipReleases.kt index 9a6235f1e..acc941928 100644 --- a/app/src/main/java/com/droiddeck/launcher/gpu/TurnipReleases.kt +++ b/app/src/main/java/com/droiddeck/launcher/gpu/TurnipReleases.kt @@ -1,6 +1,7 @@ package com.droiddeck.launcher.gpu import com.droiddeck.launcher.core.Hashes +import com.droiddeck.launcher.R import android.content.Context import com.droiddeck.launcher.core.FileUtils import org.json.JSONArray @@ -95,7 +96,7 @@ object TurnipReleases { var lastError: Exception? = null for (src in SOURCES) { try { - val releases = JSONArray(get("https://api.github.com/repos/${src.repo}/releases?per_page=15")) + val releases = JSONArray(get(context, "https://api.github.com/repos/${src.repo}/releases?per_page=15")) val seen = HashSet() var newest: String? = null for (i in 0 until releases.length()) { @@ -135,7 +136,7 @@ object TurnipReleases { return parse(stored) } - private fun get(url: String): String { + private fun get(context: Context, url: String): String { val c = URL(url).openConnection() as HttpURLConnection c.connectTimeout = 15_000 c.readTimeout = 20_000 @@ -143,8 +144,8 @@ object TurnipReleases { c.setRequestProperty("User-Agent", "DroidDeck-app") try { val code = c.responseCode - if (code == 403 || code == 429) throw IOException("GitHub's rate limit was hit - try again later") - if (code != 200) throw IOException("GitHub answered HTTP $code") + if (code == 403 || code == 429) throw IOException(context.getString(R.string.turnip_rate_limited)) + if (code != 200) throw IOException(context.getString(R.string.turnip_github_http, code)) return c.inputStream.bufferedReader().use { it.readText() } } finally { c.disconnect() @@ -194,7 +195,7 @@ object TurnipReleases { /** Download an asset into the cache; the caller imports it and deletes the file. */ fun download(context: Context, asset: Asset, progress: (Int) -> Unit): File { - if (asset.sha256.isEmpty()) throw IOException("This driver list predates checksums - refresh it and try again") + if (asset.sha256.isEmpty()) throw IOException(context.getString(R.string.turnip_list_predates_checksums)) val target = File(context.cacheDir, asset.name) val c = URL(asset.url).openConnection() as HttpURLConnection c.connectTimeout = 15_000 @@ -202,7 +203,7 @@ object TurnipReleases { c.setRequestProperty("User-Agent", "DroidDeck-app") c.instanceFollowRedirects = true try { - if (c.responseCode != 200) throw IOException("download answered HTTP ${c.responseCode}") + if (c.responseCode != 200) throw IOException(context.getString(R.string.turnip_download_http, c.responseCode)) val total = c.contentLengthLong.takeIf { it > 0 } ?: asset.size c.inputStream.use { input -> FileOutputStream(target).use { out -> @@ -220,7 +221,7 @@ object TurnipReleases { } } if (!asset.sha256.equals(Hashes.sha256(target), ignoreCase = true)) { - throw IOException("Checksum mismatch - the download was discarded") + throw IOException(context.getString(R.string.turnip_checksum_mismatch)) } return target } catch (e: Exception) { diff --git a/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt b/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt index 1f51f666d..04cf9e2d1 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/ComponentsManager.kt @@ -1,6 +1,7 @@ package com.droiddeck.launcher.session import com.droiddeck.launcher.core.Hashes +import com.droiddeck.launcher.R import android.content.Context import android.util.Log import com.droiddeck.launcher.core.Downloader @@ -415,23 +416,23 @@ object ComponentsManager { val views = protons(context).map { p -> val comps = COMPONENTS.associateWith { comp -> val detected = if (comp == "fex") fexVersion(p.dir) else readVersionFile(File(p.dir, "${COMP_DIR.getValue(comp)}/version")) - val detail = if (comp == "fex") (if (File(p.dir, FEX_FILES[2]).isFile) "with Linux helpers" else "DLLs only") else "" + val detail = if (comp == "fex") context.getString(if (File(p.dir, FEX_FILES[2]).isFile) R.string.cmgr_fex_linux_helpers else R.string.cmgr_fex_dlls_only) else "" val active = state.sub("active").optJSONObject(p.id)?.optJSONObject(comp) val inUseLabel = when { - active == null -> "Original" - active.optString("protonVersion") != p.version -> "Original (Proton updated since the last swap)" + active == null -> context.getString(R.string.comp_tag_original) + active.optString("protonVersion") != p.version -> context.getString(R.string.cmgr_original_proton_updated) else -> { val want = active.optJSONObject("files") ?: JSONObject() val fp = fingerprint(p.dir, comp) - if (want.keys().asSequence().all { fp[it] == want.optString(it) }) active.optString("label", active.optString("file")) - else "Changed outside Components" + if (want.keys().asSequence().all { fp[it] == want.optString(it) }) originalLabel(context, active) ?: active.optString("label", active.optString("file")) + else context.getString(R.string.cmgr_changed_outside) } } val q = state.sub("queued").optJSONObject(p.id)?.optJSONObject(comp) Component( - detected.ifEmpty { "not present" }, detail, inUseLabel, + detected.ifEmpty { context.getString(R.string.cmgr_not_present) }, detail, inUseLabel, active?.optString("file")?.takeIf { active.optString("protonVersion") == p.version }, - q?.optString("label"), + q?.let { originalLabel(context, it) ?: it.optString("label") }, ) } ProtonView(p, comps, originals(context, p.id), gameRunning(context, p), reapplied[p.dir.name] ?: 0, syncPacks.tool(p.guestPath)) @@ -439,6 +440,16 @@ object ComponentsManager { Snapshot(views, packages(context)) } + /** + * The label of a chosen or queued original, in the app's language: the stored "label" was written + * in English when it was chosen. Null for a package, whose label is its version. + */ + private fun originalLabel(context: Context, entry: JSONObject): String? = when { + entry.optString("kind") == "original" -> context.getString(R.string.cmgr_original_version, entry.optString("protonVersion")) + entry.optString("file").startsWith("original ") -> context.getString(R.string.cmgr_original_from, entry.optString("file").removePrefix("original ")) + else -> null + } + // ------------------------------------------------------------------ launch-time enforcement /** @@ -512,12 +523,12 @@ object ComponentsManager { if (inUse(context, p)) { state.sub("queued").sub(p.id).put(info.comp, JSONObject().put("kind", "package").put("file", wcp.name).put("label", info.version)) saveState(context, state) - return "${LABEL[info.comp]} ${info.version} goes into ${p.name} when the game running on it closes." + return context.getString(R.string.cmgr_swap_queued, LABEL[info.comp], info.version, p.name) } applyPackage(context, p, info, wcp, state) state.sub("queued").optJSONObject(p.id)?.remove(info.comp) saveState(context, state) - "${LABEL[info.comp]} ${info.version} is now in ${p.name}. It applies the next time a game starts." + context.getString(R.string.cmgr_swap_done, LABEL[info.comp], info.version, p.name) } private fun applyPackage(context: Context, p: Proton, info: Package, wcp: File, state: JSONObject) { @@ -533,17 +544,17 @@ object ComponentsManager { fun restore(context: Context, protonId: String, comp: String, protonVersion: String): String = synchronized(lock) { val p = proton(context, protonId) val wcp = File(originalsDir(context), "$protonId/${safeName(protonVersion)}/$comp.wcp") - check(wcp.isFile) { "That original bundle is not stored" } + check(wcp.isFile) { context.getString(R.string.cmgr_original_missing) } val state = loadState(context) if (inUse(context, p)) { state.sub("queued").sub(p.id).put(comp, JSONObject().put("kind", "original").put("protonVersion", protonVersion).put("label", "Original $protonVersion")) saveState(context, state) - return "${LABEL[comp]} of ${p.name} goes back to its original when the game running on it closes." + return context.getString(R.string.cmgr_restore_queued, LABEL[comp], p.name) } applyOriginal(context, p, comp, protonVersion, wcp, state) state.sub("queued").optJSONObject(p.id)?.remove(comp) saveState(context, state) - "${LABEL[comp]} of ${p.name} restored from its $protonVersion original." + context.getString(R.string.cmgr_restore_done, LABEL[comp], p.name, protonVersion) } private fun applyOriginal(context: Context, p: Proton, comp: String, protonVersion: String, wcp: File, state: JSONObject) { @@ -599,22 +610,22 @@ object ComponentsManager { val active = state.sub("active") for (pid in active.keys()) { val comps = active.optJSONObject(pid) ?: continue - for (comp in comps.keys()) check(comps.optJSONObject(comp)?.optString("file") != name) { "$name is in use in $pid. Swap that Proton to something else first." } + for (comp in comps.keys()) check(comps.optJSONObject(comp)?.optString("file") != name) { context.getString(R.string.cmgr_package_in_use, name, pid) } } val f = File(packagesDir(context), name) - check(f.isFile) { "$name is not stored" } + check(f.isFile) { context.getString(R.string.cmgr_package_not_stored, name) } f.delete() - "Deleted $name." + context.getString(R.string.cmgr_deleted, name) } fun deleteOriginal(context: Context, protonId: String, comp: String, protonVersion: String): String = synchronized(lock) { val p = proton(context, protonId) - check(safeName(p.version) != protonVersion) { "The installed build's original is kept: it is the way back." } + check(safeName(p.version) != protonVersion) { context.getString(R.string.cmgr_original_kept) } val f = File(originalsDir(context), "$protonId/${safeName(protonVersion)}/$comp.wcp") - check(f.isFile) { "Not stored" } + check(f.isFile) { context.getString(R.string.cmgr_not_stored) } f.delete() f.parentFile?.takeIf { it.listFiles().isNullOrEmpty() }?.delete() - "Deleted the $protonVersion original of ${LABEL[comp]}." + context.getString(R.string.cmgr_deleted_original, protonVersion, LABEL[comp]) } /** Imports a -linux .wcp from anywhere the app can read (the file picker hands over a copy). */ @@ -662,13 +673,13 @@ object ComponentsManager { /** Downloads a catalog item into storage, verifying the release's sha256 and the package type. */ fun download(context: Context, item: CatalogItem, progress: (Int) -> Unit): Package { - require(item.url.startsWith("https://github.com/$NIGHTLIES/releases/download/")) { "Downloads come only from the Nightlies releases" } - require(Hashes.isGithubSha256(item.digest)) { "This package list predates checksums - refresh it and try again" } + require(item.url.startsWith("https://github.com/$NIGHTLIES/releases/download/")) { context.getString(R.string.cmgr_nightlies_only) } + require(Hashes.isGithubSha256(item.digest)) { context.getString(R.string.cmgr_list_predates_checksums) } val dest = File(packagesDir(context), safeName(item.file)) val part = File(dest.parentFile, dest.name + ".part") part.delete() - check(Downloader.downloadFile(item.url, part, false) { f -> progress(if (f < 0) -1 else (f * 100).toInt().coerceIn(0, 100)) }) { "Download failed" } - if (!Hashes.sha256(part).equals(item.digest.substringAfter(':'), true)) { part.delete(); error("Checksum mismatch") } + check(Downloader.downloadFile(item.url, part, false) { f -> progress(if (f < 0) -1 else (f * 100).toInt().coerceIn(0, 100)) }) { context.getString(R.string.user_apps_download_failed) } + if (!Hashes.sha256(part).equals(item.digest.substringAfter(':'), true)) { part.delete(); error(context.getString(R.string.cmgr_checksum_mismatch)) } val info = runCatching { packageInfo(part) }.getOrElse { part.delete(); throw it } synchronized(lock) { part.renameTo(dest) } return info.copy(file = dest.name) diff --git a/app/src/main/java/com/droiddeck/launcher/session/EsyncPacks.kt b/app/src/main/java/com/droiddeck/launcher/session/EsyncPacks.kt index 7c1664d34..696657b1a 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/EsyncPacks.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/EsyncPacks.kt @@ -49,7 +49,6 @@ object EsyncPacks { private const val TOOLS = "tools.tsv" private const val FLOOR = "floor" private const val PACK_FAILURES = "packFailures" - private const val PROGRESS_LABEL = "Fetching droiddeck-esync pack" private const val INDEX_LIMIT = 4L shl 20 private const val SIG_LIMIT = 4096L private const val ASSET_LIMIT = 256L shl 20 @@ -618,14 +617,15 @@ object EsyncPacks { return false } try { - onProgress?.invoke(PROGRESS_LABEL, -1) + val label = context.getString(com.droiddeck.launcher.R.string.esync_fetching) + onProgress?.invoke(label, -1) var reported = -1 val fetched = FileOutputStream(part).use { output -> fetch(entry.asset.url, entry.asset.size, output) { done -> val percent = (done * 100 / entry.asset.size).toInt().coerceIn(0, 100) if (percent != reported) { reported = percent - onProgress?.invoke(PROGRESS_LABEL, percent) + onProgress?.invoke(label, percent) } } } diff --git a/app/src/main/java/com/droiddeck/launcher/session/GameStorage.kt b/app/src/main/java/com/droiddeck/launcher/session/GameStorage.kt index 88ab5ab16..1de64dcff 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/GameStorage.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/GameStorage.kt @@ -4,6 +4,7 @@ import android.content.Context import android.os.Environment import android.os.StatFs import android.os.storage.StorageManager +import com.droiddeck.launcher.R import com.droiddeck.launcher.core.FileUtils import java.io.File @@ -26,8 +27,8 @@ object GameStorage { val removable = try { Environment.isExternalStorageRemovable(dir) } catch (e: Exception) { false } if (!removable) return@mapNotNull null val volume = try { sm?.getStorageVolume(dir) } catch (e: Exception) { null } - val name = volume?.getDescription(context)?.takeIf { it.isNotBlank() && !it.equals("android", true) } ?: "SD card" - Option("$name · ${free(dir)} free", File(dir, "steam").absolutePath) + val name = volume?.getDescription(context)?.takeIf { it.isNotBlank() && !it.equals("android", true) } ?: context.getString(R.string.gstore_sd_card) + Option("$name · " + context.getString(R.string.gstore_free, free(dir)), File(dir, "steam").absolutePath) } } @@ -66,5 +67,5 @@ object GameStorage { /** The label the client shows for a chosen folder: its last name, or the volume's. */ fun labelFor(context: Context, path: String): String = options(context).firstOrNull { it.path == path }?.label?.substringBefore(" ·") - ?: File(path).name.ifEmpty { "Folder" } + ?: File(path).name.ifEmpty { context.getString(R.string.gstore_folder) } } diff --git a/app/src/main/java/com/droiddeck/launcher/session/LoadingState.kt b/app/src/main/java/com/droiddeck/launcher/session/LoadingState.kt index 566300c61..f6229d01c 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/LoadingState.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/LoadingState.kt @@ -19,7 +19,7 @@ import java.nio.charset.StandardCharsets */ class LoadingState(context: Context, steam: Boolean = true) { var visible by mutableStateOf(true) - var step by mutableStateOf("Starting the session…") + var step by mutableStateOf(context.getString(com.droiddeck.launcher.R.string.loadstate_starting)) var percent by mutableIntStateOf(-1) var elapsed by mutableStateOf("") var hint by mutableStateOf("") @@ -31,12 +31,13 @@ class LoadingState(context: Context, steam: Boolean = true) { private val hints = context.resources.getStringArray( if (steam) com.droiddeck.launcher.R.array.loading_hints else com.droiddeck.launcher.R.array.loading_hints_desktop, ) + private val res = context.resources private val startedAt = SystemClock.elapsedRealtime() /** Once a second: the clock and the hint. */ fun tick() { val seconds = (SystemClock.elapsedRealtime() - startedAt) / 1000 - elapsed = String.format(java.util.Locale.US, "%d:%02d elapsed · still working", seconds / 60, seconds % 60) + elapsed = res.getString(com.droiddeck.launcher.R.string.loadstate_elapsed, seconds / 60, seconds % 60) hint = hints[((seconds / 8) % hints.size).toInt()] } @@ -101,9 +102,9 @@ class LoadingState(context: Context, steam: Boolean = true) { } return when { downloadAt > stepAt && downloadPercent >= 0 -> - Pair("Downloading the Steam client update · $downloadPercent%", downloadPercent) + Pair(context.getString(com.droiddeck.launcher.R.string.loadstate_steam_update, downloadPercent), downloadPercent) clientDownloadAt == stepAt && clientDownload != null -> - Pair("Downloading the Steam client · $clientDownload", clientPercent) + Pair(context.getString(com.droiddeck.launcher.R.string.loadstate_steam_client, clientDownload), clientPercent) stepText != null -> Pair(stepText, -1) else -> null } diff --git a/app/src/main/java/com/droiddeck/launcher/session/OfflineMode.kt b/app/src/main/java/com/droiddeck/launcher/session/OfflineMode.kt index 79e2a907e..edce7d40a 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/OfflineMode.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/OfflineMode.kt @@ -2,6 +2,7 @@ package com.droiddeck.launcher.session import android.content.Context import android.util.Log +import com.droiddeck.launcher.R import com.droiddeck.launcher.runtime.LinuxRuntime import java.io.File @@ -39,7 +40,7 @@ object OfflineMode { Log.w(TAG, "could not read loginusers.vdf", e); return null } if (!REMEMBER.containsMatchIn(text)) return null - return PERSONA.find(text)?.groupValues?.get(1)?.takeIf { it.isNotBlank() } ?: "signed in" + return PERSONA.find(text)?.groupValues?.get(1)?.takeIf { it.isNotBlank() } ?: context.getString(R.string.offline_signed_in) } fun enabled(context: Context): Boolean = prefs(context).getBoolean(KEY, false) diff --git a/app/src/main/java/com/droiddeck/launcher/session/ProtonExtras.kt b/app/src/main/java/com/droiddeck/launcher/session/ProtonExtras.kt index 7340d2048..268210888 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/ProtonExtras.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/ProtonExtras.kt @@ -1,6 +1,7 @@ package com.droiddeck.launcher.session import com.droiddeck.launcher.core.Hashes +import com.droiddeck.launcher.R import android.content.Context import android.os.StatFs import android.util.Log @@ -59,70 +60,72 @@ object ProtonExtras { /** Downloads, verifies and registers the latest build without waiting for another session. */ @Synchronized fun install(context: Context, tool: Tool, onProgress: (String, Int) -> Unit): String? { - if (installInProgress) return "A compatibility tool install is already running" + if (installInProgress) return context.getString(R.string.pextra_busy) installInProgress = true return try { installNow(context, tool, onProgress) } finally { installInProgress = false } } private fun installNow(context: Context, tool: Tool, onProgress: (String, Int) -> Unit): String? { - if (SessionState.running) return "Stop the active session before installing compatibility tools" - if (!LinuxRuntime.isInstalled(context)) return "Install the Linux runtime first" + if (SessionState.running) return context.getString(R.string.pextra_stop_session) + if (!LinuxRuntime.isInstalled(context)) return context.getString(R.string.user_apps_runtime_required) - val asset = findLatestAsset(tool) ?: return "Could not find an ARM64 release for ${tool.name}" + val asset = findLatestAsset(tool) ?: return context.getString(R.string.pextra_no_release, tool.name) val downloads = File(context.filesDir, "proton-downloads").apply { mkdirs() } val archive = File(downloads, "${tool.id}-${asset.name}") if (asset.size > 0 && archive.length() > asset.size) archive.delete() val missing = (asset.size - archive.length()).coerceAtLeast(0L) if (StatFs(LinuxRuntime.rootDir(context).path).availableBytes < NEED_BYTES + missing) { - return "At least 4 GB free plus the download size is required to install ${tool.name}" + return context.getString(R.string.pextra_no_space, tool.name) } - onProgress("Downloading ${tool.name} ${asset.tag}", 0) + val downloading = context.getString(R.string.pextra_downloading, tool.name, asset.tag) + onProgress(downloading, 0) var downloaded = false for (attempt in 0 until 5) { downloaded = Downloader.downloadFile(asset.url, archive, true) { fraction -> - onProgress("Downloading ${tool.name} ${asset.tag}", if (fraction < 0) -1 else (fraction * 100f).toInt().coerceIn(0, 100)) + onProgress(downloading, if (fraction < 0) -1 else (fraction * 100f).toInt().coerceIn(0, 100)) } if (downloaded) break if (attempt < 4) { - onProgress("Retrying download · attempt ${attempt + 2} of 5", -1) + onProgress(context.getString(R.string.pextra_retrying, attempt + 2, 5), -1) try { Thread.sleep(3_000) } catch (_: InterruptedException) { Thread.currentThread().interrupt() - return "Download interrupted" + return context.getString(R.string.pextra_interrupted) } } } - if (!downloaded) return "Download failed; the partial file is kept so a retry can resume" + if (!downloaded) return context.getString(R.string.pextra_download_failed) if (asset.size > 0 && archive.length() != asset.size) { if (archive.length() > asset.size) { archive.delete() - return "Download exceeded the release size and was deleted" + return context.getString(R.string.pextra_too_big) } - return "Download was incomplete; the partial file is kept for a resumable retry" + return context.getString(R.string.pextra_incomplete) } // One checksum has to be found and has to match: GitHub's sha256 digest, else the // release's own sha512 file. A download neither vouches for is not installed. - onProgress("Verifying download", -1) + onProgress(context.getString(R.string.pextra_verifying), -1) val verified = when { asset.sha256 != null -> asset.sha256.equals(Hashes.sha256(archive), ignoreCase = true) asset.sha512 != null -> { val expected = Downloader.downloadString(asset.sha512)?.let { Regex("(?i)\\b[0-9a-f]{128}\\b").find(it)?.value } - ?: return "Could not read the release checksum; try again" + ?: return context.getString(R.string.pextra_checksum_unreadable) expected.equals(Hashes.sha512(archive), ignoreCase = true) } else -> { archive.delete() - return "${tool.name} ${asset.tag} publishes no checksum; nothing was installed" + return context.getString(R.string.pextra_no_checksum, tool.name, asset.tag) } } if (!verified) { archive.delete() - return "Checksum mismatch; the download was deleted" + return context.getString(R.string.pextra_checksum_mismatch) } - if (SessionState.running) return "A session started during the download; stop it before installing compatibility tools" - onProgress("Installing ${tool.name}", -1) + if (SessionState.running) return context.getString(R.string.pextra_session_started) + val installing = context.getString(R.string.pextra_installing, tool.name) + onProgress(installing, -1) return try { val root = LinuxRuntime.rootDir(context) LinuxRuntime.writeAccounts(context) @@ -146,17 +149,17 @@ object ProtonExtras { lines.forEach { line -> Log.i(TAG, line) if (line.contains("unpacking", ignoreCase = true) || line.contains("adopted", ignoreCase = true)) { - onProgress("Installing ${tool.name}", -1) + onProgress(installing, -1) } } } val status = child.waitFor() - if (status != 0) "${tool.name} installation failed (exit $status)" + if (status != 0) context.getString(R.string.pextra_install_failed, tool.name, status) else { archive.delete() unqueue(context, tool) if (EsyncPacks.enabled(context)) { - onProgress("Fetching droiddeck-esync pack", -1) + onProgress(context.getString(R.string.esync_fetching), -1) try { EsyncPacks.fetchWanted(context, root, onProgress) } catch (t: Throwable) { @@ -167,7 +170,7 @@ object ProtonExtras { } } catch (e: Exception) { Log.e(TAG, "install ${tool.id}", e) - e.message ?: "Could not install ${tool.name}" + e.message ?: context.getString(R.string.pextra_could_not_install, tool.name) } } diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionLogShare.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionLogShare.kt index bbe627a2e..fc0601282 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionLogShare.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionLogShare.kt @@ -4,6 +4,7 @@ import android.content.ClipData import android.content.Context import android.content.Intent import androidx.core.content.FileProvider +import com.droiddeck.launcher.R import com.droiddeck.launcher.core.LogRedactor import com.droiddeck.launcher.runtime.LinuxRuntime import java.io.File @@ -75,6 +76,6 @@ object SessionLogShare { .putExtra(Intent.EXTRA_SUBJECT, zip.nameWithoutExtension) .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) send.clipData = ClipData.newRawUri(zip.name, uri) - return Intent.createChooser(send, "Share session logs").addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + return Intent.createChooser(send, context.getString(R.string.logshare_chooser)).addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } } diff --git a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt index cda903be3..4b0434801 100644 --- a/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt +++ b/app/src/main/java/com/droiddeck/launcher/session/SessionPrefs.kt @@ -1,6 +1,7 @@ package com.droiddeck.launcher.session import android.content.Context +import com.droiddeck.launcher.R import com.droiddeck.launcher.core.TextureFiltering import com.droiddeck.launcher.gpu.ScreenEffects import org.json.JSONObject @@ -95,6 +96,16 @@ object SessionPrefs { SHAPE_WIDE to "Always 16:9", ) + /** [shapeChoices] labelled in the app's language. */ + fun shapeChoices(context: Context): List> = shapeChoices.map { (mode, label) -> + mode to when (mode) { + SHAPE_AUTO -> context.getString(R.string.sprefs_shape_auto) + SHAPE_EXACT -> context.getString(R.string.sprefs_shape_exact) + SHAPE_WIDE -> context.getString(R.string.sprefs_shape_wide) + else -> label + } + } + /** * The shape of the display the session presents: the panel's own (never narrower than 16:9), * exactly the panel's (a 4:3 or 3:2 handheld, drawn edge to edge), or a fixed 16:9. A foldable defaults to 16:9, which sits with modest bars on either of its @@ -667,6 +678,10 @@ object SessionPrefs { val fpsLimitChoices = listOf(0 to "Off", 30 to "30", 40 to "40", 45 to "45", 60 to "60", 90 to "90", 120 to "120") + /** [fpsLimitChoices] labelled in the app's language. */ + fun fpsLimitChoices(context: Context): List> = + fpsLimitChoices.map { (fps, label) -> fps to if (fps == 0) context.getString(R.string.frame_gen_off) else label } + /** * How the compositor resizes the session onto the panel (WaylandCompositor.nativeSetUpscaler's * modes): Off and Linear both filter bilinearly, Nearest keeps pixels square for 2D and old @@ -678,6 +693,16 @@ object SessionPrefs { 8 to "Snapdragon GSR (quality)", 7 to "NVIDIA NIS", 6 to "Sharpen only", ) + /** The [upscalerChoices] labels that are words rather than product names. */ + private val upscalerLabels = mapOf( + 0 to R.string.frame_gen_off, 1 to R.string.sprefs_upscaler_linear, 2 to R.string.sprefs_upscaler_nearest, + 5 to R.string.sprefs_upscaler_fsr_fit, 8 to R.string.sprefs_upscaler_gsr_quality, 6 to R.string.sprefs_upscaler_sharpen, + ) + + /** [upscalerChoices] labelled in the app's language; the English list stays for the device report. */ + fun upscalerChoices(context: Context): List> = + upscalerChoices.map { (mode, label) -> mode to (upscalerLabels[mode]?.let(context::getString) ?: label) } + fun upscaler(context: Context): Int = prefs(context).getInt("upscaler", 0).takeIf { m -> upscalerChoices.any { it.first == m } } ?: 0 @@ -705,6 +730,10 @@ object SessionPrefs { val textureAnisotropyChoices = TextureFiltering.ANISOTROPY.map { it to if (it == 0) "Off" else "${it}x" } + /** [textureAnisotropyChoices] labelled in the app's language. */ + fun textureAnisotropyChoices(context: Context): List> = + textureAnisotropyChoices.map { (value, label) -> value to if (value == 0) context.getString(R.string.frame_gen_off) else label } + val textureLodBiasChoices = TextureFiltering.LOD_BIAS.map { it to when (it) { TextureFiltering.LOD_BIAS_OFF -> "Off" @@ -713,6 +742,15 @@ object SessionPrefs { } } + /** [textureLodBiasChoices] labelled in the app's language. */ + fun textureLodBiasChoices(context: Context): List> = textureLodBiasChoices.map { (value, label) -> + value to when (value) { + TextureFiltering.LOD_BIAS_OFF -> context.getString(R.string.frame_gen_off) + TextureFiltering.LOD_BIAS_AUTO -> context.getString(R.string.sprefs_lod_bias_auto) + else -> label + } + } + /** Anisotropic filtering forced on DirectX 9-11 games (core/TextureFiltering); 0 = the game's own. */ fun textureAnisotropy(context: Context): Int = prefs(context).getInt("textureAnisotropy", 0).takeIf { it in TextureFiltering.ANISOTROPY } ?: 0 diff --git a/app/src/main/java/com/droiddeck/launcher/wayland/HdrSupport.kt b/app/src/main/java/com/droiddeck/launcher/wayland/HdrSupport.kt index 02df141e5..714746d20 100644 --- a/app/src/main/java/com/droiddeck/launcher/wayland/HdrSupport.kt +++ b/app/src/main/java/com/droiddeck/launcher/wayland/HdrSupport.kt @@ -4,6 +4,7 @@ import android.content.Context import android.os.Build import android.view.Display import android.view.WindowManager +import com.droiddeck.launcher.R /** * What the panel says about HDR, read from android.view.Display: the gate the compositor's HDR10 @@ -21,14 +22,9 @@ class HdrProbe( val ratio: Float, val displayId: Int, val name: String, -) { /** Why HDR cannot be offered, or null when it can. */ - val reason: String? = when { - Build.VERSION.SDK_INT < 29 -> "needs Android 10 or newer (display layers with a color space)" - !hdr10 -> if (formats.isEmpty()) "this display reports no HDR support" else "this display supports $formats, not HDR10" - else -> null - } -} + val reason: String?, +) object HdrSupport { fun probe(context: Context): HdrProbe { @@ -38,14 +34,15 @@ object HdrSupport { @Suppress("DEPRECATION") (context.getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay } - if (display == null) return HdrProbe(false, "", 0f, 0f, 0f, false, -1f, -1, "none") + if (display == null) return HdrProbe(false, "", 0f, 0f, 0f, false, -1f, -1, "none", reason(context, false, "")) val caps = try { display.hdrCapabilities } catch (e: Exception) { null } val types = caps?.supportedHdrTypes ?: IntArray(0) val formats = types.joinToString(", ") { typeName(it) } val ratioAvailable = Build.VERSION.SDK_INT >= 34 && try { display.isHdrSdrRatioAvailable } catch (e: Exception) { false } val ratio = if (ratioAvailable) try { display.hdrSdrRatio } catch (e: Exception) { -1f } else -1f + val hdr10 = types.contains(Display.HdrCapabilities.HDR_TYPE_HDR10) return HdrProbe( - hdr10 = types.contains(Display.HdrCapabilities.HDR_TYPE_HDR10), + hdr10 = hdr10, formats = formats, maxLuminance = caps?.desiredMaxLuminance ?: 0f, maxAverageLuminance = caps?.desiredMaxAverageLuminance ?: 0f, @@ -54,9 +51,16 @@ object HdrSupport { ratio = ratio, displayId = display.displayId, name = display.name ?: "display", + reason = reason(context, hdr10, formats), ) } + private fun reason(context: Context, hdr10: Boolean, formats: String): String? = when { + Build.VERSION.SDK_INT < 29 -> context.getString(R.string.hdr_needs_android_10) + !hdr10 -> if (formats.isEmpty()) context.getString(R.string.hdr_none) else context.getString(R.string.hdr_not_hdr10, formats) + else -> null + } + private fun typeName(t: Int): String = when (t) { Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION -> "Dolby Vision" Display.HdrCapabilities.HDR_TYPE_HDR10 -> "HDR10" diff --git a/app/src/main/res/values/strings_g4.xml b/app/src/main/res/values/strings_g4.xml new file mode 100644 index 000000000..4037a8b69 --- /dev/null +++ b/app/src/main/res/values/strings_g4.xml @@ -0,0 +1,144 @@ + + + + Starting the session… + + %1$d:%2$02d elapsed · still working + Downloading the Steam client update · %1$d%% + + Downloading the Steam client · %1$s + + + A compatibility tool install is already running + Stop the active session before installing compatibility tools + Could not find an ARM64 release for %1$s + At least 4 GB free plus the download size is required to install %1$s + Downloading %1$s %2$s + Retrying download · attempt %1$d of %2$d + Download interrupted + Download failed; the partial file is kept so a retry can resume + Download exceeded the release size and was deleted + Download was incomplete; the partial file is kept for a resumable retry + Verifying download + Could not read the release checksum; try again + %1$s %2$s publishes no checksum; nothing was installed + Checksum mismatch; the download was deleted + A session started during the download; stop it before installing compatibility tools + Installing %1$s + + %1$s installation failed (exit %2$d) + Could not install %1$s + Fetching droiddeck-esync pack + + + with Linux helpers + DLLs only + Original (Proton updated since the last swap) + Changed outside Components + + not present + + Original %1$s + Original from %1$s + %1$s %2$s goes into %3$s when the game running on it closes. + %1$s %2$s is now in %3$s. It applies the next time a game starts. + That original bundle is not stored + + %1$s of %2$s goes back to its original when the game running on it closes. + + %1$s of %2$s restored from its %3$s original. + + %1$s is in use in %2$s. Swap that Proton to something else first. + %1$s is not stored + Deleted %1$s. + The installed build\'s original is kept: it is the way back. + Not stored + + Deleted the %1$s original of %2$s. + Downloads come only from the Nightlies releases + This package list predates checksums - refresh it and try again + Checksum mismatch + + + Auto (16:9+) + Match screen + Always 16:9 + Linear + Nearest + AMD FSR 1 (fit) + Snapdragon GSR (quality) + Sharpen only + Auto (match scaling) + + + Nothing applied. + Game Clarity + Sharpening with a touch of contrast - the everyday one. + Vivid + Colour pushed hard. Deband on so skies don\'t stripe. + Cinematic + Deeper contrast, cooler colour, edges smoothed. + Competitive + Shadows lifted and heavy sharpening - spot people first. + Adaptive Sharpen + Sharpening only, colour untouched. + Filmic + Film-like curve, slightly muted. + Arcade + Loud and punchy. + Retro CRT + The CRT tube and analogue signal passes together. + Upscale Sharp + For running below panel resolution. + Pixel Clean + Crisp pixels for 2D and old titles. + Anime Edge + Toon outlines plus sharpening. + + + Not an Adreno GPU + Supported + Experimental: its drivers are test builds + Below tested hardware (Adreno 725 and newer): it may not run + Not supported: DroidDeck needs an Adreno (Snapdragon) GPU + + + GitHub\'s rate limit was hit - try again later + GitHub answered HTTP %1$d + download answered HTTP %1$d + This driver list predates checksums - refresh it and try again + Checksum mismatch - the download was discarded + + Not an AdrenoTools driver: %1$s + no meta.json + meta.json is unreadable (%1$s) + this is a Linux runtime driver (import it under \"Linux runtime driver\") + this is a Wayland game driver, which this app has no use for + meta.json names no libraryName, so nothing could be handed to AdrenoTools + + meta.json names %1$s, which is not in the zip + %1$s is not a 64-bit AArch64 ELF shared library + %1$s links glibc - a \"-Linux\" Turnip, which belongs under \"Linux runtime driver\" + No libvulkan_freedreno*.so in this zip. An Android (AdrenoTools) or -Wayland Turnip zip is not a Linux runtime driver. + %1$s is not a 64-bit AArch64 ELF shared library. + %1$s is not a glibc driver - it links Android\'s libc. The Linux runtime needs a \"-Linux\" zip; a plain or \"-Wayland\" Turnip cannot be loaded by the Steam client at all. + Not a driver bundle: no manifest.json naming an android and a linux driver + Not a driver bundle: %1$s points outside it + Not a driver bundle: it unpacks to more than %1$d MB + + + SD card + + %1$s free + Folder + + Share session logs + + signed in + + + needs Android 10 or newer (display layers with a color space) + this display reports no HDR support + + this display supports %1$s, not HDR10 +