import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:share_plus/share_plus.dart'; import 'package:url_launcher/url_launcher.dart'; import '../widgets/shared/mini_header_bar.dart'; import '../../shared/widgets/core_card.dart'; import '../../shared/widgets/mini_avatar.dart'; import '../../shared/widgets/action_row.dart'; import '../../shared/widgets/dynamic_fab.dart'; import '../../shared/widgets/core_shimmer.dart'; import '../../shared/widgets/info_badge.dart'; import '../../shared/design_system/tokens/app_typography.dart'; import '../../shared/design_system/tokens/app_colors.dart'; import '../../core/di/injection.dart'; import '../../features/feed/presentation/bloc/unified_feed_bloc.dart'; import '../../features/feed/presentation/bloc/unified_feed_event.dart'; import '../../features/feed/presentation/bloc/unified_feed_state.dart'; import '../../features/feed/domain/entities/feed_item.dart'; import '../../features/solidarity/presentation/pages/demandes_aide_page_wrapper.dart'; /// UnionFlow Mobile - Flux d'Actualité Unifié (Mode DRY) /// N'affiche que des CoreCard dynamiques en écoutant UnifiedFeedBloc. class UnifiedFeedPage extends StatelessWidget { const UnifiedFeedPage({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return BlocProvider( create: (_) => getIt()..add(const LoadFeedRequested()), child: const _UnifiedFeedView(), ); } } class _UnifiedFeedView extends StatefulWidget { const _UnifiedFeedView(); @override State<_UnifiedFeedView> createState() => _UnifiedFeedViewState(); } class _UnifiedFeedViewState extends State<_UnifiedFeedView> { final ScrollController _scrollController = ScrollController(); @override void initState() { super.initState(); _scrollController.addListener(_onScroll); } @override void dispose() { _scrollController.dispose(); super.dispose(); } void _onScroll() { if (_isBottom) { context.read().add(FeedLoadMoreRequested()); } } bool get _isBottom { if (!_scrollController.hasClients) return false; final maxScroll = _scrollController.position.maxScrollExtent; final currentScroll = _scrollController.offset; return currentScroll >= (maxScroll * 0.9); } @override Widget build(BuildContext context) { return BlocListener( listenWhen: (previous, current) => current is UnifiedFeedLoaded && (current as UnifiedFeedLoaded).loadMoreErrorMessage != null, listener: (context, state) { final loadedState = state as UnifiedFeedLoaded; final msg = loadedState.loadMoreErrorMessage; if (msg != null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(msg), behavior: SnackBarBehavior.floating), ); context.read().add(ClearLoadMoreError()); } }, child: Scaffold( appBar: MiniHeaderBar( title: 'Accueil', trailing: Builder( builder: (ctx) => IconButton( icon: const Icon(Icons.refresh), onPressed: () => ctx.read().add(const LoadFeedRequested(isRefresh: true)), tooltip: 'Actualiser', ), ), ), body: RefreshIndicator( color: AppColors.primaryGreen, backgroundColor: Theme.of(context).scaffoldBackgroundColor, onRefresh: () async { context.read().add(const LoadFeedRequested(isRefresh: true)); }, child: BlocBuilder( builder: (context, state) { if (state is UnifiedFeedInitial || state is UnifiedFeedLoading) { return const Padding( padding: EdgeInsets.all(8.0), child: CoreShimmer(itemCount: 4), ); } if (state is UnifiedFeedError) { return Center( child: Text( state.message, style: AppTypography.bodyTextSmall.copyWith(color: AppColors.error), ), ); } if (state is UnifiedFeedLoaded) { if (state.items.isEmpty) { return const Center( child: Text('Aucune activité récente.', style: AppTypography.subtitleSmall), ); } return ListView.builder( controller: _scrollController, itemCount: state.hasReachedMax ? state.items.length : state.items.length + 1, itemBuilder: (context, index) { if (index >= state.items.length) { return const Center( child: Padding( padding: EdgeInsets.symmetric(vertical: 16), child: CircularProgressIndicator(strokeWidth: 2), ), ); } final item = state.items[index]; return _buildFeedCard(context, item); }, ); } return const SizedBox.shrink(); }, ), ), floatingActionButton: DynamicFAB( icon: Icons.add, onPressed: () => _showCreateBottomSheet(context), ), ), ); } Widget _buildFeedCard(BuildContext context, FeedItem item) { return CoreCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ MiniAvatar( imageUrl: item.authorAvatarUrl, fallbackText: item.authorName.isNotEmpty ? item.authorName[0] : 'U', ), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( item.authorName, style: AppTypography.actionText, maxLines: 1, overflow: TextOverflow.ellipsis, ), Row( children: [ Text( _formatDate(item.createdAt), style: AppTypography.subtitleSmall, ), if (item.type != FeedItemType.post) ...[ const SizedBox(width: 6), InfoBadge( text: item.type.name.toUpperCase(), backgroundColor: AppColors.primaryGreen.withOpacity(0.1), textColor: AppColors.primaryGreen, ), ] ], ), ], ), ), IconButton( padding: EdgeInsets.zero, constraints: const BoxConstraints(), icon: Icon( Icons.more_vert, size: 16, color: Theme.of(context).brightness == Brightness.dark ? AppColors.textSecondaryDark : AppColors.textSecondaryLight, ), onPressed: () => _showPostOptionsMenu(context, item), ) ], ), const SizedBox(height: 8), Text( item.content, style: AppTypography.bodyTextSmall, ), const SizedBox(height: 8), ActionRow( likesCount: item.likesCount, commentsCount: item.commentsCount, isLiked: item.isLikedByMe, onLike: () { context.read().add(FeedItemLiked(item.id)); }, onComment: () => _onComment(context, item), onShare: () => Share.share('${item.content}\n— ${item.authorName}', subject: item.type.name), customActionLabel: item.customActionLabel, customActionIcon: item.customActionLabel != null ? Icons.arrow_forward : null, onCustomAction: item.customActionLabel != null ? () => _onCustomAction(context, item) : null, ), ], ), ); } void _showCreateBottomSheet(BuildContext context) { showModalBottomSheet( context: context, builder: (ctx) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const Icon(Icons.post_add_outlined), title: const Text('Nouveau post'), onTap: () { Navigator.pop(ctx); ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Création de post — à brancher sur l\'API.')), ); }, ), ListTile( leading: const Icon(Icons.volunteer_activism_outlined), title: const Text('Demande d\'aide'), onTap: () { Navigator.pop(ctx); Navigator.of(context).push( MaterialPageRoute(builder: (_) => const DemandesAidePageWrapper()), ); }, ), ], ), ), ); } void _onComment(BuildContext context, FeedItem item) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Commentaires pour "${item.content.length > 30 ? "${item.content.substring(0, 30)}..." : item.content}" — à venir.'), behavior: SnackBarBehavior.floating, ), ); } void _onCustomAction(BuildContext context, FeedItem item) { if (item.actionUrlTarget != null && item.actionUrlTarget!.isNotEmpty) { final uri = Uri.tryParse(item.actionUrlTarget!); if (uri != null) { launchUrl(uri, mode: LaunchMode.externalApplication).catchError((_) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Impossible d\'ouvrir le lien.')), ); return false; }); return; } } ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Action: ${item.customActionLabel ?? "—"}'), behavior: SnackBarBehavior.floating), ); } void _showPostOptionsMenu(BuildContext context, FeedItem item) { showModalBottomSheet( context: context, builder: (ctx) => SafeArea( child: Column( mainAxisSize: MainAxisSize.min, children: [ ListTile( leading: const Icon(Icons.edit_outlined), title: const Text('Modifier'), onTap: () => Navigator.pop(ctx), ), ListTile( leading: const Icon(Icons.delete_outline), title: const Text('Supprimer'), onTap: () => Navigator.pop(ctx), ), ListTile( leading: const Icon(Icons.flag_outlined), title: const Text('Signaler'), onTap: () => Navigator.pop(ctx), ), ], ), ), ); } String _formatDate(DateTime date) { // Dans un vrai projet, utiliser intl pour "Il y a 2h" return '${date.day}/${date.month}/${date.year} ${date.hour}:${date.minute.toString().padLeft(2, '0')}'; } }