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 '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';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const MyApp());
|
runApp(const MyApp());
|
||||||
@@ -28,6 +30,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 +57,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 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() {
|
void _startTimer() {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
@@ -44,11 +213,14 @@ 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: 16), (timer) {
|
_timer = Timer.periodic(const Duration(milliseconds: 16), (timer) {
|
||||||
@@ -65,6 +237,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,51 +245,64 @@ 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 as ISO 8601 duration without unnecessary zeros
|
||||||
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
|
// Round milliseconds to tenths of a second
|
||||||
// This helps avoid floating point inconsistencies (0.201, 0.301, etc.)
|
final tenthsOfSecond = (milliseconds / 100).round();
|
||||||
final milliseconds = ((duration.inMilliseconds % 1000) / 100).round() * 100;
|
|
||||||
final formattedMs = (milliseconds ~/ 100).toString();
|
|
||||||
|
|
||||||
// Start with PT prefix
|
// Build ISO 8601 format without unnecessary zeros
|
||||||
String result = 'PT';
|
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) {
|
if (hours > 0) {
|
||||||
result += '${hours}H';
|
buffer.write('${hours}H');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (minutes > 0 || hours > 0) {
|
// Add minutes only if hours exist or minutes non-zero
|
||||||
result += '${minutes}M';
|
if (minutes > 0) {
|
||||||
|
buffer.write('${minutes}M');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always include seconds with a single decimal place (tenth of a second)
|
// Always include seconds
|
||||||
result += '${seconds}.${formattedMs}S';
|
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) {
|
void _handleKeyEvent(RawKeyEvent event) {
|
||||||
@@ -139,12 +325,6 @@ class _MyHomePageState extends State<MyHomePage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
|
||||||
void initState() {
|
|
||||||
super.initState();
|
|
||||||
_focusNode.requestFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
|
@@ -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:
|
||||||
|
Reference in New Issue
Block a user