timer_page.dart 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  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 }
  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. // Total timer duration in seconds (for progress calculation)
  26. int _totalDurationSeconds = 0;
  27. // Background services
  28. final BackgroundTimerService _backgroundTimerService =
  29. BackgroundTimerService();
  30. // Settings
  31. late TimerSettings _settings;
  32. bool _settingsLoaded = false;
  33. final AudioManager _audioManager = AudioManager();
  34. // Wheel controllers
  35. final FixedExtentScrollController _hoursController =
  36. FixedExtentScrollController(initialItem: 0);
  37. final FixedExtentScrollController _minutesController =
  38. FixedExtentScrollController(initialItem: 10);
  39. final FixedExtentScrollController _secondsController =
  40. FixedExtentScrollController(initialItem: 0);
  41. @override
  42. void initState() {
  43. super.initState();
  44. WidgetsBinding.instance.addObserver(this);
  45. _initializeBackgroundTimer();
  46. _loadSettings();
  47. _restoreTimerState();
  48. }
  49. @override
  50. void dispose() {
  51. _audioManager.dispose();
  52. _backgroundTimerService.dispose();
  53. if (ScreenManager.isWakeLockEnabled) {
  54. ScreenManager.disableWakeLock();
  55. }
  56. WidgetsBinding.instance.removeObserver(this);
  57. _hoursController.dispose();
  58. _minutesController.dispose();
  59. _secondsController.dispose();
  60. super.dispose();
  61. }
  62. @override
  63. void didChangeAppLifecycleState(AppLifecycleState state) {
  64. // No additional handling required as background service handles the state
  65. }
  66. /// Restore timer state from persistent storage
  67. Future<void> _restoreTimerState() async {
  68. final timerData = await TimerData.load();
  69. if (timerData.isRunning) {
  70. setState(() {
  71. timerState = TimerState.running;
  72. _remainingSeconds = timerData.calculateRemainingSeconds();
  73. _totalDurationSeconds = _backgroundTimerService.totalDurationSeconds;
  74. });
  75. } else if (timerData.isPaused && timerData.pausedRemaining != null) {
  76. setState(() {
  77. timerState = TimerState.pause;
  78. _remainingSeconds = timerData.pausedRemaining!;
  79. _totalDurationSeconds = _backgroundTimerService.totalDurationSeconds;
  80. });
  81. }
  82. }
  83. /// Initialize the background timer service
  84. Future<void> _initializeBackgroundTimer() async {
  85. await _backgroundTimerService.initialize(
  86. onTimerComplete: _timerCompleted,
  87. onTimerTick: (remainingSeconds) {
  88. setState(() {
  89. _remainingSeconds = remainingSeconds;
  90. if (remainingSeconds <= 0 && timerState != TimerState.finish) {
  91. timerState = TimerState.finish;
  92. }
  93. });
  94. },
  95. );
  96. }
  97. Future<void> _loadSettings() async {
  98. _settings = await TimerSettings.loadSettings();
  99. setState(() {
  100. _settingsLoaded = true;
  101. });
  102. }
  103. /// Start a new timer or resume an existing paused timer
  104. void _startTimer(String status) {
  105. if (status == "resume") {
  106. setState(() {
  107. timerState = TimerState.running;
  108. });
  109. _backgroundTimerService.resumeTimer(
  110. _remainingSeconds, _totalDurationSeconds);
  111. } else if (status == "start") {
  112. // 获取界面输入的时间
  113. int _hourstmp = _hoursController.selectedItem;
  114. int _minutestmp = _minutesController.selectedItem;
  115. int _secondstmp = _secondsController.selectedItem;
  116. // 计算总秒数
  117. final totalSeconds = _hourstmp * 3600 + _minutestmp * 60 + _secondstmp;
  118. if (totalSeconds <= 0) return;
  119. setState(() {
  120. timerState = TimerState.running;
  121. _remainingSeconds = totalSeconds;
  122. _totalDurationSeconds = totalSeconds;
  123. });
  124. _backgroundTimerService.startTimer(totalSeconds);
  125. } else if (status == "loop") {
  126. // 计算总秒数
  127. final totalSeconds = _hours * 3600 + _minutes * 60 + _seconds;
  128. if (totalSeconds <= 0) return;
  129. setState(() {
  130. timerState = TimerState.running;
  131. _remainingSeconds = totalSeconds;
  132. _totalDurationSeconds = totalSeconds;
  133. });
  134. _backgroundTimerService.startTimer(totalSeconds);
  135. }
  136. }
  137. /// Pause the running timer
  138. void _pauseTimer() {
  139. _backgroundTimerService.pauseTimer();
  140. setState(() {
  141. timerState = TimerState.pause;
  142. });
  143. }
  144. /// Reset the timer to initial state
  145. void _resetTimer() {
  146. _backgroundTimerService.cancelTimer();
  147. _audioManager.stopSound();
  148. _audioManager.stopVibration();
  149. setState(() {
  150. timerState = TimerState.prepare;
  151. });
  152. }
  153. /// Handle timer completion
  154. Future<void> _timerCompleted() async {
  155. setState(() {
  156. timerState = TimerState.finish;
  157. });
  158. // Play sound and vibrate
  159. if (_settings.vibrate) {
  160. _audioManager.triggerVibration();
  161. }
  162. _audioManager.playSound(_settings.sound, _settings.volume, _settings.loop);
  163. // If loop is enabled, restart the timer
  164. if (_settings.loop) {
  165. Future.delayed(Duration(seconds: 5), () {
  166. _audioManager.stopSound();
  167. _audioManager.stopVibration();
  168. });
  169. _startTimer("loop");
  170. }
  171. }
  172. String _formatTime(int seconds) {
  173. final hours = seconds ~/ 3600;
  174. final minutes = (seconds % 3600) ~/ 60;
  175. final secs = seconds % 60;
  176. return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}';
  177. }
  178. @override
  179. Widget build(BuildContext context) {
  180. if (!_settingsLoaded) {
  181. return Center(child: CircularProgressIndicator());
  182. }
  183. if (timerState != TimerState.prepare) {
  184. return _buildCountdownView();
  185. } else {
  186. return _buildTimerSetupView();
  187. }
  188. }
  189. Widget _buildTimerSetupView() {
  190. return Scaffold(
  191. backgroundColor: Colors.white,
  192. body: Column(
  193. mainAxisAlignment: MainAxisAlignment.center,
  194. children: [
  195. Expanded(
  196. child: Row(
  197. children: [
  198. Expanded(
  199. child: _buildTimerWheel(
  200. _hoursController,
  201. List.generate(24, (index) => index),
  202. (value) {
  203. setState(() {
  204. _hours = value;
  205. });
  206. },
  207. 'H',
  208. ),
  209. ),
  210. Expanded(
  211. child: _buildTimerWheel(
  212. _minutesController,
  213. List.generate(60, (index) => index),
  214. (value) {
  215. setState(() {
  216. _minutes = value;
  217. });
  218. },
  219. 'M',
  220. ),
  221. ),
  222. Expanded(
  223. child: _buildTimerWheel(
  224. _secondsController,
  225. List.generate(60, (index) => index),
  226. (value) {
  227. setState(() {
  228. _seconds = value;
  229. });
  230. },
  231. 'S',
  232. ),
  233. ),
  234. ],
  235. ),
  236. ),
  237. SizedBox(height: 20),
  238. Padding(
  239. padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 20),
  240. child: Row(
  241. mainAxisAlignment: MainAxisAlignment.spaceAround,
  242. children: [
  243. _buildCircleButton(
  244. Icons.sunny,
  245. Colors.grey[600]!,
  246. () {
  247. ScreenManager.toggleWakeLock();
  248. setState(() {});
  249. },
  250. isActive: ScreenManager.isWakeLockEnabled,
  251. ),
  252. _buildCircleButton(
  253. Icons.play_arrow,
  254. Colors.blue,
  255. () => _startTimer("start"),
  256. ),
  257. _buildCircleButton(
  258. Icons.settings,
  259. Colors.grey[600]!,
  260. () async {
  261. final result = await Navigator.push(
  262. context,
  263. MaterialPageRoute(
  264. builder: (context) =>
  265. TimerSettingsPage(settings: _settings)),
  266. );
  267. if (result != null) {
  268. setState(() {
  269. _settings = result;
  270. });
  271. }
  272. },
  273. ),
  274. ],
  275. ),
  276. ),
  277. ],
  278. ),
  279. );
  280. }
  281. Widget _buildCountdownView() {
  282. final totalMinutes = _remainingSeconds ~/ 60;
  283. return Scaffold(
  284. backgroundColor: Colors.white,
  285. body: Column(
  286. mainAxisAlignment: MainAxisAlignment.center,
  287. children: [
  288. Expanded(
  289. child: Center(
  290. child: Column(
  291. mainAxisAlignment: MainAxisAlignment.center,
  292. children: [
  293. Container(
  294. width: 300,
  295. height: 300,
  296. decoration: BoxDecoration(
  297. shape: BoxShape.circle,
  298. border: Border.all(
  299. color: Colors.blue.withOpacity(0.3),
  300. width: 3,
  301. ),
  302. ),
  303. child: Stack(
  304. alignment: Alignment.center,
  305. children: [
  306. // Timer progress
  307. SizedBox(
  308. width: 300,
  309. height: 300,
  310. child: CircularProgressIndicator(
  311. value: timerState == TimerState.finish
  312. ? 1
  313. : _totalDurationSeconds > 0
  314. ? _remainingSeconds / _totalDurationSeconds
  315. : 0,
  316. strokeWidth: 5,
  317. backgroundColor: Colors.grey.withOpacity(0.1),
  318. color: Colors.blue,
  319. ),
  320. ),
  321. // Time display
  322. Column(
  323. mainAxisAlignment: MainAxisAlignment.center,
  324. children: [
  325. Text(
  326. _formatTime(_remainingSeconds),
  327. style: TextStyle(
  328. fontSize: 40, fontWeight: FontWeight.bold),
  329. ),
  330. Text(
  331. 'Total ${totalMinutes} minutes',
  332. style:
  333. TextStyle(fontSize: 16, color: Colors.grey),
  334. ),
  335. ],
  336. ),
  337. // Indicator dot
  338. Positioned(
  339. bottom: 0,
  340. child: Container(
  341. width: 20,
  342. height: 20,
  343. decoration: BoxDecoration(
  344. color: Colors.blue,
  345. shape: BoxShape.circle,
  346. ),
  347. ),
  348. ),
  349. ],
  350. ),
  351. ),
  352. ],
  353. ),
  354. ),
  355. ),
  356. Padding(
  357. padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 20),
  358. child: Row(
  359. mainAxisAlignment: MainAxisAlignment.spaceAround,
  360. children: [
  361. // 唤醒屏幕
  362. _buildCircleButton(
  363. Icons.sunny,
  364. Colors.grey[600]!,
  365. () {
  366. ScreenManager.toggleWakeLock();
  367. setState(() {});
  368. },
  369. isActive: ScreenManager.isWakeLockEnabled,
  370. ),
  371. // 暂停/开始
  372. timerState == TimerState.running
  373. ? _buildCircleButton(
  374. Icons.pause,
  375. Colors.blue,
  376. () => _pauseTimer(),
  377. )
  378. : _buildCircleButton(
  379. Icons.play_arrow,
  380. Colors.blue,
  381. () => _startTimer("resume"),
  382. ),
  383. // 重置
  384. _buildCircleButton(
  385. Icons.stop,
  386. Colors.red,
  387. () => _resetTimer(),
  388. ),
  389. ],
  390. ),
  391. ),
  392. ],
  393. ),
  394. );
  395. }
  396. Widget _buildCircleButton(IconData icon, Color color, VoidCallback onPressed,
  397. {bool isActive = false}) {
  398. return Container(
  399. width: 70,
  400. height: 70,
  401. decoration: BoxDecoration(
  402. shape: BoxShape.circle,
  403. color: isActive ? color : Colors.white,
  404. boxShadow: [
  405. BoxShadow(
  406. color: Colors.black.withOpacity(0.1),
  407. blurRadius: 8,
  408. offset: Offset(0, 2),
  409. ),
  410. ],
  411. ),
  412. child: IconButton(
  413. icon: Icon(icon, size: 30),
  414. color: isActive ? Colors.white : color,
  415. onPressed: onPressed,
  416. ),
  417. );
  418. }
  419. Widget _buildTimerWheel(
  420. FixedExtentScrollController controller,
  421. List<int> items,
  422. ValueChanged<int> onChanged,
  423. String unit,
  424. ) {
  425. return Container(
  426. height: 400,
  427. decoration: BoxDecoration(
  428. border: Border(
  429. top: BorderSide(color: Colors.grey.withOpacity(0.3), width: 1),
  430. bottom: BorderSide(color: Colors.grey.withOpacity(0.3), width: 1),
  431. ),
  432. ),
  433. child: Stack(
  434. children: [
  435. // Center highlight
  436. Positioned.fill(
  437. child: Center(
  438. child: Container(
  439. height: 50,
  440. decoration: BoxDecoration(
  441. color: Colors.blue.withOpacity(0.1),
  442. borderRadius: BorderRadius.circular(8),
  443. ),
  444. ),
  445. ),
  446. ),
  447. Container(
  448. alignment: Alignment.centerRight,
  449. padding: EdgeInsets.only(right: 15),
  450. child: Text(
  451. unit,
  452. style: TextStyle(
  453. fontSize: 18,
  454. fontWeight: FontWeight.bold,
  455. ),
  456. ),
  457. ),
  458. ListWheelScrollView(
  459. controller: controller,
  460. physics: FixedExtentScrollPhysics(),
  461. diameterRatio: 1.5,
  462. itemExtent: 50,
  463. children: items.map((value) {
  464. return Center(
  465. child: Text(
  466. value.toString().padLeft(2, '0'),
  467. style: TextStyle(
  468. fontSize: 30,
  469. color: Colors.black,
  470. fontWeight: FontWeight.w500,
  471. ),
  472. ),
  473. );
  474. }).toList(),
  475. onSelectedItemChanged: onChanged,
  476. ),
  477. ],
  478. ),
  479. );
  480. }
  481. }