Compare commits
	
		
			11 Commits
		
	
	
		
			b7a7eee777
			...
			v1.0.1
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| eb870c4d49 | |||
| 897069c08b | |||
| a1b577ef4c | |||
| 64895c12b9 | |||
| 7601cf3a22 | |||
| 8c7150a1ed | |||
| a29f090895 | |||
| 8640ad0499 | |||
| e62c74b72c | |||
| 75454c6edc | |||
| 997dd5b6b3 | 
							
								
								
									
										377
									
								
								lib/main.dart
									
									
									
									
									
								
							
							
						
						
									
										377
									
								
								lib/main.dart
									
									
									
									
									
								
							@@ -1,6 +1,9 @@
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'dart:async';
 | 
					import 'dart:async';
 | 
				
			||||||
import 'package:flutter/services.dart';
 | 
					import 'package:flutter/services.dart';
 | 
				
			||||||
 | 
					import 'dart:io';
 | 
				
			||||||
 | 
					import 'dart:convert';
 | 
				
			||||||
 | 
					import 'dart:developer' as developer;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
void main() {
 | 
					void main() {
 | 
				
			||||||
  runApp(const MyApp());
 | 
					  runApp(const MyApp());
 | 
				
			||||||
@@ -12,11 +15,20 @@ class MyApp extends StatelessWidget {
 | 
				
			|||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return MaterialApp(
 | 
					    return MaterialApp(
 | 
				
			||||||
      title: 'Simple Timer',
 | 
					      title: 'Just a timer',
 | 
				
			||||||
      theme: ThemeData(
 | 
					      darkTheme: ThemeData(
 | 
				
			||||||
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
 | 
					        brightness: Brightness.dark,
 | 
				
			||||||
 | 
					        useMaterial3: true,
 | 
				
			||||||
 | 
					        scaffoldBackgroundColor: const Color(0xFF111827),
 | 
				
			||||||
 | 
					        colorSchemeSeed: const Color(0xFF111827),
 | 
				
			||||||
 | 
					        highlightColor: const Color(0xff1f2937),
 | 
				
			||||||
 | 
					        textTheme: TextTheme(
 | 
				
			||||||
 | 
					          bodyLarge: TextStyle(fontSize: 22),
 | 
				
			||||||
 | 
					          bodyMedium: TextStyle(fontSize: 20),
 | 
				
			||||||
 | 
					          bodySmall: TextStyle(fontSize: 17),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      home: const MyHomePage(title: 'Simple Timer'),
 | 
					      home: const MyHomePage(title: 'Just a timer'),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -28,6 +40,25 @@ class MyHomePage extends StatefulWidget {
 | 
				
			|||||||
  State<MyHomePage> createState() => _MyHomePageState();
 | 
					  State<MyHomePage> createState() => _MyHomePageState();
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TimerEvent {
 | 
				
			||||||
 | 
					  final String type; // "start", "pause", "lap", "stop"
 | 
				
			||||||
 | 
					  final DateTime timestamp;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  TimerEvent(this.type, this.timestamp);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Map<String, dynamic> toJson() => {
 | 
				
			||||||
 | 
					    'type': type,
 | 
				
			||||||
 | 
					    'timestamp': timestamp.toIso8601String(),
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  factory TimerEvent.fromJson(Map<String, dynamic> json) {
 | 
				
			||||||
 | 
					    return TimerEvent(
 | 
				
			||||||
 | 
					      json['type'] as String,
 | 
				
			||||||
 | 
					      DateTime.parse(json['timestamp'] as String),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _MyHomePageState extends State<MyHomePage> {
 | 
					class _MyHomePageState extends State<MyHomePage> {
 | 
				
			||||||
  Timer? _timer;
 | 
					  Timer? _timer;
 | 
				
			||||||
  DateTime? _startTime;
 | 
					  DateTime? _startTime;
 | 
				
			||||||
@@ -36,6 +67,154 @@ class _MyHomePageState extends State<MyHomePage> {
 | 
				
			|||||||
  bool _isRunning = false;
 | 
					  bool _isRunning = false;
 | 
				
			||||||
  List<String> _laps = [];
 | 
					  List<String> _laps = [];
 | 
				
			||||||
  final FocusNode _focusNode = FocusNode();
 | 
					  final FocusNode _focusNode = FocusNode();
 | 
				
			||||||
 | 
					  List<TimerEvent> _events = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  void initState() {
 | 
				
			||||||
 | 
					    super.initState();
 | 
				
			||||||
 | 
					    _focusNode.requestFocus();
 | 
				
			||||||
 | 
					    _loadTimerData();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Simple file operations using app's temporary directory
 | 
				
			||||||
 | 
					  Future<File> get _timerFile async {
 | 
				
			||||||
 | 
					    final directory = Directory.systemTemp.path;
 | 
				
			||||||
 | 
					    return File('$directory/timer_data.json');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _saveTimerData() async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final file = await _timerFile;
 | 
				
			||||||
 | 
					      final data = {
 | 
				
			||||||
 | 
					        'events': _events.map((e) => e.toJson()).toList(),
 | 
				
			||||||
 | 
					        'laps': _laps,
 | 
				
			||||||
 | 
					      };
 | 
				
			||||||
 | 
					      await file.writeAsString(jsonEncode(data));
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      // Handle error silently using proper logging
 | 
				
			||||||
 | 
					      developer.log('Failed to save timer data: $e', name: 'simple_timer');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> _loadTimerData() async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final file = await _timerFile;
 | 
				
			||||||
 | 
					      if (await file.exists()) {
 | 
				
			||||||
 | 
					        final jsonData = await file.readAsString();
 | 
				
			||||||
 | 
					        final data = jsonDecode(jsonData) as Map<String, dynamic>;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Load events
 | 
				
			||||||
 | 
					        _events = (data['events'] as List)
 | 
				
			||||||
 | 
					            .map((e) => TimerEvent.fromJson(e as Map<String, dynamic>))
 | 
				
			||||||
 | 
					            .toList();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Load laps
 | 
				
			||||||
 | 
					        _laps = (data['laps'] as List).cast<String>();
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Reconstruct timer state from events
 | 
				
			||||||
 | 
					        _reconstructTimerState();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      // Handle error silently using proper logging
 | 
				
			||||||
 | 
					      developer.log('Failed to load timer data: $e', name: 'simple_timer');
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _reconstructTimerState() {
 | 
				
			||||||
 | 
					    if (_events.isEmpty) return;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Find the most recent event sequence
 | 
				
			||||||
 | 
					    DateTime? lastStartTime;
 | 
				
			||||||
 | 
					    DateTime? lastPauseTime;
 | 
				
			||||||
 | 
					    Duration totalPausedTime = Duration.zero;
 | 
				
			||||||
 | 
					    Duration accumulatedRunningTime = Duration.zero;
 | 
				
			||||||
 | 
					    bool wasRunning = false;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Process events chronologically to determine the current state
 | 
				
			||||||
 | 
					    for (int i = 0; i < _events.length; i++) {
 | 
				
			||||||
 | 
					      final event = _events[i];
 | 
				
			||||||
 | 
					      
 | 
				
			||||||
 | 
					      switch (event.type) {
 | 
				
			||||||
 | 
					        case 'start':
 | 
				
			||||||
 | 
					          if (lastStartTime == null) {
 | 
				
			||||||
 | 
					            // New start
 | 
				
			||||||
 | 
					            lastStartTime = event.timestamp;
 | 
				
			||||||
 | 
					            wasRunning = true;
 | 
				
			||||||
 | 
					          } else if (lastPauseTime != null) {
 | 
				
			||||||
 | 
					            // Resume after pause - add the pause duration to totalPausedTime
 | 
				
			||||||
 | 
					            totalPausedTime += event.timestamp.difference(lastPauseTime);
 | 
				
			||||||
 | 
					            lastPauseTime = null;
 | 
				
			||||||
 | 
					            wasRunning = true;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					        case 'pause':
 | 
				
			||||||
 | 
					          if (lastStartTime != null && lastPauseTime == null) {
 | 
				
			||||||
 | 
					            // Pause an active timer
 | 
				
			||||||
 | 
					            lastPauseTime = event.timestamp;
 | 
				
			||||||
 | 
					            accumulatedRunningTime += lastPauseTime.difference(lastStartTime);
 | 
				
			||||||
 | 
					            wasRunning = false;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					          
 | 
				
			||||||
 | 
					        case 'stop':
 | 
				
			||||||
 | 
					          // Reset everything
 | 
				
			||||||
 | 
					          lastStartTime = null;
 | 
				
			||||||
 | 
					          lastPauseTime = null;
 | 
				
			||||||
 | 
					          totalPausedTime = Duration.zero;
 | 
				
			||||||
 | 
					          accumulatedRunningTime = Duration.zero;
 | 
				
			||||||
 | 
					          wasRunning = false;
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    // Determine timer state based on most recent events
 | 
				
			||||||
 | 
					    if (lastStartTime != null) {
 | 
				
			||||||
 | 
					      if (wasRunning) {
 | 
				
			||||||
 | 
					        // Timer was running when app was closed
 | 
				
			||||||
 | 
					        // Calculate elapsed time: current time - start time - total paused time
 | 
				
			||||||
 | 
					        final now = DateTime.now();
 | 
				
			||||||
 | 
					        _elapsedTime = now.difference(lastStartTime) - totalPausedTime;
 | 
				
			||||||
 | 
					        _startTime = lastStartTime;
 | 
				
			||||||
 | 
					        _pauseTime = null;
 | 
				
			||||||
 | 
					        _isRunning = true;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
 | 
					        // Start the timer without resetting anything
 | 
				
			||||||
 | 
					        _resumeTimerFromSavedState();
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        // Timer was paused
 | 
				
			||||||
 | 
					        _startTime = lastStartTime;
 | 
				
			||||||
 | 
					        _pauseTime = lastPauseTime;
 | 
				
			||||||
 | 
					        _elapsedTime = accumulatedRunningTime;
 | 
				
			||||||
 | 
					        _isRunning = false;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Resume the timer from a saved state without resetting anything
 | 
				
			||||||
 | 
					  void _resumeTimerFromSavedState() {
 | 
				
			||||||
 | 
					    // Safety check
 | 
				
			||||||
 | 
					    if (_startTime == null) return;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    _timer = Timer.periodic(const Duration(milliseconds: 16), (timer) {
 | 
				
			||||||
 | 
					      setState(() {
 | 
				
			||||||
 | 
					        // When restoring, we need the total elapsed time since the original start
 | 
				
			||||||
 | 
					        // This accounts for any pauses that happened before
 | 
				
			||||||
 | 
					        final now = DateTime.now();
 | 
				
			||||||
 | 
					        _elapsedTime = now.difference(_startTime!);
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    setState(() {
 | 
				
			||||||
 | 
					      _isRunning = true;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  void _addEvent(String type) {
 | 
				
			||||||
 | 
					    final event = TimerEvent(type, DateTime.now());
 | 
				
			||||||
 | 
					    _events.add(event);
 | 
				
			||||||
 | 
					    _saveTimerData();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _startTimer() {
 | 
					  void _startTimer() {
 | 
				
			||||||
    final now = DateTime.now();
 | 
					    final now = DateTime.now();
 | 
				
			||||||
@@ -44,14 +223,17 @@ class _MyHomePageState extends State<MyHomePage> {
 | 
				
			|||||||
      final pausedDuration = now.difference(_pauseTime!);
 | 
					      final pausedDuration = now.difference(_pauseTime!);
 | 
				
			||||||
      _startTime = _startTime?.add(pausedDuration);
 | 
					      _startTime = _startTime?.add(pausedDuration);
 | 
				
			||||||
      _pauseTime = null;
 | 
					      _pauseTime = null;
 | 
				
			||||||
 | 
					      _addEvent('start');
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      // Starting fresh
 | 
					      // Starting fresh
 | 
				
			||||||
      _startTime = now;
 | 
					      _startTime = now;
 | 
				
			||||||
      _elapsedTime = Duration.zero;
 | 
					      _elapsedTime = Duration.zero;
 | 
				
			||||||
      _laps = [];
 | 
					      _laps = [];
 | 
				
			||||||
 | 
					      _events = []; // Clear previous events
 | 
				
			||||||
 | 
					      _addEvent('start');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    _timer = Timer.periodic(const Duration(milliseconds: 100), (timer) {
 | 
					    _timer = Timer.periodic(const Duration(milliseconds: 16), (timer) {
 | 
				
			||||||
      setState(() {
 | 
					      setState(() {
 | 
				
			||||||
        _elapsedTime = DateTime.now().difference(_startTime!);
 | 
					        _elapsedTime = DateTime.now().difference(_startTime!);
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
@@ -65,6 +247,7 @@ class _MyHomePageState extends State<MyHomePage> {
 | 
				
			|||||||
  void _pauseTimer() {
 | 
					  void _pauseTimer() {
 | 
				
			||||||
    _timer?.cancel();
 | 
					    _timer?.cancel();
 | 
				
			||||||
    _pauseTime = DateTime.now();
 | 
					    _pauseTime = DateTime.now();
 | 
				
			||||||
 | 
					    _addEvent('pause');
 | 
				
			||||||
    setState(() {
 | 
					    setState(() {
 | 
				
			||||||
      _isRunning = false;
 | 
					      _isRunning = false;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@@ -72,64 +255,72 @@ class _MyHomePageState extends State<MyHomePage> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  void _stopTimer() {
 | 
					  void _stopTimer() {
 | 
				
			||||||
    _timer?.cancel();
 | 
					    _timer?.cancel();
 | 
				
			||||||
 | 
					    _addEvent('stop');
 | 
				
			||||||
    setState(() {
 | 
					    setState(() {
 | 
				
			||||||
      _isRunning = false;
 | 
					      _isRunning = false;
 | 
				
			||||||
      _startTime = null;
 | 
					      _startTime = null;
 | 
				
			||||||
      _pauseTime = null;
 | 
					      _pauseTime = null;
 | 
				
			||||||
      _elapsedTime = Duration.zero;
 | 
					      _elapsedTime = Duration.zero;
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					    _saveTimerData();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _recordLap() {
 | 
					  void _recordLap() {
 | 
				
			||||||
    if (_isRunning) {
 | 
					    if (_isRunning) {
 | 
				
			||||||
      final now = DateTime.now();
 | 
					      final now = DateTime.now();
 | 
				
			||||||
 | 
					      _addEvent('lap');
 | 
				
			||||||
      setState(() {
 | 
					      setState(() {
 | 
				
			||||||
        // Store both timestamp and formatted duration for this lap
 | 
					        // Store both timestamp and formatted duration for this lap
 | 
				
			||||||
        _laps.add('${now.toIso8601String()} - ${_formatDuration(_elapsedTime)}');
 | 
					        _laps.add('${now.toIso8601String()} - ${_formatDurationForLap(_elapsedTime)}');
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 | 
					      _saveTimerData();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  String _formatDuration(Duration duration) {
 | 
					  String _formatDuration(Duration duration) {
 | 
				
			||||||
    // Format the duration as ISO 8601 duration format without 0 values
 | 
					    // Format with fixed width on a single line, hiding zero hours/minutes
 | 
				
			||||||
    final hours = duration.inHours;
 | 
					    final hours = duration.inHours;
 | 
				
			||||||
    final minutes = duration.inMinutes.remainder(60);
 | 
					    final minutes = duration.inMinutes.remainder(60);
 | 
				
			||||||
    final seconds = duration.inSeconds.remainder(60);
 | 
					    final seconds = duration.inSeconds.remainder(60);
 | 
				
			||||||
 | 
					    final milliseconds = duration.inMilliseconds.remainder(1000);
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // Round milliseconds to nearest 100ms for more consistent display
 | 
					    // Use padLeft to ensure consistent width with leading zeros
 | 
				
			||||||
    // This helps avoid floating point inconsistencies (0.201, 0.301, etc.)
 | 
					    final secondsStr = seconds.toString().padLeft(2, '0');
 | 
				
			||||||
    final milliseconds = ((duration.inMilliseconds % 1000) / 100).round() * 100;
 | 
					    final msStr = milliseconds.toString().padLeft(3, '0');
 | 
				
			||||||
    final formattedMs = (milliseconds ~/ 100).toString();
 | 
					 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // Start with PT prefix
 | 
					    // Build output - only show non-zero hours and minutes
 | 
				
			||||||
    String result = 'PT';
 | 
					    StringBuffer result = StringBuffer();
 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    // Only add parts that are non-zero (except if everything is zero, then show 0S)
 | 
					 | 
				
			||||||
    if (hours > 0) {
 | 
					    if (hours > 0) {
 | 
				
			||||||
      result += '${hours}H';
 | 
					      result.write('${hours.toString().padLeft(2, '0')}H ');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    
 | 
					    if (hours > 0 || minutes > 0) {
 | 
				
			||||||
    if (minutes > 0 || hours > 0) {
 | 
					      result.write('${minutes.toString().padLeft(2, '0')}M ');
 | 
				
			||||||
      result += '${minutes}M';
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    // Always show seconds and milliseconds
 | 
				
			||||||
 | 
					    result.write('${secondsStr}S ${msStr}MS');
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // Always include seconds with a single decimal place (tenth of a second)
 | 
					    return result.toString();
 | 
				
			||||||
    result += '${seconds}.${formattedMs}S';
 | 
					 | 
				
			||||||
    
 | 
					 | 
				
			||||||
    return result;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  void _handleKeyEvent(RawKeyEvent event) {
 | 
					  // For lap recording - use the same format
 | 
				
			||||||
    if (event is RawKeyDownEvent) {
 | 
					  String _formatDurationForLap(Duration duration) {
 | 
				
			||||||
 | 
					    return _formatDuration(duration);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Update keyboard handler method to use modern KeyEvent API
 | 
				
			||||||
 | 
					  void _handleKeyEvent(KeyEvent event) {
 | 
				
			||||||
 | 
					    // Only process key down events to avoid duplicate triggers
 | 
				
			||||||
 | 
					    if (event is KeyDownEvent) {
 | 
				
			||||||
 | 
					      // Handle Space key
 | 
				
			||||||
      if (event.logicalKey == LogicalKeyboardKey.space) {
 | 
					      if (event.logicalKey == LogicalKeyboardKey.space) {
 | 
				
			||||||
        // Space: Start if paused, Lap if running
 | 
					 | 
				
			||||||
        if (_isRunning) {
 | 
					        if (_isRunning) {
 | 
				
			||||||
          _recordLap();
 | 
					          _recordLap();
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
          _startTimer();
 | 
					          _startTimer();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      } else if (event.logicalKey == LogicalKeyboardKey.enter) {
 | 
					      }
 | 
				
			||||||
        // Enter: Stop if paused, Pause if running
 | 
					      // Handle Enter key
 | 
				
			||||||
 | 
					      else if (event.logicalKey == LogicalKeyboardKey.enter) {
 | 
				
			||||||
        if (_isRunning) {
 | 
					        if (_isRunning) {
 | 
				
			||||||
          _pauseTimer();
 | 
					          _pauseTimer();
 | 
				
			||||||
        } else {
 | 
					        } else {
 | 
				
			||||||
@@ -139,12 +330,6 @@ class _MyHomePageState extends State<MyHomePage> {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					 | 
				
			||||||
  void initState() {
 | 
					 | 
				
			||||||
    super.initState();
 | 
					 | 
				
			||||||
    _focusNode.requestFocus();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  void dispose() {
 | 
					  void dispose() {
 | 
				
			||||||
    _timer?.cancel();
 | 
					    _timer?.cancel();
 | 
				
			||||||
@@ -154,78 +339,84 @@ class _MyHomePageState extends State<MyHomePage> {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context) {
 | 
					  Widget build(BuildContext context) {
 | 
				
			||||||
    return RawKeyboardListener(
 | 
					    // Use Focus directly to capture key events
 | 
				
			||||||
 | 
					    return Focus(
 | 
				
			||||||
      focusNode: _focusNode,
 | 
					      focusNode: _focusNode,
 | 
				
			||||||
      onKey: _handleKeyEvent,
 | 
					 | 
				
			||||||
      autofocus: true,
 | 
					      autofocus: true,
 | 
				
			||||||
 | 
					      onKeyEvent: (FocusNode node, KeyEvent event) {
 | 
				
			||||||
 | 
					        _handleKeyEvent(event);
 | 
				
			||||||
 | 
					        // Always return KeyEventResult.handled to indicate we've processed the event
 | 
				
			||||||
 | 
					        return KeyEventResult.handled;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
      child: Scaffold(
 | 
					      child: Scaffold(
 | 
				
			||||||
        appBar: AppBar(
 | 
					        appBar: AppBar(
 | 
				
			||||||
          backgroundColor: Theme.of(context).colorScheme.inversePrimary,
 | 
					          backgroundColor: Theme.of(context).colorScheme.inversePrimary,
 | 
				
			||||||
          title: Text(widget.title),
 | 
					          title: Text(widget.title),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        body: Center(
 | 
					        body: GestureDetector(
 | 
				
			||||||
          child: Column(
 | 
					          // This ensures tapping anywhere gives focus back to our listener
 | 
				
			||||||
            mainAxisAlignment: MainAxisAlignment.center,
 | 
					          onTap: () => _focusNode.requestFocus(),
 | 
				
			||||||
            children: [
 | 
					          behavior: HitTestBehavior.translucent,
 | 
				
			||||||
              Text(
 | 
					          child: Center(
 | 
				
			||||||
                'Time Elapsed:',
 | 
					            child: Column(
 | 
				
			||||||
                style: Theme.of(context).textTheme.headlineSmall,
 | 
					              mainAxisAlignment: MainAxisAlignment.center,
 | 
				
			||||||
              ),
 | 
					              children: [
 | 
				
			||||||
              const SizedBox(height: 10),
 | 
					                const SizedBox(height: 10),
 | 
				
			||||||
              Container(
 | 
					                // Use a fixed height container to prevent expanding and force horizontal scrolling if needed
 | 
				
			||||||
                width: 300, // Fixed width container to prevent "flashing"
 | 
					                SizedBox(
 | 
				
			||||||
                alignment: Alignment.center,
 | 
					                  height: 50,
 | 
				
			||||||
                child: Text(
 | 
					                  child: SingleChildScrollView(
 | 
				
			||||||
                  _formatDuration(_elapsedTime),
 | 
					                    scrollDirection: Axis.horizontal,
 | 
				
			||||||
                  style: Theme.of(context).textTheme.headlineMedium?.copyWith(
 | 
					                    child: Text(
 | 
				
			||||||
                    fontFamily: 'monospace', // Use monospaced font for fixed-width characters
 | 
					                      _formatDuration(_elapsedTime),
 | 
				
			||||||
 | 
					                      style: Theme.of(context).textTheme.headlineMedium?.copyWith(
 | 
				
			||||||
 | 
					                        fontFamily: 'monospace', // Use monospaced font for fixed-width characters
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                      softWrap: false, // Prevent text wrapping
 | 
				
			||||||
 | 
					                      overflow: TextOverflow.visible, // Allow text to extend beyond bounds
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					                const SizedBox(height: 30),
 | 
				
			||||||
              const SizedBox(height: 30),
 | 
					                Row(
 | 
				
			||||||
              Row(
 | 
					                  mainAxisAlignment: MainAxisAlignment.center,
 | 
				
			||||||
                mainAxisAlignment: MainAxisAlignment.center,
 | 
					                  children: [
 | 
				
			||||||
                children: [
 | 
					                    ElevatedButton(
 | 
				
			||||||
                  ElevatedButton(
 | 
					                      onPressed: _isRunning ? _pauseTimer : _startTimer,
 | 
				
			||||||
                    onPressed: _isRunning ? _pauseTimer : _startTimer,
 | 
					                      child: Text(_isRunning ? 'Pause' : 'Start'),
 | 
				
			||||||
                    child: Text(_isRunning ? 'Pause' : 'Start'),
 | 
					                    ),
 | 
				
			||||||
 | 
					                    const SizedBox(width: 20),
 | 
				
			||||||
 | 
					                    ElevatedButton(
 | 
				
			||||||
 | 
					                      onPressed: _stopTimer,
 | 
				
			||||||
 | 
					                      child: const Text('Stop'),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    const SizedBox(width: 20),
 | 
				
			||||||
 | 
					                    ElevatedButton(
 | 
				
			||||||
 | 
					                      onPressed: _recordLap,
 | 
				
			||||||
 | 
					                      child: const Text('Lap'),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ],
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                const SizedBox(height: 20),
 | 
				
			||||||
 | 
					                if (_laps.isNotEmpty) ...[
 | 
				
			||||||
 | 
					                  Text(
 | 
				
			||||||
 | 
					                    'Laps:',
 | 
				
			||||||
 | 
					                    style: Theme.of(context).textTheme.titleLarge,
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  const SizedBox(width: 20),
 | 
					                  const SizedBox(height: 10),
 | 
				
			||||||
                  ElevatedButton(
 | 
					                  Expanded(
 | 
				
			||||||
                    onPressed: _stopTimer,
 | 
					                    child: ListView.builder(
 | 
				
			||||||
                    child: const Text('Stop'),
 | 
					                      itemCount: _laps.length,
 | 
				
			||||||
                  ),
 | 
					                      itemBuilder: (context, index) {
 | 
				
			||||||
                  const SizedBox(width: 20),
 | 
					                        return ListTile(
 | 
				
			||||||
                  ElevatedButton(
 | 
					                          title: Text('Lap ${(index + 1).toString().padLeft(3, '0')}: ${_laps[index]}'),
 | 
				
			||||||
                    onPressed: _recordLap,
 | 
					                        );
 | 
				
			||||||
                    child: const Text('Lap'),
 | 
					                      },
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                ],
 | 
					                ],
 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              const SizedBox(height: 10),
 | 
					 | 
				
			||||||
              Text(
 | 
					 | 
				
			||||||
                'Keyboard: SPACE for Start/Lap, ENTER for Pause/Stop',
 | 
					 | 
				
			||||||
                style: Theme.of(context).textTheme.bodySmall,
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
              const SizedBox(height: 20),
 | 
					 | 
				
			||||||
              if (_laps.isNotEmpty) ...[
 | 
					 | 
				
			||||||
                Text(
 | 
					 | 
				
			||||||
                  'Laps:',
 | 
					 | 
				
			||||||
                  style: Theme.of(context).textTheme.titleLarge,
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
                const SizedBox(height: 10),
 | 
					 | 
				
			||||||
                Expanded(
 | 
					 | 
				
			||||||
                  child: ListView.builder(
 | 
					 | 
				
			||||||
                    itemCount: _laps.length,
 | 
					 | 
				
			||||||
                    itemBuilder: (context, index) {
 | 
					 | 
				
			||||||
                      return ListTile(
 | 
					 | 
				
			||||||
                        title: Text('Lap ${index + 1}: ${_laps[index]}'),
 | 
					 | 
				
			||||||
                      );
 | 
					 | 
				
			||||||
                    },
 | 
					 | 
				
			||||||
                  ),
 | 
					 | 
				
			||||||
                ),
 | 
					 | 
				
			||||||
              ],
 | 
					              ],
 | 
				
			||||||
            ],
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,8 @@
 | 
				
			|||||||
import FlutterMacOS
 | 
					import FlutterMacOS
 | 
				
			||||||
import Foundation
 | 
					import Foundation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import path_provider_foundation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
 | 
					func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
 | 
				
			||||||
 | 
					  PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										82
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										82
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -57,6 +57,14 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.3.2"
 | 
					    version: "1.3.2"
 | 
				
			||||||
 | 
					  ffi:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: ffi
 | 
				
			||||||
 | 
					      sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "2.1.4"
 | 
				
			||||||
  flutter:
 | 
					  flutter:
 | 
				
			||||||
    dependency: "direct main"
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description: flutter
 | 
					    description: flutter
 | 
				
			||||||
@@ -139,6 +147,70 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.9.1"
 | 
					    version: "1.9.1"
 | 
				
			||||||
 | 
					  path_provider:
 | 
				
			||||||
 | 
					    dependency: "direct main"
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: path_provider
 | 
				
			||||||
 | 
					      sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "2.1.5"
 | 
				
			||||||
 | 
					  path_provider_android:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: path_provider_android
 | 
				
			||||||
 | 
					      sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "2.2.16"
 | 
				
			||||||
 | 
					  path_provider_foundation:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: path_provider_foundation
 | 
				
			||||||
 | 
					      sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "2.4.1"
 | 
				
			||||||
 | 
					  path_provider_linux:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: path_provider_linux
 | 
				
			||||||
 | 
					      sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "2.2.1"
 | 
				
			||||||
 | 
					  path_provider_platform_interface:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: path_provider_platform_interface
 | 
				
			||||||
 | 
					      sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "2.1.2"
 | 
				
			||||||
 | 
					  path_provider_windows:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: path_provider_windows
 | 
				
			||||||
 | 
					      sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "2.3.0"
 | 
				
			||||||
 | 
					  platform:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: platform
 | 
				
			||||||
 | 
					      sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "3.1.6"
 | 
				
			||||||
 | 
					  plugin_platform_interface:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: plugin_platform_interface
 | 
				
			||||||
 | 
					      sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "2.1.8"
 | 
				
			||||||
  sky_engine:
 | 
					  sky_engine:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: transitive
 | 
				
			||||||
    description: flutter
 | 
					    description: flutter
 | 
				
			||||||
@@ -208,6 +280,14 @@ packages:
 | 
				
			|||||||
      url: "https://pub.dev"
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "14.3.1"
 | 
					    version: "14.3.1"
 | 
				
			||||||
 | 
					  xdg_directories:
 | 
				
			||||||
 | 
					    dependency: transitive
 | 
				
			||||||
 | 
					    description:
 | 
				
			||||||
 | 
					      name: xdg_directories
 | 
				
			||||||
 | 
					      sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
 | 
				
			||||||
 | 
					      url: "https://pub.dev"
 | 
				
			||||||
 | 
					    source: hosted
 | 
				
			||||||
 | 
					    version: "1.1.0"
 | 
				
			||||||
sdks:
 | 
					sdks:
 | 
				
			||||||
  dart: ">=3.7.0 <4.0.0"
 | 
					  dart: ">=3.7.0 <4.0.0"
 | 
				
			||||||
  flutter: ">=3.18.0-18.0.pre.54"
 | 
					  flutter: ">=3.27.0"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -34,6 +34,7 @@ dependencies:
 | 
				
			|||||||
  # The following adds the Cupertino Icons font to your application.
 | 
					  # The following adds the Cupertino Icons font to your application.
 | 
				
			||||||
  # Use with the CupertinoIcons class for iOS style icons.
 | 
					  # Use with the CupertinoIcons class for iOS style icons.
 | 
				
			||||||
  cupertino_icons: ^1.0.8
 | 
					  cupertino_icons: ^1.0.8
 | 
				
			||||||
 | 
					  path_provider: ^2.1.2  # For accessing local file system
 | 
				
			||||||
 | 
					
 | 
				
			||||||
dev_dependencies:
 | 
					dev_dependencies:
 | 
				
			||||||
  flutter_test:
 | 
					  flutter_test:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,8 +23,8 @@ echo "Creating a release..."
 | 
				
			|||||||
TOKEN="$GITEA_API_KEY"
 | 
					TOKEN="$GITEA_API_KEY"
 | 
				
			||||||
GITEA="https://git.site.quack-lab.dev"
 | 
					GITEA="https://git.site.quack-lab.dev"
 | 
				
			||||||
REPO="dave/flutter-simple-timer"
 | 
					REPO="dave/flutter-simple-timer"
 | 
				
			||||||
ZIP="calorie-counter-${TAG}.zip"
 | 
					ZIP="simple-timer-${TAG}.zip"
 | 
				
			||||||
APK="calorie-counter-${TAG}.apk"
 | 
					APK="simple-timer-${TAG}.apk"
 | 
				
			||||||
# Create a release
 | 
					# Create a release
 | 
				
			||||||
RELEASE_RESPONSE=$(curl -s -X POST \
 | 
					RELEASE_RESPONSE=$(curl -s -X POST \
 | 
				
			||||||
  -H "Authorization: token $TOKEN" \
 | 
					  -H "Authorization: token $TOKEN" \
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,10 +25,10 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
 | 
				
			|||||||
  project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
 | 
					  project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  FlutterWindow window(project);
 | 
					  FlutterWindow window(project);
 | 
				
			||||||
  Win32Window::Size size(1920, 1080);
 | 
					  Win32Window::Size size(1200, 800);
 | 
				
			||||||
  Win32Window::Point origin((GetSystemMetrics(SM_CXSCREEN) - size.width) / 2,
 | 
					  Win32Window::Point origin((GetSystemMetrics(SM_CXSCREEN) - size.width) / 2,
 | 
				
			||||||
                            (GetSystemMetrics(SM_CYSCREEN) - size.height) / 2);
 | 
					                            (GetSystemMetrics(SM_CYSCREEN) - size.height) / 2);
 | 
				
			||||||
  if (!window.Create(L"simple_timer", origin, size)) {
 | 
					  if (!window.Create(L"Just a timer", origin, size)) {
 | 
				
			||||||
    return EXIT_FAILURE;
 | 
					    return EXIT_FAILURE;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  window.SetQuitOnClose(true);
 | 
					  window.SetQuitOnClose(true);
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user