Improve tag emoji assignment

This commit is contained in:
MM20
2024-11-11 20:48:04 +01:00
parent 76ee00f404
commit a21dde5741
20 changed files with 569 additions and 69 deletions
+1
View File
@@ -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(
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
@@ -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,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)
}
)
}
}
@@ -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