Browse Source

Merge branch 'release/1.0.3'

liuyuqi-dellpc 3 months ago
parent
commit
614b053aee

+ 1 - 1
analysis_options.yaml

@@ -7,7 +7,7 @@
 
 # The following line activates a set of recommended lints for Flutter apps,
 # packages, and plugins designed to encourage good coding practices.
-include: package:flutter_lints/flutter.yaml
+# include: package:flutter_lints/flutter.yaml
 
 linter:
   # The lint rules applied to this project can be customized in the

+ 26 - 20
lib/index_page.dart

@@ -1,6 +1,3 @@
-import 'dart:async';
-import 'dart:math' as math;
-
 import 'package:flutter/material.dart';
 import 'package:flutter_clock/pages/alarm_page.dart';
 import 'package:flutter_clock/pages/clock_page.dart';
@@ -23,9 +20,6 @@ class _IndexPageState extends State<IndexPage>
   late TabController _tabController;
   late List<Widget> _tabs;
 
-  final _utcMidnightRadiansOffset = radiansFromDegrees(-64);
-  static const _secondsInDay = 86400;
-  DateTime _localTime = DateTime.now();
 
   @override
   void initState() {
@@ -40,11 +34,9 @@ class _IndexPageState extends State<IndexPage>
       Tab(child: Text('Alarm', style: TextStyle(fontSize: 16.sp))),
       Tab(child: Text('Clock', style: TextStyle(fontSize: 16.sp))),
       Tab(child: Text('Stopwatch', style: TextStyle(fontSize: 16.sp))),
-      Tab(child: Text('Timer', style: TextStyle(fontSize: 16.sp))),
+      Tab(child: Text('Timer', style: TextStyle(fontSize: 16.sp))),  
     ];
     
-    Timer.periodic(Duration(seconds: 1),
-        (_) => setState(() => _localTime = DateTime.now()));
   }
 
   @override
@@ -67,6 +59,8 @@ class _IndexPageState extends State<IndexPage>
           unselectedLabelColor: Colors.black,
           indicatorColor: Colors.blue,
           indicatorWeight: 3.h,
+          labelStyle: TextStyle(fontWeight: FontWeight.bold),
+          unselectedLabelStyle: TextStyle(fontWeight: FontWeight.normal),
         ),
         // actions: [
         //   IconButton(
@@ -81,22 +75,34 @@ class _IndexPageState extends State<IndexPage>
       body: TabBarView(
         controller: _tabController,
         children: [
-          AlarmPage(),
-          ClockPage(),
-          StopwatchPage(),
-          TimerPage(),
+          // Wrap each page with AutomaticKeepAlive widget to preserve state
+          KeepAlivePage(child: AlarmPage()),
+          KeepAlivePage(child: ClockPage()),
+          KeepAlivePage(child: StopwatchPage()),
+          KeepAlivePage(child: TimerPage()),
         ],
       ),
     );
   }
+}
+
+/// A wrapper widget that keeps its child alive when switching between tabs
+class KeepAlivePage extends StatefulWidget {
+  final Widget child;
+
+  const KeepAlivePage({Key? key, required this.child}) : super(key: key);
+
+  @override
+  _KeepAlivePageState createState() => _KeepAlivePageState();
+}
 
-  static double radiansFromDegrees(double degrees) => degrees * math.pi / 180;
+class _KeepAlivePageState extends State<KeepAlivePage> with AutomaticKeepAliveClientMixin {
+  @override
+  bool get wantKeepAlive => true;
 
-  static double radiansFromTime(DateTime time) {
-    final midnightToday = DateTime(time.year, time.month, time.day);
-    final secondsSinceMidnight = midnightToday.difference(time).inSeconds;
-    final percent = secondsSinceMidnight / _secondsInDay;
-    final degrees = percent * 360;
-    return radiansFromDegrees(degrees);
+  @override
+  Widget build(BuildContext context) {
+    super.build(context); // Important: This must be called
+    return widget.child;
   }
 }

+ 13 - 0
lib/main.dart

@@ -69,6 +69,19 @@ class MyApp extends StatelessWidget {
             primarySwatch: Colors.blue,
             visualDensity: VisualDensity.adaptivePlatformDensity,
             fontFamily: 'Roboto',
+            brightness: Brightness.light,
+            tabBarTheme: TabBarTheme(
+              labelColor: Colors.blue,
+              unselectedLabelColor: Colors.black87,
+              indicatorSize: TabBarIndicatorSize.tab,
+              labelStyle: TextStyle(fontWeight: FontWeight.bold),
+              unselectedLabelStyle: TextStyle(fontWeight: FontWeight.normal),
+            ),
+            appBarTheme: AppBarTheme(
+              color: Colors.white,
+              foregroundColor: Colors.black,
+              elevation: 1.0,
+            ),
           ),
           home: IndexPage(),
         );

+ 5 - 0
lib/model/timer_settings.dart

@@ -5,17 +5,20 @@ class TimerSettings {
   static const String _volumeKey = 'timer_volume';
   static const String _vibrateKey = 'timer_vibrate';
   static const String _loopKey = 'timer_loop';
+  static const String _alignToHourKey = 'timer_align_to_hour';
 
   String sound; 
   double volume;
   bool vibrate; // 是否震动
   bool loop;  // 是否循环
+  bool alignToHour; // 整点对齐
 
   TimerSettings({
     this.sound = 'Dripping',
     this.volume = 0.7,
     this.vibrate = true,
     this.loop = true,
+    this.alignToHour = false,
   });
 
   // Save settings to SharedPreferences
@@ -25,6 +28,7 @@ class TimerSettings {
     await prefs.setDouble(_volumeKey, volume);
     await prefs.setBool(_vibrateKey, vibrate);
     await prefs.setBool(_loopKey, loop);
+    await prefs.setBool(_alignToHourKey, alignToHour);
   }
 
   // Load settings from SharedPreferences
@@ -35,6 +39,7 @@ class TimerSettings {
       volume: prefs.getDouble(_volumeKey) ?? 0.7,
       vibrate: prefs.getBool(_vibrateKey) ?? true,
       loop: prefs.getBool(_loopKey) ?? true,
+      alignToHour: prefs.getBool(_alignToHourKey) ?? false,
     );
   }
 }

+ 266 - 5
lib/pages/alarm_page.dart

@@ -1,4 +1,52 @@
 import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
+import 'package:intl/intl.dart';
+
+class AlarmModel {
+  final int id;
+  final TimeOfDay time;
+  final List<bool> days; // 7 days from Monday to Sunday
+  final String label;
+  bool isActive;
+
+  AlarmModel({
+    required this.id,
+    required this.time,
+    required this.days,
+    required this.label,
+    this.isActive = true,
+  });
+
+  String get timeString {
+    final hour = time.hour;
+    final minute = time.minute;
+    return '${hour.toString().padLeft(2, '0')}:${minute.toString().padLeft(2, '0')}';
+  }
+
+  String get repeatDaysString {
+    List<String> dayShortNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
+    List<String> activeDays = [];
+
+    for (int i = 0; i < 7; i++) {
+      if (days[i]) {
+        activeDays.add(dayShortNames[i]);
+      }
+    }
+
+    if (activeDays.isEmpty) {
+      return 'One time';
+    } else if (activeDays.length == 7) {
+      return 'Every day';
+    } else if (activeDays.length == 5 && 
+               days[0] && days[1] && days[2] && days[3] && days[4]) {
+      return 'Weekdays';
+    } else if (activeDays.length == 2 && days[5] && days[6]) {
+      return 'Weekends';
+    } else {
+      return activeDays.join(', ');
+    }
+  }
+}
 
 class AlarmPage extends StatefulWidget {
   @override
@@ -6,16 +54,229 @@ class AlarmPage extends StatefulWidget {
 }
 
 class _AlarmPageState extends State<AlarmPage> {
+  List<AlarmModel> _alarms = [];
+  int _nextId = 0;
+
+  @override
+  void initState() {
+    super.initState();
+    // Add some sample alarms
+    _addAlarm(
+      TimeOfDay(hour: 8, minute: 0),
+      [true, true, true, true, true, false, false],
+      'Wake up',
+    );
+    _addAlarm(
+      TimeOfDay(hour: 18, minute: 30),
+      [true, true, true, true, true, true, true],
+      'Evening reminder',
+    );
+  }
+
+  void _addAlarm(TimeOfDay time, List<bool> days, String label) {
+    setState(() {
+      _alarms.add(
+        AlarmModel(
+          id: _nextId++,
+          time: time,
+          days: days,
+          label: label,
+        ),
+      );
+    });
+  }
+
+  void _toggleAlarm(int id) {
+    setState(() {
+      for (int i = 0; i < _alarms.length; i++) {
+        if (_alarms[i].id == id) {
+          _alarms[i].isActive = !_alarms[i].isActive;
+          break;
+        }
+      }
+    });
+  }
+
+  void _deleteAlarm(int id) {
+    setState(() {
+      _alarms.removeWhere((alarm) => alarm.id == id);
+    });
+  }
+
+  Future<void> _showAddAlarmDialog() async {
+    final TimeOfDay initialTime = TimeOfDay.now();
+    final List<bool> days = List.generate(7, (_) => false);
+    final TextEditingController labelController = TextEditingController();
+
+    await showDialog(
+      context: context,
+      builder: (BuildContext context) {
+        TimeOfDay selectedTime = initialTime;
+
+        return StatefulBuilder(
+          builder: (context, setState) {
+            return AlertDialog(
+              title: Text('Add Alarm'),
+              content: SingleChildScrollView(
+                child: Column(
+                  mainAxisSize: MainAxisSize.min,
+                  children: [
+                    // Time picker button
+                    ListTile(
+                      title: Text('Time'),
+                      subtitle: Text(
+                        '${selectedTime.hour.toString().padLeft(2, '0')}:${selectedTime.minute.toString().padLeft(2, '0')}',
+                        style: TextStyle(fontSize: 24.sp),
+                      ),
+                      onTap: () async {
+                        final TimeOfDay? picked = await showTimePicker(
+                          context: context,
+                          initialTime: selectedTime,
+                        );
+                        if (picked != null) {
+                          setState(() {
+                            selectedTime = picked;
+                          });
+                        }
+                      },
+                    ),
+                    SizedBox(height: 10.h),
+                    // Repeat days
+                    Text('Repeat', style: TextStyle(fontWeight: FontWeight.bold)),
+                    _buildWeekdaySelector(days, setState),
+                    SizedBox(height: 10.h),
+                    // Label
+                    TextField(
+                      controller: labelController,
+                      decoration: InputDecoration(
+                        labelText: 'Label',
+                        border: OutlineInputBorder(),
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+              actions: [
+                TextButton(
+                  onPressed: () => Navigator.of(context).pop(),
+                  child: Text('Cancel'),
+                ),
+                TextButton(
+                  onPressed: () {
+                    _addAlarm(
+                      selectedTime,
+                      List.from(days),
+                      labelController.text.isNotEmpty
+                          ? labelController.text
+                          : 'Alarm',
+                    );
+                    Navigator.of(context).pop();
+                  },
+                  child: Text('Save'),
+                ),
+              ],
+            );
+          },
+        );
+      },
+    );
+  }
+
+  Widget _buildWeekdaySelector(List<bool> days, StateSetter setState) {
+    List<String> dayShortNames = ['M', 'T', 'W', 'T', 'F', 'S', 'S'];
+    return Row(
+      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+      children: List.generate(7, (index) {
+        return InkWell(
+          onTap: () {
+            setState(() {
+              days[index] = !days[index];
+            });
+          },
+          child: CircleAvatar(
+            radius: 18.r,
+            backgroundColor: days[index] ? Colors.blue : Colors.grey[300],
+            child: Text(
+              dayShortNames[index],
+              style: TextStyle(
+                color: days[index] ? Colors.white : Colors.black87,
+                fontWeight: FontWeight.bold,
+              ),
+            ),
+          ),
+        );
+      }),
+    );
+  }
+
   @override
   Widget build(BuildContext context) {
     return Scaffold(
       backgroundColor: Colors.white,
-      body: Center(
-        child: Text(
-          'Alarm Feature Coming Soon',
-          style: TextStyle(fontSize: 20),
-        ),
+      floatingActionButton: FloatingActionButton(
+        onPressed: _showAddAlarmDialog,
+        child: Icon(Icons.add),
+        tooltip: 'Add Alarm',
       ),
+      body: _alarms.isEmpty
+          ? Center(
+              child: Text(
+                'No alarms set',
+                style: TextStyle(fontSize: 18.sp, color: Colors.grey),
+              ),
+            )
+          : ListView.builder(
+              itemCount: _alarms.length,
+              itemBuilder: (context, index) {
+                final alarm = _alarms[index];
+                return Card(
+                  margin: EdgeInsets.symmetric(horizontal: 16.w, vertical: 8.h),
+                  child: ListTile(
+                    title: Text(
+                      alarm.timeString,
+                      style: TextStyle(
+                        fontSize: 24.sp,
+                        fontWeight: FontWeight.bold,
+                        color: alarm.isActive ? Colors.black : Colors.grey,
+                      ),
+                    ),
+                    subtitle: Column(
+                      crossAxisAlignment: CrossAxisAlignment.start,
+                      children: [
+                        Text(
+                          alarm.repeatDaysString,
+                          style: TextStyle(
+                            fontSize: 14.sp,
+                            color: alarm.isActive ? Colors.black87 : Colors.grey,
+                          ),
+                        ),
+                        Text(
+                          alarm.label,
+                          style: TextStyle(
+                            fontSize: 16.sp,
+                            fontWeight: FontWeight.w500,
+                            color: alarm.isActive ? Colors.black87 : Colors.grey,
+                          ),
+                        ),
+                      ],
+                    ),
+                    trailing: Row(
+                      mainAxisSize: MainAxisSize.min,
+                      children: [
+                        Switch(
+                          value: alarm.isActive,
+                          onChanged: (_) => _toggleAlarm(alarm.id),
+                        ),
+                        IconButton(
+                          icon: Icon(Icons.delete, color: Colors.red),
+                          onPressed: () => _deleteAlarm(alarm.id),
+                        ),
+                      ],
+                    ),
+                  ),
+                );
+              },
+            ),
     );
   }
 }

+ 173 - 5
lib/pages/stopwatch_page.dart

@@ -1,4 +1,6 @@
+import 'dart:async';
 import 'package:flutter/material.dart';
+import 'package:flutter_screenutil/flutter_screenutil.dart';
 
 class StopwatchPage extends StatefulWidget {
   @override
@@ -6,16 +8,182 @@ class StopwatchPage extends StatefulWidget {
 }
 
 class _StopwatchPageState extends State<StopwatchPage> {
+  bool _isRunning = false;
+  Stopwatch _stopwatch = Stopwatch();
+  Timer? _timer;
+  List<String> _laps = [];
+  
+  @override
+  void initState() {
+    super.initState();
+  }
+
+  @override
+  void dispose() {
+    _timer?.cancel();
+    super.dispose();
+  }
+
+  void _startStop() {
+    setState(() {
+      if (_isRunning) {
+        _isRunning = false;
+        _timer?.cancel();
+        _stopwatch.stop();
+      } else {
+        _isRunning = true;
+        _stopwatch.start();
+        _timer = Timer.periodic(Duration(milliseconds: 10), (timer) {
+          setState(() {});
+        });
+      }
+    });
+  }
+
+  void _resetStopwatch() {
+    setState(() {
+      _stopwatch.reset();
+      _laps.clear();
+    });
+  }
+
+  void _addLap() {
+    if (_isRunning) {
+      setState(() {
+        _laps.insert(0, _formatTime(_stopwatch.elapsedMilliseconds));
+      });
+    }
+  }
+
+  String _formatTime(int milliseconds) {
+    int hundreds = (milliseconds / 10).truncate() % 100;
+    int seconds = (milliseconds / 1000).truncate() % 60;
+    int minutes = (milliseconds / 60000).truncate() % 60;
+    int hours = (milliseconds / 3600000).truncate();
+
+    String hoursStr = hours > 0 ? '${hours.toString().padLeft(2, '0')}:' : '';
+    String minutesStr = minutes.toString().padLeft(2, '0');
+    String secondsStr = seconds.toString().padLeft(2, '0');
+    String hundredsStr = hundreds.toString().padLeft(2, '0');
+
+    return '$hoursStr$minutesStr:$secondsStr.$hundredsStr';
+  }
+
   @override
   Widget build(BuildContext context) {
     return Scaffold(
       backgroundColor: Colors.white,
-      body: Center(
-        child: Text(
-          'Stopwatch Feature Coming Soon',
-          style: TextStyle(fontSize: 20),
-        ),
+      body: Column(
+        children: [
+          SizedBox(height: 50.h),
+          // Stopwatch display
+          Text(
+            _formatTime(_stopwatch.elapsedMilliseconds),
+            style: TextStyle(
+              fontSize: 60.sp,
+              fontWeight: FontWeight.bold,
+              fontFamily: 'monospace',
+            ),
+          ),
+          SizedBox(height: 30.h),
+          // Controls
+          Row(
+            mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+            children: [
+              _buildControlButton(
+                icon: _isRunning ? Icons.pause : Icons.play_arrow,
+                label: _isRunning ? 'Stop' : 'Start',
+                onPressed: _startStop,
+                color: _isRunning ? Colors.red : Colors.green,
+              ),
+              _buildControlButton(
+                icon: Icons.refresh,
+                label: 'Reset',
+                onPressed: _stopwatch.elapsedMilliseconds > 0 ? _resetStopwatch : null,
+                color: Colors.blue,
+              ),
+              _buildControlButton(
+                icon: Icons.flag,
+                label: 'Lap',
+                onPressed: _isRunning ? _addLap : null,
+                color: Colors.orange,
+              ),
+            ],
+          ),
+          SizedBox(height: 20.h),
+          // Laps
+          Expanded(
+            child: _laps.isEmpty
+                ? Center(
+                    child: Text(
+                      'No laps recorded',
+                      style: TextStyle(
+                        fontSize: 16.sp,
+                        color: Colors.grey,
+                      ),
+                    ),
+                  )
+                : ListView.builder(
+                    itemCount: _laps.length,
+                    itemBuilder: (context, index) {
+                      return ListTile(
+                        dense: true,
+                        leading: CircleAvatar(
+                          radius: 14.r,
+                          backgroundColor: Colors.blue[100],
+                          child: Text(
+                            '${_laps.length - index}',
+                            style: TextStyle(
+                              fontSize: 12.sp,
+                              color: Colors.black87,
+                            ),
+                          ),
+                        ),
+                        title: Text(
+                          'Lap ${_laps.length - index}',
+                          style: TextStyle(
+                            fontSize: 16.sp,
+                          ),
+                        ),
+                        trailing: Text(
+                          _laps[index],
+                          style: TextStyle(
+                            fontSize: 16.sp,
+                            fontFamily: 'monospace',
+                            fontWeight: FontWeight.bold,
+                          ),
+                        ),
+                      );
+                    },
+                  ),
+          ),
+        ],
       ),
     );
   }
+
+  Widget _buildControlButton({
+    required IconData icon,
+    required String label,
+    required VoidCallback? onPressed,
+    required Color color,
+  }) {
+    return Column(
+      children: [
+        FloatingActionButton(
+          onPressed: onPressed,
+          backgroundColor: onPressed == null ? Colors.grey[300] : color,
+          child: Icon(icon, color: Colors.white),
+        ),
+        SizedBox(height: 8.h),
+        Text(
+          label,
+          style: TextStyle(
+            fontSize: 14.sp,
+            color: onPressed == null ? Colors.grey : Colors.black87,
+          ),
+        ),
+      ],
+    );
+  }
 }

+ 267 - 18
lib/pages/timer/timer_page.dart

@@ -15,7 +15,7 @@ class TimerPage extends StatefulWidget {
   _TimerPageState createState() => _TimerPageState();
 }
 
-enum TimerState { prepare, running, pause, finish }
+enum TimerState { prepare, running, pause, finish, waiting }
 
 class _TimerPageState extends State<TimerPage> with WidgetsBindingObserver {
   // Timer duration values
@@ -26,6 +26,12 @@ class _TimerPageState extends State<TimerPage> with WidgetsBindingObserver {
   // For timer controller
   TimerState timerState = TimerState.prepare;
   int _remainingSeconds = 0;
+  
+  // Waiting for alignment timer
+  Timer? _alignmentTimer;
+  DateTime? _alignmentTargetTime;
+  DateTime? _alignmentStartTime; // 记录开始等待的时间点
+  Timer? _alignmentUIUpdateTimer; // For refreshing UI during waiting
 
   // Total timer duration in seconds (for progress calculation)
   int _totalDurationSeconds = 0;
@@ -60,6 +66,8 @@ class _TimerPageState extends State<TimerPage> with WidgetsBindingObserver {
   void dispose() {
     _audioManager.dispose();
     _backgroundTimerService.dispose();
+    _alignmentTimer?.cancel();
+    _alignmentUIUpdateTimer?.cancel(); // Cancel UI update timer
     if (ScreenManager.isWakeLockEnabled) {
       ScreenManager.disableWakeLock();
     }
@@ -116,6 +124,99 @@ class _TimerPageState extends State<TimerPage> with WidgetsBindingObserver {
     });
   }
 
+  /// Calculate the next 10-minute alignment time
+  DateTime _calculateNextAlignmentTime() {
+    final now = DateTime.now();
+    // 计算下一个整10分钟的时间点
+    int nextMinutes = ((now.minute ~/ 10) + 1) * 10;
+    int nextHour = now.hour;
+    
+    // 处理进位
+    if (nextMinutes >= 60) {
+      nextMinutes = 0;
+      nextHour = (nextHour + 1) % 24;
+    }
+    
+    return DateTime(
+      now.year, 
+      now.month, 
+      now.day, 
+      nextHour, 
+      nextMinutes, 
+      0, 
+      0, 
+      0
+    );
+  }
+
+  // 计算等待进度
+  double _calculateWaitingProgress() {
+    if (_alignmentTargetTime == null) return 0.0;
+    
+    final now = DateTime.now();
+    final targetTime = _alignmentTargetTime!;
+    final startTime = _alignmentStartTime ?? now;
+    
+    // 如果还没开始等待或已经过了目标时间
+    if (now.isAfter(targetTime)) return 1.0;
+    if (now.isBefore(startTime)) return 0.0;
+    
+    // 计算总等待时间和已等待时间
+    final totalWaitDuration = targetTime.difference(startTime).inMilliseconds;
+    final elapsedWaitDuration = now.difference(startTime).inMilliseconds;
+    
+    // 计算进度比例
+    if (totalWaitDuration <= 0) return 1.0;
+    final progress = elapsedWaitDuration / totalWaitDuration;
+    
+    // 确保进度在0.0到1.0之间
+    return progress.clamp(0.0, 1.0);
+  }
+  
+  /// Wait until the next 10-minute mark and then start the timer
+  void _startTimerWithAlignment(int totalSeconds) {
+    // 取消任何现有的对齐定时器
+    _alignmentTimer?.cancel();
+    _alignmentUIUpdateTimer?.cancel();
+    
+    // 计算下一个整10分钟的时间点
+    final alignmentTime = _calculateNextAlignmentTime();
+    _alignmentTargetTime = alignmentTime;
+    _alignmentStartTime = DateTime.now(); // 记录开始等待的时间
+    
+    // 设置状态为等待
+    setState(() {
+      timerState = TimerState.waiting;
+    });
+    
+    // 计算等待时间(毫秒)
+    final waitDuration = alignmentTime.difference(DateTime.now());
+    
+    // 创建定时器等待到指定时间
+    _alignmentTimer = Timer(waitDuration, () {
+      // 时间到了,启动实际的倒计时
+      setState(() {
+        timerState = TimerState.running;
+        _remainingSeconds = totalSeconds;
+        _totalDurationSeconds = totalSeconds;
+        _alignmentTargetTime = null;
+        _alignmentStartTime = null;
+      });
+      
+      _backgroundTimerService.startTimer(totalSeconds);
+      _alignmentUIUpdateTimer?.cancel(); // Cancel UI update timer when alignment is done
+    });
+    
+    // 创建UI刷新定时器,每秒更新一次
+    _alignmentUIUpdateTimer = Timer.periodic(Duration(seconds: 1), (timer) {
+      if (mounted) {
+        setState(() {
+          // 仅用于刷新UI
+        });
+      }
+    });
+  }
+
   /// Start a new timer or resume an existing paused timer
   void _startTimer(String status) {
     if (status == "resume") {
@@ -132,32 +233,56 @@ class _TimerPageState extends State<TimerPage> with WidgetsBindingObserver {
       // 计算总秒数
       final totalSeconds = _hourstmp * 3600 + _minutestmp * 60 + _secondstmp;
       if (totalSeconds <= 0) return;
-
-      setState(() {
-        timerState = TimerState.running;
-        _remainingSeconds = totalSeconds;
-        _totalDurationSeconds = totalSeconds;
-      });
-
-      _backgroundTimerService.startTimer(totalSeconds);
+      
+      // 保存选择的时间值
+      _hours = _hourstmp;
+      _minutes = _minutestmp;
+      _seconds = _secondstmp;
+      
+      // 检查是否需要整点对齐
+      if (_settings.alignToHour && _settings.loop && totalSeconds >= 600) { // 只有在循环模式、时间至少10分钟时才启用整点对齐
+        _startTimerWithAlignment(totalSeconds);
+      } else {
+        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);
+      
+      // 检查是否需要整点对齐
+      if (_settings.alignToHour && _settings.loop && totalSeconds >= 600) { // 只有在循环模式、时间至少10分钟时才启用整点对齐
+        _startTimerWithAlignment(totalSeconds);
+      } else {
+        setState(() {
+          timerState = TimerState.running;
+          _remainingSeconds = totalSeconds;
+          _totalDurationSeconds = totalSeconds;
+        });
+        
+        _backgroundTimerService.startTimer(totalSeconds);
+      }
     }
   }
 
   /// Pause the running timer
   void _pauseTimer() {
-    _backgroundTimerService.pauseTimer();
+    if (timerState == TimerState.waiting) {
+      // 如果在等待对齐状态,取消等待定时器
+      _alignmentTimer?.cancel();
+      _alignmentUIUpdateTimer?.cancel(); // Cancel UI update timer
+      _alignmentTargetTime = null;
+      _alignmentStartTime = null;
+    } else {
+      _backgroundTimerService.pauseTimer();
+    }
+    
     setState(() {
       timerState = TimerState.pause;
     });
@@ -168,6 +293,11 @@ class _TimerPageState extends State<TimerPage> with WidgetsBindingObserver {
     _backgroundTimerService.cancelTimer();
     _audioManager.stopSound();
     _audioManager.stopVibration();
+    _alignmentTimer?.cancel();
+    _alignmentUIUpdateTimer?.cancel(); // Cancel UI update timer
+    _alignmentTargetTime = null;
+    _alignmentStartTime = null;
+    
     setState(() {
       timerState = TimerState.prepare;
     });
@@ -201,6 +331,23 @@ class _TimerPageState extends State<TimerPage> with WidgetsBindingObserver {
     final secs = seconds % 60;
     return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${secs.toString().padLeft(2, '0')}';
   }
+  
+  // 格式化剩余等待时间
+  String _formatWaitingTime() {
+    if (_alignmentTargetTime == null) return "00:00";
+    
+    final now = DateTime.now();
+    final difference = _alignmentTargetTime!.difference(now);
+    
+    // 如果差异为负,说明已经过了目标时间
+    if (difference.isNegative) return "00:00";
+    
+    // 计算分钟和秒数
+    final minutes = difference.inMinutes;
+    final seconds = difference.inSeconds % 60;
+    
+    return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
+  }
 
   @override
   Widget build(BuildContext context) {
@@ -310,6 +457,12 @@ class _TimerPageState extends State<TimerPage> with WidgetsBindingObserver {
 
   Widget _buildCountdownView() {
     final totalMinutes = _remainingSeconds ~/ 60;
+    
+    // 如果处于等待整点对齐状态,显示不同的UI
+    if (timerState == TimerState.waiting) {
+      return _buildWaitingForAlignmentView();
+    }
+    
     return Scaffold(
       backgroundColor: Colors.white,
       body: Column(
@@ -423,6 +576,102 @@ class _TimerPageState extends State<TimerPage> with WidgetsBindingObserver {
       ),
     );
   }
+  
+  // 构建等待整点对齐的视图
+  Widget _buildWaitingForAlignmentView() {
+    final nextAlignmentTime = _alignmentTargetTime;
+    final waitingProgress = _calculateWaitingProgress();
+    
+    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.orange.withOpacity(0.3),
+                        width: 3,
+                      ),
+                    ),
+                    child: Stack(
+                      alignment: Alignment.center,
+                      children: [
+                        // 等待进度指示器
+                        SizedBox(
+                          width: 300,
+                          height: 300,
+                          child: CircularProgressIndicator(
+                            value: waitingProgress, // 显示确定的等待进度
+                            strokeWidth: 5,
+                            backgroundColor: Colors.grey.withOpacity(0.1),
+                            color: Colors.orange,
+                          ),
+                        ),
+                        // 等待时间显示
+                        Column(
+                          mainAxisAlignment: MainAxisAlignment.center,
+                          children: [
+                            Text(
+                              _formatWaitingTime(),
+                              style: TextStyle(
+                                  fontSize: 40, fontWeight: FontWeight.bold),
+                            ),
+                            Text(
+                              '等待对齐整10分钟',
+                              style: TextStyle(fontSize: 16, color: Colors.grey),
+                            ),
+                            SizedBox(height: 10),
+                            if (nextAlignmentTime != null)
+                              Text(
+                                '将在 ${nextAlignmentTime.hour.toString().padLeft(2, '0')}:${nextAlignmentTime.minute.toString().padLeft(2, '0')} 开始',
+                                style: TextStyle(fontSize: 14, color: Colors.orange),
+                              ),
+                          ],
+                        ),
+                      ],
+                    ),
+                  ),
+                ],
+              ),
+            ),
+          ),
+          Padding(
+            padding: const EdgeInsets.symmetric(horizontal: 50, vertical: 20),
+            child: Row(
+              mainAxisAlignment: MainAxisAlignment.spaceAround,
+              children: [
+                // 唤醒屏幕
+                _buildCircleButton(
+                  Icons.sunny,
+                  Colors.grey[600]!,
+                  () {
+                    ScreenManager.toggleWakeLock();
+                    setState(() {});
+                  },
+                  isActive: ScreenManager.isWakeLockEnabled,
+                ),
+                // 在等待状态只显示取消按钮
+                _buildCircleButton(
+                  Icons.stop,
+                  Colors.red,
+                  () => _resetTimer(),
+                ),
+              ],
+            ),
+          ),
+        ],
+      ),
+    );
+  }
 
   Widget _buildCircleButton(IconData icon, Color color, VoidCallback onPressed,
       {bool isActive = false}) {

+ 59 - 0
lib/pages/timer/timer_settings_page.dart

@@ -19,6 +19,7 @@ class _TimerSettingsPageState extends State<TimerSettingsPage> {
   late double _volume;
   late bool _vibrate;
   late bool _loop;
+  late bool _alignToHour;
 
   final List<String> _availableSounds = ['Dripping', 'Alarm', 'Bell'];
 
@@ -29,6 +30,7 @@ class _TimerSettingsPageState extends State<TimerSettingsPage> {
     _volume = widget.settings.volume;
     _vibrate = widget.settings.vibrate;
     _loop = widget.settings.loop;
+    _alignToHour = widget.settings.alignToHour;
   }
   
   // Save settings
@@ -38,6 +40,7 @@ class _TimerSettingsPageState extends State<TimerSettingsPage> {
       volume: _volume,
       vibrate: _vibrate,
       loop: _loop,
+      alignToHour: _alignToHour,
     );
     await updatedSettings.saveSettings();
     return updatedSettings;
@@ -75,6 +78,7 @@ class _TimerSettingsPageState extends State<TimerSettingsPage> {
             _buildVolumeSetting(),
             _buildVibrateSetting(),
             _buildLoopSetting(),
+            _buildAlignToHourSetting(),
           ],
         ),
       ),
@@ -198,6 +202,10 @@ class _TimerSettingsPageState extends State<TimerSettingsPage> {
                 onChanged: (value) {
                   setState(() {
                     _loop = value;
+                    // 如果循环被关闭,也关闭整点对齐
+                    if (!value) {
+                      _alignToHour = false;
+                    }
                   });
                   _saveSettings();
                 },
@@ -209,6 +217,57 @@ class _TimerSettingsPageState extends State<TimerSettingsPage> {
       ],
     );
   }
+  
+  Widget _buildAlignToHourSetting() {
+    // 只有当倒计时满10分钟且启用了循环倒计时时才启用该选项
+    bool isEnabled = _loop;
+    
+    return Column(
+      crossAxisAlignment: CrossAxisAlignment.start,
+      children: [
+        Padding(
+          padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
+          child: Row(
+            mainAxisAlignment: MainAxisAlignment.spaceBetween,
+            children: [
+              Column(
+                crossAxisAlignment: CrossAxisAlignment.start,
+                children: [
+                  Text(
+                    '整点对齐',
+                    style: TextStyle(
+                      fontSize: 18,
+                      color: isEnabled ? Colors.black : Colors.grey,
+                    ),
+                  ),
+                  Text(
+                    '倒计时将等待整10分钟再开始',
+                    style: TextStyle(
+                      fontSize: 12,
+                      color: Colors.grey,
+                    ),
+                  ),
+                ],
+              ),
+              Switch(
+                value: _alignToHour && isEnabled,
+                activeColor: Colors.orange,
+                onChanged: isEnabled
+                    ? (value) {
+                        setState(() {
+                          _alignToHour = value;
+                        });
+                        _saveSettings();
+                      }
+                    : null,
+              ),
+            ],
+          ),
+        ),
+        Divider(),
+      ],
+    );
+  }
 
   void _showSoundPicker() {
     showModalBottomSheet(

+ 0 - 2
lib/utils/background_timer_service.dart

@@ -3,7 +3,6 @@ import 'dart:isolate';
 import 'dart:ui';
 
 import 'package:flutter_clock/model/timer_data.dart';
-import 'package:flutter_clock/utils/audio_manager.dart';
 
 /// A service to handle background timer operations
 class BackgroundTimerService {
@@ -12,7 +11,6 @@ class BackgroundTimerService {
   // Isolate for background processing
   Isolate? _isolate;
   ReceivePort? _receivePort;
-  SendPort? _sendPort;
   
   // Callback when timer completes
   Function? _onTimerComplete;

+ 1 - 1
lib/views/side_menu_item.dart

@@ -42,7 +42,7 @@ class SideMenuItem extends StatelessWidget {
                     SizedBox(width: kDefaultPadding * 0.75),
                     Text(
                       title,
-                      style: Theme.of(context).textTheme.button?.copyWith(
+                      style: Theme.of(context).textTheme.labelLarge?.copyWith(
                             color:
                                 (isActive || isHover) ? kTextColor : kGrayColor,
                           ),

+ 8 - 0
linux/flutter/generated_plugin_registrant.cc

@@ -7,9 +7,17 @@
 #include "generated_plugin_registrant.h"
 
 #include <audioplayers_linux/audioplayers_linux_plugin.h>
+#include <screen_retriever/screen_retriever_plugin.h>
+#include <window_manager/window_manager_plugin.h>
 
 void fl_register_plugins(FlPluginRegistry* registry) {
   g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
       fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin");
   audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar);
+  g_autoptr(FlPluginRegistrar) screen_retriever_registrar =
+      fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin");
+  screen_retriever_plugin_register_with_registrar(screen_retriever_registrar);
+  g_autoptr(FlPluginRegistrar) window_manager_registrar =
+      fl_plugin_registry_get_registrar_for_plugin(registry, "WindowManagerPlugin");
+  window_manager_plugin_register_with_registrar(window_manager_registrar);
 }

+ 2 - 0
linux/flutter/generated_plugins.cmake

@@ -4,6 +4,8 @@
 
 list(APPEND FLUTTER_PLUGIN_LIST
   audioplayers_linux
+  screen_retriever
+  window_manager
 )
 
 list(APPEND FLUTTER_FFI_PLUGIN_LIST

+ 6 - 0
windows/flutter/generated_plugin_registrant.cc

@@ -7,8 +7,14 @@
 #include "generated_plugin_registrant.h"
 
 #include <audioplayers_windows/audioplayers_windows_plugin.h>
+#include <screen_retriever/screen_retriever_plugin.h>
+#include <window_manager/window_manager_plugin.h>
 
 void RegisterPlugins(flutter::PluginRegistry* registry) {
   AudioplayersWindowsPluginRegisterWithRegistrar(
       registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
+  ScreenRetrieverPluginRegisterWithRegistrar(
+      registry->GetRegistrarForPlugin("ScreenRetrieverPlugin"));
+  WindowManagerPluginRegisterWithRegistrar(
+      registry->GetRegistrarForPlugin("WindowManagerPlugin"));
 }

+ 2 - 0
windows/flutter/generated_plugins.cmake

@@ -4,6 +4,8 @@
 
 list(APPEND FLUTTER_PLUGIN_LIST
   audioplayers_windows
+  screen_retriever
+  window_manager
 )
 
 list(APPEND FLUTTER_FFI_PLUGIN_LIST