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>
|
||||||
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
<option name="composableFile" value="true" />
|
<option name="composableFile" value="true" />
|
||||||
|
<option name="previewFile" value="true" />
|
||||||
</inspection_tool>
|
</inspection_tool>
|
||||||
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
<option name="composableFile" value="true" />
|
<option name="composableFile" value="true" />
|
||||||
|
|||||||
@ -100,6 +100,7 @@ dependencies {
|
|||||||
|
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
implementation(libs.androidx.browser)
|
implementation(libs.androidx.browser)
|
||||||
|
implementation(libs.androidx.emojipicker)
|
||||||
|
|
||||||
implementation(libs.androidx.lifecycle.viewmodelcompose)
|
implementation(libs.androidx.lifecycle.viewmodelcompose)
|
||||||
implementation(libs.androidx.lifecycle.runtimecompose)
|
implementation(libs.androidx.lifecycle.runtimecompose)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package de.mm20.launcher2.ui.common
|
package de.mm20.launcher2.ui.common
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.animateColor
|
import androidx.compose.animation.animateColor
|
||||||
import androidx.compose.animation.core.animateDp
|
import androidx.compose.animation.core.animateDp
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
@ -8,16 +9,20 @@ import androidx.compose.foundation.background
|
|||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.Close
|
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.FilterChipDefaults
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.InputChipDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.minimumInteractiveComponentSize
|
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.graphics.Color
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
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.search.Tag
|
||||||
import de.mm20.launcher2.ui.ktx.splitLeadingEmoji
|
import de.mm20.launcher2.ui.ktx.toPixels
|
||||||
|
import org.koin.androidx.compose.inject
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TagChip(
|
fun TagChip(
|
||||||
@ -39,15 +49,12 @@ fun TagChip(
|
|||||||
tag: Tag,
|
tag: Tag,
|
||||||
selected: Boolean = false,
|
selected: Boolean = false,
|
||||||
dragged: Boolean = false,
|
dragged: Boolean = false,
|
||||||
|
compact: Boolean = false,
|
||||||
onClick: () -> Unit = {},
|
onClick: () -> Unit = {},
|
||||||
onLongClick: (() -> Unit)? = null,
|
onLongClick: (() -> Unit)? = null,
|
||||||
clearable: Boolean = false,
|
clearable: Boolean = false,
|
||||||
onClear: (() -> Unit)? = null,
|
onClear: (() -> Unit)? = null,
|
||||||
) {
|
) {
|
||||||
val (emoji, tagName) = remember(tag.tag) {
|
|
||||||
tag.tag.splitLeadingEmoji()
|
|
||||||
}
|
|
||||||
|
|
||||||
val shape = MaterialTheme.shapes.small
|
val shape = MaterialTheme.shapes.small
|
||||||
|
|
||||||
val transition = updateTransition(
|
val transition = updateTransition(
|
||||||
@ -56,10 +63,10 @@ fun TagChip(
|
|||||||
|
|
||||||
val backgroundColor by transition.animateColor(
|
val backgroundColor by transition.animateColor(
|
||||||
transitionSpec = {
|
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
|
0 -> Color.Transparent
|
||||||
2 -> MaterialTheme.colorScheme.surfaceContainerLow
|
2 -> MaterialTheme.colorScheme.surfaceContainerLow
|
||||||
else -> MaterialTheme.colorScheme.secondaryContainer
|
else -> MaterialTheme.colorScheme.secondaryContainer
|
||||||
@ -79,17 +86,28 @@ fun TagChip(
|
|||||||
}
|
}
|
||||||
val elevation by transition.animateDp(
|
val elevation by transition.animateDp(
|
||||||
transitionSpec = {
|
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
|
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(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.minimumInteractiveComponentSize()
|
.padding(vertical = 8.dp)
|
||||||
.height(32.dp)
|
.height(32.dp)
|
||||||
|
.widthIn(min = 48.dp)
|
||||||
.shadow(elevation, shape, true)
|
.shadow(elevation, shape, true)
|
||||||
.border(borderWidth, borderColor, shape)
|
.border(borderWidth, borderColor, shape)
|
||||||
.background(backgroundColor)
|
.background(backgroundColor)
|
||||||
@ -99,28 +117,34 @@ fun TagChip(
|
|||||||
)
|
)
|
||||||
.padding(horizontal = 8.dp),
|
.padding(horizontal = 8.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
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(
|
Text(
|
||||||
emoji,
|
tag.tag,
|
||||||
modifier = Modifier.width(FilterChipDefaults.IconSize),
|
style = MaterialTheme.typography.labelLarge,
|
||||||
textAlign = TextAlign.Center,
|
color = textColor,
|
||||||
)
|
modifier = Modifier.padding(horizontal = 8.dp)
|
||||||
} else {
|
|
||||||
Icon(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(FilterChipDefaults.IconSize),
|
|
||||||
imageVector = Icons.Rounded.Tag,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = iconColor
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
|
||||||
tagName ?: emoji ?: "",
|
|
||||||
style = MaterialTheme.typography.labelLarge,
|
|
||||||
color = textColor,
|
|
||||||
modifier = Modifier.padding(horizontal = 8.dp)
|
|
||||||
)
|
|
||||||
if (clearable) {
|
if (clearable) {
|
||||||
Icon(
|
Icon(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@ -84,7 +84,7 @@ fun Icons(actions: List<ToolbarAction>, slots: Int) {
|
|||||||
val action = actions[i]
|
val action = actions[i]
|
||||||
val tooltipState = rememberTooltipState()
|
val tooltipState = rememberTooltipState()
|
||||||
TooltipBox(
|
TooltipBox(
|
||||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
|
||||||
tooltip = {
|
tooltip = {
|
||||||
PlainTooltip {
|
PlainTooltip {
|
||||||
Text(action.label)
|
Text(action.label)
|
||||||
|
|||||||
@ -186,7 +186,7 @@ fun LazyVerticalDragAndDropGrid(
|
|||||||
horizontalArrangement,
|
horizontalArrangement,
|
||||||
flingBehavior,
|
flingBehavior,
|
||||||
userScrollEnabled,
|
userScrollEnabled,
|
||||||
content,
|
content = content,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,7 +215,7 @@ fun LazyHorizontalDragAndDropGrid(
|
|||||||
verticalArrangement,
|
verticalArrangement,
|
||||||
flingBehavior,
|
flingBehavior,
|
||||||
userScrollEnabled,
|
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 action = actions.customActions[i]
|
||||||
val tooltipState = rememberTooltipState()
|
val tooltipState = rememberTooltipState()
|
||||||
TooltipBox(
|
TooltipBox(
|
||||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
|
||||||
state = tooltipState,
|
state = tooltipState,
|
||||||
tooltip = {
|
tooltip = {
|
||||||
PlainTooltip {
|
PlainTooltip {
|
||||||
@ -458,7 +458,7 @@ fun CustomActions(
|
|||||||
val action = actions.customActions.last()
|
val action = actions.customActions.last()
|
||||||
TooltipBox(
|
TooltipBox(
|
||||||
state = tooltipState,
|
state = tooltipState,
|
||||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
|
||||||
tooltip = {
|
tooltip = {
|
||||||
PlainTooltip {
|
PlainTooltip {
|
||||||
Text(action.name.toString())
|
Text(action.name.toString())
|
||||||
|
|||||||
@ -184,7 +184,7 @@ fun NotesWidget(
|
|||||||
val tooltipState = rememberTooltipState()
|
val tooltipState = rememberTooltipState()
|
||||||
TooltipBox(
|
TooltipBox(
|
||||||
state = tooltipState,
|
state = tooltipState,
|
||||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
|
||||||
tooltip = {
|
tooltip = {
|
||||||
PlainTooltip {
|
PlainTooltip {
|
||||||
Text(
|
Text(
|
||||||
@ -227,7 +227,7 @@ fun NotesWidget(
|
|||||||
if (isLastWidget == false) {
|
if (isLastWidget == false) {
|
||||||
TooltipBox(
|
TooltipBox(
|
||||||
state = tooltipState,
|
state = tooltipState,
|
||||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
|
||||||
tooltip = {
|
tooltip = {
|
||||||
PlainTooltip {
|
PlainTooltip {
|
||||||
Text(stringResource(R.string.notes_widget_action_dismiss))
|
Text(stringResource(R.string.notes_widget_action_dismiss))
|
||||||
|
|||||||
@ -56,7 +56,7 @@ fun CorePaletteColorPreference(
|
|||||||
|
|
||||||
TooltipBox(
|
TooltipBox(
|
||||||
state = tooltipState,
|
state = tooltipState,
|
||||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
|
||||||
tooltip = {
|
tooltip = {
|
||||||
PlainTooltip {
|
PlainTooltip {
|
||||||
Text(title)
|
Text(title)
|
||||||
|
|||||||
@ -85,7 +85,7 @@ fun ThemeColorPreference(
|
|||||||
|
|
||||||
TooltipBox(
|
TooltipBox(
|
||||||
state = tooltipState,
|
state = tooltipState,
|
||||||
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(),
|
||||||
tooltip = { PlainTooltip { Text(title) } }
|
tooltip = { PlainTooltip { Text(title) } }
|
||||||
) {
|
) {
|
||||||
ColorSwatch(
|
ColorSwatch(
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import androidx.compose.foundation.clickable
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
@ -22,8 +23,11 @@ import androidx.compose.foundation.text.KeyboardActions
|
|||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.rounded.Check
|
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.material.icons.rounded.Warning
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
import androidx.compose.material3.OutlinedButton
|
||||||
@ -38,6 +42,10 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
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.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
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.BottomSheetDialog
|
||||||
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
import de.mm20.launcher2.ui.component.ShapedLauncherIcon
|
||||||
import de.mm20.launcher2.ui.component.SmallMessage
|
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.ktx.toPixels
|
||||||
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
import de.mm20.launcher2.ui.locals.LocalGridSettings
|
||||||
|
|
||||||
@ -87,12 +96,18 @@ fun EditTagSheet(
|
|||||||
Text(stringResource(R.string.action_next))
|
Text(stringResource(R.string.action_next))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if (viewModel.page == EditTagSheetPage.PickItems) {
|
||||||
{
|
{
|
||||||
OutlinedButton(onClick = { viewModel.closeItemPicker() }) {
|
OutlinedButton(onClick = { viewModel.closeItemPicker() }) {
|
||||||
Text(stringResource(id = R.string.ok))
|
Text(stringResource(id = R.string.ok))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
{
|
||||||
|
OutlinedButton(onClick = { viewModel.closeIconPicker() }) {
|
||||||
|
Text(stringResource(id = android.R.string.cancel))
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onDismissRequest = {
|
onDismissRequest = {
|
||||||
if (viewModel.page == EditTagSheetPage.CustomizeTag) {
|
if (viewModel.page == EditTagSheetPage.CustomizeTag) {
|
||||||
@ -109,6 +124,7 @@ fun EditTagSheet(
|
|||||||
EditTagSheetPage.CreateTag -> CreateNewTagPage(viewModel, it)
|
EditTagSheetPage.CreateTag -> CreateNewTagPage(viewModel, it)
|
||||||
EditTagSheetPage.PickItems -> PickItems(viewModel, it)
|
EditTagSheetPage.PickItems -> PickItems(viewModel, it)
|
||||||
EditTagSheetPage.CustomizeTag -> CustomizeTag(viewModel, it)
|
EditTagSheetPage.CustomizeTag -> CustomizeTag(viewModel, it)
|
||||||
|
EditTagSheetPage.PickIcon -> PickIcon(viewModel, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -238,19 +254,72 @@ fun ListItem(
|
|||||||
@Composable
|
@Composable
|
||||||
fun CustomizeTag(viewModel: EditTagSheetVM, paddingValues: PaddingValues) {
|
fun CustomizeTag(viewModel: EditTagSheetVM, paddingValues: PaddingValues) {
|
||||||
val iconSize = 32.dp.toPixels()
|
val iconSize = 32.dp.toPixels()
|
||||||
|
val tagEmoji = viewModel.tagEmoji
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(paddingValues)
|
.padding(paddingValues)
|
||||||
) {
|
) {
|
||||||
OutlinedTextField(
|
|
||||||
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
singleLine = true,
|
) {
|
||||||
label = { Text(stringResource(R.string.tag_name)) },
|
val outlineVariant = MaterialTheme.colorScheme.outlineVariant
|
||||||
value = viewModel.tagName,
|
Box(
|
||||||
onValueChange = { viewModel.tagName = it }
|
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) {
|
val icon1 = remember(viewModel.taggedItems.getOrNull(0)?.key) {
|
||||||
viewModel.taggedItems.getOrNull(0)?.let {
|
viewModel.taggedItems.getOrNull(0)?.let {
|
||||||
viewModel.getIcon(it, iconSize.toInt())
|
viewModel.getIcon(it, iconSize.toInt())
|
||||||
@ -286,21 +355,27 @@ fun CustomizeTag(viewModel: EditTagSheetVM, paddingValues: PaddingValues) {
|
|||||||
.height(32.dp),
|
.height(32.dp),
|
||||||
contentAlignment = Alignment.CenterEnd
|
contentAlignment = Alignment.CenterEnd
|
||||||
) {
|
) {
|
||||||
ShapedLauncherIcon(
|
icon1?.value?.let {
|
||||||
size = 32.dp,
|
ShapedLauncherIcon(
|
||||||
icon = { icon1?.value },
|
size = 32.dp,
|
||||||
modifier = Modifier.offset(x = -0.dp)
|
icon = { it },
|
||||||
)
|
modifier = Modifier.offset(x = -0.dp)
|
||||||
ShapedLauncherIcon(
|
)
|
||||||
size = 32.dp,
|
}
|
||||||
icon = { icon2?.value },
|
icon2?.value?.let {
|
||||||
modifier = Modifier.offset(x = -16.dp)
|
ShapedLauncherIcon(
|
||||||
)
|
size = 32.dp,
|
||||||
ShapedLauncherIcon(
|
icon = { it },
|
||||||
size = 32.dp,
|
modifier = Modifier.offset(x = -16.dp)
|
||||||
icon = { icon3?.value },
|
)
|
||||||
modifier = Modifier.offset(x = -32.dp)
|
}
|
||||||
)
|
icon3?.value?.let {
|
||||||
|
ShapedLauncherIcon(
|
||||||
|
size = 32.dp,
|
||||||
|
icon = { it },
|
||||||
|
modifier = Modifier.offset(x = -32.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AnimatedVisibility(viewModel.tagNameExists || viewModel.taggedItems.isEmpty()) {
|
AnimatedVisibility(viewModel.tagNameExists || viewModel.taggedItems.isEmpty()) {
|
||||||
@ -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.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import de.mm20.launcher2.applications.AppRepository
|
import de.mm20.launcher2.applications.AppRepository
|
||||||
|
import de.mm20.launcher2.data.customattrs.CustomTextIcon
|
||||||
import de.mm20.launcher2.icons.IconService
|
import de.mm20.launcher2.icons.IconService
|
||||||
import de.mm20.launcher2.icons.LauncherIcon
|
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.SavableSearchable
|
||||||
import de.mm20.launcher2.search.SearchService
|
import de.mm20.launcher2.search.SearchService
|
||||||
|
import de.mm20.launcher2.search.Tag
|
||||||
import de.mm20.launcher2.services.tags.TagsService
|
import de.mm20.launcher2.services.tags.TagsService
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@ -31,6 +35,7 @@ class EditTagSheetVM : ViewModel(), KoinComponent {
|
|||||||
private var oldTagName by mutableStateOf<String?>(null)
|
private var oldTagName by mutableStateOf<String?>(null)
|
||||||
private var allTags by mutableStateOf(emptySet<String>())
|
private var allTags by mutableStateOf(emptySet<String>())
|
||||||
var tagName by mutableStateOf("")
|
var tagName by mutableStateOf("")
|
||||||
|
var tagEmoji by mutableStateOf<String?>(null)
|
||||||
|
|
||||||
var loading by mutableStateOf(true)
|
var loading by mutableStateOf(true)
|
||||||
|
|
||||||
@ -46,7 +51,6 @@ class EditTagSheetVM : ViewModel(), KoinComponent {
|
|||||||
|
|
||||||
|
|
||||||
fun init(tag: String?) {
|
fun init(tag: String?) {
|
||||||
Log.d("MM20", "Init with tag: $tag")
|
|
||||||
loading = true
|
loading = true
|
||||||
this.oldTagName = tag
|
this.oldTagName = tag
|
||||||
this.tagName = tag ?: ""
|
this.tagName = tag ?: ""
|
||||||
@ -55,6 +59,9 @@ class EditTagSheetVM : ViewModel(), KoinComponent {
|
|||||||
viewModelScope.launch(Dispatchers.Default) {
|
viewModelScope.launch(Dispatchers.Default) {
|
||||||
allTags = tagService.getAllTags().first().toSet()
|
allTags = tagService.getAllTags().first().toSet()
|
||||||
val items = if (tag != null) tagService.getTaggedItems(tag).first() else emptyList()
|
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()
|
val apps = appRepository.findMany().first { it.isNotEmpty() }.sorted()
|
||||||
taggedItems = items
|
taggedItems = items
|
||||||
taggableApps = apps.map { app -> TaggableItem(app, items.any { app.key == it.key }) }
|
taggableApps = apps.map { app -> TaggableItem(app, items.any { app.key == it.key }) }
|
||||||
@ -69,9 +76,19 @@ class EditTagSheetVM : ViewModel(), KoinComponent {
|
|||||||
fun save() {
|
fun save() {
|
||||||
val oldName = oldTagName
|
val oldName = oldTagName
|
||||||
val newName = tagName
|
val newName = tagName
|
||||||
|
val tagEmoji = tagEmoji
|
||||||
if (taggedItems.isEmpty() && oldName != null) tagService.deleteTag(oldName)
|
if (taggedItems.isEmpty() && oldName != null) tagService.deleteTag(oldName)
|
||||||
else if (oldName != null) tagService.updateTag(oldName, newName = newName, items = taggedItems)
|
else if (oldName != null) tagService.updateTag(oldName, newName = newName, items = taggedItems)
|
||||||
else tagService.createTag(tagName, 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
|
loading = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,6 +106,19 @@ class EditTagSheetVM : ViewModel(), KoinComponent {
|
|||||||
page = EditTagSheetPage.PickItems
|
page = EditTagSheetPage.PickItems
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun openIconPicker() {
|
||||||
|
page = EditTagSheetPage.PickIcon
|
||||||
|
}
|
||||||
|
|
||||||
|
fun closeIconPicker() {
|
||||||
|
page = EditTagSheetPage.CustomizeTag
|
||||||
|
}
|
||||||
|
|
||||||
|
fun selectIcon(emoji: String?) {
|
||||||
|
tagEmoji = emoji
|
||||||
|
closeIconPicker()
|
||||||
|
}
|
||||||
|
|
||||||
fun closeItemPicker() {
|
fun closeItemPicker() {
|
||||||
page = EditTagSheetPage.CustomizeTag
|
page = EditTagSheetPage.CustomizeTag
|
||||||
}
|
}
|
||||||
@ -114,6 +144,7 @@ enum class EditTagSheetPage {
|
|||||||
CreateTag,
|
CreateTag,
|
||||||
PickItems,
|
PickItems,
|
||||||
CustomizeTag,
|
CustomizeTag,
|
||||||
|
PickIcon,
|
||||||
}
|
}
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
|
|||||||
@ -1002,4 +1002,5 @@
|
|||||||
<item quantity="one">%1$s list selected</item>
|
<item quantity="one">%1$s list selected</item>
|
||||||
<item quantity="other">%1$s lists selected</item>
|
<item quantity="other">%1$s lists selected</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
|
<string name="reset_icon">Reset icon</string>
|
||||||
</resources>
|
</resources>
|
||||||
@ -119,6 +119,12 @@ sealed class CustomIcon : CustomAttribute {
|
|||||||
}
|
}
|
||||||
"force_themed_icon" -> ForceThemedIcon
|
"force_themed_icon" -> ForceThemedIcon
|
||||||
"default_placeholder_icon" -> DefaultPlaceholderIcon
|
"default_placeholder_icon" -> DefaultPlaceholderIcon
|
||||||
|
"custom_text_icon" -> {
|
||||||
|
CustomTextIcon(
|
||||||
|
text = payload.getString("text"),
|
||||||
|
color = payload.getInt("color")
|
||||||
|
)
|
||||||
|
}
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -234,3 +240,16 @@ data object DefaultPlaceholderIcon: CustomIcon() {
|
|||||||
).toString()
|
).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)
|
ksp(libs.androidx.roomcompiler)
|
||||||
api(libs.androidx.room)
|
api(libs.androidx.room)
|
||||||
implementation(libs.koin.android)
|
implementation(libs.koin.android)
|
||||||
|
implementation(libs.emoji4j)
|
||||||
|
|
||||||
implementation(project(":core:i18n"))
|
implementation(project(":core:i18n"))
|
||||||
implementation(project(":core:ktx"))
|
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_23_24
|
||||||
import de.mm20.launcher2.database.migrations.Migration_24_25
|
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_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_6_7
|
||||||
import de.mm20.launcher2.database.migrations.Migration_7_8
|
import de.mm20.launcher2.database.migrations.Migration_7_8
|
||||||
import de.mm20.launcher2.database.migrations.Migration_8_9
|
import de.mm20.launcher2.database.migrations.Migration_8_9
|
||||||
@ -55,7 +56,7 @@ import java.util.UUID
|
|||||||
SearchActionEntity::class,
|
SearchActionEntity::class,
|
||||||
ThemeEntity::class,
|
ThemeEntity::class,
|
||||||
PluginEntity::class,
|
PluginEntity::class,
|
||||||
], version = 26, exportSchema = true
|
], version = 27, exportSchema = true
|
||||||
)
|
)
|
||||||
@TypeConverters(ComponentNameConverter::class)
|
@TypeConverters(ComponentNameConverter::class)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
@ -154,6 +155,7 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
Migration_23_24(),
|
Migration_23_24(),
|
||||||
Migration_24_25(),
|
Migration_24_25(),
|
||||||
Migration_25_26(),
|
Migration_25_26(),
|
||||||
|
Migration_26_27(),
|
||||||
).build()
|
).build()
|
||||||
if (_instance == null) _instance = instance
|
if (_instance == null) _instance = instance
|
||||||
return 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.CustomAttributesRepository
|
||||||
import de.mm20.launcher2.data.customattrs.CustomIcon
|
import de.mm20.launcher2.data.customattrs.CustomIcon
|
||||||
import de.mm20.launcher2.data.customattrs.CustomIconPackIcon
|
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.LegacyCustomIconPackIcon
|
||||||
import de.mm20.launcher2.data.customattrs.CustomThemedIcon
|
import de.mm20.launcher2.data.customattrs.CustomThemedIcon
|
||||||
import de.mm20.launcher2.data.customattrs.DefaultPlaceholderIcon
|
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.CalendarIconProvider
|
||||||
import de.mm20.launcher2.icons.providers.CompatIconProvider
|
import de.mm20.launcher2.icons.providers.CompatIconProvider
|
||||||
import de.mm20.launcher2.icons.providers.CustomIconPackIconProvider
|
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.LegacyCustomIconPackIconProvider
|
||||||
import de.mm20.launcher2.icons.providers.CustomThemedIconProvider
|
import de.mm20.launcher2.icons.providers.CustomThemedIconProvider
|
||||||
import de.mm20.launcher2.icons.providers.DynamicClockIconProvider
|
import de.mm20.launcher2.icons.providers.DynamicClockIconProvider
|
||||||
@ -203,6 +205,9 @@ class IconService(
|
|||||||
if (customIcon is DefaultPlaceholderIcon) {
|
if (customIcon is DefaultPlaceholderIcon) {
|
||||||
return iconProviders.value.lastOrNull()?.let { listOf(it) } ?: emptyList()
|
return iconProviders.value.lastOrNull()?.let { listOf(it) } ?: emptyList()
|
||||||
}
|
}
|
||||||
|
if (customIcon is CustomTextIcon) {
|
||||||
|
return listOf(CustomTextIconProvider(customIcon))
|
||||||
|
}
|
||||||
return emptyList()
|
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) {
|
if (newName != null && newName != tag) {
|
||||||
customAttributesRepository.renameTag(tag, newName).join()
|
customAttributesRepository.renameTag(tag, newName).join()
|
||||||
val pinnedTags = searchableRepository.get(
|
searchableRepository.replace(Tag(tag).key, Tag(newName))
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user