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

View File

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

View File

@@ -1,5 +1,50 @@
import SwiftUI 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 // MARK: - Groups list
struct GroupsView: View { struct GroupsView: View {
@@ -23,7 +68,7 @@ struct GroupsView: View {
GroupCombinedView(api: api, group: g) GroupCombinedView(api: api, group: g)
} label: { } label: {
HStack { HStack {
Text(g.icon ?? "👥") GroupIconView(icon: g.icon)
Text(g.name) Text(g.name)
Spacer() Spacer()
if let n = g.memberCount { if let n = g.memberCount {
@@ -71,12 +116,12 @@ struct GroupEditSheet: View {
@AppStorage("appLanguage") private var appLang = "system" @AppStorage("appLanguage") private var appLang = "system"
@State private var name = "" @State private var name = ""
@State private var icon = "👥" @State private var icon = "people"
@State private var directory: [DirectoryUser] = [] @State private var directory: [DirectoryUser] = []
@State private var selected: Set<Int> = [] @State private var selected: Set<Int> = []
@State private var error = "" @State private var error = ""
private let icons = ["👥", "👨‍👩‍👧", "🏠", "❤️", "🧑‍🤝‍🧑", "", "🎓", "💼", "🎉", "🐶", "✈️", "🎵", "🍕", "📚", "🌳", ""] private let icons = GroupIcons.keys
private let cols = [GridItem(.adaptive(minimum: 46))] private let cols = [GridItem(.adaptive(minimum: 46))]
var body: some View { var body: some View {
@@ -88,9 +133,11 @@ struct GroupEditSheet: View {
Section(L10n.t("group.icon", appLang)) { Section(L10n.t("group.icon", appLang)) {
LazyVGrid(columns: cols, spacing: 8) { LazyVGrid(columns: cols, spacing: 8) {
ForEach(icons, id: \.self) { ic in ForEach(icons, id: \.self) { ic in
Text(ic).font(.title2) Image(systemName: GroupIcons.symbol(ic))
.font(.title3)
.frame(width: 44, height: 44) .frame(width: 44, height: 44)
.background(ic == icon ? Color.accentColor.opacity(0.25) : Color(.systemGray6)) .background(ic == icon ? Color.accentColor.opacity(0.25) : Color(.systemGray6))
.foregroundStyle(ic == icon ? Color.accentColor : .primary)
.clipShape(RoundedRectangle(cornerRadius: 8)) .clipShape(RoundedRectangle(cornerRadius: 8))
.onTapGesture { icon = ic } .onTapGesture { icon = ic }
} }
@@ -144,13 +191,13 @@ struct GroupManageSheet: View {
@State private var group: CalGroup? @State private var group: CalGroup?
@State private var name = "" @State private var name = ""
@State private var icon = "👥" @State private var icon = "people"
@State private var directory: [DirectoryUser] = [] @State private var directory: [DirectoryUser] = []
@State private var memberIds: Set<Int> = [] @State private var memberIds: Set<Int> = []
@State private var showDeleteConfirm = false @State private var showDeleteConfirm = false
@State private var error = "" @State private var error = ""
private let icons = ["👥", "👨‍👩‍👧", "🏠", "❤️", "🧑‍🤝‍🧑", "", "🎓", "💼", "🎉", "🐶", "✈️", "🎵", "🍕", "📚", "🌳", ""] private let icons = GroupIcons.keys
private let cols = [GridItem(.adaptive(minimum: 46))] private let cols = [GridItem(.adaptive(minimum: 46))]
var body: some View { var body: some View {
@@ -162,9 +209,11 @@ struct GroupManageSheet: View {
Section(L10n.t("group.icon", appLang)) { Section(L10n.t("group.icon", appLang)) {
LazyVGrid(columns: cols, spacing: 8) { LazyVGrid(columns: cols, spacing: 8) {
ForEach(icons, id: \.self) { ic in ForEach(icons, id: \.self) { ic in
Text(ic).font(.title2) Image(systemName: GroupIcons.symbol(ic))
.font(.title3)
.frame(width: 44, height: 44) .frame(width: 44, height: 44)
.background(ic == icon ? Color.accentColor.opacity(0.25) : Color(.systemGray6)) .background(ic == icon ? Color.accentColor.opacity(0.25) : Color(.systemGray6))
.foregroundStyle(ic == icon ? Color.accentColor : .primary)
.clipShape(RoundedRectangle(cornerRadius: 8)) .clipShape(RoundedRectangle(cornerRadius: 8))
.onTapGesture { icon = ic } .onTapGesture { icon = ic }
} }
@@ -218,7 +267,7 @@ struct GroupManageSheet: View {
if let g = try? await api.getGroup(id: groupId) { if let g = try? await api.getGroup(id: groupId) {
group = g group = g
name = g.name name = g.name
icon = g.icon ?? "👥" icon = GroupIcons.isKey(g.icon) ? g.icon! : "people"
let me = UserDefaults.standard.integer(forKey: "userId") let me = UserDefaults.standard.integer(forKey: "userId")
memberIds = Set((g.members ?? []).map { $0.id }.filter { $0 != me }) 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) Text(L10n.t("groups.combined_empty", appLang)).foregroundStyle(.secondary)
} }
} }
.navigationTitle("\(group.icon ?? "👥") \(group.name)") .navigationTitle(group.name)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItem(placement: .topBarTrailing) { ToolbarItem(placement: .topBarTrailing) {
@@ -340,13 +389,11 @@ struct GroupCombinedView: View {
.task(id: anchor) { await load() } .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 { private func displayTitle(_ ev: CalEvent) -> String {
if let dt = ev.displayTitle, !dt.isEmpty { return dt }
let me = UserDefaults.standard.integer(forKey: "userId") let me = UserDefaults.standard.integer(forKey: "userId")
if ev.isGroupEvent { if let c = ev.creator, ev.isGroupEvent, c.id != me { return "\(firstName(c.displayName)): \(ev.title)" }
if let c = ev.creator, c.id != me { return "👥 \(firstName(c.displayName)): \(ev.title)" }
return "👥 \(ev.title)"
}
if let o = ev.owner, o.id != me { return "\(firstName(o.displayName)): \(ev.title)" } if let o = ev.owner, o.id != me { return "\(firstName(o.displayName)): \(ev.title)" }
return ev.title return ev.title
} }