Widget anpassung vorbereitung
This commit is contained in:
53
CalendarrWidgets/CalendarrTimelineProvider.swift
Normal file
53
CalendarrWidgets/CalendarrTimelineProvider.swift
Normal file
@@ -0,0 +1,53 @@
|
||||
import WidgetKit
|
||||
|
||||
struct CalendarrEntry: TimelineEntry {
|
||||
let date: Date
|
||||
let snapshot: WidgetSnapshot?
|
||||
}
|
||||
|
||||
struct CalendarrTimelineProvider: TimelineProvider {
|
||||
func placeholder(in context: Context) -> CalendarrEntry {
|
||||
CalendarrEntry(date: .now, snapshot: WidgetStore.read())
|
||||
}
|
||||
|
||||
func getSnapshot(in context: Context, completion: @escaping (CalendarrEntry) -> Void) {
|
||||
completion(CalendarrEntry(date: .now, snapshot: WidgetStore.read()))
|
||||
}
|
||||
|
||||
func getTimeline(in context: Context, completion: @escaping (Timeline<CalendarrEntry>) -> Void) {
|
||||
let snapshot = WidgetStore.read()
|
||||
let now = Date()
|
||||
|
||||
// Provide one entry per hour for the next 24h so the widget keeps
|
||||
// re-rendering as time progresses (past events drop off, "now" 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: snapshot))
|
||||
}
|
||||
// Ask iOS to refresh in 30 min to pick up any new data the app wrote.
|
||||
let refreshAt = Calendar.current.date(byAdding: .minute, value: 30, to: now) ?? now
|
||||
completion(Timeline(entries: entries, policy: .after(refreshAt)))
|
||||
}
|
||||
}
|
||||
|
||||
// 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 }
|
||||
}
|
||||
}
|
||||
10
CalendarrWidgets/CalendarrWidgets.entitlements
Normal file
10
CalendarrWidgets/CalendarrWidgets.entitlements
Normal file
@@ -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>
|
||||
141
CalendarrWidgets/CalendarrWidgets.swift
Normal file
141
CalendarrWidgets/CalendarrWidgets.swift
Normal file
@@ -0,0 +1,141 @@
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct CalendarrWidgetBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
TodayWidget()
|
||||
TwoDaysWidget()
|
||||
ThreeDaysWidget()
|
||||
ThisWeekWidget()
|
||||
TwoWeeksWidget()
|
||||
UpcomingWidget()
|
||||
UpNextWidget()
|
||||
}
|
||||
}
|
||||
|
||||
// Shared chrome modifier — keeps every 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 {
|
||||
StaticConfiguration(kind: kind, 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 {
|
||||
StaticConfiguration(kind: kind, 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 {
|
||||
StaticConfiguration(kind: kind, 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 {
|
||||
StaticConfiguration(kind: kind, 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 {
|
||||
StaticConfiguration(kind: kind, 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 {
|
||||
StaticConfiguration(kind: kind, 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 {
|
||||
StaticConfiguration(kind: kind, 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])
|
||||
}
|
||||
}
|
||||
29
CalendarrWidgets/Info.plist
Normal file
29
CalendarrWidgets/Info.plist
Normal 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>
|
||||
117
CalendarrWidgets/ThisWeekWidgetView.swift
Normal file
117
CalendarrWidgets/ThisWeekWidgetView.swift
Normal file
@@ -0,0 +1,117 @@
|
||||
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 // Monday
|
||||
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: 2) {
|
||||
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))
|
||||
}
|
||||
GeometryReader { geo in
|
||||
let colW = geo.size.width / 7
|
||||
HStack(spacing: 0) {
|
||||
ForEach(Array(weekDays.enumerated()), id: \.offset) { idx, day in
|
||||
dayColumn(day, snapshot: s, primary: primary, accent: accent)
|
||||
.frame(width: colW)
|
||||
.overlay(alignment: .trailing) {
|
||||
if idx < 6 {
|
||||
Rectangle()
|
||||
.fill(Color(widgetHex: s.lineColorHex).opacity(0.35))
|
||||
.frame(width: 0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} 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(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: 15, height: 15)
|
||||
.background(isToday ? primary : Color.clear)
|
||||
.clipShape(Circle())
|
||||
ForEach(evs.prefix(2)) { ev in
|
||||
eventPill(ev)
|
||||
}
|
||||
if evs.count > 2 {
|
||||
Text("+\(evs.count - 2)")
|
||||
.font(.system(size: 7))
|
||||
.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))
|
||||
}
|
||||
}
|
||||
131
CalendarrWidgets/ThreeDaysWidgetView.swift
Normal file
131
CalendarrWidgets/ThreeDaysWidgetView.swift
Normal file
@@ -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(ev.isAllDay ? WidgetL10n.t("widget.allday", lang) : timeFmt.string(from: ev.start))
|
||||
.font(.system(size: 8))
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.leading, 5)
|
||||
}
|
||||
.padding(.horizontal, 3)
|
||||
}
|
||||
}
|
||||
84
CalendarrWidgets/TodayWidgetView.swift
Normal file
84
CalendarrWidgets/TodayWidgetView.swift
Normal file
@@ -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(ev.isAllDay ? WidgetL10n.t("widget.allday", lang) : timeFmt.string(from: ev.start))
|
||||
.font(.caption2)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
109
CalendarrWidgets/TwoDaysWidgetView.swift
Normal file
109
CalendarrWidgets/TwoDaysWidgetView.swift
Normal file
@@ -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(ev.isAllDay ? WidgetL10n.t("widget.allday", lang) : timeFmt.string(from: ev.start))
|
||||
.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)
|
||||
}
|
||||
}
|
||||
132
CalendarrWidgets/TwoWeeksWidgetView.swift
Normal file
132
CalendarrWidgets/TwoWeeksWidgetView.swift
Normal file
@@ -0,0 +1,132 @@
|
||||
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 3 colored dots
|
||||
HStack(spacing: 1) {
|
||||
ForEach(evs.prefix(3).indices, id: \.self) { i in
|
||||
Circle()
|
||||
.fill(Color(widgetHex: evs[i].colorHex))
|
||||
.frame(width: 3, height: 3)
|
||||
}
|
||||
}
|
||||
.frame(height: 3)
|
||||
if evs.count > 3 {
|
||||
Text("+\(evs.count - 3)")
|
||||
.font(.system(size: 6))
|
||||
.foregroundStyle(accent)
|
||||
}
|
||||
Spacer(minLength: 0)
|
||||
}
|
||||
.padding(.top, 1)
|
||||
}
|
||||
}
|
||||
173
CalendarrWidgets/UpNextWidgetView.swift
Normal file
173
CalendarrWidgets/UpNextWidgetView.swift
Normal 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(ev.isAllDay ? WidgetL10n.t("widget.allday", lang) : timeFmt.string(from: ev.start))
|
||||
.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)
|
||||
}
|
||||
}
|
||||
130
CalendarrWidgets/UpcomingWidgetView.swift
Normal file
130
CalendarrWidgets/UpcomingWidgetView.swift
Normal file
@@ -0,0 +1,130 @@
|
||||
import SwiftUI
|
||||
import WidgetKit
|
||||
|
||||
private let rowHeight: CGFloat = 16
|
||||
private let dayHeaderHeight: CGFloat = 14
|
||||
private let maxEventsPerDay: Int = 3
|
||||
private let maxTotalRows: Int = 15
|
||||
|
||||
struct UpcomingWidgetView: View {
|
||||
let entry: CalendarrEntry
|
||||
|
||||
private var snapshot: WidgetSnapshot? { entry.snapshot }
|
||||
private var lang: String { snapshot?.language ?? "system" }
|
||||
|
||||
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: 5, 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)
|
||||
}
|
||||
}
|
||||
90
CalendarrWidgets/WidgetSupport.swift
Normal file
90
CalendarrWidgets/WidgetSupport.swift
Normal file
@@ -0,0 +1,90 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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 5 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 5 Tage",
|
||||
"widget.display.upcoming_desc": "Termine der nächsten 5 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."
|
||||
],
|
||||
"en": [
|
||||
"widget.today": "Today",
|
||||
"widget.tomorrow": "Tomorrow",
|
||||
"widget.no_events": "No events",
|
||||
"widget.allday": "All-day",
|
||||
"widget.more": "+%d more",
|
||||
"widget.upcoming": "Next 5 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 5 days",
|
||||
"widget.display.upcoming_desc": "Events for the next 5 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."
|
||||
]
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user