From 2a4836c42f16dfad4467023fe0e72a0917344c28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Horv=C3=A1th=20Gergely?= Date: Wed, 28 Jan 2026 23:37:27 +0100 Subject: [PATCH] Improve iOS widgets: UI, timeline and JSON fixes Multiple widget improvements and bug fixes: - Models: WidgetGrade now exposes subjectNameWithWeight and teacherName for display. - AveragesProvider: added isFiltered flag, improved subject filtering logic and propagated flag to entries. - Averages views: show filtered count badge, use configurable max visible items, and respect isFiltered. - Grades views: display subject name with weight, show teacher in grade rows (renamed flag to showTeacher) and preserve topic display. - TimetableProvider: build unique transition times for timeline updates, sort entries, and determine current lesson using the provided date instead of Date(). - Timetable views: compute visibleLessons window around the current lesson based on entry.date and use isLessonActive checks against entry.date. - iOS widget JSON helper: fix subject.category serialization structure, use grade.creationDate for recordDate, and use grade.teacher for teacherName. These changes fix incorrect JSON exports, improve timeline accuracy, and enhance widget UX when subjects are filtered or when showing teacher/weight info. --- .../HomeWidgetsExtension/Models/Grade.swift | 11 ++++ .../Providers/AveragesProvider.swift | 12 ++-- .../Providers/TimetableProvider.swift | 17 ++++-- .../Views/AveragesViews.swift | 60 +++++++++++++++++-- .../Views/GradesViews.swift | 23 +++---- .../Views/TimetableViews.swift | 47 +++++++++++---- firka/lib/helpers/db/ios_widget_helper.dart | 21 +++---- 7 files changed, 145 insertions(+), 46 deletions(-) diff --git a/firka/ios/HomeWidgetsExtension/Models/Grade.swift b/firka/ios/HomeWidgetsExtension/Models/Grade.swift index d44a9c95..8bb36401 100644 --- a/firka/ios/HomeWidgetsExtension/Models/Grade.swift +++ b/firka/ios/HomeWidgetsExtension/Models/Grade.swift @@ -37,4 +37,15 @@ struct WidgetGrade: Codable, Identifiable { default: return .gray } } + + var subjectNameWithWeight: String { + if let weight = weightPercentage, weight != 100 { + return "\(subject.name) (\(weight)%)" + } + return subject.name + } + + var teacherName: String? { + subject.teacherName + } } diff --git a/firka/ios/HomeWidgetsExtension/Providers/AveragesProvider.swift b/firka/ios/HomeWidgetsExtension/Providers/AveragesProvider.swift index f4db0270..5f92eedd 100644 --- a/firka/ios/HomeWidgetsExtension/Providers/AveragesProvider.swift +++ b/firka/ios/HomeWidgetsExtension/Providers/AveragesProvider.swift @@ -7,6 +7,7 @@ struct AveragesEntry: TimelineEntry { let overallAverage: Double? let subjectAverages: [SubjectAverage] let locale: String + let isFiltered: Bool } struct AveragesProvider: AppIntentTimelineProvider { @@ -19,7 +20,8 @@ struct AveragesProvider: AppIntentTimelineProvider { configuration: AveragesWidgetIntent(), overallAverage: nil, subjectAverages: [], - locale: "hu" + locale: "hu", + isFiltered: false ) } @@ -39,9 +41,10 @@ struct AveragesProvider: AppIntentTimelineProvider { let data = WidgetData.load() var subjectAverages = data?.averages.subjects ?? [] + let isFiltered = configuration.selectedSubjects?.isEmpty == false - if let selectedSubjects = configuration.selectedSubjects, !selectedSubjects.isEmpty { - let selectedIds = Set(selectedSubjects.map { $0.id }) + if isFiltered { + let selectedIds = Set(configuration.selectedSubjects!.map { $0.id }) subjectAverages = subjectAverages.filter { selectedIds.contains($0.uid) } } @@ -50,7 +53,8 @@ struct AveragesProvider: AppIntentTimelineProvider { configuration: configuration, overallAverage: data?.averages.overall, subjectAverages: subjectAverages, - locale: data?.locale ?? "hu" + locale: data?.locale ?? "hu", + isFiltered: isFiltered ) } } diff --git a/firka/ios/HomeWidgetsExtension/Providers/TimetableProvider.swift b/firka/ios/HomeWidgetsExtension/Providers/TimetableProvider.swift index b58a50fa..a0af11ef 100644 --- a/firka/ios/HomeWidgetsExtension/Providers/TimetableProvider.swift +++ b/firka/ios/HomeWidgetsExtension/Providers/TimetableProvider.swift @@ -73,17 +73,25 @@ struct TimetableProvider: AppIntentTimelineProvider { entries.append(createEntry(for: configuration, date: now)) + var transitionTimes: Set = [] + for lesson in todayLessons { if lesson.start > now { - entries.append(createEntry(for: configuration, date: lesson.start)) + transitionTimes.insert(lesson.start) } if lesson.end > now { - entries.append(createEntry(for: configuration, date: lesson.end.addingTimeInterval(1))) + transitionTimes.insert(lesson.end.addingTimeInterval(1)) } } let midnight = Calendar.current.startOfDay(for: now.addingTimeInterval(86400)) - entries.append(createEntry(for: configuration, date: midnight)) + transitionTimes.insert(midnight) + + for time in transitionTimes { + entries.append(createEntry(for: configuration, date: time)) + } + + entries.sort { $0.date < $1.date } return Timeline(entries: entries, policy: .atEnd) } @@ -166,8 +174,7 @@ struct TimetableProvider: AppIntentTimelineProvider { } let currentLesson = lessons.first { lesson in - let now = Date() - return now >= lesson.start && now <= lesson.end + return date >= lesson.start && date <= lesson.end } let nextLesson = lessons.first { $0.start > date } diff --git a/firka/ios/HomeWidgetsExtension/Views/AveragesViews.swift b/firka/ios/HomeWidgetsExtension/Views/AveragesViews.swift index cf23934d..3ac01f2b 100644 --- a/firka/ios/HomeWidgetsExtension/Views/AveragesViews.swift +++ b/firka/ios/HomeWidgetsExtension/Views/AveragesViews.swift @@ -47,20 +47,46 @@ struct AveragesSmallView: View { struct AveragesMediumView: View { let entry: AveragesEntry let localization: WidgetLocalization + private let maxVisible = 4 var style: WidgetStyleType { (entry.configuration.style ?? .appTheme) == .liquidGlass ? .liquidGlass : .appTheme } + var totalCount: Int { + entry.subjectAverages.count + } + + var showingCount: Int { + min(totalCount, maxVisible) + } + var body: some View { ZStack { WidgetBackground(style: style, colors: nil) VStack(alignment: .leading, spacing: 8) { - Text(localization.string("subject_averages")) - .font(.caption) - .fontWeight(.medium) - .widgetTextStyle(style, colors: nil, isPrimary: false) + HStack { + Text(localization.string("subject_averages")) + .font(.caption) + .fontWeight(.medium) + .widgetTextStyle(style, colors: nil, isPrimary: false) + + Spacer() + + if entry.isFiltered && totalCount > maxVisible { + Text("\(showingCount)/\(totalCount)") + .font(.caption2) + .fontWeight(.medium) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background( + Capsule() + .fill(Color.secondary.opacity(0.2)) + ) + .widgetTextStyle(style, colors: nil, isPrimary: false) + } + } if entry.subjectAverages.isEmpty { Spacer() @@ -69,7 +95,7 @@ struct AveragesMediumView: View { .widgetTextStyle(style, colors: nil, isPrimary: false) Spacer() } else { - ForEach(entry.subjectAverages.prefix(4)) { subject in + ForEach(entry.subjectAverages.prefix(maxVisible)) { subject in AverageRow(subject: subject, style: style) } Spacer() @@ -83,11 +109,20 @@ struct AveragesMediumView: View { struct AveragesLargeView: View { let entry: AveragesEntry let localization: WidgetLocalization + private let maxVisible = 9 var style: WidgetStyleType { (entry.configuration.style ?? .appTheme) == .liquidGlass ? .liquidGlass : .appTheme } + var totalCount: Int { + entry.subjectAverages.count + } + + var showingCount: Int { + min(totalCount, maxVisible) + } + var body: some View { ZStack { WidgetBackground(style: style, colors: nil) @@ -101,6 +136,19 @@ struct AveragesLargeView: View { Spacer() + if entry.isFiltered && totalCount > maxVisible { + Text("\(showingCount)/\(totalCount)") + .font(.caption) + .fontWeight(.medium) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background( + Capsule() + .fill(Color.secondary.opacity(0.2)) + ) + .widgetTextStyle(style, colors: nil, isPrimary: false) + } + if let overall = entry.overallAverage { Text(String(format: "%.2f", overall)) .font(.headline) @@ -116,7 +164,7 @@ struct AveragesLargeView: View { .widgetTextStyle(style, colors: nil, isPrimary: false) Spacer() } else { - ForEach(entry.subjectAverages.prefix(8)) { subject in + ForEach(entry.subjectAverages.prefix(maxVisible)) { subject in AverageRow(subject: subject, style: style, showGradeCount: true) } Spacer() diff --git a/firka/ios/HomeWidgetsExtension/Views/GradesViews.swift b/firka/ios/HomeWidgetsExtension/Views/GradesViews.swift index 22bb9d0d..ab69ce5c 100644 --- a/firka/ios/HomeWidgetsExtension/Views/GradesViews.swift +++ b/firka/ios/HomeWidgetsExtension/Views/GradesViews.swift @@ -29,7 +29,7 @@ struct GradesSmallView: View { Spacer() } - Text(grade.subject.name) + Text(grade.subjectNameWithWeight) .font(.subheadline) .fontWeight(.medium) .widgetTextStyle(style, colors: nil) @@ -78,7 +78,7 @@ struct GradesMediumView: View { Spacer() } else { ForEach(entry.grades.prefix(3)) { grade in - GradeRow(grade: grade, style: style, showType: true) + GradeRow(grade: grade, style: style, showTeacher: true) } } } @@ -114,7 +114,7 @@ struct GradesLargeView: View { Spacer() } else { ForEach(entry.grades.prefix(6)) { grade in - GradeRow(grade: grade, style: style, showType: true, showTopic: true) + GradeRow(grade: grade, style: style, showTeacher: true, showTopic: true) } } } @@ -127,7 +127,7 @@ struct GradesLargeView: View { struct GradeRow: View { let grade: WidgetGrade let style: WidgetStyleType - var showType: Bool = false + var showTeacher: Bool = false var showTopic: Bool = false var body: some View { @@ -139,23 +139,24 @@ struct GradeRow: View { .frame(width: 32) VStack(alignment: .leading, spacing: 2) { - Text(grade.subject.name) + Text(grade.subjectNameWithWeight) .font(.subheadline) .fontWeight(.medium) .widgetTextStyle(style, colors: nil) .lineLimit(1) HStack(spacing: 4) { - if showType { - Text(grade.type.name) + if showTeacher, let teacher = grade.teacherName { + Text(teacher) + .font(.caption) + .widgetTextStyle(style, colors: nil, isPrimary: false) + .lineLimit(1) + + Text("•") .font(.caption) .widgetTextStyle(style, colors: nil, isPrimary: false) } - Text("•") - .font(.caption) - .widgetTextStyle(style, colors: nil, isPrimary: false) - Text(grade.dateString) .font(.caption) .widgetTextStyle(style, colors: nil, isPrimary: false) diff --git a/firka/ios/HomeWidgetsExtension/Views/TimetableViews.swift b/firka/ios/HomeWidgetsExtension/Views/TimetableViews.swift index 1fcf8e5d..13334db6 100644 --- a/firka/ios/HomeWidgetsExtension/Views/TimetableViews.swift +++ b/firka/ios/HomeWidgetsExtension/Views/TimetableViews.swift @@ -79,9 +79,36 @@ struct TimetableMediumView: View { (entry.configuration.style ?? .appTheme) == .liquidGlass ? .liquidGlass : .appTheme } - var remainingLessons: [WidgetLesson] { - let now = Date() - return entry.lessons.filter { $0.end > now } + func isLessonActive(_ lesson: WidgetLesson) -> Bool { + let checkDate = entry.date + return checkDate >= lesson.start && checkDate <= lesson.end + } + + var currentLessonIndex: Int { + let checkDate = entry.date + if let index = entry.lessons.firstIndex(where: { checkDate >= $0.start && checkDate <= $0.end }) { + return index + } + if let index = entry.lessons.firstIndex(where: { $0.start > checkDate }) { + return index + } + return max(0, entry.lessons.count - 1) + } + + var visibleLessons: [WidgetLesson] { + let totalLessons = entry.lessons.count + let maxVisible = 4 + + if totalLessons <= maxVisible { + return Array(entry.lessons) + } + + var startIndex = max(0, currentLessonIndex - 1) + + startIndex = min(startIndex, totalLessons - maxVisible) + + let endIndex = min(startIndex + maxVisible, totalLessons) + return Array(entry.lessons[startIndex.. now } + func isLessonActive(_ lesson: WidgetLesson) -> Bool { + let checkDate = entry.date + return checkDate >= lesson.start && checkDate <= lesson.end } var body: some View { @@ -127,8 +154,8 @@ struct TimetableLargeView: View { .fontWeight(.semibold) .widgetTextStyle(style, colors: nil) - ForEach(remainingLessons.prefix(7)) { lesson in - LessonRow(lesson: lesson, isActive: lesson.isCurrentlyActive, style: style, showRoom: true) + ForEach(entry.lessons.prefix(7)) { lesson in + LessonRow(lesson: lesson, isActive: isLessonActive(lesson), style: style, showRoom: true) } } .padding() diff --git a/firka/lib/helpers/db/ios_widget_helper.dart b/firka/lib/helpers/db/ios_widget_helper.dart index 325975e9..cecebb86 100644 --- a/firka/lib/helpers/db/ios_widget_helper.dart +++ b/firka/lib/helpers/db/ios_widget_helper.dart @@ -108,10 +108,10 @@ class IOSWidgetHelper { 'subject': subject != null ? { 'uid': subject.uid, 'name': subject.name, - 'category': subject.category != null ? { - 'uid': subject.category!.uid, - 'name': subject.category!.name, - 'description': subject.category!.description, + 'category': subject.category, { + 'uid': subject.category.uid, + 'name': subject.category.name, + 'description': subject.category.description, } : null, 'sortIndex': subject.sortIndex, 'teacherName': subject.teacherName, @@ -132,17 +132,18 @@ class IOSWidgetHelper { static Map _gradeToJson(Grade grade) { return { 'uid': grade.uid, - 'recordDate': _formatDateTimeWithOffset(grade.recordDate), + 'recordDate': _formatDateTimeWithOffset(grade.creationDate), 'subject': { 'uid': grade.subject.uid, 'name': grade.subject.name, - 'category': grade.subject.category != null ? { - 'uid': grade.subject.category!.uid, - 'name': grade.subject.category!.name, - 'description': grade.subject.category!.description, + 'category': grade.subject.category, { + 'uid': grade.subject.category.uid, + 'name': grade.subject.category.name, + 'description': grade.subject.category.description, } : null, 'sortIndex': grade.subject.sortIndex, - 'teacherName': grade.subject.teacherName, + // Use the grade's teacher field, not subject.teacherName (which is usually null for grades) + 'teacherName': grade.teacher, }, 'topic': grade.topic, 'type': {