diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 24324d07..bee00f3b 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -1656,6 +1656,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 43447cda..d4d7a7f4 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 @@ -127,6 +130,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() } } @@ -300,6 +304,11 @@ fun LibraryScreen( selectedProviderId = selectedProviderId, selectedType = selectedType, selectedCloudItemKey = selectedCloudItemKey, + searchQuery = cloudSearchQuery, + onSearchQueryChange = { + cloudSearchQuery = it + selectedCloudItemKey = null + }, onProviderSelected = { selectedProviderId = it selectedTypeName = null @@ -436,6 +445,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, @@ -481,8 +492,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) { @@ -507,6 +529,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() } @@ -550,6 +580,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(