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>
114 lines
4.4 KiB
Swift
114 lines
4.4 KiB
Swift
import SwiftUI
|
|
import WidgetKit
|
|
import CalendarrCore
|
|
|
|
/// Time, title, location. The variant that answers the actual question.
|
|
struct NextEventComplicationView: View {
|
|
let entry: WatchComplicationEntry
|
|
@Environment(\.widgetFamily) private var family
|
|
|
|
var body: some View {
|
|
let next = WatchComplicationSupport.nextEvent(in: entry.snapshot, at: entry.date)
|
|
switch family {
|
|
case .accessoryInline:
|
|
inline(next)
|
|
default:
|
|
rectangular(next)
|
|
}
|
|
}
|
|
|
|
@ViewBuilder
|
|
private func rectangular(_ next: SnapshotEvent?) -> some View {
|
|
VStack(alignment: .leading, spacing: 1) {
|
|
if let next {
|
|
Text(next.isAllDay
|
|
? WatchL10n.t("watch.allday", entry.language)
|
|
: WatchComplicationSupport.time(next.start, language: entry.language))
|
|
.font(.system(size: 12, weight: .semibold))
|
|
.widgetAccentable()
|
|
Text(next.title)
|
|
.font(.system(size: 15))
|
|
.lineLimit(1)
|
|
if !next.location.isEmpty {
|
|
Text(next.location)
|
|
.font(.system(size: 11))
|
|
.lineLimit(1)
|
|
}
|
|
} else {
|
|
Image(systemName: "calendar").font(.system(size: 13)).widgetAccentable()
|
|
Text(WatchL10n.t("watch.no_events", entry.language)).font(.system(size: 13))
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
}
|
|
|
|
private func inline(_ next: SnapshotEvent?) -> some View {
|
|
let text: String = {
|
|
guard let next else { return WatchL10n.t("watch.no_events", entry.language) }
|
|
if next.isAllDay { return next.title }
|
|
return "\(WatchComplicationSupport.time(next.start, language: entry.language)) \(next.title)"
|
|
}()
|
|
return Label(text, systemImage: "calendar")
|
|
}
|
|
}
|
|
|
|
/// The next two, so back-to-back appointments are visible at once.
|
|
struct NextTwoComplicationView: View {
|
|
let entry: WatchComplicationEntry
|
|
|
|
var body: some View {
|
|
let events = WatchComplicationSupport.upcoming(in: entry.snapshot, at: entry.date, limit: 2)
|
|
VStack(alignment: .leading, spacing: 3) {
|
|
if events.isEmpty {
|
|
Image(systemName: "calendar").font(.system(size: 13)).widgetAccentable()
|
|
Text(WatchL10n.t("watch.no_events", entry.language)).font(.system(size: 13))
|
|
} else {
|
|
ForEach(events) { event in
|
|
HStack(spacing: 5) {
|
|
Text(event.isAllDay
|
|
? WatchL10n.t("watch.allday", entry.language)
|
|
: WatchComplicationSupport.time(event.start, language: entry.language))
|
|
.font(.system(size: 12, weight: .semibold))
|
|
.widgetAccentable()
|
|
Text(event.title)
|
|
.font(.system(size: 13))
|
|
.lineLimit(1)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
}
|
|
}
|
|
|
|
/// Weekday and date plus the next appointment, so one slot does both jobs.
|
|
struct DatePlusEventComplicationView: View {
|
|
let entry: WatchComplicationEntry
|
|
|
|
var body: some View {
|
|
let next = WatchComplicationSupport.nextEvent(in: entry.snapshot, at: entry.date)
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(dateLine)
|
|
.font(.system(size: 11, weight: .semibold))
|
|
.widgetAccentable()
|
|
if let next {
|
|
Text(next.isAllDay
|
|
? next.title
|
|
: "\(WatchComplicationSupport.time(next.start, language: entry.language)) \(next.title)")
|
|
.font(.system(size: 14))
|
|
.lineLimit(1)
|
|
} else {
|
|
Text(WatchL10n.t("watch.no_events", entry.language)).font(.system(size: 14))
|
|
}
|
|
}
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
}
|
|
|
|
private var dateLine: String {
|
|
let formatter = DateFormatter()
|
|
formatter.locale = WatchL10n.locale(entry.language)
|
|
formatter.setLocalizedDateFormatFromTemplate("EEE d MMMM")
|
|
return formatter.string(from: entry.date).uppercased()
|
|
}
|
|
}
|