Add saving to and reading from a file

To make sure when our timer runs it runs
This commit is contained in:
2025-03-14 23:55:42 +01:00
parent 75454c6edc
commit e62c74b72c
4 changed files with 285 additions and 22 deletions

View File

@@ -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();

View File

@@ -5,6 +5,8 @@
import FlutterMacOS
import Foundation
import path_provider_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
}

View File

@@ -57,6 +57,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.3.2"
ffi:
dependency: transitive
description:
name: ffi
sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
flutter:
dependency: "direct main"
description: flutter
@@ -139,6 +147,70 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description: flutter
@@ -208,6 +280,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dart: ">=3.7.0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54"
flutter: ">=3.27.0"

View File

@@ -34,6 +34,7 @@ dependencies:
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
path_provider: ^2.1.2 # For accessing local file system
dev_dependencies:
flutter_test: