Migrate websearches to search actions
This commit is contained in:
parent
f862a578a1
commit
872f55625f
@ -42,7 +42,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation(project(":favorites"))
|
implementation(project(":favorites"))
|
||||||
implementation(project(":widgets"))
|
implementation(project(":widgets"))
|
||||||
implementation(project(":search"))
|
implementation(project(":search-actions"))
|
||||||
implementation(project(":preferences"))
|
implementation(project(":preferences"))
|
||||||
implementation(project(":ktx"))
|
implementation(project(":ktx"))
|
||||||
implementation(project(":customattrs"))
|
implementation(project(":customattrs"))
|
||||||
|
|||||||
@ -5,7 +5,7 @@ enum class BackupComponent(val value: String) {
|
|||||||
Favorites("favorites"),
|
Favorites("favorites"),
|
||||||
Widgets("widgets"),
|
Widgets("widgets"),
|
||||||
Customizations("customizations"),
|
Customizations("customizations"),
|
||||||
Websearches("websearches");
|
SearchActions("searchactions");
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun fromValue(value: String): BackupComponent? {
|
fun fromValue(value: String): BackupComponent? {
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import de.mm20.launcher2.favorites.FavoritesRepository
|
|||||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||||
import de.mm20.launcher2.preferences.export
|
import de.mm20.launcher2.preferences.export
|
||||||
import de.mm20.launcher2.preferences.import
|
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 de.mm20.launcher2.widgets.WidgetRepository
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@ -23,7 +23,7 @@ class BackupManager(
|
|||||||
private val dataStore: LauncherDataStore,
|
private val dataStore: LauncherDataStore,
|
||||||
private val favoritesRepository: FavoritesRepository,
|
private val favoritesRepository: FavoritesRepository,
|
||||||
private val widgetRepository: WidgetRepository,
|
private val widgetRepository: WidgetRepository,
|
||||||
private val websearchRepository: WebsearchRepository,
|
private val searchActionRepository: SearchActionRepository,
|
||||||
private val customAttrsRepository: CustomAttributesRepository,
|
private val customAttrsRepository: CustomAttributesRepository,
|
||||||
) {
|
) {
|
||||||
private val scope = CoroutineScope(Dispatchers.Default + Job())
|
private val scope = CoroutineScope(Dispatchers.Default + Job())
|
||||||
@ -70,8 +70,8 @@ class BackupManager(
|
|||||||
widgetRepository.export(backupDir)
|
widgetRepository.export(backupDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (include.contains(BackupComponent.Websearches)) {
|
if (include.contains(BackupComponent.SearchActions)) {
|
||||||
websearchRepository.export(backupDir)
|
searchActionRepository.export(backupDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (include.contains(BackupComponent.Customizations)) {
|
if (include.contains(BackupComponent.Customizations)) {
|
||||||
@ -111,8 +111,8 @@ class BackupManager(
|
|||||||
widgetRepository.import(restoreDir)
|
widgetRepository.import(restoreDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (include.contains(BackupComponent.Websearches)) {
|
if (include.contains(BackupComponent.SearchActions)) {
|
||||||
websearchRepository.import(restoreDir)
|
searchActionRepository.import(restoreDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (include.contains(BackupComponent.Customizations)) {
|
if (include.contains(BackupComponent.Customizations)) {
|
||||||
@ -183,7 +183,7 @@ class BackupManager(
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val BackupFormatMajor = 1
|
private const val BackupFormatMajor = 1
|
||||||
private const val BackupFormatMinor = 3
|
private const val BackupFormatMinor = 4
|
||||||
internal const val BackupFormat = "$BackupFormatMajor.$BackupFormatMinor"
|
internal const val BackupFormat = "$BackupFormatMajor.$BackupFormatMinor"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
468
database/schemas/de.mm20.launcher2.database.AppDatabase/19.json
Normal file
468
database/schemas/de.mm20.launcher2.database.AppDatabase/19.json
Normal file
@ -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')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,18 +7,34 @@ import androidx.room.Database
|
|||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
import androidx.room.TypeConverters
|
import androidx.room.TypeConverters
|
||||||
import androidx.room.migration.Migration
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
import de.mm20.launcher2.database.entities.*
|
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,
|
@Database(
|
||||||
|
entities = [
|
||||||
|
ForecastEntity::class,
|
||||||
SavedSearchableEntity::class,
|
SavedSearchableEntity::class,
|
||||||
WebsearchEntity::class,
|
|
||||||
CurrencyEntity::class,
|
CurrencyEntity::class,
|
||||||
IconEntity::class,
|
IconEntity::class,
|
||||||
IconPackEntity::class,
|
IconPackEntity::class,
|
||||||
WidgetEntity::class,
|
WidgetEntity::class,
|
||||||
CustomAttributeEntity::class], version = 18, exportSchema = true)
|
CustomAttributeEntity::class,
|
||||||
|
SearchActionEntity::class,
|
||||||
|
], version = 19, exportSchema = true
|
||||||
|
)
|
||||||
@TypeConverters(ComponentNameConverter::class, StringListConverter::class)
|
@TypeConverters(ComponentNameConverter::class, StringListConverter::class)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|
||||||
@ -39,15 +55,22 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
.addCallback(object : Callback() {
|
.addCallback(object : Callback() {
|
||||||
override fun onCreate(db: SupportSQLiteDatabase) {
|
override fun onCreate(db: SupportSQLiteDatabase) {
|
||||||
super.onCreate(db)
|
super.onCreate(db)
|
||||||
db.execSQL("INSERT INTO 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 );")
|
|
||||||
|
|
||||||
db.execSQL("INSERT INTO Widget (type, data, height, position, label) VALUES " +
|
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', 'weather', -1, 0, '${context.getString(R.string.widget_name_weather)}')," +
|
||||||
"('internal', 'music', -1, 1, '${context.getString(R.string.widget_name_music)}')," +
|
"('internal', 'music', -1, 1, '${context.getString(R.string.widget_name_music)}')," +
|
||||||
"('internal', 'calendar', -1, 2, '${context.getString(R.string.widget_name_calendar)}');")
|
"('internal', 'calendar', -1, 2, '${context.getString(R.string.widget_name_calendar)}');"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.addMigrations(
|
.addMigrations(
|
||||||
@ -63,6 +86,7 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
Migration_15_16(),
|
Migration_15_16(),
|
||||||
Migration_16_17(),
|
Migration_16_17(),
|
||||||
Migration_17_18(),
|
Migration_17_18(),
|
||||||
|
Migration_18_19(),
|
||||||
).build()
|
).build()
|
||||||
if (_instance == null) _instance = instance
|
if (_instance == null) _instance = instance
|
||||||
return instance
|
return instance
|
||||||
@ -70,118 +94,3 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -6,6 +6,7 @@ import androidx.room.OnConflictStrategy
|
|||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import de.mm20.launcher2.database.entities.CustomAttributeEntity
|
import de.mm20.launcher2.database.entities.CustomAttributeEntity
|
||||||
import de.mm20.launcher2.database.entities.SavedSearchableEntity
|
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.WebsearchEntity
|
||||||
import de.mm20.launcher2.database.entities.WidgetEntity
|
import de.mm20.launcher2.database.entities.WidgetEntity
|
||||||
|
|
||||||
@ -30,14 +31,14 @@ interface BackupRestoreDao {
|
|||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun importWidgets(items: List<WidgetEntity>)
|
suspend fun importWidgets(items: List<WidgetEntity>)
|
||||||
|
|
||||||
@Query("DELETE FROM Websearch")
|
@Query("DELETE FROM SearchAction")
|
||||||
suspend fun wipeWebsearches()
|
suspend fun wipeSearchActions()
|
||||||
|
|
||||||
@Query("SELECT * FROM Websearch LIMIT :limit OFFSET :offset")
|
@Query("SELECT * FROM SearchAction LIMIT :limit OFFSET :offset")
|
||||||
suspend fun exportWebsearches(limit: Int, offset: Int): List<WebsearchEntity>
|
suspend fun exportSearchActions(limit: Int, offset: Int): List<SearchActionEntity>
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
suspend fun importWebsearches(items: List<WebsearchEntity>)
|
suspend fun importSearchActions(items: List<SearchActionEntity>)
|
||||||
|
|
||||||
@Query("DELETE FROM CustomAttributes")
|
@Query("DELETE FROM CustomAttributes")
|
||||||
suspend fun wipeCustomAttributes()
|
suspend fun wipeCustomAttributes()
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package de.mm20.launcher2.database
|
|||||||
|
|
||||||
import androidx.room.*
|
import androidx.room.*
|
||||||
import de.mm20.launcher2.database.entities.SavedSearchableEntity
|
import de.mm20.launcher2.database.entities.SavedSearchableEntity
|
||||||
import de.mm20.launcher2.database.entities.WebsearchEntity
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
@ -114,18 +113,6 @@ interface SearchDao {
|
|||||||
@Query("SELECT * FROM SEARCHABLE WHERE hidden = 1")
|
@Query("SELECT * FROM SEARCHABLE WHERE hidden = 1")
|
||||||
fun getHiddenItems(): Flow<List<SavedSearchableEntity>>
|
fun getHiddenItems(): Flow<List<SavedSearchableEntity>>
|
||||||
|
|
||||||
@Query("SELECT * FROM Websearch ORDER BY label ASC")
|
|
||||||
fun getWebSearches(): Flow<List<WebsearchEntity>>
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
|
||||||
fun insertWebsearch(websearch: WebsearchEntity)
|
|
||||||
|
|
||||||
@Delete
|
|
||||||
fun deleteWebsearch(websearch: WebsearchEntity)
|
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
|
||||||
fun insertAllWebsearches(websearches: List<WebsearchEntity>)
|
|
||||||
|
|
||||||
@Query("UPDATE Searchable SET launchCount = launchCount + 1 WHERE `key` = :key")
|
@Query("UPDATE Searchable SET launchCount = launchCount + 1 WHERE `key` = :key")
|
||||||
fun incrementExistingLaunchCount(key: String)
|
fun incrementExistingLaunchCount(key: String)
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,16 @@
|
|||||||
package de.mm20.launcher2.database.entities
|
package de.mm20.launcher2.database.entities
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
|
||||||
|
@Entity(tableName = "SearchAction")
|
||||||
data class SearchActionEntity(
|
data class SearchActionEntity(
|
||||||
|
@PrimaryKey val position: Int,
|
||||||
val type: String,
|
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 options: String? = null,
|
||||||
val position: Int,
|
|
||||||
)
|
)
|
||||||
@ -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`")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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`))")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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`))")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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`;")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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) {
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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`")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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}")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -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`) );")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -651,6 +651,7 @@
|
|||||||
<string name="backup_component_favorites">Favorites & hidden apps</string>
|
<string name="backup_component_favorites">Favorites & hidden apps</string>
|
||||||
<string name="backup_component_settings">Settings</string>
|
<string name="backup_component_settings">Settings</string>
|
||||||
<string name="backup_component_websearches">Web search shortcuts</string>
|
<string name="backup_component_websearches">Web search shortcuts</string>
|
||||||
|
<string name="backup_component_searchactions">Web & app search shortcuts</string>
|
||||||
<string name="backup_component_widgets">Built-in widgets</string>
|
<string name="backup_component_widgets">Built-in widgets</string>
|
||||||
<string name="backup_component_customizations">Customizations</string>
|
<string name="backup_component_customizations">Customizations</string>
|
||||||
<string name="backup_complete">The backup has been completed.</string>
|
<string name="backup_complete">The backup has been completed.</string>
|
||||||
|
|||||||
@ -39,10 +39,14 @@ dependencies {
|
|||||||
implementation(libs.androidx.core)
|
implementation(libs.androidx.core)
|
||||||
|
|
||||||
implementation(libs.koin.android)
|
implementation(libs.koin.android)
|
||||||
|
implementation(libs.jsoup)
|
||||||
|
implementation(libs.okhttp)
|
||||||
|
implementation(libs.coil.core)
|
||||||
|
|
||||||
implementation(project(":base"))
|
implementation(project(":base"))
|
||||||
implementation(project(":database"))
|
implementation(project(":database"))
|
||||||
implementation(project(":ktx"))
|
implementation(project(":ktx"))
|
||||||
implementation(project(":preferences"))
|
implementation(project(":preferences"))
|
||||||
|
implementation(project(":crashreporter"))
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -4,6 +4,6 @@ import org.koin.android.ext.koin.androidContext
|
|||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val searchActionsModule = module {
|
val searchActionsModule = module {
|
||||||
single<SearchActionRepository> { SearchActionRepositoryImpl() }
|
single<SearchActionRepository> { SearchActionRepositoryImpl(androidContext(), get()) }
|
||||||
single<SearchActionService> { SearchActionServiceImpl(androidContext(), get(), TextClassifierImpl()) }
|
single<SearchActionService> { SearchActionServiceImpl(androidContext(), get(), TextClassifierImpl()) }
|
||||||
}
|
}
|
||||||
@ -1,14 +1,123 @@
|
|||||||
package de.mm20.launcher2.searchactions
|
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 de.mm20.launcher2.searchactions.builders.SearchActionBuilder
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
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 {
|
interface SearchActionRepository {
|
||||||
fun getSearchActionBuilders(filter: TextType?): Flow<List<SearchActionBuilder>>
|
fun getSearchActionBuilders(filter: TextType?): Flow<List<SearchActionBuilder>>
|
||||||
|
|
||||||
|
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<List<SearchActionBuilder>> {
|
override fun getSearchActionBuilders(filter: TextType?): Flow<List<SearchActionBuilder>> {
|
||||||
TODO("Not yet implemented")
|
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<SearchActionEntity>()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -1,8 +1,20 @@
|
|||||||
package de.mm20.launcher2.searchactions
|
package de.mm20.launcher2.searchactions
|
||||||
|
|
||||||
import android.content.Context
|
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.preferences.Settings.SearchActionSettings
|
||||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||||
|
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
|
||||||
import de.mm20.launcher2.searchactions.builders.CallActionBuilder
|
import de.mm20.launcher2.searchactions.builders.CallActionBuilder
|
||||||
import de.mm20.launcher2.searchactions.builders.CreateContactActionBuilder
|
import de.mm20.launcher2.searchactions.builders.CreateContactActionBuilder
|
||||||
import de.mm20.launcher2.searchactions.builders.EmailActionBuilder
|
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.SearchActionBuilder
|
||||||
import de.mm20.launcher2.searchactions.builders.SetAlarmActionBuilder
|
import de.mm20.launcher2.searchactions.builders.SetAlarmActionBuilder
|
||||||
import de.mm20.launcher2.searchactions.builders.TimerActionBuilder
|
import de.mm20.launcher2.searchactions.builders.TimerActionBuilder
|
||||||
|
import de.mm20.launcher2.searchactions.builders.WebsearchActionBuilder
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
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 {
|
interface SearchActionService {
|
||||||
fun search(settings: SearchActionSettings, query: String): Flow<ImmutableList<SearchAction>>
|
fun search(settings: SearchActionSettings, query: String): Flow<ImmutableList<SearchAction>>
|
||||||
|
|
||||||
|
suspend fun importWebsearch(url: String, iconSize: Int): WebsearchActionBuilder?
|
||||||
|
suspend fun createIcon(uri: Uri, size: Int): String?
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class SearchActionServiceImpl(
|
internal class SearchActionServiceImpl(
|
||||||
@ -27,7 +58,10 @@ internal class SearchActionServiceImpl(
|
|||||||
private val repository: SearchActionRepository,
|
private val repository: SearchActionRepository,
|
||||||
private val textClassifier: TextClassifier,
|
private val textClassifier: TextClassifier,
|
||||||
) : SearchActionService {
|
) : SearchActionService {
|
||||||
override fun search(settings: SearchActionSettings, query: String): Flow<ImmutableList<SearchAction>> = flow {
|
override fun search(
|
||||||
|
settings: SearchActionSettings,
|
||||||
|
query: String
|
||||||
|
): Flow<ImmutableList<SearchAction>> = flow {
|
||||||
if (query.isBlank()) {
|
if (query.isBlank()) {
|
||||||
emit(persistentListOf())
|
emit(persistentListOf())
|
||||||
return@flow
|
return@flow
|
||||||
@ -50,4 +84,125 @@ internal class SearchActionServiceImpl(
|
|||||||
emit(builders.mapNotNull { it.build(context, classificationResult) }.toImmutableList())
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,16 +10,16 @@ interface SearchAction : Searchable {
|
|||||||
fun start(context: Context)
|
fun start(context: Context)
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class SearchActionIcon {
|
enum class SearchActionIcon(value: Int) {
|
||||||
Search,
|
Search(0),
|
||||||
Website,
|
Custom(1),
|
||||||
Alarm,
|
Website(2),
|
||||||
Timer,
|
Alarm(3),
|
||||||
Contact,
|
Timer(4),
|
||||||
Phone,
|
Contact(5),
|
||||||
Email,
|
Phone(6),
|
||||||
Message,
|
Email(7),
|
||||||
Calendar,
|
Message(8),
|
||||||
Translate,
|
Calendar(9),
|
||||||
Custom,
|
Translate(10),
|
||||||
}
|
}
|
||||||
@ -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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,29 +2,27 @@ package de.mm20.launcher2.searchactions.builders
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import de.mm20.launcher2.searchactions.actions.SearchAction
|
|
||||||
import de.mm20.launcher2.searchactions.TextClassificationResult
|
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.OpenUrlAction
|
||||||
|
import de.mm20.launcher2.searchactions.actions.SearchAction
|
||||||
|
import de.mm20.launcher2.searchactions.actions.SearchActionIcon
|
||||||
import java.net.URLEncoder
|
import java.net.URLEncoder
|
||||||
|
|
||||||
class WebsearchActionBuilder(
|
class WebsearchActionBuilder(
|
||||||
val label: String,
|
val label: String,
|
||||||
val urlTemplate: String,
|
val urlTemplate: String,
|
||||||
val filter: TextType? = null,
|
val icon: SearchActionIcon = SearchActionIcon.Search,
|
||||||
val encoding: QueryEncoding,
|
val customIcon: String? = null,
|
||||||
|
val encoding: QueryEncoding = QueryEncoding.UrlEncode,
|
||||||
) : SearchActionBuilder {
|
) : SearchActionBuilder {
|
||||||
|
|
||||||
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction? {
|
override fun build(context: Context, classifiedQuery: TextClassificationResult): SearchAction {
|
||||||
if (filter == null || classifiedQuery.type == filter) {
|
|
||||||
val url = urlTemplate.replace("\${1}", encodeQuery(classifiedQuery.text, encoding))
|
val url = urlTemplate.replace("\${1}", encodeQuery(classifiedQuery.text, encoding))
|
||||||
return OpenUrlAction(
|
return OpenUrlAction(
|
||||||
label = label,
|
label = label,
|
||||||
url = url,
|
url = url,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun encodeQuery(query: String, encoding: QueryEncoding): String {
|
private fun encodeQuery(query: String, encoding: QueryEncoding): String {
|
||||||
|
|||||||
@ -19,5 +19,4 @@ val searchModule = module {
|
|||||||
get(),
|
get(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
single<WebsearchRepository> { WebsearchRepositoryImpl(androidContext(), get()) }
|
|
||||||
}
|
}
|
||||||
@ -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<List<Websearch>>
|
|
||||||
|
|
||||||
fun getWebsearches(): Flow<List<Websearch>>
|
|
||||||
|
|
||||||
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<List<Websearch>> = 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<List<Websearch>> =
|
|
||||||
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<WebsearchEntity>()
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -170,7 +170,7 @@ fun RestoreBackupSheet(
|
|||||||
imageVector = when (component) {
|
imageVector = when (component) {
|
||||||
BackupComponent.Favorites -> Icons.Rounded.Star
|
BackupComponent.Favorites -> Icons.Rounded.Star
|
||||||
BackupComponent.Settings -> Icons.Rounded.Settings
|
BackupComponent.Settings -> Icons.Rounded.Settings
|
||||||
BackupComponent.Websearches -> Icons.Rounded.TravelExplore
|
BackupComponent.SearchActions -> Icons.Rounded.ArrowOutward
|
||||||
BackupComponent.Widgets -> Icons.Rounded.Widgets
|
BackupComponent.Widgets -> Icons.Rounded.Widgets
|
||||||
BackupComponent.Customizations -> Icons.Rounded.Edit
|
BackupComponent.Customizations -> Icons.Rounded.Edit
|
||||||
},
|
},
|
||||||
@ -181,7 +181,7 @@ fun RestoreBackupSheet(
|
|||||||
when (component) {
|
when (component) {
|
||||||
BackupComponent.Favorites -> R.string.backup_component_favorites
|
BackupComponent.Favorites -> R.string.backup_component_favorites
|
||||||
BackupComponent.Settings -> R.string.backup_component_settings
|
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.Widgets -> R.string.backup_component_widgets
|
||||||
BackupComponent.Customizations -> R.string.backup_component_customizations
|
BackupComponent.Customizations -> R.string.backup_component_customizations
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,7 +10,6 @@ import de.mm20.launcher2.permissions.PermissionsManager
|
|||||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||||
import de.mm20.launcher2.search.SavableSearchable
|
import de.mm20.launcher2.search.SavableSearchable
|
||||||
import de.mm20.launcher2.search.SearchService
|
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.AppShortcut
|
||||||
import de.mm20.launcher2.search.data.Calculator
|
import de.mm20.launcher2.search.data.Calculator
|
||||||
import de.mm20.launcher2.search.data.CalendarEvent
|
import de.mm20.launcher2.search.data.CalendarEvent
|
||||||
@ -40,7 +39,6 @@ class SearchVM : ViewModel(), KoinComponent {
|
|||||||
private val dataStore: LauncherDataStore by inject()
|
private val dataStore: LauncherDataStore by inject()
|
||||||
|
|
||||||
private val searchService: SearchService by inject()
|
private val searchService: SearchService by inject()
|
||||||
private val websearchRepository: WebsearchRepository by inject()
|
|
||||||
|
|
||||||
val isSearching = MutableLiveData(false)
|
val isSearching = MutableLiveData(false)
|
||||||
val searchQuery = MutableLiveData<String>("")
|
val searchQuery = MutableLiveData<String>("")
|
||||||
|
|||||||
@ -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.searchactions.SearchActionsSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.unitconverter.UnitConverterSettingsScreen
|
import de.mm20.launcher2.ui.settings.unitconverter.UnitConverterSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.weatherwidget.WeatherWidgetSettingsScreen
|
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.widgets.WidgetsSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.wikipedia.WikipediaSettingsScreen
|
import de.mm20.launcher2.ui.settings.wikipedia.WikipediaSettingsScreen
|
||||||
import de.mm20.launcher2.ui.theme.LauncherTheme
|
import de.mm20.launcher2.ui.theme.LauncherTheme
|
||||||
@ -119,9 +118,6 @@ class SettingsActivity : BaseActivity() {
|
|||||||
composable("settings/search/files") {
|
composable("settings/search/files") {
|
||||||
FileSearchSettingsScreen()
|
FileSearchSettingsScreen()
|
||||||
}
|
}
|
||||||
composable("settings/search/websearch") {
|
|
||||||
WebSearchSettingsScreen()
|
|
||||||
}
|
|
||||||
composable("settings/search/searchactions") {
|
composable("settings/search/searchactions") {
|
||||||
SearchActionsSettingsScreen()
|
SearchActionsSettingsScreen()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -135,11 +135,11 @@ fun CreateBackupSheet(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
BackupableComponent(
|
BackupableComponent(
|
||||||
title = stringResource(R.string.backup_component_websearches),
|
title = stringResource(R.string.backup_component_searchactions),
|
||||||
icon = Icons.Rounded.TravelExplore,
|
icon = Icons.Rounded.TravelExplore,
|
||||||
checked = components.contains(BackupComponent.Websearches),
|
checked = components.contains(BackupComponent.SearchActions),
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
viewModel.toggleComponent(BackupComponent.Websearches)
|
viewModel.toggleComponent(BackupComponent.SearchActions)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
SmallMessage(
|
SmallMessage(
|
||||||
|
|||||||
@ -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),
|
|
||||||
)
|
|
||||||
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user