202 lines
8.2 KiB
Dart
202 lines
8.2 KiB
Dart
import 'dart:async';
|
|
import 'dart:developer';
|
|
|
|
import 'package:firebase_messaging/firebase_messaging.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:persistent_bottom_nav_bar_v2/persistent_bottom_nav_bar_v2.dart';
|
|
|
|
import 'api/mhsl/breaker/get_breakers/get_breakers_response.dart';
|
|
import 'api/mhsl/server/user_index/update/update_userindex.dart';
|
|
import 'main.dart';
|
|
import 'model/data_cleaner.dart';
|
|
import 'notification/notification_controller.dart';
|
|
import 'notification/notification_tasks.dart';
|
|
import 'notification/notify_updater.dart';
|
|
import 'state/app/modules/app_modules.dart';
|
|
import 'state/app/modules/breaker/bloc/breaker_bloc.dart';
|
|
import 'state/app/modules/chat_list/bloc/chat_list_bloc.dart';
|
|
import 'state/app/modules/settings/bloc/settings_cubit.dart';
|
|
import 'state/app/modules/timetable/bloc/timetable_bloc.dart';
|
|
import 'storage/settings.dart' as model;
|
|
import 'utils/debouncer.dart';
|
|
import 'view/pages/overhang.dart';
|
|
import 'widget/breaker/breaker.dart';
|
|
|
|
class App extends StatefulWidget {
|
|
const App({super.key});
|
|
|
|
@override
|
|
State<App> createState() => _AppState();
|
|
}
|
|
|
|
class _AppState extends State<App> with WidgetsBindingObserver {
|
|
late Timer _refetchChats;
|
|
late Timer _updateTimings;
|
|
// Tracked via the bottom-nav controller's listener so it always reflects the
|
|
// user's actual position, even between rapid setting emits where the
|
|
// controller hasn't caught up to a scheduled jump yet.
|
|
int _knownTotalTabs = 1;
|
|
bool _userOnLastTab = false;
|
|
|
|
void _onTabControllerChanged() {
|
|
_userOnLastTab = Main.bottomNavigator.index == _knownTotalTabs - 1;
|
|
}
|
|
|
|
@override
|
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
|
log('AppLifecycle: $state');
|
|
if (state == AppLifecycleState.resumed) {
|
|
Debouncer.throttle('appLifecycleState', const Duration(seconds: 10), () {
|
|
if (!mounted) return;
|
|
log('Refreshing due to LifecycleChange');
|
|
NotificationTasks.updateProviders(context);
|
|
});
|
|
}
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
Main.bottomNavigator = PersistentTabController(initialIndex: 0);
|
|
Main.bottomNavigator.addListener(_onTabControllerChanged);
|
|
WidgetsBinding.instance.addObserver(this);
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (!mounted) return;
|
|
context.read<BreakerBloc>().refresh();
|
|
context.read<ChatListBloc>().refresh();
|
|
// App is freshly mounted on every login (BlocConsumer in main.dart
|
|
// swaps it in for Login), so this also covers the post-logout case
|
|
// where the bloc was reset to an empty state and needs a fresh fetch.
|
|
context.read<TimetableBloc>().refresh();
|
|
});
|
|
|
|
_updateTimings = Timer.periodic(const Duration(seconds: 30), (_) {
|
|
if (mounted) setState(() {});
|
|
});
|
|
|
|
_refetchChats = Timer.periodic(const Duration(seconds: 60), (_) {
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (!mounted) return;
|
|
context.read<ChatListBloc>().refresh();
|
|
});
|
|
});
|
|
|
|
UpdateUserIndex.index();
|
|
|
|
if (context.read<SettingsCubit>().val().notificationSettings.enabled) {
|
|
void update() => NotifyUpdater.registerToServer();
|
|
FirebaseMessaging.instance.onTokenRefresh.listen((_) => update());
|
|
update();
|
|
}
|
|
|
|
FirebaseMessaging.onMessage.listen((message) {
|
|
if (!mounted) return;
|
|
NotificationController.onForegroundMessageHandler(message, context);
|
|
});
|
|
FirebaseMessaging.onBackgroundMessage(NotificationController.onBackgroundMessageHandler);
|
|
|
|
FirebaseMessaging.onMessageOpenedApp.listen((message) {
|
|
if (!mounted) return;
|
|
NotificationController.onAppOpenedByNotification(message, context);
|
|
});
|
|
FirebaseMessaging.instance.getInitialMessage().then((message) {
|
|
if (message == null || !mounted) return;
|
|
NotificationController.onAppOpenedByNotification(message, context);
|
|
});
|
|
|
|
DataCleaner.cleanOldCache();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_refetchChats.cancel();
|
|
_updateTimings.cancel();
|
|
Main.bottomNavigator.removeListener(_onTabControllerChanged);
|
|
WidgetsBinding.instance.removeObserver(this);
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) => BlocBuilder<SettingsCubit, model.Settings>(
|
|
builder: (context, _) {
|
|
final bottomBarModules = AppModule.getBottomBarModules(context);
|
|
final totalTabs = bottomBarModules.length + 1;
|
|
final currentIndex = Main.bottomNavigator.index;
|
|
|
|
// The bottom-bar layout is identified by the ordered list of module
|
|
// names plus the trailing 'more' slot. Whenever this layout changes
|
|
// — slot count, reordering, or hiding a module — we recreate the
|
|
// entire PersistentTabView via the [layoutKey] below. The package
|
|
// caches per-tab navigator state by index in `_navigatorKeys`, and
|
|
// its internal `alignLength` only ever appends or trims at the end.
|
|
// So when the module sitting at e.g. index 3 changes, the navigator
|
|
// at that index still serves the old screen's route stack and the
|
|
// user sees stale content. Re-mounting clears those stacks; the
|
|
// trade-off (losing in-tab pushed routes on a settings change) is
|
|
// acceptable since the user explicitly re-shaped the bar.
|
|
final layoutKey = ValueKey('${bottomBarModules.map((m) => m.module.name).join('|')}|more');
|
|
|
|
if (totalTabs != _knownTotalTabs) {
|
|
var targetIndex = currentIndex;
|
|
if (_userOnLastTab) {
|
|
targetIndex = totalTabs - 1;
|
|
} else if (currentIndex >= totalTabs) {
|
|
targetIndex = totalTabs - 1;
|
|
}
|
|
// Re-mounting PTV with a new key constructs fresh internals from
|
|
// its controller's current index. If the controller still points
|
|
// past the new tab list, Style6BottomNavBar (and others) crash on
|
|
// out-of-range access during initState. Replace the controller
|
|
// atomically with one initialised at the safe target index so the
|
|
// new PTV mounts cleanly.
|
|
if (targetIndex != currentIndex) {
|
|
Main.bottomNavigator.removeListener(_onTabControllerChanged);
|
|
Main.bottomNavigator = PersistentTabController(initialIndex: targetIndex);
|
|
Main.bottomNavigator.addListener(_onTabControllerChanged);
|
|
_userOnLastTab = targetIndex == totalTabs - 1;
|
|
}
|
|
}
|
|
|
|
_knownTotalTabs = totalTabs;
|
|
|
|
return PersistentTabView(
|
|
key: layoutKey,
|
|
controller: Main.bottomNavigator,
|
|
navBarOverlap: const NavBarOverlap.none(),
|
|
backgroundColor: Theme.of(context).colorScheme.primary,
|
|
handleAndroidBackButtonPress: true,
|
|
screenTransitionAnimation: const ScreenTransitionAnimation(
|
|
curve: Curves.easeOutQuad,
|
|
duration: Duration(milliseconds: 200),
|
|
),
|
|
tabs: [
|
|
...bottomBarModules.map((e) => e.toBottomTab(context)),
|
|
PersistentTabConfig(
|
|
screen: const Breaker(breaker: BreakerArea.more, child: Overhang()),
|
|
item: ItemConfig(
|
|
activeForegroundColor: Theme.of(context).primaryColor,
|
|
inactiveForegroundColor: Theme.of(context).colorScheme.secondary,
|
|
icon: const Icon(Icons.apps),
|
|
title: 'Mehr',
|
|
),
|
|
),
|
|
],
|
|
navBarBuilder: (config) => Style6BottomNavBar(
|
|
// Style6BottomNavBar builds its internal animation controller list
|
|
// in initState and never grows it on didUpdateWidget. Keying by the
|
|
// item count forces a fresh State whenever the slot count changes,
|
|
// which avoids a RangeError when more tabs slide in.
|
|
key: ValueKey(config.items.length),
|
|
navBarConfig: config,
|
|
navBarDecoration: NavBarDecoration(
|
|
border: const Border(top: BorderSide(width: 1, color: Colors.grey)),
|
|
color: Theme.of(context).colorScheme.surface,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|