Improve tag emoji assignment
This commit is contained in:
parent
76ee00f404
commit
a21dde5741
1
.idea/inspectionProfiles/Project_Default.xml
generated
1
.idea/inspectionProfiles/Project_Default.xml
generated
@ -39,6 +39,7 @@
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
<option name="previewFile" value="true" />
|
||||
</inspection_tool>
|
||||
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||
<option name="composableFile" value="true" />
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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<IconService>()
|
||||
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(
|
||||
emoji,
|
||||
text = foregroundLayer.text,
|
||||
modifier = Modifier.width(FilterChipDefaults.IconSize),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
} else {
|
||||
} else if (foregroundLayer is VectorLayer && !compact) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
.size(FilterChipDefaults.IconSize),
|
||||
imageVector = Icons.Rounded.Tag,
|
||||
imageVector = foregroundLayer.vector,
|
||||
contentDescription = null,
|
||||
tint = iconColor
|
||||
)
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(foregroundLayer != null && (!compact || foregroundLayer is VectorLayer)) {
|
||||
Text(
|
||||
tagName ?: emoji ?: "",
|
||||
tag.tag,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
color = textColor,
|
||||
modifier = Modifier.padding(horizontal = 8.dp)
|
||||
)
|
||||
}
|
||||
if (clearable) {
|
||||
Icon(
|
||||
modifier = Modifier
|
||||
|
||||
@ -84,7 +84,7 @@ fun Icons(actions: List<ToolbarAction>, slots: Int) {
|
||||
val action = actions[i]
|
||||
val tooltipState = rememberTooltipState()
|
||||
TooltipBox(
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(action.label)
|
||||
|
||||
@ -186,7 +186,7 @@ fun LazyVerticalDragAndDropGrid(
|
||||
horizontalArrangement,
|
||||
flingBehavior,
|
||||
userScrollEnabled,
|
||||
content,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
@ -215,7 +215,7 @@ fun LazyHorizontalDragAndDropGrid(
|
||||
verticalArrangement,
|
||||
flingBehavior,
|
||||
userScrollEnabled,
|
||||
content,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -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<Int, Array<Array<String>>>()
|
||||
}
|
||||
|
||||
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()
|
||||
)
|
||||
}
|
||||
@ -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())
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -56,7 +56,7 @@ fun CorePaletteColorPreference(
|
||||
|
||||
TooltipBox(
|
||||
state = tooltipState,
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
|
||||
tooltip = {
|
||||
PlainTooltip {
|
||||
Text(title)
|
||||
|
||||
@ -85,7 +85,7 @@ fun ThemeColorPreference(
|
||||
|
||||
TooltipBox(
|
||||
state = tooltipState,
|
||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
|
||||
tooltip = { PlainTooltip { Text(title) } }
|
||||
) {
|
||||
ColorSwatch(
|
||||
|
||||
@ -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,23 +355,29 @@ fun CustomizeTag(viewModel: EditTagSheetVM, paddingValues: PaddingValues) {
|
||||
.height(32.dp),
|
||||
contentAlignment = Alignment.CenterEnd
|
||||
) {
|
||||
icon1?.value?.let {
|
||||
ShapedLauncherIcon(
|
||||
size = 32.dp,
|
||||
icon = { icon1?.value },
|
||||
icon = { it },
|
||||
modifier = Modifier.offset(x = -0.dp)
|
||||
)
|
||||
}
|
||||
icon2?.value?.let {
|
||||
ShapedLauncherIcon(
|
||||
size = 32.dp,
|
||||
icon = { icon2?.value },
|
||||
icon = { it },
|
||||
modifier = Modifier.offset(x = -16.dp)
|
||||
)
|
||||
}
|
||||
icon3?.value?.let {
|
||||
ShapedLauncherIcon(
|
||||
size = 32.dp,
|
||||
icon = { icon3?.value },
|
||||
icon = { it },
|
||||
modifier = Modifier.offset(x = -32.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(viewModel.tagNameExists || viewModel.taggedItems.isEmpty()) {
|
||||
SmallMessage(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@ -314,3 +389,43 @@ 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)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@ -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<String?>(null)
|
||||
private var allTags by mutableStateOf(emptySet<String>())
|
||||
var tagName by mutableStateOf("")
|
||||
var tagEmoji by mutableStateOf<String?>(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
|
||||
|
||||
@ -1002,4 +1002,5 @@
|
||||
<item quantity="one">%1$s list selected</item>
|
||||
<item quantity="other">%1$s lists selected</item>
|
||||
</plurals>
|
||||
<string name="reset_icon">Reset icon</string>
|
||||
</resources>
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -234,3 +240,16 @@ data object DefaultPlaceholderIcon: CustomIcon() {
|
||||
).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()
|
||||
}
|
||||
}
|
||||
@ -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"))
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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<String?, String> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user