import 'package:flutter/material.dart'; import 'dart:ui'; /// Widget d'overlay de chargement moderne avec spinner circulaire /// Affiche un spinner animé avec fond flou et message optionnel class LoadingSpinOverlay extends StatefulWidget { final String? message; final Color backgroundColor; final Color spinnerColor; final Color textColor; final double blurAmount; final double spinnerSize; final bool showCard; const LoadingSpinOverlay({ super.key, this.message, this.backgroundColor = Colors.black54, this.spinnerColor = Colors.blue, this.textColor = Colors.white, this.blurAmount = 8.0, this.spinnerSize = 50.0, this.showCard = true, }); @override State createState() => _LoadingSpinOverlayState(); } class _LoadingSpinOverlayState extends State with TickerProviderStateMixin { late AnimationController _fadeController; late AnimationController _rotationController; late Animation _fadeAnimation; @override void initState() { super.initState(); _fadeController = AnimationController( vsync: this, duration: const Duration(milliseconds: 300), ); _rotationController = AnimationController( vsync: this, duration: const Duration(seconds: 1), ); _fadeAnimation = Tween( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _fadeController, curve: Curves.easeInOut, )); _fadeController.forward(); _rotationController.repeat(); } @override void dispose() { _fadeController.dispose(); _rotationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return FadeTransition( opacity: _fadeAnimation, child: BackdropFilter( filter: ImageFilter.blur( sigmaX: widget.blurAmount, sigmaY: widget.blurAmount, ), child: Container( color: widget.backgroundColor, child: Center( child: widget.showCard ? _buildCardContent() : _buildSimpleContent(), ), ), ), ); } Widget _buildCardContent() { return Material( color: Colors.transparent, child: Container( padding: const EdgeInsets.all(32), constraints: const BoxConstraints( maxWidth: 280, ), decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.92), // Semi-transparent borderRadius: BorderRadius.circular(20), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.15), blurRadius: 20, spreadRadius: 2, offset: const Offset(0, 8), ), ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Spinner simple de Flutter SizedBox( width: widget.spinnerSize, height: widget.spinnerSize, child: CircularProgressIndicator( strokeWidth: 3, valueColor: AlwaysStoppedAnimation(widget.spinnerColor), ), ), if (widget.message != null) ...[ const SizedBox(height: 24), Text( widget.message!, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.grey[800], letterSpacing: 0.3, ), textAlign: TextAlign.center, ), ], ], ), ), ); } Widget _buildSimpleContent() { return Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox( width: widget.spinnerSize, height: widget.spinnerSize, child: CircularProgressIndicator( strokeWidth: 3, valueColor: AlwaysStoppedAnimation(widget.spinnerColor), ), ), if (widget.message != null) ...[ const SizedBox(height: 20), Text( widget.message!, style: TextStyle( fontSize: 18, fontWeight: FontWeight.w500, color: widget.textColor, letterSpacing: 0.5, ), textAlign: TextAlign.center, ), ], ], ); } } /// Classe utilitaire pour gérer l'overlay de chargement avec spinner class LoadingSpinOverlayUtils { static OverlayEntry? _currentOverlay; /// Affiche l'overlay de chargement avec spinner static OverlayEntry show({ required BuildContext context, String? message, double blurAmount = 8.0, bool showCard = true, Color? spinnerColor, }) { // Fermer l'overlay existant s'il y en a un hide(); final theme = Theme.of(context); final overlayEntry = OverlayEntry( builder: (context) => LoadingSpinOverlay( message: message, blurAmount: blurAmount, showCard: showCard, spinnerColor: spinnerColor ?? theme.colorScheme.primary, ), ); _currentOverlay = overlayEntry; Overlay.of(context).insert(overlayEntry); return overlayEntry; } /// Met à jour le message de l'overlay existant static void updateMessage({ required OverlayEntry overlayEntry, String? message, }) { overlayEntry.markNeedsBuild(); } /// Cache l'overlay de chargement static void hide() { _currentOverlay?.remove(); _currentOverlay = null; } /// Cache un overlay spécifique static void hideSpecific(OverlayEntry? overlayEntry) { overlayEntry?.remove(); if (_currentOverlay == overlayEntry) { _currentOverlay = null; } } }