Make search action order customizable

This commit is contained in:
MM20 2022-11-05 17:59:09 +01:00
parent 960c3b70f5
commit 3463b3b800
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
29 changed files with 526 additions and 253 deletions

View File

@ -2,7 +2,7 @@
"formatVersion": 1, "formatVersion": 1,
"database": { "database": {
"version": 19, "version": 19,
"identityHash": "c6adf1dca8ade5117d4e8952a67786fa", "identityHash": "968a73b13f39e00505a4adf1bda01394",
"entities": [ "entities": [
{ {
"tableName": "forecasts", "tableName": "forecasts",
@ -398,7 +398,7 @@
}, },
{ {
"tableName": "SearchAction", "tableName": "SearchAction",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`position` INTEGER NOT NULL, `type` TEXT NOT NULL, `data` TEXT NOT NULL, `label` TEXT, `icon` INTEGER NOT NULL, `color` INTEGER NOT NULL, `customIcon` TEXT, `options` TEXT, PRIMARY KEY(`position`))", "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`position` INTEGER NOT NULL, `type` TEXT NOT NULL, `data` TEXT, `label` TEXT, `icon` INTEGER, `color` INTEGER, `customIcon` TEXT, `options` TEXT, PRIMARY KEY(`position`))",
"fields": [ "fields": [
{ {
"fieldPath": "position", "fieldPath": "position",
@ -416,7 +416,7 @@
"fieldPath": "data", "fieldPath": "data",
"columnName": "data", "columnName": "data",
"affinity": "TEXT", "affinity": "TEXT",
"notNull": true "notNull": false
}, },
{ {
"fieldPath": "label", "fieldPath": "label",
@ -428,13 +428,13 @@
"fieldPath": "icon", "fieldPath": "icon",
"columnName": "icon", "columnName": "icon",
"affinity": "INTEGER", "affinity": "INTEGER",
"notNull": true "notNull": false
}, },
{ {
"fieldPath": "color", "fieldPath": "color",
"columnName": "color", "columnName": "color",
"affinity": "INTEGER", "affinity": "INTEGER",
"notNull": true "notNull": false
}, },
{ {
"fieldPath": "customIcon", "fieldPath": "customIcon",
@ -462,7 +462,7 @@
"views": [], "views": [],
"setupQueries": [ "setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c6adf1dca8ade5117d4e8952a67786fa')" "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '968a73b13f39e00505a4adf1bda01394')"
] ]
} }
} }

View File

@ -57,13 +57,23 @@ abstract class AppDatabase : RoomDatabase() {
.addCallback(object : Callback() { .addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) { override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db) super.onCreate(db)
db.execSQL("INSERT INTO `SearchAction` (`position`, `type`) VALUES" +
"(0, 'call')," +
"(1, 'message')," +
"(2, 'email')," +
"(3, 'contact')," +
"(4, 'alarm')," +
"(5, 'timer')," +
"(6, 'calendar')," +
"(7, 'website')"
)
db.execSQL("INSERT INTO `SearchAction` (`position`, `type`, `data`, `label`, `color`, `icon`, `customIcon`, `options`) " + db.execSQL("INSERT INTO `SearchAction` (`position`, `type`, `data`, `label`, `color`, `icon`, `customIcon`, `options`) " +
"VALUES (?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?)", "VALUES (?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?)",
arrayOf( arrayOf(
0, "url", context.getString(R.string.default_websearch_1_url), context.getString(R.string.default_websearch_1_name), 0, 0, null, null, 8, "url", context.getString(R.string.default_websearch_1_url), context.getString(R.string.default_websearch_1_name), 0, 0, null, null,
0, "url", context.getString(R.string.default_websearch_2_url), context.getString(R.string.default_websearch_2_name), 0, 0, null, null, 9, "url", context.getString(R.string.default_websearch_2_url), context.getString(R.string.default_websearch_2_name), 0, 0, null, null,
0, "url", context.getString(R.string.default_websearch_3_url), context.getString(R.string.default_websearch_3_name), 0, 0, null, null, 10, "url", context.getString(R.string.default_websearch_3_url), context.getString(R.string.default_websearch_3_name), 0, 0, null, null,
) )
) )

View File

@ -1,7 +1,9 @@
package de.mm20.launcher2.database package de.mm20.launcher2.database
import androidx.room.Dao import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query import androidx.room.Query
import androidx.room.Transaction
import de.mm20.launcher2.database.entities.SearchActionEntity import de.mm20.launcher2.database.entities.SearchActionEntity
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -9,4 +11,16 @@ import kotlinx.coroutines.flow.Flow
interface SearchActionDao { interface SearchActionDao {
@Query("SELECT * FROM SearchAction ORDER BY position ASC") @Query("SELECT * FROM SearchAction ORDER BY position ASC")
fun getSearchActions(): Flow<List<SearchActionEntity>> fun getSearchActions(): Flow<List<SearchActionEntity>>
@Transaction
suspend fun replaceAll(actions: List<SearchActionEntity>) {
deleteAll()
insertAll(actions)
}
@Query("DELETE FROM `SearchAction`")
suspend fun deleteAll()
@Insert
suspend fun insertAll(actions: List<SearchActionEntity>)
} }

View File

@ -7,10 +7,10 @@ import androidx.room.PrimaryKey
data class SearchActionEntity( data class SearchActionEntity(
@PrimaryKey val position: Int, @PrimaryKey val position: Int,
val type: String, val type: String,
val data: String, val data: String? = null,
val label: String?, val label: String? = null,
val icon: Int, val icon: Int? = null,
val color: Int = 0, val color: Int? = null,
val customIcon: String? = null, val customIcon: String? = null,
val options: String? = null, val options: String? = null,
) )

View File

@ -9,9 +9,19 @@ class Migration_18_19 : Migration(18, 19) {
override fun migrate(database: SupportSQLiteDatabase) { override fun migrate(database: SupportSQLiteDatabase) {
val websearches = val websearches =
database.query("SELECT label, urlTemplate, color, icon, encoding FROM `Websearch` ORDER BY label ASC") database.query("SELECT label, urlTemplate, color, icon, encoding FROM `Websearch` ORDER BY label ASC")
database.execSQL("CREATE TABLE IF NOT EXISTS `SearchAction` (`position` INTEGER NOT NULL, `type` TEXT NOT NULL, `data` TEXT NOT NULL, `label` TEXT, `icon` INTEGER NOT NULL, `color` INTEGER NOT NULL, `customIcon` TEXT, `options` TEXT, PRIMARY KEY(`position`))" database.execSQL("CREATE TABLE IF NOT EXISTS `SearchAction` (`position` INTEGER NOT NULL, `type` TEXT NOT NULL, `data` TEXT, `label` TEXT, `icon` INTEGER, `color` INTEGER, `customIcon` TEXT, `options` TEXT, PRIMARY KEY(`position`))"
) )
var position = 0 database.execSQL("INSERT INTO `SearchAction` (`position`, `type`) VALUES" +
"(0, 'call')," +
"(1, 'message')," +
"(2, 'email')," +
"(3, 'contact')," +
"(4, 'alarm')," +
"(5, 'timer')," +
"(6, 'calendar')," +
"(7, 'website')"
)
var position = 8
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)

View File

@ -22,7 +22,7 @@ internal val Context.dataStore: LauncherDataStore by dataStore(
} }
) )
internal const val SchemaVersion = 12 internal const val SchemaVersion = 11
internal fun getMigrations(context: Context): List<DataMigration<Settings>> { internal fun getMigrations(context: Context): List<DataMigration<Settings>> {
return listOf( return listOf(
@ -37,6 +37,5 @@ internal fun getMigrations(context: Context): List<DataMigration<Settings>> {
Migration_8_9(), Migration_8_9(),
Migration_9_10(), Migration_9_10(),
Migration_10_11(), Migration_10_11(),
Migration_11_12(),
) )
} }

View File

@ -162,17 +162,6 @@ fun createFactorySettings(context: Context): Settings {
Settings.WidgetSettings.newBuilder() Settings.WidgetSettings.newBuilder()
.setEditButton(true) .setEditButton(true)
) )
.setSearchActions(
Settings.SearchActionSettings.newBuilder()
.setCall(true)
.setContact(true)
.setEmail(true)
.setMessage(true)
.setOpenUrl(true)
.setScheduleEvent(true)
.setSetAlarm(true)
.setStartTimer(true)
)
.build() .build()
} }

View File

@ -288,16 +288,4 @@ message Settings {
bool edit_button = 1; bool edit_button = 1;
} }
WidgetSettings widgets = 26; WidgetSettings widgets = 26;
message SearchActionSettings {
bool call = 1;
bool message = 2;
bool email = 3;
bool contact = 4;
bool open_url = 5;
bool schedule_event = 6;
bool set_alarm = 7;
bool start_timer = 8;
}
SearchActionSettings search_actions = 27;
} }

View File

@ -5,10 +5,21 @@ import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.database.AppDatabase
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
import de.mm20.launcher2.searchactions.builders.CallActionBuilder
import de.mm20.launcher2.searchactions.builders.CreateContactActionBuilder
import de.mm20.launcher2.searchactions.builders.EmailActionBuilder
import de.mm20.launcher2.searchactions.builders.MessageActionBuilder
import de.mm20.launcher2.searchactions.builders.OpenUrlActionBuilder
import de.mm20.launcher2.searchactions.builders.ScheduleEventActionBuilder
import de.mm20.launcher2.searchactions.builders.SearchActionBuilder import de.mm20.launcher2.searchactions.builders.SearchActionBuilder
import de.mm20.launcher2.searchactions.builders.SetAlarmActionBuilder
import de.mm20.launcher2.searchactions.builders.TimerActionBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.json.JSONArray import org.json.JSONArray
import org.json.JSONException import org.json.JSONException
@ -17,6 +28,9 @@ import java.util.UUID
interface SearchActionRepository { interface SearchActionRepository {
fun getSearchActionBuilders(): Flow<List<SearchActionBuilder>> fun getSearchActionBuilders(): Flow<List<SearchActionBuilder>>
fun getBuiltinSearchActionBuilders(): List<SearchActionBuilder>
fun saveSearchActionBuilders(builders: List<SearchActionBuilder>)
suspend fun export(toDir: File) suspend fun export(toDir: File)
suspend fun import(fromDir: File) suspend fun import(fromDir: File)
@ -26,9 +40,35 @@ internal class SearchActionRepositoryImpl(
private val context: Context, private val context: Context,
private val database: AppDatabase private val database: AppDatabase
): SearchActionRepository { ): SearchActionRepository {
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
override fun getSearchActionBuilders(): Flow<List<SearchActionBuilder>> { override fun getSearchActionBuilders(): Flow<List<SearchActionBuilder>> {
val dao = database.searchActionDao() val dao = database.searchActionDao()
return dao.getSearchActions().map { it.mapNotNull { SearchActionBuilder.from(it) } } return dao.getSearchActions().map { it.mapNotNull { SearchActionBuilder.from(context, it) } }
}
override fun getBuiltinSearchActionBuilders(): List<SearchActionBuilder> {
val allActions = listOf(
CallActionBuilder(context),
MessageActionBuilder(context),
CreateContactActionBuilder(context),
EmailActionBuilder(context),
ScheduleEventActionBuilder(context),
SetAlarmActionBuilder(context),
TimerActionBuilder(context),
OpenUrlActionBuilder(context),
)
return allActions
}
override fun saveSearchActionBuilders(builders: List<SearchActionBuilder>) {
scope.launch {
val dao = database.searchActionDao()
dao.replaceAll(
builders.mapIndexed { i, it -> SearchActionBuilder.toDatabaseEntity(it, i) }
)
}
} }
override suspend fun export(toDir: File) = withContext(Dispatchers.IO) { override suspend fun export(toDir: File) = withContext(Dispatchers.IO) {

View File

@ -2,13 +2,9 @@ package de.mm20.launcher2.searchactions
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.LauncherActivityInfo
import android.content.pm.LauncherApps
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo import android.content.pm.ResolveInfo
import android.graphics.Bitmap import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.os.UserHandle
import android.util.Log import android.util.Log
import android.util.Xml import android.util.Xml
import androidx.core.graphics.drawable.toBitmap import androidx.core.graphics.drawable.toBitmap
@ -16,20 +12,10 @@ import coil.imageLoader
import coil.request.ImageRequest import coil.request.ImageRequest
import coil.size.Scale import coil.size.Scale
import de.mm20.launcher2.crashreporter.CrashReporter import de.mm20.launcher2.crashreporter.CrashReporter
import de.mm20.launcher2.database.entities.WebsearchEntity
import de.mm20.launcher2.ktx.jsonObjectOf
import de.mm20.launcher2.preferences.Settings.SearchActionSettings
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.CallActionBuilder
import de.mm20.launcher2.searchactions.builders.CreateContactActionBuilder
import de.mm20.launcher2.searchactions.builders.EmailActionBuilder
import de.mm20.launcher2.searchactions.builders.MessageActionBuilder
import de.mm20.launcher2.searchactions.builders.OpenUrlActionBuilder
import de.mm20.launcher2.searchactions.builders.ScheduleEventActionBuilder
import de.mm20.launcher2.searchactions.builders.SearchActionBuilder import de.mm20.launcher2.searchactions.builders.SearchActionBuilder
import de.mm20.launcher2.searchactions.builders.SetAlarmActionBuilder
import de.mm20.launcher2.searchactions.builders.TimerActionBuilder
import de.mm20.launcher2.searchactions.builders.WebsearchActionBuilder import de.mm20.launcher2.searchactions.builders.WebsearchActionBuilder
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
@ -42,8 +28,6 @@ import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.json.JSONArray
import org.json.JSONException
import org.jsoup.Jsoup import org.jsoup.Jsoup
import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException import org.xmlpull.v1.XmlPullParserException
@ -54,12 +38,18 @@ import java.net.URL
import java.util.UUID import java.util.UUID
interface SearchActionService { interface SearchActionService {
fun search(settings: SearchActionSettings, query: String): Flow<ImmutableList<SearchAction>> fun search(query: String): Flow<ImmutableList<SearchAction>>
fun getSearchActionBuilders(): Flow<List<SearchActionBuilder>>
fun getDisabledActionBuilders(): Flow<List<SearchActionBuilder>>
fun saveSearchActionBuilders(builders: List<SearchActionBuilder>)
suspend fun importWebsearch(url: String, iconSize: Int): WebsearchActionBuilder? suspend fun importWebsearch(url: String, iconSize: Int): WebsearchActionBuilder?
suspend fun createIcon(uri: Uri, size: Int): String?
suspend fun getSearchActivities(): List<ResolveInfo> suspend fun getSearchActivities(): List<ResolveInfo>
suspend fun createIcon(uri: Uri, size: Int): String?
} }
internal class SearchActionServiceImpl( internal class SearchActionServiceImpl(
@ -68,7 +58,6 @@ internal class SearchActionServiceImpl(
private val textClassifier: TextClassifier, private val textClassifier: TextClassifier,
) : SearchActionService { ) : SearchActionService {
override fun search( override fun search(
settings: SearchActionSettings,
query: String query: String
): Flow<ImmutableList<SearchAction>> = flow { ): Flow<ImmutableList<SearchAction>> = flow {
if (query.isBlank()) { if (query.isBlank()) {
@ -76,28 +65,33 @@ internal class SearchActionServiceImpl(
return@flow return@flow
} }
val builders = mutableListOf<SearchActionBuilder>()
if (settings.call) builders.add(CallActionBuilder)
if (settings.message) builders.add(MessageActionBuilder)
if (settings.contact) builders.add(CreateContactActionBuilder)
if (settings.email) builders.add(EmailActionBuilder)
if (settings.openUrl) builders.add(OpenUrlActionBuilder)
if (settings.scheduleEvent) builders.add(ScheduleEventActionBuilder)
if (settings.setAlarm) builders.add(SetAlarmActionBuilder)
if (settings.startTimer) builders.add(TimerActionBuilder)
val classificationResult = textClassifier.classify(context, query) val classificationResult = textClassifier.classify(context, query)
val other = repository.getSearchActionBuilders() val builders = repository.getSearchActionBuilders()
emitAll( emitAll(
other.map { builders.map {
(builders + it).mapNotNull { it.build(context, classificationResult) }.toImmutableList() it.mapNotNull { it.build(context, classificationResult) }.toImmutableList()
} }
) )
} }
override fun getSearchActionBuilders(): Flow<List<SearchActionBuilder>> {
return repository.getSearchActionBuilders()
}
override fun getDisabledActionBuilders(): Flow<List<SearchActionBuilder>> {
val allActions = repository.getBuiltinSearchActionBuilders()
return getSearchActionBuilders().map { enabled ->
allActions.filter { action -> !enabled.any { it.key == action.key } }
}
}
override fun saveSearchActionBuilders(builders: List<SearchActionBuilder>) {
repository.saveSearchActionBuilders(builders)
}
override suspend fun importWebsearch(url: String, iconSize: Int): WebsearchActionBuilder? = override suspend fun importWebsearch(url: String, iconSize: Int): WebsearchActionBuilder? =
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
try { try {

View File

@ -1,21 +1,27 @@
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.pm.LauncherActivityInfo
import de.mm20.launcher2.searchactions.TextClassificationResult import de.mm20.launcher2.searchactions.TextClassificationResult
import de.mm20.launcher2.searchactions.TextType import de.mm20.launcher2.searchactions.TextType
import de.mm20.launcher2.searchactions.actions.AppSearchAction 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
class AppSearchActionBuilder( class AppSearchActionBuilder(
val label: String, override val label: String,
val activity: LauncherActivityInfo, val componentName: ComponentName,
val filter: TextType? = null, override val icon: SearchActionIcon = SearchActionIcon.Search,
) : SearchActionBuilder { override val iconColor: Int = 0,
override val customIcon: String? = null,
) : CustomizableSearchActionBuilder {
override val key: String
get() = "app://${componentName.flattenToShortString()}"
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? { override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
return AppSearchAction( return AppSearchAction(
label = label, label = label,
componentName = activity.componentName, componentName = componentName,
query = classifiedQuery.text, query = classifiedQuery.text,
) )
} }

View File

@ -5,8 +5,18 @@ import de.mm20.launcher2.searchactions.R
import de.mm20.launcher2.searchactions.actions.SearchAction import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.searchactions.TextClassificationResult import de.mm20.launcher2.searchactions.TextClassificationResult
import de.mm20.launcher2.searchactions.actions.CallAction import de.mm20.launcher2.searchactions.actions.CallAction
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
object CallActionBuilder: SearchActionBuilder { class CallActionBuilder(
override val label: String
): SearchActionBuilder {
constructor(context: Context): this(context.getString(R.string.search_action_call))
override val key: String
get() = "call"
override val icon: SearchActionIcon = SearchActionIcon.Phone
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? { override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
if (classifiedQuery.phoneNumber != null) { if (classifiedQuery.phoneNumber != null) {

View File

@ -5,8 +5,18 @@ import de.mm20.launcher2.searchactions.R
import de.mm20.launcher2.searchactions.TextClassificationResult import de.mm20.launcher2.searchactions.TextClassificationResult
import de.mm20.launcher2.searchactions.actions.CreateContactAction import de.mm20.launcher2.searchactions.actions.CreateContactAction
import de.mm20.launcher2.searchactions.actions.SearchAction import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
object CreateContactActionBuilder : SearchActionBuilder { class CreateContactActionBuilder(
override val label: String
) : SearchActionBuilder {
constructor(context: Context) : this(context.getString(R.string.search_action_contact))
override val key: String
get() = "contact"
override val icon: SearchActionIcon = SearchActionIcon.Contact
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? { override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
if (classifiedQuery.phoneNumber != null || classifiedQuery.email != null) { if (classifiedQuery.phoneNumber != null || classifiedQuery.email != null) {

View File

@ -0,0 +1,3 @@
package de.mm20.launcher2.searchactions.builders
sealed interface CustomizableSearchActionBuilder: SearchActionBuilder

View File

@ -5,8 +5,18 @@ import de.mm20.launcher2.searchactions.R
import de.mm20.launcher2.searchactions.TextClassificationResult import de.mm20.launcher2.searchactions.TextClassificationResult
import de.mm20.launcher2.searchactions.actions.EmailAction import de.mm20.launcher2.searchactions.actions.EmailAction
import de.mm20.launcher2.searchactions.actions.SearchAction import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
object EmailActionBuilder: SearchActionBuilder { class EmailActionBuilder(
override val label: String
): SearchActionBuilder {
constructor(context: Context) : this(context.getString(R.string.search_action_email))
override val key: String
get() = "email"
override val icon: SearchActionIcon = SearchActionIcon.Email
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? { override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
if (classifiedQuery.email != null) { if (classifiedQuery.email != null) {

View File

@ -5,8 +5,18 @@ import de.mm20.launcher2.searchactions.R
import de.mm20.launcher2.searchactions.TextClassificationResult import de.mm20.launcher2.searchactions.TextClassificationResult
import de.mm20.launcher2.searchactions.actions.MessageAction import de.mm20.launcher2.searchactions.actions.MessageAction
import de.mm20.launcher2.searchactions.actions.SearchAction import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
object MessageActionBuilder: SearchActionBuilder { class MessageActionBuilder(
override val label: String
): SearchActionBuilder {
constructor(context: Context) : this(context.getString(R.string.search_action_message))
override val key: String
get() = "message"
override val icon: SearchActionIcon = SearchActionIcon.Message
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? { override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
if (classifiedQuery.phoneNumber != null) { if (classifiedQuery.phoneNumber != null) {

View File

@ -6,8 +6,18 @@ import de.mm20.launcher2.searchactions.TextClassificationResult
import de.mm20.launcher2.searchactions.actions.MessageAction import de.mm20.launcher2.searchactions.actions.MessageAction
import de.mm20.launcher2.searchactions.actions.OpenUrlAction import de.mm20.launcher2.searchactions.actions.OpenUrlAction
import de.mm20.launcher2.searchactions.actions.SearchAction import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
object OpenUrlActionBuilder : SearchActionBuilder { class OpenUrlActionBuilder(
override val label: String
) : SearchActionBuilder {
constructor(context: Context) : this(context.getString(R.string.search_action_open_url))
override val key: String
get() = "website"
override val icon: SearchActionIcon = SearchActionIcon.Website
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? { override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
if (classifiedQuery.url != null) { if (classifiedQuery.url != null) {

View File

@ -6,9 +6,19 @@ import de.mm20.launcher2.searchactions.TextClassificationResult
import de.mm20.launcher2.searchactions.actions.MessageAction import de.mm20.launcher2.searchactions.actions.MessageAction
import de.mm20.launcher2.searchactions.actions.ScheduleEventAction import de.mm20.launcher2.searchactions.actions.ScheduleEventAction
import de.mm20.launcher2.searchactions.actions.SearchAction import de.mm20.launcher2.searchactions.actions.SearchAction
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
import java.time.LocalDateTime import java.time.LocalDateTime
object ScheduleEventActionBuilder : SearchActionBuilder { class ScheduleEventActionBuilder(
override val label: String
) : SearchActionBuilder {
constructor(context: Context) : this(context.getString(R.string.search_action_event))
override val key: String
get() = "calendar"
override val icon: SearchActionIcon = SearchActionIcon.Calendar
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? { override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
if (classifiedQuery.date != null) { if (classifiedQuery.date != null) {

View File

@ -1,7 +1,9 @@
package de.mm20.launcher2.searchactions.builders package de.mm20.launcher2.searchactions.builders
import android.content.Context import android.content.Context
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.searchactions.TextClassificationResult import de.mm20.launcher2.searchactions.TextClassificationResult
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
@ -9,10 +11,18 @@ import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
interface SearchActionBuilder { interface SearchActionBuilder {
val label: String
val icon: SearchActionIcon
val iconColor: Int
get() = 0
val customIcon: String?
get() = null
val key: String
fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction?
companion object { companion object {
fun from(entity: SearchActionEntity): SearchActionBuilder? { internal fun from(context: Context, entity: SearchActionEntity): SearchActionBuilder? {
val options = entity.options?.let { val options = entity.options?.let {
try { try {
JSONObject(it) JSONObject(it)
@ -24,8 +34,8 @@ interface SearchActionBuilder {
"url" -> { "url" -> {
return WebsearchActionBuilder( return WebsearchActionBuilder(
label = entity.label ?: "", label = entity.label ?: "",
urlTemplate = entity.data, urlTemplate = entity.data ?: return null,
color = entity.color, iconColor = entity.color ?: 0,
icon = SearchActionIcon.fromInt(entity.icon), icon = SearchActionIcon.fromInt(entity.icon),
customIcon = entity.customIcon, customIcon = entity.customIcon,
encoding = WebsearchActionBuilder.QueryEncoding.fromInt(options?.optInt("encoding")) encoding = WebsearchActionBuilder.QueryEncoding.fromInt(options?.optInt("encoding"))
@ -34,9 +44,38 @@ interface SearchActionBuilder {
"app" -> { "app" -> {
return null return null
} }
"call" -> return CallActionBuilder(context)
"message" -> return MessageActionBuilder(context)
"email" -> return EmailActionBuilder(context)
"contact" -> return CreateContactActionBuilder(context)
"alarm" -> return SetAlarmActionBuilder(context)
"timer" -> return TimerActionBuilder(context)
"calendar" -> return ScheduleEventActionBuilder(context)
"website" -> return OpenUrlActionBuilder(context)
else -> return null else -> return null
} }
} }
}
internal fun toDatabaseEntity(builder: SearchActionBuilder, position: Int): SearchActionEntity {
return when(builder) {
is WebsearchActionBuilder -> SearchActionEntity(
position = position,
type = "url",
label = builder.label,
data = builder.urlTemplate,
color = builder.iconColor,
icon = builder.icon.toInt(),
customIcon = builder.customIcon,
options = jsonObjectOf(
"encoding" to builder.encoding.toInt()
).toString()
)
//is AppSearchActionBuilder -> null
else -> SearchActionEntity(
position = position,
type = builder.key,
)
}
}
}
} }

View File

@ -4,10 +4,20 @@ import android.content.Context
import de.mm20.launcher2.searchactions.R import de.mm20.launcher2.searchactions.R
import de.mm20.launcher2.searchactions.TextClassificationResult import de.mm20.launcher2.searchactions.TextClassificationResult
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.SetAlarmAction import de.mm20.launcher2.searchactions.actions.SetAlarmAction
import java.time.LocalDate import java.time.LocalDate
object SetAlarmActionBuilder : SearchActionBuilder { class SetAlarmActionBuilder(
override val label: String
) : SearchActionBuilder {
constructor(context: Context) : this(context.getString(R.string.search_action_alarm))
override val key: String
get() = "alarm"
override val icon: SearchActionIcon = SearchActionIcon.Alarm
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? { override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
if (classifiedQuery.time != null) { if (classifiedQuery.time != null) {

View File

@ -3,10 +3,19 @@ package de.mm20.launcher2.searchactions.builders
import android.content.Context import android.content.Context
import de.mm20.launcher2.searchactions.R import de.mm20.launcher2.searchactions.R
import de.mm20.launcher2.searchactions.TextClassificationResult import de.mm20.launcher2.searchactions.TextClassificationResult
import de.mm20.launcher2.searchactions.actions.TimerAction
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.TimerAction
object TimerActionBuilder : SearchActionBuilder { class TimerActionBuilder(
override val label: String,
) : SearchActionBuilder {
constructor(context: Context) : this(context.getString(R.string.search_action_timer))
override val key: String
get() = "timer"
override val icon: SearchActionIcon = SearchActionIcon.Timer
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? { override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
if (classifiedQuery.timespan != null && classifiedQuery.timespan.seconds <= 86400) { if (classifiedQuery.timespan != null && classifiedQuery.timespan.seconds <= 86400) {

View File

@ -9,13 +9,16 @@ import de.mm20.launcher2.searchactions.actions.SearchActionIcon
import java.net.URLEncoder import java.net.URLEncoder
class WebsearchActionBuilder( class WebsearchActionBuilder(
val label: String, override val label: String,
val urlTemplate: String, val urlTemplate: String,
val icon: SearchActionIcon = SearchActionIcon.Search, override val icon: SearchActionIcon = SearchActionIcon.Search,
val color: Int = 0, override val iconColor: Int = 0,
val customIcon: String? = null, override val customIcon: String? = null,
val encoding: QueryEncoding = QueryEncoding.UrlEncode, val encoding: QueryEncoding = QueryEncoding.UrlEncode,
) : SearchActionBuilder { ) : CustomizableSearchActionBuilder {
override val key: String
get() = "web://$urlTemplate"
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction { override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction {
val url = urlTemplate.replace("\${1}", encodeQuery(classifiedQuery.text, encoding)) val url = urlTemplate.replace("\${1}", encodeQuery(classifiedQuery.text, encoding))
@ -24,7 +27,7 @@ class WebsearchActionBuilder(
url = url, url = url,
icon = icon, icon = icon,
customIcon = customIcon, customIcon = customIcon,
iconColor = color, iconColor = iconColor,
) )
} }

View File

@ -13,7 +13,6 @@ import de.mm20.launcher2.preferences.Settings.CalculatorSearchSettings
import de.mm20.launcher2.preferences.Settings.CalendarSearchSettings import de.mm20.launcher2.preferences.Settings.CalendarSearchSettings
import de.mm20.launcher2.preferences.Settings.ContactsSearchSettings import de.mm20.launcher2.preferences.Settings.ContactsSearchSettings
import de.mm20.launcher2.preferences.Settings.FilesSearchSettings import de.mm20.launcher2.preferences.Settings.FilesSearchSettings
import de.mm20.launcher2.preferences.Settings.SearchActionSettings
import de.mm20.launcher2.preferences.Settings.UnitConverterSearchSettings import de.mm20.launcher2.preferences.Settings.UnitConverterSearchSettings
import de.mm20.launcher2.preferences.Settings.WebsiteSearchSettings import de.mm20.launcher2.preferences.Settings.WebsiteSearchSettings
import de.mm20.launcher2.preferences.Settings.WikipediaSearchSettings import de.mm20.launcher2.preferences.Settings.WikipediaSearchSettings
@ -59,7 +58,6 @@ interface SearchService {
unitConverter: UnitConverterSearchSettings, unitConverter: UnitConverterSearchSettings,
websites: WebsiteSearchSettings, websites: WebsiteSearchSettings,
wikipedia: WikipediaSearchSettings, wikipedia: WikipediaSearchSettings,
searchActions: SearchActionSettings,
): Flow<ImmutableList<Searchable>> ): Flow<ImmutableList<Searchable>>
} }
@ -87,7 +85,6 @@ internal class SearchServiceImpl(
unitConverter: UnitConverterSearchSettings, unitConverter: UnitConverterSearchSettings,
websites: WebsiteSearchSettings, websites: WebsiteSearchSettings,
wikipedia: WikipediaSearchSettings, wikipedia: WikipediaSearchSettings,
searchActions: SearchActionSettings,
): Flow<ImmutableList<Searchable>> = channelFlow { ): Flow<ImmutableList<Searchable>> = channelFlow {
supervisorScope { supervisorScope {
val results = MutableStateFlow(SearchResults()) val results = MutableStateFlow(SearchResults())
@ -220,7 +217,7 @@ internal class SearchServiceImpl(
} }
} }
launch { launch {
searchActionService.search(searchActions, query) searchActionService.search(query)
.collectLatest { r -> .collectLatest { r ->
results.update { results.update {
it.copy(searchActions = r) it.copy(searchActions = r)

View File

@ -0,0 +1,54 @@
package de.mm20.launcher2.ui.component
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Alarm
import androidx.compose.material.icons.rounded.Call
import androidx.compose.material.icons.rounded.Email
import androidx.compose.material.icons.rounded.Event
import androidx.compose.material.icons.rounded.Language
import androidx.compose.material.icons.rounded.Person
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material.icons.rounded.Sms
import androidx.compose.material.icons.rounded.Timer
import androidx.compose.material.icons.rounded.Translate
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
@Composable
fun SearchActionIcon(
icon: SearchActionIcon,
color: Int,
customIcon: String? = null
) {
val tint = when(color) {
0 -> MaterialTheme.colorScheme.primary
1 -> Color.Unspecified
else -> Color(color)
}
if (icon != SearchActionIcon.Custom) {
Icon(
imageVector = getSearchActionIconVector(icon),
contentDescription = null,
tint = tint,
)
}
}
fun getSearchActionIconVector(icon: SearchActionIcon): ImageVector {
return when (icon) {
SearchActionIcon.Phone -> Icons.Rounded.Call
SearchActionIcon.Website -> Icons.Rounded.Language
SearchActionIcon.Alarm -> Icons.Rounded.Alarm
SearchActionIcon.Timer -> Icons.Rounded.Timer
SearchActionIcon.Contact -> Icons.Rounded.Person
SearchActionIcon.Email -> Icons.Rounded.Email
SearchActionIcon.Message -> Icons.Rounded.Sms
SearchActionIcon.Calendar -> Icons.Rounded.Event
SearchActionIcon.Translate -> Icons.Rounded.Translate
else -> Icons.Rounded.Search
}
}

View File

@ -88,7 +88,7 @@ data class LazyDragAndDropListState(
fun startDrag(offset: Offset): Boolean { fun startDrag(offset: Offset): Boolean {
val draggedItem = listState.layoutInfo.visibleItemsInfo.find { val draggedItem = listState.layoutInfo.visibleItemsInfo.find {
Rect( Rect(
it.offset.toOffset(), (it.offset + listState.layoutInfo.beforeContentPadding).toOffset(),
it.size.toSize() it.size.toSize()
).contains(offset) ).contains(offset)
} ?: return false } ?: return false
@ -240,13 +240,16 @@ fun LazyDragAndDropRow(
verticalAlignment: Alignment.Vertical = Alignment.Top, verticalAlignment: Alignment.Vertical = Alignment.Top,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true, userScrollEnabled: Boolean = true,
bidirectionalDrag: Boolean = true,
content: LazyListScope.() -> Unit content: LazyListScope.() -> Unit
) { ) {
LazyRow( LazyRow(
modifier = modifier.dragAndDrop( modifier = modifier.dragAndDrop(
state, state,
LocalLayoutDirection.current == LayoutDirection.Rtl, isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl,
LocalHapticFeedback.current hapticFeedback = LocalHapticFeedback.current,
dragVertical = bidirectionalDrag,
dragHorizontal = true,
), ),
state = state.listState, state = state.listState,
contentPadding = contentPadding, contentPadding = contentPadding,
@ -269,13 +272,16 @@ fun LazyDragAndDropColumn(
horizontalAlignment: Alignment.Horizontal = Alignment.Start, horizontalAlignment: Alignment.Horizontal = Alignment.Start,
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(), flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
userScrollEnabled: Boolean = true, userScrollEnabled: Boolean = true,
bidirectionalDrag: Boolean = true,
content: LazyListScope.() -> Unit content: LazyListScope.() -> Unit
) { ) {
LazyColumn( LazyColumn(
modifier = modifier.dragAndDrop( modifier = modifier.dragAndDrop(
state, state,
LocalLayoutDirection.current == LayoutDirection.Rtl, isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl,
LocalHapticFeedback.current hapticFeedback = LocalHapticFeedback.current,
dragVertical = true,
dragHorizontal = bidirectionalDrag,
), ),
state = state.listState, state = state.listState,
contentPadding = contentPadding, contentPadding = contentPadding,
@ -291,6 +297,8 @@ fun LazyDragAndDropColumn(
fun Modifier.dragAndDrop( fun Modifier.dragAndDrop(
state: LazyDragAndDropListState, state: LazyDragAndDropListState,
isRtl: Boolean, isRtl: Boolean,
dragVertical: Boolean = true,
dragHorizontal: Boolean = true,
hapticFeedback: HapticFeedback hapticFeedback: HapticFeedback
) = ) =
this then pointerInput(null) { this then pointerInput(null) {
@ -302,9 +310,18 @@ fun Modifier.dragAndDrop(
} }
}, },
onDrag = { _, dragAmount -> onDrag = { _, dragAmount ->
scope.launch { state.drag(dragAmount.let { scope.launch {
if (isRtl) it.copy(x = -it.x) else it state.drag(
}) } when {
!dragVertical && !dragHorizontal -> Offset.Zero
dragVertical && !dragHorizontal -> Offset(0f, dragAmount.y)
!dragVertical && dragHorizontal && isRtl -> Offset(-dragAmount.x, 0f)
!dragVertical && dragHorizontal && !isRtl -> Offset(dragAmount.x, 0f)
dragVertical && dragHorizontal && isRtl -> Offset(-dragAmount.x, dragAmount.y)
else -> dragAmount
}
)
}
}, },
onDragCancel = { onDragCancel = {
scope.launch { state.cancelDrag() } scope.launch { state.cancelDrag() }

View File

@ -95,7 +95,6 @@ class SearchVM : ViewModel(), KoinComponent {
shortcuts = it.appShortcutSearch, shortcuts = it.appShortcutSearch,
websites = it.websiteSearch, websites = it.websiteSearch,
wikipedia = it.wikipediaSearch, wikipedia = it.wikipediaSearch,
searchActions = it.searchActions,
).collectLatest { results -> ).collectLatest { results ->
hiddenItemKeys.collectLatest { hiddenKeys -> hiddenItemKeys.collectLatest { hiddenKeys ->
val hidden = mutableListOf<SavableSearchable>() val hidden = mutableListOf<SavableSearchable>()

View File

@ -6,17 +6,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Alarm
import androidx.compose.material.icons.rounded.Call
import androidx.compose.material.icons.rounded.Email
import androidx.compose.material.icons.rounded.Event
import androidx.compose.material.icons.rounded.Language
import androidx.compose.material.icons.rounded.Person
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material.icons.rounded.Sms
import androidx.compose.material.icons.rounded.Timer
import androidx.compose.material.icons.rounded.Translate
import androidx.compose.material3.AssistChip import androidx.compose.material3.AssistChip
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -28,7 +17,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
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.ui.component.SearchActionIcon
@Composable @Composable
fun SearchBarActions( fun SearchBarActions(
@ -53,27 +42,7 @@ fun SearchBarActions(
}, },
label = { Text(it.label) }, label = { Text(it.label) },
leadingIcon = { leadingIcon = {
val icon = it.icon SearchActionIcon(icon = it.icon, color = it.iconColor, customIcon = it.customIcon)
if (it.icon != SearchActionIcon.Custom) {
Icon(
imageVector = when (it.icon) {
SearchActionIcon.Phone -> Icons.Rounded.Call
SearchActionIcon.Website -> Icons.Rounded.Language
SearchActionIcon.Alarm -> Icons.Rounded.Alarm
SearchActionIcon.Timer -> Icons.Rounded.Timer
SearchActionIcon.Contact -> Icons.Rounded.Person
SearchActionIcon.Email -> Icons.Rounded.Email
SearchActionIcon.Message -> Icons.Rounded.Sms
SearchActionIcon.Calendar -> Icons.Rounded.Event
SearchActionIcon.Translate -> Icons.Rounded.Translate
else -> Icons.Rounded.Search
},
contentDescription = null,
tint = if (it.iconColor == 0) MaterialTheme.colorScheme.primary else Color(
it.iconColor
)
)
}
} }
/*leadingIcon = { /*leadingIcon = {
val icon = it.icon val icon = it.icon

View File

@ -1,115 +1,166 @@
package de.mm20.launcher2.ui.settings.searchactions package de.mm20.launcher2.ui.settings.searchactions
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Alarm import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.CalendarToday import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.Call import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material.icons.rounded.Email import androidx.compose.material3.FloatingActionButton
import androidx.compose.material.icons.rounded.Event import androidx.compose.material3.Icon
import androidx.compose.material.icons.rounded.Language import androidx.compose.material3.IconButton
import androidx.compose.material.icons.rounded.Person import androidx.compose.material3.MaterialTheme
import androidx.compose.material.icons.rounded.Sms import androidx.compose.material3.Scaffold
import androidx.compose.material.icons.rounded.Timer import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import de.mm20.launcher2.preferences.Settings import com.google.accompanist.systemuicontroller.rememberSystemUiController
import de.mm20.launcher2.searchactions.builders.CustomizableSearchActionBuilder
import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory import de.mm20.launcher2.ui.component.SearchActionIcon
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen import de.mm20.launcher2.ui.component.getSearchActionIconVector
import de.mm20.launcher2.ui.component.preferences.Preference
import de.mm20.launcher2.ui.component.preferences.SwitchPreference import de.mm20.launcher2.ui.component.preferences.SwitchPreference
import de.mm20.launcher2.ui.launcher.helper.DraggableItem
import de.mm20.launcher2.ui.launcher.helper.LazyDragAndDropColumn
import de.mm20.launcher2.ui.launcher.helper.rememberLazyDragAndDropListState
import de.mm20.launcher2.ui.locals.LocalNavController
@Composable @Composable
fun SearchActionsSettingsScreen() { fun SearchActionsSettingsScreen() {
val viewModel: SearchActionsSettingsScreenVM = viewModel() val viewModel: SearchActionsSettingsScreenVM = viewModel()
val settings by viewModel.searchActionSettings.observeAsState( val navController = LocalNavController.current
Settings.SearchActionSettings.getDefaultInstance() val systemUiController = rememberSystemUiController()
systemUiController.setStatusBarColor(MaterialTheme.colorScheme.surface)
systemUiController.setNavigationBarColor(Color.Black)
val context = LocalContext.current
val colorScheme = MaterialTheme.colorScheme
val activity = LocalContext.current as? AppCompatActivity
val listState = rememberLazyDragAndDropListState(
onDragStart = {
it.key != "divider" && !(it.key as String).startsWith("disabled-")
},
onItemMove = { from, to -> viewModel.moveItem(from.index, to.index) }
) )
PreferenceScreen(stringResource(id = R.string.preference_screen_search_actions)) { val searchActions by viewModel.searchActions.observeAsState(emptyList())
item { val disabledActions by viewModel.disabledActions.observeAsState(emptyList())
PreferenceCategory {
SwitchPreference( Scaffold(
icon = Icons.Rounded.Call, floatingActionButton = {
title = stringResource(R.string.search_action_call), FloatingActionButton(onClick = { /*TODO*/ }) {
value = settings.call, Icon(imageVector = Icons.Rounded.Add, contentDescription = null)
onValueChanged = { }
viewModel.updateSettings { },
setCall(it) topBar = {
CenterAlignedTopAppBar(
title = {
Text(
stringResource(id = R.string.preference_screen_search_actions),
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(horizontal = 16.dp),
maxLines = 1
)
},
navigationIcon = {
IconButton(onClick = {
if (navController?.navigateUp() != true) {
activity?.onBackPressed()
} }
}, }) {
Icon(imageVector = Icons.Rounded.ArrowBack, contentDescription = "Back")
}
},
)
}) {
LazyDragAndDropColumn(
state = listState,
bidirectionalDrag = false,
contentPadding = it,
modifier = Modifier
.fillMaxSize()
) {
items(
items = searchActions,
key = { it.key }
) { item ->
DraggableItem(
state = listState,
key = item.key
) {
val elevation by animateDpAsState(if (it) 4.dp else 0.dp)
Surface(
shadowElevation = elevation,
tonalElevation = elevation,
modifier = Modifier.zIndex(if (it) 1f else 0f)
) {
if (item is CustomizableSearchActionBuilder) {
Preference(
icon = {
SearchActionIcon(
icon = item.icon,
color = item.iconColor,
customIcon = item.customIcon
)
},
title = item.label
)
} else {
SwitchPreference(
icon = getSearchActionIconVector(item.icon),
title = item.label,
value = true,
onValueChanged = {
viewModel.removeAction(item)
}
)
}
}
}
}
item(key = "divider") {
Box(
modifier = Modifier
.fillMaxWidth()
.height(0.5.dp)
.background(
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)
)
) )
}
items(
items = disabledActions,
key = { "disabled-${it.key}" }
) { item ->
SwitchPreference( SwitchPreference(
icon = Icons.Rounded.Sms, icon = getSearchActionIconVector(item.icon),
title = stringResource(R.string.search_action_message), title = item.label,
value = settings.message, value = false,
onValueChanged = { onValueChanged = {
viewModel.updateSettings { viewModel.addAction(item)
setMessage(it) }
}
},
)
SwitchPreference(
icon = Icons.Rounded.Email,
title = stringResource(R.string.search_action_email),
value = settings.email,
onValueChanged = {
viewModel.updateSettings {
setEmail(it)
}
},
)
SwitchPreference(
icon = Icons.Rounded.Person,
title = stringResource(R.string.search_action_contact),
value = settings.contact,
onValueChanged = {
viewModel.updateSettings {
setContact(it)
}
},
)
SwitchPreference(
icon = Icons.Rounded.Alarm,
title = stringResource(R.string.search_action_alarm),
value = settings.setAlarm,
onValueChanged = {
viewModel.updateSettings {
setSetAlarm(it)
}
},
)
SwitchPreference(
icon = Icons.Rounded.Timer,
title = stringResource(R.string.search_action_timer),
value = settings.startTimer,
onValueChanged = {
viewModel.updateSettings {
setStartTimer(it)
}
},
)
SwitchPreference(
icon = Icons.Rounded.Event,
title = stringResource(R.string.search_action_event),
value = settings.scheduleEvent,
onValueChanged = {
viewModel.updateSettings {
setScheduleEvent(it)
}
},
)
SwitchPreference(
icon = Icons.Rounded.Language,
title = stringResource(R.string.search_action_open_url),
value = settings.openUrl,
onValueChanged = {
viewModel.updateSettings {
setOpenUrl(it)
}
},
) )
} }
} }

View File

@ -2,27 +2,39 @@ package de.mm20.launcher2.ui.settings.searchactions
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.preferences.LauncherDataStore import de.mm20.launcher2.searchactions.builders.SearchActionBuilder
import de.mm20.launcher2.preferences.Settings.SearchActionSettings
import kotlinx.coroutines.flow.map
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
class SearchActionsSettingsScreenVM : ViewModel(), KoinComponent { class SearchActionsSettingsScreenVM : ViewModel(), KoinComponent {
private val dataStore: LauncherDataStore by inject() private val searchActionService: SearchActionService by inject()
val searchActionSettings = dataStore.data.map { it.searchActions }.asLiveData() val searchActions = searchActionService
.getSearchActionBuilders()
.asLiveData()
fun updateSettings(block: SearchActionSettings.Builder.() -> SearchActionSettings.Builder) { val disabledActions = searchActionService
viewModelScope.launch { .getDisabledActionBuilders()
dataStore.updateData { .asLiveData()
it.toBuilder()
.setSearchActions( fun addAction(searchAction: SearchActionBuilder) {
it.searchActions.toBuilder().block() val actions =
).build() searchActions.value?.filter { it.key != searchAction.key }?.plus(searchAction) ?: return
} searchActionService.saveSearchActionBuilders(actions)
} }
fun removeAction(searchAction: SearchActionBuilder) {
val actions = searchActions.value?.filter { it.key != searchAction.key } ?: return
searchActionService.saveSearchActionBuilders(actions)
}
fun moveItem(fromIndex: Int, toIndex: Int) {
val actions = searchActions.value?.toMutableList() ?: return
if (fromIndex > actions.lastIndex) return
if (toIndex > actions.lastIndex) return
val item = actions.removeAt(fromIndex)
actions.add(toIndex, item)
searchActionService.saveSearchActionBuilders(actions)
} }
} }