Migrate websearch settings

This commit is contained in:
MM20 2022-01-20 21:11:23 +01:00
parent c91576847b
commit 401a493864
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
11 changed files with 643 additions and 251 deletions

View File

@ -54,10 +54,6 @@ class PreferencesSearchFragment : PreferenceFragmentCompat() {
}
true
}
findPreference<Preference>("search_edit_websearch")?.setOnPreferenceClickListener {
setSettingsScreen(PreferencesWebSearchesFragment())
true
}
}
private suspend fun updateGoogleDrive() {

View File

@ -1,233 +0,0 @@
package de.mm20.launcher2.fragment
import android.app.Activity
import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.PorterDuff
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.EditText
import android.widget.ImageView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.graphics.scale
import androidx.lifecycle.Observer
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.bottomsheets.BottomSheet
import com.afollestad.materialdialogs.color.colorChooser
import com.afollestad.materialdialogs.customview.customView
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.transition.Transition
import de.mm20.launcher2.R
import de.mm20.launcher2.ktx.dp
import de.mm20.launcher2.search.WebsearchViewModel
import de.mm20.launcher2.search.data.Websearch
import org.koin.androidx.viewmodel.ext.android.viewModel
import java.io.File
import java.io.FileOutputStream
import java.lang.ref.WeakReference
class PreferencesWebSearchesFragment : PreferenceFragmentCompat() {
private lateinit var rootView: View
private var sheetIcon: WeakReference<ImageView>? = null
private val viewModel: WebsearchViewModel by viewModel()
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
preferenceScreen = preferenceManager.createPreferenceScreen(activity)
val searches = viewModel.allWebsearches
searches.observe(context as AppCompatActivity, Observer {
updatePreferenceScreen(it)
})
}
private fun updatePreferenceScreen(searches: List<Websearch>) {
preferenceScreen.removeAll()
for (search in searches) {
val pref = Preference(context)
pref.title = search.label
if (search.icon == null) {
val drawable =
resources.getDrawable(R.drawable.ic_search, requireActivity().theme).mutate()
drawable.setTintMode(PorterDuff.Mode.SRC_ATOP)
drawable.setTint(search.color)
pref.icon = drawable
} else {
Glide.with(requireContext())
.asDrawable()
.load(search.icon)
.into(object : SimpleTarget<Drawable>() {
override fun onResourceReady(
resource: Drawable,
transition: Transition<in Drawable>?
) {
pref.icon = resource
}
})
}
pref.setOnPreferenceClickListener {
editSearch(search)
true
}
preferenceScreen.addPreference(pref)
}
val newPref = Preference(activity)
newPref.setTitle(R.string.preference_websearch_new)
newPref.setIcon(R.drawable.ic_preference_websearch_new)
newPref.setOnPreferenceClickListener {
editSearch(null)
true
}
preferenceScreen.addPreference(newPref)
}
private fun editSearch(search: Websearch?) {
val websearch = search ?: Websearch("", "", 0xFF555555.toInt(), null, null)
val dialogView = LayoutInflater.from(activity).inflate(R.layout.dialog_websearch, null)
val nameEdit = dialogView.findViewById<EditText>(R.id.websearchName)
nameEdit.setText(websearch.label)
val urlEdit = dialogView.findViewById<EditText>(R.id.websearchUrl)
urlEdit.setText(websearch.urlTemplate)
val iconView = dialogView.findViewById<ImageView>(R.id.websearchIcon)
iconView.apply {
if (websearch.icon == null) {
setImageResource(R.drawable.ic_search)
imageTintList = ColorStateList.valueOf(websearch.color)
} else {
Glide.with(this)
.load(websearch.icon)
.into(this)
}
sheetIcon = WeakReference(this)
}
val sheet = MaterialDialog(requireContext(), BottomSheet())
.cornerRadius(8f)
.customView(view = dialogView)
val radius = 8 * dialogView.dp
dialogView.background = GradientDrawable().apply {
cornerRadii = floatArrayOf(
radius, radius, // top left
radius, radius, // top right
0f, 0f, // bottom left
0f, 0f // bottom right
)
}
var newColor = websearch.color
var newIcon: String? = websearch.icon
sheet.noAutoDismiss()
.positiveButton(android.R.string.ok) {
val newUrl = urlEdit.text.toString()
val newName = nameEdit.text.toString()
if (!newUrl.contains("\${1}")) {
urlEdit.error = getString(R.string.websearch_dialog_url_error)
return@positiveButton
}
File(requireContext().cacheDir, "websearch-tmp").takeIf { it.exists() }?.let {
websearch.icon?.let { File(it).takeIf { it.exists() }?.delete() }
val newFile =
File(requireContext().filesDir, "websearch-${System.currentTimeMillis()}")
it.copyTo(newFile, true)
it.delete()
newIcon = newFile.absolutePath
}
if (newIcon == null) {
websearch.icon?.let { File(it).takeIf { it.exists() }?.delete() }
}
websearch.urlTemplate = newUrl
websearch.label = newName
websearch.icon = newIcon
websearch.color = newColor
viewModel.insertWebsearch(websearch)
sheet.dismiss()
}
sheet.negativeButton(android.R.string.cancel) {
sheet.cancel()
}
@Suppress("DEPRECATION")
sheet.neutralButton(R.string.menu_delete) {
sheet.dismiss()
websearch.icon?.let { File(it).takeIf { it.exists() }?.delete() }
viewModel.deleteWebsearch(websearch)
}
sheet.setOnCancelListener {
File(requireContext().cacheDir, "websearch-tmp").takeIf { it.exists() }?.delete()
}
dialogView.findViewById<View>(R.id.websearchIcon).setOnClickListener {
MaterialDialog(requireContext()).show {
@Suppress("DEPRECATION")
neutralButton(R.string.custom_icon) {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "image/*"
try {
startActivityForResult(intent, 24)
} catch (e: ActivityNotFoundException) {
}
dismiss()
}
title(R.string.websearch_dialog_choose_icon_color)
colorChooser(
colors = context.resources.getIntArray(R.array.color_chooser_presets),
allowCustomArgb = true,
showAlphaSelector = false
) { _, color ->
iconView.setImageResource(R.drawable.ic_search)
iconView.imageTintList = ColorStateList.valueOf(color)
newColor = color
newIcon = null
File(requireContext().cacheDir, "websearch-tmp").takeIf { it.exists() }
?.delete()
dismiss()
}
}
}
sheet.show()
}
override fun onResume() {
super.onResume()
(activity as AppCompatActivity).supportActionBar
?.setTitle(R.string.preference_search_edit_websearch)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
val dataUri = data?.data
if (requestCode == 24 && resultCode == Activity.RESULT_OK && dataUri != null) {
val stream = requireActivity().contentResolver.openInputStream(dataUri)
val icon = BitmapFactory.decodeStream(stream)
val scaledIcon =
icon.scale((32 * requireContext().dp).toInt(), (32 * requireContext().dp).toInt())
val out = FileOutputStream(File(requireContext().cacheDir, "websearch-tmp"))
scaledIcon.compress(Bitmap.CompressFormat.PNG, 100, out)
out.close()
sheetIcon?.get()?.apply {
imageTintList = null
setImageBitmap(scaledIcon)
}
}
}
}

View File

@ -137,6 +137,8 @@
<string name="websearch_dialog_url_hint">URL</string>
<string name="websearch_dialog_url_description">„${1}“ wird durch den eigentlichen Suchbegriff ersetzt.</string>
<string name="websearch_dialog_url_error">In dieser URL fehlt der Platzhalter „${1}“</string>
<string name="websearch_dialog_replace_icon">Symbol ersetzen</string>
<string name="websearch_dialog_delete_icon">Symbol löschen</string>
<string name="preference_weather_provider">Wetterdienst</string>
<string name="provider_openweathermap">OpenWeatherMap</string>
<string name="provider_brightsky">Deutscher Wetterdienst (nur Deutschland)</string>
@ -467,6 +469,9 @@
<string name="missing_permission_file_search">Speicher-Berechtigung wird benötigt um lokale Dateien zu durchsuchen</string>
<string name="missing_permission_file_search_android10">Alle Dateien verwalten-Berechtigung wird benötigt um lokale Dateien zu durchsuchen</string>
<string name="websearch_dialog_create_title">Websuche hinzufügen</string>
<string name="websearch_dialog_edit_title">Websuche bearbeiten</string>
<string name="no_account_nextcloud">Sie haben noch kein Nextcloud-Konto verbunden</string>
<string name="no_account_owncloud">Sie haben noch kein Owncloud-Konto verbunden</string>
<string name="no_account_microsoft">Sie haben noch kein Microsoft-Konto verbunden</string>

View File

@ -181,6 +181,8 @@
<string name="websearch_dialog_url_hint">URL</string>
<string name="websearch_dialog_url_description">\'${1}\' will be replaced by the actual search term.</string>
<string name="websearch_dialog_url_error">The placeholder \'${1}\' is missing in this URL</string>
<string name="websearch_dialog_replace_icon">Replace icon</string>
<string name="websearch_dialog_delete_icon">Delete icon</string>
<string name="preference_weather_provider">Provider</string>
<string name="provider_metno">MET Norway</string>
<string name="provider_openweathermap">OpenWeatherMap</string>
@ -506,6 +508,9 @@
<string name="music_widget_no_data">No media has been played yet</string>
<string name="websearch_dialog_create_title">Add web search</string>
<string name="websearch_dialog_edit_title">Edit web search</string>
<string name="no_account_nextcloud">You haven\'t connected a Nextcloud account yet</string>
<string name="no_account_owncloud">You haven\'t connected an Owncloud account yet</string>
<string name="no_account_microsoft">You haven\'t connected a Microsoft account yet</string>

View File

@ -24,8 +24,6 @@ android {
}
}
compileOptions {
isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
@ -50,8 +48,6 @@ android {
}
dependencies {
coreLibraryDesugaring(libs.desugar)
implementation(libs.bundles.kotlin)
implementation(libs.androidx.compose.runtime)
@ -68,6 +64,8 @@ dependencies {
implementation(libs.androidx.navigation.compose)
implementation(libs.composecolorpicker)
// Legacy dependencies
implementation(libs.androidx.transition)
@ -104,6 +102,9 @@ dependencies {
implementation(libs.koin.android)
implementation(libs.koin.androidxcompose)
implementation(libs.coil.core)
implementation(libs.coil.compose)
implementation(project(":base"))
implementation(project(":i18n"))
implementation(project(":compat"))

View File

@ -15,7 +15,7 @@ import androidx.compose.ui.unit.dp
@Composable
fun Preference(
title: String,
icon: ImageVector? = null,
icon: @Composable (() -> Unit),
summary: String? = null,
onClick: () -> Unit = {},
controls: @Composable (() -> Unit)? = null,
@ -30,17 +30,12 @@ fun Preference(
.alpha(if (enabled) 1f else 0.38f),
) {
Box(
modifier = Modifier.width(56.dp),
modifier = Modifier
.width(56.dp)
.padding(start = 4.dp),
contentAlignment = Alignment.CenterStart
) {
if (icon != null) {
Icon(
modifier = Modifier.padding(start = 4.dp),
imageVector = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
)
}
icon()
}
Column(
modifier = Modifier.weight(1f)
@ -62,4 +57,26 @@ fun Preference(
}
}
}
}
@Composable
fun Preference(
title: String,
icon: ImageVector? = null,
summary: String? = null,
onClick: () -> Unit = {},
controls: @Composable (() -> Unit)? = null,
enabled: Boolean = true
) {
Preference(
title, icon = {
if (icon != null) {
Icon(
imageVector = icon,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
)
}
}, summary, onClick, controls, enabled
)
}

View File

@ -22,6 +22,7 @@ import de.mm20.launcher2.ui.locals.LocalNavController
@Composable
fun PreferenceScreen(
title: String,
floatingActionButton: @Composable () -> Unit = {},
content: LazyListScope.() -> Unit
) {
val navController = LocalNavController.current
@ -34,6 +35,7 @@ fun PreferenceScreen(
modifier = Modifier.systemBarsPadding()
) {
Scaffold(
floatingActionButton = floatingActionButton,
topBar = {
CenterAlignedTopAppBar(
title = {

View File

@ -35,6 +35,7 @@ import de.mm20.launcher2.ui.settings.accounts.AccountsSettingsScreen
import de.mm20.launcher2.ui.settings.buildinfo.BuildInfoSettingsScreen
import de.mm20.launcher2.ui.settings.filesearch.FileSearchSettingsScreen
import de.mm20.launcher2.ui.settings.weatherwidget.WeatherWidgetSettingsScreen
import de.mm20.launcher2.ui.settings.websearch.WebSearchSettingsScreen
import de.mm20.launcher2.ui.settings.widgets.WidgetsSettingsScreen
import de.mm20.launcher2.ui.settings.wikipedia.WikipediaSettingsScreen
@ -101,6 +102,9 @@ class SettingsActivity : BaseActivity() {
composable("settings/search/files") {
FileSearchSettingsScreen()
}
composable("settings/search/websearch") {
WebSearchSettingsScreen()
}
composable("settings/widgets") {
WidgetsSettingsScreen()
}

View File

@ -149,6 +149,9 @@ fun SearchSettingsScreen() {
switchValue = webSearch == true,
onSwitchChanged = {
viewModel.setWebSearch(it)
},
onClick = {
navController?.navigate("settings/search/websearch")
}
)
}

View File

@ -0,0 +1,523 @@
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.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.TextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
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.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.rememberImagePainter
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.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.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)
}
}
) {
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
}
)
}
}
@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 {
Image(
painter = rememberImagePainter(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
)
}
}
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
@Composable
fun EditWebsearchDialog(
title: String,
value: Websearch,
onValueSaved: (Websearch) -> Unit,
onValueDeleted: ((Websearch) -> Unit)? = null,
onCancel: () -> Unit
) {
val context = LocalContext.current
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 color by remember { mutableStateOf(value.color) }
var icon by remember { mutableStateOf(value.icon) }
val scope = rememberCoroutineScope()
val viewModel: WebSearchSettingsScreenVM = viewModel()
val iconSizePx = 32.dp.toPixels().toInt()
val chooseIconLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.GetContent()
) {
if (it != null) {
scope.launch {
icon = viewModel.createIcon(context, it, iconSizePx)
}
}
}
Dialog(
onDismissRequest = onCancel,
properties = DialogProperties(usePlatformDefaultWidth = false)
) {
Surface(
shape = RectangleShape,
modifier = Modifier.fillMaxSize()
) {
Scaffold(
topBar = {
SmallTopAppBar(
navigationIcon = {
IconButton(onClick = {
if (icon != value.icon) {
icon?.let { viewModel.deleteIcon(it) }
}
onCancel()
}) {
Icon(imageVector = Icons.Rounded.Close, contentDescription = null)
}
},
title = {
Text(
text = title
)
},
actions = {
IconButton(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
onValueSaved(value)
} else {
showError = true
}
}) {
Icon(imageVector = Icons.Rounded.Save, contentDescription = null)
}
if (onValueDeleted != null) {
Box {
IconButton(onClick = {
showDropdown = true
}) {
Icon(
imageVector = Icons.Rounded.MoreVert,
contentDescription = null
)
}
DropdownMenu(
expanded = showDropdown,
onDismissRequest = { showDropdown = false }) {
DropdownMenuItem(onClick = {
onValueDeleted(value)
onCancel()
}) {
Text(
text = stringResource(R.string.menu_delete),
style = MaterialTheme.typography.labelLarge
)
}
}
}
}
}
)
}
) {
Column(modifier = Modifier.padding(16.dp)) {
if (icon != null) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = rememberImagePainter(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), style = MaterialTheme.typography.labelLarge)
}
TextButton(
onClick = {
icon = null
},
modifier = Modifier.padding(4.dp),
colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.error
)
) {
Text(stringResource(R.string.websearch_dialog_delete_icon), style = MaterialTheme.typography.labelLarge)
}
}
} else {
ColorPicker(
value = color,
onColorSelected = { color = it }
)
TextButton(
onClick = {
chooseIconLauncher.launch("image/*")
},
modifier = Modifier
.padding(4.dp)
.align(Alignment.End)
) {
Text(
stringResource(R.string.custom_icon),
style = MaterialTheme.typography.labelLarge
)
}
}
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(top = 24.dp),
value = label,
onValueChange = {
label = it
},
label = {
Text(text = stringResource(R.string.websearch_dialog_name_hint))
}
)
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp),
value = urlTemplate,
onValueChange = {
urlTemplate = it
},
label = {
Text(text = stringResource(R.string.websearch_dialog_url_hint))
},
)
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
)
}
}
}
}
}
@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)
)
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),
)

View File

@ -0,0 +1,69 @@
package de.mm20.launcher2.ui.settings.websearch
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import androidx.core.graphics.scale
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
import java.io.FileOutputStream
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(context: Context, uri: Uri, size: Int): String? = withContext(
Dispatchers.IO) {
val file = File(context.dataDir, System.currentTimeMillis().toString())
val stream = context.contentResolver.openInputStream(uri)
val icon = BitmapFactory.decodeStream(stream) ?: return@withContext null
val (scaledW, scaledH) = if (icon.width > icon.height) {
size * icon.width / icon.height to size
} else {
size to size * icon.height / icon.width
}
val scaledIcon = icon.scale(scaledW, scaledH)
val out = FileOutputStream(file)
scaledIcon.compress(Bitmap.CompressFormat.PNG, 100, out)
out.close()
return@withContext file.absolutePath
}
fun deleteIcon(path: String) {
viewModelScope.launch {
withContext(Dispatchers.IO) {
File(path).delete()
}
}
}
}