merged develop
This commit is contained in:
+34
-24
@@ -6,6 +6,7 @@ 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/marianumconnect/marianumconnect_api.dart';
|
||||
import 'api/marianumconnect/queries/get_breakers/get_breakers_response.dart';
|
||||
import 'api/marianumconnect/queries/telemetry_heartbeat/telemetry_heartbeat.dart';
|
||||
import 'main.dart';
|
||||
@@ -45,18 +46,15 @@ class App extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
late Timer _updateTimings;
|
||||
StreamSubscription<RemoteMessage>? _onMessageSub;
|
||||
StreamSubscription<RemoteMessage>? _onMessageOpenedAppSub;
|
||||
StreamSubscription<String>? _fcmTokenRefreshSub;
|
||||
int _knownTotalTabs = 1;
|
||||
int _lastTabIndex = 0;
|
||||
bool _userOnLastTab = false;
|
||||
DateTime? _lastTelemetryAt;
|
||||
|
||||
static const Duration _chatListActiveInterval = Duration(seconds: 15);
|
||||
static const Duration _chatListIdleInterval = Duration(seconds: 60);
|
||||
static const Duration _telemetryInterval = Duration(minutes: 15);
|
||||
|
||||
void _onTabControllerChanged() {
|
||||
final newIndex = Main.bottomNavigator.index;
|
||||
@@ -78,18 +76,15 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
bloc.setAutoRefreshInterval(
|
||||
talkIsActive ? _chatListActiveInterval : _chatListIdleInterval,
|
||||
);
|
||||
if (talkIsActive && refresh) bloc.refresh();
|
||||
// The poll keeps the list current; switching back to the tab shortly
|
||||
// after a fetch doesn't need another one mid tab transition.
|
||||
if (talkIsActive && refresh) {
|
||||
bloc.refreshIfOlderThan(_chatListActiveInterval);
|
||||
}
|
||||
}
|
||||
|
||||
// Wall-clock throttle rather than Debouncer.throttle: a Timer does not tick
|
||||
// reliably while the app is suspended, so the window would still be open on
|
||||
// the resume it is supposed to let through.
|
||||
void _reportTelemetry() {
|
||||
if (!mounted) return;
|
||||
final now = DateTime.now();
|
||||
final last = _lastTelemetryAt;
|
||||
if (last != null && now.difference(last) < _telemetryInterval) return;
|
||||
_lastTelemetryAt = now;
|
||||
TelemetryHeartbeat.report(
|
||||
notificationsEnabled: context
|
||||
.read<SettingsCubit>()
|
||||
@@ -103,6 +98,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
log('AppLifecycle: $state');
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
MarianumConnectApi.resetConnections();
|
||||
_reportTelemetry();
|
||||
Debouncer.throttle('appLifecycleState', const Duration(seconds: 10), () {
|
||||
if (!mounted) return;
|
||||
@@ -113,6 +109,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
_syncChatListPolling(refresh: false);
|
||||
_handlePendingWidgetNavigation();
|
||||
} else if (mounted) {
|
||||
context.read<SettingsCubit>().flushSilentSave();
|
||||
// Stop polling while backgrounded: a silent refresh failing in the
|
||||
// background would otherwise leave an error that flashes on the next
|
||||
// resume before the foreground refetch replaces it.
|
||||
@@ -162,7 +159,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
void _handlePendingShare() {
|
||||
if (!mounted) return;
|
||||
final share = ShareIntentListener.pending.value;
|
||||
if (share == null) return;
|
||||
if (share == null || ShareIntentListener.instance.isShown(share)) return;
|
||||
// A second share would otherwise leave the previous share-flow page
|
||||
// on top with stale (already-cleared) file paths.
|
||||
// Sharing targets Talk chats and Files folders only.
|
||||
@@ -181,6 +178,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
if (navigator.canPop()) {
|
||||
navigator.popUntil((route) => route.isFirst || route is PopupRoute);
|
||||
}
|
||||
ShareIntentListener.instance.markShown(share);
|
||||
AppRoutes.openShareTarget(context, share);
|
||||
}
|
||||
|
||||
@@ -198,19 +196,18 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
context.read<BreakerBloc>().refresh();
|
||||
context.read<ChatListBloc>().refresh();
|
||||
// Initial publish in case hydrated storage already has data. No refresh
|
||||
// needed: PrimaryTimetableScope hands out a freshly loading bloc.
|
||||
_publishWidget(context.read<TimetableBloc>());
|
||||
// needed: PrimaryTimetableScope hands out a freshly loading bloc. The
|
||||
// widget still shows its last snapshot, so this waits until the
|
||||
// cold-start frames are done (a bloc emit in the meantime supersedes it).
|
||||
Debouncer.debounce('widgetPublish', const Duration(seconds: 3), () {
|
||||
if (mounted) _publishWidget(context.read<TimetableBloc>());
|
||||
});
|
||||
unawaited(_handlePendingWidgetNavigation());
|
||||
ShareIntentListener.instance.attach();
|
||||
ShareIntentListener.pending.addListener(_handlePendingShare);
|
||||
_handlePendingShare();
|
||||
_syncChatListPolling();
|
||||
});
|
||||
|
||||
_updateTimings = Timer.periodic(const Duration(seconds: 30), (_) {
|
||||
if (mounted) setState(() {});
|
||||
});
|
||||
|
||||
_reportTelemetry();
|
||||
|
||||
// A refreshed FCM token invalidates the existing push subscription — the
|
||||
@@ -253,18 +250,20 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
NotificationController.onAppOpenedByNotification(message, context);
|
||||
});
|
||||
|
||||
DataCleaner.cleanOldCache();
|
||||
// Housekeeping only; kept out of the cold-start window.
|
||||
Future<void>.delayed(
|
||||
const Duration(seconds: 20),
|
||||
DataCleaner.cleanOldCache,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_updateTimings.cancel();
|
||||
_onMessageSub?.cancel();
|
||||
_onMessageOpenedAppSub?.cancel();
|
||||
_fcmTokenRefreshSub?.cancel();
|
||||
PushTapRouter.pendingTarget.removeListener(_onPushTargetPending);
|
||||
ShareIntentListener.pending.removeListener(_handlePendingShare);
|
||||
ShareIntentListener.instance.detach();
|
||||
Main.bottomNavigator.removeListener(_onTabControllerChanged);
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
@@ -280,7 +279,7 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
// one widget reload for the burst is enough.
|
||||
listener: (_, _) => Debouncer.debounce(
|
||||
'widgetPublish',
|
||||
const Duration(milliseconds: 500),
|
||||
const Duration(seconds: 1),
|
||||
() {
|
||||
if (mounted) _publishWidget(context.read<TimetableBloc>());
|
||||
},
|
||||
@@ -290,7 +289,18 @@ class _AppState extends State<App> with WidgetsBindingObserver {
|
||||
|
||||
Widget _buildShell(
|
||||
BuildContext context,
|
||||
) => BlocBuilder<SettingsCubit, model.Settings>(
|
||||
) => BlocSelector<SettingsCubit, model.Settings, Object>(
|
||||
// Only the module layout shapes the shell; copied values because the
|
||||
// settings object is mutated in place.
|
||||
selector: (settings) {
|
||||
final m = settings.modulesSettings;
|
||||
return (
|
||||
m.moduleOrder.join(','),
|
||||
m.hiddenModules.join(','),
|
||||
m.autoFillBottomBar,
|
||||
m.fixedBottomBarSlots,
|
||||
);
|
||||
},
|
||||
builder: (context, _) {
|
||||
final bottomBarModules = AppModule.getBottomBarModules(context);
|
||||
final totalTabs = bottomBarModules.length + 1;
|
||||
|
||||
Reference in New Issue
Block a user