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 createState() => _ShimmerLoadingState(); } class _ShimmerLoadingState extends State with SingleTickerProviderStateMixin { late AnimationController _controller; late Animation _animation; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: widget.duration, )..repeat(); _animation = Tween(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, ); } }