162 lines
6.0 KiB
Swift
162 lines
6.0 KiB
Swift
import SwiftUI
|
||
import WidgetKit
|
||
|
||
struct TimetableWeekView: View {
|
||
let entry: TimetableEntry
|
||
|
||
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())
|
||
.widgetThemeOverride(entry.themeMode)
|
||
}
|
||
|
||
@ViewBuilder
|
||
private func content(data: WidgetTimetableData) -> some View {
|
||
VStack(alignment: .leading, spacing: 4) {
|
||
header(data: data)
|
||
dayHeaderRow(data: data)
|
||
GeometryReader { geo in
|
||
let totalMin = CGFloat(data.periods.last?.virtualEndMinutes ?? FALLBACK_VIRTUAL_MINUTES)
|
||
let hourHeight = max(
|
||
MIN_HOUR_HEIGHT,
|
||
min(MAX_HOUR_HEIGHT, geo.size.height / max(totalMin, 60) * 60)
|
||
)
|
||
let dayColumnWidth = (geo.size.width - 28 - 4) / 5
|
||
let subjectOnly = dayColumnWidth < 70
|
||
HStack(alignment: .top, spacing: 0) {
|
||
timeLabelsColumn(hourHeight: hourHeight, periods: data.periods)
|
||
.frame(width: 28, alignment: .topTrailing)
|
||
columnDivider
|
||
ForEach(0..<5, id: \.self) { offset in
|
||
column(
|
||
data: data,
|
||
offset: offset,
|
||
hourHeight: hourHeight,
|
||
subjectOnly: subjectOnly
|
||
)
|
||
.frame(maxWidth: .infinity)
|
||
if offset < 4 { columnDivider }
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
private var columnDivider: some View {
|
||
Rectangle()
|
||
.fill(Color.primary.opacity(0.13))
|
||
.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 {
|
||
Text("KW \(week) · \(shortDate(data.anchorDate))–\(shortDate(endDate))")
|
||
.font(.system(size: 13, weight: .semibold))
|
||
.foregroundStyle(.primary)
|
||
Spacer()
|
||
Text("Stand: \(freshnessLabel(for: data.fetchedAt))")
|
||
.font(.system(size: 10))
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
}
|
||
|
||
private func dayHeaderRow(data: WidgetTimetableData) -> some View {
|
||
let cal = Calendar.current
|
||
return HStack(spacing: 0) {
|
||
Spacer().frame(width: 28)
|
||
columnDivider
|
||
ForEach(0..<5, id: \.self) { offset in
|
||
let day = cal.date(byAdding: .day, value: offset, to: data.anchorDate) ?? data.anchorDate
|
||
VStack(spacing: 0) {
|
||
Text(weekday(for: day))
|
||
.font(.system(size: 11, weight: .bold))
|
||
.foregroundStyle(.primary)
|
||
Text(shortDate(day))
|
||
.font(.system(size: 9))
|
||
.foregroundStyle(.secondary)
|
||
}
|
||
.frame(maxWidth: .infinity)
|
||
if offset < 4 { columnDivider }
|
||
}
|
||
}
|
||
}
|
||
|
||
private func timeLabelsColumn(hourHeight: CGFloat, periods: [WidgetPeriod]) -> some View {
|
||
let totalMin = periods.last?.virtualEndMinutes ?? FALLBACK_VIRTUAL_MINUTES
|
||
let totalHeight = CGFloat(totalMin) * hourHeight / 60.0
|
||
return ZStack(alignment: .topTrailing) {
|
||
ForEach(periodBoundaries(periods), id: \.self) { virtualMin in
|
||
Rectangle()
|
||
.fill(Color.primary.opacity(0.08))
|
||
.frame(height: 1)
|
||
.offset(y: CGFloat(virtualMin) * hourHeight / 60.0)
|
||
}
|
||
ForEach(periods, id: \.startMinutes) { period in
|
||
VStack(alignment: .trailing, spacing: -2) {
|
||
Text(String(format: "%02d:%02d", period.startMinutes / 60, period.startMinutes % 60))
|
||
.font(.system(size: 8))
|
||
.foregroundStyle(.primary)
|
||
.lineLimit(1)
|
||
Text("\(period.name).")
|
||
.font(.system(size: 6, weight: .bold))
|
||
.foregroundStyle(.secondary)
|
||
.lineLimit(1)
|
||
}
|
||
.offset(y: CGFloat(period.virtualStartMinutes) * hourHeight / 60.0)
|
||
}
|
||
}
|
||
.frame(height: totalHeight, alignment: .topTrailing)
|
||
}
|
||
|
||
private func column(
|
||
data: WidgetTimetableData,
|
||
offset: Int,
|
||
hourHeight: CGFloat,
|
||
subjectOnly: Bool
|
||
) -> some View {
|
||
let cal = Calendar.current
|
||
let day = cal.date(byAdding: .day, value: offset, to: data.anchorDate) ?? data.anchorDate
|
||
let lessonsForDay = data.lessons.filter { cal.isDate($0.start, inSameDayAs: day) }
|
||
return TimeGridView(
|
||
lessons: lessonsForDay,
|
||
periods: data.periods,
|
||
anchorDate: day,
|
||
hourHeight: hourHeight,
|
||
showRoom: !subjectOnly,
|
||
showTeacher: !subjectOnly,
|
||
showTimeLabels: false,
|
||
horizontalPadding: 3
|
||
)
|
||
}
|
||
|
||
private func weekday(for date: Date) -> String {
|
||
let f = DateFormatter()
|
||
f.locale = Locale(identifier: "de_DE")
|
||
f.dateFormat = "EE"
|
||
return f.string(from: date)
|
||
}
|
||
|
||
private func placeholder(_ message: String) -> some View {
|
||
VStack(spacing: 4) {
|
||
Text("Marianum Stundenplan")
|
||
.font(.system(size: 14, weight: .semibold))
|
||
Text(message)
|
||
.font(.caption)
|
||
.foregroundStyle(.secondary)
|
||
.multilineTextAlignment(.center)
|
||
}
|
||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||
}
|
||
}
|