Add birthday feature (iOS)
- LocalCalendar/CalEvent gain is_birthday + birthday fields; API sends rrule/external_uid/birth_year and can create/update birthday calendars - BirthdaysImporter: mirrors Contacts birthdays into a chosen birthday calendar, reconciling by external_uid (leaves manual entries untouched) - AccountsView: "birthday calendar" toggle + notify-days when creating a calendar, and a "Birthdays from Contacts" section (enable + target + sync now) - FAB long-press menu -> "New birthday" opens a minimal name+date mask (BirthdayEditorSheet) targeting birthday calendars, with year-unknown option - Event bars render display_title (age) and a cake icon via EventLabel - NSContactsUsageDescription added to both app build configs Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -67,10 +67,17 @@ struct CalEvent: Identifiable, Hashable {
|
||||
var reminders: [Int] = []
|
||||
// True for events from a calendar shared with the user read-only.
|
||||
var readOnly: Bool = false
|
||||
// True for events from a birthday calendar — clients show a cake icon and the
|
||||
// server bakes the age into `displayTitle`.
|
||||
var isBirthday: Bool = false
|
||||
|
||||
// Group view supplies a server-resolved colour; otherwise per-event then calendar colour.
|
||||
var effectiveColor: String { displayColor ?? color ?? calendarColor }
|
||||
|
||||
// Title to render: the server-decorated one (birthday age, group prefix) wins
|
||||
// over the raw title, which is kept for editing.
|
||||
var renderTitle: String { displayTitle ?? title }
|
||||
|
||||
static func from(json: [String: Any]) -> CalEvent? {
|
||||
guard
|
||||
let title = json["title"] as? String,
|
||||
@@ -110,7 +117,8 @@ struct CalEvent: Identifiable, Hashable {
|
||||
displayColor: (json["display_color"] as? String).flatMap { $0.isEmpty ? nil : $0 },
|
||||
displayTitle: (json["display_title"] as? String).flatMap { $0.isEmpty ? nil : $0 },
|
||||
reminders: (json["reminders"] as? [Int]) ?? (json["reminders"] as? [Any])?.compactMap { ($0 as? Int) ?? Int("\($0)") } ?? [],
|
||||
readOnly: json["read_only"] as? Bool ?? false
|
||||
readOnly: json["read_only"] as? Bool ?? false,
|
||||
isBirthday: json["is_birthday"] as? Bool ?? false
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -174,3 +182,21 @@ func formatISO(_ date: Date, allDay: Bool) -> String {
|
||||
}
|
||||
return isoBasic.string(from: date)
|
||||
}
|
||||
|
||||
/// Inline label for an event in a calendar grid: a leading birthday (cake) icon
|
||||
/// when the event is a birthday, followed by the render title (which carries the
|
||||
/// server-computed age). Icon and text inherit the surrounding font/colour, so
|
||||
/// the label matches whatever bar it's dropped into.
|
||||
struct EventLabel: View {
|
||||
let event: CalEvent
|
||||
var body: some View {
|
||||
if event.isBirthday {
|
||||
HStack(spacing: 3) {
|
||||
Image(systemName: "birthday.cake.fill").imageScale(.small)
|
||||
Text(event.renderTitle)
|
||||
}
|
||||
} else {
|
||||
Text(event.renderTitle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user