Make search action order customizable

This commit is contained in:
MM20
2022-11-05 17:59:24 +01:00
parent 960c3b70f5
commit 3463b3b800
29 changed files with 526 additions and 253 deletions
@@ -0,0 +1,54 @@
package de.mm20.launcher2.ui.component
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Alarm
import androidx.compose.material.icons.rounded.Call
import androidx.compose.material.icons.rounded.Email
import androidx.compose.material.icons.rounded.Event
import androidx.compose.material.icons.rounded.Language
import androidx.compose.material.icons.rounded.Person
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material.icons.rounded.Sms
import androidx.compose.material.icons.rounded.Timer
import androidx.compose.material.icons.rounded.Translate
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
@Composable
fun SearchActionIcon(
icon: SearchActionIcon,
color: Int,
customIcon: String? = null
) {
val tint = when(color) {
0 -> MaterialTheme.colorScheme.primary
1 -> Color.Unspecified
else -> Color(color)
}
if (icon != SearchActionIcon.Custom) {
Icon(
imageVector = getSearchActionIconVector(icon),
contentDescription = null,
tint = tint,
)
}
}
fun getSearchActionIconVector(icon: SearchActionIcon): ImageVector {
return when (icon) {
SearchActionIcon.Phone -> Icons.Rounded.Call
SearchActionIcon.Website -> Icons.Rounded.Language
SearchActionIcon.Alarm -> Icons.Rounded.Alarm
SearchActionIcon.Timer -> Icons.Rounded.Timer
SearchActionIcon.Contact -> Icons.Rounded.Person
SearchActionIcon.Email -> Icons.Rounded.Email
SearchActionIcon.Message -> Icons.Rounded.Sms
SearchActionIcon.Calendar -> Icons.Rounded.Event
SearchActionIcon.Translate -> Icons.Rounded.Translate
else -> Icons.Rounded.Search
}
}
@@ -88,7 +88,7 @@ data class LazyDragAndDropListState(
fun startDrag(offset: Offset): Boolean {
val draggedItem = listState.layoutInfo.visibleItemsInfo.find {
Rect(
it.offset.toOffset(),
(it.offset + listState.layoutInfo.beforeContentPadding).toOffset(),
it.size.toSize()
).contains(offset)
} ?: return false
@@ -240,13 +240,16 @@ fun LazyDragAndDropRow(
verticalAlignment: Alignment.Vertical = Alignment.Top,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true,
bidirectionalDrag: Boolean = true,
content: LazyListScope.() -> Unit
) {
LazyRow(
modifier = modifier.dragAndDrop(
state,
LocalLayoutDirection.current == LayoutDirection.Rtl,
LocalHapticFeedback.current
isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl,
hapticFeedback = LocalHapticFeedback.current,
dragVertical = bidirectionalDrag,
dragHorizontal = true,
),
state = state.listState,
contentPadding = contentPadding,
@@ -269,13 +272,16 @@ fun LazyDragAndDropColumn(
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true,
bidirectionalDrag: Boolean = true,
content: LazyListScope.() -> Unit
) {
LazyColumn(
modifier = modifier.dragAndDrop(
state,
LocalLayoutDirection.current == LayoutDirection.Rtl,
LocalHapticFeedback.current
isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl,
hapticFeedback = LocalHapticFeedback.current,
dragVertical = true,
dragHorizontal = bidirectionalDrag,
),
state = state.listState,
contentPadding = contentPadding,
@@ -291,6 +297,8 @@ fun LazyDragAndDropColumn(
fun Modifier.dragAndDrop(
state: LazyDragAndDropListState,
isRtl: Boolean,
dragVertical: Boolean = true,
dragHorizontal: Boolean = true,
hapticFeedback: HapticFeedback
) =
this then pointerInput(null) {
@@ -302,9 +310,18 @@ fun Modifier.dragAndDrop(
}
},
onDrag = { _, dragAmount ->
scope.launch { state.drag(dragAmount.let {
if (isRtl) it.copy(x = -it.x) else it
}) }
scope.launch {
state.drag(
when {
!dragVertical && !dragHorizontal -> Offset.Zero
dragVertical && !dragHorizontal -> Offset(0f, dragAmount.y)
!dragVertical && dragHorizontal && isRtl -> Offset(-dragAmount.x, 0f)
!dragVertical && dragHorizontal && !isRtl -> Offset(dragAmount.x, 0f)
dragVertical && dragHorizontal && isRtl -> Offset(-dragAmount.x, dragAmount.y)
else -> dragAmount
}
)
}
},
onDragCancel = {
scope.launch { state.cancelDrag() }
@@ -95,7 +95,6 @@ class SearchVM : ViewModel(), KoinComponent {
shortcuts = it.appShortcutSearch,
websites = it.websiteSearch,
wikipedia = it.wikipediaSearch,
searchActions = it.searchActions,
).collectLatest { results ->
hiddenItemKeys.collectLatest { hiddenKeys ->
val hidden = mutableListOf<SavableSearchable>()
@@ -6,17 +6,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Alarm
import androidx.compose.material.icons.rounded.Call
import androidx.compose.material.icons.rounded.Email
import androidx.compose.material.icons.rounded.Event
import androidx.compose.material.icons.rounded.Language
import androidx.compose.material.icons.rounded.Person
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material.icons.rounded.Sms
import androidx.compose.material.icons.rounded.Timer
import androidx.compose.material.icons.rounded.Translate
import androidx.compose.material3.AssistChip
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -28,7 +17,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
import de.mm20.launcher2.ui.component.SearchActionIcon
@Composable
fun SearchBarActions(
@@ -53,27 +42,7 @@ fun SearchBarActions(
},
label = { Text(it.label) },
leadingIcon = {
val icon = it.icon
if (it.icon != SearchActionIcon.Custom) {
Icon(
imageVector = when (it.icon) {
SearchActionIcon.Phone -> Icons.Rounded.Call
SearchActionIcon.Website -> Icons.Rounded.Language
SearchActionIcon.Alarm -> Icons.Rounded.Alarm
SearchActionIcon.Timer -> Icons.Rounded.Timer
SearchActionIcon.Contact -> Icons.Rounded.Person
SearchActionIcon.Email -> Icons.Rounded.Email
SearchActionIcon.Message -> Icons.Rounded.Sms
SearchActionIcon.Calendar -> Icons.Rounded.Event
SearchActionIcon.Translate -> Icons.Rounded.Translate
else -> Icons.Rounded.Search
},
contentDescription = null,
tint = if (it.iconColor == 0) MaterialTheme.colorScheme.primary else Color(
it.iconColor
)
)
}
SearchActionIcon(icon = it.icon, color = it.iconColor, customIcon = it.customIcon)
}
/*leadingIcon = {
val icon = it.icon
@@ -1,115 +1,166 @@
package de.mm20.launcher2.ui.settings.searchactions
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
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.rounded.Alarm
import androidx.compose.material.icons.rounded.CalendarToday
import androidx.compose.material.icons.rounded.Call
import androidx.compose.material.icons.rounded.Email
import androidx.compose.material.icons.rounded.Event
import androidx.compose.material.icons.rounded.Language
import androidx.compose.material.icons.rounded.Person
import androidx.compose.material.icons.rounded.Sms
import androidx.compose.material.icons.rounded.Timer
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.FloatingActionButton
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.getValue
import androidx.compose.runtime.livedata.observeAsState
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 de.mm20.launcher2.preferences.Settings
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import de.mm20.launcher2.searchactions.builders.CustomizableSearchActionBuilder
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
import de.mm20.launcher2.ui.component.SearchActionIcon
import de.mm20.launcher2.ui.component.getSearchActionIconVector
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import de.mm20.launcher2.ui.launcher.helper.DraggableItem
import de.mm20.launcher2.ui.launcher.helper.LazyDragAndDropColumn
import de.mm20.launcher2.ui.launcher.helper.rememberLazyDragAndDropListState
import de.mm20.launcher2.ui.locals.LocalNavController
@Composable
fun SearchActionsSettingsScreen() {
val viewModel: SearchActionsSettingsScreenVM = viewModel()
val settings by viewModel.searchActionSettings.observeAsState(
Settings.SearchActionSettings.getDefaultInstance()
val navController = LocalNavController.current
val systemUiController = rememberSystemUiController()
systemUiController.setStatusBarColor(MaterialTheme.colorScheme.surface)
systemUiController.setNavigationBarColor(Color.Black)
val context = LocalContext.current
val colorScheme = MaterialTheme.colorScheme
val activity = LocalContext.current as? AppCompatActivity
val listState = rememberLazyDragAndDropListState(
onDragStart = {
it.key != "divider" && !(it.key as String).startsWith("disabled-")
},
onItemMove = { from, to -> viewModel.moveItem(from.index, to.index) }
)
PreferenceScreen(stringResource(id = R.string.preference_screen_search_actions)) {
item {
PreferenceCategory {
SwitchPreference(
icon = Icons.Rounded.Call,
title = stringResource(R.string.search_action_call),
value = settings.call,
onValueChanged = {
viewModel.updateSettings {
setCall(it)
val searchActions by viewModel.searchActions.observeAsState(emptyList())
val disabledActions by viewModel.disabledActions.observeAsState(emptyList())
Scaffold(
floatingActionButton = {
FloatingActionButton(onClick = { /*TODO*/ }) {
Icon(imageVector = Icons.Rounded.Add, contentDescription = null)
}
},
topBar = {
CenterAlignedTopAppBar(
title = {
Text(
stringResource(id = R.string.preference_screen_search_actions),
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")
}
},
)
}) {
LazyDragAndDropColumn(
state = listState,
bidirectionalDrag = false,
contentPadding = it,
modifier = Modifier
.fillMaxSize()
) {
items(
items = searchActions,
key = { it.key }
) { item ->
DraggableItem(
state = listState,
key = item.key
) {
val elevation by animateDpAsState(if (it) 4.dp else 0.dp)
Surface(
shadowElevation = elevation,
tonalElevation = elevation,
modifier = Modifier.zIndex(if (it) 1f else 0f)
) {
if (item is CustomizableSearchActionBuilder) {
Preference(
icon = {
SearchActionIcon(
icon = item.icon,
color = item.iconColor,
customIcon = item.customIcon
)
},
title = item.label
)
} else {
SwitchPreference(
icon = getSearchActionIconVector(item.icon),
title = item.label,
value = true,
onValueChanged = {
viewModel.removeAction(item)
}
)
}
}
}
}
item(key = "divider") {
Box(
modifier = Modifier
.fillMaxWidth()
.height(0.5.dp)
.background(
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
)
)
}
items(
items = disabledActions,
key = { "disabled-${it.key}" }
) { item ->
SwitchPreference(
icon = Icons.Rounded.Sms,
title = stringResource(R.string.search_action_message),
value = settings.message,
icon = getSearchActionIconVector(item.icon),
title = item.label,
value = false,
onValueChanged = {
viewModel.updateSettings {
setMessage(it)
}
},
)
SwitchPreference(
icon = Icons.Rounded.Email,
title = stringResource(R.string.search_action_email),
value = settings.email,
onValueChanged = {
viewModel.updateSettings {
setEmail(it)
}
},
)
SwitchPreference(
icon = Icons.Rounded.Person,
title = stringResource(R.string.search_action_contact),
value = settings.contact,
onValueChanged = {
viewModel.updateSettings {
setContact(it)
}
},
)
SwitchPreference(
icon = Icons.Rounded.Alarm,
title = stringResource(R.string.search_action_alarm),
value = settings.setAlarm,
onValueChanged = {
viewModel.updateSettings {
setSetAlarm(it)
}
},
)
SwitchPreference(
icon = Icons.Rounded.Timer,
title = stringResource(R.string.search_action_timer),
value = settings.startTimer,
onValueChanged = {
viewModel.updateSettings {
setStartTimer(it)
}
},
)
SwitchPreference(
icon = Icons.Rounded.Event,
title = stringResource(R.string.search_action_event),
value = settings.scheduleEvent,
onValueChanged = {
viewModel.updateSettings {
setScheduleEvent(it)
}
},
)
SwitchPreference(
icon = Icons.Rounded.Language,
title = stringResource(R.string.search_action_open_url),
value = settings.openUrl,
onValueChanged = {
viewModel.updateSettings {
setOpenUrl(it)
}
},
viewModel.addAction(item)
}
)
}
}
@@ -2,27 +2,39 @@ package de.mm20.launcher2.ui.settings.searchactions
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.preferences.LauncherDataStore
import de.mm20.launcher2.preferences.Settings.SearchActionSettings
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import de.mm20.launcher2.searchactions.SearchActionService
import de.mm20.launcher2.searchactions.builders.SearchActionBuilder
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
class SearchActionsSettingsScreenVM : ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject()
private val searchActionService: SearchActionService by inject()
val searchActionSettings = dataStore.data.map { it.searchActions }.asLiveData()
val searchActions = searchActionService
.getSearchActionBuilders()
.asLiveData()
fun updateSettings(block: SearchActionSettings.Builder.() -> SearchActionSettings.Builder) {
viewModelScope.launch {
dataStore.updateData {
it.toBuilder()
.setSearchActions(
it.searchActions.toBuilder().block()
).build()
}
}
val disabledActions = searchActionService
.getDisabledActionBuilders()
.asLiveData()
fun addAction(searchAction: SearchActionBuilder) {
val actions =
searchActions.value?.filter { it.key != searchAction.key }?.plus(searchAction) ?: return
searchActionService.saveSearchActionBuilders(actions)
}
fun removeAction(searchAction: SearchActionBuilder) {
val actions = searchActions.value?.filter { it.key != searchAction.key } ?: return
searchActionService.saveSearchActionBuilders(actions)
}
fun moveItem(fromIndex: Int, toIndex: Int) {
val actions = searchActions.value?.toMutableList() ?: return
if (fromIndex > actions.lastIndex) return
if (toIndex > actions.lastIndex) return
val item = actions.removeAt(fromIndex)
actions.add(toIndex, item)
searchActionService.saveSearchActionBuilders(actions)
}
}