[SHARED] Add applications search

This commit is contained in:
Kamil Gurgul
2025-02-12 10:47:56 +01:00
parent ed7c41589c
commit 0addf44aa5
11 changed files with 95 additions and 6 deletions
@@ -1,6 +1,7 @@
package com.kgurgul.cpuinfo.utils
import java.text.Collator
import java.text.Normalizer
actual fun smartCompare(a: String, b: String): Int {
val collator = Collator.getInstance().apply {
@@ -8,3 +9,7 @@ actual fun smartCompare(a: String, b: String): Int {
}
return collator.compare(a.lowercase(), b.lowercase())
}
actual fun String.normalize(): String {
return Normalizer.normalize(this, Normalizer.Form.NFD)
}
@@ -12,6 +12,7 @@ import com.kgurgul.cpuinfo.domain.observable.RamDataObservable
import com.kgurgul.cpuinfo.domain.observable.SensorsDataObservable
import com.kgurgul.cpuinfo.domain.observable.StorageDataObservable
import com.kgurgul.cpuinfo.domain.observable.TemperatureDataObservable
import com.kgurgul.cpuinfo.domain.result.FilterApplicationsInteractor
import com.kgurgul.cpuinfo.domain.result.GetHardwareDataInteractor
import com.kgurgul.cpuinfo.domain.result.GetLicensesInteractor
import com.kgurgul.cpuinfo.domain.result.GetPackageNameInteractor
@@ -36,4 +37,5 @@ val domainModule = module {
factoryOf(::GetPackageNameInteractor)
factoryOf(::GetScreenDataInteractor)
factoryOf(::GetLicensesInteractor)
factoryOf(::FilterApplicationsInteractor)
}
@@ -0,0 +1,31 @@
package com.kgurgul.cpuinfo.domain.result
import com.kgurgul.cpuinfo.domain.ResultInteractor
import com.kgurgul.cpuinfo.domain.model.ExtendedApplicationData
import com.kgurgul.cpuinfo.utils.IDispatchersProvider
import com.kgurgul.cpuinfo.utils.removeNonSpacingMarks
import kotlinx.coroutines.CoroutineDispatcher
class FilterApplicationsInteractor(
private val dispatchersProvider: IDispatchersProvider
) : ResultInteractor<FilterApplicationsInteractor.Params, List<ExtendedApplicationData>>() {
override val dispatcher: CoroutineDispatcher
get() = dispatchersProvider.default
override suspend fun doWork(params: Params): List<ExtendedApplicationData> {
val normalizedQuery = params.searchQuery
.trim()
.lowercase()
.removeNonSpacingMarks()
return params.applications.filter {
it.name.lowercase().removeNonSpacingMarks().contains(normalizedQuery)
|| it.packageName.lowercase().removeNonSpacingMarks().contains(normalizedQuery)
}
}
data class Params(
val applications: List<ExtendedApplicationData>,
val searchQuery: String,
)
}
@@ -1,5 +1,6 @@
package com.kgurgul.cpuinfo.features.applications
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
@@ -236,7 +237,7 @@ private fun TopBar(
PrimaryTopAppBar(
title = stringResource(Res.string.applications),
actions = {
/*AnimatedVisibility(visible = showSearch) {
AnimatedVisibility(visible = showSearch) {
SearchTextField(
searchQuery = searchQuery,
onSearchQueryChanged = onSearchQueryChanged,
@@ -250,7 +251,7 @@ private fun TopBar(
contentDescription = null,
)
}
}*/
}
IconButton(onClick = { showMenu = !showMenu }) {
Icon(
imageVector = Icons.Default.MoreVert,
@@ -9,6 +9,7 @@ import com.kgurgul.cpuinfo.domain.action.IExternalAppAction
import com.kgurgul.cpuinfo.domain.model.ExtendedApplicationData
import com.kgurgul.cpuinfo.domain.model.sortOrderFromBoolean
import com.kgurgul.cpuinfo.domain.observable.ApplicationsDataObservable
import com.kgurgul.cpuinfo.domain.result.FilterApplicationsInteractor
import com.kgurgul.cpuinfo.domain.result.GetPackageNameInteractor
import com.kgurgul.cpuinfo.shared.Res
import com.kgurgul.cpuinfo.shared.app_open
@@ -18,9 +19,12 @@ import com.kgurgul.cpuinfo.utils.wrappers.Result
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
@@ -33,6 +37,7 @@ class ApplicationsViewModel(
private val getPackageNameInteractor: GetPackageNameInteractor,
private val userPreferencesRepository: IUserPreferencesRepository,
private val externalAppAction: IExternalAppAction,
private val filterApplicationsInteractor: FilterApplicationsInteractor,
) : ViewModel() {
private val localDataFlow = MutableStateFlow(LocalUiState())
@@ -46,28 +51,41 @@ class ApplicationsViewModel(
),
)
}
val searchQuery = savedStateHandle.getStateFlow(key = SEARCH_QUERY_KEY, initialValue = "")
private val debouncedSearchQueryFlow = searchQuery.mapLatest { query ->
if (query.isNotEmpty()) {
delay(SEARCH_DEBOUNCE_MS)
}
query
}
val uiStateFlow = combine(
localDataFlow,
userPreferencesFlow,
applicationsDataObservable.observe(),
) { localData, userPreferences, applicationsResult ->
debouncedSearchQueryFlow,
) { localData, userPreferences, applicationsResult, serchQuery ->
if (applicationsResult is Result.Success) {
cachedApplications.clear()
cachedApplications.addAll(applicationsResult.data)
}
val filteredApplications = if (serchQuery.isEmpty()) {
cachedApplications
} else {
filterApplicationsInteractor.invoke(
FilterApplicationsInteractor.Params(cachedApplications, serchQuery)
)
}
UiState(
isLoading = applicationsResult is Result.Loading,
withSystemApps = userPreferences.withSystemApps,
isSortAscending = userPreferences.isApplicationsSortingAscending,
isDialogVisible = localData.isDialogVisible,
nativeLibs = localData.nativeLibs,
applications = cachedApplications.toImmutableList(),
applications = filteredApplications.toImmutableList(),
snackbarMessage = localData.snackbarMessage,
)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), UiState())
val searchQuery = savedStateHandle.getStateFlow(key = SEARCH_QUERY_KEY, initialValue = "")
fun onRefreshApplications() {
val currentUiState = uiStateFlow.value
applicationsDataObservable.invoke(
@@ -168,3 +186,4 @@ class ApplicationsViewModel(
}
private const val SEARCH_QUERY_KEY = "searchQuery"
private const val SEARCH_DEBOUNCE_MS = 300L
@@ -56,3 +56,8 @@ fun Double.round4(): Double = try {
}
expect fun smartCompare(a: String, b: String): Int
expect fun String.normalize(): String
fun String.removeNonSpacingMarks() = normalize()
.replace("\\p{Mn}+".toRegex(), "")
@@ -8,6 +8,7 @@ import com.kgurgul.cpuinfo.data.provider.FakeApplicationsDataProvider
import com.kgurgul.cpuinfo.data.provider.FakePackageNameProvider
import com.kgurgul.cpuinfo.domain.action.FakeExternalAppAction
import com.kgurgul.cpuinfo.domain.observable.ApplicationsDataObservable
import com.kgurgul.cpuinfo.domain.result.FilterApplicationsInteractor
import com.kgurgul.cpuinfo.domain.result.GetPackageNameInteractor
import com.kgurgul.cpuinfo.shared.Res
import com.kgurgul.cpuinfo.shared.app_open
@@ -44,6 +45,9 @@ class ApplicationsViewModelTest {
)
private val fakeExternalAppAction = FakeExternalAppAction()
private val savedStateHandle = SavedStateHandle()
private val filterApplicationsInteractor = FilterApplicationsInteractor(
dispatchersProvider = coroutineTestRule.testDispatcherProvider
)
private lateinit var viewModel: ApplicationsViewModel
@@ -58,6 +62,7 @@ class ApplicationsViewModelTest {
getPackageNameInteractor = getPackageNameInteractor,
userPreferencesRepository = fakeUserPreferencesRepository,
externalAppAction = fakeExternalAppAction,
filterApplicationsInteractor = filterApplicationsInteractor,
)
}
@@ -1,6 +1,7 @@
package com.kgurgul.cpuinfo.utils
import java.text.Collator
import java.text.Normalizer
import okio.Path.Companion.toPath
actual fun smartCompare(a: String, b: String): Int {
@@ -10,6 +11,10 @@ actual fun smartCompare(a: String, b: String): Int {
return collator.compare(a.lowercase(), b.lowercase())
}
actual fun String.normalize(): String {
return Normalizer.normalize(this, Normalizer.Form.NFD)
}
fun getAppConfigPath(appName: String): String {
val os = System.getProperty("os.name").lowercase()
val appNamePath = appName.toPath()
@@ -1,9 +1,15 @@
package com.kgurgul.cpuinfo.utils
import platform.Foundation.NSString
import platform.Foundation.decomposedStringWithCanonicalMapping
import platform.Foundation.localizedCompare
@Suppress("CAST_NEVER_SUCCEEDS")
actual fun smartCompare(a: String, b: String): Int {
return (a as NSString).localizedCompare(b).toInt()
}
@Suppress("CAST_NEVER_SUCCEEDS")
actual fun String.normalize(): String {
return (this as NSString).decomposedStringWithCanonicalMapping
}
@@ -7,6 +7,10 @@ actual fun smartCompare(a: String, b: String): Int {
return collator.compare(a.lowercase(), b.lowercase())
}
actual fun String.normalize(): String {
return jsNormalize(this.toJsString()).toString()
}
fun JsArray<out JsString>.toList(): List<String> {
val list = mutableListOf<String>()
for (i in 0 until this.length) {
@@ -30,3 +34,5 @@ external fun getTotalStorage(): Promise<JsBigInt>
external fun getUsedStorage(): Promise<JsBigInt>
external fun isPdfViewerEnabled(): Boolean
external fun jsNormalize(value: JsString): JsString
@@ -39,3 +39,7 @@ function isPdfViewerEnabled() {
return false;
}
}
function jsNormalize(value) {
return value.normalize("NFD")
}