Add filter bar customizations

This commit is contained in:
MM20
2024-05-01 16:21:39 +02:00
parent 5d824108c7
commit 94893a2c43
12 changed files with 330 additions and 175 deletions
@@ -301,7 +301,7 @@ fun Modifier.dragAndDrop(
dragHorizontal: Boolean = true,
hapticFeedback: HapticFeedback
) =
this then pointerInput(null) {
this then Modifier.pointerInput(null) {
val scope = CoroutineScope(coroutineContext)
detectDragGesturesAfterLongPress(
onDragStart = { offset ->
@@ -96,6 +96,7 @@ class SearchVM : ViewModel(), KoinComponent {
private val defaultFilters = searchFilterSettings.defaultFilter.stateIn(viewModelScope, SharingStarted.Eagerly, SearchFilters())
val filters = mutableStateOf(defaultFilters.value)
val filterBar = searchFilterSettings.filterBar
val filterBarItems = searchFilterSettings.filterBarItems
val separateWorkProfile = searchUiSettings.separateWorkProfile
@@ -31,14 +31,21 @@ import androidx.compose.material3.VerticalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.preferences.KeyboardFilterBarItem
import de.mm20.launcher2.search.SearchFilters
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.icons.Wikipedia
@Composable
fun KeyboardFilterBar(filters: SearchFilters, onFiltersChange: (SearchFilters) -> Unit) {
fun KeyboardFilterBar(
filters: SearchFilters,
onFiltersChange: (SearchFilters) -> Unit,
items: List<KeyboardFilterBarItem>,
) {
val context = LocalContext.current
val allCategoriesEnabled = filters.allCategoriesEnabled
Column(
modifier = Modifier
@@ -54,178 +61,32 @@ fun KeyboardFilterBar(filters: SearchFilters, onFiltersChange: (SearchFilters) -
.padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
FilterChip(
selected = filters.allowNetwork,
onClick = {
onFiltersChange(filters.copy(allowNetwork = !filters.allowNetwork))
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Language,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
for (i in items.indices) {
val item = items[i]
val prevItem = items.getOrNull(i - 1)
if (prevItem != null && prevItem.isCategory != item.isCategory) {
VerticalDivider(
modifier = Modifier
.height(36.dp)
.padding(end = 8.dp)
)
},
label = { Text(stringResource(R.string.search_filter_online)) }
)
VerticalDivider(
modifier = Modifier
.height(36.dp)
.padding(horizontal = 8.dp)
)
FilterChip(
modifier = Modifier.padding(end = 8.dp),
selected = filters.apps && !allCategoriesEnabled,
onClick = {
onFiltersChange(filters.toggleApps())
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Apps,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = { Text(stringResource(R.string.search_filter_apps)) }
)
FilterChip(
modifier = Modifier.padding(end = 8.dp),
selected = filters.files && !allCategoriesEnabled,
onClick = {
onFiltersChange(filters.toggleFiles())
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Description,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = { Text(stringResource(R.string.preference_search_files)) }
)
FilterChip(
modifier = Modifier.padding(end = 8.dp),
selected = filters.contacts && !allCategoriesEnabled,
onClick = {
onFiltersChange(filters.toggleContacts())
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Person,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = { Text(stringResource(R.string.preference_search_contacts)) }
)
FilterChip(
modifier = Modifier.padding(end = 8.dp),
selected = filters.events && !allCategoriesEnabled,
onClick = {
onFiltersChange(filters.toggleEvents())
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Today,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = { Text(stringResource(R.string.preference_search_calendar)) }
)
FilterChip(
modifier = Modifier.padding(end = 8.dp),
selected = filters.shortcuts && !allCategoriesEnabled,
onClick = {
onFiltersChange(filters.toggleShortcuts())
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.AppShortcut,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = { Text(stringResource(R.string.preference_search_appshortcuts)) }
)
FilterChip(
modifier = Modifier.padding(end = 8.dp),
selected = filters.articles && !allCategoriesEnabled,
onClick = {
onFiltersChange(filters.toggleArticles())
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Wikipedia,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = { Text(stringResource(R.string.preference_search_wikipedia)) }
)
FilterChip(
modifier = Modifier.padding(end = 8.dp),
selected = filters.websites && !allCategoriesEnabled,
onClick = {
onFiltersChange(filters.toggleWebsites())
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Public,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = { Text(stringResource(R.string.preference_search_websites)) }
)
FilterChip(
modifier = Modifier.padding(end = 8.dp),
selected = filters.places && !allCategoriesEnabled,
onClick = {
onFiltersChange(filters.togglePlaces())
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Place,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = { Text(stringResource(R.string.preference_search_locations)) }
)
FilterChip(
selected = filters.tools && !allCategoriesEnabled,
onClick = {
onFiltersChange(filters.toggleTools())
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.Handyman,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = { Text(stringResource(R.string.search_filter_tools)) }
)
VerticalDivider(
modifier = Modifier
.height(36.dp)
.padding(horizontal = 8.dp)
)
FilterChip(
selected = filters.hiddenItems,
onClick = {
onFiltersChange(filters.copy(hiddenItems = !filters.hiddenItems))
},
leadingIcon = {
Icon(
imageVector = Icons.Rounded.VisibilityOff,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = { Text(stringResource(R.string.preference_hidden_items)) }
)
}
FilterChip(
modifier = Modifier.padding(end = if (i == items.lastIndex) 0.dp else 8.dp),
selected = filters.isSelected(item),
onClick = {
onFiltersChange(filters.toggle(item))
},
leadingIcon = {
Icon(
imageVector = item.icon,
contentDescription = null,
modifier = Modifier.size(FilterChipDefaults.IconSize)
)
},
label = { Text(item.getLabel(context)) }
)
}
}
HorizontalDivider()
}
@@ -0,0 +1,88 @@
package de.mm20.launcher2.ui.launcher.search.filters
import android.content.Context
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.AppShortcut
import androidx.compose.material.icons.rounded.Apps
import androidx.compose.material.icons.rounded.Description
import androidx.compose.material.icons.rounded.Handyman
import androidx.compose.material.icons.rounded.Language
import androidx.compose.material.icons.rounded.Person
import androidx.compose.material.icons.rounded.Place
import androidx.compose.material.icons.rounded.Public
import androidx.compose.material.icons.rounded.Today
import androidx.compose.material.icons.rounded.VisibilityOff
import de.mm20.launcher2.preferences.KeyboardFilterBarItem
import de.mm20.launcher2.search.SearchFilters
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.icons.Wikipedia
val KeyboardFilterBarItem.icon
get() = when (this) {
KeyboardFilterBarItem.Apps -> Icons.Rounded.Apps
KeyboardFilterBarItem.Events -> Icons.Rounded.Today
KeyboardFilterBarItem.Contacts -> Icons.Rounded.Person
KeyboardFilterBarItem.Places -> Icons.Rounded.Place
KeyboardFilterBarItem.Files -> Icons.Rounded.Description
KeyboardFilterBarItem.Tools -> Icons.Rounded.Handyman
KeyboardFilterBarItem.Articles -> Icons.Rounded.Wikipedia
KeyboardFilterBarItem.Websites -> Icons.Rounded.Public
KeyboardFilterBarItem.Shortcuts -> Icons.Rounded.AppShortcut
KeyboardFilterBarItem.HiddenResults -> Icons.Rounded.VisibilityOff
KeyboardFilterBarItem.OnlineResults -> Icons.Rounded.Language
}
fun KeyboardFilterBarItem.getLabel(context: Context): String {
return when (this) {
KeyboardFilterBarItem.Apps -> context.getString(R.string.search_filter_apps)
KeyboardFilterBarItem.Events -> context.getString(R.string.preference_search_calendar)
KeyboardFilterBarItem.Contacts -> context.getString(R.string.preference_search_contacts)
KeyboardFilterBarItem.Places -> context.getString(R.string.preference_search_locations)
KeyboardFilterBarItem.Files -> context.getString(R.string.preference_search_files)
KeyboardFilterBarItem.Tools -> context.getString(R.string.search_filter_tools)
KeyboardFilterBarItem.Articles -> context.getString(R.string.preference_search_wikipedia)
KeyboardFilterBarItem.Websites -> context.getString(R.string.preference_search_websites)
KeyboardFilterBarItem.Shortcuts -> context.getString(R.string.preference_search_appshortcuts)
KeyboardFilterBarItem.HiddenResults -> context.getString(R.string.preference_hidden_items)
KeyboardFilterBarItem.OnlineResults -> context.getString(R.string.search_filter_online)
}
}
val KeyboardFilterBarItem.isCategory
get() = when (this) {
KeyboardFilterBarItem.OnlineResults, KeyboardFilterBarItem.HiddenResults -> false
else -> true
}
fun SearchFilters.isSelected(item: KeyboardFilterBarItem): Boolean {
if (item.isCategory && allCategoriesEnabled) return false
return when (item) {
KeyboardFilterBarItem.Apps -> apps
KeyboardFilterBarItem.Events -> events
KeyboardFilterBarItem.Contacts -> contacts
KeyboardFilterBarItem.Places -> places
KeyboardFilterBarItem.Files -> files
KeyboardFilterBarItem.Tools -> tools
KeyboardFilterBarItem.Articles -> articles
KeyboardFilterBarItem.Websites -> websites
KeyboardFilterBarItem.Shortcuts -> shortcuts
KeyboardFilterBarItem.HiddenResults -> hiddenItems
KeyboardFilterBarItem.OnlineResults -> allowNetwork
}
}
fun SearchFilters.toggle(item: KeyboardFilterBarItem): SearchFilters {
return when (item) {
KeyboardFilterBarItem.Apps -> return toggleApps()
KeyboardFilterBarItem.Events -> return toggleEvents()
KeyboardFilterBarItem.Contacts -> return toggleContacts()
KeyboardFilterBarItem.Places -> return togglePlaces()
KeyboardFilterBarItem.Files -> return toggleFiles()
KeyboardFilterBarItem.Tools -> return toggleTools()
KeyboardFilterBarItem.Articles -> return toggleArticles()
KeyboardFilterBarItem.Websites -> return toggleWebsites()
KeyboardFilterBarItem.Shortcuts -> return toggleShortcuts()
KeyboardFilterBarItem.HiddenResults -> return copy(hiddenItems = !hiddenItems)
KeyboardFilterBarItem.OnlineResults -> return copy(allowNetwork = !allowNetwork)
}
}
@@ -137,11 +137,13 @@ fun LauncherSearchBar(
exit = fadeOut(),
modifier = Modifier.align(Alignment.BottomCenter)
) {
val items by searchVM.filterBarItems.collectAsState(emptyList())
KeyboardFilterBar(
filters = searchVM.filters.value,
onFiltersChange = {
searchVM.setFilters(it)
}
},
items = items
)
}
}
@@ -42,6 +42,7 @@ import de.mm20.launcher2.ui.settings.debug.DebugSettingsScreen
import de.mm20.launcher2.ui.settings.easteregg.EasterEggSettingsScreen
import de.mm20.launcher2.ui.settings.favorites.FavoritesSettingsScreen
import de.mm20.launcher2.ui.settings.filesearch.FileSearchSettingsScreen
import de.mm20.launcher2.ui.settings.filterbar.FilterBarSettingsScreen
import de.mm20.launcher2.ui.settings.gestures.GestureSettingsScreen
import de.mm20.launcher2.ui.settings.hiddenitems.HiddenItemsSettingsScreen
import de.mm20.launcher2.ui.settings.homescreen.HomescreenSettingsScreen
@@ -170,6 +171,9 @@ class SettingsActivity : BaseActivity() {
composable("settings/search/tags") {
TagsSettingsScreen()
}
composable("settings/search/filterbar") {
FilterBarSettingsScreen()
}
composable(ROUTE_WEATHER_INTEGRATION) {
WeatherIntegrationSettingsScreen()
}
@@ -0,0 +1,139 @@
package de.mm20.launcher2.ui.settings.filterbar
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.lifecycle.viewmodel.compose.viewModel
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import de.mm20.launcher2.preferences.KeyboardFilterBarItem
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.dragndrop.DraggableItem
import de.mm20.launcher2.ui.component.dragndrop.LazyDragAndDropColumn
import de.mm20.launcher2.ui.component.dragndrop.rememberLazyDragAndDropListState
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import de.mm20.launcher2.ui.launcher.search.filters.getLabel
import de.mm20.launcher2.ui.launcher.search.filters.icon
import de.mm20.launcher2.ui.launcher.search.filters.isCategory
import de.mm20.launcher2.ui.locals.LocalNavController
@Composable
fun FilterBarSettingsScreen() {
val viewModel: FilterBarSettingsScreenVM = viewModel()
val navController = LocalNavController.current
val systemUiController = rememberSystemUiController()
systemUiController.setStatusBarColor(MaterialTheme.colorScheme.surface)
systemUiController.setNavigationBarColor(Color.Black)
val context = LocalContext.current
val activity = LocalContext.current as? AppCompatActivity
val listState = rememberLazyDragAndDropListState(
onDragStart = {
it.key is KeyboardFilterBarItem
},
onItemMove = { from, to ->
val item = (from.key as? KeyboardFilterBarItem) ?: return@rememberLazyDragAndDropListState
val toItem = (to.key as? KeyboardFilterBarItem) ?: return@rememberLazyDragAndDropListState
viewModel.moveItem(item, toItem)
}
)
val enabledItems by viewModel.filterBarItems.collectAsState()
val disabledItems by remember {
derivedStateOf {
KeyboardFilterBarItem.entries.filter { enabledItems?.contains(it) == false }
}
}
Scaffold(
topBar = {
CenterAlignedTopAppBar(
title = {
Text(
stringResource(id = R.string.preference_customize_filter_bar),
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(horizontal = 16.dp),
maxLines = 1
)
},
navigationIcon = {
IconButton(onClick = {
if (navController?.navigateUp() != true) {
activity?.onBackPressed()
}
}) {
Icon(imageVector = Icons.Rounded.ArrowBack, contentDescription = "Back")
}
},
)
}) {
if (enabledItems == null) {
return@Scaffold
}
LazyDragAndDropColumn(
state = listState,
bidirectionalDrag = false,
contentPadding = it,
modifier = Modifier
.fillMaxSize()
) {
for (i in 0 until KeyboardFilterBarItem.entries.size) {
val item = enabledItems!!.getOrNull(i) ?: disabledItems[i - enabledItems!!.size]
val prevItem = enabledItems!!.getOrNull(i - 1) ?: disabledItems.getOrNull(i - enabledItems!!.size - 1)
if (prevItem != null && prevItem.isCategory != item.isCategory) {
item(key = "divider-$i") {
HorizontalDivider()
}
}
item(key = item) {
DraggableItem(state = listState, key = item) {
val elevation by animateDpAsState(if (it) 4.dp else 0.dp)
Surface(
shadowElevation = elevation,
tonalElevation = elevation,
modifier = Modifier.zIndex(if (it) 1f else 0f)
) {
SwitchPreference(
title = item.getLabel(context),
icon = item.icon,
value = enabledItems!!.contains(item), onValueChanged = {
if (it) {
viewModel.addAction(item)
} else {
viewModel.removeAction(item)
}
}
)
}
}
}
}
}
}
}
@@ -0,0 +1,41 @@
package de.mm20.launcher2.ui.settings.filterbar
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.preferences.KeyboardFilterBarItem
import de.mm20.launcher2.preferences.search.SearchFilterSettings
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.stateIn
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class FilterBarSettingsScreenVM(
) : ViewModel(), KoinComponent {
private val searchFilterSettings: SearchFilterSettings by inject()
val filterBarItems = searchFilterSettings.filterBarItems
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), null)
fun moveItem(item: KeyboardFilterBarItem, toItem: KeyboardFilterBarItem) {
val items = filterBarItems.value?.toMutableList() ?: return
val fromIndex = items.indexOf(item)
val toIndex = items.indexOf(toItem)
if (fromIndex > items.lastIndex) return
if (toIndex > items.lastIndex) return
if (fromIndex != -1) items.removeAt(fromIndex)
if (toIndex != -1) items.add(toIndex, item)
searchFilterSettings.setFilterBarItems(items)
}
fun addAction(item: KeyboardFilterBarItem) {
val items = filterBarItems.value?.toMutableList() ?: return
items.add(item)
searchFilterSettings.setFilterBarItems(items)
}
fun removeAction(item: KeyboardFilterBarItem) {
val items = filterBarItems.value?.toMutableList() ?: return
items.remove(item)
searchFilterSettings.setFilterBarItems(items)
}
}
@@ -290,6 +290,15 @@ fun SearchSettingsScreen() {
viewModel.setFilterBar(it)
}
)
AnimatedVisibility(filterBar == true) {
Preference(
title = stringResource(R.string.preference_customize_filter_bar),
summary = stringResource(R.string.preference_customize_filter_bar_summary),
onClick = {
navController?.navigate("settings/search/filterbar")
}
)
}
}
}
if (hasWorkProfile) {