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
28 changed files with 571 additions and 485 deletions
@@ -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
@@ -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()) {
@@ -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>()
@@ -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(
@@ -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(
@@ -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)
@@ -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(
@@ -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(
@@ -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(
@@ -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(
@@ -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)
}
}
}
@@ -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()
}
@@ -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()
@@ -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) }
@@ -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())
@@ -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
)
}
}
}
@@ -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?> {
@@ -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)