forked from firka/firka
Add next-school-day support to iOS widgets
Add support for showing the next school day in widgets. Changes include: new localization keys (next_school_day_timetable, no_lessons_ahead) and a formatShortDate helper; updated Hungarian hours abbreviation. Widget model updated (TimetableData) to include nextSchoolDay and nextSchoolDayDate. TimetableProvider now sets isNextSchoolDay/nextSchoolDayDate and returns an entry when a future school day is found. Views (inline, lock screen, small/medium/large) updated to display the next school day header/date and show no_lessons_ahead where appropriate. Flutter helpers (widget cache/IOS helper) extended to serialize nextSchoolDayLessons/nextSchoolDayDate and to search up to a week ahead for the next school day when tomorrow is empty.
This commit is contained in:
@@ -19,6 +19,16 @@ struct WidgetLocalization {
|
||||
"en": "Tomorrow's timetable",
|
||||
"de": "Stundenplan morgen"
|
||||
],
|
||||
"next_school_day_timetable": [
|
||||
"hu": "Következő órarend (%@)",
|
||||
"en": "Next timetable (%@)",
|
||||
"de": "Nächster Stundenplan (%@)"
|
||||
],
|
||||
"no_lessons_ahead": [
|
||||
"hu": "Nincs óra a héten",
|
||||
"en": "No lessons this week",
|
||||
"de": "Kein Unterricht diese Woche"
|
||||
],
|
||||
"current_lesson": [
|
||||
"hu": "Jelenlegi óra",
|
||||
"en": "Current lesson",
|
||||
@@ -200,7 +210,7 @@ struct WidgetLocalization {
|
||||
"de": "Min"
|
||||
],
|
||||
"hours_abbrev": [
|
||||
"hu": "ó",
|
||||
"hu": "óra",
|
||||
"en": "h",
|
||||
"de": "Std"
|
||||
],
|
||||
@@ -225,4 +235,34 @@ struct WidgetLocalization {
|
||||
let template = string(key)
|
||||
return template.replacingOccurrences(of: "%d", with: "\(arg)")
|
||||
}
|
||||
|
||||
static func formatShortDate(_ isoString: String?, locale: String = "hu") -> String {
|
||||
guard let isoString = isoString else { return "" }
|
||||
|
||||
let isoFormatter = ISO8601DateFormatter()
|
||||
isoFormatter.formatOptions = [.withInternetDateTime]
|
||||
|
||||
let shortFormatter = DateFormatter()
|
||||
shortFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS"
|
||||
shortFormatter.locale = Locale(identifier: "en_US_POSIX")
|
||||
|
||||
let date: Date?
|
||||
if let d = isoFormatter.date(from: isoString) {
|
||||
date = d
|
||||
} else if let d = shortFormatter.date(from: isoString) {
|
||||
date = d
|
||||
} else {
|
||||
let simple = DateFormatter()
|
||||
simple.dateFormat = "yyyy-MM-dd"
|
||||
simple.locale = Locale(identifier: "en_US_POSIX")
|
||||
date = simple.date(from: String(isoString.prefix(10)))
|
||||
}
|
||||
|
||||
guard let date = date else { return "" }
|
||||
|
||||
let displayFormatter = DateFormatter()
|
||||
displayFormatter.locale = Locale(identifier: locale == "de" ? "de_DE" : locale == "en" ? "en_US" : "hu_HU")
|
||||
displayFormatter.dateFormat = locale == "hu" ? "MMM d." : "MMM d"
|
||||
return displayFormatter.string(from: date)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,12 +34,20 @@ struct TimetableInlineWidgetView: View {
|
||||
} else if let current = entry.currentLesson {
|
||||
let remaining = minutesRemaining(until: current.end)
|
||||
Text("\(current.subject.name) · \(remaining) \(localization.string("minutes_abbrev"))")
|
||||
} else if entry.isNextSchoolDay {
|
||||
if let first = entry.lessons.first {
|
||||
let dateStr = WidgetLocalization.formatShortDate(entry.nextSchoolDayDateString, locale: localization.locale)
|
||||
let lessonNum = first.lessonNumber ?? 1
|
||||
Text("\(dateStr): \(lessonNum). \(first.subject.name)")
|
||||
} else {
|
||||
Text(localization.string("no_lessons_ahead"))
|
||||
}
|
||||
} else if entry.isNextDay {
|
||||
if let first = entry.lessons.first {
|
||||
let lessonNum = first.lessonNumber ?? 1
|
||||
Text("\(localization.string("tomorrow")): \(lessonNum). \(first.subject.name)")
|
||||
} else {
|
||||
Text(localization.string("no_lessons"))
|
||||
Text(localization.string("no_lessons_ahead"))
|
||||
}
|
||||
} else if let next = entry.nextLesson {
|
||||
let until = minutesRemaining(until: next.start)
|
||||
@@ -52,7 +60,7 @@ struct TimetableInlineWidgetView: View {
|
||||
Text("→ \(next.subject.name) · \(until) \(localization.string("minutes_abbrev"))")
|
||||
}
|
||||
} else {
|
||||
Text(localization.string("no_lessons"))
|
||||
Text(localization.string("no_lessons_ahead"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -59,12 +59,20 @@ struct TimetableInlineView: View {
|
||||
} else if let current = entry.currentLesson {
|
||||
let remaining = minutesRemaining(until: current.end)
|
||||
Text("\(current.subject.name) - \(remaining) \(localization.string("minutes_short"))")
|
||||
} else if entry.isNextSchoolDay {
|
||||
if let first = entry.lessons.first {
|
||||
let dateStr = WidgetLocalization.formatShortDate(entry.nextSchoolDayDateString, locale: localization.locale)
|
||||
let lessonNum = first.lessonNumber ?? 1
|
||||
Text("\(dateStr): \(lessonNum). \(first.subject.name)")
|
||||
} else {
|
||||
Text(localization.string("no_lessons_ahead"))
|
||||
}
|
||||
} else if entry.isNextDay {
|
||||
if let first = entry.lessons.first {
|
||||
let lessonNum = first.lessonNumber ?? 1
|
||||
Text("\(localization.string("tomorrow")): \(lessonNum). \(first.subject.name)")
|
||||
} else {
|
||||
Text(localization.string("no_lessons"))
|
||||
Text(localization.string("no_lessons_ahead"))
|
||||
}
|
||||
} else if let next = entry.nextLesson {
|
||||
let until = minutesRemaining(until: next.start)
|
||||
@@ -77,7 +85,7 @@ struct TimetableInlineView: View {
|
||||
Text("\(next.subject.name) \(localization.string("in_minutes", until))")
|
||||
}
|
||||
} else {
|
||||
Text(localization.string("no_lessons"))
|
||||
Text(localization.string("no_lessons_ahead"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,7 +117,7 @@ struct TimetableCircularView: View {
|
||||
.font(.system(.title2, design: .rounded, weight: .bold))
|
||||
}
|
||||
.gaugeStyle(.accessoryCircularCapacity)
|
||||
} else if entry.isNextDay {
|
||||
} else if entry.isNextSchoolDay || entry.isNextDay {
|
||||
let lessonCount = entry.lessons.count
|
||||
if lessonCount > 0 {
|
||||
ZStack {
|
||||
@@ -220,13 +228,21 @@ struct TimetableRectangularView: View {
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
} else if let next = entry.nextLesson {
|
||||
let until = minutesRemaining(until: next.start)
|
||||
let isFutureDay = entry.isNextDay || entry.isNextSchoolDay
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
HStack {
|
||||
Text(entry.isNextDay ? localization.string("tomorrow") : localization.string("next"))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
if entry.isNextSchoolDay {
|
||||
let dateStr = WidgetLocalization.formatShortDate(entry.nextSchoolDayDateString, locale: localization.locale)
|
||||
Text(dateStr)
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
} else {
|
||||
Text(entry.isNextDay ? localization.string("tomorrow") : localization.string("next"))
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
if until > 0 && !entry.isNextDay {
|
||||
if until > 0 && !isFutureDay {
|
||||
if until > 60 {
|
||||
Text(localization.string("in_hours", until / 60))
|
||||
.font(.caption)
|
||||
|
||||
@@ -95,7 +95,7 @@ struct WidgetData: Codable {
|
||||
lastUpdated: nil,
|
||||
locale: "hu",
|
||||
theme: "dark",
|
||||
timetable: TimetableData(today: [], tomorrow: [], currentBreak: nil),
|
||||
timetable: TimetableData(today: [], tomorrow: [], nextSchoolDay: nil, nextSchoolDayDate: nil, currentBreak: nil),
|
||||
grades: [],
|
||||
averages: AveragesData(overall: nil, subjects: [])
|
||||
)
|
||||
@@ -105,6 +105,8 @@ struct WidgetData: Codable {
|
||||
struct TimetableData: Codable {
|
||||
let today: [WidgetLesson]
|
||||
let tomorrow: [WidgetLesson]
|
||||
let nextSchoolDay: [WidgetLesson]?
|
||||
let nextSchoolDayDate: String?
|
||||
let currentBreak: BreakInfo?
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ struct TimetableEntry: TimelineEntry {
|
||||
let currentLesson: WidgetLesson?
|
||||
let nextLesson: WidgetLesson?
|
||||
let isNextDay: Bool
|
||||
let isNextSchoolDay: Bool
|
||||
let nextSchoolDayDateString: String?
|
||||
let breakInfo: BreakInfo?
|
||||
let state: TimetableState
|
||||
let debugInfo: String
|
||||
@@ -35,6 +37,8 @@ struct TimetableProvider: AppIntentTimelineProvider {
|
||||
currentLesson: nil,
|
||||
nextLesson: nil,
|
||||
isNextDay: false,
|
||||
isNextSchoolDay: false,
|
||||
nextSchoolDayDateString: nil,
|
||||
breakInfo: nil,
|
||||
state: .normal,
|
||||
debugInfo: "placeholder"
|
||||
@@ -62,6 +66,8 @@ struct TimetableProvider: AppIntentTimelineProvider {
|
||||
currentLesson: nil,
|
||||
nextLesson: nil,
|
||||
isNextDay: false,
|
||||
isNextSchoolDay: false,
|
||||
nextSchoolDayDateString: nil,
|
||||
breakInfo: breakInfo,
|
||||
state: .onBreak,
|
||||
debugInfo: WidgetData.lastError
|
||||
@@ -152,6 +158,8 @@ struct TimetableProvider: AppIntentTimelineProvider {
|
||||
currentLesson: nil,
|
||||
nextLesson: nil,
|
||||
isNextDay: false,
|
||||
isNextSchoolDay: false,
|
||||
nextSchoolDayDateString: nil,
|
||||
breakInfo: nil,
|
||||
state: .loginRequired,
|
||||
debugInfo: WidgetData.lastError
|
||||
@@ -167,6 +175,8 @@ struct TimetableProvider: AppIntentTimelineProvider {
|
||||
currentLesson: nil,
|
||||
nextLesson: nil,
|
||||
isNextDay: false,
|
||||
isNextSchoolDay: false,
|
||||
nextSchoolDayDateString: nil,
|
||||
breakInfo: breakInfo,
|
||||
state: .onBreak,
|
||||
debugInfo: WidgetData.lastError
|
||||
@@ -202,6 +212,23 @@ struct TimetableProvider: AppIntentTimelineProvider {
|
||||
}
|
||||
|
||||
if lessons.isEmpty {
|
||||
if let nextSchoolDayLessons = data.timetable.nextSchoolDay, !nextSchoolDayLessons.isEmpty {
|
||||
return TimetableEntry(
|
||||
date: date,
|
||||
configuration: configuration,
|
||||
data: data,
|
||||
lessons: nextSchoolDayLessons,
|
||||
currentLesson: nil,
|
||||
nextLesson: nextSchoolDayLessons.first,
|
||||
isNextDay: false,
|
||||
isNextSchoolDay: true,
|
||||
nextSchoolDayDateString: data.timetable.nextSchoolDayDate,
|
||||
breakInfo: nil,
|
||||
state: .normal,
|
||||
debugInfo: WidgetData.lastError
|
||||
)
|
||||
}
|
||||
|
||||
return TimetableEntry(
|
||||
date: date,
|
||||
configuration: configuration,
|
||||
@@ -210,6 +237,8 @@ struct TimetableProvider: AppIntentTimelineProvider {
|
||||
currentLesson: nil,
|
||||
nextLesson: nil,
|
||||
isNextDay: isNextDay,
|
||||
isNextSchoolDay: false,
|
||||
nextSchoolDayDateString: nil,
|
||||
breakInfo: nil,
|
||||
state: isNextDay ? .noMoreLessons : .unavailable,
|
||||
debugInfo: WidgetData.lastError
|
||||
@@ -229,6 +258,8 @@ struct TimetableProvider: AppIntentTimelineProvider {
|
||||
currentLesson: currentLesson,
|
||||
nextLesson: nextLesson,
|
||||
isNextDay: isNextDay,
|
||||
isNextSchoolDay: false,
|
||||
nextSchoolDayDateString: nil,
|
||||
breakInfo: nil,
|
||||
state: .normal,
|
||||
debugInfo: WidgetData.lastError
|
||||
|
||||
@@ -47,6 +47,13 @@ struct TimetableSmallView: View {
|
||||
.widgetTextStyle(style, colors: nil, isPrimary: false)
|
||||
|
||||
if let lesson = displayLesson {
|
||||
if entry.isNextSchoolDay {
|
||||
let dateStr = WidgetLocalization.formatShortDate(entry.nextSchoolDayDateString, locale: localization.locale)
|
||||
Text(dateStr)
|
||||
.font(.caption2)
|
||||
.widgetTextStyle(style, colors: nil, isPrimary: false)
|
||||
}
|
||||
|
||||
Text(lesson.displayName)
|
||||
.font(.subheadline)
|
||||
.fontWeight(.semibold)
|
||||
@@ -73,7 +80,7 @@ struct TimetableSmallView: View {
|
||||
(style == .liquidGlass ? liquidGlassSecondary : .secondary))
|
||||
}
|
||||
} else {
|
||||
Text(localization.string("no_lessons"))
|
||||
Text(localization.string("no_lessons_ahead"))
|
||||
.font(.subheadline)
|
||||
.widgetTextStyle(style, colors: nil, isPrimary: false)
|
||||
}
|
||||
@@ -125,12 +132,23 @@ struct TimetableMediumView: View {
|
||||
return Array(entry.lessons[startIndex..<endIndex])
|
||||
}
|
||||
|
||||
var headerText: String {
|
||||
if entry.isNextSchoolDay {
|
||||
let dateStr = WidgetLocalization.formatShortDate(entry.nextSchoolDayDateString, locale: localization.locale)
|
||||
return localization.string("next_school_day_timetable", dateStr)
|
||||
} else if entry.isNextDay {
|
||||
return localization.string("tomorrow_timetable")
|
||||
} else {
|
||||
return localization.string("today_timetable")
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
WidgetBackground(style: style, colors: nil)
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(entry.isNextDay ? localization.string("tomorrow_timetable") : localization.string("today_timetable"))
|
||||
Text(headerText)
|
||||
.font(.caption)
|
||||
.fontWeight(.semibold)
|
||||
.widgetTextStyle(style, colors: nil, isPrimary: false)
|
||||
@@ -160,12 +178,23 @@ struct TimetableLargeView: View {
|
||||
return checkDate >= lesson.start && checkDate <= lesson.end
|
||||
}
|
||||
|
||||
var headerText: String {
|
||||
if entry.isNextSchoolDay {
|
||||
let dateStr = WidgetLocalization.formatShortDate(entry.nextSchoolDayDateString, locale: localization.locale)
|
||||
return localization.string("next_school_day_timetable", dateStr)
|
||||
} else if entry.isNextDay {
|
||||
return localization.string("tomorrow_timetable")
|
||||
} else {
|
||||
return localization.string("today_timetable")
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
WidgetBackground(style: style, colors: nil)
|
||||
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
Text(entry.isNextDay ? localization.string("tomorrow_timetable") : localization.string("today_timetable"))
|
||||
Text(headerText)
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
.widgetTextStyle(style, colors: nil)
|
||||
|
||||
@@ -28,6 +28,8 @@ class IOSWidgetHelper {
|
||||
required String theme,
|
||||
required List<Lesson> todayLessons,
|
||||
required List<Lesson> tomorrowLessons,
|
||||
List<Lesson> nextSchoolDayLessons = const [],
|
||||
DateTime? nextSchoolDayDate,
|
||||
required List<Grade> grades,
|
||||
required Map<String, double> subjectAverages,
|
||||
required double? overallAverage,
|
||||
@@ -53,6 +55,8 @@ class IOSWidgetHelper {
|
||||
'timetable': {
|
||||
'today': todayLessons.map((l) => _lessonToJson(l)).toList(),
|
||||
'tomorrow': tomorrowLessons.map((l) => _lessonToJson(l)).toList(),
|
||||
'nextSchoolDay': nextSchoolDayLessons.map((l) => _lessonToJson(l)).toList(),
|
||||
'nextSchoolDayDate': nextSchoolDayDate?.toIso8601String(),
|
||||
'currentBreak': currentBreak != null ? {
|
||||
'name': currentBreak.name,
|
||||
'nameKey': currentBreak.nameKey,
|
||||
|
||||
@@ -82,6 +82,8 @@ class WidgetCacheHelper {
|
||||
required String theme,
|
||||
required List<Lesson> todayLessons,
|
||||
required List<Lesson> tomorrowLessons,
|
||||
List<Lesson> nextSchoolDayLessons = const [],
|
||||
DateTime? nextSchoolDayDate,
|
||||
required List<Grade> grades,
|
||||
required Map<String, double> subjectAverages,
|
||||
required double? overallAverage,
|
||||
@@ -92,6 +94,8 @@ class WidgetCacheHelper {
|
||||
theme: theme,
|
||||
todayLessons: todayLessons,
|
||||
tomorrowLessons: tomorrowLessons,
|
||||
nextSchoolDayLessons: nextSchoolDayLessons,
|
||||
nextSchoolDayDate: nextSchoolDayDate,
|
||||
grades: grades,
|
||||
subjectAverages: subjectAverages,
|
||||
overallAverage: overallAverage,
|
||||
@@ -160,6 +164,26 @@ class WidgetCacheHelper {
|
||||
|
||||
debugPrint('iOS widget refresh: ${todayLessons.length} today lessons, ${tomorrowLessons.length} tomorrow lessons');
|
||||
|
||||
List<Lesson> nextSchoolDayLessons = [];
|
||||
DateTime? nextSchoolDayDate;
|
||||
if (tomorrowLessons.isEmpty) {
|
||||
for (int i = 2; i <= 7; i++) {
|
||||
final dayMidnight = todayMidnight.add(Duration(days: i));
|
||||
final dayResponse = await client.getTimeTable(
|
||||
dayMidnight,
|
||||
dayMidnight.add(Duration(hours: 23, minutes: 59)),
|
||||
forceCache: false,
|
||||
);
|
||||
final dayLessons = dayResponse.response ?? [];
|
||||
if (dayLessons.isNotEmpty) {
|
||||
nextSchoolDayLessons = dayLessons;
|
||||
nextSchoolDayDate = dayMidnight;
|
||||
debugPrint('iOS widget: Next school day found ${i} days ahead with ${dayLessons.length} lessons');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final gradesResponse = await client.getGrades(forceCache: false);
|
||||
final grades = gradesResponse.response ?? [];
|
||||
|
||||
@@ -199,6 +223,8 @@ class WidgetCacheHelper {
|
||||
theme: theme,
|
||||
todayLessons: todayLessons,
|
||||
tomorrowLessons: tomorrowLessons,
|
||||
nextSchoolDayLessons: nextSchoolDayLessons,
|
||||
nextSchoolDayDate: nextSchoolDayDate,
|
||||
grades: grades,
|
||||
subjectAverages: subjectAverages,
|
||||
overallAverage: overallAverage,
|
||||
|
||||
Reference in New Issue
Block a user