diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/ListPreference.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/ListPreference.kt index ad0047ab7..5dd9b52e3 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/ListPreference.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/preferences/ListPreference.kt @@ -9,6 +9,7 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @@ -21,6 +22,7 @@ fun ListPreference( items: List>, value: T, summary: String? = items.firstOrNull { value == it.value }?.label, + containerColor: Color = MaterialTheme.colorScheme.surface, onValueChanged: (T) -> Unit, enabled: Boolean = true, itemLabel: @Composable RowScope.(item: ListPreferenceItem) -> Unit = { @@ -36,6 +38,7 @@ fun ListPreference( icon = icon, iconPadding = iconPadding, enabled = enabled, + containerColor = containerColor, onClick = { showDialog = true } diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/EditSearchActionSheet.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/EditSearchActionSheet.kt index fbd3d8cfb..65def7c89 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/EditSearchActionSheet.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/EditSearchActionSheet.kt @@ -51,6 +51,9 @@ import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Surface import androidx.compose.material3.Switch import androidx.compose.material3.Text @@ -65,6 +68,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity @@ -114,7 +118,9 @@ fun EditSearchActionSheet( Column( modifier = when (page) { EditSearchActionPage.InitAppSearch, EditSearchActionPage.PickIcon -> Modifier - else -> Modifier.verticalScroll(rememberScrollState()).padding(it) + else -> Modifier + .verticalScroll(rememberScrollState()) + .padding(it) } ) { when (page) { @@ -184,7 +190,9 @@ fun EditSearchActionSheet( if (button != null) { Row( - modifier = Modifier.fillMaxWidth().padding(vertical = 16.dp), + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), horizontalArrangement = Arrangement.End, ) { button() @@ -400,6 +408,7 @@ fun CustomizeWebSearch(viewModel: EditSearchActionSheetVM) { value = searchAction!!.label, onValueChange = { viewModel.setLabel(it) }, label = { Text(stringResource(R.string.search_action_label)) }, + singleLine = true, ) } @@ -490,7 +499,8 @@ fun CustomizeWebSearch(viewModel: EditSearchActionSheetVM) { onValueChanged = { viewModel.setQueryEncoding(it) }, - iconPadding = false + iconPadding = false, + containerColor = Color.Transparent ) } } @@ -538,13 +548,15 @@ fun CustomizeAppSearch(viewModel: EditSearchActionSheetVM) { value = searchAction!!.label, onValueChange = { viewModel.setLabel(it) }, label = { Text(stringResource(R.string.search_action_label)) }, + singleLine = true, ) } var showAppDropdown by remember { mutableStateOf(false) } - Box(modifier = Modifier - .fillMaxWidth() - .clickable { showAppDropdown = !showAppDropdown }) { + Box( + modifier = Modifier + .fillMaxWidth() + .clickable { showAppDropdown = !showAppDropdown }) { OutlinedTextFieldDefaults.DecorationBox( value = selectedApp?.label ?: "", enabled = true, @@ -563,7 +575,7 @@ fun CustomizeAppSearch(viewModel: EditSearchActionSheetVM) { SearchActionIcon( size = 24.dp, componentName = selectedApp.componentName, - icon = de.mm20.launcher2.searchactions.actions.SearchActionIcon.Custom, + icon = SearchActionIcon.Custom, color = 1, ) } @@ -586,7 +598,7 @@ fun CustomizeAppSearch(viewModel: EditSearchActionSheetVM) { SearchActionIcon( size = 24.dp, componentName = app.componentName, - icon = de.mm20.launcher2.searchactions.actions.SearchActionIcon.Custom, + icon = SearchActionIcon.Custom, color = 1, ) } @@ -619,81 +631,180 @@ fun CustomizeAppSearch(viewModel: EditSearchActionSheetVM) { fun CustomizeCustomIntent(viewModel: EditSearchActionSheetVM) { val searchAction by viewModel.searchAction - val action = searchAction + val action = (searchAction as? CustomIntentActionBuilder) ?: return - if (action is CustomIntentActionBuilder) { - Column( - modifier = Modifier - .fillMaxWidth() + Column( + modifier = Modifier + .fillMaxWidth() + ) { + Row( + verticalAlignment = Alignment.Bottom ) { - Row( - verticalAlignment = Alignment.Bottom - ) { - SearchActionIconTile(onClick = { - viewModel.openIconPicker() - }) { - SearchActionIcon( - builder = searchAction!!, size = 24.dp - ) - } - OutlinedTextField( - modifier = Modifier - .weight(1f) - .padding(start = 16.dp), - value = action.label, - onValueChange = { viewModel.setLabel(it) }, - label = { Text(stringResource(R.string.search_action_label)) }, + SearchActionIconTile(onClick = { + viewModel.openIconPicker() + }) { + SearchActionIcon( + builder = searchAction!!, size = 24.dp ) } - - OutlinedTextField( modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp), - value = action.baseIntent.action ?: "", - onValueChange = { viewModel.setIntentAction(it) }, - label = { Text("Action") }, + .weight(1f) + .padding(start = 16.dp), + value = action.label, + onValueChange = { viewModel.setLabel(it) }, + label = { Text(stringResource(R.string.search_action_label)) }, + singleLine = true, ) + } - OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - value = action.baseIntent.categories?.firstOrNull() ?: "", - onValueChange = { viewModel.setIntentCategory(it) }, - label = { Text("Category") }, - ) + Text( + text = "Query", + modifier = Modifier.padding(top = 24.dp, bottom = 8.dp), + style = MaterialTheme.typography.titleSmall, + ) - OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - value = action.queryKey, - onValueChange = { viewModel.setIntentQueryExtra(it) }, - label = { Text("Extra key") }, - supportingText = { - Text("The key of the string extra that the search term is passed as.") + SingleChoiceSegmentedButtonRow( + modifier = Modifier.fillMaxWidth() + ) { + SegmentedButton( + selected = action.queryKey == null, + onClick = { + viewModel.setQueryKey(null) }, + shape = SegmentedButtonDefaults.itemShape(0, 2) + ) { + Text("Data") + } + SegmentedButton( + selected = action.queryKey != null, + onClick = { + viewModel.setQueryKey("") + }, + shape = SegmentedButtonDefaults.itemShape(1, 2) + ) { + Text("String extra") + } + } + + if (action.queryKey != null) { + + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + value = action.queryKey ?: "", + onValueChange = { viewModel.setQueryKey(it) }, + label = { Text("Extra key") }, + singleLine = true, isError = viewModel.customIntentKeyError.value ) + } - var showAdvanced by remember { - mutableStateOf(false) - } + val placeholderBackground = MaterialTheme.colorScheme.tertiary + val placeholderColor = MaterialTheme.colorScheme.onTertiary - AnimatedVisibility(!showAdvanced) { - TextButton( - modifier = Modifier.padding(top = 16.dp), - onClick = { showAdvanced = true }) { - Text(stringResource(id = R.string.websearch_dialog_advanced)) - } - } + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + value = action.queryTemplate ?: "", + onValueChange = { viewModel.setIntentQueryTemplate(it) }, + label = { Text(if (action.queryKey == null) "Data template" else "String extra template") }, + supportingText = { + Text( + if (action.queryKey == null) { + "The URI template that is used to construct the intent\\'s data URI. Use ‘\${1}’ as a placeholder for the actual search term, e.g. geo:0,0?q=,\${1}" + } else { + "The template that is used to construct the string that is passed to the intent as a string extra. Use ‘\${1}’ as a placeholder for the actual search term" + } + ) + }, + singleLine = true, + visualTransformation = { + TransformedText(buildAnnotatedString { + append(it) + val placeholderIndex = it.indexOf("\${1}") + if (placeholderIndex != -1) { + addStyle( + SpanStyle( + fontWeight = FontWeight.Bold, + color = placeholderColor, + background = placeholderBackground, + ), + placeholderIndex, placeholderIndex + 4 + ) + } + }, OffsetMapping.Identity) + }, + isError = viewModel.customIntentTemplateError.value + ) - AnimatedVisibility(showAdvanced) { - IntentExtrasEditor(viewModel) + Text( + text = "Base intent", + modifier = Modifier.padding(top = 24.dp, bottom = 8.dp), + style = MaterialTheme.typography.titleSmall, + ) + + + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp), + value = action.baseIntent.action ?: "", + onValueChange = { viewModel.setIntentAction(it) }, + label = { Text("Action") }, + singleLine = true, + ) + + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + value = action.baseIntent.categories?.firstOrNull() ?: "", + onValueChange = { viewModel.setIntentCategory(it) }, + label = { Text("Category") }, + singleLine = true, + ) + + if (action.queryKey != null) { + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + value = action.baseIntent.dataString ?: "", + onValueChange = { viewModel.setIntentData(it) }, + label = { Text("Data") }, + singleLine = true, + ) + } + + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + value = action.baseIntent.type ?: "", + onValueChange = { viewModel.setIntentType(it) }, + label = { Text("Type") }, + singleLine = true, + ) + + + var showAdvanced by remember { + mutableStateOf(false) + } + + AnimatedVisibility(!showAdvanced) { + TextButton( + modifier = Modifier.padding(top = 16.dp), + onClick = { showAdvanced = true }) { + Text(stringResource(id = R.string.websearch_dialog_advanced)) } } + + AnimatedVisibility(showAdvanced) { + IntentExtrasEditor(viewModel) + } } } @@ -765,7 +876,9 @@ fun PickIcon(viewModel: EditSearchActionSheetVM, paddingValues: PaddingValues) { } else { Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(paddingValues).fillMaxWidth() + modifier = Modifier + .padding(paddingValues) + .fillMaxWidth() ) { SearchActionIconTile { diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/EditSearchActionSheetVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/EditSearchActionSheetVM.kt index cac1dd133..c2f088d31 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/EditSearchActionSheetVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/EditSearchActionSheetVM.kt @@ -9,6 +9,7 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.dp +import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import de.mm20.launcher2.ktx.romanize @@ -16,8 +17,8 @@ import de.mm20.launcher2.searchactions.SearchActionService import de.mm20.launcher2.searchactions.actions.SearchActionIcon import de.mm20.launcher2.searchactions.builders.AppSearchActionBuilder import de.mm20.launcher2.searchactions.builders.CustomIntentActionBuilder -import de.mm20.launcher2.searchactions.builders.CustomizableSearchActionBuilder import de.mm20.launcher2.searchactions.builders.CustomWebsearchActionBuilder +import de.mm20.launcher2.searchactions.builders.CustomizableSearchActionBuilder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flow import kotlinx.coroutines.launch @@ -182,6 +183,7 @@ class EditSearchActionSheetVM : ViewModel(), KoinComponent { private val invalidWebsearchUrl = mutableStateOf(null) val websearchInvalidUrlError = derivedStateOf { invalidWebsearchUrl.value == (searchAction.value as? CustomWebsearchActionBuilder)?.urlTemplate } + val customIntentTemplateError = mutableStateOf(false) val customIntentKeyError = mutableStateOf(false) fun validate(): Boolean { val action = searchAction.value ?: return false @@ -192,9 +194,9 @@ class EditSearchActionSheetVM : ViewModel(), KoinComponent { return valid } if (action is CustomIntentActionBuilder) { - val valid = action.queryKey.isNotBlank() - customIntentKeyError.value = !valid - return valid + customIntentTemplateError.value = !(action.queryTemplate.isNullOrEmpty() || action.queryTemplate!!.contains("\${1}")) + customIntentKeyError.value = action.queryKey != null && action.queryKey!!.isEmpty() + return !customIntentTemplateError.value && !customIntentKeyError.value } return true } @@ -218,7 +220,12 @@ class EditSearchActionSheetVM : ViewModel(), KoinComponent { deleteCustomIcon(action.customIcon) } searchAction.value = when (action) { - is CustomWebsearchActionBuilder -> action.copy(icon = icon, customIcon = null, iconColor = 0) + is CustomWebsearchActionBuilder -> action.copy( + icon = icon, + customIcon = null, + iconColor = 0 + ) + is CustomIntentActionBuilder -> action.copy( icon = icon, customIcon = null, @@ -484,7 +491,37 @@ class EditSearchActionSheetVM : ViewModel(), KoinComponent { } } - fun setIntentQueryExtra(key: String) { + fun setIntentType(type: String) { + val searchAction = searchAction.value ?: return + this.searchAction.value = when (searchAction) { + is CustomIntentActionBuilder -> searchAction.copy( + baseIntent = searchAction.baseIntent.cloneFilter().also { + val extras = searchAction.baseIntent.extras?.deepCopy() ?: Bundle() + it.replaceExtras(extras) + it.setDataAndType(it.data, type.takeIf { it.isNotBlank() }) + }, + ) + + else -> searchAction + } + } + + fun setIntentData(data: String) { + val searchAction = searchAction.value ?: return + this.searchAction.value = when (searchAction) { + is CustomIntentActionBuilder -> searchAction.copy( + baseIntent = searchAction.baseIntent.cloneFilter().also { + val extras = searchAction.baseIntent.extras?.deepCopy() ?: Bundle() + it.replaceExtras(extras) + it.setDataAndType(data.takeIf { it.isNotEmpty() }?.toUri(), it.type) + }, + ) + + else -> searchAction + } + } + + fun setQueryKey(key: String?) { val action = searchAction.value ?: return searchAction.value = when (action) { is CustomIntentActionBuilder -> action.copy( @@ -495,6 +532,17 @@ class EditSearchActionSheetVM : ViewModel(), KoinComponent { } } + fun setIntentQueryTemplate(template: String) { + val action = searchAction.value ?: return + searchAction.value = when (action) { + is CustomIntentActionBuilder -> action.copy( + queryTemplate = template.takeIf { it.isNotEmpty() }, + ) + + else -> action + } + } + fun setQueryEncoding(encoding: CustomWebsearchActionBuilder.QueryEncoding) { val action = searchAction.value ?: return searchAction.value = when (action) { diff --git a/data/search-actions/src/main/java/de/mm20/launcher2/searchactions/actions/CustomIntentAction.kt b/data/search-actions/src/main/java/de/mm20/launcher2/searchactions/actions/CustomIntentAction.kt index 23266e43a..d2448d7a5 100644 --- a/data/search-actions/src/main/java/de/mm20/launcher2/searchactions/actions/CustomIntentAction.kt +++ b/data/search-actions/src/main/java/de/mm20/launcher2/searchactions/actions/CustomIntentAction.kt @@ -2,20 +2,33 @@ package de.mm20.launcher2.searchactions.actions import android.content.Context import android.content.Intent +import android.net.Uri +import androidx.core.net.toUri import de.mm20.launcher2.ktx.tryStartActivity class CustomIntentAction( override val label: String, val query: String, - private val queryKey: String, + private val queryKey: String?, + private val queryTemplate: String? = null, private val baseIntent: Intent, override val icon: SearchActionIcon = SearchActionIcon.Custom, override val iconColor: Int = 1, override val customIcon: String? = null, ) : SearchAction { override fun start(context: Context) { + val query = if (queryTemplate.isNullOrBlank()) { + query + } else { + queryTemplate.replace("\${1}", query) + } + val intent = Intent(baseIntent).also { - it.putExtra(queryKey, query) + if (queryKey == null) { + it.setDataAndType(query.toUri(), baseIntent.type) + } else { + it.putExtra(queryKey, query) + } } context.tryStartActivity(intent) } diff --git a/data/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/CustomIntentActionBuilder.kt b/data/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/CustomIntentActionBuilder.kt index d82f1c98d..36a3153a1 100644 --- a/data/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/CustomIntentActionBuilder.kt +++ b/data/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/CustomIntentActionBuilder.kt @@ -9,7 +9,11 @@ import de.mm20.launcher2.searchactions.actions.SearchActionIcon data class CustomIntentActionBuilder( override val label: String, - val queryKey: String, + /** + * The extra of the intent to put the query in. If null, the query will be passed in the data URI. + */ + val queryKey: String?, + val queryTemplate: String? = null, val baseIntent: Intent, override val icon: SearchActionIcon = SearchActionIcon.Custom, override val iconColor: Int = 1, @@ -20,7 +24,7 @@ data class CustomIntentActionBuilder( override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction { return CustomIntentAction( - label, classifiedQuery.text, queryKey, baseIntent, icon, iconColor, customIcon + label, classifiedQuery.text, queryKey, queryTemplate, baseIntent, icon, iconColor, customIcon ) } } \ No newline at end of file diff --git a/data/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/SearchActionBuilder.kt b/data/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/SearchActionBuilder.kt index bc1d52968..0276b276a 100644 --- a/data/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/SearchActionBuilder.kt +++ b/data/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/SearchActionBuilder.kt @@ -57,7 +57,8 @@ interface SearchActionBuilder { iconColor = entity.color ?: 0, icon = SearchActionIcon.fromInt(entity.icon), customIcon = entity.customIcon, - queryKey = options?.getString("extra")?.takeIf { it.isNotEmpty() } ?: return null + queryKey = options?.optString("extra"), + queryTemplate = options?.optString("template")?.takeIf { it.isNotBlank() } ) } "call" -> return CallActionBuilder(context) @@ -107,7 +108,8 @@ interface SearchActionBuilder { icon = builder.icon.toInt(), customIcon = builder.customIcon, options = jsonObjectOf( - "extra" to builder.queryKey + "extra" to builder.queryKey, + "template" to builder.queryTemplate, ).toString() ) else -> SearchActionEntity(