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(