Merge branch 'main' into ui/launcher-refresh
No files matched your search
@@ -1,4 +1,9 @@
|
||||
# DroidDeck
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="artwork/droiddeck-banner-dark.svg">
|
||||
<img alt="DroidDeck" src="artwork/droiddeck-banner-light.svg" width="100%">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
Run Valve's native ARM64 Steam client on an Adreno Android device. Steam runs in a Linux runtime under proot, with gamescope and an in-app Vulkan compositor. Windows games use Valve's ARM64 Proton and FEX. A desktop with LXQt, Firefox, and emulators is also available.
|
||||
|
||||
|
||||
@@ -237,7 +237,9 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
val needRuntime = com.droiddeck.launcher.runtime.LinuxRuntimeInstaller.installedVersion(this) == null
|
||||
val needDesktop = intent.getStringExtra(SessionService.EXTRA_MODE) == SessionService.MODE_DESKTOP &&
|
||||
!com.droiddeck.launcher.runtime.DesktopCatalog.desktopInstalled(this)
|
||||
if (needRuntime || needDesktop) installThenStart(needRuntime, needDesktop)
|
||||
val needProton = (intent.getStringExtra(SessionService.EXTRA_MODE) ?: SessionService.MODE_STEAM) == SessionService.MODE_STEAM &&
|
||||
com.droiddeck.launcher.runtime.DesktopCatalog.protonSeedNeeded(this)
|
||||
if (needRuntime || needDesktop || needProton) installThenStart(needRuntime, needDesktop, needProton)
|
||||
}
|
||||
hud = PerfHud(this)
|
||||
hud.onPresentingWindowChanged = {
|
||||
@@ -542,17 +544,23 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
* First Play (or Desktop) on a fresh install: the runtime, and for the desktop its package,
|
||||
* are downloaded and unpacked here, on the loading screen's own line and bar, and the session
|
||||
* starts when they are in. Nothing else changes.
|
||||
*
|
||||
* The first Steam session also gets Valve's ARM64 Proton here, before the client has ever
|
||||
* started, so the compatibility tool is installed and the default at sign-in with no install
|
||||
* dialog, download or client restart. Not being able to fetch it stops nothing: the session
|
||||
* then asks the client for it after sign-in, as it always has.
|
||||
*/
|
||||
private fun installThenStart(runtime: Boolean, desktop: Boolean) {
|
||||
private fun installThenStart(runtime: Boolean, desktop: Boolean, proton: Boolean = false) {
|
||||
if (SessionState.stopRequested) return
|
||||
installingRuntime = true
|
||||
SessionState.installing = if (runtime) "runtime" else "desktop"
|
||||
SessionState.installing = if (runtime) "runtime" else if (desktop) "desktop" else "proton"
|
||||
SessionEvents.transition(
|
||||
SessionPhase.INSTALLING_RUNTIME,
|
||||
if (runtime) "runtime.installing" else "desktop.installing",
|
||||
"${SessionState.installing}.installing",
|
||||
mapOf("component" to SessionState.installing),
|
||||
)
|
||||
loading.step = if (runtime) "downloading the Linux runtime" else "downloading the desktop"
|
||||
loading.step = if (runtime) "downloading the Linux runtime" else if (desktop) "downloading the desktop"
|
||||
else "downloading Proton Experimental (ARM64)"
|
||||
loading.percent = -1
|
||||
Thread({
|
||||
var failedComponent: String? = null
|
||||
@@ -567,6 +575,13 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
problem = installDesktop()
|
||||
if (problem == null) SessionEvents.record("desktop.ready") else failedComponent = "desktop"
|
||||
}
|
||||
if (problem == null && proton && !SessionState.stopRequested) {
|
||||
SessionState.installing = "proton"
|
||||
SessionEvents.transition(SessionPhase.INSTALLING_RUNTIME, "proton.installing", mapOf("component" to "proton"))
|
||||
val protonProblem = installProton()
|
||||
if (protonProblem == null) SessionEvents.record("proton.ready")
|
||||
else SessionEvents.record("proton.skipped", mapOf("reason" to protonProblem))
|
||||
}
|
||||
uiHandler.post {
|
||||
installingRuntime = false
|
||||
SessionState.installing = null
|
||||
@@ -632,6 +647,15 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
return problem?.let { "The desktop did not install ($it). Check the connection and press Desktop again." }
|
||||
}
|
||||
|
||||
/** Null when the ARM64 Proton is in, else why not; the session goes on either way. */
|
||||
private fun installProton(): String? {
|
||||
uiHandler.post { loading.percent = -1; loading.step = "downloading Proton Experimental (ARM64)" }
|
||||
val catalog = com.droiddeck.launcher.runtime.DesktopCatalog
|
||||
val entry = catalog.fetch(catalog.STEAM_SEED_URL)?.firstOrNull { it.id == catalog.PROTON_SEED_ID }
|
||||
?: return "catalog unreachable"
|
||||
return catalog.install(this, entry, progressFor("Proton Experimental (ARM64)", entry.size / 1_000_000))
|
||||
}
|
||||
|
||||
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||
if (installingRuntime || SessionState.stopRequested) return
|
||||
if (!SessionState.running) {
|
||||
|
||||
@@ -18,6 +18,12 @@ import java.io.File
|
||||
object DesktopCatalog {
|
||||
private const val TAG = "DesktopCatalog"
|
||||
const val CATALOG_URL = "https://raw.githubusercontent.com/The412Banner/winlator-contents/main/desktop.json"
|
||||
/**
|
||||
* Valve's Proton Experimental (ARM64) with its appmanifest, laid out as the client keeps it.
|
||||
* In a catalog of its own so it never shows among the packages a user picks from.
|
||||
*/
|
||||
const val STEAM_SEED_URL = "https://raw.githubusercontent.com/The412Banner/winlator-contents/main/steam-seed.json"
|
||||
const val PROTON_SEED_ID = "proton-arm64"
|
||||
|
||||
class Entry(
|
||||
val id: String, val name: String, val tier: Int, val version: String, val kind: String,
|
||||
@@ -26,8 +32,8 @@ object DesktopCatalog {
|
||||
val icon: String, val category: String,
|
||||
)
|
||||
|
||||
fun fetch(): List<Entry>? {
|
||||
val body = Downloader.downloadString(CATALOG_URL) ?: return null
|
||||
fun fetch(url: String = CATALOG_URL): List<Entry>? {
|
||||
val body = Downloader.downloadString(url) ?: return null
|
||||
return try {
|
||||
val json = JSONObject(body)
|
||||
val arr = json.getJSONArray("packages")
|
||||
@@ -57,6 +63,15 @@ object DesktopCatalog {
|
||||
fun installed(context: Context, id: String): String? =
|
||||
FileUtils.readString(marker(context, id))?.trim()?.takeIf { it.isNotEmpty() }
|
||||
|
||||
/**
|
||||
* True until the ARM64 Proton has been placed once. Its appmanifest counts too, so an install
|
||||
* the client fetched itself is never overwritten; after the first placement the client owns it
|
||||
* and updates it like any other app, and a user who removes it is not given it back.
|
||||
*/
|
||||
fun protonSeedNeeded(context: Context): Boolean =
|
||||
installed(context, PROTON_SEED_ID) == null &&
|
||||
!File(LinuxRuntime.rootDir(context), "root/.local/share/Steam/steamapps/appmanifest_4427310.acf").isFile
|
||||
|
||||
// labwc comes with the hosted desktop package itself (its launcher is staged by the app at
|
||||
// every session, so it cannot tell whether the package is there); SessionFiles uses the same test.
|
||||
fun desktopInstalled(context: Context): Boolean =
|
||||
|
||||
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 367 B After Width: | Height: | Size: 924 B |
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 233 B After Width: | Height: | Size: 530 B |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 508 B After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 4.4 KiB |
|
Before Width: | Height: | Size: 897 B After Width: | Height: | Size: 2.6 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 4.2 KiB |
|
Before Width: | Height: | Size: 8.2 KiB After Width: | Height: | Size: 11 KiB |
@@ -1,5 +1,5 @@
|
||||
# DroidDeck mark
|
||||
|
||||
`droiddeck-mark.svg` is the editable source for the split-D mark. The blue orb is `#1A9FFF`; its halo and the two separator bars are transparent cutouts. The bars are centered on x=63.97, the endpoint of the curved half in the original Steam Deck symbol, and use the halo's 15.83-unit thickness. The mark is optically centered in its view box with a small rightward offset to balance its asymmetric silhouette.
|
||||
`droiddeck-mark.svg` is the editable source for the white split-D mark. The blue orb is `#1A9FFF`; its halo and the two separator bars are transparent cutouts. The bars are centered on x=63.97, the endpoint of the curved half in the original Steam Deck symbol, and use the halo's 15.83-unit thickness. The mark is optically centered in its view box with a small rightward offset to balance its asymmetric silhouette. Place it on black when a background is needed.
|
||||
|
||||
Android currently consumes raster resources for the launcher layers and the Compose logo. The `mipmap-*/ic_launcher_foreground.png` files are density-sized exports of the SVG, with the mark kept inside the adaptive icon safe area. The matching backgrounds are white. `drawable-nodpi/logo.png` composes the same mark on a white circular badge for use on the app's dark surfaces.
|
||||
Android currently consumes raster resources for the launcher layers and the Compose logo. The `mipmap-*/ic_launcher_foreground.png` files are density-sized exports of the SVG, with the mark kept inside the adaptive icon safe area. The matching backgrounds are black. `drawable-nodpi/logo.png` composes the same mark on a black circular badge.
|
||||
@@ -0,0 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="360" viewBox="0 0 1280 360" role="img" aria-labelledby="t" xmlns:c2pa="http://c2pa.org/manifest"><metadata><c2pa:manifest>AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTpjNDEzNmY0YS0xYWE5LTQ4MzEtYTgwNS1iOGJkYTkzOWM3OWMAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaOdgBuqxauxA5Ft/U5O/L/0AAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDo2YTI3MjgxMC1iYjQ3LTQyZmItOWE4ZC1kOGJhM2QxOTg2MzJscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNo7FQMclkxxkOM9r48dH+p5QAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggoYQr3GKI/CBS/tjJfXabQnWGTIXEWN2Q4HY/bRZcAd2kZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaCmdTerrFFCgrujAStdxo3AAAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCDdCEqOYsAzpUmsE8GjJyHfVbPv34OtUVI9Zxt08oNaTWRuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBi3Zmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOmM0MTM2ZjRhLTFhYTktNDgzMS1hODA1LWI4YmRhOTM5Yzc5Yy9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOjVkZjQ2ZTE0LTQzNjQtNGZmZC1hMWVhLTBlNmQ3M2NjMmM0ZXJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCChhCvcYoj8IFL+2Ml9dptCdYZMhcRY3ZDgdj9tFlwB3aJjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggEranykZZVM3ediUytxX7Gk1wpijJyo8W1hbSxY8qGHSiY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggd9tQukbXKUDL9GyrZINf6cuup075WZSflrCFunu5uDB0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALine truncated
|
||||
<title id="t">DroidDeck</title>
|
||||
<defs>
|
||||
<radialGradient id="bg" cx="0.36" cy="0.46" r="0.95" gradientTransform="translate(0.36 0.46) scale(0.48 1) translate(-0.36 -0.46)">
|
||||
<stop offset="0" stop-color="#1B222B"/><stop offset="0.42" stop-color="#0F1419"/><stop offset="1" stop-color="#05070A"/></radialGradient>
|
||||
<radialGradient id="glow"><stop offset="0.35" stop-color="#1A9FFF" stop-opacity="0.17"/><stop offset="1" stop-color="#1A9FFF" stop-opacity="0"/></radialGradient>
|
||||
<clipPath id="card"><rect width="1280" height="360" rx="26"/></clipPath>
|
||||
</defs>
|
||||
<g clip-path="url(#card)">
|
||||
<rect width="1280" height="360" fill="url(#bg)"/>
|
||||
<circle cx="289.75" cy="182.36" r="155.76" fill="none" stroke="#1A9FFF" stroke-opacity="0.091" stroke-width="12"/><circle cx="289.75" cy="182.36" r="155.76" fill="none" stroke="#1A9FFF" stroke-opacity="0.260" stroke-width="2.4"/><circle cx="289.75" cy="182.36" r="241.9" fill="none" stroke="#1A9FFF" stroke-opacity="0.052" stroke-width="9.5"/><circle cx="289.75" cy="182.36" r="241.9" fill="none" stroke="#1A9FFF" stroke-opacity="0.150" stroke-width="1.9"/><circle cx="289.75" cy="182.36" r="348.1" fill="none" stroke="#1A9FFF" stroke-opacity="0.028" stroke-width="7"/><circle cx="289.75" cy="182.36" r="348.1" fill="none" stroke="#1A9FFF" stroke-opacity="0.080" stroke-width="1.4"/><circle cx="289.75" cy="182.36" r="123.9" fill="url(#glow)"/>
|
||||
<g transform="translate(239.135 123.36) scale(0.527445) translate(31.98 0)"><path fill="#FFFFFF" d="M-31.98 0H56.055V40.489A71.81 71.81 0 0 0 56.055 183.231V223.72H-31.98Z"/><path fill="#FFFFFF" d="M71.885 0.28A111.86 111.86 0 0 1 71.885 223.44V183.234A71.81 71.81 0 0 0 71.885 40.486Z"/><circle fill="#1A9FFF" cx="63.98" cy="111.86" r="55.98"/></g><path fill="#FFFFFF" d="M361.45 241.36V160.13H387.02V241.36ZM387.02 196.73 376.32 188.38Q379.5 174.17 387.02 166.31Q394.54 158.46 407.91 158.46Q413.76 158.46 418.19 160.21Q422.62 161.97 425.96 165.65L410.75 184.87Q409.08 183.03 406.57 182.03Q404.07 181.02 400.89 181.02Q394.54 181.02 390.78 184.95Q387.02 188.88 387.02 196.73ZM467.91 243.2Q455.38 243.2 445.27 237.6Q435.15 232 429.3 222.31Q423.45 212.61 423.45 200.58Q423.45 188.54 429.3 179.02Q435.15 169.49 445.18 163.89Q455.21 158.29 467.91 158.29Q480.62 158.29 490.64 163.81Q500.67 169.32 506.52 178.93Q512.37 188.54 512.37 200.58Q512.37 212.61 506.52 222.31Q500.67 232 490.64 237.6Q480.62 243.2 467.91 243.2ZM467.91 219.97Q473.43 219.97 477.61 217.54Q481.79 215.12 484.04 210.69Q486.3 206.26 486.3 200.58Q486.3 194.9 483.96 190.63Q481.62 186.37 477.52 183.95Q473.43 181.52 467.91 181.52Q462.56 181.52 458.39 183.95Q454.21 186.37 451.87 190.72Q449.53 195.06 449.53 200.75Q449.53 206.26 451.87 210.69Q454.21 215.12 458.39 217.54Q462.56 219.97 467.91 219.97ZM523.4 241.36V160.13H548.98V241.36ZM536.27 148.93Q530.26 148.93 526.33 144.84Q522.4 140.74 522.4 134.89Q522.4 128.88 526.33 124.86Q530.26 120.85 536.27 120.85Q542.29 120.85 546.13 124.86Q549.98 128.88 549.98 134.89Q549.98 140.74 546.13 144.84Q542.29 148.93 536.27 148.93ZM599.95 243.03Q588.42 243.03 579.48 237.52Q570.54 232 565.44 222.47Q560.34 212.95 560.34 200.75Q560.34 188.54 565.44 179.02Q570.54 169.49 579.48 163.97Q588.42 158.46 599.95 158.46Q608.31 158.46 615.08 161.63Q621.85 164.81 626.28 170.41Q630.71 176.01 631.21 183.2V217.46Q630.71 224.65 626.36 230.41Q622.02 236.18 615.16 239.61Q608.31 243.03 599.95 243.03ZM604.47 219.97Q609.98 219.97 613.99 217.54Q618 215.12 620.34 210.77Q622.68 206.43 622.68 200.75Q622.68 195.06 620.43 190.8Q618.17 186.54 614.08 184.03Q609.98 181.52 604.63 181.52Q599.28 181.52 595.19 184.03Q591.09 186.54 588.67 190.88Q586.25 195.23 586.25 200.75Q586.25 206.26 588.59 210.61Q590.93 214.95 595.11 217.46Q599.28 219.97 604.47 219.97ZM646.75 241.36H621.68V219.46L625.53 199.74L621.18 180.02V120.02H646.75ZM683.19 241.36V218.29H712.44Q723.13 218.29 731.16 214.03Q739.18 209.77 743.53 201.58Q747.87 193.39 747.87 182.19Q747.87 170.99 743.44 162.97Q739.01 154.95 731.07 150.6Q723.13 146.26 712.44 146.26H682.35V123.36H712.77Q726.14 123.36 737.42 127.62Q748.71 131.88 757.15 139.82Q765.59 147.76 770.18 158.54Q774.78 169.32 774.78 182.36Q774.78 195.23 770.18 206.09Q765.59 216.96 757.23 224.81Q748.87 232.67 737.59 237.01Q726.31 241.36 713.11 241.36ZM665.47 241.36V123.36H691.71V241.36ZM827.6 243.2Q814.39 243.2 804.11 237.77Q793.83 232.33 787.98 222.64Q782.13 212.95 782.13 200.75Q782.13 188.54 787.9 178.93Q793.67 169.32 803.53 163.81Q813.39 158.29 825.76 158.29Q837.79 158.29 846.98 163.47Q856.18 168.65 861.44 177.85Q866.71 187.04 866.71 198.91Q866.71 201.08 866.46 203.5Q866.21 205.93 865.54 209.1L795.51 209.27V191.72L854.67 191.55L843.64 198.91Q843.47 191.89 841.47 187.29Q839.46 182.69 835.54 180.27Q831.61 177.85 825.92 177.85Q819.91 177.85 815.48 180.61Q811.05 183.36 808.63 188.38Q806.2 193.39 806.2 200.58Q806.2 207.77 808.79 212.86Q811.38 217.96 816.15 220.72Q820.91 223.48 827.43 223.48Q833.45 223.48 838.29 221.39Q843.14 219.3 846.82 215.12L860.86 229.16Q854.84 236.18 846.32 239.69Q837.79 243.2 827.6 243.2ZM916.68 243.2Q904.15 243.2 893.95 237.68Q883.75 232.17 877.9 222.47Q872.05 212.78 872.05 200.75Q872.05 188.54 877.99 178.93Q883.92 169.32 894.12 163.81Q904.31 158.29 917.02 158.29Q926.54 158.29 934.48 161.55Q942.42 164.81 948.6 171.33L932.22 187.71Q929.38 184.53 925.62 183.03Q921.86 181.52 917.02 181.52Q911.5 181.52 907.24 183.95Q902.98 186.37 900.55 190.63Q898.13 194.9 898.13 200.58Q898.13 206.26 900.55 210.61Q902.98 214.95 907.32 217.46Q911.67 219.97 917.02 219.97Q922.03 219.97 925.96 218.21Q929.88 216.46 932.73 213.28L948.94 229.66Q942.59 236.35 934.56 239.77Q926.54 243.2 916.68 243.2ZM1010.61 241.36 982.2 199.24 1010.45 160.13H1039.03L1004.93 204.09L1005.77 193.56L1040.7 241.36ZM958.63 241.36V120.02H984.21V241.36Z"/>
|
||||
|
||||
</g>
|
||||
<rect x="0.5" y="0.5" width="1279" height="359" rx="25.5" fill="none" stroke="rgba(255,255,255,0.07)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -0,0 +1,16 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="360" viewBox="0 0 1280 360" role="img" aria-labelledby="t" xmlns:c2pa="http://c2pa.org/manifest"><metadata><c2pa:manifest>AAAWgmp1bWIAAAAeanVtZGMycGEAEQAQgAAAqgA4m3EDYzJwYQAAABZcanVtYgAAAEdqdW1kYzJtYQARABCAAACqADibcQN1cm46YzJwYTozODE2MThiZi02NDJmLTQ2MjYtODBmOS0zODNkMTg1MTg1YzYAAAADl2p1bWIAAAApanVtZGMyYXMAEQAQgAAAqgA4m3EDYzJwYS5hc3NlcnRpb25zAAAAALxqdW1iAAAARGp1bWRjYm9yABEAEIAAAKoAOJtxE2MycGEuaW5ncmVkaWVudC52MwAAAAAYYzJzaNJTd2RbvEwJ3CejXuiiUa8AAABwY2JvcqNpZGM6Zm9ybWF0bWltYWdlL3N2Zyt4bWxqaW5zdGFuY2VJRHgseG1wOmlpZDpiZWIyNDdlNC0zMGYzLTQwNTMtOGRiMy02YjM5ZDFiMDE5MjVscmVsYXRpb25zaGlwaHBhcmVudE9mAAAB4mp1bWIAAABBanVtZGNib3IAEQAQgAAAqgA4m3ETYzJwYS5hY3Rpb25zLnYyAAAAABhjMnNoH5LUfnG8pzZTvyp93dxAjAAAAZljYm9yomdhY3Rpb25zgqJmYWN0aW9ua2MycGEub3BlbmVkanBhcmFtZXRlcnOha2luZ3JlZGllbnRzgaJjdXJseC1zZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmluZ3JlZGllbnQudjNkaGFzaFggp0bIyQE4HprwqtllS1/PuFew+dPPCPuezvtPD9UACASkZmFjdGlvbngdY29tLmFudGhyb3BpYy5jbGF1ZGUucHJvdmlkZWRqcGFyYW1ldGVyc6F4H2NvbS5hbnRocm9waWMub3JpZ2luLWNvbmZpZGVuY2VndW5rbm93bmtkZXNjcmlwdGlvbnhmQ2xhdWRlIHByb3ZpZGVkIHRoaXMgZmlsZSBhdCB0aGUgcmVxdWVzdCBvZiBhIHVzZXIgYW5kIG1heSBoYXZlIGNyZWF0ZWQgb3IgbW9kaWZpZWQgdGhlIGZpbGUgY29udGVudHMubXNvZnR3YXJlQWdlbnShZG5hbWVmQ2xhdWRlcmFsbEFjdGlvbnNJbmNsdWRlZPUAAADIanVtYgAAAEBqdW1kY2JvcgARABCAAACqADibcRNjMnBhLmhhc2guZGF0YQAAAAAYYzJzaCjAw2/dKHURaCMsIpPL4O0AAACAY2JvcqVjYWxnZnNoYTI1NmNwYWRNAAAAAAAAAAAAAAAAAGRoYXNoWCCEQ7KUH92rkmFULm/paDTTakKh2Kq0tGvmqok6E4sdumRuYW1lbmp1bWJmIG1hbmlmZXN0amV4Y2x1c2lvbnOBomVzdGFydBi3Zmxlbmd0aBkeBAAAAj5qdW1iAAAAJ2p1bWRjMmNsABEAEIAAAKoAOJtxA2MycGEuY2xhaW0udjIAAAACD2Nib3KlY2FsZ2ZzaGEyNTZpc2lnbmF0dXJleE1zZWxmI2p1bWJmPS9jMnBhL3VybjpjMnBhOjM4MTYxOGJmLTY0MmYtNDYyNi04MGY5LTM4M2QxODUxODVjNi9jMnBhLnNpZ25hdHVyZWppbnN0YW5jZUlEeCx4bXA6aWlkOjM4NDAyMDFkLWVjMTEtNGExYS1iYWU3LTFjYzgzNzUwMzY5YnJjcmVhdGVkX2Fzc2VydGlvbnODomN1cmx4LXNlbGYjanVtYmY9YzJwYS5hc3NlcnRpb25zL2MycGEuaW5ncmVkaWVudC52M2RoYXNoWCCnRsjJATgemvCq2WVLX8+4V7D5088I+57O+08P1QAIBKJjdXJseCpzZWxmI2p1bWJmPWMycGEuYXNzZXJ0aW9ucy9jMnBhLmFjdGlvbnMudjJkaGFzaFggg3t3BJoakmzmieZvRQ5KFPJxvI/zSgQVZG0rhdu4JumiY3VybHgpc2VsZiNqdW1iZj1jMnBhLmFzc2VydGlvbnMvYzJwYS5oYXNoLmRhdGFkaGFzaFggZjySb2zXGJyOOu+/uDpQ8CRteSnMgEk64l3gBu0UM5t0Y2xhaW1fZ2VuZXJhdG9yX2luZm+jZG5hbWVvQW50aHJvcGljIEZpbGVzZ3ZlcnNpb25lMS4wLjBrc3BlY1ZlcnNpb25lMi40LjAAABA4anVtYgAAAChqdW1kYzJjcwARABCAAACqADibcQNjMnBhLnNpZ25hdHVyZQAAABAIY2JvctKEWQISogEmGCFZAgowggIGMIIBjaADAgECAhRA5aAK7sI50L64g/oGQgU9Z1UTADAKBggqhkjOPQQDAzBJMRcwFQYDVQQKEw5BbnRocm9waWMsIFBCQzEuMCwGA1UEAxMlQW50aHJvcGljIENvbnRlbnQgQ3JlZGVudGlhbHMgUm9vdCBDQTAeFw0yNjA4MDcxODQzNTZaFw0yODA4MDYxOTQzNTZaMEQxFzAVBgNVBAoTDkFudGhyb3BpYywgUEJDMSkwJwYDVQQDEyBBbnRocm9waWMgQ2xhdWRlIENvbnRlbnQgU2lnbmluZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJh6CmvLUBgFFNU0vUKlOVtE6djd17L5SuwX0LemFisBM3dkd/3cyjxFA3Qo5S46fX0/ihY0VZ7mfb9KF703t5OjWDBWMA4GA1UdDwEB/wQEAwIHgDAVBgNVHSUEDjAMBgorBgEEAYPoXgIBMAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUzlHiBIFOZFsj+OPEz5o+nMHXXMIwCgYIKoZIzj0EAwMDZwAwZAIwMXMdFJ4BetLLVY7ORuE9noqbbAZOZn/aArXyTwFAZfKrPzxF2vPoJNf1+UCdg1XGAjBwX1zd9WGqYkqmL5SFqw1QySjr1zJfpJM9+1rdDwSPLMOPOjKuiXjoU/pUUeG9RwmhY3BhZFkNngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAALine truncated
|
||||
<title id="t">DroidDeck</title>
|
||||
<defs>
|
||||
<radialGradient id="bg" cx="0.36" cy="0.46" r="0.95" gradientTransform="translate(0.36 0.46) scale(0.48 1) translate(-0.36 -0.46)">
|
||||
<stop offset="0" stop-color="#F4F8FB"/><stop offset="0.42" stop-color="#E4ECF2"/><stop offset="1" stop-color="#CAD7E2"/></radialGradient>
|
||||
<radialGradient id="glow"><stop offset="0.35" stop-color="#1A9FFF" stop-opacity="0.0"/><stop offset="1" stop-color="#1A9FFF" stop-opacity="0"/></radialGradient>
|
||||
<clipPath id="card"><rect width="1280" height="360" rx="26"/></clipPath>
|
||||
</defs>
|
||||
<g clip-path="url(#card)">
|
||||
<rect width="1280" height="360" fill="url(#bg)"/>
|
||||
<circle cx="289.75" cy="182.36" r="155.76" fill="none" stroke="#1A9FFF" stroke-opacity="0.300" stroke-width="2.4"/><circle cx="289.75" cy="182.36" r="241.9" fill="none" stroke="#1A9FFF" stroke-opacity="0.170" stroke-width="1.9"/><circle cx="289.75" cy="182.36" r="348.1" fill="none" stroke="#1A9FFF" stroke-opacity="0.090" stroke-width="1.4"/>
|
||||
<g transform="translate(239.135 123.36) scale(0.527445) translate(31.98 0)"><path fill="#1B1B1B" d="M-31.98 0H56.055V40.489A71.81 71.81 0 0 0 56.055 183.231V223.72H-31.98Z"/><path fill="#1B1B1B" d="M71.885 0.28A111.86 111.86 0 0 1 71.885 223.44V183.234A71.81 71.81 0 0 0 71.885 40.486Z"/><circle fill="#1A9FFF" cx="63.98" cy="111.86" r="55.98"/></g><path fill="#1B1B1B" d="M361.45 241.36V160.13H387.02V241.36ZM387.02 196.73 376.32 188.38Q379.5 174.17 387.02 166.31Q394.54 158.46 407.91 158.46Q413.76 158.46 418.19 160.21Q422.62 161.97 425.96 165.65L410.75 184.87Q409.08 183.03 406.57 182.03Q404.07 181.02 400.89 181.02Q394.54 181.02 390.78 184.95Q387.02 188.88 387.02 196.73ZM467.91 243.2Q455.38 243.2 445.27 237.6Q435.15 232 429.3 222.31Q423.45 212.61 423.45 200.58Q423.45 188.54 429.3 179.02Q435.15 169.49 445.18 163.89Q455.21 158.29 467.91 158.29Q480.62 158.29 490.64 163.81Q500.67 169.32 506.52 178.93Q512.37 188.54 512.37 200.58Q512.37 212.61 506.52 222.31Q500.67 232 490.64 237.6Q480.62 243.2 467.91 243.2ZM467.91 219.97Q473.43 219.97 477.61 217.54Q481.79 215.12 484.04 210.69Q486.3 206.26 486.3 200.58Q486.3 194.9 483.96 190.63Q481.62 186.37 477.52 183.95Q473.43 181.52 467.91 181.52Q462.56 181.52 458.39 183.95Q454.21 186.37 451.87 190.72Q449.53 195.06 449.53 200.75Q449.53 206.26 451.87 210.69Q454.21 215.12 458.39 217.54Q462.56 219.97 467.91 219.97ZM523.4 241.36V160.13H548.98V241.36ZM536.27 148.93Q530.26 148.93 526.33 144.84Q522.4 140.74 522.4 134.89Q522.4 128.88 526.33 124.86Q530.26 120.85 536.27 120.85Q542.29 120.85 546.13 124.86Q549.98 128.88 549.98 134.89Q549.98 140.74 546.13 144.84Q542.29 148.93 536.27 148.93ZM599.95 243.03Q588.42 243.03 579.48 237.52Q570.54 232 565.44 222.47Q560.34 212.95 560.34 200.75Q560.34 188.54 565.44 179.02Q570.54 169.49 579.48 163.97Q588.42 158.46 599.95 158.46Q608.31 158.46 615.08 161.63Q621.85 164.81 626.28 170.41Q630.71 176.01 631.21 183.2V217.46Q630.71 224.65 626.36 230.41Q622.02 236.18 615.16 239.61Q608.31 243.03 599.95 243.03ZM604.47 219.97Q609.98 219.97 613.99 217.54Q618 215.12 620.34 210.77Q622.68 206.43 622.68 200.75Q622.68 195.06 620.43 190.8Q618.17 186.54 614.08 184.03Q609.98 181.52 604.63 181.52Q599.28 181.52 595.19 184.03Q591.09 186.54 588.67 190.88Q586.25 195.23 586.25 200.75Q586.25 206.26 588.59 210.61Q590.93 214.95 595.11 217.46Q599.28 219.97 604.47 219.97ZM646.75 241.36H621.68V219.46L625.53 199.74L621.18 180.02V120.02H646.75ZM683.19 241.36V218.29H712.44Q723.13 218.29 731.16 214.03Q739.18 209.77 743.53 201.58Q747.87 193.39 747.87 182.19Q747.87 170.99 743.44 162.97Q739.01 154.95 731.07 150.6Q723.13 146.26 712.44 146.26H682.35V123.36H712.77Q726.14 123.36 737.42 127.62Q748.71 131.88 757.15 139.82Q765.59 147.76 770.18 158.54Q774.78 169.32 774.78 182.36Q774.78 195.23 770.18 206.09Q765.59 216.96 757.23 224.81Q748.87 232.67 737.59 237.01Q726.31 241.36 713.11 241.36ZM665.47 241.36V123.36H691.71V241.36ZM827.6 243.2Q814.39 243.2 804.11 237.77Q793.83 232.33 787.98 222.64Q782.13 212.95 782.13 200.75Q782.13 188.54 787.9 178.93Q793.67 169.32 803.53 163.81Q813.39 158.29 825.76 158.29Q837.79 158.29 846.98 163.47Q856.18 168.65 861.44 177.85Q866.71 187.04 866.71 198.91Q866.71 201.08 866.46 203.5Q866.21 205.93 865.54 209.1L795.51 209.27V191.72L854.67 191.55L843.64 198.91Q843.47 191.89 841.47 187.29Q839.46 182.69 835.54 180.27Q831.61 177.85 825.92 177.85Q819.91 177.85 815.48 180.61Q811.05 183.36 808.63 188.38Q806.2 193.39 806.2 200.58Q806.2 207.77 808.79 212.86Q811.38 217.96 816.15 220.72Q820.91 223.48 827.43 223.48Q833.45 223.48 838.29 221.39Q843.14 219.3 846.82 215.12L860.86 229.16Q854.84 236.18 846.32 239.69Q837.79 243.2 827.6 243.2ZM916.68 243.2Q904.15 243.2 893.95 237.68Q883.75 232.17 877.9 222.47Q872.05 212.78 872.05 200.75Q872.05 188.54 877.99 178.93Q883.92 169.32 894.12 163.81Q904.31 158.29 917.02 158.29Q926.54 158.29 934.48 161.55Q942.42 164.81 948.6 171.33L932.22 187.71Q929.38 184.53 925.62 183.03Q921.86 181.52 917.02 181.52Q911.5 181.52 907.24 183.95Q902.98 186.37 900.55 190.63Q898.13 194.9 898.13 200.58Q898.13 206.26 900.55 210.61Q902.98 214.95 907.32 217.46Q911.67 219.97 917.02 219.97Q922.03 219.97 925.96 218.21Q929.88 216.46 932.73 213.28L948.94 229.66Q942.59 236.35 934.56 239.77Q926.54 243.2 916.68 243.2ZM1010.61 241.36 982.2 199.24 1010.45 160.13H1039.03L1004.93 204.09L1005.77 193.56L1040.7 241.36ZM958.63 241.36V120.02H984.21V241.36Z"/>
|
||||
|
||||
</g>
|
||||
<rect x="0.5" y="0.5" width="1279" height="359" rx="25.5" fill="none" stroke="rgba(19,26,33,0.08)"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 13 KiB |
@@ -8,6 +8,6 @@
|
||||
<rect x="56.055" y="178.72" width="15.83" height="45" fill="#000"/>
|
||||
</mask>
|
||||
</defs>
|
||||
<path fill="#1b1b1b" mask="url(#negative-space)" d="M-31.98 0H63.97c61.84 0 111.96 50.08 111.96 111.86S125.81 223.72 63.97 223.72H-31.98V0Z"/>
|
||||
<path fill="#FFFFFF" mask="url(#negative-space)" d="M-31.98 0H63.97c61.84 0 111.96 50.08 111.96 111.86S125.81 223.72 63.97 223.72H-31.98V0Z"/>
|
||||
<circle cx="63.98" cy="111.86" r="55.98" fill="#1A9FFF"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 826 B After Width: | Height: | Size: 826 B |
|
After Width: | Height: | Size: 268 KiB |
@@ -312,18 +312,35 @@ case $mode in
|
||||
# in yet - so the install dialog only appeared after the user restarted the client by hand.
|
||||
# With no remembered sign-in, ask again once the sign-in has happened: the client binary
|
||||
# started a second time with a URL hands it to the running instance and exits.
|
||||
#
|
||||
# The request is also dropped while the client is still in its post-logon compat phase (it
|
||||
# probes every registered tool then): with publicbeta 1790380355 that phase took 27 s and a
|
||||
# request sent 15 s after sign-in vanished (2026-09-26, no dialog; 1790121765 was done in
|
||||
# time). So wait for the client to say the phase is over ("Waiting for compat in post-logon
|
||||
# took" in its console log; at most 90 s if it never does), then ask - and ask again, twice
|
||||
# at most, if no install has started a minute later.
|
||||
if ! grep -q '"RememberPassword"[[:space:]]*"1"' "$steam_root/config/loginusers.vdf" 2>/dev/null; then
|
||||
(
|
||||
_bl_acf="$steam_root/steamapps/appmanifest_4427310.acf"
|
||||
_bl_conn="$steam_root/logs/connection_log.txt"
|
||||
_bl_cons="$steam_root/logs/console_log.txt"
|
||||
_bl_from=$(stat -c %s "$_bl_conn" 2>/dev/null || echo 0)
|
||||
_bl_cfrom=$(stat -c %s "$_bl_cons" 2>/dev/null || echo 0)
|
||||
for _ in $(seq 1 180); do
|
||||
sleep 10
|
||||
[ -f "$steam_root/steamapps/appmanifest_4427310.acf" ] && exit 0
|
||||
[ -f "$_bl_acf" ] && exit 0
|
||||
if tail -c +$((_bl_from + 1)) "$_bl_conn" 2>/dev/null | grep -q "RecvMsgClientLogOnResponse() : processing complete"; then
|
||||
sleep 15
|
||||
[ -f "$steam_root/steamapps/appmanifest_4427310.acf" ] && exit 0
|
||||
step "signed in: asking the client again for the ARM64 Proton"
|
||||
"$steam_root/steamrtarm64/steam" steam://install/4427310 >/dev/null 2>&1 || true
|
||||
for _ in $(seq 1 45); do
|
||||
tail -c +$((_bl_cfrom + 1)) "$_bl_cons" 2>/dev/null | grep -q "Waiting for compat in post-logon took" && break
|
||||
sleep 2
|
||||
done
|
||||
for _bl_try in 1 2 3; do
|
||||
sleep 10
|
||||
[ -f "$_bl_acf" ] && exit 0
|
||||
step "signed in: asking the client again for the ARM64 Proton (try $_bl_try of 3)"
|
||||
"$steam_root/steamrtarm64/steam" steam://install/4427310 >/dev/null 2>&1 || true
|
||||
sleep 50
|
||||
done
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include <sys/types.h>
|
||||
|
||||
int bl_status_without_tracer(const char *path, int flags) __attribute__((visibility("hidden")));
|
||||
int bl_writable_retry(int dirfd, const char *path, int flags) __attribute__((visibility("hidden")));
|
||||
|
||||
static int retry_without_noatime(int fd, int flags) {
|
||||
return fd < 0 && errno == EPERM && (flags & O_NOATIME);
|
||||
@@ -67,6 +68,8 @@ int open(const char *path, int flags, ...) {
|
||||
}
|
||||
fd = real_open(path, flags, mode);
|
||||
if (retry_without_noatime(fd, flags)) fd = real_open(path, flags & ~O_NOATIME, mode);
|
||||
/* A read-only file the Steam client rewrites (perms.c). */
|
||||
if (fd < 0 && bl_writable_retry(AT_FDCWD, path, flags)) fd = real_open(path, flags & ~O_NOATIME, mode);
|
||||
return fd;
|
||||
}
|
||||
|
||||
@@ -91,6 +94,7 @@ int openat(int dirfd, const char *path, int flags, ...) {
|
||||
}
|
||||
fd = real_openat(dirfd, path, flags, mode);
|
||||
if (retry_without_noatime(fd, flags)) fd = real_openat(dirfd, path, flags & ~O_NOATIME, mode);
|
||||
if (fd < 0 && bl_writable_retry(dirfd, path, flags)) fd = real_openat(dirfd, path, flags & ~O_NOATIME, mode);
|
||||
return fd;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/*
|
||||
* Root's way past a read-only file, for the Steam client.
|
||||
*
|
||||
* The client runs as "root" inside the session - the rootfs names the app's own uid root, and
|
||||
* proot (-i) shows it that uid - but the kernel treats it as the ordinary user it is. The client
|
||||
* leaves many content files read-only (r-x------,
|
||||
* ~290 files of Proton Experimental (ARM64) alone), and when an update later rewrites one of them
|
||||
* it opens the file for writing expecting root's permission to override the mode. The kernel
|
||||
* refuses (EACCES), the client reports "Missing file permissions" and cancels the update
|
||||
* (2026-09-26: Proton Experimental (ARM64), "CFileWriter: errno: 13 ... wineopenxr.dll").
|
||||
*
|
||||
* So for the client's own processes, a refused write-open of a file this user owns is answered the
|
||||
* way root's would be: the owner's write bit is added - to the file, or to the directory a new
|
||||
* file is being created in - and the open is tried once more. chmod only succeeds on what the app
|
||||
* owns, so nothing it could not already change is touched. Games, Wine and everything else keep
|
||||
* ordinary permissions: only the processes started from the client's own directories take this.
|
||||
*/
|
||||
#define _GNU_SOURCE
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <limits.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
static int in_steam_dirs(const char *path) {
|
||||
return path != NULL && (strstr(path, "/Steam/steamrtarm64/") != NULL
|
||||
|| strstr(path, "/Steam/ubuntu12_64/") != NULL || strstr(path, "/Steam/linuxarm64/") != NULL);
|
||||
}
|
||||
|
||||
/*
|
||||
* The client and its helpers run from the Steam root's platform directories. The kernel's
|
||||
* /proc/self/exe names proot's loader for every process in the session, so the program's own
|
||||
* name - the path it was started by - is asked as well.
|
||||
*/
|
||||
static int is_steam_client(void) {
|
||||
static int cached = -1;
|
||||
char exe[PATH_MAX];
|
||||
ssize_t n;
|
||||
|
||||
if (cached >= 0) return cached;
|
||||
n = readlink("/proc/self/exe", exe, sizeof(exe) - 1);
|
||||
if (n > 0) exe[n] = '\0';
|
||||
cached = (n > 0 && in_steam_dirs(exe)) || in_steam_dirs(program_invocation_name);
|
||||
return cached;
|
||||
}
|
||||
|
||||
static int grant_owner_write(int dirfd, const char *path) {
|
||||
struct stat st;
|
||||
|
||||
if (fstatat(dirfd, path, &st, 0) != 0 || (st.st_mode & S_IWUSR)) return 0;
|
||||
return fchmodat(dirfd, path, (st.st_mode & 07777) | S_IWUSR, 0) == 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* After an open refused with EACCES: returns 1 when a write bit was added and the open is worth
|
||||
* one more try. errno is left as the open set it.
|
||||
*/
|
||||
__attribute__((visibility("hidden")))
|
||||
int bl_writable_retry(int dirfd, const char *path, int flags) {
|
||||
int saved = errno;
|
||||
int granted = 0;
|
||||
char parent[PATH_MAX];
|
||||
const char *slash;
|
||||
|
||||
if (saved != EACCES || path == NULL || (flags & O_ACCMODE) == O_RDONLY || !is_steam_client()) return 0;
|
||||
if (faccessat(dirfd, path, F_OK, 0) == 0) {
|
||||
granted = grant_owner_write(dirfd, path);
|
||||
} else if (flags & O_CREAT) {
|
||||
/* A new file in a read-only directory. */
|
||||
slash = strrchr(path, '/');
|
||||
if (slash == NULL) {
|
||||
granted = grant_owner_write(dirfd, ".");
|
||||
} else if ((size_t) (slash - path) < sizeof(parent)) {
|
||||
memcpy(parent, path, slash - path);
|
||||
parent[slash - path] = '\0';
|
||||
granted = grant_owner_write(dirfd, parent[0] ? parent : "/");
|
||||
}
|
||||
}
|
||||
errno = saved;
|
||||
return granted;
|
||||
}
|
||||