Requiring both ends to sit exactly on midnight was too strict. A holiday stored as the 16th at 00:00 through the 21st at 01:00 slipped through and rendered as a one-hour slot. Starting on a day boundary and running into a later day is enough to call it all-day. The agenda row now names the weekday on both sides when an event crosses a day. Showing only "00:00 – 01:00" for something that runs from Wednesday to Monday is true and useless. The timezone handling above this is deliberately untouched: timed events show the right time, so the UTC assumption for timestamps without an offset is not what produced the odd hour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
90 lines
3.5 KiB
Swift
90 lines
3.5 KiB
Swift
import SwiftUI
|
||
import CalendarrCore
|
||
|
||
/// One event: a colour bar for the calendar, the time range, the title.
|
||
struct WatchEventRow: View {
|
||
let event: SnapshotEvent
|
||
let language: String
|
||
|
||
var body: some View {
|
||
HStack(alignment: .top, spacing: 7) {
|
||
RoundedRectangle(cornerRadius: 2)
|
||
.fill(Color(watchHex: event.colorHex))
|
||
.frame(width: 3)
|
||
VStack(alignment: .leading, spacing: 1) {
|
||
Text(WatchEventFormat.timeRange(event, language: language))
|
||
.font(.system(size: 11))
|
||
.foregroundStyle(.secondary)
|
||
Text(event.title)
|
||
.font(.system(size: 14))
|
||
.lineLimit(2)
|
||
}
|
||
Spacer(minLength: 0)
|
||
}
|
||
.padding(.vertical, 2)
|
||
.fixedSize(horizontal: false, vertical: true)
|
||
}
|
||
}
|
||
|
||
extension Color {
|
||
/// Local copy of the hex initialiser: `CalendarrCore` is Foundation-only by
|
||
/// design, so colour parsing cannot live there.
|
||
init(watchHex hex: String) {
|
||
let cleaned = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
|
||
var value: UInt64 = 0
|
||
Scanner(string: cleaned).scanHexInt64(&value)
|
||
guard cleaned.count == 6 else { self = .gray; return }
|
||
self.init(red: Double((value >> 16) & 0xFF) / 255,
|
||
green: Double((value >> 8) & 0xFF) / 255,
|
||
blue: Double(value & 0xFF) / 255)
|
||
}
|
||
}
|
||
|
||
enum WatchEventFormat {
|
||
static func timeRange(_ event: SnapshotEvent, language: String,
|
||
calendar: Calendar = Calendar(identifier: .gregorian)) -> String {
|
||
if event.isAllDay { return WatchL10n.t("watch.allday", language) }
|
||
|
||
let time = DateFormatter()
|
||
time.locale = WatchL10n.locale(language)
|
||
time.dateFormat = "HH:mm"
|
||
|
||
let start = time.string(from: event.start)
|
||
guard event.end > event.start else { return start }
|
||
|
||
// Without the weekday, an event running from Wednesday to Monday reads
|
||
// as a one-hour slot: "00:00 – 01:00" is true and useless.
|
||
if calendar.startOfDay(for: event.end) > calendar.startOfDay(for: event.start) {
|
||
let day = DateFormatter()
|
||
day.locale = WatchL10n.locale(language)
|
||
day.setLocalizedDateFormatFromTemplate("EEE")
|
||
return "\(day.string(from: event.start)) \(start) – "
|
||
+ "\(day.string(from: event.end)) \(time.string(from: event.end))"
|
||
}
|
||
return "\(start) – \(time.string(from: event.end))"
|
||
}
|
||
|
||
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)
|
||
}
|
||
|
||
/// "Heute", "Morgen", else "Do 11. Sep".
|
||
static func dayHeader(_ day: Date, language: String,
|
||
calendar: Calendar, now: Date = Date()) -> String {
|
||
if calendar.isDate(day, inSameDayAs: now) {
|
||
return WatchL10n.t("watch.today", language)
|
||
}
|
||
if let tomorrow = calendar.date(byAdding: .day, value: 1, to: calendar.startOfDay(for: now)),
|
||
calendar.isDate(day, inSameDayAs: tomorrow) {
|
||
return WatchL10n.t("watch.tomorrow", language)
|
||
}
|
||
let formatter = DateFormatter()
|
||
formatter.locale = WatchL10n.locale(language)
|
||
formatter.setLocalizedDateFormatFromTemplate("EEE d MMM")
|
||
return formatter.string(from: day)
|
||
}
|
||
}
|