Search actions settings [wip]

This commit is contained in:
MM20 2022-11-17 21:46:20 +01:00
parent ca51056bf4
commit 0697068c0c
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
16 changed files with 1306 additions and 42 deletions

View File

@ -25,7 +25,7 @@ class Migration_18_19 : Migration(18, 19) {
while (websearches.moveToNext()) {
val label = websearches.getString(0)
val data = websearches.getString(1)
val color = websearches.getInt(2)
val color = 0
val icon = websearches.getStringOrNull(3)
val encoding = websearches.getStringOrNull(4)

View File

@ -238,6 +238,8 @@
<!-- Close a dialog -->
<string name="close">Close</string>
<string name="save">Save</string>
<string name="skip">Skip</string>
<string name="action_continue">Continue</string>
<!-- Turn something off / disable a functionality of the launcher -->
<string name="turn_off">Turn off</string>
<string name="widget_action_adjust_height">Adjust height</string>
@ -651,7 +653,7 @@
<string name="backup_component_favorites">Favorites &amp; hidden apps</string>
<string name="backup_component_settings">Settings</string>
<string name="backup_component_websearches">Web search shortcuts</string>
<string name="backup_component_searchactions">Web &amp; app search shortcuts</string>
<string name="backup_component_searchactions">Quick actions</string>
<string name="backup_component_widgets">Built-in widgets</string>
<string name="backup_component_customizations">Customizations</string>
<string name="backup_complete">The backup has been completed.</string>
@ -688,4 +690,19 @@
<string name="search_action_contact">Add to contacts</string>
<string name="search_action_open_url">View website</string>
<string name="search_action_event">Schedule event</string>
<string name="create_search_action_type">What kind of action do you want to create?</string>
<string name="create_search_action_type_web">Search on a website</string>
<string name="create_search_action_type_app">Search in an app</string>
<string name="create_search_action_type_intent">Custom intent</string>
<string name="create_search_action_title">New quick action</string>
<string name="edit_search_action_title">Edit quick action</string>
<string name="create_search_action_pick_app">Pick an app to search:</string>
<string name="create_search_action_website_url">Enter the address of the website:</string>
<string name="create_search_action_website_invalid_url">The given website cannot automatically be imported as a web search. You can try a different website or enter the required data manually in the next step.</string>
<string name="search_action_label">Name</string>
<string name="search_action_app">App</string>
<string name="search_action_websearch_url">URL template</string>
<string name="search_action_websearch_url_hint">The URL template that is used to construct the web search URL. Use ${1} as a placeholder for the actual search term, e.g. https://google.com?q=${1}.</string>
<string name="more_information">More information</string>
</resources>

View File

@ -1,8 +1,11 @@
package de.mm20.launcher2.searchactions
import android.app.SearchManager
import android.app.SearchableInfo
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.ResolveInfo
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.net.Uri
import android.util.Log
@ -14,7 +17,6 @@ import coil.size.Scale
import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
import de.mm20.launcher2.searchactions.builders.CallActionBuilder
import de.mm20.launcher2.searchactions.builders.SearchActionBuilder
import de.mm20.launcher2.searchactions.builders.WebsearchActionBuilder
import kotlinx.collections.immutable.ImmutableList
@ -47,7 +49,7 @@ interface SearchActionService {
suspend fun importWebsearch(url: String, iconSize: Int): WebsearchActionBuilder?
suspend fun getSearchActivities(): List<ResolveInfo>
suspend fun getSearchActivities(): List<ComponentName>
suspend fun createIcon(uri: Uri, size: Int): String?
}
@ -100,6 +102,16 @@ internal class SearchActionServiceImpl(
} else {
"https://$url"
}
if (u.contains("${1}")) {
return@withContext WebsearchActionBuilder(
urlTemplate = u,
label = "",
iconColor = 0,
icon = SearchActionIcon.Search,
)
}
val document = Jsoup.parse(URL(u), 5000)
val metaElements =
document.select("link[rel=\"search\"][href][type=\"application/opensearchdescription+xml\"]")
@ -185,6 +197,7 @@ internal class SearchActionServiceImpl(
label = label ?: "",
icon = if (localIconUrl == null) SearchActionIcon.Search else SearchActionIcon.Custom,
customIcon = localIconUrl,
iconColor = if (localIconUrl == null) 0 else 1,
urlTemplate = urlTemplate ?: ""
)
}
@ -214,9 +227,19 @@ internal class SearchActionServiceImpl(
return@withContext file.absolutePath
}
override suspend fun getSearchActivities(): List<ResolveInfo> {
val packageManager = context.packageManager
val intent = Intent(Intent.ACTION_SEARCH)
return packageManager.queryIntentActivities(intent, 0)
override suspend fun getSearchActivities(): List<ComponentName> {
return withContext(Dispatchers.Default) {
val resolveInfos = context.packageManager.queryIntentActivities(
Intent(Intent.ACTION_SEARCH).addCategory(Intent.CATEGORY_DEFAULT), PackageManager.GET_META_DATA,
)
resolveInfos.mapNotNull {
if (!it.activityInfo.exported || !it.activityInfo.enabled) return@mapNotNull null
if (it.activityInfo.permission != null && context.checkSelfPermission(it.activityInfo.permission) != PackageManager.PERMISSION_GRANTED) {
return@mapNotNull null
}
val componentName = ComponentName(it.activityInfo.packageName, it.activityInfo.name)
componentName
}
}
}
}

View File

@ -10,15 +10,16 @@ data class AppSearchAction(
override val label: String,
val componentName: ComponentName,
val query: String,
override val icon: SearchActionIcon = SearchActionIcon.Custom,
override val iconColor: Int = 1,
override val customIcon: String? = null,
): SearchAction {
override val icon: SearchActionIcon = SearchActionIcon.Search
override val iconColor: Int = 0
override val customIcon: String? = null
override fun start(context: Context) {
val intent = Intent(Intent.ACTION_SEARCH).apply {
component = componentName
putExtra(SearchManager.QUERY, query)
putExtra(SearchManager.USER_QUERY, query)
}
context.tryStartActivity(intent)
}

View File

@ -0,0 +1,22 @@
package de.mm20.launcher2.searchactions.actions
import android.content.Context
import android.content.Intent
import de.mm20.launcher2.ktx.tryStartActivity
class CustomIntentAction(
override val label: String,
val query: String,
val queryKey: String,
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 intent = Intent(baseIntent).also {
it.putExtra(queryKey, query)
}
context.tryStartActivity(intent)
}
}

View File

@ -8,10 +8,10 @@ import de.mm20.launcher2.searchactions.actions.AppSearchAction
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
class AppSearchActionBuilder(
data class AppSearchActionBuilder(
override val label: String,
val componentName: ComponentName,
override val icon: SearchActionIcon = SearchActionIcon.Search,
override val icon: SearchActionIcon = SearchActionIcon.Custom,
override val iconColor: Int = 0,
override val customIcon: String? = null,
) : CustomizableSearchActionBuilder {
@ -23,6 +23,9 @@ class AppSearchActionBuilder(
label = label,
componentName = componentName,
query = classifiedQuery.text,
icon = icon,
iconColor = iconColor,
customIcon = customIcon,
)
}
}

View File

@ -0,0 +1,26 @@
package de.mm20.launcher2.searchactions.builders
import android.content.Context
import android.content.Intent
import de.mm20.launcher2.searchactions.TextClassificationResult
import de.mm20.launcher2.searchactions.actions.CustomIntentAction
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
data class CustomIntentActionBuilder(
override val label: String,
val queryKey: String,
val baseIntent: Intent,
override val icon: SearchActionIcon = SearchActionIcon.Custom,
override val iconColor: Int = 1,
override val customIcon: String? = null,
) : CustomizableSearchActionBuilder {
override val key: String
get() = "intent://${baseIntent.toUri(0)}"
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction {
return CustomIntentAction(
label, classifiedQuery.text, queryKey, baseIntent, icon, iconColor, customIcon
)
}
}

View File

@ -1,6 +1,8 @@
package de.mm20.launcher2.searchactions.builders
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.media.metrics.Event
import de.mm20.launcher2.database.entities.SearchActionEntity
import de.mm20.launcher2.ktx.jsonObjectOf
@ -42,7 +44,23 @@ interface SearchActionBuilder {
)
}
"app" -> {
return null
return AppSearchActionBuilder(
label = entity.label ?: "",
componentName = ComponentName.unflattenFromString(entity.data ?: return null) ?: return null,
iconColor = entity.color ?: 0,
icon = SearchActionIcon.fromInt(entity.icon),
customIcon = entity.customIcon,
)
}
"intent" -> {
return CustomIntentActionBuilder(
entity.label ?: "",
baseIntent = Intent.parseUri(entity.data, 0),
iconColor = entity.color ?: 0,
icon = SearchActionIcon.fromInt(entity.icon),
customIcon = entity.customIcon,
queryKey = options?.getString("extra")?.takeIf { it.isNotEmpty() } ?: return null
)
}
"call" -> return CallActionBuilder(context)
"message" -> return MessageActionBuilder(context)
@ -70,7 +88,28 @@ interface SearchActionBuilder {
"encoding" to builder.encoding.toInt()
).toString()
)
//is AppSearchActionBuilder -> null
is AppSearchActionBuilder -> SearchActionEntity(
position = position,
type = "app",
label = builder.label,
data = builder.componentName.flattenToShortString(),
color = builder.iconColor,
icon = builder.icon.toInt(),
customIcon = builder.customIcon,
options = null
)
is CustomIntentActionBuilder -> SearchActionEntity(
position = position,
type = "intent",
label = builder.label,
data = builder.baseIntent.toUri(0),
color = builder.iconColor,
icon = builder.icon.toInt(),
customIcon = builder.customIcon,
options = jsonObjectOf(
"extra" to builder.queryKey
).toString()
)
else -> SearchActionEntity(
position = position,
type = builder.key,

View File

@ -8,7 +8,7 @@ import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
import java.net.URLEncoder
class WebsearchActionBuilder(
data class WebsearchActionBuilder(
override val label: String,
val urlTemplate: String,
override val icon: SearchActionIcon = SearchActionIcon.Search,

View File

@ -3,7 +3,19 @@ package de.mm20.launcher2.ui.component
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.material.FixedThreshold
import androidx.compose.material.FractionalThreshold
@ -13,7 +25,12 @@ import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
@ -193,16 +210,15 @@ fun BottomSheetDialog(
}
}
val elevation by animateDpAsState(if (swipeState.offset.value == 0f) 0.dp else 1.dp)
Surface(
modifier = Modifier
.wrapContentHeight()
.fillMaxWidth(),
tonalElevation = elevation,
) {
if (confirmButton != null || dismissButton != null) {
if (confirmButton != null || dismissButton != null) {
val elevation by animateDpAsState(if (swipeState.offset.value == 0f) 0.dp else 1.dp)
Surface(
modifier = Modifier
.wrapContentHeight()
.fillMaxWidth(),
tonalElevation = elevation,
) {
Row(
modifier = Modifier
.fillMaxWidth()

View File

@ -1,5 +1,8 @@
package de.mm20.launcher2.ui.component
import android.content.ComponentName
import android.graphics.drawable.Drawable
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Alarm
import androidx.compose.material.icons.rounded.Call
@ -7,6 +10,7 @@ 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.PersonAdd
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material.icons.rounded.Sms
import androidx.compose.material.icons.rounded.Timer
@ -14,37 +18,103 @@ 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.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.isSpecified
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import de.mm20.launcher2.searchactions.actions.AppSearchAction
import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
import de.mm20.launcher2.searchactions.builders.AppSearchActionBuilder
import de.mm20.launcher2.searchactions.builders.CustomizableSearchActionBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@Composable
fun SearchActionIcon(
icon: SearchActionIcon,
color: Int,
customIcon: String? = null
color: Int = 0,
customIcon: String? = null,
componentName: ComponentName? = null,
size: Dp = 20.dp
) {
val tint = when(color) {
0 -> MaterialTheme.colorScheme.primary
1 -> Color.Unspecified
else -> Color(color)
}
if (icon != SearchActionIcon.Custom) {
if (icon != SearchActionIcon.Custom || customIcon == null && componentName == null) {
Icon(
modifier = Modifier.size(size),
imageVector = getSearchActionIconVector(icon),
contentDescription = null,
tint = tint,
)
} else if (customIcon == null && componentName != null) {
val context = LocalContext.current
var drawable by remember(componentName) { mutableStateOf<Drawable?>(null) }
LaunchedEffect(componentName) {
drawable = withContext(Dispatchers.IO) {
context.packageManager.getActivityIcon(componentName)
}
}
AsyncImage(
model = drawable,
contentDescription = null,
modifier = Modifier.size(size),
colorFilter = if (tint.isSpecified) ColorFilter.tint(tint) else null
)
} else {
AsyncImage(
model = customIcon,
contentDescription = null,
modifier = Modifier.size(size),
colorFilter = if (tint.isSpecified) ColorFilter.tint(tint) else null
)
}
}
@Composable
fun SearchActionIcon(action: SearchAction, size: Dp = 20.dp) {
SearchActionIcon(
icon = action.icon,
color = action.iconColor,
customIcon = action.customIcon,
componentName = (action as? AppSearchAction)?.componentName,
size = size,
)
}
@Composable
fun SearchActionIcon(builder: CustomizableSearchActionBuilder, size: Dp = 20.dp) {
SearchActionIcon(
icon = builder.icon,
color = builder.iconColor,
customIcon = builder.customIcon,
componentName = (builder as? AppSearchActionBuilder)?.componentName,
size = size,
)
}
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.Contact -> Icons.Rounded.PersonAdd
SearchActionIcon.Email -> Icons.Rounded.Email
SearchActionIcon.Message -> Icons.Rounded.Sms
SearchActionIcon.Calendar -> Icons.Rounded.Event

View File

@ -47,9 +47,7 @@ fun SearchBarActions(
label = { Text(it.label) },
leadingIcon = {
SearchActionIcon(
icon = it.icon,
color = it.iconColor,
customIcon = it.customIcon
action = it
)
}
/*leadingIcon = {

View File

@ -0,0 +1,706 @@
package de.mm20.launcher2.ui.settings.searchactions
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
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.material.icons.Icons
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.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.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
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.OffsetMapping
import androidx.compose.ui.text.input.TransformedText
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
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.CustomizableSearchActionBuilder
import de.mm20.launcher2.searchactions.builders.WebsearchActionBuilder
import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.SearchActionIcon
import de.mm20.launcher2.ui.ktx.toPixels
@Composable
fun EditSearchActionSheet(
initialSearchAction: CustomizableSearchActionBuilder?,
onSave: (CustomizableSearchActionBuilder) -> Unit,
onDismiss: () -> Unit,
onDelete: () -> Unit = {},
) {
val viewModel: EditSearchActionSheetVM = viewModel()
LaunchedEffect(initialSearchAction) {
viewModel.init(initialSearchAction)
}
val createNew by viewModel.createNew
val page by viewModel.currentPage
val searchAction by viewModel.searchAction
BottomSheetDialog(
onDismissRequest = {
viewModel.onDismiss()
onDismiss()
},
dismissOnBackPress = {
page != EditSearchActionPage.PickIcon
},
swipeToDismiss = {
page != EditSearchActionPage.PickIcon
},
confirmButton = when (page) {
EditSearchActionPage.CustomizeAppSearch,
EditSearchActionPage.CustomizeWebSearch,
EditSearchActionPage.CustomizeCustomIntent -> {
{
Button(onClick = {
if (viewModel.validate()) {
viewModel.onSave()
searchAction?.let { onSave(it) }
}
}) {
Text(stringResource(R.string.save))
}
}
}
EditSearchActionPage.InitWebSearch -> {
{
val density = LocalDensity.current
Button(
onClick = {
if (viewModel.skipWebsearchImport.value) {
viewModel.skipWebsearchImport()
} else {
viewModel.importWebsearch(density)
}
},
enabled = !viewModel.loadingWebsearch.value
) {
Text(
stringResource(
if (viewModel.skipWebsearchImport.value) {
R.string.skip
} else {
R.string.action_continue
}
)
)
}
}
}
EditSearchActionPage.PickIcon -> {
{
OutlinedButton(onClick = {
viewModel.applyIcon()
}) {
Text(stringResource(R.string.ok))
}
}
}
else -> null
},
actions = {
var showMenu by remember { mutableStateOf(false) }
if (!createNew) {
IconButton(onClick = { showMenu = !showMenu }) {
Icon(imageVector = Icons.Rounded.MoreVert, contentDescription = null)
DropdownMenu(expanded = showMenu, onDismissRequest = { showMenu = false }) {
DropdownMenuItem(
text = { Text(stringResource(id = R.string.menu_delete)) },
leadingIcon = {
Icon(imageVector = Icons.Rounded.Delete, contentDescription = null)
},
onClick = {
onDelete()
showMenu = false
}
)
}
}
}
},
title = {
Text(
stringResource(
if (createNew) {
R.string.create_search_action_title
} else {
R.string.edit_search_action_title
}
)
)
}) {
when (page) {
EditSearchActionPage.SelectType -> SelectTypePage(viewModel)
EditSearchActionPage.InitWebSearch -> InitWebSearchPage(viewModel)
EditSearchActionPage.InitAppSearch -> InitAppSearchPage(viewModel)
EditSearchActionPage.CustomizeWebSearch -> CustomizeWebSearch(viewModel)
EditSearchActionPage.CustomizeCustomIntent -> CustomizeCustomIntent(viewModel)
EditSearchActionPage.CustomizeAppSearch -> CustomizeAppSearch(viewModel)
EditSearchActionPage.PickIcon -> PickIcon(viewModel)
}
}
}
@Composable
private fun SelectTypePage(viewModel: EditSearchActionSheetVM) {
Column {
Text(
text = stringResource(R.string.create_search_action_type),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.secondary
)
OutlinedCard(
onClick = { viewModel.initWebSearch() },
modifier = Modifier
.padding(top = 12.dp)
.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Rounded.TravelExplore,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
)
Text(
text = stringResource(R.string.create_search_action_type_web),
modifier = Modifier.padding(start = 16.dp),
style = MaterialTheme.typography.labelLarge,
)
}
}
OutlinedCard(
onClick = { viewModel.initAppSearch() },
modifier = Modifier
.padding(top = 12.dp)
.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Rounded.ManageSearch,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
)
Text(
text = stringResource(R.string.create_search_action_type_app),
modifier = Modifier.padding(start = 16.dp),
style = MaterialTheme.typography.labelLarge,
)
}
}
OutlinedCard(
onClick = { viewModel.initCustomIntent() },
modifier = Modifier
.padding(top = 12.dp, bottom = 16.dp)
.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = Icons.Rounded.Android,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
)
Text(
text = stringResource(R.string.create_search_action_type_intent),
modifier = Modifier.padding(start = 16.dp),
style = MaterialTheme.typography.labelLarge,
)
}
}
}
}
@Composable
private fun InitAppSearchPage(viewModel: EditSearchActionSheetVM) {
val context = LocalContext.current
val searchableApps by remember { viewModel.getSearchableApps(context) }.collectAsState(null)
if (searchableApps != null) {
LazyColumn(
contentPadding = PaddingValues(bottom = 16.dp)
) {
item {
Text(
text = stringResource(R.string.create_search_action_pick_app),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.secondary
)
}
items(searchableApps!!) {
OutlinedCard(
onClick = { viewModel.selectSearchableApp(it) },
modifier = Modifier
.padding(top = 12.dp)
.fillMaxWidth()
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
SearchActionIcon(
size = 24.dp,
componentName = it.componentName,
icon = de.mm20.launcher2.searchactions.actions.SearchActionIcon.Custom,
color = 1,
)
Text(
text = it.label,
modifier = Modifier.padding(start = 16.dp),
style = MaterialTheme.typography.labelLarge,
)
}
}
}
}
}
}
@Composable
private fun InitWebSearchPage(viewModel: EditSearchActionSheetVM) {
var url by viewModel.initWebsearchUrl
val importError by viewModel.websearchImportError
val loading by viewModel.loadingWebsearch
Column(
modifier = Modifier.fillMaxWidth()
) {
Text(
text = stringResource(R.string.create_search_action_website_url),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.secondary
)
val density = LocalDensity.current
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(top = 12.dp),
value = url, onValueChange = { url = it },
singleLine = true,
keyboardActions = KeyboardActions(onDone = { viewModel.importWebsearch(density) }),
enabled = !loading,
trailingIcon = {
if (loading) {
CircularProgressIndicator(modifier = Modifier.size(24.dp))
}
}
)
if (importError) {
Surface(
modifier = Modifier.padding(top = 16.dp),
color = MaterialTheme.colorScheme.surfaceVariant,
shape = MaterialTheme.shapes.medium,
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
tonalElevation = 0.dp,
shadowElevation = 0.dp
) {
Text(
modifier = Modifier.padding(16.dp),
text = stringResource(R.string.create_search_action_website_invalid_url),
style = MaterialTheme.typography.bodySmall
)
}
}
}
}
@Composable
fun CustomizeWebSearch(viewModel: EditSearchActionSheetVM) {
val searchAction by viewModel.searchAction
Column {
if (searchAction != null && searchAction is WebsearchActionBuilder) {
Row(
modifier = Modifier.padding(bottom = 16.dp),
verticalAlignment = Alignment.Bottom
) {
SearchActionIconTile(onClick = {
viewModel.openIconPicker()
}) {
SearchActionIcon(
builder = searchAction!!, size = 24.dp
)
}
OutlinedTextField(
modifier = Modifier
.weight(1f)
.padding(start = 16.dp),
value = searchAction!!.label,
onValueChange = { viewModel.setLabel(it) },
label = { Text(stringResource(R.string.search_action_label)) },
)
}
val placeholderBackground = MaterialTheme.colorScheme.tertiary
val placeholderColor = MaterialTheme.colorScheme.onTertiary
OutlinedTextField(
modifier = Modifier
.fillMaxWidth(),
singleLine = true,
value = (searchAction as WebsearchActionBuilder).urlTemplate,
onValueChange = { viewModel.setUrlTemplate(it) },
label = { Text(stringResource(R.string.search_action_websearch_url)) },
supportingText = {
if (viewModel.websearchInvalidUrlError.value) {
Text(stringResource(R.string.websearch_dialog_url_error))
} else {
Column {
Text(stringResource(R.string.search_action_websearch_url_hint))
/** TODO: Write user guide for this and link it here
Text(
stringResource(R.string.more_information),
modifier = Modifier
.padding(vertical = 4.dp)
.clickable { },
color = MaterialTheme.colorScheme.secondary,
style = LocalTextStyle.current.copy(textDecoration = TextDecoration.Underline)
)
*/
}
}
},
isError = viewModel.websearchInvalidUrlError.value,
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)
}
)
}
}
}
@Composable
fun CustomizeAppSearch(viewModel: EditSearchActionSheetVM) {
val searchAction by viewModel.searchAction
val context = LocalContext.current
val availableSearchApps by remember { viewModel.getSearchableApps(context) }.collectAsState(
initial = emptyList()
)
val selectedApp =
remember(availableSearchApps, (searchAction as? AppSearchActionBuilder)?.componentName) {
availableSearchApps.find { it.componentName == (searchAction as? AppSearchActionBuilder)?.componentName }
}
Column {
if (searchAction != null) {
Row(
modifier = Modifier.padding(bottom = 16.dp),
verticalAlignment = Alignment.Bottom,
) {
SearchActionIconTile(onClick = {
viewModel.openIconPicker()
}) {
SearchActionIcon(
builder = searchAction!!, size = 24.dp
)
}
OutlinedTextField(
modifier = Modifier
.weight(1f)
.padding(start = 16.dp),
value = searchAction!!.label,
onValueChange = { viewModel.setLabel(it) },
label = { Text(stringResource(R.string.search_action_label)) },
)
}
var showAppDropdown by remember { mutableStateOf(false) }
Box(modifier = Modifier
.fillMaxWidth()
.clickable { showAppDropdown = !showAppDropdown }) {
TextFieldDefaults.OutlinedTextFieldDecorationBox(
value = selectedApp?.label ?: "",
enabled = true,
label = { Text(stringResource(R.string.search_action_app)) },
innerTextField = {
Text(
selectedApp?.label ?: "",
modifier = Modifier.fillMaxWidth()
)
},
interactionSource = remember { MutableInteractionSource() },
singleLine = true,
visualTransformation = VisualTransformation.None,
leadingIcon = {
if (selectedApp != null) {
SearchActionIcon(
size = 24.dp,
componentName = selectedApp.componentName,
icon = de.mm20.launcher2.searchactions.actions.SearchActionIcon.Custom,
color = 1,
)
}
},
trailingIcon = {
Icon(imageVector = Icons.Rounded.ArrowDropDown, contentDescription = null)
}
)
DropdownMenu(
expanded = showAppDropdown,
onDismissRequest = { showAppDropdown = false }) {
for (app in availableSearchApps) {
DropdownMenuItem(
text = { Text(app.label) },
onClick = {
viewModel.setComponentName(app.componentName)
showAppDropdown = false
},
leadingIcon = {
SearchActionIcon(
size = 24.dp,
componentName = app.componentName,
icon = de.mm20.launcher2.searchactions.actions.SearchActionIcon.Custom,
color = 1,
)
}
)
}
}
}
}
}
}
@Composable
fun CustomizeCustomIntent(viewModel: EditSearchActionSheetVM) {
val searchAction by viewModel.searchAction
if (searchAction != null) {
Column {
Row(
verticalAlignment = Alignment.Bottom
) {
SearchActionIconTile(onClick = {
viewModel.openIconPicker()
}) {
SearchActionIcon(
builder = searchAction!!, size = 24.dp
)
}
OutlinedTextField(
modifier = Modifier
.weight(1f)
.padding(start = 16.dp),
value = searchAction!!.label,
onValueChange = { viewModel.setLabel(it) },
label = { Text(stringResource(R.string.search_action_label)) },
)
}
}
}
}
@Composable
fun PickIcon(viewModel: EditSearchActionSheetVM) {
val action by viewModel.searchAction
val iconSizePx = 20.dp.toPixels()
val pickIconLauncher =
rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) {
if (it != null) viewModel.importIcon(it, iconSizePx.toInt())
}
if (action?.customIcon == null) {
val availableIcons =
remember { SearchActionIcon.values().filter { it != SearchActionIcon.Custom } }
Column {
LazyVerticalGrid(columns = GridCells.Adaptive(64.dp)) {
if (action is AppSearchActionBuilder) {
item {
Box(
modifier = Modifier.padding(vertical = 8.dp),
contentAlignment = Alignment.Center
) {
val isSelected =
action?.icon == SearchActionIcon.Custom && action?.customIcon == null
SearchActionIconTile(isSelected, onClick = {
viewModel.setCustomIcon(null)
}) {
SearchActionIcon(
icon = SearchActionIcon.Custom,
componentName = (action as AppSearchActionBuilder).componentName,
size = 24.dp,
color = 1,
)
}
}
}
}
items(availableIcons) {
Box(
modifier = Modifier.padding(vertical = 8.dp),
contentAlignment = Alignment.Center
) {
val isSelected = action?.icon == it
SearchActionIconTile(isSelected, onClick = {
viewModel.setIcon(it)
}) {
SearchActionIcon(
icon = it,
size = 24.dp,
color = 0,
)
}
}
}
}
TextButton(
modifier = Modifier.padding(top = 16.dp, bottom = 24.dp),
onClick = { pickIconLauncher.launch("image/*") }) {
Text(stringResource(R.string.websearch_dialog_custom_icon))
}
}
} else {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
SearchActionIconTile {
SearchActionIcon(builder = action!!, size = 24.dp)
}
Row(
modifier = Modifier.padding(top = 16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
modifier = Modifier.padding(end = 16.dp),
text = "Monochrome",
textAlign = TextAlign.End,
style = MaterialTheme.typography.labelMedium,
)
Switch(
checked = action?.iconColor == 0,
onCheckedChange = { viewModel.setIconColor(if (it) 0 else 1) })
}
Row(
modifier = Modifier
.padding(top = 24.dp)
.horizontalScroll(rememberScrollState())
) {
OutlinedButton(
modifier = Modifier.padding(start = 16.dp),
onClick = { pickIconLauncher.launch("image/*") }) {
Text(stringResource(R.string.websearch_dialog_replace_icon))
}
OutlinedButton(
modifier = Modifier.padding(start = 16.dp),
onClick = { viewModel.setIcon(SearchActionIcon.Search) },
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.error
)
) {
Text(stringResource(R.string.websearch_dialog_delete_icon))
}
}
}
}
}
@Composable
private fun SearchActionIconTile(
filled: Boolean = true,
onClick: () -> Unit = {},
icon: @Composable () -> Unit,
) {
Box(
modifier = Modifier
.clip(MaterialTheme.shapes.medium)
.then(
if (filled) Modifier.background(MaterialTheme.colorScheme.surfaceVariant)
else Modifier.border(
1.dp,
MaterialTheme.colorScheme.outline,
MaterialTheme.shapes.medium
)
)
.clickable(onClick = onClick)
) {
Box(modifier = Modifier.padding(16.dp)) {
icon()
}
}
}

View File

@ -0,0 +1,277 @@
package de.mm20.launcher2.ui.settings.searchactions
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.net.Uri
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.ktx.romanize
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.WebsearchActionBuilder
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.File
import kotlin.math.roundToInt
class EditSearchActionSheetVM : ViewModel(), KoinComponent {
private val searchActionService: SearchActionService by inject()
private var initialCustomIcon: String? = null
val currentPage = mutableStateOf(EditSearchActionPage.SelectType)
val createNew = mutableStateOf(false)
val searchAction = mutableStateOf<CustomizableSearchActionBuilder?>(null)
fun init(searchAction: CustomizableSearchActionBuilder?) {
initialCustomIcon = searchAction?.customIcon
currentPage.value = when (searchAction) {
is AppSearchActionBuilder -> EditSearchActionPage.CustomizeAppSearch
is WebsearchActionBuilder -> EditSearchActionPage.CustomizeWebSearch
else -> EditSearchActionPage.SelectType
}
createNew.value = searchAction == null
this.searchAction.value = searchAction
}
fun getSearchableApps(context: Context) = flow {
val items = withContext(Dispatchers.Default) {
searchActionService.getSearchActivities().map {
SearchableApp(
label = context.packageManager.getActivityInfo(it, 0)
.loadLabel(context.packageManager).toString(),
componentName = it
)
}.sortedBy { it.label.romanize().lowercase() }
}
emit(items)
}
fun initAppSearch() {
currentPage.value = EditSearchActionPage.InitAppSearch
}
fun initWebSearch() {
currentPage.value = EditSearchActionPage.InitWebSearch
}
fun selectSearchableApp(app: SearchableApp) {
searchAction.value = AppSearchActionBuilder(
label = app.label,
componentName = app.componentName,
icon = SearchActionIcon.Custom,
customIcon = null,
iconColor = 1,
)
currentPage.value = EditSearchActionPage.CustomizeAppSearch
}
fun initCustomIntent() {
searchAction.value = CustomIntentActionBuilder(
label = "",
queryKey = "",
baseIntent = Intent(),
icon = SearchActionIcon.Search,
customIcon = null,
iconColor = 0,
)
currentPage.value = EditSearchActionPage.CustomizeCustomIntent
}
fun setLabel(label: String) {
val action = searchAction.value ?: return
val newAction = when (action) {
is CustomIntentActionBuilder -> action.copy(label = label)
is AppSearchActionBuilder -> action.copy(label = label)
is WebsearchActionBuilder -> action.copy(label = label)
}
searchAction.value = newAction
}
fun setComponentName(componentName: ComponentName) {
val action = searchAction.value ?: return
val newAction = when (action) {
is CustomIntentActionBuilder -> action.copy(
baseIntent = action.baseIntent.setComponent(
componentName
)
)
is AppSearchActionBuilder -> action.copy(componentName = componentName)
is WebsearchActionBuilder -> action
}
searchAction.value = newAction
}
val initWebsearchUrl = mutableStateOf("")
/**
* Last imported URL that failed (if the current URL is equal to this, show an error banner)
*/
private val websearchImportErrorUrl = mutableStateOf<String?>(null)
val websearchImportError =
derivedStateOf { websearchImportErrorUrl.value == initWebsearchUrl.value }
val loadingWebsearch = mutableStateOf(false)
val skipWebsearchImport = derivedStateOf { websearchImportError.value || initWebsearchUrl.value.isEmpty() }
fun importWebsearch(density: Density) {
if (loadingWebsearch.value) return
viewModelScope.launch {
val url = initWebsearchUrl.value
loadingWebsearch.value = true
val action = searchActionService.importWebsearch(
url,
with(density) { 20.dp.toPx().roundToInt() })
if (action == null) {
websearchImportErrorUrl.value = url
} else {
websearchImportErrorUrl.value = null
searchAction.value = action
currentPage.value = EditSearchActionPage.CustomizeWebSearch
}
loadingWebsearch.value = false
}
}
fun skipWebsearchImport() {
searchAction.value = WebsearchActionBuilder(
urlTemplate = "",
iconColor = 0,
icon = SearchActionIcon.Search,
label = "",
)
currentPage.value = EditSearchActionPage.CustomizeWebSearch
}
fun setUrlTemplate(template: String) {
val action = searchAction.value ?: return
if (action is WebsearchActionBuilder) {
searchAction.value = action.copy(
urlTemplate = template
)
}
}
private val invalidWebsearchUrl = mutableStateOf<String?>(null)
val websearchInvalidUrlError = derivedStateOf { invalidWebsearchUrl.value == (searchAction.value as? WebsearchActionBuilder)?.urlTemplate }
fun validate() : Boolean {
val action = searchAction.value ?: return false
if (action is WebsearchActionBuilder) {
val valid = action.urlTemplate.contains("\${1}")
invalidWebsearchUrl.value = if(valid) null else action.urlTemplate
return valid
}
return true
}
fun onSave() {
val action = searchAction.value ?: return
if (initialCustomIcon != action.customIcon) deleteCustomIcon(initialCustomIcon)
}
fun onDismiss() {
val action = searchAction.value ?: return
val newIcon = action.customIcon
if (newIcon != initialCustomIcon) {
deleteCustomIcon(newIcon)
}
}
fun setIcon(icon: SearchActionIcon) {
val action = searchAction.value ?: return
if (action.customIcon != initialCustomIcon) {
deleteCustomIcon(action.customIcon)
}
searchAction.value = when(action) {
is WebsearchActionBuilder -> action.copy(icon = icon, customIcon = null, iconColor = 0)
is CustomIntentActionBuilder -> action.copy(icon = icon, customIcon = null, iconColor = 0)
is AppSearchActionBuilder -> action.copy(icon = icon, customIcon = null, iconColor = 0)
}
}
fun setCustomIcon(iconPath: String?) {
val action = searchAction.value ?: return
if (action.customIcon != initialCustomIcon) {
deleteCustomIcon(action.customIcon)
}
searchAction.value = when(action) {
is WebsearchActionBuilder -> action.copy(customIcon = iconPath, iconColor = 1, icon = SearchActionIcon.Custom)
is CustomIntentActionBuilder -> action.copy(customIcon = iconPath, iconColor = 1, icon = SearchActionIcon.Custom)
is AppSearchActionBuilder -> action.copy(customIcon = iconPath, iconColor = 1, icon = SearchActionIcon.Custom)
}
}
private fun deleteCustomIcon(path: String?) {
path ?: return
viewModelScope.launch(Dispatchers.IO) {
File(path).delete()
}
}
fun openIconPicker() {
currentPage.value = EditSearchActionPage.PickIcon
}
fun applyIcon() {
currentPage.value = when(searchAction.value) {
is AppSearchActionBuilder -> EditSearchActionPage.CustomizeAppSearch
is WebsearchActionBuilder -> EditSearchActionPage.CustomizeWebSearch
is CustomIntentActionBuilder -> EditSearchActionPage.CustomizeCustomIntent
null -> EditSearchActionPage.SelectType
}
}
fun setIconColor(color: Int) {
val action = searchAction.value ?: return
searchAction.value = when(action) {
is WebsearchActionBuilder -> action.copy(iconColor = color)
is CustomIntentActionBuilder -> action.copy(iconColor = color)
is AppSearchActionBuilder -> action.copy(iconColor = color)
}
}
fun importIcon(uri: Uri, size: Int) {
viewModelScope.launch {
val path = searchActionService.createIcon(uri, size)
setCustomIcon(path)
}
}
}
enum class EditSearchActionPage {
SelectType,
InitAppSearch,
InitWebSearch,
CustomizeAppSearch,
CustomizeWebSearch,
CustomizeCustomIntent,
PickIcon,
}
data class SearchableApp(
val label: String,
val componentName: ComponentName,
)

View File

@ -69,7 +69,7 @@ fun SearchActionsSettingsScreen() {
Scaffold(
floatingActionButton = {
FloatingActionButton(onClick = { /*TODO*/ }) {
FloatingActionButton(onClick = { viewModel.createAction() }) {
Icon(imageVector = Icons.Rounded.Add, contentDescription = null)
}
},
@ -119,13 +119,12 @@ fun SearchActionsSettingsScreen() {
if (item is CustomizableSearchActionBuilder) {
Preference(
icon = {
SearchActionIcon(
icon = item.icon,
color = item.iconColor,
customIcon = item.customIcon
)
SearchActionIcon(item)
},
title = item.label
title = item.label,
onClick = {
viewModel.editAction(item)
}
)
} else {
SwitchPreference(
@ -165,4 +164,33 @@ fun SearchActionsSettingsScreen() {
}
}
}
val editAction by viewModel.showEditDialogFor.observeAsState(null)
val createAction by viewModel.showCreateDialog.observeAsState(false)
if (createAction) {
EditSearchActionSheet(
initialSearchAction = null,
onSave = {
viewModel.addAction(it)
},
onDismiss = {
viewModel.dismissDialogs()
}
)
}
if (editAction != null) {
EditSearchActionSheet(
initialSearchAction = editAction,
onSave = {
viewModel.updateAction(editAction!!, it)
},
onDismiss = {
viewModel.dismissDialogs()
},
onDelete = {
viewModel.removeAction(editAction!!)
}
)
}
}

View File

@ -1,11 +1,17 @@
package de.mm20.launcher2.ui.settings.searchactions
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.searchactions.SearchActionService
import de.mm20.launcher2.searchactions.builders.CustomizableSearchActionBuilder
import de.mm20.launcher2.searchactions.builders.SearchActionBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.File
class SearchActionsSettingsScreenVM : ViewModel(), KoinComponent {
private val searchActionService: SearchActionService by inject()
@ -22,11 +28,27 @@ class SearchActionsSettingsScreenVM : ViewModel(), KoinComponent {
val actions =
searchActions.value?.filter { it.key != searchAction.key }?.plus(searchAction) ?: return
searchActionService.saveSearchActionBuilders(actions)
showCreateDialog.value = false
}
fun removeAction(searchAction: SearchActionBuilder) {
val actions = searchActions.value?.filter { it.key != searchAction.key } ?: return
searchActionService.saveSearchActionBuilders(actions)
showEditDialogFor.value = null
if(searchAction is CustomizableSearchActionBuilder && searchAction.customIcon != null) {
viewModelScope.launch(Dispatchers.IO) {
File(searchAction.customIcon).delete()
}
}
}
fun updateAction(old: SearchActionBuilder, new: SearchActionBuilder) {
val actions =
searchActions.value
?.mapNotNull { if (it.key == old.key) new else if (it.key == new.key) null else it }
?: return
searchActionService.saveSearchActionBuilders(actions)
showEditDialogFor.value = null
}
fun moveItem(fromIndex: Int, toIndex: Int) {
@ -37,4 +59,20 @@ class SearchActionsSettingsScreenVM : ViewModel(), KoinComponent {
actions.add(toIndex, item)
searchActionService.saveSearchActionBuilders(actions)
}
val showEditDialogFor = MutableLiveData<CustomizableSearchActionBuilder?>(null)
val showCreateDialog = MutableLiveData(false)
fun editAction(action: CustomizableSearchActionBuilder) {
showEditDialogFor.value = action
}
fun createAction() {
showCreateDialog.value = true
}
fun dismissDialogs() {
showCreateDialog.value = false
showEditDialogFor.value = null
}
}