Migrate websearches to search actions
This commit is contained in:
@@ -170,7 +170,7 @@ fun RestoreBackupSheet(
|
||||
imageVector = when (component) {
|
||||
BackupComponent.Favorites -> Icons.Rounded.Star
|
||||
BackupComponent.Settings -> Icons.Rounded.Settings
|
||||
BackupComponent.Websearches -> Icons.Rounded.TravelExplore
|
||||
BackupComponent.SearchActions -> Icons.Rounded.ArrowOutward
|
||||
BackupComponent.Widgets -> Icons.Rounded.Widgets
|
||||
BackupComponent.Customizations -> Icons.Rounded.Edit
|
||||
},
|
||||
@@ -181,7 +181,7 @@ fun RestoreBackupSheet(
|
||||
when (component) {
|
||||
BackupComponent.Favorites -> R.string.backup_component_favorites
|
||||
BackupComponent.Settings -> R.string.backup_component_settings
|
||||
BackupComponent.Websearches -> R.string.backup_component_websearches
|
||||
BackupComponent.SearchActions -> R.string.backup_component_searchactions
|
||||
BackupComponent.Widgets -> R.string.backup_component_widgets
|
||||
BackupComponent.Customizations -> R.string.backup_component_customizations
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||
import de.mm20.launcher2.search.SavableSearchable
|
||||
import de.mm20.launcher2.search.SearchService
|
||||
import de.mm20.launcher2.search.WebsearchRepository
|
||||
import de.mm20.launcher2.search.data.AppShortcut
|
||||
import de.mm20.launcher2.search.data.Calculator
|
||||
import de.mm20.launcher2.search.data.CalendarEvent
|
||||
@@ -40,7 +39,6 @@ class SearchVM : ViewModel(), KoinComponent {
|
||||
private val dataStore: LauncherDataStore by inject()
|
||||
|
||||
private val searchService: SearchService by inject()
|
||||
private val websearchRepository: WebsearchRepository by inject()
|
||||
|
||||
val isSearching = MutableLiveData(false)
|
||||
val searchQuery = MutableLiveData<String>("")
|
||||
|
||||
@@ -47,7 +47,6 @@ import de.mm20.launcher2.ui.settings.search.SearchSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.searchactions.SearchActionsSettingsScreen
|
||||
import de.mm20.launcher2.ui.settings.unitconverter.UnitConverterSettingsScreen
|
||||
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
|
||||
import de.mm20.launcher2.ui.theme.LauncherTheme
|
||||
@@ -119,9 +118,6 @@ class SettingsActivity : BaseActivity() {
|
||||
composable("settings/search/files") {
|
||||
FileSearchSettingsScreen()
|
||||
}
|
||||
composable("settings/search/websearch") {
|
||||
WebSearchSettingsScreen()
|
||||
}
|
||||
composable("settings/search/searchactions") {
|
||||
SearchActionsSettingsScreen()
|
||||
}
|
||||
|
||||
@@ -135,11 +135,11 @@ fun CreateBackupSheet(
|
||||
}
|
||||
)
|
||||
BackupableComponent(
|
||||
title = stringResource(R.string.backup_component_websearches),
|
||||
title = stringResource(R.string.backup_component_searchactions),
|
||||
icon = Icons.Rounded.TravelExplore,
|
||||
checked = components.contains(BackupComponent.Websearches),
|
||||
checked = components.contains(BackupComponent.SearchActions),
|
||||
onCheckedChange = {
|
||||
viewModel.toggleComponent(BackupComponent.Websearches)
|
||||
viewModel.toggleComponent(BackupComponent.SearchActions)
|
||||
}
|
||||
)
|
||||
SmallMessage(
|
||||
|
||||
@@ -1,692 +0,0 @@
|
||||
package de.mm20.launcher2.ui.settings.websearch
|
||||
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Add
|
||||
import androidx.compose.material.icons.rounded.ArrowForward
|
||||
import androidx.compose.material.icons.rounded.Check
|
||||
import androidx.compose.material.icons.rounded.CloudDownload
|
||||
import androidx.compose.material.icons.rounded.MoreVert
|
||||
import androidx.compose.material.icons.rounded.Search
|
||||
import androidx.compose.material.icons.rounded.Tag
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil.compose.AsyncImage
|
||||
import com.godaddy.android.colorpicker.ClassicColorPicker
|
||||
import de.mm20.launcher2.search.data.Websearch
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.BottomSheetDialog
|
||||
import de.mm20.launcher2.ui.component.preferences.ListPreference
|
||||
import de.mm20.launcher2.ui.component.preferences.Preference
|
||||
import de.mm20.launcher2.ui.component.preferences.PreferenceCategory
|
||||
import de.mm20.launcher2.ui.component.preferences.PreferenceScreen
|
||||
import de.mm20.launcher2.ui.ktx.toHexString
|
||||
import de.mm20.launcher2.ui.ktx.toPixels
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
@Composable
|
||||
fun WebSearchSettingsScreen() {
|
||||
val viewModel: WebSearchSettingsScreenVM = viewModel()
|
||||
val websearches by viewModel.websearches.observeAsState(emptyList())
|
||||
var showNewDialog by remember { mutableStateOf(false) }
|
||||
PreferenceScreen(
|
||||
title = stringResource(R.string.preference_search_websearch),
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = { showNewDialog = true }) {
|
||||
Icon(imageVector = Icons.Rounded.Add, contentDescription = null)
|
||||
}
|
||||
},
|
||||
helpUrl = "https://kvaesitso.mm20.de/docs/user-guide/search/websearches"
|
||||
) {
|
||||
item {
|
||||
PreferenceCategory {
|
||||
for (websearch in websearches) {
|
||||
WebsearchPreference(
|
||||
value = websearch,
|
||||
onValueChanged = {
|
||||
viewModel.updateWebsearch(it)
|
||||
},
|
||||
onValueDeleted = { viewModel.deleteWebsearch(it) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (showNewDialog) {
|
||||
EditWebsearchDialog(
|
||||
title = stringResource(R.string.websearch_dialog_create_title),
|
||||
value = Websearch(
|
||||
label = "",
|
||||
urlTemplate = "",
|
||||
color = 0,
|
||||
icon = null
|
||||
),
|
||||
onValueSaved = {
|
||||
viewModel.createWebsearch(it)
|
||||
showNewDialog = false
|
||||
},
|
||||
onCancel = {
|
||||
showNewDialog = false
|
||||
},
|
||||
enableImport = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun WebsearchPreference(
|
||||
value: Websearch,
|
||||
onValueChanged: (Websearch) -> Unit,
|
||||
onValueDeleted: (Websearch) -> Unit,
|
||||
) {
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
Preference(
|
||||
title = value.label,
|
||||
summary = value.urlTemplate,
|
||||
onClick = {
|
||||
showDialog = true
|
||||
},
|
||||
icon = {
|
||||
val icon = value.icon
|
||||
if (icon == null) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Search,
|
||||
contentDescription = null,
|
||||
tint = value.color.takeIf { it != 0 }?.let { Color(it) }
|
||||
?: MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
} else {
|
||||
AsyncImage(
|
||||
model = File(icon),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.sizeIn(maxWidth = 24.dp, maxHeight = 24.dp),
|
||||
contentScale = ContentScale.Inside
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
if (showDialog) {
|
||||
EditWebsearchDialog(
|
||||
title = stringResource(R.string.websearch_dialog_edit_title),
|
||||
value = value,
|
||||
onValueSaved = {
|
||||
onValueChanged(it)
|
||||
showDialog = false
|
||||
},
|
||||
onCancel = {
|
||||
showDialog = false
|
||||
},
|
||||
onValueDeleted = onValueDeleted
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EditWebsearchDialog(
|
||||
title: String,
|
||||
value: Websearch,
|
||||
onValueSaved: (Websearch) -> Unit,
|
||||
onValueDeleted: ((Websearch) -> Unit)? = null,
|
||||
onCancel: () -> Unit,
|
||||
enableImport: Boolean = false
|
||||
) {
|
||||
var showDropdown by remember { mutableStateOf(false) }
|
||||
|
||||
var label by remember { mutableStateOf(value.label) }
|
||||
var showError by remember { mutableStateOf(false) }
|
||||
var urlTemplate by remember { mutableStateOf(value.urlTemplate) }
|
||||
var encoding by remember { mutableStateOf(value.encoding) }
|
||||
var color by remember { mutableStateOf(value.color) }
|
||||
var icon by remember { mutableStateOf(value.icon) }
|
||||
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
var showImport by remember { mutableStateOf(false) }
|
||||
var loadingImport by remember { mutableStateOf(false) }
|
||||
var importError by remember { mutableStateOf(false) }
|
||||
|
||||
val viewModel: WebSearchSettingsScreenVM = viewModel()
|
||||
|
||||
val iconSizePx = 32.dp.toPixels().toInt()
|
||||
|
||||
val chooseIconLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.GetContent()
|
||||
) {
|
||||
if (it != null) {
|
||||
scope.launch {
|
||||
icon = viewModel.createIcon(it, iconSizePx)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BottomSheetDialog(onDismissRequest = onCancel,
|
||||
title = { Text(title) },
|
||||
actions = {
|
||||
if (enableImport) {
|
||||
Box {
|
||||
IconButton(onClick = {
|
||||
showImport = !showImport
|
||||
importError = false
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.CloudDownload,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
if (onValueDeleted != null) {
|
||||
Box {
|
||||
IconButton(onClick = {
|
||||
showDropdown = true
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.MoreVert,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = showDropdown,
|
||||
onDismissRequest = { showDropdown = false }) {
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
text = stringResource(R.string.menu_delete)
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
onValueDeleted(value)
|
||||
onCancel()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(onClick = {
|
||||
if (urlTemplate.contains("\${1}")) {
|
||||
value.label = label
|
||||
value.urlTemplate = urlTemplate
|
||||
if (value.icon != icon) {
|
||||
value.icon?.let {
|
||||
viewModel.deleteIcon(it)
|
||||
}
|
||||
}
|
||||
value.icon = icon
|
||||
value.color = color
|
||||
value.encoding = encoding
|
||||
onValueSaved(value)
|
||||
} else {
|
||||
showError = true
|
||||
}
|
||||
}) {
|
||||
Text(stringResource(R.string.save))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
OutlinedButton(onClick = {
|
||||
if (icon != value.icon) {
|
||||
icon?.let { viewModel.deleteIcon(it) }
|
||||
}
|
||||
onCancel()
|
||||
}) {
|
||||
Text(stringResource(android.R.string.cancel))
|
||||
}
|
||||
}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
AnimatedVisibility(showImport) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 24.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
var importUrl by remember { mutableStateOf("") }
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(bottom = 16.dp, top = 8.dp, end = 8.dp),
|
||||
label = { Text(stringResource(R.string.websearch_dialog_import_url)) },
|
||||
value = importUrl,
|
||||
onValueChange = {
|
||||
importUrl = it
|
||||
importError = false
|
||||
},
|
||||
textStyle = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
if (loadingImport) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.padding(12.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
} else {
|
||||
IconButton(onClick = {
|
||||
scope.launch {
|
||||
loadingImport = true
|
||||
val websearch =
|
||||
viewModel.importWebsearch(
|
||||
importUrl,
|
||||
iconSizePx
|
||||
)
|
||||
if (websearch != null) {
|
||||
label = websearch.label
|
||||
icon = websearch.icon
|
||||
urlTemplate = websearch.urlTemplate
|
||||
color = websearch.color
|
||||
showImport = false
|
||||
} else {
|
||||
importError = true
|
||||
}
|
||||
loadingImport = false
|
||||
}
|
||||
}) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.ArrowForward,
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(importError) {
|
||||
Column(
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.websearch_dialog_import_error),
|
||||
style = MaterialTheme.typography.labelSmall
|
||||
)
|
||||
TextButton(
|
||||
modifier = Modifier.align(Alignment.End),
|
||||
onClick = { showImport = false }) {
|
||||
Text(
|
||||
text = stringResource(android.R.string.ok),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (icon != null) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
AsyncImage(
|
||||
model = icon?.let { File(it) },
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(end = 16.dp)
|
||||
.size(48.dp)
|
||||
)
|
||||
TextButton(
|
||||
onClick = {
|
||||
chooseIconLauncher.launch("image/*")
|
||||
},
|
||||
modifier = Modifier.padding(4.dp)
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.websearch_dialog_replace_icon),
|
||||
)
|
||||
}
|
||||
TextButton(
|
||||
onClick = {
|
||||
icon = null
|
||||
},
|
||||
modifier = Modifier.padding(4.dp),
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
contentColor = MaterialTheme.colorScheme.error
|
||||
)
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.websearch_dialog_delete_icon),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ColorPicker(
|
||||
value = color,
|
||||
onColorSelected = { color = it }
|
||||
)
|
||||
TextButton(
|
||||
onClick = {
|
||||
chooseIconLauncher.launch("image/*")
|
||||
},
|
||||
modifier = Modifier
|
||||
.padding(4.dp)
|
||||
.align(Alignment.End)
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.websearch_dialog_custom_icon),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 24.dp),
|
||||
value = label,
|
||||
onValueChange = {
|
||||
label = it
|
||||
},
|
||||
label = {
|
||||
Text(text = stringResource(R.string.websearch_dialog_name))
|
||||
},
|
||||
textStyle = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 16.dp),
|
||||
value = urlTemplate,
|
||||
onValueChange = {
|
||||
urlTemplate = it
|
||||
},
|
||||
label = {
|
||||
Text(text = stringResource(R.string.websearch_dialog_url))
|
||||
},
|
||||
textStyle = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
AnimatedVisibility(showError) {
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
text = stringResource(R.string.websearch_dialog_url_error),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
Text(
|
||||
modifier = Modifier.padding(top = 8.dp),
|
||||
text = stringResource(R.string.websearch_dialog_url_description),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
|
||||
var showAdvanced by remember { mutableStateOf(false) }
|
||||
|
||||
AnimatedVisibility(!showAdvanced) {
|
||||
TextButton(
|
||||
modifier = Modifier.padding(vertical = 16.dp).align(Alignment.End),
|
||||
onClick = { showAdvanced = true }) {
|
||||
Text(stringResource(R.string.websearch_dialog_advanced))
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedVisibility(showAdvanced) {
|
||||
Column(
|
||||
modifier = Modifier.padding(top = 16.dp)
|
||||
) {
|
||||
Divider()
|
||||
ListPreference(
|
||||
title = stringResource(R.string.websearch_dialog_query_encoding),
|
||||
items = listOf(
|
||||
stringResource(R.string.websearch_dialog_query_encoding_url) to Websearch.QueryEncoding.UrlEncode,
|
||||
stringResource(R.string.websearch_dialog_query_encoding_form) to Websearch.QueryEncoding.FormData,
|
||||
stringResource(R.string.websearch_dialog_query_encoding_none) to Websearch.QueryEncoding.None,
|
||||
),
|
||||
iconPadding = false,
|
||||
value = encoding,
|
||||
onValueChanged = {
|
||||
encoding = it
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColorPicker(
|
||||
value: Int,
|
||||
onColorSelected: (Int) -> Unit
|
||||
) {
|
||||
var selectedColorIndex = -1
|
||||
val isCustomColor = !ColorPresets.contains(Color(value)) && value != 0
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
var showCustomColorPicker by remember { mutableStateOf(false) }
|
||||
|
||||
Column {
|
||||
AnimatedVisibility(!showCustomColorPicker) {
|
||||
LazyRow(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.height(64.dp),
|
||||
state = listState
|
||||
) {
|
||||
item {
|
||||
if (value == 0) selectedColorIndex = 0
|
||||
ColorSwatch(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
checked = value == 0,
|
||||
onClick = {
|
||||
onColorSelected(0)
|
||||
}
|
||||
)
|
||||
}
|
||||
items(ColorPresets) {
|
||||
ColorSwatch(
|
||||
color = it,
|
||||
checked = value == it.toArgb(),
|
||||
onClick = {
|
||||
onColorSelected(it.toArgb())
|
||||
}
|
||||
)
|
||||
}
|
||||
item {
|
||||
CustomColorSwatch(
|
||||
checked = isCustomColor,
|
||||
onClick = {
|
||||
showCustomColorPicker = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
LaunchedEffect(null) {
|
||||
if (isCustomColor) listState.scrollToItem(ColorPresets.size + 1)
|
||||
else if (value != 0) listState.scrollToItem(ColorPresets.indexOf(Color(value)) + 1)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(showCustomColorPicker) {
|
||||
Column {
|
||||
ClassicColorPicker(
|
||||
color = Color(value),
|
||||
showAlphaBar = false,
|
||||
modifier = Modifier.height(200.dp),
|
||||
onColorChanged = {
|
||||
onColorSelected(it.toColor().toArgb())
|
||||
})
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(bottom = 24.dp, top = 8.dp)
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
var textFieldValue by remember(value) {
|
||||
mutableStateOf(
|
||||
Color(value).toHexString().substring(1)
|
||||
)
|
||||
}
|
||||
TextField(
|
||||
value = textFieldValue,
|
||||
leadingIcon = {
|
||||
Icon(imageVector = Icons.Rounded.Tag, contentDescription = null)
|
||||
},
|
||||
onValueChange = {
|
||||
textFieldValue = it
|
||||
if (it.length == 6) it.toLongOrNull(16)?.let {
|
||||
onColorSelected((it or 0xFF000000).toInt())
|
||||
}
|
||||
},
|
||||
singleLine = true,
|
||||
modifier = Modifier.width(150.dp),
|
||||
textStyle = MaterialTheme.typography.bodyLarge,
|
||||
)
|
||||
TextButton(onClick = { showCustomColorPicker = false }) {
|
||||
Text(
|
||||
stringResource(android.R.string.ok),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColorSwatch(
|
||||
color: Color,
|
||||
checked: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp)
|
||||
.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color)
|
||||
.clickable { onClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
if (checked) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Check, contentDescription = null,
|
||||
tint = if (color.luminance() > 0.5f) Color.Black else Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CustomColorSwatch(
|
||||
checked: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp)
|
||||
.size(48.dp)
|
||||
.clip(CircleShape)
|
||||
.clickable { onClick() },
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Canvas(modifier = Modifier.fillMaxSize()) {
|
||||
val brush = Brush.sweepGradient(
|
||||
listOf(
|
||||
Color.Red,
|
||||
Color.Magenta,
|
||||
Color.Blue,
|
||||
Color.Cyan,
|
||||
Color.Green,
|
||||
Color.Yellow,
|
||||
Color.Red
|
||||
)
|
||||
)
|
||||
|
||||
drawRect(brush)
|
||||
}
|
||||
if (checked) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Check, contentDescription = null,
|
||||
tint = Color.White
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val ColorPresets = listOf(
|
||||
Color(0xFFEF5350),
|
||||
Color(0xFFEC407A),
|
||||
Color(0xFFAB47BC),
|
||||
Color(0xFF7E57C2),
|
||||
Color(0xFF5C6BC0),
|
||||
Color(0xFF42A5F5),
|
||||
Color(0xFF29B6F6),
|
||||
Color(0xFF26C6DA),
|
||||
Color(0xFF26A69A),
|
||||
Color(0xFF66BB6A),
|
||||
Color(0xFF9CCC65),
|
||||
Color(0xFFD4E157),
|
||||
Color(0xFFFFEE58),
|
||||
Color(0xFFFFCA28),
|
||||
Color(0xFFFFA726),
|
||||
Color(0xFFFF7043),
|
||||
Color(0xFF8D6E63),
|
||||
Color(0xFFBDBDBD),
|
||||
Color(0xFF78909C),
|
||||
)
|
||||
@@ -1,55 +0,0 @@
|
||||
package de.mm20.launcher2.ui.settings.websearch
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.search.WebsearchRepository
|
||||
import de.mm20.launcher2.search.data.Websearch
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import java.io.File
|
||||
|
||||
class WebSearchSettingsScreenVM: ViewModel(), KoinComponent {
|
||||
private val repository: WebsearchRepository by inject()
|
||||
|
||||
val websearches = repository.getWebsearches().asLiveData()
|
||||
|
||||
fun createWebsearch(websearch: Websearch) {
|
||||
repository.insertWebsearch(websearch)
|
||||
}
|
||||
|
||||
fun updateWebsearch(websearch: Websearch) {
|
||||
repository.insertWebsearch(websearch)
|
||||
}
|
||||
|
||||
fun deleteWebsearch(websearch: Websearch) {
|
||||
websearch.icon?.let { deleteIcon(it) }
|
||||
repository.deleteWebsearch(websearch)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Read a user-selected icon, scale it down and copy it to the app's data dir
|
||||
* @return the absolute path of the copied file
|
||||
*/
|
||||
suspend fun createIcon(uri: Uri, size: Int): String? {
|
||||
return repository.createIcon(uri, size)
|
||||
}
|
||||
|
||||
suspend fun importWebsearch(url: String, size: Int): Websearch? {
|
||||
return repository.importWebsearch(url, size)
|
||||
}
|
||||
|
||||
|
||||
fun deleteIcon(path: String) {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
File(path).delete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user