Files
Calendarr-IOS/CalendarrWidgets/LockScreenWidgetViews.swift
Scarriffle b1e0cf1fdc WIP: Widget-, Sync- & Event-Editor-Änderungen
Zwischenstand vor den Sharing/Gruppen/Import-Export-Features (gesichert,
damit die neuen Features sauber darauf aufbauen).
2026-05-31 19:22:12 +02:00

289 lines
9.5 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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")
}
}