timer_page.dart 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. import 'dart:async';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter_clock/model/timer_settings.dart';
  4. import 'package:flutter_clock/pages/timer_settings_page.dart';
  5. import 'package:flutter_clock/utils/audio_manager.dart';
  6. import 'package:flutter_clock/utils/screen_manager.dart';
  7. /// Description: 倒计时页面
  8. /// Time : 04/06/2025 Sunday
  9. /// Author : liuyuqi.gov@msn.cn
  10. class TimerPage extends StatefulWidget {
  11. @override
  12. _TimerPageState createState() => _TimerPageState();
  13. }
  14. class _TimerPageState extends State<TimerPage> with WidgetsBindingObserver {
  15. // Timer duration values
  16. int _hours = 0;
  17. int _minutes = 10;
  18. int _seconds = 0;
  19. // For timer controller
  20. Timer? _timer;
  21. bool _isRunning = false;
  22. bool _isCompleted = false;
  23. int _remainingSeconds = 0;
  24. // Settings
  25. late TimerSettings _settings;
  26. bool _settingsLoaded = false;
  27. final AudioManager _audioManager = AudioManager();
  28. // Wheel controllers
  29. final FixedExtentScrollController _hoursController =
  30. FixedExtentScrollController(initialItem: 0);
  31. final FixedExtentScrollController _minutesController =
  32. FixedExtentScrollController(initialItem: 10);
  33. final FixedExtentScrollController _secondsController =
  34. FixedExtentScrollController(initialItem: 0);
  35. @override
  36. void initState() {
  37. super.initState();
  38. WidgetsBinding.instance.addObserver(this);
  39. _loadSettings();
  40. }
  41. @override
  42. void dispose() {
  43. _timer?.cancel();
  44. _audioManager.dispose();
  45. if (ScreenManager.isWakeLockEnabled) {
  46. ScreenManager.disableWakeLock();
  47. }
  48. WidgetsBinding.instance.removeObserver(this);
  49. _hoursController.dispose();
  50. _minutesController.dispose();
  51. _secondsController.dispose();
  52. super.dispose();
  53. }
  54. @override
  55. void didChangeAppLifecycleState(AppLifecycleState state) {
  56. if (state == AppLifecycleState.resumed && _isRunning) {
  57. _syncTimer();
  58. }
  59. }
  60. Future<void> _loadSettings() async {
  61. _settings = await TimerSettings.loadSettings();
  62. setState(() {
  63. _settingsLoaded = true;
  64. });
  65. }
  66. void _startTimer() {
  67. final totalSeconds = _hours * 3600 + _minutes * 60 + _seconds;
  68. if (totalSeconds <= 0) return;
  69. setState(() {
  70. _isRunning = true;
  71. _isCompleted = false;
  72. _remainingSeconds = totalSeconds;
  73. });
  74. _timer = Timer.periodic(Duration(seconds: 1), (timer) {
  75. setState(() {
  76. if (_remainingSeconds > 0) {
  77. _remainingSeconds--;
  78. } else {
  79. _isCompleted = true;
  80. _timerCompleted();
  81. }
  82. });
  83. });
  84. }
  85. void _pauseTimer() {
  86. _timer?.cancel();
  87. setState(() {
  88. _isRunning = false;
  89. });
  90. }
  91. void _resetTimer() {
  92. _timer?.cancel();
  93. _audioManager.stopSound();
  94. _audioManager.stopVibration();
  95. setState(() {
  96. _isRunning = false;
  97. _isCompleted = false;
  98. });
  99. }
  100. void _syncTimer() {
  101. // Recalculate elapsed time if app was in background
  102. }
  103. Future<void> _timerCompleted() async {
  104. _timer?.cancel();
  105. _isRunning = false;
  106. // Play sound and vibrate
  107. if (_settings.vibrate) {
  108. _audioManager.triggerVibration();
  109. }
  110. _audioManager.playSound(_settings.sound, _settings.volume, _settings.loop);
  111. // If loop is enabled, restart the timer
  112. if (_settings.loop) {
  113. Future.delayed(Duration(seconds: 5), () {
  114. _audioManager.stopSound();
  115. _audioManager.stopVibration();
  116. _startTimer();
  117. });
  118. }
  119. }
  120. String _formatTime(int seconds) {
  121. final hours = seconds ~/ 3600;
  122. final minutes = (seconds % 3600) ~/ 60;
  123. final secs = seconds % 60;
  124. return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}';
  125. }
  126. @override
  127. Widget build(BuildContext context) {
  128. if (!_settingsLoaded) {
  129. return Center(child: CircularProgressIndicator());
  130. }
  131. if (_isRunning || _isCompleted) {
  132. return _buildCountdownView();
  133. } else {
  134. return _buildTimerSetupView();
  135. }
  136. }
  137. Widget _buildTimerSetupView() {
  138. return Scaffold(
  139. backgroundColor: Colors.white,
  140. body: Column(
  141. mainAxisAlignment: MainAxisAlignment.center,
  142. children: [
  143. Expanded(
  144. child: Row(
  145. children: [
  146. Expanded(
  147. child: _buildTimerWheel(
  148. _hoursController,
  149. List.generate(24, (index) => index),
  150. (value) {
  151. setState(() {
  152. _hours = value;
  153. });
  154. },
  155. 'H',
  156. ),
  157. ),
  158. Expanded(
  159. child: _buildTimerWheel(
  160. _minutesController,
  161. List.generate(60, (index) => index),
  162. (value) {
  163. setState(() {
  164. _minutes = value;
  165. });
  166. },
  167. 'M',
  168. ),
  169. ),
  170. Expanded(
  171. child: _buildTimerWheel(
  172. _secondsController,
  173. List.generate(60, (index) => index),
  174. (value) {
  175. setState(() {
  176. _seconds = value;
  177. });
  178. },
  179. 'S',
  180. ),
  181. ),
  182. ],
  183. ),
  184. ),
  185. SizedBox(height: 20),
  186. Padding(
  187. padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 20),
  188. child: Row(
  189. mainAxisAlignment: MainAxisAlignment.spaceAround,
  190. children: [
  191. _buildCircleButton(
  192. Icons.phone_android,
  193. Colors.grey[600]!,
  194. () {
  195. ScreenManager.toggleWakeLock();
  196. setState(() {});
  197. },
  198. isActive: ScreenManager.isWakeLockEnabled,
  199. ),
  200. _buildCircleButton(
  201. Icons.play_arrow,
  202. Colors.blue,
  203. () => _startTimer(),
  204. ),
  205. _buildCircleButton(
  206. Icons.settings,
  207. Colors.grey[600]!,
  208. () async {
  209. final result = await Navigator.push(
  210. context,
  211. MaterialPageRoute(
  212. builder: (context) =>
  213. TimerSettingsPage(settings: _settings)),
  214. );
  215. if (result != null) {
  216. setState(() {
  217. _settings = result;
  218. });
  219. }
  220. },
  221. ),
  222. ],
  223. ),
  224. ),
  225. ],
  226. ),
  227. );
  228. }
  229. Widget _buildCountdownView() {
  230. final totalMinutes = _remainingSeconds ~/ 60;
  231. return Scaffold(
  232. backgroundColor: Colors.white,
  233. body: Column(
  234. mainAxisAlignment: MainAxisAlignment.center,
  235. children: [
  236. Expanded(
  237. child: Center(
  238. child: Column(
  239. mainAxisAlignment: MainAxisAlignment.center,
  240. children: [
  241. Container(
  242. width: 300,
  243. height: 300,
  244. decoration: BoxDecoration(
  245. shape: BoxShape.circle,
  246. border: Border.all(
  247. color: Colors.blue.withOpacity(0.3),
  248. width: 1,
  249. ),
  250. ),
  251. child: Stack(
  252. alignment: Alignment.center,
  253. children: [
  254. // Timer progress
  255. SizedBox(
  256. width: 300,
  257. height: 300,
  258. child: CircularProgressIndicator(
  259. value: _isCompleted
  260. ? 1
  261. : _remainingSeconds /
  262. (_hours * 3600 + _minutes * 60 + _seconds),
  263. strokeWidth: 1,
  264. backgroundColor: Colors.grey.withOpacity(0.1),
  265. color: Colors.blue,
  266. ),
  267. ),
  268. // Time display
  269. Column(
  270. mainAxisAlignment: MainAxisAlignment.center,
  271. children: [
  272. Text(
  273. _formatTime(_remainingSeconds),
  274. style: TextStyle(
  275. fontSize: 40, fontWeight: FontWeight.bold),
  276. ),
  277. Text(
  278. 'Total ${totalMinutes} minutes',
  279. style:
  280. TextStyle(fontSize: 16, color: Colors.grey),
  281. ),
  282. ],
  283. ),
  284. // Indicator dot
  285. Positioned(
  286. bottom: 0,
  287. child: Container(
  288. width: 10,
  289. height: 10,
  290. decoration: BoxDecoration(
  291. color: Colors.blue,
  292. shape: BoxShape.circle,
  293. ),
  294. ),
  295. ),
  296. ],
  297. ),
  298. ),
  299. ],
  300. ),
  301. ),
  302. ),
  303. Padding(
  304. padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 20),
  305. child: Row(
  306. mainAxisAlignment: MainAxisAlignment.spaceAround,
  307. children: [
  308. // 唤醒屏幕
  309. _buildCircleButton(
  310. Icons.phone_android,
  311. Colors.grey[600]!,
  312. () {
  313. ScreenManager.toggleWakeLock();
  314. setState(() {});
  315. },
  316. isActive: ScreenManager.isWakeLockEnabled,
  317. ),
  318. // 暂停/开始
  319. _isRunning
  320. ? _buildCircleButton(
  321. Icons.pause,
  322. Colors.blue,
  323. () => _pauseTimer(),
  324. )
  325. : _buildCircleButton(
  326. Icons.play_arrow,
  327. Colors.blue,
  328. () => _startTimer(),
  329. ),
  330. // 重置
  331. _buildCircleButton(
  332. Icons.stop,
  333. Colors.red,
  334. () => _resetTimer(),
  335. ),
  336. ],
  337. ),
  338. ),
  339. ],
  340. ),
  341. );
  342. }
  343. Widget _buildCircleButton(IconData icon, Color color, VoidCallback onPressed,
  344. {bool isActive = false}) {
  345. return Container(
  346. width: 70,
  347. height: 70,
  348. decoration: BoxDecoration(
  349. shape: BoxShape.circle,
  350. color: isActive ? color : Colors.white,
  351. boxShadow: [
  352. BoxShadow(
  353. color: Colors.black.withOpacity(0.1),
  354. blurRadius: 8,
  355. offset: Offset(0, 2),
  356. ),
  357. ],
  358. ),
  359. child: IconButton(
  360. icon: Icon(icon, size: 30),
  361. color: isActive ? Colors.white : color,
  362. onPressed: onPressed,
  363. ),
  364. );
  365. }
  366. Widget _buildTimerWheel(
  367. FixedExtentScrollController controller,
  368. List<int> items,
  369. ValueChanged<int> onChanged,
  370. String unit,
  371. ) {
  372. return Column(
  373. children: [
  374. Expanded(
  375. child: Container(
  376. decoration: BoxDecoration(
  377. border: Border(
  378. top: BorderSide(color: Colors.grey.withOpacity(0.3), width: 1),
  379. bottom:
  380. BorderSide(color: Colors.grey.withOpacity(0.3), width: 1),
  381. ),
  382. ),
  383. child: Stack(
  384. children: [
  385. // Center highlight
  386. Positioned.fill(
  387. child: Center(
  388. child: Container(
  389. height: 50,
  390. decoration: BoxDecoration(
  391. color: Colors.blue.withOpacity(0.1),
  392. borderRadius: BorderRadius.circular(8),
  393. ),
  394. ),
  395. ),
  396. ),
  397. ListWheelScrollView(
  398. controller: controller,
  399. physics: FixedExtentScrollPhysics(),
  400. diameterRatio: 1.5,
  401. itemExtent: 50,
  402. children: items.map((value) {
  403. return Center(
  404. child: Text(
  405. value.toString().padLeft(2, '0'),
  406. style: TextStyle(
  407. fontSize: 30,
  408. color: Colors.black,
  409. fontWeight: FontWeight.w500,
  410. ),
  411. ),
  412. );
  413. }).toList(),
  414. onSelectedItemChanged: onChanged,
  415. ),
  416. ],
  417. ),
  418. ),
  419. ),
  420. SizedBox(height: 8),
  421. Text(
  422. unit,
  423. style: TextStyle(
  424. fontSize: 18,
  425. fontWeight: FontWeight.bold,
  426. ),
  427. ),
  428. ],
  429. );
  430. }
  431. }