Add search-only visibility level to items

This commit is contained in:
MM20 2024-06-17 22:59:12 +02:00
parent 1251924553
commit c7ea840fc5
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
28 changed files with 571 additions and 485 deletions

View File

@ -8,6 +8,7 @@ import de.mm20.launcher2.preferences.search.FavoritesSettings
import de.mm20.launcher2.preferences.search.FavoritesSettingsData import de.mm20.launcher2.preferences.search.FavoritesSettingsData
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.data.Tag import de.mm20.launcher2.search.data.Tag
import de.mm20.launcher2.searchable.PinnedLevel
import de.mm20.launcher2.services.favorites.FavoritesService import de.mm20.launcher2.services.favorites.FavoritesService
import de.mm20.launcher2.widgets.CalendarWidget import de.mm20.launcher2.widgets.CalendarWidget
import de.mm20.launcher2.widgets.WidgetRepository import de.mm20.launcher2.widgets.WidgetRepository
@ -29,8 +30,7 @@ abstract class FavoritesVM : ViewModel(), KoinComponent {
val pinnedTags = favoritesService.getFavorites( val pinnedTags = favoritesService.getFavorites(
includeTypes = listOf("tag"), includeTypes = listOf("tag"),
manuallySorted = true, minPinnedLevel = PinnedLevel.AutomaticallySorted,
automaticallySorted = true,
).map { ).map {
it.filterIsInstance<Tag>() it.filterIsInstance<Tag>()
} }
@ -52,15 +52,15 @@ abstract class FavoritesVM : ViewModel(), KoinComponent {
val pinned = favoritesService.getFavorites( val pinned = favoritesService.getFavorites(
excludeTypes = if (excludeCalendar) listOf("calendar", "tag") else listOf("tag"), excludeTypes = if (excludeCalendar) listOf("calendar", "tag") else listOf("tag"),
manuallySorted = true, minPinnedLevel = PinnedLevel.AutomaticallySorted,
automaticallySorted = true,
limit = 10 * columns, limit = 10 * columns,
) )
if (includeFrequentlyUsed) { if (includeFrequentlyUsed) {
emitAll(pinned.flatMapLatest { pinned -> emitAll(pinned.flatMapLatest { pinned ->
favoritesService.getFavorites( favoritesService.getFavorites(
excludeTypes = if (excludeCalendar) listOf("calendar", "tag") else listOf("tag"), excludeTypes = if (excludeCalendar) listOf("calendar", "tag") else listOf("tag"),
frequentlyUsed = true, maxPinnedLevel = PinnedLevel.FrequentlyUsed,
minPinnedLevel = PinnedLevel.FrequentlyUsed,
limit = frequentlyUsedRows * columns - pinned.size % columns, limit = frequentlyUsedRows * columns - pinned.size % columns,
).map { ).map {
pinned + it pinned + it

View File

@ -4,11 +4,14 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
@ -28,7 +31,6 @@ import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@ -58,9 +60,11 @@ fun OutlinedTagsInputField(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
tags: List<String>, tags: List<String>,
onTagsChange: (tags: List<String>) -> Unit, onTagsChange: (tags: List<String>) -> Unit,
label: @Composable (() -> Unit)? = null,
placeholder: @Composable (() -> Unit)? = null, placeholder: @Composable (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
textStyle: TextStyle = MaterialTheme.typography.bodyLarge, textStyle: TextStyle = LocalTextStyle.current,
textColor: Color = LocalContentColor.current, textColor: Color = LocalContentColor.current,
onAutocomplete: (suspend (query: String) -> List<String>)? = null onAutocomplete: (suspend (query: String) -> List<String>)? = null
) { ) {
@ -127,14 +131,23 @@ fun OutlinedTagsInputField(
}), }),
decorationBox = { innerTextField -> decorationBox = { innerTextField ->
OutlinedTextFieldDefaults.DecorationBox( OutlinedTextFieldDefaults.DecorationBox(
contentPadding = PaddingValues(0.dp), contentPadding = PaddingValues(
value = value, start = 16.dp,
end = 0.dp,
top = 12.dp,
bottom = 12.dp
),
value = tags.joinToString() + value,
label = label,
leadingIcon = leadingIcon,
innerTextField = { innerTextField = {
Box { Box(
contentAlignment = Alignment.CenterStart,
) {
Row( Row(
modifier = Modifier modifier = Modifier
.horizontalScroll(rememberScrollState()) .requiredHeight(32.dp)
.padding(horizontal = 16.dp), .horizontalScroll(rememberScrollState()),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
for ((i, tag) in tags.withIndex()) { for ((i, tag) in tags.withIndex()) {

View File

@ -32,6 +32,7 @@ import de.mm20.launcher2.search.Website
import de.mm20.launcher2.search.data.Calculator import de.mm20.launcher2.search.data.Calculator
import de.mm20.launcher2.search.data.UnitConverter import de.mm20.launcher2.search.data.UnitConverter
import de.mm20.launcher2.searchable.SavableSearchableRepository import de.mm20.launcher2.searchable.SavableSearchableRepository
import de.mm20.launcher2.searchable.VisibilityLevel
import de.mm20.launcher2.searchactions.actions.SearchAction import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.services.favorites.FavoritesService import de.mm20.launcher2.services.favorites.FavoritesService
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
@ -102,13 +103,6 @@ class SearchVM : ViewModel(), KoinComponent {
val separateWorkProfile = searchUiSettings.separateWorkProfile val separateWorkProfile = searchUiSettings.separateWorkProfile
private val hiddenItemKeys = searchableRepository
.getKeys(
hidden = true,
limit = 9999,
)
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
val bestMatch = mutableStateOf<Searchable?>(null) val bestMatch = mutableStateOf<Searchable?>(null)
init { init {
@ -251,6 +245,10 @@ class SearchVM : ViewModel(), KoinComponent {
} }
} }
val hiddenItemKeys = searchableRepository.getKeys(
maxVisibility = if (query.isEmpty()) VisibilityLevel.SearchOnly else VisibilityLevel.Hidden,
)
hiddenItemKeys.collectLatest { hiddenKeys -> hiddenItemKeys.collectLatest { hiddenKeys ->
val hidden = mutableListOf<SavableSearchable>() val hidden = mutableListOf<SavableSearchable>()
val apps = mutableListOf<Application>() val apps = mutableListOf<Application>()

View File

@ -379,38 +379,6 @@ fun AppItem(
) )
} }
val isHidden by viewModel.isHidden.collectAsState(false)
val hideAction = if (isHidden) {
DefaultToolbarAction(
label = stringResource(R.string.menu_unhide),
icon = Icons.Rounded.Visibility,
action = {
viewModel.unhide()
onBack()
}
)
} else {
DefaultToolbarAction(
label = stringResource(R.string.menu_hide),
icon = Icons.Rounded.VisibilityOff,
action = {
viewModel.hide()
onBack()
lifecycleOwner.lifecycleScope.launch {
val result = snackbarHostState.showSnackbar(
message = context.getString(R.string.msg_item_hidden, app.label),
actionLabel = context.getString(R.string.action_undo),
duration = SnackbarDuration.Short,
)
if (result == SnackbarResult.ActionPerformed) {
viewModel.unhide()
}
}
})
}
toolbarActions.add(hideAction)
Toolbar( Toolbar(
leftActions = listOf( leftActions = listOf(
DefaultToolbarAction( DefaultToolbarAction(

View File

@ -221,39 +221,6 @@ fun CalendarItem(
toolbarActions.add(favAction) toolbarActions.add(favAction)
} }
val isHidden by viewModel.isHidden.collectAsState(false)
val hideAction = if (isHidden) {
DefaultToolbarAction(
label = stringResource(R.string.menu_unhide),
icon = Icons.Rounded.Visibility,
action = {
viewModel.unhide()
onBack()
}
)
} else {
DefaultToolbarAction(
label = stringResource(R.string.menu_hide),
icon = Icons.Rounded.VisibilityOff,
action = {
viewModel.hide()
onBack()
lifecycleOwner.lifecycleScope.launch {
val result = snackbarHostState.showSnackbar(
message = context.getString(
R.string.msg_item_hidden,
calendar.label
),
actionLabel = context.getString(R.string.action_undo),
duration = SnackbarDuration.Short,
)
if (result == SnackbarResult.ActionPerformed) {
viewModel.unhide()
}
}
})
}
toolbarActions.add( toolbarActions.add(
DefaultToolbarAction( DefaultToolbarAction(
label = stringResource(R.string.menu_calendar_open_externally), label = stringResource(R.string.menu_calendar_open_externally),
@ -272,8 +239,6 @@ fun CalendarItem(
action = { sheetManager.showCustomizeSearchableModal(calendar) } action = { sheetManager.showCustomizeSearchableModal(calendar) }
)) ))
toolbarActions.add(hideAction)
Toolbar( Toolbar(
leftActions = listOf( leftActions = listOf(
DefaultToolbarAction( DefaultToolbarAction(

View File

@ -78,18 +78,6 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent {
searchable.value?.let { favoritesService.unpinItem(it) } searchable.value?.let { favoritesService.unpinItem(it) }
} }
val isHidden = searchable.flatMapLatest {
if (it == null) emptyFlow() else favoritesService.isHidden(it)
}
fun hide() {
searchable.value?.let { favoritesService.hideItem(it) }
}
fun unhide() {
searchable.value?.let { favoritesService.unhideItem(it) }
}
val badge = searchable.flatMapLatest { val badge = searchable.flatMapLatest {
if (it == null) emptyFlow() else badgeService.getBadge(it) if (it == null) emptyFlow() else badgeService.getBadge(it)
}.stateIn(viewModelScope, SharingStarted.Lazily, null) }.stateIn(viewModelScope, SharingStarted.Lazily, null)

View File

@ -205,39 +205,6 @@ fun ContactItem(
toolbarActions.add(favAction) toolbarActions.add(favAction)
} }
val isHidden by viewModel.isHidden.collectAsState(false)
val hideAction = if (isHidden) {
DefaultToolbarAction(
label = stringResource(R.string.menu_unhide),
icon = Icons.Rounded.Visibility,
action = {
viewModel.unhide()
onBack()
}
)
} else {
DefaultToolbarAction(
label = stringResource(R.string.menu_hide),
icon = Icons.Rounded.VisibilityOff,
action = {
viewModel.hide()
onBack()
lifecycleOwner.lifecycleScope.launch {
val result = snackbarHostState.showSnackbar(
message = context.getString(
R.string.msg_item_hidden,
contact.label
),
actionLabel = context.getString(R.string.action_undo),
duration = SnackbarDuration.Short,
)
if (result == SnackbarResult.ActionPerformed) {
viewModel.unhide()
}
}
})
}
toolbarActions.add( toolbarActions.add(
DefaultToolbarAction( DefaultToolbarAction(
label = stringResource(R.string.menu_contacts_open_externally), label = stringResource(R.string.menu_contacts_open_externally),
@ -255,7 +222,6 @@ fun ContactItem(
action = { sheetManager.showCustomizeSearchableModal(contact) } action = { sheetManager.showCustomizeSearchableModal(contact) }
)) ))
toolbarActions.add(hideAction)
Toolbar( Toolbar(
leftActions = listOf( leftActions = listOf(

View File

@ -257,41 +257,6 @@ fun FileItem(
action = { sheetManager.showCustomizeSearchableModal(file) } action = { sheetManager.showCustomizeSearchableModal(file) }
)) ))
val isHidden by viewModel.isHidden.collectAsState(false)
val hideAction = if (isHidden) {
DefaultToolbarAction(
label = stringResource(R.string.menu_unhide),
icon = Icons.Rounded.Visibility,
action = {
viewModel.unhide()
onBack()
}
)
} else {
DefaultToolbarAction(
label = stringResource(R.string.menu_hide),
icon = Icons.Rounded.VisibilityOff,
action = {
viewModel.hide()
onBack()
lifecycleOwner.lifecycleScope.launch {
val result = snackbarHostState.showSnackbar(
message = context.getString(
R.string.msg_item_hidden,
file.label
),
actionLabel = context.getString(R.string.action_undo),
duration = SnackbarDuration.Short,
)
if (result == SnackbarResult.ActionPerformed) {
viewModel.unhide()
}
}
})
}
toolbarActions.add(hideAction)
Toolbar( Toolbar(
leftActions = listOf( leftActions = listOf(
DefaultToolbarAction( DefaultToolbarAction(

View File

@ -775,41 +775,6 @@ fun LocationItem(
} }
} }
val isHidden by viewModel.isHidden.collectAsState(false)
val hideAction = if (isHidden) {
DefaultToolbarAction(
label = stringResource(R.string.menu_unhide),
icon = Icons.Rounded.Visibility,
action = {
viewModel.unhide()
onBack()
}
)
} else {
DefaultToolbarAction(
label = stringResource(R.string.menu_hide),
icon = Icons.Rounded.VisibilityOff,
action = {
viewModel.hide()
onBack()
lifecycleOwner.lifecycleScope.launch {
val result = snackbarHostState.showSnackbar(
message = context.getString(
R.string.msg_item_hidden,
location.labelOverride ?: location.label
),
actionLabel = context.getString(R.string.action_undo),
duration = SnackbarDuration.Short,
)
if (result == SnackbarResult.ActionPerformed) {
viewModel.unhide()
}
}
})
}
toolbarActions.add(hideAction)
Toolbar( Toolbar(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
leftActions = listOf(DefaultToolbarAction( leftActions = listOf(DefaultToolbarAction(

View File

@ -218,41 +218,6 @@ fun AppShortcutItem(
)) ))
} }
val isHidden by viewModel.isHidden.collectAsState(false)
val hideAction = if (isHidden) {
DefaultToolbarAction(
label = stringResource(R.string.menu_unhide),
icon = Icons.Rounded.Visibility,
action = {
viewModel.unhide()
onBack()
}
)
} else {
DefaultToolbarAction(
label = stringResource(R.string.menu_hide),
icon = Icons.Rounded.VisibilityOff,
action = {
viewModel.hide()
onBack()
lifecycleOwner.lifecycleScope.launch {
val result = snackbarHostState.showSnackbar(
message = context.getString(
R.string.msg_item_hidden,
shortcut.label
),
actionLabel = context.getString(R.string.action_undo),
duration = SnackbarDuration.Short,
)
if (result == SnackbarResult.ActionPerformed) {
viewModel.unhide()
}
}
})
}
toolbarActions.add(hideAction)
Toolbar( Toolbar(
leftActions = listOf( leftActions = listOf(
DefaultToolbarAction( DefaultToolbarAction(

View File

@ -1,9 +1,7 @@
package de.mm20.launcher2.ui.launcher.sheets package de.mm20.launcher2.ui.launcher.sheets
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.drawable.InsetDrawable
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.animation.animateContentSize import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -19,17 +17,25 @@ import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.Label
import androidx.compose.material.icons.outlined.Visibility
import androidx.compose.material.icons.rounded.ArrowDropDown import androidx.compose.material.icons.rounded.ArrowDropDown
import androidx.compose.material.icons.rounded.Edit import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.FilterAlt import androidx.compose.material.icons.rounded.FilterAlt
import androidx.compose.material.icons.rounded.Search import androidx.compose.material.icons.rounded.Search
import androidx.compose.material.icons.rounded.Tag
import androidx.compose.material.icons.rounded.Visibility
import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -44,7 +50,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -55,7 +60,10 @@ import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.BadgeIcon import de.mm20.launcher2.badges.BadgeIcon
import de.mm20.launcher2.icons.CustomIconWithPreview import de.mm20.launcher2.icons.CustomIconWithPreview
import de.mm20.launcher2.icons.IconPack import de.mm20.launcher2.icons.IconPack
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.CalendarEvent
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.searchable.VisibilityLevel
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.OutlinedTagsInputField import de.mm20.launcher2.ui.component.OutlinedTagsInputField
@ -84,9 +92,11 @@ fun CustomizeSearchableSheet(
BottomSheetDialog( BottomSheetDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { title = if (pickIcon) {
Text(stringResource(if (pickIcon) R.string.icon_picker_title else R.string.menu_customize)) {
}, Text(stringResource(R.string.icon_picker_title))
}
} else null,
dismissible = { !pickIcon }, dismissible = { !pickIcon },
confirmButton = if (pickIcon) { confirmButton = if (pickIcon) {
{ {
@ -126,44 +136,178 @@ fun CustomizeSearchableSheet(
mutableStateOf(searchable.labelOverride ?: "") mutableStateOf(searchable.labelOverride ?: "")
} }
OutlinedTextField( OutlinedTextField(
modifier = modifier = Modifier
Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 24.dp), .padding(top = 24.dp, bottom = 16.dp),
value = customLabelValue, value = customLabelValue,
onValueChange = { onValueChange = {
customLabelValue = it customLabelValue = it
}, },
singleLine = true, singleLine = true,
label = {
Text(stringResource(R.string.customize_item_label))
},
placeholder = { placeholder = {
Text(searchable.label) Text(searchable.label)
}, },
leadingIcon = {
Icon(Icons.AutoMirrored.Rounded.Label, null)
}
) )
var tags by remember { mutableStateOf(emptyList<String>()) } var tags by remember { mutableStateOf(emptyList<String>()) }
var visibility by remember { mutableStateOf(VisibilityLevel.Default) }
LaunchedEffect(searchable.key) { LaunchedEffect(searchable.key) {
visibility = viewModel.getVisibility().first()
tags = viewModel.getTags().first() tags = viewModel.getTags().first()
} }
OutlinedTagsInputField( OutlinedTagsInputField(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .padding(top = 8.dp)
.padding(vertical = 16.dp), .fillMaxWidth(),
tags = tags, onTagsChange = { tags = it.distinct() }, tags = tags, onTagsChange = { tags = it.distinct() },
placeholder = { label = {
Text(stringResource(R.string.customize_tags_placeholder)) Text(stringResource(R.string.customize_item_tags))
}, },
textStyle = MaterialTheme.typography.bodyMedium,
onAutocomplete = { onAutocomplete = {
viewModel.autocompleteTags(it).minus(tags.toSet()) viewModel.autocompleteTags(it).minus(tags.toSet())
},
leadingIcon = {
Icon(Icons.Rounded.Tag, null)
} }
) )
var showDropdown by remember {
mutableStateOf(false)
}
ExposedDropdownMenuBox(
expanded = showDropdown,
onExpandedChange = { showDropdown = it },
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
) {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.menuAnchor(MenuAnchorType.PrimaryNotEditable),
value = when (visibility) {
VisibilityLevel.Default -> {
when (searchable) {
is Application -> stringResource(R.string.item_visibility_app_default)
is CalendarEvent -> stringResource(R.string.item_visibility_calendar_default)
else -> stringResource(R.string.item_visibility_search_only)
}
}
VisibilityLevel.SearchOnly -> stringResource(R.string.item_visibility_search_only)
VisibilityLevel.Hidden -> stringResource(R.string.item_visibility_hidden)
},
label = {
Text(stringResource(R.string.customize_item_visibility))
},
onValueChange = {},
readOnly = true,
singleLine = true,
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = showDropdown) },
leadingIcon = {
Icon(
when (visibility) {
VisibilityLevel.Default -> Icons.Rounded.Visibility
VisibilityLevel.SearchOnly -> Icons.Outlined.Visibility
VisibilityLevel.Hidden -> Icons.Rounded.VisibilityOff
},
null
)
},
colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(),
)
ExposedDropdownMenu(
expanded = showDropdown,
onDismissRequest = {
showDropdown = false
}
) {
if (searchable is Application) {
DropdownMenuItem(
onClick = {
visibility = VisibilityLevel.Default
showDropdown = false
},
text = {
Text(stringResource(R.string.item_visibility_app_default))
},
leadingIcon = {
Icon(Icons.Rounded.Visibility, null)
}
)
} else if (searchable is CalendarEvent) {
DropdownMenuItem(
onClick = {
visibility = VisibilityLevel.Default
showDropdown = false
},
text = {
Text(stringResource(R.string.item_visibility_app_default))
},
leadingIcon = {
Icon(Icons.Rounded.Visibility, null)
}
)
} else {
DropdownMenuItem(
onClick = {
visibility = VisibilityLevel.Default
showDropdown = false
},
text = {
Text(stringResource(R.string.item_visibility_search_only))
},
leadingIcon = {
Icon(Icons.Rounded.Visibility, null)
}
)
}
if (searchable is Application || searchable is CalendarEvent) {
DropdownMenuItem(
onClick = {
visibility = VisibilityLevel.SearchOnly
showDropdown = false
},
text = {
Text(stringResource(R.string.item_visibility_search_only))
},
leadingIcon = {
Icon(
Icons.Outlined.Visibility,
null
)
}
)
}
DropdownMenuItem(
onClick = {
visibility = VisibilityLevel.Hidden
showDropdown = false
},
text = {
Text(stringResource(R.string.item_visibility_hidden))
},
leadingIcon = {
Icon(Icons.Rounded.VisibilityOff, null)
}
)
}
}
DisposableEffect(searchable.key) { DisposableEffect(searchable.key) {
onDispose { onDispose {
viewModel.setCustomLabel(customLabelValue) viewModel.setCustomLabel(customLabelValue)
viewModel.setTags(tags) viewModel.setTags(tags)
viewModel.setVisibility(visibility)
} }
} }
} }

View File

@ -8,6 +8,8 @@ import de.mm20.launcher2.icons.IconPack
import de.mm20.launcher2.icons.IconService import de.mm20.launcher2.icons.IconService
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.searchable.VisibilityLevel
import de.mm20.launcher2.services.favorites.FavoritesService
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.cancelAndJoin
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
@ -25,6 +27,7 @@ class CustomizeSearchableSheetVM(
) : KoinComponent { ) : KoinComponent {
private val iconService: IconService by inject() private val iconService: IconService by inject()
private val customAttributesRepository: CustomAttributesRepository by inject() private val customAttributesRepository: CustomAttributesRepository by inject()
private val favoritesService: FavoritesService by inject()
val isIconPickerOpen = mutableStateOf(false) val isIconPickerOpen = mutableStateOf(false)
@ -89,10 +92,18 @@ class CustomizeSearchableSheetVM(
customAttributesRepository.setTags(searchable, tags) customAttributesRepository.setTags(searchable, tags)
} }
fun setVisibility(visibility: VisibilityLevel) {
favoritesService.setVisibility(searchable, visibility)
}
fun getTags(): Flow<List<String>> { fun getTags(): Flow<List<String>> {
return customAttributesRepository.getTags(searchable) return customAttributesRepository.getTags(searchable)
} }
fun getVisibility(): Flow<VisibilityLevel> {
return favoritesService.getVisibility(searchable)
}
suspend fun autocompleteTags(query: String): List<String> { suspend fun autocompleteTags(query: String): List<String> {
return customAttributesRepository.getAllTags(startsWith = query).first() return customAttributesRepository.getAllTags(startsWith = query).first()
} }

View File

@ -22,6 +22,7 @@ import de.mm20.launcher2.appshortcuts.AppShortcut
import de.mm20.launcher2.preferences.search.FavoritesSettings import de.mm20.launcher2.preferences.search.FavoritesSettings
import de.mm20.launcher2.search.Searchable import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.search.data.Tag import de.mm20.launcher2.search.data.Tag
import de.mm20.launcher2.searchable.PinnedLevel
import de.mm20.launcher2.services.favorites.FavoritesService import de.mm20.launcher2.services.favorites.FavoritesService
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
@ -60,21 +61,22 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent {
loading.value = showLoadingIndicator loading.value = showLoadingIndicator
manuallySorted = mutableListOf() manuallySorted = mutableListOf()
manuallySorted = favoritesService.getFavorites( manuallySorted = favoritesService.getFavorites(
manuallySorted = true, minPinnedLevel = PinnedLevel.AutomaticallySorted,
excludeTypes = listOf("tag"), excludeTypes = listOf("tag"),
).first().toMutableList() ).first().toMutableList()
automaticallySorted = favoritesService.getFavorites( automaticallySorted = favoritesService.getFavorites(
automaticallySorted = true, minPinnedLevel = PinnedLevel.AutomaticallySorted,
maxPinnedLevel = PinnedLevel.AutomaticallySorted,
excludeTypes = listOf("tag"), excludeTypes = listOf("tag"),
).first().toMutableList() ).first().toMutableList()
frequentlyUsed = favoritesService.getFavorites( frequentlyUsed = favoritesService.getFavorites(
frequentlyUsed = true, minPinnedLevel = PinnedLevel.FrequentlyUsed,
maxPinnedLevel = PinnedLevel.FrequentlyUsed,
excludeTypes = listOf("tag"), excludeTypes = listOf("tag"),
).first().toMutableList() ).first().toMutableList()
val pinnedTags = favoritesService.getFavorites( val pinnedTags = favoritesService.getFavorites(
includeTypes = listOf("tag"), includeTypes = listOf("tag"),
manuallySorted = true, minPinnedLevel = PinnedLevel.AutomaticallySorted,
automaticallySorted = true,
).first().filterIsInstance<Tag>().toMutableList() ).first().filterIsInstance<Tag>().toMutableList()
availableTags.value = availableTags.value =
customAttributesRepository customAttributesRepository
@ -296,7 +298,8 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent {
favoritesService.unpinItem(item.item) favoritesService.unpinItem(item.item)
viewModelScope.launch { viewModelScope.launch {
frequentlyUsed = favoritesService.getFavorites( frequentlyUsed = favoritesService.getFavorites(
frequentlyUsed = true, minPinnedLevel = PinnedLevel.FrequentlyUsed,
maxPinnedLevel = PinnedLevel.FrequentlyUsed,
excludeTypes = listOf("tag"), excludeTypes = listOf("tag"),
).first().toMutableList() ).first().toMutableList()
buildItemList() buildItemList()

View File

@ -14,6 +14,8 @@ import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.permissions.PermissionGroup import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.permissions.PermissionsManager
import de.mm20.launcher2.search.CalendarEvent import de.mm20.launcher2.search.CalendarEvent
import de.mm20.launcher2.searchable.PinnedLevel
import de.mm20.launcher2.searchable.VisibilityLevel
import de.mm20.launcher2.services.favorites.FavoritesService import de.mm20.launcher2.services.favorites.FavoritesService
import de.mm20.launcher2.widgets.CalendarWidget import de.mm20.launcher2.widgets.CalendarWidget
import de.mm20.launcher2.widgets.CalendarWidgetConfig import de.mm20.launcher2.widgets.CalendarWidgetConfig
@ -42,8 +44,7 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
val pinnedCalendarEvents = val pinnedCalendarEvents =
favoritesService.getFavorites( favoritesService.getFavorites(
includeTypes = listOf("calendar"), includeTypes = listOf("calendar"),
automaticallySorted = true, minPinnedLevel = PinnedLevel.AutomaticallySorted,
manuallySorted = true,
).stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) ).stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
val nextEvents = mutableStateOf<List<CalendarEvent>>(emptyList()) val nextEvents = mutableStateOf<List<CalendarEvent>>(emptyList())
var availableDates = listOf(LocalDate.now()) var availableDates = listOf(LocalDate.now())
@ -172,7 +173,7 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
).collectLatest { events -> ).collectLatest { events ->
searchableRepository.getKeys( searchableRepository.getKeys(
includeTypes = listOf("calendar"), includeTypes = listOf("calendar"),
hidden = true, maxVisibility = VisibilityLevel.SearchOnly,
limit = 9999, limit = 9999,
).collectLatest { hidden -> ).collectLatest { hidden ->
upcomingEvents = events.filter { !hidden.contains(it.key) } upcomingEvents = events.filter { !hidden.contains(it.key) }

View File

@ -10,6 +10,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import de.mm20.launcher2.preferences.ui.UiSettings import de.mm20.launcher2.preferences.ui.UiSettings
import de.mm20.launcher2.searchable.PinnedLevel
import de.mm20.launcher2.services.favorites.FavoritesService import de.mm20.launcher2.services.favorites.FavoritesService
import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid
import de.mm20.launcher2.widgets.CalendarWidget import de.mm20.launcher2.widgets.CalendarWidget
@ -44,9 +45,7 @@ class FavoritesPartProvider : PartProvider, KoinComponent {
val favorites by remember(columns, excludeCalendar) { val favorites by remember(columns, excludeCalendar) {
favoritesService.getFavorites( favoritesService.getFavorites(
excludeTypes = if (excludeCalendar) listOf("calendar", "tag") else listOf("tag"), excludeTypes = if (excludeCalendar) listOf("calendar", "tag") else listOf("tag"),
manuallySorted = true, minPinnedLevel = PinnedLevel.FrequentlyUsed,
automaticallySorted = true,
frequentlyUsed = true,
limit = columns limit = columns
) )
}.collectAsState(emptyList()) }.collectAsState(emptyList())

View File

@ -1,7 +1,7 @@
package de.mm20.launcher2.ui.settings.hiddenitems package de.mm20.launcher2.ui.settings.hiddenitems
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -9,6 +9,8 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Visibility
import androidx.compose.material.icons.rounded.Check
import androidx.compose.material.icons.rounded.Visibility import androidx.compose.material.icons.rounded.Visibility
import androidx.compose.material.icons.rounded.VisibilityOff import androidx.compose.material.icons.rounded.VisibilityOff
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
@ -25,13 +27,15 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.icons.LauncherIcon import de.mm20.launcher2.icons.LauncherIcon
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.search.CalendarEvent
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.searchable.VisibilityLevel
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.ShapedLauncherIcon import de.mm20.launcher2.ui.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
@ -42,7 +46,6 @@ import de.mm20.launcher2.ui.component.preferences.SwitchPreference
fun HiddenItemsSettingsScreen() { fun HiddenItemsSettingsScreen() {
val viewModel: HiddenItemsSettingsScreenVM = viewModel() val viewModel: HiddenItemsSettingsScreenVM = viewModel()
val context = LocalContext.current
val density = LocalDensity.current val density = LocalDensity.current
val apps by viewModel.allApps.collectAsState() val apps by viewModel.allApps.collectAsState()
@ -66,63 +69,41 @@ fun HiddenItemsSettingsScreen() {
viewModel.getIcon(searchable, with(density) { 32.dp.roundToPx() }) viewModel.getIcon(searchable, with(density) { 32.dp.roundToPx() })
}.collectAsState(null) }.collectAsState(null)
val isHidden by remember(searchable.key) { val visibility by remember(searchable.key) {
viewModel.isHidden(searchable) viewModel.getVisibility(searchable)
}.collectAsState(false) }.collectAsState(null)
var showPopup by remember(searchable.key) { var showDropdown by remember { mutableStateOf(false) }
mutableStateOf(false)
}
Box { Box {
HiddenItem( HiddenItem(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.combinedClickable( .clickable {
onClick = { if (searchable is Application || searchable is CalendarEvent) {
viewModel.setHidden(searchable, !isHidden) showDropdown = true
}, } else {
onLongClick = { if (visibility == null) return@clickable
showPopup = true viewModel.setVisibility(
searchable,
if (visibility == VisibilityLevel.Default) VisibilityLevel.Hidden else VisibilityLevel.Default
)
} }
), },
icon = icon, icon = icon,
label = searchable.label, label = searchable.label,
isHidden = isHidden, visibility = visibility,
)
VisibilityDropdown(
expanded = showDropdown,
onDismissRequest = { showDropdown = false },
item = searchable,
value = visibility,
onValueChanged = {
viewModel.setVisibility(searchable, it)
showDropdown = false
}
) )
DropdownMenu(
expanded = showPopup,
onDismissRequest = { showPopup = false },
offset = DpOffset(16.dp, 0.dp)
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.menu_launch)) },
onClick = {
viewModel.launch(context, searchable)
showPopup = false
})
DropdownMenuItem(
text = { Text(stringResource(R.string.menu_app_info)) },
onClick = {
viewModel.openAppInfo(context, searchable)
showPopup = false
})
DropdownMenuItem(
text = {
Text(
stringResource(
if (isHidden) R.string.menu_unhide else R.string.menu_hide
)
)
},
onClick = {
viewModel.setHidden(searchable, !isHidden)
showPopup = false
})
}
} }
} }
@ -142,67 +123,141 @@ fun HiddenItemsSettingsScreen() {
viewModel.getIcon(searchable, with(density) { 32.dp.roundToPx() }) viewModel.getIcon(searchable, with(density) { 32.dp.roundToPx() })
}.collectAsState(null) }.collectAsState(null)
val isHidden by remember(searchable.key) { val visibility by remember(searchable.key) {
viewModel.isHidden(searchable) viewModel.getVisibility(searchable)
}.collectAsState(false) }.collectAsState(null)
var showPopup by remember(searchable.key) { var showDropdown by remember { mutableStateOf(false) }
mutableStateOf(false)
}
Box { Box {
HiddenItem( HiddenItem(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.combinedClickable( .clickable {
onClick = { if (searchable is Application || searchable is CalendarEvent) {
viewModel.setHidden(searchable, !isHidden) showDropdown = true
}, } else {
onLongClick = { if (visibility == null) return@clickable
showPopup = true viewModel.setVisibility(
searchable,
if (visibility == VisibilityLevel.Default) VisibilityLevel.Hidden else VisibilityLevel.Default
)
} }
), },
icon = icon, icon = icon,
label = searchable.label, label = searchable.label,
isHidden = isHidden, visibility = visibility,
)
VisibilityDropdown(
expanded = showDropdown,
onDismissRequest = { showDropdown = false },
item = searchable,
value = visibility,
onValueChanged = {
viewModel.setVisibility(searchable, it)
showDropdown = false
}
) )
DropdownMenu(
expanded = showPopup,
onDismissRequest = { showPopup = false },
offset = DpOffset(16.dp, 0.dp)
) {
DropdownMenuItem(
text = { Text(stringResource(R.string.menu_open_file)) },
onClick = {
viewModel.launch(context, searchable)
showPopup = false
})
DropdownMenuItem(
text = {
Text(
stringResource(
if (isHidden) R.string.menu_unhide else R.string.menu_hide
)
)
},
onClick = {
viewModel.setHidden(searchable, !isHidden)
showPopup = false
})
}
} }
} }
} }
} }
@Composable @Composable
fun HiddenItem( private fun VisibilityDropdown(
expanded: Boolean,
onDismissRequest: () -> Unit,
item: SavableSearchable,
value: VisibilityLevel?,
onValueChanged: (VisibilityLevel) -> Unit,
) {
DropdownMenu(expanded = expanded, onDismissRequest = onDismissRequest) {
DropdownMenuItem(
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Visibility,
contentDescription = null
)
},
text = {
Text(
when (item) {
is Application -> stringResource(R.string.item_visibility_app_default)
is CalendarEvent -> stringResource(R.string.item_visibility_calendar_default)
else -> stringResource(R.string.item_visibility_search_only)
}
)
},
onClick = {
onValueChanged(VisibilityLevel.Default)
},
trailingIcon = {
if (value == VisibilityLevel.Default) {
Icon(
imageVector = Icons.Rounded.Check,
tint = MaterialTheme.colorScheme.primary,
contentDescription = null
)
}
}
)
if (item is Application || item is CalendarEvent) {
DropdownMenuItem(
leadingIcon = {
Icon(
imageVector = Icons.Outlined.Visibility,
contentDescription = null
)
},
text = {
Text(stringResource(R.string.item_visibility_search_only))
},
onClick = {
onValueChanged(VisibilityLevel.SearchOnly)
},
trailingIcon = {
if (value == VisibilityLevel.SearchOnly) {
Icon(
imageVector = Icons.Rounded.Check,
tint = MaterialTheme.colorScheme.primary,
contentDescription = null
)
}
}
)
}
DropdownMenuItem(
leadingIcon = {
Icon(
imageVector = Icons.Rounded.VisibilityOff,
contentDescription = null
)
},
text = {
Text(stringResource(R.string.item_visibility_hidden))
},
onClick = {
onValueChanged(VisibilityLevel.Hidden)
},
trailingIcon = {
if (value == VisibilityLevel.Hidden) {
Icon(
imageVector = Icons.Rounded.Check,
tint = MaterialTheme.colorScheme.primary,
contentDescription = null
)
}
}
)
}
}
@Composable
private fun HiddenItem(
modifier: Modifier, modifier: Modifier,
icon: LauncherIcon?, icon: LauncherIcon?,
label: String, label: String,
isHidden: Boolean, visibility: VisibilityLevel?,
) { ) {
Row( Row(
modifier = modifier modifier = modifier
@ -219,11 +274,18 @@ fun HiddenItem(
modifier = Modifier.weight(1f, fill = true), modifier = Modifier.weight(1f, fill = true),
style = MaterialTheme.typography.titleMedium style = MaterialTheme.typography.titleMedium
) )
Icon( if (visibility != null) {
modifier = Modifier.alpha(if (isHidden) 0.3f else 1f), Icon(
imageVector = if (isHidden) Icons.Rounded.VisibilityOff else Icons.Rounded.Visibility, modifier = Modifier.alpha(if (visibility == VisibilityLevel.Hidden) 0.3f else 1f),
tint = if (isHidden) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.primary, imageVector = when (visibility) {
contentDescription = null VisibilityLevel.Hidden -> Icons.Rounded.VisibilityOff
) VisibilityLevel.Default -> Icons.Rounded.Visibility
VisibilityLevel.SearchOnly -> Icons.Outlined.Visibility
},
tint = if (visibility == VisibilityLevel.Default) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurfaceVariant,
contentDescription = null
)
}
} }
} }

View File

@ -12,6 +12,7 @@ import de.mm20.launcher2.ktx.isAtLeastApiLevel
import de.mm20.launcher2.preferences.ui.SearchUiSettings import de.mm20.launcher2.preferences.ui.SearchUiSettings
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.Application import de.mm20.launcher2.search.Application
import de.mm20.launcher2.searchable.VisibilityLevel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
@ -20,7 +21,6 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
@ -36,20 +36,18 @@ class HiddenItemsSettingsScreenVM : ViewModel(), KoinComponent {
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
val hiddenItems: StateFlow<List<SavableSearchable>> = flow { val hiddenItems: StateFlow<List<SavableSearchable>> = flow {
val hidden = val hidden =
searchableRepository.get(hidden = true).first().filter { it !is Application }.sorted() searchableRepository.get(
maxVisibility = VisibilityLevel.SearchOnly,
).first().filter { it !is Application }.sorted()
emit(hidden) emit(hidden)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
fun isHidden(searchable: SavableSearchable): Flow<Boolean> { fun getVisibility(searchable: SavableSearchable): Flow<VisibilityLevel> {
return searchableRepository.isHidden(searchable) return searchableRepository.getVisibility(searchable)
} }
fun setHidden(searchable: SavableSearchable, hidden: Boolean) { fun setVisibility(searchable: SavableSearchable, visibilityLevel: VisibilityLevel) {
if (hidden) { searchableRepository.upsert(searchable, visibilityLevel)
searchableRepository.upsert(searchable, hidden = true, pinned = false)
} else {
searchableRepository.update(searchable, hidden = false)
}
} }
fun getIcon(searchable: SavableSearchable, size: Int): Flow<LauncherIcon?> { fun getIcon(searchable: SavableSearchable, size: Int): Flow<LauncherIcon?> {

View File

@ -121,9 +121,6 @@ class IconsSettingsScreenVM(
favoritesService.getFavorites( favoritesService.getFavorites(
includeTypes = listOf("app"), includeTypes = listOf("app"),
limit = grid.columnCount, limit = grid.columnCount,
manuallySorted = true,
automaticallySorted = true,
frequentlyUsed = true,
) )
}.shareIn(viewModelScope, started = SharingStarted.WhileSubscribed(), 1) }.shareIn(viewModelScope, started = SharingStarted.WhileSubscribed(), 1)

View File

@ -688,7 +688,7 @@
<string name="create_app_shortcut">Create shortcut</string> <string name="create_app_shortcut">Create shortcut</string>
<string name="frequently_used_show_in_favorites">Show in favorites</string> <string name="frequently_used_show_in_favorites">Show in favorites</string>
<string name="frequently_used_rows">Number of rows</string> <string name="frequently_used_rows">Number of rows</string>
<string name="customize_tags_placeholder">Tags</string> <string name="customize_tags_placeholder">Tags</string>
<string name="preference_screen_search_actions">Quick actions</string> <string name="preference_screen_search_actions">Quick actions</string>
<string name="preference_search_search_actions_summary">Configure quick actions and search shortcuts</string> <string name="preference_search_search_actions_summary">Configure quick actions and search shortcuts</string>
<string name="search_action_call">Call</string> <string name="search_action_call">Call</string>
@ -942,4 +942,11 @@
<string name="poi_category_courthouse">Courthouse</string> <string name="poi_category_courthouse">Courthouse</string>
<string name="poi_category_townhall">Townhall</string> <string name="poi_category_townhall">Townhall</string>
<string name="preference_search_locations_radius_large_radius_warning">Large search radii can significantly slow down the search.</string> <string name="preference_search_locations_radius_large_radius_warning">Large search radii can significantly slow down the search.</string>
<string name="customize_item_label">Label</string>
<string name="customize_item_tags">Tags</string>
<string name="customize_item_visibility">Show in</string>
<string name="item_visibility_app_default">App grid &amp; search results</string>
<string name="item_visibility_calendar_default">Calendar widget &amp; search results</string>
<string name="item_visibility_search_only">Search results</string>
<string name="item_visibility_hidden">Never</string>
</resources> </resources>

View File

@ -34,16 +34,19 @@ interface SearchableDao {
"SELECT * FROM Searchable " + "SELECT * FROM Searchable " +
"WHERE (" + "WHERE (" +
"(:manuallySorted AND pinPosition > 1) OR " + "(:manuallySorted AND pinPosition > 1) OR " +
"(:automaticallySorted AND pinPosition = 1) OR" + "(:automaticallySorted AND pinPosition = 1) OR " +
"(:frequentlyUsed AND pinPosition = 0 AND launchCount > 0 AND hidden = 0) OR " + "(:frequentlyUsed AND pinPosition = 0 AND launchCount > 0) OR " +
"(:hidden AND hidden = 1)" + "(:unused AND pinPosition = 0 AND launchCount = 0)" +
") AND hidden = :hidden ORDER BY pinPosition DESC, weight DESC, launchCount DESC LIMIT :limit" ") AND (hidden <= :minVisibility AND hidden >= :maxVisibility) " +
"ORDER BY pinPosition DESC, weight DESC, launchCount DESC LIMIT :limit"
) )
fun get( fun get(
manuallySorted: Boolean = false, manuallySorted: Boolean = false,
automaticallySorted: Boolean = false, automaticallySorted: Boolean = false,
frequentlyUsed: Boolean = false, frequentlyUsed: Boolean = false,
hidden: Boolean = false, unused: Boolean = false,
minVisibility: Int = 2,
maxVisibility: Int = 0,
limit: Int, limit: Int,
): Flow<List<SavedSearchableEntity>> ): Flow<List<SavedSearchableEntity>>
@ -52,17 +55,20 @@ interface SearchableDao {
"WHERE (`type` IN (:includeTypes)) AND " + "WHERE (`type` IN (:includeTypes)) AND " +
"(" + "(" +
"(:manuallySorted AND pinPosition > 1) OR " + "(:manuallySorted AND pinPosition > 1) OR " +
"(:automaticallySorted AND pinPosition = 1) OR" + "(:automaticallySorted AND pinPosition = 1) OR " +
"(:frequentlyUsed AND pinPosition = 0 AND launchCount > 0 AND hidden = 0) OR " + "(:frequentlyUsed AND pinPosition = 0 AND launchCount > 0) OR" +
"(:hidden AND hidden = 1)" + "(:unused AND pinPosition = 0 AND launchCount = 0)" +
") ORDER BY pinPosition DESC, weight DESC, launchCount DESC LIMIT :limit" ") AND (hidden <= :minVisibility AND hidden >= :maxVisibility) " +
"ORDER BY pinPosition DESC, weight DESC, launchCount DESC LIMIT :limit"
) )
fun getIncludeTypes( fun getIncludeTypes(
includeTypes: List<String>?, includeTypes: List<String>?,
manuallySorted: Boolean = false, manuallySorted: Boolean = false,
automaticallySorted: Boolean = false, automaticallySorted: Boolean = false,
frequentlyUsed: Boolean = false, frequentlyUsed: Boolean = false,
hidden: Boolean = false, unused: Boolean = false,
minVisibility: Int = 2,
maxVisibility: Int = 0,
limit: Int, limit: Int,
): Flow<List<SavedSearchableEntity>> ): Flow<List<SavedSearchableEntity>>
@ -71,17 +77,20 @@ interface SearchableDao {
"WHERE (`type` NOT IN (:excludeTypes)) AND " + "WHERE (`type` NOT IN (:excludeTypes)) AND " +
"(" + "(" +
"(:manuallySorted AND pinPosition > 1) OR " + "(:manuallySorted AND pinPosition > 1) OR " +
"(:automaticallySorted AND pinPosition = 1) OR" + "(:automaticallySorted AND pinPosition = 1) OR " +
"(:frequentlyUsed AND pinPosition = 0 AND launchCount > 0 AND hidden = 0) OR " + "(:frequentlyUsed AND pinPosition = 0 AND launchCount > 0) OR " +
"(:hidden AND hidden = 1)" + "(:unused AND pinPosition = 0 AND launchCount = 0)" +
") ORDER BY pinPosition DESC, weight DESC, launchCount DESC LIMIT :limit" ") AND (hidden <= :minVisibility AND hidden >= :maxVisibility) " +
"ORDER BY pinPosition DESC, weight DESC, launchCount DESC LIMIT :limit"
) )
fun getExcludeTypes( fun getExcludeTypes(
excludeTypes: List<String>?, excludeTypes: List<String>?,
manuallySorted: Boolean = false, manuallySorted: Boolean = false,
automaticallySorted: Boolean = false, automaticallySorted: Boolean = false,
frequentlyUsed: Boolean = false, frequentlyUsed: Boolean = false,
hidden: Boolean = false, unused: Boolean = false,
minVisibility: Int = 2,
maxVisibility: Int = 0,
limit: Int, limit: Int,
): Flow<List<SavedSearchableEntity>> ): Flow<List<SavedSearchableEntity>>
@ -89,16 +98,19 @@ interface SearchableDao {
"SELECT `key` FROM Searchable " + "SELECT `key` FROM Searchable " +
"WHERE (" + "WHERE (" +
"(:manuallySorted AND pinPosition > 1) OR " + "(:manuallySorted AND pinPosition > 1) OR " +
"(:automaticallySorted AND pinPosition = 1) OR" + "(:automaticallySorted AND pinPosition = 1) OR " +
"(:frequentlyUsed AND pinPosition = 0 AND launchCount > 0) OR " + "(:frequentlyUsed AND pinPosition = 0 AND launchCount > 0) OR " +
"(:hidden AND hidden = 1)" + "(:unused AND pinPosition = 0 AND launchCount = 0)" +
") AND hidden = :hidden ORDER BY pinPosition DESC, weight DESC, launchCount DESC LIMIT :limit" ") AND (hidden <= :minVisibility AND hidden >= :maxVisibility) " +
"ORDER BY pinPosition DESC, weight DESC, launchCount DESC LIMIT :limit"
) )
fun getKeys( fun getKeys(
manuallySorted: Boolean = false, manuallySorted: Boolean = false,
automaticallySorted: Boolean = false, automaticallySorted: Boolean = false,
frequentlyUsed: Boolean = false, frequentlyUsed: Boolean = false,
hidden: Boolean = false, unused: Boolean = false,
minVisibility: Int = 2,
maxVisibility: Int = 0,
limit: Int, limit: Int,
): Flow<List<String>> ): Flow<List<String>>
@ -109,15 +121,18 @@ interface SearchableDao {
"(:manuallySorted AND pinPosition > 1) OR " + "(:manuallySorted AND pinPosition > 1) OR " +
"(:automaticallySorted AND pinPosition = 1) OR" + "(:automaticallySorted AND pinPosition = 1) OR" +
"(:frequentlyUsed AND pinPosition = 0 AND launchCount > 0) OR " + "(:frequentlyUsed AND pinPosition = 0 AND launchCount > 0) OR " +
"(:hidden AND hidden = 1)" + "(:unused AND pinPosition = 0 AND launchCount = 0)" +
") AND hidden = :hidden ORDER BY pinPosition DESC, weight DESC, launchCount DESC LIMIT :limit" ") AND (hidden <= :minVisibility AND hidden >= :maxVisibility) " +
"ORDER BY pinPosition DESC, weight DESC, launchCount DESC LIMIT :limit"
) )
fun getKeysIncludeTypes( fun getKeysIncludeTypes(
includeTypes: List<String>?, includeTypes: List<String>?,
manuallySorted: Boolean = false, manuallySorted: Boolean = false,
automaticallySorted: Boolean = false, automaticallySorted: Boolean = false,
frequentlyUsed: Boolean = false, frequentlyUsed: Boolean = false,
hidden: Boolean = false, unused: Boolean = false,
minVisibility: Int = 2,
maxVisibility: Int = 0,
limit: Int, limit: Int,
): Flow<List<String>> ): Flow<List<String>>
@ -126,17 +141,20 @@ interface SearchableDao {
"WHERE (`type` NOT IN (:excludeTypes)) AND " + "WHERE (`type` NOT IN (:excludeTypes)) AND " +
"(" + "(" +
"(:manuallySorted AND pinPosition > 1) OR " + "(:manuallySorted AND pinPosition > 1) OR " +
"(:automaticallySorted AND pinPosition = 1) OR" + "(:automaticallySorted AND pinPosition = 1) OR " +
"(:frequentlyUsed AND pinPosition = 0 AND launchCount > 0) OR " + "(:frequentlyUsed AND pinPosition = 0 AND launchCount > 0) OR " +
"(:hidden AND hidden = 1)" + "(:unused AND pinPosition = 0 AND launchCount = 0)" +
") AND hidden = :hidden ORDER BY pinPosition DESC, weight DESC, launchCount DESC LIMIT :limit" ") AND (hidden <= :minVisibility AND hidden >= :maxVisibility) " +
"ORDER BY pinPosition DESC, weight DESC, launchCount DESC LIMIT :limit"
) )
fun getKeysExcludeTypes( fun getKeysExcludeTypes(
excludeTypes: List<String>?, excludeTypes: List<String>?,
manuallySorted: Boolean = false, manuallySorted: Boolean = false,
automaticallySorted: Boolean = false, automaticallySorted: Boolean = false,
frequentlyUsed: Boolean = false, frequentlyUsed: Boolean = false,
hidden: Boolean = false, unused: Boolean = false,
minVisibility: Int = 2,
maxVisibility: Int = 0,
limit: Int, limit: Int,
): Flow<List<String>> ): Flow<List<String>>
@ -184,7 +202,7 @@ interface SearchableDao {
fun sortByWeight(keys: List<String>): Flow<List<String>> fun sortByWeight(keys: List<String>): Flow<List<String>>
@Query("SELECT hidden FROM Searchable WHERE `key` = :key UNION SELECT 0 as hidden ORDER BY hidden DESC LIMIT 1") @Query("SELECT hidden FROM Searchable WHERE `key` = :key UNION SELECT 0 as hidden ORDER BY hidden DESC LIMIT 1")
fun isHidden(key: String): Flow<Boolean> fun getVisibility(key: String): Flow<Int>
@Query("SELECT pinPosition FROM Searchable WHERE `key` = :key UNION SELECT 0 as pinPosition ORDER BY pinPosition DESC LIMIT 1") @Query("SELECT pinPosition FROM Searchable WHERE `key` = :key UNION SELECT 0 as pinPosition ORDER BY pinPosition DESC LIMIT 1")
fun isPinned(key: String): Flow<Boolean> fun isPinned(key: String): Flow<Boolean>

View File

@ -11,7 +11,7 @@ data class SavedSearchableEntity(
@ColumnInfo(name = "searchable") val serializedSearchable: String, @ColumnInfo(name = "searchable") val serializedSearchable: String,
@ColumnInfo(defaultValue = "0") val launchCount: Int, @ColumnInfo(defaultValue = "0") val launchCount: Int,
@ColumnInfo(defaultValue = "0") val pinPosition: Int, @ColumnInfo(defaultValue = "0") val pinPosition: Int,
@ColumnInfo(defaultValue = "0") val hidden: Boolean, @ColumnInfo(name="hidden", defaultValue = "0") val visibility: Int,
@ColumnInfo(defaultValue = "0.0") val weight: Double @ColumnInfo(defaultValue = "0.0") val weight: Double
) )

View File

@ -0,0 +1,8 @@
package de.mm20.launcher2.searchable
enum class PinnedLevel {
NotPinned,
FrequentlyUsed,
AutomaticallySorted,
ManuallySorted,
}

View File

@ -20,7 +20,6 @@ import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.json.JSONArray import org.json.JSONArray
@ -32,7 +31,7 @@ import org.koin.core.error.NoBeanDefFoundException
import org.koin.core.qualifier.named import org.koin.core.qualifier.named
import java.io.File import java.io.File
interface SavableSearchableRepository: Backupable { interface SavableSearchableRepository : Backupable {
fun insert( fun insert(
searchable: SavableSearchable, searchable: SavableSearchable,
@ -40,7 +39,7 @@ interface SavableSearchableRepository: Backupable {
fun upsert( fun upsert(
searchable: SavableSearchable, searchable: SavableSearchable,
hidden: Boolean? = null, visibility: VisibilityLevel? = null,
pinned: Boolean? = null, pinned: Boolean? = null,
launchCount: Int? = null, launchCount: Int? = null,
weight: Double? = null, weight: Double? = null,
@ -48,7 +47,7 @@ interface SavableSearchableRepository: Backupable {
fun update( fun update(
searchable: SavableSearchable, searchable: SavableSearchable,
hidden: Boolean? = null, visibility: VisibilityLevel? = null,
pinned: Boolean? = null, pinned: Boolean? = null,
launchCount: Int? = null, launchCount: Int? = null,
weight: Double? = null, weight: Double? = null,
@ -61,29 +60,35 @@ interface SavableSearchableRepository: Backupable {
searchable: SavableSearchable, searchable: SavableSearchable,
) )
/**
* @param minVisibility the minimum visibility of the searchables to return. A visible is
* considered to be "lower" when it makes an item less visible.
* @param maxVisibility the maximum visibility of the searchables to return. A visible is
* considered to be "higher" when it makes an item more visible.
*/
fun get( fun get(
includeTypes: List<String>? = null, includeTypes: List<String>? = null,
excludeTypes: List<String>? = null, excludeTypes: List<String>? = null,
manuallySorted: Boolean = false, minPinnedLevel: PinnedLevel = PinnedLevel.NotPinned,
automaticallySorted: Boolean = false, maxPinnedLevel: PinnedLevel = PinnedLevel.ManuallySorted,
frequentlyUsed: Boolean = false, minVisibility: VisibilityLevel = VisibilityLevel.Hidden,
hidden: Boolean = false, maxVisibility: VisibilityLevel = VisibilityLevel.Default,
limit: Int = 100, limit: Int = 100,
): Flow<List<SavableSearchable>> ): Flow<List<SavableSearchable>>
fun getKeys( fun getKeys(
includeTypes: List<String>? = null, includeTypes: List<String>? = null,
excludeTypes: List<String>? = null, excludeTypes: List<String>? = null,
manuallySorted: Boolean = false, minPinnedLevel: PinnedLevel = PinnedLevel.NotPinned,
automaticallySorted: Boolean = false, maxPinnedLevel: PinnedLevel = PinnedLevel.ManuallySorted,
frequentlyUsed: Boolean = false, minVisibility: VisibilityLevel = VisibilityLevel.Hidden,
hidden: Boolean = false, maxVisibility: VisibilityLevel = VisibilityLevel.Default,
limit: Int = 100, limit: Int = 100,
): Flow<List<String>> ): Flow<List<String>>
fun isPinned(searchable: SavableSearchable): Flow<Boolean> fun isPinned(searchable: SavableSearchable): Flow<Boolean>
fun isHidden(searchable: SavableSearchable): Flow<Boolean> fun getVisibility(searchable: SavableSearchable): Flow<VisibilityLevel>
fun updateFavorites( fun updateFavorites(
manuallySorted: List<SavableSearchable>, manuallySorted: List<SavableSearchable>,
automaticallySorted: List<SavableSearchable>, automaticallySorted: List<SavableSearchable>,
@ -132,7 +137,7 @@ internal class SavableSearchableRepositoryImpl(
key = searchable.key, key = searchable.key,
type = searchable.domain, type = searchable.domain,
serializedSearchable = searchable.serialize() ?: return@launch, serializedSearchable = searchable.serialize() ?: return@launch,
hidden = false, visibility = VisibilityLevel.Default.value,
launchCount = 0, launchCount = 0,
weight = 0.0, weight = 0.0,
pinPosition = 0, pinPosition = 0,
@ -144,7 +149,7 @@ internal class SavableSearchableRepositoryImpl(
override fun upsert( override fun upsert(
searchable: SavableSearchable, searchable: SavableSearchable,
hidden: Boolean?, visibility: VisibilityLevel?,
pinned: Boolean?, pinned: Boolean?,
launchCount: Int?, launchCount: Int?,
weight: Double? weight: Double?
@ -156,7 +161,7 @@ internal class SavableSearchableRepositoryImpl(
SavedSearchableEntity( SavedSearchableEntity(
key = searchable.key, key = searchable.key,
type = searchable.domain, type = searchable.domain,
hidden = hidden ?: entity?.hidden ?: false, visibility = visibility?.value ?: entity?.visibility ?: 0,
pinPosition = pinned?.let { if (it) 1 else 0 } ?: entity?.pinPosition ?: 0, pinPosition = pinned?.let { if (it) 1 else 0 } ?: entity?.pinPosition ?: 0,
launchCount = launchCount ?: entity?.launchCount ?: 0, launchCount = launchCount ?: entity?.launchCount ?: 0,
weight = weight ?: entity?.weight ?: 0.0, weight = weight ?: entity?.weight ?: 0.0,
@ -168,7 +173,7 @@ internal class SavableSearchableRepositoryImpl(
override fun update( override fun update(
searchable: SavableSearchable, searchable: SavableSearchable,
hidden: Boolean?, visibility: VisibilityLevel?,
pinned: Boolean?, pinned: Boolean?,
launchCount: Int?, launchCount: Int?,
weight: Double? weight: Double?
@ -180,7 +185,7 @@ internal class SavableSearchableRepositoryImpl(
SavedSearchableEntity( SavedSearchableEntity(
key = searchable.key, key = searchable.key,
type = searchable.domain, type = searchable.domain,
hidden = hidden ?: entity?.hidden ?: false, visibility = visibility?.value ?: entity?.visibility ?: 0,
pinPosition = pinned?.let { if (it) 1 else 0 } ?: entity?.pinPosition ?: 0, pinPosition = pinned?.let { if (it) 1 else 0 } ?: entity?.pinPosition ?: 0,
launchCount = launchCount ?: entity?.launchCount ?: 0, launchCount = launchCount ?: entity?.launchCount ?: 0,
weight = weight ?: entity?.weight ?: 0.0, weight = weight ?: entity?.weight ?: 0.0,
@ -198,7 +203,8 @@ internal class SavableSearchableRepositoryImpl(
WeightFactor.High -> WEIGHT_FACTOR_HIGH WeightFactor.High -> WEIGHT_FACTOR_HIGH
else -> WEIGHT_FACTOR_MEDIUM else -> WEIGHT_FACTOR_MEDIUM
} }
val item = SavedSearchable(searchable.key, searchable, 0, 0, false, 0.0) val item =
SavedSearchable(searchable.key, searchable, 0, 0, VisibilityLevel.Default, 0.0)
item.toDatabaseEntity()?.let { item.toDatabaseEntity()?.let {
database.searchableDao() database.searchableDao()
.touch(it, weightFactor) .touch(it, weightFactor)
@ -209,37 +215,43 @@ internal class SavableSearchableRepositoryImpl(
override fun get( override fun get(
includeTypes: List<String>?, includeTypes: List<String>?,
excludeTypes: List<String>?, excludeTypes: List<String>?,
manuallySorted: Boolean, minPinnedLevel: PinnedLevel,
automaticallySorted: Boolean, maxPinnedLevel: PinnedLevel,
frequentlyUsed: Boolean, minVisibility: VisibilityLevel,
hidden: Boolean, maxVisibility: VisibilityLevel,
limit: Int limit: Int
): Flow<List<SavableSearchable>> { ): Flow<List<SavableSearchable>> {
val dao = database.searchableDao() val dao = database.searchableDao()
val entities = when { val entities = when {
includeTypes == null && excludeTypes == null -> dao.get( includeTypes == null && excludeTypes == null -> dao.get(
manuallySorted = manuallySorted, manuallySorted = PinnedLevel.ManuallySorted in minPinnedLevel..maxPinnedLevel,
automaticallySorted = automaticallySorted, automaticallySorted = PinnedLevel.AutomaticallySorted in minPinnedLevel..maxPinnedLevel,
frequentlyUsed = frequentlyUsed, frequentlyUsed = PinnedLevel.FrequentlyUsed in minPinnedLevel..maxPinnedLevel,
hidden = hidden, unused = PinnedLevel.NotPinned in minPinnedLevel..maxPinnedLevel,
minVisibility = minVisibility.value,
maxVisibility = maxVisibility.value,
limit = limit limit = limit
) )
includeTypes == null -> dao.getExcludeTypes( includeTypes == null -> dao.getExcludeTypes(
excludeTypes = excludeTypes, excludeTypes = excludeTypes,
manuallySorted = manuallySorted, manuallySorted = PinnedLevel.ManuallySorted in minPinnedLevel..maxPinnedLevel,
automaticallySorted = automaticallySorted, automaticallySorted = PinnedLevel.AutomaticallySorted in minPinnedLevel..maxPinnedLevel,
frequentlyUsed = frequentlyUsed, frequentlyUsed = PinnedLevel.FrequentlyUsed in minPinnedLevel..maxPinnedLevel,
hidden = hidden, unused = PinnedLevel.NotPinned in minPinnedLevel..maxPinnedLevel,
minVisibility = minVisibility.value,
maxVisibility = maxVisibility.value,
limit = limit limit = limit
) )
excludeTypes == null -> dao.getIncludeTypes( excludeTypes == null -> dao.getIncludeTypes(
includeTypes = includeTypes, includeTypes = includeTypes,
manuallySorted = manuallySorted, manuallySorted = PinnedLevel.ManuallySorted in minPinnedLevel..maxPinnedLevel,
automaticallySorted = automaticallySorted, automaticallySorted = PinnedLevel.AutomaticallySorted in minPinnedLevel..maxPinnedLevel,
frequentlyUsed = frequentlyUsed, frequentlyUsed = PinnedLevel.FrequentlyUsed in minPinnedLevel..maxPinnedLevel,
hidden = hidden, unused = PinnedLevel.NotPinned in minPinnedLevel..maxPinnedLevel,
minVisibility = minVisibility.value,
maxVisibility = maxVisibility.value,
limit = limit limit = limit
) )
@ -254,37 +266,43 @@ internal class SavableSearchableRepositoryImpl(
override fun getKeys( override fun getKeys(
includeTypes: List<String>?, includeTypes: List<String>?,
excludeTypes: List<String>?, excludeTypes: List<String>?,
manuallySorted: Boolean, minPinnedLevel: PinnedLevel,
automaticallySorted: Boolean, maxPinnedLevel: PinnedLevel,
frequentlyUsed: Boolean, minVisibility: VisibilityLevel,
hidden: Boolean, maxVisibility: VisibilityLevel,
limit: Int limit: Int
): Flow<List<String>> { ): Flow<List<String>> {
val dao = database.searchableDao() val dao = database.searchableDao()
return when { return when {
includeTypes == null && excludeTypes == null -> dao.getKeys( includeTypes == null && excludeTypes == null -> dao.getKeys(
manuallySorted = manuallySorted, manuallySorted = PinnedLevel.ManuallySorted in minPinnedLevel..maxPinnedLevel,
automaticallySorted = automaticallySorted, automaticallySorted = PinnedLevel.AutomaticallySorted in minPinnedLevel..maxPinnedLevel,
frequentlyUsed = frequentlyUsed, frequentlyUsed = PinnedLevel.FrequentlyUsed in minPinnedLevel..maxPinnedLevel,
hidden = hidden, unused = PinnedLevel.NotPinned in minPinnedLevel..maxPinnedLevel,
minVisibility = minVisibility.value,
maxVisibility = maxVisibility.value,
limit = limit limit = limit
) )
includeTypes == null -> dao.getKeysExcludeTypes( includeTypes == null -> dao.getKeysExcludeTypes(
excludeTypes = excludeTypes, excludeTypes = excludeTypes,
manuallySorted = manuallySorted, manuallySorted = PinnedLevel.ManuallySorted in minPinnedLevel..maxPinnedLevel,
automaticallySorted = automaticallySorted, automaticallySorted = PinnedLevel.AutomaticallySorted in minPinnedLevel..maxPinnedLevel,
frequentlyUsed = frequentlyUsed, frequentlyUsed = PinnedLevel.FrequentlyUsed in minPinnedLevel..maxPinnedLevel,
hidden = hidden, unused = PinnedLevel.NotPinned in minPinnedLevel..maxPinnedLevel,
minVisibility = minVisibility.value,
maxVisibility = maxVisibility.value,
limit = limit limit = limit
) )
excludeTypes == null -> dao.getKeysIncludeTypes( excludeTypes == null -> dao.getKeysIncludeTypes(
includeTypes = includeTypes, includeTypes = includeTypes,
manuallySorted = manuallySorted, manuallySorted = PinnedLevel.ManuallySorted in minPinnedLevel..maxPinnedLevel,
automaticallySorted = automaticallySorted, automaticallySorted = PinnedLevel.AutomaticallySorted in minPinnedLevel..maxPinnedLevel,
frequentlyUsed = frequentlyUsed, frequentlyUsed = PinnedLevel.FrequentlyUsed in minPinnedLevel..maxPinnedLevel,
hidden = hidden, unused = PinnedLevel.NotPinned in minPinnedLevel..maxPinnedLevel,
minVisibility = minVisibility.value,
maxVisibility = maxVisibility.value,
limit = limit limit = limit
) )
@ -296,8 +314,10 @@ internal class SavableSearchableRepositoryImpl(
return database.searchableDao().isPinned(searchable.key) return database.searchableDao().isPinned(searchable.key)
} }
override fun isHidden(searchable: SavableSearchable): Flow<Boolean> { override fun getVisibility(searchable: SavableSearchable): Flow<VisibilityLevel> {
return database.searchableDao().isHidden(searchable.key) return database.searchableDao().getVisibility(searchable.key).map {
VisibilityLevel.fromInt(it)
}
} }
override fun delete(searchable: SavableSearchable) { override fun delete(searchable: SavableSearchable) {
@ -367,7 +387,7 @@ internal class SavableSearchableRepositoryImpl(
searchable = searchable, searchable = searchable,
launchCount = entity.launchCount, launchCount = entity.launchCount,
pinPosition = entity.pinPosition, pinPosition = entity.pinPosition,
hidden = entity.hidden, visibility = VisibilityLevel.fromInt(entity.visibility),
weight = entity.weight weight = entity.weight
) )
} }
@ -405,7 +425,7 @@ internal class SavableSearchableRepositoryImpl(
jsonObjectOf( jsonObjectOf(
"key" to fav.key, "key" to fav.key,
"type" to fav.type, "type" to fav.type,
"hidden" to fav.hidden, "visibility" to fav.visibility,
"launchCount" to fav.launchCount, "launchCount" to fav.launchCount,
"pinPosition" to fav.pinPosition, "pinPosition" to fav.pinPosition,
"searchable" to fav.serializedSearchable, "searchable" to fav.serializedSearchable,
@ -441,7 +461,7 @@ internal class SavableSearchableRepositoryImpl(
type = json.optString("type").takeIf { it.isNotEmpty() } ?: continue, type = json.optString("type").takeIf { it.isNotEmpty() } ?: continue,
serializedSearchable = json.getString("searchable"), serializedSearchable = json.getString("searchable"),
launchCount = json.getInt("launchCount"), launchCount = json.getInt("launchCount"),
hidden = json.getBoolean("hidden"), visibility = json.optInt("visibility", 0),
pinPosition = json.getInt("pinPosition"), pinPosition = json.getInt("pinPosition"),
weight = json.optDouble("weight").takeIf { !it.isNaN() } ?: 0.0 weight = json.optDouble("weight").takeIf { !it.isNaN() } ?: 0.0
) )

View File

@ -11,7 +11,7 @@ data class SavedSearchable(
val searchable: SavableSearchable?, val searchable: SavableSearchable?,
var launchCount: Int, var launchCount: Int,
var pinPosition: Int, var pinPosition: Int,
var hidden: Boolean, var visibility: VisibilityLevel,
var weight: Double var weight: Double
) { ) {
fun toDatabaseEntity(): SavedSearchableEntity? { fun toDatabaseEntity(): SavedSearchableEntity? {
@ -21,7 +21,7 @@ data class SavedSearchable(
key = key, key = key,
type = searchable.domain, type = searchable.domain,
serializedSearchable = data, serializedSearchable = data,
hidden = hidden, visibility = visibility.value,
pinPosition = pinPosition, pinPosition = pinPosition,
launchCount = launchCount, launchCount = launchCount,
weight = weight weight = weight

View File

@ -0,0 +1,32 @@
package de.mm20.launcher2.searchable
enum class VisibilityLevel(val value: Int) {
/**
* Default visibility:
* - apps are shown in app drawer
* - calendar events are shown in calendar widget
* - everything else is shown in search results
* - items can appear in frequently used section
*/
Default(0),
/**
* Search only visibility:
* - items are only shown in search results
* - items can appear in frequently used section
* - items are not shown in app drawer or calendar widget
*/
SearchOnly(1),
/**
* Hidden visibility:
* - items are not shown in search results
* - items are not shown in app drawer or calendar widget
* - items are not shown in frequently used section
*/
Hidden(2);
companion object {
internal fun fromInt(value: Int) = entries.firstOrNull() { it.value == value } ?: Default
}
}

View File

@ -1,14 +1,13 @@
package de.mm20.launcher2.badges.providers package de.mm20.launcher2.badges.providers
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.HourglassBottom
import androidx.compose.material.icons.rounded.VisibilityOff import androidx.compose.material.icons.rounded.VisibilityOff
import de.mm20.launcher2.badges.Badge import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.BadgeIcon import de.mm20.launcher2.badges.BadgeIcon
import de.mm20.launcher2.badges.R
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.Searchable import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.searchable.SavableSearchableRepository import de.mm20.launcher2.searchable.SavableSearchableRepository
import de.mm20.launcher2.searchable.VisibilityLevel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -28,7 +27,7 @@ class HiddenItemBadgeProvider(
private val scope = CoroutineScope(Job() + Dispatchers.Default) private val scope = CoroutineScope(Job() + Dispatchers.Default)
private val hiddenItemKeys = searchableRepository.getKeys( private val hiddenItemKeys = searchableRepository.getKeys(
hidden = true, maxVisibility = VisibilityLevel.Hidden,
limit = 9999, limit = 9999,
).shareIn(scope, SharingStarted.WhileSubscribed(), 1) ).shareIn(scope, SharingStarted.WhileSubscribed(), 1)

View File

@ -1,7 +1,9 @@
package de.mm20.launcher2.services.favorites package de.mm20.launcher2.services.favorites
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.searchable.PinnedLevel
import de.mm20.launcher2.searchable.SavableSearchableRepository import de.mm20.launcher2.searchable.SavableSearchableRepository
import de.mm20.launcher2.searchable.VisibilityLevel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
class FavoritesService( class FavoritesService(
@ -10,18 +12,17 @@ class FavoritesService(
fun getFavorites( fun getFavorites(
includeTypes: List<String>? = null, includeTypes: List<String>? = null,
excludeTypes: List<String>? = null, excludeTypes: List<String>? = null,
manuallySorted: Boolean = false, minPinnedLevel: PinnedLevel = PinnedLevel.FrequentlyUsed,
automaticallySorted: Boolean = false, maxPinnedLevel: PinnedLevel = PinnedLevel.ManuallySorted,
frequentlyUsed: Boolean = false,
limit: Int = 100, limit: Int = 100,
): Flow<List<SavableSearchable>> { ): Flow<List<SavableSearchable>> {
return searchableRepository.get( return searchableRepository.get(
includeTypes = includeTypes, includeTypes = includeTypes,
excludeTypes = excludeTypes, excludeTypes = excludeTypes,
manuallySorted = manuallySorted, minPinnedLevel = minPinnedLevel,
automaticallySorted = automaticallySorted, maxPinnedLevel = maxPinnedLevel,
frequentlyUsed = frequentlyUsed,
limit = limit, limit = limit,
minVisibility = VisibilityLevel.SearchOnly,
) )
} }
@ -29,8 +30,8 @@ class FavoritesService(
return searchableRepository.isPinned(searchable) return searchableRepository.isPinned(searchable)
} }
fun isHidden(searchable: SavableSearchable): Flow<Boolean> { fun getVisibility(searchable: SavableSearchable): Flow<VisibilityLevel> {
return searchableRepository.isHidden(searchable) return searchableRepository.getVisibility(searchable)
} }
fun pinItem(searchable: SavableSearchable) { fun pinItem(searchable: SavableSearchable) {
@ -44,7 +45,7 @@ class FavoritesService(
searchableRepository.update( searchableRepository.update(
searchable, searchable,
pinned = false, pinned = false,
hidden = false, visibility = VisibilityLevel.Default,
weight = 0.0, weight = 0.0,
launchCount = 0, launchCount = 0,
) )
@ -57,17 +58,10 @@ class FavoritesService(
) )
} }
fun hideItem(searchable: SavableSearchable) { fun setVisibility(searchable: SavableSearchable, visibility: VisibilityLevel) {
searchableRepository.upsert( searchableRepository.upsert(
searchable, searchable,
hidden = true, visibility = visibility,
)
}
fun unhideItem(searchable: SavableSearchable) {
searchableRepository.upsert(
searchable,
hidden = false,
) )
} }

View File

@ -4,6 +4,7 @@ import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
import de.mm20.launcher2.searchable.SavableSearchableRepository import de.mm20.launcher2.searchable.SavableSearchableRepository
import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.data.Tag import de.mm20.launcher2.search.data.Tag
import de.mm20.launcher2.searchable.PinnedLevel
import de.mm20.launcher2.services.tags.TagsService import de.mm20.launcher2.services.tags.TagsService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -46,8 +47,7 @@ internal class TagsServiceImpl(
customAttributesRepository.renameTag(tag, newName).join() customAttributesRepository.renameTag(tag, newName).join()
val pinnedTags = searchableRepository.get( val pinnedTags = searchableRepository.get(
includeTypes = listOf(Tag.Domain), includeTypes = listOf(Tag.Domain),
manuallySorted = true, minPinnedLevel = PinnedLevel.AutomaticallySorted,
automaticallySorted = true
).first() ).first()
val oldTag = Tag(tag) val oldTag = Tag(tag)
if (pinnedTags.any { it.key == oldTag.key }) { if (pinnedTags.any { it.key == oldTag.key }) {