Merge pull request #22 from The412Banner/feat/first-login-proton

First sign-in fetches the ARM64 Proton and restarts the client once
This commit is contained in:
The412Banner authored and GitHub committed 2026-09-23 19:08:29 -04:00
commit 54a4677f11
7 files changed
+116 -12

No files matched your search

@@ -179,6 +179,8 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
CursorOverlay(cursorPos, cursorVisible, resources.displayMetrics.density)
if (hud.text.isNotEmpty()) HudText(hud.text)
if (loading.visible) LoadingOverlay(loading.step, loading.percent, loading.elapsed, loading.hint, loading.ended)
// Opening the drawer takes the controller away from the game: release its pad.
androidx.compose.runtime.LaunchedEffect(drawerOpen) { if (drawerOpen) padBridge?.releaseAll() }
SessionDrawer(drawerOpen, DrawerActions(
steam = SessionState.mode == SessionService.MODE_STEAM,
isHomeApp = isHomeApp,
@@ -607,6 +609,10 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_MUTE ->
return super.dispatchKeyEvent(event)
}
// With the drawer open the controller drives the drawer, not the game: Android's own
// handling moves focus with the d-pad and stick, and its fallbacks make A select and B Back
// (which closes the drawer) - the same as on the app's main screen.
if (drawerOpen && event.device != null && PadBridge.isFromController(event.device)) return super.dispatchKeyEvent(event)
if (event.keyCode != KeyEvent.KEYCODE_BACK && padBridge?.onKeyEvent(event) == true) return true
// A hardware keyboard, forwarded to the compositor's wl_keyboard. Back is left to the
// activity, which opens the drawer.
@@ -648,6 +654,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
}
override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
if (drawerOpen && event.device != null && PadBridge.isFromController(event.device)) return super.dispatchGenericMotionEvent(event)
if (padBridge?.onMotionEvent(event) == true) return true
if (event.isFromSource(android.view.InputDevice.SOURCE_MOUSE) && !drawerOpen && onMouse(event)) return true
return super.dispatchGenericMotionEvent(event)
@@ -166,6 +166,15 @@ public final class PadBridge {
else publish();
}
/**
* Everything released and centred - for when the controller stops feeding the game (the
* session drawer opened), so a button or stick held at that moment is not left down in it.
*/
public synchronized void releaseAll() {
state.clear();
publish();
}
/** Starts the same Guide-then-A sequence used by the touch QAM button. */
public synchronized void triggerQam() {
startQamChord();
@@ -337,9 +337,16 @@ private fun Rail(
}
}
// Outlined when a controller is on it, like every other control; the ripple alone was
// a faint box on the dark rail.
val creditsSrc = remember { MutableInteractionSource() }
val creditsHot = rememberHot(creditsSrc)
Text(
"credits", fontSize = 11.sp, color = colors.onSurfaceVariant,
modifier = Modifier.clip(Shape10).clickable(onClick = a.onCredits).padding(horizontal = 10.dp, vertical = 8.dp),
"credits", fontSize = 11.sp, color = if (creditsHot) LocalPalette.current.signal else colors.onSurfaceVariant,
modifier = Modifier.clip(Shape10)
.border(2.dp, if (creditsHot) LocalPalette.current.signal else Color.Transparent, Shape10)
.hoverable(creditsSrc).clickable(interactionSource = creditsSrc, indication = null, onClick = a.onCredits)
.padding(horizontal = 10.dp, vertical = 8.dp),
)
}
}
@@ -789,6 +796,9 @@ private fun PrimaryButton(text: String, enabled: Boolean = true, onClick: () ->
.clip(Shape12)
.background(if (enabled) Brush.linearGradient(listOf(colors.primary, pal.primary2)) else Brush.linearGradient(listOf(colors.surfaceVariant, colors.surfaceVariant)))
.shine(hot, 0.45f)
// The grow and shine alone barely show on the light fill: outline it when a
// controller is on it, as the other controls are.
.border(2.dp, if (hot) pal.signal else Color.Transparent, Shape12)
.hoverable(src).clickable(interactionSource = src, indication = LocalIndication.current, enabled = enabled, onClick = onClick)
.padding(horizontal = 18.dp, vertical = 11.dp),
) {
@@ -4,6 +4,9 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.ui.focus.focusRequester
import androidx.compose.material3.ButtonDefaults
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.interaction.PressInteraction
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -177,6 +180,16 @@ fun SessionDrawer(open: Boolean, a: DrawerActions) {
val host = rememberMenuHost()
var androidAppsExpanded by rememberSaveable { mutableStateOf(false) }
val veil by animateFloatAsState(if (open) 1f else 0f, Motion.tw(260), label = "veil")
// A controller starts on the drawer's first control, highlighted, as on the app's main screen:
// the STEAM button, or in a desktop session the first setting.
val firstFocus = remember { androidx.compose.ui.focus.FocusRequester() }
val steamButtons = a.onSteamMenu != null && a.onQam != null
LaunchedEffect(open) {
if (open) {
androidx.compose.runtime.withFrameNanos { }
runCatching { firstFocus.requestFocus() }
}
}
if (open || veil > 0.01f) Box(
modifier = Modifier.fillMaxSize().graphicsLayer { alpha = veil }.background(Color(0x8A000000))
.clickable(interactionSource = remember { MutableInteractionSource() }, indication = null) { host.open = null; a.onClose() },
@@ -203,6 +216,7 @@ fun SessionDrawer(open: Boolean, a: DrawerActions) {
}
if (a.onSteamMenu != null && a.onQam != null) {
val qamInteraction = remember { MutableInteractionSource() }
val qamHot = qamInteraction.collectIsFocusedAsState().value || qamInteraction.collectIsHoveredAsState().value
var qamStartedOnPress by remember { mutableStateOf(false) }
LaunchedEffect(qamInteraction) {
qamInteraction.interactions.collect { interaction ->
@@ -217,10 +231,15 @@ fun SessionDrawer(open: Boolean, a: DrawerActions) {
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp),
) {
val steamSrc = remember { MutableInteractionSource() }
val steamHot = steamSrc.collectIsFocusedAsState().value || steamSrc.collectIsHoveredAsState().value
OutlinedButton(
onClick = { host.open = null; a.onSteamMenu.invoke() },
modifier = Modifier.weight(1f).height(48.dp),
interactionSource = steamSrc,
modifier = Modifier.weight(1f).height(48.dp).focusRequester(firstFocus),
shape = RoundedCornerShape(12.dp),
border = BorderStroke(if (steamHot) 2.dp else 1.dp, if (steamHot) pal.signal else colors.outline),
colors = ButtonDefaults.outlinedButtonColors(containerColor = if (steamHot) pal.signal.copy(alpha = 0.16f) else Color.Transparent),
) {
Text("STEAM", fontSize = 13.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 0.5.sp)
}
@@ -233,6 +252,8 @@ fun SessionDrawer(open: Boolean, a: DrawerActions) {
interactionSource = qamInteraction,
modifier = Modifier.weight(1f).height(48.dp).semantics { contentDescription = "Open Quick Access Menu" },
shape = RoundedCornerShape(12.dp),
border = BorderStroke(if (qamHot) 2.dp else 1.dp, if (qamHot) pal.signal else colors.outline),
colors = ButtonDefaults.outlinedButtonColors(containerColor = if (qamHot) pal.signal.copy(alpha = 0.16f) else Color.Transparent),
) {
Text("…", fontSize = 22.sp, fontWeight = FontWeight.SemiBold)
}
@@ -268,7 +289,10 @@ fun SessionDrawer(open: Boolean, a: DrawerActions) {
}
SettingsGroup("Now") {
ToggleRow(host, "hud", "Performance HUD", null, a.hudOn, onChange = a.onHud)
ToggleRow(
host, "hud", "Performance HUD", null, a.hudOn,
chipModifier = if (steamButtons) Modifier else Modifier.focusRequester(firstFocus), onChange = a.onHud,
)
val fgOpen = host.open == "fg"
val fgLabel = when (a.frameGenEngine) {
FrameGen.ENGINE_WINFG -> "Win-FG ${a.frameGenMultiplier}×"
@@ -242,8 +242,8 @@ fun <T> ChoiceRow(
}
@Composable
fun ToggleRow(host: MenuHost, key: String, label: String, hint: String?, checked: Boolean, enabled: Boolean = true, onChange: (Boolean) -> Unit) =
ChoiceRow(host, key, label, hint, listOf(true to "On", false to "Off"), checked, enabled, onPick = onChange)
fun ToggleRow(host: MenuHost, key: String, label: String, hint: String?, checked: Boolean, enabled: Boolean = true, chipModifier: Modifier = Modifier, onChange: (Boolean) -> Unit) =
ChoiceRow(host, key, label, hint, listOf(true to "On", false to "Off"), checked, enabled, chipModifier = chipModifier, onPick = onChange)
@Composable
fun MultiRow(
+2 -2
View File
@@ -3,11 +3,11 @@
<string name="app_name">DroidDeck</string>
<string-array name="loading_hints">
<item>First run downloads Steam. This may take a few minutes.</item>
<item>Steam keeps running in the background.</item>
<item>By default, leaving the app pauses Steam; it picks up where it left off when you come back.</item>
<item>Your controller appears as an Xbox 360 pad. Otherwise, use the on-screen controls.</item>
</string-array>
<string name="session_channel">Session</string>
<string name="session_channel_description">Keeps Steam running in the background</string>
<string name="session_channel_description">Shows while a Steam or desktop session is open</string>
<string name="session_notification">Steam is running</string>
<string name="session_notification_paused">Session paused - tap Resume when ready</string>
<string name="resume_session">Resume</string>
@@ -252,6 +252,49 @@ case $mode in
if [ ! -f "$steam_root/steamapps/appmanifest_4427310.acf" ]; then
step "asking the client to fetch the ARM64 Proton"
set -- "$@" steam://install/4427310
# Ported from Bannerlator (bannerlator-session, device-proven 2026-09-22). The client decides
# a title's compatibility tool when the title is installed and keeps that decision for the
# rest of its run, and the tool is set up before the client comes up. So once the depot has
# fully landed, restart the client once - its own -shutdown, then the loop below, which knows
# the marker - and every title installed from then on runs through the tool, with the
# refreshed tool list and defaults, and no second launch by the user. A first run only: with
# the depot present at session start nothing here is armed.
(
_bl_acf="$steam_root/steamapps/appmanifest_4427310.acf"
for _ in $(seq 1 360); do
sleep 10
# StateFlags 4 = fully installed; a moment more for the client to finish its bookkeeping.
if grep -q '"StateFlags"[[:space:]]*"4"' "$_bl_acf" 2>/dev/null; then
sleep 10
bannerlator-steam-compat "$steam_root" >/dev/null 2>&1 || true
step "compatibility layer installed: restarting the Steam client once"
: > "$HOME/.bl-restart-once"
"$steam_root/steamrtarm64/steam" -shutdown >/dev/null 2>&1 || true
exit 0
fi
done
) &
# A client that starts signed out drops that request - on a fresh install nobody has signed
# 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.
if ! grep -q '"RememberPassword"[[:space:]]*"1"' "$steam_root/config/loginusers.vdf" 2>/dev/null; then
(
_bl_conn="$steam_root/logs/connection_log.txt"
_bl_from=$(stat -c %s "$_bl_conn" 2>/dev/null || echo 0)
for _ in $(seq 1 180); do
sleep 10
[ -f "$steam_root/steamapps/appmanifest_4427310.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
exit 0
fi
done
) &
fi
fi
# The tool is registered before the client starts and finds the depot at launch time, so
# nothing here waits for a download. What can still change under a running client is which
@@ -384,17 +427,21 @@ BUSCONF
# channel Armada bootstraps its ARM64 client from. Whatever wrote package/beta, the branch on
# disk has to agree with the one forced here - a disagreement is the restart loop above, and
# it is silent apart from rc=42 in this log.
# Checked before every start, not once: the client rewrites package/beta during its own run,
# and a restart below (rc=42, or the one after the ARM64 Proton lands) would otherwise start it
# again on the branch it just wrote.
steam_channel=${BL_STEAM_CHANNEL:-publicbeta}
steam_beta=$steam_root/package/beta
if [ -f "$steam_beta" ] && [ "$(cat "$steam_beta" 2>/dev/null)" != "$steam_channel" ]; then
echo "== steam: client branch on disk was '$(cat "$steam_beta" 2>/dev/null)', set to '$steam_channel' to match -clientbeta"
echo "$steam_channel" > "$steam_beta"
fi
echo "== steam args: ${_bl_ui[*]} -clientbeta $steam_channel -no-cef-sandbox -cef-force-gpu -cef-ozone-platform=x11 -cef-use-gl=angle -cef-use-angle=vulkan $*"
# An update that cannot install asks for a restart (rc=42) every time; the count is capped so a
# session can never again loop in silence, which reads as a menu opening and closing by itself.
steam_updates=0
while :; do
rm -f "$HOME/.bl-restart-once"
if [ -f "$steam_beta" ] && [ "$(cat "$steam_beta" 2>/dev/null)" != "$steam_channel" ]; then
echo "== steam: client branch on disk was '$(cat "$steam_beta" 2>/dev/null)', set to '$steam_channel' to match -clientbeta"
echo "$steam_channel" > "$steam_beta"
fi
step "starting the Steam client${BL_STEAM_UI:+ ($BL_STEAM_UI UI)}"
"$steam_root/steamrtarm64/steam" "${_bl_ui[@]}" -clientbeta "$steam_channel" \
-no-cef-sandbox -cef-force-gpu -cef-ozone-platform=x11 \
@@ -402,6 +449,13 @@ BUSCONF
rc=$?
echo "== steam exited rc=$rc"
collect_steam_logs
# The session asked for this exit (the ARM64 Proton landed): start again, without the URL
# arguments, which were acted on. Not counted against the update cap.
if [ -f "$HOME/.bl-restart-once" ]; then
echo "== steam: restarting once so the new compatibility layer is in the client's lists"
set --
continue
fi
[ "$rc" -eq 42 ] || break
steam_updates=$((steam_updates + 1))
if [ "$steam_updates" -ge 3 ]; then