diff --git a/app/src/main/java/com/illusionware/npsbrowser/activities/MainActivity.kt b/app/src/main/java/com/illusionware/npsbrowser/activities/MainActivity.kt index 8e4f800..9cdc78f 100644 --- a/app/src/main/java/com/illusionware/npsbrowser/activities/MainActivity.kt +++ b/app/src/main/java/com/illusionware/npsbrowser/activities/MainActivity.kt @@ -189,15 +189,17 @@ fun HomePage( navigateToDetailsScreen: (item: PackageItem) -> Unit, settingsPrefs: SettingsPreferences, settingsViewModel: SettingsViewModel, + packageListViewModel: PackageListViewModel = viewModel( + factory = PackageListViewModel.Factory + ), ) { - var isSearchOpen by rememberSaveable { mutableStateOf(false)} - var searchQuery by rememberSaveable { mutableStateOf("")} val searchBar = remember { FocusRequester() } var layoutDialogOpen by remember { mutableStateOf(false) } val context = LocalContext.current val systemUiController = rememberSystemUiController() val navbarScrimColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp) + val uiState = packageListViewModel.uiState.collectAsStateWithLifecycle().value val tsvs = settingsViewModel.availableTsvs() var selectedBarItem by rememberSaveable { mutableStateOf(ConsoleType.PSVITA) } @@ -215,22 +217,26 @@ fun HomePage( topBar = { TopAppBar( title = { - if (!isSearchOpen) { + if (!uiState.isSearchOpen) { Text(text = stringResource(id = R.string.app_name)) } }, actions = { - if (isSearchOpen) { + if (uiState.isSearchOpen) { NPSIconButton( tooltip = "Close Search", - onClick = { isSearchOpen = false } + onClick = { + packageListViewModel.closeSearch() + } ){ Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = "Close Search") } Spacer(Modifier.width(12.dp)) TextField( - value = searchQuery, - onValueChange = {str -> searchQuery = str}, + value = uiState.searchQuery, + onValueChange = { + packageListViewModel.search(it) + }, Modifier .weight(1.0f) .focusRequester(searchBar), @@ -248,10 +254,12 @@ fun HomePage( ), ) Spacer(Modifier.width(12.dp)) - if (searchQuery.isNotEmpty()) { + if (uiState.searchQuery.isNotEmpty()) { NPSIconButton( tooltip = "Clear Search", - onClick = { searchQuery = "" } + onClick = { + packageListViewModel.clearSearch() + } ){ Icon(imageVector = Icons.Filled.Close, contentDescription = "Clear Search") } @@ -261,10 +269,10 @@ fun HomePage( searchBar.requestFocus() } } - if (!isSearchOpen) { + if (!uiState.isSearchOpen) { NPSIconButton( tooltip = stringResource(id = R.string.search_tooltip), - onClick = { isSearchOpen = true } + onClick = { packageListViewModel.openSearch() } ){ Icon(imageVector = Icons.Filled.Search, contentDescription = stringResource( id = R.string.search_tooltip @@ -306,7 +314,8 @@ fun HomePage( PackageList( navigateToDetailsScreen = navigateToDetailsScreen, type = selectedBarItem, - settingsPrefs = settingsPrefs + settingsPrefs = settingsPrefs, + viewModel = packageListViewModel, ) } } @@ -321,6 +330,18 @@ fun HomePage( } } +@Composable +fun NoResults() { + Box( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + contentAlignment = Alignment.Center + ) { + Text(text = "No results found", color = MaterialTheme.colorScheme.outline) + } +} + @Composable fun NavBar(selectedTab: ConsoleType, onTabSelected: (ConsoleType) -> Unit) { NavigationBar { @@ -411,9 +432,7 @@ fun NoTsvs(padding: PaddingValues = PaddingValues(0.dp), navigateToSettings: () fun PackageList( type: ConsoleType = ConsoleType.PSVITA, navigateToDetailsScreen: (item: PackageItem) -> Unit, - viewModel: PackageListViewModel = viewModel( - factory = PackageListViewModel.Factory - ), + viewModel: PackageListViewModel, settingsPrefs: SettingsPreferences, ) { val uiState = viewModel.uiState.collectAsStateWithLifecycle().value @@ -447,7 +466,7 @@ fun PackageList( CircularProgressIndicator() } } else { - if (packages.isEmpty()) { + if (packages.isEmpty() && uiState.searchQuery.isEmpty()) { Box( modifier = Modifier .fillMaxSize() @@ -461,6 +480,8 @@ fun PackageList( color = MaterialTheme.colorScheme.outline ) } + } else if (packages.isEmpty() && uiState.searchQuery.isNotEmpty()) { + NoResults() } else { if (settingsPrefs.layout == ItemLayout.LIST.ordinal) { LazyColumn { diff --git a/app/src/main/java/com/illusionware/npsbrowser/viewmodels/PackageListData.kt b/app/src/main/java/com/illusionware/npsbrowser/viewmodels/PackageListData.kt index 1c1fbb5..bf49359 100644 --- a/app/src/main/java/com/illusionware/npsbrowser/viewmodels/PackageListData.kt +++ b/app/src/main/java/com/illusionware/npsbrowser/viewmodels/PackageListData.kt @@ -19,6 +19,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import okhttp3.internal.toLongOrDefault import java.io.InputStream @@ -27,10 +28,13 @@ data class PackageListUiState( val packages: ArrayList = ArrayList(), val error: String = "", val isLoading: Boolean = true, + val isSearchOpen: Boolean = false, + val searchQuery: String = "", ) class PackageListViewModel(app: NPSApp, settingsRepo: SettingsPreferencesRepository): ViewModel() { private val _uiState = MutableStateFlow(PackageListUiState()) + private val immutablePackages = ArrayList() val uiState: StateFlow = _uiState.asStateFlow() private val tsvReader = csvReader { @@ -52,6 +56,7 @@ class PackageListViewModel(app: NPSApp, settingsRepo: SettingsPreferencesReposit list.sortBy { it.name } _uiState.emit(PackageListUiState(isLoading = false, packages = list)) + immutablePackages.addAll(list) settingsRepo.tsvFiles().collect { tsv -> _uiState.emit(PackageListUiState()) @@ -70,10 +75,45 @@ class PackageListViewModel(app: NPSApp, settingsRepo: SettingsPreferencesReposit list.sortBy { it.name } _uiState.emit(PackageListUiState(isLoading = false, packages = list)) + + immutablePackages.addAll(list) } } } + fun search(query: String) { + _uiState.update { + it.copy( + packages = immutablePackages.filter { item -> + item.name.contains(query, true) + } as ArrayList, + searchQuery = query + ) + } + } + + fun openSearch() { + _uiState.update { + it.copy(isSearchOpen = true) + } + } + + fun closeSearch() { + _uiState.update { + it.copy( + packages = immutablePackages, + searchQuery = "", + isSearchOpen = false + ) + } + } + + fun clearSearch() { + _uiState.update { + it.copy(packages = immutablePackages, searchQuery = "") + } + } + private fun parsePackageFromTSV( it: Triple, app: NPSApp ): ArrayList {