Migrate websearch settings
This commit is contained in:
parent
c91576847b
commit
401a493864
@ -54,10 +54,6 @@ class PreferencesSearchFragment : PreferenceFragmentCompat() {
|
|||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
findPreference<Preference>("search_edit_websearch")?.setOnPreferenceClickListener {
|
|
||||||
setSettingsScreen(PreferencesWebSearchesFragment())
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun updateGoogleDrive() {
|
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_hint">URL</string>
|
||||||
<string name="websearch_dialog_url_description">„${1}“ wird durch den eigentlichen Suchbegriff ersetzt.</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_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="preference_weather_provider">Wetterdienst</string>
|
||||||
<string name="provider_openweathermap">OpenWeatherMap</string>
|
<string name="provider_openweathermap">OpenWeatherMap</string>
|
||||||
<string name="provider_brightsky">Deutscher Wetterdienst (nur Deutschland)</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">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="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_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_owncloud">Sie haben noch kein Owncloud-Konto verbunden</string>
|
||||||
<string name="no_account_microsoft">Sie haben noch kein Microsoft-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_hint">URL</string>
|
||||||
<string name="websearch_dialog_url_description">\'${1}\' will be replaced by the actual search term.</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_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="preference_weather_provider">Provider</string>
|
||||||
<string name="provider_metno">MET Norway</string>
|
<string name="provider_metno">MET Norway</string>
|
||||||
<string name="provider_openweathermap">OpenWeatherMap</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="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_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_owncloud">You haven\'t connected an Owncloud account yet</string>
|
||||||
<string name="no_account_microsoft">You haven\'t connected a Microsoft account yet</string>
|
<string name="no_account_microsoft">You haven\'t connected a Microsoft account yet</string>
|
||||||
|
|||||||
@ -24,8 +24,6 @@ android {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
isCoreLibraryDesugaringEnabled = true
|
|
||||||
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
@ -50,8 +48,6 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
coreLibraryDesugaring(libs.desugar)
|
|
||||||
|
|
||||||
implementation(libs.bundles.kotlin)
|
implementation(libs.bundles.kotlin)
|
||||||
|
|
||||||
implementation(libs.androidx.compose.runtime)
|
implementation(libs.androidx.compose.runtime)
|
||||||
@ -68,6 +64,8 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.navigation.compose)
|
||||||
|
|
||||||
|
implementation(libs.composecolorpicker)
|
||||||
|
|
||||||
// Legacy dependencies
|
// Legacy dependencies
|
||||||
implementation(libs.androidx.transition)
|
implementation(libs.androidx.transition)
|
||||||
|
|
||||||
@ -104,6 +102,9 @@ dependencies {
|
|||||||
implementation(libs.koin.android)
|
implementation(libs.koin.android)
|
||||||
implementation(libs.koin.androidxcompose)
|
implementation(libs.koin.androidxcompose)
|
||||||
|
|
||||||
|
implementation(libs.coil.core)
|
||||||
|
implementation(libs.coil.compose)
|
||||||
|
|
||||||
implementation(project(":base"))
|
implementation(project(":base"))
|
||||||
implementation(project(":i18n"))
|
implementation(project(":i18n"))
|
||||||
implementation(project(":compat"))
|
implementation(project(":compat"))
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
@Composable
|
@Composable
|
||||||
fun Preference(
|
fun Preference(
|
||||||
title: String,
|
title: String,
|
||||||
icon: ImageVector? = null,
|
icon: @Composable (() -> Unit),
|
||||||
summary: String? = null,
|
summary: String? = null,
|
||||||
onClick: () -> Unit = {},
|
onClick: () -> Unit = {},
|
||||||
controls: @Composable (() -> Unit)? = null,
|
controls: @Composable (() -> Unit)? = null,
|
||||||
@ -30,17 +30,12 @@ fun Preference(
|
|||||||
.alpha(if (enabled) 1f else 0.38f),
|
.alpha(if (enabled) 1f else 0.38f),
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.width(56.dp),
|
modifier = Modifier
|
||||||
|
.width(56.dp)
|
||||||
|
.padding(start = 4.dp),
|
||||||
contentAlignment = Alignment.CenterStart
|
contentAlignment = Alignment.CenterStart
|
||||||
) {
|
) {
|
||||||
if (icon != null) {
|
icon()
|
||||||
Icon(
|
|
||||||
modifier = Modifier.padding(start = 4.dp),
|
|
||||||
imageVector = icon,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.weight(1f)
|
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
|
@Composable
|
||||||
fun PreferenceScreen(
|
fun PreferenceScreen(
|
||||||
title: String,
|
title: String,
|
||||||
|
floatingActionButton: @Composable () -> Unit = {},
|
||||||
content: LazyListScope.() -> Unit
|
content: LazyListScope.() -> Unit
|
||||||
) {
|
) {
|
||||||
val navController = LocalNavController.current
|
val navController = LocalNavController.current
|
||||||
@ -34,6 +35,7 @@ fun PreferenceScreen(
|
|||||||
modifier = Modifier.systemBarsPadding()
|
modifier = Modifier.systemBarsPadding()
|
||||||
) {
|
) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
floatingActionButton = floatingActionButton,
|
||||||
topBar = {
|
topBar = {
|
||||||
CenterAlignedTopAppBar(
|
CenterAlignedTopAppBar(
|
||||||
title = {
|
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.buildinfo.BuildInfoSettingsScreen
|
||||||
import de.mm20.launcher2.ui.settings.filesearch.FileSearchSettingsScreen
|
import de.mm20.launcher2.ui.settings.filesearch.FileSearchSettingsScreen
|
||||||
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
|
||||||
|
|
||||||
@ -101,6 +102,9 @@ class SettingsActivity : BaseActivity() {
|
|||||||
composable("settings/search/files") {
|
composable("settings/search/files") {
|
||||||
FileSearchSettingsScreen()
|
FileSearchSettingsScreen()
|
||||||
}
|
}
|
||||||
|
composable("settings/search/websearch") {
|
||||||
|
WebSearchSettingsScreen()
|
||||||
|
}
|
||||||
composable("settings/widgets") {
|
composable("settings/widgets") {
|
||||||
WidgetsSettingsScreen()
|
WidgetsSettingsScreen()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -149,6 +149,9 @@ fun SearchSettingsScreen() {
|
|||||||
switchValue = webSearch == true,
|
switchValue = webSearch == true,
|
||||||
onSwitchChanged = {
|
onSwitchChanged = {
|
||||||
viewModel.setWebSearch(it)
|
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