Make search action order customizable
This commit is contained in:
parent
960c3b70f5
commit
3463b3b800
@ -2,7 +2,7 @@
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 19,
|
||||
"identityHash": "c6adf1dca8ade5117d4e8952a67786fa",
|
||||
"identityHash": "968a73b13f39e00505a4adf1bda01394",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "forecasts",
|
||||
@ -398,7 +398,7 @@
|
||||
},
|
||||
{
|
||||
"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": [
|
||||
{
|
||||
"fieldPath": "position",
|
||||
@ -416,7 +416,7 @@
|
||||
"fieldPath": "data",
|
||||
"columnName": "data",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "label",
|
||||
@ -428,13 +428,13 @@
|
||||
"fieldPath": "icon",
|
||||
"columnName": "icon",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "color",
|
||||
"columnName": "color",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
"notNull": false
|
||||
},
|
||||
{
|
||||
"fieldPath": "customIcon",
|
||||
@ -462,7 +462,7 @@
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"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')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -57,13 +57,23 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
.addCallback(object : Callback() {
|
||||
override fun onCreate(db: SupportSQLiteDatabase) {
|
||||
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`) " +
|
||||
"VALUES (?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?), (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
arrayOf(
|
||||
0, "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,
|
||||
0, "url", context.getString(R.string.default_websearch_3_url), context.getString(R.string.default_websearch_3_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,
|
||||
9, "url", context.getString(R.string.default_websearch_2_url), context.getString(R.string.default_websearch_2_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,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
package de.mm20.launcher2.database
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import de.mm20.launcher2.database.entities.SearchActionEntity
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@ -9,4 +11,16 @@ import kotlinx.coroutines.flow.Flow
|
||||
interface SearchActionDao {
|
||||
@Query("SELECT * FROM SearchAction ORDER BY position ASC")
|
||||
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>)
|
||||
}
|
||||
@ -7,10 +7,10 @@ import androidx.room.PrimaryKey
|
||||
data class SearchActionEntity(
|
||||
@PrimaryKey val position: Int,
|
||||
val type: String,
|
||||
val data: String,
|
||||
val label: String?,
|
||||
val icon: Int,
|
||||
val color: Int = 0,
|
||||
val data: String? = null,
|
||||
val label: String? = null,
|
||||
val icon: Int? = null,
|
||||
val color: Int? = null,
|
||||
val customIcon: String? = null,
|
||||
val options: String? = null,
|
||||
)
|
||||
@ -9,9 +9,19 @@ class Migration_18_19 : Migration(18, 19) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
val websearches =
|
||||
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()) {
|
||||
val label = websearches.getString(0)
|
||||
val data = websearches.getString(1)
|
||||
|
||||
@ -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>> {
|
||||
return listOf(
|
||||
@ -37,6 +37,5 @@ internal fun getMigrations(context: Context): List<DataMigration<Settings>> {
|
||||
Migration_8_9(),
|
||||
Migration_9_10(),
|
||||
Migration_10_11(),
|
||||
Migration_11_12(),
|
||||
)
|
||||
}
|
||||
@ -162,17 +162,6 @@ fun createFactorySettings(context: Context): Settings {
|
||||
Settings.WidgetSettings.newBuilder()
|
||||
.setEditButton(true)
|
||||
)
|
||||
.setSearchActions(
|
||||
Settings.SearchActionSettings.newBuilder()
|
||||
.setCall(true)
|
||||
.setContact(true)
|
||||
.setEmail(true)
|
||||
.setMessage(true)
|
||||
.setOpenUrl(true)
|
||||
.setScheduleEvent(true)
|
||||
.setSetAlarm(true)
|
||||
.setStartTimer(true)
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
@ -288,16 +288,4 @@ message Settings {
|
||||
bool edit_button = 1;
|
||||
}
|
||||
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;
|
||||
}
|
||||
@ -5,10 +5,21 @@ import de.mm20.launcher2.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.database.AppDatabase
|
||||
import de.mm20.launcher2.database.entities.SearchActionEntity
|
||||
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.SetAlarmActionBuilder
|
||||
import de.mm20.launcher2.searchactions.builders.TimerActionBuilder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
@ -17,6 +28,9 @@ import java.util.UUID
|
||||
|
||||
interface SearchActionRepository {
|
||||
fun getSearchActionBuilders(): Flow<List<SearchActionBuilder>>
|
||||
fun getBuiltinSearchActionBuilders(): List<SearchActionBuilder>
|
||||
|
||||
fun saveSearchActionBuilders(builders: List<SearchActionBuilder>)
|
||||
|
||||
suspend fun export(toDir: File)
|
||||
suspend fun import(fromDir: File)
|
||||
@ -26,9 +40,35 @@ internal class SearchActionRepositoryImpl(
|
||||
private val context: Context,
|
||||
private val database: AppDatabase
|
||||
): SearchActionRepository {
|
||||
|
||||
private val scope = CoroutineScope(Dispatchers.Default + SupervisorJob())
|
||||
override fun getSearchActionBuilders(): Flow<List<SearchActionBuilder>> {
|
||||
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) {
|
||||
|
||||
@ -2,13 +2,9 @@ package de.mm20.launcher2.searchactions
|
||||
|
||||
import android.content.Context
|
||||
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.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.os.UserHandle
|
||||
import android.util.Log
|
||||
import android.util.Xml
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
@ -16,20 +12,10 @@ import coil.imageLoader
|
||||
import coil.request.ImageRequest
|
||||
import coil.size.Scale
|
||||
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.SearchActionIcon
|
||||
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.SetAlarmActionBuilder
|
||||
import de.mm20.launcher2.searchactions.builders.TimerActionBuilder
|
||||
import de.mm20.launcher2.searchactions.builders.WebsearchActionBuilder
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
@ -42,8 +28,6 @@ import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.jsoup.Jsoup
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import org.xmlpull.v1.XmlPullParserException
|
||||
@ -54,12 +38,18 @@ import java.net.URL
|
||||
import java.util.UUID
|
||||
|
||||
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 createIcon(uri: Uri, size: Int): String?
|
||||
|
||||
suspend fun getSearchActivities(): List<ResolveInfo>
|
||||
|
||||
suspend fun createIcon(uri: Uri, size: Int): String?
|
||||
}
|
||||
|
||||
internal class SearchActionServiceImpl(
|
||||
@ -68,7 +58,6 @@ internal class SearchActionServiceImpl(
|
||||
private val textClassifier: TextClassifier,
|
||||
) : SearchActionService {
|
||||
override fun search(
|
||||
settings: SearchActionSettings,
|
||||
query: String
|
||||
): Flow<ImmutableList<SearchAction>> = flow {
|
||||
if (query.isBlank()) {
|
||||
@ -76,28 +65,33 @@ internal class SearchActionServiceImpl(
|
||||
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 other = repository.getSearchActionBuilders()
|
||||
val builders = repository.getSearchActionBuilders()
|
||||
|
||||
emitAll(
|
||||
other.map {
|
||||
(builders + it).mapNotNull { it.build(context, classificationResult) }.toImmutableList()
|
||||
builders.map {
|
||||
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? =
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
|
||||
@ -1,21 +1,27 @@
|
||||
package de.mm20.launcher2.searchactions.builders
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.pm.LauncherActivityInfo
|
||||
import de.mm20.launcher2.searchactions.TextClassificationResult
|
||||
import de.mm20.launcher2.searchactions.TextType
|
||||
import de.mm20.launcher2.searchactions.actions.AppSearchAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
|
||||
|
||||
class AppSearchActionBuilder(
|
||||
val label: String,
|
||||
val activity: LauncherActivityInfo,
|
||||
val filter: TextType? = null,
|
||||
) : SearchActionBuilder {
|
||||
override val label: String,
|
||||
val componentName: ComponentName,
|
||||
override val icon: SearchActionIcon = SearchActionIcon.Search,
|
||||
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? {
|
||||
return AppSearchAction(
|
||||
label = label,
|
||||
componentName = activity.componentName,
|
||||
componentName = componentName,
|
||||
query = classifiedQuery.text,
|
||||
)
|
||||
}
|
||||
|
||||
@ -5,8 +5,18 @@ import de.mm20.launcher2.searchactions.R
|
||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||
import de.mm20.launcher2.searchactions.TextClassificationResult
|
||||
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? {
|
||||
if (classifiedQuery.phoneNumber != null) {
|
||||
|
||||
@ -5,8 +5,18 @@ import de.mm20.launcher2.searchactions.R
|
||||
import de.mm20.launcher2.searchactions.TextClassificationResult
|
||||
import de.mm20.launcher2.searchactions.actions.CreateContactAction
|
||||
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? {
|
||||
if (classifiedQuery.phoneNumber != null || classifiedQuery.email != null) {
|
||||
|
||||
@ -0,0 +1,3 @@
|
||||
package de.mm20.launcher2.searchactions.builders
|
||||
|
||||
sealed interface CustomizableSearchActionBuilder: SearchActionBuilder
|
||||
@ -5,8 +5,18 @@ import de.mm20.launcher2.searchactions.R
|
||||
import de.mm20.launcher2.searchactions.TextClassificationResult
|
||||
import de.mm20.launcher2.searchactions.actions.EmailAction
|
||||
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? {
|
||||
if (classifiedQuery.email != null) {
|
||||
|
||||
@ -5,8 +5,18 @@ import de.mm20.launcher2.searchactions.R
|
||||
import de.mm20.launcher2.searchactions.TextClassificationResult
|
||||
import de.mm20.launcher2.searchactions.actions.MessageAction
|
||||
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? {
|
||||
if (classifiedQuery.phoneNumber != null) {
|
||||
|
||||
@ -6,8 +6,18 @@ import de.mm20.launcher2.searchactions.TextClassificationResult
|
||||
import de.mm20.launcher2.searchactions.actions.MessageAction
|
||||
import de.mm20.launcher2.searchactions.actions.OpenUrlAction
|
||||
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? {
|
||||
if (classifiedQuery.url != null) {
|
||||
|
||||
@ -6,9 +6,19 @@ import de.mm20.launcher2.searchactions.TextClassificationResult
|
||||
import de.mm20.launcher2.searchactions.actions.MessageAction
|
||||
import de.mm20.launcher2.searchactions.actions.ScheduleEventAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
|
||||
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? {
|
||||
if (classifiedQuery.date != null) {
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
package de.mm20.launcher2.searchactions.builders
|
||||
|
||||
import android.content.Context
|
||||
import android.media.metrics.Event
|
||||
import de.mm20.launcher2.database.entities.SearchActionEntity
|
||||
import de.mm20.launcher2.ktx.jsonObjectOf
|
||||
import de.mm20.launcher2.searchactions.TextClassificationResult
|
||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
|
||||
@ -9,10 +11,18 @@ import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
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?
|
||||
|
||||
companion object {
|
||||
fun from(entity: SearchActionEntity): SearchActionBuilder? {
|
||||
internal fun from(context: Context, entity: SearchActionEntity): SearchActionBuilder? {
|
||||
val options = entity.options?.let {
|
||||
try {
|
||||
JSONObject(it)
|
||||
@ -24,8 +34,8 @@ interface SearchActionBuilder {
|
||||
"url" -> {
|
||||
return WebsearchActionBuilder(
|
||||
label = entity.label ?: "",
|
||||
urlTemplate = entity.data,
|
||||
color = entity.color,
|
||||
urlTemplate = entity.data ?: return null,
|
||||
iconColor = entity.color ?: 0,
|
||||
icon = SearchActionIcon.fromInt(entity.icon),
|
||||
customIcon = entity.customIcon,
|
||||
encoding = WebsearchActionBuilder.QueryEncoding.fromInt(options?.optInt("encoding"))
|
||||
@ -34,9 +44,38 @@ interface SearchActionBuilder {
|
||||
"app" -> {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,10 +4,20 @@ import android.content.Context
|
||||
import de.mm20.launcher2.searchactions.R
|
||||
import de.mm20.launcher2.searchactions.TextClassificationResult
|
||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
|
||||
import de.mm20.launcher2.searchactions.actions.SetAlarmAction
|
||||
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? {
|
||||
if (classifiedQuery.time != null) {
|
||||
|
||||
@ -3,10 +3,19 @@ package de.mm20.launcher2.searchactions.builders
|
||||
import android.content.Context
|
||||
import de.mm20.launcher2.searchactions.R
|
||||
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.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? {
|
||||
if (classifiedQuery.timespan != null && classifiedQuery.timespan.seconds <= 86400) {
|
||||
|
||||
@ -9,13 +9,16 @@ import de.mm20.launcher2.searchactions.actions.SearchActionIcon
|
||||
import java.net.URLEncoder
|
||||
|
||||
class WebsearchActionBuilder(
|
||||
val label: String,
|
||||
override val label: String,
|
||||
val urlTemplate: String,
|
||||
val icon: SearchActionIcon = SearchActionIcon.Search,
|
||||
val color: Int = 0,
|
||||
val customIcon: String? = null,
|
||||
override val icon: SearchActionIcon = SearchActionIcon.Search,
|
||||
override val iconColor: Int = 0,
|
||||
override val customIcon: String? = null,
|
||||
val encoding: QueryEncoding = QueryEncoding.UrlEncode,
|
||||
) : SearchActionBuilder {
|
||||
) : CustomizableSearchActionBuilder {
|
||||
|
||||
override val key: String
|
||||
get() = "web://$urlTemplate"
|
||||
|
||||
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction {
|
||||
val url = urlTemplate.replace("\${1}", encodeQuery(classifiedQuery.text, encoding))
|
||||
@ -24,7 +27,7 @@ class WebsearchActionBuilder(
|
||||
url = url,
|
||||
icon = icon,
|
||||
customIcon = customIcon,
|
||||
iconColor = color,
|
||||
iconColor = iconColor,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -13,7 +13,6 @@ import de.mm20.launcher2.preferences.Settings.CalculatorSearchSettings
|
||||
import de.mm20.launcher2.preferences.Settings.CalendarSearchSettings
|
||||
import de.mm20.launcher2.preferences.Settings.ContactsSearchSettings
|
||||
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.WebsiteSearchSettings
|
||||
import de.mm20.launcher2.preferences.Settings.WikipediaSearchSettings
|
||||
@ -59,7 +58,6 @@ interface SearchService {
|
||||
unitConverter: UnitConverterSearchSettings,
|
||||
websites: WebsiteSearchSettings,
|
||||
wikipedia: WikipediaSearchSettings,
|
||||
searchActions: SearchActionSettings,
|
||||
): Flow<ImmutableList<Searchable>>
|
||||
}
|
||||
|
||||
@ -87,7 +85,6 @@ internal class SearchServiceImpl(
|
||||
unitConverter: UnitConverterSearchSettings,
|
||||
websites: WebsiteSearchSettings,
|
||||
wikipedia: WikipediaSearchSettings,
|
||||
searchActions: SearchActionSettings,
|
||||
): Flow<ImmutableList<Searchable>> = channelFlow {
|
||||
supervisorScope {
|
||||
val results = MutableStateFlow(SearchResults())
|
||||
@ -220,7 +217,7 @@ internal class SearchServiceImpl(
|
||||
}
|
||||
}
|
||||
launch {
|
||||
searchActionService.search(searchActions, query)
|
||||
searchActionService.search(query)
|
||||
.collectLatest { r ->
|
||||
results.update {
|
||||
it.copy(searchActions = r)
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -88,7 +88,7 @@ data class LazyDragAndDropListState(
|
||||
fun startDrag(offset: Offset): Boolean {
|
||||
val draggedItem = listState.layoutInfo.visibleItemsInfo.find {
|
||||
Rect(
|
||||
it.offset.toOffset(),
|
||||
(it.offset + listState.layoutInfo.beforeContentPadding).toOffset(),
|
||||
it.size.toSize()
|
||||
).contains(offset)
|
||||
} ?: return false
|
||||
@ -240,13 +240,16 @@ fun LazyDragAndDropRow(
|
||||
verticalAlignment: Alignment.Vertical = Alignment.Top,
|
||||
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
|
||||
userScrollEnabled: Boolean = true,
|
||||
bidirectionalDrag: Boolean = true,
|
||||
content: LazyListScope.() -> Unit
|
||||
) {
|
||||
LazyRow(
|
||||
modifier = modifier.dragAndDrop(
|
||||
state,
|
||||
LocalLayoutDirection.current == LayoutDirection.Rtl,
|
||||
LocalHapticFeedback.current
|
||||
isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl,
|
||||
hapticFeedback = LocalHapticFeedback.current,
|
||||
dragVertical = bidirectionalDrag,
|
||||
dragHorizontal = true,
|
||||
),
|
||||
state = state.listState,
|
||||
contentPadding = contentPadding,
|
||||
@ -269,13 +272,16 @@ fun LazyDragAndDropColumn(
|
||||
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
||||
flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
|
||||
userScrollEnabled: Boolean = true,
|
||||
bidirectionalDrag: Boolean = true,
|
||||
content: LazyListScope.() -> Unit
|
||||
) {
|
||||
LazyColumn(
|
||||
modifier = modifier.dragAndDrop(
|
||||
state,
|
||||
LocalLayoutDirection.current == LayoutDirection.Rtl,
|
||||
LocalHapticFeedback.current
|
||||
isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl,
|
||||
hapticFeedback = LocalHapticFeedback.current,
|
||||
dragVertical = true,
|
||||
dragHorizontal = bidirectionalDrag,
|
||||
),
|
||||
state = state.listState,
|
||||
contentPadding = contentPadding,
|
||||
@ -291,6 +297,8 @@ fun LazyDragAndDropColumn(
|
||||
fun Modifier.dragAndDrop(
|
||||
state: LazyDragAndDropListState,
|
||||
isRtl: Boolean,
|
||||
dragVertical: Boolean = true,
|
||||
dragHorizontal: Boolean = true,
|
||||
hapticFeedback: HapticFeedback
|
||||
) =
|
||||
this then pointerInput(null) {
|
||||
@ -302,9 +310,18 @@ fun Modifier.dragAndDrop(
|
||||
}
|
||||
},
|
||||
onDrag = { _, dragAmount ->
|
||||
scope.launch { state.drag(dragAmount.let {
|
||||
if (isRtl) it.copy(x = -it.x) else it
|
||||
}) }
|
||||
scope.launch {
|
||||
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 = {
|
||||
scope.launch { state.cancelDrag() }
|
||||
|
||||
@ -95,7 +95,6 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
shortcuts = it.appShortcutSearch,
|
||||
websites = it.websiteSearch,
|
||||
wikipedia = it.wikipediaSearch,
|
||||
searchActions = it.searchActions,
|
||||
).collectLatest { results ->
|
||||
hiddenItemKeys.collectLatest { hiddenKeys ->
|
||||
val hidden = mutableListOf<SavableSearchable>()
|
||||
|
||||
@ -6,17 +6,6 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
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.Icon
|
||||
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.unit.dp
|
||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
|
||||
import de.mm20.launcher2.ui.component.SearchActionIcon
|
||||
|
||||
@Composable
|
||||
fun SearchBarActions(
|
||||
@ -53,27 +42,7 @@ fun SearchBarActions(
|
||||
},
|
||||
label = { Text(it.label) },
|
||||
leadingIcon = {
|
||||
val icon = it.icon
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
SearchActionIcon(icon = it.icon, color = it.iconColor, customIcon = it.customIcon)
|
||||
}
|
||||
/*leadingIcon = {
|
||||
val icon = it.icon
|
||||
|
||||
@ -1,115 +1,166 @@
|
||||
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.rounded.Alarm
|
||||
import androidx.compose.material.icons.rounded.CalendarToday
|
||||
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.Sms
|
||||
import androidx.compose.material.icons.rounded.Timer
|
||||
import androidx.compose.material.icons.rounded.Add
|
||||
import androidx.compose.material.icons.rounded.ArrowBack
|
||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
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.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
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.component.preferences.PreferenceCategory
|
||||
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
|
||||
import de.mm20.launcher2.ui.component.SearchActionIcon
|
||||
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.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
|
||||
fun SearchActionsSettingsScreen() {
|
||||
val viewModel: SearchActionsSettingsScreenVM = viewModel()
|
||||
val settings by viewModel.searchActionSettings.observeAsState(
|
||||
Settings.SearchActionSettings.getDefaultInstance()
|
||||
val navController = LocalNavController.current
|
||||
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)) {
|
||||
item {
|
||||
PreferenceCategory {
|
||||
SwitchPreference(
|
||||
icon = Icons.Rounded.Call,
|
||||
title = stringResource(R.string.search_action_call),
|
||||
value = settings.call,
|
||||
onValueChanged = {
|
||||
viewModel.updateSettings {
|
||||
setCall(it)
|
||||
val searchActions by viewModel.searchActions.observeAsState(emptyList())
|
||||
val disabledActions by viewModel.disabledActions.observeAsState(emptyList())
|
||||
|
||||
Scaffold(
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = { /*TODO*/ }) {
|
||||
Icon(imageVector = Icons.Rounded.Add, contentDescription = null)
|
||||
}
|
||||
},
|
||||
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(
|
||||
icon = Icons.Rounded.Sms,
|
||||
title = stringResource(R.string.search_action_message),
|
||||
value = settings.message,
|
||||
icon = getSearchActionIconVector(item.icon),
|
||||
title = item.label,
|
||||
value = false,
|
||||
onValueChanged = {
|
||||
viewModel.updateSettings {
|
||||
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)
|
||||
}
|
||||
},
|
||||
viewModel.addAction(item)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,27 +2,39 @@ package de.mm20.launcher2.ui.settings.searchactions
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.preferences.Settings.SearchActionSettings
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import de.mm20.launcher2.searchactions.SearchActionService
|
||||
import de.mm20.launcher2.searchactions.builders.SearchActionBuilder
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
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) {
|
||||
viewModelScope.launch {
|
||||
dataStore.updateData {
|
||||
it.toBuilder()
|
||||
.setSearchActions(
|
||||
it.searchActions.toBuilder().block()
|
||||
).build()
|
||||
}
|
||||
}
|
||||
val disabledActions = searchActionService
|
||||
.getDisabledActionBuilders()
|
||||
.asLiveData()
|
||||
|
||||
fun addAction(searchAction: SearchActionBuilder) {
|
||||
val actions =
|
||||
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)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user