import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/diary_cubit.dart'; import 'package:eatvio_app/features/diary/presentation/bloc/diary_state.dart'; /// Тонкий баннер вверху экрана дневника, указывающий на офлайн-режим /// и количество операций, ожидающих синхронизацию. /// /// Показывается только когда нет интернета или есть pending операции. /// Автоматически скрывается при восстановлении соединения и пустой очереди. class OfflineBannerWidget extends StatelessWidget { const OfflineBannerWidget({super.key}); @override Widget build(BuildContext context) { return BlocBuilder( buildWhen: (prev, curr) { if (prev is DiaryStateLoaded && curr is DiaryStateLoaded) { return prev.pendingOpsCount != curr.pendingOpsCount || prev.isOfflineMode != curr.isOfflineMode; } return prev.runtimeType != curr.runtimeType; }, builder: (context, state) { if (state is! DiaryStateLoaded) return const SizedBox.shrink(); final isOffline = state.isOfflineMode; final pending = state.pendingOpsCount; // Не показываем если онлайн и очердь пуста if (!isOffline && pending == 0) return const SizedBox.shrink(); return _OfflineBanner(isOffline: isOffline, pendingCount: pending); }, ); } } class _OfflineBanner extends StatefulWidget { final bool isOffline; final int pendingCount; const _OfflineBanner({required this.isOffline, required this.pendingCount}); @override State<_OfflineBanner> createState() => _OfflineBannerState(); } class _OfflineBannerState extends State<_OfflineBanner> with SingleTickerProviderStateMixin { late final AnimationController _controller; late final Animation _slideAnim; @override void initState() { super.initState(); _controller = AnimationController( vsync: this, duration: const Duration(milliseconds: 300), ); _slideAnim = Tween(begin: -1.0, end: 0.0).animate( CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic), ); _controller.forward(); } @override void dispose() { _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final (color, icon, text) = _resolveStyle(); return AnimatedBuilder( animation: _slideAnim, builder: (context, child) => Transform.translate( offset: Offset(0, _slideAnim.value * 40), child: child, ), child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 9), decoration: BoxDecoration( color: color, boxShadow: [ BoxShadow( color: color.withValues(alpha: 0.35), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Row( children: [ Icon(icon, color: Colors.white, size: 16), const SizedBox(width: 8), Expanded( child: Text( text, style: const TextStyle( color: Colors.white, fontSize: 12, fontWeight: FontWeight.w600, letterSpacing: 0.2, ), ), ), if (widget.isOffline) const _PulsingDot(), ], ), ), ); } (Color, IconData, String) _resolveStyle() { if (widget.isOffline && widget.pendingCount > 0) { return ( const Color(0xFFE85D04), Icons.cloud_off_rounded, 'Офлайн · ${widget.pendingCount} ${_inflect(widget.pendingCount)} в очереди', ); } else if (widget.isOffline) { return ( const Color(0xFF6B7280), Icons.cloud_off_rounded, 'Офлайн · данные из кэша', ); } else { // Онлайн, но есть pending операции (только что восстановилось соединение) return ( const Color(0xFF059669), Icons.sync_rounded, 'Синхронизация: ${widget.pendingCount} ${_inflect(widget.pendingCount)}…', ); } } String _inflect(int n) { if (n % 100 >= 11 && n % 100 <= 19) return 'изменений'; switch (n % 10) { case 1: return 'изменение'; case 2: case 3: case 4: return 'изменения'; default: return 'изменений'; } } } /// Мигающая точка — визуальный индикатор офлайн-статуса. class _PulsingDot extends StatefulWidget { const _PulsingDot(); @override State<_PulsingDot> createState() => _PulsingDotState(); } class _PulsingDotState extends State<_PulsingDot> with SingleTickerProviderStateMixin { late final AnimationController _ctrl; late final Animation _anim; @override void initState() { super.initState(); _ctrl = AnimationController( vsync: this, duration: const Duration(milliseconds: 800), )..repeat(reverse: true); _anim = Tween(begin: 0.3, end: 1.0).animate(_ctrl); } @override void dispose() { _ctrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return FadeTransition( opacity: _anim, child: Container( width: 8, height: 8, decoration: const BoxDecoration( color: Colors.white, shape: BoxShape.circle, ), ), ); } }