forked from firka/firka
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:
10
firka/ios/HomeWidgetsExtension.entitlements
Normal file
10
firka/ios/HomeWidgetsExtension.entitlements
Normal 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>
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
42
firka/ios/HomeWidgetsExtension/AveragesWidget.swift
Normal file
42
firka/ios/HomeWidgetsExtension/AveragesWidget.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
42
firka/ios/HomeWidgetsExtension/GradesWidget.swift
Normal file
42
firka/ios/HomeWidgetsExtension/GradesWidget.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
123
firka/ios/HomeWidgetsExtension/Helpers/Localization.swift
Normal file
123
firka/ios/HomeWidgetsExtension/Helpers/Localization.swift
Normal 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)")
|
||||
}
|
||||
}
|
||||
11
firka/ios/HomeWidgetsExtension/HomeWidgetsBundle.swift
Normal file
11
firka/ios/HomeWidgetsExtension/HomeWidgetsBundle.swift
Normal file
@@ -0,0 +1,11 @@
|
||||
import WidgetKit
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct HomeWidgetsBundle: WidgetBundle {
|
||||
var body: some Widget {
|
||||
TimetableWidget()
|
||||
GradesWidget()
|
||||
AveragesWidget()
|
||||
}
|
||||
}
|
||||
11
firka/ios/HomeWidgetsExtension/Info.plist
Normal file
11
firka/ios/HomeWidgetsExtension/Info.plist
Normal 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>
|
||||
47
firka/ios/HomeWidgetsExtension/Intents/AveragesIntent.swift
Normal file
47
firka/ios/HomeWidgetsExtension/Intents/AveragesIntent.swift
Normal 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]?
|
||||
}
|
||||
10
firka/ios/HomeWidgetsExtension/Intents/GradesIntent.swift
Normal file
10
firka/ios/HomeWidgetsExtension/Intents/GradesIntent.swift
Normal 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?
|
||||
}
|
||||
45
firka/ios/HomeWidgetsExtension/Intents/TimetableIntent.swift
Normal file
45
firka/ios/HomeWidgetsExtension/Intents/TimetableIntent.swift
Normal 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?
|
||||
}
|
||||
18
firka/ios/HomeWidgetsExtension/Models/Average.swift
Normal file
18
firka/ios/HomeWidgetsExtension/Models/Average.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
40
firka/ios/HomeWidgetsExtension/Models/Grade.swift
Normal file
40
firka/ios/HomeWidgetsExtension/Models/Grade.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
37
firka/ios/HomeWidgetsExtension/Models/Lesson.swift
Normal file
37
firka/ios/HomeWidgetsExtension/Models/Lesson.swift
Normal 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
|
||||
}
|
||||
}
|
||||
15
firka/ios/HomeWidgetsExtension/Models/Subject.swift
Normal file
15
firka/ios/HomeWidgetsExtension/Models/Subject.swift
Normal 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?
|
||||
}
|
||||
30
firka/ios/HomeWidgetsExtension/Models/WidgetColors.swift
Normal file
30
firka/ios/HomeWidgetsExtension/Models/WidgetColors.swift
Normal 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) }
|
||||
}
|
||||
129
firka/ios/HomeWidgetsExtension/Models/WidgetData.swift
Normal file
129
firka/ios/HomeWidgetsExtension/Models/WidgetData.swift
Normal 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 }
|
||||
}
|
||||
@@ -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"
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
167
firka/ios/HomeWidgetsExtension/Providers/TimetableProvider.swift
Normal file
167
firka/ios/HomeWidgetsExtension/Providers/TimetableProvider.swift
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
57
firka/ios/HomeWidgetsExtension/TimetableWidget.swift
Normal file
57
firka/ios/HomeWidgetsExtension/TimetableWidget.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
167
firka/ios/HomeWidgetsExtension/Views/AveragesViews.swift
Normal file
167
firka/ios/HomeWidgetsExtension/Views/AveragesViews.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
169
firka/ios/HomeWidgetsExtension/Views/GradesViews.swift
Normal file
169
firka/ios/HomeWidgetsExtension/Views/GradesViews.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
257
firka/ios/HomeWidgetsExtension/Views/TimetableViews.swift
Normal file
257
firka/ios/HomeWidgetsExtension/Views/TimetableViews.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
102
firka/ios/HomeWidgetsExtension/Views/WidgetStyles.swift
Normal file
102
firka/ios/HomeWidgetsExtension/Views/WidgetStyles.swift
Normal 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))
|
||||
}
|
||||
}
|
||||
28
firka/ios/HomeWidgetsExtension/de.lproj/Localizable.strings
Normal file
28
firka/ios/HomeWidgetsExtension/de.lproj/Localizable.strings
Normal 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";
|
||||
28
firka/ios/HomeWidgetsExtension/en.lproj/Localizable.strings
Normal file
28
firka/ios/HomeWidgetsExtension/en.lproj/Localizable.strings
Normal 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";
|
||||
28
firka/ios/HomeWidgetsExtension/hu.lproj/Localizable.strings
Normal file
28
firka/ios/HomeWidgetsExtension/hu.lproj/Localizable.strings
Normal 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";
|
||||
10
firka/ios/HomeWidgetsExtensionExtension.entitlements
Normal file
10
firka/ios/HomeWidgetsExtensionExtension.entitlements
Normal 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>
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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
|
||||
|
||||
34
firka/ios/Runner/HomeWidgetMethodChannel.swift
Normal file
34
firka/ios/Runner/HomeWidgetMethodChannel.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
193
firka/lib/helpers/db/ios_widget_helper.dart
Normal file
193
firka/lib/helpers/db/ios_widget_helper.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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!;
|
||||
|
||||
Reference in New Issue
Block a user