From d06a3dee6982bdfb723faf5bd15fc6d434fc7124 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 d44a9c9..8bb3640 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 f4db027..5f92eed 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 b58a50f..a0af11e 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 cf23934..3ac01f2 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 22bb9d0..ab69ce5 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 1fcf8e5..13334db 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 325975e..cecebb8 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': {