fix: diary loading

This commit is contained in:
mso7nueh
2026-04-24 16:54:04 +03:00
parent 10c7e0a584
commit 9ef9a7289c
13 changed files with 532 additions and 83 deletions

View File

@@ -0,0 +1,202 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/glossary_sync_cubit.dart';
import 'package:eatvio_app/features/diary/presentation/bloc/glossary_sync_state.dart';
/// Компактный баннер синхронизации глоссария.
///
/// Показывает текущий статус:
/// - checking → «Проверяю обновления…»
/// - downloading → «Загрузка продуктов: 45%»
/// - processing → «Обработка: 12 500 / 200 000»
/// - completed → «✓ Синхронизировано 200 000 продуктов»
/// - error → «✕ Ошибка синхронизации»
/// - idle/upToDate → скрыт
class GlossarySyncBannerWidget extends StatelessWidget {
const GlossarySyncBannerWidget({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<GlossarySyncCubit, GlossarySyncState>(
buildWhen: (prev, curr) => prev.runtimeType != curr.runtimeType || prev != curr,
builder: (context, state) {
return state.when(
idle: () => const SizedBox.shrink(),
upToDate: () => const SizedBox.shrink(),
checking: () => _buildBanner(
color: const Color(0xFF3B82F6),
icon: Icons.sync_rounded,
text: 'Проверяю обновления продуктов…',
showSpinner: true,
),
downloading: (progress) => _buildBanner(
color: const Color(0xFF3B82F6),
icon: Icons.cloud_download_rounded,
text: progress != null
? 'Загрузка продуктов: ${(progress * 100).toInt()}%'
: 'Загрузка продуктов…',
showSpinner: true,
progress: progress,
),
processing: (processed, total) => _buildBanner(
color: const Color(0xFF8B5CF6),
icon: Icons.storage_rounded,
text: total > 0
? 'Обработка: ${_formatNumber(processed)} / ${_formatNumber(total)}'
: 'Обработка данных…',
showSpinner: true,
progress: total > 0 ? processed / total : null,
),
completed: (syncedCount) => _buildBanner(
color: const Color(0xFF059669),
icon: Icons.check_circle_rounded,
text: 'Синхронизировано ${_formatNumber(syncedCount)} продуктов',
),
error: (message) => _buildBanner(
color: const Color(0xFFDC2626),
icon: Icons.error_outline_rounded,
text: message,
),
);
},
);
}
Widget _buildBanner({
required Color color,
required IconData icon,
required String text,
bool showSpinner = false,
double? progress,
}) {
return _AnimatedBanner(
color: color,
icon: icon,
text: text,
showSpinner: showSpinner,
progress: progress,
);
}
static String _formatNumber(int n) {
if (n < 1000) return '$n';
if (n < 1000000) {
final thousands = n ~/ 1000;
final remainder = (n % 1000) ~/ 100;
return remainder > 0 ? '$thousands ${remainder}00' : '${thousands}k';
}
return '${(n / 1000000).toStringAsFixed(1)}M';
}
}
class _AnimatedBanner extends StatefulWidget {
final Color color;
final IconData icon;
final String text;
final bool showSpinner;
final double? progress;
const _AnimatedBanner({
required this.color,
required this.icon,
required this.text,
this.showSpinner = false,
this.progress,
});
@override
State<_AnimatedBanner> createState() => _AnimatedBannerState();
}
class _AnimatedBannerState extends State<_AnimatedBanner>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _slideAnim;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 300),
);
_slideAnim = Tween<double>(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) {
return AnimatedBuilder(
animation: _slideAnim,
builder: (context, child) => Transform.translate(
offset: Offset(0, _slideAnim.value * 36),
child: child,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: widget.color,
boxShadow: [
BoxShadow(
color: widget.color.withValues(alpha: 0.3),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
if (widget.showSpinner)
SizedBox(
width: 14,
height: 14,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
value: widget.progress,
),
)
else
Icon(widget.icon, color: Colors.white, size: 16),
const SizedBox(width: 8),
Expanded(
child: Text(
widget.text,
style: const TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
letterSpacing: 0.2,
),
),
),
],
),
),
// Тонкий прогресс-бар под баннером
if (widget.progress != null)
LinearProgressIndicator(
value: widget.progress,
minHeight: 2,
backgroundColor: widget.color.withValues(alpha: 0.3),
valueColor: AlwaysStoppedAnimation<Color>(
Colors.white.withValues(alpha: 0.8),
),
),
],
),
);
}
}