Files
flutter-simple-timer/lib/main.dart

235 lines
6.5 KiB
Dart

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<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
Timer? _timer;
DateTime? _startTime;
DateTime? _pauseTime;
Duration _elapsedTime = Duration.zero;
bool _isRunning = false;
List<String> _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: 16), (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]}'),
);
},
),
),
],
],
),
),
),
);
}
}