feat: Add local filters to Trakt Library view all screen

- Added "All", "Movies", and "Series" FilterChips to the `CatalogHeader`.
- Computed `filteredItems` to apply filtering for the Trakt Library screen based on the item type (movies vs series/tv).
- Applied Material Theme `FilterChipDefaults` properties to ensure accurate theming.
This commit is contained in:
harrydbarnes 2026-05-02 18:36:31 +00:00
parent d328d62c66
commit b73cccc43b
5 changed files with 630 additions and 3 deletions

View file

@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
@ -21,6 +22,8 @@ import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -47,6 +50,7 @@ import com.nuvio.app.core.ui.NuvioNetworkOfflineCard
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import com.nuvio.app.core.format.formatReleaseDateForDisplay import com.nuvio.app.core.format.formatReleaseDateForDisplay
import com.nuvio.app.core.ui.NuvioBackButton import com.nuvio.app.core.ui.NuvioBackButton
import com.nuvio.app.features.catalog.INTERNAL_LIBRARY_MANIFEST_URL
import com.nuvio.app.core.ui.rememberPosterCardStyleUiState import com.nuvio.app.core.ui.rememberPosterCardStyleUiState
import com.nuvio.app.core.ui.posterCardClickable import com.nuvio.app.core.ui.posterCardClickable
import com.nuvio.app.core.ui.nuvioSafeBottomPadding import com.nuvio.app.core.ui.nuvioSafeBottomPadding
@ -80,6 +84,15 @@ fun CatalogScreen(
var headerHeightPx by remember { mutableIntStateOf(0) } var headerHeightPx by remember { mutableIntStateOf(0) }
var observedOfflineState by remember { mutableStateOf(false) } var observedOfflineState by remember { mutableStateOf(false) }
val isTraktLibrary = manifestUrl == INTERNAL_LIBRARY_MANIFEST_URL && subtitle.contains("Trakt", ignoreCase = true)
var selectedFilter by remember { mutableIntStateOf(0) }
val filteredItems = remember(uiState.items, selectedFilter) {
if (selectedFilter == 1) uiState.items.filter { it.type.lowercase() in listOf("movie", "film") }
else if (selectedFilter == 2) uiState.items.filter { it.type.lowercase() in listOf("series", "tv", "show") }
else uiState.items
}
LaunchedEffect(manifestUrl, type, catalogId, genre, supportsPagination) { LaunchedEffect(manifestUrl, type, catalogId, genre, supportsPagination) {
CatalogRepository.load( CatalogRepository.load(
manifestUrl = manifestUrl, manifestUrl = manifestUrl,
@ -152,11 +165,11 @@ fun CatalogScreen(
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(18.dp), verticalArrangement = Arrangement.spacedBy(18.dp),
) { ) {
if (uiState.items.isEmpty() && uiState.isLoading) { if (filteredItems.isEmpty() && uiState.isLoading) {
items(columns * 3) { items(columns * 3) {
CatalogSkeletonTile(cornerRadiusDp = posterCardStyle.cornerRadiusDp) CatalogSkeletonTile(cornerRadiusDp = posterCardStyle.cornerRadiusDp)
} }
} else if (uiState.items.isEmpty()) { } else if (filteredItems.isEmpty()) {
item(span = { GridItemSpan(maxLineSpan) }) { item(span = { GridItemSpan(maxLineSpan) }) {
CatalogEmptyState( CatalogEmptyState(
errorMessage = uiState.errorMessage, errorMessage = uiState.errorMessage,
@ -176,7 +189,7 @@ fun CatalogScreen(
} }
} else { } else {
items( items(
items = uiState.items.withDuplicateSafeLazyKeys { item -> item.stableKey() }, items = filteredItems.withDuplicateSafeLazyKeys { item -> item.stableKey() },
key = { item -> item.lazyKey }, key = { item -> item.lazyKey },
) { keyedItem -> ) { keyedItem ->
val item = keyedItem.value val item = keyedItem.value
@ -198,6 +211,9 @@ fun CatalogScreen(
CatalogHeader( CatalogHeader(
title = title, title = title,
subtitle = subtitle, subtitle = subtitle,
showFilters = isTraktLibrary,
selectedFilter = selectedFilter,
onFilterSelected = { selectedFilter = it },
modifier = Modifier.onSizeChanged { headerHeightPx = it.height }, modifier = Modifier.onSizeChanged { headerHeightPx = it.height },
onBack = onBack, onBack = onBack,
) )
@ -209,6 +225,9 @@ fun CatalogScreen(
private fun CatalogHeader( private fun CatalogHeader(
title: String, title: String,
subtitle: String, subtitle: String,
showFilters: Boolean = false,
selectedFilter: Int = 0,
onFilterSelected: (Int) -> Unit = {},
onBack: () -> Unit, onBack: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
@ -246,6 +265,41 @@ private fun CatalogHeader(
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
) )
} }
if (showFilters) {
Spacer(modifier = Modifier.height(12.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
FilterChip(
selected = selectedFilter == 0,
onClick = { onFilterSelected(0) },
label = { Text(stringResource(Res.string.collections_tab_all)) },
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
selectedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer,
),
shape = RoundedCornerShape(16.dp),
)
FilterChip(
selected = selectedFilter == 1,
onClick = { onFilterSelected(1) },
label = { Text(stringResource(Res.string.media_movies)) },
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
selectedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer,
),
shape = RoundedCornerShape(16.dp),
)
FilterChip(
selected = selectedFilter == 2,
onClick = { onFilterSelected(2) },
label = { Text(stringResource(Res.string.media_series)) },
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
selectedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer,
),
shape = RoundedCornerShape(16.dp),
)
}
}
} }
} }

View file

@ -0,0 +1,431 @@
package com.nuvio.app.features.catalog
import androidx.compose.foundation.background
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.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.FilterChip
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onSizeChanged
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.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.nuvio.app.core.network.NetworkCondition
import com.nuvio.app.core.network.NetworkStatusRepository
import com.nuvio.app.core.ui.NuvioNetworkOfflineCard
import coil3.compose.AsyncImage
import com.nuvio.app.core.format.formatReleaseDateForDisplay
import com.nuvio.app.core.ui.NuvioBackButton
import com.nuvio.app.core.ui.rememberPosterCardStyleUiState
import com.nuvio.app.core.ui.posterCardClickable
import com.nuvio.app.core.ui.nuvioSafeBottomPadding
import com.nuvio.app.core.ui.withDuplicateSafeLazyKeys
import com.nuvio.app.features.home.MetaPreview
import com.nuvio.app.features.home.PosterShape
import com.nuvio.app.features.home.stableKey
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import nuvio.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource
@Composable
fun CatalogScreen(
title: String,
subtitle: String,
manifestUrl: String,
type: String,
catalogId: String,
supportsPagination: Boolean,
genre: String? = null,
onBack: () -> Unit,
onPosterClick: ((MetaPreview) -> Unit)? = null,
modifier: Modifier = Modifier,
) {
val uiState by CatalogRepository.uiState.collectAsStateWithLifecycle()
val posterCardStyle = rememberPosterCardStyleUiState()
val networkStatusUiState by NetworkStatusRepository.uiState.collectAsStateWithLifecycle()
val gridState = rememberLazyGridState()
var headerHeightPx by remember { mutableIntStateOf(0) }
var observedOfflineState by remember { mutableStateOf(false) }
val isTraktLibrary = manifestUrl == INTERNAL_LIBRARY_MANIFEST_URL && subtitle.contains("Trakt", ignoreCase = true)
var selectedFilter by remember { mutableIntStateOf(0) }
val filteredItems = remember(uiState.items, selectedFilter) {
if (selectedFilter == 1) uiState.items.filter { it.type.lowercase() in listOf("movie", "film") }
else if (selectedFilter == 2) uiState.items.filter { it.type.lowercase() in listOf("series", "tv", "show") }
else uiState.items
}
LaunchedEffect(manifestUrl, type, catalogId, genre, supportsPagination) {
CatalogRepository.load(
manifestUrl = manifestUrl,
type = type,
catalogId = catalogId,
genre = genre,
supportsPagination = supportsPagination,
force = false,
)
}
LaunchedEffect(gridState, uiState.canLoadMore, uiState.isLoading) {
snapshotFlow { gridState.layoutInfo }
.map { layoutInfo ->
val lastVisible = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
lastVisible >= layoutInfo.totalItemsCount - 6
}
.distinctUntilChanged()
.filter { it && uiState.canLoadMore && !uiState.isLoading }
.collect {
CatalogRepository.loadMore()
}
}
LaunchedEffect(networkStatusUiState.condition, manifestUrl, type, catalogId, genre, supportsPagination) {
when (networkStatusUiState.condition) {
NetworkCondition.NoInternet,
NetworkCondition.ServersUnreachable,
-> {
observedOfflineState = true
}
NetworkCondition.Online -> {
if (!observedOfflineState) return@LaunchedEffect
observedOfflineState = false
CatalogRepository.load(
manifestUrl = manifestUrl,
type = type,
catalogId = catalogId,
genre = genre,
supportsPagination = supportsPagination,
force = true,
)
}
NetworkCondition.Unknown,
NetworkCondition.Checking,
-> Unit
}
}
BoxWithConstraints(
modifier = modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background),
) {
val columns = remember(maxWidth) { catalogGridColumnsForWidth(maxWidth) }
Box(modifier = Modifier.fillMaxSize()) {
LazyVerticalGrid(
columns = GridCells.Fixed(columns),
state = gridState,
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(
start = 16.dp,
top = with(androidx.compose.ui.platform.LocalDensity.current) { headerHeightPx.toDp() } + 12.dp,
end = 16.dp,
bottom = nuvioSafeBottomPadding(28.dp),
),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(18.dp),
) {
if (filteredItems.isEmpty() && uiState.isLoading) {
items(columns * 3) {
CatalogSkeletonTile(cornerRadiusDp = posterCardStyle.cornerRadiusDp)
}
} else if (filteredItems.isEmpty()) {
item(span = { GridItemSpan(maxLineSpan) }) {
CatalogEmptyState(
errorMessage = uiState.errorMessage,
networkCondition = networkStatusUiState.condition,
onRetry = {
NetworkStatusRepository.requestRefresh(force = true)
CatalogRepository.load(
manifestUrl = manifestUrl,
type = type,
catalogId = catalogId,
genre = genre,
supportsPagination = supportsPagination,
force = true,
)
},
)
}
} else {
items(
items = filteredItems.withDuplicateSafeLazyKeys { item -> item.stableKey() },
key = { item -> item.lazyKey },
) { keyedItem ->
val item = keyedItem.value
CatalogPosterTile(
item = item,
cornerRadiusDp = posterCardStyle.cornerRadiusDp,
hideLabels = posterCardStyle.hideLabelsEnabled,
onClick = onPosterClick?.let { { it(item) } },
)
}
if (uiState.isLoading) {
item(span = { GridItemSpan(maxLineSpan) }) {
CatalogLoadingFooter()
}
}
}
}
CatalogHeader(
title = title,
subtitle = subtitle,
showFilters = isTraktLibrary,
selectedFilter = selectedFilter,
onFilterSelected = { selectedFilter = it },
modifier = Modifier.onSizeChanged { headerHeightPx = it.height },
onBack = onBack,
)
}
}
}
@Composable
private fun CatalogHeader(
title: String,
subtitle: String,
showFilters: Boolean = false,
selectedFilter: Int = 0,
onFilterSelected: (Int) -> Unit = {},
onBack: () -> Unit,
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background)
.padding(horizontal = 16.dp)
.padding(top = 52.dp, bottom = 12.dp),
) {
NuvioBackButton(
onClick = onBack,
modifier = Modifier
.size(40.dp),
containerColor = MaterialTheme.colorScheme.surface,
contentColor = MaterialTheme.colorScheme.onSurface,
iconSize = 24.dp,
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = title,
style = MaterialTheme.typography.displaySmall,
color = MaterialTheme.colorScheme.onBackground,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
if (subtitle.isNotBlank()) {
Spacer(modifier = Modifier.height(6.dp))
Text(
text = subtitle,
style = MaterialTheme.typography.bodyMedium.copy(
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
),
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
if (showFilters) {
Spacer(modifier = Modifier.height(12.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
FilterChip(
selected = selectedFilter == 0,
onClick = { onFilterSelected(0) },
label = { Text(stringResource(Res.string.collections_tab_all)) },
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
selectedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer,
),
shape = RoundedCornerShape(16.dp),
)
FilterChip(
selected = selectedFilter == 1,
onClick = { onFilterSelected(1) },
label = { Text(stringResource(Res.string.media_movies)) },
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
selectedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer,
),
shape = RoundedCornerShape(16.dp),
)
FilterChip(
selected = selectedFilter == 2,
onClick = { onFilterSelected(2) },
label = { Text(stringResource(Res.string.media_series)) },
colors = FilterChipDefaults.filterChipColors(
selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
selectedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer,
),
shape = RoundedCornerShape(16.dp),
)
}
}
}
}
@Composable
private fun CatalogPosterTile(
item: MetaPreview,
cornerRadiusDp: Int,
hideLabels: Boolean,
onClick: (() -> Unit)? = null,
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(item.posterShape.catalogAspectRatio())
.clip(RoundedCornerShape(cornerRadiusDp.dp))
.background(MaterialTheme.colorScheme.surface)
.posterCardClickable(onClick = onClick, onLongClick = null),
) {
if (item.poster != null) {
AsyncImage(
model = item.poster,
contentDescription = item.name,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop,
)
}
}
if (!hideLabels) {
Text(
text = item.name,
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
color = MaterialTheme.colorScheme.onBackground,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
val detail = item.releaseInfo?.let { formatReleaseDateForDisplay(it) }
if (detail != null) {
Text(
text = detail,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
} else {
Spacer(modifier = Modifier.height(8.dp))
}
}
}
}
@Composable
private fun CatalogSkeletonTile(cornerRadiusDp: Int) {
Box(
modifier = Modifier
.fillMaxWidth()
.aspectRatio(0.68f)
.clip(RoundedCornerShape(cornerRadiusDp.dp))
.background(MaterialTheme.colorScheme.surface),
)
}
@Composable
private fun CatalogEmptyState(
errorMessage: String?,
networkCondition: NetworkCondition,
onRetry: (() -> Unit)? = null,
) {
if (networkCondition == NetworkCondition.NoInternet || networkCondition == NetworkCondition.ServersUnreachable) {
NuvioNetworkOfflineCard(
condition = networkCondition,
onRetry = onRetry,
)
return
}
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 48.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
Text(
text = stringResource(Res.string.catalog_empty_title),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onBackground,
)
Text(
text = errorMessage ?: stringResource(Res.string.catalog_empty_message),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
@Composable
private fun CatalogLoadingFooter() {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 12.dp),
contentAlignment = Alignment.Center,
) {
CircularProgressIndicator(
modifier = Modifier.size(22.dp),
color = MaterialTheme.colorScheme.primary,
strokeWidth = 2.dp,
)
}
}
private fun PosterShape.catalogAspectRatio(): Float =
when (this) {
PosterShape.Poster -> 0.68f
PosterShape.Square -> 1f
PosterShape.Landscape -> 1.2f
}
private fun catalogGridColumnsForWidth(screenWidth: Dp): Int =
when {
screenWidth >= 1400.dp -> 7
screenWidth >= 1200.dp -> 6
screenWidth >= 1000.dp -> 5
screenWidth >= 840.dp -> 4
else -> 3
}

120
patch.diff Normal file
View file

@ -0,0 +1,120 @@
--- composeApp/src/commonMain/kotlin/com/nuvio/app/features/catalog/CatalogScreen.kt
+++ composeApp/src/commonMain/kotlin/com/nuvio/app/features/catalog/CatalogScreen.kt
@@ -10,6 +10,7 @@
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.grid.GridCells
@@ -19,6 +20,8 @@
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.FilterChip
+import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -83,6 +86,16 @@
var headerHeightPx by remember { mutableIntStateOf(0) }
var observedOfflineState by remember { mutableStateOf(false) }
+ val isTraktLibrary = manifestUrl == INTERNAL_LIBRARY_MANIFEST_URL &&
+ subtitle == stringResource(Res.string.compose_catalog_subtitle_trakt_library)
+ var selectedFilter by remember { mutableIntStateOf(0) }
+
+ val filteredItems = remember(uiState.items, selectedFilter) {
+ if (selectedFilter == 1) uiState.items.filter { it.type.lowercase() in listOf("movie", "film") }
+ else if (selectedFilter == 2) uiState.items.filter { it.type.lowercase() in listOf("series", "tv", "show") }
+ else uiState.items
+ }
+
LaunchedEffect(manifestUrl, type, catalogId, genre, supportsPagination) {
CatalogRepository.load(
manifestUrl = manifestUrl,
@@ -149,11 +162,11 @@
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(18.dp),
) {
- if (uiState.items.isEmpty() && uiState.isLoading) {
+ if (filteredItems.isEmpty() && uiState.isLoading) {
items(columns * 3) {
CatalogSkeletonTile(cornerRadiusDp = posterCardStyle.cornerRadiusDp)
}
- } else if (uiState.items.isEmpty()) {
+ } else if (filteredItems.isEmpty()) {
item(span = { GridItemSpan(maxLineSpan) }) {
CatalogEmptyState(
errorMessage = uiState.errorMessage,
@@ -172,7 +185,7 @@
}
} else {
items(
- items = uiState.items.withDuplicateSafeLazyKeys { item -> item.stableKey() },
+ items = filteredItems.withDuplicateSafeLazyKeys { item -> item.stableKey() },
key = { item -> item.lazyKey },
) { keyedItem ->
val item = keyedItem.value
@@ -194,6 +207,9 @@
CatalogHeader(
title = title,
subtitle = subtitle,
+ showFilters = isTraktLibrary,
+ selectedFilter = selectedFilter,
+ onFilterSelected = { selectedFilter = it },
modifier = Modifier.onSizeChanged { headerHeightPx = it.height },
onBack = onBack,
)
@@ -206,6 +222,9 @@
private fun CatalogHeader(
title: String,
subtitle: String,
+ showFilters: Boolean = false,
+ selectedFilter: Int = 0,
+ onFilterSelected: (Int) -> Unit = {},
onBack: () -> Unit,
modifier: Modifier = Modifier,
) {
@@ -237,6 +256,41 @@
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
+ if (showFilters) {
+ Spacer(modifier = Modifier.height(12.dp))
+ Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
+ FilterChip(
+ selected = selectedFilter == 0,
+ onClick = { onFilterSelected(0) },
+ label = { Text(stringResource(Res.string.collections_tab_all)) },
+ colors = FilterChipDefaults.filterChipColors(
+ selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
+ selectedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer,
+ ),
+ shape = RoundedCornerShape(16.dp),
+ )
+ FilterChip(
+ selected = selectedFilter == 1,
+ onClick = { onFilterSelected(1) },
+ label = { Text(stringResource(Res.string.media_movies)) },
+ colors = FilterChipDefaults.filterChipColors(
+ selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
+ selectedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer,
+ ),
+ shape = RoundedCornerShape(16.dp),
+ )
+ FilterChip(
+ selected = selectedFilter == 2,
+ onClick = { onFilterSelected(2) },
+ label = { Text(stringResource(Res.string.media_series)) },
+ colors = FilterChipDefaults.filterChipColors(
+ selectedContainerColor = MaterialTheme.colorScheme.primaryContainer,
+ selectedLabelColor = MaterialTheme.colorScheme.onPrimaryContainer,
+ ),
+ shape = RoundedCornerShape(16.dp),
+ )
+ }
+ }
}
}

12
patch2.diff Normal file
View file

@ -0,0 +1,12 @@
--- composeApp/src/commonMain/kotlin/com/nuvio/app/features/catalog/CatalogScreen.kt
+++ composeApp/src/commonMain/kotlin/com/nuvio/app/features/catalog/CatalogScreen.kt
@@ -86,8 +86,7 @@
var headerHeightPx by remember { mutableIntStateOf(0) }
var observedOfflineState by remember { mutableStateOf(false) }
- val isTraktLibrary = manifestUrl == INTERNAL_LIBRARY_MANIFEST_URL &&
- subtitle == stringResource(Res.string.compose_catalog_subtitle_trakt_library)
+ val isTraktLibrary = manifestUrl == INTERNAL_LIBRARY_MANIFEST_URL && subtitle.contains("Trakt", ignoreCase = true)
var selectedFilter by remember { mutableIntStateOf(0) }
val filteredItems = remember(uiState.items, selectedFilter) {

10
patch3.diff Normal file
View file

@ -0,0 +1,10 @@
--- composeApp/src/commonMain/kotlin/com/nuvio/app/features/catalog/CatalogScreen.kt
+++ composeApp/src/commonMain/kotlin/com/nuvio/app/features/catalog/CatalogScreen.kt
@@ -37,6 +37,7 @@
import coil3.compose.AsyncImage
import com.nuvio.app.core.format.formatReleaseDateForDisplay
import com.nuvio.app.core.ui.NuvioBackButton
+import com.nuvio.app.features.catalog.INTERNAL_LIBRARY_MANIFEST_URL
import com.nuvio.app.core.ui.rememberPosterCardStyleUiState
import com.nuvio.app.core.ui.posterCardClickable
import com.nuvio.app.core.ui.nuvioSafeBottomPadding