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:
@@ -30,6 +30,7 @@ struct CalendarHostView: View {
|
||||
@State private var showFilter = false
|
||||
@State private var didApplyDefaultView = false
|
||||
@State private var groups: [CalGroup] = []
|
||||
@State private var showNewBirthday = false
|
||||
|
||||
private var titleString: String {
|
||||
if store.viewType == .month {
|
||||
@@ -380,6 +381,21 @@ struct CalendarHostView: View {
|
||||
|
||||
// MARK: – FAB buttons
|
||||
|
||||
/// Long-press menu on the create button: a plain new event, or a new birthday
|
||||
/// (which opens a minimal name + date mask that only targets birthday calendars).
|
||||
@ViewBuilder private var fabMenu: some View {
|
||||
Button {
|
||||
editorContext = .create(.now)
|
||||
} label: {
|
||||
Label(L10n.t("event.new_title", appLang), systemImage: "plus")
|
||||
}
|
||||
Button {
|
||||
showNewBirthday = true
|
||||
} label: {
|
||||
Label(L10n.t("birthday.new", appLang), systemImage: "birthday.cake.fill")
|
||||
}
|
||||
}
|
||||
|
||||
/// Standard solid FAB (flat mode)
|
||||
private var solidFAB: some View {
|
||||
Button {
|
||||
@@ -393,6 +409,7 @@ struct CalendarHostView: View {
|
||||
.clipShape(Circle())
|
||||
.shadow(radius: 4, y: 2)
|
||||
}
|
||||
.contextMenu { fabMenu }
|
||||
.padding(.trailing, 20).padding(.bottom, 20)
|
||||
}
|
||||
|
||||
@@ -410,6 +427,7 @@ struct CalendarHostView: View {
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.glassEffect(in: Circle())
|
||||
.contextMenu { fabMenu }
|
||||
.padding(.trailing, 20).padding(.bottom, 20)
|
||||
} else {
|
||||
solidFAB
|
||||
@@ -421,6 +439,7 @@ struct CalendarHostView: View {
|
||||
private var calendarSheets: CalendarSheets {
|
||||
CalendarSheets(store: store, editorContext: $editorContext,
|
||||
selectedEvent: $selectedEvent, showFilter: $showFilter,
|
||||
showNewBirthday: $showNewBirthday,
|
||||
api: api,
|
||||
reload: { await onNavigate() },
|
||||
reloadForce: { await reloadVisible(force: true) })
|
||||
@@ -449,6 +468,14 @@ struct CalendarHostView: View {
|
||||
Task(priority: .background) {
|
||||
await store.prefetchBackground(api: api, months: cacheMonths)
|
||||
}
|
||||
// 2b. Mirror Contacts birthdays into the bound birthday calendar, if the
|
||||
// user enabled it, then refresh so new birthdays appear immediately.
|
||||
if BirthdaysImporter.isEnabled {
|
||||
Task(priority: .background) {
|
||||
await BirthdaysImporter.sync(api: api)
|
||||
await forceReload()
|
||||
}
|
||||
}
|
||||
// 3. Periodic settings + visibility pull (tied to this .task's lifetime).
|
||||
while !Task.isCancelled {
|
||||
try? await Task.sleep(for: .seconds(600))
|
||||
@@ -538,12 +565,16 @@ private struct CalendarSheets: ViewModifier {
|
||||
@Binding var editorContext: CalEditorContext?
|
||||
@Binding var selectedEvent: CalEvent?
|
||||
@Binding var showFilter: Bool
|
||||
@Binding var showNewBirthday: Bool
|
||||
let api: CalendarrAPI
|
||||
let reload: () async -> Void
|
||||
let reloadForce: () async -> Void
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.sheet(isPresented: $showNewBirthday) {
|
||||
BirthdayEditorSheet(api: api) { await reloadForce() }
|
||||
}
|
||||
// Use sheet(item:) so the editing event is captured atomically –
|
||||
// avoiding the race where sheet(isPresented:) evaluates its content
|
||||
// before the editingEvent state update propagates.
|
||||
|
||||
Reference in New Issue
Block a user