From 807db6a57bc0b5e0fb0d93a7e637763489245f57 Mon Sep 17 00:00:00 2001 From: Guido Schmit Date: Mon, 1 Jun 2026 20:24:48 +0200 Subject: [PATCH] feat: non-emoji group icons (Material icons) for consistent look (Android) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../calendarr/ui/calendar/CalendarScreen.kt | 7 +- .../ui/calendar/CalendarViewModel.kt | 3 +- .../calendarr/ui/groups/GroupsScreen.kt | 73 +++++++++++++++++-- 3 files changed, 71 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarScreen.kt b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarScreen.kt index 4e930f1..b0af4a9 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarScreen.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarScreen.kt @@ -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, 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, diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt index bfc4daa..eb4b5fe 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/calendar/CalendarViewModel.kt @@ -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 -> "" } diff --git a/app/src/main/java/com/scarriffle/calendarr/ui/groups/GroupsScreen.kt b/app/src/main/java/com/scarriffle/calendarr/ui/groups/GroupsScreen.kt index 75b9c33..52f4503 100644 --- a/app/src/main/java/com/scarriffle/calendarr/ui/groups/GroupsScreen.kt +++ b/app/src/main/java/com/scarriffle/calendarr/ui/groups/GroupsScreen.kt @@ -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()) } var existingMembers by remember { mutableStateOf(setOf()) } var detail by remember { mutableStateOf(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, + ) } } }