mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 03:21:08 +00:00
Merge pull request #41 from Droid-Deck/fix/ui-ux-critique
Fix/UI ux critique
This commit is contained in:
commit
9aed4e7a5c
20 files changed
+301
-180
No files matched your search
@@ -365,7 +365,7 @@ class MainActivity : ComponentActivity() {
|
||||
val result = runCatching {
|
||||
kotlinx.coroutines.runBlocking { WirelessAdbFix.pair(this@MainActivity, host, port, code) }
|
||||
}
|
||||
val error = result.exceptionOrNull()?.let { it.localizedMessage ?: "Wireless ADB pairing failed" }
|
||||
val error = result.exceptionOrNull()?.let { it.localizedMessage ?: "Wireless debugging pairing failed" }
|
||||
ui.post { complete(error) }
|
||||
}, "wireless-adb-pair").start()
|
||||
},
|
||||
@@ -378,7 +378,7 @@ class MainActivity : ComponentActivity() {
|
||||
onWirelessAdbApply = { host, port, enabled, complete ->
|
||||
Thread({
|
||||
val result = runCatching { WirelessAdbFix.setChildProcessLimit(this@MainActivity, host, port, enabled) }
|
||||
val error = result.exceptionOrNull()?.let { it.localizedMessage ?: "Wireless ADB command failed" }
|
||||
val error = result.exceptionOrNull()?.let { it.localizedMessage ?: "Wireless debugging command failed" }
|
||||
ui.post {
|
||||
if (error == null) refreshPhantomStatus()
|
||||
complete(error)
|
||||
@@ -388,7 +388,7 @@ class MainActivity : ComponentActivity() {
|
||||
onSetPhantomProcessLimit = { enabled, complete ->
|
||||
Thread({
|
||||
val result = runCatching { WirelessAdbFix.setUsingSavedPairing(this@MainActivity, enabled) }
|
||||
val error = result.exceptionOrNull()?.let { it.localizedMessage ?: "Wireless ADB is unavailable" }
|
||||
val error = result.exceptionOrNull()?.let { it.localizedMessage ?: "Wireless debugging is unavailable" }
|
||||
ui.post {
|
||||
if (error == null) refreshPhantomStatus()
|
||||
complete(error)
|
||||
@@ -518,6 +518,7 @@ class MainActivity : ComponentActivity() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
com.droiddeck.launcher.ui.Motion.refresh(this)
|
||||
refreshPhantomStatus()
|
||||
// Swaps queued while a game ran on that Proton go in once nothing uses it (usually the
|
||||
// session has just ended). Cheap when nothing is queued.
|
||||
|
||||
@@ -273,6 +273,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
override fun handleOnBackPressed() {
|
||||
// The PC keyboard closes first (B on a controller), then Back is the drawer again.
|
||||
when {
|
||||
::loading.isInitialized && loading.ended -> finish()
|
||||
drawerOpen -> drawerOpen = false
|
||||
pcKeyboardOpen -> pcKeyboardOpen = false
|
||||
else -> routeBackAction()
|
||||
@@ -299,6 +300,10 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
title = loadingTitle(), steam = loadingMode() == SessionService.MODE_STEAM,
|
||||
// As the drawer's Stop does: the service stops, and a start still installing is cancelled.
|
||||
onCancel = { SessionService.stop(this@SessionActivity); finish() },
|
||||
endedDetail = loading.endedDetail,
|
||||
onRetry = { retrySession() },
|
||||
onShareLogs = { shareCurrentSessionLogs() },
|
||||
onClose = { finish() },
|
||||
)
|
||||
// Opening the drawer takes the controller away from the game: release its pad.
|
||||
androidx.compose.runtime.LaunchedEffect(drawerOpen) {
|
||||
@@ -599,6 +604,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
SessionEvents.fail(code, problem)
|
||||
collectStartArtifacts("session start failed")
|
||||
loading.showEnded(problem)
|
||||
focusEndedScreen()
|
||||
return@post
|
||||
}
|
||||
loading.percent = -1
|
||||
@@ -888,19 +894,30 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
|
||||
private fun showEnded(status: Int, hint: String?) {
|
||||
run {
|
||||
if (status != 0) {
|
||||
loading.showEnded(
|
||||
"The session ended ($status)\n${SessionState.logFile?.path ?: "-"}" +
|
||||
(if (hint != null) "\n\n$hint" else "")
|
||||
)
|
||||
// A moment on screen, so a failure is readable rather than a flash of black;
|
||||
// longer when there is advice to read.
|
||||
Handler(Looper.getMainLooper()).postDelayed({ finish() }, if (hint != null) 9000 else 4000)
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
if (status == 0) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
// It stays until the user leaves: a failure read against a timer is a failure not read.
|
||||
loading.showEnded(
|
||||
hint ?: "Steam stopped unexpectedly. Share the logs with a bug report, or try again.",
|
||||
"Exit status $status · ${SessionState.logFile?.path ?: "no log"}",
|
||||
)
|
||||
focusEndedScreen()
|
||||
}
|
||||
|
||||
/** The ended screen's buttons take the pad: it is no longer driving a game. */
|
||||
private fun focusEndedScreen() {
|
||||
drawerOpen = false
|
||||
padBridge?.releaseAll()
|
||||
uiHandler.post { sessionOverlay.requestFocus() }
|
||||
}
|
||||
|
||||
/** The same start again, in a fresh activity: the ended one keeps no session to reuse. */
|
||||
private fun retrySession() {
|
||||
val again = Intent(intent)
|
||||
finish()
|
||||
startActivity(again)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -956,6 +973,14 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
return true
|
||||
}
|
||||
// The ended screen is a page of buttons: the pad drives them, and B leaves.
|
||||
if (::loading.isInitialized && loading.ended) {
|
||||
if (event.keyCode == KeyEvent.KEYCODE_BUTTON_B || event.keyCode == KeyEvent.KEYCODE_BACK) {
|
||||
if (event.action == KeyEvent.ACTION_DOWN && event.repeatCount == 0) finish()
|
||||
return true
|
||||
}
|
||||
return super.dispatchKeyEvent(event)
|
||||
}
|
||||
if (drawerOpen) {
|
||||
if (fromController && (event.keyCode == KeyEvent.KEYCODE_BUTTON_L1 || event.keyCode == KeyEvent.KEYCODE_BUTTON_R1)) {
|
||||
if (event.action == KeyEvent.ACTION_DOWN && event.repeatCount == 0) {
|
||||
@@ -1443,6 +1468,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
com.droiddeck.launcher.ui.Motion.refresh(this)
|
||||
refreshHomeApp()
|
||||
if (intent?.action == SessionService.ACTION_RESUME) {
|
||||
intent.action = null
|
||||
|
||||
@@ -40,9 +40,11 @@ object FrameGen {
|
||||
}
|
||||
|
||||
/** "Off", "Win-FG 2×", "LSFG 3×" - for the main screen. */
|
||||
fun label(context: Context): String = when (engine(context)) {
|
||||
ENGINE_WINFG -> "Win-FG ${multiplier(context)}×"
|
||||
ENGINE_LSFG -> "LSFG ${multiplier(context)}×"
|
||||
fun label(context: Context): String = label(engine(context), multiplier(context))
|
||||
|
||||
fun label(engine: String, multiplier: Int): String = when (engine) {
|
||||
ENGINE_WINFG -> "Win-FG $multiplier×"
|
||||
ENGINE_LSFG -> "LSFG $multiplier×"
|
||||
else -> "Off"
|
||||
}
|
||||
|
||||
@@ -82,47 +84,5 @@ object FrameGen {
|
||||
return problem
|
||||
}
|
||||
|
||||
/**
|
||||
* Off, or an engine at 2x/3x/4x; saved on pick, then [onChanged]. The LSFG rows are greyed
|
||||
* and unpickable until Lossless Scaling is installed in the Steam client - the engine cannot
|
||||
* run without its DLL, so offering it would only produce a setting that silently does nothing.
|
||||
*/
|
||||
fun showPicker(activity: android.app.Activity, onChanged: () -> Unit) {
|
||||
val lsfgReady = LsfgNative.isInstalled(activity)
|
||||
val lsfgNote = if (lsfgReady) "" else " - install Lossless Scaling in Steam"
|
||||
val choices = listOf("Off",
|
||||
"Win-FG 2×", "Win-FG 3×", "Win-FG 4×",
|
||||
"LSFG 2×$lsfgNote", "LSFG 3×$lsfgNote", "LSFG 4×$lsfgNote")
|
||||
val current = when (engine(activity)) {
|
||||
ENGINE_WINFG -> multiplier(activity) - 1
|
||||
ENGINE_LSFG -> if (lsfgReady) multiplier(activity) + 2 else 0
|
||||
else -> 0
|
||||
}
|
||||
val adapter = object : android.widget.ArrayAdapter<String>(
|
||||
activity, android.R.layout.simple_list_item_single_choice, choices) {
|
||||
override fun isEnabled(position: Int) = lsfgReady || position < 4
|
||||
override fun areAllItemsEnabled() = lsfgReady
|
||||
override fun getView(position: Int, convertView: android.view.View?, parent: android.view.ViewGroup): android.view.View {
|
||||
val view = super.getView(position, convertView, parent)
|
||||
view.isEnabled = isEnabled(position)
|
||||
view.alpha = if (isEnabled(position)) 1f else 0.38f
|
||||
return view
|
||||
}
|
||||
}
|
||||
android.app.AlertDialog.Builder(activity)
|
||||
.setTitle(com.droiddeck.launcher.R.string.frame_gen_title)
|
||||
.setSingleChoiceItems(adapter, current) { dialog, which ->
|
||||
when (which) {
|
||||
0 -> set(activity, ENGINE_OFF, 2)
|
||||
in 1..3 -> set(activity, ENGINE_WINFG, which + 1)
|
||||
else -> set(activity, ENGINE_LSFG, which - 2)
|
||||
}
|
||||
dialog.dismiss()
|
||||
onChanged()
|
||||
}
|
||||
.setNeutralButton(android.R.string.ok, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private const val TAG = "FrameGen"
|
||||
}
|
||||
@@ -24,6 +24,8 @@ class LoadingState(context: Context) {
|
||||
var elapsed by mutableStateOf("")
|
||||
var hint by mutableStateOf("")
|
||||
var ended by mutableStateOf(false)
|
||||
/** The technical side of an ended session (exit status, log path), under the advice in [step]. */
|
||||
var endedDetail by mutableStateOf<String?>(null)
|
||||
|
||||
private val hints = context.resources.getStringArray(com.droiddeck.launcher.R.array.loading_hints)
|
||||
private val startedAt = SystemClock.elapsedRealtime()
|
||||
@@ -35,10 +37,11 @@ class LoadingState(context: Context) {
|
||||
hint = hints[((seconds / 8) % hints.size).toInt()]
|
||||
}
|
||||
|
||||
fun showEnded(message: String) {
|
||||
fun showEnded(message: String, detail: String? = null) {
|
||||
visible = true
|
||||
ended = true
|
||||
step = message
|
||||
endedDetail = detail
|
||||
}
|
||||
|
||||
/** Re-reads the end of the log and updates the line and the bar. */
|
||||
|
||||
@@ -325,7 +325,7 @@ private fun SmallButton(text: String, enabled: Boolean = true, accent: Boolean =
|
||||
val hot = (src.collectIsFocusedAsState().value || src.collectIsHoveredAsState().value) && enabled
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier.heightIn(min = 40.dp).clip(RoundedCornerShape(10.dp))
|
||||
modifier = modifier.heightIn(min = 44.dp).clip(RoundedCornerShape(10.dp))
|
||||
.background(if (hot) pal.signal else colors.surfaceVariant)
|
||||
.border(2.dp, if (hot) colors.onBackground else pal.line2, RoundedCornerShape(10.dp))
|
||||
.hoverable(src).clickable(interactionSource = src, indication = null, enabled = enabled, onClick = onClick)
|
||||
|
||||
@@ -41,12 +41,12 @@ fun ColumnScope.ControllerRows(host: MenuHost, oscMode: String, c: ControllerPre
|
||||
oscMode, note = "Auto shows all controls without a controller. Steam + QAM shows only those buttons.", onPick = a.onOsc,
|
||||
)
|
||||
val tintOpen = host.open == "controller-tint"
|
||||
SettingsRow("Colour", "Tint of the on-screen buttons", highlighted = tintOpen) {
|
||||
SettingsRow("Color", "Tint of the on-screen buttons", highlighted = tintOpen) {
|
||||
Box {
|
||||
ValueChip(ControllerPrefs.tints.firstOrNull { it.first == c.tint }?.second ?: "Custom", tintOpen) {
|
||||
host.open = if (tintOpen) null else "controller-tint"
|
||||
}
|
||||
AnchoredMenu(tintOpen, onDismiss = { if (host.open == "controller-tint") host.open = null }, title = "Colour") { firstItemFocus ->
|
||||
AnchoredMenu(tintOpen, onDismiss = { if (host.open == "controller-tint") host.open = null }, title = "Color") { firstItemFocus ->
|
||||
ControllerPrefs.tints.forEachIndexed { index, (color, name) ->
|
||||
MenuItem(name, checked = c.tint == color, leading = { Swatch(color) }, focusRequester = if (index == 0) firstItemFocus else null) {
|
||||
a.onTint(color)
|
||||
@@ -59,7 +59,7 @@ fun ColumnScope.ControllerRows(host: MenuHost, oscMode: String, c: ControllerPre
|
||||
ChoiceRow(host, "controller-opacity", "Opacity", null, ControllerPrefs.opacities.map { it to "$it%" }, c.opacity, onPick = a.onOpacity)
|
||||
ChoiceRow(host, "controller-size", "Button size", "100% keeps the standard size", ControllerPrefs.sizes.map { it to "$it%" }, c.size, onPick = a.onSize)
|
||||
ToggleRow(host, "controller-stick-click", "Stick click", "Double-tap a stick and hold for L3 or R3", c.stickClick, onChange = a.onStickClick)
|
||||
ToggleRow(host, "controller-adaptive", "Adaptive sticks", "A stick centres where your thumb lands and returns home when you let go", c.adaptiveSticks, onChange = a.onAdaptiveSticks)
|
||||
ToggleRow(host, "controller-adaptive", "Adaptive sticks", "A stick centers where your thumb lands and returns home when you let go", c.adaptiveSticks, onChange = a.onAdaptiveSticks)
|
||||
SettingsRow("Layout", if (c.customLayout) "Custom positions saved" else "Placed for this screen's size and your grip") {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
SecondaryButton("Edit") { a.onEditLayout() }
|
||||
@@ -68,7 +68,7 @@ fun ColumnScope.ControllerRows(host: MenuHost, oscMode: String, c: ControllerPre
|
||||
}
|
||||
val remapped = c.mapping.count { (id, target) -> id != target }
|
||||
ActionRow("Button mapping", if (remapped == 0) "Every button sends its own input" else "$remapped of ${c.mapping.size} buttons remapped", "Configure", a.onMapping)
|
||||
ActionRow("Reset controller", "Restore the default colour, opacity, size, stick behaviour, mapping and layout", "Reset", a.onResetAll)
|
||||
ActionRow("Reset controller", "Restore the default color, opacity, size, stick behavior, mapping and layout", "Reset", a.onResetAll)
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.droiddeck.launcher.ui
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import com.droiddeck.launcher.gpu.FrameGen
|
||||
|
||||
/**
|
||||
* The frame generation picker, anchored to whatever opened it (host key "fg"). One copy for Setup,
|
||||
* a game's launch settings and the session drawer, so they say the same thing.
|
||||
*/
|
||||
@Composable
|
||||
internal fun FrameGenMenu(
|
||||
host: MenuHost, engine: String, multiplier: Int, lsfgReady: Boolean,
|
||||
onPick: (engine: String, multiplier: Int) -> Unit,
|
||||
) {
|
||||
AnchoredMenu(host.open == "fg", onDismiss = { if (host.open == "fg") host.open = null }, title = "Frame generation") { firstItemFocus ->
|
||||
val need = if (lsfgReady) null else "Needs Lossless Scaling installed in Steam"
|
||||
fun pick(e: String, m: Int) { onPick(e, m); host.open = null }
|
||||
MenuItem("Off", checked = engine == FrameGen.ENGINE_OFF, focusRequester = firstItemFocus) { pick(FrameGen.ENGINE_OFF, 2) }
|
||||
for (m in 2..4) MenuItem(FrameGen.label(FrameGen.ENGINE_WINFG, m), checked = engine == FrameGen.ENGINE_WINFG && multiplier == m) { pick(FrameGen.ENGINE_WINFG, m) }
|
||||
for (m in 2..4) MenuItem(FrameGen.label(FrameGen.ENGINE_LSFG, m), checked = engine == FrameGen.ENGINE_LSFG && multiplier == m, enabled = lsfgReady, detail = need) { pick(FrameGen.ENGINE_LSFG, m) }
|
||||
}
|
||||
}
|
||||
@@ -223,10 +223,11 @@ private fun Content(
|
||||
if (e.installed) {
|
||||
Rise(3) {
|
||||
Actions {
|
||||
PrimaryButton("Open ${e.name}", enabled = s.ready && !s.busy, main = true) { a.onEmulator(e) }
|
||||
PrimaryButton("Open ${e.name}", enabled = !s.busy, main = true) { a.onEmulator(e) }
|
||||
BusyChip(s)
|
||||
SecondaryButton("ROMs folder", onClick = a.onRoms)
|
||||
if (pkg != null) SecondaryButton(
|
||||
if (pkg.kind == "appimage") "Remove" else "Forget",
|
||||
if (pkg.kind == "appimage") "Remove" else "Hide",
|
||||
enabled = s.packageBusyId == null && !s.sessionRunning,
|
||||
) { a.onRemovePackage(pkg.id) }
|
||||
}
|
||||
@@ -274,7 +275,7 @@ private fun Content(
|
||||
if (s.packagePercent >= 0) LinearProgressIndicator(progress = { s.packagePercent / 100f }, modifier = Modifier.fillMaxWidth().height(4.dp))
|
||||
else LinearProgressIndicator(modifier = Modifier.fillMaxWidth().height(4.dp))
|
||||
}
|
||||
if (pkg?.kind == "tar") Rise(5) { Note("Forgetting this package hides it from Desktop; its files remain in the Linux runtime.") }
|
||||
if (pkg?.kind == "tar") Rise(5) { Note("Hiding this package takes it off Desktop; its files stay in the Linux runtime.") }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -296,8 +297,8 @@ private fun Content(
|
||||
Spacer(Modifier.height(14.dp))
|
||||
Actions {
|
||||
Image(painterResource(e.iconRes), null, modifier = Modifier.size(40.dp))
|
||||
PrimaryButton("Launch in ${e.name}", enabled = s.ready && !s.busy, main = true) { a.onRom(g) }
|
||||
ActionChip(g.hostPath.extension.uppercase().ifEmpty { "folder" }, ok = false)
|
||||
PrimaryButton("Launch in ${e.name}", enabled = !s.busy, main = true) { a.onRom(g) }
|
||||
if (s.busy) BusyChip(s) else ActionChip(g.hostPath.extension.uppercase().ifEmpty { "folder" }, ok = false)
|
||||
}
|
||||
}
|
||||
if (g.art != null && !narrow) Poster(g.art, g.name, Modifier.width(detailPosterWidth))
|
||||
@@ -315,7 +316,7 @@ private fun Content(
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> Note("Select an item.")
|
||||
else -> Note("That page is gone. Pick a section on the left.")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -331,6 +332,7 @@ private fun DesktopCard(s: FrontEndState, a: FrontEndActions) {
|
||||
// Enabled without a runtime or the desktop: the session's loading screen installs them first.
|
||||
PrimaryButton(if (s.desktopInstalled) "Open desktop" else "Install & open desktop", enabled = !s.busy, main = true, onClick = a.onDesktop)
|
||||
Cog(onClick = a.onDesktopSettings)
|
||||
BusyChip(s)
|
||||
}
|
||||
}
|
||||
Column(
|
||||
|
||||
@@ -62,16 +62,29 @@ import com.droiddeck.launcher.frontend.Library
|
||||
@Composable
|
||||
internal fun GamesPage(s: FrontEndState, a: FrontEndActions, selected: String, onSelect: (String) -> Unit, modifier: Modifier) {
|
||||
val games = remember(s.steamGames) { s.steamGames.sortedByDescending { it.lastPlayed } }
|
||||
val current = games.firstOrNull { "app:${it.appId}" == selected } ?: games.firstOrNull() ?: return
|
||||
val host = rememberMenuHost()
|
||||
val narrow = LocalNarrowPane.current
|
||||
val current = games.firstOrNull { "app:${it.appId}" == selected } ?: games.firstOrNull()
|
||||
if (current == null) {
|
||||
Column(modifier = modifier.padding(horizontal = if (narrow) 16.dp else 22.dp, vertical = if (narrow) 12.dp else 18.dp)) {
|
||||
Rise(0) { PageHeader("Games") }
|
||||
Rise(1) { Note("Games you install in Steam show up here. Open Steam, install one, and it appears after the session.") }
|
||||
Rise(2) {
|
||||
Actions { PrimaryButton("Play Steam", enabled = !s.busy, main = true, icon = Icons.Filled.PlayArrow, modifier = Modifier.padding(top = 12.dp), onClick = a.onPlay) }
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
val host = rememberMenuHost()
|
||||
if (games.size == 1) {
|
||||
Column(modifier = modifier.verticalScroll(rememberScrollState()).padding(horizontal = if (narrow) 16.dp else 22.dp, vertical = if (narrow) 12.dp else 18.dp)) {
|
||||
Rise(0) {
|
||||
Row(verticalAlignment = Alignment.Bottom) {
|
||||
GameHero(current, Modifier.weight(1f).heightIn(min = if (narrow) 190.dp else 250.dp)) {
|
||||
GameHeroCopy(current, if (narrow) 28.sp else 38.sp)
|
||||
PrimaryButton("Launch", enabled = s.ready && !s.busy, main = true, icon = Icons.Filled.PlayArrow) { a.onSteamGame(current) }
|
||||
Actions {
|
||||
PrimaryButton("Launch", enabled = !s.busy, main = true, icon = Icons.Filled.PlayArrow) { a.onSteamGame(current) }
|
||||
BusyChip(s)
|
||||
}
|
||||
}
|
||||
if (!narrow) Poster(current.art, current.name, Modifier.width(168.dp))
|
||||
}
|
||||
@@ -92,7 +105,10 @@ internal fun GamesPage(s: FrontEndState, a: FrontEndActions, selected: String, o
|
||||
) {
|
||||
GameHero(current, Modifier.fillMaxWidth().heightIn(min = if (narrow) 170.dp else 200.dp)) {
|
||||
GameHeroCopy(current, if (narrow) 24.sp else 32.sp)
|
||||
PrimaryButton("Launch", enabled = s.ready && !s.busy, main = true, icon = Icons.Filled.PlayArrow) { a.onSteamGame(current) }
|
||||
Actions {
|
||||
PrimaryButton("Launch", enabled = !s.busy, main = true, icon = Icons.Filled.PlayArrow) { a.onSteamGame(current) }
|
||||
BusyChip(s)
|
||||
}
|
||||
}
|
||||
SectionTitle("Launch settings", null)
|
||||
LaunchSettings(s, a, host)
|
||||
@@ -183,6 +199,12 @@ internal fun PageHeader(title: String, trailing: @Composable RowScope.() -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
/** Why a launch button is greyed out: the runtime is being worked on. Nothing when it is not. */
|
||||
@Composable
|
||||
internal fun BusyChip(s: FrontEndState) {
|
||||
if (s.busy) ActionChip(if (s.percent >= 0) "Runtime busy · ${s.percent}%" else "Runtime busy", ok = false)
|
||||
}
|
||||
|
||||
/** Whether Steam can start, said where Play is rather than only in Setup. */
|
||||
@Composable
|
||||
internal fun RuntimeChip(s: FrontEndState) = when {
|
||||
|
||||
@@ -53,6 +53,8 @@ import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
@@ -78,7 +80,7 @@ import com.droiddeck.launcher.core.PhantomProcessLimit
|
||||
|
||||
/** What the pad's face buttons do here, along the bottom edge as on a console. */
|
||||
@Composable
|
||||
internal fun ControllerHints() {
|
||||
internal fun ControllerHints(select: String = "Select", tabs: Boolean = false) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
@@ -88,7 +90,8 @@ internal fun ControllerHints() {
|
||||
horizontalArrangement = Arrangement.spacedBy(18.dp, Alignment.End),
|
||||
modifier = Modifier.fillMaxWidth().height(34.dp).background(colors.surface).padding(horizontal = 20.dp),
|
||||
) {
|
||||
HintGlyph("A", "Select")
|
||||
if (tabs) HintGlyph("LB RB", "Tabs")
|
||||
HintGlyph("A", select)
|
||||
HintGlyph("B", "Back")
|
||||
}
|
||||
}
|
||||
@@ -98,7 +101,12 @@ internal fun ControllerHints() {
|
||||
private fun HintGlyph(button: String, action: String) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Box(contentAlignment = Alignment.Center, modifier = Modifier.size(20.dp).clip(CircleShape).background(colors.onBackground)) {
|
||||
// Face buttons are round; bumpers are a wider keycap.
|
||||
val shape = if (button.length == 1) CircleShape else RoundedCornerShape(6.dp)
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier.height(20.dp).widthIn(min = 20.dp).clip(shape).background(colors.onBackground).padding(horizontal = if (button.length == 1) 0.dp else 6.dp),
|
||||
) {
|
||||
Text(button, fontSize = 12.sp, fontWeight = FontWeight.Bold, color = colors.background)
|
||||
}
|
||||
Text(action, fontSize = 13.sp, color = colors.onSurfaceVariant)
|
||||
@@ -133,12 +141,13 @@ internal fun SideRail(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier.weight(1f).fillMaxWidth().verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
if (s.isHomeApp) RailItem("Apps", Icons.Outlined.Apps, "android-apps", selected == "android-apps", compact, iconOnly, onFocus = { onFocusSelect("android-apps") }) { onSelect("android-apps") }
|
||||
RailItem("Steam", Icons.Outlined.SportsEsports, "steam", selected == "steam", compact, iconOnly, onFocus = { onFocusSelect("steam") }) { onSelect("steam") }
|
||||
// Games appears once there is one: an empty list is no place to land.
|
||||
if (s.steamGames.isNotEmpty()) RailItem("Games", Icons.Outlined.VideoLibrary, "games", selected == "games", compact, iconOnly, onFocus = { onFocusSelect("games") }) { onSelect("games") }
|
||||
// Always there, so the items below it never move; an empty library says how to fill it.
|
||||
RailItem("Games", Icons.Outlined.VideoLibrary, "games", selected == "games", compact, iconOnly, onFocus = { onFocusSelect("games") }) { onSelect("games") }
|
||||
RailItem("Desktop", Icons.Outlined.DesktopWindows, "desktop", selected == "desktop", compact, iconOnly, onFocus = { onFocusSelect("desktop") }) { onSelect("desktop") }
|
||||
RailItem("Components", Icons.Outlined.Layers, "components", selected == "components", compact, iconOnly, onFocus = { onFocusSelect("components") }) { onSelect("components") }
|
||||
// Home mode's extra section goes last, so it shifts nothing above it.
|
||||
if (s.isHomeApp) RailItem("Apps", Icons.Outlined.Apps, "android-apps", selected == "android-apps", compact, iconOnly, onFocus = { onFocusSelect("android-apps") }) { onSelect("android-apps") }
|
||||
RailItem("Setup", Icons.Outlined.Tune, "setup", selected == "setup", compact, iconOnly, badge = setupNeedsAttention, onFocus = { onFocusSelect("setup") }) { onSelect("setup") }
|
||||
}
|
||||
AnimatedVisibility(s.busy, enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut()) {
|
||||
@@ -222,7 +231,9 @@ private fun ResumeRailItem(name: String, compact: Boolean, iconOnly: Boolean, on
|
||||
val focused by src.collectIsFocusedAsState()
|
||||
val hovered by src.collectIsHoveredAsState()
|
||||
val pulse = rememberInfiniteTransition(label = "pulse")
|
||||
val ringScale by pulse.animateFloat(0.4f, 1.6f, infiniteRepeatable(tween(1600, easing = Motion.Ease), RepeatMode.Restart), label = "ring")
|
||||
val ringScale by pulse.animateFloat(0.4f, 1.6f, infiniteRepeatable(tween(Motion.ms(1600).coerceAtLeast(1), easing = Motion.Ease), RepeatMode.Restart), label = "ring")
|
||||
// Animations off: the live dot alone, no pulsing ring.
|
||||
val still = Motion.scale == 0f
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(3.dp),
|
||||
@@ -238,7 +249,7 @@ private fun ResumeRailItem(name: String, compact: Boolean, iconOnly: Boolean, on
|
||||
.padding(vertical = if (iconOnly) 15.dp else if (compact) 6.dp else 9.dp, horizontal = 4.dp),
|
||||
) {
|
||||
Box(modifier = Modifier.size(14.dp), contentAlignment = Alignment.Center) {
|
||||
Box(modifier = Modifier.size(14.dp).graphicsLayer { scaleX = ringScale; scaleY = ringScale; alpha = (1.6f - ringScale) / 1.2f }.border(1.5.dp, pal.good, CircleShape))
|
||||
if (!still) Box(modifier = Modifier.size(14.dp).graphicsLayer { scaleX = ringScale; scaleY = ringScale; alpha = (1.6f - ringScale) / 1.2f }.border(1.5.dp, pal.good, CircleShape))
|
||||
Box(modifier = Modifier.size(7.dp).clip(CircleShape).background(pal.good))
|
||||
}
|
||||
// Icons only: the live dot alone says something is running; its description says what.
|
||||
|
||||
@@ -71,6 +71,7 @@ import com.droiddeck.launcher.core.PhantomProcessLimit
|
||||
import com.droiddeck.launcher.core.PhantomProcessStatus
|
||||
import com.droiddeck.launcher.session.SessionPrefs
|
||||
import kotlin.math.roundToInt
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
|
||||
class FrontEndState(
|
||||
val installed: String?,
|
||||
@@ -147,10 +148,10 @@ class FrontEndActions(
|
||||
val onCheckLatestBuild: () -> Unit = {},
|
||||
val onRefreshPhantomStatus: () -> Unit = {},
|
||||
val onOpenDeveloperOptions: (Int?) -> Unit = {},
|
||||
val onWirelessAdbPair: (String, Int, String, (String?) -> Unit) -> Unit = { _, _, _, done -> done("Wireless ADB is unavailable") },
|
||||
val onWirelessAdbPair: (String, Int, String, (String?) -> Unit) -> Unit = { _, _, _, done -> done("Wireless debugging is unavailable") },
|
||||
val onFindWirelessAdbPort: (String, (Int?) -> Unit) -> Unit = { _, done -> done(null) },
|
||||
val onWirelessAdbApply: (String, Int, Boolean, (String?) -> Unit) -> Unit = { _, _, _, done -> done("Wireless ADB is unavailable") },
|
||||
val onSetPhantomProcessLimit: (Boolean, (String?) -> Unit) -> Unit = { _, done -> done("Wireless ADB is unavailable") },
|
||||
val onWirelessAdbApply: (String, Int, Boolean, (String?) -> Unit) -> Unit = { _, _, _, done -> done("Wireless debugging is unavailable") },
|
||||
val onSetPhantomProcessLimit: (Boolean, (String?) -> Unit) -> Unit = { _, done -> done("Wireless debugging is unavailable") },
|
||||
val onCopyPhantomCommand: (Boolean) -> Unit = {},
|
||||
val onDismissPhantomGate: () -> Unit = {},
|
||||
val controller: ControllerActions? = null,
|
||||
@@ -158,6 +159,10 @@ class FrontEndActions(
|
||||
|
||||
internal object Motion {
|
||||
var scale = 1f
|
||||
/** The system's animator scale, re-read on each resume so "Remove animations" applies without a restart. */
|
||||
fun refresh(context: android.content.Context) {
|
||||
scale = Settings.Global.getFloat(context.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f)
|
||||
}
|
||||
val Ease = CubicBezierEasing(0.2f, 0.8f, 0.2f, 1f)
|
||||
fun ms(base: Int) = (base * scale).roundToInt()
|
||||
fun <T> tw(base: Int, delay: Int = 0): FiniteAnimationSpec<T> = if (scale == 0f) snap() else tween(ms(base), ms(delay), Ease)
|
||||
@@ -229,7 +234,8 @@ internal class FrontFocus {
|
||||
// and the last one focused.
|
||||
val items = HashMap<String, FocusRequester>()
|
||||
val attached = HashMap<String, Int>()
|
||||
var last: String? = null
|
||||
// State, so the controller hints follow it: A launches from a game row but selects elsewhere.
|
||||
var last by mutableStateOf<String?>(null)
|
||||
// The first tile of the page's grid: Down from the page's buttons goes to it, not to whichever
|
||||
// tile happens to sit under the button.
|
||||
val firstTile = FocusRequester()
|
||||
@@ -278,6 +284,22 @@ internal fun Modifier.paneItem(id: String): Modifier {
|
||||
|
||||
internal val LocalFrontFocus = staticCompositionLocalOf<FrontFocus?> { null }
|
||||
|
||||
/** The pane's exit animation (170ms) and a frame's margin: a new page is in after this. */
|
||||
private const val PAGE_EXIT_MS = 200
|
||||
|
||||
/**
|
||||
* Asks [target] for focus once a frame until [done] (or a focus request lands), for at most half a
|
||||
* second: a control can take focus only once it is laid out, and that is a frame or two, not a
|
||||
* fixed number of milliseconds.
|
||||
*/
|
||||
internal suspend fun focusWithinFrames(done: () -> Boolean, target: () -> FocusRequester) {
|
||||
repeat(30) {
|
||||
androidx.compose.runtime.withFrameNanos { }
|
||||
if (done()) return
|
||||
if (runCatching { target().requestFocus() }.isSuccess && done()) return
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun FrontEndScreen(s: FrontEndState, a: FrontEndActions, page: (@Composable () -> Unit)? = null) {
|
||||
val frontFocus = remember { FrontFocus() }
|
||||
@@ -323,11 +345,11 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com
|
||||
selected = if (selected.startsWith("emu:")) "desktop" else "emu:" + selected.removePrefix("rom:").substringBefore(':')
|
||||
}
|
||||
LaunchedEffect(s.isHomeApp) { if (!s.isHomeApp && selected == "android-apps") selected = "steam" }
|
||||
// The last game uninstalled takes the Games tab with it.
|
||||
// The last game uninstalled leaves the Games tab on its empty state.
|
||||
LaunchedEffect(s.steamGames.isEmpty()) {
|
||||
if (s.steamGames.isEmpty() && (selected == "games" || selected.startsWith("app:"))) selected = "steam"
|
||||
if (s.steamGames.isEmpty() && selected.startsWith("app:")) selected = "games"
|
||||
}
|
||||
remember { Motion.scale = Settings.Global.getFloat(ctx.contentResolver, Settings.Global.ANIMATOR_DURATION_SCALE, 1f); true }
|
||||
remember { Motion.refresh(ctx); true }
|
||||
|
||||
val railSelection = when {
|
||||
s.pageKey == "performance" || s.pageKey == "protons" || s.pageKey == "controller-mapping" -> "setup"
|
||||
@@ -367,32 +389,27 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com
|
||||
LaunchedEffect(processSettingsPageVisible) {
|
||||
if (processSettingsPageVisible) return@LaunchedEffect
|
||||
snapshotFlow { window.isWindowFocused }.first { it }
|
||||
repeat(20) {
|
||||
if (anyFocused) return@LaunchedEffect
|
||||
if (inputModeManager.inputMode != InputMode.Keyboard) inputModeManager.requestInputMode(InputMode.Keyboard)
|
||||
val target = if (frontFocus.primaryAttached > 0) frontFocus.primary else frontFocus.railFor(railSelection)
|
||||
runCatching { target.requestFocus() }
|
||||
kotlinx.coroutines.delay(100)
|
||||
}
|
||||
if (inputModeManager.inputMode != InputMode.Keyboard) inputModeManager.requestInputMode(InputMode.Keyboard)
|
||||
focusWithinFrames({ anyFocused }) { if (frontFocus.primaryAttached > 0) frontFocus.primary else frontFocus.railFor(railSelection) }
|
||||
}
|
||||
// A tile or button that opens a page goes away with the page it was on, and focus with it;
|
||||
// the pad then had nothing to move from (a press landed back on the rail's first item). So
|
||||
// once the new page is in, a controller lands on its main button.
|
||||
LaunchedEffect(selected, s.pageKey, processSettingsPageVisible) {
|
||||
if (processSettingsPageVisible) return@LaunchedEffect
|
||||
kotlinx.coroutines.delay(450)
|
||||
if (!anyFocused && inputModeManager.inputMode == InputMode.Keyboard) runCatching {
|
||||
if (frontFocus.primaryAttached > 0) frontFocus.primary.requestFocus()
|
||||
else frontFocus.railFor(railSelection).requestFocus()
|
||||
}
|
||||
// Past the old page's exit (170ms), then the first frame the new page takes focus.
|
||||
kotlinx.coroutines.delay(Motion.ms(PAGE_EXIT_MS).toLong())
|
||||
if (anyFocused || inputModeManager.inputMode != InputMode.Keyboard) return@LaunchedEffect
|
||||
focusWithinFrames({ anyFocused }) { if (frontFocus.primaryAttached > 0) frontFocus.primary else frontFocus.railFor(railSelection) }
|
||||
}
|
||||
// A rail item picked with a controller moves on into its page, once the page is in.
|
||||
LaunchedEffect(railPicks) {
|
||||
if (railPicks == 0 || processSettingsPageVisible) return@LaunchedEffect
|
||||
kotlinx.coroutines.delay(450)
|
||||
if (inputModeManager.inputMode == InputMode.Keyboard && frontFocus.primaryAttached > 0) runCatching {
|
||||
frontFocus.paneEntry().requestFocus()
|
||||
}
|
||||
kotlinx.coroutines.delay(Motion.ms(PAGE_EXIT_MS).toLong())
|
||||
if (inputModeManager.inputMode != InputMode.Keyboard) return@LaunchedEffect
|
||||
// Waits for the new page's main button rather than a fixed guess at how long it takes.
|
||||
withTimeoutOrNull(1_000) { snapshotFlow { frontFocus.primaryAttached > 0 }.first { it } } ?: return@LaunchedEffect
|
||||
focusWithinFrames({ frontFocus.focusedRail == null && anyFocused }) { frontFocus.paneEntry() }
|
||||
}
|
||||
val paneFocus = Modifier
|
||||
.focusProperties { enter = { frontFocus.paneEntry() } }
|
||||
@@ -417,7 +434,13 @@ private fun FrontEndScreenBody(s: FrontEndState, a: FrontEndActions, page: (@Com
|
||||
AnimatedVisibility(
|
||||
inputModeManager.inputMode == InputMode.Keyboard,
|
||||
enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut(),
|
||||
) { ControllerHints() }
|
||||
) {
|
||||
val onGame = frontFocus.focusedRail == null && frontFocus.last?.startsWith("game:") == true
|
||||
ControllerHints(
|
||||
select = if (onGame) "Launch" else "Select",
|
||||
tabs = railSelection == "setup" && s.pageKey == null && frontFocus.focusedRail == null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -65,12 +65,10 @@ import androidx.compose.ui.platform.LocalInputModeManager
|
||||
import androidx.compose.ui.input.InputMode
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.droiddeck.launcher.gpu.FrameGen
|
||||
import com.droiddeck.launcher.core.DeviceSupport
|
||||
import com.droiddeck.launcher.core.PhantomProcessLimit
|
||||
import com.droiddeck.launcher.core.PhantomProcessStatus
|
||||
@@ -209,8 +207,8 @@ internal fun SetupPanel(
|
||||
) { SecondaryButton(runtime, enabled = !s.busy, compact = true, onClick = a.onRuntime) }
|
||||
CheckRow(
|
||||
if (limitBlocks) CheckState.WARN else CheckState.OK,
|
||||
if (limitBlocks) "Android may close Steam" else "Android process limit",
|
||||
if (limitBlocks) "“Restrict child processes” is on - it takes a minute to turn off" else PhantomProcessLimit.title(s.phantomProcessStatus),
|
||||
"Child-process limit",
|
||||
if (limitBlocks) "On · Android may close Steam. Turning it off takes a minute" else PhantomProcessLimit.title(s.phantomProcessStatus),
|
||||
) {
|
||||
if (limitBlocks) PrimaryButton(if (showLimitDetails) "Hide" else "Fix it", compact = true) { showLimitDetails = !showLimitDetails }
|
||||
else if (s.phantomProcessStatus != PhantomProcessStatus.NOT_APPLICABLE) {
|
||||
@@ -218,32 +216,25 @@ internal fun SetupPanel(
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(showLimitDetails, enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut()) {
|
||||
// One sentence and at most three buttons. The computer route and its
|
||||
// raw command live on the full page Wireless debugging opens.
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(start = 56.dp, end = 14.dp, top = 4.dp, bottom = 10.dp)) {
|
||||
Text(
|
||||
PhantomProcessLimit.instructions(s.phantomProcessStatus),
|
||||
if (limitBlocks) PhantomProcessLimit.gateInstructions(s.phantomProcessStatus)
|
||||
else PhantomProcessLimit.instructions(s.phantomProcessStatus),
|
||||
fontSize = 14.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(vertical = 4.dp),
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(vertical = 6.dp)) {
|
||||
SecondaryButton("Developer options", compact = true, onClick = onOpenDeveloperOptions)
|
||||
SecondaryButton("Check again", compact = true, onClick = a.onRefreshPhantomStatus)
|
||||
SecondaryButton("Copy ADB command", compact = true, onClick = { a.onCopyPhantomCommand(false) })
|
||||
}
|
||||
Text(
|
||||
PhantomProcessLimit.ADB_COMMAND, fontSize = 12.sp, fontFamily = FontFamily.Monospace,
|
||||
color = colors.onSurfaceVariant, modifier = Modifier.padding(vertical = 4.dp),
|
||||
)
|
||||
if (s.phantomProcessStatus != PhantomProcessStatus.NOT_APPLICABLE) {
|
||||
Text(
|
||||
"With wireless debugging paired, DroidDeck can switch the limit itself. Turning it off is recommended for Steam sessions.",
|
||||
fontSize = 14.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(top = 8.dp),
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(vertical = 6.dp)) {
|
||||
SecondaryButton("Turn limit off", compact = true, enabled = !processLimitBusy && s.phantomProcessStatus != PhantomProcessStatus.DISABLED) { setProcessLimit(false) }
|
||||
SecondaryButton("Turn limit on", compact = true, enabled = !processLimitBusy && s.phantomProcessStatus != PhantomProcessStatus.ENABLED) { setProcessLimit(true) }
|
||||
Actions {
|
||||
if (limitBlocks) {
|
||||
PrimaryButton("Developer options", compact = true, onClick = onOpenDeveloperOptions)
|
||||
SecondaryButton("Use Wireless debugging", compact = true, enabled = !processLimitBusy) { setProcessLimit(false) }
|
||||
SecondaryButton("Check again", compact = true, onClick = a.onRefreshPhantomStatus)
|
||||
} else if (s.phantomProcessStatus == PhantomProcessStatus.DISABLED) {
|
||||
SecondaryButton("Turn limit on", compact = true, enabled = !processLimitBusy) { setProcessLimit(true) }
|
||||
}
|
||||
if (processLimitBusy) LinearProgressIndicator(modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp))
|
||||
processLimitMessage?.let { Text(it, fontSize = 14.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(vertical = 4.dp)) }
|
||||
}
|
||||
if (processLimitBusy) LinearProgressIndicator(modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp))
|
||||
processLimitMessage?.let { Text(it, fontSize = 14.sp, color = colors.onSurfaceVariant, modifier = Modifier.padding(vertical = 4.dp)) }
|
||||
}
|
||||
}
|
||||
CheckRow(
|
||||
@@ -278,15 +269,13 @@ internal fun SetupPanel(
|
||||
FrameGenMenu(s, a, host)
|
||||
}
|
||||
}
|
||||
SettingsRow("Session logs", "${if (s.logsEnabled) "Enabled" else "Disabled"} · logs are saved after each session") {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
SecondaryButton(if (s.logsEnabled) "Turn off" else "Turn on") { a.onLogs() }
|
||||
SecondaryButton("Share latest") { a.onShareLogs() }
|
||||
}
|
||||
}
|
||||
SettingsRow("Offline mode", s.offlineAccount?.let { if (s.offline) "Enabled for $it" else "Signed in as $it" } ?: "Sign in to Steam first") {
|
||||
SecondaryButton(if (s.offline) "Turn off" else "Turn on", enabled = s.offlineAccount != null) { a.onOffline() }
|
||||
}
|
||||
ToggleRow(host, "logs", "Session logs", "Saved after each session", s.logsEnabled) { a.onLogs() }
|
||||
ActionRow("Latest session logs", "Send them with a bug report", "Share logs", a.onShareLogs)
|
||||
ToggleRow(
|
||||
host, "offline", "Offline mode",
|
||||
s.offlineAccount?.let { "Signed in as $it" } ?: "Sign in to Steam first",
|
||||
s.offline, enabled = s.offlineAccount != null,
|
||||
) { a.onOffline() }
|
||||
}
|
||||
}
|
||||
3 -> {
|
||||
@@ -337,7 +326,7 @@ private fun ToolGrid(s: FrontEndState, a: FrontEndActions) {
|
||||
val columns = if (LocalNarrowPane.current) 2 else 4
|
||||
val tools = listOf(
|
||||
ToolSpec(Icons.Outlined.Folder, "Files", "Browse and manage files", a.onFiles),
|
||||
ToolSpec(Icons.Outlined.Extension, "Compatibility tools", "Install ARM64 Proton builds", a.onProtons),
|
||||
ToolSpec(Icons.Outlined.Extension, "Proton versions", "Install ARM64 Proton builds", a.onProtons),
|
||||
ToolSpec(Icons.Outlined.Speed, "Performance", "CPU core assignment", a.onPerformance),
|
||||
ToolSpec(Icons.Outlined.VideogameAsset, "ROMs folder", s.romsDir ?: "Choose where emulator games are stored", a.onRoms),
|
||||
)
|
||||
@@ -380,12 +369,7 @@ private fun ToolCard(t: ToolSpec, modifier: Modifier) {
|
||||
|
||||
@Composable
|
||||
private fun FrameGenMenu(s: FrontEndState, a: FrontEndActions, host: MenuHost) {
|
||||
AnchoredMenu(host.open == "fg", onDismiss = { if (host.open == "fg") host.open = null }, title = "Frame generation") { firstItemFocus ->
|
||||
val need = if (s.lsfgReady) null else "Install Lossless Scaling in Steam"
|
||||
MenuItem("Off", checked = s.frameGenEngine == FrameGen.ENGINE_OFF, focusRequester = firstItemFocus) { a.onFrameGenPick(FrameGen.ENGINE_OFF, 2); host.open = null }
|
||||
for (m in 2..4) MenuItem("Win-FG ${m}×", checked = s.frameGenEngine == FrameGen.ENGINE_WINFG && s.frameGenMultiplier == m) { a.onFrameGenPick(FrameGen.ENGINE_WINFG, m); host.open = null }
|
||||
for (m in 2..4) MenuItem("LSFG ${m}×", checked = s.frameGenEngine == FrameGen.ENGINE_LSFG && s.frameGenMultiplier == m, enabled = s.lsfgReady, detail = need) { a.onFrameGenPick(FrameGen.ENGINE_LSFG, m); host.open = null }
|
||||
}
|
||||
FrameGenMenu(host, s.frameGenEngine, s.frameGenMultiplier, s.lsfgReady, a.onFrameGenPick)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -398,7 +382,7 @@ internal fun LaunchSettings(s: FrontEndState, a: FrontEndActions, host: MenuHost
|
||||
val columns = if (LocalNarrowPane.current) 2 else 3
|
||||
val controller = a.controller
|
||||
val cards = buildList<@Composable (Modifier) -> Unit> {
|
||||
add { m -> SettingCard("Compatibility", "Proton & components", "card:components", m) { a.onComponents(true) } }
|
||||
add { m -> SettingCard("Components", "FEX, DXVK, VKD3D", "card:components", m) { a.onComponents(true) } }
|
||||
add { m ->
|
||||
Box(m) {
|
||||
SettingCard("Frame generation", s.frameGenLabel, "card:fg", Modifier.fillMaxSize()) {
|
||||
|
||||
@@ -111,7 +111,7 @@ internal fun BackLink(label: String, compact: Boolean = false, onClick: () -> Un
|
||||
val shape = RoundedCornerShape(22.dp)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.paneItem("back:$label").heightIn(min = if (compact) 36.dp else 44.dp)
|
||||
modifier = Modifier.paneItem("back:$label").heightIn(min = if (compact) 44.dp else 48.dp)
|
||||
.clip(shape)
|
||||
.background(if (hot) pal.signal.copy(alpha = 0.14f) else Color.White.copy(alpha = 0.04f))
|
||||
.border(2.dp, if (hot) pal.signal else Color.Transparent, shape)
|
||||
@@ -166,6 +166,8 @@ internal fun PrimaryButton(
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = modifier.then(track)
|
||||
// 48dp to touch on a handheld; compact rows keep 44.
|
||||
.heightIn(min = if (compact) 44.dp else 48.dp)
|
||||
.then(if (frontFocus != null) Modifier.focusRequester(frontFocus.primary).onFocusChanged { if (it.isFocused) frontFocus.last = FrontFocus.PRIMARY } else Modifier)
|
||||
.graphicsLayer { scaleX = scale; scaleY = scale; shadowElevation = if (enabled) lift.dp.toPx() else 0f; shape = Shape12; clip = false; ambientShadowColor = pal.signal; spotShadowColor = pal.signal }
|
||||
.clip(Shape12)
|
||||
@@ -202,7 +204,8 @@ internal fun SecondaryButton(text: String, enabled: Boolean = true, compact: Boo
|
||||
val edge by animateColorAsState(if (hot) pal.signal else pal.line2, Motion.tw(250), label = "secEdge")
|
||||
val fill by animateColorAsState(if (hot) pal.signal.copy(alpha = 0.14f) else Color.White.copy(alpha = 0.03f), Motion.tw(250), label = "secFill")
|
||||
Box(
|
||||
modifier = modifier.paneItem("btn:$text").downToFirstTile().graphicsLayer { scaleX = scale; scaleY = scale }.clip(Shape12).background(fill).border(1.dp, edge, Shape12)
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier.paneItem("btn:$text").downToFirstTile().heightIn(min = if (compact) 44.dp else 48.dp).graphicsLayer { scaleX = scale; scaleY = scale }.clip(Shape12).background(fill).border(1.dp, edge, Shape12)
|
||||
.alpha(if (enabled) 1f else 0.5f)
|
||||
.hoverable(src).clickable(interactionSource = src, indication = LocalIndication.current, enabled = enabled, onClick = onClick)
|
||||
.controllerConfirm(enabled = enabled, onClick = onClick)
|
||||
@@ -211,7 +214,7 @@ internal fun SecondaryButton(text: String, enabled: Boolean = true, compact: Boo
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun Cog(size: androidx.compose.ui.unit.Dp = 42.dp, onClick: () -> Unit) {
|
||||
internal fun Cog(size: androidx.compose.ui.unit.Dp = 48.dp, onClick: () -> Unit) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val src = remember { MutableInteractionSource() }
|
||||
val hot = rememberHot(src)
|
||||
|
||||
@@ -94,7 +94,7 @@ private fun CompactGateInstructions(
|
||||
)
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
PrimaryButton("Open Developer options", compact = true, onClick = onOpenDeveloperOptions)
|
||||
SecondaryButton("Use on-device ADB", compact = true, onClick = onSetUpWirelessAdb)
|
||||
SecondaryButton("Use Wireless debugging", compact = true, onClick = onSetUpWirelessAdb)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,7 +133,7 @@ private fun GateInstructions(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
PrimaryButton("Open Developer options", onClick = onOpenDeveloperOptions)
|
||||
SecondaryButton("Use on-device ADB", onClick = onSetUpWirelessAdb)
|
||||
SecondaryButton("Use Wireless debugging", onClick = onSetUpWirelessAdb)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ fun ProtonPage(
|
||||
val colors = MaterialTheme.colorScheme
|
||||
SettingsPage(
|
||||
host,
|
||||
title = "Compatibility tools",
|
||||
title = "Proton versions",
|
||||
eyebrow = "Setup",
|
||||
lede = "Download and install an ARM64 Proton build now, then select it per game in Steam > Properties > Compatibility.",
|
||||
onBack = onBack,
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.runtime.produceState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
@@ -125,6 +126,11 @@ private fun readableStep(step: String): Boolean {
|
||||
fun LoadingOverlay(
|
||||
step: String, percent: Int, elapsed: String, hint: String, ended: Boolean,
|
||||
title: String = "Starting Steam", steam: Boolean = true, onCancel: (() -> Unit)? = null,
|
||||
/** An ended session's exit status and log path, shown small under the advice. */
|
||||
endedDetail: String? = null,
|
||||
onRetry: (() -> Unit)? = null,
|
||||
onShareLogs: (() -> Unit)? = null,
|
||||
onClose: (() -> Unit)? = null,
|
||||
) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val stages = remember(steam) { loadStages(steam) }
|
||||
@@ -176,6 +182,12 @@ fun LoadingOverlay(
|
||||
modifier = Modifier.padding(top = 6.dp).widthIn(max = 520.dp),
|
||||
)
|
||||
if (!ended) LoadSegments(stages.size, active, percent, Modifier.padding(top = 18.dp))
|
||||
if (ended && endedDetail != null) Text(
|
||||
endedDetail, fontSize = 12.sp, fontFamily = FontFamily.Monospace, color = colors.onSurfaceVariant.copy(alpha = 0.8f),
|
||||
textAlign = TextAlign.Center, maxLines = 3, overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(top = 12.dp).widthIn(max = 520.dp),
|
||||
)
|
||||
if (ended) EndedActions(onRetry, onShareLogs, onClose)
|
||||
}
|
||||
if (!ended && hint.isNotEmpty()) Text(
|
||||
hint, fontSize = 13.sp, lineHeight = 18.sp, color = colors.onSurfaceVariant.copy(alpha = 0.8f), textAlign = TextAlign.Center,
|
||||
@@ -186,13 +198,31 @@ fun LoadingOverlay(
|
||||
}
|
||||
}
|
||||
|
||||
/** What to do about an ended session: nothing leaves on a timer, and the pad starts on Try again. */
|
||||
@Composable
|
||||
private fun EndedActions(onRetry: (() -> Unit)?, onShareLogs: (() -> Unit)?, onClose: (() -> Unit)?) {
|
||||
val first = remember { FocusRequester() }
|
||||
LaunchedEffect(Unit) {
|
||||
androidx.compose.runtime.withFrameNanos { }
|
||||
runCatching { first.requestFocus() }
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier.padding(top = 22.dp)) {
|
||||
var firstUsed = false
|
||||
fun claim(): Modifier = if (firstUsed) Modifier else { firstUsed = true; Modifier.focusRequester(first) }
|
||||
if (onRetry != null) PrimaryButton("Try again", modifier = claim(), onClick = onRetry)
|
||||
if (onShareLogs != null) SecondaryButton("Share logs", modifier = claim(), onClick = onShareLogs)
|
||||
if (onClose != null) SecondaryButton("Back", modifier = claim(), onClick = onClose)
|
||||
}
|
||||
}
|
||||
|
||||
/** One segment per stage: done ones filled, the current one filling (or running, with no percent). */
|
||||
@Composable
|
||||
private fun LoadSegments(count: Int, active: Int, percent: Int, modifier: Modifier = Modifier) {
|
||||
val pal = LocalPalette.current
|
||||
val track = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.12f)
|
||||
val run = rememberInfiniteTransition(label = "segment")
|
||||
.animateFloat(-0.4f, 1f, infiniteRepeatable(tween(1300, easing = FastOutSlowInEasing)), label = "run")
|
||||
.animateFloat(-0.4f, 1f, infiniteRepeatable(tween(Motion.ms(1300).coerceAtLeast(1), easing = FastOutSlowInEasing)), label = "run")
|
||||
val still = Motion.scale == 0f
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
modifier = modifier.semantics { contentDescription = "Step ${active + 1} of $count" },
|
||||
@@ -204,6 +234,7 @@ private fun LoadSegments(count: Int, active: Int, percent: Int, modifier: Modifi
|
||||
.drawBehind {
|
||||
if (i != active) return@drawBehind
|
||||
if (percent >= 0) drawRect(pal.signal, size = Size(size.width * percent.coerceIn(0, 100) / 100f, size.height))
|
||||
else if (still) drawRect(pal.signal.copy(alpha = 0.5f))
|
||||
else drawRect(pal.signal, topLeft = Offset(size.width * run.value, 0f), size = Size(size.width * 0.4f, size.height))
|
||||
},
|
||||
)
|
||||
|
||||
@@ -316,20 +316,11 @@ fun SessionDrawer(open: Boolean, page: Int, controllerActive: Boolean, onPageCha
|
||||
chipModifier = focus.track(page, "fill"), onChange = a.onFillScreen,
|
||||
)
|
||||
val fgOpen = host.open == "fg"
|
||||
val fgLabel = when (a.frameGenEngine) {
|
||||
FrameGen.ENGINE_WINFG -> "Win-FG ${a.frameGenMultiplier}×"
|
||||
FrameGen.ENGINE_LSFG -> "LSFG ${a.frameGenMultiplier}×"
|
||||
else -> "Off"
|
||||
}
|
||||
val fgLabel = FrameGen.label(a.frameGenEngine, a.frameGenMultiplier)
|
||||
SettingsRow("Frame generation", null, highlighted = fgOpen) {
|
||||
Box {
|
||||
ValueChip(fgLabel, fgOpen, modifier = focus.track(page, "fg")) { host.open = if (fgOpen) null else "fg" }
|
||||
AnchoredMenu(fgOpen, onDismiss = { if (host.open == "fg") host.open = null }, title = "Frame generation") { firstItemFocus ->
|
||||
val need = if (a.lsfgReady) null else "Requires Lossless Scaling"
|
||||
MenuItem("Off", checked = a.frameGenEngine == FrameGen.ENGINE_OFF, focusRequester = firstItemFocus) { a.onFrameGenPick(FrameGen.ENGINE_OFF, 2); host.open = null }
|
||||
for (m in 2..4) MenuItem("Win-FG ${m}×", checked = a.frameGenEngine == FrameGen.ENGINE_WINFG && a.frameGenMultiplier == m) { a.onFrameGenPick(FrameGen.ENGINE_WINFG, m); host.open = null }
|
||||
for (m in 2..4) MenuItem("LSFG ${m}×", checked = a.frameGenEngine == FrameGen.ENGINE_LSFG && a.frameGenMultiplier == m, enabled = a.lsfgReady, detail = need) { a.onFrameGenPick(FrameGen.ENGINE_LSFG, m); host.open = null }
|
||||
}
|
||||
FrameGenMenu(host, a.frameGenEngine, a.frameGenMultiplier, a.lsfgReady, a.onFrameGenPick)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -349,16 +340,16 @@ fun SessionDrawer(open: Boolean, page: Int, controllerActive: Boolean, onPageCha
|
||||
}
|
||||
SettingsGroup("Keyboard") {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth().padding(8.dp)) {
|
||||
DrawerOutlineButton("Hardware", modifier = Modifier.weight(1f).height(42.dp).then(focus.track(page, "hardware"))) {
|
||||
DrawerOutlineButton("PC keyboard", modifier = Modifier.weight(1f).height(48.dp).then(focus.track(page, "hardware"))) {
|
||||
host.open = null; a.onHardwareKeyboard()
|
||||
}
|
||||
DrawerOutlineButton("Android", modifier = Modifier.weight(1f).height(42.dp).then(focus.track(page, "android"))) {
|
||||
DrawerOutlineButton("Android", modifier = Modifier.weight(1f).height(48.dp).then(focus.track(page, "android"))) {
|
||||
host.open = null; a.onKeyboard()
|
||||
}
|
||||
}
|
||||
}
|
||||
if (a.steam && a.secondScreenDisplays.isNotEmpty()) SettingsGroup("Second screen") {
|
||||
ChoiceRow(host, "second-screen-mode", "Controls", null,
|
||||
ChoiceRow(host, "second-screen-mode", "Shows", null,
|
||||
listOf(SecondScreenMode.NONE, SecondScreenMode.KEYBOARD_TRACKPAD, SecondScreenMode.TERMINAL).map { it to it.label },
|
||||
a.secondScreenMode, chipModifier = focus.track(page, "second-screen-mode"), onPick = a.onSecondScreenMode)
|
||||
if (a.secondScreenDisplays.size > 1) ChoiceRow(host, "second-screen-display", "Display", null,
|
||||
@@ -420,7 +411,7 @@ fun SessionDrawer(open: Boolean, page: Int, controllerActive: Boolean, onPageCha
|
||||
}
|
||||
SettingsGroup("Support") {
|
||||
SettingsRow("Session logs", "Send this session's logs with a bug report") {
|
||||
DrawerOutlineButton("Send logs", modifier = focus.track(page, "share-logs")) {
|
||||
DrawerOutlineButton("Share logs", modifier = focus.track(page, "share-logs")) {
|
||||
host.open = null
|
||||
a.onShareLogs()
|
||||
}
|
||||
@@ -509,7 +500,7 @@ private fun StopSessionButton(modifier: Modifier = Modifier, onClick: () -> Unit
|
||||
val shape = RoundedCornerShape(20.dp)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
modifier = modifier.heightIn(min = 40.dp).semantics { contentDescription = "Stop session" }
|
||||
modifier = modifier.heightIn(min = 44.dp).semantics { contentDescription = "Stop session" }
|
||||
.clip(shape).background(fill).border(if (hot) 2.dp else 1.dp, colors.error.copy(alpha = if (hot) 0.9f else 0.55f), shape)
|
||||
.hoverable(src).clickable(interactionSource = src, indication = LocalIndication.current, onClick = onClick)
|
||||
.controllerConfirm(onClick = onClick)
|
||||
|
||||
@@ -19,6 +19,8 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.ui.focus.focusProperties
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.stateDescription
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
@@ -149,7 +151,7 @@ internal fun TabStrip(
|
||||
.border(2.dp, if (!hot) Color.Transparent else if (on) colors.onBackground else pal.signal, tabShape)
|
||||
.hoverable(src).clickable(interactionSource = src, indication = null, role = Role.Tab, onClick = pick)
|
||||
.controllerConfirm(onClick = pick)
|
||||
.padding(horizontal = 16.dp, vertical = 9.dp),
|
||||
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -301,6 +303,7 @@ fun ValueChip(text: String, open: Boolean, enabled: Boolean = true, modifier: Mo
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween,
|
||||
modifier = modifier
|
||||
.widthIn(min = LocalChipMinWidth.current)
|
||||
.heightIn(min = 44.dp)
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.background(colors.surfaceVariant)
|
||||
.border(1.dp, edge, RoundedCornerShape(10.dp))
|
||||
@@ -385,7 +388,46 @@ fun <T> ChoiceRow(
|
||||
|
||||
@Composable
|
||||
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)
|
||||
SettingsRow(label, hint) {
|
||||
ToggleSwitch(checked, enabled, label, chipModifier) { host.open = null; onChange(it) }
|
||||
}
|
||||
|
||||
/** An on/off switch: one tap or one A press flips it, where a menu of On and Off took three. */
|
||||
@Composable
|
||||
fun ToggleSwitch(checked: Boolean, enabled: Boolean = true, label: String? = null, modifier: Modifier = Modifier, onChange: (Boolean) -> Unit) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
val pal = LocalPalette.current
|
||||
val src = remember { MutableInteractionSource() }
|
||||
val hot = src.collectIsFocusedAsState().value || src.collectIsHoveredAsState().value
|
||||
val track by animateColorAsState(if (checked) pal.signal else colors.surfaceVariant, Motion.tw(180), label = "switchTrack")
|
||||
val edge by animateColorAsState(if (hot) pal.signal else if (checked) pal.signal else pal.line2, Motion.tw(180), label = "switchEdge")
|
||||
val knob by animateFloatAsState(if (checked) 1f else 0f, Motion.sp(0.7f), label = "switchKnob")
|
||||
val flip = { onChange(!checked) }
|
||||
val shape = RoundedCornerShape(99.dp)
|
||||
// 48dp tall to touch; the visible track sits inside it.
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier
|
||||
.heightIn(min = 48.dp).widthIn(min = 64.dp)
|
||||
.alpha(if (enabled) 1f else 0.5f)
|
||||
.hoverable(src)
|
||||
.clickable(interactionSource = src, indication = null, enabled = enabled, role = Role.Switch, onClick = flip)
|
||||
.controllerConfirm(enabled = enabled, onClick = flip)
|
||||
.semantics { stateDescription = if (checked) "On" else "Off"; if (label != null) contentDescription = label },
|
||||
) {
|
||||
Box(
|
||||
Modifier.size(width = 52.dp, height = 30.dp).clip(shape).background(track)
|
||||
.border(if (hot) 2.dp else 1.dp, if (hot && checked) colors.onBackground else edge, shape),
|
||||
) {
|
||||
Box(
|
||||
Modifier.padding(4.dp).size(22.dp)
|
||||
.graphicsLayer { translationX = knob * 22.dp.toPx() }
|
||||
.clip(CircleShape)
|
||||
.background(if (checked) pal.onSignal else colors.onSurfaceVariant),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MultiRow(
|
||||
|
||||
@@ -172,9 +172,9 @@ fun WirelessAdbFixPage(
|
||||
SettingsPage(
|
||||
host = rememberMenuHost(),
|
||||
title = when {
|
||||
compactSplit && step < 2 -> "Wireless ADB"
|
||||
step == 2 -> "Wireless ADB ready"
|
||||
else -> "Set up on-device ADB"
|
||||
compactSplit && step < 2 -> "Wireless debugging"
|
||||
step == 2 -> "Child-process limit updated"
|
||||
else -> "Change the child-process limit"
|
||||
},
|
||||
eyebrow = "Setup",
|
||||
lede = when {
|
||||
@@ -201,7 +201,7 @@ fun WirelessAdbFixPage(
|
||||
) {
|
||||
SettingsGroup("Child-process limit", compact = compactSplit) {
|
||||
Text(
|
||||
"Android confirmed the limit is ${if (desiredEnabled) "enabled" else "disabled"}.",
|
||||
"Android confirmed the limit is ${if (desiredEnabled) "on" else "off"}.",
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(14.dp),
|
||||
)
|
||||
@@ -332,7 +332,7 @@ private fun WirelessStepForm(
|
||||
StatusMessage(busy, message, Modifier.weight(1f), isError = messageIsError)
|
||||
SecondaryButton("Pair again", compact = compact, enabled = !busy, onClick = onPairAgain)
|
||||
PrimaryButton(
|
||||
if (desiredEnabled) "Enable limit" else "Disable limit",
|
||||
if (desiredEnabled) "Turn limit on" else "Turn limit off",
|
||||
compact = compact,
|
||||
enabled = !busy && parseAdbAddress(connectionAddress) != null,
|
||||
onClick = onApply,
|
||||
|
||||
@@ -24,7 +24,7 @@ class HdrProbe(
|
||||
) {
|
||||
/** 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 colour space)"
|
||||
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
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user