## Corrections Critiques ### Race Condition - Statuts de Messages - Fix : Les icônes de statut (✓, ✓✓, ✓✓ bleu) ne s'affichaient pas - Cause : WebSocket delivery confirmations arrivaient avant messages locaux - Solution : Pattern Optimistic UI dans chat_bloc.dart - Création message temporaire immédiate - Ajout à la liste AVANT requête HTTP - Remplacement par message serveur à la réponse - Fichier : lib/presentation/state_management/chat_bloc.dart ## Implémentation TODOs (13/21) ### Social (social_header_widget.dart) - ✅ Copier lien du post dans presse-papiers - ✅ Partage natif via Share.share() - ✅ Dialogue de signalement avec 5 raisons ### Partage (share_post_dialog.dart) - ✅ Interface sélection d'amis avec checkboxes - ✅ Partage externe via Share API ### Média (media_upload_service.dart) - ✅ Parsing JSON réponse backend - ✅ Méthode deleteMedia() pour suppression - ✅ Génération miniature vidéo ### Posts (create_post_dialog.dart, edit_post_dialog.dart) - ✅ Extraction URL depuis uploads - ✅ Documentation chargement médias ### Chat (conversations_screen.dart) - ✅ Navigation vers notifications - ✅ ConversationSearchDelegate pour recherche ## Nouveaux Fichiers ### Configuration - build-prod.ps1 : Script build production avec dart-define - lib/core/constants/env_config.dart : Gestion environnements ### Documentation - TODOS_IMPLEMENTED.md : Documentation complète TODOs ## Améliorations ### Architecture - Refactoring injection de dépendances - Amélioration routing et navigation - Optimisation providers (UserProvider, FriendsProvider) ### UI/UX - Amélioration thème et couleurs - Optimisation animations - Meilleure gestion erreurs ### Services - Configuration API avec env_config - Amélioration datasources (events, users) - Optimisation modèles de données
361 lines
12 KiB
Dart
361 lines
12 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:provider/provider.dart';
|
|
|
|
import '../../../core/constants/design_system.dart';
|
|
import '../../../core/constants/env_config.dart';
|
|
import '../../../core/theme/theme_provider.dart';
|
|
import '../../../core/utils/page_transitions.dart';
|
|
import '../../../data/datasources/event_remote_data_source.dart';
|
|
import '../../state_management/event_bloc.dart';
|
|
import '../../widgets/animated_widgets.dart';
|
|
import '../../widgets/custom_button.dart';
|
|
import '../../widgets/custom_snackbar.dart';
|
|
import '../../widgets/modern_empty_state.dart';
|
|
import '../../widgets/shimmer_loading.dart';
|
|
import '../dialogs/add_event_dialog.dart';
|
|
import 'event_card.dart';
|
|
|
|
/// Écran principal des événements avec design moderne et compact.
|
|
///
|
|
/// Cet écran affiche une liste d'événements avec gestion d'états améliorée,
|
|
/// animations fluides, et interface utilisateur optimisée.
|
|
///
|
|
/// **Fonctionnalités:**
|
|
/// - Affichage de la liste des événements
|
|
/// - Création d'événements
|
|
/// - Actions sur les événements (réaction, participation, etc.)
|
|
/// - Gestion des états (chargement, erreur, vide)
|
|
/// - Pull-to-refresh
|
|
class EventScreen extends StatefulWidget {
|
|
const EventScreen({
|
|
required this.userId,
|
|
required this.userFirstName,
|
|
required this.userLastName,
|
|
required this.profileImageUrl,
|
|
super.key,
|
|
});
|
|
|
|
final String userId;
|
|
final String userFirstName;
|
|
final String userLastName;
|
|
final String profileImageUrl;
|
|
|
|
@override
|
|
State<EventScreen> createState() => _EventScreenState();
|
|
}
|
|
|
|
class _EventScreenState extends State<EventScreen> {
|
|
late final EventRemoteDataSource _eventRemoteDataSource;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_eventRemoteDataSource = EventRemoteDataSource(http.Client());
|
|
_loadEvents();
|
|
}
|
|
|
|
/// Charge les événements au démarrage.
|
|
void _loadEvents() {
|
|
context.read<EventBloc>().add(LoadEvents(widget.userId));
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final theme = Theme.of(context);
|
|
|
|
return Scaffold(
|
|
appBar: _buildAppBar(theme),
|
|
body: BlocBuilder<EventBloc, EventState>(
|
|
builder: (context, state) {
|
|
if (state is EventLoading) {
|
|
return _buildLoadingState(theme);
|
|
} else if (state is EventLoaded) {
|
|
return _buildLoadedState(context, theme, state);
|
|
} else if (state is EventError) {
|
|
return _buildErrorState(context, theme, state.message);
|
|
}
|
|
return _buildEmptyState(context, theme);
|
|
},
|
|
),
|
|
floatingActionButton: _buildFloatingActionButton(context, theme),
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// APP BAR
|
|
// ============================================================================
|
|
|
|
/// Construit la barre d'application.
|
|
PreferredSizeWidget _buildAppBar(ThemeData theme) {
|
|
return AppBar(
|
|
elevation: 0,
|
|
scrolledUnderElevation: 2,
|
|
title: Text(
|
|
'Événements',
|
|
style: theme.textTheme.titleLarge?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
letterSpacing: -0.5,
|
|
),
|
|
),
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.search_rounded, size: 22),
|
|
tooltip: 'Rechercher',
|
|
onPressed: _handleSearch,
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.add_circle_outline_rounded, size: 22),
|
|
tooltip: 'Créer un événement',
|
|
onPressed: _navigateToCreateEvent,
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// ÉTATS
|
|
// ============================================================================
|
|
|
|
/// Construit l'état de chargement avec skeleton loaders.
|
|
Widget _buildLoadingState(ThemeData theme) {
|
|
return SkeletonList(
|
|
itemCount: 3,
|
|
skeletonWidget: const EventCardSkeleton(),
|
|
);
|
|
}
|
|
|
|
/// Construit l'état avec événements chargés.
|
|
Widget _buildLoadedState(
|
|
BuildContext context,
|
|
ThemeData theme,
|
|
EventLoaded state,
|
|
) {
|
|
final events = state.events;
|
|
|
|
if (events.isEmpty) {
|
|
return _buildEmptyState(context, theme);
|
|
}
|
|
|
|
return RefreshIndicator(
|
|
onRefresh: _handleRefresh,
|
|
color: theme.colorScheme.primary,
|
|
child: ListView.separated(
|
|
padding: DesignSystem.paddingAll(DesignSystem.spacingLg),
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
itemCount: events.length,
|
|
separatorBuilder: (context, index) =>
|
|
const SizedBox(height: DesignSystem.spacingMd),
|
|
itemBuilder: (context, index) {
|
|
final event = events[index];
|
|
return FadeInWidget(
|
|
delay: Duration(milliseconds: index * 50),
|
|
child: EventCard(
|
|
key: ValueKey(event.id),
|
|
event: event,
|
|
userId: widget.userId,
|
|
userFirstName: widget.userFirstName,
|
|
userLastName: widget.userLastName,
|
|
profileImageUrl: widget.profileImageUrl,
|
|
onReact: () => _handleReact(event.id),
|
|
onComment: () => _handleComment(event.id),
|
|
onShare: () => _handleShare(event.id),
|
|
onParticipate: () => _handleParticipate(event.id),
|
|
onCloseEvent: () => _handleCloseEvent(event.id),
|
|
onReopenEvent: () => _handleReopenEvent(event.id),
|
|
onRemoveEvent: (String eventId) => _handleRemoveEvent(eventId),
|
|
status: event.status,
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Construit l'état vide.
|
|
Widget _buildEmptyState(BuildContext context, ThemeData theme) {
|
|
return RefreshIndicator(
|
|
onRefresh: _handleRefresh,
|
|
color: theme.colorScheme.primary,
|
|
child: SingleChildScrollView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
child: SizedBox(
|
|
height: MediaQuery.of(context).size.height - 200,
|
|
child: ModernEmptyState(
|
|
illustration: EmptyStateIllustration.events,
|
|
title: 'Aucun événement disponible',
|
|
description: 'Créez votre premier événement et commencez à organiser des moments inoubliables avec vos amis',
|
|
actionLabel: 'Créer un événement',
|
|
onAction: _navigateToCreateEvent,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Construit l'état d'erreur.
|
|
Widget _buildErrorState(
|
|
BuildContext context,
|
|
ThemeData theme,
|
|
String message,
|
|
) {
|
|
return RefreshIndicator(
|
|
onRefresh: _handleRefresh,
|
|
color: theme.colorScheme.primary,
|
|
child: SingleChildScrollView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
child: SizedBox(
|
|
height: MediaQuery.of(context).size.height - 200,
|
|
child: Center(
|
|
child: Padding(
|
|
padding: DesignSystem.paddingAll(DesignSystem.spacingXl),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Icon(
|
|
Icons.error_outline_rounded,
|
|
size: 56,
|
|
color: theme.colorScheme.error.withOpacity(0.7),
|
|
),
|
|
const SizedBox(height: DesignSystem.spacingLg),
|
|
Text(
|
|
'Erreur',
|
|
style: theme.textTheme.titleMedium?.copyWith(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 17,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: DesignSystem.spacingSm),
|
|
Text(
|
|
message,
|
|
style: theme.textTheme.bodyMedium?.copyWith(
|
|
color: theme.colorScheme.onSurface.withOpacity(0.6),
|
|
fontSize: 14,
|
|
),
|
|
textAlign: TextAlign.center,
|
|
),
|
|
const SizedBox(height: DesignSystem.spacingXl),
|
|
CustomButton(
|
|
text: 'Réessayer',
|
|
icon: Icons.refresh_rounded,
|
|
onPressed: _loadEvents,
|
|
variant: ButtonVariant.outlined,
|
|
size: ButtonSize.medium,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// FLOATING ACTION BUTTON
|
|
// ============================================================================
|
|
|
|
/// Construit le bouton flottant (compact).
|
|
Widget _buildFloatingActionButton(BuildContext context, ThemeData theme) {
|
|
return FloatingActionButton(
|
|
onPressed: _navigateToCreateEvent,
|
|
tooltip: 'Créer un événement',
|
|
elevation: 2,
|
|
child: const Icon(Icons.add_rounded, size: 26),
|
|
);
|
|
}
|
|
|
|
// ============================================================================
|
|
// ACTIONS
|
|
// ============================================================================
|
|
|
|
/// Gère la recherche.
|
|
void _handleSearch() {
|
|
context.showInfo('Recherche à venir');
|
|
}
|
|
|
|
/// Navigue vers la création d'événement avec animation.
|
|
void _navigateToCreateEvent() {
|
|
context.pushSlideUp(
|
|
AddEventPage(
|
|
userId: widget.userId,
|
|
userFirstName: widget.userFirstName,
|
|
userLastName: widget.userLastName,
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Gère le rafraîchissement.
|
|
Future<void> _handleRefresh() async {
|
|
context.read<EventBloc>().add(LoadEvents(widget.userId));
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
}
|
|
|
|
/// Gère la réaction à un événement.
|
|
Future<void> _handleReact(String eventId) async {
|
|
try {
|
|
await _eventRemoteDataSource.reactToEvent(eventId, widget.userId);
|
|
if (mounted) {
|
|
context.showSuccess('Réaction enregistrée');
|
|
// Recharger les événements pour mettre à jour les compteurs
|
|
context.read<EventBloc>().add(LoadEvents(widget.userId));
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
context.showError('Erreur lors de la réaction: ${e.toString()}');
|
|
}
|
|
if (EnvConfig.enableDetailedLogs) {
|
|
debugPrint('[EventScreen] Erreur réaction: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Gère le commentaire sur un événement.
|
|
void _handleComment(String eventId) {
|
|
context.showInfo('Fonctionnalité de commentaires à venir');
|
|
}
|
|
|
|
/// Gère le partage d'un événement.
|
|
void _handleShare(String eventId) {
|
|
context.showInfo('Fonctionnalité de partage à venir');
|
|
}
|
|
|
|
/// Gère la participation à un événement.
|
|
Future<void> _handleParticipate(String eventId) async {
|
|
try {
|
|
await _eventRemoteDataSource.participateInEvent(eventId, widget.userId);
|
|
if (mounted) {
|
|
context.showSuccess('Participation enregistrée');
|
|
// Recharger les événements pour mettre à jour les participants
|
|
context.read<EventBloc>().add(LoadEvents(widget.userId));
|
|
}
|
|
} catch (e) {
|
|
if (mounted) {
|
|
context.showError('Erreur lors de la participation: ${e.toString()}');
|
|
}
|
|
if (EnvConfig.enableDetailedLogs) {
|
|
debugPrint('[EventScreen] Erreur participation: $e');
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Gère la fermeture d'un événement.
|
|
void _handleCloseEvent(String eventId) {
|
|
context.read<EventBloc>().add(CloseEvent(eventId));
|
|
context.showSuccess('L\'événement a été fermé avec succès');
|
|
}
|
|
|
|
/// Gère la réouverture d'un événement.
|
|
void _handleReopenEvent(String eventId) {
|
|
context.read<EventBloc>().add(ReopenEvent(eventId));
|
|
context.showSuccess('L\'événement a été rouvert avec succès');
|
|
}
|
|
|
|
/// Gère la suppression d'un événement.
|
|
void _handleRemoveEvent(String eventId) {
|
|
context.read<EventBloc>().add(RemoveEvent(eventId));
|
|
}
|
|
}
|