Migrate websearch settings
This commit is contained in:
parent
c91576847b
commit
401a493864
@ -54,10 +54,6 @@ class PreferencesSearchFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
true
|
||||
}
|
||||
findPreference<Preference>("search_edit_websearch")?.setOnPreferenceClickListener {
|
||||
setSettingsScreen(PreferencesWebSearchesFragment())
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateGoogleDrive() {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
@ -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 = {
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -149,6 +149,9 @@ fun SearchSettingsScreen() {
|
||||
switchValue = webSearch == true,
|
||||
onSwitchChanged = {
|
||||
viewModel.setWebSearch(it)
|
||||
},
|
||||
onClick = {
|
||||
navController?.navigate("settings/search/websearch")
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -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),
|
||||
)
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user