diff --git a/lib/main.dart b/lib/main.dart index 54d836b..0370aea 100644 --- a/lib/main.dart +++ b/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 createState() => _MyHomePageState(); } +class TimerEvent { + final String type; // "start", "pause", "lap", "stop" + final DateTime timestamp; + + TimerEvent(this.type, this.timestamp); + + Map toJson() => { + 'type': type, + 'timestamp': timestamp.toIso8601String(), + }; + + factory TimerEvent.fromJson(Map json) { + return TimerEvent( + json['type'] as String, + DateTime.parse(json['timestamp'] as String), + ); + } +} + class _MyHomePageState extends State { Timer? _timer; DateTime? _startTime; @@ -36,6 +57,154 @@ class _MyHomePageState extends State { bool _isRunning = false; List _laps = []; final FocusNode _focusNode = FocusNode(); + List _events = []; + + @override + void initState() { + super.initState(); + _focusNode.requestFocus(); + _loadTimerData(); + } + + // Simple file operations using app's temporary directory + Future get _timerFile async { + final directory = Directory.systemTemp.path; + return File('$directory/timer_data.json'); + } + + Future _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 _loadTimerData() async { + try { + final file = await _timerFile; + if (await file.exists()) { + final jsonData = await file.readAsString(); + final data = jsonDecode(jsonData) as Map; + + // Load events + _events = (data['events'] as List) + .map((e) => TimerEvent.fromJson(e as Map)) + .toList(); + + // Load laps + _laps = (data['laps'] as List).cast(); + + // 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 { 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 { void _pauseTimer() { _timer?.cancel(); _pauseTime = DateTime.now(); + _addEvent('pause'); setState(() { _isRunning = false; }); @@ -72,51 +245,64 @@ class _MyHomePageState extends State { 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 { } } - @override - void initState() { - super.initState(); - _focusNode.requestFocus(); - } - @override void dispose() { _timer?.cancel(); diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..e777c67 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,8 @@ import FlutterMacOS import Foundation +import path_provider_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index c2c57f7..a240571 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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" diff --git a/pubspec.yaml b/pubspec.yaml index a89fd3d..35f6b63 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: