- Fixed lesson language data; backend now passes the correct values.

- Updated handling of cancelled lessons: removed timer and now displaying the localized “Cancelled” text.
- Aligned the icon and lesson number in the Dynamic Island so they are now level with the label text.
This commit is contained in:
Horváth Gergely
2025-11-24 21:21:12 +01:00
committed by 4831c0
parent 768d0904a8
commit f631d52d5a
5 changed files with 241 additions and 116 deletions

View File

@@ -11,21 +11,36 @@ struct TimetableActivityAttributes: ActivityAttributes {
var startTime: Date
var endTime: Date
var lessonNumber: Int?
var mode: String? // "lesson" | "break" | "seasonalBreak" | "xmas" | "newYear"
var message: String?
var season: String?
var nextLessonName: String?
var nextRoomName: String?
var nextStartTime: Date?
var isSubstitution: Bool
var isCancelled: Bool
var substituteTeacher: String?
var currentTime: Date
var labels: Labels?
struct Labels: Codable, Hashable {
var title: String?
var timerLabel: String?
var cancelledText: String?
var substitutionText: String?
var roomLabel: String?
var teacherLabel: String?
var themeLabel: String?
var nextLabel: String?
var firstLessonLabel: String?
var startTimeLabel: String?
}
enum CodingKeys: String, CodingKey {
case isBreak
case lessonName
@@ -45,9 +60,10 @@ struct TimetableActivityAttributes: ActivityAttributes {
case isCancelled
case substituteTeacher
case currentTime
case labels
}
init(isBreak: Bool, lessonName: String, lessonTheme: String?, roomName: String?, teacherName: String?, startTime: Date, endTime: Date, lessonNumber: Int?, mode: String?, message: String?, season: String?, nextLessonName: String?, nextRoomName: String?, nextStartTime: Date?, isSubstitution: Bool, isCancelled: Bool, substituteTeacher: String?, currentTime: Date) {
init(isBreak: Bool, lessonName: String, lessonTheme: String?, roomName: String?, teacherName: String?, startTime: Date, endTime: Date, lessonNumber: Int?, mode: String?, message: String?, season: String?, nextLessonName: String?, nextRoomName: String?, nextStartTime: Date?, isSubstitution: Bool, isCancelled: Bool, substituteTeacher: String?, currentTime: Date, labels: Labels? = nil) {
self.isBreak = isBreak
self.lessonName = lessonName
self.lessonTheme = lessonTheme
@@ -66,6 +82,7 @@ struct TimetableActivityAttributes: ActivityAttributes {
self.isCancelled = isCancelled
self.substituteTeacher = substituteTeacher
self.currentTime = currentTime
self.labels = labels
}
init(from decoder: Decoder) throws {
@@ -108,7 +125,8 @@ struct TimetableActivityAttributes: ActivityAttributes {
isSubstitution = try container.decode(Bool.self, forKey: .isSubstitution)
isCancelled = try container.decode(Bool.self, forKey: .isCancelled)
substituteTeacher = try container.decodeIfPresent(String.self, forKey: .substituteTeacher)
labels = try container.decodeIfPresent(Labels.self, forKey: .labels)
let currentTimeStr = try container.decode(String.self, forKey: .currentTime)
guard let currentTimeDate = isoFormatter.date(from: currentTimeStr) else {
throw DecodingError.dataCorruptedError(forKey: .currentTime, in: container, debugDescription: "Invalid currentTime format: \(currentTimeStr)")
@@ -145,7 +163,8 @@ struct TimetableActivityAttributes: ActivityAttributes {
try container.encode(isSubstitution, forKey: .isSubstitution)
try container.encode(isCancelled, forKey: .isCancelled)
try container.encodeIfPresent(substituteTeacher, forKey: .substituteTeacher)
try container.encodeIfPresent(labels, forKey: .labels)
try container.encode(isoFormatter.string(from: currentTime), forKey: .currentTime)
}
}
@@ -302,7 +321,25 @@ extension TimetableActivityAttributes.ContentState {
} else {
nextStartTime = nil
}
let labels: Labels?
if let labelsDict = json["labels"] as? [String: Any] {
labels = Labels(
title: labelsDict["title"] as? String,
timerLabel: labelsDict["timerLabel"] as? String,
cancelledText: labelsDict["cancelledText"] as? String,
substitutionText: labelsDict["substitutionText"] as? String,
roomLabel: labelsDict["roomLabel"] as? String,
teacherLabel: labelsDict["teacherLabel"] as? String,
themeLabel: labelsDict["themeLabel"] as? String,
nextLabel: labelsDict["nextLabel"] as? String,
firstLessonLabel: labelsDict["firstLessonLabel"] as? String,
startTimeLabel: labelsDict["startTimeLabel"] as? String
)
} else {
labels = nil
}
return TimetableActivityAttributes.ContentState(
isBreak: isBreak,
lessonName: lessonName,
@@ -321,7 +358,8 @@ extension TimetableActivityAttributes.ContentState {
isSubstitution: isSubstitution,
isCancelled: isCancelled,
substituteTeacher: json["substituteTeacher"] as? String,
currentTime: currentTime
currentTime: currentTime,
labels: labels
)
}
}

View File

@@ -49,24 +49,7 @@ struct TimetableLiveActivity: Widget {
return DynamicIsland {
// Expanded UI
DynamicIslandExpandedRegion(.leading) {
let season = context.state.season ?? ""
HStack(alignment: .center, spacing: 4) {
if mode == "beforeSchool" {
Image(systemName: SeasonalIconHelper.iconName(for: mode, season: season))
.font(.system(size: 18))
.foregroundColor(SeasonalIconHelper.iconColor(for: mode))
} else if !SeasonalIconHelper.isSeasonalMode(mode) && !context.state.isBreak {
if let lessonNumber = context.state.lessonNumber {
Text("\(lessonNumber).")
.font(.system(size: 16, weight: .bold))
.foregroundColor(.white)
} else {
EmptyView()
}
} else {
EmptyView()
}
}
EmptyView()
}
DynamicIslandExpandedRegion(.trailing) {
@@ -78,7 +61,7 @@ struct TimetableLiveActivity: Widget {
if SeasonalIconHelper.isSeasonalMode(mode) {
EmptyView()
} else if mode == "beforeSchool", let timeString = beforeSchoolTime {
Text("Kezdés: \(timeString)")
Text("\(context.state.labels?.startTimeLabel ?? "Kezdés:") \(timeString)")
.font(.system(size: 14, weight: .medium))
.foregroundColor(.gray)
} else {
@@ -92,23 +75,28 @@ struct TimetableLiveActivity: Widget {
let season = context.state.season ?? ""
VStack(spacing: 4) {
if mode == "beforeSchool" {
Text("Hamarosan suli")
.font(.system(size: 18, weight: .bold))
.foregroundColor(.white)
HStack(alignment: .center, spacing: 6) {
Image(systemName: SeasonalIconHelper.iconName(for: mode, season: season))
.font(.system(size: 18))
.foregroundColor(SeasonalIconHelper.iconColor(for: mode))
Text(context.state.labels?.title ?? "Hamarosan suli")
.font(.system(size: 18, weight: .bold))
.foregroundColor(.white)
}
VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 4) {
Text("Első órád:")
Text(context.state.labels?.firstLessonLabel ?? "Első órád:")
.font(.system(size: 12))
.foregroundColor(.gray)
Text(context.state.lessonName)
.font(.system(size: 12, weight: .semibold))
.foregroundColor(.white)
}
if let roomName = context.state.roomName {
HStack(spacing: 4) {
Text("Terem:")
Text(context.state.labels?.roomLabel ?? "Terem:")
.font(.system(size: 12))
.foregroundColor(.gray)
Text(roomName)
@@ -119,7 +107,7 @@ struct TimetableLiveActivity: Widget {
if let teacherName = context.state.teacherName {
HStack(spacing: 4) {
Text("Tanár:")
Text(context.state.labels?.teacherLabel ?? "Tanár:")
.font(.system(size: 12))
.foregroundColor(.gray)
Text(teacherName)
@@ -130,7 +118,6 @@ struct TimetableLiveActivity: Widget {
}
} else if SeasonalIconHelper.isSeasonalMode(mode) {
if mode == "xmas" || mode == "newYearEve" || mode == "newYearDay" {
// Global holidays: show message prominently
HStack(alignment: .center, spacing: 6) {
Image(systemName: SeasonalIconHelper.iconName(for: mode, season: season))
.font(.system(size: 18))
@@ -141,7 +128,6 @@ struct TimetableLiveActivity: Widget {
.lineLimit(2)
}
} else {
// Seasonal breaks: show holiday title
HStack(alignment: .center, spacing: 6) {
Image(systemName: SeasonalIconHelper.iconName(for: mode, season: season))
.font(.system(size: 18))
@@ -153,13 +139,18 @@ struct TimetableLiveActivity: Widget {
}
}
} else if context.state.isBreak {
Text("Szünet")
.font(.system(size: 18, weight: .bold))
.foregroundColor(.white)
HStack(alignment: .center, spacing: 6) {
Image(systemName: "cup.and.saucer.fill")
.font(.system(size: 18))
.foregroundColor(SeasonalIconHelper.iconColor(for: mode))
Text(context.state.labels?.title ?? "Szünet")
.font(.system(size: 18, weight: .bold))
.foregroundColor(.white)
}
if let nextLessonName = context.state.nextLessonName {
HStack(spacing: 4) {
Text("Következő:")
Text(context.state.labels?.nextLabel ?? "Következő:")
.font(.system(size: 14))
.foregroundColor(.gray)
Text(nextLessonName)
@@ -167,43 +158,46 @@ struct TimetableLiveActivity: Widget {
.foregroundColor(.white)
}
}
if let nextRoomName = context.state.nextRoomName {
Text("Terem: \(nextRoomName)")
Text("\(context.state.labels?.roomLabel ?? "Terem:") \(nextRoomName)")
.font(.system(size: 12))
.foregroundColor(.gray)
}
} else {
Text(context.state.lessonName)
.font(.system(size: 18, weight: .bold))
.foregroundColor(.white)
.lineLimit(1)
if let lessonTheme = context.state.lessonTheme, !lessonTheme.isEmpty {
Text(lessonTheme)
.font(.system(size: 12))
.foregroundColor(.gray)
HStack(alignment: .center, spacing: 6) {
if let lessonNumber = context.state.lessonNumber {
Text("\(lessonNumber).")
.font(.system(size: 18, weight: .bold))
.foregroundColor(.white)
}
Text(context.state.lessonName)
.font(.system(size: 18, weight: .bold))
.foregroundColor(.white)
.lineLimit(1)
}
if !(context.state.isCancelled ?? false) {
if let lessonTheme = context.state.lessonTheme, !lessonTheme.isEmpty {
Text(lessonTheme)
.font(.system(size: 12))
.foregroundColor(.gray)
.lineLimit(1)
}
}
HStack(spacing: 8) {
if let roomName = context.state.roomName {
Label(roomName, systemImage: "door.left.hand.closed")
.font(.system(size: 12))
.foregroundColor(.gray)
}
if context.state.isSubstitution ?? false {
Label("Helyettesítés", systemImage: "arrow.triangle.2.circlepath")
Label(context.state.labels?.substitutionText ?? "Helyettesítés", systemImage: "arrow.triangle.2.circlepath")
.font(.system(size: 12))
.foregroundColor(.orange)
}
if context.state.isCancelled ?? false {
Label("Elmaradt", systemImage: "xmark.circle")
.font(.system(size: 12))
.foregroundColor(.red)
}
}
}
}
@@ -234,6 +228,11 @@ struct TimetableLiveActivity: Widget {
.foregroundColor(.green)
.multilineTextAlignment(.center)
.monospacedDigit()
} else if context.state.isCancelled ?? false {
Text(context.state.labels?.cancelledText ?? "Elmaradt")
.font(.system(size: 20, weight: .bold, design: .rounded))
.foregroundColor(.red)
.multilineTextAlignment(.center)
} else {
Text(timerInterval: context.state.currentTime...context.state.endTime, countsDown: true)
.font(.system(size: 24, weight: .bold, design: .rounded))
@@ -258,7 +257,6 @@ struct TimetableLiveActivity: Widget {
.foregroundColor(SeasonalIconHelper.iconColor(for: mode))
} compactTrailing: {
if SeasonalIconHelper.isSeasonalMode(mode) {
// Show timer for New Year's Eve countdown and seasonal breaks
if mode == "newYearEve" || mode == "seasonalBreak" {
Text(timerInterval: context.state.currentTime...context.state.endTime, countsDown: true)
.font(.system(size: 12, weight: .semibold, design: .rounded))
@@ -266,10 +264,13 @@ struct TimetableLiveActivity: Widget {
.monospacedDigit()
.frame(width: 50)
} else {
// No timer for xmas and newYearDay
Text("")
.frame(width: 50)
}
} else if context.state.isCancelled ?? false {
Text("")
.font(.system(size: 12, weight: .semibold))
.frame(width: 50)
} else {
Text(timerInterval: context.state.currentTime...context.state.endTime, countsDown: true)
.font(.system(size: 12, weight: .semibold, design: .rounded))
@@ -346,30 +347,27 @@ struct TimetableLiveActivityView: View {
VStack(alignment: .leading, spacing: 2) {
if mode == "beforeSchool" {
Text("Hamarosan suli")
Text(context.state.labels?.title ?? "Hamarosan suli")
.font(.system(size: 20, weight: .bold))
.foregroundColor(.white)
} else if SeasonalIconHelper.isSeasonalMode(mode) {
// Check if it's a special holiday with a prominent message
if mode == "xmas" || mode == "newYearEve" || mode == "newYearDay" {
// Global holidays: show message prominently
Text(context.state.message ?? context.state.lessonName)
.font(.system(size: 20, weight: .bold))
.foregroundColor(.white)
.lineLimit(2)
} else {
// Seasonal breaks: show holiday title
Text(SeasonalIconHelper.holidayTitle(for: context.state.season))
.font(.system(size: 20, weight: .bold))
.foregroundColor(.white)
.lineLimit(1)
}
} else if context.state.isBreak {
Text("Szünet")
Text(context.state.labels?.title ?? "Szünet")
.font(.system(size: 20, weight: .bold))
.foregroundColor(.white)
if let nextLessonName = context.state.nextLessonName {
Text("Következő: \(nextLessonName)")
Text("\(context.state.labels?.nextLabel ?? "Következő:") \(nextLessonName)")
.font(.system(size: 14))
.foregroundColor(.gray)
}
@@ -392,7 +390,7 @@ struct TimetableLiveActivityView: View {
if SeasonalIconHelper.isSeasonalMode(mode) {
EmptyView()
} else if mode == "beforeSchool", let timeString = beforeSchoolTime {
Text("Kezdés: \(timeString)")
Text("\(context.state.labels?.startTimeLabel ?? "Kezdés:") \(timeString)")
.font(.system(size: 14, weight: .medium))
.foregroundColor(.white)
} else {
@@ -407,7 +405,7 @@ struct TimetableLiveActivityView: View {
EmptyView()
} else if context.state.isBreak {
if let _ = context.state.nextStartTime {
Text("Kezdés: \(context.state.formattedNextStartTime)")
Text("\(context.state.labels?.startTimeLabel ?? "Kezdés:") \(context.state.formattedNextStartTime)")
.font(.system(size: 12))
.foregroundColor(.gray)
} else {
@@ -434,17 +432,17 @@ struct TimetableLiveActivityView: View {
if mode2 == "beforeSchool" {
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 4) {
Text("Első órád:")
Text(context.state.labels?.firstLessonLabel ?? "Első órád:")
.font(.system(size: 12, weight: .semibold))
.foregroundColor(.gray)
Text(context.state.lessonName)
.font(.system(size: 12))
.foregroundColor(.white)
}
if let roomName = context.state.roomName {
HStack(spacing: 4) {
Text("Terem:")
Text(context.state.labels?.roomLabel ?? "Terem:")
.font(.system(size: 12, weight: .semibold))
.foregroundColor(.gray)
Text(roomName)
@@ -452,10 +450,10 @@ struct TimetableLiveActivityView: View {
.foregroundColor(.white)
}
}
if let teacherName = context.state.teacherName {
HStack(spacing: 4) {
Text("Tanár:")
Text(context.state.labels?.teacherLabel ?? "Tanár:")
.font(.system(size: 12, weight: .semibold))
.foregroundColor(.gray)
Text(teacherName)
@@ -469,15 +467,17 @@ struct TimetableLiveActivityView: View {
EmptyView()
} else if !context.state.isBreak {
VStack(alignment: .leading, spacing: 4) {
if let lessonTheme = context.state.lessonTheme, !lessonTheme.isEmpty {
HStack {
Text("Téma:")
.font(.system(size: 12, weight: .semibold))
.foregroundColor(.gray)
Text(lessonTheme)
.font(.system(size: 12))
.foregroundColor(.white)
.lineLimit(1)
if !(context.state.isCancelled ?? false) {
if let lessonTheme = context.state.lessonTheme, !lessonTheme.isEmpty {
HStack {
Text(context.state.labels?.themeLabel ?? "Téma:")
.font(.system(size: 12, weight: .semibold))
.foregroundColor(.gray)
Text(lessonTheme)
.font(.system(size: 12))
.foregroundColor(.white)
.lineLimit(1)
}
}
}
@@ -485,7 +485,7 @@ struct TimetableLiveActivityView: View {
HStack(spacing: 4) {
Image(systemName: "arrow.triangle.2.circlepath")
.font(.system(size: 12))
Text("Helyettesítés")
Text(context.state.labels?.substitutionText ?? "Helyettesítés")
.font(.system(size: 12, weight: .semibold))
if let substituteTeacher = context.state.substituteTeacher {
Text("(\(substituteTeacher))")
@@ -494,16 +494,6 @@ struct TimetableLiveActivityView: View {
}
.foregroundColor(.orange)
}
if context.state.isCancelled ?? false {
HStack(spacing: 4) {
Image(systemName: "xmark.circle")
.font(.system(size: 12))
Text("Elmaradt óra")
.font(.system(size: 12, weight: .semibold))
}
.foregroundColor(.red)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
} else if let nextRoomName = context.state.nextRoomName {
@@ -553,25 +543,32 @@ struct TimetableLiveActivityView: View {
.multilineTextAlignment(.center)
.monospacedDigit()
} else {
let labelText: String = {
if mode3 == "newYearEve" {
return "Új év"
} else if mode3 == "beforeSchool" {
return "Első óra kezdése"
} else if context.state.isBreak {
return "Szünet vége"
} else {
return "Óra vége"
}
}()
Text(labelText)
.font(.system(size: 10))
.foregroundColor(.gray)
Text(timerInterval: context.state.currentTime...context.state.endTime, countsDown: true)
.font(.system(size: 32, weight: .bold, design: .rounded))
.foregroundColor(.green)
.multilineTextAlignment(.center)
.monospacedDigit()
if context.state.isCancelled ?? false {
Text(context.state.labels?.cancelledText ?? "Elmaradt")
.font(.system(size: 32, weight: .bold, design: .rounded))
.foregroundColor(.red)
.multilineTextAlignment(.center)
} else {
let labelText: String = {
if mode3 == "newYearEve" {
return "Új év"
} else if mode3 == "beforeSchool" {
return context.state.labels?.timerLabel ?? "Első óra kezdése"
} else if context.state.isBreak {
return context.state.labels?.timerLabel ?? "Szünet vége"
} else {
return context.state.labels?.timerLabel ?? "Óra vége"
}
}()
Text(labelText)
.font(.system(size: 10))
.foregroundColor(.gray)
Text(timerInterval: context.state.currentTime...context.state.endTime, countsDown: true)
.font(.system(size: 32, weight: .bold, design: .rounded))
.foregroundColor(.green)
.multilineTextAlignment(.center)
.monospacedDigit()
}
}
}
Spacer()

View File

@@ -434,7 +434,46 @@ class LiveActivityService {
final endOfWeek = startOfWeek.add(const Duration(days: 6));
final timetableResponse = await client.getTimeTable(startOfWeek, endOfWeek);
final allLessons = timetableResponse.response ?? [];
List<Lesson> allLessons = timetableResponse.response ?? [];
final nextMonday = endOfWeek.add(const Duration(days: 1));
final nextMondayEnd = nextMonday.add(const Duration(days: 1));
try {
final nextMondayResponse = await client.getTimeTable(nextMonday, nextMondayEnd);
if (nextMondayResponse.response != null && nextMondayResponse.response!.isNotEmpty) {
final mondayLessons = nextMondayResponse.response!;
mondayLessons.sort((a, b) => a.start.compareTo(b.start));
final firstLesson = mondayLessons.first;
final markedLesson = Lesson(
uid: '${firstLesson.uid}__FOR_NOTIFICATION_ONLY',
date: firstLesson.date,
start: firstLesson.start,
end: firstLesson.end,
name: firstLesson.name,
lessonNumber: firstLesson.lessonNumber,
teacher: firstLesson.teacher,
theme: firstLesson.theme,
roomName: firstLesson.roomName,
substituteTeacher: firstLesson.substituteTeacher,
type: firstLesson.type,
state: firstLesson.state,
canStudentEditHomework: firstLesson.canStudentEditHomework,
isHomeworkComplete: firstLesson.isHomeworkComplete,
attachments: firstLesson.attachments,
isDigitalLesson: firstLesson.isDigitalLesson,
digitalSupportDeviceTypeList: firstLesson.digitalSupportDeviceTypeList,
createdAt: firstLesson.createdAt ?? firstLesson.lastModifiedAt ?? DateTime.now(),
lastModifiedAt: firstLesson.lastModifiedAt,
);
allLessons.add(markedLesson);
_logger.info('Added first lesson from next Monday (${firstLesson.name}) marked for notification scheduling only');
}
} catch (e) {
_logger.warning('Could not fetch next Monday first lesson: $e');
}
if (allLessons.isEmpty) {
await LiveActivityManager.endAllActivities();

View File

@@ -96,6 +96,30 @@ class _HomeScreenState extends FirkaState<HomeScreen> {
});
}
void _setupNotificationListener() {
final notificationChannel = MethodChannel('firka.app/notifications');
notificationChannel.setMethodCallHandler((call) async {
if (call.method == 'onNotificationTapped') {
logger.info('Notification tapped: ${call.arguments}');
final args = call.arguments as Map<Object?, Object?>?;
if (args == null) return;
final action = args['action'] as String?;
final route = args['route'] as String?;
if (action != null || route != null) {
logger.info('Navigating to timetable from notification');
setState(() {
homeScreenPage = HomePage.timetable;
_pageController.jumpToPage(HomePage.timetable.index);
});
}
}
});
}
void prefetch() async {
if (_prefetched) return;
@@ -245,6 +269,8 @@ class _HomeScreenState extends FirkaState<HomeScreen> {
if (mounted) setState(() {});
});
_setupNotificationListener();
prefetch();
_preloadImages();

View File

@@ -719,6 +719,31 @@ class _SettingsScreenState extends FirkaState<SettingsScreen> {
));
continue;
}
if (item is SettingsButton) {
widgets.add(GestureDetector(
child: FirkaCard(
left: [
item.iconType != null
? Row(
children: [
FirkaIconWidget(item.iconType!, item.iconData!,
color: appStyle.colors.accent),
SizedBox(width: 8),
],
)
: SizedBox(),
Text(item.title,
style: appStyle.fonts.B_16SB
.apply(color: appStyle.colors.textPrimary))
],
),
onTap: () async {
await item.onTap();
},
));
continue;
}
if (item is SettingsLogs) {
final logFileRegex = RegExp(r'^(\d{4})_(\d{2})_(\d{2})\.log$');