forked from firka/firka
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.
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,17 +73,25 @@ struct TimetableProvider: AppIntentTimelineProvider {
|
||||
|
||||
entries.append(createEntry(for: configuration, date: now))
|
||||
|
||||
var transitionTimes: Set<Date> = []
|
||||
|
||||
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 }
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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..<endIndex])
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
@@ -94,8 +121,8 @@ struct TimetableMediumView: View {
|
||||
.fontWeight(.medium)
|
||||
.widgetTextStyle(style, colors: nil, isPrimary: false)
|
||||
|
||||
ForEach(remainingLessons.prefix(4)) { lesson in
|
||||
LessonRow(lesson: lesson, isActive: lesson.isCurrentlyActive, style: style)
|
||||
ForEach(visibleLessons) { lesson in
|
||||
LessonRow(lesson: lesson, isActive: isLessonActive(lesson), style: style)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
@@ -112,9 +139,9 @@ struct TimetableLargeView: 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 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()
|
||||
|
||||
@@ -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<String, dynamic> _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': {
|
||||
|
||||
Reference in New Issue
Block a user