/
siblun
/
ReadFlow
Обзор
Документация
Войти
/
siblun
/
ReadFlow
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
lib/presentation/screens/timer_screen.dart
368 строк
12 KB
siblun
Add docs
21 апр 2026, 16:13
21 апр 2026, 16:13
c40d018
Код
Авторство
О чём код?
import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:go_router/go_router.dart'; import 'package:provider/provider.dart'; import '../../data/repositories/session_repository_impl.dart'; import '../../domain/models/book_model.dart'; import '../providers/session_provider.dart'; import '../theme/app_colors.dart'; import '../theme/app_typography.dart'; import '../widgets/end_session_bottom_sheet.dart'; /// Иммерсивный экран таймера сессии чтения. class TimerScreen extends StatefulWidget { final Book book; const TimerScreen({super.key, required this.book}); @override State<TimerScreen> createState() => _TimerScreenState(); } class _TimerScreenState extends State<TimerScreen> with WidgetsBindingObserver{ final _pageController = TextEditingController(); int _currentPage = 0; int _sessionNumber = 1; @override void initState() { super.initState(); _currentPage = widget.book.currentPage; _pageController.text = _currentPage.toString(); WidgetsBinding.instance.addPostFrameCallback((_) async { if (!mounted) return; final provider = context.read<SessionProvider>(); await provider.initialize(widget.book.id); setState(() { _sessionNumber = provider.bookSessions.length + 1; }); await provider.startOrResumeSession(widget.book.id, widget.book.currentPage); }); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); _pageController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnnotatedRegion<SystemUiOverlayStyle>( value: SystemUiOverlayStyle( statusBarColor: Colors.transparent, statusBarIconBrightness: Brightness.light, statusBarBrightness: Brightness.dark, systemNavigationBarColor: AppColors.textMain, systemNavigationBarIconBrightness: Brightness.light, ), child: PopScope( canPop: false, child: Scaffold( backgroundColor: AppColors.textMain, extendBodyBehindAppBar: true, body: Stack( children: [ Positioned.fill( child: Container( decoration: BoxDecoration( gradient: RadialGradient( center: Alignment(0.50, 0.50), radius: 1.24, colors: [ AppColors.background.withValues(alpha: 0.03), AppColors.background.withValues(alpha: 0.0), ], ), ), ), ), SafeArea( child: Padding( padding: EdgeInsets.all(32.w), child: Column( children: [ SizedBox(height: 80.h), Expanded( child: Center( child: _buildTimer(), ), ), _buildButtons(), SizedBox(height: 24.h), _buildSessionCounter(), ], ), ), ), ], ), ), ), ); } Widget _buildTimer() { return Consumer<SessionProvider>( builder: (context, provider, child) { return TweenAnimationBuilder<double>( duration: const Duration(seconds: 1), tween: Tween(begin: 1.0, end: provider.isRunning ? 1.05 : 1.0), curve: Curves.easeInOut, builder: (context, scale, child) { return Transform.scale( scale: scale, child: Stack( alignment: Alignment.center, children: [ Container( width: 280.w, height: 280.w, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: AppColors.background.withValues(alpha: 0.05), width: 1.w, ), ), ), SizedBox( width: 260.w, height: 260.w, child: CircularProgressIndicator( value: (provider.elapsedTime.inSeconds % 3600) / 3600, color: AppColors.primaryAction.withValues(alpha: 0.5), backgroundColor: Colors.transparent, ), ), Column( mainAxisSize: MainAxisSize.min, children: [ Text( _formatTime(provider.elapsedTime), style: AppTypography.timerDisplay.copyWith( fontSize: 72.sp, fontWeight: FontWeight.w200, color: AppColors.background, letterSpacing: -2, ), ), if (!provider.isRunning) Text( 'ПАУЗА', style: Theme.of(context).textTheme.labelMedium?.copyWith( color: AppColors.primaryAction, letterSpacing: 2, ), ), ], ), ], ), ); }, ); }, ); } String _formatTime(Duration duration) { final minutes = duration.inMinutes.toString().padLeft(2, '0'); final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0'); return '$minutes:$seconds'; } Widget _buildButtons() { return Consumer<SessionProvider>( builder: (context, provider, child) { return Column( children: [ Container( width: double.infinity, height: 56.h, decoration: ShapeDecoration( color: AppColors.primaryAction, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(28.r), ), shadows: [ BoxShadow( color: Colors.black.withValues(alpha: 0.1), blurRadius: 6.r, offset: Offset(0, 4.h), spreadRadius: -4.r, ), ], ), child: Material( color: Colors.transparent, child: InkWell( onTap: () => _togglePause(provider), borderRadius: BorderRadius.circular(28.r), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( provider.isRunning ? Icons.pause_rounded : Icons.play_arrow_rounded, color: AppColors.background, size: 24.w, ), SizedBox(width: 12.w), Text( provider.isRunning ? 'Пауза' : 'Продолжить', style: AppTypography.buttonText.copyWith( fontSize: 16.sp, fontWeight: FontWeight.w600, color: AppColors.background, ), ), ], ), ), ), ), SizedBox(height: 16.h), Container( width: double.infinity, height: 56.h, decoration: ShapeDecoration( color: Colors.transparent, shape: RoundedRectangleBorder( side: BorderSide( color: AppColors.background.withValues(alpha: 0.2), width: 1.w, ), borderRadius: BorderRadius.circular(28.r), ), ), child: Material( color: Colors.transparent, child: InkWell( onTap: () => _showEndDialog(context, provider), borderRadius: BorderRadius.circular(28.r), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.close_rounded, color: AppColors.accentSecondary, size: 20.w, ), SizedBox(width: 12.w), Text( 'Завершить', style: AppTypography.bodyMedium.copyWith( fontSize: 14.sp, fontWeight: FontWeight.w600, color: AppColors.accentSecondary, ), ), ], ), ), ), ), ], ); }, ); } Widget _buildSessionCounter() { return Opacity( opacity: 0.3, child: Text( 'SESSION ${_sessionNumber.toString().padLeft(2, '0')}', style: AppTypography.label.copyWith( fontSize: 9.sp, fontWeight: FontWeight.w400, letterSpacing: 2.25, color: AppColors.background, ), ), ); } void _togglePause(SessionProvider provider) async { if (provider.isRunning) { await provider.pause(); } else { await provider.resume(); } } void _showEndDialog(BuildContext context, SessionProvider provider) async { await provider.pause(); if (!context.mounted) return; final sessionRepo = context.read<SessionRepositoryImpl>(); final lastSession = await sessionRepo.getLastCompletedSession(widget.book.id); final pagesReadInLastSession = lastSession?.pagesRead; if (!context.mounted) return; showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (ctx) => Padding( padding: EdgeInsets.only(bottom: MediaQuery.of(ctx).viewInsets.bottom), child: DraggableScrollableSheet( initialChildSize: 0.85, minChildSize: 0.6, maxChildSize: 0.95, expand: false, builder: (context, scrollController) { return SingleChildScrollView( controller: scrollController, physics: const ClampingScrollPhysics(), child: EndSessionBottomSheet( bookId: widget.book.id, sessionDuration: provider.elapsedTime, currentPage: _currentPage, totalPages: widget.book.totalPages, pagesReadInLastSession: pagesReadInLastSession, lastPage: widget.book.currentPage, onSave: (endPage, chapter, note) async { final success = await context.read<SessionProvider>().endCurrentSession( endPage, note: note, ); if (success && context.mounted) { context.pop(); context.pop(); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: const Text('Сессия сохранена'), ), ); } }, onDelete: () async { await provider.cancelCurrentSession(); if (context.mounted) { context.pop(); context.pop(); } }, ), ); }, ), ), ); } }