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:
Guido Schmit
2026-06-01 20:24:48 +02:00
parent 644b532104
commit 807db6a57b
3 changed files with 71 additions and 12 deletions

View File

@@ -64,6 +64,7 @@ import com.scarriffle.calendarr.ui.accounts.AccountsScreen
import com.scarriffle.calendarr.domain.model.Group import com.scarriffle.calendarr.domain.model.Group
import com.scarriffle.calendarr.ui.event.EventDetailScreen import com.scarriffle.calendarr.ui.event.EventDetailScreen
import com.scarriffle.calendarr.ui.event.EventEditorSheet 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.groups.GroupsScreen
import com.scarriffle.calendarr.ui.menu.MenuSheet import com.scarriffle.calendarr.ui.menu.MenuSheet
import com.scarriffle.calendarr.ui.profile.ProfileScreen import com.scarriffle.calendarr.ui.profile.ProfileScreen
@@ -420,7 +421,8 @@ private fun GroupSwitcher(groups: List<Group>, activeGroup: Group?, onSwitchGrou
) )
groups.forEach { g -> groups.forEach { g ->
DropdownMenuItem( 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) }, trailingIcon = { if (activeGroup?.id == g.id) Icon(Icons.Filled.Check, contentDescription = null) },
onClick = { open = false; onSwitchGroup(g) }, 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), Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
GroupIcon(group.icon, modifier = Modifier.padding(end = 6.dp))
Text( Text(
"${tr("groups.view")}: ${group.icon ?: "👥"} ${group.name}", "${tr("groups.view")}: ${group.name}",
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,

View File

@@ -228,8 +228,7 @@ class CalendarViewModel @Inject constructor(
val serverTitle = ev.displayTitle?.takeIf { it.isNotEmpty() } val serverTitle = ev.displayTitle?.takeIf { it.isNotEmpty() }
if (serverTitle != null) return@map ev.copy(title = serverTitle) if (serverTitle != null) return@map ev.copy(title = serverTitle)
val prefix = when { val prefix = when {
ev.isGroupEvent && ev.creator != null && ev.creator.id != me -> "👥 ${firstName(ev.creator.displayName)}: " ev.isGroupEvent && ev.creator != null && ev.creator.id != me -> "${firstName(ev.creator.displayName)}: "
ev.isGroupEvent -> "👥 "
ev.owner != null && ev.owner.id != me -> "${firstName(ev.owner.displayName)}: " ev.owner != null && ev.owner.id != me -> "${firstName(ev.owner.displayName)}: "
else -> "" else -> ""
} }

View File

@@ -21,10 +21,22 @@ import androidx.compose.foundation.shape.RoundedCornerShape
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.filled.Add 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.ChevronRight
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Delete 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.Tune
import androidx.compose.material.icons.filled.Work
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
@@ -51,6 +63,7 @@ import androidx.compose.runtime.setValue
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.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel 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.ui.tr
import com.scarriffle.calendarr.util.colorFromHex 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
@@ -103,7 +156,7 @@ fun GroupsScreen(
Modifier.fillMaxWidth().clickable { onOpenGroupView(g) }.padding(vertical = 10.dp), Modifier.fillMaxWidth().clickable { onOpenGroupView(g) }.padding(vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically, 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)) { Column(Modifier.weight(1f).padding(start = 12.dp)) {
Text(g.name, style = MaterialTheme.typography.bodyLarge) Text(g.name, style = MaterialTheme.typography.bodyLarge)
Text( Text(
@@ -155,7 +208,7 @@ private fun GroupEditSheet(
) { ) {
val me = vm.currentUserId val me = vm.currentUserId
var name by remember { mutableStateOf(existing?.name ?: "") } 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 selected by remember { mutableStateOf(setOf<Int>()) }
var existingMembers by remember { mutableStateOf(setOf<Int>()) } var existingMembers by remember { mutableStateOf(setOf<Int>()) }
var detail by remember { mutableStateOf<Group?>(null) } var detail by remember { mutableStateOf<Group?>(null) }
@@ -170,7 +223,7 @@ private fun GroupEditSheet(
detail = g detail = g
if (g != null) { if (g != null) {
name = g.name 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() val members = g.members.map { it.id }.filter { it != me }.toSet()
existingMembers = members existingMembers = members
selected = members selected = members
@@ -199,7 +252,7 @@ private fun GroupEditSheet(
Text(tr("groups.icon"), style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold) Text(tr("groups.icon"), style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold)
Spacer(Modifier.size(8.dp)) Spacer(Modifier.size(8.dp))
FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(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 val sel = ic == icon
Box( Box(
Modifier.size(44.dp).clip(RoundedCornerShape(8.dp)) Modifier.size(44.dp).clip(RoundedCornerShape(8.dp))
@@ -207,7 +260,11 @@ private fun GroupEditSheet(
.clickable { icon = ic }, .clickable { icon = ic },
contentAlignment = Alignment.Center, 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,
)
} }
} }
} }