Files
Calendarr-IOS/CalendarrWidgets/NowNextWidgetView.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

163 lines
6.2 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
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)
}
}