Search actions settings [wip]
This commit is contained in:
parent
ca51056bf4
commit
0697068c0c
@ -25,7 +25,7 @@ class Migration_18_19 : Migration(18, 19) {
|
|||||||
while (websearches.moveToNext()) {
|
while (websearches.moveToNext()) {
|
||||||
val label = websearches.getString(0)
|
val label = websearches.getString(0)
|
||||||
val data = websearches.getString(1)
|
val data = websearches.getString(1)
|
||||||
val color = websearches.getInt(2)
|
val color = 0
|
||||||
val icon = websearches.getStringOrNull(3)
|
val icon = websearches.getStringOrNull(3)
|
||||||
val encoding = websearches.getStringOrNull(4)
|
val encoding = websearches.getStringOrNull(4)
|
||||||
|
|
||||||
|
|||||||
@ -238,6 +238,8 @@
|
|||||||
<!-- Close a dialog -->
|
<!-- Close a dialog -->
|
||||||
<string name="close">Close</string>
|
<string name="close">Close</string>
|
||||||
<string name="save">Save</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 -->
|
<!-- Turn something off / disable a functionality of the launcher -->
|
||||||
<string name="turn_off">Turn off</string>
|
<string name="turn_off">Turn off</string>
|
||||||
<string name="widget_action_adjust_height">Adjust height</string>
|
<string name="widget_action_adjust_height">Adjust height</string>
|
||||||
@ -651,7 +653,7 @@
|
|||||||
<string name="backup_component_favorites">Favorites & hidden apps</string>
|
<string name="backup_component_favorites">Favorites & hidden apps</string>
|
||||||
<string name="backup_component_settings">Settings</string>
|
<string name="backup_component_settings">Settings</string>
|
||||||
<string name="backup_component_websearches">Web search shortcuts</string>
|
<string name="backup_component_websearches">Web search shortcuts</string>
|
||||||
<string name="backup_component_searchactions">Web & 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_widgets">Built-in widgets</string>
|
||||||
<string name="backup_component_customizations">Customizations</string>
|
<string name="backup_component_customizations">Customizations</string>
|
||||||
<string name="backup_complete">The backup has been completed.</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_contact">Add to contacts</string>
|
||||||
<string name="search_action_open_url">View website</string>
|
<string name="search_action_open_url">View website</string>
|
||||||
<string name="search_action_event">Schedule event</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>
|
</resources>
|
||||||
@ -1,8 +1,11 @@
|
|||||||
package de.mm20.launcher2.searchactions
|
package de.mm20.launcher2.searchactions
|
||||||
|
|
||||||
|
import android.app.SearchManager
|
||||||
|
import android.app.SearchableInfo
|
||||||
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.ResolveInfo
|
import android.content.pm.PackageManager
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@ -14,7 +17,6 @@ import coil.size.Scale
|
|||||||
import de.mm20.launcher2.crashreporter.CrashReporter
|
import de.mm20.launcher2.crashreporter.CrashReporter
|
||||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||||
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
|
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.SearchActionBuilder
|
||||||
import de.mm20.launcher2.searchactions.builders.WebsearchActionBuilder
|
import de.mm20.launcher2.searchactions.builders.WebsearchActionBuilder
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
@ -47,7 +49,7 @@ interface SearchActionService {
|
|||||||
|
|
||||||
suspend fun importWebsearch(url: String, iconSize: Int): WebsearchActionBuilder?
|
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?
|
suspend fun createIcon(uri: Uri, size: Int): String?
|
||||||
}
|
}
|
||||||
@ -100,6 +102,16 @@ internal class SearchActionServiceImpl(
|
|||||||
} else {
|
} else {
|
||||||
"https://$url"
|
"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 document = Jsoup.parse(URL(u), 5000)
|
||||||
val metaElements =
|
val metaElements =
|
||||||
document.select("link[rel=\"search\"][href][type=\"application/opensearchdescription+xml\"]")
|
document.select("link[rel=\"search\"][href][type=\"application/opensearchdescription+xml\"]")
|
||||||
@ -185,6 +197,7 @@ internal class SearchActionServiceImpl(
|
|||||||
label = label ?: "",
|
label = label ?: "",
|
||||||
icon = if (localIconUrl == null) SearchActionIcon.Search else SearchActionIcon.Custom,
|
icon = if (localIconUrl == null) SearchActionIcon.Search else SearchActionIcon.Custom,
|
||||||
customIcon = localIconUrl,
|
customIcon = localIconUrl,
|
||||||
|
iconColor = if (localIconUrl == null) 0 else 1,
|
||||||
urlTemplate = urlTemplate ?: ""
|
urlTemplate = urlTemplate ?: ""
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -214,9 +227,19 @@ internal class SearchActionServiceImpl(
|
|||||||
return@withContext file.absolutePath
|
return@withContext file.absolutePath
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getSearchActivities(): List<ResolveInfo> {
|
override suspend fun getSearchActivities(): List<ComponentName> {
|
||||||
val packageManager = context.packageManager
|
return withContext(Dispatchers.Default) {
|
||||||
val intent = Intent(Intent.ACTION_SEARCH)
|
val resolveInfos = context.packageManager.queryIntentActivities(
|
||||||
return packageManager.queryIntentActivities(intent, 0)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -10,15 +10,16 @@ data class AppSearchAction(
|
|||||||
override val label: String,
|
override val label: String,
|
||||||
val componentName: ComponentName,
|
val componentName: ComponentName,
|
||||||
val query: String,
|
val query: String,
|
||||||
|
override val icon: SearchActionIcon = SearchActionIcon.Custom,
|
||||||
|
override val iconColor: Int = 1,
|
||||||
|
override val customIcon: String? = null,
|
||||||
): SearchAction {
|
): SearchAction {
|
||||||
override val icon: SearchActionIcon = SearchActionIcon.Search
|
|
||||||
override val iconColor: Int = 0
|
|
||||||
override val customIcon: String? = null
|
|
||||||
|
|
||||||
override fun start(context: Context) {
|
override fun start(context: Context) {
|
||||||
val intent = Intent(Intent.ACTION_SEARCH).apply {
|
val intent = Intent(Intent.ACTION_SEARCH).apply {
|
||||||
component = componentName
|
component = componentName
|
||||||
putExtra(SearchManager.QUERY, query)
|
putExtra(SearchManager.QUERY, query)
|
||||||
|
putExtra(SearchManager.USER_QUERY, query)
|
||||||
}
|
}
|
||||||
context.tryStartActivity(intent)
|
context.tryStartActivity(intent)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -8,10 +8,10 @@ import de.mm20.launcher2.searchactions.actions.AppSearchAction
|
|||||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||||
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
|
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
|
||||||
|
|
||||||
class AppSearchActionBuilder(
|
data class AppSearchActionBuilder(
|
||||||
override val label: String,
|
override val label: String,
|
||||||
val componentName: ComponentName,
|
val componentName: ComponentName,
|
||||||
override val icon: SearchActionIcon = SearchActionIcon.Search,
|
override val icon: SearchActionIcon = SearchActionIcon.Custom,
|
||||||
override val iconColor: Int = 0,
|
override val iconColor: Int = 0,
|
||||||
override val customIcon: String? = null,
|
override val customIcon: String? = null,
|
||||||
) : CustomizableSearchActionBuilder {
|
) : CustomizableSearchActionBuilder {
|
||||||
@ -23,6 +23,9 @@ class AppSearchActionBuilder(
|
|||||||
label = label,
|
label = label,
|
||||||
componentName = componentName,
|
componentName = componentName,
|
||||||
query = classifiedQuery.text,
|
query = classifiedQuery.text,
|
||||||
|
icon = icon,
|
||||||
|
iconColor = iconColor,
|
||||||
|
customIcon = customIcon,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,8 @@
|
|||||||
package de.mm20.launcher2.searchactions.builders
|
package de.mm20.launcher2.searchactions.builders
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.media.metrics.Event
|
import android.media.metrics.Event
|
||||||
import de.mm20.launcher2.database.entities.SearchActionEntity
|
import de.mm20.launcher2.database.entities.SearchActionEntity
|
||||||
import de.mm20.launcher2.ktx.jsonObjectOf
|
import de.mm20.launcher2.ktx.jsonObjectOf
|
||||||
@ -42,7 +44,23 @@ interface SearchActionBuilder {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
"app" -> {
|
"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)
|
"call" -> return CallActionBuilder(context)
|
||||||
"message" -> return MessageActionBuilder(context)
|
"message" -> return MessageActionBuilder(context)
|
||||||
@ -70,7 +88,28 @@ interface SearchActionBuilder {
|
|||||||
"encoding" to builder.encoding.toInt()
|
"encoding" to builder.encoding.toInt()
|
||||||
).toString()
|
).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(
|
else -> SearchActionEntity(
|
||||||
position = position,
|
position = position,
|
||||||
type = builder.key,
|
type = builder.key,
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import de.mm20.launcher2.searchactions.actions.SearchAction
|
|||||||
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
|
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
|
|
||||||
class WebsearchActionBuilder(
|
data class WebsearchActionBuilder(
|
||||||
override val label: String,
|
override val label: String,
|
||||||
val urlTemplate: String,
|
val urlTemplate: String,
|
||||||
override val icon: SearchActionIcon = SearchActionIcon.Search,
|
override val icon: SearchActionIcon = SearchActionIcon.Search,
|
||||||
|
|||||||
@ -3,7 +3,19 @@ package de.mm20.launcher2.ui.component
|
|||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.foundation.gestures.Orientation
|
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.foundation.shape.CornerSize
|
||||||
import androidx.compose.material.FixedThreshold
|
import androidx.compose.material.FixedThreshold
|
||||||
import androidx.compose.material.FractionalThreshold
|
import androidx.compose.material.FractionalThreshold
|
||||||
@ -13,7 +25,12 @@ import androidx.compose.material3.CenterAlignedTopAppBar
|
|||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clipToBounds
|
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(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
package de.mm20.launcher2.ui.component
|
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.Icons
|
||||||
import androidx.compose.material.icons.rounded.Alarm
|
import androidx.compose.material.icons.rounded.Alarm
|
||||||
import androidx.compose.material.icons.rounded.Call
|
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.Event
|
||||||
import androidx.compose.material.icons.rounded.Language
|
import androidx.compose.material.icons.rounded.Language
|
||||||
import androidx.compose.material.icons.rounded.Person
|
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.Search
|
||||||
import androidx.compose.material.icons.rounded.Sms
|
import androidx.compose.material.icons.rounded.Sms
|
||||||
import androidx.compose.material.icons.rounded.Timer
|
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.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
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.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.graphics.isSpecified
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
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.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
|
@Composable
|
||||||
fun SearchActionIcon(
|
fun SearchActionIcon(
|
||||||
icon: SearchActionIcon,
|
icon: SearchActionIcon,
|
||||||
color: Int,
|
color: Int = 0,
|
||||||
customIcon: String? = null
|
customIcon: String? = null,
|
||||||
|
componentName: ComponentName? = null,
|
||||||
|
size: Dp = 20.dp
|
||||||
) {
|
) {
|
||||||
val tint = when(color) {
|
val tint = when(color) {
|
||||||
0 -> MaterialTheme.colorScheme.primary
|
0 -> MaterialTheme.colorScheme.primary
|
||||||
1 -> Color.Unspecified
|
1 -> Color.Unspecified
|
||||||
else -> Color(color)
|
else -> Color(color)
|
||||||
}
|
}
|
||||||
if (icon != SearchActionIcon.Custom) {
|
if (icon != SearchActionIcon.Custom || customIcon == null && componentName == null) {
|
||||||
Icon(
|
Icon(
|
||||||
|
modifier = Modifier.size(size),
|
||||||
imageVector = getSearchActionIconVector(icon),
|
imageVector = getSearchActionIconVector(icon),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = tint,
|
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 {
|
fun getSearchActionIconVector(icon: SearchActionIcon): ImageVector {
|
||||||
return when (icon) {
|
return when (icon) {
|
||||||
SearchActionIcon.Phone -> Icons.Rounded.Call
|
SearchActionIcon.Phone -> Icons.Rounded.Call
|
||||||
SearchActionIcon.Website -> Icons.Rounded.Language
|
SearchActionIcon.Website -> Icons.Rounded.Language
|
||||||
SearchActionIcon.Alarm -> Icons.Rounded.Alarm
|
SearchActionIcon.Alarm -> Icons.Rounded.Alarm
|
||||||
SearchActionIcon.Timer -> Icons.Rounded.Timer
|
SearchActionIcon.Timer -> Icons.Rounded.Timer
|
||||||
SearchActionIcon.Contact -> Icons.Rounded.Person
|
SearchActionIcon.Contact -> Icons.Rounded.PersonAdd
|
||||||
SearchActionIcon.Email -> Icons.Rounded.Email
|
SearchActionIcon.Email -> Icons.Rounded.Email
|
||||||
SearchActionIcon.Message -> Icons.Rounded.Sms
|
SearchActionIcon.Message -> Icons.Rounded.Sms
|
||||||
SearchActionIcon.Calendar -> Icons.Rounded.Event
|
SearchActionIcon.Calendar -> Icons.Rounded.Event
|
||||||
|
|||||||
@ -47,9 +47,7 @@ fun SearchBarActions(
|
|||||||
label = { Text(it.label) },
|
label = { Text(it.label) },
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
SearchActionIcon(
|
SearchActionIcon(
|
||||||
icon = it.icon,
|
action = it
|
||||||
color = it.iconColor,
|
|
||||||
customIcon = it.customIcon
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/*leadingIcon = {
|
/*leadingIcon = {
|
||||||
|
|||||||
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
)
|
||||||
@ -69,7 +69,7 @@ fun SearchActionsSettingsScreen() {
|
|||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
FloatingActionButton(onClick = { /*TODO*/ }) {
|
FloatingActionButton(onClick = { viewModel.createAction() }) {
|
||||||
Icon(imageVector = Icons.Rounded.Add, contentDescription = null)
|
Icon(imageVector = Icons.Rounded.Add, contentDescription = null)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -119,13 +119,12 @@ fun SearchActionsSettingsScreen() {
|
|||||||
if (item is CustomizableSearchActionBuilder) {
|
if (item is CustomizableSearchActionBuilder) {
|
||||||
Preference(
|
Preference(
|
||||||
icon = {
|
icon = {
|
||||||
SearchActionIcon(
|
SearchActionIcon(item)
|
||||||
icon = item.icon,
|
|
||||||
color = item.iconColor,
|
|
||||||
customIcon = item.customIcon
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
title = item.label
|
title = item.label,
|
||||||
|
onClick = {
|
||||||
|
viewModel.editAction(item)
|
||||||
|
}
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
SwitchPreference(
|
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!!)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,11 +1,17 @@
|
|||||||
package de.mm20.launcher2.ui.settings.searchactions
|
package de.mm20.launcher2.ui.settings.searchactions
|
||||||
|
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.asLiveData
|
import androidx.lifecycle.asLiveData
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
import de.mm20.launcher2.searchactions.SearchActionService
|
import de.mm20.launcher2.searchactions.SearchActionService
|
||||||
|
import de.mm20.launcher2.searchactions.builders.CustomizableSearchActionBuilder
|
||||||
import de.mm20.launcher2.searchactions.builders.SearchActionBuilder
|
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.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
class SearchActionsSettingsScreenVM : ViewModel(), KoinComponent {
|
class SearchActionsSettingsScreenVM : ViewModel(), KoinComponent {
|
||||||
private val searchActionService: SearchActionService by inject()
|
private val searchActionService: SearchActionService by inject()
|
||||||
@ -22,11 +28,27 @@ class SearchActionsSettingsScreenVM : ViewModel(), KoinComponent {
|
|||||||
val actions =
|
val actions =
|
||||||
searchActions.value?.filter { it.key != searchAction.key }?.plus(searchAction) ?: return
|
searchActions.value?.filter { it.key != searchAction.key }?.plus(searchAction) ?: return
|
||||||
searchActionService.saveSearchActionBuilders(actions)
|
searchActionService.saveSearchActionBuilders(actions)
|
||||||
|
showCreateDialog.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAction(searchAction: SearchActionBuilder) {
|
fun removeAction(searchAction: SearchActionBuilder) {
|
||||||
val actions = searchActions.value?.filter { it.key != searchAction.key } ?: return
|
val actions = searchActions.value?.filter { it.key != searchAction.key } ?: return
|
||||||
searchActionService.saveSearchActionBuilders(actions)
|
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) {
|
fun moveItem(fromIndex: Int, toIndex: Int) {
|
||||||
@ -37,4 +59,20 @@ class SearchActionsSettingsScreenVM : ViewModel(), KoinComponent {
|
|||||||
actions.add(toIndex, item)
|
actions.add(toIndex, item)
|
||||||
searchActionService.saveSearchActionBuilders(actions)
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user