Make search action order customizable
This commit is contained in:
@@ -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
|
||||
|
||||
+143
-92
@@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+28
-16
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user