diff --git a/Calendarr iOS/Models/CalEvent.swift b/Calendarr iOS/Models/CalEvent.swift index 94ae950..b5338d3 100644 --- a/Calendarr iOS/Models/CalEvent.swift +++ b/Calendarr iOS/Models/CalEvent.swift @@ -91,7 +91,14 @@ struct CalEvent: Identifiable, Hashable { else if let n = json["id"] as? Int { id = String(n) } else { return nil } - let isAllDay = json["allDay"] as? Bool ?? false + // A date-only string is an all-day event whether or not the flag says + // so — `parseDate` already treats it that way. Trusting the flag alone + // is what put "00:00" in front of birthdays everywhere they are shown. + let startIsDateOnly: Bool = { + let clean = startStr.trimmingCharacters(in: .whitespaces) + return clean.count == 10 && !clean.contains("T") + }() + let isAllDay = (json["allDay"] as? Bool ?? false) || startIsDateOnly let startDate = parseDate(startStr, allDay: isAllDay) let endDate = parseDate(endStr, allDay: isAllDay) guard let s = startDate, let e = endDate else { return nil } diff --git a/CalendarrWatchComplications/SmallComplicationViews.swift b/CalendarrWatchComplications/SmallComplicationViews.swift index acb7874..4ad708a 100644 --- a/CalendarrWatchComplications/SmallComplicationViews.swift +++ b/CalendarrWatchComplications/SmallComplicationViews.swift @@ -67,7 +67,10 @@ struct CountdownComplicationView: View { } } -/// Day number and month. No appointment information — a date replacement. +/// Weekday and day number, the way a date is actually read. The ring appears +/// only while an appointment is running, and then shows how far through it we +/// are — an empty track around an idle day is noise, so there is no Gauge at +/// all in that case. struct DateComplicationView: View { let entry: WatchComplicationEntry @Environment(\.widgetFamily) private var family @@ -76,26 +79,47 @@ struct DateComplicationView: View { Calendar(identifier: .gregorian).component(.day, from: entry.date) } + private var weekday: String { + WatchComplicationSupport.weekdayAbbreviation(entry.date, language: entry.language) + } + var body: some View { switch family { case .accessoryInline: - Label("\(WatchComplicationSupport.monthAbbreviation(entry.date, language: entry.language)) \(day)", - systemImage: "calendar") + Label("\(weekday) \(day)", systemImage: "calendar") default: - // A closed gauge, filling the slot edge to edge — the WidgetKit - // equivalent of ClockKit's ClosedGaugeText, which no longer exists. - // The ring is not decoration: it drains as the day does. - Gauge(value: WatchComplicationSupport.dayRemainingFraction(at: entry.date)) { - Text(WatchComplicationSupport.monthAbbreviation(entry.date, language: entry.language)) + circular + } + } + + @ViewBuilder + private var circular: some View { + if let running = WatchComplicationSupport.runningEvent(in: entry.snapshot, at: entry.date) { + Gauge(value: WatchComplicationSupport.progress(of: running, at: entry.date)) { + Text(weekday) } currentValueLabel: { - Text("\(day)") - .font(.system(size: 20, weight: .semibold)) - .lineLimit(1) - .minimumScaleFactor(0.6) + dateStack } .gaugeStyle(.accessoryCircularCapacity) - .widgetAccentable() .eventBezelLabel(entry) + } else { + ZStack { + AccessoryWidgetBackground() + dateStack + } + .eventBezelLabel(entry) + } + } + + private var dateStack: some View { + VStack(spacing: -2) { + Text(weekday) + .font(.system(size: 11, weight: .semibold)) + .widgetAccentable() + Text("\(day)") + .font(.system(size: 23, weight: .semibold)) + .lineLimit(1) + .minimumScaleFactor(0.7) } } } diff --git a/CalendarrWatchComplications/WatchComplicationSupport.swift b/CalendarrWatchComplications/WatchComplicationSupport.swift index d507fb6..e2e1efa 100644 --- a/CalendarrWatchComplications/WatchComplicationSupport.swift +++ b/CalendarrWatchComplications/WatchComplicationSupport.swift @@ -22,6 +22,32 @@ enum WatchComplicationSupport { return snapshot.events.filter { $0.start < dayEnd && $0.end > dayStart }.count } + /// The appointment happening right now, if there is one. All-day events are + /// excluded on purpose: one of those is "running" for twenty-four hours and + /// would hold the ring at some arbitrary fraction all day, which says + /// nothing. + static func runningEvent(in snapshot: CalendarrSnapshot?, at now: Date) -> SnapshotEvent? { + snapshot?.events + .filter { !$0.isAllDay && $0.start <= now && $0.end > now } + .min { $0.end < $1.end } + } + + /// How far through an appointment we are, 0 at its start and 1 at its end. + static func progress(of event: SnapshotEvent, at now: Date) -> Double { + let total = event.end.timeIntervalSince(event.start) + guard total > 0 else { return 0 } + return min(1, max(0, now.timeIntervalSince(event.start) / total)) + } + + static func weekdayAbbreviation(_ date: Date, language: String) -> String { + let formatter = DateFormatter() + formatter.locale = WatchL10n.locale(language) + formatter.dateFormat = "EEE" + return formatter.string(from: date) + .replacingOccurrences(of: ".", with: "") + .uppercased() + } + /// What a countdown counts to: the start if it has not begun, otherwise the /// end. Counting up from a start time that has passed reads as broken. static func countdownTarget(_ event: SnapshotEvent, at now: Date) -> Date {