From 299a769f74bdefc63d6b996ce1955f2b8222936c Mon Sep 17 00:00:00 2001 From: Armand <4831c0@proton.me> Date: Sat, 28 Feb 2026 10:25:13 +0100 Subject: [PATCH] firka: migrate state management to Bloc - Add flutter_bloc dependency - Create ThemeCubit, SettingsCubit, ProfilePictureCubit, ReauthCubit, HomeRefreshCubit - Replace UpdateNotifier/ValueNotifier with Bloc across app - Remove update_notifier.dart and FirkaState globalUpdate listener - Provide cubits via MultiBlocProvider at app root --- firka/lib/api/client/kreta_client.dart | 18 ++-- firka/lib/app/app_state.dart | 20 ++--- firka/lib/app/initialization.dart | 32 ++++--- firka/lib/app/initialization_screen.dart | 90 +++++++++++-------- firka/lib/core/bloc/home_refresh_cubit.dart | 19 ++++ .../lib/core/bloc/profile_picture_cubit.dart | 15 ++++ firka/lib/core/bloc/reauth_cubit.dart | 19 ++++ firka/lib/core/bloc/settings_cubit.dart | 15 ++++ firka/lib/core/bloc/theme_cubit.dart | 20 +++++ firka/lib/core/profile_picture.dart | 2 +- firka/lib/core/settings.dart | 21 +++-- firka/lib/core/state/firka_state.dart | 31 +------ firka/lib/core/state/update_notifier.dart | 7 -- firka/lib/data/widget.dart | 8 +- firka/lib/routing/app_router.dart | 42 ++------- firka/lib/services/live_activity_service.dart | 2 +- firka/lib/services/watch_sync_helper.dart | 20 ++--- .../ui/components/common_bottom_sheets.dart | 4 +- firka/lib/ui/components/firka_card.dart | 10 ++- firka/lib/ui/components/firka_shadow.dart | 6 +- firka/lib/ui/phone/pages/extras/extras.dart | 36 +++++--- .../lib/ui/phone/pages/home/home_grades.dart | 46 ++++------ .../phone/pages/home/home_grades_subject.dart | 49 +++++----- firka/lib/ui/phone/pages/home/home_main.dart | 44 ++++----- .../ui/phone/pages/home/home_timetable.dart | 64 ++++++------- .../phone/pages/home/home_timetable_mo.dart | 49 +++++----- .../ui/phone/screens/home/home_screen.dart | 73 ++++++++------- .../ui/phone/screens/login/login_screen.dart | 6 +- .../screens/settings/settings_screen.dart | 3 +- .../lib/ui/phone/widgets/bottom_nav_icon.dart | 2 - firka/lib/ui/phone/widgets/login_webview.dart | 3 +- firka/pubspec.yaml | 1 + 32 files changed, 403 insertions(+), 374 deletions(-) create mode 100644 firka/lib/core/bloc/home_refresh_cubit.dart create mode 100644 firka/lib/core/bloc/profile_picture_cubit.dart create mode 100644 firka/lib/core/bloc/reauth_cubit.dart create mode 100644 firka/lib/core/bloc/settings_cubit.dart create mode 100644 firka/lib/core/bloc/theme_cubit.dart delete mode 100644 firka/lib/core/state/update_notifier.dart diff --git a/firka/lib/api/client/kreta_client.dart b/firka/lib/api/client/kreta_client.dart index f21a6620..87ee7845 100644 --- a/firka/lib/api/client/kreta_client.dart +++ b/firka/lib/api/client/kreta_client.dart @@ -13,6 +13,7 @@ import 'package:intl/intl.dart'; import 'package:isar_community/isar.dart'; import 'package:firka/app/app_state.dart'; +import 'package:firka/core/bloc/reauth_cubit.dart'; import 'package:firka/data/models/token_model.dart'; import 'package:firka/data/util.dart'; import 'package:firka/core/debug_helper.dart'; @@ -60,26 +61,23 @@ class KretaClient { Completer? _tokenMutexCompleter; TokenModel model; Isar isar; + final ReauthCubit _reauthCubit; - static bool needsReauth = false; + KretaClient(this.model, this.isar, this._reauthCubit); - static final ValueNotifier reauthStateNotifier = ValueNotifier(false); + bool get needsReauth => _reauthCubit.state.needsReauth; - static void clearReauthFlag() { - needsReauth = false; - reauthStateNotifier.value = false; + void clearReauthFlag() { + _reauthCubit.clear(); debugPrint('[KretaClient] Reauth flag cleared'); } - static Future _setReauthFlag() async { + Future _setReauthFlag() async { if (needsReauth) return; - needsReauth = true; - reauthStateNotifier.value = true; + _reauthCubit.setNeedsReauth(true); debugPrint('[KretaClient] Reauth flag set'); } - KretaClient(this.model, this.isar); - Future _refreshModelWithCrossDeviceLease( TokenModel sourceToken, ) async { diff --git a/firka/lib/app/app_state.dart b/firka/lib/app/app_state.dart index 90795ae6..0e9b695d 100644 --- a/firka/lib/app/app_state.dart +++ b/firka/lib/app/app_state.dart @@ -3,8 +3,12 @@ import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:firka/api/client/kreta_client.dart'; +import 'package:firka/core/bloc/home_refresh_cubit.dart'; +import 'package:firka/core/bloc/profile_picture_cubit.dart'; +import 'package:firka/core/bloc/reauth_cubit.dart'; +import 'package:firka/core/bloc/settings_cubit.dart'; +import 'package:firka/core/bloc/theme_cubit.dart'; import 'package:firka/data/models/token_model.dart'; -import 'package:firka/core/state/update_notifier.dart'; import 'package:firka/core/settings.dart'; import 'package:firka/l10n/app_localizations.dart'; import 'package:logging/logging.dart'; @@ -24,13 +28,6 @@ GoRouter? appRouter; final dio = Dio(); final isBeta = true; -final ValueNotifier isLightMode = ValueNotifier(true); -final UpdateNotifier globalUpdate = UpdateNotifier(); - -/// Used by home shell screens for pull-to-refresh coordination. -final UpdateNotifier homeUpdateNotifier = UpdateNotifier(); -final UpdateNotifier homeUpdateFinishedNotifier = UpdateNotifier(); - class DeviceInfo { String model; @@ -56,8 +53,11 @@ class AppInitialization { bool hasWatchListener = false; Uint8List? profilePicture; SettingsStore settings; - UpdateNotifier settingsUpdateNotifier = UpdateNotifier(); - UpdateNotifier profilePictureUpdateNotifier = UpdateNotifier(); + ThemeCubit? themeCubit; + SettingsCubit? settingsCubit; + ProfilePictureCubit? profilePictureCubit; + ReauthCubit? reauthCubit; + HomeRefreshCubit? homeRefreshCubit; AppLocalizations l10n; final GlobalKey navigatorKey; diff --git a/firka/lib/app/initialization.dart b/firka/lib/app/initialization.dart index a8db8d65..fce00435 100644 --- a/firka/lib/app/initialization.dart +++ b/firka/lib/app/initialization.dart @@ -5,6 +5,11 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; import 'package:firka/app/app_state.dart'; +import 'package:firka/core/bloc/home_refresh_cubit.dart'; +import 'package:firka/core/bloc/profile_picture_cubit.dart'; +import 'package:firka/core/bloc/reauth_cubit.dart'; +import 'package:firka/core/bloc/settings_cubit.dart'; +import 'package:firka/core/bloc/theme_cubit.dart'; import 'package:firka/services/active_account_helper.dart'; import 'package:firka/api/client/kreta_client.dart'; import 'package:firka/data/models/app_settings_model.dart'; @@ -101,6 +106,9 @@ Future initLang(AppInitialization data) async { } void initTheme(AppInitialization data) { + final themeCubit = data.themeCubit; + if (themeCubit == null) return; + final brightness = SchedulerBinding.instance.platformDispatcher.platformBrightness; @@ -109,24 +117,29 @@ void initTheme(AppInitialization data) { .activeIndex) { case 1: appStyle = lightStyle; - isLightMode.value = true; + themeCubit.setLightMode(true); break; case 2: appStyle = darkStyle; - isLightMode.value = false; + themeCubit.setLightMode(false); break; default: if (brightness == Brightness.dark) { appStyle = darkStyle; - isLightMode.value = false; + themeCubit.setLightMode(false); } else { appStyle = lightStyle; - isLightMode.value = true; + themeCubit.setLightMode(true); } } } Future _initData(AppInitialization init) async { + init.themeCubit ??= ThemeCubit(); + init.settingsCubit ??= SettingsCubit(); + init.profilePictureCubit ??= ProfilePictureCubit(); + init.reauthCubit ??= ReauthCubit(); + init.homeRefreshCubit ??= HomeRefreshCubit(); await init.settings.load(init.isar.appSettingsModels); await initLang(init); initTheme(init); @@ -136,7 +149,6 @@ Future _initData(AppInitialization init) async { var dispatcher = SchedulerBinding.instance.platformDispatcher; dispatcher.onPlatformBrightnessChanged = () { - globalUpdate.update(); initTheme(init); }; @@ -158,7 +170,7 @@ Future _initData(AppInitialization init) async { "[Init] System locale changed in auto mode: $previousLocale -> $nextLocale", ); } - globalUpdate.update(); + init.themeCubit?.refresh(); }()); }; @@ -197,12 +209,12 @@ Future _initData(AppInitialization init) async { return; } logger.fine("Initializing kréta client as: ${token.studentId}"); - init.client = KretaClient(token, init.isar); + init.client = KretaClient(token, init.isar, init.reauthCubit!); if (Platform.isIOS) { final expiryDate = token.expiryDate; if (expiryDate != null && expiryDate.isAfter(DateTime.now())) { - KretaClient.clearReauthFlag(); + init.reauthCubit?.clear(); } unawaited(() async { @@ -287,10 +299,6 @@ Future initializeApp() async { await _initData(init); - init.settingsUpdateNotifier.addListener(() { - logger.finest("Settings updated"); - }); - return init; } diff --git a/firka/lib/app/initialization_screen.dart b/firka/lib/app/initialization_screen.dart index 59284c6d..6dcb9c85 100644 --- a/firka/lib/app/initialization_screen.dart +++ b/firka/lib/app/initialization_screen.dart @@ -2,9 +2,15 @@ import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:firka/app/app_state.dart'; import 'package:firka/app/initialization.dart'; +import 'package:firka/core/bloc/home_refresh_cubit.dart'; +import 'package:firka/core/bloc/profile_picture_cubit.dart'; +import 'package:firka/core/bloc/reauth_cubit.dart'; +import 'package:firka/core/bloc/settings_cubit.dart'; +import 'package:firka/core/bloc/theme_cubit.dart'; import 'package:firka/core/firka_bundle.dart'; import 'package:firka/routing/app_router.dart'; import 'package:firka/services/watch_sync_helper.dart'; @@ -100,44 +106,58 @@ class _InitializationScreenState extends State { appRouter = _router; } - return MaterialApp.router( - title: 'Firka', - key: const ValueKey('firkaApp'), - routerConfig: _router!, - theme: ThemeData( - primarySwatch: Colors.lightGreen, - visualDensity: VisualDensity.adaptivePlatformDensity, - ), - localizationsDelegates: const [ - AppLocalizations.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, + final themeCubit = initData.themeCubit!; + final settingsCubit = initData.settingsCubit!; + final profilePictureCubit = initData.profilePictureCubit!; + final reauthCubit = initData.reauthCubit!; + final homeRefreshCubit = initData.homeRefreshCubit!; + return MultiBlocProvider( + providers: [ + BlocProvider.value(value: themeCubit), + BlocProvider.value(value: settingsCubit), + BlocProvider.value(value: profilePictureCubit), + BlocProvider.value(value: reauthCubit), + BlocProvider.value(value: homeRefreshCubit), ], - supportedLocales: AppLocalizations.supportedLocales, - builder: (context, child) { - return ValueListenableBuilder( - valueListenable: isLightMode, - builder: (context, isLight, _) { - final overlay = SystemUiOverlayStyle( - statusBarColor: Colors.transparent, - statusBarIconBrightness: isLight - ? Brightness.dark - : Brightness.light, - statusBarBrightness: isLight - ? Brightness.light - : Brightness.dark, - systemStatusBarContrastEnforced: false, - ); + child: MaterialApp.router( + title: 'Firka', + key: const ValueKey('firkaApp'), + routerConfig: _router!, + theme: ThemeData( + primarySwatch: Colors.lightGreen, + visualDensity: VisualDensity.adaptivePlatformDensity, + ), + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + ], + supportedLocales: AppLocalizations.supportedLocales, + builder: (context, child) { + return BlocBuilder( + builder: (context, themeState) { + final isLight = themeState.isLightMode; + final overlay = SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + statusBarIconBrightness: isLight + ? Brightness.dark + : Brightness.light, + statusBarBrightness: isLight + ? Brightness.light + : Brightness.dark, + systemStatusBarContrastEnforced: false, + ); - SystemChrome.setSystemUIOverlayStyle(overlay); + SystemChrome.setSystemUIOverlayStyle(overlay); - return AnnotatedRegion( - value: overlay, - child: child ?? const SizedBox.shrink(), - ); - }, - ); - }, + return AnnotatedRegion( + value: overlay, + child: child ?? const SizedBox.shrink(), + ); + }, + ); + }, + ), ); } diff --git a/firka/lib/core/bloc/home_refresh_cubit.dart b/firka/lib/core/bloc/home_refresh_cubit.dart new file mode 100644 index 00000000..221f687f --- /dev/null +++ b/firka/lib/core/bloc/home_refresh_cubit.dart @@ -0,0 +1,19 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; + +class HomeRefreshState { + final int refreshTrigger; + + const HomeRefreshState({this.refreshTrigger = 0}); +} + +class HomeRefreshCubit extends Cubit { + HomeRefreshCubit() : super(const HomeRefreshState()); + + void requestRefresh() { + emit(HomeRefreshState(refreshTrigger: state.refreshTrigger + 1)); + } + + void onRefreshComplete() { + emit(HomeRefreshState(refreshTrigger: state.refreshTrigger)); + } +} diff --git a/firka/lib/core/bloc/profile_picture_cubit.dart b/firka/lib/core/bloc/profile_picture_cubit.dart new file mode 100644 index 00000000..545636b3 --- /dev/null +++ b/firka/lib/core/bloc/profile_picture_cubit.dart @@ -0,0 +1,15 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ProfilePictureState { + final int version; + + const ProfilePictureState({this.version = 0}); +} + +class ProfilePictureCubit extends Cubit { + ProfilePictureCubit() : super(const ProfilePictureState()); + + void notifyChanged() { + emit(ProfilePictureState(version: state.version + 1)); + } +} diff --git a/firka/lib/core/bloc/reauth_cubit.dart b/firka/lib/core/bloc/reauth_cubit.dart new file mode 100644 index 00000000..df08c80a --- /dev/null +++ b/firka/lib/core/bloc/reauth_cubit.dart @@ -0,0 +1,19 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ReauthState { + final bool needsReauth; + + const ReauthState({this.needsReauth = false}); +} + +class ReauthCubit extends Cubit { + ReauthCubit() : super(const ReauthState()); + + void setNeedsReauth(bool value) { + emit(ReauthState(needsReauth: value)); + } + + void clear() { + emit(const ReauthState(needsReauth: false)); + } +} diff --git a/firka/lib/core/bloc/settings_cubit.dart b/firka/lib/core/bloc/settings_cubit.dart new file mode 100644 index 00000000..44a608f2 --- /dev/null +++ b/firka/lib/core/bloc/settings_cubit.dart @@ -0,0 +1,15 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; + +class SettingsState { + final int version; + + const SettingsState({this.version = 0}); +} + +class SettingsCubit extends Cubit { + SettingsCubit() : super(const SettingsState()); + + void notifyChanged() { + emit(SettingsState(version: state.version + 1)); + } +} diff --git a/firka/lib/core/bloc/theme_cubit.dart b/firka/lib/core/bloc/theme_cubit.dart new file mode 100644 index 00000000..aebb5474 --- /dev/null +++ b/firka/lib/core/bloc/theme_cubit.dart @@ -0,0 +1,20 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ThemeState { + final bool isLightMode; + + const ThemeState({required this.isLightMode}); +} + +class ThemeCubit extends Cubit { + ThemeCubit({bool initialLightMode = true}) + : super(ThemeState(isLightMode: initialLightMode)); + + void setLightMode(bool isLight) { + emit(ThemeState(isLightMode: isLight)); + } + + void refresh() { + emit(ThemeState(isLightMode: state.isLightMode)); + } +} diff --git a/firka/lib/core/profile_picture.dart b/firka/lib/core/profile_picture.dart index eb1b0976..8ca82408 100644 --- a/firka/lib/core/profile_picture.dart +++ b/firka/lib/core/profile_picture.dart @@ -21,5 +21,5 @@ Future pickProfilePicture( await File(p.join(dataDir.path, "profile.webp")).writeAsBytes(bytes); data.profilePicture = bytes; - data.profilePictureUpdateNotifier.update(); + data.profilePictureCubit?.notifyChanged(); } diff --git a/firka/lib/core/settings.dart b/firka/lib/core/settings.dart index 67755f7f..d8cbab5b 100644 --- a/firka/lib/core/settings.dart +++ b/firka/lib/core/settings.dart @@ -227,7 +227,7 @@ class SettingsStore { initData.settings = SettingsStore(initData.l10n); await initData.settings.load(initData.isar.appSettingsModels); - globalUpdate.update(); + initData.themeCubit?.refresh(); runApp(InitializationScreen()); }, ), @@ -340,7 +340,6 @@ class SettingsStore { always, () async { initTheme(initData); - globalUpdate.update(); }, ), }), @@ -660,7 +659,7 @@ class SettingsStore { await item.save(model); } - initData.settingsUpdateNotifier.update(); + initData.settingsCubit?.notifyChanged(); } Future load(IsarCollection model) async { @@ -754,7 +753,7 @@ class SettingsGroup implements SettingsItem { await item.save(model); } - initData.settingsUpdateNotifier.update(); + initData.settingsCubit?.notifyChanged(); } } @@ -796,7 +795,7 @@ class SettingsSubGroup implements SettingsItem { await item.save(model); } - initData.settingsUpdateNotifier.update(); + initData.settingsCubit?.notifyChanged(); } } @@ -1010,7 +1009,7 @@ class SettingsKretenAccountPicker implements SettingsItem { await model.put(v); - initData.settingsUpdateNotifier.update(); + initData.settingsCubit?.notifyChanged(); } } @@ -1078,7 +1077,7 @@ class SettingsAppIconPicker implements SettingsItem { await model.put(v); - initData.settingsUpdateNotifier.update(); + initData.settingsCubit?.notifyChanged(); } } @@ -1129,7 +1128,7 @@ class SettingsBoolean implements SettingsItem { await model.put(v); - initData.settingsUpdateNotifier.update(); + initData.settingsCubit?.notifyChanged(); } } @@ -1176,7 +1175,7 @@ class SettingsItemsRadio implements SettingsItem { await model.put(v); - initData.settingsUpdateNotifier.update(); + initData.settingsCubit?.notifyChanged(); } } @@ -1242,7 +1241,7 @@ class SettingsDouble implements SettingsItem { await model.put(v); - initData.settingsUpdateNotifier.update(); + initData.settingsCubit?.notifyChanged(); } } @@ -1288,7 +1287,7 @@ class SettingsString implements SettingsItem { await model.put(v); - initData.settingsUpdateNotifier.update(); + initData.settingsCubit?.notifyChanged(); } } diff --git a/firka/lib/core/state/firka_state.dart b/firka/lib/core/state/firka_state.dart index 3b4543e2..5fa6eed1 100644 --- a/firka/lib/core/state/firka_state.dart +++ b/firka/lib/core/state/firka_state.dart @@ -1,32 +1,3 @@ -import 'package:firka/app/app_state.dart'; import 'package:flutter/widgets.dart'; -abstract class FirkaState extends State { - @override - @mustCallSuper - void initState() { - super.initState(); - globalUpdate.addListener(_doUpdate); - } - - void _doUpdate() { - if (mounted) setState(() {}); - } - - @override - @mustCallSuper - void didChangeDependencies() { - super.didChangeDependencies(); - - globalUpdate.removeListener(_doUpdate); - globalUpdate.addListener(_doUpdate); - } - - @override - @mustCallSuper - void dispose() { - super.dispose(); - - globalUpdate.removeListener(_doUpdate); - } -} +abstract class FirkaState extends State {} diff --git a/firka/lib/core/state/update_notifier.dart b/firka/lib/core/state/update_notifier.dart deleted file mode 100644 index f34fdea1..00000000 --- a/firka/lib/core/state/update_notifier.dart +++ /dev/null @@ -1,7 +0,0 @@ -import 'package:flutter/material.dart'; - -class UpdateNotifier with ChangeNotifier { - void update() { - notifyListeners(); - } -} diff --git a/firka/lib/data/widget.dart b/firka/lib/data/widget.dart index c5b1ba27..1cc3ea01 100644 --- a/firka/lib/data/widget.dart +++ b/firka/lib/data/widget.dart @@ -7,8 +7,8 @@ import 'package:firka/api/model/timetable.dart'; import 'package:firka/core/debug_helper.dart'; import 'package:firka/data/ios_widget_helper.dart'; import 'package:firka/core/settings.dart'; -import 'package:firka/app/app_state.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter/scheduler.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; @@ -149,7 +149,11 @@ class WidgetCacheHelper { theme = 'dark'; break; default: - theme = isLightMode.value ? 'light' : 'dark'; + theme = + SchedulerBinding.instance.platformDispatcher.platformBrightness == + Brightness.light + ? 'light' + : 'dark'; } final now = timeNow(); diff --git a/firka/lib/routing/app_router.dart b/firka/lib/routing/app_router.dart index c2f56f08..115404d6 100644 --- a/firka/lib/routing/app_router.dart +++ b/firka/lib/routing/app_router.dart @@ -111,11 +111,7 @@ GoRouter createAppRouter() { key: state.pageKey, child: DefaultAssetBundle( bundle: FirkaBundle(), - child: HomeMainScreen( - initData, - homeUpdateNotifier, - homeUpdateFinishedNotifier, - ), + child: HomeMainScreen(initData), ), ), routes: [ @@ -126,11 +122,7 @@ GoRouter createAppRouter() { activeSubjectUid = uid; return DefaultAssetBundle( bundle: FirkaBundle(), - child: HomeGradesSubjectScreen( - initData, - homeUpdateNotifier, - homeUpdateFinishedNotifier, - ), + child: HomeGradesSubjectScreen(initData), ); }, ), @@ -146,11 +138,7 @@ GoRouter createAppRouter() { key: state.pageKey, child: DefaultAssetBundle( bundle: FirkaBundle(), - child: HomeGradesScreen( - initData, - homeUpdateNotifier, - homeUpdateFinishedNotifier, - ), + child: HomeGradesScreen(initData), ), ), routes: [ @@ -161,11 +149,7 @@ GoRouter createAppRouter() { activeSubjectUid = uid; return DefaultAssetBundle( bundle: FirkaBundle(), - child: HomeGradesSubjectScreen( - initData, - homeUpdateNotifier, - homeUpdateFinishedNotifier, - ), + child: HomeGradesSubjectScreen(initData), ); }, ), @@ -181,11 +165,7 @@ GoRouter createAppRouter() { key: state.pageKey, child: DefaultAssetBundle( bundle: FirkaBundle(), - child: HomeTimetableScreen( - initData, - homeUpdateNotifier, - homeUpdateFinishedNotifier, - ), + child: HomeTimetableScreen(initData), ), ), routes: [ @@ -193,11 +173,7 @@ GoRouter createAppRouter() { path: 'monthly', builder: (context, state) => DefaultAssetBundle( bundle: FirkaBundle(), - child: HomeTimetableMonthlyScreen( - initData, - homeUpdateNotifier, - homeUpdateFinishedNotifier, - ), + child: HomeTimetableMonthlyScreen(initData), ), ), GoRoute( @@ -207,11 +183,7 @@ GoRouter createAppRouter() { activeSubjectUid = uid; return DefaultAssetBundle( bundle: FirkaBundle(), - child: HomeGradesSubjectScreen( - initData, - homeUpdateNotifier, - homeUpdateFinishedNotifier, - ), + child: HomeGradesSubjectScreen(initData), ); }, ), diff --git a/firka/lib/services/live_activity_service.dart b/firka/lib/services/live_activity_service.dart index 74fa9d18..e5943409 100644 --- a/firka/lib/services/live_activity_service.dart +++ b/firka/lib/services/live_activity_service.dart @@ -290,7 +290,7 @@ class LiveActivityService { ); } - globalUpdate.update(); + initData.themeCubit?.refresh(); } catch (e) { _logger.warning('Error syncing global settings: $e'); } diff --git a/firka/lib/services/watch_sync_helper.dart b/firka/lib/services/watch_sync_helper.dart index 96ee93e0..6d2ef46d 100644 --- a/firka/lib/services/watch_sync_helper.dart +++ b/firka/lib/services/watch_sync_helper.dart @@ -442,7 +442,7 @@ class WatchSyncHelper { if (initDone) { initData.tokens = []; } - KretaClient.clearReauthFlag(); + if (initDone) initData.reauthCubit?.clear(); await prefs.setBool(_iosFreshInstallHandledKey, true); return true; @@ -529,7 +529,7 @@ class WatchSyncHelper { ); final expiryDate = token?.expiryDate; if (expiryDate != null && expiryDate.isAfter(DateTime.now())) { - KretaClient.clearReauthFlag(); + if (initDone) initData.reauthCubit?.clear(); debugPrint( '[WatchSync] Cleared reauth flag after iCloud notification (token is valid)', ); @@ -562,7 +562,7 @@ class WatchSyncHelper { return {'error': 'token_incomplete'}; } - if (KretaClient.needsReauth) { + if (initData.client.needsReauth) { debugPrint('[WatchSync] iPhone needs reauth'); return {'error': 'needsReauth'}; } @@ -699,7 +699,7 @@ class WatchSyncHelper { initData.tokens = await initData.isar.tokenModels.where().findAll(); if (isForActiveAccount) { initData.client.model = newToken; - KretaClient.clearReauthFlag(); + if (initDone) initData.reauthCubit?.clear(); } else { debugPrint( '[WatchSync] Stored token for inactive account ($watchStudentIdNorm), active is $expectedStudentIdNorm', @@ -918,7 +918,7 @@ class WatchSyncHelper { (expectedStudentIdNorm == null || newToken.studentIdNorm == expectedStudentIdNorm); if (shouldClearReauth) { - KretaClient.clearReauthFlag(); + if (initDone) initData.reauthCubit?.clear(); } debugPrint( @@ -1008,7 +1008,7 @@ class WatchSyncHelper { currentToken.accessToken != null && currentToken.refreshToken != null && currentToken.expiryDate != null && - !KretaClient.needsReauth) { + !(initData.reauthCubit?.state.needsReauth ?? false)) { debugPrint('[WatchSync] Sending iPhone token to Watch (no response)'); await _sendTokenToWatchInternal( currentToken, @@ -1025,7 +1025,7 @@ class WatchSyncHelper { currentToken.accessToken != null && currentToken.refreshToken != null && currentToken.expiryDate != null && - !KretaClient.needsReauth) { + !(initData.reauthCubit?.state.needsReauth ?? false)) { debugPrint( '[WatchSync] Sending iPhone token to Watch (Watch has no token)', ); @@ -1058,7 +1058,7 @@ class WatchSyncHelper { currentToken.accessToken != null && currentToken.refreshToken != null && currentToken.expiryDate != null && - !KretaClient.needsReauth) { + !(initData.reauthCubit?.state.needsReauth ?? false)) { await _sendTokenToWatchInternal( currentToken, allowExpiredAccessToken: true, @@ -1080,7 +1080,7 @@ class WatchSyncHelper { currentToken.expiryDate, skew: const Duration(), ) && - !KretaClient.needsReauth) { + !(initData.reauthCubit?.state.needsReauth ?? false)) { await _sendTokenToWatchInternal( currentToken, allowExpiredAccessToken: true, @@ -1135,7 +1135,7 @@ class WatchSyncHelper { if (expectedStudentIdNorm == null || newToken.studentIdNorm == expectedStudentIdNorm) { - KretaClient.clearReauthFlag(); + if (initDone) initData.reauthCubit?.clear(); } debugPrint( diff --git a/firka/lib/ui/components/common_bottom_sheets.dart b/firka/lib/ui/components/common_bottom_sheets.dart index 24a6ce65..ac6f756b 100644 --- a/firka/lib/ui/components/common_bottom_sheets.dart +++ b/firka/lib/ui/components/common_bottom_sheets.dart @@ -9,12 +9,14 @@ import 'package:firka/core/settings.dart'; import 'package:firka/ui/components/firka_shadow.dart'; import 'package:firka/ui/shared/firka_icon.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:flutter_svg/svg.dart'; import 'package:majesticons_flutter/majesticons_flutter.dart'; import 'package:intl/intl.dart'; import 'package:firka/app/app_state.dart'; +import 'package:firka/core/bloc/theme_cubit.dart'; import 'package:firka/ui/theme/style.dart'; import 'package:firka/ui/phone/pages/home/home_grades.dart'; import 'package:firka/ui/phone/widgets/lesson.dart'; @@ -859,7 +861,7 @@ Future showHomeworkBottomSheet( shadow: true, child: Card( color: appStyle.colors.card, - shadowColor: isLightMode.value + shadowColor: context.watch().state.isLightMode ? null : Colors.transparent, child: Align( diff --git a/firka/lib/ui/components/firka_card.dart b/firka/lib/ui/components/firka_card.dart index c062bd49..e72370f1 100644 --- a/firka/lib/ui/components/firka_card.dart +++ b/firka/lib/ui/components/firka_card.dart @@ -1,7 +1,8 @@ -import 'package:firka/ui/components/firka_shadow.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:firka/app/app_state.dart'; +import 'package:firka/core/bloc/theme_cubit.dart'; +import 'package:firka/ui/components/firka_shadow.dart'; import 'package:firka/ui/theme/style.dart'; enum Attach { none, bottom, top } @@ -35,6 +36,7 @@ class FirkaCard extends StatelessWidget { var attached = this.attached != null ? this.attached! : Attach.none; final defaultRounding = 16.0; final attachedRounding = 8.0; + final isLight = context.watch().state.isLightMode; if (extra != null) { return SizedBox( @@ -44,7 +46,7 @@ class FirkaCard extends StatelessWidget { shadow: shadow, child: Card( color: color ?? appStyle.colors.card, - shadowColor: isLightMode.value && shadow + shadowColor: isLight && shadow ? null : Colors.transparent, shape: RoundedRectangleBorder( @@ -94,7 +96,7 @@ class FirkaCard extends StatelessWidget { shadow: shadow, child: Card( color: color ?? appStyle.colors.card, - shadowColor: isLightMode.value && shadow + shadowColor: isLight && shadow ? null : Colors.transparent, shape: RoundedRectangleBorder( diff --git a/firka/lib/ui/components/firka_shadow.dart b/firka/lib/ui/components/firka_shadow.dart index 35c93b0a..f3e7e134 100644 --- a/firka/lib/ui/components/firka_shadow.dart +++ b/firka/lib/ui/components/firka_shadow.dart @@ -1,6 +1,7 @@ -import 'package:firka/app/app_state.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:firka/core/bloc/theme_cubit.dart'; import 'package:firka/ui/theme/style.dart'; class FirkaShadow extends StatelessWidget { @@ -31,7 +32,8 @@ class FirkaShadow extends StatelessWidget { return ClipRRect(borderRadius: borderRadius, child: child); } - if (isLightMode.value) { + final isLight = context.watch().state.isLightMode; + if (isLight) { return child; } else { return Container( diff --git a/firka/lib/ui/phone/pages/extras/extras.dart b/firka/lib/ui/phone/pages/extras/extras.dart index 0dba1219..a5c37427 100644 --- a/firka/lib/ui/phone/pages/extras/extras.dart +++ b/firka/lib/ui/phone/pages/extras/extras.dart @@ -1,12 +1,14 @@ -import 'package:firka/data/models/app_settings_model.dart'; -import 'package:firka/core/settings.dart'; -import 'package:firka/ui/components/firka_shadow.dart'; -import 'package:firka/app/app_state.dart'; -import 'package:firka/ui/theme/style.dart'; -import 'package:firka/ui/shared/firka_icon.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:majesticons_flutter/majesticons_flutter.dart'; +import 'package:firka/app/app_state.dart'; +import 'package:firka/core/bloc/theme_cubit.dart'; +import 'package:firka/core/settings.dart'; +import 'package:firka/data/models/app_settings_model.dart'; +import 'package:firka/ui/components/firka_shadow.dart'; +import 'package:firka/ui/shared/firka_icon.dart'; +import 'package:firka/ui/theme/style.dart'; import 'package:go_router/go_router.dart'; void showExtrasBottomSheet(BuildContext context, AppInitialization data) { @@ -28,7 +30,9 @@ void showExtrasBottomSheet(BuildContext context, AppInitialization data) { shadow: true, child: Card( color: appStyle.colors.card, - shadowColor: isLightMode.value ? null : Colors.transparent, + shadowColor: context.watch().state.isLightMode + ? null + : Colors.transparent, child: Align( alignment: Alignment.centerLeft, child: Padding( @@ -130,7 +134,11 @@ void showExtrasBottomSheet(BuildContext context, AppInitialization data) { shadow: true, child: Card( color: appStyle.colors.card, - shadowColor: isLightMode.value + shadowColor: + context + .watch() + .state + .isLightMode ? null : Colors.transparent, child: Align( @@ -179,7 +187,11 @@ void showExtrasBottomSheet(BuildContext context, AppInitialization data) { shadow: true, child: Card( color: appStyle.colors.card, - shadowColor: isLightMode.value + shadowColor: + context + .watch() + .state + .isLightMode ? null : Colors.transparent, child: Align( @@ -238,6 +250,8 @@ void showExtrasBottomSheet(BuildContext context, AppInitialization data) { onTap: () async { if (isDebug()) return; if (debugCounter == 10) { + final navigator = Navigator.of(context); + final router = GoRouter.of(context); data.settings .group("settings") .setBoolean( @@ -257,8 +271,8 @@ void showExtrasBottomSheet(BuildContext context, AppInitialization data) { .group("settings")["developer_enabled"]! .postUpdate(); - context.pop(); - context.go('/home'); + navigator.pop(); + router.go('/home'); } else if (debugCounter < 10) { debugCounter++; } diff --git a/firka/lib/ui/phone/pages/home/home_grades.dart b/firka/lib/ui/phone/pages/home/home_grades.dart index 903ad3b1..0dda3042 100644 --- a/firka/lib/ui/phone/pages/home/home_grades.dart +++ b/firka/lib/ui/phone/pages/home/home_grades.dart @@ -6,6 +6,7 @@ import 'package:firka/ui/components/grade_helpers.dart'; import 'package:firka/ui/phone/widgets/grade_chart.dart'; import 'package:firka/ui/shared/grade_small_card.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:firka/api/consts.dart'; @@ -15,22 +16,15 @@ import 'package:firka/api/model/subject.dart'; import 'package:firka/api/model/timetable.dart'; import 'package:firka/core/debug_helper.dart'; import 'package:firka/core/state/firka_state.dart'; -import 'package:firka/core/state/update_notifier.dart'; import 'package:firka/app/app_state.dart'; +import 'package:firka/core/bloc/home_refresh_cubit.dart'; import 'package:firka/ui/theme/style.dart'; import 'package:firka/ui/shared/delayed_spinner.dart'; class HomeGradesScreen extends StatefulWidget { final AppInitialization data; - final UpdateNotifier updateNotifier; - final UpdateNotifier finishNotifier; - const HomeGradesScreen( - this.data, - this.updateNotifier, - this.finishNotifier, { - super.key, - }); + const HomeGradesScreen(this.data, {super.key}); @override State createState() => _HomeGradesScreen(); @@ -48,15 +42,8 @@ class _HomeGradesScreen extends FirkaState { ApiResponse>? classGroups; ApiResponse>? lessons; - @override - void didUpdateWidget(HomeGradesScreen oldWidget) { - super.didUpdateWidget(oldWidget); - - widget.updateNotifier.removeListener(updateListener); - widget.updateNotifier.addListener(updateListener); - } - - void updateListener() async { + void _onRefreshRequested(BuildContext context) async { + final cubit = context.read(); var now = timeNow(); var start = now.subtract(Duration(days: now.weekday - 1)); var end = start.add(Duration(days: 6)); @@ -72,16 +59,16 @@ class _HomeGradesScreen extends FirkaState { ); await Future.delayed(Duration(milliseconds: 100)); } - if (mounted) setState(() {}); - widget.finishNotifier.update(); + if (mounted) { + setState(() {}); + cubit.onRefreshComplete(); + } } @override void initState() { super.initState(); - widget.updateNotifier.addListener(updateListener); - (() async { var now = timeNow(); var start = now.subtract(Duration(days: now.weekday - 1)); @@ -100,13 +87,18 @@ class _HomeGradesScreen extends FirkaState { } @override - void dispose() { - super.dispose(); - widget.updateNotifier.removeListener(updateListener); + Widget build(BuildContext context) { + return BlocListener( + listenWhen: (previous, current) => + current.refreshTrigger != previous.refreshTrigger, + listener: (context, state) { + _onRefreshRequested(context); + }, + child: _buildContent(context), + ); } - @override - Widget build(BuildContext context) { + Widget _buildContent(BuildContext context) { if (grades == null || week == null) { return SizedBox( height: MediaQuery.of(context).size.height / 1.35, diff --git a/firka/lib/ui/phone/pages/home/home_grades_subject.dart b/firka/lib/ui/phone/pages/home/home_grades_subject.dart index 94ea8fc3..6478f2f6 100644 --- a/firka/lib/ui/phone/pages/home/home_grades_subject.dart +++ b/firka/lib/ui/phone/pages/home/home_grades_subject.dart @@ -7,26 +7,20 @@ import 'package:firka/ui/phone/pages/home/home_grades.dart'; import 'package:firka/ui/shared/class_icon.dart'; import 'package:firka/ui/shared/firka_icon.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:flutter_svg/svg.dart'; import 'package:majesticons_flutter/majesticons_flutter.dart'; -import 'package:firka/core/state/firka_state.dart'; -import 'package:firka/core/state/update_notifier.dart'; import 'package:firka/app/app_state.dart'; +import 'package:firka/core/bloc/home_refresh_cubit.dart'; +import 'package:firka/core/state/firka_state.dart'; import 'package:firka/ui/theme/style.dart'; class HomeGradesSubjectScreen extends StatefulWidget { final AppInitialization data; - final UpdateNotifier updateNotifier; - final UpdateNotifier finishNotifier; - const HomeGradesSubjectScreen( - this.data, - this.updateNotifier, - this.finishNotifier, { - super.key, - }); + const HomeGradesSubjectScreen(this.data, {super.key}); @override State createState() => _HomeGradesSubjectScreen(); @@ -35,30 +29,22 @@ class HomeGradesSubjectScreen extends StatefulWidget { class _HomeGradesSubjectScreen extends FirkaState { Iterable? grades; - @override - void didUpdateWidget(HomeGradesSubjectScreen oldWidget) { - super.didUpdateWidget(oldWidget); - - widget.updateNotifier.removeListener(updateListener); - widget.updateNotifier.addListener(updateListener); - } - - void updateListener() async { + void _onRefreshRequested(BuildContext context) async { + final cubit = context.read(); grades = (await widget.data.client.getGrades(forceCache: false)).response! .where((grade) => grade.subject.uid == activeSubjectUid) .where((grade) => grade.type.name != "felevi_jegy_ertekeles"); - if (mounted) setState(() {}); - - widget.finishNotifier.update(); + if (mounted) { + setState(() {}); + cubit.onRefreshComplete(); + } } @override void initState() { super.initState(); - widget.updateNotifier.addListener(updateListener); - (() async { grades = (await widget.data.client.getGrades()).response! .where((grade) => grade.subject.uid == activeSubjectUid) @@ -69,13 +55,18 @@ class _HomeGradesSubjectScreen extends FirkaState { } @override - void dispose() { - super.dispose(); - widget.updateNotifier.removeListener(updateListener); + Widget build(BuildContext context) { + return BlocListener( + listenWhen: (previous, current) => + current.refreshTrigger != previous.refreshTrigger, + listener: (context, state) { + _onRefreshRequested(context); + }, + child: _buildContent(context), + ); } - @override - Widget build(BuildContext context) { + Widget _buildContent(BuildContext context) { if (grades != null && grades!.isNotEmpty && activeSubjectUid != "") { var aGrade = grades!.first; var groups = grades!.groupList((grade) => grade.recordDate); diff --git a/firka/lib/ui/phone/pages/home/home_main.dart b/firka/lib/ui/phone/pages/home/home_main.dart index 72f81d7b..988656a2 100644 --- a/firka/lib/ui/phone/pages/home/home_main.dart +++ b/firka/lib/ui/phone/pages/home/home_main.dart @@ -10,6 +10,7 @@ import 'package:firka/ui/phone/widgets/info_board_item.dart'; import 'package:firka/ui/phone/widgets/lesson_small.dart'; import 'package:firka/ui/shared/delayed_spinner.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:majesticons_flutter/majesticons_flutter.dart'; @@ -22,8 +23,8 @@ import 'package:firka/core/debug_helper.dart'; import 'package:firka/core/state/firka_state.dart'; import 'package:firka/ui/components/firka_card.dart'; import 'package:firka/ui/components/grade.dart'; -import 'package:firka/core/state/update_notifier.dart'; import 'package:firka/app/app_state.dart'; +import 'package:firka/core/bloc/home_refresh_cubit.dart'; import 'package:firka/ui/theme/style.dart'; import 'package:firka/ui/shared/firka_icon.dart'; import '../../widgets/home_main_welcome.dart'; @@ -31,15 +32,8 @@ import '../../widgets/lesson_big.dart'; class HomeMainScreen extends StatefulWidget { final AppInitialization data; - final UpdateNotifier updateNotifier; - final UpdateNotifier finishNotifier; - const HomeMainScreen( - this.data, - this.updateNotifier, - this.finishNotifier, { - super.key, - }); + const HomeMainScreen(this.data, {super.key}); @override State createState() => _HomeMainScreen(); @@ -58,18 +52,12 @@ class _HomeMainScreen extends FirkaState { Student? student; Timer? timer; - @override - void didUpdateWidget(HomeMainScreen oldWidget) { - super.didUpdateWidget(oldWidget); - - widget.updateNotifier.removeListener(updateListener); - widget.updateNotifier.addListener(updateListener); - } - - void updateListener() async { + void _onRefreshRequested(BuildContext context) async { + final cubit = context.read(); await fetchData(cacheOnly: false); - - widget.finishNotifier.update(); + if (mounted) { + cubit.onRefreshComplete(); + } } Future fetchData({bool cacheOnly = true}) async { @@ -190,8 +178,6 @@ class _HomeMainScreen extends FirkaState { void initState() { super.initState(); - widget.updateNotifier.addListener(updateListener); - now = timeNow(); if (!mounted) return; @@ -210,14 +196,22 @@ class _HomeMainScreen extends FirkaState { @override void dispose() { super.dispose(); - - widget.updateNotifier.removeListener(updateListener); - timer?.cancel(); } @override Widget build(BuildContext context) { + return BlocListener( + listenWhen: (previous, current) => + current.refreshTrigger != previous.refreshTrigger, + listener: (context, state) { + _onRefreshRequested(context); + }, + child: _buildContent(context), + ); + } + + Widget _buildContent(BuildContext context) { Widget welcomeWidget = SizedBox(); Widget nextClass = SizedBox(); Widget? nextTest; diff --git a/firka/lib/ui/phone/pages/home/home_timetable.dart b/firka/lib/ui/phone/pages/home/home_timetable.dart index 6bb550f6..0a8029e8 100644 --- a/firka/lib/ui/phone/pages/home/home_timetable.dart +++ b/firka/lib/ui/phone/pages/home/home_timetable.dart @@ -13,6 +13,7 @@ import 'package:firka/ui/phone/screens/settings/settings_screen.dart'; import 'package:firka/ui/phone/widgets/bubble_test.dart'; import 'package:firka/ui/shared/delayed_spinner.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:flutter/services.dart'; import 'package:majesticons_flutter/majesticons_flutter.dart'; @@ -20,23 +21,17 @@ import 'package:transparent_pointer/transparent_pointer.dart'; import 'package:firka/api/consts.dart'; import 'package:firka/core/state/firka_state.dart'; -import 'package:firka/core/state/update_notifier.dart'; import 'package:firka/app/app_state.dart'; +import 'package:firka/core/bloc/home_refresh_cubit.dart'; +import 'package:firka/core/bloc/settings_cubit.dart'; import 'package:firka/ui/shared/firka_icon.dart'; import '../../widgets/bottom_tt_icon.dart'; import '../../widgets/tt_day.dart'; class HomeTimetableScreen extends StatefulWidget { final AppInitialization data; - final UpdateNotifier updateNotifier; - final UpdateNotifier finishNotifier; - const HomeTimetableScreen( - this.data, - this.updateNotifier, - this.finishNotifier, { - super.key, - }); + const HomeTimetableScreen(this.data, {super.key}); @override State createState() => _HomeTimetableScreen(); @@ -70,9 +65,6 @@ class _HomeTimetableScreen extends FirkaState void initState() { super.initState(); - widget.updateNotifier.addListener(updateListener); - widget.data.settingsUpdateNotifier.addListener(settingsUpdateListener); - now = timeNow(); initForWeek(now!); @@ -252,27 +244,15 @@ class _HomeTimetableScreen extends FirkaState } } - void updateListener() async { + void _onRefreshRequested(BuildContext context) async { + final cubit = context.read(); if (now != null) { await initForWeek(now!, forceCache: false); - if (mounted) setState(() {}); + if (mounted) { + setState(() {}); + cubit.onRefreshComplete(); + } } - widget.finishNotifier.update(); - } - - void settingsUpdateListener() { - if (mounted) setState(() {}); - } - - @override - void didUpdateWidget(HomeTimetableScreen oldWidget) { - super.didUpdateWidget(oldWidget); - - widget.updateNotifier.removeListener(updateListener); - widget.updateNotifier.addListener(updateListener); - - widget.data.settingsUpdateNotifier.removeListener(settingsUpdateListener); - widget.data.settingsUpdateNotifier.addListener(settingsUpdateListener); } bool animating = false; @@ -371,6 +351,22 @@ class _HomeTimetableScreen extends FirkaState @override Widget build(BuildContext context) { + return BlocListener( + listener: (context, state) { + if (mounted) setState(() {}); + }, + child: BlocListener( + listenWhen: (previous, current) => + current.refreshTrigger != previous.refreshTrigger, + listener: (context, state) { + _onRefreshRequested(context); + }, + child: _buildContent(context), + ), + ); + } + + Widget _buildContent(BuildContext context) { if (lessons != null && tests != null && events != null && dates != null) { List ttWidgets = []; List ttDays = []; @@ -773,12 +769,4 @@ class _HomeTimetableScreen extends FirkaState ); } } - - @override - void dispose() { - super.dispose(); - - widget.updateNotifier.removeListener(updateListener); - widget.data.settingsUpdateNotifier.removeListener(settingsUpdateListener); - } } diff --git a/firka/lib/ui/phone/pages/home/home_timetable_mo.dart b/firka/lib/ui/phone/pages/home/home_timetable_mo.dart index 11bf82ce..3a018619 100644 --- a/firka/lib/ui/phone/pages/home/home_timetable_mo.dart +++ b/firka/lib/ui/phone/pages/home/home_timetable_mo.dart @@ -7,29 +7,23 @@ import 'package:firka/core/settings.dart'; import 'package:firka/ui/theme/style.dart'; import 'package:firka/ui/shared/delayed_spinner.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:majesticons_flutter/majesticons_flutter.dart'; import 'package:transparent_pointer/transparent_pointer.dart'; import 'package:firka/api/model/test.dart'; -import 'package:firka/core/state/firka_state.dart'; -import 'package:firka/core/state/update_notifier.dart'; import 'package:firka/app/app_state.dart'; +import 'package:firka/core/bloc/home_refresh_cubit.dart'; +import 'package:firka/core/state/firka_state.dart'; import 'package:firka/ui/shared/firka_icon.dart'; import '../../screens/settings/settings_screen.dart'; class HomeTimetableMonthlyScreen extends StatefulWidget { final AppInitialization data; - final UpdateNotifier updateNotifier; - final UpdateNotifier finishNotifier; - const HomeTimetableMonthlyScreen( - this.data, - this.updateNotifier, - this.finishNotifier, { - super.key, - }); + const HomeTimetableMonthlyScreen(this.data, {super.key}); @override State createState() => @@ -95,41 +89,38 @@ class _HomeTimetableMonthlyScreen } } - @override - void didUpdateWidget(HomeTimetableMonthlyScreen oldWidget) { - super.didUpdateWidget(oldWidget); - - widget.updateNotifier.removeListener(updateListener); - widget.updateNotifier.addListener(updateListener); - } - - void updateListener() async { + void _onRefreshRequested(BuildContext context) async { + final cubit = context.read(); if (now != null) { await initForMonth(now!, forceCache: false); - setState(() {}); + if (mounted) { + setState(() {}); + cubit.onRefreshComplete(); + } } - widget.finishNotifier.update(); } @override void initState() { super.initState(); - widget.updateNotifier.addListener(updateListener); - now = timeNow(); initForMonth(now!); } @override - void dispose() { - super.dispose(); - - widget.updateNotifier.removeListener(updateListener); + Widget build(BuildContext context) { + return BlocListener( + listenWhen: (previous, current) => + current.refreshTrigger != previous.refreshTrigger, + listener: (context, state) { + _onRefreshRequested(context); + }, + child: _buildContent(context), + ); } - @override - Widget build(BuildContext context) { + Widget _buildContent(BuildContext context) { if (lessons != null && omissions != null && tests != null && diff --git a/firka/lib/ui/phone/screens/home/home_screen.dart b/firka/lib/ui/phone/screens/home/home_screen.dart index b7e40d59..30a74f45 100644 --- a/firka/lib/ui/phone/screens/home/home_screen.dart +++ b/firka/lib/ui/phone/screens/home/home_screen.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:io'; -import 'package:firka/api/client/kreta_client.dart'; import 'package:firka/api/client/kreta_stream.dart'; import 'package:firka/api/exceptions/token.dart'; import 'package:firka/core/extensions.dart'; @@ -14,12 +13,16 @@ import 'package:firka/ui/theme/style.dart'; import 'package:firka/ui/phone/pages/extras/reauth_toast.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter/services.dart'; import 'package:home_widget/home_widget.dart'; import 'package:majesticons_flutter/majesticons_flutter.dart'; import 'package:firka/data/widget.dart'; import 'package:firka/core/debug_helper.dart'; +import 'package:firka/core/bloc/profile_picture_cubit.dart'; +import 'package:firka/core/bloc/reauth_cubit.dart'; +import 'package:firka/core/bloc/settings_cubit.dart'; import 'package:firka/core/state/firka_state.dart'; import 'package:firka/core/image_preloader.dart'; import 'package:firka/ui/shared/delayed_spinner.dart'; @@ -118,7 +121,7 @@ class _HomeScreenState extends FirkaState final now = DateTime.now(); final shouldRunRecovery = - KretaClient.needsReauth || + initData.client.needsReauth || activeToken == null || activeToken.expiryDate == null || activeToken.expiryDate!.isBefore(now.add(const Duration(seconds: 60))); @@ -153,7 +156,7 @@ class _HomeScreenState extends FirkaState if (selectedToken != null) { initData.client.model = selectedToken; } - KretaClient.clearReauthFlag(); + initData.reauthCubit?.clear(); logger.info('[Home] Secondary iCloud recovery applied a fresher token'); } catch (e) { logger.warning('[Home] Secondary iCloud recovery failed: $e'); @@ -223,7 +226,7 @@ class _HomeScreenState extends FirkaState } if (!_disposed && - (LiveActivityService.isTokenExpired || KretaClient.needsReauth)) { + (LiveActivityService.isTokenExpired || initData.client.needsReauth)) { activeToast = ActiveToastType.reauth; setState(() { toast = buildReauthToast(context, initData, () { @@ -394,10 +397,6 @@ class _HomeScreenState extends FirkaState WidgetsBinding.instance.addObserver(this); - initData.settingsUpdateNotifier.addListener(settingsUpdateListener); - initData.profilePictureUpdateNotifier.addListener(_onProfilePictureUpdated); - KretaClient.reauthStateNotifier.addListener(_onReauthStateChanged); - _setupNotificationListener(); _setupWidgetDeepLinkListener(); @@ -412,24 +411,6 @@ class _HomeScreenState extends FirkaState } } - void _onReauthStateChanged() { - if (!mounted || _disposed) return; - if (!KretaClient.needsReauth && activeToast == ActiveToastType.reauth) { - setState(() { - activeToast = ActiveToastType.none; - toast = null; - }); - } - } - - void settingsUpdateListener() { - if (mounted) setState(() {}); - } - - void _onProfilePictureUpdated() { - if (mounted) setState(() {}); - } - Future _preloadImages() async { final imagePaths = initData.settings.appIcons.keys .map((icon) => "assets/images/icons/$icon.webp") @@ -508,12 +489,35 @@ class _HomeScreenState extends FirkaState } SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); - return Scaffold( - backgroundColor: appStyle.colors.background, - body: SafeArea( - child: SizedBox( - height: MediaQuery.of(context).size.height, - child: Stack(children: [widget.child, toast ?? SizedBox.shrink()]), + return BlocListener( + listener: (context, state) { + if (mounted) setState(() {}); + }, + child: BlocListener( + listener: (context, state) { + if (mounted) setState(() {}); + }, + child: BlocListener( + listener: (context, state) { + if (!mounted || _disposed) return; + if (!state.needsReauth && activeToast == ActiveToastType.reauth) { + setState(() { + activeToast = ActiveToastType.none; + toast = null; + }); + } + }, + child: Scaffold( + backgroundColor: appStyle.colors.background, + body: SafeArea( + child: SizedBox( + height: MediaQuery.of(context).size.height, + child: Stack( + children: [widget.child, toast ?? SizedBox.shrink()], + ), + ), + ), + ), ), ), ); @@ -557,11 +561,6 @@ class _HomeScreenState extends FirkaState @override void dispose() { WidgetsBinding.instance.removeObserver(this); - initData.settingsUpdateNotifier.removeListener(settingsUpdateListener); - initData.profilePictureUpdateNotifier.removeListener( - _onProfilePictureUpdated, - ); - KretaClient.reauthStateNotifier.removeListener(_onReauthStateChanged); _disposed = true; _fetching = false; diff --git a/firka/lib/ui/phone/screens/login/login_screen.dart b/firka/lib/ui/phone/screens/login/login_screen.dart index 3372d0e4..5bf462cc 100644 --- a/firka/lib/ui/phone/screens/login/login_screen.dart +++ b/firka/lib/ui/phone/screens/login/login_screen.dart @@ -5,9 +5,11 @@ import 'package:firka/core/firka_bundle.dart'; import 'package:firka/app/app_state.dart'; import 'package:firka/ui/phone/widgets/login_webview.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter/services.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:firka/core/bloc/theme_cubit.dart'; import 'package:firka/core/state/firka_state.dart'; import 'package:firka/core/image_preloader.dart'; import 'package:firka/ui/theme/style.dart'; @@ -107,7 +109,9 @@ class _LoginScreenState extends FirkaState { ); } - final carousel = isLightMode.value ? "carousel" : "carousel_dark"; + final carousel = context.watch().state.isLightMode + ? "carousel" + : "carousel_dark"; final paddingWidthHorizontal = MediaQuery.of(context).size.width - diff --git a/firka/lib/ui/phone/screens/settings/settings_screen.dart b/firka/lib/ui/phone/screens/settings/settings_screen.dart index c7c160d8..fef5e417 100644 --- a/firka/lib/ui/phone/screens/settings/settings_screen.dart +++ b/firka/lib/ui/phone/screens/settings/settings_screen.dart @@ -24,7 +24,6 @@ import 'package:firka/data/widget.dart'; import 'package:firka/core/firka_bundle.dart'; import 'package:firka/app/initialization_screen.dart'; import 'package:firka/core/state/firka_state.dart'; -import 'package:firka/api/client/kreta_client.dart'; import 'package:firka/core/settings.dart'; import 'package:firka/services/live_activity_service.dart'; import 'package:firka/services/watch_sync_helper.dart'; @@ -984,7 +983,7 @@ class _SettingsScreenState extends FirkaState { '[Settings] Failed to clear iCloud token: $e', ); } - KretaClient.clearReauthFlag(); + initData.client.clearReauthFlag(); } if (!mounted) return; context.go('/login'); diff --git a/firka/lib/ui/phone/widgets/bottom_nav_icon.dart b/firka/lib/ui/phone/widgets/bottom_nav_icon.dart index e17e3266..791a2b3f 100644 --- a/firka/lib/ui/phone/widgets/bottom_nav_icon.dart +++ b/firka/lib/ui/phone/widgets/bottom_nav_icon.dart @@ -1,5 +1,3 @@ -import 'dart:typed_data'; - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; diff --git a/firka/lib/ui/phone/widgets/login_webview.dart b/firka/lib/ui/phone/widgets/login_webview.dart index 1772dbb0..bf33b185 100644 --- a/firka/lib/ui/phone/widgets/login_webview.dart +++ b/firka/lib/ui/phone/widgets/login_webview.dart @@ -8,7 +8,6 @@ import 'package:flutter/material.dart'; import 'package:isar_community/isar.dart'; import 'package:webview_flutter/webview_flutter.dart'; -import 'package:firka/api/client/kreta_client.dart'; import 'package:firka/services/watch_sync_helper.dart'; import 'package:firka/api/consts.dart'; import 'package:firka/api/token_grant.dart'; @@ -148,7 +147,7 @@ class _LoginWebviewWidgetState extends FirkaState if (!mounted) return NavigationDecision.prevent; - KretaClient.clearReauthFlag(); + widget.data.reauthCubit?.clear(); if (Platform.isIOS) { LiveActivityService.clearTokenExpiration(); } diff --git a/firka/pubspec.yaml b/firka/pubspec.yaml index 76e35f61..c73b539d 100644 --- a/firka/pubspec.yaml +++ b/firka/pubspec.yaml @@ -52,6 +52,7 @@ dependencies: fl_chart: ^1.1.1 flutter_native_splash: ^2.4.7 go_router: ^17.1.0 + flutter_bloc: ^9.0.0 dev_dependencies: flutter_test: