mirror of
https://github.com/Droid-Deck/DroidDeck.git
synced 2026-11-12 03:21:08 +00:00
Fix Nova layouts and session drawer shortcuts (#1574)
Improve Nova’s compact layouts with scrolling store tabs and build details, remove the redundant Updates heading, preserve full portrait artwork, and add drawer PiP and Select/Start shortcuts for Guide/QAM.
This commit is contained in:
1 parent
679e9476c3
commit
12eac3e9ee
6 files changed
+112
-41
No files matched your search
@@ -271,6 +271,7 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
}
|
||||
private val resumeKeysDown = mutableSetOf<Pair<Int, Int>>()
|
||||
private val drawerShortcutKeysDown = mutableSetOf<Pair<Int, Int>>()
|
||||
|
||||
private var drawerOpen by mutableStateOf(false)
|
||||
private var quitFrom by mutableStateOf<androidx.compose.ui.geometry.Rect?>(null)
|
||||
@@ -2071,6 +2072,20 @@ class SessionActivity : ComponentActivity(), SurfaceHolder.Callback {
|
||||
}
|
||||
return super.dispatchKeyEvent(event)
|
||||
}
|
||||
val drawerShortcutKey = event.deviceId to event.keyCode
|
||||
if (fromController && (event.keyCode == KeyEvent.KEYCODE_BUTTON_SELECT || event.keyCode == KeyEvent.KEYCODE_BUTTON_START) &&
|
||||
((drawerOpen && SessionState.mode == SessionService.MODE_STEAM) || drawerShortcutKey in drawerShortcutKeysDown)) {
|
||||
if (event.action == KeyEvent.ACTION_DOWN) {
|
||||
drawerShortcutKeysDown.add(drawerShortcutKey)
|
||||
if (event.repeatCount == 0) {
|
||||
releaseDrawerDirection()
|
||||
if (event.keyCode == KeyEvent.KEYCODE_BUTTON_SELECT) sendSteamGuide() else triggerSteamQam()
|
||||
}
|
||||
} else if (event.action == KeyEvent.ACTION_UP) {
|
||||
drawerShortcutKeysDown.remove(drawerShortcutKey)
|
||||
}
|
||||
return true
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -21,6 +21,7 @@ import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.IntrinsicSize
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -206,7 +207,6 @@ internal fun SetupPanel(
|
||||
runCatching { chipFocus[tab].requestFocus() }
|
||||
}
|
||||
}
|
||||
val wide = !LocalNarrowPane.current
|
||||
val runtimeOk = s.ready && !s.busy && !s.removalPending
|
||||
val checks = listOf(gpuOk, runtimeOk, !limitBlocks, signedIn)
|
||||
val passing = checks.count { it }
|
||||
@@ -453,10 +453,12 @@ internal fun SetupPanel(
|
||||
)
|
||||
}
|
||||
}
|
||||
if (wide) Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.Top) {
|
||||
Column(Modifier.weight(1f)) { look() }
|
||||
Column(Modifier.weight(1f)) { device(); art(); download() }
|
||||
} else { look(); device(); art(); download() }
|
||||
BoxWithConstraints(Modifier.fillMaxWidth()) {
|
||||
if (maxWidth >= 840.dp && !LocalNarrowPane.current) Row(horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.Top) {
|
||||
Column(Modifier.weight(1f)) { look() }
|
||||
Column(Modifier.weight(1f)) { device(); art(); download() }
|
||||
} else Column { look(); device(); art(); download() }
|
||||
}
|
||||
}
|
||||
STORAGE_TAB -> StoragePanel(s, a)
|
||||
4 -> {
|
||||
|
||||
@@ -426,6 +426,9 @@ internal fun SessionDrawer(open: Boolean, requestedPage: SessionDrawerPage, cont
|
||||
) {
|
||||
Text(a.title ?: if (a.steam) stringResource(R.string.drawer_steam) else stringResource(R.string.drawer_desktop), fontSize = if (short) 18.sp else 20.sp, fontWeight = FontWeight.Bold,
|
||||
color = colors.onBackground, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f))
|
||||
if (a.pipSupported) SteamActionPill(
|
||||
stringResource(R.string.pip_title), Icons.Outlined.PictureInPictureAlt, focus.track(page, "pip"),
|
||||
) { host.open = null; a.onPip() }
|
||||
if (a.onSteamMenu != null && a.onQam != null) {
|
||||
val qamInteraction = remember { MutableInteractionSource() }
|
||||
var qamStartedOnPress by remember { mutableStateOf(false) }
|
||||
@@ -438,13 +441,13 @@ internal fun SessionDrawer(open: Boolean, requestedPage: SessionDrawerPage, cont
|
||||
}
|
||||
}
|
||||
}
|
||||
SteamActionPill(stringResource(R.string.drawer_steam_menu), Icons.Outlined.Home, focus.track(page, "steam")) {
|
||||
SteamActionPill(stringResource(R.string.drawer_steam_menu), Icons.Outlined.Home, focus.track(page, "steam"), shortcut = "⧉") {
|
||||
host.open = null
|
||||
a.onSteamMenu.invoke()
|
||||
}
|
||||
SteamActionPill(
|
||||
stringResource(R.string.drawer_open_qam), Icons.Outlined.MoreHoriz, focus.track(page, "qam"),
|
||||
interactionSource = qamInteraction,
|
||||
interactionSource = qamInteraction, shortcut = "☰",
|
||||
onConfirm = { host.open = null; a.onQam.invoke() },
|
||||
) {
|
||||
host.open = null
|
||||
@@ -711,8 +714,6 @@ internal fun SessionDrawer(open: Boolean, requestedPage: SessionDrawerPage, cont
|
||||
a.pipAutoEnter, onChange = a.onPipAutoEnter)
|
||||
}
|
||||
Column(Modifier.fillMaxWidth().padding(top = 12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
if (a.pipSupported) DrawerActionButton(stringResource(R.string.pip_title), Icons.Outlined.PictureInPictureAlt,
|
||||
modifier = focus.track(page, "pip")) { host.open = null; a.onPip() }
|
||||
if (!a.isHomeApp) DrawerActionButton(stringResource(R.string.drawer_background), Icons.AutoMirrored.Outlined.ExitToApp,
|
||||
modifier = focus.track(page, "background")) { host.open = null; a.onBackground() }
|
||||
}
|
||||
@@ -818,6 +819,7 @@ private fun SteamActionPill(
|
||||
description: String, icon: androidx.compose.ui.graphics.vector.ImageVector, modifier: Modifier,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
onConfirm: (() -> Unit)? = null,
|
||||
shortcut: String? = null,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
val colors = MaterialTheme.colorScheme
|
||||
@@ -826,7 +828,7 @@ private fun SteamActionPill(
|
||||
val shape = RoundedCornerShape(22.dp)
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier.size(width = 52.dp, height = 44.dp).semantics { contentDescription = description }
|
||||
modifier = modifier.size(44.dp).semantics { contentDescription = description }
|
||||
.clip(shape).background(if (hot) pal.signal.copy(alpha = 0.14f) else Color.Transparent)
|
||||
.glideBorder(hot, shape, pal.signal, pal.line)
|
||||
.hoverable(interactionSource)
|
||||
@@ -834,6 +836,10 @@ private fun SteamActionPill(
|
||||
.controllerConfirm(onClick = onConfirm ?: onClick),
|
||||
) {
|
||||
Icon(icon, contentDescription = null, tint = if (hot) pal.signal else colors.onBackground, modifier = Modifier.size(20.dp))
|
||||
if (shortcut != null) Text(
|
||||
shortcut, fontSize = 8.sp, lineHeight = 9.sp, color = colors.onSurfaceVariant,
|
||||
modifier = Modifier.align(Alignment.BottomEnd).padding(end = 8.dp, bottom = 6.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,8 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.focusGroup
|
||||
import androidx.compose.foundation.relocation.BringIntoViewRequester
|
||||
import androidx.compose.foundation.relocation.bringIntoViewRequester
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.hoverable
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
@@ -611,6 +613,7 @@ private fun RowScope.ArrangeStoreChip(
|
||||
) { onToggleTab(store) }
|
||||
}
|
||||
|
||||
@OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
private fun StoreChips(
|
||||
selected: String, focused: String?, active: Int, tabs: List<StoreTab>, arranging: Boolean,
|
||||
@@ -620,7 +623,6 @@ private fun StoreChips(
|
||||
val pal = LocalPalette.current
|
||||
val keys = tabs.filter { it.shown }.map { it.store.id } + DOWNLOADS
|
||||
val bounds = remember { androidx.compose.runtime.mutableStateMapOf<String, Rect>() }
|
||||
val rootBounds = remember { HashMap<String, Rect>() }
|
||||
val highlighted = focused ?: selected
|
||||
val inputMode = androidx.compose.ui.platform.LocalInputModeManager.current.inputMode
|
||||
val fillShown = inputMode != androidx.compose.ui.input.InputMode.Keyboard && !arranging
|
||||
@@ -630,47 +632,59 @@ private fun StoreChips(
|
||||
val gap = 8.dp
|
||||
val gapPx = with(androidx.compose.ui.platform.LocalDensity.current) { gap.toPx() }
|
||||
val drag = remember { ChipDrag() }
|
||||
val tabScroll = rememberScrollState()
|
||||
Box(Modifier.fillMaxWidth()) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(gap), verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {
|
||||
for (tab in tabs) {
|
||||
val store = tab.store
|
||||
if (arranging) {
|
||||
val index = tabs.indexOfFirst { it.store == store }
|
||||
key(store) { ArrangeStoreChip(tab, index, tabs.lastIndex, gapPx, drag, onFocusChip, onMoveTab, onToggleTab) }
|
||||
} else if (tab.shown) {
|
||||
val signedIn = StoresState.isSignedIn(store) && StoresState.expired[store] != true
|
||||
key(store) {
|
||||
StoreChip(
|
||||
label = store.shortLabel, on = selected == store.id, under = fillShown && highlighted == store.id, key = store.id, mark = sourceColours(store.id).dot,
|
||||
modifier = Modifier.weight(1f).onGloballyPositioned { bounds[store.id] = it.boundsInParent(); rootBounds[store.id] = it.boundsInRoot() },
|
||||
lead = { StoreDot(store, signedIn) },
|
||||
description = store.label + if (signedIn || !StoresState.accountsLoaded) "" else " " + stringResource(R.string.stores_chip_signed_out),
|
||||
onFocused = onFocusChip,
|
||||
) { onStore(store) }
|
||||
Box(Modifier.weight(1f).then(if (arranging) Modifier else Modifier.edgeFade(tabScroll).horizontalScroll(tabScroll))) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(gap), verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = (if (arranging) Modifier.fillMaxWidth() else Modifier).focusGroup(),
|
||||
) {
|
||||
for (tab in tabs) {
|
||||
val store = tab.store
|
||||
if (arranging) {
|
||||
val index = tabs.indexOfFirst { it.store == store }
|
||||
key(store) { ArrangeStoreChip(tab, index, tabs.lastIndex, gapPx, drag, onFocusChip, onMoveTab, onToggleTab) }
|
||||
} else if (tab.shown) {
|
||||
val signedIn = StoresState.isSignedIn(store) && StoresState.expired[store] != true
|
||||
key(store) {
|
||||
val reveal = remember { BringIntoViewRequester() }
|
||||
LaunchedEffect(selected) { if (selected == store.id) reveal.bringIntoView() }
|
||||
StoreChip(
|
||||
label = store.shortLabel, on = selected == store.id, under = fillShown && highlighted == store.id, key = store.id, mark = sourceColours(store.id).dot,
|
||||
modifier = Modifier.bringIntoViewRequester(reveal).onGloballyPositioned { bounds[store.id] = it.boundsInParent() },
|
||||
lead = { StoreDot(store, signedIn) },
|
||||
description = store.label + if (signedIn || !StoresState.accountsLoaded) "" else " " + stringResource(R.string.stores_chip_signed_out),
|
||||
onFocused = onFocusChip,
|
||||
) { onStore(store) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
ChipGlide(keys.indexOf(highlighted), bounds[highlighted], tint, visible = fillShown && highlighted != DOWNLOADS)
|
||||
}
|
||||
if (arranging) {
|
||||
IconChip(Icons.Filled.Check, stringResource(R.string.stores_tabs_arrange_done)) { onArrangeDone() }
|
||||
} else {
|
||||
StoreChip(
|
||||
label = null, on = selected == DOWNLOADS, under = fillShown && highlighted == DOWNLOADS, key = DOWNLOADS, mark = pal.signal,
|
||||
modifier = Modifier.onGloballyPositioned {
|
||||
bounds[DOWNLOADS] = it.boundsInParent()
|
||||
StoresMotion.downloadsChip = it.boundsInRoot()
|
||||
},
|
||||
lead = { Icon(Icons.Outlined.Download, null, modifier = Modifier.size(18.dp)) },
|
||||
trail = if (active > 0) { { CountPill(active) } } else null,
|
||||
description = stringResource(R.string.stores_tab_downloads),
|
||||
onFocused = onFocusChip,
|
||||
) { onDownloads() }
|
||||
Box {
|
||||
StoreChip(
|
||||
label = null, on = selected == DOWNLOADS, under = fillShown && highlighted == DOWNLOADS, key = DOWNLOADS, mark = pal.signal,
|
||||
modifier = Modifier.onGloballyPositioned {
|
||||
bounds[DOWNLOADS] = it.boundsInParent()
|
||||
StoresMotion.downloadsChip = it.boundsInRoot()
|
||||
},
|
||||
lead = { Icon(Icons.Outlined.Download, null, modifier = Modifier.size(18.dp)) },
|
||||
trail = if (active > 0) { { CountPill(active) } } else null,
|
||||
description = stringResource(R.string.stores_tab_downloads),
|
||||
onFocused = onFocusChip,
|
||||
) { onDownloads() }
|
||||
ChipGlide(keys.indexOf(DOWNLOADS), bounds[DOWNLOADS], tint, visible = fillShown && highlighted == DOWNLOADS)
|
||||
}
|
||||
var cogAt by remember { mutableStateOf<Rect?>(null) }
|
||||
Box(Modifier.onGloballyPositioned { cogAt = it.boundsInRoot() }) {
|
||||
IconChip(Icons.Filled.Settings, stringResource(R.string.stores_settings)) { onSettings(cogAt) }
|
||||
}
|
||||
}
|
||||
}
|
||||
ChipGlide(keys.indexOf(highlighted), bounds[highlighted], tint, visible = fillShown)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1323,7 +1337,8 @@ internal fun CardFace(
|
||||
Spacer(Modifier.matchParentSize().background(Color.Black.copy(alpha = if (blurs) 0.35f else 0.7f)))
|
||||
AsyncImage(model = art, contentDescription = null, contentScale = ContentScale.Fit, modifier = Modifier.matchParentSize().padding(bottom = CardStrip))
|
||||
} else if (art != null) AsyncImage(
|
||||
model = art, contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.matchParentSize(),
|
||||
model = art, contentDescription = null, contentScale = if (capsule) ContentScale.Fit else ContentScale.Crop,
|
||||
modifier = Modifier.matchParentSize().padding(bottom = if (capsule) CardStrip else 0.dp),
|
||||
onSuccess = { r ->
|
||||
val drawn = r.result.drawable
|
||||
if (capsule && drawn.intrinsicWidth > 0 && drawn.intrinsicHeight > 0) landscape = drawn.intrinsicWidth > drawn.intrinsicHeight * 0.95f
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.basicMarquee
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.hoverable
|
||||
@@ -88,6 +89,7 @@ class UpdatesActions(
|
||||
val onDismissPermission: () -> Unit = {},
|
||||
)
|
||||
|
||||
@OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
internal fun UpdatesPage(s: FrontEndState, a: FrontEndActions, modifier: Modifier) {
|
||||
val u = s.updates
|
||||
@@ -95,10 +97,14 @@ internal fun UpdatesPage(s: FrontEndState, a: FrontEndActions, modifier: Modifie
|
||||
val me = remember { AppUpdates.installed() }
|
||||
val colors = MaterialTheme.colorScheme
|
||||
Column(modifier = modifier) {
|
||||
PageHeader(stringResource(R.string.upd_title)) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(bottom = 10.dp),
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.upd_build, me.version, s.buildLabel), fontSize = 12.5.sp, color = colors.onSurfaceVariant,
|
||||
maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f).padding(top = 6.dp),
|
||||
maxLines = 1, modifier = Modifier.weight(1f)
|
||||
.basicMarquee(iterations = Int.MAX_VALUE, initialDelayMillis = 700, velocity = 40.dp),
|
||||
)
|
||||
val checked = u.catalog?.checkedAt?.takeIf { it > 0 }
|
||||
if (!LocalNarrowPane.current) Text(
|
||||
|
||||
@@ -98,6 +98,33 @@ class SessionSecondScreenTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@Config(sdk = [34], shadows = [PadBridgeRoutingTest.RegisteredKeyEvent::class])
|
||||
fun drawerShortcutsConsumeRepeatsAndReleasesAfterClosing() {
|
||||
val device = InputDeviceBuilder.newBuilder().setId(43).setName("Drawer shortcut test pad")
|
||||
.setDescriptor("drawer-shortcut-pad").setExternal(true).setSources(InputDevice.SOURCE_GAMEPAD).build()
|
||||
shadowOf(context.getSystemService(InputManager::class.java)).addInputDevice(device)
|
||||
SessionState.mode = SessionService.MODE_STEAM
|
||||
val activity = Robolectric.buildActivity(SessionActivity::class.java).get()
|
||||
val open: MutableState<Boolean> = ReflectionHelpers.getField(activity, "drawerOpen\$delegate")
|
||||
val controller: MutableState<Boolean> = ReflectionHelpers.getField(activity, "drawerControllerActive\$delegate")
|
||||
controller.value = true
|
||||
for (button in listOf(KeyEvent.KEYCODE_BUTTON_SELECT, KeyEvent.KEYCODE_BUTTON_START)) {
|
||||
fun event(action: Int, repeat: Int = 0) = KeyEvent(0L, 0L, action, button, repeat, 0,
|
||||
device.id, 0, 0, InputDevice.SOURCE_GAMEPAD)
|
||||
open.value = true
|
||||
assertTrue(activity.dispatchKeyEvent(event(KeyEvent.ACTION_DOWN)))
|
||||
assertEquals(false, open.value)
|
||||
open.value = true
|
||||
assertTrue(activity.dispatchKeyEvent(event(KeyEvent.ACTION_DOWN, 1)))
|
||||
assertEquals(true, open.value)
|
||||
open.value = false
|
||||
assertTrue(activity.dispatchKeyEvent(event(KeyEvent.ACTION_UP)))
|
||||
val held: Set<Pair<Int, Int>> = ReflectionHelpers.getField(activity, "drawerShortcutKeysDown")
|
||||
assertTrue(held.isEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
@Test fun sessionMicrophoneCanBeTurnedOff() {
|
||||
val changes = mutableListOf<Boolean>()
|
||||
compose.setContent {
|
||||
|
||||
Reference in new issue
Block a user