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:
Christoph 2024-12-05 17:44:34 +00:00 committed by GitHub
parent 2b08cb7413
commit 8c4bfb7dc9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 132 additions and 182 deletions

View File

@ -147,7 +147,7 @@ fun AssistantScaffold(
val density = LocalDensity.current val density = LocalDensity.current
val maxSearchBarOffset = with(density) { 128.dp.toPx() } val maxSearchBarOffset = with(density) { 128.dp.toPx() }
var searchBarOffset by remember { var searchBarOffset by remember {
mutableStateOf(0f) mutableFloatStateOf(0f)
} }
val nestedScrollConnection = remember { val nestedScrollConnection = remember {
@ -159,7 +159,7 @@ fun AssistantScaffold(
} }
} }
} }
val actions by searchVM.searchActionResults val actions = searchVM.searchActionResults
val webSearchPadding by animateDpAsState( val webSearchPadding by animateDpAsState(
if (actions.isEmpty()) 0.dp else 48.dp if (actions.isEmpty()) 0.dp else 48.dp
) )

View File

@ -51,7 +51,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -124,7 +124,7 @@ fun PagerScaffold(
val isSearchOpen by viewModel.isSearchOpen val isSearchOpen by viewModel.isSearchOpen
val isWidgetEditMode by viewModel.isWidgetEditMode val isWidgetEditMode by viewModel.isWidgetEditMode
val actions by searchVM.searchActionResults val actions = searchVM.searchActionResults
val widgetsScrollState = rememberScrollState() val widgetsScrollState = rememberScrollState()
val searchState = rememberLazyListState() val searchState = rememberLazyListState()
@ -272,7 +272,7 @@ fun PagerScaffold(
} }
} }
val searchBarOffset = remember { mutableStateOf(0f) } val searchBarOffset = remember { mutableFloatStateOf(0f) }
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
@ -341,8 +341,8 @@ fun PagerScaffold(
} }
val deltaSearchBarOffset = val deltaSearchBarOffset =
consumed.y * if (isSearchOpen && reverseSearchResults) 1 else -1 consumed.y * if (isSearchOpen && reverseSearchResults) 1 else -1
searchBarOffset.value = searchBarOffset.floatValue =
(searchBarOffset.value + deltaSearchBarOffset).coerceIn(0f, maxSearchBarOffset) (searchBarOffset.floatValue + deltaSearchBarOffset).coerceIn(0f, maxSearchBarOffset)
return super.onPostScroll(consumed, available, source) return super.onPostScroll(consumed, available, source)
} }

View File

@ -47,6 +47,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
@ -106,7 +107,7 @@ fun PullDownScaffold(
val density = LocalDensity.current val density = LocalDensity.current
val context = LocalContext.current val context = LocalContext.current
val actions by searchVM.searchActionResults val actions = searchVM.searchActionResults
val isSearchOpen by viewModel.isSearchOpen val isSearchOpen by viewModel.isSearchOpen
val isWidgetEditMode by viewModel.isWidgetEditMode val isWidgetEditMode by viewModel.isWidgetEditMode
@ -116,7 +117,7 @@ fun PullDownScaffold(
val pagerState = rememberPagerState { 2 } val pagerState = rememberPagerState { 2 }
val offsetY = remember { mutableStateOf(0f) } val offsetY = remember { mutableFloatStateOf(0f) }
val maxOffset = with(density) { 64.dp.toPx() } val maxOffset = with(density) { 64.dp.toPx() }
val toggleSearchThreshold = with(density) { 48.dp.toPx() } val toggleSearchThreshold = with(density) { 48.dp.toPx() }
@ -153,13 +154,13 @@ fun PullDownScaffold(
val isOverThreshold by remember { val isOverThreshold by remember {
derivedStateOf { derivedStateOf {
offsetY.value.absoluteValue > toggleSearchThreshold offsetY.floatValue.absoluteValue > toggleSearchThreshold
} }
} }
val dragProgress by remember { val dragProgress by remember {
derivedStateOf { 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() } val maxSearchBarOffset = with(density) { 128.dp.toPx() }
@ -281,7 +282,7 @@ fun PullDownScaffold(
} }
LaunchedEffect(isWidgetEditMode) { LaunchedEffect(isWidgetEditMode) {
if (!isWidgetEditMode) searchBarOffset.value = 0f if (!isWidgetEditMode) searchBarOffset.floatValue = 0f
} }
val handleBackOrHomeEvent = { val handleBackOrHomeEvent = {
@ -321,7 +322,7 @@ fun PullDownScaffold(
LaunchedEffect(isOverThreshold) { LaunchedEffect(isOverThreshold) {
if (isOverThreshold) { if (isOverThreshold) {
view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY) view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY)
} else if (offsetY.value != 0f) { } else if (offsetY.floatValue != 0f) {
view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE) view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY_RELEASE)
} }
} }
@ -342,15 +343,15 @@ fun PullDownScaffold(
val canPullUp = isSearchOpen && isSearchAtBottom val canPullUp = isSearchOpen && isSearchAtBottom
val consumed = when { val consumed = when {
canPullUp && available.y < 0 || offsetY.value < 0 -> { canPullUp && available.y < 0 || offsetY.floatValue < 0 -> {
val consumed = available.y 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 consumed
} }
canPullDown && available.y > 0 || offsetY.value > 0 -> { canPullDown && available.y > 0 || offsetY.floatValue > 0 -> {
val consumed = available.y 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 consumed
} }
@ -367,17 +368,17 @@ fun PullDownScaffold(
): Offset { ): Offset {
val deltaSearchBarOffset = val deltaSearchBarOffset =
consumed.y * if (isSearchOpen && reverseSearchResults) 1 else -1 consumed.y * if (isSearchOpen && reverseSearchResults) 1 else -1
searchBarOffset.value = searchBarOffset.floatValue =
(searchBarOffset.value + deltaSearchBarOffset).coerceIn(0f, maxSearchBarOffset) (searchBarOffset.floatValue + deltaSearchBarOffset).coerceIn(0f, maxSearchBarOffset)
return super.onPostScroll(consumed, available, source) return super.onPostScroll(consumed, available, source)
} }
override suspend fun onPreFling(available: Velocity): Velocity { override suspend fun onPreFling(available: Velocity): Velocity {
if (offsetY.value > toggleSearchThreshold || offsetY.value < -toggleSearchThreshold) { if (offsetY.floatValue > toggleSearchThreshold || offsetY.floatValue < -toggleSearchThreshold) {
viewModel.toggleSearch() viewModel.toggleSearch()
} }
if (!isWidgetEditMode) gestureManager.dispatchDragEnd() if (!isWidgetEditMode) gestureManager.dispatchDragEnd()
if (offsetY.value != 0f) { if (offsetY.floatValue != 0f) {
offsetY.animateTo(0f) offsetY.animateTo(0f)
return available return available
} }
@ -406,7 +407,7 @@ fun PullDownScaffold(
} }
) )
} }
.offset { IntOffset(0, offsetY.value.toInt()) }, .offset { IntOffset(0, offsetY.floatValue.toInt()) },
contentAlignment = Alignment.TopCenter contentAlignment = Alignment.TopCenter
) { ) {
BoxWithConstraints( BoxWithConstraints(
@ -576,7 +577,7 @@ fun PullDownScaffold(
val searchBarLevel by remember { val searchBarLevel by remember {
derivedStateOf { derivedStateOf {
when { when {
offsetY.value != 0f -> SearchBarLevel.Raised offsetY.floatValue != 0f -> SearchBarLevel.Raised
!isSearchOpen && isWidgetsAtStart && (fillClockHeight || !bottomSearchBar) -> SearchBarLevel.Resting !isSearchOpen && isWidgetsAtStart && (fillClockHeight || !bottomSearchBar) -> SearchBarLevel.Resting
isSearchOpen && isSearchAtTop && !bottomSearchBar -> SearchBarLevel.Active isSearchOpen && isSearchAtTop && !bottomSearchBar -> SearchBarLevel.Active
isSearchOpen && isSearchAtBottom && bottomSearchBar -> SearchBarLevel.Active isSearchOpen && isSearchAtBottom && bottomSearchBar -> SearchBarLevel.Active
@ -613,7 +614,7 @@ fun PullDownScaffold(
darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark, darkColors = LocalPreferDarkContentOverWallpaper.current && searchBarColor == SearchBarColors.Auto || searchBarColor == SearchBarColors.Dark,
bottomSearchBar = bottomSearchBar, bottomSearchBar = bottomSearchBar,
searchBarOffset = { 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) { with(density) {
(editModeSearchBarOffset - if (bottomSearchBar) keyboardFilterBarPadding else 0.dp) (editModeSearchBarOffset - if (bottomSearchBar) keyboardFilterBarPadding else 0.dp)
.toPx() .toPx()

View File

@ -1,6 +1,5 @@
package de.mm20.launcher2.ui.launcher.search package de.mm20.launcher2.ui.launcher.search
import android.util.Log
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
@ -75,22 +74,22 @@ fun SearchColumn(
val hideFavs by viewModel.hideFavorites val hideFavs by viewModel.hideFavorites
val favoritesEnabled by viewModel.favoritesEnabled.collectAsState(false) val favoritesEnabled by viewModel.favoritesEnabled.collectAsState(false)
val apps by viewModel.appResults val apps = viewModel.appResults
val workApps by viewModel.workAppResults val workApps = viewModel.workAppResults
val privateApps by viewModel.privateSpaceAppResults val privateApps = viewModel.privateSpaceAppResults
val profiles by viewModel.profiles.collectAsState(emptyList()) val profiles by viewModel.profiles.collectAsState(emptyList())
val profileStates by viewModel.profileStates.collectAsState(emptyList()) val profileStates by viewModel.profileStates.collectAsState(emptyList())
val appShortcuts by viewModel.appShortcutResults val appShortcuts = viewModel.appShortcutResults
val contacts by viewModel.contactResults val contacts = viewModel.contactResults
val files by viewModel.fileResults val files = viewModel.fileResults
val events by viewModel.calendarResults val events = viewModel.calendarResults
val unitConverter by viewModel.unitConverterResults val unitConverter = viewModel.unitConverterResults
val calculator by viewModel.calculatorResults val calculator = viewModel.calculatorResults
val wikipedia by viewModel.articleResults val wikipedia = viewModel.articleResults
val locations by viewModel.locationResults val locations = viewModel.locationResults
val website by viewModel.websiteResults val website = viewModel.websiteResults
val hiddenResults by viewModel.hiddenResults val hiddenResults = viewModel.hiddenResults
val bestMatch by viewModel.bestMatch val bestMatch by viewModel.bestMatch

View File

@ -2,7 +2,9 @@ package de.mm20.launcher2.ui.launcher.search
import android.content.Context import android.content.Context
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.devicepose.DevicePoseProvider 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.ResultScore
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchFilters import de.mm20.launcher2.search.SearchFilters
import de.mm20.launcher2.search.SearchResults
import de.mm20.launcher2.search.SearchService import de.mm20.launcher2.search.SearchService
import de.mm20.launcher2.search.Searchable import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.Website import de.mm20.launcher2.search.Website
@ -79,8 +82,6 @@ class SearchVM : ViewModel(), KoinComponent {
val expandedCategory = mutableStateOf<SearchCategory?>(null) val expandedCategory = mutableStateOf<SearchCategory?>(null)
val locationResults = mutableStateOf<List<Location>>(emptyList())
val profiles = profileManager.profiles.shareIn( val profiles = profileManager.profiles.shareIn(
viewModelScope, viewModelScope,
SharingStarted.WhileSubscribed(), SharingStarted.WhileSubscribed(),
@ -104,22 +105,25 @@ class SearchVM : ViewModel(), KoinComponent {
} }
} }
val appResults = mutableStateOf<List<Application>>(emptyList()) val appResults = mutableStateListOf<Application>()
val workAppResults = mutableStateOf<List<Application>>(emptyList()) val workAppResults = mutableStateListOf<Application>()
val privateSpaceAppResults = mutableStateOf<List<Application>>(emptyList()) val privateSpaceAppResults = mutableStateListOf<Application>()
val appShortcutResults = mutableStateOf<List<AppShortcut>>(emptyList()) val appShortcutResults = mutableStateListOf<AppShortcut>()
val fileResults = mutableStateOf<List<File>>(emptyList()) val fileResults = mutableStateListOf<File>()
val contactResults = mutableStateOf<List<Contact>>(emptyList()) val contactResults = mutableStateListOf<Contact>()
val calendarResults = mutableStateOf<List<CalendarEvent>>(emptyList()) val calendarResults = mutableStateListOf<CalendarEvent>()
val articleResults = mutableStateOf<List<Article>>(emptyList()) val articleResults = mutableStateListOf<Article>()
val websiteResults = mutableStateOf<List<Website>>(emptyList()) val websiteResults = mutableStateListOf<Website>()
val calculatorResults = mutableStateOf<List<Calculator>>(emptyList()) val calculatorResults = mutableStateListOf<Calculator>()
val unitConverterResults = mutableStateOf<List<UnitConverter>>(emptyList()) val unitConverterResults = mutableStateListOf<UnitConverter>()
val searchActionResults = mutableStateOf<List<SearchAction>>(emptyList()) val searchActionResults = mutableStateListOf<SearchAction>()
val locationResults = mutableStateListOf<Location>()
var previousResults: SearchResults? = null
val hiddenResultsButton = searchUiSettings.hiddenItemsButton val hiddenResultsButton = searchUiSettings.hiddenItemsButton
val hiddenResults = mutableStateOf<List<SavableSearchable>>(emptyList()) val hiddenResults = mutableStateListOf<SavableSearchable>()
val favoritesEnabled = searchUiSettings.favorites val favoritesEnabled = searchUiSettings.favorites
val hideFavorites = mutableStateOf(false) val hideFavorites = mutableStateOf(false)
@ -179,7 +183,6 @@ class SearchVM : ViewModel(), KoinComponent {
} }
searchQuery.value = query searchQuery.value = query
isSearchEmpty.value = query.isEmpty() isSearchEmpty.value = query.isEmpty()
hiddenResults.value = emptyList()
val filters = filters.value val filters = filters.value
@ -218,15 +221,6 @@ class SearchVM : ViewModel(), KoinComponent {
flowOf(emptyList()) flowOf(emptyList())
} }
val allApps = searchService.getAllApps() 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 allApps
.combine(hiddenItemKeys) { results, hiddenKeys -> results to hiddenKeys } .combine(hiddenItemKeys) { results, hiddenKeys -> results to hiddenKeys }
@ -253,11 +247,13 @@ class SearchVM : ViewModel(), KoinComponent {
) )
} }
hiddenItems += hiddenPrivateApps hiddenItems += hiddenPrivateApps
previousResults = SearchResults(apps = apps)
appResults.value = apps searchActionResults.clear()
workAppResults.value = workApps appResults.mergeWith(apps)
privateSpaceAppResults.value = privateApps workAppResults.mergeWith(workApps)
hiddenResults.value = hiddenItems privateSpaceAppResults.mergeWith(privateApps)
hiddenResults.mergeWith(hiddenItems)
} }
} else { } else {
@ -267,127 +263,65 @@ class SearchVM : ViewModel(), KoinComponent {
searchService.search( searchService.search(
query, query,
filters = if (query.isEmpty()) filters.copy(apps = true) else filters, filters = if (query.isEmpty()) filters.copy(apps = true) else filters,
previousResults,
) )
.combine(hiddenItemKeys) { results, hiddenKeys -> results to hiddenKeys } .combine(hiddenItemKeys) { results, hiddenKeys -> results to hiddenKeys }
.collectLatest { (results, hiddenKeys) -> .collectLatest { (results, hiddenKeys) ->
val hiddenItems = mutableListOf<SavableSearchable>() previousResults = results
if (results.apps != null) { hiddenResults.clear()
val (hiddenApps, apps) = results.apps!!.partition { workAppResults.clear()
hiddenKeys.contains( privateSpaceAppResults.clear()
it.key
)
}
hiddenItems += hiddenApps
appResults.value = apps.applyRanking(query)
} else {
appResults.value = emptyList()
}
workAppResults.value = emptyList()
privateSpaceAppResults.value = emptyList()
if (results.shortcuts != null) { appResults.mergeWith(results.apps, hiddenKeys, query)
val (hiddenShortcuts, shortcuts) = results.shortcuts!!.partition { appShortcutResults.mergeWith(results.shortcuts, hiddenKeys, query)
hiddenKeys.contains( fileResults.mergeWith(results.files, hiddenKeys, query)
it.key
)
}
hiddenItems += hiddenShortcuts
appShortcutResults.value = shortcuts.applyRanking(query)
} else {
appShortcutResults.value = emptyList()
}
if (results.files != null) { contactResults.mergeWith(
val (hiddenFiles, files) = results.files!!.partition { results.contacts?.filterNot { hiddenKeys.contains(it.key) }
hiddenKeys.contains( ?.applyRanking(query)
it.key )
) calendarResults.mergeWith(
} results.calendars?.filterNot { hiddenKeys.contains(it.key) }
hiddenItems += hiddenFiles ?.applyRanking(query)
fileResults.value = files.applyRanking(query) )
} else { locationResults.mergeWith(
fileResults.value = emptyList() results.locations?.filterNot { hiddenKeys.contains(it.key) }
} ?.let { locations ->
devicePoseProvider.lastLocation?.let {
if (results.contacts != null) { locations.asSequence()
val (hiddenContacts, contacts) = results.contacts!!.partition { .sortedWith { a, b ->
hiddenKeys.contains( a.distanceTo(it).compareTo(b.distanceTo(it))
it.key }
) .distinctBy { it.key }
} .toList()
hiddenItems += hiddenContacts } ?: locations.applyRanking(query)
contactResults.value = contacts.applyRanking(query) }
} else { )
contactResults.value = emptyList() articleResults.mergeWith(
} results.wikipedia?.applyRanking(query)
)
if (results.calendars != null) { websiteResults.mergeWith(
val (hiddenEvents, events) = results.calendars!!.partition { results.websites?.applyRanking(query)
hiddenKeys.contains( )
it.key calculatorResults.mergeWith(results.calculators)
) unitConverterResults.mergeWith(results.unitConverters)
}
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()
if (results.searchActions != null) { if (results.searchActions != null) {
searchActionResults.value = results.searchActions!! searchActionResults.mergeWith(results.searchActions!!)
} }
if (launchOnEnter.value) { if (launchOnEnter.value) {
bestMatch.value = when { bestMatch.value = when {
appResults.value.isNotEmpty() -> appResults.value.first() appResults.isNotEmpty() -> appResults.first()
appShortcutResults.value.isNotEmpty() -> appShortcutResults.value.first() appShortcutResults.isNotEmpty() -> appShortcutResults.first()
calendarResults.value.isNotEmpty() -> calendarResults.value.first() calendarResults.isNotEmpty() -> calendarResults.first()
locationResults.value.isNotEmpty() -> locationResults.value.first() locationResults.isNotEmpty() -> locationResults.first()
contactResults.value.isNotEmpty() -> contactResults.value.first() contactResults.isNotEmpty() -> contactResults.first()
articleResults.value.isNotEmpty() -> articleResults.value.first() articleResults.isNotEmpty() -> articleResults.first()
websiteResults.value.isNotEmpty() -> websiteResults.value.first() websiteResults.isNotEmpty() -> websiteResults.first()
fileResults.value.isNotEmpty() -> fileResults.value.first() fileResults.isNotEmpty() -> fileResults.first()
searchActionResults.value.isNotEmpty() -> searchActionResults.value.first() searchActionResults.isNotEmpty() -> searchActionResults.first()
else -> null else -> null
} }
} else { } else {
@ -494,6 +428,23 @@ class SearchVM : ViewModel(), KoinComponent {
} }
return sorted.distinctBy { it.key }.toList() 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))
} }

View File

@ -68,7 +68,7 @@ fun LauncherSearchBar(
val searchVM: SearchVM = viewModel() val searchVM: SearchVM = viewModel()
val hiddenItemsButtonEnabled by searchVM.hiddenResultsButton.collectAsState(false) val hiddenItemsButtonEnabled by searchVM.hiddenResultsButton.collectAsState(false)
val hiddenItems by searchVM.hiddenResults val hiddenItems = searchVM.hiddenResults
val sheetManager = LocalBottomSheetManager.current val sheetManager = LocalBottomSheetManager.current

View File

@ -1,7 +1,5 @@
package de.mm20.launcher2.search package de.mm20.launcher2.search
import kotlinx.coroutines.Deferred
interface Searchable { interface Searchable {
val score: ResultScore val score: ResultScore
get() = ResultScore.Unspecified get() = ResultScore.Unspecified

View File

@ -1,6 +1,5 @@
package de.mm20.launcher2.search package de.mm20.launcher2.search
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface SearchableRepository<T : Searchable> { interface SearchableRepository<T : Searchable> {

View File

@ -30,6 +30,7 @@ interface SearchService {
fun search( fun search(
query: String, query: String,
filters: SearchFilters, filters: SearchFilters,
initialResults: SearchResults? = null,
): Flow<SearchResults> ): Flow<SearchResults>
fun getAllApps(): Flow<AllAppsResults> fun getAllApps(): Flow<AllAppsResults>
@ -54,9 +55,10 @@ internal class SearchServiceImpl(
override fun search( override fun search(
query: String, query: String,
filters: SearchFilters, filters: SearchFilters,
initialResults: SearchResults?,
): Flow<SearchResults> = flow { ): Flow<SearchResults> = flow {
supervisorScope { supervisorScope {
val results = MutableStateFlow(SearchResults()) val results = MutableStateFlow(initialResults ?: SearchResults())
val customAttrResults = customAttributesRepository.search(query) val customAttrResults = customAttributesRepository.search(query)
.map { items -> .map { items ->