From 872f55625f48f129232fdd1494921c230ba6bff8 Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Thu, 3 Nov 2022 22:17:37 +0100 Subject: [PATCH] Migrate websearches to search actions --- backup/build.gradle.kts | 2 +- .../mm20/launcher2/backup/BackupComponent.kt | 2 +- .../de/mm20/launcher2/backup/BackupManager.kt | 14 +- .../19.json | 468 ++++++++++++ .../de/mm20/launcher2/database/AppDatabase.kt | 215 ++---- .../launcher2/database/BackupRestoreDao.kt | 11 +- .../de/mm20/launcher2/database/SearchDao.kt | 13 - .../database/entities/SearchActionEntity.kt | 12 +- .../database/migrations/Migration_10_11.kt | 13 + .../database/migrations/Migration_11_12.kt | 10 + .../database/migrations/Migration_12_13.kt | 10 + .../database/migrations/Migration_13_14.kt | 10 + .../database/migrations/Migration_14_15.kt | 9 + .../database/migrations/Migration_15_16.kt | 19 + .../database/migrations/Migration_16_17.kt | 10 + .../database/migrations/Migration_17_18.kt | 17 + .../database/migrations/Migration_18_19.kt | 44 ++ .../database/migrations/Migration_6_7.kt | 14 + .../database/migrations/Migration_7_8.kt | 14 + .../database/migrations/Migration_8_9.kt | 38 + .../database/migrations/Migration_9_10.kt | 11 + i18n/src/main/res/values/strings.xml | 1 + search-actions/build.gradle.kts | 4 + .../de/mm20/launcher2/searchactions/Module.kt | 2 +- .../searchactions/SearchActionRepository.kt | 111 ++- .../searchactions/SearchActionService.kt | 157 +++- .../searchactions/actions/AppSearchAction.kt | 24 + .../searchactions/actions/SearchAction.kt | 24 +- .../builders/AppSearchActionBuilder.kt | 22 + .../builders/WebsearchActionBuilder.kt | 24 +- .../java/de/mm20/launcher2/search/Module.kt | 1 - .../launcher2/search/WebsearchRepository.kt | 306 -------- .../launcher2/ui/common/RestoreBackupSheet.kt | 4 +- .../launcher2/ui/launcher/search/SearchVM.kt | 2 - .../launcher2/ui/settings/SettingsActivity.kt | 4 - .../ui/settings/backup/CreateBackupSheet.kt | 6 +- .../websearch/WebSearchSettingsScreen.kt | 692 ------------------ .../websearch/WebSearchSettingsScreenVM.kt | 55 -- 38 files changed, 1120 insertions(+), 1275 deletions(-) create mode 100644 database/schemas/de.mm20.launcher2.database.AppDatabase/19.json create mode 100644 database/src/main/java/de/mm20/launcher2/database/migrations/Migration_10_11.kt create mode 100644 database/src/main/java/de/mm20/launcher2/database/migrations/Migration_11_12.kt create mode 100644 database/src/main/java/de/mm20/launcher2/database/migrations/Migration_12_13.kt create mode 100644 database/src/main/java/de/mm20/launcher2/database/migrations/Migration_13_14.kt create mode 100644 database/src/main/java/de/mm20/launcher2/database/migrations/Migration_14_15.kt create mode 100644 database/src/main/java/de/mm20/launcher2/database/migrations/Migration_15_16.kt create mode 100644 database/src/main/java/de/mm20/launcher2/database/migrations/Migration_16_17.kt create mode 100644 database/src/main/java/de/mm20/launcher2/database/migrations/Migration_17_18.kt create mode 100644 database/src/main/java/de/mm20/launcher2/database/migrations/Migration_18_19.kt create mode 100644 database/src/main/java/de/mm20/launcher2/database/migrations/Migration_6_7.kt create mode 100644 database/src/main/java/de/mm20/launcher2/database/migrations/Migration_7_8.kt create mode 100644 database/src/main/java/de/mm20/launcher2/database/migrations/Migration_8_9.kt create mode 100644 database/src/main/java/de/mm20/launcher2/database/migrations/Migration_9_10.kt create mode 100644 search-actions/src/main/java/de/mm20/launcher2/searchactions/actions/AppSearchAction.kt create mode 100644 search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/AppSearchActionBuilder.kt delete mode 100644 search/src/main/java/de/mm20/launcher2/search/WebsearchRepository.kt delete mode 100644 ui/src/main/java/de/mm20/launcher2/ui/settings/websearch/WebSearchSettingsScreen.kt delete mode 100644 ui/src/main/java/de/mm20/launcher2/ui/settings/websearch/WebSearchSettingsScreenVM.kt diff --git a/backup/build.gradle.kts b/backup/build.gradle.kts index f6d3c799..81e27b78 100644 --- a/backup/build.gradle.kts +++ b/backup/build.gradle.kts @@ -42,7 +42,7 @@ dependencies { implementation(project(":favorites")) implementation(project(":widgets")) - implementation(project(":search")) + implementation(project(":search-actions")) implementation(project(":preferences")) implementation(project(":ktx")) implementation(project(":customattrs")) diff --git a/backup/src/main/java/de/mm20/launcher2/backup/BackupComponent.kt b/backup/src/main/java/de/mm20/launcher2/backup/BackupComponent.kt index b06c77d1..4ffd0d23 100644 --- a/backup/src/main/java/de/mm20/launcher2/backup/BackupComponent.kt +++ b/backup/src/main/java/de/mm20/launcher2/backup/BackupComponent.kt @@ -5,7 +5,7 @@ enum class BackupComponent(val value: String) { Favorites("favorites"), Widgets("widgets"), Customizations("customizations"), - Websearches("websearches"); + SearchActions("searchactions"); companion object { fun fromValue(value: String): BackupComponent? { diff --git a/backup/src/main/java/de/mm20/launcher2/backup/BackupManager.kt b/backup/src/main/java/de/mm20/launcher2/backup/BackupManager.kt index ec2e670a..26e758ad 100644 --- a/backup/src/main/java/de/mm20/launcher2/backup/BackupManager.kt +++ b/backup/src/main/java/de/mm20/launcher2/backup/BackupManager.kt @@ -8,7 +8,7 @@ import de.mm20.launcher2.favorites.FavoritesRepository import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.preferences.export import de.mm20.launcher2.preferences.import -import de.mm20.launcher2.search.WebsearchRepository +import de.mm20.launcher2.searchactions.SearchActionRepository import de.mm20.launcher2.widgets.WidgetRepository import kotlinx.coroutines.* import java.io.File @@ -23,7 +23,7 @@ class BackupManager( private val dataStore: LauncherDataStore, private val favoritesRepository: FavoritesRepository, private val widgetRepository: WidgetRepository, - private val websearchRepository: WebsearchRepository, + private val searchActionRepository: SearchActionRepository, private val customAttrsRepository: CustomAttributesRepository, ) { private val scope = CoroutineScope(Dispatchers.Default + Job()) @@ -70,8 +70,8 @@ class BackupManager( widgetRepository.export(backupDir) } - if (include.contains(BackupComponent.Websearches)) { - websearchRepository.export(backupDir) + if (include.contains(BackupComponent.SearchActions)) { + searchActionRepository.export(backupDir) } if (include.contains(BackupComponent.Customizations)) { @@ -111,8 +111,8 @@ class BackupManager( widgetRepository.import(restoreDir) } - if (include.contains(BackupComponent.Websearches)) { - websearchRepository.import(restoreDir) + if (include.contains(BackupComponent.SearchActions)) { + searchActionRepository.import(restoreDir) } if (include.contains(BackupComponent.Customizations)) { @@ -183,7 +183,7 @@ class BackupManager( companion object { private const val BackupFormatMajor = 1 - private const val BackupFormatMinor = 3 + private const val BackupFormatMinor = 4 internal const val BackupFormat = "$BackupFormatMajor.$BackupFormatMinor" } } diff --git a/database/schemas/de.mm20.launcher2.database.AppDatabase/19.json b/database/schemas/de.mm20.launcher2.database.AppDatabase/19.json new file mode 100644 index 00000000..48180e2b --- /dev/null +++ b/database/schemas/de.mm20.launcher2.database.AppDatabase/19.json @@ -0,0 +1,468 @@ +{ + "formatVersion": 1, + "database": { + "version": 19, + "identityHash": "c6adf1dca8ade5117d4e8952a67786fa", + "entities": [ + { + "tableName": "forecasts", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`timestamp` INTEGER NOT NULL, `temperature` REAL NOT NULL, `minTemp` REAL NOT NULL, `maxTemp` REAL NOT NULL, `pressure` REAL NOT NULL, `humidity` REAL NOT NULL, `icon` INTEGER NOT NULL, `condition` TEXT NOT NULL, `clouds` INTEGER NOT NULL, `windSpeed` REAL NOT NULL, `windDirection` REAL NOT NULL, `rain` REAL NOT NULL, `snow` REAL NOT NULL, `night` INTEGER NOT NULL, `location` TEXT NOT NULL, `provider` TEXT NOT NULL, `providerUrl` TEXT NOT NULL, `rainProbability` INTEGER NOT NULL, `snowProbability` INTEGER NOT NULL, `updateTime` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))", + "fields": [ + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "temperature", + "columnName": "temperature", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "minTemp", + "columnName": "minTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "maxTemp", + "columnName": "maxTemp", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "pressure", + "columnName": "pressure", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "humidity", + "columnName": "humidity", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "condition", + "columnName": "condition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clouds", + "columnName": "clouds", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "windSpeed", + "columnName": "windSpeed", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "windDirection", + "columnName": "windDirection", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "precipitation", + "columnName": "rain", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "snow", + "columnName": "snow", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "night", + "columnName": "night", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "location", + "columnName": "location", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "provider", + "columnName": "provider", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "providerUrl", + "columnName": "providerUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "precipProbability", + "columnName": "rainProbability", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "snowProbability", + "columnName": "snowProbability", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "updateTime", + "columnName": "updateTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "timestamp" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Searchable", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `type` TEXT NOT NULL, `searchable` TEXT NOT NULL, `launchCount` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, PRIMARY KEY(`key`))", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "serializedSearchable", + "columnName": "searchable", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "launchCount", + "columnName": "launchCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinPosition", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "key" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Currency", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`symbol` TEXT NOT NULL, `value` REAL NOT NULL, `lastUpdate` INTEGER NOT NULL, PRIMARY KEY(`symbol`))", + "fields": [ + { + "fieldPath": "symbol", + "columnName": "symbol", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "REAL", + "notNull": true + }, + { + "fieldPath": "lastUpdate", + "columnName": "lastUpdate", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "symbol" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Icons", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `componentName` TEXT, `drawable` TEXT, `iconPack` TEXT NOT NULL, `scale` REAL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "componentName", + "columnName": "componentName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "drawable", + "columnName": "drawable", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "iconPack", + "columnName": "iconPack", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scale", + "columnName": "scale", + "affinity": "REAL", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "IconPack", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `packageName` TEXT NOT NULL, `version` TEXT NOT NULL, `scale` REAL NOT NULL, PRIMARY KEY(`packageName`))", + "fields": [ + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "packageName", + "columnName": "packageName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "scale", + "columnName": "scale", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "packageName" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Widget", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` TEXT NOT NULL, `data` TEXT NOT NULL, `height` INTEGER NOT NULL, `position` INTEGER NOT NULL, `label` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "height", + "columnName": "height", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "CustomAttributes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `type` TEXT NOT NULL, `value` TEXT NOT NULL, `id` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "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`))", + "fields": [ + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "label", + "columnName": "label", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "customIcon", + "columnName": "customIcon", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "options", + "columnName": "options", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "position" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "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')" + ] + } +} \ 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 5a9c72d6..1e0e1e49 100644 --- a/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt +++ b/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt @@ -7,18 +7,34 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters -import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import de.mm20.launcher2.database.entities.* +import de.mm20.launcher2.database.migrations.Migration_10_11 +import de.mm20.launcher2.database.migrations.Migration_11_12 +import de.mm20.launcher2.database.migrations.Migration_12_13 +import de.mm20.launcher2.database.migrations.Migration_13_14 +import de.mm20.launcher2.database.migrations.Migration_14_15 +import de.mm20.launcher2.database.migrations.Migration_15_16 +import de.mm20.launcher2.database.migrations.Migration_16_17 +import de.mm20.launcher2.database.migrations.Migration_17_18 +import de.mm20.launcher2.database.migrations.Migration_18_19 +import de.mm20.launcher2.database.migrations.Migration_6_7 +import de.mm20.launcher2.database.migrations.Migration_7_8 +import de.mm20.launcher2.database.migrations.Migration_8_9 +import de.mm20.launcher2.database.migrations.Migration_9_10 -@Database(entities = [ForecastEntity::class, - SavedSearchableEntity::class, - WebsearchEntity::class, - CurrencyEntity::class, - IconEntity::class, - IconPackEntity::class, - WidgetEntity::class, - CustomAttributeEntity::class], version = 18, exportSchema = true) +@Database( + entities = [ + ForecastEntity::class, + SavedSearchableEntity::class, + CurrencyEntity::class, + IconEntity::class, + IconPackEntity::class, + WidgetEntity::class, + CustomAttributeEntity::class, + SearchActionEntity::class, + ], version = 19, exportSchema = true +) @TypeConverters(ComponentNameConverter::class, StringListConverter::class) abstract class AppDatabase : RoomDatabase() { @@ -34,154 +50,47 @@ abstract class AppDatabase : RoomDatabase() { private var _instance: AppDatabase? = null fun getInstance(context: Context): AppDatabase { val instance = _instance - ?: Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "room") - //.fallbackToDestructiveMigration() - .addCallback(object : Callback() { - override fun onCreate(db: SupportSQLiteDatabase) { - super.onCreate(db) - db.execSQL("INSERT INTO Websearch (urlTemplate, label, color, icon) VALUES " + - "('${context.getString(R.string.default_websearch_1_url)}', '${context.getString(R.string.default_websearch_1_name)}', 0, NULL )," + - "('${context.getString(R.string.default_websearch_2_url)}', '${context.getString(R.string.default_websearch_2_name)}', 0, NULL )," + - "('${context.getString(R.string.default_websearch_3_url)}', '${context.getString(R.string.default_websearch_3_name)}', 0, NULL );") + ?: Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "room") + //.fallbackToDestructiveMigration() + .addCallback(object : Callback() { + override fun onCreate(db: SupportSQLiteDatabase) { + super.onCreate(db) - db.execSQL("INSERT INTO Widget (type, data, height, position, label) VALUES " + - "('internal', 'weather', -1, 0, '${context.getString(R.string.widget_name_weather)}')," + - "('internal', 'music', -1, 1, '${context.getString(R.string.widget_name_music)}')," + - "('internal', 'calendar', -1, 2, '${context.getString(R.string.widget_name_calendar)}');") - } - }) - .addMigrations( - Migration_6_7(), - Migration_7_8(), - Migration_8_9(), - Migration_9_10(), - Migration_10_11(), - Migration_11_12(), - Migration_12_13(), - Migration_13_14(), - Migration_14_15(), - Migration_15_16(), - Migration_16_17(), - Migration_17_18(), - ).build() + 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, + ) + ) + + db.execSQL( + "INSERT INTO Widget (type, data, height, position, label) VALUES " + + "('internal', 'weather', -1, 0, '${context.getString(R.string.widget_name_weather)}')," + + "('internal', 'music', -1, 1, '${context.getString(R.string.widget_name_music)}')," + + "('internal', 'calendar', -1, 2, '${context.getString(R.string.widget_name_calendar)}');" + ) + } + }) + .addMigrations( + Migration_6_7(), + Migration_7_8(), + Migration_8_9(), + Migration_9_10(), + Migration_10_11(), + Migration_11_12(), + Migration_12_13(), + Migration_13_14(), + Migration_14_15(), + Migration_15_16(), + Migration_16_17(), + Migration_17_18(), + Migration_18_19(), + ).build() if (_instance == null) _instance = instance return instance } } } -class Migration_6_7 : Migration(6, 7) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("CREATE TABLE Searchable2 (`key` TEXT NOT NULL, `searchable` TEXT, `launchCount` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, `inAllApps` INTEGER NOT NULL, PRIMARY KEY(`key`))") - database.execSQL("INSERT INTO Searchable2 SELECT * FROM Searchable") - database.execSQL("DROP TABLE Searchable") - database.execSQL("ALTER TABLE Searchable2 RENAME TO Searchable") - } - -} - -class Migration_7_8 : Migration(7, 8) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("CREATE TABLE IF NOT EXISTS `${ForecastEntity.TABLE_NAME}2` (`timestamp` INTEGER NOT NULL, `temperature` REAL NOT NULL, `minTemp` REAL NOT NULL, `maxTemp` REAL NOT NULL, `pressure` REAL NOT NULL, `humidity` REAL NOT NULL, `icon` INTEGER NOT NULL, `condition` TEXT NOT NULL, `clouds` INTEGER NOT NULL, `windSpeed` REAL NOT NULL, `windDirection` REAL NOT NULL, `rain` REAL NOT NULL, `snow` REAL NOT NULL, `night` INTEGER NOT NULL, `location` TEXT NOT NULL, `provider` TEXT NOT NULL, `providerUrl` TEXT NOT NULL, `rainPropability` INTEGER NOT NULL, `snowProbability` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))") - database.execSQL("INSERT INTO ${ForecastEntity.TABLE_NAME}2 SELECT *, -1 as rainPropability, -1 as snowPropability FROM ${ForecastEntity.TABLE_NAME}") - database.execSQL("DROP TABLE ${ForecastEntity.TABLE_NAME}") - database.execSQL("ALTER TABLE ${ForecastEntity.TABLE_NAME}2 RENAME TO ${ForecastEntity.TABLE_NAME}") - } -} - -class Migration_8_9 : Migration(8, 9) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("CREATE TABLE IF NOT EXISTS `${ForecastEntity.TABLE_NAME}2` (" + - "`timestamp` INTEGER NOT NULL, " + - "`temperature` REAL NOT NULL, " + - "`minTemp` REAL NOT NULL, " + - "`maxTemp` REAL NOT NULL, " + - "`pressure` REAL NOT NULL, " + - "`humidity` REAL NOT NULL, " + - "`icon` INTEGER NOT NULL, " + - "`condition` TEXT NOT NULL, " + - "`clouds` INTEGER NOT NULL, " + - "`windSpeed` REAL NOT NULL, " + - "`windDirection` REAL NOT NULL, " + - "`rain` REAL NOT NULL, " + - "`snow` REAL NOT NULL, " + - "`night` INTEGER NOT NULL, " + - "`location` TEXT NOT NULL, " + - "`provider` TEXT NOT NULL, " + - "`providerUrl` TEXT NOT NULL, " + - "`rainProbability` INTEGER NOT NULL, " + - "`snowProbability` INTEGER NOT NULL, " + - "`updateTime` INTEGER NOT NULL, " + - "PRIMARY KEY(`timestamp`))") - database.execSQL("INSERT INTO ${ForecastEntity.TABLE_NAME}2 SELECT timestamp, temperature, minTemp, maxTemp, pressure, humidity, icon, condition, clouds, windSpeed, windDirection, rain, snow, night, location, provider, providerUrl, rainPropability as rainProbability, snowProbability, 0 as updateTime FROM ${ForecastEntity.TABLE_NAME}") - database.execSQL("DROP TABLE ${ForecastEntity.TABLE_NAME}") - database.execSQL("ALTER TABLE ${ForecastEntity.TABLE_NAME}2 RENAME TO ${ForecastEntity.TABLE_NAME}") - } - -} - -class Migration_9_10 : Migration(9, 10) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("CREATE TABLE IF NOT EXISTS `Plugins` (`packageName` TEXT NOT NULL, `label` TEXT NOT NULL, `description` TEXT NOT NULL, `pluginClassName` TEXT NOT NULL, `enabled` INTEGER NOT NULL, PRIMARY KEY(`packageName`) );") - } - -} - -class Migration_10_11 : Migration(10, 11) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("CREATE TABLE IF NOT EXISTS `temp` (`key` TEXT NOT NULL, `searchable` TEXT NOT NULL, `launchCount` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, PRIMARY KEY(`key`))") - database.execSQL("INSERT INTO `temp` SELECT `key`, `searchable`, `launchCount`, `pinned`, `hidden` FROM `Searchable`") - database.execSQL("DROP TABLE `Searchable`") - database.execSQL("ALTER TABLE `temp` RENAME TO `Searchable`") - } -} - -class Migration_11_12 : Migration(11, 12) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("CREATE TABLE IF NOT EXISTS `Currency` (`symbol` TEXT NOT NULL, `value` REAL NOT NULL, `lastUpdate` INTEGER NOT NULL, PRIMARY KEY(`symbol`))") - } -} - -class Migration_12_13 : Migration(12, 13) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("CREATE TABLE IF NOT EXISTS `Plugin` (`packageName` TEXT NOT NULL, `data` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`packageName`, `data`))") - } -} -class Migration_13_14 : Migration(13, 14) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("DROP TABLE IF EXISTS `Plugins`;") - } -} -class Migration_14_15 : Migration(14, 15) { - override fun migrate(database: SupportSQLiteDatabase) { - } -} -class Migration_15_16 : Migration(15, 16) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL(""" - CREATE TABLE IF NOT EXISTS `CustomAttributes` ( - `key` TEXT NOT NULL, - `type` TEXT NOT NULL, - `value` TEXT NOT NULL, - `id` INTEGER PRIMARY KEY AUTOINCREMENT - ) - """.trimIndent()) - } -} - -class Migration_16_17 : Migration(16, 17) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE Websearch ADD COLUMN encoding INTEGER") - } -} - -class Migration_17_18: Migration(17, 18) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE Searchable ADD COLUMN type TEXT NOT NULL DEFAULT ''") - database.execSQL(""" - UPDATE Searchable - SET type = SUBSTR(`key`, 0, INSTR(`key`, '://')), - searchable = SUBSTR(`searchable`, INSTR(`searchable`, '#') + 1) - """.trimIndent()) - } -} \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/BackupRestoreDao.kt b/database/src/main/java/de/mm20/launcher2/database/BackupRestoreDao.kt index 506de4c4..7d4d6423 100644 --- a/database/src/main/java/de/mm20/launcher2/database/BackupRestoreDao.kt +++ b/database/src/main/java/de/mm20/launcher2/database/BackupRestoreDao.kt @@ -6,6 +6,7 @@ import androidx.room.OnConflictStrategy import androidx.room.Query import de.mm20.launcher2.database.entities.CustomAttributeEntity import de.mm20.launcher2.database.entities.SavedSearchableEntity +import de.mm20.launcher2.database.entities.SearchActionEntity import de.mm20.launcher2.database.entities.WebsearchEntity import de.mm20.launcher2.database.entities.WidgetEntity @@ -30,14 +31,14 @@ interface BackupRestoreDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun importWidgets(items: List) - @Query("DELETE FROM Websearch") - suspend fun wipeWebsearches() + @Query("DELETE FROM SearchAction") + suspend fun wipeSearchActions() - @Query("SELECT * FROM Websearch LIMIT :limit OFFSET :offset") - suspend fun exportWebsearches(limit: Int, offset: Int): List + @Query("SELECT * FROM SearchAction LIMIT :limit OFFSET :offset") + suspend fun exportSearchActions(limit: Int, offset: Int): List @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun importWebsearches(items: List) + suspend fun importSearchActions(items: List) @Query("DELETE FROM CustomAttributes") suspend fun wipeCustomAttributes() diff --git a/database/src/main/java/de/mm20/launcher2/database/SearchDao.kt b/database/src/main/java/de/mm20/launcher2/database/SearchDao.kt index a941d966..dc613d04 100644 --- a/database/src/main/java/de/mm20/launcher2/database/SearchDao.kt +++ b/database/src/main/java/de/mm20/launcher2/database/SearchDao.kt @@ -2,7 +2,6 @@ package de.mm20.launcher2.database import androidx.room.* import de.mm20.launcher2.database.entities.SavedSearchableEntity -import de.mm20.launcher2.database.entities.WebsearchEntity import kotlinx.coroutines.flow.Flow @Dao @@ -114,18 +113,6 @@ interface SearchDao { @Query("SELECT * FROM SEARCHABLE WHERE hidden = 1") fun getHiddenItems(): Flow> - @Query("SELECT * FROM Websearch ORDER BY label ASC") - fun getWebSearches(): Flow> - - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertWebsearch(websearch: WebsearchEntity) - - @Delete - fun deleteWebsearch(websearch: WebsearchEntity) - - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insertAllWebsearches(websearches: List) - @Query("UPDATE Searchable SET launchCount = launchCount + 1 WHERE `key` = :key") fun incrementExistingLaunchCount(key: String) 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 3de51873..bbb5fcd8 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 @@ -1,8 +1,16 @@ package de.mm20.launcher2.database.entities +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "SearchAction") data class SearchActionEntity( + @PrimaryKey val position: Int, val type: String, - val data: String? = null, + val data: String, + val label: String?, + val icon: Int, + val color: Int = 0, + val customIcon: String? = null, val options: String? = null, - val position: Int, ) \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_10_11.kt b/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_10_11.kt new file mode 100644 index 00000000..00a1abf1 --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_10_11.kt @@ -0,0 +1,13 @@ +package de.mm20.launcher2.database.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration_10_11 : Migration(10, 11) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE IF NOT EXISTS `temp` (`key` TEXT NOT NULL, `searchable` TEXT NOT NULL, `launchCount` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, PRIMARY KEY(`key`))") + database.execSQL("INSERT INTO `temp` SELECT `key`, `searchable`, `launchCount`, `pinned`, `hidden` FROM `Searchable`") + database.execSQL("DROP TABLE `Searchable`") + database.execSQL("ALTER TABLE `temp` RENAME TO `Searchable`") + } +} \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_11_12.kt b/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_11_12.kt new file mode 100644 index 00000000..65d7e183 --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_11_12.kt @@ -0,0 +1,10 @@ +package de.mm20.launcher2.database.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration_11_12 : Migration(11, 12) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE IF NOT EXISTS `Currency` (`symbol` TEXT NOT NULL, `value` REAL NOT NULL, `lastUpdate` INTEGER NOT NULL, PRIMARY KEY(`symbol`))") + } +} \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_12_13.kt b/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_12_13.kt new file mode 100644 index 00000000..c4e8a68d --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_12_13.kt @@ -0,0 +1,10 @@ +package de.mm20.launcher2.database.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration_12_13 : Migration(12, 13) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE IF NOT EXISTS `Plugin` (`packageName` TEXT NOT NULL, `data` TEXT NOT NULL, `type` TEXT NOT NULL, PRIMARY KEY(`packageName`, `data`))") + } +} \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_13_14.kt b/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_13_14.kt new file mode 100644 index 00000000..e8431eb8 --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_13_14.kt @@ -0,0 +1,10 @@ +package de.mm20.launcher2.database.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration_13_14 : Migration(13, 14) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("DROP TABLE IF EXISTS `Plugins`;") + } +} \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_14_15.kt b/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_14_15.kt new file mode 100644 index 00000000..6476d1f3 --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_14_15.kt @@ -0,0 +1,9 @@ +package de.mm20.launcher2.database.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration_14_15 : Migration(14, 15) { + override fun migrate(database: SupportSQLiteDatabase) { + } +} \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_15_16.kt b/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_15_16.kt new file mode 100644 index 00000000..5d1a0e0b --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_15_16.kt @@ -0,0 +1,19 @@ +package de.mm20.launcher2.database.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration_15_16 : Migration(15, 16) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + """ + CREATE TABLE IF NOT EXISTS `CustomAttributes` ( + `key` TEXT NOT NULL, + `type` TEXT NOT NULL, + `value` TEXT NOT NULL, + `id` INTEGER PRIMARY KEY AUTOINCREMENT + ) + """.trimIndent() + ) + } +} \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_16_17.kt b/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_16_17.kt new file mode 100644 index 00000000..5d2c30f0 --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_16_17.kt @@ -0,0 +1,10 @@ +package de.mm20.launcher2.database.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration_16_17 : Migration(16, 17) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE Websearch ADD COLUMN encoding INTEGER") + } +} \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_17_18.kt b/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_17_18.kt new file mode 100644 index 00000000..a79968d5 --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_17_18.kt @@ -0,0 +1,17 @@ +package de.mm20.launcher2.database.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration_17_18 : Migration(17, 18) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("ALTER TABLE Searchable ADD COLUMN type TEXT NOT NULL DEFAULT ''") + database.execSQL( + """ + UPDATE Searchable + SET type = SUBSTR(`key`, 0, INSTR(`key`, '://')), + searchable = SUBSTR(`searchable`, INSTR(`searchable`, '#') + 1) + """.trimIndent() + ) + } +} \ 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 new file mode 100644 index 00000000..4c6ee005 --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_18_19.kt @@ -0,0 +1,44 @@ +package de.mm20.launcher2.database.migrations + +import androidx.core.database.getStringOrNull +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import de.mm20.launcher2.ktx.jsonObjectOf + +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`))" + ) + var position = 0 + while (websearches.moveToNext()) { + val label = websearches.getString(0) + val data = websearches.getString(1) + val color = websearches.getInt(2) + val icon = websearches.getStringOrNull(3) + val encoding = websearches.getStringOrNull(4) + + val options = encoding?.let{ + jsonObjectOf("encoding" to encoding).toString() + } + + database.execSQL( + "INSERT INTO `SearchAction` (`position`, `type`, `data`, `label`, `color`, `icon`, `customIcon`, `options`)" + + "VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + arrayOf( + position, + "url", + data, + label, + color, + if (icon == null) 0 else 1, + icon, + options + ) + ) + position++ + } + database.execSQL("DROP TABLE `Websearch`") + } +} \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_6_7.kt b/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_6_7.kt new file mode 100644 index 00000000..fee71670 --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_6_7.kt @@ -0,0 +1,14 @@ +package de.mm20.launcher2.database.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration_6_7 : Migration(6, 7) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE Searchable2 (`key` TEXT NOT NULL, `searchable` TEXT, `launchCount` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `hidden` INTEGER NOT NULL, `inAllApps` INTEGER NOT NULL, PRIMARY KEY(`key`))") + database.execSQL("INSERT INTO Searchable2 SELECT * FROM Searchable") + database.execSQL("DROP TABLE Searchable") + database.execSQL("ALTER TABLE Searchable2 RENAME TO Searchable") + } + +} \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_7_8.kt b/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_7_8.kt new file mode 100644 index 00000000..70fc11c9 --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_7_8.kt @@ -0,0 +1,14 @@ +package de.mm20.launcher2.database.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import de.mm20.launcher2.database.entities.ForecastEntity + +class Migration_7_8 : Migration(7, 8) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE IF NOT EXISTS `${ForecastEntity.TABLE_NAME}2` (`timestamp` INTEGER NOT NULL, `temperature` REAL NOT NULL, `minTemp` REAL NOT NULL, `maxTemp` REAL NOT NULL, `pressure` REAL NOT NULL, `humidity` REAL NOT NULL, `icon` INTEGER NOT NULL, `condition` TEXT NOT NULL, `clouds` INTEGER NOT NULL, `windSpeed` REAL NOT NULL, `windDirection` REAL NOT NULL, `rain` REAL NOT NULL, `snow` REAL NOT NULL, `night` INTEGER NOT NULL, `location` TEXT NOT NULL, `provider` TEXT NOT NULL, `providerUrl` TEXT NOT NULL, `rainPropability` INTEGER NOT NULL, `snowProbability` INTEGER NOT NULL, PRIMARY KEY(`timestamp`))") + database.execSQL("INSERT INTO ${ForecastEntity.TABLE_NAME}2 SELECT *, -1 as rainPropability, -1 as snowPropability FROM ${ForecastEntity.TABLE_NAME}") + database.execSQL("DROP TABLE ${ForecastEntity.TABLE_NAME}") + database.execSQL("ALTER TABLE ${ForecastEntity.TABLE_NAME}2 RENAME TO ${ForecastEntity.TABLE_NAME}") + } +} \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_8_9.kt b/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_8_9.kt new file mode 100644 index 00000000..5d2990e9 --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_8_9.kt @@ -0,0 +1,38 @@ +package de.mm20.launcher2.database.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import de.mm20.launcher2.database.entities.ForecastEntity + +class Migration_8_9 : Migration(8, 9) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + "CREATE TABLE IF NOT EXISTS `${ForecastEntity.TABLE_NAME}2` (" + + "`timestamp` INTEGER NOT NULL, " + + "`temperature` REAL NOT NULL, " + + "`minTemp` REAL NOT NULL, " + + "`maxTemp` REAL NOT NULL, " + + "`pressure` REAL NOT NULL, " + + "`humidity` REAL NOT NULL, " + + "`icon` INTEGER NOT NULL, " + + "`condition` TEXT NOT NULL, " + + "`clouds` INTEGER NOT NULL, " + + "`windSpeed` REAL NOT NULL, " + + "`windDirection` REAL NOT NULL, " + + "`rain` REAL NOT NULL, " + + "`snow` REAL NOT NULL, " + + "`night` INTEGER NOT NULL, " + + "`location` TEXT NOT NULL, " + + "`provider` TEXT NOT NULL, " + + "`providerUrl` TEXT NOT NULL, " + + "`rainProbability` INTEGER NOT NULL, " + + "`snowProbability` INTEGER NOT NULL, " + + "`updateTime` INTEGER NOT NULL, " + + "PRIMARY KEY(`timestamp`))" + ) + database.execSQL("INSERT INTO ${ForecastEntity.TABLE_NAME}2 SELECT timestamp, temperature, minTemp, maxTemp, pressure, humidity, icon, condition, clouds, windSpeed, windDirection, rain, snow, night, location, provider, providerUrl, rainPropability as rainProbability, snowProbability, 0 as updateTime FROM ${ForecastEntity.TABLE_NAME}") + database.execSQL("DROP TABLE ${ForecastEntity.TABLE_NAME}") + database.execSQL("ALTER TABLE ${ForecastEntity.TABLE_NAME}2 RENAME TO ${ForecastEntity.TABLE_NAME}") + } + +} \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_9_10.kt b/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_9_10.kt new file mode 100644 index 00000000..410ae0dc --- /dev/null +++ b/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_9_10.kt @@ -0,0 +1,11 @@ +package de.mm20.launcher2.database.migrations + +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class Migration_9_10 : Migration(9, 10) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("CREATE TABLE IF NOT EXISTS `Plugins` (`packageName` TEXT NOT NULL, `label` TEXT NOT NULL, `description` TEXT NOT NULL, `pluginClassName` TEXT NOT NULL, `enabled` INTEGER NOT NULL, PRIMARY KEY(`packageName`) );") + } + +} \ No newline at end of file diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml index 1aa3a66e..33943460 100644 --- a/i18n/src/main/res/values/strings.xml +++ b/i18n/src/main/res/values/strings.xml @@ -651,6 +651,7 @@ Favorites & hidden apps Settings Web search shortcuts + Web & app search shortcuts Built-in widgets Customizations The backup has been completed. diff --git a/search-actions/build.gradle.kts b/search-actions/build.gradle.kts index f17a0cd2..bc45fd9c 100644 --- a/search-actions/build.gradle.kts +++ b/search-actions/build.gradle.kts @@ -39,10 +39,14 @@ dependencies { implementation(libs.androidx.core) implementation(libs.koin.android) + implementation(libs.jsoup) + implementation(libs.okhttp) + implementation(libs.coil.core) implementation(project(":base")) implementation(project(":database")) implementation(project(":ktx")) implementation(project(":preferences")) + implementation(project(":crashreporter")) } \ No newline at end of file diff --git a/search-actions/src/main/java/de/mm20/launcher2/searchactions/Module.kt b/search-actions/src/main/java/de/mm20/launcher2/searchactions/Module.kt index a6d1245b..4425ecdf 100644 --- a/search-actions/src/main/java/de/mm20/launcher2/searchactions/Module.kt +++ b/search-actions/src/main/java/de/mm20/launcher2/searchactions/Module.kt @@ -4,6 +4,6 @@ import org.koin.android.ext.koin.androidContext import org.koin.dsl.module val searchActionsModule = module { - single { SearchActionRepositoryImpl() } + single { SearchActionRepositoryImpl(androidContext(), get()) } single { SearchActionServiceImpl(androidContext(), get(), TextClassifierImpl()) } } \ 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 f2ca6bbd..7c226123 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 @@ -1,14 +1,123 @@ package de.mm20.launcher2.searchactions +import android.content.Context +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.SearchActionBuilder +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.withContext +import org.json.JSONArray +import org.json.JSONException +import java.io.File +import java.util.UUID interface SearchActionRepository { fun getSearchActionBuilders(filter: TextType?): Flow> + + suspend fun export(toDir: File) + suspend fun import(fromDir: File) } -internal class SearchActionRepositoryImpl: SearchActionRepository { +internal class SearchActionRepositoryImpl( + private val context: Context, + private val database: AppDatabase +): SearchActionRepository { override fun getSearchActionBuilders(filter: TextType?): Flow> { TODO("Not yet implemented") } + + override suspend fun export(toDir: File) = withContext(Dispatchers.IO) { + val dao = database.backupDao() + var page = 0 + var iconCounter = 0 + do { + val websearches = dao.exportSearchActions(limit = 100, offset = page * 100) + val jsonArray = JSONArray() + for (websearch in websearches) { + var customIcon = websearch.customIcon + if (customIcon != null) { + val fileName = "asset.searchaction.${iconCounter.toString().padStart(4, '0')}" + val iconAssetFile = File(toDir, fileName) + File(customIcon).inputStream().use { inStream -> + iconAssetFile.outputStream().use { outStream -> + inStream.copyTo(outStream) + } + } + customIcon = fileName + + iconCounter++ + } + jsonArray.put( + jsonObjectOf( + "color" to websearch.color, + "label" to websearch.label, + "data" to websearch.data, + "icon" to websearch.icon, + "customIcon" to customIcon, + "options" to websearch.options, + "position" to websearch.position, + "type" to websearch.type, + ) + ) + } + + val file = File(toDir, "searchactions.${page.toString().padStart(4, '0')}") + file.bufferedWriter().use { + it.write(jsonArray.toString()) + } + page++ + } while (websearches.size == 100) + } + + override suspend fun import(fromDir: File) = withContext(Dispatchers.IO) { + val dao = database.backupDao() + dao.wipeSearchActions() + + val files = fromDir.listFiles { _, name -> name.startsWith("searchactions.") } ?: return@withContext + + for (file in files) { + val searchActions = mutableListOf() + try { + val jsonArray = JSONArray(file.inputStream().reader().readText()) + + for (i in 0 until jsonArray.length()) { + val json = jsonArray.getJSONObject(i) + + val customIcon = json.optString("customIcon").takeIf { it.isNotEmpty() } + + var iconFile: File? = null + + if (customIcon != null) { + val asset = File(fromDir, customIcon) + iconFile = File(context.filesDir, UUID.randomUUID().toString()) + asset.inputStream().use { inStream -> + iconFile.outputStream().use { outStream -> + inStream.copyTo(outStream) + } + } + } + + val entity = SearchActionEntity( + position = json.getInt("position"), + data = json.getString("data"), + color = json.optInt("color", 0), + label = json.getString("label"), + icon = json.optInt("icon", 0), + customIcon = iconFile?.absolutePath, + options = json.optString("options").takeIf { it.isNotEmpty() }, + type = json.getString("type"), + ) + searchActions.add(entity) + } + + dao.importSearchActions(searchActions) + + } catch (e: JSONException) { + CrashReporter.logException(e) + } + } + } } \ No newline at end of file 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 42d90a4e..2a39ff55 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 @@ -1,8 +1,20 @@ package de.mm20.launcher2.searchactions import android.content.Context +import android.graphics.Bitmap +import android.net.Uri +import android.util.Log +import android.util.Xml +import androidx.core.graphics.drawable.toBitmap +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 @@ -12,14 +24,33 @@ 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 import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow +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 +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.net.URL +import java.util.UUID interface SearchActionService { fun search(settings: SearchActionSettings, query: String): Flow> + + suspend fun importWebsearch(url: String, iconSize: Int): WebsearchActionBuilder? + suspend fun createIcon(uri: Uri, size: Int): String? + } internal class SearchActionServiceImpl( @@ -27,7 +58,10 @@ internal class SearchActionServiceImpl( private val repository: SearchActionRepository, private val textClassifier: TextClassifier, ) : SearchActionService { - override fun search(settings: SearchActionSettings, query: String): Flow> = flow { + override fun search( + settings: SearchActionSettings, + query: String + ): Flow> = flow { if (query.isBlank()) { emit(persistentListOf()) return@flow @@ -50,4 +84,125 @@ internal class SearchActionServiceImpl( emit(builders.mapNotNull { it.build(context, classificationResult) }.toImmutableList()) } + override suspend fun importWebsearch(url: String, iconSize: Int): WebsearchActionBuilder? = + withContext(Dispatchers.IO) { + try { + val u = if (url.startsWith("http://") || url.startsWith("https://")) { + url + } else { + "https://$url" + } + val document = Jsoup.parse(URL(u), 5000) + val metaElements = + document.select("link[rel=\"search\"][href][type=\"application/opensearchdescription+xml\"]") + val openSearchHref = metaElements + .getOrNull(0) + ?.absUrl("href") + ?.takeIf { it.isNotEmpty() } + ?: return@withContext run { + Log.d("MM20", "Specified URL does not implement the OpenSearch protocol") + null + } + + val httpClient = OkHttpClient() + val request = Request.Builder() + .url(openSearchHref) + .build() + val response = httpClient.newCall(request).execute() + val inputStream = response.body?.byteStream() ?: return@withContext null + + var label: String? = null + var urlTemplate: String? = null + var icon: String? = null + var iconSize: Int = 0 + var iconUrl: String? = null + + inputStream.use { + val parser = Xml.newPullParser() + parser.setInput(inputStream.reader()) + while (parser.next() != XmlPullParser.END_DOCUMENT) { + if (parser.eventType == XmlPullParser.START_TAG) { + when (parser.name) { + "ShortName" -> { + parser.next() + if (parser.eventType == XmlPullParser.TEXT) { + label = parser.text + } + } + + "LongName" -> { + parser.next() + if (parser.eventType == XmlPullParser.TEXT) { + if (label != null) label = parser.text + } + } + + "Image" -> { + val size = + parser.getAttributeValue(null, "width")?.toIntOrNull() ?: 0 + if (size > iconSize || iconUrl == null) { + parser.next() + if (parser.eventType == XmlPullParser.TEXT) { + iconUrl = parser.text + iconSize = size + } + } + } + + "Url" -> { + if (parser.getAttributeValue(null, "type") == "text/html") { + val rel = parser.getAttributeValue(null, "rel") + if (rel == null || rel == "results") { + val template = + parser.getAttributeValue(null, "template") + ?.takeIf { it.isNotEmpty() } ?: continue + urlTemplate = template + .replace("{searchTerms}", "\${1}") + .replace("{startPage?}", "1") + } + } + } + + else -> continue + } + } + } + + val localIconUrl = iconUrl?.let { + val uri = Uri.parse(it) + createIcon(uri, iconSize) + } + + return@withContext WebsearchActionBuilder( + label = label ?: "", + icon = if (localIconUrl == null) SearchActionIcon.Search else SearchActionIcon.Custom, + customIcon = localIconUrl, + urlTemplate = urlTemplate ?: "" + ) + } + } catch (e: IOException) { + CrashReporter.logException(e) + } catch (e: XmlPullParserException) { + CrashReporter.logException(e) + } + return@withContext null + } + + override suspend fun createIcon(uri: Uri, size: Int): String? = withContext( + Dispatchers.IO + ) { + val file = File(context.filesDir, UUID.randomUUID().toString()) + val imageRequest = ImageRequest.Builder(context) + .data(uri) + .size(size) + .scale(Scale.FIT) + .build() + val drawable = + context.imageLoader.execute(imageRequest).drawable ?: return@withContext null + val scaledIcon = drawable.toBitmap() + val out = FileOutputStream(file) + scaledIcon.compress(Bitmap.CompressFormat.PNG, 100, out) + out.close() + return@withContext file.absolutePath + } } \ No newline at end of file diff --git a/search-actions/src/main/java/de/mm20/launcher2/searchactions/actions/AppSearchAction.kt b/search-actions/src/main/java/de/mm20/launcher2/searchactions/actions/AppSearchAction.kt new file mode 100644 index 00000000..46182254 --- /dev/null +++ b/search-actions/src/main/java/de/mm20/launcher2/searchactions/actions/AppSearchAction.kt @@ -0,0 +1,24 @@ +package de.mm20.launcher2.searchactions.actions + +import android.app.SearchManager +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import de.mm20.launcher2.ktx.tryStartActivity + +data class AppSearchAction( + override val label: String, + val componentName: ComponentName, + val query: String, +): SearchAction { + override val icon: SearchActionIcon = SearchActionIcon.Search + override val iconColor: Int = 0 + + override fun start(context: Context) { + val intent = Intent(Intent.ACTION_SEARCH).apply { + component = componentName + putExtra(SearchManager.QUERY, query) + } + context.tryStartActivity(intent) + } +} \ No newline at end of file diff --git a/search-actions/src/main/java/de/mm20/launcher2/searchactions/actions/SearchAction.kt b/search-actions/src/main/java/de/mm20/launcher2/searchactions/actions/SearchAction.kt index 145a6c88..c966a249 100644 --- a/search-actions/src/main/java/de/mm20/launcher2/searchactions/actions/SearchAction.kt +++ b/search-actions/src/main/java/de/mm20/launcher2/searchactions/actions/SearchAction.kt @@ -10,16 +10,16 @@ interface SearchAction : Searchable { fun start(context: Context) } -enum class SearchActionIcon { - Search, - Website, - Alarm, - Timer, - Contact, - Phone, - Email, - Message, - Calendar, - Translate, - Custom, +enum class SearchActionIcon(value: Int) { + Search(0), + Custom(1), + Website(2), + Alarm(3), + Timer(4), + Contact(5), + Phone(6), + Email(7), + Message(8), + Calendar(9), + Translate(10), } \ No newline at end of file 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 new file mode 100644 index 00000000..e321f37d --- /dev/null +++ b/search-actions/src/main/java/de/mm20/launcher2/searchactions/builders/AppSearchActionBuilder.kt @@ -0,0 +1,22 @@ +package de.mm20.launcher2.searchactions.builders + +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 + +class AppSearchActionBuilder( + val label: String, + val activity: LauncherActivityInfo, + val filter: TextType? = null, +) : SearchActionBuilder { + override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? { + return AppSearchAction( + label = label, + componentName = activity.componentName, + query = classifiedQuery.text, + ) + } +} \ No newline at end of file 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 fc7d16ad..d7cca418 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 @@ -2,28 +2,26 @@ package de.mm20.launcher2.searchactions.builders import android.content.Context import android.net.Uri -import de.mm20.launcher2.searchactions.actions.SearchAction import de.mm20.launcher2.searchactions.TextClassificationResult -import de.mm20.launcher2.searchactions.TextType import de.mm20.launcher2.searchactions.actions.OpenUrlAction +import de.mm20.launcher2.searchactions.actions.SearchAction +import de.mm20.launcher2.searchactions.actions.SearchActionIcon import java.net.URLEncoder class WebsearchActionBuilder( val label: String, val urlTemplate: String, - val filter: TextType? = null, - val encoding: QueryEncoding, + val icon: SearchActionIcon = SearchActionIcon.Search, + val customIcon: String? = null, + val encoding: QueryEncoding = QueryEncoding.UrlEncode, ) : SearchActionBuilder { - override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? { - if (filter == null || classifiedQuery.type == filter) { - val url = urlTemplate.replace("\${1}", encodeQuery(classifiedQuery.text, encoding)) - return OpenUrlAction( - label = label, - url = url, - ) - } - return null + override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction { + val url = urlTemplate.replace("\${1}", encodeQuery(classifiedQuery.text, encoding)) + return OpenUrlAction( + label = label, + url = url, + ) } diff --git a/search/src/main/java/de/mm20/launcher2/search/Module.kt b/search/src/main/java/de/mm20/launcher2/search/Module.kt index 0c4c5554..8ee8a1a2 100644 --- a/search/src/main/java/de/mm20/launcher2/search/Module.kt +++ b/search/src/main/java/de/mm20/launcher2/search/Module.kt @@ -19,5 +19,4 @@ val searchModule = module { get(), ) } - single { WebsearchRepositoryImpl(androidContext(), get()) } } \ No newline at end of file diff --git a/search/src/main/java/de/mm20/launcher2/search/WebsearchRepository.kt b/search/src/main/java/de/mm20/launcher2/search/WebsearchRepository.kt deleted file mode 100644 index 6f02b060..00000000 --- a/search/src/main/java/de/mm20/launcher2/search/WebsearchRepository.kt +++ /dev/null @@ -1,306 +0,0 @@ -package de.mm20.launcher2.search - -import android.content.Context -import android.graphics.Bitmap -import android.net.Uri -import android.util.Log -import android.util.Xml -import androidx.core.graphics.drawable.toBitmap -import coil.imageLoader -import coil.request.ImageRequest -import coil.size.Scale -import de.mm20.launcher2.crashreporter.CrashReporter -import de.mm20.launcher2.database.AppDatabase -import de.mm20.launcher2.database.entities.WebsearchEntity -import de.mm20.launcher2.database.entities.WidgetEntity -import de.mm20.launcher2.ktx.jsonObjectOf -import de.mm20.launcher2.preferences.LauncherDataStore -import de.mm20.launcher2.search.data.Websearch -import kotlinx.coroutines.* -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.channelFlow -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.flow.map -import okhttp3.OkHttpClient -import okhttp3.Request -import org.json.JSONArray -import org.json.JSONException -import org.jsoup.Jsoup -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject -import org.xmlpull.v1.XmlPullParser -import org.xmlpull.v1.XmlPullParserException -import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import java.net.URL - -interface WebsearchRepository { - fun search(query: String): Flow> - - fun getWebsearches(): Flow> - - fun insertWebsearch(websearch: Websearch) - fun deleteWebsearch(websearch: Websearch) - - suspend fun importWebsearch(url: String, iconSize: Int): Websearch? - suspend fun createIcon(uri: Uri, size: Int): String? - - suspend fun export(toDir: File) - suspend fun import(fromDir: File) -} - -internal class WebsearchRepositoryImpl( - private val context: Context, - private val database: AppDatabase -) : WebsearchRepository, KoinComponent { - - private val dataStore: LauncherDataStore by inject() - - private val scope = CoroutineScope(Job() + Dispatchers.Default) - - override fun search(query: String): Flow> = channelFlow { - if (query.isEmpty()) { - send(emptyList()) - return@channelFlow - } - dataStore.data.map { it.webSearch.enabled }.collectLatest { - if (it) { - withContext(Dispatchers.IO) { - database.searchDao().getWebSearches().map { - it.map { Websearch(it, query) } - } - }.collectLatest { - send(it) - } - } else { - send(emptyList()) - } - } - } - - override fun getWebsearches(): Flow> = - database.searchDao().getWebSearches().map { - it.map { Websearch(it) } - } - - override fun insertWebsearch(websearch: Websearch) { - scope.launch { - withContext(Dispatchers.IO) { - database.searchDao().insertWebsearch(websearch.toDatabaseEntity()) - } - } - } - - override fun deleteWebsearch(websearch: Websearch) { - scope.launch { - withContext(Dispatchers.IO) { - database.searchDao().deleteWebsearch(websearch.toDatabaseEntity()) - } - } - } - - override suspend fun importWebsearch(url: String, iconSize: Int): Websearch? = - withContext(Dispatchers.IO) { - try { - val u = if (url.startsWith("http://") || url.startsWith("https://")) { - url - } else { - "https://$url" - } - val document = Jsoup.parse(URL(u), 5000) - val metaElements = - document.select("link[rel=\"search\"][href][type=\"application/opensearchdescription+xml\"]") - val openSearchHref = metaElements - .getOrNull(0) - ?.absUrl("href") - ?.takeIf { it.isNotEmpty() } - ?: return@withContext run { - Log.d("MM20", "Specified URL does not implement the OpenSearch protocol") - null - } - - val httpClient = OkHttpClient() - val request = Request.Builder() - .url(openSearchHref) - .build() - val response = httpClient.newCall(request).execute() - val inputStream = response.body?.byteStream() ?: return@withContext null - - var label: String? = null - var urlTemplate: String? = null - var icon: String? = null - var iconSize: Int = 0 - var iconUrl: String? = null - - inputStream.use { - val parser = Xml.newPullParser() - parser.setInput(inputStream.reader()) - while (parser.next() != XmlPullParser.END_DOCUMENT) { - if (parser.eventType == XmlPullParser.START_TAG) { - when (parser.name) { - "ShortName" -> { - parser.next() - if (parser.eventType == XmlPullParser.TEXT) { - label = parser.text - } - } - "LongName" -> { - parser.next() - if (parser.eventType == XmlPullParser.TEXT) { - if (label != null) label = parser.text - } - } - "Image" -> { - val size = - parser.getAttributeValue(null, "width")?.toIntOrNull() ?: 0 - if (size > iconSize || iconUrl == null) { - parser.next() - if (parser.eventType == XmlPullParser.TEXT) { - iconUrl = parser.text - iconSize = size - } - } - } - "Url" -> { - if (parser.getAttributeValue(null, "type") == "text/html") { - val rel = parser.getAttributeValue(null, "rel") - if (rel == null || rel == "results") { - val template = - parser.getAttributeValue(null, "template") - ?.takeIf { it.isNotEmpty() } ?: continue - urlTemplate = template - .replace("{searchTerms}", "\${1}") - .replace("{startPage?}", "1") - } - } - } - else -> continue - } - } - } - - val localIconUrl = iconUrl?.let { - val uri = Uri.parse(it) - createIcon(uri, iconSize) - } - - return@withContext Websearch( - urlTemplate = urlTemplate ?: "", - label = label ?: "", - icon = localIconUrl, - color = 0, - ) - } - } catch (e: IOException) { - CrashReporter.logException(e) - } catch (e: XmlPullParserException) { - CrashReporter.logException(e) - } - return@withContext null - } - - override suspend fun createIcon(uri: Uri, size: Int): String? = withContext( - Dispatchers.IO - ) { - val file = File(context.dataDir, System.currentTimeMillis().toString()) - val imageRequest = ImageRequest.Builder(context) - .data(uri) - .size(size) - .scale(Scale.FIT) - .build() - val drawable = context.imageLoader.execute(imageRequest).drawable ?: return@withContext null - val scaledIcon = drawable.toBitmap() - val out = FileOutputStream(file) - scaledIcon.compress(Bitmap.CompressFormat.PNG, 100, out) - out.close() - return@withContext file.absolutePath - } - - override suspend fun export(toDir: File) = withContext(Dispatchers.IO) { - val dao = database.backupDao() - var page = 0 - var iconCounter = 0 - do { - val websearches = dao.exportWebsearches(limit = 100, offset = page * 100) - val jsonArray = JSONArray() - for (websearch in websearches) { - var icon = websearch.icon - if (icon != null) { - val fileName = "asset.websearch.${iconCounter.toString().padStart(4, '0')}" - val iconAssetFile = File(toDir, fileName) - File(icon).inputStream().use { inStream -> - iconAssetFile.outputStream().use { outStream -> - inStream.copyTo(outStream) - } - } - icon = fileName - - iconCounter++ - } - jsonArray.put( - jsonObjectOf( - "color" to websearch.color, - "label" to websearch.label, - "template" to websearch.urlTemplate, - "icon" to icon, - "encoding" to websearch.encoding, - ) - ) - } - - val file = File(toDir, "websearches.${page.toString().padStart(4, '0')}") - file.bufferedWriter().use { - it.write(jsonArray.toString()) - } - page++ - } while (websearches.size == 100) - } - - override suspend fun import(fromDir: File) = withContext(Dispatchers.IO) { - val dao = database.backupDao() - dao.wipeWebsearches() - - val files = fromDir.listFiles { _, name -> name.startsWith("websearches.") } ?: return@withContext - - for (file in files) { - val websearches = mutableListOf() - try { - val jsonArray = JSONArray(file.inputStream().reader().readText()) - - for (i in 0 until jsonArray.length()) { - val json = jsonArray.getJSONObject(i) - - val icon = json.optString("icon").takeIf { it.isNotEmpty() } - - var iconFile: File? = null - - if (icon != null) { - val asset = File(fromDir, icon) - iconFile = File(context.filesDir, icon) - asset.inputStream().use { inStream -> - iconFile.outputStream().use { outStream -> - inStream.copyTo(outStream) - } - } - } - - val entity = WebsearchEntity( - urlTemplate = json.getString("template"), - color = json.optInt("color", 0), - label = json.getString("label"), - icon = iconFile?.absolutePath, - encoding = json.optInt("encoding"), - id = null - ) - websearches.add(entity) - } - - dao.importWebsearches(websearches) - - } catch (e: JSONException) { - CrashReporter.logException(e) - } - } - } -} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/common/RestoreBackupSheet.kt b/ui/src/main/java/de/mm20/launcher2/ui/common/RestoreBackupSheet.kt index 6080169e..18eeb732 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/common/RestoreBackupSheet.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/common/RestoreBackupSheet.kt @@ -170,7 +170,7 @@ fun RestoreBackupSheet( imageVector = when (component) { BackupComponent.Favorites -> Icons.Rounded.Star BackupComponent.Settings -> Icons.Rounded.Settings - BackupComponent.Websearches -> Icons.Rounded.TravelExplore + BackupComponent.SearchActions -> Icons.Rounded.ArrowOutward BackupComponent.Widgets -> Icons.Rounded.Widgets BackupComponent.Customizations -> Icons.Rounded.Edit }, @@ -181,7 +181,7 @@ fun RestoreBackupSheet( when (component) { BackupComponent.Favorites -> R.string.backup_component_favorites BackupComponent.Settings -> R.string.backup_component_settings - BackupComponent.Websearches -> R.string.backup_component_websearches + BackupComponent.SearchActions -> R.string.backup_component_searchactions BackupComponent.Widgets -> R.string.backup_component_widgets BackupComponent.Customizations -> R.string.backup_component_customizations } 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 cc528e35..e9cf207a 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 @@ -10,7 +10,6 @@ import de.mm20.launcher2.permissions.PermissionsManager import de.mm20.launcher2.preferences.LauncherDataStore import de.mm20.launcher2.search.SavableSearchable import de.mm20.launcher2.search.SearchService -import de.mm20.launcher2.search.WebsearchRepository import de.mm20.launcher2.search.data.AppShortcut import de.mm20.launcher2.search.data.Calculator import de.mm20.launcher2.search.data.CalendarEvent @@ -40,7 +39,6 @@ class SearchVM : ViewModel(), KoinComponent { private val dataStore: LauncherDataStore by inject() private val searchService: SearchService by inject() - private val websearchRepository: WebsearchRepository by inject() val isSearching = MutableLiveData(false) val searchQuery = MutableLiveData("") diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt index 9958b77d..015b598f 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/SettingsActivity.kt @@ -47,7 +47,6 @@ import de.mm20.launcher2.ui.settings.search.SearchSettingsScreen import de.mm20.launcher2.ui.settings.searchactions.SearchActionsSettingsScreen import de.mm20.launcher2.ui.settings.unitconverter.UnitConverterSettingsScreen import de.mm20.launcher2.ui.settings.weatherwidget.WeatherWidgetSettingsScreen -import de.mm20.launcher2.ui.settings.websearch.WebSearchSettingsScreen import de.mm20.launcher2.ui.settings.widgets.WidgetsSettingsScreen import de.mm20.launcher2.ui.settings.wikipedia.WikipediaSettingsScreen import de.mm20.launcher2.ui.theme.LauncherTheme @@ -119,9 +118,6 @@ class SettingsActivity : BaseActivity() { composable("settings/search/files") { FileSearchSettingsScreen() } - composable("settings/search/websearch") { - WebSearchSettingsScreen() - } composable("settings/search/searchactions") { SearchActionsSettingsScreen() } diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/backup/CreateBackupSheet.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/backup/CreateBackupSheet.kt index e15e83b5..52f3ec66 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/settings/backup/CreateBackupSheet.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/settings/backup/CreateBackupSheet.kt @@ -135,11 +135,11 @@ fun CreateBackupSheet( } ) BackupableComponent( - title = stringResource(R.string.backup_component_websearches), + title = stringResource(R.string.backup_component_searchactions), icon = Icons.Rounded.TravelExplore, - checked = components.contains(BackupComponent.Websearches), + checked = components.contains(BackupComponent.SearchActions), onCheckedChange = { - viewModel.toggleComponent(BackupComponent.Websearches) + viewModel.toggleComponent(BackupComponent.SearchActions) } ) SmallMessage( diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/websearch/WebSearchSettingsScreen.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/websearch/WebSearchSettingsScreen.kt deleted file mode 100644 index 02a132bb..00000000 --- a/ui/src/main/java/de/mm20/launcher2/ui/settings/websearch/WebSearchSettingsScreen.kt +++ /dev/null @@ -1,692 +0,0 @@ -package de.mm20.launcher2.ui.settings.websearch - -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -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.layout.size -import androidx.compose.foundation.layout.sizeIn -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.lazy.LazyRow -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Add -import androidx.compose.material.icons.rounded.ArrowForward -import androidx.compose.material.icons.rounded.Check -import androidx.compose.material.icons.rounded.CloudDownload -import androidx.compose.material.icons.rounded.MoreVert -import androidx.compose.material.icons.rounded.Search -import androidx.compose.material.icons.rounded.Tag -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.Divider -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.material3.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.livedata.observeAsState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.luminance -import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import coil.compose.AsyncImage -import com.godaddy.android.colorpicker.ClassicColorPicker -import de.mm20.launcher2.search.data.Websearch -import de.mm20.launcher2.ui.R -import de.mm20.launcher2.ui.component.BottomSheetDialog -import de.mm20.launcher2.ui.component.preferences.ListPreference -import de.mm20.launcher2.ui.component.preferences.Preference -import de.mm20.launcher2.ui.component.preferences.PreferenceCategory -import de.mm20.launcher2.ui.component.preferences.PreferenceScreen -import de.mm20.launcher2.ui.ktx.toHexString -import de.mm20.launcher2.ui.ktx.toPixels -import kotlinx.coroutines.launch -import java.io.File - -@Composable -fun WebSearchSettingsScreen() { - val viewModel: WebSearchSettingsScreenVM = viewModel() - val websearches by viewModel.websearches.observeAsState(emptyList()) - var showNewDialog by remember { mutableStateOf(false) } - PreferenceScreen( - title = stringResource(R.string.preference_search_websearch), - floatingActionButton = { - FloatingActionButton(onClick = { showNewDialog = true }) { - Icon(imageVector = Icons.Rounded.Add, contentDescription = null) - } - }, - helpUrl = "https://kvaesitso.mm20.de/docs/user-guide/search/websearches" - ) { - item { - PreferenceCategory { - for (websearch in websearches) { - WebsearchPreference( - value = websearch, - onValueChanged = { - viewModel.updateWebsearch(it) - }, - onValueDeleted = { viewModel.deleteWebsearch(it) } - ) - } - } - } - } - if (showNewDialog) { - EditWebsearchDialog( - title = stringResource(R.string.websearch_dialog_create_title), - value = Websearch( - label = "", - urlTemplate = "", - color = 0, - icon = null - ), - onValueSaved = { - viewModel.createWebsearch(it) - showNewDialog = false - }, - onCancel = { - showNewDialog = false - }, - enableImport = true - ) - } -} - -@Composable -fun WebsearchPreference( - value: Websearch, - onValueChanged: (Websearch) -> Unit, - onValueDeleted: (Websearch) -> Unit, -) { - var showDialog by remember { mutableStateOf(false) } - Preference( - title = value.label, - summary = value.urlTemplate, - onClick = { - showDialog = true - }, - icon = { - val icon = value.icon - if (icon == null) { - Icon( - imageVector = Icons.Rounded.Search, - contentDescription = null, - tint = value.color.takeIf { it != 0 }?.let { Color(it) } - ?: MaterialTheme.colorScheme.primary, - ) - } else { - AsyncImage( - model = File(icon), - contentDescription = null, - modifier = Modifier.sizeIn(maxWidth = 24.dp, maxHeight = 24.dp), - contentScale = ContentScale.Inside - ) - } - } - ) - if (showDialog) { - EditWebsearchDialog( - title = stringResource(R.string.websearch_dialog_edit_title), - value = value, - onValueSaved = { - onValueChanged(it) - showDialog = false - }, - onCancel = { - showDialog = false - }, - onValueDeleted = onValueDeleted - ) - } -} - -@Composable -fun EditWebsearchDialog( - title: String, - value: Websearch, - onValueSaved: (Websearch) -> Unit, - onValueDeleted: ((Websearch) -> Unit)? = null, - onCancel: () -> Unit, - enableImport: Boolean = false -) { - var showDropdown by remember { mutableStateOf(false) } - - var label by remember { mutableStateOf(value.label) } - var showError by remember { mutableStateOf(false) } - var urlTemplate by remember { mutableStateOf(value.urlTemplate) } - var encoding by remember { mutableStateOf(value.encoding) } - var color by remember { mutableStateOf(value.color) } - var icon by remember { mutableStateOf(value.icon) } - - val scope = rememberCoroutineScope() - - var showImport by remember { mutableStateOf(false) } - var loadingImport by remember { mutableStateOf(false) } - var importError by remember { mutableStateOf(false) } - - val viewModel: WebSearchSettingsScreenVM = viewModel() - - val iconSizePx = 32.dp.toPixels().toInt() - - val chooseIconLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.GetContent() - ) { - if (it != null) { - scope.launch { - icon = viewModel.createIcon(it, iconSizePx) - } - } - } - - - BottomSheetDialog(onDismissRequest = onCancel, - title = { Text(title) }, - actions = { - if (enableImport) { - Box { - IconButton(onClick = { - showImport = !showImport - importError = false - }) { - Icon( - imageVector = Icons.Rounded.CloudDownload, - contentDescription = null - ) - } - - } - } - if (onValueDeleted != null) { - Box { - IconButton(onClick = { - showDropdown = true - }) { - Icon( - imageVector = Icons.Rounded.MoreVert, - contentDescription = null - ) - } - DropdownMenu( - expanded = showDropdown, - onDismissRequest = { showDropdown = false }) { - DropdownMenuItem( - text = { - Text( - text = stringResource(R.string.menu_delete) - ) - }, - onClick = { - onValueDeleted(value) - onCancel() - }) - } - } - } - }, - confirmButton = { - Button(onClick = { - if (urlTemplate.contains("\${1}")) { - value.label = label - value.urlTemplate = urlTemplate - if (value.icon != icon) { - value.icon?.let { - viewModel.deleteIcon(it) - } - } - value.icon = icon - value.color = color - value.encoding = encoding - onValueSaved(value) - } else { - showError = true - } - }) { - Text(stringResource(R.string.save)) - } - }, - dismissButton = { - OutlinedButton(onClick = { - if (icon != value.icon) { - icon?.let { viewModel.deleteIcon(it) } - } - onCancel() - }) { - Text(stringResource(android.R.string.cancel)) - } - } - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .verticalScroll(rememberScrollState()) - ) { - AnimatedVisibility(showImport) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 24.dp) - ) { - Column( - modifier = Modifier - .padding(horizontal = 16.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - var importUrl by remember { mutableStateOf("") } - OutlinedTextField( - modifier = Modifier - .weight(1f) - .padding(bottom = 16.dp, top = 8.dp, end = 8.dp), - label = { Text(stringResource(R.string.websearch_dialog_import_url)) }, - value = importUrl, - onValueChange = { - importUrl = it - importError = false - }, - textStyle = MaterialTheme.typography.bodyLarge, - ) - if (loadingImport) { - CircularProgressIndicator( - modifier = Modifier - .padding(12.dp) - .size(24.dp) - ) - } else { - IconButton(onClick = { - scope.launch { - loadingImport = true - val websearch = - viewModel.importWebsearch( - importUrl, - iconSizePx - ) - if (websearch != null) { - label = websearch.label - icon = websearch.icon - urlTemplate = websearch.urlTemplate - color = websearch.color - showImport = false - } else { - importError = true - } - loadingImport = false - } - }) { - Icon( - imageVector = Icons.Rounded.ArrowForward, - contentDescription = null - ) - } - } - } - AnimatedVisibility(importError) { - Column( - modifier = Modifier.padding(bottom = 8.dp) - ) { - Text( - text = stringResource(R.string.websearch_dialog_import_error), - style = MaterialTheme.typography.labelSmall - ) - TextButton( - modifier = Modifier.align(Alignment.End), - onClick = { showImport = false }) { - Text( - text = stringResource(android.R.string.ok), - style = MaterialTheme.typography.labelMedium - ) - } - } - } - } - } - } - - if (icon != null) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - AsyncImage( - model = icon?.let { File(it) }, - contentDescription = null, - modifier = Modifier - .padding(end = 16.dp) - .size(48.dp) - ) - TextButton( - onClick = { - chooseIconLauncher.launch("image/*") - }, - modifier = Modifier.padding(4.dp) - ) { - Text( - stringResource(R.string.websearch_dialog_replace_icon), - ) - } - TextButton( - onClick = { - icon = null - }, - modifier = Modifier.padding(4.dp), - colors = ButtonDefaults.textButtonColors( - contentColor = MaterialTheme.colorScheme.error - ) - ) { - Text( - stringResource(R.string.websearch_dialog_delete_icon), - ) - } - } - } else { - ColorPicker( - value = color, - onColorSelected = { color = it } - ) - TextButton( - onClick = { - chooseIconLauncher.launch("image/*") - }, - modifier = Modifier - .padding(4.dp) - .align(Alignment.End) - ) { - Text( - stringResource(R.string.websearch_dialog_custom_icon), - ) - } - - } - - OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .padding(top = 24.dp), - value = label, - onValueChange = { - label = it - }, - label = { - Text(text = stringResource(R.string.websearch_dialog_name)) - }, - textStyle = MaterialTheme.typography.bodyLarge, - ) - OutlinedTextField( - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp), - value = urlTemplate, - onValueChange = { - urlTemplate = it - }, - label = { - Text(text = stringResource(R.string.websearch_dialog_url)) - }, - textStyle = MaterialTheme.typography.bodyLarge, - ) - AnimatedVisibility(showError) { - Text( - modifier = Modifier.padding(top = 8.dp), - text = stringResource(R.string.websearch_dialog_url_error), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.error - ) - } - Text( - modifier = Modifier.padding(top = 8.dp), - text = stringResource(R.string.websearch_dialog_url_description), - style = MaterialTheme.typography.labelMedium - ) - - var showAdvanced by remember { mutableStateOf(false) } - - AnimatedVisibility(!showAdvanced) { - TextButton( - modifier = Modifier.padding(vertical = 16.dp).align(Alignment.End), - onClick = { showAdvanced = true }) { - Text(stringResource(R.string.websearch_dialog_advanced)) - } - } - - AnimatedVisibility(showAdvanced) { - Column( - modifier = Modifier.padding(top = 16.dp) - ) { - Divider() - ListPreference( - title = stringResource(R.string.websearch_dialog_query_encoding), - items = listOf( - stringResource(R.string.websearch_dialog_query_encoding_url) to Websearch.QueryEncoding.UrlEncode, - stringResource(R.string.websearch_dialog_query_encoding_form) to Websearch.QueryEncoding.FormData, - stringResource(R.string.websearch_dialog_query_encoding_none) to Websearch.QueryEncoding.None, - ), - iconPadding = false, - value = encoding, - onValueChanged = { - encoding = it - } - ) - } - } - } - } -} - -@Composable -private fun ColorPicker( - value: Int, - onColorSelected: (Int) -> Unit -) { - var selectedColorIndex = -1 - val isCustomColor = !ColorPresets.contains(Color(value)) && value != 0 - val listState = rememberLazyListState() - - var showCustomColorPicker by remember { mutableStateOf(false) } - - Column { - AnimatedVisibility(!showCustomColorPicker) { - LazyRow( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.height(64.dp), - state = listState - ) { - item { - if (value == 0) selectedColorIndex = 0 - ColorSwatch( - color = MaterialTheme.colorScheme.primary, - checked = value == 0, - onClick = { - onColorSelected(0) - } - ) - } - items(ColorPresets) { - ColorSwatch( - color = it, - checked = value == it.toArgb(), - onClick = { - onColorSelected(it.toArgb()) - } - ) - } - item { - CustomColorSwatch( - checked = isCustomColor, - onClick = { - showCustomColorPicker = true - } - ) - } - } - LaunchedEffect(null) { - if (isCustomColor) listState.scrollToItem(ColorPresets.size + 1) - else if (value != 0) listState.scrollToItem(ColorPresets.indexOf(Color(value)) + 1) - } - } - AnimatedVisibility(showCustomColorPicker) { - Column { - ClassicColorPicker( - color = Color(value), - showAlphaBar = false, - modifier = Modifier.height(200.dp), - onColorChanged = { - onColorSelected(it.toColor().toArgb()) - }) - Row( - modifier = Modifier - .padding(bottom = 24.dp, top = 8.dp) - .fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - var textFieldValue by remember(value) { - mutableStateOf( - Color(value).toHexString().substring(1) - ) - } - TextField( - value = textFieldValue, - leadingIcon = { - Icon(imageVector = Icons.Rounded.Tag, contentDescription = null) - }, - onValueChange = { - textFieldValue = it - if (it.length == 6) it.toLongOrNull(16)?.let { - onColorSelected((it or 0xFF000000).toInt()) - } - }, - singleLine = true, - modifier = Modifier.width(150.dp), - textStyle = MaterialTheme.typography.bodyLarge, - ) - TextButton(onClick = { showCustomColorPicker = false }) { - Text( - stringResource(android.R.string.ok), - style = MaterialTheme.typography.labelMedium - ) - } - } - } - } - } - -} - -@Composable -private fun ColorSwatch( - color: Color, - checked: Boolean, - onClick: () -> Unit -) { - Box( - modifier = Modifier - .padding(horizontal = 12.dp) - .size(48.dp) - .clip(CircleShape) - .background(color) - .clickable { onClick() }, - contentAlignment = Alignment.Center - ) { - if (checked) { - Icon( - imageVector = Icons.Rounded.Check, contentDescription = null, - tint = if (color.luminance() > 0.5f) Color.Black else Color.White - ) - } - } -} - -@Composable -private fun CustomColorSwatch( - checked: Boolean, - onClick: () -> Unit -) { - Box( - modifier = Modifier - .padding(horizontal = 12.dp) - .size(48.dp) - .clip(CircleShape) - .clickable { onClick() }, - contentAlignment = Alignment.Center - ) { - Canvas(modifier = Modifier.fillMaxSize()) { - val brush = Brush.sweepGradient( - listOf( - Color.Red, - Color.Magenta, - Color.Blue, - Color.Cyan, - Color.Green, - Color.Yellow, - Color.Red - ) - ) - - drawRect(brush) - } - if (checked) { - Icon( - imageVector = Icons.Rounded.Check, contentDescription = null, - tint = Color.White - ) - } - } -} - -private val ColorPresets = listOf( - Color(0xFFEF5350), - Color(0xFFEC407A), - Color(0xFFAB47BC), - Color(0xFF7E57C2), - Color(0xFF5C6BC0), - Color(0xFF42A5F5), - Color(0xFF29B6F6), - Color(0xFF26C6DA), - Color(0xFF26A69A), - Color(0xFF66BB6A), - Color(0xFF9CCC65), - Color(0xFFD4E157), - Color(0xFFFFEE58), - Color(0xFFFFCA28), - Color(0xFFFFA726), - Color(0xFFFF7043), - Color(0xFF8D6E63), - Color(0xFFBDBDBD), - Color(0xFF78909C), -) diff --git a/ui/src/main/java/de/mm20/launcher2/ui/settings/websearch/WebSearchSettingsScreenVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/settings/websearch/WebSearchSettingsScreenVM.kt deleted file mode 100644 index 3e73cae9..00000000 --- a/ui/src/main/java/de/mm20/launcher2/ui/settings/websearch/WebSearchSettingsScreenVM.kt +++ /dev/null @@ -1,55 +0,0 @@ -package de.mm20.launcher2.ui.settings.websearch - -import android.net.Uri -import androidx.lifecycle.ViewModel -import androidx.lifecycle.asLiveData -import androidx.lifecycle.viewModelScope -import de.mm20.launcher2.search.WebsearchRepository -import de.mm20.launcher2.search.data.Websearch -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.koin.core.component.KoinComponent -import org.koin.core.component.inject -import java.io.File - -class WebSearchSettingsScreenVM: ViewModel(), KoinComponent { - private val repository: WebsearchRepository by inject() - - val websearches = repository.getWebsearches().asLiveData() - - fun createWebsearch(websearch: Websearch) { - repository.insertWebsearch(websearch) - } - - fun updateWebsearch(websearch: Websearch) { - repository.insertWebsearch(websearch) - } - - fun deleteWebsearch(websearch: Websearch) { - websearch.icon?.let { deleteIcon(it) } - repository.deleteWebsearch(websearch) - } - - - /** - * Read a user-selected icon, scale it down and copy it to the app's data dir - * @return the absolute path of the copied file - */ - suspend fun createIcon(uri: Uri, size: Int): String? { - return repository.createIcon(uri, size) - } - - suspend fun importWebsearch(url: String, size: Int): Websearch? { - return repository.importWebsearch(url, size) - } - - - fun deleteIcon(path: String) { - viewModelScope.launch { - withContext(Dispatchers.IO) { - File(path).delete() - } - } - } -} \ No newline at end of file