From e558bb84af7a0c5edaa79edfa96f3b6ec7fb7e63 Mon Sep 17 00:00:00 2001 From: KhooLy <73142442+KhooLy@users.noreply.github.com> Date: Tue, 21 Jul 2026 14:41:17 +0300 Subject: [PATCH] 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. --- .../com/fluxa/app/shared/FluxaAppHost.kt | 12 +++++++++++ .../com/fluxa/app/shared/TvSidebarNav.kt | 8 ++++++- .../feature/catalog/TvCatalogHomeScreen.kt | 21 +++++++++++++++++-- .../shared/feature/discover/DiscoverScreen.kt | 11 +++++++--- .../shared/feature/library/LibraryScreen.kt | 9 ++++++-- .../com/fluxa/app/ui/catalog/CatalogCard.kt | 4 ++-- .../fluxa/app/ui/catalog/WindowWidthClass.kt | 21 +++++++++++++++++++ 7 files changed, 76 insertions(+), 10 deletions(-) create mode 100644 shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/WindowWidthClass.kt diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/FluxaAppHost.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/FluxaAppHost.kt index 0da4826..f5932ef 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/FluxaAppHost.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/FluxaAppHost.kt @@ -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 ) + } + } } diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/TvSidebarNav.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/TvSidebarNav.kt index 112e03b..d1c7b1c 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/TvSidebarNav.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/TvSidebarNav.kt @@ -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 -> diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/catalog/TvCatalogHomeScreen.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/catalog/TvCatalogHomeScreen.kt index 8d17aa4..d3524c9 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/catalog/TvCatalogHomeScreen.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/catalog/TvCatalogHomeScreen.kt @@ -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) ) { diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/discover/DiscoverScreen.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/discover/DiscoverScreen.kt index 16472d1..c10013f 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/discover/DiscoverScreen.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/discover/DiscoverScreen.kt @@ -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), diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/library/LibraryScreen.kt b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/library/LibraryScreen.kt index 59a1ede..85c7af0 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/library/LibraryScreen.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/shared/feature/library/LibraryScreen.kt @@ -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) }) diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/CatalogCard.kt b/shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/CatalogCard.kt index e821096..b9a3e5b 100644 --- a/shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/CatalogCard.kt +++ b/shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/CatalogCard.kt @@ -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 } diff --git a/shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/WindowWidthClass.kt b/shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/WindowWidthClass.kt new file mode 100644 index 0000000..419263b --- /dev/null +++ b/shared/src/commonMain/kotlin/com/fluxa/app/ui/catalog/WindowWidthClass.kt @@ -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 +}