From 79269f461a3b79ca7447effb2185da932f743fa3 Mon Sep 17 00:00:00 2001 From: YLaskco <2punksdnb@gmail.com> Date: Thu, 23 Jul 2026 10:18:39 -0400 Subject: [PATCH] feat(library): search the cloud library The Cloud view only had provider and type filters, so finding a specific release in a large debrid account meant scrolling the whole list, even though the item names and file names are already loaded on the client. Adds a local search field under the toolbar that narrows the already loaded items. It matches the item name or any of its file names, since the useful identifier is often in the filename rather than the title, and it stacks on top of the existing provider and type filters. --- .../composeResources/values/strings.xml | 1 + .../app/features/library/LibraryScreen.kt | 64 ++++++++++++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 1e20233ab..34830c9b7 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -1657,6 +1657,7 @@ %1$d playable files All Refresh cloud library + Search cloud library Select provider Select type Ready to play diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryScreen.kt index ae561f6cb..b5b2682d3 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/library/LibraryScreen.kt @@ -31,15 +31,18 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.InsertDriveFile import androidx.compose.material.icons.automirrored.rounded.ArrowBack +import androidx.compose.material.icons.rounded.Close import androidx.compose.material.icons.rounded.GridView import androidx.compose.material.icons.rounded.PlayArrow import androidx.compose.material.icons.rounded.Refresh +import androidx.compose.material.icons.rounded.Search import androidx.compose.material.icons.rounded.ViewAgenda import com.nuvio.app.core.ui.NuvioLoadingIndicator import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -132,6 +135,7 @@ fun LibraryScreen( } var selectedProviderId by rememberSaveable { mutableStateOf(null) } var selectedTypeName by rememberSaveable { mutableStateOf(null) } + var cloudSearchQuery by rememberSaveable { mutableStateOf("") } val selectedType = remember(selectedTypeName) { selectedTypeName?.let { runCatching { CloudLibraryItemType.valueOf(it) }.getOrNull() } } @@ -305,6 +309,11 @@ fun LibraryScreen( selectedProviderId = selectedProviderId, selectedType = selectedType, selectedCloudItemKey = selectedCloudItemKey, + searchQuery = cloudSearchQuery, + onSearchQueryChange = { + cloudSearchQuery = it + selectedCloudItemKey = null + }, onProviderSelected = { selectedProviderId = it selectedTypeName = null @@ -443,6 +452,8 @@ private fun LazyListScope.cloudLibraryContent( selectedProviderId: String?, selectedType: CloudLibraryItemType?, selectedCloudItemKey: String?, + searchQuery: String, + onSearchQueryChange: (String) -> Unit, onProviderSelected: (String?) -> Unit, onTypeSelected: (CloudLibraryItemType?) -> Unit, onItemSelected: (CloudLibraryItem) -> Unit, @@ -488,8 +499,19 @@ private fun LazyListScope.cloudLibraryContent( .distinct() .sortedBy { type -> type.ordinal } val effectiveSelectedType = selectedType?.takeIf { type -> type in availableTypes } - val filteredItems = providerItems + val typeFilteredItems = providerItems .filter { item -> effectiveSelectedType == null || item.type == effectiveSelectedType } + // Local filter over the already-loaded library. Matches the item name or any of its + // file names, since the useful identifier is often in the filename, not the title. + val trimmedQuery = searchQuery.trim() + val filteredItems = if (trimmedQuery.isEmpty()) { + typeFilteredItems + } else { + typeFilteredItems.filter { item -> + item.name.contains(trimmedQuery, ignoreCase = true) || + item.files.any { file -> file.name.contains(trimmedQuery, ignoreCase = true) } + } + } val selectedItem = filteredItems.firstOrNull { it.stableKey == selectedCloudItemKey } if (selectedItem != null) { @@ -514,6 +536,14 @@ private fun LazyListScope.cloudLibraryContent( ) } + item(key = "cloud-library-search") { + CloudLibrarySearchField( + query = searchQuery, + onQueryChange = onSearchQueryChange, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + ) + } + uiState.providers .filter { providerState -> selectedProviderId == null || providerState.providerId == selectedProviderId } .filter { providerState -> !providerState.errorMessage.isNullOrBlank() && providerState.items.isEmpty() } @@ -557,6 +587,38 @@ private fun LazyListScope.cloudLibraryContent( } } +@Composable +private fun CloudLibrarySearchField( + query: String, + onQueryChange: (String) -> Unit, + modifier: Modifier = Modifier, +) { + OutlinedTextField( + value = query, + onValueChange = onQueryChange, + modifier = modifier.fillMaxWidth(), + singleLine = true, + shape = RoundedCornerShape(12.dp), + placeholder = { Text(stringResource(Res.string.cloud_library_search_label)) }, + leadingIcon = { + Icon( + imageVector = Icons.Rounded.Search, + contentDescription = null, + ) + }, + trailingIcon = { + if (query.isNotEmpty()) { + IconButton(onClick = { onQueryChange("") }) { + Icon( + imageVector = Icons.Rounded.Close, + contentDescription = stringResource(Res.string.compose_search_clear), + ) + } + } + }, + ) +} + private fun LazyListScope.cloudLibrarySkeletonItems() { item(key = "cloud-library-skeleton-toolbar") { CloudLibrarySkeletonToolbar(