import 'dart:async'; import 'package:flutter/material.dart'; import '../../../../l10n/app_localizations.dart'; import '../../../../shared/theme/design_tokens.dart'; import '../../../../shared/widgets/app_loading_indicator.dart'; import '../../../../shared/widgets/notification/notification_detail_bottom_sheet.dart'; import '../../data/models/notification_item.dart'; import '../../data/models/notification_payload.dart'; import '../../data/repositories/notification_repository.dart'; import '../bloc/notification_bloc.dart'; import '../widgets/notification_list_item.dart'; class NotificationCenterScreen extends StatefulWidget { const NotificationCenterScreen({ super.key, required this.repository, this.onNavigateToRoute, this.onOpenUrl, this.onUnreadCountChanged, }); final NotificationRepository repository; final void Function(String route, {String? entityId, String? tab})? onNavigateToRoute; final void Function(String url)? onOpenUrl; final Future Function()? onUnreadCountChanged; @override State createState() => _NotificationCenterScreenState(); } class _NotificationCenterScreenState extends State { NotificationBloc? _bloc; late final ScrollController _scrollController; String get _currentLocale { final locale = Localizations.localeOf(context); if (locale.scriptCode == 'Hant') return 'zh_Hant'; return locale.languageCode; } @override void initState() { super.initState(); _scrollController = ScrollController()..addListener(_onScroll); } @override void didChangeDependencies() { super.didChangeDependencies(); if (_bloc == null) { _bloc = NotificationBloc( repository: widget.repository, locale: _currentLocale, ); _bloc!.handleEvent(LoadNotifications()); _bloc!.addListener(_onStateChanged); } } void _onStateChanged() { setState(() {}); } void _onScroll() { if (!_scrollController.hasClients || _bloc == null) return; final position = _scrollController.position; if (position.pixels >= position.maxScrollExtent - 240) { unawaited(_bloc!.handleEvent(LoadMoreNotifications())); } } @override void dispose() { _bloc?.removeListener(_onStateChanged); _bloc?.dispose(); _scrollController.removeListener(_onScroll); _scrollController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final colors = Theme.of(context).colorScheme; final l10n = AppLocalizations.of(context)!; final state = _bloc!.state; return Scaffold( backgroundColor: colors.surfaceContainerLow, appBar: AppBar( title: Text(l10n.notifyCenterTitle), centerTitle: true, backgroundColor: colors.surfaceContainerLow, surfaceTintColor: colors.surfaceContainerLow, actions: [ if (state.items.any((item) => !item.isRead)) TextButton( onPressed: _onMarkAllRead, child: Text( l10n.notifyMarkAllRead, style: TextStyle(color: colors.primary), ), ), ], ), body: RefreshIndicator( onRefresh: () => _bloc!.handleEvent(RefreshNotifications()), child: _buildBody(state, colors, l10n), ), ); } Widget _buildBody( NotificationState state, ColorScheme colors, AppLocalizations l10n, ) { if (state.status == NotificationStatus.loading && state.items.isEmpty) { return const Center(child: AppLoadingIndicator()); } if (state.status == NotificationStatus.error && state.items.isEmpty) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.error_outline, size: 48, color: colors.error), const SizedBox(height: AppSpacing.md), Text( l10n.notifyLoadFailed, style: TextStyle(color: colors.onSurfaceVariant), ), const SizedBox(height: AppSpacing.sm), FilledButton( onPressed: () => _bloc!.handleEvent(LoadNotifications()), child: Text(l10n.notifyRetry), ), ], ), ); } if (state.items.isEmpty) { return ListView( children: [ SizedBox( height: MediaQuery.of(context).size.height * 0.5, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.notifications_none_outlined, size: 64, color: colors.outline, ), const SizedBox(height: AppSpacing.md), Text( l10n.notifyEmpty, style: TextStyle( color: colors.onSurfaceVariant, fontSize: 16, ), ), ], ), ), ), ], ); } return ListView.builder( controller: _scrollController, itemCount: state.items.length + (state.hasMore ? 1 : 0), itemBuilder: (context, index) { if (index == state.items.length && state.hasMore) { return const Padding( padding: EdgeInsets.all(AppSpacing.lg), child: Center(child: AppLoadingIndicator()), ); } final item = state.items[index]; return NotificationListItem( item: item, onTap: () => _handleNotificationTap(context, item), ); }, ); } Future _handleNotificationTap( BuildContext context, NotificationItem item, ) async { final wasUnread = !item.isRead; if (!item.isRead) { await _bloc!.handleEvent(MarkNotificationRead(notificationId: item.id)); final updatedIndex = _bloc!.state.items.indexWhere( (n) => n.id == item.id, ); if (wasUnread && updatedIndex >= 0 && _bloc!.state.items[updatedIndex].isRead) { await widget.onUnreadCountChanged?.call(); } } if (context.mounted) { await showNotificationDetailBottomSheet( context: context, item: item, onMarkRead: () async {}, ); } _executePayload(item.payload); } void _executePayload(NotificationPayload payload) { switch (payload) { case NotificationPayloadNone(): break; case NotificationPayloadRoute(:final route, :final entityId, :final tab): widget.onNavigateToRoute?.call(route, entityId: entityId, tab: tab); case NotificationPayloadUrl(:final url): widget.onOpenUrl?.call(url); } } void _onMarkAllRead() { unawaited(_markAllRead()); } Future _markAllRead() async { final unreadBefore = _bloc!.state.items.any((item) => !item.isRead); await _bloc!.handleEvent(MarkAllNotificationsRead()); final unreadAfter = _bloc!.state.items.any((item) => !item.isRead); if (unreadBefore && !unreadAfter) { await widget.onUnreadCountChanged?.call(); } } }