Add saving to and reading from a file
To make sure when our timer runs it runs
This commit is contained in:
222
lib/main.dart
222
lib/main.dart
@@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'dart:io';
|
||||
import 'dart:convert';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
@@ -28,6 +30,25 @@ class MyHomePage extends StatefulWidget {
|
||||
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> {
|
||||
Timer? _timer;
|
||||
DateTime? _startTime;
|
||||
@@ -36,6 +57,154 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
bool _isRunning = false;
|
||||
List<String> _laps = [];
|
||||
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 for simplicity
|
||||
print('Failed to save timer data: $e');
|
||||
}
|
||||
}
|
||||
|
||||
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 for simplicity
|
||||
print('Failed to load timer data: $e');
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
final now = DateTime.now();
|
||||
@@ -44,11 +213,14 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
final pausedDuration = now.difference(_pauseTime!);
|
||||
_startTime = _startTime?.add(pausedDuration);
|
||||
_pauseTime = null;
|
||||
_addEvent('start');
|
||||
} else {
|
||||
// Starting fresh
|
||||
_startTime = now;
|
||||
_elapsedTime = Duration.zero;
|
||||
_laps = [];
|
||||
_events = []; // Clear previous events
|
||||
_addEvent('start');
|
||||
}
|
||||
|
||||
_timer = Timer.periodic(const Duration(milliseconds: 16), (timer) {
|
||||
@@ -65,6 +237,7 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
void _pauseTimer() {
|
||||
_timer?.cancel();
|
||||
_pauseTime = DateTime.now();
|
||||
_addEvent('pause');
|
||||
setState(() {
|
||||
_isRunning = false;
|
||||
});
|
||||
@@ -72,51 +245,64 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
|
||||
void _stopTimer() {
|
||||
_timer?.cancel();
|
||||
_addEvent('stop');
|
||||
setState(() {
|
||||
_isRunning = false;
|
||||
_startTime = null;
|
||||
_pauseTime = null;
|
||||
_elapsedTime = Duration.zero;
|
||||
});
|
||||
_saveTimerData();
|
||||
}
|
||||
|
||||
void _recordLap() {
|
||||
if (_isRunning) {
|
||||
final now = DateTime.now();
|
||||
_addEvent('lap');
|
||||
setState(() {
|
||||
// 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) {
|
||||
// Format the duration as ISO 8601 duration format without 0 values
|
||||
// Format as ISO 8601 duration without unnecessary zeros
|
||||
final hours = duration.inHours;
|
||||
final minutes = duration.inMinutes.remainder(60);
|
||||
final seconds = duration.inSeconds.remainder(60);
|
||||
final milliseconds = duration.inMilliseconds.remainder(1000);
|
||||
|
||||
// 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();
|
||||
// Round milliseconds to tenths of a second
|
||||
final tenthsOfSecond = (milliseconds / 100).round();
|
||||
|
||||
// Start with PT prefix
|
||||
String result = 'PT';
|
||||
// Build ISO 8601 format without unnecessary zeros
|
||||
StringBuffer buffer = StringBuffer();
|
||||
|
||||
// Only add parts that are non-zero (except if everything is zero, then show 0S)
|
||||
// Add hours only if non-zero
|
||||
if (hours > 0) {
|
||||
result += '${hours}H';
|
||||
buffer.write('${hours}H');
|
||||
}
|
||||
|
||||
if (minutes > 0 || hours > 0) {
|
||||
result += '${minutes}M';
|
||||
// Add minutes only if hours exist or minutes non-zero
|
||||
if (minutes > 0) {
|
||||
buffer.write('${minutes}M');
|
||||
}
|
||||
|
||||
// Always include seconds with a single decimal place (tenth of a second)
|
||||
result += '${seconds}.${formattedMs}S';
|
||||
// Always include seconds
|
||||
buffer.write('${seconds}');
|
||||
if (tenthsOfSecond > 0) {
|
||||
buffer.write('.${tenthsOfSecond}');
|
||||
}
|
||||
buffer.write('S');
|
||||
|
||||
return result;
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
// For lap recording
|
||||
String _formatDurationForLap(Duration duration) {
|
||||
return _formatDuration(duration);
|
||||
}
|
||||
|
||||
void _handleKeyEvent(RawKeyEvent event) {
|
||||
@@ -139,12 +325,6 @@ class _MyHomePageState extends State<MyHomePage> {
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
|
Reference in New Issue
Block a user