Birthdays: explicit calendar creation, sync never auto-creates (iOS)
- Contacts sync fills only an EXISTING birthday calendar (birthdayCalendar), never auto-creates it — deleting it no longer resurrects it on sync - Accounts + menu gains "Geburtstagskalender" (only when none exists) to create the single birthday calendar explicitly - the Birthdays-from-Contacts section shows a "create one first" hint when there is no birthday calendar; enabling sync only requests Contacts access Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -292,6 +292,7 @@ private let strings: [String: [String: String]] = [
|
|||||||
"accounts.loading": "Lade Konten…",
|
"accounts.loading": "Lade Konten…",
|
||||||
"accounts.add.caldav": "CalDAV-Konto",
|
"accounts.add.caldav": "CalDAV-Konto",
|
||||||
"accounts.add.local": "Lokaler Kalender",
|
"accounts.add.local": "Lokaler Kalender",
|
||||||
|
"accounts.add.birthday": "Geburtstagskalender",
|
||||||
"accounts.add.ical": "iCal-URL abonnieren",
|
"accounts.add.ical": "iCal-URL abonnieren",
|
||||||
"accounts.add.ha": "Home Assistant",
|
"accounts.add.ha": "Home Assistant",
|
||||||
"accounts.caldav.header": "CalDAV-Konten",
|
"accounts.caldav.header": "CalDAV-Konten",
|
||||||
@@ -650,6 +651,7 @@ private let strings: [String: [String: String]] = [
|
|||||||
"accounts.loading": "Loading accounts…",
|
"accounts.loading": "Loading accounts…",
|
||||||
"accounts.add.caldav": "CalDAV account",
|
"accounts.add.caldav": "CalDAV account",
|
||||||
"accounts.add.local": "Local calendar",
|
"accounts.add.local": "Local calendar",
|
||||||
|
"accounts.add.birthday": "Birthday calendar",
|
||||||
"accounts.add.ical": "Subscribe to iCal URL",
|
"accounts.add.ical": "Subscribe to iCal URL",
|
||||||
"accounts.add.ha": "Home Assistant",
|
"accounts.add.ha": "Home Assistant",
|
||||||
"accounts.caldav.header": "CalDAV accounts",
|
"accounts.caldav.header": "CalDAV accounts",
|
||||||
|
|||||||
@@ -141,7 +141,10 @@ enum BirthdaysImporter {
|
|||||||
guard isEnabled else { return }
|
guard isEnabled else { return }
|
||||||
guard await requestAccess() else { return }
|
guard await requestAccess() else { return }
|
||||||
guard let contacts = try? readContactBirthdays() else { return }
|
guard let contacts = try? readContactBirthdays() else { return }
|
||||||
guard let cal = await ensureBirthdayCalendar(api: api) else { return }
|
// Only fill an EXISTING birthday calendar — never auto-create it. The
|
||||||
|
// calendar is created explicitly; deleting it means sync has nowhere to
|
||||||
|
// go (and must not resurrect it).
|
||||||
|
guard let cal = await birthdayCalendar(api: api) else { return }
|
||||||
guard let existing = try? await api.getBirthdayEntries(calendarId: cal.id) else { return }
|
guard let existing = try? await api.getBirthdayEntries(calendarId: cal.id) else { return }
|
||||||
|
|
||||||
// Only reconcile THIS device's contact rows; leave other devices'
|
// Only reconcile THIS device's contact rows; leave other devices'
|
||||||
|
|||||||
@@ -47,13 +47,9 @@ struct AccountsView: View {
|
|||||||
haSection
|
haSection
|
||||||
}
|
}
|
||||||
.onChange(of: birthdaysSyncEnabled) { _, on in
|
.onChange(of: birthdaysSyncEnabled) { _, on in
|
||||||
if on {
|
// Enabling only asks for Contacts access — it must NOT create
|
||||||
Task {
|
// the birthday calendar. That's an explicit action.
|
||||||
_ = await BirthdaysImporter.requestAccess()
|
if on { Task { _ = await BirthdaysImporter.requestAccess() } }
|
||||||
_ = await BirthdaysImporter.ensureBirthdayCalendar(api: api)
|
|
||||||
await load()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,6 +60,12 @@ struct AccountsView: View {
|
|||||||
Menu {
|
Menu {
|
||||||
Button(L10n.t("accounts.add.caldav", appLang)) { showAddCalDAV = true }
|
Button(L10n.t("accounts.add.caldav", appLang)) { showAddCalDAV = true }
|
||||||
Button(L10n.t("accounts.add.local", appLang)) { showAddLocal = true }
|
Button(L10n.t("accounts.add.local", appLang)) { showAddLocal = true }
|
||||||
|
// Only one birthday calendar per account.
|
||||||
|
if birthdayCalendar == nil {
|
||||||
|
Button(L10n.t("accounts.add.birthday", appLang)) {
|
||||||
|
Task { await createBirthdayCalendar() }
|
||||||
|
}
|
||||||
|
}
|
||||||
Button(L10n.t("accounts.add.ical", appLang)) { showAddICal = true }
|
Button(L10n.t("accounts.add.ical", appLang)) { showAddICal = true }
|
||||||
Button(L10n.t("accounts.add.ha", appLang)) { showAddHA = true }
|
Button(L10n.t("accounts.add.ha", appLang)) { showAddHA = true }
|
||||||
} label: {
|
} label: {
|
||||||
@@ -235,14 +237,13 @@ struct AccountsView: View {
|
|||||||
|
|
||||||
@ViewBuilder var birthdayContactsSection: some View {
|
@ViewBuilder var birthdayContactsSection: some View {
|
||||||
Section {
|
Section {
|
||||||
|
if let cal = birthdayCalendar {
|
||||||
Toggle(L10n.t("birthday.contacts.sync", appLang), isOn: $birthdaysSyncEnabled)
|
Toggle(L10n.t("birthday.contacts.sync", appLang), isOn: $birthdaysSyncEnabled)
|
||||||
if birthdaysSyncEnabled {
|
if birthdaysSyncEnabled {
|
||||||
if let cal = birthdayCalendar {
|
|
||||||
BirthdayNotifyPicker(days: $birthdayNotify, appLang: appLang)
|
BirthdayNotifyPicker(days: $birthdayNotify, appLang: appLang)
|
||||||
.onChange(of: birthdayNotify) { _, v in
|
.onChange(of: birthdayNotify) { _, v in
|
||||||
Task { try? await api.updateLocalCalendarBirthday(id: cal.id, notifyDaysBefore: v) }
|
Task { try? await api.updateLocalCalendarBirthday(id: cal.id, notifyDaysBefore: v) }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
Button {
|
Button {
|
||||||
Task { await syncBirthdays() }
|
Task { await syncBirthdays() }
|
||||||
} label: {
|
} label: {
|
||||||
@@ -253,6 +254,12 @@ struct AccountsView: View {
|
|||||||
}
|
}
|
||||||
.disabled(isSyncingBirthdays)
|
.disabled(isSyncingBirthdays)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// No birthday calendar yet — sync has nothing to fill. Create one
|
||||||
|
// first via the + menu (never auto-created by sync).
|
||||||
|
Text(L10n.t("birthday.contacts.need_calendar", appLang))
|
||||||
|
.font(.caption).foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
} header: {
|
} header: {
|
||||||
Text(L10n.t("birthday.contacts.header", appLang))
|
Text(L10n.t("birthday.contacts.header", appLang))
|
||||||
} footer: {
|
} footer: {
|
||||||
@@ -260,6 +267,12 @@ struct AccountsView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Explicitly create the single birthday calendar (from the + menu).
|
||||||
|
private func createBirthdayCalendar() async {
|
||||||
|
_ = await BirthdaysImporter.ensureBirthdayCalendar(api: api)
|
||||||
|
await load()
|
||||||
|
}
|
||||||
|
|
||||||
private func syncBirthdays() async {
|
private func syncBirthdays() async {
|
||||||
isSyncingBirthdays = true
|
isSyncingBirthdays = true
|
||||||
defer { isSyncingBirthdays = false }
|
defer { isSyncingBirthdays = false }
|
||||||
|
|||||||
Reference in New Issue
Block a user