Add window width class and TV focus restoration

Introduce a runtime WindowWidthClass (Compact/Medium/Expanded) provided at
the app root and use it to drive responsive grid columns on Discover and
Library. Add initial focus, focusRestorer, and focusGroup to the TV home and
sidebar, apply focusRestorer to the shared Discover/Library grids, and
strengthen the focused card highlight.
This commit is contained in:
KhooLy 2026-07-21 14:41:17 +03:00
parent 0860871297
commit e558bb84af
7 changed files with 76 additions and 10 deletions

View file

@ -1,6 +1,9 @@
package com.fluxa.app.shared
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@ -356,6 +359,13 @@ fun FluxaAppHost(
}
}
BoxWithConstraints(modifier = Modifier.fillMaxSize()) {
val widthClass = if (deviceType == com.fluxa.app.ui.catalog.DeviceType.TV) {
com.fluxa.app.ui.catalog.WindowWidthClass.Expanded
} else {
com.fluxa.app.ui.catalog.widthClassFor(maxWidth)
}
CompositionLocalProvider(com.fluxa.app.ui.catalog.LocalWindowWidthClass provides widthClass) {
FluxaApp(
state = appState.uiState,
deviceType = deviceType,
@ -557,4 +567,6 @@ fun FluxaAppHost(
showNavigationBar = showNavigationBar,
modifier = modifier
)
}
}
}

View file

@ -1,7 +1,10 @@
@file:OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
package com.fluxa.app.shared
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusGroup
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -26,6 +29,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
@ -59,7 +63,9 @@ fun TvSidebarNav(
.fillMaxHeight()
.width(84.dp)
.background(Color.Black.copy(alpha = 0.55f))
.padding(vertical = 24.dp),
.padding(vertical = 24.dp)
.focusGroup()
.focusRestorer(),
verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically)
) {
TvSidebarItems.forEach { item ->

View file

@ -1,3 +1,5 @@
@file:OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
package com.fluxa.app.shared.feature.catalog
import androidx.compose.foundation.background
@ -18,10 +20,16 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.foundation.focusGroup
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
@ -38,17 +46,25 @@ fun TvCatalogHomeScreen(
language: String? = null,
modifier: Modifier = Modifier
) {
val columnFocus = remember { FocusRequester() }
Box(modifier = modifier.fillMaxSize()) {
if (state.rows.isEmpty()) {
TvCatalogHomeLoading(Modifier.fillMaxSize())
} else {
LaunchedEffect(Unit) { runCatching { columnFocus.requestFocus() } }
LazyColumn(
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxSize()
.focusRequester(columnFocus)
.focusRestorer(),
contentPadding = PaddingValues(top = 44.dp, bottom = 64.dp),
verticalArrangement = Arrangement.spacedBy(30.dp)
) {
items(state.rows, key = { it.id }) { row ->
Column(verticalArrangement = Arrangement.spacedBy(14.dp)) {
Column(
modifier = Modifier.focusGroup(),
verticalArrangement = Arrangement.spacedBy(14.dp)
) {
Text(
text = row.title,
color = Color.White,
@ -59,6 +75,7 @@ fun TvCatalogHomeScreen(
modifier = Modifier.padding(horizontal = 58.dp)
)
LazyRow(
modifier = Modifier.focusRestorer(),
contentPadding = PaddingValues(horizontal = 58.dp),
horizontalArrangement = Arrangement.spacedBy(20.dp)
) {

View file

@ -1,8 +1,11 @@
@file:OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
package com.fluxa.app.shared.feature.discover
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.ui.focus.focusRestorer
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -59,6 +62,8 @@ import com.fluxa.app.shared.feature.search.SearchResultRows
import com.fluxa.app.shared.feature.search.SearchResults
import com.fluxa.app.ui.catalog.CatalogCard
import com.fluxa.app.ui.catalog.FluxaColors
import com.fluxa.app.ui.catalog.LocalWindowWidthClass
import com.fluxa.app.ui.catalog.gridColumns
@Composable
fun DiscoverScreen(
@ -141,8 +146,8 @@ fun DiscoverScreen(
}
LazyVerticalGrid(
state = gridState,
columns = GridCells.Fixed(3),
modifier = Modifier.weight(1f).fillMaxWidth(),
columns = GridCells.Fixed(LocalWindowWidthClass.current.gridColumns()),
modifier = Modifier.weight(1f).fillMaxWidth().focusRestorer(),
contentPadding = PaddingValues(bottom = 120.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
@ -201,7 +206,7 @@ private fun DiscoverSearchField(
@Composable
private fun DiscoverSkeletonGrid(modifier: Modifier = Modifier) {
LazyVerticalGrid(
columns = GridCells.Fixed(3),
columns = GridCells.Fixed(LocalWindowWidthClass.current.gridColumns()),
modifier = modifier.fillMaxWidth(),
contentPadding = PaddingValues(bottom = 120.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),

View file

@ -1,3 +1,5 @@
@file:OptIn(androidx.compose.foundation.ExperimentalFoundationApi::class)
package com.fluxa.app.shared.feature.library
import androidx.compose.foundation.background
@ -43,6 +45,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRestorer
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
@ -55,6 +58,8 @@ import com.fluxa.app.shared.feature.discover.DiscoverFilterOptionUiModel
import com.fluxa.app.shared.image.FluxaRemoteImage
import com.fluxa.app.ui.catalog.CatalogCard
import com.fluxa.app.ui.catalog.FluxaColors
import com.fluxa.app.ui.catalog.LocalWindowWidthClass
import com.fluxa.app.ui.catalog.gridColumns
@Composable
fun LibraryScreen(
@ -265,11 +270,11 @@ private fun LibraryItemGrid(
return
}
LazyVerticalGrid(
columns = GridCells.Fixed(3),
columns = GridCells.Fixed(LocalWindowWidthClass.current.gridColumns()),
contentPadding = PaddingValues(start = 16.dp, end = 16.dp, top = 12.dp, bottom = 120.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxSize()
modifier = Modifier.fillMaxSize().focusRestorer()
) {
items(items, key = { "${it.type}:${it.id}" }) { item ->
CatalogCard(model = item.card.copy(showTitleBar = true), onClick = { onItemSelected(item) })

View file

@ -72,8 +72,8 @@ fun CatalogCard(
.then(
if (focused) {
Modifier
.scale(1.05f)
.border(2.dp, Color.White, RoundedCornerShape(8.dp))
.scale(1.1f)
.border(3.dp, Color.White, RoundedCornerShape(8.dp))
} else {
Modifier
}

View file

@ -0,0 +1,21 @@
package com.fluxa.app.ui.catalog
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
enum class WindowWidthClass { Compact, Medium, Expanded }
val LocalWindowWidthClass = compositionLocalOf { WindowWidthClass.Compact }
fun widthClassFor(maxWidth: Dp): WindowWidthClass = when {
maxWidth < 600.dp -> WindowWidthClass.Compact
maxWidth < 840.dp -> WindowWidthClass.Medium
else -> WindowWidthClass.Expanded
}
fun WindowWidthClass.gridColumns(): Int = when (this) {
WindowWidthClass.Compact -> 3
WindowWidthClass.Medium -> 5
WindowWidthClass.Expanded -> 7
}