Widget änderungen, sync änderungen

This commit is contained in:
Scarriffle
2026-05-28 21:43:18 +02:00
parent 4125bfc728
commit e71fd7512f
9 changed files with 726 additions and 57 deletions

View File

@@ -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.accentColor.opacity(0.5)
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)
}
}