iOS: localization fixes, per-calendar reminders, widget polish

C1 — Localization: route the remaining hardcoded German strings through
L10n (LoginView, ServerSetupView, SettingsView email, EventDetailSheet) so
"System Default" + English device language shows fully English text.

C2 — Per-calendar reminders: parse the new reminders_enabled flag on every
calendar type; CalendarStore persists a reminderDisabledKeys set and passes
it to NotificationScheduler, which skips events of muted calendars (default
and per-event reminders). Filter sheet gains a per-calendar reminder toggle
(leading swipe + bell.slash indicator), reconciled from the server and
synced back via PUT.

C3 — Widgets:
- Shared WidgetTime.range helper; Today / Today & Tomorrow / Three Days /
  Up Next now show start–end instead of only the start time.
- This Week: show up to 6 events per day (was 3) to use the height.
- Two Weeks: mini event-title pills instead of bare dots.
- Two Months: weeks expand to fill the column (no more empty lower third).
- Day & Events: smaller header/strip/rows so content stops clipping.
- Next 5 days → Next 7 days (range + labels), higher row cap.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-06-09 20:14:39 +02:00
parent 13d80981c6
commit c0edca338e
20 changed files with 256 additions and 65 deletions

View File

@@ -41,11 +41,11 @@ struct CalendarDayWidgetView: View {
VStack(alignment: .leading, spacing: 0) {
header(primary: primary)
weekStrip(snapshot: s, primary: primary, accent: accent)
.padding(.vertical, 5)
.padding(.vertical, 3)
Rectangle()
.fill(Color(widgetHex: s.lineColorHex).opacity(0.4))
.frame(height: 0.5)
.padding(.bottom, 6)
.padding(.bottom, 4)
eventList(accent: accent)
}
} else {
@@ -61,9 +61,9 @@ struct CalendarDayWidgetView: View {
private func header(primary: Color) -> some View {
HStack(alignment: .top, spacing: 6) {
Text("\(cal.component(.day, from: entry.date))")
.font(.system(size: 36, weight: .bold))
.font(.system(size: 30, weight: .bold))
.foregroundStyle(primary)
.frame(width: 44, alignment: .leading)
.frame(width: 40, alignment: .leading)
.minimumScaleFactor(0.7)
VStack(alignment: .leading, spacing: 1) {
Text(monthFmt.string(from: entry.date).uppercased())
@@ -125,12 +125,12 @@ struct CalendarDayWidgetView: View {
.foregroundStyle(.secondary)
Spacer(minLength: 0)
} else {
VStack(alignment: .leading, spacing: 5) {
VStack(alignment: .leading, spacing: 4) {
ForEach(upcomingEvents.prefix(3)) { ev in
HStack(alignment: .center, spacing: 6) {
RoundedRectangle(cornerRadius: 1.5)
.fill(Color(widgetHex: ev.colorHex))
.frame(width: 3, height: 26)
.frame(width: 3, height: 22)
VStack(alignment: .leading, spacing: 1) {
Text(ev.title)
.font(.system(size: 11, weight: .semibold))

View File

@@ -89,11 +89,11 @@ struct ThisWeekWidgetView: View {
.frame(width: 16, height: 16)
.background(isToday ? primary : Color.clear)
.clipShape(Circle())
ForEach(evs.prefix(3)) { ev in
ForEach(evs.prefix(6)) { ev in
eventPill(ev)
}
if evs.count > 3 {
Text("+\(evs.count - 3)")
if evs.count > 6 {
Text("+\(evs.count - 6)")
.font(.system(size: 6.5))
.foregroundStyle(accent)
}

View File

@@ -121,7 +121,7 @@ struct ThreeDaysWidgetView: View {
.lineLimit(1)
Spacer(minLength: 0)
}
Text(ev.isAllDay ? WidgetL10n.t("widget.allday", lang) : timeFmt.string(from: ev.start))
Text(WidgetTime.range(ev, lang: lang))
.font(.system(size: 8))
.foregroundStyle(.secondary)
.padding(.leading, 5)

View File

@@ -74,7 +74,7 @@ struct TodayWidgetView: View {
Text(ev.title)
.font(.caption.weight(.medium))
.lineLimit(1)
Text(ev.isAllDay ? WidgetL10n.t("widget.allday", lang) : timeFmt.string(from: ev.start))
Text(WidgetTime.range(ev, lang: lang))
.font(.caption2)
.foregroundStyle(.secondary)
}

View File

@@ -92,7 +92,7 @@ struct TwoDaysWidgetView: View {
Text(ev.title)
.font(.system(size: 11, weight: .medium))
.lineLimit(1)
Text(ev.isAllDay ? WidgetL10n.t("widget.allday", lang) : timeFmt.string(from: ev.start))
Text(WidgetTime.range(ev, lang: lang))
.font(.system(size: 9))
.foregroundStyle(.secondary)
}

View File

@@ -110,9 +110,11 @@ struct TwoMonthWidgetView: View {
primary: primary, accent: accent)
}
}
// Distribute weeks across the full column height instead of
// top-packing them (which left the lower portion empty).
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
Spacer(minLength: 0)
}
}

View File

@@ -111,19 +111,22 @@ struct TwoWeeksWidgetView: View {
.frame(width: 12, height: 12)
.background(isToday ? primary : Color.clear)
.clipShape(Circle())
// Up to 3 colored dots
HStack(spacing: 1) {
ForEach(evs.prefix(3).indices, id: \.self) { i in
Circle()
.fill(Color(widgetHex: evs[i].colorHex))
.frame(width: 3, height: 3)
}
// Up to 2 mini event-title pills (the cell has room for titles).
ForEach(evs.prefix(2)) { ev in
Text(ev.title)
.font(.system(size: 6, weight: .medium))
.lineLimit(1)
.foregroundStyle(.white)
.padding(.horizontal, 1.5)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(widgetHex: ev.colorHex))
.clipShape(RoundedRectangle(cornerRadius: 1.5))
}
.frame(height: 3)
if evs.count > 3 {
Text("+\(evs.count - 3)")
if evs.count > 2 {
Text("+\(evs.count - 2)")
.font(.system(size: 6))
.foregroundStyle(accent)
.frame(maxWidth: .infinity, alignment: .leading)
}
Spacer(minLength: 0)
}

View File

@@ -108,7 +108,7 @@ struct UpNextWidgetView: View {
Text(ev.title)
.font(.system(size: 10, weight: .semibold))
.lineLimit(1)
Text(ev.isAllDay ? WidgetL10n.t("widget.allday", lang) : timeFmt.string(from: ev.start))
Text(WidgetTime.range(ev, lang: lang))
.font(.system(size: 9))
.foregroundStyle(.secondary)
}

View File

@@ -4,7 +4,7 @@ import WidgetKit
private let rowHeight: CGFloat = 16
private let dayHeaderHeight: CGFloat = 14
private let maxEventsPerDay: Int = 3
private let maxTotalRows: Int = 15
private let maxTotalRows: Int = 22
struct UpcomingWidgetView: View {
let entry: CalendarrEntry
@@ -16,7 +16,7 @@ struct UpcomingWidgetView: View {
guard let s = snapshot else { return [] }
let cal = Calendar.current
let now = entry.date
let events = WidgetHelpers.upcoming(from: now, daysAhead: 5, in: s)
let events = WidgetHelpers.upcoming(from: now, daysAhead: 7, in: s)
var buckets: [Date: [WidgetEvent]] = [:]
for ev in events {
let key = cal.startOfDay(for: ev.start)

View File

@@ -18,6 +18,21 @@ extension Color {
}
}
/// Shared event time formatting for all widgets: "start end", or the
/// localized all-day label. Keeps every widget's event row consistent.
enum WidgetTime {
static func range(_ ev: WidgetEvent, lang: String) -> String {
if ev.isAllDay { return WidgetL10n.t("widget.allday", lang) }
let f = DateFormatter()
f.locale = WidgetL10n.locale(lang)
f.dateFormat = "HH:mm"
let start = f.string(from: ev.start)
// Hide a redundant identical end time (zero-length events).
if ev.end <= ev.start { return start }
return "\(start) \(f.string(from: ev.end))"
}
}
enum WidgetL10n {
static func t(_ key: String, _ stored: String) -> String {
let lang: String
@@ -46,14 +61,14 @@ enum WidgetL10n {
"widget.no_events": "Keine Termine",
"widget.allday": "Ganztägig",
"widget.more": "+%d weitere",
"widget.upcoming": "Nächste 5 Tage",
"widget.upcoming": "Nächste 7 Tage",
"widget.no_data": "Keine Daten App einmal öffnen",
"widget.display.today_title": "Heute",
"widget.display.today_desc": "Heutige Termine auf einen Blick.",
"widget.display.days_title": "Heute & Morgen",
"widget.display.days_desc": "Termine der nächsten zwei Tage.",
"widget.display.upcoming_title": "Nächste 5 Tage",
"widget.display.upcoming_desc": "Termine der nächsten 5 Tage.",
"widget.display.upcoming_title": "Nächste 7 Tage",
"widget.display.upcoming_desc": "Termine der nächsten 7 Tage.",
"widget.display.thisweek_title": "Diese Woche",
"widget.display.thisweek_desc": "Wochenraster mit Terminen.",
"widget.display.twoweeks_title": "Zwei Wochen",
@@ -84,14 +99,14 @@ enum WidgetL10n {
"widget.no_events": "No events",
"widget.allday": "All-day",
"widget.more": "+%d more",
"widget.upcoming": "Next 5 days",
"widget.upcoming": "Next 7 days",
"widget.no_data": "No data open the app once",
"widget.display.today_title": "Today",
"widget.display.today_desc": "Today's events at a glance.",
"widget.display.days_title": "Today & tomorrow",
"widget.display.days_desc": "Events for the next two days.",
"widget.display.upcoming_title": "Next 5 days",
"widget.display.upcoming_desc": "Events for the next 5 days.",
"widget.display.upcoming_title": "Next 7 days",
"widget.display.upcoming_desc": "Events for the next 7 days.",
"widget.display.thisweek_title": "This Week",
"widget.display.thisweek_desc": "Week grid with events.",
"widget.display.twoweeks_title": "Two Weeks",