Files
afterwork/lib/presentation/widgets/shimmer_loading.dart
dahoud 92612abbd7 fix(chat): Correction race condition + Implémentation TODOs
## 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
2026-01-10 10:43:17 +00:00

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,
);
}
}