From beb4127ef82bcf6b857b15c79bf4c7f0821bf384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Horv=C3=A1th=20Gergely?= Date: Tue, 27 Jan 2026 18:58:25 +0100 Subject: [PATCH] Add iOS Home Widgets Extension with timetable, grades, and averages Introduces a new HomeWidgetsExtension for iOS, including widgets for timetable, recent grades, and subject averages. Adds widget models, providers, intents, localization, and SwiftUI views. Updates project files and main app to support widget data sharing and communication. Also includes new Dart helper for widget data and updates to relevant Flutter screens. --- firka/ios/HomeWidgetsExtension.entitlements | 10 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 ++ .../Assets.xcassets/Contents.json | 6 + .../WidgetBackground.colorset/Contents.json | 11 + .../HomeWidgetsExtension/AveragesWidget.swift | 42 ++ .../HomeWidgetsExtension/GradesWidget.swift | 42 ++ .../Helpers/Localization.swift | 123 +++++ .../HomeWidgetsBundle.swift | 11 + firka/ios/HomeWidgetsExtension/Info.plist | 11 + .../Intents/AveragesIntent.swift | 47 ++ .../Intents/GradesIntent.swift | 10 + .../Intents/TimetableIntent.swift | 45 ++ .../HomeWidgetsExtension/Models/Average.swift | 18 + .../HomeWidgetsExtension/Models/Grade.swift | 40 ++ .../HomeWidgetsExtension/Models/Lesson.swift | 37 ++ .../HomeWidgetsExtension/Models/Subject.swift | 15 + .../Models/WidgetColors.swift | 30 ++ .../Models/WidgetData.swift | 129 +++++ .../Providers/AveragesProvider.swift | 56 ++ .../Providers/GradesProvider.swift | 41 ++ .../Providers/TimetableProvider.swift | 167 ++++++ .../TimetableWidget.swift | 57 ++ .../Views/AveragesViews.swift | 167 ++++++ .../Views/GradesViews.swift | 169 ++++++ .../Views/TimetableViews.swift | 257 +++++++++ .../Views/WidgetStyles.swift | 102 ++++ .../de.lproj/Localizable.strings | 28 + .../en.lproj/Localizable.strings | 28 + .../hu.lproj/Localizable.strings | 28 + ...HomeWidgetsExtensionExtension.entitlements | 10 + firka/ios/Runner.xcodeproj/project.pbxproj | 486 +++++++++++++----- firka/ios/Runner/AppDelegate.swift | 2 + .../ios/Runner/HomeWidgetMethodChannel.swift | 34 ++ firka/ios/Runner/Runner.entitlements | 2 +- firka/lib/helpers/db/ios_widget_helper.dart | 193 +++++++ firka/lib/helpers/db/widget.dart | 173 +++++++ firka/lib/main.dart | 4 + .../ui/phone/screens/home/home_screen.dart | 4 + .../screens/settings/settings_screen.dart | 2 + 40 files changed, 2563 insertions(+), 120 deletions(-) create mode 100644 firka/ios/HomeWidgetsExtension.entitlements create mode 100644 firka/ios/HomeWidgetsExtension/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 firka/ios/HomeWidgetsExtension/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 firka/ios/HomeWidgetsExtension/Assets.xcassets/Contents.json create mode 100644 firka/ios/HomeWidgetsExtension/Assets.xcassets/WidgetBackground.colorset/Contents.json create mode 100644 firka/ios/HomeWidgetsExtension/AveragesWidget.swift create mode 100644 firka/ios/HomeWidgetsExtension/GradesWidget.swift create mode 100644 firka/ios/HomeWidgetsExtension/Helpers/Localization.swift create mode 100644 firka/ios/HomeWidgetsExtension/HomeWidgetsBundle.swift create mode 100644 firka/ios/HomeWidgetsExtension/Info.plist create mode 100644 firka/ios/HomeWidgetsExtension/Intents/AveragesIntent.swift create mode 100644 firka/ios/HomeWidgetsExtension/Intents/GradesIntent.swift create mode 100644 firka/ios/HomeWidgetsExtension/Intents/TimetableIntent.swift create mode 100644 firka/ios/HomeWidgetsExtension/Models/Average.swift create mode 100644 firka/ios/HomeWidgetsExtension/Models/Grade.swift create mode 100644 firka/ios/HomeWidgetsExtension/Models/Lesson.swift create mode 100644 firka/ios/HomeWidgetsExtension/Models/Subject.swift create mode 100644 firka/ios/HomeWidgetsExtension/Models/WidgetColors.swift create mode 100644 firka/ios/HomeWidgetsExtension/Models/WidgetData.swift create mode 100644 firka/ios/HomeWidgetsExtension/Providers/AveragesProvider.swift create mode 100644 firka/ios/HomeWidgetsExtension/Providers/GradesProvider.swift create mode 100644 firka/ios/HomeWidgetsExtension/Providers/TimetableProvider.swift create mode 100644 firka/ios/HomeWidgetsExtension/TimetableWidget.swift create mode 100644 firka/ios/HomeWidgetsExtension/Views/AveragesViews.swift create mode 100644 firka/ios/HomeWidgetsExtension/Views/GradesViews.swift create mode 100644 firka/ios/HomeWidgetsExtension/Views/TimetableViews.swift create mode 100644 firka/ios/HomeWidgetsExtension/Views/WidgetStyles.swift create mode 100644 firka/ios/HomeWidgetsExtension/de.lproj/Localizable.strings create mode 100644 firka/ios/HomeWidgetsExtension/en.lproj/Localizable.strings create mode 100644 firka/ios/HomeWidgetsExtension/hu.lproj/Localizable.strings create mode 100644 firka/ios/HomeWidgetsExtensionExtension.entitlements create mode 100644 firka/ios/Runner/HomeWidgetMethodChannel.swift create mode 100644 firka/lib/helpers/db/ios_widget_helper.dart diff --git a/firka/ios/HomeWidgetsExtension.entitlements b/firka/ios/HomeWidgetsExtension.entitlements new file mode 100644 index 0000000..471c7d3 --- /dev/null +++ b/firka/ios/HomeWidgetsExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.app.firka.firkaa + + + diff --git a/firka/ios/HomeWidgetsExtension/Assets.xcassets/AccentColor.colorset/Contents.json b/firka/ios/HomeWidgetsExtension/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/firka/ios/HomeWidgetsExtension/Assets.xcassets/AppIcon.appiconset/Contents.json b/firka/ios/HomeWidgetsExtension/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/firka/ios/HomeWidgetsExtension/Assets.xcassets/Contents.json b/firka/ios/HomeWidgetsExtension/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/firka/ios/HomeWidgetsExtension/Assets.xcassets/WidgetBackground.colorset/Contents.json b/firka/ios/HomeWidgetsExtension/Assets.xcassets/WidgetBackground.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/Assets.xcassets/WidgetBackground.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/firka/ios/HomeWidgetsExtension/AveragesWidget.swift b/firka/ios/HomeWidgetsExtension/AveragesWidget.swift new file mode 100644 index 0000000..f148d3b --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/AveragesWidget.swift @@ -0,0 +1,42 @@ +import WidgetKit +import SwiftUI + +struct AveragesWidget: Widget { + let kind: String = "AveragesWidget" + + var body: some WidgetConfiguration { + AppIntentConfiguration( + kind: kind, + intent: AveragesWidgetIntent.self, + provider: AveragesProvider() + ) { entry in + AveragesWidgetView(entry: entry) + .containerBackground(.clear, for: .widget) + } + .configurationDisplayName(LocalizedStringResource("widget_averages_title", defaultValue: "Averages")) + .description(LocalizedStringResource("widget_averages_description", defaultValue: "Shows subject averages")) + .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) + } +} + +struct AveragesWidgetView: View { + @Environment(\.widgetFamily) var family + let entry: AveragesEntry + + var localization: WidgetLocalization { + WidgetLocalization(locale: entry.locale) + } + + var body: some View { + switch family { + case .systemSmall: + AveragesSmallView(entry: entry, localization: localization) + case .systemMedium: + AveragesMediumView(entry: entry, localization: localization) + case .systemLarge: + AveragesLargeView(entry: entry, localization: localization) + default: + AveragesMediumView(entry: entry, localization: localization) + } + } +} diff --git a/firka/ios/HomeWidgetsExtension/GradesWidget.swift b/firka/ios/HomeWidgetsExtension/GradesWidget.swift new file mode 100644 index 0000000..01fe1b7 --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/GradesWidget.swift @@ -0,0 +1,42 @@ +import WidgetKit +import SwiftUI + +struct GradesWidget: Widget { + let kind: String = "GradesWidget" + + var body: some WidgetConfiguration { + AppIntentConfiguration( + kind: kind, + intent: GradesWidgetIntent.self, + provider: GradesProvider() + ) { entry in + GradesWidgetView(entry: entry) + .containerBackground(.clear, for: .widget) + } + .configurationDisplayName(LocalizedStringResource("widget_grades_title", defaultValue: "Recent Grades")) + .description(LocalizedStringResource("widget_grades_description", defaultValue: "Shows your recent grades")) + .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) + } +} + +struct GradesWidgetView: View { + @Environment(\.widgetFamily) var family + let entry: GradesEntry + + var localization: WidgetLocalization { + WidgetLocalization(locale: entry.locale) + } + + var body: some View { + switch family { + case .systemSmall: + GradesSmallView(entry: entry, localization: localization) + case .systemMedium: + GradesMediumView(entry: entry, localization: localization) + case .systemLarge: + GradesLargeView(entry: entry, localization: localization) + default: + GradesMediumView(entry: entry, localization: localization) + } + } +} diff --git a/firka/ios/HomeWidgetsExtension/Helpers/Localization.swift b/firka/ios/HomeWidgetsExtension/Helpers/Localization.swift new file mode 100644 index 0000000..2bad8db --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/Helpers/Localization.swift @@ -0,0 +1,123 @@ +import Foundation + +struct WidgetLocalization { + let locale: String + + init(locale: String = "hu") { + self.locale = locale + } + + private var translations: [String: [String: String]] { + [ + "today_timetable": [ + "hu": "Mai órarend", + "en": "Today's timetable", + "de": "Stundenplan heute" + ], + "tomorrow_timetable": [ + "hu": "Holnapi órarend", + "en": "Tomorrow's timetable", + "de": "Stundenplan morgen" + ], + "current_lesson": [ + "hu": "Jelenlegi óra", + "en": "Current lesson", + "de": "Aktuelle Stunde" + ], + "next_lesson": [ + "hu": "Következő óra", + "en": "Next lesson", + "de": "Nächste Stunde" + ], + "recent_grades": [ + "hu": "Legutóbbi jegyek", + "en": "Recent grades", + "de": "Letzte Noten" + ], + "subject_averages": [ + "hu": "Tantárgyi átlagok", + "en": "Subject averages", + "de": "Fachdurchschnitte" + ], + "overall_average": [ + "hu": "Tanulmányi átlag", + "en": "Overall average", + "de": "Gesamtdurchschnitt" + ], + "no_lessons": [ + "hu": "Nincs több óra ma", + "en": "No more lessons today", + "de": "Keine Stunden mehr heute" + ], + "no_grades": [ + "hu": "Még nincsenek jegyeid", + "en": "No grades yet", + "de": "Noch keine Noten" + ], + "no_averages": [ + "hu": "Még nincsenek átlagok", + "en": "No averages yet", + "de": "Noch keine Durchschnitte" + ], + "login_required": [ + "hu": "Jelentkezz be újra", + "en": "Please log in again", + "de": "Bitte erneut anmelden" + ], + "timetable_unavailable": [ + "hu": "Az órarend még nem elérhető", + "en": "Timetable not available yet", + "de": "Stundenplan noch nicht verfügbar" + ], + "happy_break": [ + "hu": "Kellemes %@ szünetet!", + "en": "Happy %@ break!", + "de": "Schöne %@ Ferien!" + ], + "days_remaining": [ + "hu": "Még %d nap", + "en": "%d days left", + "de": "Noch %d Tage" + ], + "break_autumn": [ + "hu": "őszi", + "en": "autumn", + "de": "Herbst" + ], + "break_winter": [ + "hu": "téli", + "en": "winter", + "de": "Winter" + ], + "break_spring": [ + "hu": "tavaszi", + "en": "spring", + "de": "Frühlings" + ], + "break_summer": [ + "hu": "nyári", + "en": "summer", + "de": "Sommer" + ], + "room": [ + "hu": "Terem", + "en": "Room", + "de": "Raum" + ] + ] + } + + func string(_ key: String) -> String { + translations[key]?[locale] ?? translations[key]?["hu"] ?? key + } + + func string(_ key: String, _ arg: String) -> String { + let template = string(key) + return template.replacingOccurrences(of: "%@", with: arg) + } + + func string(_ key: String, _ arg: Int) -> String { + let template = string(key) + return template.replacingOccurrences(of: "%d", with: "\(arg)") + } +} diff --git a/firka/ios/HomeWidgetsExtension/HomeWidgetsBundle.swift b/firka/ios/HomeWidgetsExtension/HomeWidgetsBundle.swift new file mode 100644 index 0000000..bec789d --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/HomeWidgetsBundle.swift @@ -0,0 +1,11 @@ +import WidgetKit +import SwiftUI + +@main +struct HomeWidgetsBundle: WidgetBundle { + var body: some Widget { + TimetableWidget() + GradesWidget() + AveragesWidget() + } +} diff --git a/firka/ios/HomeWidgetsExtension/Info.plist b/firka/ios/HomeWidgetsExtension/Info.plist new file mode 100644 index 0000000..0f118fb --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/Info.plist @@ -0,0 +1,11 @@ + + + + + NSExtension + + NSExtensionPointIdentifier + com.apple.widgetkit-extension + + + diff --git a/firka/ios/HomeWidgetsExtension/Intents/AveragesIntent.swift b/firka/ios/HomeWidgetsExtension/Intents/AveragesIntent.swift new file mode 100644 index 0000000..1366071 --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/Intents/AveragesIntent.swift @@ -0,0 +1,47 @@ +import AppIntents +import WidgetKit + +struct SubjectEntity: AppEntity { + let id: String + let name: String + + static var typeDisplayRepresentation: TypeDisplayRepresentation { + TypeDisplayRepresentation(name: LocalizedStringResource("subjects_type", defaultValue: "Subjects")) + } + + static var defaultQuery = SubjectQuery() + + var displayRepresentation: DisplayRepresentation { + DisplayRepresentation(title: "\(name)") + } +} + +struct SubjectQuery: EntityQuery { + func entities(for identifiers: [String]) async throws -> [SubjectEntity] { + let data = WidgetData.load() + return data?.averages.subjects + .filter { identifiers.contains($0.uid) } + .map { SubjectEntity(id: $0.uid, name: $0.name) } ?? [] + } + + func suggestedEntities() async throws -> [SubjectEntity] { + let data = WidgetData.load() + return data?.averages.subjects + .map { SubjectEntity(id: $0.uid, name: $0.name) } ?? [] + } + + func defaultResult() async -> SubjectEntity? { + try? await suggestedEntities().first + } +} + +struct AveragesWidgetIntent: WidgetConfigurationIntent { + static var title: LocalizedStringResource = LocalizedStringResource("widget_averages_title", defaultValue: "Averages") + static var description: IntentDescription = IntentDescription(LocalizedStringResource("widget_averages_description", defaultValue: "Shows subject averages")) + + @Parameter(title: LocalizedStringResource("param_style", defaultValue: "Style"), default: .appTheme) + var style: WidgetStyle? + + @Parameter(title: LocalizedStringResource("param_subjects", defaultValue: "Subjects")) + var selectedSubjects: [SubjectEntity]? +} diff --git a/firka/ios/HomeWidgetsExtension/Intents/GradesIntent.swift b/firka/ios/HomeWidgetsExtension/Intents/GradesIntent.swift new file mode 100644 index 0000000..32d844b --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/Intents/GradesIntent.swift @@ -0,0 +1,10 @@ +import AppIntents +import WidgetKit + +struct GradesWidgetIntent: WidgetConfigurationIntent { + static var title: LocalizedStringResource = LocalizedStringResource("widget_grades_title", defaultValue: "Recent Grades") + static var description: IntentDescription = IntentDescription(LocalizedStringResource("widget_grades_description", defaultValue: "Shows your recent grades")) + + @Parameter(title: LocalizedStringResource("param_style", defaultValue: "Style"), default: .appTheme) + var style: WidgetStyle? +} diff --git a/firka/ios/HomeWidgetsExtension/Intents/TimetableIntent.swift b/firka/ios/HomeWidgetsExtension/Intents/TimetableIntent.swift new file mode 100644 index 0000000..72389c3 --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/Intents/TimetableIntent.swift @@ -0,0 +1,45 @@ +import AppIntents +import WidgetKit + +enum TimetableDisplayMode: String, AppEnum { + case current = "current" + case next = "next" + + static var typeDisplayRepresentation: TypeDisplayRepresentation { + TypeDisplayRepresentation(name: LocalizedStringResource("display_mode_type", defaultValue: "Display Mode")) + } + + static var caseDisplayRepresentations: [TimetableDisplayMode: DisplayRepresentation] { + [ + .current: DisplayRepresentation(title: LocalizedStringResource("display_mode_current", defaultValue: "Current Lesson")), + .next: DisplayRepresentation(title: LocalizedStringResource("display_mode_next", defaultValue: "Next Lesson")) + ] + } +} + +enum WidgetStyle: String, AppEnum { + case liquidGlass = "liquid_glass" + case appTheme = "app_theme" + + static var typeDisplayRepresentation: TypeDisplayRepresentation { + TypeDisplayRepresentation(name: LocalizedStringResource("style_type", defaultValue: "Style")) + } + + static var caseDisplayRepresentations: [WidgetStyle: DisplayRepresentation] { + [ + .liquidGlass: DisplayRepresentation(title: LocalizedStringResource("style_liquid_glass", defaultValue: "Liquid Glass")), + .appTheme: DisplayRepresentation(title: LocalizedStringResource("style_app_theme", defaultValue: "App Theme")) + ] + } +} + +struct TimetableWidgetIntent: WidgetConfigurationIntent { + static var title: LocalizedStringResource = LocalizedStringResource("widget_timetable_title", defaultValue: "Timetable") + static var description: IntentDescription = IntentDescription(LocalizedStringResource("widget_timetable_description", defaultValue: "Shows your daily timetable")) + + @Parameter(title: LocalizedStringResource("param_style", defaultValue: "Style"), default: .appTheme) + var style: WidgetStyle? + + @Parameter(title: LocalizedStringResource("param_display_mode_small", defaultValue: "Small Widget Display"), default: .current) + var displayMode: TimetableDisplayMode? +} diff --git a/firka/ios/HomeWidgetsExtension/Models/Average.swift b/firka/ios/HomeWidgetsExtension/Models/Average.swift new file mode 100644 index 0000000..8a8ce00 --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/Models/Average.swift @@ -0,0 +1,18 @@ +import Foundation +import SwiftUI + +extension SubjectAverage { + var averageColor: Color { + switch average { + case 4.5...: return .green + case 3.5..<4.5: return .blue + case 2.5..<3.5: return .yellow + case 1.5..<2.5: return .orange + default: return .red + } + } + + var formattedAverage: String { + String(format: "%.2f", average) + } +} diff --git a/firka/ios/HomeWidgetsExtension/Models/Grade.swift b/firka/ios/HomeWidgetsExtension/Models/Grade.swift new file mode 100644 index 0000000..d44a9c9 --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/Models/Grade.swift @@ -0,0 +1,40 @@ +import Foundation +import SwiftUI + +struct WidgetGrade: Codable, Identifiable { + let uid: String + let recordDate: Date + let subject: WidgetSubject + let topic: String? + let type: NameUidDesc + let numericValue: Int? + let strValue: String? + let weightPercentage: Int? + + var id: String { uid } + + var displayValue: String { + if let numeric = numericValue { + return "\(numeric)" + } + return strValue ?? "" + } + + var dateString: String { + let formatter = DateFormatter() + formatter.dateFormat = "MMM d." + return formatter.string(from: recordDate) + } + + var gradeColor: Color { + guard let value = numericValue else { return .gray } + switch value { + case 5: return .green + case 4: return .blue + case 3: return .yellow + case 2: return .orange + case 1: return .red + default: return .gray + } + } +} diff --git a/firka/ios/HomeWidgetsExtension/Models/Lesson.swift b/firka/ios/HomeWidgetsExtension/Models/Lesson.swift new file mode 100644 index 0000000..f8719d9 --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/Models/Lesson.swift @@ -0,0 +1,37 @@ +import Foundation + +struct WidgetLesson: Codable, Identifiable { + let uid: String + let date: String + let start: Date + let end: Date + let name: String + let lessonNumber: Int? + let teacher: String? + let subject: WidgetSubject + let theme: String? + let roomName: String? + let isCancelled: Bool + let isSubstitution: Bool + + var id: String { uid } + + var displayName: String { + subject.name + } + + var timeString: String { + let formatter = DateFormatter() + formatter.dateFormat = "HH:mm" + return formatter.string(from: start) + } + + var isCurrentlyActive: Bool { + let now = Date() + return now >= start && now <= end + } + + var isUpcoming: Bool { + return Date() < start + } +} diff --git a/firka/ios/HomeWidgetsExtension/Models/Subject.swift b/firka/ios/HomeWidgetsExtension/Models/Subject.swift new file mode 100644 index 0000000..e1dbba4 --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/Models/Subject.swift @@ -0,0 +1,15 @@ +import Foundation + +struct WidgetSubject: Codable { + let uid: String + let name: String + let category: NameUidDesc? + let sortIndex: Int + let teacherName: String? +} + +struct NameUidDesc: Codable { + let uid: String + let name: String + let description: String? +} diff --git a/firka/ios/HomeWidgetsExtension/Models/WidgetColors.swift b/firka/ios/HomeWidgetsExtension/Models/WidgetColors.swift new file mode 100644 index 0000000..74f4859 --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/Models/WidgetColors.swift @@ -0,0 +1,30 @@ +import SwiftUI + +struct WidgetColors: Codable { + let background: Int + let textPrimary: Int + let textSecondary: Int + let textTertiary: Int + let card: Int + let accent: Int + let grade5: Int + let grade4: Int + let grade3: Int + let grade2: Int + let grade1: Int + + func color(from argb: Int) -> Color { + let alpha = Double((argb >> 24) & 0xFF) / 255.0 + let red = Double((argb >> 16) & 0xFF) / 255.0 + let green = Double((argb >> 8) & 0xFF) / 255.0 + let blue = Double(argb & 0xFF) / 255.0 + return Color(.sRGB, red: red, green: green, blue: blue, opacity: alpha) + } + + var backgroundColor: Color { color(from: background) } + var textPrimaryColor: Color { color(from: textPrimary) } + var textSecondaryColor: Color { color(from: textSecondary) } + var textTertiaryColor: Color { color(from: textTertiary) } + var cardColor: Color { color(from: card) } + var accentColor: Color { color(from: accent) } +} diff --git a/firka/ios/HomeWidgetsExtension/Models/WidgetData.swift b/firka/ios/HomeWidgetsExtension/Models/WidgetData.swift new file mode 100644 index 0000000..d8a1932 --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/Models/WidgetData.swift @@ -0,0 +1,129 @@ +import Foundation + +struct WidgetData: Codable { + let lastUpdated: Date? + let locale: String + let theme: String + let timetable: TimetableData + let grades: [WidgetGrade] + let averages: AveragesData + + static var lastError: String = "Not loaded yet" + + static func load() -> WidgetData? { + guard let containerURL = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: "group.app.firka.firkaa" + ) else { + lastError = "No App Group container" + return nil + } + + let fileURL = containerURL.appendingPathComponent("widget_data.json") + + let fileExists = FileManager.default.fileExists(atPath: fileURL.path) + if !fileExists { + lastError = "File not found at: \(fileURL.path)" + return nil + } + + guard let data = try? Data(contentsOf: fileURL) else { + lastError = "Could not read file" + return nil + } + + lastError = "Read \(data.count) bytes, decoding..." + + let decoder = JSONDecoder() + + let iso8601Full = ISO8601DateFormatter() + iso8601Full.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + + let iso8601 = ISO8601DateFormatter() + iso8601.formatOptions = [.withInternetDateTime] + + let formatterLocal = DateFormatter() + formatterLocal.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS" + formatterLocal.locale = Locale(identifier: "en_US_POSIX") + formatterLocal.timeZone = TimeZone.current + + let formatterShort = DateFormatter() + formatterShort.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" + formatterShort.locale = Locale(identifier: "en_US_POSIX") + formatterShort.timeZone = TimeZone.current + + decoder.dateDecodingStrategy = .custom { decoder in + let container = try decoder.singleValueContainer() + let dateString = try container.decode(String.self) + + if let date = iso8601Full.date(from: dateString) { + return date + } + if let date = iso8601.date(from: dateString) { + return date + } + if let date = formatterLocal.date(from: dateString) { + return date + } + if let date = formatterShort.date(from: dateString) { + return date + } + + throw DecodingError.dataCorruptedError(in: container, debugDescription: "Invalid date: \(dateString)") + } + + do { + let widgetData = try decoder.decode(WidgetData.self, from: data) + lastError = "OK: \(widgetData.timetable.today.count) lessons, \(widgetData.grades.count) grades" + return widgetData + } catch let DecodingError.keyNotFound(key, context) { + lastError = "Missing key: \(key.stringValue) in \(context.codingPath.map { $0.stringValue }.joined(separator: "."))" + return nil + } catch let DecodingError.typeMismatch(type, context) { + lastError = "Type mismatch: \(type) at \(context.codingPath.map { $0.stringValue }.joined(separator: "."))" + return nil + } catch let DecodingError.valueNotFound(type, context) { + lastError = "Null value: \(type) at \(context.codingPath.map { $0.stringValue }.joined(separator: "."))" + return nil + } catch { + lastError = "Other: \(error)" + return nil + } + } + + static var placeholder: WidgetData { + WidgetData( + lastUpdated: nil, + locale: "hu", + theme: "dark", + timetable: TimetableData(today: [], tomorrow: [], currentBreak: nil), + grades: [], + averages: AveragesData(overall: nil, subjects: []) + ) + } +} + +struct TimetableData: Codable { + let today: [WidgetLesson] + let tomorrow: [WidgetLesson] + let currentBreak: BreakInfo? +} + +struct BreakInfo: Codable { + let name: String + let nameKey: String + let endDate: Date +} + +struct AveragesData: Codable { + let overall: Double? + let subjects: [SubjectAverage] +} + +struct SubjectAverage: Codable, Identifiable { + let uid: String + let name: String + let average: Double + let gradeCount: Int + + var id: String { uid } +} diff --git a/firka/ios/HomeWidgetsExtension/Providers/AveragesProvider.swift b/firka/ios/HomeWidgetsExtension/Providers/AveragesProvider.swift new file mode 100644 index 0000000..f4db027 --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/Providers/AveragesProvider.swift @@ -0,0 +1,56 @@ +import WidgetKit +import SwiftUI + +struct AveragesEntry: TimelineEntry { + let date: Date + let configuration: AveragesWidgetIntent + let overallAverage: Double? + let subjectAverages: [SubjectAverage] + let locale: String +} + +struct AveragesProvider: AppIntentTimelineProvider { + typealias Entry = AveragesEntry + typealias Intent = AveragesWidgetIntent + + func placeholder(in context: Context) -> AveragesEntry { + AveragesEntry( + date: Date(), + configuration: AveragesWidgetIntent(), + overallAverage: nil, + subjectAverages: [], + locale: "hu" + ) + } + + func snapshot(for configuration: AveragesWidgetIntent, in context: Context) async -> AveragesEntry { + createEntry(for: configuration) + } + + func timeline(for configuration: AveragesWidgetIntent, in context: Context) async -> Timeline { + let entry = createEntry(for: configuration) + + // Refresh every 30 minutes + let refreshDate = Date().addingTimeInterval(30 * 60) + return Timeline(entries: [entry], policy: .after(refreshDate)) + } + + private func createEntry(for configuration: AveragesWidgetIntent) -> AveragesEntry { + let data = WidgetData.load() + + var subjectAverages = data?.averages.subjects ?? [] + + if let selectedSubjects = configuration.selectedSubjects, !selectedSubjects.isEmpty { + let selectedIds = Set(selectedSubjects.map { $0.id }) + subjectAverages = subjectAverages.filter { selectedIds.contains($0.uid) } + } + + return AveragesEntry( + date: Date(), + configuration: configuration, + overallAverage: data?.averages.overall, + subjectAverages: subjectAverages, + locale: data?.locale ?? "hu" + ) + } +} diff --git a/firka/ios/HomeWidgetsExtension/Providers/GradesProvider.swift b/firka/ios/HomeWidgetsExtension/Providers/GradesProvider.swift new file mode 100644 index 0000000..3b81dd8 --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/Providers/GradesProvider.swift @@ -0,0 +1,41 @@ +import WidgetKit +import SwiftUI + +struct GradesEntry: TimelineEntry { + let date: Date + let configuration: GradesWidgetIntent + let grades: [WidgetGrade] + let locale: String +} + +struct GradesProvider: AppIntentTimelineProvider { + typealias Entry = GradesEntry + typealias Intent = GradesWidgetIntent + + func placeholder(in context: Context) -> GradesEntry { + GradesEntry(date: Date(), configuration: GradesWidgetIntent(), grades: [], locale: "hu") + } + + func snapshot(for configuration: GradesWidgetIntent, in context: Context) async -> GradesEntry { + let data = WidgetData.load() + return GradesEntry( + date: Date(), + configuration: configuration, + grades: data?.grades ?? [], + locale: data?.locale ?? "hu" + ) + } + + func timeline(for configuration: GradesWidgetIntent, in context: Context) async -> Timeline { + let data = WidgetData.load() + let entry = GradesEntry( + date: Date(), + configuration: configuration, + grades: data?.grades ?? [], + locale: data?.locale ?? "hu" + ) + + let refreshDate = Date().addingTimeInterval(30 * 60) + return Timeline(entries: [entry], policy: .after(refreshDate)) + } +} diff --git a/firka/ios/HomeWidgetsExtension/Providers/TimetableProvider.swift b/firka/ios/HomeWidgetsExtension/Providers/TimetableProvider.swift new file mode 100644 index 0000000..db79470 --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/Providers/TimetableProvider.swift @@ -0,0 +1,167 @@ +import WidgetKit +import SwiftUI + +struct TimetableEntry: TimelineEntry { + let date: Date + let configuration: TimetableWidgetIntent + let data: WidgetData? + let lessons: [WidgetLesson] + let currentLesson: WidgetLesson? + let nextLesson: WidgetLesson? + let isNextDay: Bool + let breakInfo: BreakInfo? + let state: TimetableState + let debugInfo: String +} + +enum TimetableState { + case normal + case noMoreLessons + case onBreak + case loginRequired + case unavailable +} + +struct TimetableProvider: AppIntentTimelineProvider { + typealias Entry = TimetableEntry + typealias Intent = TimetableWidgetIntent + + func placeholder(in context: Context) -> TimetableEntry { + TimetableEntry( + date: Date(), + configuration: TimetableWidgetIntent(), + data: nil, + lessons: [], + currentLesson: nil, + nextLesson: nil, + isNextDay: false, + breakInfo: nil, + state: .normal, + debugInfo: "placeholder" + ) + } + + func snapshot(for configuration: TimetableWidgetIntent, in context: Context) async -> TimetableEntry { + createEntry(for: configuration, date: Date()) + } + + func timeline(for configuration: TimetableWidgetIntent, in context: Context) async -> Timeline { + var entries: [TimetableEntry] = [] + let now = Date() + + let data = WidgetData.load() + + // If on break, create single entry + if let breakInfo = data?.timetable.currentBreak { + let entry = TimetableEntry( + date: now, + configuration: configuration, + data: data, + lessons: [], + currentLesson: nil, + nextLesson: nil, + isNextDay: false, + breakInfo: breakInfo, + state: .onBreak, + debugInfo: WidgetData.lastError + ) + entries.append(entry) + return Timeline(entries: entries, policy: .after(Calendar.current.startOfDay(for: now.addingTimeInterval(86400)))) + } + + let todayLessons = data?.timetable.today ?? [] + + entries.append(createEntry(for: configuration, date: now)) + + for lesson in todayLessons { + if lesson.start > now { + entries.append(createEntry(for: configuration, date: lesson.start)) + } + if lesson.end > now { + entries.append(createEntry(for: configuration, date: lesson.end.addingTimeInterval(1))) + } + } + + let midnight = Calendar.current.startOfDay(for: now.addingTimeInterval(86400)) + entries.append(createEntry(for: configuration, date: midnight)) + + return Timeline(entries: entries, policy: .atEnd) + } + + private func createEntry(for configuration: TimetableWidgetIntent, date: Date) -> TimetableEntry { + let data = WidgetData.load() + + guard let data = data else { + return TimetableEntry( + date: date, + configuration: configuration, + data: nil, + lessons: [], + currentLesson: nil, + nextLesson: nil, + isNextDay: false, + breakInfo: nil, + state: .loginRequired, + debugInfo: WidgetData.lastError + ) + } + + if let breakInfo = data.timetable.currentBreak { + return TimetableEntry( + date: date, + configuration: configuration, + data: data, + lessons: [], + currentLesson: nil, + nextLesson: nil, + isNextDay: false, + breakInfo: breakInfo, + state: .onBreak, + debugInfo: WidgetData.lastError + ) + } + + var lessons = data.timetable.today + var isNextDay = false + + let lastLesson = lessons.last + if let last = lastLesson, date > last.end { + lessons = data.timetable.tomorrow + isNextDay = true + } + + if lessons.isEmpty { + return TimetableEntry( + date: date, + configuration: configuration, + data: data, + lessons: [], + currentLesson: nil, + nextLesson: nil, + isNextDay: isNextDay, + breakInfo: nil, + state: isNextDay ? .noMoreLessons : .unavailable, + debugInfo: WidgetData.lastError + ) + } + + let currentLesson = lessons.first { lesson in + let now = Date() + return now >= lesson.start && now <= lesson.end + } + let nextLesson = lessons.first { $0.start > date } + + return TimetableEntry( + date: date, + configuration: configuration, + data: data, + lessons: lessons, + currentLesson: currentLesson, + nextLesson: nextLesson, + isNextDay: isNextDay, + breakInfo: nil, + state: .normal, + debugInfo: WidgetData.lastError + ) + } +} diff --git a/firka/ios/HomeWidgetsExtension/TimetableWidget.swift b/firka/ios/HomeWidgetsExtension/TimetableWidget.swift new file mode 100644 index 0000000..d66ca34 --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/TimetableWidget.swift @@ -0,0 +1,57 @@ +import WidgetKit +import SwiftUI + +struct TimetableWidget: Widget { + let kind: String = "TimetableWidget" + + var body: some WidgetConfiguration { + AppIntentConfiguration( + kind: kind, + intent: TimetableWidgetIntent.self, + provider: TimetableProvider() + ) { entry in + TimetableWidgetView(entry: entry) + .containerBackground(.clear, for: .widget) + } + .configurationDisplayName(LocalizedStringResource("widget_timetable_title", defaultValue: "Timetable")) + .description(LocalizedStringResource("widget_timetable_description", defaultValue: "Shows your daily timetable")) + .supportedFamilies([.systemSmall, .systemMedium, .systemLarge]) + } +} + +struct TimetableWidgetView: View { + @Environment(\.widgetFamily) var family + let entry: TimetableEntry + + var localization: WidgetLocalization { + WidgetLocalization(locale: entry.data?.locale ?? "hu") + } + + var style: WidgetStyleType { + (entry.configuration.style ?? .appTheme) == .liquidGlass ? .liquidGlass : .appTheme + } + + var body: some View { + switch entry.state { + case .onBreak: + if let breakInfo = entry.breakInfo { + BreakView(breakInfo: breakInfo, localization: localization, style: style) + } + case .loginRequired: + EmptyStateView(message: localization.string("login_required"), style: style) + case .unavailable: + EmptyStateView(message: localization.string("timetable_unavailable"), style: style) + case .noMoreLessons, .normal: + switch family { + case .systemSmall: + TimetableSmallView(entry: entry, localization: localization) + case .systemMedium: + TimetableMediumView(entry: entry, localization: localization) + case .systemLarge: + TimetableLargeView(entry: entry, localization: localization) + default: + TimetableMediumView(entry: entry, localization: localization) + } + } + } +} diff --git a/firka/ios/HomeWidgetsExtension/Views/AveragesViews.swift b/firka/ios/HomeWidgetsExtension/Views/AveragesViews.swift new file mode 100644 index 0000000..cf23934 --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/Views/AveragesViews.swift @@ -0,0 +1,167 @@ +import SwiftUI +import WidgetKit + +struct AveragesSmallView: View { + let entry: AveragesEntry + let localization: WidgetLocalization + + var style: WidgetStyleType { + (entry.configuration.style ?? .appTheme) == .liquidGlass ? .liquidGlass : .appTheme + } + + var body: some View { + ZStack { + WidgetBackground(style: style, colors: nil) + + VStack(spacing: 8) { + Text(localization.string("overall_average")) + .font(.caption) + .widgetTextStyle(style, colors: nil, isPrimary: false) + + if let average = entry.overallAverage { + Text(String(format: "%.2f", average)) + .font(.system(size: 36, weight: .bold)) + .minimumScaleFactor(0.5) + .foregroundStyle(averageColor(for: average)) + } else { + Text("-") + .font(.system(size: 36, weight: .bold)) + .widgetTextStyle(style, colors: nil, isPrimary: false) + } + } + .padding() + } + } + + func averageColor(for value: Double) -> Color { + switch value { + case 4.5...: return .green + case 3.5..<4.5: return .blue + case 2.5..<3.5: return .yellow + case 1.5..<2.5: return .orange + default: return .red + } + } +} + +struct AveragesMediumView: View { + let entry: AveragesEntry + let localization: WidgetLocalization + + var style: WidgetStyleType { + (entry.configuration.style ?? .appTheme) == .liquidGlass ? .liquidGlass : .appTheme + } + + 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) + + if entry.subjectAverages.isEmpty { + Spacer() + Text(localization.string("no_averages")) + .font(.subheadline) + .widgetTextStyle(style, colors: nil, isPrimary: false) + Spacer() + } else { + ForEach(entry.subjectAverages.prefix(4)) { subject in + AverageRow(subject: subject, style: style) + } + Spacer() + } + } + .padding() + } + } +} + +struct AveragesLargeView: View { + let entry: AveragesEntry + let localization: WidgetLocalization + + var style: WidgetStyleType { + (entry.configuration.style ?? .appTheme) == .liquidGlass ? .liquidGlass : .appTheme + } + + var body: some View { + ZStack { + WidgetBackground(style: style, colors: nil) + + VStack(alignment: .leading, spacing: 8) { + HStack { + Text(localization.string("subject_averages")) + .font(.headline) + .fontWeight(.semibold) + .widgetTextStyle(style, colors: nil) + + Spacer() + + if let overall = entry.overallAverage { + Text(String(format: "%.2f", overall)) + .font(.headline) + .fontWeight(.bold) + .foregroundStyle(averageColor(for: overall)) + } + } + + if entry.subjectAverages.isEmpty { + Spacer() + Text(localization.string("no_averages")) + .font(.subheadline) + .widgetTextStyle(style, colors: nil, isPrimary: false) + Spacer() + } else { + ForEach(entry.subjectAverages.prefix(8)) { subject in + AverageRow(subject: subject, style: style, showGradeCount: true) + } + Spacer() + } + } + .padding() + } + } + + func averageColor(for value: Double) -> Color { + switch value { + case 4.5...: return .green + case 3.5..<4.5: return .blue + case 2.5..<3.5: return .yellow + case 1.5..<2.5: return .orange + default: return .red + } + } +} + +struct AverageRow: View { + let subject: SubjectAverage + let style: WidgetStyleType + var showGradeCount: Bool = false + + var body: some View { + HStack(spacing: 12) { + Text(subject.name) + .font(.subheadline) + .widgetTextStyle(style, colors: nil) + .lineLimit(1) + + Spacer() + + if showGradeCount { + Text("(\(subject.gradeCount))") + .font(.caption) + .widgetTextStyle(style, colors: nil, isPrimary: false) + } + + Text(subject.formattedAverage) + .font(.subheadline) + .fontWeight(.bold) + .foregroundStyle(subject.averageColor) + } + .padding(.vertical, 4) + } +} diff --git a/firka/ios/HomeWidgetsExtension/Views/GradesViews.swift b/firka/ios/HomeWidgetsExtension/Views/GradesViews.swift new file mode 100644 index 0000000..22bb9d0 --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/Views/GradesViews.swift @@ -0,0 +1,169 @@ +import SwiftUI +import WidgetKit + +struct GradesSmallView: View { + let entry: GradesEntry + let localization: WidgetLocalization + + var style: WidgetStyleType { + (entry.configuration.style ?? .appTheme) == .liquidGlass ? .liquidGlass : .appTheme + } + + var body: some View { + ZStack { + WidgetBackground(style: style, colors: nil) + + VStack(alignment: .leading, spacing: 4) { + Text(localization.string("recent_grades")) + .font(.caption) + .widgetTextStyle(style, colors: nil, isPrimary: false) + + if let grade = entry.grades.first { + Spacer() + + HStack { + Text(grade.displayValue) + .font(.system(size: 48, weight: .bold)) + .foregroundStyle(grade.gradeColor) + + Spacer() + } + + Text(grade.subject.name) + .font(.subheadline) + .fontWeight(.medium) + .widgetTextStyle(style, colors: nil) + .lineLimit(1) + + Text(grade.dateString) + .font(.caption) + .widgetTextStyle(style, colors: nil, isPrimary: false) + } else { + Spacer() + Text(localization.string("no_grades")) + .font(.subheadline) + .widgetTextStyle(style, colors: nil, isPrimary: false) + } + + Spacer() + } + .padding() + } + } +} + +struct GradesMediumView: View { + let entry: GradesEntry + let localization: WidgetLocalization + + var style: WidgetStyleType { + (entry.configuration.style ?? .appTheme) == .liquidGlass ? .liquidGlass : .appTheme + } + + var body: some View { + ZStack { + WidgetBackground(style: style, colors: nil) + + VStack(alignment: .leading, spacing: 8) { + Text(localization.string("recent_grades")) + .font(.caption) + .fontWeight(.medium) + .widgetTextStyle(style, colors: nil, isPrimary: false) + + if entry.grades.isEmpty { + Spacer() + Text(localization.string("no_grades")) + .font(.subheadline) + .widgetTextStyle(style, colors: nil, isPrimary: false) + Spacer() + } else { + ForEach(entry.grades.prefix(3)) { grade in + GradeRow(grade: grade, style: style, showType: true) + } + } + } + .padding() + } + .clipped() + } +} + +struct GradesLargeView: View { + let entry: GradesEntry + let localization: WidgetLocalization + + var style: WidgetStyleType { + (entry.configuration.style ?? .appTheme) == .liquidGlass ? .liquidGlass : .appTheme + } + + var body: some View { + ZStack { + WidgetBackground(style: style, colors: nil) + + VStack(alignment: .leading, spacing: 8) { + Text(localization.string("recent_grades")) + .font(.headline) + .fontWeight(.semibold) + .widgetTextStyle(style, colors: nil) + + if entry.grades.isEmpty { + Spacer() + Text(localization.string("no_grades")) + .font(.subheadline) + .widgetTextStyle(style, colors: nil, isPrimary: false) + Spacer() + } else { + ForEach(entry.grades.prefix(6)) { grade in + GradeRow(grade: grade, style: style, showType: true, showTopic: true) + } + } + } + .padding() + } + .clipped() + } +} + +struct GradeRow: View { + let grade: WidgetGrade + let style: WidgetStyleType + var showType: Bool = false + var showTopic: Bool = false + + var body: some View { + HStack(spacing: 12) { + Text(grade.displayValue) + .font(.title2) + .fontWeight(.bold) + .foregroundStyle(grade.gradeColor) + .frame(width: 32) + + VStack(alignment: .leading, spacing: 2) { + Text(grade.subject.name) + .font(.subheadline) + .fontWeight(.medium) + .widgetTextStyle(style, colors: nil) + .lineLimit(1) + + HStack(spacing: 4) { + if showType { + Text(grade.type.name) + .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) + } + } + + Spacer() + } + .padding(.vertical, 4) + } +} diff --git a/firka/ios/HomeWidgetsExtension/Views/TimetableViews.swift b/firka/ios/HomeWidgetsExtension/Views/TimetableViews.swift new file mode 100644 index 0000000..2466179 --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/Views/TimetableViews.swift @@ -0,0 +1,257 @@ +import SwiftUI +import WidgetKit + +struct TimetableSmallView: View { + let entry: TimetableEntry + let localization: WidgetLocalization + + var style: WidgetStyleType { + (entry.configuration.style ?? .appTheme) == .liquidGlass ? .liquidGlass : .appTheme + } + + var displayLesson: WidgetLesson? { + (entry.configuration.displayMode ?? .current) == .current ? entry.currentLesson : entry.nextLesson + } + + var body: some View { + ZStack { + WidgetBackground(style: style, colors: nil) + + VStack(alignment: .leading, spacing: 4) { + Text((entry.configuration.displayMode ?? .current) == .current ? + localization.string("current_lesson") : + localization.string("next_lesson")) + .font(.caption) + .widgetTextStyle(style, colors: nil, isPrimary: false) + + if let lesson = displayLesson { + Text(lesson.displayName) + .font(.headline) + .fontWeight(.semibold) + .strikethrough(lesson.isCancelled, color: .red) + .foregroundColor(lesson.isCancelled ? .red : + lesson.isSubstitution ? .orange : + (style == .liquidGlass ? .white : .primary)) + .lineLimit(2) + + Text(lesson.timeString) + .font(.subheadline) + .foregroundColor(lesson.isCancelled ? .red.opacity(0.8) : + lesson.isSubstitution ? .orange.opacity(0.8) : + (style == .liquidGlass ? .white.opacity(0.7) : .secondary)) + + if let room = lesson.roomName { + Text(room) + .font(.caption2) + .lineLimit(2) + .minimumScaleFactor(0.8) + .foregroundColor(lesson.isCancelled ? .red.opacity(0.7) : + lesson.isSubstitution ? .orange.opacity(0.7) : + (style == .liquidGlass ? .white.opacity(0.6) : .secondary)) + } + } else { + Text(localization.string("no_lessons")) + .font(.subheadline) + .widgetTextStyle(style, colors: nil, isPrimary: false) + } + + Spacer() + } + .padding() + } + } +} + +struct TimetableMediumView: View { + let entry: TimetableEntry + let localization: WidgetLocalization + + var style: WidgetStyleType { + (entry.configuration.style ?? .appTheme) == .liquidGlass ? .liquidGlass : .appTheme + } + + var remainingLessons: [WidgetLesson] { + let now = Date() + return entry.lessons.filter { $0.end > now } + } + + 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")) + .font(.caption) + .fontWeight(.medium) + .widgetTextStyle(style, colors: nil, isPrimary: false) + + ForEach(remainingLessons.prefix(4)) { lesson in + LessonRow(lesson: lesson, isActive: lesson.isCurrentlyActive, style: style) + } + } + .padding() + } + .clipped() + } +} + +struct TimetableLargeView: View { + let entry: TimetableEntry + let localization: WidgetLocalization + + var style: WidgetStyleType { + (entry.configuration.style ?? .appTheme) == .liquidGlass ? .liquidGlass : .appTheme + } + + var remainingLessons: [WidgetLesson] { + let now = Date() + return entry.lessons.filter { $0.end > now } + } + + 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")) + .font(.headline) + .fontWeight(.semibold) + .widgetTextStyle(style, colors: nil) + + ForEach(remainingLessons.prefix(7)) { lesson in + LessonRow(lesson: lesson, isActive: lesson.isCurrentlyActive, style: style, showRoom: true) + } + } + .padding() + } + .clipped() + } +} + +struct LessonRow: View { + let lesson: WidgetLesson + let isActive: Bool + let style: WidgetStyleType + var showRoom: Bool = false + + var lessonTextColor: Color? { + if lesson.isCancelled { + return .red + } else if lesson.isSubstitution { + return .orange + } + return nil + } + + var numberBackgroundColor: Color { + if lesson.isCancelled { + return Color.red.opacity(0.3) + } else if lesson.isSubstitution { + return Color.orange.opacity(0.3) + } else if isActive { + return Color.green.opacity(0.3) + } + return Color.secondary.opacity(0.2) + } + + var body: some View { + HStack(spacing: 12) { + if let number = lesson.lessonNumber { + Text("\(number)") + .font(.caption) + .fontWeight(.bold) + .frame(width: 24, height: 24) + .background( + Circle() + .fill(numberBackgroundColor) + ) + .foregroundColor(lessonTextColor ?? (style == .liquidGlass ? .white : .primary)) + } + + Text(lesson.displayName) + .font(.subheadline) + .fontWeight(isActive ? .semibold : .regular) + .strikethrough(lesson.isCancelled, color: .red) + .foregroundColor(lessonTextColor ?? (style == .liquidGlass ? .white : .primary)) + .lineLimit(1) + + Spacer() + + Text(lesson.timeString) + .font(.caption) + .foregroundColor(lessonTextColor?.opacity(0.8) ?? (style == .liquidGlass ? .white.opacity(0.7) : .secondary)) + + if showRoom, let room = lesson.roomName { + Text(room) + .font(.caption2) + .padding(.horizontal, 6) + .padding(.vertical, 2) + .background( + Capsule() + .fill(lesson.isCancelled ? Color.red.opacity(0.2) : + lesson.isSubstitution ? Color.orange.opacity(0.2) : + Color.secondary.opacity(0.2)) + ) + .foregroundColor(lessonTextColor?.opacity(0.8) ?? (style == .liquidGlass ? .white.opacity(0.7) : .secondary)) + } + } + .padding(.vertical, 4) + .padding(.horizontal, 8) + .currentLessonGlow(isActive: isActive && !lesson.isCancelled) + } +} + +struct BreakView: View { + let breakInfo: BreakInfo + let localization: WidgetLocalization + let style: WidgetStyleType + + var daysRemaining: Int { + Calendar.current.dateComponents([.day], from: Date(), to: breakInfo.endDate).day ?? 0 + } + + var body: some View { + ZStack { + WidgetBackground(style: style, colors: nil) + + VStack(spacing: 12) { + Image(systemName: "snowflake") + .font(.largeTitle) + .widgetTextStyle(style, colors: nil) + + Text(localization.string("happy_break", localization.string(breakInfo.nameKey))) + .font(.headline) + .multilineTextAlignment(.center) + .widgetTextStyle(style, colors: nil) + + Text(localization.string("days_remaining", daysRemaining)) + .font(.subheadline) + .widgetTextStyle(style, colors: nil, isPrimary: false) + } + .padding() + } + } +} + +struct EmptyStateView: View { + let message: String + let style: WidgetStyleType + + var body: some View { + ZStack { + WidgetBackground(style: style, colors: nil) + + VStack(spacing: 8) { + Image(systemName: "calendar.badge.exclamationmark") + .font(.largeTitle) + .widgetTextStyle(style, colors: nil, isPrimary: false) + + Text(message) + .font(.subheadline) + .multilineTextAlignment(.center) + .widgetTextStyle(style, colors: nil, isPrimary: false) + } + .padding() + } + } +} diff --git a/firka/ios/HomeWidgetsExtension/Views/WidgetStyles.swift b/firka/ios/HomeWidgetsExtension/Views/WidgetStyles.swift new file mode 100644 index 0000000..8062dc9 --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/Views/WidgetStyles.swift @@ -0,0 +1,102 @@ +import SwiftUI +import WidgetKit + +enum WidgetStyleType: String, Codable, CaseIterable { + case liquidGlass = "liquid_glass" + case appTheme = "app_theme" + + var displayName: String { + switch self { + case .liquidGlass: return "Liquid Glass" + case .appTheme: return "App Theme" + } + } +} + +struct WidgetBackground: View { + let style: WidgetStyleType + let colors: WidgetColors? + + @Environment(\.colorScheme) var colorScheme + + var body: some View { + switch style { + case .liquidGlass: + if #available(iOS 26.0, *) { + Color.clear + } else { + Rectangle() + .fill(.ultraThinMaterial) + } + case .appTheme: + if let colors = colors { + Rectangle() + .fill(colors.backgroundColor) + } else { + Rectangle() + .fill(Color(.systemBackground)) + } + } + } +} + +struct WidgetTextStyle: ViewModifier { + let style: WidgetStyleType + let colors: WidgetColors? + let isPrimary: Bool + + @Environment(\.colorScheme) var colorScheme + @Environment(\.widgetRenderingMode) var renderingMode + + func body(content: Content) -> some View { + switch style { + case .liquidGlass: + if renderingMode == .accented { + content + } else { + content.foregroundStyle(isPrimary ? + (colorScheme == .dark ? .white : .black) : + (colorScheme == .dark ? .white.opacity(0.7) : .black.opacity(0.6))) + } + case .appTheme: + if let colors = colors { + content.foregroundStyle(isPrimary ? colors.textPrimaryColor : colors.textSecondaryColor) + } else { + content.foregroundStyle(isPrimary ? .primary : .secondary) + } + } + } +} + +extension View { + func widgetTextStyle(_ style: WidgetStyleType, colors: WidgetColors?, isPrimary: Bool = true) -> some View { + modifier(WidgetTextStyle(style: style, colors: colors, isPrimary: isPrimary)) + } +} + +struct GlowEffect: ViewModifier { + let isActive: Bool + let color: Color + + func body(content: Content) -> some View { + if isActive { + content + .background( + RoundedRectangle(cornerRadius: 12) + .fill(color.opacity(0.15)) + ) + .overlay( + RoundedRectangle(cornerRadius: 12) + .stroke(color.opacity(0.3), lineWidth: 1) + ) + } else { + content + } + } +} + +extension View { + func currentLessonGlow(isActive: Bool, color: Color = .green) -> some View { + modifier(GlowEffect(isActive: isActive, color: color)) + } +} diff --git a/firka/ios/HomeWidgetsExtension/de.lproj/Localizable.strings b/firka/ios/HomeWidgetsExtension/de.lproj/Localizable.strings new file mode 100644 index 0000000..6ac9ef6 --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/de.lproj/Localizable.strings @@ -0,0 +1,28 @@ +/* Widget Titles */ +"widget_timetable_title" = "Stundenplan"; +"widget_grades_title" = "Letzte Noten"; +"widget_averages_title" = "Durchschnitte"; + +/* Widget Descriptions */ +"widget_timetable_description" = "Zeigt deinen täglichen Stundenplan"; +"widget_grades_description" = "Zeigt deine letzten Noten"; +"widget_averages_description" = "Zeigt Fachdurchschnitte"; + +/* Parameter Titles */ +"param_style" = "Stil"; +"param_display_mode_small" = "Anzeige (kleines Widget)"; +"param_subjects" = "Fächer"; + +/* Style Options */ +"style_type" = "Stil"; +"style_liquid_glass" = "Liquid Glass"; +"style_app_theme" = "App-Design"; + +/* Display Mode Options */ +"display_mode_type" = "Anzeigemodus"; +"display_mode_current" = "Aktuelle Stunde"; +"display_mode_next" = "Nächste Stunde"; + +/* Subject Selection */ +"subjects_type" = "Fächer"; +"no_subjects_available" = "Keine Fächer verfügbar"; diff --git a/firka/ios/HomeWidgetsExtension/en.lproj/Localizable.strings b/firka/ios/HomeWidgetsExtension/en.lproj/Localizable.strings new file mode 100644 index 0000000..a118371 --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/en.lproj/Localizable.strings @@ -0,0 +1,28 @@ +/* Widget Titles */ +"widget_timetable_title" = "Timetable"; +"widget_grades_title" = "Recent Grades"; +"widget_averages_title" = "Averages"; + +/* Widget Descriptions */ +"widget_timetable_description" = "Shows your daily timetable"; +"widget_grades_description" = "Shows your recent grades"; +"widget_averages_description" = "Shows subject averages"; + +/* Parameter Titles */ +"param_style" = "Style"; +"param_display_mode_small" = "Display (small widget)"; +"param_subjects" = "Subjects"; + +/* Style Options */ +"style_type" = "Style"; +"style_liquid_glass" = "Liquid Glass"; +"style_app_theme" = "App Theme"; + +/* Display Mode Options */ +"display_mode_type" = "Display Mode"; +"display_mode_current" = "Current Lesson"; +"display_mode_next" = "Next Lesson"; + +/* Subject Selection */ +"subjects_type" = "Subjects"; +"no_subjects_available" = "No subjects available"; diff --git a/firka/ios/HomeWidgetsExtension/hu.lproj/Localizable.strings b/firka/ios/HomeWidgetsExtension/hu.lproj/Localizable.strings new file mode 100644 index 0000000..6ad722f --- /dev/null +++ b/firka/ios/HomeWidgetsExtension/hu.lproj/Localizable.strings @@ -0,0 +1,28 @@ +/* Widget Titles */ +"widget_timetable_title" = "Órarend"; +"widget_grades_title" = "Legutóbbi jegyek"; +"widget_averages_title" = "Átlagok"; + +/* Widget Descriptions */ +"widget_timetable_description" = "A napi órarendet mutatja"; +"widget_grades_description" = "A legutóbbi jegyeidet mutatja"; +"widget_averages_description" = "A tantárgyi átlagokat mutatja"; + +/* Parameter Titles */ +"param_style" = "Stílus"; +"param_display_mode_small" = "Megjelenítés (kis widget)"; +"param_subjects" = "Tantárgyak"; + +/* Style Options */ +"style_type" = "Stílus"; +"style_liquid_glass" = "Liquid Glass"; +"style_app_theme" = "Alkalmazás témája"; + +/* Display Mode Options */ +"display_mode_type" = "Megjelenítési mód"; +"display_mode_current" = "Jelenlegi óra"; +"display_mode_next" = "Következő óra"; + +/* Subject Selection */ +"subjects_type" = "Tantárgyak"; +"no_subjects_available" = "Nincsenek tantárgyak"; diff --git a/firka/ios/HomeWidgetsExtensionExtension.entitlements b/firka/ios/HomeWidgetsExtensionExtension.entitlements new file mode 100644 index 0000000..fda7eff --- /dev/null +++ b/firka/ios/HomeWidgetsExtensionExtension.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.app.firka.firkaa + + + diff --git a/firka/ios/Runner.xcodeproj/project.pbxproj b/firka/ios/Runner.xcodeproj/project.pbxproj index 12c34a2..d35bb66 100644 --- a/firka/ios/Runner.xcodeproj/project.pbxproj +++ b/firka/ios/Runner.xcodeproj/project.pbxproj @@ -3,21 +3,24 @@ archiveVersion = 1; classes = { }; - objectVersion = 70; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ + 14578EED4EA309B337AB389E /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A749415A687CBFC3F46FA876 /* Pods_RunnerTests.framework */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 2D1A72FA250BC71FB05757CE /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E57B0CD5BC5E83062121FE65 /* Pods_Runner.framework */; }; + 213F8C0F6B5418B02DE14204 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 035E9CCBCC6585D0F5639031 /* Pods_Runner.framework */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 4F30C7592E8FBF26008BB46C /* LiveActivityMethodChannelManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F30C7582E8FBF26008BB46C /* LiveActivityMethodChannelManager.swift */; }; 4F30C7672E8FBF9D008BB46C /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F30C7662E8FBF9D008BB46C /* WidgetKit.framework */; }; 4F30C7692E8FBF9D008BB46C /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F30C7682E8FBF9D008BB46C /* SwiftUI.framework */; }; 4F30C7782E8FBF9F008BB46C /* TimetableWidgetExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 4F30C7652E8FBF9D008BB46C /* TimetableWidgetExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 4F30C79A2E8FC427008BB46C /* TimetableActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F30C7992E8FC427008BB46C /* TimetableActivityAttributes.swift */; }; + 4F45A1752F27FA3C0020E6F1 /* HomeWidgetMethodChannel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F45A1732F27FA3C0020E6F1 /* HomeWidgetMethodChannel.swift */; }; + 4FE64E342F27B07A006F9205 /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F30C7662E8FBF9D008BB46C /* WidgetKit.framework */; }; + 4FE64E352F27B07A006F9205 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F30C7682E8FBF9D008BB46C /* SwiftUI.framework */; }; + 4FE64E422F27B07B006F9205 /* HomeWidgetsExtensionExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 4FE64E332F27B079006F9205 /* HomeWidgetsExtensionExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; - 7762B298B1A9C855D1874A96 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6436F72EE81AA1892BF629F8 /* Pods_RunnerTests.framework */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -38,6 +41,13 @@ remoteGlobalIDString = 4F30C7642E8FBF9D008BB46C; remoteInfo = TimetableWidgetExtension; }; + 4FE64E402F27B07B006F9205 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 4FE64E322F27B079006F9205; + remoteInfo = HomeWidgetsExtensionExtension; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -48,6 +58,7 @@ dstSubfolderSpec = 13; files = ( 4F30C7782E8FBF9F008BB46C /* TimetableWidgetExtension.appex in Embed Foundation Extensions */, + 4FE64E422F27B07B006F9205 /* HomeWidgetsExtensionExtension.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -65,25 +76,26 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 0EE927DD3F0F54BDE10EFE01 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 035E9CCBCC6585D0F5639031 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 2224F577A1AE7BBF50F1FA78 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 248F3DE56A05CAECFEBD617C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 4836947EC3B04B475B3DA1F8 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 485F3791F25A288C749509B2 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 4F25FCBD2EB1790E0060DAAA /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; - 4F25FCBE2EB17D810060DAAA /* TimetableWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TimetableWidgetExtension.entitlements; sourceTree = ""; }; 4F30C7582E8FBF26008BB46C /* LiveActivityMethodChannelManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityMethodChannelManager.swift; sourceTree = ""; }; 4F30C7652E8FBF9D008BB46C /* TimetableWidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = TimetableWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 4F30C7662E8FBF9D008BB46C /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 4F30C7682E8FBF9D008BB46C /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; - 4F30C7992E8FC427008BB46C /* TimetableActivityAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimetableActivityAttributes.swift; sourceTree = ""; }; - 6436F72EE81AA1892BF629F8 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4F45A1732F27FA3C0020E6F1 /* HomeWidgetMethodChannel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeWidgetMethodChannel.swift; sourceTree = ""; }; + 4F959B792F289CA600FF7F03 /* TimetableWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TimetableWidgetExtension.entitlements; sourceTree = ""; }; + 4F959B9C2F289CA600FF7F03 /* HomeWidgetsExtensionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = HomeWidgetsExtensionExtension.entitlements; sourceTree = ""; }; + 4FE64E332F27B079006F9205 /* HomeWidgetsExtensionExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = HomeWidgetsExtensionExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7673DE33F16FE6D0BCB75811 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - 76B2553ECF760C8F6A043E50 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; @@ -92,23 +104,62 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B222D922BB8257D2341337A4 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; - BDDC8A00836B054E202CC327 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; - E57B0CD5BC5E83062121FE65 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + A749415A687CBFC3F46FA876 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + AB2E15171B6907C52E8C2B42 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + AE756C46C544099A30412EAF /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + EBD040A65B2746AF6A3D5C40 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - 4F6C1D3E2ECD3FBD00F819D7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */ = { + 4F4E70D02EF565FF00C90AD1 /* Exceptions for "TimetableWidget" folder in "Runner" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + TimetableActivityAttributes.swift, + ); + target = 97C146ED1CF9000F007C117D /* Runner */; + }; + 4F6C1D3E2ECD3FBD00F819D7 /* Exceptions for "TimetableWidget" folder in "TimetableWidgetExtension" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( Info.plist, ); target = 4F30C7642E8FBF9D008BB46C /* TimetableWidgetExtension */; }; + 4FE64E472F27B07B006F9205 /* Exceptions for "HomeWidgetsExtension" folder in "HomeWidgetsExtensionExtension" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 4FE64E322F27B079006F9205 /* HomeWidgetsExtensionExtension */; + }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ - 4F30C76A2E8FBF9D008BB46C /* TimetableWidget */ = {isa = PBXFileSystemSynchronizedRootGroup; exceptions = (4F6C1D3E2ECD3FBD00F819D7 /* PBXFileSystemSynchronizedBuildFileExceptionSet */, ); explicitFileTypes = {}; explicitFolders = (); path = TimetableWidget; sourceTree = ""; }; + 4F30C76A2E8FBF9D008BB46C /* TimetableWidget */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 4F4E70D02EF565FF00C90AD1 /* Exceptions for "TimetableWidget" folder in "Runner" target */, + 4F6C1D3E2ECD3FBD00F819D7 /* Exceptions for "TimetableWidget" folder in "TimetableWidgetExtension" target */, + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = TimetableWidget; + sourceTree = ""; + }; + 4FE64E362F27B07A006F9205 /* HomeWidgetsExtension */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 4FE64E472F27B07B006F9205 /* Exceptions for "HomeWidgetsExtension" folder in "HomeWidgetsExtensionExtension" target */, + ); + explicitFileTypes = { + }; + explicitFolders = ( + ); + path = HomeWidgetsExtension; + sourceTree = ""; + }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ @@ -121,11 +172,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 4FE64E302F27B079006F9205 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4FE64E352F27B07A006F9205 /* SwiftUI.framework in Frameworks */, + 4FE64E342F27B07A006F9205 /* WidgetKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 2D1A72FA250BC71FB05757CE /* Pods_Runner.framework in Frameworks */, + 213F8C0F6B5418B02DE14204 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -133,7 +193,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 7762B298B1A9C855D1874A96 /* Pods_RunnerTests.framework in Frameworks */, + 14578EED4EA309B337AB389E /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -153,8 +213,8 @@ children = ( 4F30C7662E8FBF9D008BB46C /* WidgetKit.framework */, 4F30C7682E8FBF9D008BB46C /* SwiftUI.framework */, - E57B0CD5BC5E83062121FE65 /* Pods_Runner.framework */, - 6436F72EE81AA1892BF629F8 /* Pods_RunnerTests.framework */, + 035E9CCBCC6585D0F5639031 /* Pods_Runner.framework */, + A749415A687CBFC3F46FA876 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -162,12 +222,12 @@ 52B477EA0F4B63DC7CE4BA83 /* Pods */ = { isa = PBXGroup; children = ( - 76B2553ECF760C8F6A043E50 /* Pods-Runner.debug.xcconfig */, - 7673DE33F16FE6D0BCB75811 /* Pods-Runner.release.xcconfig */, - 2224F577A1AE7BBF50F1FA78 /* Pods-Runner.profile.xcconfig */, - 0EE927DD3F0F54BDE10EFE01 /* Pods-RunnerTests.debug.xcconfig */, - B222D922BB8257D2341337A4 /* Pods-RunnerTests.release.xcconfig */, - BDDC8A00836B054E202CC327 /* Pods-RunnerTests.profile.xcconfig */, + 485F3791F25A288C749509B2 /* Pods-Runner.debug.xcconfig */, + 248F3DE56A05CAECFEBD617C /* Pods-Runner.release.xcconfig */, + AB2E15171B6907C52E8C2B42 /* Pods-Runner.profile.xcconfig */, + EBD040A65B2746AF6A3D5C40 /* Pods-RunnerTests.debug.xcconfig */, + AE756C46C544099A30412EAF /* Pods-RunnerTests.release.xcconfig */, + 4836947EC3B04B475B3DA1F8 /* Pods-RunnerTests.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -186,10 +246,12 @@ 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( - 4F25FCBE2EB17D810060DAAA /* TimetableWidgetExtension.entitlements */, + 4F959B792F289CA600FF7F03 /* TimetableWidgetExtension.entitlements */, + 4F959B9C2F289CA600FF7F03 /* HomeWidgetsExtensionExtension.entitlements */, 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 4F30C76A2E8FBF9D008BB46C /* TimetableWidget */, + 4FE64E362F27B07A006F9205 /* HomeWidgetsExtension */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, 52B477EA0F4B63DC7CE4BA83 /* Pods */, @@ -203,6 +265,7 @@ 97C146EE1CF9000F007C117D /* Runner.app */, 331C8081294A63A400263BE5 /* RunnerTests.xctest */, 4F30C7652E8FBF9D008BB46C /* TimetableWidgetExtension.appex */, + 4FE64E332F27B079006F9205 /* HomeWidgetsExtensionExtension.appex */, ); name = Products; sourceTree = ""; @@ -211,7 +274,6 @@ isa = PBXGroup; children = ( 4F25FCBD2EB1790E0060DAAA /* Runner.entitlements */, - 4F30C7992E8FC427008BB46C /* TimetableActivityAttributes.swift */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, @@ -221,6 +283,7 @@ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 4F30C7582E8FBF26008BB46C /* LiveActivityMethodChannelManager.swift */, + 4F45A1732F27FA3C0020E6F1 /* HomeWidgetMethodChannel.swift */, ); path = Runner; sourceTree = ""; @@ -232,7 +295,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - A860FB1CB44F70AAB3A8ECC8 /* [CP] Check Pods Manifest.lock */, + 815908FE3DE50BB6C87AA0DF /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, C45E0924473D697285AAFD0B /* Frameworks */, @@ -267,11 +330,31 @@ productReference = 4F30C7652E8FBF9D008BB46C /* TimetableWidgetExtension.appex */; productType = "com.apple.product-type.app-extension"; }; + 4FE64E322F27B079006F9205 /* HomeWidgetsExtensionExtension */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4FE64E462F27B07B006F9205 /* Build configuration list for PBXNativeTarget "HomeWidgetsExtensionExtension" */; + buildPhases = ( + 4FE64E2F2F27B079006F9205 /* Sources */, + 4FE64E302F27B079006F9205 /* Frameworks */, + 4FE64E312F27B079006F9205 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 4FE64E362F27B07A006F9205 /* HomeWidgetsExtension */, + ); + name = HomeWidgetsExtensionExtension; + productName = HomeWidgetsExtensionExtension; + productReference = 4FE64E332F27B079006F9205 /* HomeWidgetsExtensionExtension.appex */; + productType = "com.apple.product-type.app-extension"; + }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 5D9E2A8A05449E9C8B9A2400 /* [CP] Check Pods Manifest.lock */, + D576F90540C8E625A9A12317 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, @@ -279,13 +362,14 @@ 4F30C77E2E8FBF9F008BB46C /* Embed Foundation Extensions */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 9705A1C41CF9048500538489 /* Embed Frameworks */, - EA27BF75630C9CBFEB0A3BF3 /* [CP] Embed Pods Frameworks */, - FCB81B57CFF4555354FC425C /* [CP] Copy Pods Resources */, + EAA586B3BBC26BBE7306869D /* [CP] Embed Pods Frameworks */, + 3061E0FD6432139C72AA9A85 /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( 4F30C7772E8FBF9F008BB46C /* PBXTargetDependency */, + 4FE64E412F27B07B006F9205 /* PBXTargetDependency */, ); name = Runner; productName = Runner; @@ -299,7 +383,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 2600; + LastSwiftUpdateCheck = 2620; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { @@ -310,6 +394,9 @@ 4F30C7642E8FBF9D008BB46C = { CreatedOnToolsVersion = 26.0; }; + 4FE64E322F27B079006F9205 = { + CreatedOnToolsVersion = 26.2; + }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; @@ -332,6 +419,7 @@ 97C146ED1CF9000F007C117D /* Runner */, 331C8080294A63A400263BE5 /* RunnerTests */, 4F30C7642E8FBF9D008BB46C /* TimetableWidgetExtension */, + 4FE64E322F27B079006F9205 /* HomeWidgetsExtensionExtension */, ); }; /* End PBXProject section */ @@ -351,6 +439,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 4FE64E312F27B079006F9205 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -365,6 +460,23 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 3061E0FD6432139C72AA9A85 /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -381,44 +493,7 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 5D9E2A8A05449E9C8B9A2400 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 9740EEB61CF901F6004384FC /* Run Script */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputPaths = ( - ); - name = "Run Script"; - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; - }; - A860FB1CB44F70AAB3A8ECC8 /* [CP] Check Pods Manifest.lock */ = { + 815908FE3DE50BB6C87AA0DF /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -440,7 +515,44 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - EA27BF75630C9CBFEB0A3BF3 /* [CP] Embed Pods Frameworks */ = { + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + D576F90540C8E625A9A12317 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + EAA586B3BBC26BBE7306869D /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -448,40 +560,15 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; - FCB81B57CFF4555354FC425C /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - inputPaths = ( - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ @@ -500,14 +587,21 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + 4FE64E2F2F27B079006F9205 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, - 4F30C79A2E8FC427008BB46C /* TimetableActivityAttributes.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 4F30C7592E8FBF26008BB46C /* LiveActivityMethodChannelManager.swift in Sources */, + 4F45A1752F27FA3C0020E6F1 /* HomeWidgetMethodChannel.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -524,6 +618,11 @@ target = 4F30C7642E8FBF9D008BB46C /* TimetableWidgetExtension */; targetProxy = 4F30C7762E8FBF9F008BB46C /* PBXContainerItemProxy */; }; + 4FE64E412F27B07B006F9205 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 4FE64E322F27B079006F9205 /* HomeWidgetsExtensionExtension */; + targetProxy = 4FE64E402F27B07B006F9205 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -610,16 +709,17 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1021; - DEVELOPMENT_TEAM = R9PZGUCNJ3; + DEVELOPMENT_TEAM = UT7MSP4GWZ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = Firka; + INFOPLIST_KEY_CFBundleDisplayName = "Firka Testing"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka; + MARKETING_VERSION = 1.0.9.1; + PRODUCT_BUNDLE_IDENTIFIER = app.firka.firkaa; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -636,7 +736,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 0EE927DD3F0F54BDE10EFE01 /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = EBD040A65B2746AF6A3D5C40 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -655,7 +755,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = B222D922BB8257D2341337A4 /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = AE756C46C544099A30412EAF /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -672,7 +772,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = BDDC8A00836B054E202CC327 /* Pods-RunnerTests.profile.xcconfig */; + baseConfigurationReference = 4836947EC3B04B475B3DA1F8 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -703,7 +803,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = R9PZGUCNJ3; + DEVELOPMENT_TEAM = UT7MSP4GWZ; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; @@ -718,10 +818,10 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.0.9.1; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka.TimetableWidgetExtension; + PRODUCT_BUNDLE_IDENTIFIER = app.firka.firkaa.TimetableWidget; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; @@ -753,7 +853,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = R9PZGUCNJ3; + DEVELOPMENT_TEAM = UT7MSP4GWZ; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; @@ -768,9 +868,9 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.0.9.1; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka.TimetableWidgetExtension; + PRODUCT_BUNDLE_IDENTIFIER = app.firka.firkaa.TimetableWidget; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; @@ -800,7 +900,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = R9PZGUCNJ3; + DEVELOPMENT_TEAM = UT7MSP4GWZ; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; @@ -815,9 +915,9 @@ "@executable_path/../../Frameworks", ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.0.9.1; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka.TimetableWidgetExtension; + PRODUCT_BUNDLE_IDENTIFIER = app.firka.firkaa.TimetableWidget; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; @@ -831,6 +931,142 @@ }; name = Profile; }; + 4FE64E432F27B07B006F9205 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = HomeWidgetsExtensionExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = UT7MSP4GWZ; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = HomeWidgetsExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = HomeWidgetsExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_BUNDLE_IDENTIFIER = app.firka.firkaa.HomeWidgetsExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 4FE64E442F27B07B006F9205 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = HomeWidgetsExtensionExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = UT7MSP4GWZ; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = HomeWidgetsExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = HomeWidgetsExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = app.firka.firkaa.HomeWidgetsExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 4FE64E452F27B07B006F9205 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME = WidgetBackground; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = HomeWidgetsExtensionExtension.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = UT7MSP4GWZ; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = HomeWidgetsExtension/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = HomeWidgetsExtension; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = app.firka.firkaa.HomeWidgetsExtension; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Profile; + }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -956,16 +1192,17 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1021; - DEVELOPMENT_TEAM = R9PZGUCNJ3; + DEVELOPMENT_TEAM = UT7MSP4GWZ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = Firka; + INFOPLIST_KEY_CFBundleDisplayName = "Firka Testing"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka; + MARKETING_VERSION = 1.0.9.1; + PRODUCT_BUNDLE_IDENTIFIER = app.firka.firkaa; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -991,16 +1228,17 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1021; - DEVELOPMENT_TEAM = R9PZGUCNJ3; + DEVELOPMENT_TEAM = UT7MSP4GWZ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = Firka; + INFOPLIST_KEY_CFBundleDisplayName = "Firka Testing"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = app.firka.firka; + MARKETING_VERSION = 1.0.9.1; + PRODUCT_BUNDLE_IDENTIFIER = app.firka.firkaa; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1038,6 +1276,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + 4FE64E462F27B07B006F9205 /* Build configuration list for PBXNativeTarget "HomeWidgetsExtensionExtension" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4FE64E432F27B07B006F9205 /* Debug */, + 4FE64E442F27B07B006F9205 /* Release */, + 4FE64E452F27B07B006F9205 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/firka/ios/Runner/AppDelegate.swift b/firka/ios/Runner/AppDelegate.swift index fe9612f..0e2bd25 100644 --- a/firka/ios/Runner/AppDelegate.swift +++ b/firka/ios/Runner/AppDelegate.swift @@ -22,6 +22,8 @@ import BackgroundTasks let controller = window?.rootViewController as! FlutterViewController + HomeWidgetMethodChannel.register(with: controller.binaryMessenger) + backgroundFetchChannel = FlutterMethodChannel(name: "firka.app/background_fetch", binaryMessenger: controller.binaryMessenger) backgroundFetchChannel?.setMethodCallHandler { [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) in diff --git a/firka/ios/Runner/HomeWidgetMethodChannel.swift b/firka/ios/Runner/HomeWidgetMethodChannel.swift new file mode 100644 index 0000000..bb21cb5 --- /dev/null +++ b/firka/ios/Runner/HomeWidgetMethodChannel.swift @@ -0,0 +1,34 @@ +import Flutter +import WidgetKit + +class HomeWidgetMethodChannel { + static let channelName = "app.firka/home_widgets" + + static func register(with messenger: FlutterBinaryMessenger) { + let channel = FlutterMethodChannel(name: channelName, binaryMessenger: messenger) + + channel.setMethodCallHandler { call, result in + switch call.method { + case "getAppGroupDirectory": + if let containerURL = FileManager.default.containerURL( + forSecurityApplicationGroupIdentifier: "group.app.firka.firkaa" + ) { + result(containerURL.path) + } else { + result(FlutterError(code: "NO_APP_GROUP", message: "App Group not available", details: nil)) + } + + case "reloadAllWidgets": + if #available(iOS 14.0, *) { + WidgetCenter.shared.reloadAllTimelines() + result(nil) + } else { + result(FlutterError(code: "UNSUPPORTED", message: "Widgets require iOS 14+", details: nil)) + } + + default: + result(FlutterMethodNotImplemented) + } + } + } +} diff --git a/firka/ios/Runner/Runner.entitlements b/firka/ios/Runner/Runner.entitlements index f016897..f096a92 100644 --- a/firka/ios/Runner/Runner.entitlements +++ b/firka/ios/Runner/Runner.entitlements @@ -8,7 +8,7 @@ com.apple.security.application-groups - group.app.firka.firka + group.app.firka.firkaa diff --git a/firka/lib/helpers/db/ios_widget_helper.dart b/firka/lib/helpers/db/ios_widget_helper.dart new file mode 100644 index 0000000..325975e --- /dev/null +++ b/firka/lib/helpers/db/ios_widget_helper.dart @@ -0,0 +1,193 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:firka/helpers/api/model/grade.dart'; +import 'package:firka/helpers/api/model/timetable.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +class IOSWidgetHelper { + static const _channel = MethodChannel('app.firka/home_widgets'); + + static Future _getAppGroupDirectory() async { + if (!Platform.isIOS) return null; + + try { + final result = await _channel.invokeMethod('getAppGroupDirectory'); + if (result != null) { + return Directory(result); + } + } catch (e) { + debugPrint('Error getting app group directory: $e'); + } + return null; + } + + static Future updateWidgetData({ + required String locale, + required String theme, + required List todayLessons, + required List tomorrowLessons, + required List grades, + required Map subjectAverages, + required double? overallAverage, + WidgetBreakInfo? currentBreak, + }) async { + if (!Platform.isIOS) return; + + debugPrint('[IOSWidget] Starting updateWidgetData...'); + debugPrint('[IOSWidget] todayLessons: ${todayLessons.length}, tomorrowLessons: ${tomorrowLessons.length}'); + debugPrint('[IOSWidget] grades: ${grades.length}, subjectAverages: ${subjectAverages.length}'); + + final dir = await _getAppGroupDirectory(); + if (dir == null) { + debugPrint('[IOSWidget] ERROR: App Group directory is null!'); + return; + } + debugPrint('[IOSWidget] App Group directory: ${dir.path}'); + + final data = { + 'lastUpdated': DateTime.now().toIso8601String(), + 'locale': locale, + 'theme': theme, + 'timetable': { + 'today': todayLessons.map((l) => _lessonToJson(l)).toList(), + 'tomorrow': tomorrowLessons.map((l) => _lessonToJson(l)).toList(), + 'currentBreak': currentBreak != null ? { + 'name': currentBreak.name, + 'nameKey': currentBreak.nameKey, + 'endDate': currentBreak.endDate.toIso8601String(), + } : null, + }, + 'grades': grades.take(20).map((g) => _gradeToJson(g)).toList(), + 'averages': { + 'overall': overallAverage, + 'subjects': subjectAverages.entries.map((e) => { + 'uid': e.key, + 'name': _getSubjectNameFromGrades(e.key, grades), + 'average': e.value, + 'gradeCount': _getGradeCount(e.key, grades), + }).toList(), + }, + }; + + final jsonString = jsonEncode(data); + debugPrint('[IOSWidget] JSON data length: ${jsonString.length} bytes'); + + final file = File('${dir.path}/widget_data.json'); + await file.writeAsString(jsonString); + debugPrint('[IOSWidget] File written to: ${file.path}'); + + final exists = await file.exists(); + debugPrint('[IOSWidget] File exists after write: $exists'); + + await reloadAllWidgets(); + debugPrint('[IOSWidget] Widget reload triggered'); + } + + /// Format DateTime with explicit timezone offset for proper Swift parsing + static String _formatDateTimeWithOffset(DateTime dt) { + final local = dt.toLocal(); + final offset = local.timeZoneOffset; + final sign = offset.isNegative ? '-' : '+'; + final hours = offset.inHours.abs().toString().padLeft(2, '0'); + final minutes = (offset.inMinutes.abs() % 60).toString().padLeft(2, '0'); + return '${local.toIso8601String()}$sign$hours:$minutes'; + } + + static Map _lessonToJson(Lesson lesson) { + final subject = lesson.subject; + return { + 'uid': lesson.uid, + 'date': lesson.date, + 'start': _formatDateTimeWithOffset(lesson.start), + 'end': _formatDateTimeWithOffset(lesson.end), + 'name': lesson.name, + 'lessonNumber': lesson.lessonNumber, + 'teacher': lesson.teacher, + '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, + } : null, + 'sortIndex': subject.sortIndex, + 'teacherName': subject.teacherName, + } : { + 'uid': '', + 'name': lesson.name, + 'category': null, + 'sortIndex': 0, + 'teacherName': null, + }, + 'theme': lesson.theme, + 'roomName': lesson.roomName, + 'isCancelled': lesson.state.name?.toLowerCase().contains('elmarad') ?? false, + 'isSubstitution': lesson.substituteTeacher != null, + }; + } + + static Map _gradeToJson(Grade grade) { + return { + 'uid': grade.uid, + 'recordDate': _formatDateTimeWithOffset(grade.recordDate), + '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, + } : null, + 'sortIndex': grade.subject.sortIndex, + 'teacherName': grade.subject.teacherName, + }, + 'topic': grade.topic, + 'type': { + 'uid': grade.type.uid, + 'name': grade.type.name, + 'description': grade.type.description, + }, + 'numericValue': grade.numericValue, + 'strValue': grade.strValue, + 'weightPercentage': grade.weightPercentage, + }; + } + + static String _getSubjectNameFromGrades(String uid, List grades) { + try { + final grade = grades.firstWhere((g) => g.subject.uid == uid); + return grade.subject.name; + } catch (e) { + return uid; + } + } + + static int _getGradeCount(String uid, List grades) { + return grades.where((g) => g.subject.uid == uid).length; + } + + static Future reloadAllWidgets() async { + if (!Platform.isIOS) return; + + try { + await _channel.invokeMethod('reloadAllWidgets'); + } catch (e) { + debugPrint('Error reloading widgets: $e'); + } + } +} + +class WidgetBreakInfo { + final String name; + final String nameKey; + final DateTime endDate; + + WidgetBreakInfo({ + required this.name, + required this.nameKey, + required this.endDate, + }); +} diff --git a/firka/lib/helpers/db/widget.dart b/firka/lib/helpers/db/widget.dart index 8b9e561..ce0aafc 100644 --- a/firka/lib/helpers/db/widget.dart +++ b/firka/lib/helpers/db/widget.dart @@ -2,8 +2,13 @@ import 'dart:convert'; import 'dart:io'; import 'package:firka/helpers/api/client/kreta_client.dart'; +import 'package:firka/helpers/api/model/grade.dart'; import 'package:firka/helpers/api/model/timetable.dart'; +import 'package:firka/helpers/db/ios_widget_helper.dart'; import 'package:firka/helpers/debug_helper.dart'; +import 'package:firka/helpers/settings.dart'; +import 'package:firka/main.dart'; +import 'package:flutter/foundation.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; @@ -68,4 +73,172 @@ class WidgetCacheHelper { jsonEncode(WidgetCacheHelper.toJson(style, lessons.response!))); } } + + static Future updateIOSWidgets({ + required String locale, + required String theme, + required List todayLessons, + required List tomorrowLessons, + required List grades, + required Map subjectAverages, + required double? overallAverage, + WidgetBreakInfo? currentBreak, + }) async { + await IOSWidgetHelper.updateWidgetData( + locale: locale, + theme: theme, + todayLessons: todayLessons, + tomorrowLessons: tomorrowLessons, + grades: grades, + subjectAverages: subjectAverages, + overallAverage: overallAverage, + currentBreak: currentBreak, + ); + } + + /// Comprehensive iOS widget refresh that collects all necessary data + /// Call this on: app open, user switch, data refresh + static Future refreshIOSWidgets(KretaClient client, SettingsStore settings) async { + if (!Platform.isIOS) return; + + try { + // Get locale + final langIndex = (settings.group("settings").subGroup("application")["language"] + as SettingsItemsRadio) + .activeIndex; + String locale; + switch (langIndex) { + case 1: + locale = 'hu'; + break; + case 2: + locale = 'en'; + break; + case 3: + locale = 'de'; + break; + default: + locale = 'hu'; // Default to Hungarian + } + + // Get theme + final themeIndex = (settings.group("settings").subGroup("customization")["theme"] + as SettingsItemsRadio) + .activeIndex; + String theme; + switch (themeIndex) { + case 1: + theme = 'light'; + break; + case 2: + theme = 'dark'; + break; + default: + theme = isLightMode.value ? 'light' : 'dark'; + } + + // Get today's and tomorrow's lessons + final now = timeNow(); + final todayMidnight = DateTime(now.year, now.month, now.day); + final tomorrowMidnight = todayMidnight.add(Duration(days: 1)); + + final todayResponse = await client.getTimeTable( + todayMidnight, + todayMidnight.add(Duration(hours: 23, minutes: 59)), + ); + final tomorrowResponse = await client.getTimeTable( + tomorrowMidnight, + tomorrowMidnight.add(Duration(hours: 23, minutes: 59)), + ); + + final todayLessons = todayResponse.response ?? []; + final tomorrowLessons = tomorrowResponse.response ?? []; + + // Get grades + final gradesResponse = await client.getGrades(); + final grades = gradesResponse.response ?? []; + + // Calculate subject averages + final Map subjectAverages = {}; + final Set subjectUids = {}; + + for (var grade in grades) { + subjectUids.add(grade.subject.uid); + } + + double overallSum = 0; + int validSubjectCount = 0; + + for (var uid in subjectUids) { + final subjectGrades = grades.where((g) => g.subject.uid == uid).toList(); + final avg = _calculateWeightedAverage(subjectGrades); + if (!avg.isNaN && avg > 0) { + subjectAverages[uid] = avg; + overallSum += avg; + validSubjectCount++; + } + } + + final double? overallAverage = validSubjectCount > 0 + ? overallSum / validSubjectCount + : null; + + // Check for break (simplified - you might want to enhance this) + WidgetBreakInfo? currentBreak; + // TODO: Add break detection if needed + + await updateIOSWidgets( + locale: locale, + theme: theme, + todayLessons: todayLessons, + tomorrowLessons: tomorrowLessons, + grades: grades, + subjectAverages: subjectAverages, + overallAverage: overallAverage, + currentBreak: currentBreak, + ); + + debugPrint('iOS widgets refreshed successfully'); + } catch (e) { + debugPrint('Error refreshing iOS widgets: $e'); + } + } + + /// Clear iOS widget data (call on logout) + static Future clearIOSWidgets() async { + if (!Platform.isIOS) return; + + try { + await updateIOSWidgets( + locale: 'hu', + theme: 'light', + todayLessons: [], + tomorrowLessons: [], + grades: [], + subjectAverages: {}, + overallAverage: null, + currentBreak: null, + ); + debugPrint('iOS widgets cleared'); + } catch (e) { + debugPrint('Error clearing iOS widgets: $e'); + } + } + + /// Calculate weighted average for a list of grades + static double _calculateWeightedAverage(List grades) { + var weightTotal = 0.0; + var sum = 0.0; + + for (var grade in grades) { + if (grade.numericValue != null) { + var weight = (grade.weightPercentage ?? 100) / 100.0; + weightTotal += weight; + sum += grade.numericValue! * weight; + } + } + + if (weightTotal == 0) return double.nan; + return sum / weightTotal; + } } diff --git a/firka/lib/main.dart b/firka/lib/main.dart index b5766b5..30c0d98 100644 --- a/firka/lib/main.dart +++ b/firka/lib/main.dart @@ -209,6 +209,10 @@ Future _initData(AppInitialization init) async { await WidgetCacheHelper.updateWidgetCache(appStyle, init.client); + if (Platform.isIOS) { + await WidgetCacheHelper.refreshIOSWidgets(init.client, init.settings); + } + if (Platform.isIOS) { final studentName = token.studentId ?? "Student"; diff --git a/firka/lib/ui/phone/screens/home/home_screen.dart b/firka/lib/ui/phone/screens/home/home_screen.dart index 408989d..076cd8b 100644 --- a/firka/lib/ui/phone/screens/home/home_screen.dart +++ b/firka/lib/ui/phone/screens/home/home_screen.dart @@ -137,6 +137,10 @@ class _HomeScreenState extends FirkaState { qualifiedAndroidName: "app.firka.naplo.glance.TimetableWidget"); } + if (Platform.isIOS) { + await WidgetCacheHelper.refreshIOSWidgets(widget.data.client, widget.data.settings); + } + if (Platform.isIOS && LiveActivityService.isTokenExpired && !_disposed) { showReauthBottomSheet(context, widget.data, widget.data.l10n.reauth); } diff --git a/firka/lib/ui/phone/screens/settings/settings_screen.dart b/firka/lib/ui/phone/screens/settings/settings_screen.dart index 2a2feb9..fec2195 100644 --- a/firka/lib/ui/phone/screens/settings/settings_screen.dart +++ b/firka/lib/ui/phone/screens/settings/settings_screen.dart @@ -20,6 +20,7 @@ import 'package:path/path.dart' as p; import 'package:share_plus/share_plus.dart'; import 'package:url_launcher/url_launcher_string.dart'; +import '../../../../helpers/db/widget.dart'; import '../../../../helpers/firka_bundle.dart'; import '../../../../helpers/firka_state.dart'; import '../../../../helpers/settings.dart'; @@ -705,6 +706,7 @@ class _SettingsScreenState extends FirkaState { onTap: () async { if (Platform.isIOS) { await LiveActivityService.onUserLogout(); + await WidgetCacheHelper.clearIOSWidgets(); } final active = widget.data.client.model.studentIdNorm!;