import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../brick/cache_warmer.dart'; import '../providers/connectivity_provider.dart'; import '../providers/sync_queue_provider.dart'; import 'offline_readiness_sheet.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; return ValueListenableBuilder( valueListenable: CacheWarmer.warmingNotifier, builder: (context, isWarming, _) { Widget? banner; if (!isOnline) { final label = pendingCount > 0 ? 'Offline — $pendingCount item${pendingCount == 1 ? '' : 's'} queued' : kIsWeb ? 'No internet connection' : '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, onTap: () => showOfflineReadinessSheet(context), ); } else if (isWarming) { banner = _BannerStrip( key: const ValueKey('warming'), backgroundColor: scheme.secondaryContainer, foregroundColor: scheme.onSecondaryContainer, icon: _SpinningIcon( icon: Icons.cloud_sync_rounded, color: scheme.onSecondaryContainer, size: 18, ), label: 'Updating offline cache…', showProgress: true, onTap: () => showOfflineReadinessSheet(context), ); } 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, onTap: () => showOfflineReadinessSheet(context), ); } 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; final VoidCallback? onTap; const _BannerStrip({ super.key, required this.backgroundColor, required this.foregroundColor, required this.icon, required this.label, required this.showProgress, this.onTap, }); @override Widget build(BuildContext context) { const radius = BorderRadius.vertical(bottom: Radius.circular(12)); return Material( color: backgroundColor, borderRadius: radius, child: InkWell( borderRadius: radius, onTap: onTap, child: SafeArea( bottom: false, child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: EdgeInsets.fromLTRB(16, 8, onTap != null ? 12 : 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 (onTap != null) Icon( Icons.chevron_right_rounded, size: 16, color: foregroundColor.withValues(alpha: 0.6), ), ], ), ), if (showProgress) ClipRRect( borderRadius: radius, 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), ); } }