diff --git a/firka/ios/HomeWidgetsExtension/Helpers/Localization.swift b/firka/ios/HomeWidgetsExtension/Helpers/Localization.swift index e8a041de..6530dfbc 100644 --- a/firka/ios/HomeWidgetsExtension/Helpers/Localization.swift +++ b/firka/ios/HomeWidgetsExtension/Helpers/Localization.swift @@ -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) + } } diff --git a/firka/ios/HomeWidgetsExtension/LockScreen/InlineWidgets.swift b/firka/ios/HomeWidgetsExtension/LockScreen/InlineWidgets.swift index 2f2e0f00..8843cd9b 100644 --- a/firka/ios/HomeWidgetsExtension/LockScreen/InlineWidgets.swift +++ b/firka/ios/HomeWidgetsExtension/LockScreen/InlineWidgets.swift @@ -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")) } } diff --git a/firka/ios/HomeWidgetsExtension/LockScreen/TimetableLockScreenWidget.swift b/firka/ios/HomeWidgetsExtension/LockScreen/TimetableLockScreenWidget.swift index 63c561b9..e389492e 100644 --- a/firka/ios/HomeWidgetsExtension/LockScreen/TimetableLockScreenWidget.swift +++ b/firka/ios/HomeWidgetsExtension/LockScreen/TimetableLockScreenWidget.swift @@ -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) diff --git a/firka/ios/HomeWidgetsExtension/Models/WidgetData.swift b/firka/ios/HomeWidgetsExtension/Models/WidgetData.swift index d8a1932f..6e96061c 100644 --- a/firka/ios/HomeWidgetsExtension/Models/WidgetData.swift +++ b/firka/ios/HomeWidgetsExtension/Models/WidgetData.swift @@ -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? } diff --git a/firka/ios/HomeWidgetsExtension/Providers/TimetableProvider.swift b/firka/ios/HomeWidgetsExtension/Providers/TimetableProvider.swift index e18918db..df2ec897 100644 --- a/firka/ios/HomeWidgetsExtension/Providers/TimetableProvider.swift +++ b/firka/ios/HomeWidgetsExtension/Providers/TimetableProvider.swift @@ -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 diff --git a/firka/ios/HomeWidgetsExtension/Views/TimetableViews.swift b/firka/ios/HomeWidgetsExtension/Views/TimetableViews.swift index 3b2f9d44..370910e5 100644 --- a/firka/ios/HomeWidgetsExtension/Views/TimetableViews.swift +++ b/firka/ios/HomeWidgetsExtension/Views/TimetableViews.swift @@ -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..= 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) diff --git a/firka/lib/helpers/db/ios_widget_helper.dart b/firka/lib/helpers/db/ios_widget_helper.dart index a110da99..6bd4cc26 100644 --- a/firka/lib/helpers/db/ios_widget_helper.dart +++ b/firka/lib/helpers/db/ios_widget_helper.dart @@ -28,6 +28,8 @@ class IOSWidgetHelper { required String theme, required List todayLessons, required List tomorrowLessons, + List nextSchoolDayLessons = const [], + DateTime? nextSchoolDayDate, required List grades, required Map 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, diff --git a/firka/lib/helpers/db/widget.dart b/firka/lib/helpers/db/widget.dart index 3200e6a5..de9dd68a 100644 --- a/firka/lib/helpers/db/widget.dart +++ b/firka/lib/helpers/db/widget.dart @@ -82,6 +82,8 @@ class WidgetCacheHelper { required String theme, required List todayLessons, required List tomorrowLessons, + List nextSchoolDayLessons = const [], + DateTime? nextSchoolDayDate, required List grades, required Map 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 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,