Files
social-app/apps/lib/main.dart
T
qzl 4b29b300da feat(calendar): implement ReminderOverlay and supporting components
- Add ReminderQueueManager for managing notification queue
- Add IOSNotificationPayloadBridge for iOS cold start handling
- Add ReminderOverlay UI component with snooze (5/15 min) and complete actions
- Update main.dart to integrate ReminderOverlay
- LocalNotificationService: remove permission fallback logic, add native grouping
2026-03-20 18:47:50 +08:00

212 lines
6.4 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'core/constants/app_constants.dart';
import 'core/cache/cache_refresh_coordinator.dart';
import 'core/di/injection.dart';
import 'core/notifications/local_notification_service.dart';
import 'core/notifications/reminder_notification_callbacks.dart';
import 'core/notifications/ios_notification_payload_bridge.dart';
import 'core/router/app_router.dart';
import 'core/startup/auth_session_bootstrapper.dart';
import 'core/theme/app_theme.dart';
import 'features/auth/presentation/bloc/auth_bloc.dart';
import 'features/auth/presentation/bloc/auth_event.dart';
import 'features/auth/presentation/bloc/auth_state.dart';
import 'features/calendar/data/services/calendar_service.dart';
import 'features/calendar/data/services/calendar_repository.dart';
import 'features/calendar/reminders/reminder_action_executor.dart';
import 'features/calendar/reminders/reminder_queue_manager.dart';
import 'features/calendar/reminders/ui/reminder_overlay.dart';
import 'features/calendar/ui/calendar_state_manager.dart';
import 'features/chat/presentation/bloc/chat_bloc.dart';
import 'features/settings/data/services/settings_user_cache.dart';
import 'features/todo/data/todo_repository.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await configureDependencies();
await AppConstants.init();
final rootNavigatorKey = GlobalKey<NavigatorState>();
sl<LocalNotificationService>().bindActionHandler(({
required action,
required payload,
}) {
return sl<ReminderActionExecutor>().handleAction(
action: action,
payload: payload,
);
});
await sl<LocalNotificationService>().initialize();
final authBloc = sl<AuthBloc>();
authBloc.add(AuthStarted());
final cacheRefreshCoordinator = CacheRefreshCoordinator(
minInterval: const Duration(minutes: 5),
onRefresh: () {
final selected = sl<CalendarStateManager>().selectedDate;
unawaited(
sl<CalendarRepository>().getDayEvents(selected, forceRefresh: true),
);
unawaited(
sl<CalendarRepository>().getMonthEvents(
DateTime(selected.year, selected.month, 1),
forceRefresh: true,
),
);
unawaited(sl<TodoRepository>().getPendingTodos(forceRefresh: true));
unawaited(sl<SettingsUserCache>().getProfile(forceRefresh: true));
},
);
WidgetsBinding.instance.addObserver(cacheRefreshCoordinator);
final prefs = await SharedPreferences.getInstance();
final payloadBridge = IOSNotificationPayloadBridge(prefs);
final pendingPayload = await payloadBridge.getPendingPayload();
final queueManager = ReminderQueueManager();
if (pendingPayload != null) {
queueManager.enqueueFromClick(pendingPayload);
await payloadBridge.clearPendingPayload();
}
runApp(
LinksyApp(
authBloc: authBloc,
rootNavigatorKey: rootNavigatorKey,
sessionBootstrapper: AuthSessionBootstrapper(
calendarService: sl<CalendarService>(),
notificationService: sl<LocalNotificationService>(),
reminderActionExecutor: sl<ReminderActionExecutor>(),
),
queueManager: queueManager,
payloadBridge: payloadBridge,
),
);
WidgetsBinding.instance.addPostFrameCallback((_) {
unawaited(
ReminderNotificationCallbacks.bindResponseHandler(
sl<LocalNotificationService>().handleNotificationResponse,
),
);
});
}
class LinksyApp extends StatefulWidget {
final AuthBloc authBloc;
final GlobalKey<NavigatorState> rootNavigatorKey;
final AuthSessionBootstrapper sessionBootstrapper;
final ReminderQueueManager queueManager;
final IOSNotificationPayloadBridge payloadBridge;
const LinksyApp({
super.key,
required this.authBloc,
required this.rootNavigatorKey,
required this.sessionBootstrapper,
required this.queueManager,
required this.payloadBridge,
});
@override
State<LinksyApp> createState() => _LinksyAppState();
}
class _LinksyAppState extends State<LinksyApp> {
OverlayEntry? _reminderOverlay;
@override
void initState() {
super.initState();
_checkAndShowReminderOverlay();
}
Future<void> _checkAndShowReminderOverlay() async {
if (widget.queueManager.currentPayload != null) {
_showReminderOverlay();
}
}
void _showReminderOverlay() {
if (_reminderOverlay != null) return;
_reminderOverlay = OverlayEntry(
builder: (context) => Positioned.fill(
child: Material(
color: Colors.black54,
child: ReminderOverlay(
queueManager: widget.queueManager,
onComplete: _onReminderComplete,
onSnooze: _onSnooze,
onArchive: _onArchive,
),
),
),
);
Overlay.of(context).insert(_reminderOverlay!);
}
void _onReminderComplete() {
_reminderOverlay?.remove();
_reminderOverlay = null;
if (!widget.queueManager.isEmpty) {
_showReminderOverlay();
}
}
Future<void> _onSnooze(int minutes) async {
final payload = widget.queueManager.currentPayload;
if (payload == null) return;
await sl<LocalNotificationService>().cancelEventReminder(payload.eventId);
final event = await sl<CalendarService>().getEventById(payload.eventId);
if (event != null) {
final snoozeTime = DateTime.now().add(Duration(minutes: minutes));
await sl<LocalNotificationService>().scheduleReminderAt(
event,
snoozeTime,
);
}
}
Future<void> _onArchive() async {
final payload = widget.queueManager.currentPayload;
if (payload == null) return;
try {
await sl<CalendarService>().archiveEvent(payload.eventId);
} catch (_) {
// archive failed, continue anyway
}
}
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: [
BlocProvider<AuthBloc>.value(value: widget.authBloc),
BlocProvider<ChatBloc>(create: (_) => ChatBloc(apiClient: sl())),
],
child: BlocListener<AuthBloc, AuthState>(
listenWhen: (previous, current) => previous != current,
listener: (context, state) {
unawaited(widget.sessionBootstrapper.syncForAuthState(state));
},
child: MaterialApp.router(
title: 'Linksy',
debugShowCheckedModeBanner: false,
theme: AppTheme.light,
routerConfig: createAppRouter(widget.authBloc),
),
),
);
}
}