This commit is contained in:
Harry Barnes 2026-05-16 01:01:48 +02:00 committed by GitHub
commit fa5a0873b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 645 additions and 5 deletions

View file

@ -11,6 +11,7 @@ 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
@ -21,6 +22,8 @@ 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
@ -47,6 +50,7 @@ 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.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
@ -82,7 +86,16 @@ fun CatalogScreen(
var headerHeightPx by remember { mutableIntStateOf(0) }
var observedOfflineState by remember { mutableStateOf(false) }
LaunchedEffect(manifestUrl, type, catalogId, genre, supportsPagination, homeCatalogSettingsUiState.hideUnreleasedContent) {
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,
@ -100,7 +113,7 @@ fun CatalogScreen(
lastVisible >= layoutInfo.totalItemsCount - 6
}
.distinctUntilChanged()
.filter { it && uiState.canLoadMore && !uiState.isLoading }
.filter { it && uiState.canLoadMore && !uiState.isLoading && selectedFilter == 0 } // Prevent aggressive fetching when filtered
.collect {
CatalogRepository.loadMore()
}
@ -154,11 +167,11 @@ fun CatalogScreen(
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,
@ -178,7 +191,7 @@ fun CatalogScreen(
}
} else {
items(
items = uiState.items.withDuplicateSafeLazyKeys { item -> item.stableKey() },
items = filteredItems.withDuplicateSafeLazyKeys { item -> item.stableKey() },
key = { item -> item.lazyKey },
) { keyedItem ->
val item = keyedItem.value
@ -200,6 +213,9 @@ fun CatalogScreen(
CatalogHeader(
title = title,
subtitle = subtitle,
showFilters = isTraktLibrary,
selectedFilter = selectedFilter,
onFilterSelected = { selectedFilter = it },
modifier = Modifier.onSizeChanged { headerHeightPx = it.height },
onBack = onBack,
)
@ -211,6 +227,9 @@ fun CatalogScreen(
private fun CatalogHeader(
title: String,
subtitle: String,
showFilters: Boolean = false,
selectedFilter: Int = 0,
onFilterSelected: (Int) -> Unit = {},
onBack: () -> Unit,
modifier: Modifier = Modifier,
) {
@ -248,6 +267,41 @@ private fun CatalogHeader(
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,432 @@
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.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
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

12
patch4.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
@@ -107,7 +107,7 @@
val lastVisible = layoutInfo.visibleItemsInfo.lastOrNull()?.index ?: -1
lastVisible >= layoutInfo.totalItemsCount - 6
}
.distinctUntilChanged()
- .filter { it && uiState.canLoadMore && !uiState.isLoading }
+ .filter { it && uiState.canLoadMore && !uiState.isLoading && selectedFilter == 0 } // Prevent aggressive fetching when filtered
.collect {
CatalogRepository.loadMore()
}