import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../providers/connectivity_provider.dart'; import '../providers/sync_queue_provider.dart'; /// Wraps [child] with a polished M3 status banner driven by connectivity + sync state. /// /// Three states: /// 1. Online + 0 pending → no banner (slides away) /// 2. Online + N pending → tertiary-toned "Syncing N item(s)…" with spinner + progress bar /// 3. Offline → error-toned "Offline — N item(s) queued" (count omitted when 0) class OfflineBanner extends ConsumerWidget { final Widget child; const OfflineBanner({super.key, required this.child}); @override Widget build(BuildContext context, WidgetRef ref) { ref.watch(connectivityMonitorProvider); final isOnline = ref.watch(isOnlineProvider); final pendingCount = ref.watch(pendingSyncCountProvider); final scheme = Theme.of(context).colorScheme; Widget? banner; if (!isOnline) { final label = pendingCount > 0 ? 'Offline — $pendingCount item${pendingCount == 1 ? '' : 's'} queued' : 'No internet — changes saved locally'; banner = _BannerStrip( key: const ValueKey('offline'), backgroundColor: scheme.errorContainer, foregroundColor: scheme.onErrorContainer, icon: Icon(Icons.wifi_off_rounded, color: scheme.onErrorContainer, size: 18), label: label, showProgress: false, ); } else if (pendingCount > 0) { final label = 'Syncing $pendingCount item${pendingCount == 1 ? '' : 's'}…'; banner = _BannerStrip( key: const ValueKey('syncing'), backgroundColor: scheme.tertiaryContainer, foregroundColor: scheme.onTertiaryContainer, icon: _SpinningIcon( icon: Icons.sync_rounded, color: scheme.onTertiaryContainer, size: 18, ), label: label, showProgress: true, ); } return Column( children: [ AnimatedSwitcher( duration: const Duration(milliseconds: 250), transitionBuilder: (child, animation) { return SlideTransition( position: Tween( begin: const Offset(0, -1), end: Offset.zero, ).animate(CurvedAnimation( parent: animation, curve: Curves.easeOutCubic, reverseCurve: Curves.easeInCubic, )), child: FadeTransition(opacity: animation, child: child), ); }, child: banner ?? const SizedBox.shrink(key: ValueKey('none')), ), Expanded(child: child), ], ); } } class _BannerStrip extends StatelessWidget { final Color backgroundColor; final Color foregroundColor; final Widget icon; final String label; final bool showProgress; const _BannerStrip({ super.key, required this.backgroundColor, required this.foregroundColor, required this.icon, required this.label, required this.showProgress, }); @override Widget build(BuildContext context) { return Material( color: backgroundColor, borderRadius: const BorderRadius.vertical(bottom: Radius.circular(12)), child: SafeArea( bottom: false, child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: EdgeInsets.fromLTRB(16, 8, 16, showProgress ? 6 : 8), child: Row( children: [ icon, const SizedBox(width: 10), Expanded( child: Text( label, style: Theme.of(context).textTheme.labelMedium?.copyWith( color: foregroundColor, fontWeight: FontWeight.w500, ), ), ), ], ), ), if (showProgress) ClipRRect( borderRadius: const BorderRadius.vertical( bottom: Radius.circular(12), ), child: LinearProgressIndicator( minHeight: 2, backgroundColor: backgroundColor, valueColor: AlwaysStoppedAnimation( foregroundColor.withValues(alpha: 0.6), ), ), ), ], ), ), ); } } class _SpinningIcon extends StatefulWidget { final IconData icon; final Color color; final double size; const _SpinningIcon({ required this.icon, required this.color, required this.size, }); @override State<_SpinningIcon> createState() => _SpinningIconState(); } class _SpinningIconState extends State<_SpinningIcon> with SingleTickerProviderStateMixin { late final AnimationController _ctrl; @override void initState() { super.initState(); _ctrl = AnimationController( vsync: this, duration: const Duration(seconds: 2), )..repeat(); } @override void dispose() { _ctrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return RotationTransition( turns: _ctrl, child: Icon(widget.icon, color: widget.color, size: widget.size), ); } }