ref: adjust cloud library ui and sorting

This commit is contained in:
tapframe 2026-05-21 15:11:42 +05:30
parent 0bfd2cb99c
commit 800d7160b1
4 changed files with 253 additions and 219 deletions

View file

@ -1350,6 +1350,8 @@
<string name="cloud_library_playable_file_count">%1$d playable files</string>
<string name="cloud_library_provider_all">All</string>
<string name="cloud_library_refresh">Refresh cloud library</string>
<string name="cloud_library_select_provider">Select provider</string>
<string name="cloud_library_select_type">Select type</string>
<string name="cloud_library_status_ready">Ready to play</string>
<string name="cloud_library_type_all">All</string>
<string name="cloud_library_type_torrents">Torrents</string>

View file

@ -0,0 +1,167 @@
package com.nuvio.app.core.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Check
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SheetState
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
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.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
data class NuvioDropdownOption(
val key: String,
val label: String,
)
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NuvioDropdownChip(
title: String,
label: String,
selectedKey: String?,
options: List<NuvioDropdownOption>,
enabled: Boolean = true,
onSelected: (NuvioDropdownOption) -> Unit,
modifier: Modifier = Modifier,
) {
var isSheetVisible by remember { mutableStateOf(false) }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val coroutineScope = rememberCoroutineScope()
Row(
modifier = modifier
.clip(RoundedCornerShape(12.dp))
.background(MaterialTheme.colorScheme.surface)
.then(
if (enabled) {
Modifier.clickable { isSheetVisible = true }
} else {
Modifier
},
)
.padding(horizontal = 12.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = label,
style = MaterialTheme.typography.labelLarge,
color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = if (enabled) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.outline,
)
}
if (isSheetVisible) {
NuvioDropdownOptionsSheet(
title = title,
options = options,
selectedKey = selectedKey,
sheetState = sheetState,
onDismiss = {
coroutineScope.launch {
dismissNuvioBottomSheet(
sheetState = sheetState,
onDismiss = { isSheetVisible = false },
)
}
},
onSelected = { option ->
onSelected(option)
coroutineScope.launch {
dismissNuvioBottomSheet(
sheetState = sheetState,
onDismiss = { isSheetVisible = false },
)
}
},
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun NuvioDropdownOptionsSheet(
title: String,
options: List<NuvioDropdownOption>,
selectedKey: String?,
sheetState: SheetState,
onDismiss: () -> Unit,
onSelected: (NuvioDropdownOption) -> Unit,
) {
NuvioModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = nuvioSafeBottomPadding(16.dp)),
) {
Text(
text = title,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
)
NuvioBottomSheetDivider()
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 420.dp),
) {
itemsIndexed(options) { index, option ->
NuvioBottomSheetActionRow(
title = option.label,
onClick = { onSelected(option) },
trailingContent = {
if (option.key == selectedKey) {
Icon(
imageVector = Icons.Rounded.Check,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp),
)
}
},
)
if (index < options.lastIndex) {
NuvioBottomSheetDivider()
}
}
}
}
}
}

View file

@ -9,6 +9,7 @@ import androidx.compose.animation.core.tween
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -19,10 +20,10 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.InsertDriveFile
@ -57,6 +58,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.nuvio.app.core.i18n.localizedByteUnit
import com.nuvio.app.core.network.NetworkCondition
import com.nuvio.app.core.network.NetworkStatusRepository
import com.nuvio.app.core.ui.NuvioDropdownChip
import com.nuvio.app.core.ui.NuvioDropdownOption
import com.nuvio.app.core.ui.NuvioScreen
import com.nuvio.app.core.ui.NuvioNetworkOfflineCard
import com.nuvio.app.core.ui.NuvioScreenHeader
@ -204,6 +207,7 @@ fun LibraryScreen(
selectedCloudItemKey = selectedCloudItemKey,
onProviderSelected = {
selectedProviderId = it
selectedTypeName = null
selectedCloudItemKey = null
},
onTypeSelected = {
@ -337,9 +341,15 @@ private fun LazyListScope.cloudLibraryContent(
}
else -> {
val filteredItems = uiState.items
val providerItems = uiState.items
.filter { item -> selectedProviderId == null || item.providerId == selectedProviderId }
.filter { item -> selectedType == null || item.type == selectedType }
val availableTypes = providerItems
.map { item -> item.type }
.distinct()
.sortedBy { type -> type.ordinal }
val effectiveSelectedType = selectedType?.takeIf { type -> type in availableTypes }
val filteredItems = providerItems
.filter { item -> effectiveSelectedType == null || item.type == effectiveSelectedType }
val selectedItem = filteredItems.firstOrNull { it.stableKey == selectedCloudItemKey }
if (selectedItem != null) {
@ -355,7 +365,8 @@ private fun LazyListScope.cloudLibraryContent(
CloudLibraryToolbar(
uiState = uiState,
selectedProviderId = selectedProviderId,
selectedType = selectedType,
selectedType = effectiveSelectedType,
availableTypes = availableTypes,
onProviderSelected = onProviderSelected,
onTypeSelected = onTypeSelected,
onRefresh = onRefresh,
@ -445,11 +456,41 @@ private fun CloudLibraryToolbar(
uiState: CloudLibraryUiState,
selectedProviderId: String?,
selectedType: CloudLibraryItemType?,
availableTypes: List<CloudLibraryItemType>,
onProviderSelected: (String?) -> Unit,
onTypeSelected: (CloudLibraryItemType?) -> Unit,
onRefresh: () -> Unit,
modifier: Modifier = Modifier,
) {
val providerOptions = buildList {
add(NuvioDropdownOption(key = "", label = stringResource(Res.string.cloud_library_provider_all)))
addAll(
uiState.providers.map { provider ->
NuvioDropdownOption(
key = provider.providerId,
label = provider.providerName,
)
},
)
}
val typeOptions = buildList {
add(NuvioDropdownOption(key = "", label = stringResource(Res.string.cloud_library_type_all)))
addAll(
availableTypes.map { type ->
NuvioDropdownOption(
key = type.name,
label = cloudLibraryTypeLabel(type),
)
},
)
}
val selectedProviderName = uiState.providers
.firstOrNull { provider -> provider.providerId == selectedProviderId }
?.providerName
?: stringResource(Res.string.cloud_library_provider_all)
val selectedTypeLabel = selectedType?.let { type -> cloudLibraryTypeLabel(type) }
?: stringResource(Res.string.cloud_library_type_all)
Column(
modifier = modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp),
@ -460,30 +501,35 @@ private fun CloudLibraryToolbar(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
LazyRow(
modifier = Modifier.weight(1f),
Row(
modifier = Modifier
.weight(1f)
.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(end = 8.dp),
) {
item {
LibraryChip(
label = stringResource(Res.string.cloud_library_provider_all),
selected = selectedProviderId == null,
onClick = { onProviderSelected(null) },
)
}
items(
items = uiState.providers,
key = { provider -> provider.providerId },
) { provider ->
LibraryChip(
label = provider.providerName,
selected = selectedProviderId == provider.providerId,
loading = provider.isLoading,
error = !provider.errorMessage.isNullOrBlank(),
onClick = { onProviderSelected(provider.providerId) },
)
}
NuvioDropdownChip(
title = stringResource(Res.string.cloud_library_select_provider),
label = selectedProviderName,
selectedKey = selectedProviderId.orEmpty(),
options = providerOptions,
enabled = providerOptions.size > 1,
onSelected = { option ->
onProviderSelected(option.key.ifBlank { null })
},
)
NuvioDropdownChip(
title = stringResource(Res.string.cloud_library_select_type),
label = selectedTypeLabel,
selectedKey = selectedType?.name.orEmpty(),
options = typeOptions,
enabled = typeOptions.size > 1,
onSelected = { option ->
val type = option.key
.takeIf { it.isNotBlank() }
?.let(CloudLibraryItemType::valueOf)
onTypeSelected(type)
},
)
}
IconButton(onClick = onRefresh) {
Icon(
@ -493,28 +539,6 @@ private fun CloudLibraryToolbar(
)
}
}
LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(end = 16.dp),
) {
item {
LibraryChip(
label = stringResource(Res.string.cloud_library_type_all),
selected = selectedType == null,
onClick = { onTypeSelected(null) },
)
}
items(
items = CloudLibraryItemType.entries,
key = { type -> type.name },
) { type ->
LibraryChip(
label = cloudLibraryTypeLabel(type),
selected = selectedType == type,
onClick = { onTypeSelected(type) },
)
}
}
}
}
@ -841,24 +865,16 @@ private fun CloudLibrarySkeletonToolbar(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
CloudSkeletonBlock(brush = brush, width = 52.dp, height = 34.dp, cornerRadius = 18.dp)
CloudSkeletonBlock(brush = brush, width = 86.dp, height = 34.dp, cornerRadius = 18.dp)
CloudSkeletonBlock(brush = brush, width = 92.dp, height = 34.dp, cornerRadius = 12.dp)
CloudSkeletonBlock(brush = brush, width = 78.dp, height = 34.dp, cornerRadius = 12.dp)
CloudSkeletonBlock(
brush = brush,
modifier = Modifier.weight(1f),
height = 34.dp,
cornerRadius = 18.dp,
cornerRadius = 12.dp,
)
CloudSkeletonBlock(brush = brush, width = 40.dp, height = 40.dp, cornerRadius = 20.dp)
}
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
CloudSkeletonBlock(brush = brush, width = 52.dp, height = 34.dp, cornerRadius = 18.dp)
CloudSkeletonBlock(brush = brush, width = 82.dp, height = 34.dp, cornerRadius = 18.dp)
CloudSkeletonBlock(brush = brush, width = 72.dp, height = 34.dp, cornerRadius = 18.dp)
CloudSkeletonBlock(brush = brush, width = 60.dp, height = 34.dp, cornerRadius = 18.dp)
}
}
}

View file

@ -2,7 +2,6 @@ package com.nuvio.app.features.search
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -13,31 +12,16 @@ 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.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Check
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SheetState
import androidx.compose.material3.Text
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@ -49,12 +33,9 @@ import androidx.compose.ui.unit.sp
import coil3.compose.AsyncImage
import com.nuvio.app.core.network.NetworkCondition
import com.nuvio.app.core.format.formatReleaseDateForDisplay
import com.nuvio.app.core.ui.NuvioDropdownChip
import com.nuvio.app.core.ui.NuvioDropdownOption
import com.nuvio.app.core.ui.NuvioNetworkOfflineCard
import com.nuvio.app.core.ui.NuvioBottomSheetActionRow
import com.nuvio.app.core.ui.NuvioBottomSheetDivider
import com.nuvio.app.core.ui.NuvioModalBottomSheet
import com.nuvio.app.core.ui.dismissNuvioBottomSheet
import com.nuvio.app.core.ui.nuvioSafeBottomPadding
import com.nuvio.app.core.ui.NuvioPosterWatchedOverlay
import com.nuvio.app.core.ui.rememberPosterCardStyleUiState
import com.nuvio.app.core.ui.posterCardClickable
@ -62,7 +43,6 @@ import com.nuvio.app.features.home.MetaPreview
import com.nuvio.app.features.home.PosterShape
import com.nuvio.app.features.home.components.HomeEmptyStateCard
import com.nuvio.app.features.watching.application.WatchingState
import kotlinx.coroutines.launch
import nuvio.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource
@ -174,19 +154,19 @@ private fun DiscoverFilterRow(
modifier = modifier.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
DiscoverDropdownChip(
NuvioDropdownChip(
title = stringResource(Res.string.discover_select_type),
label = state.selectedType?.displayTypeLabel() ?: stringResource(Res.string.discover_type),
selectedKey = state.selectedType,
options = state.typeOptions.map { DiscoverOptionItem(key = it, label = it.displayTypeLabel()) },
options = state.typeOptions.map { NuvioDropdownOption(key = it, label = it.displayTypeLabel()) },
enabled = state.typeOptions.isNotEmpty(),
onSelected = { onTypeSelected(it.key) },
)
DiscoverDropdownChip(
NuvioDropdownChip(
title = stringResource(Res.string.discover_select_catalog),
label = state.selectedCatalog?.catalogName ?: stringResource(Res.string.discover_catalog),
selectedKey = state.selectedCatalogKey,
options = state.catalogOptions.map { option -> DiscoverOptionItem(key = option.key, label = option.catalogName) },
options = state.catalogOptions.map { option -> NuvioDropdownOption(key = option.key, label = option.catalogName) },
enabled = state.catalogOptions.isNotEmpty(),
onSelected = { onCatalogSelected(it.key) },
)
@ -194,11 +174,11 @@ private fun DiscoverFilterRow(
val selectedCatalog = state.selectedCatalog
val genreOptions = buildList {
if (selectedCatalog?.genreRequired != true) {
add(DiscoverOptionItem(key = "", label = stringResource(Res.string.discover_all_genres)))
add(NuvioDropdownOption(key = "", label = stringResource(Res.string.discover_all_genres)))
}
addAll(state.genreOptions.map { genre -> DiscoverOptionItem(key = genre, label = genre) })
addAll(state.genreOptions.map { genre -> NuvioDropdownOption(key = genre, label = genre) })
}
DiscoverDropdownChip(
NuvioDropdownChip(
title = stringResource(Res.string.discover_select_genre),
label = state.selectedGenre ?: stringResource(Res.string.discover_all_genres),
selectedKey = state.selectedGenre ?: "",
@ -211,132 +191,6 @@ private fun DiscoverFilterRow(
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun DiscoverDropdownChip(
title: String,
label: String,
selectedKey: String?,
options: List<DiscoverOptionItem>,
enabled: Boolean,
onSelected: (DiscoverOptionItem) -> Unit,
) {
var isSheetVisible by remember { mutableStateOf(false) }
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val coroutineScope = rememberCoroutineScope()
Row(
modifier = Modifier
.clip(RoundedCornerShape(12.dp))
.background(MaterialTheme.colorScheme.surface)
.then(
if (enabled) {
Modifier.clickable { isSheetVisible = true }
} else {
Modifier
},
)
.padding(horizontal = 12.dp, vertical = 8.dp),
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = label,
style = MaterialTheme.typography.labelLarge,
color = if (enabled) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Icon(
imageVector = Icons.Rounded.KeyboardArrowDown,
contentDescription = null,
modifier = Modifier.size(18.dp),
tint = if (enabled) MaterialTheme.colorScheme.onSurfaceVariant else MaterialTheme.colorScheme.outline,
)
}
if (isSheetVisible) {
DiscoverOptionsSheet(
title = title,
options = options,
selectedKey = selectedKey,
sheetState = sheetState,
onDismiss = {
coroutineScope.launch {
dismissNuvioBottomSheet(
sheetState = sheetState,
onDismiss = { isSheetVisible = false },
)
}
},
onSelected = { option ->
onSelected(option)
coroutineScope.launch {
dismissNuvioBottomSheet(
sheetState = sheetState,
onDismiss = { isSheetVisible = false },
)
}
},
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun DiscoverOptionsSheet(
title: String,
options: List<DiscoverOptionItem>,
selectedKey: String?,
sheetState: SheetState,
onDismiss: () -> Unit,
onSelected: (DiscoverOptionItem) -> Unit,
) {
NuvioModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = nuvioSafeBottomPadding(16.dp)),
) {
Text(
text = title,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.onSurface,
)
NuvioBottomSheetDivider()
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 420.dp),
) {
itemsIndexed(options) { index, option ->
NuvioBottomSheetActionRow(
title = option.label,
onClick = { onSelected(option) },
trailingContent = {
if (option.key == selectedKey) {
Icon(
imageVector = Icons.Rounded.Check,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(20.dp),
)
}
},
)
if (index < options.lastIndex) {
NuvioBottomSheetDivider()
}
}
}
}
}
}
@Composable
private fun DiscoverGridRow(
items: List<MetaPreview>,
@ -518,11 +372,6 @@ private fun DiscoverEmptyStateCard(
)
}
private data class DiscoverOptionItem(
val key: String,
val label: String,
)
@Composable
private fun String.displayTypeLabel(): String =
when (lowercase()) {