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 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
|
||||||
)
|
)
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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> {
|
||||||
|
|||||||
@ -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 ->
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user