SearchVM, SearchService: improve search UX by reducing flickering (#1117)
* Fix: don't treat PH OpeningHours with specified time as `everyDay` * Use SnapshotStateList in SearchVM and don't clear search-results if there are no new search results from SearchableRepositories * Rename parameter * mergeStateLists: refactor as extension method, add apps to SearchResults by default --------- Co-authored-by: MM20 <15646950+MM2-0@users.noreply.github.com>
This commit is contained in:
parent
2b08cb7413
commit
8c4bfb7dc9
@ -147,7 +147,7 @@ fun AssistantScaffold(
|
||||
val density = LocalDensity.current
|
||||
val maxSearchBarOffset = with(density) { 128.dp.toPx() }
|
||||
var searchBarOffset by remember {
|
||||
mutableStateOf(0f)
|
||||
mutableFloatStateOf(0f)
|
||||
}
|
||||
|
||||
val nestedScrollConnection = remember {
|
||||
@ -159,7 +159,7 @@ fun AssistantScaffold(
|
||||
}
|
||||
}
|
||||
}
|
||||
val actions by searchVM.searchActionResults
|
||||
val actions = searchVM.searchActionResults
|
||||
val webSearchPadding by animateDpAsState(
|
||||
if (actions.isEmpty()) 0.dp else 48.dp
|
||||
)
|
||||
|
||||
@ -51,7 +51,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
@ -124,7 +124,7 @@ fun PagerScaffold(
|
||||
val isSearchOpen by viewModel.isSearchOpen
|
||||
val isWidgetEditMode by viewModel.isWidgetEditMode
|
||||
|
||||
val actions by searchVM.searchActionResults
|
||||
val actions = searchVM.searchActionResults
|
||||
|
||||
val widgetsScrollState = rememberScrollState()
|
||||
val searchState = rememberLazyListState()
|
||||
@ -272,7 +272,7 @@ fun PagerScaffold(
|
||||
}
|
||||
}
|
||||
|
||||
val searchBarOffset = remember { mutableStateOf(0f) }
|
||||
val searchBarOffset = remember { mutableFloatStateOf(0f) }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
@ -341,8 +341,8 @@ fun PagerScaffold(
|
||||
}
|
||||
val deltaSearchBarOffset =
|
||||
consumed.y * if (isSearchOpen && reverseSearchResults) 1 else -1
|
||||
searchBarOffset.value =
|
||||
(searchBarOffset.value + deltaSearchBarOffset).coerceIn(0f, maxSearchBarOffset)
|
||||
searchBarOffset.floatValue =
|
||||
(searchBarOffset.floatValue + deltaSearchBarOffset).coerceIn(0f, maxSearchBarOffset)
|
||||
return super.onPostScroll(consumed, available, source)
|
||||
}
|
||||
|
||||
|
||||
@ -47,6 +47,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
@ -106,7 +107,7 @@ fun PullDownScaffold(
|
||||
val density = LocalDensity.current
|
||||
val context = LocalContext.current
|
||||
|
||||
val actions by searchVM.searchActionResults
|
||||
val actions = searchVM.searchActionResults
|
||||
|
||||
val isSearchOpen by viewModel.isSearchOpen
|
||||
val isWidgetEditMode by viewModel.isWidgetEditMode
|
||||
@ -116,7 +117,7 @@ fun PullDownScaffold(
|
||||
|
||||
val pagerState = rememberPagerState { 2 }
|
||||
|
||||
val offsetY = remember { mutableStateOf(0f) }
|
||||
val offsetY = remember { mutableFloatStateOf(0f) }
|
||||
val maxOffset = with(density) { 64.dp.toPx() }
|
||||
val toggleSearchThreshold = with(density) { 48.dp.toPx() }
|
||||
|
||||
@ -153,13 +154,13 @@ fun PullDownScaffold(
|
||||
|
||||
val isOverThreshold by remember {
|
||||
derivedStateOf {
|
||||
offsetY.value.absoluteValue > toggleSearchThreshold
|
||||
offsetY.floatValue.absoluteValue > toggleSearchThreshold
|
||||
}
|
||||
}
|
||||
|
||||
val dragProgress by remember {
|
||||
derivedStateOf {
|
||||
(offsetY.value.absoluteValue / toggleSearchThreshold).coerceAtMost(1f)
|
||||
(offsetY.floatValue.absoluteValue / toggleSearchThreshold).coerceAtMost(1f)
|
||||
}
|
||||
}
|
||||
|
||||
@ -236,7 +237,7 @@ fun PullDownScaffold(
|
||||
}
|
||||
}
|
||||
|
||||
val searchBarOffset = remember { mutableStateOf(0f) }
|
||||
val searchBarOffset = remember { mutableFloatStateOf(0f) }
|
||||
|
||||
val maxSearchBarOffset = with(density) { 128.dp.toPx() }
|
||||
|
||||
@ -281,7 +282,7 @@ fun PullDownScaffold(
|
||||
}
|
||||
|
||||
LaunchedEffect(isWidgetEditMode) {
|
||||
if (!isWidgetEditMode) searchBarOffset.value = 0f
|
||||
if (!isWidgetEditMode) searchBarOffset.floatValue = 0f
|
||||
}
|
||||
|
||||
val handleBackOrHomeEvent = {
|
||||
@ -321,7 +322,7 @@ fun PullDownScaffold(
|
||||
LaunchedEffect(isOverThreshold) {
|
||||
if (isOverThreshold) {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
|
||||
} else if (offsetY.value != 0f) {
|
||||
} else if (offsetY.floatValue != 0f) {
|
||||
view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE)
|
||||
}
|
||||
}
|
||||
@ -342,15 +343,15 @@ fun PullDownScaffold(
|
||||
val canPullUp = isSearchOpen && isSearchAtBottom
|
||||
|
||||
val consumed = when {
|
||||
canPullUp && available.y < 0 || offsetY.value < 0 -> {
|
||||
canPullUp && available.y < 0 || offsetY.floatValue < 0 -> {
|
||||
val consumed = available.y
|
||||
offsetY.value = (offsetY.value + (consumed * 0.5f)).coerceIn(-maxOffset, 0f)
|
||||
offsetY.floatValue = (offsetY.floatValue + (consumed * 0.5f)).coerceIn(-maxOffset, 0f)
|
||||
consumed
|
||||
}
|
||||
|
||||
canPullDown && available.y > 0 || offsetY.value > 0 -> {
|
||||
canPullDown && available.y > 0 || offsetY.floatValue > 0 -> {
|
||||
val consumed = available.y
|
||||
offsetY.value = (offsetY.value + (consumed * 0.5f)).coerceIn(0f, maxOffset)
|
||||
offsetY.floatValue = (offsetY.floatValue + (consumed * 0.5f)).coerceIn(0f, maxOffset)
|
||||
consumed
|
||||
}
|
||||
|
||||
@ -367,17 +368,17 @@ fun PullDownScaffold(
|
||||
): Offset {
|
||||
val deltaSearchBarOffset =
|
||||
consumed.y * if (isSearchOpen && reverseSearchResults) 1 else -1
|
||||
searchBarOffset.value =
|
||||
(searchBarOffset.value + deltaSearchBarOffset).coerceIn(0f, maxSearchBarOffset)
|
||||
searchBarOffset.floatValue =
|
||||
(searchBarOffset.floatValue + deltaSearchBarOffset).coerceIn(0f, maxSearchBarOffset)
|
||||
return super.onPostScroll(consumed, available, source)
|
||||
}
|
||||
|
||||
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||
if (offsetY.value > toggleSearchThreshold || offsetY.value < -toggleSearchThreshold) {
|
||||
if (offsetY.floatValue > toggleSearchThreshold || offsetY.floatValue < -toggleSearchThreshold) {
|
||||
viewModel.toggleSearch()
|
||||
}
|
||||
if (!isWidgetEditMode) gestureManager.dispatchDragEnd()
|
||||
if (offsetY.value != 0f) {
|
||||
if (offsetY.floatValue != 0f) {
|
||||
offsetY.animateTo(0f)
|
||||
return available
|
||||
}
|
||||
@ -406,7 +407,7 @@ fun PullDownScaffold(
|
||||
}
|
||||
)
|
||||
}
|
||||
.offset { IntOffset(0, offsetY.value.toInt()) },
|
||||
.offset { IntOffset(0, offsetY.floatValue.toInt()) },
|
||||
contentAlignment = Alignment.TopCenter
|
||||
) {
|
||||
BoxWithConstraints(
|
||||
@ -576,7 +577,7 @@ fun PullDownScaffold(
|
||||
val searchBarLevel by remember {
|
||||
derivedStateOf {
|
||||
when {
|
||||
offsetY.value != 0f -> SearchBarLevel.Raised
|
||||
offsetY.floatValue != 0f -> SearchBarLevel.Raised
|
||||
!isSearchOpen && isWidgetsAtStart && (fillClockHeight || !bottomSearchBar) -> SearchBarLevel.Resting
|
||||
isSearchOpen && isSearchAtTop && !bottomSearchBar -> SearchBarLevel.Active
|
||||
isSearchOpen && isSearchAtBottom && bottomSearchBar -> SearchBarLevel.Active
|
||||
@ -613,7 +614,7 @@ fun PullDownScaffold(
|
||||
darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark,
|
||||
bottomSearchBar = bottomSearchBar,
|
||||
searchBarOffset = {
|
||||
(if (searchBarFocused || fixedSearchBar) 0 else searchBarOffset.value.toInt() * (if (bottomSearchBar) 1 else -1)) +
|
||||
(if (searchBarFocused || fixedSearchBar) 0 else searchBarOffset.floatValue.toInt() * (if (bottomSearchBar) 1 else -1)) +
|
||||
with(density) {
|
||||
(editModeSearchBarOffset - if (bottomSearchBar) keyboardFilterBarPadding else 0.dp)
|
||||
.toPx()
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package de.mm20.launcher2.ui.launcher.search
|
||||
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
@ -75,22 +74,22 @@ fun SearchColumn(
|
||||
val hideFavs by viewModel.hideFavorites
|
||||
val favoritesEnabled by viewModel.favoritesEnabled.collectAsState(false)
|
||||
|
||||
val apps by viewModel.appResults
|
||||
val workApps by viewModel.workAppResults
|
||||
val privateApps by viewModel.privateSpaceAppResults
|
||||
val apps = viewModel.appResults
|
||||
val workApps = viewModel.workAppResults
|
||||
val privateApps = viewModel.privateSpaceAppResults
|
||||
val profiles by viewModel.profiles.collectAsState(emptyList())
|
||||
val profileStates by viewModel.profileStates.collectAsState(emptyList())
|
||||
|
||||
val appShortcuts by viewModel.appShortcutResults
|
||||
val contacts by viewModel.contactResults
|
||||
val files by viewModel.fileResults
|
||||
val events by viewModel.calendarResults
|
||||
val unitConverter by viewModel.unitConverterResults
|
||||
val calculator by viewModel.calculatorResults
|
||||
val wikipedia by viewModel.articleResults
|
||||
val locations by viewModel.locationResults
|
||||
val website by viewModel.websiteResults
|
||||
val hiddenResults by viewModel.hiddenResults
|
||||
val appShortcuts = viewModel.appShortcutResults
|
||||
val contacts = viewModel.contactResults
|
||||
val files = viewModel.fileResults
|
||||
val events = viewModel.calendarResults
|
||||
val unitConverter = viewModel.unitConverterResults
|
||||
val calculator = viewModel.calculatorResults
|
||||
val wikipedia = viewModel.articleResults
|
||||
val locations = viewModel.locationResults
|
||||
val website = viewModel.websiteResults
|
||||
val hiddenResults = viewModel.hiddenResults
|
||||
|
||||
val bestMatch by viewModel.bestMatch
|
||||
|
||||
|
||||
@ -2,7 +2,9 @@ package de.mm20.launcher2.ui.launcher.search
|
||||
|
||||
import android.content.Context
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.devicepose.DevicePoseProvider
|
||||
@ -28,6 +30,7 @@ import de.mm20.launcher2.search.Location
|
||||
import de.mm20.launcher2.search.ResultScore
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.SearchFilters
|
||||
import de.mm20.launcher2.search.SearchResults
|
||||
import de.mm20.launcher2.search.SearchService
|
||||
import de.mm20.launcher2.search.Searchable
|
||||
import de.mm20.launcher2.search.Website
|
||||
@ -79,8 +82,6 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
|
||||
val expandedCategory = mutableStateOf<SearchCategory?>(null)
|
||||
|
||||
val locationResults = mutableStateOf<List<Location>>(emptyList())
|
||||
|
||||
val profiles = profileManager.profiles.shareIn(
|
||||
viewModelScope,
|
||||
SharingStarted.WhileSubscribed(),
|
||||
@ -104,22 +105,25 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
}
|
||||
}
|
||||
|
||||
val appResults = mutableStateOf<List<Application>>(emptyList())
|
||||
val workAppResults = mutableStateOf<List<Application>>(emptyList())
|
||||
val privateSpaceAppResults = mutableStateOf<List<Application>>(emptyList())
|
||||
val appResults = mutableStateListOf<Application>()
|
||||
val workAppResults = mutableStateListOf<Application>()
|
||||
val privateSpaceAppResults = mutableStateListOf<Application>()
|
||||
|
||||
val appShortcutResults = mutableStateOf<List<AppShortcut>>(emptyList())
|
||||
val fileResults = mutableStateOf<List<File>>(emptyList())
|
||||
val contactResults = mutableStateOf<List<Contact>>(emptyList())
|
||||
val calendarResults = mutableStateOf<List<CalendarEvent>>(emptyList())
|
||||
val articleResults = mutableStateOf<List<Article>>(emptyList())
|
||||
val websiteResults = mutableStateOf<List<Website>>(emptyList())
|
||||
val calculatorResults = mutableStateOf<List<Calculator>>(emptyList())
|
||||
val unitConverterResults = mutableStateOf<List<UnitConverter>>(emptyList())
|
||||
val searchActionResults = mutableStateOf<List<SearchAction>>(emptyList())
|
||||
val appShortcutResults = mutableStateListOf<AppShortcut>()
|
||||
val fileResults = mutableStateListOf<File>()
|
||||
val contactResults = mutableStateListOf<Contact>()
|
||||
val calendarResults = mutableStateListOf<CalendarEvent>()
|
||||
val articleResults = mutableStateListOf<Article>()
|
||||
val websiteResults = mutableStateListOf<Website>()
|
||||
val calculatorResults = mutableStateListOf<Calculator>()
|
||||
val unitConverterResults = mutableStateListOf<UnitConverter>()
|
||||
val searchActionResults = mutableStateListOf<SearchAction>()
|
||||
val locationResults = mutableStateListOf<Location>()
|
||||
|
||||
var previousResults: SearchResults? = null
|
||||
|
||||
val hiddenResultsButton = searchUiSettings.hiddenItemsButton
|
||||
val hiddenResults = mutableStateOf<List<SavableSearchable>>(emptyList())
|
||||
val hiddenResults = mutableStateListOf<SavableSearchable>()
|
||||
|
||||
val favoritesEnabled = searchUiSettings.favorites
|
||||
val hideFavorites = mutableStateOf(false)
|
||||
@ -179,7 +183,6 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
}
|
||||
searchQuery.value = query
|
||||
isSearchEmpty.value = query.isEmpty()
|
||||
hiddenResults.value = emptyList()
|
||||
|
||||
val filters = filters.value
|
||||
|
||||
@ -218,15 +221,6 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
flowOf(emptyList())
|
||||
}
|
||||
val allApps = searchService.getAllApps()
|
||||
fileResults.value = emptyList()
|
||||
contactResults.value = emptyList()
|
||||
calendarResults.value = emptyList()
|
||||
locationResults.value = emptyList()
|
||||
articleResults.value = emptyList()
|
||||
websiteResults.value = emptyList()
|
||||
calculatorResults.value = emptyList()
|
||||
unitConverterResults.value = emptyList()
|
||||
searchActionResults.value = emptyList()
|
||||
|
||||
allApps
|
||||
.combine(hiddenItemKeys) { results, hiddenKeys -> results to hiddenKeys }
|
||||
@ -253,11 +247,13 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
)
|
||||
}
|
||||
hiddenItems += hiddenPrivateApps
|
||||
previousResults = SearchResults(apps = apps)
|
||||
|
||||
appResults.value = apps
|
||||
workAppResults.value = workApps
|
||||
privateSpaceAppResults.value = privateApps
|
||||
hiddenResults.value = hiddenItems
|
||||
searchActionResults.clear()
|
||||
appResults.mergeWith(apps)
|
||||
workAppResults.mergeWith(workApps)
|
||||
privateSpaceAppResults.mergeWith(privateApps)
|
||||
hiddenResults.mergeWith(hiddenItems)
|
||||
}
|
||||
|
||||
} else {
|
||||
@ -267,127 +263,65 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
searchService.search(
|
||||
query,
|
||||
filters = if (query.isEmpty()) filters.copy(apps = true) else filters,
|
||||
previousResults,
|
||||
)
|
||||
.combine(hiddenItemKeys) { results, hiddenKeys -> results to hiddenKeys }
|
||||
.collectLatest { (results, hiddenKeys) ->
|
||||
val hiddenItems = mutableListOf<SavableSearchable>()
|
||||
previousResults = results
|
||||
|
||||
if (results.apps != null) {
|
||||
val (hiddenApps, apps) = results.apps!!.partition {
|
||||
hiddenKeys.contains(
|
||||
it.key
|
||||
)
|
||||
}
|
||||
hiddenItems += hiddenApps
|
||||
appResults.value = apps.applyRanking(query)
|
||||
} else {
|
||||
appResults.value = emptyList()
|
||||
}
|
||||
workAppResults.value = emptyList()
|
||||
privateSpaceAppResults.value = emptyList()
|
||||
hiddenResults.clear()
|
||||
workAppResults.clear()
|
||||
privateSpaceAppResults.clear()
|
||||
|
||||
if (results.shortcuts != null) {
|
||||
val (hiddenShortcuts, shortcuts) = results.shortcuts!!.partition {
|
||||
hiddenKeys.contains(
|
||||
it.key
|
||||
)
|
||||
}
|
||||
hiddenItems += hiddenShortcuts
|
||||
appShortcutResults.value = shortcuts.applyRanking(query)
|
||||
} else {
|
||||
appShortcutResults.value = emptyList()
|
||||
}
|
||||
appResults.mergeWith(results.apps, hiddenKeys, query)
|
||||
appShortcutResults.mergeWith(results.shortcuts, hiddenKeys, query)
|
||||
fileResults.mergeWith(results.files, hiddenKeys, query)
|
||||
|
||||
if (results.files != null) {
|
||||
val (hiddenFiles, files) = results.files!!.partition {
|
||||
hiddenKeys.contains(
|
||||
it.key
|
||||
)
|
||||
}
|
||||
hiddenItems += hiddenFiles
|
||||
fileResults.value = files.applyRanking(query)
|
||||
} else {
|
||||
fileResults.value = emptyList()
|
||||
}
|
||||
|
||||
if (results.contacts != null) {
|
||||
val (hiddenContacts, contacts) = results.contacts!!.partition {
|
||||
hiddenKeys.contains(
|
||||
it.key
|
||||
)
|
||||
}
|
||||
hiddenItems += hiddenContacts
|
||||
contactResults.value = contacts.applyRanking(query)
|
||||
} else {
|
||||
contactResults.value = emptyList()
|
||||
}
|
||||
|
||||
if (results.calendars != null) {
|
||||
val (hiddenEvents, events) = results.calendars!!.partition {
|
||||
hiddenKeys.contains(
|
||||
it.key
|
||||
)
|
||||
}
|
||||
hiddenItems += hiddenEvents
|
||||
calendarResults.value = events.applyRanking(query)
|
||||
} else {
|
||||
calendarResults.value = emptyList()
|
||||
}
|
||||
|
||||
if (results.locations != null && results.locations!!.isNotEmpty()) {
|
||||
val (hiddenLocations, locations) = results.locations!!.partition {
|
||||
hiddenKeys.contains(
|
||||
it.key
|
||||
)
|
||||
}
|
||||
hiddenItems += hiddenLocations
|
||||
val lastLocation = devicePoseProvider.lastLocation
|
||||
if (lastLocation != null) {
|
||||
locationResults.value = locations.asSequence()
|
||||
.sortedWith { a, b ->
|
||||
a.distanceTo(lastLocation)
|
||||
.compareTo(b.distanceTo(lastLocation))
|
||||
}
|
||||
.distinctBy { it.key }
|
||||
.toList()
|
||||
} else {
|
||||
locationResults.value = locations.applyRanking(query)
|
||||
}
|
||||
} else {
|
||||
locationResults.value = emptyList()
|
||||
}
|
||||
|
||||
if (results.wikipedia != null) {
|
||||
articleResults.value = results.wikipedia!!.applyRanking(query)
|
||||
} else {
|
||||
articleResults.value = emptyList()
|
||||
}
|
||||
|
||||
if (results.websites != null) {
|
||||
websiteResults.value = results.websites!!.applyRanking(query)
|
||||
} else {
|
||||
websiteResults.value = emptyList()
|
||||
}
|
||||
|
||||
|
||||
calculatorResults.value = results.calculators ?: emptyList()
|
||||
unitConverterResults.value = results.unitConverters ?: emptyList()
|
||||
contactResults.mergeWith(
|
||||
results.contacts?.filterNot { hiddenKeys.contains(it.key) }
|
||||
?.applyRanking(query)
|
||||
)
|
||||
calendarResults.mergeWith(
|
||||
results.calendars?.filterNot { hiddenKeys.contains(it.key) }
|
||||
?.applyRanking(query)
|
||||
)
|
||||
locationResults.mergeWith(
|
||||
results.locations?.filterNot { hiddenKeys.contains(it.key) }
|
||||
?.let { locations ->
|
||||
devicePoseProvider.lastLocation?.let {
|
||||
locations.asSequence()
|
||||
.sortedWith { a, b ->
|
||||
a.distanceTo(it).compareTo(b.distanceTo(it))
|
||||
}
|
||||
.distinctBy { it.key }
|
||||
.toList()
|
||||
} ?: locations.applyRanking(query)
|
||||
}
|
||||
)
|
||||
articleResults.mergeWith(
|
||||
results.wikipedia?.applyRanking(query)
|
||||
)
|
||||
websiteResults.mergeWith(
|
||||
results.websites?.applyRanking(query)
|
||||
)
|
||||
calculatorResults.mergeWith(results.calculators)
|
||||
unitConverterResults.mergeWith(results.unitConverters)
|
||||
|
||||
if (results.searchActions != null) {
|
||||
searchActionResults.value = results.searchActions!!
|
||||
searchActionResults.mergeWith(results.searchActions!!)
|
||||
}
|
||||
|
||||
if (launchOnEnter.value) {
|
||||
bestMatch.value = when {
|
||||
appResults.value.isNotEmpty() -> appResults.value.first()
|
||||
appShortcutResults.value.isNotEmpty() -> appShortcutResults.value.first()
|
||||
calendarResults.value.isNotEmpty() -> calendarResults.value.first()
|
||||
locationResults.value.isNotEmpty() -> locationResults.value.first()
|
||||
contactResults.value.isNotEmpty() -> contactResults.value.first()
|
||||
articleResults.value.isNotEmpty() -> articleResults.value.first()
|
||||
websiteResults.value.isNotEmpty() -> websiteResults.value.first()
|
||||
fileResults.value.isNotEmpty() -> fileResults.value.first()
|
||||
searchActionResults.value.isNotEmpty() -> searchActionResults.value.first()
|
||||
appResults.isNotEmpty() -> appResults.first()
|
||||
appShortcutResults.isNotEmpty() -> appShortcutResults.first()
|
||||
calendarResults.isNotEmpty() -> calendarResults.first()
|
||||
locationResults.isNotEmpty() -> locationResults.first()
|
||||
contactResults.isNotEmpty() -> contactResults.first()
|
||||
articleResults.isNotEmpty() -> articleResults.first()
|
||||
websiteResults.isNotEmpty() -> websiteResults.first()
|
||||
fileResults.isNotEmpty() -> fileResults.first()
|
||||
searchActionResults.isNotEmpty() -> searchActionResults.first()
|
||||
else -> null
|
||||
}
|
||||
} else {
|
||||
@ -494,6 +428,23 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
}
|
||||
return sorted.distinctBy { it.key }.toList()
|
||||
}
|
||||
|
||||
private fun <T> SnapshotStateList<T>.mergeWith(newItems: List<T>?) {
|
||||
val items = newItems ?: emptyList()
|
||||
val diff = toSet() subtract items.toSet()
|
||||
removeAll(diff)
|
||||
for ((i, item) in items.withIndex()) {
|
||||
if (i < size)
|
||||
set(i, item)
|
||||
else
|
||||
add(item)
|
||||
}
|
||||
}
|
||||
private suspend fun <T : SavableSearchable> SnapshotStateList<T>.mergeWith(
|
||||
newItems: List<T>?,
|
||||
hiddenKeys: List<String>,
|
||||
query: String
|
||||
) = this.mergeWith((newItems ?: emptyList()).filterNot { hiddenKeys.contains(it.key) }.applyRanking(query))
|
||||
}
|
||||
|
||||
|
||||
|
||||
@ -68,7 +68,7 @@ fun LauncherSearchBar(
|
||||
|
||||
val searchVM: SearchVM = viewModel()
|
||||
val hiddenItemsButtonEnabled by searchVM.hiddenResultsButton.collectAsState(false)
|
||||
val hiddenItems by searchVM.hiddenResults
|
||||
val hiddenItems = searchVM.hiddenResults
|
||||
|
||||
val sheetManager = LocalBottomSheetManager.current
|
||||
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
package de.mm20.launcher2.search
|
||||
|
||||
import kotlinx.coroutines.Deferred
|
||||
|
||||
interface Searchable {
|
||||
val score: ResultScore
|
||||
get() = ResultScore.Unspecified
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
package de.mm20.launcher2.search
|
||||
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface SearchableRepository<T : Searchable> {
|
||||
|
||||
@ -30,6 +30,7 @@ interface SearchService {
|
||||
fun search(
|
||||
query: String,
|
||||
filters: SearchFilters,
|
||||
initialResults: SearchResults? = null,
|
||||
): Flow<SearchResults>
|
||||
|
||||
fun getAllApps(): Flow<AllAppsResults>
|
||||
@ -54,9 +55,10 @@ internal class SearchServiceImpl(
|
||||
override fun search(
|
||||
query: String,
|
||||
filters: SearchFilters,
|
||||
initialResults: SearchResults?,
|
||||
): Flow<SearchResults> = flow {
|
||||
supervisorScope {
|
||||
val results = MutableStateFlow(SearchResults())
|
||||
val results = MutableStateFlow(initialResults ?: SearchResults())
|
||||
|
||||
val customAttrResults = customAttributesRepository.search(query)
|
||||
.map { items ->
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user