WIP: Widget-, Sync- & Event-Editor-Änderungen

Zwischenstand vor den Sharing/Gruppen/Import-Export-Features (gesichert,
damit die neuen Features sauber darauf aufbauen).
This commit is contained in:
Scarriffle
2026-05-31 19:22:12 +02:00
parent e71fd7512f
commit b1e0cf1fdc
11 changed files with 375 additions and 75 deletions

View File

@@ -12,7 +12,11 @@ struct CalendarrWidgetBundle: WidgetBundle {
UpcomingWidget()
UpNextWidget()
CalendarDayWidget()
TwoMonthWidget()
NowNextEventsWidget()
LockScreenWidget()
LockScreenCountWidget()
LockScreenCountdownWidget()
}
}
@@ -157,7 +161,37 @@ struct CalendarDayWidget: Widget {
}
}
// MARK: Lock Screen (circular, rectangular, inline)
// MARK: Two Month calendar grid (medium + large)
struct TwoMonthWidget: Widget {
let kind: String = "TwoMonthWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: CalendarrTimelineProvider()) { entry in
TwoMonthWidgetView(entry: entry).calendarrChrome(entry.snapshot)
}
.configurationDisplayName(WidgetL10n.t("widget.display.twomonth_title", "system"))
.description(WidgetL10n.t("widget.display.twomonth_desc", "system"))
.supportedFamilies([.systemMedium, .systemLarge])
}
}
// MARK: Now & Next events (medium)
struct NowNextEventsWidget: Widget {
let kind: String = "NowNextEventsWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: CalendarrTimelineProvider()) { entry in
NowNextWidgetView(entry: entry).calendarrChrome(entry.snapshot)
}
.configurationDisplayName(WidgetL10n.t("widget.display.nownext_title", "system"))
.description(WidgetL10n.t("widget.display.nownext_desc", "system"))
.supportedFamilies([.systemMedium])
}
}
// MARK: Lock Screen: date (circular, rectangular, inline)
struct LockScreenWidget: Widget {
let kind: String = "LockScreenWidget"
@@ -173,3 +207,37 @@ struct LockScreenWidget: Widget {
.supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryInline])
}
}
// MARK: Lock Screen: today event count (circular, rectangular, inline)
struct LockScreenCountWidget: Widget {
let kind: String = "LockScreenCountWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: CalendarrTimelineProvider()) { entry in
LockScreenCountWidgetView(entry: entry)
.containerBackground(for: .widget) { Color.clear }
.environment(\.locale, WidgetL10n.locale(entry.snapshot?.language ?? "system"))
}
.configurationDisplayName(WidgetL10n.t("widget.display.lockscreen_count_title", "system"))
.description(WidgetL10n.t("widget.display.lockscreen_count_desc", "system"))
.supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryInline])
}
}
// MARK: Lock Screen: countdown to next event (circular, rectangular, inline)
struct LockScreenCountdownWidget: Widget {
let kind: String = "LockScreenCountdownWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: CalendarrTimelineProvider()) { entry in
LockScreenCountdownWidgetView(entry: entry)
.containerBackground(for: .widget) { Color.clear }
.environment(\.locale, WidgetL10n.locale(entry.snapshot?.language ?? "system"))
}
.configurationDisplayName(WidgetL10n.t("widget.display.lockscreen_countdown_title", "system"))
.description(WidgetL10n.t("widget.display.lockscreen_countdown_desc", "system"))
.supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryInline])
}
}

View File

@@ -1,6 +1,8 @@
import SwiftUI
import WidgetKit
// MARK: Date widget (existing)
struct LockScreenWidgetView: View {
let entry: CalendarrEntry
@Environment(\.widgetFamily) private var family
@@ -97,3 +99,190 @@ struct LockScreenWidgetView: View {
return Label(text, systemImage: "calendar")
}
}
// MARK: Today event count widget
struct LockScreenCountWidgetView: View {
let entry: CalendarrEntry
@Environment(\.widgetFamily) private var family
private var snapshot: WidgetSnapshot? { entry.snapshot }
private var lang: String { snapshot?.language ?? "system" }
private var timeFmt: DateFormatter {
let f = DateFormatter()
f.locale = WidgetL10n.locale(lang)
f.dateFormat = "HH:mm"
return f
}
private var todayEvents: [WidgetEvent] {
guard let s = snapshot else { return [] }
return WidgetHelpers.upcoming(from: entry.date, daysAhead: 1, in: s)
}
@ViewBuilder
var body: some View {
switch family {
case .accessoryCircular:
circularView
case .accessoryRectangular:
rectangularView
default:
inlineView
}
}
private var circularView: some View {
ZStack {
AccessoryWidgetBackground()
VStack(spacing: 1) {
Image(systemName: "calendar")
.font(.system(size: 10, weight: .semibold))
.widgetAccentable()
Text("\(todayEvents.count)")
.font(.system(size: 22, weight: .bold))
.minimumScaleFactor(0.7)
.widgetAccentable()
}
}
}
private var rectangularView: some View {
let countLabel = "\(todayEvents.count) \(WidgetL10n.t("widget.events_count", lang))"
return VStack(alignment: .leading, spacing: 3) {
HStack(spacing: 4) {
Text(WidgetL10n.t("widget.today", lang).uppercased())
.font(.system(size: 9, weight: .bold))
.widgetAccentable()
Text("· \(countLabel)")
.font(.system(size: 9))
}
if todayEvents.isEmpty {
Text(WidgetL10n.t("widget.no_events", lang))
.font(.system(size: 12))
.foregroundStyle(.secondary)
} else {
ForEach(todayEvents.prefix(2)) { ev in
HStack(spacing: 4) {
Text(ev.isAllDay ? "·" : timeFmt.string(from: ev.start))
.font(.system(size: 10, weight: .semibold))
.widgetAccentable()
.frame(width: 32, alignment: .leading)
Text(ev.title)
.font(.system(size: 11, weight: .medium))
.lineLimit(1)
}
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
private var inlineView: some View {
let label = "\(todayEvents.count) \(WidgetL10n.t("widget.events_count", lang))"
return Label(label, systemImage: "calendar.badge.clock")
}
}
// MARK: Countdown to next event widget
struct LockScreenCountdownWidgetView: View {
let entry: CalendarrEntry
@Environment(\.widgetFamily) private var family
private var snapshot: WidgetSnapshot? { entry.snapshot }
private var lang: String { snapshot?.language ?? "system" }
private var timeFmt: DateFormatter {
let f = DateFormatter()
f.locale = WidgetL10n.locale(lang)
f.dateFormat = "HH:mm"
return f
}
private var nextEvent: WidgetEvent? {
guard let s = snapshot else { return nil }
return WidgetHelpers.upcoming(from: entry.date, daysAhead: 1, in: s).first
}
private var isRunning: Bool {
guard let ev = nextEvent, !ev.isAllDay else { return false }
return ev.start <= entry.date && ev.end > entry.date
}
private var countdownText: String {
guard let ev = nextEvent else { return WidgetL10n.t("widget.no_events", lang) }
if isRunning { return WidgetL10n.t("widget.running", lang) }
if ev.isAllDay { return WidgetL10n.t("widget.allday", lang) }
let total = Int(max(0, ev.start.timeIntervalSince(entry.date)) / 60)
if total < 60 { return "in \(total)m" }
let h = total / 60; let m = total % 60
return m == 0 ? "in \(h)h" : "in \(h)h \(m)m"
}
@ViewBuilder
var body: some View {
switch family {
case .accessoryCircular:
circularView
case .accessoryRectangular:
rectangularView
default:
inlineView
}
}
private var circularView: some View {
ZStack {
AccessoryWidgetBackground()
VStack(spacing: 1) {
Text(countdownText)
.font(.system(size: 13, weight: .bold))
.minimumScaleFactor(0.5)
.lineLimit(1)
.widgetAccentable()
if let ev = nextEvent, !ev.isAllDay {
Text(timeFmt.string(from: ev.start))
.font(.system(size: 8))
.lineLimit(1)
}
}
.padding(.horizontal, 4)
}
}
private var rectangularView: some View {
VStack(alignment: .leading, spacing: 2) {
if let ev = nextEvent {
Text(countdownText)
.font(.system(size: 11, weight: .semibold))
.widgetAccentable()
Text(ev.title)
.font(.system(size: 14, weight: .bold))
.lineLimit(1)
let timeStr = ev.isAllDay
? WidgetL10n.t("widget.allday", lang)
: "\(timeFmt.string(from: ev.start)) \(timeFmt.string(from: ev.end))"
Text(timeStr)
.font(.system(size: 11))
.lineLimit(1)
} else {
Image(systemName: "timer")
.font(.system(size: 13))
.widgetAccentable()
Text(WidgetL10n.t("widget.no_events", lang))
.font(.system(size: 13))
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
private var inlineView: some View {
let text: String = {
guard let ev = nextEvent else { return WidgetL10n.t("widget.no_events", lang) }
return "\(ev.title) \(countdownText)"
}()
return Label(text, systemImage: "timer")
}
}

View File

@@ -62,7 +62,7 @@ struct NowNextWidgetView: View {
private func featuredCard(snapshot: WidgetSnapshot) -> some View {
let ev = featuredEvent
let baseColor = ev.map { Color(widgetHex: $0.colorHex) } ?? Color.accentColor.opacity(0.5)
let baseColor = ev.map { Color(widgetHex: $0.colorHex) } ?? Color(widgetHex: snapshot.primaryColorHex)
return ZStack(alignment: .leading) {
LinearGradient(

View File

@@ -64,8 +64,19 @@ enum WidgetL10n {
"widget.display.upnext_desc": "Nächste Termine mit Monatsübersicht.",
"widget.display.calday_title": "Tag & Termine",
"widget.display.calday_desc": "Datum, Wochenübersicht und nächste Termine.",
"widget.display.lockscreen_title": "Sperrbildschirm",
"widget.display.lockscreen_desc": "Datum und nächster Termin auf dem Sperrbildschirm."
"widget.display.lockscreen_title": "Datum",
"widget.display.lockscreen_desc": "Aktuelles Datum und nächster Termin.",
"widget.display.twomonth_title": "Zwei Monate",
"widget.display.twomonth_desc": "Aktueller und nächster Monat auf einen Blick.",
"widget.display.nownext_title": "Jetzt & Nächstes",
"widget.display.nownext_desc": "Aktueller Termin und nächste Ereignisse.",
"widget.cw": "KW",
"widget.running": "Läuft",
"widget.events_count": "Termine",
"widget.display.lockscreen_count_title": "Termine heute",
"widget.display.lockscreen_count_desc": "Anzahl und Liste heutiger Termine.",
"widget.display.lockscreen_countdown_title": "Countdown",
"widget.display.lockscreen_countdown_desc": "Zeit bis zum nächsten Termin."
],
"en": [
"widget.today": "Today",
@@ -91,8 +102,19 @@ enum WidgetL10n {
"widget.display.upnext_desc": "Next events with month overview.",
"widget.display.calday_title": "Day & Events",
"widget.display.calday_desc": "Date, week overview and upcoming events.",
"widget.display.lockscreen_title": "Lock Screen",
"widget.display.lockscreen_desc": "Date and next event on the lock screen."
"widget.display.lockscreen_title": "Date",
"widget.display.lockscreen_desc": "Current date and next event.",
"widget.display.twomonth_title": "Two Months",
"widget.display.twomonth_desc": "Current and next month at a glance.",
"widget.display.nownext_title": "Now & Next",
"widget.display.nownext_desc": "Current event and upcoming events.",
"widget.cw": "W",
"widget.running": "Running",
"widget.events_count": "Events",
"widget.display.lockscreen_count_title": "Today's Events",
"widget.display.lockscreen_count_desc": "Count and list of today's events.",
"widget.display.lockscreen_countdown_title": "Countdown",
"widget.display.lockscreen_countdown_desc": "Time until your next event."
]
]
}