mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-11 02:51:10 +00:00
Merge branch 'wip/l10n-g4' into feat/language-selection
This commit is contained in:
commit
bd458667db
16 files changed
+328
-105
No files matched your search
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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<Look> = 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),
|
||||
)
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String>()
|
||||
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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) }
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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<Pair<String, String>> = 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<Pair<Int, String>> =
|
||||
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<Pair<Int, String>> =
|
||||
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<Pair<Int, String>> =
|
||||
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<Pair<String, String>> = 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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Session loading screen -->
|
||||
<string name="loadstate_starting">Starting the session…</string>
|
||||
<!-- %1$d minutes, %2$02d seconds since the session started -->
|
||||
<string name="loadstate_elapsed">%1$d:%2$02d elapsed · still working</string>
|
||||
<string name="loadstate_steam_update">Downloading the Steam client update · %1$d%%</string>
|
||||
<!-- %1$s is the client's own download progress, as it prints it -->
|
||||
<string name="loadstate_steam_client">Downloading the Steam client · %1$s</string>
|
||||
|
||||
<!-- Installing GE-Proton / proton-cachyos (%1$s is the tool's name, %2$s its release tag) -->
|
||||
<string name="pextra_busy">A compatibility tool install is already running</string>
|
||||
<string name="pextra_stop_session">Stop the active session before installing compatibility tools</string>
|
||||
<string name="pextra_no_release">Could not find an ARM64 release for %1$s</string>
|
||||
<string name="pextra_no_space">At least 4 GB free plus the download size is required to install %1$s</string>
|
||||
<string name="pextra_downloading">Downloading %1$s %2$s</string>
|
||||
<string name="pextra_retrying">Retrying download · attempt %1$d of %2$d</string>
|
||||
<string name="pextra_interrupted">Download interrupted</string>
|
||||
<string name="pextra_download_failed">Download failed; the partial file is kept so a retry can resume</string>
|
||||
<string name="pextra_too_big">Download exceeded the release size and was deleted</string>
|
||||
<string name="pextra_incomplete">Download was incomplete; the partial file is kept for a resumable retry</string>
|
||||
<string name="pextra_verifying">Verifying download</string>
|
||||
<string name="pextra_checksum_unreadable">Could not read the release checksum; try again</string>
|
||||
<string name="pextra_no_checksum">%1$s %2$s publishes no checksum; nothing was installed</string>
|
||||
<string name="pextra_checksum_mismatch">Checksum mismatch; the download was deleted</string>
|
||||
<string name="pextra_session_started">A session started during the download; stop it before installing compatibility tools</string>
|
||||
<string name="pextra_installing">Installing %1$s</string>
|
||||
<!-- %2$d is the installer's exit status -->
|
||||
<string name="pextra_install_failed">%1$s installation failed (exit %2$d)</string>
|
||||
<string name="pextra_could_not_install">Could not install %1$s</string>
|
||||
<string name="esync_fetching">Fetching droiddeck-esync pack</string>
|
||||
|
||||
<!-- Components (FEX / DXVK / VKD3D-Proton swapped into a Proton). %1$s component, %2$s version, %3$s Proton -->
|
||||
<string name="cmgr_fex_linux_helpers">with Linux helpers</string>
|
||||
<string name="cmgr_fex_dlls_only">DLLs only</string>
|
||||
<string name="cmgr_original_proton_updated">Original (Proton updated since the last swap)</string>
|
||||
<string name="cmgr_changed_outside">Changed outside Components</string>
|
||||
<!-- A component version that could not be found in a Proton -->
|
||||
<string name="cmgr_not_present">not present</string>
|
||||
<!-- %1$s is the Proton build the original files were saved from -->
|
||||
<string name="cmgr_original_version">Original %1$s</string>
|
||||
<string name="cmgr_original_from">Original from %1$s</string>
|
||||
<string name="cmgr_swap_queued">%1$s %2$s goes into %3$s when the game running on it closes.</string>
|
||||
<string name="cmgr_swap_done">%1$s %2$s is now in %3$s. It applies the next time a game starts.</string>
|
||||
<string name="cmgr_original_missing">That original bundle is not stored</string>
|
||||
<!-- %1$s component, %2$s Proton -->
|
||||
<string name="cmgr_restore_queued">%1$s of %2$s goes back to its original when the game running on it closes.</string>
|
||||
<!-- %1$s component, %2$s Proton, %3$s the Proton build the original came from -->
|
||||
<string name="cmgr_restore_done">%1$s of %2$s restored from its %3$s original.</string>
|
||||
<!-- %1$s package file, %2$s Proton id -->
|
||||
<string name="cmgr_package_in_use">%1$s is in use in %2$s. Swap that Proton to something else first.</string>
|
||||
<string name="cmgr_package_not_stored">%1$s is not stored</string>
|
||||
<string name="cmgr_deleted">Deleted %1$s.</string>
|
||||
<string name="cmgr_original_kept">The installed build\'s original is kept: it is the way back.</string>
|
||||
<string name="cmgr_not_stored">Not stored</string>
|
||||
<!-- %1$s Proton build, %2$s component -->
|
||||
<string name="cmgr_deleted_original">Deleted the %1$s original of %2$s.</string>
|
||||
<string name="cmgr_nightlies_only">Downloads come only from the Nightlies releases</string>
|
||||
<string name="cmgr_list_predates_checksums">This package list predates checksums - refresh it and try again</string>
|
||||
<string name="cmgr_checksum_mismatch">Checksum mismatch</string>
|
||||
|
||||
<!-- Display choices -->
|
||||
<string name="sprefs_shape_auto">Auto (16:9+)</string>
|
||||
<string name="sprefs_shape_exact">Match screen</string>
|
||||
<string name="sprefs_shape_wide">Always 16:9</string>
|
||||
<string name="sprefs_upscaler_linear">Linear</string>
|
||||
<string name="sprefs_upscaler_nearest">Nearest</string>
|
||||
<string name="sprefs_upscaler_fsr_fit">AMD FSR 1 (fit)</string>
|
||||
<string name="sprefs_upscaler_gsr_quality">Snapdragon GSR (quality)</string>
|
||||
<string name="sprefs_upscaler_sharpen">Sharpen only</string>
|
||||
<string name="sprefs_lod_bias_auto">Auto (match scaling)</string>
|
||||
|
||||
<!-- Screen effect Looks: preset names and the line under the row -->
|
||||
<string name="look_off_desc">Nothing applied.</string>
|
||||
<string name="look_game_clarity">Game Clarity</string>
|
||||
<string name="look_game_clarity_desc">Sharpening with a touch of contrast - the everyday one.</string>
|
||||
<string name="look_vivid">Vivid</string>
|
||||
<string name="look_vivid_desc">Colour pushed hard. Deband on so skies don\'t stripe.</string>
|
||||
<string name="look_cinematic">Cinematic</string>
|
||||
<string name="look_cinematic_desc">Deeper contrast, cooler colour, edges smoothed.</string>
|
||||
<string name="look_competitive">Competitive</string>
|
||||
<string name="look_competitive_desc">Shadows lifted and heavy sharpening - spot people first.</string>
|
||||
<string name="look_adaptive_sharpen">Adaptive Sharpen</string>
|
||||
<string name="look_adaptive_sharpen_desc">Sharpening only, colour untouched.</string>
|
||||
<string name="look_filmic">Filmic</string>
|
||||
<string name="look_filmic_desc">Film-like curve, slightly muted.</string>
|
||||
<string name="look_arcade">Arcade</string>
|
||||
<string name="look_arcade_desc">Loud and punchy.</string>
|
||||
<string name="look_retro_crt">Retro CRT</string>
|
||||
<string name="look_retro_crt_desc">The CRT tube and analogue signal passes together.</string>
|
||||
<string name="look_upscale_sharp">Upscale Sharp</string>
|
||||
<string name="look_upscale_sharp_desc">For running below panel resolution.</string>
|
||||
<string name="look_pixel_clean">Pixel Clean</string>
|
||||
<string name="look_pixel_clean_desc">Crisp pixels for 2D and old titles.</string>
|
||||
<string name="look_anime_edge">Anime Edge</string>
|
||||
<string name="look_anime_edge_desc">Toon outlines plus sharpening.</string>
|
||||
|
||||
<!-- GPU support line -->
|
||||
<string name="gpuinfo_not_adreno">Not an Adreno GPU</string>
|
||||
<string name="gpuinfo_supported">Supported</string>
|
||||
<string name="gpuinfo_experimental">Experimental: its drivers are test builds</string>
|
||||
<string name="gpuinfo_below_tested">Below tested hardware (Adreno 725 and newer): it may not run</string>
|
||||
<string name="gpuinfo_unsupported">Not supported: DroidDeck needs an Adreno (Snapdragon) GPU</string>
|
||||
|
||||
<!-- Driver downloads and imports -->
|
||||
<string name="turnip_rate_limited">GitHub\'s rate limit was hit - try again later</string>
|
||||
<string name="turnip_github_http">GitHub answered HTTP %1$d</string>
|
||||
<string name="turnip_download_http">download answered HTTP %1$d</string>
|
||||
<string name="turnip_list_predates_checksums">This driver list predates checksums - refresh it and try again</string>
|
||||
<string name="turnip_checksum_mismatch">Checksum mismatch - the download was discarded</string>
|
||||
<!-- %1$s is one of the turnip_reject_* reasons -->
|
||||
<string name="turnip_not_adrenotools">Not an AdrenoTools driver: %1$s</string>
|
||||
<string name="turnip_reject_no_meta">no meta.json</string>
|
||||
<string name="turnip_reject_meta_unreadable">meta.json is unreadable (%1$s)</string>
|
||||
<string name="turnip_reject_linux_driver">this is a Linux runtime driver (import it under \"Linux runtime driver\")</string>
|
||||
<string name="turnip_reject_wayland">this is a Wayland game driver, which this app has no use for</string>
|
||||
<string name="turnip_reject_no_library_name">meta.json names no libraryName, so nothing could be handed to AdrenoTools</string>
|
||||
<!-- %1$s is a library file name -->
|
||||
<string name="turnip_reject_library_missing">meta.json names %1$s, which is not in the zip</string>
|
||||
<string name="turnip_reject_not_aarch64">%1$s is not a 64-bit AArch64 ELF shared library</string>
|
||||
<string name="turnip_reject_glibc">%1$s links glibc - a \"-Linux\" Turnip, which belongs under \"Linux runtime driver\"</string>
|
||||
<string name="lvd_no_library">No libvulkan_freedreno*.so in this zip. An Android (AdrenoTools) or -Wayland Turnip zip is not a Linux runtime driver.</string>
|
||||
<string name="lvd_not_aarch64">%1$s is not a 64-bit AArch64 ELF shared library.</string>
|
||||
<string name="lvd_not_glibc">%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.</string>
|
||||
<string name="bundle_no_manifest">Not a driver bundle: no manifest.json naming an android and a linux driver</string>
|
||||
<string name="bundle_entry_outside">Not a driver bundle: %1$s points outside it</string>
|
||||
<string name="bundle_too_large">Not a driver bundle: it unpacks to more than %1$d MB</string>
|
||||
|
||||
<!-- Game storage -->
|
||||
<string name="gstore_sd_card">SD card</string>
|
||||
<!-- %1$s is a size, like 12.3 GB -->
|
||||
<string name="gstore_free">%1$s free</string>
|
||||
<string name="gstore_folder">Folder</string>
|
||||
|
||||
<string name="logshare_chooser">Share session logs</string>
|
||||
<!-- Stands in for the Steam account name when the client did not record one -->
|
||||
<string name="offline_signed_in">signed in</string>
|
||||
|
||||
<!-- Why the HDR switch is greyed out -->
|
||||
<string name="hdr_needs_android_10">needs Android 10 or newer (display layers with a color space)</string>
|
||||
<string name="hdr_none">this display reports no HDR support</string>
|
||||
<!-- %1$s lists HDR formats, like Dolby Vision, HDR10+ -->
|
||||
<string name="hdr_not_hdr10">this display supports %1$s, not HDR10</string>
|
||||
</resources>
|
||||
Reference in new issue
Block a user