## 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
459 lines
14 KiB
Dart
459 lines
14 KiB
Dart
import 'package:flutter/material.dart';
|
|
import '../../core/constants/design_system.dart';
|
|
|
|
/// Widget Shimmer pour effets de chargement élégants
|
|
///
|
|
/// Utilisé pour afficher un état de chargement avec effet de brillance
|
|
/// (shimmer effect) au lieu d'un simple CircularProgressIndicator.
|
|
///
|
|
/// **Usage:**
|
|
/// ```dart
|
|
/// ShimmerLoading(
|
|
/// child: Container(
|
|
/// width: 200,
|
|
/// height: 100,
|
|
/// decoration: BoxDecoration(
|
|
/// color: Colors.white,
|
|
/// borderRadius: DesignSystem.borderRadiusLg,
|
|
/// ),
|
|
/// ),
|
|
/// )
|
|
/// ```
|
|
class ShimmerLoading extends StatefulWidget {
|
|
const ShimmerLoading({
|
|
required this.child,
|
|
this.baseColor,
|
|
this.highlightColor,
|
|
this.duration = const Duration(milliseconds: 1500),
|
|
super.key,
|
|
});
|
|
|
|
final Widget child;
|
|
final Color? baseColor;
|
|
final Color? highlightColor;
|
|
final Duration duration;
|
|
|
|
@override
|
|
State<ShimmerLoading> createState() => _ShimmerLoadingState();
|
|
}
|
|
|
|
class _ShimmerLoadingState extends State<ShimmerLoading>
|
|
with SingleTickerProviderStateMixin {
|
|
late AnimationController _controller;
|
|
late Animation<double> _animation;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_controller = AnimationController(
|
|
vsync: this,
|
|
duration: widget.duration,
|
|
)..repeat();
|
|
|
|
_animation = Tween<double>(begin: -2, end: 2).animate(
|
|
CurvedAnimation(
|
|
parent: _controller,
|
|
curve: Curves.easeInOutSine,
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
final baseColor = widget.baseColor ??
|
|
(isDark ? Colors.grey[850]! : Colors.grey[300]!);
|
|
final highlightColor = widget.highlightColor ??
|
|
(isDark ? Colors.grey[800]! : Colors.grey[100]!);
|
|
|
|
return AnimatedBuilder(
|
|
animation: _animation,
|
|
builder: (context, child) {
|
|
return ShaderMask(
|
|
shaderCallback: (bounds) {
|
|
return LinearGradient(
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
colors: [
|
|
baseColor,
|
|
highlightColor,
|
|
baseColor,
|
|
],
|
|
stops: [
|
|
0.0,
|
|
_animation.value,
|
|
1.0,
|
|
],
|
|
transform: _SlidingGradientTransform(_animation.value),
|
|
).createShader(bounds);
|
|
},
|
|
blendMode: BlendMode.srcATop,
|
|
child: widget.child,
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SlidingGradientTransform extends GradientTransform {
|
|
const _SlidingGradientTransform(this.slidePercent);
|
|
|
|
final double slidePercent;
|
|
|
|
@override
|
|
Matrix4? transform(Rect bounds, {TextDirection? textDirection}) {
|
|
return Matrix4.translationValues(bounds.width * slidePercent, 0, 0);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// SKELETON WIDGETS PRÉ-CONSTRUITS
|
|
// ============================================================================
|
|
|
|
/// Skeleton pour une carte d'événement
|
|
class EventCardSkeleton extends StatelessWidget {
|
|
const EventCardSkeleton({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
final baseColor = isDark ? Colors.grey[850]! : Colors.grey[300]!;
|
|
|
|
return ShimmerLoading(
|
|
child: Card(
|
|
margin: DesignSystem.paddingAll(DesignSystem.spacingMd),
|
|
child: Padding(
|
|
padding: DesignSystem.paddingAll(DesignSystem.spacingLg),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Image skeleton
|
|
Container(
|
|
width: double.infinity,
|
|
height: 200,
|
|
decoration: BoxDecoration(
|
|
color: baseColor,
|
|
borderRadius: DesignSystem.borderRadiusLg,
|
|
),
|
|
),
|
|
DesignSystem.verticalSpace(DesignSystem.spacingLg),
|
|
// Title skeleton
|
|
Container(
|
|
width: double.infinity,
|
|
height: 24,
|
|
decoration: BoxDecoration(
|
|
color: baseColor,
|
|
borderRadius: DesignSystem.borderRadiusSm,
|
|
),
|
|
),
|
|
DesignSystem.verticalSpace(DesignSystem.spacingSm),
|
|
// Description skeleton
|
|
Container(
|
|
width: MediaQuery.of(context).size.width * 0.7,
|
|
height: 16,
|
|
decoration: BoxDecoration(
|
|
color: baseColor,
|
|
borderRadius: DesignSystem.borderRadiusSm,
|
|
),
|
|
),
|
|
DesignSystem.verticalSpace(DesignSystem.spacingLg),
|
|
// Buttons skeleton
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Container(
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
color: baseColor,
|
|
borderRadius: DesignSystem.borderRadiusMd,
|
|
),
|
|
),
|
|
),
|
|
DesignSystem.horizontalSpace(DesignSystem.spacingSm),
|
|
Expanded(
|
|
child: Container(
|
|
height: 40,
|
|
decoration: BoxDecoration(
|
|
color: baseColor,
|
|
borderRadius: DesignSystem.borderRadiusMd,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Skeleton pour une carte d'ami
|
|
class FriendCardSkeleton extends StatelessWidget {
|
|
const FriendCardSkeleton({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
final baseColor = isDark ? Colors.grey[850]! : Colors.grey[300]!;
|
|
|
|
return ShimmerLoading(
|
|
child: Container(
|
|
margin: DesignSystem.paddingAll(DesignSystem.spacingSm),
|
|
padding: DesignSystem.paddingAll(DesignSystem.spacingMd),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).cardColor,
|
|
borderRadius: DesignSystem.borderRadiusLg,
|
|
),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
// Avatar skeleton
|
|
Container(
|
|
width: DesignSystem.avatarSizeLg,
|
|
height: DesignSystem.avatarSizeLg,
|
|
decoration: BoxDecoration(
|
|
color: baseColor,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
DesignSystem.verticalSpace(DesignSystem.spacingSm),
|
|
// Name skeleton
|
|
Container(
|
|
width: 80,
|
|
height: 14,
|
|
decoration: BoxDecoration(
|
|
color: baseColor,
|
|
borderRadius: DesignSystem.borderRadiusSm,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Skeleton pour un post social
|
|
class SocialPostSkeleton extends StatelessWidget {
|
|
const SocialPostSkeleton({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
final baseColor = isDark ? Colors.grey[850]! : Colors.grey[300]!;
|
|
|
|
return ShimmerLoading(
|
|
child: Card(
|
|
margin: DesignSystem.paddingAll(DesignSystem.spacingMd),
|
|
child: Padding(
|
|
padding: DesignSystem.paddingAll(DesignSystem.spacingLg),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Header (avatar + nom)
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: DesignSystem.avatarSizeMd,
|
|
height: DesignSystem.avatarSizeMd,
|
|
decoration: BoxDecoration(
|
|
color: baseColor,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
DesignSystem.horizontalSpace(DesignSystem.spacingMd),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Container(
|
|
width: 120,
|
|
height: 16,
|
|
decoration: BoxDecoration(
|
|
color: baseColor,
|
|
borderRadius: DesignSystem.borderRadiusSm,
|
|
),
|
|
),
|
|
DesignSystem.verticalSpace(DesignSystem.spacingXs),
|
|
Container(
|
|
width: 80,
|
|
height: 12,
|
|
decoration: BoxDecoration(
|
|
color: baseColor,
|
|
borderRadius: DesignSystem.borderRadiusSm,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
DesignSystem.verticalSpace(DesignSystem.spacingLg),
|
|
// Contenu
|
|
Container(
|
|
width: double.infinity,
|
|
height: 16,
|
|
decoration: BoxDecoration(
|
|
color: baseColor,
|
|
borderRadius: DesignSystem.borderRadiusSm,
|
|
),
|
|
),
|
|
DesignSystem.verticalSpace(DesignSystem.spacingSm),
|
|
Container(
|
|
width: MediaQuery.of(context).size.width * 0.8,
|
|
height: 16,
|
|
decoration: BoxDecoration(
|
|
color: baseColor,
|
|
borderRadius: DesignSystem.borderRadiusSm,
|
|
),
|
|
),
|
|
DesignSystem.verticalSpace(DesignSystem.spacingLg),
|
|
// Actions
|
|
Row(
|
|
children: List.generate(
|
|
3,
|
|
(index) => Padding(
|
|
padding: DesignSystem.paddingHorizontal(
|
|
DesignSystem.spacingMd,
|
|
),
|
|
child: Container(
|
|
width: 60,
|
|
height: 32,
|
|
decoration: BoxDecoration(
|
|
color: baseColor,
|
|
borderRadius: DesignSystem.borderRadiusMd,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Skeleton générique pour liste
|
|
class ListItemSkeleton extends StatelessWidget {
|
|
const ListItemSkeleton({
|
|
this.height = 80,
|
|
super.key,
|
|
});
|
|
|
|
final double height;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final isDark = Theme.of(context).brightness == Brightness.dark;
|
|
final baseColor = isDark ? Colors.grey[850]! : Colors.grey[300]!;
|
|
|
|
return ShimmerLoading(
|
|
child: Container(
|
|
height: height,
|
|
margin: DesignSystem.paddingVertical(DesignSystem.spacingSm),
|
|
padding: DesignSystem.paddingAll(DesignSystem.spacingLg),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).cardColor,
|
|
borderRadius: DesignSystem.borderRadiusLg,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 48,
|
|
height: 48,
|
|
decoration: BoxDecoration(
|
|
color: baseColor,
|
|
shape: BoxShape.circle,
|
|
),
|
|
),
|
|
DesignSystem.horizontalSpace(DesignSystem.spacingLg),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Container(
|
|
width: double.infinity,
|
|
height: 16,
|
|
decoration: BoxDecoration(
|
|
color: baseColor,
|
|
borderRadius: DesignSystem.borderRadiusSm,
|
|
),
|
|
),
|
|
DesignSystem.verticalSpace(DesignSystem.spacingSm),
|
|
Container(
|
|
width: 150,
|
|
height: 12,
|
|
decoration: BoxDecoration(
|
|
color: baseColor,
|
|
borderRadius: DesignSystem.borderRadiusSm,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Widget pour afficher une grille de skeletons
|
|
class SkeletonGrid extends StatelessWidget {
|
|
const SkeletonGrid({
|
|
required this.itemCount,
|
|
required this.skeletonWidget,
|
|
this.crossAxisCount = 2,
|
|
super.key,
|
|
});
|
|
|
|
final int itemCount;
|
|
final Widget skeletonWidget;
|
|
final int crossAxisCount;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GridView.builder(
|
|
padding: DesignSystem.paddingAll(DesignSystem.spacingLg),
|
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
|
crossAxisCount: crossAxisCount,
|
|
crossAxisSpacing: DesignSystem.spacingMd,
|
|
mainAxisSpacing: DesignSystem.spacingMd,
|
|
childAspectRatio: 0.8,
|
|
),
|
|
itemCount: itemCount,
|
|
itemBuilder: (context, index) => skeletonWidget,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Widget pour afficher une liste de skeletons
|
|
class SkeletonList extends StatelessWidget {
|
|
const SkeletonList({
|
|
required this.itemCount,
|
|
required this.skeletonWidget,
|
|
super.key,
|
|
});
|
|
|
|
final int itemCount;
|
|
final Widget skeletonWidget;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ListView.builder(
|
|
padding: DesignSystem.paddingAll(DesignSystem.spacingLg),
|
|
itemCount: itemCount,
|
|
itemBuilder: (context, index) => skeletonWidget,
|
|
);
|
|
}
|
|
}
|