import SwiftUI import WidgetKit struct TimetableWeekView: View { let entry: TimetableEntry @Environment(\.widgetPalette) private var palette var body: some View { ZStack { if !entry.isLoggedIn { placeholder("Bitte einloggen, um den Stundenplan zu laden") } else if let data = entry.data { content(data: data) } else { placeholder("Lade…") } } .background(MarianumWatermark()) } @ViewBuilder private func content(data: WidgetTimetableData) -> some View { let cal = Calendar.current let days = (0..<5).map { offset in cal.date(byAdding: .day, value: offset, to: data.anchorDate) ?? data.anchorDate } let weekLessons = data.lessons.filter { lesson in days.contains { cal.isDate(lesson.start, inSameDayAs: $0) } } let periods = trimTrailingPeriods(data.periods, lessons: weekLessons) VStack(alignment: .leading, spacing: 0) { header(data: data) dayHeaderRow(days: days) .padding(.top, GRID_MARGIN_TOP) GeometryReader { geo in let height = resolveHourHeight(forGridHeight: geo.size.height, periods: periods) HStack(alignment: .top, spacing: 0) { PeriodRuler(periods: periods, hourHeight: height) columnDivider ForEach(days.indices, id: \.self) { index in TimeGridView( lessons: weekLessons.filter { cal.isDate($0.start, inSameDayAs: days[index]) }, periods: periods, hourHeight: height, holiday: holiday(on: days[index], data: data), isToday: cal.isDate(days[index], inSameDayAs: entry.date) ) if index < 4 { columnDivider } } } } .padding(.top, 2) } } private func holiday(on day: Date, data: WidgetTimetableData) -> WidgetDayInfo? { data.days?.first { $0.isHoliday && Calendar.current.isDate($0.date, inSameDayAs: day) } } private var columnDivider: some View { Rectangle() .fill(palette.divider) .frame(width: 1) } private func header(data: WidgetTimetableData) -> some View { let cal = Calendar.current let week = cal.component(.weekOfYear, from: data.anchorDate) let endDate = cal.date(byAdding: .day, value: 4, to: data.anchorDate) ?? data.anchorDate return HStack(spacing: 6) { Text("KW \(week) · \(shortDate(data.anchorDate))–\(shortDate(endDate))") .font(.system(size: 14, weight: .bold)) .foregroundStyle(palette.textPrimary) .lineLimit(1) .minimumScaleFactor(0.65) .frame(maxWidth: .infinity, alignment: .leading) Text("Stand: \(freshnessLabel(for: data.fetchedAt, relativeTo: entry.date))") .font(.system(size: 10)) .foregroundStyle(palette.textSecondary) .lineLimit(1) .fixedSize() } .frame(height: TITLE_ROW_HEIGHT) } /// Mirrors the in-app _DayHeaderCell: weekday in caps, day number below, /// today inside an accent circle. private func dayHeaderRow(days: [Date]) -> some View { // The fixed height keeps `columnDivider`'s vertically-flexible // Rectangle from stealing space from the GeometryReader below. HStack(spacing: 0) { Spacer().frame(width: RULER_WIDTH) columnDivider ForEach(days.indices, id: \.self) { index in dayHeaderCell(days[index]) .frame(maxWidth: .infinity) if index < 4 { columnDivider } } } .frame(maxWidth: .infinity, minHeight: WEEK_HEADER_HEIGHT, maxHeight: WEEK_HEADER_HEIGHT) } private func dayHeaderCell(_ day: Date) -> some View { // `entry.date`, not `Date()` — WidgetKit archives entries ahead of // time, the midnight entry has to mark its own day. let isToday = Calendar.current.isDate(day, inSameDayAs: entry.date) return VStack(spacing: 1) { Text(WidgetDateFormatters.weekdayShort.string(from: day).uppercased()) .font(.system(size: 9, weight: .medium)) .foregroundStyle(isToday ? palette.accent : palette.textSecondary) .lineLimit(1) Text("\(Calendar.current.component(.day, from: day))") .font(.system(size: 11, weight: isToday ? .bold : .regular)) .foregroundStyle(isToday ? palette.onAccent : palette.textPrimary) .lineLimit(1) .frame(width: 18, height: 18) .background(Circle().fill(isToday ? palette.accent : Color.clear)) } } private func placeholder(_ message: String) -> some View { VStack(spacing: 4) { Text("Marianum Stundenplan") .font(.system(size: 14, weight: .semibold)) .foregroundStyle(palette.textPrimary) Text(message) .font(.caption) .foregroundStyle(palette.textSecondary) .multilineTextAlignment(.center) } .frame(maxWidth: .infinity, maxHeight: .infinity) } }