Merge pull request #141 from Droid-Deck/release/0.3.0

Release 0.3.0: version bump, Deck mode for everyone, release notes and media, Updates page fix
This commit is contained in:
Kurt Himebauch authored and GitHub committed 2026-10-02 01:39:31 -04:00
commit 8a547c9d5c
19 files changed
+196 -12

No files matched your search

+56
View File
@@ -62,3 +62,59 @@ jobs:
label: ${{ inputs.version }}
artifact: droiddeck-release
secrets: inherit
# The release is made here, from the apks this run signed, so it traces back to its Build APK run.
publish:
name: publish the release
needs: [find, sign]
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: write
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: actions/download-artifact@v4
with:
pattern: droiddeck-release-*
merge-multiple: true
path: out
- name: Publish
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ inputs.version }}
BUILD_RUN: ${{ needs.find.outputs.run_id }}
run: |
set -euo pipefail
notes="docs/releases/$VERSION.md"
[ -f "$notes" ] || { echo "::error::$notes is missing: add the release notes first"; exit 1; }
if gh release view "$VERSION" -R "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "::error::release $VERSION already exists"; exit 1
fi
# The notes' first line is the title; media paths become raw URLs at this release's tag.
title=$(sed -n '1s/^# *//p' "$notes")
[ -n "$title" ] || title="DroidDeck $VERSION"
raw="https://raw.githubusercontent.com/$GITHUB_REPOSITORY/$VERSION/docs/releases/media/"
{
sed '1{/^# /d;}' "$notes" | sed -E "s#(\]\(|src=\")media/#\1$raw#g"
echo
echo "---"
echo
echo "Built by [Build APK run $BUILD_RUN]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$BUILD_RUN) from \`$GITHUB_SHA\`, signed and published by [Release APKs run $GITHUB_RUN_ID]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)."
} > "$RUNNER_TEMP/body.md"
( cd out && cat SHA256SUMS-*.txt | sort -k2 > SHA256SUMS.txt && rm SHA256SUMS-*.txt && cat SHA256SUMS.txt )
gh release create "$VERSION" -R "$GITHUB_REPOSITORY" out/*.apk out/SHA256SUMS.txt \
--target "$GITHUB_SHA" --title "$title" --notes-file "$RUNNER_TEMP/body.md" --latest
echo "Published [$title]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/releases/tag/$VERSION)" >> "$GITHUB_STEP_SUMMARY"
# A release made with this run's token fires no release event, so the in-app catalog is refreshed here.
catalog:
needs: publish
uses: ./.github/workflows/update-catalog.yml
permissions:
contents: read
pull-requests: read
secrets: inherit
+2 -2
View File
@@ -79,8 +79,8 @@ android {
// file out of its own data directory, and this app's entire job is to exec a rootfs:
// proot, gamescope, the Steam client and everything it launches all live under files/.
targetSdkVersion 28
versionCode 9
versionName "0.2.0"
versionCode 10
versionName "0.3.0"
buildConfigField "String", "BUILD_LABEL", "\"${escapedBuildLabel}\""
buildConfigField "String", "SOURCE_COMMIT", "\"${sourceCommit}\""
buildConfigField "int", "PR_NUMBER", "${prNumber}"
@@ -14,7 +14,7 @@ class App : Application() {
override fun onCreate() {
super.onCreate()
CrashHandler.install(this)
// Before anything else touches the session prefs: a new install's first read decides it.
// Before anything else touches the session prefs, so the first session already runs in Deck mode.
com.droiddeck.launcher.session.SessionPrefs.settleDeckModeDefault(this)
com.droiddeck.launcher.session.SessionPrefs.settleGpuDriverMode(this)
com.droiddeck.launcher.update.AppUpdates.init(this)
@@ -357,21 +357,20 @@ object SessionPrefs {
prefs(context).getString("steamController", CONTROLLER_DECK) ?: CONTROLLER_DECK
fun setSteamController(context: Context, id: String) { prefs(context).edit().putString("steamController", id).apply() }
/** Runs the SteamOS gamepad client with its Quick Access performance controls. On for new installs (settleDeckModeDefault). */
/** Runs the SteamOS gamepad client with its Quick Access performance controls. On by default (settleDeckModeDefault). */
fun steamDeckMode(context: Context): Boolean = prefs(context).getBoolean("steamDeckMode", true)
fun setSteamDeckMode(context: Context, on: Boolean) { prefs(context).edit().putBoolean("steamDeckMode", on).apply() }
/**
* Deck mode became the default for new installs; an install from before keeps what it ran with
* (off), so an update never changes its interface or restarts the client on its own. Run once at
* process start, before anything reads or writes these prefs: a new install has neither prefs
* nor a runtime yet. The answer is written down, so it is decided once.
* Deck mode is the default: every install, new or from before, is moved to it once. An install
* from before 0.3.0 had off written down for it whether or not anyone chose it, so the move
* can't tell a choice from that default and moves everyone; turning it off afterwards sticks.
* Run once at process start, before anything reads or writes these prefs.
*/
fun settleDeckModeDefault(context: Context) {
val p = prefs(context)
if (p.contains("steamDeckMode")) return
val existing = p.all.isNotEmpty() || java.io.File(context.filesDir, "linuxfs").exists()
p.edit().putBoolean("steamDeckMode", !existing).apply()
if (p.getBoolean("deckModeMoved", false)) return
p.edit().putBoolean("steamDeckMode", true).putBoolean("deckModeMoved", true).apply()
}
/**
@@ -156,6 +156,7 @@ private fun StatusPanel(s: FrontEndState, u: UpdatesState, ua: UpdatesActions, m
!me.updatable -> Look(colors.onSurfaceVariant, stringResource(R.string.upd_signed_status), stringResource(R.string.upd_signed_headline),
stringResource(R.string.upd_signed_detail))
catalog == null -> Look(colors.onSurfaceVariant, if (u.checking) stringResource(R.string.common_checking) else stringResource(R.string.upd_not_checked), stringResource(R.string.upd_title), null)
offered && installBlock != null -> Look(colors.onSurfaceVariant, stringResource(R.string.upd_blocked_status), newBuild(u.follow, release!!), null)
offer == Offer.UPDATE -> Look(AttentionAmber, stringResource(R.string.upd_available), newBuild(u.follow, release!!), null)
offer == Offer.SWITCH -> Look(pal.signal, stringResource(R.string.upd_switch), newBuild(u.follow, release!!), null)
offer == Offer.AHEAD -> Look(pal.signal, stringResource(R.string.upd_ahead_status), stringResource(R.string.upd_ahead_headline),
@@ -177,7 +178,7 @@ private fun StatusPanel(s: FrontEndState, u: UpdatesState, ua: UpdatesActions, m
ReleaseNotes(changeTitle(release!!.title.ifBlank { release.tag }), release.summary, release.tag)
}
if (look.detail != null) Text(look.detail, fontSize = 14.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(top = 10.dp))
if (installBlock != null && (offered || offer == Offer.AHEAD)) {
if (installBlock != null && offered) {
Text(installBlock, fontSize = 13.sp, color = AttentionAmber, modifier = Modifier.padding(top = 10.dp))
}
if (u.error != null) Text(u.error, fontSize = 13.sp, color = pal.error, modifier = Modifier.padding(top = 12.dp))
@@ -190,6 +191,7 @@ private fun StatusPanel(s: FrontEndState, u: UpdatesState, ua: UpdatesActions, m
if (u.percent >= 0) LinearProgressIndicator(progress = { u.percent / 100f }, modifier = Modifier.fillMaxWidth().height(6.dp).clip(CircleShape))
else LinearProgressIndicator(modifier = Modifier.fillMaxWidth().height(6.dp).clip(CircleShape))
}
offered && installBlock != null -> Unit
offer == Offer.UPDATE -> PrimaryButton(stringResource(R.string.upd_update_button, button), enabled = installable, main = true) { ua.onInstall(release!!) }
offer == Offer.SWITCH -> PrimaryButton(stringResource(R.string.upd_install_button, button), enabled = installable, main = true) { ua.onInstall(release!!) }
offer == Offer.AHEAD && installBlock == null ->
+1
View File
@@ -576,6 +576,7 @@
<string name="upd_signed_headline">No se puede actualizar sobre la instalada</string>
<string name="upd_signed_detail">Esta copia no está firmada con la clave de publicación de DroidDeck, así que las actualizaciones no se pueden instalar sobre ella. Desinstálala para cambiar.</string>
<string name="upd_available">Actualización disponible</string>
<string name="upd_blocked_status">No se puede instalar</string>
<string name="upd_switch">Lista para cambiar</string>
<string name="upd_ahead_status">Por delante de Stable</string>
<string name="upd_ahead_headline">Vas por delante de Stable</string>
+1
View File
@@ -608,6 +608,7 @@
<string name="upd_signed_headline">Can\'t update in place</string>
<string name="upd_signed_detail">This copy isn\'t signed with DroidDeck\'s release key, so updates can\'t install over it. Uninstall it to switch.</string>
<string name="upd_available">Update available</string>
<string name="upd_blocked_status">Can\'t install</string>
<string name="upd_switch">Ready to switch</string>
<string name="upd_ahead_status">Ahead of Stable</string>
<string name="upd_ahead_headline">You\'re ahead of Stable</string>
+125
View File
@@ -0,0 +1,125 @@
# DroidDeck 0.3.0 (beta)
<p align="center"><a href="https://discord.gg/JRGAvawjsm"><img src="https://img.shields.io/badge/Discord-Join%20the%20community-5865F2?logo=discord&logoColor=white" alt="Join the DroidDeck Discord"></a></p>
## Highlights
### Steam, as on a Steam Deck
Deck mode is now the default. Steam's performance overlay goes from level 1 to 4 in Quick Access and shows GPU and CPU load, clocks, temperatures, VRAM and battery, even on phones that lock those files away. Games keep running behind the Quick Access Menu and the Steam menu, and Quick Access shows battery life and time to full.
![Performance overlay at level 4 over Alan Wake's American Nightmare](media/0.3.0/mangohud-level4.jpg)
### A Steam Deck controller
In Steam sessions the client reads your pad as a Steam Deck controller, with a real Quick Access button and your handheld's gyro. Games get Steam Input's virtual pads, so your Steam Input layouts reach them. On a dual-screen handheld, the second screen can be the Deck's back grips and trackpads.
<p>
<img src="media/0.3.0/deck-controller.gif" width="60%" alt="Steam's Steam Deck Controller test on the top screen reading the trackpads and clicks played on the second screen's Deck trackpads and grips">
</p>
### Faster to Steam, faster in games
Steam's own startup is down from about 31 seconds to about 6, and that makes **Offline mode** work. proot has been optimized for calls games make most, and there's a new per-mode frame limiter.
![Cloud stepping out of a doorway into the open, played with the on-screen controls](media/0.3.0/in-game.gif)
### GPU drivers that set themselves
A new **GPU drivers** tab detects your GPU and keeps a matched pair of runtime and display drivers installed for it, including DroidDeck's own DD-Turnip builds. Switch to **Manual** to pick a pair yourself.
![Components GPU drivers tab on an Adreno 740, set to Auto with DD-Turnip v0.1.1 in use](media/0.3.0/gpu-drivers.jpg)
### Linux apps from Flathub, bring your own Appimage, and more
Turn on the **Flathub Store** in Setup to install ARM64 apps and games from Flathub. They open full screen from the front end with your controller. The Desktop page also takes your own apps: a script, an AppImage, a GitHub repo's newest ARM64 AppImage, or a Flatpak ID.
<p>
<img src="media/0.3.0/store-app.jpg" width="49%" alt="Space Cadet Pinball's page in the Flathub Store">
<img src="media/0.3.0/add-app.jpg" width="49%" alt="Adding Zen Browser from its GitHub repo, with icons suggested from the repo">
</p>
![Desktop page with added AppImages and Flatpak apps next to the emulators](media/0.3.0/desktop-apps.jpg)
### Updates in the app
The **Updates** page installs new builds without leaving DroidDeck: **Stable**, **Preview** (each build of main) or a **Test build** of an open pull request. Every download is checked against its checksum and DroidDeck's release key before Android sees it.
![Updates page with the Stable, Preview and Test builds channels](media/0.3.0/updates.jpg)
### A front end that moves as one
One focus ring slides between controls, settings flood out of the cog the way Play floods into a session, and Stop asks before it ends anything. DroidDeck is now available in Spanish, with more languages coming soon (contributions welcome).
<p>
<img src="media/0.3.0/focus-ring.gif" width="59%" alt="Moving the focus ring with a D-pad, opening Steam settings from the cog and backing out">
<img src="media/0.3.0/stop-session.gif" width="39%" alt="Stopping a session: the confirmation grows out of the Stop button, then the session floods back into Play Steam">
</p>
---
## Everything in 0.3.0
### Steam
- Steam's startup down from about 31 s to about 6 s, so Offline mode works (@xXJSONDeruloXx, #69)
- Deck mode on for everyone, moved once on update, always on the Steam Deck client branch so Steam stops offering a downgrade (@xXJSONDeruloXx, #100, #101, #141)
- Steam's performance overlay (MangoHud), levels 1–4, with Adreno GPU, CPU, VRAM and temperature stats, and a switch to turn it off (@xXJSONDeruloXx, #55, #101)
- The overlay keeps running and filled in under an enforcing SELinux policy, as on retail phones (@xXJSONDeruloXx, #127)
- Games keep running behind the Quick Access Menu and the Steam menu (@xXJSONDeruloXx, #131)
- A game that minimized itself behind the Steam menu comes back on Resume (@The412Banner, #89)
- Quick Access shows battery life and time to full (@xXJSONDeruloXx, #117)
- Steam shuts down cleanly when you stop a session, so it keeps its caches (@xXJSONDeruloXx, #68)
- Games you own in a second library or games folder become real Steam games instead of shortcuts (@maxjivi05, #62)
- Imported games keep their installed build, so Steam doesn't re-verify them (@maxjivi05, #108)
- Launch options on added games are kept between sessions (@xXJSONDeruloXx, #111)
- Choose the host name Steam sees (@magarto, #61)
- Stretch 16:9 games onto screens narrower than 16:9, such as foldables (opt-in) (@magarto, #63)
### Controls
- Steam Deck controller for the Steam client: a real Quick Access button, the handheld's gyro, and Steam Input's virtual pads for games (@xXJSONDeruloXx, #86)
- Deck back grips and trackpads on the second screen (@xXJSONDeruloXx, #86)
- Xbox 360 controller option, with the right axis layout for games that have Steam Input off (@xXJSONDeruloXx, #86, #102)
- The Steam client finds the Deck controller under an enforcing SELinux policy (@xXJSONDeruloXx, #125)
- Touch scrolls Steam on phones with no controller at start (@xXJSONDeruloXx, #123)
- Quick Access from Back comes sooner, and no longer opens the focused game under load (@xXJSONDeruloXx, #70, #73)
- Shift and Caps Lock type capitals (@xXJSONDeruloXx, #71)
- Adaptive sticks hide when idle and appear where you touch (@maxjivi05, #53)
### Display and performance
- GPU drivers tab: Auto keeps a matched runtime and display pair for your GPU, Manual lets you pick one (@xXJSONDeruloXx, #103)
- DD-Turnip, DroidDeck's own driver bundles (@maxjivi05, #106)
- Adreno 840v2 (retail SM8850) works with the runtime's own driver (@xXJSONDeruloXx, #93)
- Setup says when a GPU is untested (Adreno 6xx, 710, 720, 722) (@xXJSONDeruloXx, #103)
- LSFG takes Lossless Scaling from your Steam library by itself, adds 60, 90 and 120 fps targets, and paces generation to the game's frames (@maxjivi05, #96, #129)
- Per-mode frame limiter, live panel refresh, opt-in GPU clock pin and Mesa's database shader cache (@maxjivi05, #87)
- proot stops less often: kompat, fake_id0 and ioctl traps trimmed, and common path lookups answered in-process (@xXJSONDeruloXx, @maxjivi05, #81, #84, #113)
- Turnip's GPU timestamp polls answered without blocking (@maxjivi05, #87)
- Cheaper defaults: Steam's xalia helper skipped, gamescope's realtime queue off, microphone off (@xXJSONDeruloXx, #85)
- Per-game environment variables, now in an editor that works with a pad (@maxjivi05, @xXJSONDeruloXx, #60, #140)
### Apps and desktop
- Flathub Store: ARM64 Flatpak apps and games, opened full screen from the front end and listed on the Desktop page (@xXJSONDeruloXx, #77)
- Add your own apps to the Desktop page: a script, an AppImage, a GitHub repo or a Flatpak ID, with icons and update checks (@maxjivi05, #130)
- The desktop hands its cursor to the app, so full-screen games skip a compositing pass (@xXJSONDeruloXx, #57)
### Launcher
- Updates page: Stable, Preview and Test builds, installed in the app and verified first (@xXJSONDeruloXx, #98, #112, #115)
- Game saves: import and export GameHub and Winlator save zips for added games (@The412Banner, #91)
- One focus ring slides between controls, settings flood out of the cog, and focus returns to what opened a page (@xXJSONDeruloXx, #105, #140)
- Stop asks first, and quitting floods back into the launch button (@xXJSONDeruloXx, #72, #105)
- Spanish, with every screen's text moved to translatable strings (@xXJSONDeruloXx, #140)
- Pair wireless debugging from a notification to lift Android's child-process limit (@xXJSONDeruloXx, #67)
### Sessions and logs
- Session folders are named by day, number and mode, the newest 30 are kept, and Setup has **Clear logs** (@xXJSONDeruloXx, #122)
- Shared logs carry controller and touch-mode details for input reports (@xXJSONDeruloXx, #125)
### Fixed
- Steam sessions failing to start with the app on adopted SD storage (@xXJSONDeruloXx, #119)
- Second-library games failing to launch or install their dependencies (@xXJSONDeruloXx, #134)
- Steam's own installs on a second library showing up as non-Steam shortcuts (@xXJSONDeruloXx, #138)
- A white veil left after closing the session menu (@xXJSONDeruloXx, #135)
- Update channel cards flashing as a controller moved over them (@xXJSONDeruloXx, #99)
- Flatpak installs failing (@maxjivi05, #82)
- gamescope respawning a crashing overlay every second (@xXJSONDeruloXx, #81)
Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB