[SHARED] Add applications search
This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
+31
@@ -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,
|
||||
)
|
||||
}
|
||||
+3
-2
@@ -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,
|
||||
|
||||
+23
-4
@@ -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(), "")
|
||||
|
||||
+5
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user