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

View File

@ -4,11 +4,14 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
@ -28,7 +31,6 @@ import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextFieldDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
@ -58,9 +60,11 @@ fun OutlinedTagsInputField(
modifier: Modifier = Modifier,
tags: List<String>,
onTagsChange: (tags: List<String>) -> Unit,
label: @Composable (() -> Unit)? = null,
placeholder: @Composable (() -> Unit)? = null,
leadingIcon: @Composable (() -> Unit)? = null,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
textStyle: TextStyle = MaterialTheme.typography.bodyLarge,
textStyle: TextStyle = LocalTextStyle.current,
textColor: Color = LocalContentColor.current,
onAutocomplete: (suspend (query: String) -> List<String>)? = null
) {
@ -127,14 +131,23 @@ fun OutlinedTagsInputField(
}),
decorationBox = { innerTextField ->
OutlinedTextFieldDefaults.DecorationBox(
contentPadding = PaddingValues(0.dp),
value = value,
contentPadding = PaddingValues(
start = 16.dp,
end = 0.dp,
top = 12.dp,
bottom = 12.dp
),
value = tags.joinToString() + value,
label = label,
leadingIcon = leadingIcon,
innerTextField = {
Box {
Box(
contentAlignment = Alignment.CenterStart,
) {
Row(
modifier = Modifier
.horizontalScroll(rememberScrollState())
.padding(horizontal = 16.dp),
.requiredHeight(32.dp)
.horizontalScroll(rememberScrollState()),
verticalAlignment = Alignment.CenterVertically
) {
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.UnitConverter
import de.mm20.launcher2.searchable.SavableSearchableRepository
import de.mm20.launcher2.searchable.VisibilityLevel
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.services.favorites.FavoritesService
import kotlinx.coroutines.CancellationException
@ -102,13 +103,6 @@ class SearchVM : ViewModel(), KoinComponent {
val separateWorkProfile = searchUiSettings.separateWorkProfile
private val hiddenItemKeys = searchableRepository
.getKeys(
hidden = true,
limit = 9999,
)
.shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1)
val bestMatch = mutableStateOf<Searchable?>(null)
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 ->
val hidden = mutableListOf<SavableSearchable>()
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(
leftActions = listOf(
DefaultToolbarAction(

View File

@ -221,39 +221,6 @@ fun CalendarItem(
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(
DefaultToolbarAction(
label = stringResource(R.string.menu_calendar_open_externally),
@ -272,8 +239,6 @@ fun CalendarItem(
action = { sheetManager.showCustomizeSearchableModal(calendar) }
))
toolbarActions.add(hideAction)
Toolbar(
leftActions = listOf(
DefaultToolbarAction(

View File

@ -78,18 +78,6 @@ class SearchableItemVM : ListItemViewModel(), KoinComponent {
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 {
if (it == null) emptyFlow() else badgeService.getBadge(it)
}.stateIn(viewModelScope, SharingStarted.Lazily, null)

View File

@ -205,39 +205,6 @@ fun ContactItem(
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(
DefaultToolbarAction(
label = stringResource(R.string.menu_contacts_open_externally),
@ -255,7 +222,6 @@ fun ContactItem(
action = { sheetManager.showCustomizeSearchableModal(contact) }
))
toolbarActions.add(hideAction)
Toolbar(
leftActions = listOf(

View File

@ -257,41 +257,6 @@ fun FileItem(
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(
leftActions = listOf(
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(
modifier = Modifier.fillMaxWidth(),
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(
leftActions = listOf(
DefaultToolbarAction(

View File

@ -1,9 +1,7 @@
package de.mm20.launcher2.ui.launcher.sheets
import android.content.pm.PackageManager
import android.graphics.drawable.InsetDrawable
import androidx.activity.compose.BackHandler
import androidx.appcompat.content.res.AppCompatResources
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.clickable
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.items
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.Edit
import androidx.compose.material.icons.rounded.FilterAlt
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.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MenuAnchorType
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
@ -44,7 +50,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
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.icons.CustomIconWithPreview
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.searchable.VisibilityLevel
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.OutlinedTagsInputField
@ -84,9 +92,11 @@ fun CustomizeSearchableSheet(
BottomSheetDialog(
onDismissRequest = onDismiss,
title = {
Text(stringResource(if (pickIcon) R.string.icon_picker_title else R.string.menu_customize))
},
title = if (pickIcon) {
{
Text(stringResource(R.string.icon_picker_title))
}
} else null,
dismissible = { !pickIcon },
confirmButton = if (pickIcon) {
{
@ -126,44 +136,178 @@ fun CustomizeSearchableSheet(
mutableStateOf(searchable.labelOverride ?: "")
}
OutlinedTextField(
modifier =
Modifier
modifier = Modifier
.fillMaxWidth()
.padding(top = 24.dp),
.padding(top = 24.dp, bottom = 16.dp),
value = customLabelValue,
onValueChange = {
customLabelValue = it
},
singleLine = true,
label = {
Text(stringResource(R.string.customize_item_label))
},
placeholder = {
Text(searchable.label)
},
leadingIcon = {
Icon(Icons.AutoMirrored.Rounded.Label, null)
}
)
var tags by remember { mutableStateOf(emptyList<String>()) }
var visibility by remember { mutableStateOf(VisibilityLevel.Default) }
LaunchedEffect(searchable.key) {
visibility = viewModel.getVisibility().first()
tags = viewModel.getTags().first()
}
OutlinedTagsInputField(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp),
.padding(top = 8.dp)
.fillMaxWidth(),
tags = tags, onTagsChange = { tags = it.distinct() },
placeholder = {
Text(stringResource(R.string.customize_tags_placeholder))
label = {
Text(stringResource(R.string.customize_item_tags))
},
textStyle = MaterialTheme.typography.bodyMedium,
onAutocomplete = {
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) {
onDispose {
viewModel.setCustomLabel(customLabelValue)
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.LauncherIcon
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.cancelAndJoin
import kotlinx.coroutines.delay
@ -25,6 +27,7 @@ class CustomizeSearchableSheetVM(
) : KoinComponent {
private val iconService: IconService by inject()
private val customAttributesRepository: CustomAttributesRepository by inject()
private val favoritesService: FavoritesService by inject()
val isIconPickerOpen = mutableStateOf(false)
@ -89,10 +92,18 @@ class CustomizeSearchableSheetVM(
customAttributesRepository.setTags(searchable, tags)
}
fun setVisibility(visibility: VisibilityLevel) {
favoritesService.setVisibility(searchable, visibility)
}
fun getTags(): Flow<List<String>> {
return customAttributesRepository.getTags(searchable)
}
fun getVisibility(): Flow<VisibilityLevel> {
return favoritesService.getVisibility(searchable)
}
suspend fun autocompleteTags(query: String): List<String> {
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.search.Searchable
import de.mm20.launcher2.search.data.Tag
import de.mm20.launcher2.searchable.PinnedLevel
import de.mm20.launcher2.services.favorites.FavoritesService
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
@ -60,21 +61,22 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent {
loading.value = showLoadingIndicator
manuallySorted = mutableListOf()
manuallySorted = favoritesService.getFavorites(
manuallySorted = true,
minPinnedLevel = PinnedLevel.AutomaticallySorted,
excludeTypes = listOf("tag"),
).first().toMutableList()
automaticallySorted = favoritesService.getFavorites(
automaticallySorted = true,
minPinnedLevel = PinnedLevel.AutomaticallySorted,
maxPinnedLevel = PinnedLevel.AutomaticallySorted,
excludeTypes = listOf("tag"),
).first().toMutableList()
frequentlyUsed = favoritesService.getFavorites(
frequentlyUsed = true,
minPinnedLevel = PinnedLevel.FrequentlyUsed,
maxPinnedLevel = PinnedLevel.FrequentlyUsed,
excludeTypes = listOf("tag"),
).first().toMutableList()
val pinnedTags = favoritesService.getFavorites(
includeTypes = listOf("tag"),
manuallySorted = true,
automaticallySorted = true,
minPinnedLevel = PinnedLevel.AutomaticallySorted,
).first().filterIsInstance<Tag>().toMutableList()
availableTags.value =
customAttributesRepository
@ -296,7 +298,8 @@ class EditFavoritesSheetVM : ViewModel(), KoinComponent {
favoritesService.unpinItem(item.item)
viewModelScope.launch {
frequentlyUsed = favoritesService.getFavorites(
frequentlyUsed = true,
minPinnedLevel = PinnedLevel.FrequentlyUsed,
maxPinnedLevel = PinnedLevel.FrequentlyUsed,
excludeTypes = listOf("tag"),
).first().toMutableList()
buildItemList()

View File

@ -14,6 +14,8 @@ import de.mm20.launcher2.ktx.tryStartActivity
import de.mm20.launcher2.permissions.PermissionGroup
import de.mm20.launcher2.permissions.PermissionsManager
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.widgets.CalendarWidget
import de.mm20.launcher2.widgets.CalendarWidgetConfig
@ -42,8 +44,7 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
val pinnedCalendarEvents =
favoritesService.getFavorites(
includeTypes = listOf("calendar"),
automaticallySorted = true,
manuallySorted = true,
minPinnedLevel = PinnedLevel.AutomaticallySorted,
).stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
val nextEvents = mutableStateOf<List<CalendarEvent>>(emptyList())
var availableDates = listOf(LocalDate.now())
@ -172,7 +173,7 @@ class CalendarWidgetVM : ViewModel(), KoinComponent {
).collectLatest { events ->
searchableRepository.getKeys(
includeTypes = listOf("calendar"),
hidden = true,
maxVisibility = VisibilityLevel.SearchOnly,
limit = 9999,
).collectLatest { hidden ->
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.ui.Modifier
import de.mm20.launcher2.preferences.ui.UiSettings
import de.mm20.launcher2.searchable.PinnedLevel
import de.mm20.launcher2.services.favorites.FavoritesService
import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid
import de.mm20.launcher2.widgets.CalendarWidget
@ -44,9 +45,7 @@ class FavoritesPartProvider : PartProvider, KoinComponent {
val favorites by remember(columns, excludeCalendar) {
favoritesService.getFavorites(
excludeTypes = if (excludeCalendar) listOf("calendar", "tag") else listOf("tag"),
manuallySorted = true,
automaticallySorted = true,
frequentlyUsed = true,
minPinnedLevel = PinnedLevel.FrequentlyUsed,
limit = columns
)
}.collectAsState(emptyList())

View File

@ -1,7 +1,7 @@
package de.mm20.launcher2.ui.settings.hiddenitems
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.Row
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.lazy.items
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.VisibilityOff
import androidx.compose.material3.DropdownMenu
@ -25,13 +27,15 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
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.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
@ -42,7 +46,6 @@ import de.mm20.launcher2.ui.component.preferences.SwitchPreference
fun HiddenItemsSettingsScreen() {
val viewModel: HiddenItemsSettingsScreenVM = viewModel()
val context = LocalContext.current
val density = LocalDensity.current
val apps by viewModel.allApps.collectAsState()
@ -66,63 +69,41 @@ fun HiddenItemsSettingsScreen() {
viewModel.getIcon(searchable, with(density) { 32.dp.roundToPx() })
}.collectAsState(null)
val isHidden by remember(searchable.key) {
viewModel.isHidden(searchable)
}.collectAsState(false)
val visibility by remember(searchable.key) {
viewModel.getVisibility(searchable)
}.collectAsState(null)
var showPopup by remember(searchable.key) {
mutableStateOf(false)
}
var showDropdown by remember { mutableStateOf(false) }
Box {
HiddenItem(
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
onClick = {
viewModel.setHidden(searchable, !isHidden)
},
onLongClick = {
showPopup = true
.clickable {
if (searchable is Application || searchable is CalendarEvent) {
showDropdown = true
} else {
if (visibility == null) return@clickable
viewModel.setVisibility(
searchable,
if (visibility == VisibilityLevel.Default) VisibilityLevel.Hidden else VisibilityLevel.Default
)
}
),
},
icon = icon,
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() })
}.collectAsState(null)
val isHidden by remember(searchable.key) {
viewModel.isHidden(searchable)
}.collectAsState(false)
val visibility by remember(searchable.key) {
viewModel.getVisibility(searchable)
}.collectAsState(null)
var showPopup by remember(searchable.key) {
mutableStateOf(false)
}
var showDropdown by remember { mutableStateOf(false) }
Box {
HiddenItem(
modifier = Modifier
.fillMaxWidth()
.combinedClickable(
onClick = {
viewModel.setHidden(searchable, !isHidden)
},
onLongClick = {
showPopup = true
.clickable {
if (searchable is Application || searchable is CalendarEvent) {
showDropdown = true
} else {
if (visibility == null) return@clickable
viewModel.setVisibility(
searchable,
if (visibility == VisibilityLevel.Default) VisibilityLevel.Hidden else VisibilityLevel.Default
)
}
),
},
icon = icon,
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
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,
icon: LauncherIcon?,
label: String,
isHidden: Boolean,
visibility: VisibilityLevel?,
) {
Row(
modifier = modifier
@ -219,11 +274,18 @@ fun HiddenItem(
modifier = Modifier.weight(1f, fill = true),
style = MaterialTheme.typography.titleMedium
)
Icon(
modifier = Modifier.alpha(if (isHidden) 0.3f else 1f),
imageVector = if (isHidden) Icons.Rounded.VisibilityOff else Icons.Rounded.Visibility,
tint = if (isHidden) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.primary,
contentDescription = null
)
if (visibility != null) {
Icon(
modifier = Modifier.alpha(if (visibility == VisibilityLevel.Hidden) 0.3f else 1f),
imageVector = when (visibility) {
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.search.SavableSearchable
import de.mm20.launcher2.search.Application
import de.mm20.launcher2.searchable.VisibilityLevel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
@ -20,7 +21,6 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
@ -36,20 +36,18 @@ class HiddenItemsSettingsScreenVM : ViewModel(), KoinComponent {
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
val hiddenItems: StateFlow<List<SavableSearchable>> = flow {
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)
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList())
fun isHidden(searchable: SavableSearchable): Flow<Boolean> {
return searchableRepository.isHidden(searchable)
fun getVisibility(searchable: SavableSearchable): Flow<VisibilityLevel> {
return searchableRepository.getVisibility(searchable)
}
fun setHidden(searchable: SavableSearchable, hidden: Boolean) {
if (hidden) {
searchableRepository.upsert(searchable, hidden = true, pinned = false)
} else {
searchableRepository.update(searchable, hidden = false)
}
fun setVisibility(searchable: SavableSearchable, visibilityLevel: VisibilityLevel) {
searchableRepository.upsert(searchable, visibilityLevel)
}
fun getIcon(searchable: SavableSearchable, size: Int): Flow<LauncherIcon?> {

View File

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

View File

@ -688,7 +688,7 @@
<string name="create_app_shortcut">Create shortcut</string>
<string name="frequently_used_show_in_favorites">Show in favorites</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_search_search_actions_summary">Configure quick actions and search shortcuts</string>
<string name="search_action_call">Call</string>
@ -942,4 +942,11 @@
<string name="poi_category_courthouse">Courthouse</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="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>

View File

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

View File

@ -11,7 +11,7 @@ data class SavedSearchableEntity(
@ColumnInfo(name = "searchable") val serializedSearchable: String,
@ColumnInfo(defaultValue = "0") val launchCount: 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
)

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

View File

@ -11,7 +11,7 @@ data class SavedSearchable(
val searchable: SavableSearchable?,
var launchCount: Int,
var pinPosition: Int,
var hidden: Boolean,
var visibility: VisibilityLevel,
var weight: Double
) {
fun toDatabaseEntity(): SavedSearchableEntity? {
@ -21,7 +21,7 @@ data class SavedSearchable(
key = key,
type = searchable.domain,
serializedSearchable = data,
hidden = hidden,
visibility = visibility.value,
pinPosition = pinPosition,
launchCount = launchCount,
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
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.HourglassBottom
import androidx.compose.material.icons.rounded.VisibilityOff
import de.mm20.launcher2.badges.Badge
import de.mm20.launcher2.badges.BadgeIcon
import de.mm20.launcher2.badges.R
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.Searchable
import de.mm20.launcher2.searchable.SavableSearchableRepository
import de.mm20.launcher2.searchable.VisibilityLevel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -28,7 +27,7 @@ class HiddenItemBadgeProvider(
private val scope = CoroutineScope(Job() + Dispatchers.Default)
private val hiddenItemKeys = searchableRepository.getKeys(
hidden = true,
maxVisibility = VisibilityLevel.Hidden,
limit = 9999,
).shareIn(scope, SharingStarted.WhileSubscribed(), 1)

View File

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

View File

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