Add multiple options for websearch query encoding

Close #174
This commit is contained in:
MM20 2022-09-25 20:59:29 +02:00
parent 69876e8884
commit 24e25148f0
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
10 changed files with 627 additions and 37 deletions

View File

@ -185,7 +185,7 @@ class BackupManager(
companion object {
private const val BackupFormatMajor = 1
private const val BackupFormatMinor = 1
private const val BackupFormatMinor = 2
internal const val BackupFormat = "$BackupFormatMajor.$BackupFormatMinor"
}
}

View File

@ -0,0 +1,450 @@
{
"formatVersion": 1,
"database": {
"version": 17,
"identityHash": "3321ba63cb650a091b1ca102198a41ba",
"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, `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": "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": "Websearch",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`urlTemplate` TEXT NOT NULL, `label` TEXT NOT NULL, `color` INTEGER NOT NULL, `icon` TEXT, `encoding` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT)",
"fields": [
{
"fieldPath": "urlTemplate",
"columnName": "urlTemplate",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "label",
"columnName": "label",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "color",
"columnName": "color",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "icon",
"columnName": "icon",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "encoding",
"columnName": "encoding",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"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": []
}
],
"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, '3321ba63cb650a091b1ca102198a41ba')"
]
}
}

View File

@ -18,7 +18,7 @@ import de.mm20.launcher2.database.entities.*
IconEntity::class,
IconPackEntity::class,
WidgetEntity::class,
CustomAttributeEntity::class], version = 16, exportSchema = true)
CustomAttributeEntity::class], version = 17, exportSchema = true)
@TypeConverters(ComponentNameConverter::class, StringListConverter::class)
abstract class AppDatabase : RoomDatabase() {
@ -61,6 +61,7 @@ abstract class AppDatabase : RoomDatabase() {
Migration_13_14(),
Migration_14_15(),
Migration_15_16(),
Migration_16_17(),
).build()
if (_instance == null) _instance = instance
return instance
@ -165,4 +166,11 @@ class Migration_15_16 : Migration(15, 16) {
)
""".trimIndent())
}
}
class Migration_16_17 : Migration(16, 17) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE Websearch ADD COLUMN encoding INTEGER")
}
}

View File

@ -9,5 +9,6 @@ data class WebsearchEntity(
var label: String,
var color: Int,
var icon: String?,
var encoding: Int?,
@PrimaryKey(autoGenerate = true) val id: Long?
)

View File

@ -208,6 +208,11 @@
<string name="websearch_dialog_custom_icon">Custom icon</string>
<string name="websearch_dialog_import_url">Import from URL</string>
<string name="websearch_dialog_import_error">The given URL cannot be imported automatically. You can try a different URL or enter the required data manually.</string>
<string name="websearch_dialog_advanced">Advanced</string>
<string name="websearch_dialog_query_endcoding">Query encoding</string>
<string name="websearch_dialog_query_endcoding_url">Percent-encoding</string>
<string name="websearch_dialog_query_endcoding_form">application/x-www-form-urlencoded</string>
<string name="websearch_dialog_query_endcoding_none">None</string>
<string name="menu_edit_widgets">Edit widgets</string>
<string name="widget_name_weather">Weather</string>
<string name="widget_name_calendar">Calendar</string>

View File

@ -244,6 +244,7 @@ internal class WebsearchRepositoryImpl(
"label" to websearch.label,
"template" to websearch.urlTemplate,
"icon" to icon,
"encoding" to websearch.encoding,
)
)
}
@ -289,6 +290,7 @@ internal class WebsearchRepositoryImpl(
color = json.optInt("color", 0),
label = json.getString("label"),
icon = iconFile?.absolutePath,
encoding = json.optInt("encoding"),
id = null
)
websearches.add(entity)

View File

@ -3,41 +3,79 @@ package de.mm20.launcher2.search.data
import android.content.Intent
import android.net.Uri
import de.mm20.launcher2.database.entities.WebsearchEntity
import java.net.URLEncoder
import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
class Websearch(
var urlTemplate: String,
var label: String,
var color: Int,
var icon: String?,
var id: Long? = null,
val query: String? = null
var urlTemplate: String,
var label: String,
var color: Int,
var icon: String?,
var id: Long? = null,
var encoding: QueryEncoding = QueryEncoding.UrlEncode,
val query: String? = null,
) {
constructor(entity: WebsearchEntity, query: String? = null) : this(
urlTemplate = entity.urlTemplate,
label = entity.label,
icon = entity.icon,
color = entity.color,
id = entity.id,
query = query
urlTemplate = entity.urlTemplate,
label = entity.label,
icon = entity.icon,
color = entity.color,
id = entity.id,
query = query,
encoding = QueryEncoding.fromInt(entity.encoding)
)
fun toDatabaseEntity(): WebsearchEntity {
return WebsearchEntity(
urlTemplate = urlTemplate,
color = color,
icon = icon,
label = label,
id = id
urlTemplate = urlTemplate,
color = color,
icon = icon,
label = label,
id = id,
encoding = encoding.toInt()
)
}
fun getLaunchIntent(): Intent? {
if(query == null) return null
if (query == null) return null
val intent = Intent(Intent.ACTION_VIEW)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
val url = urlTemplate.replace("\${1}", Uri.encode(query))
val url = urlTemplate.replace("\${1}", encodeQuery(query, encoding))
intent.data = Uri.parse(url)
return intent
}
private fun encodeQuery(query: String, encoding: QueryEncoding): String {
return when(encoding) {
QueryEncoding.UrlEncode -> Uri.encode(query)
QueryEncoding.FormData -> URLEncoder.encode(query, "UTF-8")
QueryEncoding.None -> query
}
}
enum class QueryEncoding {
UrlEncode,
FormData,
None;
fun toInt(): Int {
return when (this) {
UrlEncode -> 0
FormData -> 1
None -> 2
}
}
companion object {
fun fromInt(value: Int?): QueryEncoding {
return when (value) {
1 -> FormData
2 -> None
else -> UrlEncode
}
}
}
}
}

View File

@ -17,6 +17,7 @@ import androidx.compose.ui.window.Dialog
fun <T> ListPreference(
title: String,
icon: ImageVector? = null,
iconPadding: Boolean = true,
items: List<ListPreferenceItem<T>>,
value: T,
summary: String? = items.firstOrNull { value == it.value }?.label,
@ -33,6 +34,7 @@ fun <T> ListPreference(
title = title,
summary = summary,
icon = icon,
iconPadding = iconPadding,
enabled = enabled,
onClick = {
showDialog = true

View File

@ -16,6 +16,7 @@ import androidx.compose.ui.unit.dp
fun Preference(
title: String,
icon: @Composable (() -> Unit),
iconPadding: Boolean = true,
summary: String? = null,
onClick: () -> Unit = {},
controls: @Composable (() -> Unit)? = null,
@ -29,13 +30,17 @@ fun Preference(
.padding(horizontal = 16.dp)
.alpha(if (enabled) 1f else 0.38f),
) {
Box(
modifier = Modifier
.width(56.dp)
.padding(start = 4.dp),
contentAlignment = Alignment.CenterStart
) {
icon()
if (iconPadding) {
Box(
modifier = Modifier
.width(56.dp)
.padding(start = 4.dp),
contentAlignment = Alignment.CenterStart
) {
icon()
}
} else {
Box(modifier = Modifier.size(0.dp))
}
Column(
modifier = Modifier.weight(1f).padding(vertical = 16.dp)
@ -63,13 +68,15 @@ fun Preference(
fun Preference(
title: String,
icon: ImageVector? = null,
iconPadding: Boolean = true,
summary: String? = null,
onClick: () -> Unit = {},
controls: @Composable (() -> Unit)? = null,
enabled: Boolean = true
) {
Preference(
title, icon = {
title,
icon = {
if (icon != null) {
Icon(
imageVector = icon,
@ -77,6 +84,6 @@ fun Preference(
tint = MaterialTheme.colorScheme.primary,
)
}
}, summary, onClick, controls, enabled
}, iconPadding, summary, onClick, controls, enabled
)
}

View File

@ -3,22 +3,65 @@ 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.*
import androidx.compose.foundation.layout.*
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.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
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.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.*
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
@ -28,6 +71,7 @@ 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
@ -146,6 +190,7 @@ fun EditWebsearchDialog(
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) }
@ -226,6 +271,7 @@ fun EditWebsearchDialog(
}
value.icon = icon
value.color = color
value.encoding = encoding
onValueSaved(value)
} else {
showError = true
@ -426,6 +472,37 @@ fun EditWebsearchDialog(
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_endcoding),
items = listOf(
stringResource(R.string.websearch_dialog_query_endcoding_url) to Websearch.QueryEncoding.UrlEncode,
stringResource(R.string.websearch_dialog_query_endcoding_form) to Websearch.QueryEncoding.FormData,
stringResource(R.string.websearch_dialog_query_endcoding_none) to Websearch.QueryEncoding.None,
),
iconPadding = false,
value = encoding,
onValueChanged = {
encoding = it
}
)
}
}
}
}
}