diff --git a/database/schemas/de.mm20.launcher2.database.AppDatabase/19.json b/database/schemas/de.mm20.launcher2.database.AppDatabase/19.json index 48180e2b..aef328d2 100644 --- a/database/schemas/de.mm20.launcher2.database.AppDatabase/19.json +++ b/database/schemas/de.mm20.launcher2.database.AppDatabase/19.json @@ -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')" ] } } \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt b/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt index 21dfb858..dac9f067 100644 --- a/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt +++ b/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt @@ -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, ) ) diff --git a/database/src/main/java/de/mm20/launcher2/database/SearchActionDao.kt b/database/src/main/java/de/mm20/launcher2/database/SearchActionDao.kt index c6281ffa..3b33b066 100644 --- a/database/src/main/java/de/mm20/launcher2/database/SearchActionDao.kt +++ b/database/src/main/java/de/mm20/launcher2/database/SearchActionDao.kt @@ -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> + + @Transaction + suspend fun replaceAll(actions: List) { + deleteAll() + insertAll(actions) + } + + @Query("DELETE FROM `SearchAction`") + suspend fun deleteAll() + + @Insert + suspend fun insertAll(actions: List) } \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/entities/SearchActionEntity.kt b/database/src/main/java/de/mm20/launcher2/database/entities/SearchActionEntity.kt index bbb5fcd8..c13c1b3a 100644 --- a/database/src/main/java/de/mm20/launcher2/database/entities/SearchActionEntity.kt +++ b/database/src/main/java/de/mm20/launcher2/database/entities/SearchActionEntity.kt @@ -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, ) \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_18_19.kt b/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_18_19.kt index 4c6ee005..382deff7 100644 --- a/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_18_19.kt +++ b/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_18_19.kt @@ -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) diff --git a/preferences/src/main/java/de/mm20/launcher2/preferences/DataStore.kt b/preferences/src/main/java/de/mm20/launcher2/preferences/DataStore.kt index 8d80b9f3..b156f5d9 100644 --- a/preferences/src/main/java/de/mm20/launcher2/preferences/DataStore.kt +++ b/preferences/src/main/java/de/mm20/launcher2/preferences/DataStore.kt @@ -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> { return listOf( @@ -37,6 +37,5 @@ internal fun getMigrations(context: Context): List> { Migration_8_9(), Migration_9_10(), Migration_10_11(), - Migration_11_12(), ) } \ No newline at end of file diff --git a/preferences/src/main/java/de/mm20/launcher2/preferences/Defaults.kt b/preferences/src/main/java/de/mm20/launcher2/preferences/Defaults.kt index 1d0ac393..1737a70e 100644 --- a/preferences/src/main/java/de/mm20/launcher2/preferences/Defaults.kt +++ b/preferences/src/main/java/de/mm20/launcher2/preferences/Defaults.kt @@ -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() } diff --git a/preferences/src/main/proto/settings.proto b/preferences/src/main/proto/settings.proto index edd65357..12100f01 100644 --- a/preferences/src/main/proto/settings.proto +++ b/preferences/src/main/proto/settings.proto @@ -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; } \ No newline at end of file diff --git a/search-actions/src/main/java/de/mm20/launcher2/searchactions/SearchActionRepository.kt b/search-actions/src/main/java/de/mm20/launcher2/searchactions/SearchActionRepository.kt index 4ad8c809..9c4c6f39 100644 --- a/search-actions/src/main/java/de/mm20/launcher2/searchactions/SearchActionRepository.kt +++ b/search-actions/src/main/java/de/mm20/launcher2/searchactions/SearchActionRepository.kt @@ -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> + fun getBuiltinSearchActionBuilders(): List + + fun saveSearchActionBuilders(builders: List) 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> { 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 { + 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) { + 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) { diff --git a/search-actions/src/main/java/de/mm20/launcher2/searchactions/SearchActionService.kt b/search-actions/src/main/java/de/mm20/launcher2/searchactions/SearchActionService.kt index 3d3b9a2d..a4025da8 100644 --- a/search-actions/src/main/java/de/mm20/launcher2/searchactions/SearchActionService.kt +++ b/search-actions/src/main/java/de/mm20/launcher2/searchactions/SearchActionService.kt @@ -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> + fun search(query: String): Flow> + + fun getSearchActionBuilders(): Flow> + fun getDisabledActionBuilders(): Flow> + + fun saveSearchActionBuilders(builders: List) suspend fun importWebsearch(url: String, iconSize: Int): WebsearchActionBuilder? - suspend fun createIcon(uri: Uri, size: Int): String? suspend fun getSearchActivities(): List + + 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> = flow { if (query.isBlank()) { @@ -76,28 +65,33 @@ internal class SearchActionServiceImpl( return@flow } - val builders = mutableListOf() - - 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> { + return repository.getSearchActionBuilders() + } + + override fun getDisabledActionBuilders(): Flow> { + val allActions = repository.getBuiltinSearchActionBuilders() + + return getSearchActionBuilders().map { enabled -> + allActions.filter { action -> !enabled.any { it.key == action.key } } + } + } + + override fun saveSearchActionBuilders(builders: List) { + repository.saveSearchActionBuilders(builders) + } + override suspend fun importWebsearch(url: String, iconSize: Int): WebsearchActionBuilder? = withContext(Dispatchers.IO) { try { diff --git a/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/AppSearchActionBuilder.kt b/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/AppSearchActionBuilder.kt index e321f37d..4e462c5b 100644 --- a/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/AppSearchActionBuilder.kt +++ b/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/AppSearchActionBuilder.kt @@ -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, ) } diff --git a/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/CallActionBuilder.kt b/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/CallActionBuilder.kt index ff8de3e3..e0a218a0 100644 --- a/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/CallActionBuilder.kt +++ b/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/CallActionBuilder.kt @@ -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) { diff --git a/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/CreateContactActionBuilder.kt b/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/CreateContactActionBuilder.kt index 4167cc9b..b45fae2f 100644 --- a/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/CreateContactActionBuilder.kt +++ b/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/CreateContactActionBuilder.kt @@ -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) { diff --git a/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/CustomizableSearchActionBuilder.kt b/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/CustomizableSearchActionBuilder.kt new file mode 100644 index 00000000..baafb0a1 --- /dev/null +++ b/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/CustomizableSearchActionBuilder.kt @@ -0,0 +1,3 @@ +package de.mm20.launcher2.searchactions.builders + +sealed interface CustomizableSearchActionBuilder: SearchActionBuilder \ No newline at end of file diff --git a/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/EmailActionBuilder.kt b/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/EmailActionBuilder.kt index 92caa936..f0679ebc 100644 --- a/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/EmailActionBuilder.kt +++ b/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/EmailActionBuilder.kt @@ -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) { diff --git a/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/MessageActionBuilder.kt b/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/MessageActionBuilder.kt index 1287d422..68bafda2 100644 --- a/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/MessageActionBuilder.kt +++ b/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/MessageActionBuilder.kt @@ -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) { diff --git a/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/OpenUrlActionBuilder.kt b/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/OpenUrlActionBuilder.kt index 6d38fe83..74560d56 100644 --- a/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/OpenUrlActionBuilder.kt +++ b/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/OpenUrlActionBuilder.kt @@ -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) { diff --git a/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/ScheduleEventActionBuilder.kt b/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/ScheduleEventActionBuilder.kt index 68269186..3f1f8535 100644 --- a/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/ScheduleEventActionBuilder.kt +++ b/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/ScheduleEventActionBuilder.kt @@ -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) { diff --git a/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/SearchActionBuilder.kt b/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/SearchActionBuilder.kt index fea14214..1b508f5b 100644 --- a/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/SearchActionBuilder.kt +++ b/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/SearchActionBuilder.kt @@ -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, + ) + } + } + } } diff --git a/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/SetAlarmActionBuilder.kt b/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/SetAlarmActionBuilder.kt index d1553cad..a2ff9c9f 100644 --- a/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/SetAlarmActionBuilder.kt +++ b/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/SetAlarmActionBuilder.kt @@ -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) { diff --git a/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/TimerActionBuilder.kt b/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/TimerActionBuilder.kt index fa87d5a2..db76a699 100644 --- a/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/TimerActionBuilder.kt +++ b/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/TimerActionBuilder.kt @@ -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) { diff --git a/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/WebsearchActionBuilder.kt b/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/WebsearchActionBuilder.kt index ee931389..3dc7a0a0 100644 --- a/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/WebsearchActionBuilder.kt +++ b/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/WebsearchActionBuilder.kt @@ -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, ) } diff --git a/search/src/main/java/de/mm20/launcher2/search/SearchService.kt b/search/src/main/java/de/mm20/launcher2/search/SearchService.kt index c9902f1d..18e5481a 100644 --- a/search/src/main/java/de/mm20/launcher2/search/SearchService.kt +++ b/search/src/main/java/de/mm20/launcher2/search/SearchService.kt @@ -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> } @@ -87,7 +85,6 @@ internal class SearchServiceImpl( unitConverter: UnitConverterSearchSettings, websites: WebsiteSearchSettings, wikipedia: WikipediaSearchSettings, - searchActions: SearchActionSettings, ): Flow> = 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) diff --git a/ui/src/main/java/de/mm20/launcher2/ui/component/SearchActionIcon.kt b/ui/src/main/java/de/mm20/launcher2/ui/component/SearchActionIcon.kt new file mode 100644 index 00000000..241648c6 --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/component/SearchActionIcon.kt @@ -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 + } +} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/helper/DragAndDropList.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/helper/DragAndDropList.kt index 47095be9..84caa51a 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/helper/DragAndDropList.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/helper/DragAndDropList.kt @@ -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() } diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt index e9cf207a..aa378d0f 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/SearchVM.kt @@ -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() diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/SearchBarActions.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/SearchBarActions.kt index bb854b1b..dcfb7648 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/SearchBarActions.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/searchbar/SearchBarActions.kt @@ -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 diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/SearchActionsSettingsScreen.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/SearchActionsSettingsScreen.kt index 78aeb858..c93fd94f 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/SearchActionsSettingsScreen.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/SearchActionsSettingsScreen.kt @@ -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) + } ) } } diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/SearchActionsSettingsScreenVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/SearchActionsSettingsScreenVM.kt index 3e192016..32f27f79 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/SearchActionsSettingsScreenVM.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/searchactions/SearchActionsSettingsScreenVM.kt @@ -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) } } \ No newline at end of file