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