Add possibility to add custom extras to app search actions

This commit is contained in:
MM20 2022-11-19 14:17:58 +01:00
parent 47deb3e537
commit a2e6302cd3
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
7 changed files with 505 additions and 21 deletions

View File

@ -4,11 +4,12 @@ import android.app.SearchManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Bundle
import de.mm20.launcher2.ktx.tryStartActivity
data class AppSearchAction(
override val label: String,
val componentName: ComponentName,
val baseIntent: Intent,
val query: String,
override val icon: SearchActionIcon = SearchActionIcon.Custom,
override val iconColor: Int = 1,
@ -16,8 +17,8 @@ data class AppSearchAction(
): SearchAction {
override fun start(context: Context) {
val intent = Intent(Intent.ACTION_SEARCH).apply {
component = componentName
val intent = Intent(baseIntent).apply {
action = Intent.ACTION_SEARCH
putExtra(SearchManager.QUERY, query)
putExtra(SearchManager.USER_QUERY, query)
}

View File

@ -7,8 +7,8 @@ import de.mm20.launcher2.ktx.tryStartActivity
class CustomIntentAction(
override val label: String,
val query: String,
val queryKey: String,
val baseIntent: Intent,
private val queryKey: String,
private val baseIntent: Intent,
override val icon: SearchActionIcon = SearchActionIcon.Custom,
override val iconColor: Int = 1,
override val customIcon: String? = null,

View File

@ -2,6 +2,8 @@ package de.mm20.launcher2.searchactions.builders
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.os.Bundle
import de.mm20.launcher2.searchactions.TextClassificationResult
import de.mm20.launcher2.searchactions.TextType
import de.mm20.launcher2.searchactions.actions.AppSearchAction
@ -10,18 +12,18 @@ import de.mm20.launcher2.searchactions.actions.SearchActionIcon
data class AppSearchActionBuilder(
override val label: String,
val componentName: ComponentName,
val baseIntent: Intent,
override val icon: SearchActionIcon = SearchActionIcon.Custom,
override val iconColor: Int = 0,
override val customIcon: String? = null,
) : CustomizableSearchActionBuilder {
override val key: String
get() = "app://${componentName.flattenToShortString()}"
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
get() = "app://${baseIntent.toUri(0)}"
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction {
return AppSearchAction(
label = label,
componentName = componentName,
baseIntent = baseIntent,
query = classifiedQuery.text,
icon = icon,
iconColor = iconColor,

View File

@ -46,7 +46,7 @@ interface SearchActionBuilder {
"app" -> {
return AppSearchActionBuilder(
label = entity.label ?: "",
componentName = ComponentName.unflattenFromString(entity.data ?: return null) ?: return null,
baseIntent = Intent.parseUri(entity.data, 0),
iconColor = entity.color ?: 0,
icon = SearchActionIcon.fromInt(entity.icon),
customIcon = entity.customIcon,
@ -92,7 +92,7 @@ interface SearchActionBuilder {
position = position,
type = "app",
label = builder.label,
data = builder.componentName.flattenToShortString(),
data = builder.baseIntent.toUri(0),
color = builder.iconColor,
icon = builder.icon.toInt(),
customIcon = builder.customIcon,

View File

@ -92,7 +92,7 @@ fun SearchActionIcon(action: SearchAction, size: Dp = 20.dp) {
icon = action.icon,
color = action.iconColor,
customIcon = action.customIcon,
componentName = (action as? AppSearchAction)?.componentName,
componentName = (action as? AppSearchAction)?.baseIntent?.component,
size = size,
)
}
@ -103,7 +103,7 @@ fun SearchActionIcon(builder: CustomizableSearchActionBuilder, size: Dp = 20.dp)
icon = builder.icon,
color = builder.iconColor,
customIcon = builder.customIcon,
componentName = (builder as? AppSearchActionBuilder)?.componentName,
componentName = (builder as? AppSearchActionBuilder)?.baseIntent?.component,
size = size,
)
}

View File

@ -2,6 +2,7 @@ package de.mm20.launcher2.ui.settings.searchactions
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
@ -21,18 +22,25 @@ import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Android
import androidx.compose.material.icons.rounded.ArrowDropDown
import androidx.compose.material.icons.rounded.Delete
import androidx.compose.material.icons.rounded.ManageSearch
import androidx.compose.material.icons.rounded.MoreVert
import androidx.compose.material.icons.rounded.RemoveCircleOutline
import androidx.compose.material.icons.rounded.ToggleOn
import androidx.compose.material.icons.rounded.TravelExplore
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.FilledIconButton
import androidx.compose.material3.FilledTonalIconButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
@ -60,6 +68,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.OffsetMapping
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
@ -68,6 +77,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
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.WebsearchActionBuilder
import de.mm20.launcher2.ui.R
@ -373,7 +383,9 @@ private fun InitWebSearchPage(viewModel: EditSearchActionSheetVM) {
fun CustomizeWebSearch(viewModel: EditSearchActionSheetVM) {
val searchAction by viewModel.searchAction
Column {
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
if (searchAction != null && searchAction is WebsearchActionBuilder) {
Row(
@ -456,11 +468,16 @@ fun CustomizeAppSearch(viewModel: EditSearchActionSheetVM) {
initial = emptyList()
)
val selectedApp =
remember(availableSearchApps, (searchAction as? AppSearchActionBuilder)?.componentName) {
availableSearchApps.find { it.componentName == (searchAction as? AppSearchActionBuilder)?.componentName }
remember(
availableSearchApps,
(searchAction as? AppSearchActionBuilder)?.baseIntent?.component
) {
availableSearchApps.find { it.componentName == (searchAction as? AppSearchActionBuilder)?.baseIntent?.component }
}
Column {
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
if (searchAction != null) {
@ -538,6 +555,23 @@ fun CustomizeAppSearch(viewModel: EditSearchActionSheetVM) {
}
}
}
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)
}
}
}
}
@ -547,7 +581,9 @@ fun CustomizeCustomIntent(viewModel: EditSearchActionSheetVM) {
val searchAction by viewModel.searchAction
if (searchAction != null) {
Column {
Column(
modifier = Modifier.verticalScroll(rememberScrollState())
) {
Row(
verticalAlignment = Alignment.Bottom
) {
@ -602,7 +638,7 @@ fun PickIcon(viewModel: EditSearchActionSheetVM) {
}) {
SearchActionIcon(
icon = SearchActionIcon.Custom,
componentName = (action as AppSearchActionBuilder).componentName,
componentName = (action as AppSearchActionBuilder).baseIntent.component,
size = 24.dp,
color = 1,
)
@ -703,4 +739,297 @@ private fun SearchActionIconTile(
icon()
}
}
}
@Composable
private fun IntentExtrasEditor(viewModel: EditSearchActionSheetVM) {
val action = viewModel.searchAction.value
val extras = remember(action?.key) {
when (action) {
is CustomIntentActionBuilder -> action.baseIntent.extras
is AppSearchActionBuilder -> action.baseIntent.extras
else -> null
}
}
val keys = remember(extras) { extras?.keySet()?.sorted() ?: emptyList() }
Column(
modifier = Modifier.padding(top = 24.dp)
) {
Text("Extras", style = MaterialTheme.typography.titleSmall)
for (key in keys) {
Row(
modifier = Modifier.padding(top = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
val value = extras?.get(key)
when (value) {
is String -> {
OutlinedTextField(
modifier = Modifier
.weight(1f)
.padding(bottom = 8.dp),
value = value,
onValueChange = { viewModel.putStringExtra(key, it) },
label = { Text(key) },
leadingIcon = {
Text("ABC", style = MaterialTheme.typography.labelSmall)
},
singleLine = true,
)
}
is Long -> {
OutlinedTextField(
modifier = Modifier
.weight(1f)
.padding(bottom = 8.dp),
value = value.toString(),
onValueChange = {
viewModel.putLongExtra(
key,
it.replace(Regex("[^0-9]"), "").toLongOrNull() ?: 0
)
},
label = { Text(key) },
leadingIcon = {
Text("1234", style = MaterialTheme.typography.labelSmall)
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true,
)
}
is Int -> {
OutlinedTextField(
modifier = Modifier
.weight(1f)
.padding(bottom = 8.dp),
value = value.toString(),
onValueChange = {
viewModel.putIntExtra(
key,
it.replace(Regex("[^0-9]"), "").toIntOrNull() ?: 0
)
},
label = { Text(key) },
leadingIcon = {
Text("123", style = MaterialTheme.typography.labelSmall)
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true,
)
}
is Double -> {
OutlinedTextField(
modifier = Modifier
.weight(1f)
.padding(bottom = 8.dp),
value = value.toString(),
onValueChange = {
viewModel.putDoubleExtra(
key,
it.replace(Regex("[^0-9.]"), "").toDoubleOrNull() ?: 0.0
)
},
label = { Text(key) },
leadingIcon = {
Text("1.00", style = MaterialTheme.typography.labelSmall)
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true,
)
}
is Float -> {
OutlinedTextField(
modifier = Modifier
.weight(1f)
.padding(bottom = 8.dp),
value = value.toString(),
onValueChange = {
viewModel.putFloatExtra(
key,
it.replace(Regex("[^0-9.]"), "").toFloatOrNull() ?: 0f
)
},
label = { Text(key) },
leadingIcon = {
Text("1.0", style = MaterialTheme.typography.labelSmall)
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
singleLine = true,
)
}
is Boolean -> {
Switch(
checked = value,
onCheckedChange = { viewModel.putBooleanExtra(key, it) })
Text(
text = key,
modifier = Modifier
.weight(1f)
.padding(start = 8.dp),
style = MaterialTheme.typography.labelSmall,
)
}
}
IconButton(
onClick = { viewModel.removeExtra(key) },
modifier = Modifier.padding(horizontal = 8.dp)
) {
Icon(imageVector = Icons.Rounded.RemoveCircleOutline, contentDescription = null)
}
}
}
OutlinedCard(
modifier = Modifier
.padding(top = 16.dp)
.fillMaxWidth(),
shape = MaterialTheme.shapes.medium,
) {
var newKey by remember { mutableStateOf("") }
var newType by remember { mutableStateOf("string") }
var showTypeDropdown by remember { mutableStateOf(false) }
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
) {
FilledTonalIconButton(
modifier = Modifier.padding(end = 8.dp),
onClick = { showTypeDropdown = !showTypeDropdown }
) {
when (newType) {
"bool" -> {
Icon(Icons.Rounded.ToggleOn, contentDescription = null)
}
"string" -> {
Text("ABC", style = MaterialTheme.typography.labelSmall)
}
"int" -> {
Text("123", style = MaterialTheme.typography.labelSmall)
}
"long" -> {
Text("1234", style = MaterialTheme.typography.labelSmall)
}
"float" -> {
Text("1.0", style = MaterialTheme.typography.labelSmall)
}
"double" -> {
Text("1.00", style = MaterialTheme.typography.labelSmall)
}
}
DropdownMenu(
expanded = showTypeDropdown,
onDismissRequest = { showTypeDropdown = false }) {
DropdownMenuItem(
leadingIcon = {
Text(
"ABC",
style = MaterialTheme.typography.labelSmall
)
},
text = { Text("String") },
onClick = {
newType = "string"
showTypeDropdown = false
})
DropdownMenuItem(
leadingIcon = {
Text(
"123",
style = MaterialTheme.typography.labelSmall
)
},
text = { Text("Integer") },
onClick = {
newType = "int"
showTypeDropdown = false
})
DropdownMenuItem(
leadingIcon = {
Text(
"1234",
style = MaterialTheme.typography.labelSmall
)
},
text = { Text("Long") },
onClick = {
newType = "long"
showTypeDropdown = false
})
DropdownMenuItem(
leadingIcon = {
Text(
"1.0",
style = MaterialTheme.typography.labelSmall
)
},
text = { Text("Float") },
onClick = {
newType = "float"
showTypeDropdown = false
})
DropdownMenuItem(
leadingIcon = {
Text(
"1.00",
style = MaterialTheme.typography.labelSmall
)
},
text = { Text("Double") },
onClick = {
newType = "double"
showTypeDropdown = false
})
DropdownMenuItem(
leadingIcon = { Icon(Icons.Rounded.ToggleOn, null) },
text = { Text("Boolean") },
onClick = {
newType = "bool"
showTypeDropdown = false
})
}
}
OutlinedTextField(
modifier = Modifier
.weight(1f)
.padding(bottom = 8.dp),
label = { Text("Key") },
value = newKey,
onValueChange = { newKey = it },
)
FilledIconButton(
modifier = Modifier.padding(start = 8.dp),
onClick = {
when (newType) {
"string" -> viewModel.putStringExtra(newKey)
"int" -> viewModel.putIntExtra(newKey)
"bool" -> viewModel.putBooleanExtra(newKey)
"long" -> viewModel.putLongExtra(newKey)
"double" -> viewModel.putDoubleExtra(newKey)
"float" -> viewModel.putFloatExtra(newKey)
}
newKey = ""
},
enabled = newKey.isNotBlank()
) {
Icon(imageVector = Icons.Rounded.Add, contentDescription = null)
}
}
}
}
}

View File

@ -4,6 +4,7 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.unit.Density
@ -73,7 +74,9 @@ class EditSearchActionSheetVM : ViewModel(), KoinComponent {
fun selectSearchableApp(app: SearchableApp) {
searchAction.value = AppSearchActionBuilder(
label = app.label,
componentName = app.componentName,
baseIntent = Intent().apply {
setComponent(app.componentName)
},
icon = SearchActionIcon.Custom,
customIcon = null,
iconColor = 1,
@ -116,7 +119,9 @@ class EditSearchActionSheetVM : ViewModel(), KoinComponent {
)
)
is AppSearchActionBuilder -> action.copy(componentName = componentName)
is AppSearchActionBuilder -> action.also {
it.baseIntent.setComponent(componentName)
}
is WebsearchActionBuilder -> action
}
@ -259,6 +264,153 @@ class EditSearchActionSheetVM : ViewModel(), KoinComponent {
setCustomIcon(path)
}
}
fun removeExtra(key: String) {
val action = searchAction.value ?: return
searchAction.value = when(action) {
is CustomIntentActionBuilder -> action.copy(
baseIntent = action.baseIntent.cloneFilter().also {
val extras = action.baseIntent.extras?.deepCopy() ?: Bundle()
extras.remove(key)
it.replaceExtras(extras)
},
)
is AppSearchActionBuilder -> action.copy(
baseIntent = action.baseIntent.cloneFilter().also {
val extras = action.baseIntent.extras?.deepCopy() ?: Bundle()
extras.remove(key)
it.replaceExtras(extras)
}
)
else -> action
}
}
fun putStringExtra(key: String, value: String = "") {
val action = searchAction.value ?: return
searchAction.value = when(action) {
is CustomIntentActionBuilder -> action.copy(
baseIntent = action.baseIntent.cloneFilter().also {
val extras = action.baseIntent.extras?.deepCopy() ?: Bundle()
extras.putString(key, value)
it.replaceExtras(extras)
},
)
is AppSearchActionBuilder -> action.copy(
baseIntent = action.baseIntent.cloneFilter().also {
val extras = action.baseIntent.extras?.deepCopy() ?: Bundle()
extras.putString(key, value)
it.replaceExtras(extras)
},
)
else -> action
}
}
fun putIntExtra(key: String, value: Int = 0) {
val action = searchAction.value ?: return
searchAction.value = when(action) {
is CustomIntentActionBuilder -> action.copy(
baseIntent = action.baseIntent.cloneFilter().also {
val extras = action.baseIntent.extras?.deepCopy() ?: Bundle()
extras.putInt(key, value)
it.replaceExtras(extras)
},
)
is AppSearchActionBuilder -> action.copy(
baseIntent = action.baseIntent.cloneFilter().also {
val extras = action.baseIntent.extras?.deepCopy() ?: Bundle()
extras.putInt(key, value)
it.replaceExtras(extras)
},
)
else -> action
}
}
fun putLongExtra(key: String, value: Long = 0L) {
val action = searchAction.value ?: return
searchAction.value = when(action) {
is CustomIntentActionBuilder -> action.copy(
baseIntent = action.baseIntent.cloneFilter().also {
val extras = action.baseIntent.extras?.deepCopy() ?: Bundle()
extras.putLong(key, value)
it.replaceExtras(extras)
},
)
is AppSearchActionBuilder -> action.copy(
baseIntent = action.baseIntent.cloneFilter().also {
val extras = action.baseIntent.extras?.deepCopy() ?: Bundle()
extras.putLong(key, value)
it.replaceExtras(extras)
},
)
else -> action
}
}
fun putFloatExtra(key: String, value: Float = 0f) {
val action = searchAction.value ?: return
searchAction.value = when(action) {
is CustomIntentActionBuilder -> action.copy(
baseIntent = action.baseIntent.cloneFilter().also {
val extras = action.baseIntent.extras?.deepCopy() ?: Bundle()
extras.putFloat(key, value)
it.replaceExtras(extras)
},
)
is AppSearchActionBuilder -> action.copy(
baseIntent = action.baseIntent.cloneFilter().also {
val extras = action.baseIntent.extras?.deepCopy() ?: Bundle()
extras.putFloat(key, value)
it.replaceExtras(extras)
},
)
else -> action
}
}
fun putDoubleExtra(key: String, value: Double = 0.0) {
val action = searchAction.value ?: return
searchAction.value = when(action) {
is CustomIntentActionBuilder -> action.copy(
baseIntent = action.baseIntent.cloneFilter().also {
val extras = action.baseIntent.extras?.deepCopy() ?: Bundle()
extras.putDouble(key, value)
it.replaceExtras(extras)
},
)
is AppSearchActionBuilder -> action.copy(
baseIntent = action.baseIntent.cloneFilter().also {
val extras = action.baseIntent.extras?.deepCopy() ?: Bundle()
extras.putDouble(key, value)
it.replaceExtras(extras)
},
)
else -> action
}
}
fun putBooleanExtra(key: String, value: Boolean = false) {
val action = searchAction.value ?: return
searchAction.value = when(action) {
is CustomIntentActionBuilder -> action.copy(
baseIntent = action.baseIntent.cloneFilter().also {
val extras = action.baseIntent.extras?.deepCopy() ?: Bundle()
extras.putBoolean(key, value)
it.replaceExtras(extras)
},
)
is AppSearchActionBuilder -> action.copy(
baseIntent = action.baseIntent.cloneFilter().also {
val extras = action.baseIntent.extras?.deepCopy() ?: Bundle()
extras.putBoolean(key, value)
it.replaceExtras(extras)
},
)
else -> action
}
}
}
enum class EditSearchActionPage {