chore(repo): iOS-App als ios/ ins Hauptrepo uebernehmen

Calendarr-IOS wird mit vollstaendiger Historie als Unterverzeichnis
ios/ eingebunden. Damit liegen Server, Web, Android und iOS in einem
gemeinsamen Repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-09-14 09:20:12 +02:00
co-authored by Claude Opus 5
107 changed files with 19373 additions and 0 deletions
@@ -0,0 +1,152 @@
import SwiftUI
import WidgetKit
struct CalendarDayWidgetView: View {
let entry: CalendarrEntry
private var snapshot: WidgetSnapshot? { entry.snapshot }
private var lang: String { snapshot?.language ?? "system" }
private var cal: Calendar {
var c = Calendar(identifier: .gregorian)
c.locale = WidgetL10n.locale(lang)
c.firstWeekday = 2
return c
}
private var weekDays: [Date] {
let start = cal.date(from: cal.dateComponents([.yearForWeekOfYear, .weekOfYear], from: entry.date)) ?? entry.date
return (0..<7).compactMap { cal.date(byAdding: .day, value: $0, to: start) }
}
private var upcomingEvents: [WidgetEvent] {
guard let s = snapshot else { return [] }
return WidgetHelpers.upcoming(from: entry.date, daysAhead: 1, in: s)
}
private var monthFmt: DateFormatter {
let f = DateFormatter(); f.locale = WidgetL10n.locale(lang); f.dateFormat = "LLLL"; return f
}
private var weekdayFmt: DateFormatter {
let f = DateFormatter(); f.locale = WidgetL10n.locale(lang); f.dateFormat = "EEEE"; return f
}
private var timeFmt: DateFormatter {
let f = DateFormatter(); f.locale = WidgetL10n.locale(lang); f.dateFormat = "HH:mm"; return f
}
var body: some View {
if let s = snapshot {
let primary = Color(widgetHex: s.primaryColorHex)
let accent = Color(widgetHex: s.accentColorHex)
VStack(alignment: .leading, spacing: 0) {
header(primary: primary)
.padding(.top, 6)
weekStrip(snapshot: s, primary: primary, accent: accent)
.padding(.vertical, 3)
Rectangle()
.fill(Color(widgetHex: s.lineColorHex).opacity(0.4))
.frame(height: 0.5)
.padding(.bottom, 4)
eventList(accent: accent)
}
} else {
Text(WidgetL10n.t("widget.no_data", lang))
.font(.caption)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
// MARK: – Header
private func header(primary: Color) -> some View {
HStack(alignment: .top, spacing: 6) {
Text("\(cal.component(.day, from: entry.date))")
.font(.system(size: 30, weight: .bold))
.foregroundStyle(primary)
.frame(width: 40, alignment: .leading)
.minimumScaleFactor(0.7)
VStack(alignment: .leading, spacing: 1) {
Text(monthFmt.string(from: entry.date).uppercased())
.font(.system(size: 11, weight: .bold))
.foregroundStyle(primary)
Text("\(WidgetL10n.t("widget.today", lang)), \(weekdayFmt.string(from: entry.date))")
.font(.system(size: 10))
.foregroundStyle(.secondary)
.lineLimit(1)
.minimumScaleFactor(0.75)
}
Spacer(minLength: 0)
}
.padding(.bottom, 2)
}
// MARK: – Week strip
private func weekStrip(snapshot: WidgetSnapshot, primary: Color, accent: Color) -> some View {
HStack(spacing: 0) {
ForEach(weekDays, id: \.self) { day in
let isToday = cal.isDateInToday(day)
let hasEvs = !WidgetHelpers.events(for: day, in: snapshot).isEmpty
VStack(spacing: 2) {
Text(shortDay(day))
.font(.system(size: 8, weight: .bold))
.foregroundStyle(isToday ? accent : .secondary)
ZStack {
if isToday {
Circle().fill(primary)
} else if hasEvs {
Circle().fill(accent.opacity(0.18))
}
Text("\(cal.component(.day, from: day))")
.font(.system(size: 11, weight: isToday ? .bold : .medium))
.foregroundStyle(isToday ? .white : .primary)
}
.frame(width: 22, height: 22)
}
.frame(maxWidth: .infinity)
}
}
}
private func shortDay(_ date: Date) -> String {
let f = DateFormatter()
f.locale = WidgetL10n.locale(lang)
f.dateFormat = "EEE"
return String(f.string(from: date).prefix(2)).uppercased()
}
// MARK: – Event list
@ViewBuilder
private func eventList(accent: Color) -> some View {
if upcomingEvents.isEmpty {
Text(WidgetL10n.t("widget.no_events", lang))
.font(.system(size: 11))
.foregroundStyle(.secondary)
Spacer(minLength: 0)
} else {
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: 22)
VStack(alignment: .leading, spacing: 1) {
Text(ev.title)
.font(.system(size: 11, weight: .semibold))
.lineLimit(1)
Text(ev.isAllDay
? WidgetL10n.t("widget.allday", lang)
: "\(timeFmt.string(from: ev.start)) – \(timeFmt.string(from: ev.end))")
.font(.system(size: 9))
.foregroundStyle(.secondary)
}
Spacer(minLength: 0)
}
}
Spacer(minLength: 0)
}
}
}
}
+47
View File
@@ -0,0 +1,47 @@
import AppIntents
import WidgetKit
/// An individual calendar option shown in the widget configuration picker.
struct CalendarAppEntity: AppEntity, Identifiable {
let id: String
let name: String
let colorHex: String
static var typeDisplayRepresentation: TypeDisplayRepresentation = "Kalender"
static var defaultQuery = CalendarEntityQuery()
var displayRepresentation: DisplayRepresentation {
DisplayRepresentation(title: LocalizedStringResource(stringLiteral: name))
}
}
/// Reads available calendars from the App Group container so the widget
/// extension can offer options without a network call.
struct CalendarEntityQuery: EntityQuery {
func entities(for identifiers: [String]) async throws -> [CalendarAppEntity] {
let ids = Set(identifiers)
return WidgetStore.readCalendars()
.filter { ids.contains($0.id) }
.map { CalendarAppEntity(id: $0.id, name: $0.name, colorHex: $0.colorHex) }
}
func suggestedEntities() async throws -> [CalendarAppEntity] {
WidgetStore.readCalendars()
.map { CalendarAppEntity(id: $0.id, name: $0.name, colorHex: $0.colorHex) }
}
}
/// Widget configuration intent: lets the user pick which calendars to show.
/// No selection means "show all calendars" (the default).
///
/// The parameter must be optional: WidgetConfigurationIntent requires every
/// parameter type to be optional, and the macOS/Mac Catalyst SDK enforces that
/// where the iOS one lets a bare array through. `nil` and `[]` are treated
/// alike downstream, so this is behaviour-preserving.
struct CalendarSelectionIntent: WidgetConfigurationIntent {
static var title: LocalizedStringResource = "Kalender auswählen"
static var description = IntentDescription("Wähle welche Kalender im Widget angezeigt werden. Leer = alle Kalender.")
@Parameter(title: "Kalender")
var selectedCalendars: [CalendarAppEntity]?
}
@@ -0,0 +1,66 @@
import WidgetKit
import AppIntents
import CalendarrCore
struct CalendarrEntry: TimelineEntry {
let date: Date
let snapshot: WidgetSnapshot?
}
struct CalendarrTimelineProvider: AppIntentTimelineProvider {
typealias Entry = CalendarrEntry
typealias Intent = CalendarSelectionIntent
func placeholder(in context: Context) -> CalendarrEntry {
CalendarrEntry(date: .now, snapshot: WidgetStore.read())
}
func snapshot(for configuration: CalendarSelectionIntent, in context: Context) async -> CalendarrEntry {
let raw = WidgetStore.read()
return CalendarrEntry(date: .now, snapshot: filtered(raw, by: configuration))
}
func timeline(for configuration: CalendarSelectionIntent, in context: Context) async -> Timeline<CalendarrEntry> {
let raw = WidgetStore.read()
let snap = filtered(raw, by: configuration)
let now = Date()
// One entry per hour for 24 h so the widget re-renders as time advances.
var entries: [CalendarrEntry] = []
for h in 0..<24 {
let date = Calendar.current.date(byAdding: .hour, value: h, to: now) ?? now
entries.append(CalendarrEntry(date: date, snapshot: snap))
}
let refreshAt = Calendar.current.date(byAdding: .minute, value: 30, to: now) ?? now
return Timeline(entries: entries, policy: .after(refreshAt))
}
// MARK: – Filtering
private func filtered(_ snapshot: WidgetSnapshot?, by config: CalendarSelectionIntent) -> WidgetSnapshot? {
// The rule lives in CalendarrCore so the watch provider cannot drift
// from this one. Nothing selected means "show all calendars".
snapshot?.filtered(toCalendarKeys: Set((config.selectedCalendars ?? []).map(\.id)))
}
}
// MARK: – Shared helpers used by all widget views
enum WidgetHelpers {
static func events(for day: Date, in snapshot: WidgetSnapshot) -> [WidgetEvent] {
let cal = Calendar.current
let dayStart = cal.startOfDay(for: day)
let dayEnd = cal.date(byAdding: .day, value: 1, to: dayStart) ?? dayStart
return snapshot.events
.filter { $0.start < dayEnd && $0.end > dayStart }
.sorted { $0.start < $1.start }
}
static func upcoming(from now: Date, daysAhead: Int, in snapshot: WidgetSnapshot) -> [WidgetEvent] {
let cal = Calendar.current
let end = cal.date(byAdding: .day, value: daysAhead, to: cal.startOfDay(for: now)) ?? now
return snapshot.events
.filter { $0.end > now && $0.start < end }
.sorted { $0.start < $1.start }
}
}
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!--
Mac Catalyst entitlements for the widget extension. Deliberately minimal:
the widget only ever reads the snapshot from the App Group container. It
does no networking, touches no Contacts, and needs no keychain access.
-->
<key>com.apple.security.app-sandbox</key>
<true/>
<!-- Must match CalendarrAppGroup.current on this platform, byte for byte. -->
<key>com.apple.security.application-groups</key>
<array>
<string>PP34X97WS3.group.com.scarriffleservices.calendarr</string>
</array>
</dict>
</plist>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.scarriffleservices.calendarr</string>
</array>
</dict>
</plist>
+243
View File
@@ -0,0 +1,243 @@
import WidgetKit
import SwiftUI
@main
struct CalendarrWidgetBundle: WidgetBundle {
var body: some Widget {
TodayWidget()
TwoDaysWidget()
ThreeDaysWidget()
ThisWeekWidget()
TwoWeeksWidget()
UpcomingWidget()
UpNextWidget()
CalendarDayWidget()
TwoMonthWidget()
NowNextEventsWidget()
LockScreenWidget()
LockScreenCountWidget()
LockScreenCountdownWidget()
}
}
// Shared chrome modifier — keeps every home-screen widget on the same theme.
private struct CalendarrWidgetChrome: ViewModifier {
let snapshot: WidgetSnapshot?
func body(content: Content) -> some View {
let lang = snapshot?.language ?? "system"
content
.containerBackground(for: .widget) {
Color(widgetHex: snapshot?.backgroundColorHex ?? "#000000")
}
.foregroundStyle(Color(widgetHex: snapshot?.textColorHex ?? "#FFFFFF"))
.environment(\.locale, WidgetL10n.locale(lang))
}
}
private extension View {
func calendarrChrome(_ snapshot: WidgetSnapshot?) -> some View {
modifier(CalendarrWidgetChrome(snapshot: snapshot))
}
}
// MARK: – Today (small)
struct TodayWidget: Widget {
let kind: String = "TodayWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, provider: CalendarrTimelineProvider()) { entry in
TodayWidgetView(entry: entry).calendarrChrome(entry.snapshot)
}
.configurationDisplayName(WidgetL10n.t("widget.display.today_title", "system"))
.description(WidgetL10n.t("widget.display.today_desc", "system"))
.supportedFamilies([.systemSmall])
}
}
// MARK: – Today & Tomorrow (medium)
struct TwoDaysWidget: Widget {
let kind: String = "TwoDaysWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, provider: CalendarrTimelineProvider()) { entry in
TwoDaysWidgetView(entry: entry).calendarrChrome(entry.snapshot)
}
.configurationDisplayName(WidgetL10n.t("widget.display.days_title", "system"))
.description(WidgetL10n.t("widget.display.days_desc", "system"))
.supportedFamilies([.systemMedium])
}
}
// MARK: – Three Days (medium)
struct ThreeDaysWidget: Widget {
let kind: String = "ThreeDaysWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, provider: CalendarrTimelineProvider()) { entry in
ThreeDaysWidgetView(entry: entry).calendarrChrome(entry.snapshot)
}
.configurationDisplayName(WidgetL10n.t("widget.display.threedays_title", "system"))
.description(WidgetL10n.t("widget.display.threedays_desc", "system"))
.supportedFamilies([.systemMedium])
}
}
// MARK: – This Week (medium)
struct ThisWeekWidget: Widget {
let kind: String = "ThisWeekWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, provider: CalendarrTimelineProvider()) { entry in
ThisWeekWidgetView(entry: entry).calendarrChrome(entry.snapshot)
}
.configurationDisplayName(WidgetL10n.t("widget.display.thisweek_title", "system"))
.description(WidgetL10n.t("widget.display.thisweek_desc", "system"))
.supportedFamilies([.systemMedium])
}
}
// MARK: – Two Weeks (medium)
struct TwoWeeksWidget: Widget {
let kind: String = "TwoWeeksWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, provider: CalendarrTimelineProvider()) { entry in
TwoWeeksWidgetView(entry: entry).calendarrChrome(entry.snapshot)
}
.configurationDisplayName(WidgetL10n.t("widget.display.twoweeks_title", "system"))
.description(WidgetL10n.t("widget.display.twoweeks_desc", "system"))
.supportedFamilies([.systemMedium])
}
}
// MARK: – Upcoming (large + extra large on iPad)
struct UpcomingWidget: Widget {
let kind: String = "UpcomingWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, provider: CalendarrTimelineProvider()) { entry in
UpcomingWidgetView(entry: entry).calendarrChrome(entry.snapshot)
}
.configurationDisplayName(WidgetL10n.t("widget.display.upcoming_title", "system"))
.description(WidgetL10n.t("widget.display.upcoming_desc", "system"))
.supportedFamilies([.systemLarge, .systemExtraLarge])
}
}
// MARK: – Up Next + Calendar (medium)
struct UpNextWidget: Widget {
let kind: String = "UpNextWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, provider: CalendarrTimelineProvider()) { entry in
UpNextWidgetView(entry: entry).calendarrChrome(entry.snapshot)
}
.configurationDisplayName(WidgetL10n.t("widget.display.upnext_title", "system"))
.description(WidgetL10n.t("widget.display.upnext_desc", "system"))
.supportedFamilies([.systemMedium])
}
}
// MARK: – Calendar Day: date + week strip + events (medium)
struct CalendarDayWidget: Widget {
let kind: String = "CalendarDayWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, provider: CalendarrTimelineProvider()) { entry in
CalendarDayWidgetView(entry: entry).calendarrChrome(entry.snapshot)
}
.configurationDisplayName(WidgetL10n.t("widget.display.calday_title", "system"))
.description(WidgetL10n.t("widget.display.calday_desc", "system"))
.supportedFamilies([.systemMedium])
}
}
// MARK: – Two Month calendar grid (medium + large)
struct TwoMonthWidget: Widget {
let kind: String = "TwoMonthWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, 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 {
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, 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"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, provider: CalendarrTimelineProvider()) { entry in
LockScreenWidgetView(entry: entry)
.containerBackground(for: .widget) { Color.clear }
.environment(\.locale, WidgetL10n.locale(entry.snapshot?.language ?? "system"))
}
.configurationDisplayName(WidgetL10n.t("widget.display.lockscreen_title", "system"))
.description(WidgetL10n.t("widget.display.lockscreen_desc", "system"))
.supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryInline])
}
}
// MARK: – Lock Screen: today event count (circular, rectangular, inline)
struct LockScreenCountWidget: Widget {
let kind: String = "LockScreenCountWidget"
var body: some WidgetConfiguration {
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, 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 {
AppIntentConfiguration(kind: kind, intent: CalendarSelectionIntent.self, 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])
}
}
+29
View File
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Calendarr Widgets</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
<key>CFBundleShortVersionString</key>
<string>$(MARKETING_VERSION)</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
</dict>
</plist>
@@ -0,0 +1,288 @@
import SwiftUI
import WidgetKit
// MARK: – Date widget (existing)
struct LockScreenWidgetView: View {
let entry: CalendarrEntry
@Environment(\.widgetFamily) private var family
private var snapshot: WidgetSnapshot? { entry.snapshot }
private var lang: String { snapshot?.language ?? "system" }
private var cal: Calendar {
var c = Calendar(identifier: .gregorian)
c.locale = WidgetL10n.locale(lang)
return c
}
private var nextEvent: WidgetEvent? {
guard let s = snapshot else { return nil }
return WidgetHelpers.upcoming(from: entry.date, daysAhead: 1, in: s).first
}
private var timeFmt: DateFormatter {
let f = DateFormatter(); f.locale = WidgetL10n.locale(lang); f.dateFormat = "HH:mm"; return f
}
private var monthAbbrev: String {
let f = DateFormatter(); f.locale = WidgetL10n.locale(lang); f.dateFormat = "LLL"
return f.string(from: entry.date).uppercased()
}
@ViewBuilder
var body: some View {
switch family {
case .accessoryCircular:
circularView
case .accessoryRectangular:
rectangularView
default:
inlineView
}
}
// MARK: – Circular: today's date
private var circularView: some View {
ZStack {
AccessoryWidgetBackground()
VStack(spacing: 0) {
Text("\(cal.component(.day, from: entry.date))")
.font(.system(size: 22, weight: .bold))
.minimumScaleFactor(0.7)
.widgetAccentable()
Text(monthAbbrev)
.font(.system(size: 8, weight: .semibold))
}
}
}
// MARK: – Rectangular: next event
private var rectangularView: some View {
VStack(alignment: .leading, spacing: 2) {
if let ev = nextEvent {
Text(ev.isAllDay
? WidgetL10n.t("widget.allday", lang)
: timeFmt.string(from: ev.start))
.font(.system(size: 11, weight: .semibold))
.widgetAccentable()
Text(ev.title)
.font(.system(size: 14, weight: .bold))
.lineLimit(1)
if !ev.location.isEmpty {
Text(ev.location)
.font(.system(size: 11))
.lineLimit(1)
}
} else {
Image(systemName: "calendar")
.font(.system(size: 13))
.widgetAccentable()
Text(WidgetL10n.t("widget.no_events", lang))
.font(.system(size: 13))
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
}
// MARK: – Inline: brief next event
private var inlineView: some View {
let text: String = {
guard let ev = nextEvent else {
return WidgetL10n.t("widget.no_events", lang)
}
return ev.isAllDay ? ev.title : "\(timeFmt.string(from: ev.start)) \(ev.title)"
}()
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")
}
}
@@ -0,0 +1,162 @@
import SwiftUI
import WidgetKit
struct NowNextWidgetView: View {
let entry: CalendarrEntry
private var snapshot: WidgetSnapshot? { entry.snapshot }
private var lang: String { snapshot?.language ?? "system" }
private var cal: Calendar {
var c = Calendar(identifier: .gregorian)
c.locale = WidgetL10n.locale(lang)
return c
}
private var timeFmt: DateFormatter {
let f = DateFormatter(); f.locale = WidgetL10n.locale(lang); f.dateFormat = "HH:mm"; return f
}
private var dayOfWeekFmt: DateFormatter {
let f = DateFormatter(); f.locale = WidgetL10n.locale(lang); f.dateFormat = "EEEE"; return f
}
// Currently running event, or next upcoming timed event, or first all-day event
private var featuredEvent: WidgetEvent? {
guard let s = snapshot else { return nil }
let pool = WidgetHelpers.upcoming(from: entry.date, daysAhead: 1, in: s)
if let running = pool.first(where: { !$0.isAllDay && $0.start <= entry.date }) { return running }
if let next = pool.first(where: { !$0.isAllDay }) { return next }
return pool.first
}
// All upcoming events today except the featured one
private var remainingEvents: [WidgetEvent] {
guard let s = snapshot else { return [] }
let pool = WidgetHelpers.upcoming(from: entry.date, daysAhead: 1, in: s)
guard let featured = featuredEvent else { return pool }
return pool.filter { $0.id != featured.id }
}
private func timeRange(_ ev: WidgetEvent) -> String {
ev.isAllDay
? WidgetL10n.t("widget.allday", lang)
: "\(timeFmt.string(from: ev.start)) – \(timeFmt.string(from: ev.end))"
}
var body: some View {
if let s = snapshot {
let line = Color(widgetHex: s.lineColorHex)
VStack(spacing: 6) {
featuredCard(snapshot: s)
bottomRow(line: line)
}
} else {
Text(WidgetL10n.t("widget.no_data", lang))
.font(.caption)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
// MARK: – Featured event card
private func featuredCard(snapshot: WidgetSnapshot) -> some View {
let ev = featuredEvent
let baseColor = ev.map { Color(widgetHex: $0.colorHex) } ?? Color(widgetHex: snapshot.primaryColorHex)
return ZStack(alignment: .leading) {
LinearGradient(
colors: [baseColor.opacity(0.75), baseColor],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
.clipShape(RoundedRectangle(cornerRadius: 10))
HStack(spacing: 0) {
VStack(alignment: .leading, spacing: 2) {
Text(ev?.title ?? WidgetL10n.t("widget.no_events", lang))
.font(.system(size: 13, weight: .bold))
.foregroundStyle(.white)
.lineLimit(1)
Text(ev.map { timeRange($0) } ?? "")
.font(.system(size: 10))
.foregroundStyle(.white.opacity(0.85))
}
.padding(.leading, 10)
.padding(.vertical, 9)
Spacer()
if ev != nil {
Image(systemName: "chevron.right")
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(.white.opacity(0.7))
.padding(.trailing, 12)
}
}
}
.frame(maxWidth: .infinity)
.fixedSize(horizontal: false, vertical: true)
}
// MARK: – Bottom: date + event list
private func bottomRow(line: Color) -> some View {
HStack(alignment: .top, spacing: 0) {
// Left: day name + large number
VStack(alignment: .leading, spacing: 0) {
Text(dayOfWeekFmt.string(from: entry.date).uppercased())
.font(.system(size: 8, weight: .bold))
.foregroundStyle(.secondary)
.lineLimit(1)
.minimumScaleFactor(0.6)
Text("\(cal.component(.day, from: entry.date))")
.font(.system(size: 30, weight: .light))
}
.frame(width: 50, alignment: .leading)
// Divider
line.opacity(0.4).frame(width: 0.5)
.padding(.horizontal, 6)
// Right: event list
VStack(alignment: .leading, spacing: 4) {
let shown = remainingEvents.prefix(2)
if shown.isEmpty {
Text(WidgetL10n.t("widget.no_events", lang))
.font(.system(size: 10))
.foregroundStyle(.secondary)
} else {
ForEach(shown) { ev in
HStack(alignment: .firstTextBaseline, spacing: 5) {
Circle()
.fill(Color(widgetHex: ev.colorHex))
.frame(width: 7, height: 7)
.padding(.top, 1)
VStack(alignment: .leading, spacing: 0) {
Text(ev.title)
.font(.system(size: 11, weight: .semibold))
.lineLimit(1)
Text(timeRange(ev))
.font(.system(size: 9))
.foregroundStyle(.secondary)
}
}
}
}
Spacer(minLength: 0)
}
Spacer(minLength: 0)
// +N badge
if remainingEvents.count > 2 {
Text("+\(remainingEvents.count - 2)")
.font(.system(size: 9, weight: .semibold))
.padding(.horizontal, 5)
.padding(.vertical, 2)
.background(.secondary.opacity(0.18), in: Capsule())
.frame(maxHeight: .infinity, alignment: .bottom)
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
@@ -0,0 +1,116 @@
import SwiftUI
import WidgetKit
struct ThisWeekWidgetView: View {
let entry: CalendarrEntry
private var snapshot: WidgetSnapshot? { entry.snapshot }
private var lang: String { snapshot?.language ?? "system" }
private var cal: Calendar {
var c = Calendar(identifier: .gregorian)
c.locale = WidgetL10n.locale(lang)
c.firstWeekday = 2
return c
}
private var weekStart: Date {
cal.date(from: cal.dateComponents([.yearForWeekOfYear, .weekOfYear], from: entry.date)) ?? entry.date
}
private var weekDays: [Date] {
(0..<7).compactMap { cal.date(byAdding: .day, value: $0, to: weekStart) }
}
private var monthHeader: String {
let f = DateFormatter()
f.locale = WidgetL10n.locale(lang)
f.dateFormat = "LLLL yyyy"
return f.string(from: weekStart).uppercased()
}
private var weekdayHeaders: [String] {
let f = DateFormatter(); f.locale = WidgetL10n.locale(lang)
let symbols = f.shortWeekdaySymbols ?? cal.shortWeekdaySymbols
let start = cal.firstWeekday - 1
return (0..<7).map { String(symbols[(start + $0) % 7].prefix(2)).uppercased() }
}
var body: some View {
if let s = snapshot {
let primary = Color(widgetHex: s.primaryColorHex)
let accent = Color(widgetHex: s.accentColorHex)
VStack(alignment: .leading, spacing: 3) {
HStack(alignment: .firstTextBaseline, spacing: 4) {
Text(monthHeader.split(separator: " ").first.map(String.init) ?? "")
.font(.system(size: 10, weight: .bold))
.foregroundStyle(primary)
Text(monthHeader.split(separator: " ").dropFirst().joined(separator: " "))
.font(.system(size: 10, weight: .semibold))
}
// Equal-width columns via maxWidth — no GeometryReader needed
HStack(spacing: 0) {
ForEach(Array(weekDays.enumerated()), id: \.offset) { idx, day in
dayColumn(day, snapshot: s, primary: primary, accent: accent)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
.overlay(alignment: .trailing) {
if idx < 6 {
Rectangle()
.fill(Color(widgetHex: s.lineColorHex).opacity(0.35))
.frame(width: 0.5)
}
}
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
} else {
Text(WidgetL10n.t("widget.no_data", lang))
.font(.caption)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
private func dayColumn(_ day: Date,
snapshot: WidgetSnapshot,
primary: Color,
accent: Color) -> some View {
let isToday = cal.isDateInToday(day)
let evs = WidgetHelpers.events(for: day, in: snapshot)
let dayIdx = (0..<7).firstIndex { i in cal.isDate(weekDays[i], inSameDayAs: day) } ?? 0
return VStack(alignment: .center, spacing: 1) {
Text(weekdayHeaders[dayIdx])
.font(.system(size: 7.5, weight: .bold))
.foregroundStyle(isToday ? accent : .secondary)
Text("\(cal.component(.day, from: day))")
.font(.system(size: 10, weight: isToday ? .bold : .semibold))
.foregroundStyle(isToday ? Color.white : Color.primary)
.frame(width: 16, height: 16)
.background(isToday ? primary : Color.clear)
.clipShape(Circle())
ForEach(evs.prefix(8)) { ev in
eventPill(ev)
}
if evs.count > 8 {
Text("+\(evs.count - 8)")
.font(.system(size: 6.5))
.foregroundStyle(accent)
}
Spacer(minLength: 0)
}
.padding(.horizontal, 1)
}
private func eventPill(_ ev: WidgetEvent) -> some View {
Text(ev.title)
.font(.system(size: 7, weight: .medium))
.lineLimit(1)
.foregroundStyle(.white)
.padding(.horizontal, 2)
.padding(.vertical, 0.5)
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color(widgetHex: ev.colorHex))
.clipShape(RoundedRectangle(cornerRadius: 1.5))
}
}
@@ -0,0 +1,131 @@
import SwiftUI
import WidgetKit
struct ThreeDaysWidgetView: View {
let entry: CalendarrEntry
private var snapshot: WidgetSnapshot? { entry.snapshot }
private var lang: String { snapshot?.language ?? "system" }
private var cal: Calendar {
var c = Calendar(identifier: .gregorian)
c.locale = WidgetL10n.locale(lang)
return c
}
private var days: [Date] {
let today = cal.startOfDay(for: entry.date)
return (0..<3).compactMap { cal.date(byAdding: .day, value: $0, to: today) }
}
private var monthHeader: String {
let f = DateFormatter()
f.locale = WidgetL10n.locale(lang)
f.dateFormat = "LLLL yyyy"
return f.string(from: entry.date).uppercased()
}
private var weekdayFmt: DateFormatter {
let f = DateFormatter()
f.locale = WidgetL10n.locale(lang)
f.dateFormat = "EEE"
return f
}
private var timeFmt: DateFormatter {
let f = DateFormatter()
f.locale = WidgetL10n.locale(lang)
f.dateFormat = "HH:mm"
return f
}
var body: some View {
if let s = snapshot {
let primary = Color(widgetHex: s.primaryColorHex)
let accent = Color(widgetHex: s.accentColorHex)
VStack(alignment: .leading, spacing: 3) {
HStack(alignment: .firstTextBaseline, spacing: 6) {
Text(monthHeader.split(separator: " ").first.map(String.init) ?? "")
.font(.system(size: 11, weight: .bold))
.foregroundStyle(primary)
Text(monthHeader.split(separator: " ").dropFirst().joined(separator: " "))
.font(.system(size: 11, weight: .semibold))
}
HStack(spacing: 0) {
ForEach(Array(days.enumerated()), id: \.offset) { idx, day in
column(for: day, snapshot: s, primary: primary, accent: accent)
.frame(maxWidth: .infinity, alignment: .topLeading)
.overlay(alignment: .trailing) {
if idx < 2 {
Rectangle()
.fill(Color(widgetHex: s.lineColorHex).opacity(0.4))
.frame(width: 0.5)
}
}
}
}
}
} else {
Text(WidgetL10n.t("widget.no_data", lang))
.font(.caption)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
private func column(for day: Date, snapshot: WidgetSnapshot, primary: Color, accent: Color) -> some View {
let isToday = cal.isDateInToday(day)
let evs = WidgetHelpers.events(for: day, in: snapshot)
return VStack(alignment: .leading, spacing: 2) {
HStack {
Text(weekdayFmt.string(from: day).uppercased() + ".")
.font(.system(size: 9, weight: .bold))
.foregroundStyle(isToday ? accent : .secondary)
Spacer()
Text("\(cal.component(.day, from: day))")
.font(.system(size: 11, weight: isToday ? .bold : .semibold))
.foregroundStyle(isToday ? Color.white : Color.primary)
.frame(width: 17, height: 17)
.background(isToday ? primary : Color.clear)
.clipShape(Circle())
}
.padding(.horizontal, 3)
if evs.isEmpty {
Text(WidgetL10n.t("widget.no_events", lang))
.font(.system(size: 9))
.foregroundStyle(.tertiary)
.padding(.horizontal, 3)
} else {
ForEach(evs.prefix(4)) { ev in
eventRow(ev)
}
if evs.count > 4 {
Text("+\(evs.count - 4)")
.font(.system(size: 8))
.foregroundStyle(accent)
.padding(.leading, 3)
}
}
Spacer(minLength: 0)
}
}
private func eventRow(_ ev: WidgetEvent) -> some View {
VStack(alignment: .leading, spacing: 0) {
HStack(spacing: 3) {
RoundedRectangle(cornerRadius: 1)
.fill(Color(widgetHex: ev.colorHex))
.frame(width: 2)
Text(ev.title)
.font(.system(size: 9, weight: .medium))
.lineLimit(1)
Spacer(minLength: 0)
}
Text(WidgetTime.range(ev, lang: lang))
.font(.system(size: 8))
.foregroundStyle(.secondary)
.padding(.leading, 5)
}
.padding(.horizontal, 3)
}
}
@@ -0,0 +1,84 @@
import SwiftUI
import WidgetKit
struct TodayWidgetView: View {
let entry: CalendarrEntry
private var snapshot: WidgetSnapshot? { entry.snapshot }
private var todayEvents: [WidgetEvent] {
guard let s = snapshot else { return [] }
return WidgetHelpers.events(for: entry.date, in: s)
}
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
}
var body: some View {
let primary = Color(widgetHex: snapshot?.primaryColorHex ?? "#4285f4")
let accent = Color(widgetHex: snapshot?.accentColorHex ?? "#ea4335")
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(WidgetL10n.t("widget.today", lang))
.font(.caption.weight(.bold))
.foregroundStyle(primary)
Spacer()
Text(headerDate)
.font(.caption2)
.foregroundStyle(.secondary)
}
if snapshot == nil {
Text(WidgetL10n.t("widget.no_data", lang))
.font(.caption)
.foregroundStyle(.secondary)
} else if todayEvents.isEmpty {
Spacer()
Text(WidgetL10n.t("widget.no_events", lang))
.font(.caption)
.foregroundStyle(.secondary)
Spacer()
} else {
ForEach(todayEvents.prefix(3)) { ev in
eventRow(ev)
}
if todayEvents.count > 3 {
Text(String(format: WidgetL10n.t("widget.more", lang), todayEvents.count - 3))
.font(.caption2)
.foregroundStyle(accent)
}
Spacer(minLength: 0)
}
}
}
private var headerDate: String {
let f = DateFormatter()
f.locale = WidgetL10n.locale(lang)
f.dateFormat = "d. MMM"
return f.string(from: entry.date)
}
private func eventRow(_ ev: WidgetEvent) -> some View {
HStack(spacing: 6) {
RoundedRectangle(cornerRadius: 2)
.fill(Color(widgetHex: ev.colorHex))
.frame(width: 3)
VStack(alignment: .leading, spacing: 1) {
Text(ev.title)
.font(.caption.weight(.medium))
.lineLimit(1)
Text(WidgetTime.range(ev, lang: lang))
.font(.caption2)
.foregroundStyle(.secondary)
}
Spacer(minLength: 0)
}
}
}
@@ -0,0 +1,109 @@
import SwiftUI
import WidgetKit
struct TwoDaysWidgetView: View {
let entry: CalendarrEntry
private var snapshot: WidgetSnapshot? { entry.snapshot }
private var lang: String { snapshot?.language ?? "system" }
private var today: Date { Calendar.current.startOfDay(for: entry.date) }
private var tomorrow: Date {
Calendar.current.date(byAdding: .day, value: 1, to: today) ?? today
}
private var timeFmt: DateFormatter {
let f = DateFormatter()
f.locale = WidgetL10n.locale(lang)
f.dateFormat = "HH:mm"
return f
}
var body: some View {
if let s = snapshot {
let primary = Color(widgetHex: s.primaryColorHex)
let accent = Color(widgetHex: s.accentColorHex)
HStack(spacing: 8) {
column(for: today,
title: WidgetL10n.t("widget.today", lang),
isToday: true,
events: WidgetHelpers.events(for: today, in: s),
primary: primary, accent: accent,
lineColor: Color(widgetHex: s.lineColorHex))
Rectangle()
.fill(Color(widgetHex: s.lineColorHex).opacity(0.4))
.frame(width: 0.5)
column(for: tomorrow,
title: WidgetL10n.t("widget.tomorrow", lang),
isToday: false,
events: WidgetHelpers.events(for: tomorrow, in: s),
primary: primary, accent: accent,
lineColor: Color(widgetHex: s.lineColorHex))
}
} else {
Text(WidgetL10n.t("widget.no_data", lang))
.font(.caption)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
private func column(for day: Date,
title: String,
isToday: Bool,
events: [WidgetEvent],
primary: Color,
accent: Color,
lineColor: Color) -> some View {
VStack(alignment: .leading, spacing: 4) {
HStack(alignment: .firstTextBaseline, spacing: 4) {
Text(title)
.font(.caption.weight(.bold))
.foregroundStyle(isToday ? primary : accent)
Text(shortDate(day))
.font(.caption2)
.foregroundStyle(.tertiary)
}
if events.isEmpty {
Text(WidgetL10n.t("widget.no_events", lang))
.font(.caption2)
.foregroundStyle(.secondary)
} else {
ForEach(events.prefix(4)) { ev in
eventRow(ev)
}
if events.count > 4 {
Text(String(format: WidgetL10n.t("widget.more", lang), events.count - 4))
.font(.system(size: 9))
.foregroundStyle(accent)
}
}
Spacer(minLength: 0)
}
.frame(maxWidth: .infinity, alignment: .topLeading)
}
private func eventRow(_ ev: WidgetEvent) -> some View {
HStack(spacing: 4) {
RoundedRectangle(cornerRadius: 1.5)
.fill(Color(widgetHex: ev.colorHex))
.frame(width: 2)
VStack(alignment: .leading, spacing: 0) {
Text(ev.title)
.font(.system(size: 11, weight: .medium))
.lineLimit(1)
Text(WidgetTime.range(ev, lang: lang))
.font(.system(size: 9))
.foregroundStyle(.secondary)
}
Spacer(minLength: 0)
}
}
private func shortDate(_ d: Date) -> String {
let f = DateFormatter()
f.locale = WidgetL10n.locale(lang)
f.dateFormat = "d. MMM"
return f.string(from: d)
}
}
@@ -0,0 +1,172 @@
import SwiftUI
import WidgetKit
struct TwoMonthWidgetView: View {
let entry: CalendarrEntry
@Environment(\.widgetFamily) private var family
private var snapshot: WidgetSnapshot? { entry.snapshot }
private var lang: String { snapshot?.language ?? "system" }
private var cal: Calendar {
var c = Calendar(identifier: .gregorian)
c.locale = WidgetL10n.locale(lang)
c.firstWeekday = 2
return c
}
private var thisMonth: Date {
cal.date(from: cal.dateComponents([.year, .month], from: entry.date)) ?? entry.date
}
private var nextMonth: Date {
cal.date(byAdding: .month, value: 1, to: thisMonth) ?? thisMonth
}
// Weekday header labels (M T W T F S S)
private var weekdayHeaders: [String] {
let f = DateFormatter(); f.locale = WidgetL10n.locale(lang)
let symbols = f.veryShortWeekdaySymbols ?? cal.veryShortWeekdaySymbols
let start = cal.firstWeekday - 1
return (0..<7).map { String(symbols[(start + $0) % 7]).uppercased() }
}
// Number of date rows to show (5 for medium, 6 for large)
private var rowCount: Int { family == .systemLarge ? 6 : 5 }
var body: some View {
if let s = snapshot {
let primary = Color(widgetHex: s.primaryColorHex)
let accent = Color(widgetHex: s.accentColorHex)
let line = Color(widgetHex: s.lineColorHex)
HStack(alignment: .top, spacing: 0) {
monthColumn(monthDate: thisMonth, snapshot: s,
primary: primary, accent: accent, line: line)
.frame(maxWidth: .infinity)
line.opacity(0.35).frame(width: 0.5)
.padding(.horizontal, 3)
monthColumn(monthDate: nextMonth, snapshot: s,
primary: primary, accent: accent, line: line)
.frame(maxWidth: .infinity)
}
} else {
Text(WidgetL10n.t("widget.no_data", lang))
.font(.caption)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
// MARK: – One month column
private func monthColumn(monthDate: Date,
snapshot: WidgetSnapshot,
primary: Color,
accent: Color,
line: Color) -> some View {
let monthFmt = DateFormatter()
monthFmt.locale = WidgetL10n.locale(lang)
monthFmt.dateFormat = "LLLL"
let name = monthFmt.string(from: monthDate).uppercased()
let start = gridStart(for: monthDate)
let wn = WidgetL10n.t("widget.cw", lang)
return VStack(alignment: .leading, spacing: 1) {
// Month name
Text(name)
.font(.system(size: 9, weight: .bold))
.foregroundStyle(primary)
// Column headers: KW + 7 weekdays
HStack(spacing: 0) {
Text(wn)
.font(.system(size: 6, weight: .bold))
.foregroundStyle(.secondary)
.frame(width: 14, alignment: .center)
ForEach(weekdayHeaders, id: \.self) { h in
Text(h)
.font(.system(size: 6.5, weight: .bold))
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity)
}
}
// Date rows
ForEach(0..<rowCount, id: \.self) { row in
let rowStart = cal.date(byAdding: .day, value: row * 7, to: start)!
let weekNum = cal.component(.weekOfYear, from: rowStart)
let inMonth = cal.isDate(rowStart, equalTo: monthDate, toGranularity: .month)
|| cal.isDate(cal.date(byAdding: .day, value: 6, to: rowStart)!,
equalTo: monthDate, toGranularity: .month)
if inMonth {
HStack(spacing: 0) {
Text("\(weekNum)")
.font(.system(size: 6))
.foregroundStyle(.secondary.opacity(0.6))
.frame(width: 14, alignment: .center)
ForEach(0..<7, id: \.self) { col in
let day = cal.date(byAdding: .day, value: col, to: rowStart)!
dayCell(day, monthDate: monthDate, snapshot: snapshot,
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)
}
}
}
}
// MARK: – Day cell
private func dayCell(_ day: Date,
monthDate: Date,
snapshot: WidgetSnapshot,
primary: Color,
accent: Color) -> some View {
let isToday = cal.isDateInToday(day)
let inMonth = cal.isDate(day, equalTo: monthDate, toGranularity: .month)
let evs = inMonth ? WidgetHelpers.events(for: day, in: snapshot) : []
let isWeekend = { () -> Bool in
let wd = cal.component(.weekday, from: day)
return wd == 1 || wd == 7
}()
return VStack(spacing: 1) {
ZStack {
if isToday { Circle().fill(primary) }
Text("\(cal.component(.day, from: day))")
.font(.system(size: 7.5, weight: isToday ? .bold : .medium))
.foregroundStyle(
isToday ? .white :
!inMonth ? Color.secondary.opacity(0.3) :
isWeekend ? Color.primary.opacity(0.5) :
Color.primary
)
}
.frame(maxWidth: .infinity)
.frame(height: 11)
// Event dots
HStack(spacing: 1) {
ForEach(evs.prefix(3).indices, id: \.self) { i in
Circle()
.fill(Color(widgetHex: evs[i].colorHex))
.frame(width: 2.5, height: 2.5)
}
}
.frame(height: 3)
}
.padding(.bottom, 1)
}
// MARK: – Grid helpers
private func gridStart(for monthDate: Date) -> Date {
let first = cal.date(from: cal.dateComponents([.year, .month], from: monthDate)) ?? monthDate
let weekday = cal.component(.weekday, from: first)
let offset = ((weekday - cal.firstWeekday) + 7) % 7
return cal.date(byAdding: .day, value: -offset, to: first) ?? first
}
}
@@ -0,0 +1,135 @@
import SwiftUI
import WidgetKit
struct TwoWeeksWidgetView: View {
let entry: CalendarrEntry
private var snapshot: WidgetSnapshot? { entry.snapshot }
private var lang: String { snapshot?.language ?? "system" }
private var cal: Calendar {
var c = Calendar(identifier: .gregorian)
c.locale = WidgetL10n.locale(lang)
c.firstWeekday = 2
return c
}
private var weekStart: Date {
cal.date(from: cal.dateComponents([.yearForWeekOfYear, .weekOfYear], from: entry.date)) ?? entry.date
}
private var fortnight: [Date] {
(0..<14).compactMap { cal.date(byAdding: .day, value: $0, to: weekStart) }
}
private var monthHeader: String {
let f = DateFormatter()
f.locale = WidgetL10n.locale(lang)
f.dateFormat = "LLLL yyyy"
return f.string(from: weekStart).uppercased()
}
private var weekdayHeaders: [String] {
let f = DateFormatter(); f.locale = WidgetL10n.locale(lang)
let symbols = f.veryShortWeekdaySymbols ?? cal.veryShortWeekdaySymbols
let start = cal.firstWeekday - 1
return (0..<7).map { symbols[(start + $0) % 7] }
}
var body: some View {
if let s = snapshot {
let primary = Color(widgetHex: s.primaryColorHex)
let accent = Color(widgetHex: s.accentColorHex)
VStack(alignment: .leading, spacing: 2) {
HStack(alignment: .firstTextBaseline, spacing: 4) {
Text(monthHeader.split(separator: " ").first.map(String.init) ?? "")
.font(.system(size: 9, weight: .bold))
.foregroundStyle(primary)
Text(monthHeader.split(separator: " ").dropFirst().joined(separator: " "))
.font(.system(size: 9, weight: .semibold))
}
weekdayRow(accent: accent)
GeometryReader { geo in
let colW = geo.size.width / 7
let rowH = geo.size.height / 2
VStack(spacing: 0) {
ForEach(0..<2, id: \.self) { row in
HStack(spacing: 0) {
ForEach(0..<7, id: \.self) { col in
let day = fortnight[row * 7 + col]
dayCell(day, snapshot: s, primary: primary, accent: accent)
.frame(width: colW, height: rowH)
.overlay(alignment: .trailing) {
if col < 6 {
Rectangle()
.fill(Color(widgetHex: s.lineColorHex).opacity(0.35))
.frame(width: 0.5)
}
}
.overlay(alignment: .top) {
if row == 1 {
Rectangle()
.fill(Color(widgetHex: s.lineColorHex).opacity(0.35))
.frame(height: 0.5)
}
}
}
}
}
}
}
}
} else {
Text(WidgetL10n.t("widget.no_data", lang))
.font(.caption)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
private func weekdayRow(accent: Color) -> some View {
HStack(spacing: 0) {
ForEach(weekdayHeaders, id: \.self) { h in
Text(h)
.font(.system(size: 7, weight: .bold))
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity)
}
}
}
private func dayCell(_ day: Date,
snapshot: WidgetSnapshot,
primary: Color,
accent: Color) -> some View {
let isToday = cal.isDateInToday(day)
let evs = WidgetHelpers.events(for: day, in: snapshot)
return VStack(alignment: .center, spacing: 0.5) {
Text("\(cal.component(.day, from: day))")
.font(.system(size: 8.5, weight: isToday ? .bold : .semibold))
.foregroundStyle(isToday ? Color.white : Color.primary)
.frame(width: 12, height: 12)
.background(isToday ? primary : Color.clear)
.clipShape(Circle())
// 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))
}
if evs.count > 2 {
Text("+\(evs.count - 2)")
.font(.system(size: 6))
.foregroundStyle(accent)
.frame(maxWidth: .infinity, alignment: .leading)
}
Spacer(minLength: 0)
}
.padding(.top, 1)
}
}
+173
View File
@@ -0,0 +1,173 @@
import SwiftUI
import WidgetKit
struct UpNextWidgetView: View {
let entry: CalendarrEntry
private var snapshot: WidgetSnapshot? { entry.snapshot }
private var lang: String { snapshot?.language ?? "system" }
private var cal: Calendar {
var c = Calendar(identifier: .gregorian)
c.locale = WidgetL10n.locale(lang)
c.firstWeekday = 2
return c
}
private var todayEvents: [WidgetEvent] {
guard let s = snapshot else { return [] }
return WidgetHelpers.events(for: entry.date, in: s)
}
/// Mini-month grid: 6 rows × 7 cols starting from the first weekday of the
/// month, padded with neighbouring days where necessary.
private var monthGrid: [Date] {
let firstOfMonth = cal.date(from: cal.dateComponents([.year, .month], from: entry.date)) ?? entry.date
let weekday = cal.component(.weekday, from: firstOfMonth)
let offset = ((weekday - cal.firstWeekday) + 7) % 7
let gridStart = cal.date(byAdding: .day, value: -offset, to: firstOfMonth) ?? firstOfMonth
return (0..<42).compactMap { cal.date(byAdding: .day, value: $0, to: gridStart) }
}
private var weekdayHeaders: [String] {
let f = DateFormatter(); f.locale = WidgetL10n.locale(lang)
let symbols = f.veryShortWeekdaySymbols ?? cal.veryShortWeekdaySymbols
let start = cal.firstWeekday - 1
return (0..<7).map { symbols[(start + $0) % 7] }
}
private var weekdayFmt: DateFormatter {
let f = DateFormatter()
f.locale = WidgetL10n.locale(lang)
f.dateFormat = "EEE"
return f
}
private var monthNameFmt: DateFormatter {
let f = DateFormatter()
f.locale = WidgetL10n.locale(lang)
f.dateFormat = "LLL"
return f
}
private var timeFmt: DateFormatter {
let f = DateFormatter()
f.locale = WidgetL10n.locale(lang)
f.dateFormat = "HH:mm"
return f
}
var body: some View {
if let s = snapshot {
let primary = Color(widgetHex: s.primaryColorHex)
let accent = Color(widgetHex: s.accentColorHex)
HStack(spacing: 8) {
leftPanel(snapshot: s, primary: primary, accent: accent)
.frame(maxWidth: .infinity, alignment: .topLeading)
miniMonth(snapshot: s, primary: primary, accent: accent)
.frame(maxWidth: .infinity, alignment: .topLeading)
}
} else {
Text(WidgetL10n.t("widget.no_data", lang))
.font(.caption)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
private func leftPanel(snapshot: WidgetSnapshot, primary: Color, accent: Color) -> some View {
VStack(alignment: .leading, spacing: 4) {
HStack(alignment: .firstTextBaseline, spacing: 6) {
Text("\(cal.component(.day, from: entry.date))")
.font(.system(size: 17, weight: .bold))
.foregroundStyle(Color.white)
.frame(width: 26, height: 26)
.background(primary)
.clipShape(RoundedRectangle(cornerRadius: 5))
VStack(alignment: .leading, spacing: 0) {
Text(weekdayFmt.string(from: entry.date).uppercased() + ".")
.font(.system(size: 11, weight: .bold))
.foregroundStyle(accent)
Text(monthNameFmt.string(from: entry.date))
.font(.system(size: 11, weight: .medium))
.foregroundStyle(.secondary)
}
}
if todayEvents.isEmpty {
Text(WidgetL10n.t("widget.no_events", lang))
.font(.system(size: 10))
.foregroundStyle(.secondary)
} else {
ForEach(todayEvents.prefix(3)) { ev in
HStack(alignment: .top, spacing: 4) {
Circle()
.fill(Color(widgetHex: ev.colorHex))
.frame(width: 5, height: 5)
.padding(.top, 4)
VStack(alignment: .leading, spacing: 0) {
Text(ev.title)
.font(.system(size: 10, weight: .semibold))
.lineLimit(1)
Text(WidgetTime.range(ev, lang: lang))
.font(.system(size: 9))
.foregroundStyle(.secondary)
}
}
}
}
Spacer(minLength: 0)
}
}
private func miniMonth(snapshot: WidgetSnapshot, primary: Color, accent: Color) -> some View {
VStack(spacing: 1) {
HStack(spacing: 0) {
ForEach(weekdayHeaders, id: \.self) { h in
Text(h)
.font(.system(size: 8, weight: .bold))
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity)
}
}
GeometryReader { geo in
let cellW = geo.size.width / 7
let cellH = geo.size.height / 6
VStack(spacing: 0) {
ForEach(0..<6, id: \.self) { row in
HStack(spacing: 0) {
ForEach(0..<7, id: \.self) { col in
miniDay(monthGrid[row * 7 + col],
snapshot: snapshot,
primary: primary,
accent: accent)
.frame(width: cellW, height: cellH)
}
}
}
}
}
}
}
private func miniDay(_ day: Date, snapshot: WidgetSnapshot, primary: Color, accent: Color) -> some View {
let isToday = cal.isDateInToday(day)
let inMonth = cal.isDate(day, equalTo: entry.date, toGranularity: .month)
let hasEvents = !WidgetHelpers.events(for: day, in: snapshot).isEmpty
return ZStack {
if isToday {
RoundedRectangle(cornerRadius: 3)
.fill(primary)
} else if hasEvents && inMonth {
RoundedRectangle(cornerRadius: 3)
.fill(accent.opacity(0.20))
}
Text("\(cal.component(.day, from: day))")
.font(.system(size: 9, weight: isToday ? .bold : .medium))
.foregroundStyle(
isToday ? Color.white :
inMonth ? Color.primary : Color.secondary.opacity(0.4)
)
}
.padding(0.5)
}
}
@@ -0,0 +1,135 @@
import SwiftUI
import WidgetKit
private let rowHeight: CGFloat = 16
private let dayHeaderHeight: CGFloat = 14
// Show all events of a day (the total-row cap below governs how much fits);
// a low per-day cap previously made busy days collapse to "+N" far too early.
private let maxEventsPerDay: Int = 25
struct UpcomingWidgetView: View {
let entry: CalendarrEntry
@Environment(\.widgetFamily) private var family
private var snapshot: WidgetSnapshot? { entry.snapshot }
private var lang: String { snapshot?.language ?? "system" }
// Fill the available height: extraLarge (iPad) fits far more rows.
private var maxTotalRows: Int { family == .systemExtraLarge ? 40 : 22 }
private var groupedWithLimits: [(Date, [WidgetEvent], Int)] {
guard let s = snapshot else { return [] }
let cal = Calendar.current
let now = entry.date
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)
buckets[key, default: []].append(ev)
}
var result: [(Date, [WidgetEvent], Int)] = []
var totalRows = 0
for date in buckets.keys.sorted() {
let allEventsForDay = buckets[date] ?? []
let eventsToShow = Array(allEventsForDay.prefix(maxEventsPerDay))
let hiddenCount = allEventsForDay.count - eventsToShow.count
// Account for day header + event rows + potential "more" row
let rowsForThisDay = 1 + eventsToShow.count + (hiddenCount > 0 ? 1 : 0)
if totalRows + rowsForThisDay <= maxTotalRows {
result.append((date, eventsToShow, hiddenCount))
totalRows += rowsForThisDay
} else {
break
}
}
return result
}
private var timeFmt: DateFormatter {
let f = DateFormatter()
f.locale = WidgetL10n.locale(lang)
f.dateFormat = "HH:mm"
return f
}
private var dayFmt: DateFormatter {
let f = DateFormatter()
f.locale = WidgetL10n.locale(lang)
f.dateFormat = "EEE d. MMM"
return f
}
var body: some View {
let primary = Color(widgetHex: snapshot?.primaryColorHex ?? "#4285f4")
let accent = Color(widgetHex: snapshot?.accentColorHex ?? "#ea4335")
VStack(alignment: .leading, spacing: 2) {
Text(WidgetL10n.t("widget.upcoming", lang))
.font(.caption.weight(.bold))
.foregroundStyle(primary)
.padding(.bottom, 2)
if snapshot == nil {
Text(WidgetL10n.t("widget.no_data", lang))
.font(.caption)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else if groupedWithLimits.isEmpty {
Text(WidgetL10n.t("widget.no_events", lang))
.font(.caption)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, maxHeight: .infinity)
} else {
VStack(alignment: .leading, spacing: 2) {
ForEach(groupedWithLimits, id: \.0) { day, evs, hiddenCount in
dayHeader(d: day, accent: accent)
ForEach(evs) { ev in
eventRow(ev)
}
if hiddenCount > 0 {
moreRow(count: hiddenCount, accent: accent)
}
}
}
Spacer(minLength: 0)
}
}
}
private func dayHeader(d: Date, accent: Color) -> some View {
let cal = Calendar.current
let isToday = cal.isDateInToday(d)
return Text(dayFmt.string(from: d))
.font(.system(size: 11, weight: .semibold))
.foregroundStyle(isToday ? accent : .secondary)
.frame(height: dayHeaderHeight, alignment: .bottomLeading)
.padding(.top, 1)
}
private func eventRow(_ ev: WidgetEvent) -> some View {
HStack(spacing: 6) {
RoundedRectangle(cornerRadius: 1.5)
.fill(Color(widgetHex: ev.colorHex))
.frame(width: 2.5)
Text(ev.isAllDay ? WidgetL10n.t("widget.allday", lang) : timeFmt.string(from: ev.start))
.font(.system(size: 10))
.foregroundStyle(.secondary)
.frame(width: 38, alignment: .leading)
Text(ev.title)
.font(.system(size: 10, weight: .medium))
.lineLimit(1)
Spacer(minLength: 0)
}
.frame(height: rowHeight)
}
private func moreRow(count: Int, accent: Color) -> some View {
Text(String(format: WidgetL10n.t("widget.more", lang), count))
.font(.system(size: 9, weight: .semibold))
.foregroundStyle(accent)
.frame(height: rowHeight)
}
}
+135
View File
@@ -0,0 +1,135 @@
import SwiftUI
// Local copy of the Color(hex:) initializer, since the widget extension
// is a separate target and cannot import the main app's Color extension.
extension Color {
init(widgetHex hex: String) {
let cleaned = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int: UInt64 = 0
Scanner(string: cleaned).scanHexInt64(&int)
let r, g, b: UInt64
switch cleaned.count {
case 6:
(r, g, b) = ((int >> 16) & 0xFF, (int >> 8) & 0xFF, int & 0xFF)
default:
(r, g, b) = (0, 0, 0)
}
self.init(red: Double(r) / 255, green: Double(g) / 255, blue: Double(b) / 255)
}
}
/// 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
if stored == "de" || stored == "en" { lang = stored }
else {
let pref = Locale.preferredLanguages.first ?? "en"
lang = pref.lowercased().hasPrefix("de") ? "de" : "en"
}
return strings[lang]?[key] ?? strings["en"]?[key] ?? key
}
static func locale(_ stored: String) -> Locale {
let lang: String
if stored == "de" || stored == "en" { lang = stored }
else {
let pref = Locale.preferredLanguages.first ?? "en"
lang = pref.lowercased().hasPrefix("de") ? "de" : "en"
}
return Locale(identifier: lang)
}
private static let strings: [String: [String: String]] = [
"de": [
"widget.today": "Heute",
"widget.tomorrow": "Morgen",
"widget.no_events": "Keine Termine",
"widget.allday": "Ganztägig",
"widget.more": "+%d weitere",
"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 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",
"widget.display.twoweeks_desc": "Zwei-Wochen-Raster mit Terminen.",
"widget.display.threedays_title": "Drei Tage",
"widget.display.threedays_desc": "Drei-Tages-Ansicht mit Terminen.",
"widget.display.upnext_title": "Up Next + Kalender",
"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": "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",
"widget.tomorrow": "Tomorrow",
"widget.no_events": "No events",
"widget.allday": "All-day",
"widget.more": "+%d more",
"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 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",
"widget.display.twoweeks_desc": "Two-week grid with events.",
"widget.display.threedays_title": "Three Days",
"widget.display.threedays_desc": "Three-day view with events.",
"widget.display.upnext_title": "Up Next + Calendar",
"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": "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."
]
]
}