diff --git a/firka/ios/HomeWidgetsExtension/AveragesWidget.swift b/firka/ios/HomeWidgetsExtension/AveragesWidget.swift index f148d3b..99dbfc4 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 01fe1b7..7b55be1 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 db79470..b58a50f 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 d66ca34..73bb6ad 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 2466179..1fcf8e5 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 0e2bd25..1d75cef 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 5db8bca..748e8e9 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 076cd8b..f4a6922 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();