Archived
The position asked for is the bezel slot: a circular complication plus a label that arcs across the top, where the tick marks otherwise are. ClockKit called that family graphicBezel; in WidgetKit it is an accessoryCircular widget with .widgetLabel attached. Nothing was attached, so that arc stayed empty while Apple's Calendar fills it — which is the difference that was visible on the watch and invisible in every check made here. All three circular kinds now carry it. Faces without a bezel slot ignore the label, so there is no cost to always providing it. cornerLabel becomes eventLabel: the same text serves the corner edge and the bezel arc, and the old name claimed otherwise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
106 lines
5.3 KiB
Swift
106 lines
5.3 KiB
Swift
import Foundation
|
|
import CalendarrCore
|
|
|
|
enum WatchComplicationSupport {
|
|
/// The first event that has not ended yet — including one running now,
|
|
/// which is what "what is next" means when you are already in a meeting.
|
|
/// Matches `WatchPushPolicy.nextEventIdentity`, deliberately.
|
|
static func nextEvent(in snapshot: CalendarrSnapshot?, at now: Date) -> SnapshotEvent? {
|
|
snapshot?.events.filter { $0.end > now }.min { $0.start < $1.start }
|
|
}
|
|
|
|
static func upcoming(in snapshot: CalendarrSnapshot?, at now: Date, limit: Int) -> [SnapshotEvent] {
|
|
guard let snapshot else { return [] }
|
|
return Array(snapshot.events.filter { $0.end > now }.sorted { $0.start < $1.start }.prefix(limit))
|
|
}
|
|
|
|
static func todayCount(in snapshot: CalendarrSnapshot?, at now: Date,
|
|
calendar: Calendar = Calendar(identifier: .gregorian)) -> Int {
|
|
guard let snapshot else { return 0 }
|
|
let dayStart = calendar.startOfDay(for: now)
|
|
guard let dayEnd = calendar.date(byAdding: .day, value: 1, to: dayStart) else { return 0 }
|
|
return snapshot.events.filter { $0.start < dayEnd && $0.end > dayStart }.count
|
|
}
|
|
|
|
/// 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 {
|
|
event.start > now ? event.start : event.end
|
|
}
|
|
|
|
/// A countdown narrow enough for a corner or a circular gauge: at most
|
|
/// three characters. `Text(style: .timer)` would be live and free of
|
|
/// timeline entries, but renders `1:23:45` and overlaps whatever sits
|
|
/// beside it — so the timeline carries minute-granular entries instead and
|
|
/// this is recomputed per entry.
|
|
static func compactCountdown(to target: Date, at now: Date, language: String) -> String {
|
|
let seconds = target.timeIntervalSince(now)
|
|
guard seconds > 0 else { return WatchL10n.t("watch.now", language) }
|
|
let minutes = Int(seconds / 60)
|
|
if minutes < 1 { return WatchL10n.t("watch.now", language) }
|
|
if minutes < 60 { return "\(minutes)m" }
|
|
let hours = minutes / 60
|
|
if hours < 24 { return "\(hours)h" }
|
|
return "\(hours / 24)d"
|
|
}
|
|
|
|
/// How full a draining ring should be, over a two-hour lead window.
|
|
static func gaugeFraction(to target: Date, at now: Date) -> Double {
|
|
let window: TimeInterval = 2 * 3600
|
|
return min(1, max(0, target.timeIntervalSince(now) / window))
|
|
}
|
|
|
|
/// How much of today is still ahead. Drains through the day, matching the
|
|
/// countdown ring rather than inventing a second direction.
|
|
static func dayRemainingFraction(at now: Date,
|
|
calendar: Calendar = Calendar(identifier: .gregorian)) -> Double {
|
|
let dayStart = calendar.startOfDay(for: now)
|
|
guard let dayEnd = calendar.date(byAdding: .day, value: 1, to: dayStart) else { return 0 }
|
|
let total = dayEnd.timeIntervalSince(dayStart)
|
|
guard total > 0 else { return 0 }
|
|
return min(1, max(0, dayEnd.timeIntervalSince(now) / total))
|
|
}
|
|
|
|
/// The share of today's appointments still ahead. Zero when there are none,
|
|
/// which leaves the ring empty rather than full — an empty day should not
|
|
/// look like an untouched one.
|
|
static func todayRemainingFraction(in snapshot: CalendarrSnapshot?, at now: Date,
|
|
calendar: Calendar = Calendar(identifier: .gregorian)) -> Double {
|
|
guard let snapshot else { return 0 }
|
|
let dayStart = calendar.startOfDay(for: now)
|
|
guard let dayEnd = calendar.date(byAdding: .day, value: 1, to: dayStart) else { return 0 }
|
|
let today = snapshot.events.filter { $0.start < dayEnd && $0.end > dayStart }
|
|
guard !today.isEmpty else { return 0 }
|
|
let ahead = today.filter { $0.end > now }.count
|
|
return min(1, max(0, Double(ahead) / Double(today.count)))
|
|
}
|
|
|
|
static func time(_ date: Date, language: String) -> String {
|
|
let formatter = DateFormatter()
|
|
formatter.locale = WatchL10n.locale(language)
|
|
formatter.dateFormat = "HH:mm"
|
|
return formatter.string(from: date)
|
|
}
|
|
|
|
static func monthAbbreviation(_ date: Date, language: String) -> String {
|
|
let formatter = DateFormatter()
|
|
formatter.locale = WatchL10n.locale(language)
|
|
formatter.dateFormat = "LLL"
|
|
return formatter.string(from: date).uppercased()
|
|
}
|
|
|
|
/// The curved label that accompanies a circular or corner complication:
|
|
/// time plus title, or a plain "no events".
|
|
///
|
|
/// On a corner this follows the screen edge. On a face with a bezel slot —
|
|
/// Infograph — the same label arcs across the top, where the tick marks
|
|
/// otherwise are. That is the position ClockKit called `graphicBezel`, and
|
|
/// in WidgetKit it is reached by attaching `.widgetLabel` to an
|
|
/// `accessoryCircular` widget.
|
|
static func eventLabel(_ event: SnapshotEvent?, at now: Date, language: String) -> String {
|
|
guard let event else { return WatchL10n.t("watch.no_events", language) }
|
|
if event.isAllDay { return event.title }
|
|
return "\(time(event.start, language: language)) \(event.title)"
|
|
}
|
|
}
|