import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_clock/model/timer_data.dart'; import 'package:flutter_clock/model/timer_settings.dart'; import 'package:flutter_clock/pages/timer/timer_settings_page.dart'; import 'package:flutter_clock/utils/audio_manager.dart'; import 'package:flutter_clock/utils/screen_manager.dart'; import 'package:flutter_clock/utils/background_timer_service.dart'; /// Description: 倒计时页面 /// Time : 04/06/2025 Sunday /// Author : liuyuqi.gov@msn.cn class TimerPage extends StatefulWidget { @override _TimerPageState createState() => _TimerPageState(); } enum TimerState { prepare, running, pause, finish } class _TimerPageState extends State with WidgetsBindingObserver { // Timer duration values int _hours = 0; int _minutes = 10; int _seconds = 0; // For timer controller TimerState timerState = TimerState.prepare; int _remainingSeconds = 0; // Total timer duration in seconds (for progress calculation) int _totalDurationSeconds = 0; // Background services final BackgroundTimerService _backgroundTimerService = BackgroundTimerService(); // Settings late TimerSettings _settings; bool _settingsLoaded = false; final AudioManager _audioManager = AudioManager(); // Wheel controllers final FixedExtentScrollController _hoursController = FixedExtentScrollController(initialItem: 0); final FixedExtentScrollController _minutesController = FixedExtentScrollController(initialItem: 10); final FixedExtentScrollController _secondsController = FixedExtentScrollController(initialItem: 0); @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _initializeBackgroundTimer(); _loadSettings(); _restoreTimerState(); } @override void dispose() { _audioManager.dispose(); _backgroundTimerService.dispose(); if (ScreenManager.isWakeLockEnabled) { ScreenManager.disableWakeLock(); } WidgetsBinding.instance.removeObserver(this); _hoursController.dispose(); _minutesController.dispose(); _secondsController.dispose(); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { // No additional handling required as background service handles the state } /// Restore timer state from persistent storage Future _restoreTimerState() async { final timerData = await TimerData.load(); if (timerData.isRunning) { setState(() { timerState = TimerState.running; _remainingSeconds = timerData.calculateRemainingSeconds(); _totalDurationSeconds = _backgroundTimerService.totalDurationSeconds; }); } else if (timerData.isPaused && timerData.pausedRemaining != null) { setState(() { timerState = TimerState.pause; _remainingSeconds = timerData.pausedRemaining!; _totalDurationSeconds = _backgroundTimerService.totalDurationSeconds; }); } } /// Initialize the background timer service Future _initializeBackgroundTimer() async { await _backgroundTimerService.initialize( onTimerComplete: _timerCompleted, onTimerTick: (remainingSeconds) { setState(() { _remainingSeconds = remainingSeconds; if (remainingSeconds <= 0 && timerState != TimerState.finish) { timerState = TimerState.finish; } }); }, ); } Future _loadSettings() async { _settings = await TimerSettings.loadSettings(); setState(() { _settingsLoaded = true; }); } /// Start a new timer or resume an existing paused timer void _startTimer(String status) { if (status == "resume") { setState(() { timerState = TimerState.running; }); _backgroundTimerService.resumeTimer( _remainingSeconds, _totalDurationSeconds); } else if (status == "start") { // 获取界面输入的时间 int _hourstmp = _hoursController.selectedItem; int _minutestmp = _minutesController.selectedItem; int _secondstmp = _secondsController.selectedItem; // 计算总秒数 final totalSeconds = _hourstmp * 3600 + _minutestmp * 60 + _secondstmp; if (totalSeconds <= 0) return; setState(() { timerState = TimerState.running; _remainingSeconds = totalSeconds; _totalDurationSeconds = totalSeconds; }); _backgroundTimerService.startTimer(totalSeconds); } else if (status == "loop") { // 计算总秒数 final totalSeconds = _hours * 3600 + _minutes * 60 + _seconds; if (totalSeconds <= 0) return; setState(() { timerState = TimerState.running; _remainingSeconds = totalSeconds; _totalDurationSeconds = totalSeconds; }); _backgroundTimerService.startTimer(totalSeconds); } } /// Pause the running timer void _pauseTimer() { _backgroundTimerService.pauseTimer(); setState(() { timerState = TimerState.pause; }); } /// Reset the timer to initial state void _resetTimer() { _backgroundTimerService.cancelTimer(); _audioManager.stopSound(); _audioManager.stopVibration(); setState(() { timerState = TimerState.prepare; }); } /// Handle timer completion Future _timerCompleted() async { setState(() { timerState = TimerState.finish; }); // Play sound and vibrate if (_settings.vibrate) { _audioManager.triggerVibration(); } _audioManager.playSound(_settings.sound, _settings.volume, _settings.loop); // If loop is enabled, restart the timer if (_settings.loop) { Future.delayed(Duration(seconds: 5), () { _audioManager.stopSound(); _audioManager.stopVibration(); }); _startTimer("loop"); } } String _formatTime(int seconds) { final hours = seconds ~/ 3600; final minutes = (seconds % 3600) ~/ 60; final secs = seconds % 60; return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}'; } @override Widget build(BuildContext context) { if (!_settingsLoaded) { return Center(child: CircularProgressIndicator()); } if (timerState != TimerState.prepare) { return _buildCountdownView(); } else { return _buildTimerSetupView(); } } Widget _buildTimerSetupView() { return Scaffold( backgroundColor: Colors.white, body: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( child: Row( children: [ Expanded( child: _buildTimerWheel( _hoursController, List.generate(24, (index) => index), (value) { setState(() { _hours = value; }); }, 'H', ), ), Expanded( child: _buildTimerWheel( _minutesController, List.generate(60, (index) => index), (value) { setState(() { _minutes = value; }); }, 'M', ), ), Expanded( child: _buildTimerWheel( _secondsController, List.generate(60, (index) => index), (value) { setState(() { _seconds = value; }); }, 'S', ), ), ], ), ), SizedBox(height: 20), Padding( padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _buildCircleButton( Icons.phone_android, Colors.grey[600]!, () { ScreenManager.toggleWakeLock(); setState(() {}); }, isActive: ScreenManager.isWakeLockEnabled, ), _buildCircleButton( Icons.play_arrow, Colors.blue, () => _startTimer("start"), ), _buildCircleButton( Icons.settings, Colors.grey[600]!, () async { final result = await Navigator.push( context, MaterialPageRoute( builder: (context) => TimerSettingsPage(settings: _settings)), ); if (result != null) { setState(() { _settings = result; }); } }, ), ], ), ), ], ), ); } Widget _buildCountdownView() { final totalMinutes = _remainingSeconds ~/ 60; return Scaffold( backgroundColor: Colors.white, body: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 300, height: 300, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: Colors.blue.withOpacity(0.3), width: 3, ), ), child: Stack( alignment: Alignment.center, children: [ // Timer progress SizedBox( width: 300, height: 300, child: CircularProgressIndicator( value: timerState == TimerState.finish ? 1 : _totalDurationSeconds > 0 ? _remainingSeconds / _totalDurationSeconds : 0, strokeWidth: 5, backgroundColor: Colors.grey.withOpacity(0.1), color: Colors.blue, ), ), // Time display Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( _formatTime(_remainingSeconds), style: TextStyle( fontSize: 40, fontWeight: FontWeight.bold), ), Text( 'Total ${totalMinutes} minutes', style: TextStyle(fontSize: 16, color: Colors.grey), ), ], ), // Indicator dot Positioned( bottom: 0, child: Container( width: 20, height: 20, decoration: BoxDecoration( color: Colors.blue, shape: BoxShape.circle, ), ), ), ], ), ), ], ), ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ // 唤醒屏幕 _buildCircleButton( Icons.phone_android, Colors.grey[600]!, () { ScreenManager.toggleWakeLock(); setState(() {}); }, isActive: ScreenManager.isWakeLockEnabled, ), // 暂停/开始 timerState == TimerState.running ? _buildCircleButton( Icons.pause, Colors.blue, () => _pauseTimer(), ) : _buildCircleButton( Icons.play_arrow, Colors.blue, () => _startTimer("resume"), ), // 重置 _buildCircleButton( Icons.stop, Colors.red, () => _resetTimer(), ), ], ), ), ], ), ); } Widget _buildCircleButton(IconData icon, Color color, VoidCallback onPressed, {bool isActive = false}) { return Container( width: 70, height: 70, decoration: BoxDecoration( shape: BoxShape.circle, color: isActive ? color : Colors.white, boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 8, offset: Offset(0, 2), ), ], ), child: IconButton( icon: Icon(icon, size: 30), color: isActive ? Colors.white : color, onPressed: onPressed, ), ); } Widget _buildTimerWheel( FixedExtentScrollController controller, List items, ValueChanged onChanged, String unit, ) { return Container( height: 400, decoration: BoxDecoration( border: Border( top: BorderSide(color: Colors.grey.withOpacity(0.3), width: 1), bottom: BorderSide(color: Colors.grey.withOpacity(0.3), width: 1), ), ), child: Stack( children: [ // Center highlight Positioned.fill( child: Center( child: Container( height: 50, decoration: BoxDecoration( color: Colors.blue.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), ), ), ), Container( alignment: Alignment.centerRight, padding: EdgeInsets.only(right: 15), child: Text( unit, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), ), ListWheelScrollView( controller: controller, physics: FixedExtentScrollPhysics(), diameterRatio: 1.5, itemExtent: 50, children: items.map((value) { return Center( child: Text( value.toString().padLeft(2, '0'), style: TextStyle( fontSize: 30, color: Colors.black, fontWeight: FontWeight.w500, ), ), ); }).toList(), onSelectedItemChanged: onChanged, ), ], ), ); } }