mirror of
https://github.com/tapframe/NuvioStreaming.git
synced 2026-05-16 23:12:12 +00:00
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:
parent
d328d62c66
commit
b73cccc43b
5 changed files with 630 additions and 3 deletions
|
|
@ -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
|
||||
|
|
@ -80,6 +84,15 @@ fun CatalogScreen(
|
|||
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,
|
||||
|
|
@ -152,11 +165,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,
|
||||
|
|
@ -176,7 +189,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
|
||||
|
|
@ -198,6 +211,9 @@ fun CatalogScreen(
|
|||
CatalogHeader(
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
showFilters = isTraktLibrary,
|
||||
selectedFilter = selectedFilter,
|
||||
onFilterSelected = { selectedFilter = it },
|
||||
modifier = Modifier.onSizeChanged { headerHeightPx = it.height },
|
||||
onBack = onBack,
|
||||
)
|
||||
|
|
@ -209,6 +225,9 @@ fun CatalogScreen(
|
|||
private fun CatalogHeader(
|
||||
title: String,
|
||||
subtitle: String,
|
||||
showFilters: Boolean = false,
|
||||
selectedFilter: Int = 0,
|
||||
onFilterSelected: (Int) -> Unit = {},
|
||||
onBack: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
|
|
@ -246,6 +265,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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
120
patch.diff
Normal 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
12
patch2.diff
Normal 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
10
patch3.diff
Normal 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
|
||||
Loading…
Reference in a new issue