Add iOS Home Widgets Extension with timetable, grades, and averages

Introduces a new HomeWidgetsExtension for iOS, including widgets for timetable, recent grades, and subject averages. Adds widget models, providers, intents, localization, and SwiftUI views. Updates project files and main app to support widget data sharing and communication. Also includes new Dart helper for widget data and updates to relevant Flutter screens.
This commit is contained in:
Horváth Gergely
2026-01-27 18:58:25 +01:00
committed by 4831c0
parent 1f57281004
commit beb4127ef8
40 changed files with 2563 additions and 120 deletions

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.app.firka.firkaa</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)")
}
}

View File

@@ -0,0 +1,11 @@
import WidgetKit
import SwiftUI
@main
struct HomeWidgetsBundle: WidgetBundle {
var body: some Widget {
TimetableWidget()
GradesWidget()
AveragesWidget()
}
}

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
</dict>
</plist>

View File

@@ -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]?
}

View File

@@ -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?
}

View File

@@ -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?
}

View File

@@ -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)
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}
}

View File

@@ -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?
}

View File

@@ -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) }
}

View File

@@ -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 }
}

View File

@@ -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<AveragesEntry> {
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"
)
}
}

View File

@@ -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<GradesEntry> {
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))
}
}

View File

@@ -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<TimetableEntry> {
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
)
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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()
}
}
}

View File

@@ -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))
}
}

View File

@@ -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";

View File

@@ -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";

View File

@@ -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";

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.app.firka.firkaa</string>
</array>
</dict>
</plist>

View File

@@ -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 = "<group>"; };
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 = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
4F25FCBD2EB1790E0060DAAA /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
4F25FCBE2EB17D810060DAAA /* TimetableWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TimetableWidgetExtension.entitlements; sourceTree = "<group>"; };
4F30C7582E8FBF26008BB46C /* LiveActivityMethodChannelManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityMethodChannelManager.swift; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
4F959B792F289CA600FF7F03 /* TimetableWidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = TimetableWidgetExtension.entitlements; sourceTree = "<group>"; };
4F959B9C2F289CA600FF7F03 /* HomeWidgetsExtensionExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = HomeWidgetsExtensionExtension.entitlements; sourceTree = "<group>"; };
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 = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
@@ -92,23 +104,62 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
/* 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 = "<group>"; };
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 = "<group>";
};
4FE64E362F27B07A006F9205 /* HomeWidgetsExtension */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
4FE64E472F27B07B006F9205 /* Exceptions for "HomeWidgetsExtension" folder in "HomeWidgetsExtensionExtension" target */,
);
explicitFileTypes = {
};
explicitFolders = (
);
path = HomeWidgetsExtension;
sourceTree = "<group>";
};
/* 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 = "<group>";
@@ -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 = "<group>";
@@ -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 = "<group>";
@@ -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 = "<group>";
@@ -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 = (

View File

@@ -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

View File

@@ -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)
}
}
}
}

View File

@@ -8,7 +8,7 @@
<true/>
<key>com.apple.security.application-groups</key>
<array>
<string>group.app.firka.firka</string>
<string>group.app.firka.firkaa</string>
</array>
</dict>
</plist>

View File

@@ -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<Directory?> _getAppGroupDirectory() async {
if (!Platform.isIOS) return null;
try {
final result = await _channel.invokeMethod<String>('getAppGroupDirectory');
if (result != null) {
return Directory(result);
}
} catch (e) {
debugPrint('Error getting app group directory: $e');
}
return null;
}
static Future<void> updateWidgetData({
required String locale,
required String theme,
required List<Lesson> todayLessons,
required List<Lesson> tomorrowLessons,
required List<Grade> grades,
required Map<String, double> 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<String, dynamic> _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<String, dynamic> _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<Grade> 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<Grade> grades) {
return grades.where((g) => g.subject.uid == uid).length;
}
static Future<void> 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,
});
}

View File

@@ -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<void> updateIOSWidgets({
required String locale,
required String theme,
required List<Lesson> todayLessons,
required List<Lesson> tomorrowLessons,
required List<Grade> grades,
required Map<String, double> 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<void> 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<String, double> subjectAverages = {};
final Set<String> 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<void> 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<Grade> 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;
}
}

View File

@@ -209,6 +209,10 @@ Future<void> _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";

View File

@@ -137,6 +137,10 @@ class _HomeScreenState extends FirkaState<HomeScreen> {
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);
}

View File

@@ -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<SettingsScreen> {
onTap: () async {
if (Platform.isIOS) {
await LiveActivityService.onUserLogout();
await WidgetCacheHelper.clearIOSWidgets();
}
final active = widget.data.client.model.studentIdNorm!;