From a21dde5741bd41e7f0862edafcdc429d690a2b81 Mon Sep 17 00:00:00 2001
From: MM20 <15646950+MM2-0@users.noreply.github.com>
Date: Mon, 11 Nov 2024 20:48:04 +0100
Subject: [PATCH] Improve tag emoji assignment
---
.idea/inspectionProfiles/Project_Default.xml | 1 +
app/ui/build.gradle.kts | 1 +
.../de/mm20/launcher2/ui/common/TagChip.kt | 80 ++++---
.../de/mm20/launcher2/ui/component/Toolbar.kt | 2 +-
.../ui/component/dragndrop/DragAndDropGrid.kt | 4 +-
.../ui/component/emojipicker/EmojiPicker.kt | 206 ++++++++++++++++++
.../ui/launcher/widgets/music/MusicWidget.kt | 4 +-
.../ui/launcher/widgets/notes/NotesWidget.kt | 4 +-
.../colorscheme/CorePaletteColorPreference.kt | 2 +-
.../colorscheme/ThemeColorPreference.kt | 2 +-
.../ui/settings/tags/EditTagSheet.kt | 159 ++++++++++++--
.../ui/settings/tags/EditTagSheetVM.kt | 33 ++-
core/i18n/src/main/res/values/strings.xml | 1 +
.../data/customattrs/CustomAttribute.kt | 19 ++
data/database/build.gradle.kts | 1 +
.../de/mm20/launcher2/database/AppDatabase.kt | 4 +-
.../database/migrations/Migration_26_27.kt | 72 ++++++
.../de/mm20/launcher2/icons/IconService.kt | 5 +
.../icons/providers/CustomTextIconProvider.kt | 29 +++
.../services/tags/impl/TagsServiceImpl.kt | 9 +-
20 files changed, 569 insertions(+), 69 deletions(-)
create mode 100644 app/ui/src/main/java/de/mm20/launcher2/ui/component/emojipicker/EmojiPicker.kt
create mode 100644 data/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_26_27.kt
create mode 100644 services/icons/src/main/java/de/mm20/launcher2/icons/providers/CustomTextIconProvider.kt
diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 63cbe31e..e0da3ee5 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -39,6 +39,7 @@
+
diff --git a/app/ui/build.gradle.kts b/app/ui/build.gradle.kts
index 52b85064..eb5efb63 100644
--- a/app/ui/build.gradle.kts
+++ b/app/ui/build.gradle.kts
@@ -100,6 +100,7 @@ dependencies {
implementation(libs.androidx.appcompat)
implementation(libs.androidx.browser)
+ implementation(libs.androidx.emojipicker)
implementation(libs.androidx.lifecycle.viewmodelcompose)
implementation(libs.androidx.lifecycle.runtimecompose)
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/common/TagChip.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/common/TagChip.kt
index e5c9531f..13debbf8 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/common/TagChip.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/common/TagChip.kt
@@ -1,5 +1,6 @@
package de.mm20.launcher2.ui.common
+import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColor
import androidx.compose.animation.core.animateDp
import androidx.compose.animation.core.tween
@@ -8,16 +9,20 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.widthIn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Close
-import androidx.compose.material.icons.rounded.Tag
+import androidx.compose.material.minimumInteractiveComponentSize
import androidx.compose.material3.FilterChipDefaults
import androidx.compose.material3.Icon
+import androidx.compose.material3.InputChipDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.minimumInteractiveComponentSize
@@ -30,8 +35,13 @@ import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
+import de.mm20.launcher2.icons.IconService
+import de.mm20.launcher2.icons.StaticLauncherIcon
+import de.mm20.launcher2.icons.TextLayer
+import de.mm20.launcher2.icons.VectorLayer
import de.mm20.launcher2.search.Tag
-import de.mm20.launcher2.ui.ktx.splitLeadingEmoji
+import de.mm20.launcher2.ui.ktx.toPixels
+import org.koin.androidx.compose.inject
@Composable
fun TagChip(
@@ -39,15 +49,12 @@ fun TagChip(
tag: Tag,
selected: Boolean = false,
dragged: Boolean = false,
+ compact: Boolean = false,
onClick: () -> Unit = {},
onLongClick: (() -> Unit)? = null,
clearable: Boolean = false,
onClear: (() -> Unit)? = null,
) {
- val (emoji, tagName) = remember(tag.tag) {
- tag.tag.splitLeadingEmoji()
- }
-
val shape = MaterialTheme.shapes.small
val transition = updateTransition(
@@ -56,10 +63,10 @@ fun TagChip(
val backgroundColor by transition.animateColor(
transitionSpec = {
- if (targetState == 0) tween(100, 200) else tween(100, 0)
+ if (targetState == 0 && initialState >= 2) tween(100, 200) else tween(100, 0)
}
) {
- when(it) {
+ when (it) {
0 -> Color.Transparent
2 -> MaterialTheme.colorScheme.surfaceContainerLow
else -> MaterialTheme.colorScheme.secondaryContainer
@@ -79,17 +86,28 @@ fun TagChip(
}
val elevation by transition.animateDp(
transitionSpec = {
- if (targetState >=2) tween(100, 200) else tween(100, 0)
+ if (targetState >= 2) tween(100, 200) else tween(100, 0)
}
) {
if (it >= 2) 8.dp else 0.dp
}
+ val iconService by inject()
+ val iconSize = InputChipDefaults.AvatarSize.toPixels()
+
+ val icon by remember(tag, iconSize) {
+ iconService.getIcon(
+ tag,
+ iconSize.toInt()
+ )
+ }.collectAsState(null)
+
Row(
modifier = modifier
- .minimumInteractiveComponentSize()
+ .padding(vertical = 8.dp)
.height(32.dp)
+ .widthIn(min = 48.dp)
.shadow(elevation, shape, true)
.border(borderWidth, borderColor, shape)
.background(backgroundColor)
@@ -99,28 +117,34 @@ fun TagChip(
)
.padding(horizontal = 8.dp),
verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center,
) {
- if (emoji != null && tagName != null) {
+ val foregroundLayer = (icon as? StaticLauncherIcon)?.foregroundLayer
+ AnimatedVisibility(foregroundLayer != null && (!compact || foregroundLayer is TextLayer)) {
+ if (foregroundLayer is TextLayer) {
+ Text(
+ text = foregroundLayer.text,
+ modifier = Modifier.width(FilterChipDefaults.IconSize),
+ textAlign = TextAlign.Center,
+ )
+ } else if (foregroundLayer is VectorLayer && !compact) {
+ Icon(
+ modifier = Modifier
+ .size(FilterChipDefaults.IconSize),
+ imageVector = foregroundLayer.vector,
+ contentDescription = null,
+ tint = iconColor
+ )
+ }
+ }
+ AnimatedVisibility(foregroundLayer != null && (!compact || foregroundLayer is VectorLayer)) {
Text(
- emoji,
- modifier = Modifier.width(FilterChipDefaults.IconSize),
- textAlign = TextAlign.Center,
- )
- } else {
- Icon(
- modifier = Modifier
- .size(FilterChipDefaults.IconSize),
- imageVector = Icons.Rounded.Tag,
- contentDescription = null,
- tint = iconColor
+ tag.tag,
+ style = MaterialTheme.typography.labelLarge,
+ color = textColor,
+ modifier = Modifier.padding(horizontal = 8.dp)
)
}
- Text(
- tagName ?: emoji ?: "",
- style = MaterialTheme.typography.labelLarge,
- color = textColor,
- modifier = Modifier.padding(horizontal = 8.dp)
- )
if (clearable) {
Icon(
modifier = Modifier
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/Toolbar.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/Toolbar.kt
index b063c2a5..2fbe8977 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/Toolbar.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/Toolbar.kt
@@ -84,7 +84,7 @@ fun Icons(actions: List, slots: Int) {
val action = actions[i]
val tooltipState = rememberTooltipState()
TooltipBox(
- positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
+ positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
tooltip = {
PlainTooltip {
Text(action.label)
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/dragndrop/DragAndDropGrid.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/dragndrop/DragAndDropGrid.kt
index 52538476..4e85aa56 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/component/dragndrop/DragAndDropGrid.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/dragndrop/DragAndDropGrid.kt
@@ -186,7 +186,7 @@ fun LazyVerticalDragAndDropGrid(
horizontalArrangement,
flingBehavior,
userScrollEnabled,
- content,
+ content = content,
)
}
@@ -215,7 +215,7 @@ fun LazyHorizontalDragAndDropGrid(
verticalArrangement,
flingBehavior,
userScrollEnabled,
- content,
+ content = content,
)
}
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/component/emojipicker/EmojiPicker.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/component/emojipicker/EmojiPicker.kt
new file mode 100644
index 00000000..b6e39033
--- /dev/null
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/component/emojipicker/EmojiPicker.kt
@@ -0,0 +1,206 @@
+package de.mm20.launcher2.ui.component.emojipicker
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconToggleButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.RichTooltip
+import androidx.compose.material3.Text
+import androidx.compose.material3.TooltipBox
+import androidx.compose.material3.TooltipDefaults
+import androidx.compose.material3.minimumInteractiveComponentSize
+import androidx.compose.material3.rememberTooltipState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateMapOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringArrayResource
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import de.mm20.launcher2.ui.R
+import kotlinx.coroutines.launch
+
+@Composable
+fun EmojiPicker(
+ modifier: Modifier = Modifier,
+ onEmojiSelected: (String) -> Unit = {},
+) {
+ val context = LocalContext.current
+
+ val scope = rememberCoroutineScope()
+
+ var selectedCategory = remember {
+ mutableIntStateOf(0)
+ }
+
+ val categories = remember {
+ val typedArray =
+ context.resources.obtainTypedArray(R.array.emoji_by_category_raw_resources_gender_inclusive)
+ IntArray(typedArray.length()) { typedArray.getResourceId(it, 0) }.also {
+ typedArray.recycle()
+ }
+ }
+ val categoryNames = stringArrayResource(R.array.category_names)
+ val categoryIcons = remember {
+ val typedArray = context.resources.obtainTypedArray(R.array.emoji_categories_icons)
+ IntArray(typedArray.length()) { typedArray.getResourceId(it, 0) }.also {
+ typedArray.recycle()
+ }
+ }
+
+ val emojis = remember {
+ mutableStateMapOf>>()
+ }
+
+ LaunchedEffect(selectedCategory.intValue) {
+ if (selectedCategory.intValue in emojis) return@LaunchedEffect
+ val categoryEmojis =
+ context.resources.openRawResource(categories[selectedCategory.intValue])
+ .bufferedReader().use {
+ it.readLines().map { line ->
+ line.split(",").toTypedArray()
+ }.toTypedArray()
+ }
+ emojis[selectedCategory.intValue] = categoryEmojis
+ }
+
+ LazyVerticalGrid(
+ GridCells.Adaptive(48.dp),
+ modifier = modifier,
+ ) {
+ stickyHeader {
+ Row(
+ modifier = Modifier
+ .padding(bottom = 8.dp)
+ .background(
+ color = MaterialTheme.colorScheme.surfaceContainerHigh,
+ shape = MaterialTheme.shapes.medium
+ )
+ .clip(MaterialTheme.shapes.medium)
+ .horizontalScroll(rememberScrollState())
+
+ ) {
+ for (i in categories.indices) {
+ IconToggleButton(
+ checked = selectedCategory.intValue == i,
+ onCheckedChange = {
+ if (it) selectedCategory.intValue = i
+ },
+ ) {
+ if (i < categoryIcons.size && categoryIcons[i] != 0) {
+ Icon(
+ painterResource(id = categoryIcons[i]),
+ contentDescription = categoryNames[i]
+ )
+ }
+ }
+ }
+ }
+ }
+ items(
+ emojis[selectedCategory.intValue]?.size ?: 0,
+ key = { selectedCategory.intValue.toString() + "-" + emojis[selectedCategory.intValue]?.get(it)?.get(0) }
+ ) { index ->
+ val emoji = emojis[selectedCategory.intValue]?.get(index) ?: return@items
+ if (emoji.size > 1) {
+ val tooltipState = rememberTooltipState(
+ isPersistent = true,
+ )
+ TooltipBox(
+ modifier = Modifier.animateItem(),
+ positionProvider = TooltipDefaults
+ .rememberTooltipPositionProvider(),
+ state = tooltipState,
+ tooltip = {
+ RichTooltip {
+ Row(
+ modifier = Modifier.horizontalScroll(rememberScrollState())
+ ) {
+ for (e in emoji) {
+ EmojiButton(
+ emoji = e,
+ onClick = {
+ onEmojiSelected(e)
+ },
+ )
+ }
+ }
+ }
+ },
+ ) {
+ EmojiButton(
+ emoji = emoji[0],
+ onClick = {
+ onEmojiSelected(emoji[0])
+ },
+ onLongClick = {
+ scope.launch {
+ tooltipState.show()
+ }
+ },
+ )
+ }
+ } else {
+ EmojiButton(
+ modifier = Modifier.animateItem(),
+ emoji = emoji[0],
+ onClick = {
+ onEmojiSelected(emoji[0])
+ },
+ )
+ }
+ }
+ }
+}
+
+@Composable
+private fun EmojiButton(
+ emoji: String,
+ onClick: () -> Unit,
+ onLongClick: (() -> Unit)? = null,
+ modifier: Modifier = Modifier,
+) {
+ Box(
+ modifier = modifier
+ .clip(MaterialTheme.shapes.medium)
+ .combinedClickable(
+ onClick = onClick,
+ onLongClick = onLongClick,
+ )
+ .minimumInteractiveComponentSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ Text(
+ text = emoji,
+ style = MaterialTheme.typography.titleLarge,
+ )
+ }
+}
+
+
+@Preview
+@Composable
+fun EmojiPickerPreview() {
+ EmojiPicker(
+ modifier = Modifier
+ .height(500.dp)
+ .fillMaxWidth()
+ )
+}
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/music/MusicWidget.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/music/MusicWidget.kt
index 65af81f6..52cc7bd5 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/music/MusicWidget.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/music/MusicWidget.kt
@@ -398,7 +398,7 @@ fun CustomActions(
val action = actions.customActions[i]
val tooltipState = rememberTooltipState()
TooltipBox(
- positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
+ positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
state = tooltipState,
tooltip = {
PlainTooltip {
@@ -458,7 +458,7 @@ fun CustomActions(
val action = actions.customActions.last()
TooltipBox(
state = tooltipState,
- positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
+ positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
tooltip = {
PlainTooltip {
Text(action.name.toString())
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidget.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidget.kt
index 03dab61b..53b202ea 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidget.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/notes/NotesWidget.kt
@@ -184,7 +184,7 @@ fun NotesWidget(
val tooltipState = rememberTooltipState()
TooltipBox(
state = tooltipState,
- positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
+ positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
tooltip = {
PlainTooltip {
Text(
@@ -227,7 +227,7 @@ fun NotesWidget(
if (isLastWidget == false) {
TooltipBox(
state = tooltipState,
- positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
+ positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
tooltip = {
PlainTooltip {
Text(stringResource(R.string.notes_widget_action_dismiss))
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/CorePaletteColorPreference.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/CorePaletteColorPreference.kt
index 4796cd02..d1308aac 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/CorePaletteColorPreference.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/CorePaletteColorPreference.kt
@@ -56,7 +56,7 @@ fun CorePaletteColorPreference(
TooltipBox(
state = tooltipState,
- positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
+ positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
tooltip = {
PlainTooltip {
Text(title)
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemeColorPreference.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemeColorPreference.kt
index 33902128..c437f568 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemeColorPreference.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemeColorPreference.kt
@@ -85,7 +85,7 @@ fun ThemeColorPreference(
TooltipBox(
state = tooltipState,
- positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
+ positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
tooltip = { PlainTooltip { Text(title) } }
) {
ColorSwatch(
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tags/EditTagSheet.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tags/EditTagSheet.kt
index 2f0b4113..a50b08c2 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tags/EditTagSheet.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tags/EditTagSheet.kt
@@ -6,6 +6,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
@@ -22,8 +23,11 @@ import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Check
+import androidx.compose.material.icons.rounded.Delete
+import androidx.compose.material.icons.rounded.Tag
import androidx.compose.material.icons.rounded.Warning
import androidx.compose.material3.Button
+import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
@@ -38,6 +42,10 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.graphics.PathEffect
+import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
@@ -48,6 +56,7 @@ import de.mm20.launcher2.ui.R
import de.mm20.launcher2.ui.component.BottomSheetDialog
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
import de.mm20.launcher2.ui.component.SmallMessage
+import de.mm20.launcher2.ui.component.emojipicker.EmojiPicker
import de.mm20.launcher2.ui.ktx.toPixels
import de.mm20.launcher2.ui.locals.LocalGridSettings
@@ -87,12 +96,18 @@ fun EditTagSheet(
Text(stringResource(R.string.action_next))
}
}
- } else {
+ } else if (viewModel.page == EditTagSheetPage.PickItems) {
{
OutlinedButton(onClick = { viewModel.closeItemPicker() }) {
Text(stringResource(id = R.string.ok))
}
}
+ } else {
+ {
+ OutlinedButton(onClick = { viewModel.closeIconPicker() }) {
+ Text(stringResource(id = android.R.string.cancel))
+ }
+ }
},
onDismissRequest = {
if (viewModel.page == EditTagSheetPage.CustomizeTag) {
@@ -109,6 +124,7 @@ fun EditTagSheet(
EditTagSheetPage.CreateTag -> CreateNewTagPage(viewModel, it)
EditTagSheetPage.PickItems -> PickItems(viewModel, it)
EditTagSheetPage.CustomizeTag -> CustomizeTag(viewModel, it)
+ EditTagSheetPage.PickIcon -> PickIcon(viewModel, it)
}
}
}
@@ -238,19 +254,72 @@ fun ListItem(
@Composable
fun CustomizeTag(viewModel: EditTagSheetVM, paddingValues: PaddingValues) {
val iconSize = 32.dp.toPixels()
+ val tagEmoji = viewModel.tagEmoji
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.fillMaxWidth()
.padding(paddingValues)
) {
- OutlinedTextField(
+
+ Row(
modifier = Modifier.fillMaxWidth(),
- singleLine = true,
- label = { Text(stringResource(R.string.tag_name)) },
- value = viewModel.tagName,
- onValueChange = { viewModel.tagName = it }
- )
+ ) {
+ val outlineVariant = MaterialTheme.colorScheme.outlineVariant
+ Box(
+ modifier = Modifier
+ .padding(end = 16.dp)
+ .clip(CircleShape)
+ .clickable {
+ viewModel.openIconPicker()
+ }
+ .size(56.dp)
+ then (
+ if (tagEmoji != null) {
+ Modifier.background(
+ MaterialTheme.colorScheme.secondaryContainer,
+ CircleShape
+ )
+ } else {
+ Modifier.drawBehind {
+ val w = with(density) { 2.dp.toPx() }
+ drawCircle(
+ color = outlineVariant,
+ style = Stroke(
+ width = w,
+ pathEffect = PathEffect.dashPathEffect(
+ floatArrayOf(
+ w * 2,
+ w * 2
+ )
+ )
+ )
+ )
+ }
+ }
+ ),
+ contentAlignment = Alignment.Center,
+ ) {
+ if (tagEmoji != null) {
+ Text(tagEmoji)
+ } else {
+ Icon(
+ Icons.Rounded.Tag,
+ null,
+ tint = MaterialTheme.colorScheme.onSurfaceVariant,
+ )
+ }
+ }
+
+ OutlinedTextField(
+ modifier = Modifier.weight(1f),
+ singleLine = true,
+ placeholder = { Text(stringResource(R.string.tag_name)) },
+ value = viewModel.tagName,
+ onValueChange = { viewModel.tagName = it },
+ )
+ }
+
val icon1 = remember(viewModel.taggedItems.getOrNull(0)?.key) {
viewModel.taggedItems.getOrNull(0)?.let {
viewModel.getIcon(it, iconSize.toInt())
@@ -286,21 +355,27 @@ fun CustomizeTag(viewModel: EditTagSheetVM, paddingValues: PaddingValues) {
.height(32.dp),
contentAlignment = Alignment.CenterEnd
) {
- ShapedLauncherIcon(
- size = 32.dp,
- icon = { icon1?.value },
- modifier = Modifier.offset(x = -0.dp)
- )
- ShapedLauncherIcon(
- size = 32.dp,
- icon = { icon2?.value },
- modifier = Modifier.offset(x = -16.dp)
- )
- ShapedLauncherIcon(
- size = 32.dp,
- icon = { icon3?.value },
- modifier = Modifier.offset(x = -32.dp)
- )
+ icon1?.value?.let {
+ ShapedLauncherIcon(
+ size = 32.dp,
+ icon = { it },
+ modifier = Modifier.offset(x = -0.dp)
+ )
+ }
+ icon2?.value?.let {
+ ShapedLauncherIcon(
+ size = 32.dp,
+ icon = { it },
+ modifier = Modifier.offset(x = -16.dp)
+ )
+ }
+ icon3?.value?.let {
+ ShapedLauncherIcon(
+ size = 32.dp,
+ icon = { it },
+ modifier = Modifier.offset(x = -32.dp)
+ )
+ }
}
}
AnimatedVisibility(viewModel.tagNameExists || viewModel.taggedItems.isEmpty()) {
@@ -313,4 +388,44 @@ fun CustomizeTag(viewModel: EditTagSheetVM, paddingValues: PaddingValues) {
)
}
}
+}
+
+@Composable
+fun PickIcon(
+ viewModel: EditTagSheetVM,
+ paddingValues: PaddingValues
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(paddingValues)
+ ) {
+ OutlinedButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 16.dp),
+ onClick = {
+ viewModel.selectIcon(null)
+ },
+ contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
+ ) {
+ Icon(
+ Icons.Rounded.Delete,
+ null,
+ modifier = Modifier
+ .padding(end = ButtonDefaults.IconSpacing)
+ .size(ButtonDefaults.IconSize)
+ )
+ Text(stringResource(R.string.reset_icon))
+ }
+ EmojiPicker(
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f),
+ onEmojiSelected = {
+ viewModel.selectIcon(it)
+ }
+ )
+ }
+
}
\ No newline at end of file
diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tags/EditTagSheetVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tags/EditTagSheetVM.kt
index 8d0c1786..b59dc080 100644
--- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tags/EditTagSheetVM.kt
+++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/tags/EditTagSheetVM.kt
@@ -9,10 +9,14 @@ import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import de.mm20.launcher2.applications.AppRepository
+import de.mm20.launcher2.data.customattrs.CustomTextIcon
import de.mm20.launcher2.icons.IconService
import de.mm20.launcher2.icons.LauncherIcon
+import de.mm20.launcher2.icons.StaticLauncherIcon
+import de.mm20.launcher2.icons.TextLayer
import de.mm20.launcher2.search.SavableSearchable
import de.mm20.launcher2.search.SearchService
+import de.mm20.launcher2.search.Tag
import de.mm20.launcher2.services.tags.TagsService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
@@ -31,6 +35,7 @@ class EditTagSheetVM : ViewModel(), KoinComponent {
private var oldTagName by mutableStateOf(null)
private var allTags by mutableStateOf(emptySet())
var tagName by mutableStateOf("")
+ var tagEmoji by mutableStateOf(null)
var loading by mutableStateOf(true)
@@ -46,7 +51,6 @@ class EditTagSheetVM : ViewModel(), KoinComponent {
fun init(tag: String?) {
- Log.d("MM20", "Init with tag: $tag")
loading = true
this.oldTagName = tag
this.tagName = tag ?: ""
@@ -55,6 +59,9 @@ class EditTagSheetVM : ViewModel(), KoinComponent {
viewModelScope.launch(Dispatchers.Default) {
allTags = tagService.getAllTags().first().toSet()
val items = if (tag != null) tagService.getTaggedItems(tag).first() else emptyList()
+ val icon = if (tag != null) iconService.getIcon(Tag(tag), 0).first() else null
+ tagEmoji = ((icon as? StaticLauncherIcon)?.foregroundLayer as? TextLayer)?.text
+
val apps = appRepository.findMany().first { it.isNotEmpty() }.sorted()
taggedItems = items
taggableApps = apps.map { app -> TaggableItem(app, items.any { app.key == it.key }) }
@@ -69,9 +76,19 @@ class EditTagSheetVM : ViewModel(), KoinComponent {
fun save() {
val oldName = oldTagName
val newName = tagName
+ val tagEmoji = tagEmoji
if (taggedItems.isEmpty() && oldName != null) tagService.deleteTag(oldName)
else if (oldName != null) tagService.updateTag(oldName, newName = newName, items = taggedItems)
else tagService.createTag(tagName, taggedItems)
+
+ if (oldName != null && oldName != newName) {
+ iconService.setCustomIcon(Tag(oldName), null)
+ }
+ if (tagEmoji != null) {
+ iconService.setCustomIcon(Tag(newName), CustomTextIcon(tagEmoji))
+ } else {
+ iconService.setCustomIcon(Tag(newName), null)
+ }
loading = true
}
@@ -89,6 +106,19 @@ class EditTagSheetVM : ViewModel(), KoinComponent {
page = EditTagSheetPage.PickItems
}
+ fun openIconPicker() {
+ page = EditTagSheetPage.PickIcon
+ }
+
+ fun closeIconPicker() {
+ page = EditTagSheetPage.CustomizeTag
+ }
+
+ fun selectIcon(emoji: String?) {
+ tagEmoji = emoji
+ closeIconPicker()
+ }
+
fun closeItemPicker() {
page = EditTagSheetPage.CustomizeTag
}
@@ -114,6 +144,7 @@ enum class EditTagSheetPage {
CreateTag,
PickItems,
CustomizeTag,
+ PickIcon,
}
@Stable
diff --git a/core/i18n/src/main/res/values/strings.xml b/core/i18n/src/main/res/values/strings.xml
index c72fc67f..af33845e 100644
--- a/core/i18n/src/main/res/values/strings.xml
+++ b/core/i18n/src/main/res/values/strings.xml
@@ -1002,4 +1002,5 @@
- %1$s list selected
- %1$s lists selected
+ Reset icon
\ No newline at end of file
diff --git a/data/customattrs/src/main/java/de/mm20/launcher2/data/customattrs/CustomAttribute.kt b/data/customattrs/src/main/java/de/mm20/launcher2/data/customattrs/CustomAttribute.kt
index 0a4114af..bb2f842e 100644
--- a/data/customattrs/src/main/java/de/mm20/launcher2/data/customattrs/CustomAttribute.kt
+++ b/data/customattrs/src/main/java/de/mm20/launcher2/data/customattrs/CustomAttribute.kt
@@ -119,6 +119,12 @@ sealed class CustomIcon : CustomAttribute {
}
"force_themed_icon" -> ForceThemedIcon
"default_placeholder_icon" -> DefaultPlaceholderIcon
+ "custom_text_icon" -> {
+ CustomTextIcon(
+ text = payload.getString("text"),
+ color = payload.getInt("color")
+ )
+ }
else -> null
}
}
@@ -233,4 +239,17 @@ data object DefaultPlaceholderIcon: CustomIcon() {
"type" to "default_placeholder_icon"
).toString()
}
+}
+
+data class CustomTextIcon(
+ val text: String,
+ val color: Int = 0,
+): CustomIcon() {
+ override fun toDatabaseValue(): String {
+ return jsonObjectOf(
+ "type" to "custom_text_icon",
+ "text" to text,
+ "color" to color,
+ ).toString()
+ }
}
\ No newline at end of file
diff --git a/data/database/build.gradle.kts b/data/database/build.gradle.kts
index cf91a04f..29d8ac0c 100644
--- a/data/database/build.gradle.kts
+++ b/data/database/build.gradle.kts
@@ -49,6 +49,7 @@ dependencies {
ksp(libs.androidx.roomcompiler)
api(libs.androidx.room)
implementation(libs.koin.android)
+ implementation(libs.emoji4j)
implementation(project(":core:i18n"))
implementation(project(":core:ktx"))
diff --git a/data/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt b/data/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt
index 06d6c6c4..a41b50ce 100644
--- a/data/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt
+++ b/data/database/src/main/java/de/mm20/launcher2/database/AppDatabase.kt
@@ -36,6 +36,7 @@ import de.mm20.launcher2.database.migrations.Migration_22_23
import de.mm20.launcher2.database.migrations.Migration_23_24
import de.mm20.launcher2.database.migrations.Migration_24_25
import de.mm20.launcher2.database.migrations.Migration_25_26
+import de.mm20.launcher2.database.migrations.Migration_26_27
import de.mm20.launcher2.database.migrations.Migration_6_7
import de.mm20.launcher2.database.migrations.Migration_7_8
import de.mm20.launcher2.database.migrations.Migration_8_9
@@ -55,7 +56,7 @@ import java.util.UUID
SearchActionEntity::class,
ThemeEntity::class,
PluginEntity::class,
- ], version = 26, exportSchema = true
+ ], version = 27, exportSchema = true
)
@TypeConverters(ComponentNameConverter::class)
abstract class AppDatabase : RoomDatabase() {
@@ -154,6 +155,7 @@ abstract class AppDatabase : RoomDatabase() {
Migration_23_24(),
Migration_24_25(),
Migration_25_26(),
+ Migration_26_27(),
).build()
if (_instance == null) _instance = instance
return instance
diff --git a/data/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_26_27.kt b/data/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_26_27.kt
new file mode 100644
index 00000000..47adf888
--- /dev/null
+++ b/data/database/src/main/java/de/mm20/launcher2/database/migrations/Migration_26_27.kt
@@ -0,0 +1,72 @@
+package de.mm20.launcher2.database.migrations
+
+import android.content.ContentValues
+import android.database.sqlite.SQLiteDatabase
+import android.util.Log
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
+import com.sigpwned.emoji4j.core.Grapheme
+import com.sigpwned.emoji4j.core.GraphemeMatcher
+import de.mm20.launcher2.ktx.jsonObjectOf
+
+class Migration_26_27 : Migration(26, 27) {
+ override fun migrate(db: SupportSQLiteDatabase) {
+ Log.d("Tags-Migration", "Migrating tags")
+ db.query("SELECT DISTINCT value FROM CustomAttributes WHERE type = 'tag'").use { cursor ->
+ while (cursor.moveToNext()) {
+ val oldName = cursor.getString(0)
+ val (emoji, newName) = oldName.splitLeadingEmoji()
+
+ if (emoji != null) {
+ db.update(
+ "CustomAttributes",
+ SQLiteDatabase.CONFLICT_FAIL,
+ ContentValues().apply {
+ put("value", newName)
+ }, "type = 'tag' AND value = ?",
+ arrayOf(oldName)
+ )
+ db.insert(
+ "CustomAttributes",
+ SQLiteDatabase.CONFLICT_REPLACE,
+ ContentValues().apply {
+ put("key", "tag://$newName")
+ put("type", "icon")
+ put("value", jsonObjectOf(
+ "type" to "custom_text_icon",
+ "text" to emoji,
+ "color" to 0,
+ ).toString())
+ }
+ )
+ db.update(
+ "Searchable",
+ SQLiteDatabase.CONFLICT_IGNORE,
+ ContentValues().apply {
+ put("key", "tag://$newName")
+ put("searchable", jsonObjectOf(
+ "tag" to newName
+ ).toString())
+ },
+ "key = ?",
+ arrayOf("tag://$oldName")
+ )
+ }
+ }
+ }
+ }
+
+ fun String.splitLeadingEmoji(): Pair {
+ val matcher = GraphemeMatcher(this)
+ if (!matcher.find()) return null to this.trim()
+ val grapheme = matcher.grapheme()
+ if (grapheme?.type == Grapheme.Type.EMOJI && matcher.start() == 0) {
+ val end = matcher.end()
+ val emoji = this.substring(0, end)
+ val tagName = this.substring(end).takeIf { it.isNotBlank() }
+ if (tagName == null) return emoji to emoji
+ return emoji to tagName
+ }
+ return null to this.trim()
+ }
+}
\ No newline at end of file
diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/IconService.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/IconService.kt
index b8d24da4..70797e4e 100644
--- a/services/icons/src/main/java/de/mm20/launcher2/icons/IconService.kt
+++ b/services/icons/src/main/java/de/mm20/launcher2/icons/IconService.kt
@@ -11,6 +11,7 @@ import de.mm20.launcher2.data.customattrs.AdaptifiedLegacyIcon
import de.mm20.launcher2.data.customattrs.CustomAttributesRepository
import de.mm20.launcher2.data.customattrs.CustomIcon
import de.mm20.launcher2.data.customattrs.CustomIconPackIcon
+import de.mm20.launcher2.data.customattrs.CustomTextIcon
import de.mm20.launcher2.data.customattrs.LegacyCustomIconPackIcon
import de.mm20.launcher2.data.customattrs.CustomThemedIcon
import de.mm20.launcher2.data.customattrs.DefaultPlaceholderIcon
@@ -19,6 +20,7 @@ import de.mm20.launcher2.data.customattrs.UnmodifiedSystemDefaultIcon
import de.mm20.launcher2.icons.providers.CalendarIconProvider
import de.mm20.launcher2.icons.providers.CompatIconProvider
import de.mm20.launcher2.icons.providers.CustomIconPackIconProvider
+import de.mm20.launcher2.icons.providers.CustomTextIconProvider
import de.mm20.launcher2.icons.providers.LegacyCustomIconPackIconProvider
import de.mm20.launcher2.icons.providers.CustomThemedIconProvider
import de.mm20.launcher2.icons.providers.DynamicClockIconProvider
@@ -203,6 +205,9 @@ class IconService(
if (customIcon is DefaultPlaceholderIcon) {
return iconProviders.value.lastOrNull()?.let { listOf(it) } ?: emptyList()
}
+ if (customIcon is CustomTextIcon) {
+ return listOf(CustomTextIconProvider(customIcon))
+ }
return emptyList()
}
diff --git a/services/icons/src/main/java/de/mm20/launcher2/icons/providers/CustomTextIconProvider.kt b/services/icons/src/main/java/de/mm20/launcher2/icons/providers/CustomTextIconProvider.kt
new file mode 100644
index 00000000..4b142249
--- /dev/null
+++ b/services/icons/src/main/java/de/mm20/launcher2/icons/providers/CustomTextIconProvider.kt
@@ -0,0 +1,29 @@
+package de.mm20.launcher2.icons.providers
+
+import de.mm20.launcher2.data.customattrs.CustomIconPackIcon
+import de.mm20.launcher2.data.customattrs.CustomTextIcon
+import de.mm20.launcher2.icons.ColorLayer
+import de.mm20.launcher2.icons.LauncherIcon
+import de.mm20.launcher2.icons.StaticLauncherIcon
+import de.mm20.launcher2.icons.TextLayer
+import de.mm20.launcher2.search.SavableSearchable
+
+class CustomTextIconProvider(
+ private val customIcon: CustomTextIcon,
+): IconProvider {
+ override suspend fun getIcon(
+ searchable: SavableSearchable,
+ size: Int
+ ): LauncherIcon? {
+ return StaticLauncherIcon(
+ foregroundLayer = TextLayer(
+ text = customIcon.text,
+ color = customIcon.color,
+ ),
+ backgroundLayer = ColorLayer(
+ color = customIcon.color,
+ ),
+ )
+ }
+
+}
\ No newline at end of file
diff --git a/services/tags/src/main/java/de/mm20/launcher2/services/tags/impl/TagsServiceImpl.kt b/services/tags/src/main/java/de/mm20/launcher2/services/tags/impl/TagsServiceImpl.kt
index b2241f10..31421d0a 100644
--- a/services/tags/src/main/java/de/mm20/launcher2/services/tags/impl/TagsServiceImpl.kt
+++ b/services/tags/src/main/java/de/mm20/launcher2/services/tags/impl/TagsServiceImpl.kt
@@ -45,14 +45,7 @@ internal class TagsServiceImpl(
}
if (newName != null && newName != tag) {
customAttributesRepository.renameTag(tag, newName).join()
- val pinnedTags = searchableRepository.get(
- includeTypes = listOf(Tag.Domain),
- minPinnedLevel = PinnedLevel.AutomaticallySorted,
- ).first()
- val oldTag = Tag(tag)
- if (pinnedTags.any { it.key == oldTag.key }) {
- searchableRepository.replace(oldTag.key, Tag(newName))
- }
+ searchableRepository.replace(Tag(tag).key, Tag(newName))
}
}