diff --git a/firka/ios/HomeWidgetsExtension/AveragesWidget.swift b/firka/ios/HomeWidgetsExtension/AveragesWidget.swift
index f148d3b1..99dbfc48 100644
--- a/firka/ios/HomeWidgetsExtension/AveragesWidget.swift
+++ b/firka/ios/HomeWidgetsExtension/AveragesWidget.swift
@@ -28,15 +28,18 @@ struct AveragesWidgetView: View {
}
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)
+ Group {
+ 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)
+ }
}
+ .widgetURL(URL(string: "firka://widget/grades"))
}
}
diff --git a/firka/ios/HomeWidgetsExtension/GradesWidget.swift b/firka/ios/HomeWidgetsExtension/GradesWidget.swift
index 01fe1b77..7b55be15 100644
--- a/firka/ios/HomeWidgetsExtension/GradesWidget.swift
+++ b/firka/ios/HomeWidgetsExtension/GradesWidget.swift
@@ -28,15 +28,18 @@ struct GradesWidgetView: View {
}
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)
+ Group {
+ 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)
+ }
}
+ .widgetURL(URL(string: "firka://widget/grades"))
}
}
diff --git a/firka/ios/HomeWidgetsExtension/Providers/TimetableProvider.swift b/firka/ios/HomeWidgetsExtension/Providers/TimetableProvider.swift
index db79470d..b58a50fa 100644
--- a/firka/ios/HomeWidgetsExtension/Providers/TimetableProvider.swift
+++ b/firka/ios/HomeWidgetsExtension/Providers/TimetableProvider.swift
@@ -90,6 +90,7 @@ struct TimetableProvider: AppIntentTimelineProvider {
private func createEntry(for configuration: TimetableWidgetIntent, date: Date) -> TimetableEntry {
let data = WidgetData.load()
+ let calendar = Calendar.current
guard let data = data else {
return TimetableEntry(
@@ -121,13 +122,32 @@ struct TimetableProvider: AppIntentTimelineProvider {
)
}
+ let entryDay = calendar.startOfDay(for: date)
+
var lessons = data.timetable.today
var isNextDay = false
- let lastLesson = lessons.last
- if let last = lastLesson, date > last.end {
+ if let firstTodayLesson = lessons.first {
+ let todayLessonDay = calendar.startOfDay(for: firstTodayLesson.start)
+
+ if entryDay > todayLessonDay {
+ lessons = data.timetable.tomorrow
+ if let firstTomorrowLesson = lessons.first {
+ let tomorrowLessonDay = calendar.startOfDay(for: firstTomorrowLesson.start)
+ isNextDay = entryDay < tomorrowLessonDay
+ }
+ } else {
+ let lastLesson = lessons.last
+ if let last = lastLesson, date > last.end {
+ lessons = data.timetable.tomorrow
+ isNextDay = true
+ }
+ }
+ } else {
lessons = data.timetable.tomorrow
- isNextDay = true
+ if !lessons.isEmpty {
+ isNextDay = true
+ }
}
if lessons.isEmpty {
diff --git a/firka/ios/HomeWidgetsExtension/TimetableWidget.swift b/firka/ios/HomeWidgetsExtension/TimetableWidget.swift
index d66ca348..73bb6ad9 100644
--- a/firka/ios/HomeWidgetsExtension/TimetableWidget.swift
+++ b/firka/ios/HomeWidgetsExtension/TimetableWidget.swift
@@ -32,26 +32,29 @@ struct TimetableWidgetView: View {
}
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)
+ Group {
+ 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)
+ }
}
}
+ .widgetURL(URL(string: "firka://widget/timetable"))
}
}
diff --git a/firka/ios/HomeWidgetsExtension/Views/TimetableViews.swift b/firka/ios/HomeWidgetsExtension/Views/TimetableViews.swift
index 24661799..1fcf8e5d 100644
--- a/firka/ios/HomeWidgetsExtension/Views/TimetableViews.swift
+++ b/firka/ios/HomeWidgetsExtension/Views/TimetableViews.swift
@@ -4,6 +4,7 @@ import WidgetKit
struct TimetableSmallView: View {
let entry: TimetableEntry
let localization: WidgetLocalization
+ @Environment(\.colorScheme) var colorScheme
var style: WidgetStyleType {
(entry.configuration.style ?? .appTheme) == .liquidGlass ? .liquidGlass : .appTheme
@@ -13,6 +14,14 @@ struct TimetableSmallView: View {
(entry.configuration.displayMode ?? .current) == .current ? entry.currentLesson : entry.nextLesson
}
+ var liquidGlassPrimary: Color {
+ colorScheme == .dark ? .white : .black
+ }
+
+ var liquidGlassSecondary: Color {
+ colorScheme == .dark ? .white.opacity(0.7) : .black.opacity(0.6)
+ }
+
var body: some View {
ZStack {
WidgetBackground(style: style, colors: nil)
@@ -31,14 +40,14 @@ struct TimetableSmallView: View {
.strikethrough(lesson.isCancelled, color: .red)
.foregroundColor(lesson.isCancelled ? .red :
lesson.isSubstitution ? .orange :
- (style == .liquidGlass ? .white : .primary))
+ (style == .liquidGlass ? liquidGlassPrimary : .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))
+ (style == .liquidGlass ? liquidGlassSecondary : .secondary))
if let room = lesson.roomName {
Text(room)
@@ -47,7 +56,7 @@ struct TimetableSmallView: View {
.minimumScaleFactor(0.8)
.foregroundColor(lesson.isCancelled ? .red.opacity(0.7) :
lesson.isSubstitution ? .orange.opacity(0.7) :
- (style == .liquidGlass ? .white.opacity(0.6) : .secondary))
+ (style == .liquidGlass ? liquidGlassSecondary : .secondary))
}
} else {
Text(localization.string("no_lessons"))
@@ -133,6 +142,7 @@ struct LessonRow: View {
let isActive: Bool
let style: WidgetStyleType
var showRoom: Bool = false
+ @Environment(\.colorScheme) var colorScheme
var lessonTextColor: Color? {
if lesson.isCancelled {
@@ -143,6 +153,14 @@ struct LessonRow: View {
return nil
}
+ var liquidGlassPrimary: Color {
+ colorScheme == .dark ? .white : .black
+ }
+
+ var liquidGlassSecondary: Color {
+ colorScheme == .dark ? .white.opacity(0.7) : .black.opacity(0.6)
+ }
+
var numberBackgroundColor: Color {
if lesson.isCancelled {
return Color.red.opacity(0.3)
@@ -165,21 +183,21 @@ struct LessonRow: View {
Circle()
.fill(numberBackgroundColor)
)
- .foregroundColor(lessonTextColor ?? (style == .liquidGlass ? .white : .primary))
+ .foregroundColor(lessonTextColor ?? (style == .liquidGlass ? liquidGlassPrimary : .primary))
}
Text(lesson.displayName)
.font(.subheadline)
.fontWeight(isActive ? .semibold : .regular)
.strikethrough(lesson.isCancelled, color: .red)
- .foregroundColor(lessonTextColor ?? (style == .liquidGlass ? .white : .primary))
+ .foregroundColor(lessonTextColor ?? (style == .liquidGlass ? liquidGlassPrimary : .primary))
.lineLimit(1)
Spacer()
Text(lesson.timeString)
.font(.caption)
- .foregroundColor(lessonTextColor?.opacity(0.8) ?? (style == .liquidGlass ? .white.opacity(0.7) : .secondary))
+ .foregroundColor(lessonTextColor?.opacity(0.8) ?? (style == .liquidGlass ? liquidGlassSecondary : .secondary))
if showRoom, let room = lesson.roomName {
Text(room)
@@ -192,7 +210,7 @@ struct LessonRow: View {
lesson.isSubstitution ? Color.orange.opacity(0.2) :
Color.secondary.opacity(0.2))
)
- .foregroundColor(lessonTextColor?.opacity(0.8) ?? (style == .liquidGlass ? .white.opacity(0.7) : .secondary))
+ .foregroundColor(lessonTextColor?.opacity(0.8) ?? (style == .liquidGlass ? liquidGlassSecondary : .secondary))
}
}
.padding(.vertical, 4)
diff --git a/firka/ios/Runner/AppDelegate.swift b/firka/ios/Runner/AppDelegate.swift
index 0e2bd25d..1d75cef2 100644
--- a/firka/ios/Runner/AppDelegate.swift
+++ b/firka/ios/Runner/AppDelegate.swift
@@ -11,6 +11,8 @@ import BackgroundTasks
private var deviceTokenString: String?
private var notificationChannel: FlutterMethodChannel?
private var backgroundFetchChannel: FlutterMethodChannel?
+ private var widgetDeepLinkChannel: FlutterMethodChannel?
+ private var pendingWidgetDeepLink: String?
private let backgroundTaskIdentifier = "app.firka.timetable.refresh"
@@ -24,6 +26,20 @@ import BackgroundTasks
HomeWidgetMethodChannel.register(with: controller.binaryMessenger)
+ widgetDeepLinkChannel = FlutterMethodChannel(name: "firka.app/widget_deep_link", binaryMessenger: controller.binaryMessenger)
+ widgetDeepLinkChannel?.setMethodCallHandler { [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) in
+ if call.method == "getPendingDeepLink" {
+ if let link = self?.pendingWidgetDeepLink {
+ self?.pendingWidgetDeepLink = nil
+ result(link)
+ } else {
+ result(nil)
+ }
+ } else {
+ result(FlutterMethodNotImplemented)
+ }
+ }
+
backgroundFetchChannel = FlutterMethodChannel(name: "firka.app/background_fetch", binaryMessenger: controller.binaryMessenger)
backgroundFetchChannel?.setMethodCallHandler { [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) in
@@ -208,4 +224,14 @@ import BackgroundTasks
// Background fetch will be scheduled from Flutter side when needed
// No automatic scheduling here to give Flutter full control
}
+
+ override func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
+ if url.scheme == "firka" && url.host == "widget" {
+ let path = url.path.replacingOccurrences(of: "/", with: "")
+ pendingWidgetDeepLink = path
+ widgetDeepLinkChannel?.invokeMethod("onWidgetDeepLink", arguments: path)
+ return true
+ }
+ return super.application(app, open: url, options: options)
+ }
}
diff --git a/firka/ios/Runner/Info.plist b/firka/ios/Runner/Info.plist
index 5db8bca9..748e8e9b 100644
--- a/firka/ios/Runner/Info.plist
+++ b/firka/ios/Runner/Info.plist
@@ -28,6 +28,17 @@
????
CFBundleVersion
$(FLUTTER_BUILD_NUMBER)
+ CFBundleURLTypes
+
+
+ CFBundleURLName
+ app.firka.firkaa
+ CFBundleURLSchemes
+
+ firka
+
+
+
LSRequiresIPhoneOS
NSAppTransportSecurity
diff --git a/firka/lib/ui/phone/screens/home/home_screen.dart b/firka/lib/ui/phone/screens/home/home_screen.dart
index 076cd8b7..f4a69220 100644
--- a/firka/lib/ui/phone/screens/home/home_screen.dart
+++ b/firka/lib/ui/phone/screens/home/home_screen.dart
@@ -79,6 +79,7 @@ class _HomeScreenState extends FirkaState {
_HomeScreenState();
final PageController _pageController = PageController();
+ HomePage? _pendingNavigation;
Widget? toast;
bool pairingDone = false;
@@ -114,15 +115,88 @@ class _HomeScreenState extends FirkaState {
if (action != null || route != null) {
logger.info('Navigating to timetable from notification');
- setState(() {
- homeScreenPage = HomePage.timetable;
- _pageController.jumpToPage(HomePage.timetable.index);
- });
+ _navigateToPage(HomePage.timetable);
}
}
});
}
+ void _setupWidgetDeepLinkListener() {
+ if (!Platform.isIOS) return;
+
+ final widgetChannel = MethodChannel('firka.app/widget_deep_link');
+
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ widgetChannel.invokeMethod('getPendingDeepLink').then((link) {
+ if (link != null) {
+ _handleWidgetDeepLink(link);
+ }
+ });
+ });
+
+ widgetChannel.setMethodCallHandler((call) async {
+ if (call.method == 'onWidgetDeepLink') {
+ final link = call.arguments as String?;
+ if (link != null) {
+ _handleWidgetDeepLink(link);
+ }
+ }
+ });
+ }
+
+ void _handleWidgetDeepLink(String link) {
+ logger.info('Widget deep link received: $link');
+
+ HomePage targetPage;
+ switch (link) {
+ case 'timetable':
+ targetPage = HomePage.timetable;
+ break;
+ case 'grades':
+ targetPage = HomePage.grades;
+ break;
+ default:
+ logger.warning('Unknown widget deep link: $link');
+ return;
+ }
+
+ _navigateToPage(targetPage);
+ }
+
+ void _navigateToPage(HomePage targetPage) {
+ if (_disposed) return;
+
+ homeScreenPage = targetPage;
+
+ if (_pageController.hasClients) {
+ setState(() {
+ _pageController.jumpToPage(targetPage.index);
+ });
+ } else {
+ _pendingNavigation = targetPage;
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ _applyPendingNavigation();
+ });
+ }
+ }
+
+ void _applyPendingNavigation() {
+ if (_disposed || _pendingNavigation == null) return;
+
+ if (_pageController.hasClients) {
+ final target = _pendingNavigation!;
+ _pendingNavigation = null;
+ setState(() {
+ homeScreenPage = target;
+ _pageController.jumpToPage(target.index);
+ });
+ } else {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ _applyPendingNavigation();
+ });
+ }
+ }
+
void prefetch() async {
if (_prefetched) return;
@@ -298,6 +372,7 @@ class _HomeScreenState extends FirkaState {
});
_setupNotificationListener();
+ _setupWidgetDeepLinkListener();
prefetch();
_preloadImages();