import 'package:flutter/material.dart'; import 'dart:async'; import 'package:flutter/services.dart'; void main() { runApp(const MyApp()); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Simple Timer', theme: ThemeData( colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), ), home: const MyHomePage(title: 'Simple Timer'), ); } } class MyHomePage extends StatefulWidget { const MyHomePage({super.key, required this.title}); final String title; @override State createState() => _MyHomePageState(); } class _MyHomePageState extends State { Timer? _timer; DateTime? _startTime; DateTime? _pauseTime; Duration _elapsedTime = Duration.zero; bool _isRunning = false; List _laps = []; final FocusNode _focusNode = FocusNode(); void _startTimer() { final now = DateTime.now(); if (_pauseTime != null) { // Resuming from pause final pausedDuration = now.difference(_pauseTime!); _startTime = _startTime?.add(pausedDuration); _pauseTime = null; } else { // Starting fresh _startTime = now; _elapsedTime = Duration.zero; _laps = []; } _timer = Timer.periodic(const Duration(milliseconds: 100), (timer) { setState(() { _elapsedTime = DateTime.now().difference(_startTime!); }); }); setState(() { _isRunning = true; }); } void _pauseTimer() { _timer?.cancel(); _pauseTime = DateTime.now(); setState(() { _isRunning = false; }); } void _stopTimer() { _timer?.cancel(); setState(() { _isRunning = false; _startTime = null; _pauseTime = null; _elapsedTime = Duration.zero; }); } void _recordLap() { if (_isRunning) { final now = DateTime.now(); setState(() { // Store both timestamp and formatted duration for this lap _laps.add('${now.toIso8601String()} - ${_formatDuration(_elapsedTime)}'); }); } } String _formatDuration(Duration duration) { // Format the duration as ISO 8601 duration format without 0 values final hours = duration.inHours; final minutes = duration.inMinutes.remainder(60); final seconds = duration.inSeconds.remainder(60); // Round milliseconds to nearest 100ms for more consistent display // This helps avoid floating point inconsistencies (0.201, 0.301, etc.) final milliseconds = ((duration.inMilliseconds % 1000) / 100).round() * 100; final formattedMs = (milliseconds ~/ 100).toString(); // Start with PT prefix String result = 'PT'; // Only add parts that are non-zero (except if everything is zero, then show 0S) if (hours > 0) { result += '${hours}H'; } if (minutes > 0 || hours > 0) { result += '${minutes}M'; } // Always include seconds with a single decimal place (tenth of a second) result += '${seconds}.${formattedMs}S'; return result; } void _handleKeyEvent(RawKeyEvent event) { if (event is RawKeyDownEvent) { if (event.logicalKey == LogicalKeyboardKey.space) { // Space: Start if paused, Lap if running if (_isRunning) { _recordLap(); } else { _startTimer(); } } else if (event.logicalKey == LogicalKeyboardKey.enter) { // Enter: Stop if paused, Pause if running if (_isRunning) { _pauseTimer(); } else { _stopTimer(); } } } } @override void initState() { super.initState(); _focusNode.requestFocus(); } @override void dispose() { _timer?.cancel(); _focusNode.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return RawKeyboardListener( focusNode: _focusNode, onKey: _handleKeyEvent, autofocus: true, child: Scaffold( appBar: AppBar( backgroundColor: Theme.of(context).colorScheme.inversePrimary, title: Text(widget.title), ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Text( 'Time Elapsed:', style: Theme.of(context).textTheme.headlineSmall, ), const SizedBox(height: 10), Container( width: 300, // Fixed width container to prevent "flashing" alignment: Alignment.center, child: Text( _formatDuration(_elapsedTime), style: Theme.of(context).textTheme.headlineMedium?.copyWith( fontFamily: 'monospace', // Use monospaced font for fixed-width characters ), ), ), const SizedBox(height: 30), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ ElevatedButton( onPressed: _isRunning ? _pauseTimer : _startTimer, 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: 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]}'), ); }, ), ), ], ], ), ), ), ); } }