forked from eatvio/eatvio_app
fix: diary loading
This commit is contained in:
@@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user