timer_page.dart 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716
  1. import 'dart:async';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter_clock/model/timer_data.dart';
  4. import 'package:flutter_clock/model/timer_settings.dart';
  5. import 'package:flutter_clock/pages/timer/timer_settings_page.dart';
  6. import 'package:flutter_clock/utils/audio_manager.dart';
  7. import 'package:flutter_clock/utils/screen_manager.dart';
  8. import 'package:flutter_clock/utils/background_timer_service.dart';
  9. /// Description: 倒计时页面
  10. /// Time : 04/06/2025 Sunday
  11. /// Author : liuyuqi.gov@msn.cn
  12. class TimerPage extends StatefulWidget {
  13. @override
  14. _TimerPageState createState() => _TimerPageState();
  15. }
  16. enum TimerState { prepare, running, pause, finish, waiting }
  17. class _TimerPageState extends State<TimerPage> with WidgetsBindingObserver {
  18. // Timer duration values
  19. int _hours = 0;
  20. int _minutes = 10;
  21. int _seconds = 0;
  22. // For timer controller
  23. TimerState timerState = TimerState.prepare;
  24. int _remainingSeconds = 0;
  25. // Waiting for alignment timer
  26. Timer? _alignmentTimer;
  27. DateTime? _alignmentTargetTime;
  28. // Total timer duration in seconds (for progress calculation)
  29. int _totalDurationSeconds = 0;
  30. // Background services
  31. final BackgroundTimerService _backgroundTimerService =
  32. BackgroundTimerService();
  33. // Settings
  34. late TimerSettings _settings;
  35. bool _settingsLoaded = false;
  36. final AudioManager _audioManager = AudioManager();
  37. // Wheel controllers
  38. final FixedExtentScrollController _hoursController =
  39. FixedExtentScrollController(initialItem: 0);
  40. final FixedExtentScrollController _minutesController =
  41. FixedExtentScrollController(initialItem: 10);
  42. final FixedExtentScrollController _secondsController =
  43. FixedExtentScrollController(initialItem: 0);
  44. @override
  45. void initState() {
  46. super.initState();
  47. WidgetsBinding.instance.addObserver(this);
  48. _initializeBackgroundTimer();
  49. _loadSettings();
  50. _restoreTimerState();
  51. }
  52. @override
  53. void dispose() {
  54. _audioManager.dispose();
  55. _backgroundTimerService.dispose();
  56. _alignmentTimer?.cancel();
  57. if (ScreenManager.isWakeLockEnabled) {
  58. ScreenManager.disableWakeLock();
  59. }
  60. WidgetsBinding.instance.removeObserver(this);
  61. _hoursController.dispose();
  62. _minutesController.dispose();
  63. _secondsController.dispose();
  64. super.dispose();
  65. }
  66. @override
  67. void didChangeAppLifecycleState(AppLifecycleState state) {
  68. // No additional handling required as background service handles the state
  69. }
  70. /// Restore timer state from persistent storage
  71. Future<void> _restoreTimerState() async {
  72. final timerData = await TimerData.load();
  73. if (timerData.isRunning) {
  74. setState(() {
  75. timerState = TimerState.running;
  76. _remainingSeconds = timerData.calculateRemainingSeconds();
  77. _totalDurationSeconds = _backgroundTimerService.totalDurationSeconds;
  78. });
  79. } else if (timerData.isPaused && timerData.pausedRemaining != null) {
  80. setState(() {
  81. timerState = TimerState.pause;
  82. _remainingSeconds = timerData.pausedRemaining!;
  83. _totalDurationSeconds = _backgroundTimerService.totalDurationSeconds;
  84. });
  85. }
  86. }
  87. /// Initialize the background timer service
  88. Future<void> _initializeBackgroundTimer() async {
  89. await _backgroundTimerService.initialize(
  90. onTimerComplete: _timerCompleted,
  91. onTimerTick: (remainingSeconds) {
  92. setState(() {
  93. _remainingSeconds = remainingSeconds;
  94. if (remainingSeconds <= 0 && timerState != TimerState.finish) {
  95. timerState = TimerState.finish;
  96. }
  97. });
  98. },
  99. );
  100. }
  101. Future<void> _loadSettings() async {
  102. _settings = await TimerSettings.loadSettings();
  103. setState(() {
  104. _settingsLoaded = true;
  105. });
  106. }
  107. /// Calculate the next 10-minute alignment time
  108. DateTime _calculateNextAlignmentTime() {
  109. final now = DateTime.now();
  110. // 计算下一个整10分钟的时间点
  111. int nextMinutes = ((now.minute ~/ 10) + 1) * 10;
  112. int nextHour = now.hour;
  113. // 处理进位
  114. if (nextMinutes >= 60) {
  115. nextMinutes = 0;
  116. nextHour = (nextHour + 1) % 24;
  117. }
  118. return DateTime(
  119. now.year,
  120. now.month,
  121. now.day,
  122. nextHour,
  123. nextMinutes,
  124. 0,
  125. 0,
  126. 0
  127. );
  128. }
  129. /// Wait until the next 10-minute mark and then start the timer
  130. void _startTimerWithAlignment(int totalSeconds) {
  131. // 取消任何现有的对齐定时器
  132. _alignmentTimer?.cancel();
  133. // 计算下一个整10分钟的时间点
  134. final alignmentTime = _calculateNextAlignmentTime();
  135. _alignmentTargetTime = alignmentTime;
  136. // 设置状态为等待
  137. setState(() {
  138. timerState = TimerState.waiting;
  139. });
  140. // 计算等待时间(毫秒)
  141. final waitDuration = alignmentTime.difference(DateTime.now());
  142. // 创建定时器等待到指定时间
  143. _alignmentTimer = Timer(waitDuration, () {
  144. // 时间到了,启动实际的倒计时
  145. setState(() {
  146. timerState = TimerState.running;
  147. _remainingSeconds = totalSeconds;
  148. _totalDurationSeconds = totalSeconds;
  149. _alignmentTargetTime = null;
  150. });
  151. _backgroundTimerService.startTimer(totalSeconds);
  152. });
  153. }
  154. /// Start a new timer or resume an existing paused timer
  155. void _startTimer(String status) {
  156. if (status == "resume") {
  157. setState(() {
  158. timerState = TimerState.running;
  159. });
  160. _backgroundTimerService.resumeTimer(
  161. _remainingSeconds, _totalDurationSeconds);
  162. } else if (status == "start") {
  163. // 获取界面输入的时间
  164. int _hourstmp = _hoursController.selectedItem;
  165. int _minutestmp = _minutesController.selectedItem;
  166. int _secondstmp = _secondsController.selectedItem;
  167. // 计算总秒数
  168. final totalSeconds = _hourstmp * 3600 + _minutestmp * 60 + _secondstmp;
  169. if (totalSeconds <= 0) return;
  170. // 保存选择的时间值
  171. _hours = _hourstmp;
  172. _minutes = _minutestmp;
  173. _seconds = _secondstmp;
  174. // 检查是否需要整点对齐
  175. if (_settings.alignToHour && _settings.loop && totalSeconds >= 600) { // 只有在循环模式、时间至少10分钟时才启用整点对齐
  176. _startTimerWithAlignment(totalSeconds);
  177. } else {
  178. setState(() {
  179. timerState = TimerState.running;
  180. _remainingSeconds = totalSeconds;
  181. _totalDurationSeconds = totalSeconds;
  182. });
  183. _backgroundTimerService.startTimer(totalSeconds);
  184. }
  185. } else if (status == "loop") {
  186. // 计算总秒数
  187. final totalSeconds = _hours * 3600 + _minutes * 60 + _seconds;
  188. if (totalSeconds <= 0) return;
  189. // 检查是否需要整点对齐
  190. if (_settings.alignToHour && _settings.loop && totalSeconds >= 600) { // 只有在循环模式、时间至少10分钟时才启用整点对齐
  191. _startTimerWithAlignment(totalSeconds);
  192. } else {
  193. setState(() {
  194. timerState = TimerState.running;
  195. _remainingSeconds = totalSeconds;
  196. _totalDurationSeconds = totalSeconds;
  197. });
  198. _backgroundTimerService.startTimer(totalSeconds);
  199. }
  200. }
  201. }
  202. /// Pause the running timer
  203. void _pauseTimer() {
  204. if (timerState == TimerState.waiting) {
  205. // 如果在等待对齐状态,取消等待定时器
  206. _alignmentTimer?.cancel();
  207. _alignmentTargetTime = null;
  208. } else {
  209. _backgroundTimerService.pauseTimer();
  210. }
  211. setState(() {
  212. timerState = TimerState.pause;
  213. });
  214. }
  215. /// Reset the timer to initial state
  216. void _resetTimer() {
  217. _backgroundTimerService.cancelTimer();
  218. _audioManager.stopSound();
  219. _audioManager.stopVibration();
  220. _alignmentTimer?.cancel();
  221. _alignmentTargetTime = null;
  222. setState(() {
  223. timerState = TimerState.prepare;
  224. });
  225. }
  226. /// Handle timer completion
  227. Future<void> _timerCompleted() async {
  228. setState(() {
  229. timerState = TimerState.finish;
  230. });
  231. // Play sound and vibrate
  232. if (_settings.vibrate) {
  233. _audioManager.triggerVibration();
  234. }
  235. _audioManager.playSound(_settings.sound, _settings.volume, _settings.loop);
  236. // If loop is enabled, restart the timer
  237. if (_settings.loop) {
  238. Future.delayed(Duration(seconds: 5), () {
  239. _audioManager.stopSound();
  240. _audioManager.stopVibration();
  241. });
  242. _startTimer("loop");
  243. }
  244. }
  245. String _formatTime(int seconds) {
  246. final hours = seconds ~/ 3600;
  247. final minutes = (seconds % 3600) ~/ 60;
  248. final secs = seconds % 60;
  249. return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}';
  250. }
  251. // 格式化剩余等待时间
  252. String _formatWaitingTime() {
  253. if (_alignmentTargetTime == null) return "00:00";
  254. final now = DateTime.now();
  255. final difference = _alignmentTargetTime!.difference(now);
  256. // 如果差异为负,说明已经过了目标时间
  257. if (difference.isNegative) return "00:00";
  258. final minutes = difference.inMinutes;
  259. final seconds = difference.inSeconds % 60;
  260. return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
  261. }
  262. @override
  263. Widget build(BuildContext context) {
  264. if (!_settingsLoaded) {
  265. return Center(child: CircularProgressIndicator());
  266. }
  267. if (timerState != TimerState.prepare) {
  268. return _buildCountdownView();
  269. } else {
  270. return _buildTimerSetupView();
  271. }
  272. }
  273. Widget _buildTimerSetupView() {
  274. return Scaffold(
  275. backgroundColor: Colors.white,
  276. body: Column(
  277. mainAxisAlignment: MainAxisAlignment.center,
  278. children: [
  279. Expanded(
  280. child: Row(
  281. children: [
  282. Expanded(
  283. child: _buildTimerWheel(
  284. _hoursController,
  285. List.generate(24, (index) => index),
  286. (value) {
  287. setState(() {
  288. _hours = value;
  289. });
  290. },
  291. 'H',
  292. ),
  293. ),
  294. Expanded(
  295. child: _buildTimerWheel(
  296. _minutesController,
  297. List.generate(60, (index) => index),
  298. (value) {
  299. setState(() {
  300. _minutes = value;
  301. });
  302. },
  303. 'M',
  304. ),
  305. ),
  306. Expanded(
  307. child: _buildTimerWheel(
  308. _secondsController,
  309. List.generate(60, (index) => index),
  310. (value) {
  311. setState(() {
  312. _seconds = value;
  313. });
  314. },
  315. 'S',
  316. ),
  317. ),
  318. ],
  319. ),
  320. ),
  321. SizedBox(height: 20),
  322. Padding(
  323. padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 20),
  324. child: Row(
  325. mainAxisAlignment: MainAxisAlignment.spaceAround,
  326. children: [
  327. _buildCircleButton(
  328. Icons.sunny,
  329. Colors.grey[600]!,
  330. () {
  331. ScreenManager.toggleWakeLock();
  332. setState(() {});
  333. },
  334. isActive: ScreenManager.isWakeLockEnabled,
  335. ),
  336. _buildCircleButton(
  337. Icons.play_arrow,
  338. Colors.blue,
  339. () => _startTimer("start"),
  340. ),
  341. _buildCircleButton(
  342. Icons.settings,
  343. Colors.grey[600]!,
  344. () async {
  345. final result = await Navigator.push(
  346. context,
  347. MaterialPageRoute(
  348. builder: (context) =>
  349. TimerSettingsPage(settings: _settings)),
  350. );
  351. if (result != null) {
  352. setState(() {
  353. _settings = result;
  354. });
  355. }
  356. },
  357. ),
  358. ],
  359. ),
  360. ),
  361. ],
  362. ),
  363. );
  364. }
  365. Widget _buildCountdownView() {
  366. final totalMinutes = _remainingSeconds ~/ 60;
  367. // 如果处于等待整点对齐状态,显示不同的UI
  368. if (timerState == TimerState.waiting) {
  369. return _buildWaitingForAlignmentView();
  370. }
  371. return Scaffold(
  372. backgroundColor: Colors.white,
  373. body: Column(
  374. mainAxisAlignment: MainAxisAlignment.center,
  375. children: [
  376. Expanded(
  377. child: Center(
  378. child: Column(
  379. mainAxisAlignment: MainAxisAlignment.center,
  380. children: [
  381. Container(
  382. width: 300,
  383. height: 300,
  384. decoration: BoxDecoration(
  385. shape: BoxShape.circle,
  386. border: Border.all(
  387. color: Colors.blue.withOpacity(0.3),
  388. width: 3,
  389. ),
  390. ),
  391. child: Stack(
  392. alignment: Alignment.center,
  393. children: [
  394. // Timer progress
  395. SizedBox(
  396. width: 300,
  397. height: 300,
  398. child: CircularProgressIndicator(
  399. value: timerState == TimerState.finish
  400. ? 1
  401. : _totalDurationSeconds > 0
  402. ? _remainingSeconds / _totalDurationSeconds
  403. : 0,
  404. strokeWidth: 5,
  405. backgroundColor: Colors.grey.withOpacity(0.1),
  406. color: Colors.blue,
  407. ),
  408. ),
  409. // Time display
  410. Column(
  411. mainAxisAlignment: MainAxisAlignment.center,
  412. children: [
  413. Text(
  414. _formatTime(_remainingSeconds),
  415. style: TextStyle(
  416. fontSize: 40, fontWeight: FontWeight.bold),
  417. ),
  418. Text(
  419. 'Total ${totalMinutes} minutes',
  420. style:
  421. TextStyle(fontSize: 16, color: Colors.grey),
  422. ),
  423. ],
  424. ),
  425. // Indicator dot
  426. Positioned(
  427. bottom: 0,
  428. child: Container(
  429. width: 20,
  430. height: 20,
  431. decoration: BoxDecoration(
  432. color: Colors.blue,
  433. shape: BoxShape.circle,
  434. ),
  435. ),
  436. ),
  437. ],
  438. ),
  439. ),
  440. ],
  441. ),
  442. ),
  443. ),
  444. Padding(
  445. padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 20),
  446. child: Row(
  447. mainAxisAlignment: MainAxisAlignment.spaceAround,
  448. children: [
  449. // 唤醒屏幕
  450. _buildCircleButton(
  451. Icons.sunny,
  452. Colors.grey[600]!,
  453. () {
  454. ScreenManager.toggleWakeLock();
  455. setState(() {});
  456. },
  457. isActive: ScreenManager.isWakeLockEnabled,
  458. ),
  459. // 暂停/开始
  460. timerState == TimerState.running
  461. ? _buildCircleButton(
  462. Icons.pause,
  463. Colors.blue,
  464. () => _pauseTimer(),
  465. )
  466. : _buildCircleButton(
  467. Icons.play_arrow,
  468. Colors.blue,
  469. () => _startTimer("resume"),
  470. ),
  471. // 重置
  472. _buildCircleButton(
  473. Icons.stop,
  474. Colors.red,
  475. () => _resetTimer(),
  476. ),
  477. ],
  478. ),
  479. ),
  480. ],
  481. ),
  482. );
  483. }
  484. // 构建等待整点对齐的视图
  485. Widget _buildWaitingForAlignmentView() {
  486. final nextAlignmentTime = _alignmentTargetTime;
  487. return Scaffold(
  488. backgroundColor: Colors.white,
  489. body: Column(
  490. mainAxisAlignment: MainAxisAlignment.center,
  491. children: [
  492. Expanded(
  493. child: Center(
  494. child: Column(
  495. mainAxisAlignment: MainAxisAlignment.center,
  496. children: [
  497. Container(
  498. width: 300,
  499. height: 300,
  500. decoration: BoxDecoration(
  501. shape: BoxShape.circle,
  502. border: Border.all(
  503. color: Colors.orange.withOpacity(0.3),
  504. width: 3,
  505. ),
  506. ),
  507. child: Stack(
  508. alignment: Alignment.center,
  509. children: [
  510. // 倒计时进度(未开始,显示脉动效果)
  511. SizedBox(
  512. width: 300,
  513. height: 300,
  514. child: CircularProgressIndicator(
  515. value: null, // 显示不确定进度
  516. strokeWidth: 5,
  517. backgroundColor: Colors.grey.withOpacity(0.1),
  518. color: Colors.orange,
  519. ),
  520. ),
  521. // 等待时间显示
  522. Column(
  523. mainAxisAlignment: MainAxisAlignment.center,
  524. children: [
  525. Text(
  526. _formatWaitingTime(),
  527. style: TextStyle(
  528. fontSize: 40, fontWeight: FontWeight.bold),
  529. ),
  530. Text(
  531. '等待对齐整10分钟',
  532. style: TextStyle(fontSize: 16, color: Colors.grey),
  533. ),
  534. SizedBox(height: 10),
  535. if (nextAlignmentTime != null)
  536. Text(
  537. '将在 ${nextAlignmentTime.hour.toString().padLeft(2, '0')}:${nextAlignmentTime.minute.toString().padLeft(2, '0')} 开始',
  538. style: TextStyle(fontSize: 14, color: Colors.orange),
  539. ),
  540. ],
  541. ),
  542. ],
  543. ),
  544. ),
  545. ],
  546. ),
  547. ),
  548. ),
  549. Padding(
  550. padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 20),
  551. child: Row(
  552. mainAxisAlignment: MainAxisAlignment.spaceAround,
  553. children: [
  554. // 唤醒屏幕
  555. _buildCircleButton(
  556. Icons.sunny,
  557. Colors.grey[600]!,
  558. () {
  559. ScreenManager.toggleWakeLock();
  560. setState(() {});
  561. },
  562. isActive: ScreenManager.isWakeLockEnabled,
  563. ),
  564. // 在等待状态只显示取消按钮
  565. _buildCircleButton(
  566. Icons.stop,
  567. Colors.red,
  568. () => _resetTimer(),
  569. ),
  570. ],
  571. ),
  572. ),
  573. ],
  574. ),
  575. );
  576. }
  577. Widget _buildCircleButton(IconData icon, Color color, VoidCallback onPressed,
  578. {bool isActive = false}) {
  579. return Container(
  580. width: 70,
  581. height: 70,
  582. decoration: BoxDecoration(
  583. shape: BoxShape.circle,
  584. color: isActive ? color : Colors.white,
  585. boxShadow: [
  586. BoxShadow(
  587. color: Colors.black.withOpacity(0.1),
  588. blurRadius: 8,
  589. offset: Offset(0, 2),
  590. ),
  591. ],
  592. ),
  593. child: IconButton(
  594. icon: Icon(icon, size: 30),
  595. color: isActive ? Colors.white : color,
  596. onPressed: onPressed,
  597. ),
  598. );
  599. }
  600. Widget _buildTimerWheel(
  601. FixedExtentScrollController controller,
  602. List<int> items,
  603. ValueChanged<int> onChanged,
  604. String unit,
  605. ) {
  606. return Container(
  607. height: 400,
  608. decoration: BoxDecoration(
  609. border: Border(
  610. top: BorderSide(color: Colors.grey.withOpacity(0.3), width: 1),
  611. bottom: BorderSide(color: Colors.grey.withOpacity(0.3), width: 1),
  612. ),
  613. ),
  614. child: Stack(
  615. children: [
  616. // Center highlight
  617. Positioned.fill(
  618. child: Center(
  619. child: Container(
  620. height: 50,
  621. decoration: BoxDecoration(
  622. color: Colors.blue.withOpacity(0.1),
  623. borderRadius: BorderRadius.circular(8),
  624. ),
  625. ),
  626. ),
  627. ),
  628. Container(
  629. alignment: Alignment.centerRight,
  630. padding: EdgeInsets.only(right: 15),
  631. child: Text(
  632. unit,
  633. style: TextStyle(
  634. fontSize: 18,
  635. fontWeight: FontWeight.bold,
  636. ),
  637. ),
  638. ),
  639. ListWheelScrollView(
  640. controller: controller,
  641. physics: FixedExtentScrollPhysics(),
  642. diameterRatio: 1.5,
  643. itemExtent: 50,
  644. children: items.map((value) {
  645. return Center(
  646. child: Text(
  647. value.toString().padLeft(2, '0'),
  648. style: TextStyle(
  649. fontSize: 30,
  650. color: Colors.black,
  651. fontWeight: FontWeight.w500,
  652. ),
  653. ),
  654. );
  655. }).toList(),
  656. onSelectedItemChanged: onChanged,
  657. ),
  658. ],
  659. ),
  660. );
  661. }
  662. }