mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-10-14 19:27:09 -04:00
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:
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(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user