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!;