feat: non-emoji group icons (Material icons) for consistent look (Android)
Group icons are semantic keys rendered as native Material icons (GroupIcons) in the picker, group list, top-bar switcher and banner — mirroring iOS/web — instead of OS emoji. Legacy emoji values render as a fallback. decorateGroup fallback no longer prepends a glyph (server display_title is authoritative). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -64,6 +64,7 @@ import com.scarriffle.calendarr.ui.accounts.AccountsScreen
|
||||
import com.scarriffle.calendarr.domain.model.Group
|
||||
import com.scarriffle.calendarr.ui.event.EventDetailScreen
|
||||
import com.scarriffle.calendarr.ui.event.EventEditorSheet
|
||||
import com.scarriffle.calendarr.ui.groups.GroupIcon
|
||||
import com.scarriffle.calendarr.ui.groups.GroupsScreen
|
||||
import com.scarriffle.calendarr.ui.menu.MenuSheet
|
||||
import com.scarriffle.calendarr.ui.profile.ProfileScreen
|
||||
@@ -420,7 +421,8 @@ private fun GroupSwitcher(groups: List<Group>, activeGroup: Group?, onSwitchGrou
|
||||
)
|
||||
groups.forEach { g ->
|
||||
DropdownMenuItem(
|
||||
text = { Text("${g.icon ?: "👥"} ${g.name}") },
|
||||
text = { Text(g.name) },
|
||||
leadingIcon = { GroupIcon(g.icon) },
|
||||
trailingIcon = { if (activeGroup?.id == g.id) Icon(Icons.Filled.Check, contentDescription = null) },
|
||||
onClick = { open = false; onSwitchGroup(g) },
|
||||
)
|
||||
@@ -436,8 +438,9 @@ private fun GroupBanner(group: Group, onExit: () -> Unit) {
|
||||
Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
GroupIcon(group.icon, modifier = Modifier.padding(end = 6.dp))
|
||||
Text(
|
||||
"${tr("groups.view")}: ${group.icon ?: "👥"} ${group.name}",
|
||||
"${tr("groups.view")}: ${group.name}",
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
|
||||
@@ -228,8 +228,7 @@ class CalendarViewModel @Inject constructor(
|
||||
val serverTitle = ev.displayTitle?.takeIf { it.isNotEmpty() }
|
||||
if (serverTitle != null) return@map ev.copy(title = serverTitle)
|
||||
val prefix = when {
|
||||
ev.isGroupEvent && ev.creator != null && ev.creator.id != me -> "👥 ${firstName(ev.creator.displayName)}: "
|
||||
ev.isGroupEvent -> "👥 "
|
||||
ev.isGroupEvent && ev.creator != null && ev.creator.id != me -> "${firstName(ev.creator.displayName)}: "
|
||||
ev.owner != null && ev.owner.id != me -> "${firstName(ev.owner.displayName)}: "
|
||||
else -> ""
|
||||
}
|
||||
|
||||
@@ -21,10 +21,22 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Celebration
|
||||
import androidx.compose.material.icons.filled.ChevronRight
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.DirectionsRun
|
||||
import androidx.compose.material.icons.filled.Favorite
|
||||
import androidx.compose.material.icons.filled.Flight
|
||||
import androidx.compose.material.icons.filled.Home
|
||||
import androidx.compose.material.icons.filled.MusicNote
|
||||
import androidx.compose.material.icons.filled.People
|
||||
import androidx.compose.material.icons.filled.Pets
|
||||
import androidx.compose.material.icons.filled.Restaurant
|
||||
import androidx.compose.material.icons.filled.School
|
||||
import androidx.compose.material.icons.filled.Star
|
||||
import androidx.compose.material.icons.filled.Tune
|
||||
import androidx.compose.material.icons.filled.Work
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Checkbox
|
||||
@@ -51,6 +63,7 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
@@ -59,9 +72,49 @@ import com.scarriffle.calendarr.ui.components.ColorPickerDialog
|
||||
import com.scarriffle.calendarr.ui.tr
|
||||
import com.scarriffle.calendarr.util.colorFromHex
|
||||
|
||||
private val GROUP_ICONS = listOf(
|
||||
"👥", "👨👩👧", "🏠", "❤️", "🧑🤝🧑", "⚽", "🎓", "💼", "🎉", "🐶", "✈️", "🎵", "🍕", "📚", "🌳", "⭐",
|
||||
)
|
||||
/**
|
||||
* Cross-platform group-icon keys (stored server-side) rendered as native
|
||||
* Material icons — consistent everywhere instead of OS-emoji that vary by
|
||||
* platform. Mirrors iOS GroupIcons / the web SVG set.
|
||||
*/
|
||||
object GroupIcons {
|
||||
val keys = listOf(
|
||||
"people", "home", "heart", "work", "school", "sports",
|
||||
"party", "pet", "travel", "music", "food", "star",
|
||||
)
|
||||
|
||||
fun vector(key: String?): ImageVector = when (key) {
|
||||
"people" -> Icons.Filled.People
|
||||
"home" -> Icons.Filled.Home
|
||||
"heart" -> Icons.Filled.Favorite
|
||||
"work" -> Icons.Filled.Work
|
||||
"school" -> Icons.Filled.School
|
||||
"sports" -> Icons.Filled.DirectionsRun
|
||||
"party" -> Icons.Filled.Celebration
|
||||
"pet" -> Icons.Filled.Pets
|
||||
"travel" -> Icons.Filled.Flight
|
||||
"music" -> Icons.Filled.MusicNote
|
||||
"food" -> Icons.Filled.Restaurant
|
||||
"star" -> Icons.Filled.Star
|
||||
else -> Icons.Filled.People
|
||||
}
|
||||
|
||||
fun isKey(s: String?): Boolean = s != null && s in keys
|
||||
}
|
||||
|
||||
/** Render a group's icon: native Material icon for keys, legacy emoji fallback. */
|
||||
@Composable
|
||||
fun GroupIcon(icon: String?, modifier: Modifier = Modifier, tint: androidx.compose.ui.graphics.Color? = null) {
|
||||
if (GroupIcons.isKey(icon)) {
|
||||
Icon(GroupIcons.vector(icon), contentDescription = null, modifier = modifier,
|
||||
tint = tint ?: androidx.compose.material3.LocalContentColor.current)
|
||||
} else if (!icon.isNullOrEmpty()) {
|
||||
Text(icon, modifier = modifier)
|
||||
} else {
|
||||
Icon(Icons.Filled.People, contentDescription = null, modifier = modifier,
|
||||
tint = tint ?: androidx.compose.material3.LocalContentColor.current)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -103,7 +156,7 @@ fun GroupsScreen(
|
||||
Modifier.fillMaxWidth().clickable { onOpenGroupView(g) }.padding(vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(g.icon ?: "👥", style = MaterialTheme.typography.titleMedium)
|
||||
GroupIcon(g.icon, tint = MaterialTheme.colorScheme.onSurface)
|
||||
Column(Modifier.weight(1f).padding(start = 12.dp)) {
|
||||
Text(g.name, style = MaterialTheme.typography.bodyLarge)
|
||||
Text(
|
||||
@@ -155,7 +208,7 @@ private fun GroupEditSheet(
|
||||
) {
|
||||
val me = vm.currentUserId
|
||||
var name by remember { mutableStateOf(existing?.name ?: "") }
|
||||
var icon by remember { mutableStateOf(existing?.icon ?: "👥") }
|
||||
var icon by remember { mutableStateOf(if (GroupIcons.isKey(existing?.icon)) existing!!.icon!! else "people") }
|
||||
var selected by remember { mutableStateOf(setOf<Int>()) }
|
||||
var existingMembers by remember { mutableStateOf(setOf<Int>()) }
|
||||
var detail by remember { mutableStateOf<Group?>(null) }
|
||||
@@ -170,7 +223,7 @@ private fun GroupEditSheet(
|
||||
detail = g
|
||||
if (g != null) {
|
||||
name = g.name
|
||||
icon = g.icon ?: "👥"
|
||||
icon = if (GroupIcons.isKey(g.icon)) g.icon!! else "people"
|
||||
val members = g.members.map { it.id }.filter { it != me }.toSet()
|
||||
existingMembers = members
|
||||
selected = members
|
||||
@@ -199,7 +252,7 @@ private fun GroupEditSheet(
|
||||
Text(tr("groups.icon"), style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold)
|
||||
Spacer(Modifier.size(8.dp))
|
||||
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
GROUP_ICONS.forEach { ic ->
|
||||
GroupIcons.keys.forEach { ic ->
|
||||
val sel = ic == icon
|
||||
Box(
|
||||
Modifier.size(44.dp).clip(RoundedCornerShape(8.dp))
|
||||
@@ -207,7 +260,11 @@ private fun GroupEditSheet(
|
||||
.clickable { icon = ic },
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Text(ic, style = MaterialTheme.typography.titleLarge)
|
||||
Icon(
|
||||
GroupIcons.vector(ic),
|
||||
contentDescription = ic,
|
||||
tint = if (sel) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user