feat: non-emoji group icons (SF Symbols) for consistent cross-platform look

Group icons are now semantic keys (people/home/heart/work/school/sports/party/
pet/travel/music/food/star) rendered as SF Symbols in the picker, group list,
switcher, banner and filter — instead of OS emoji that looked different on every
platform. Legacy emoji values still render as a fallback. GroupCombinedView uses
the server display_title.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-06-01 19:20:32 +02:00
parent 451d3d4d6b
commit 68349d36e5
3 changed files with 67 additions and 19 deletions

View File

@@ -229,8 +229,8 @@ struct CalendarHostView: View {
}
ForEach(groups) { g in
Button { switchGroup(g) } label: {
Label("\(g.icon ?? "👥") \(g.name)",
systemImage: store.activeGroup?.id == g.id ? "checkmark" : "person.2")
Label(g.name,
systemImage: store.activeGroup?.id == g.id ? "checkmark" : GroupIcons.symbol(g.icon))
}
}
} label: {
@@ -244,8 +244,9 @@ struct CalendarHostView: View {
@ViewBuilder private var groupBanner: some View {
if let g = store.activeGroup {
HStack(spacing: 8) {
Text("\(L10n.t("groups.view_label", appLang)): \(g.icon ?? "👥") \(g.name)")
HStack(spacing: 6) {
GroupIconView(icon: g.icon).font(.subheadline)
Text("\(L10n.t("groups.view_label", appLang)): \(g.name)")
.font(.subheadline).lineLimit(1)
Spacer()
Button(L10n.t("groups.exit", appLang)) { switchGroup(nil) }

View File

@@ -179,7 +179,7 @@ struct CalendarFilterSheet: View {
private var groupFilterList: some View {
if let g = groupDetail {
List {
Section(header: Text("\(g.icon ?? "👥") \(g.name)")) {
Section(header: Label(g.name, systemImage: GroupIcons.symbol(g.icon))) {
ForEach(g.members ?? []) { m in
groupRow(name: m.displayName ?? "",
colorHex: m.color ?? "#4285f4",

View File

@@ -1,5 +1,50 @@
import SwiftUI
// MARK: - Group icons (cross-platform, non-emoji)
/// Canonical group-icon keys stored server-side and rendered as native SF
/// Symbols here (Material on Android, SVG on web), so groups look consistent
/// instead of relying on OS-specific emoji rendering.
enum GroupIcons {
static let keys = ["people", "home", "heart", "work", "school", "sports",
"party", "pet", "travel", "music", "food", "star"]
static func symbol(_ key: String?) -> String {
switch key {
case "people": return "person.2.fill"
case "home": return "house.fill"
case "heart": return "heart.fill"
case "work": return "briefcase.fill"
case "school": return "graduationcap.fill"
case "sports": return "figure.run"
case "party": return "party.popper.fill"
case "pet": return "pawprint.fill"
case "travel": return "airplane"
case "music": return "music.note"
case "food": return "fork.knife"
case "star": return "star.fill"
default: return "person.2.fill"
}
}
static func isKey(_ s: String?) -> Bool { if let s { return keys.contains(s) }; return false }
}
/// Render a group's icon: native SF Symbol for known keys, the legacy emoji for
/// pre-migration groups, else a default people glyph.
struct GroupIconView: View {
let icon: String?
var body: some View {
if GroupIcons.isKey(icon) {
Image(systemName: GroupIcons.symbol(icon))
} else if let e = icon, !e.isEmpty {
Text(e)
} else {
Image(systemName: "person.2.fill")
}
}
}
// MARK: - Groups list
struct GroupsView: View {
@@ -23,7 +68,7 @@ struct GroupsView: View {
GroupCombinedView(api: api, group: g)
} label: {
HStack {
Text(g.icon ?? "👥")
GroupIconView(icon: g.icon)
Text(g.name)
Spacer()
if let n = g.memberCount {
@@ -71,12 +116,12 @@ struct GroupEditSheet: View {
@AppStorage("appLanguage") private var appLang = "system"
@State private var name = ""
@State private var icon = "👥"
@State private var icon = "people"
@State private var directory: [DirectoryUser] = []
@State private var selected: Set<Int> = []
@State private var error = ""
private let icons = ["👥", "👨‍👩‍👧", "🏠", "❤️", "🧑‍🤝‍🧑", "", "🎓", "💼", "🎉", "🐶", "✈️", "🎵", "🍕", "📚", "🌳", ""]
private let icons = GroupIcons.keys
private let cols = [GridItem(.adaptive(minimum: 46))]
var body: some View {
@@ -88,9 +133,11 @@ struct GroupEditSheet: View {
Section(L10n.t("group.icon", appLang)) {
LazyVGrid(columns: cols, spacing: 8) {
ForEach(icons, id: \.self) { ic in
Text(ic).font(.title2)
Image(systemName: GroupIcons.symbol(ic))
.font(.title3)
.frame(width: 44, height: 44)
.background(ic == icon ? Color.accentColor.opacity(0.25) : Color(.systemGray6))
.foregroundStyle(ic == icon ? Color.accentColor : .primary)
.clipShape(RoundedRectangle(cornerRadius: 8))
.onTapGesture { icon = ic }
}
@@ -144,13 +191,13 @@ struct GroupManageSheet: View {
@State private var group: CalGroup?
@State private var name = ""
@State private var icon = "👥"
@State private var icon = "people"
@State private var directory: [DirectoryUser] = []
@State private var memberIds: Set<Int> = []
@State private var showDeleteConfirm = false
@State private var error = ""
private let icons = ["👥", "👨‍👩‍👧", "🏠", "❤️", "🧑‍🤝‍🧑", "", "🎓", "💼", "🎉", "🐶", "✈️", "🎵", "🍕", "📚", "🌳", ""]
private let icons = GroupIcons.keys
private let cols = [GridItem(.adaptive(minimum: 46))]
var body: some View {
@@ -162,9 +209,11 @@ struct GroupManageSheet: View {
Section(L10n.t("group.icon", appLang)) {
LazyVGrid(columns: cols, spacing: 8) {
ForEach(icons, id: \.self) { ic in
Text(ic).font(.title2)
Image(systemName: GroupIcons.symbol(ic))
.font(.title3)
.frame(width: 44, height: 44)
.background(ic == icon ? Color.accentColor.opacity(0.25) : Color(.systemGray6))
.foregroundStyle(ic == icon ? Color.accentColor : .primary)
.clipShape(RoundedRectangle(cornerRadius: 8))
.onTapGesture { icon = ic }
}
@@ -218,7 +267,7 @@ struct GroupManageSheet: View {
if let g = try? await api.getGroup(id: groupId) {
group = g
name = g.name
icon = g.icon ?? "👥"
icon = GroupIcons.isKey(g.icon) ? g.icon! : "people"
let me = UserDefaults.standard.integer(forKey: "userId")
memberIds = Set((g.members ?? []).map { $0.id }.filter { $0 != me })
}
@@ -324,7 +373,7 @@ struct GroupCombinedView: View {
Text(L10n.t("groups.combined_empty", appLang)).foregroundStyle(.secondary)
}
}
.navigationTitle("\(group.icon ?? "👥") \(group.name)")
.navigationTitle(group.name)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
@@ -340,13 +389,11 @@ struct GroupCombinedView: View {
.task(id: anchor) { await load() }
}
// Prefix others' events with their first name; group events with 👥 + creator.
// Prefer the server-decorated title; fall back to a name prefix.
private func displayTitle(_ ev: CalEvent) -> String {
if let dt = ev.displayTitle, !dt.isEmpty { return dt }
let me = UserDefaults.standard.integer(forKey: "userId")
if ev.isGroupEvent {
if let c = ev.creator, c.id != me { return "👥 \(firstName(c.displayName)): \(ev.title)" }
return "👥 \(ev.title)"
}
if let c = ev.creator, ev.isGroupEvent, c.id != me { return "\(firstName(c.displayName)): \(ev.title)" }
if let o = ev.owner, o.id != me { return "\(firstName(o.displayName)): \(ev.title)" }
return ev.title
}