Compare commits
	
		
			12 Commits
		
	
	
		
			b7a7eee777
			...
			master
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| c5973f9c92 | |||
| eb870c4d49 | |||
| 897069c08b | |||
| a1b577ef4c | |||
| 64895c12b9 | |||
| 7601cf3a22 | |||
| 8c7150a1ed | |||
| a29f090895 | |||
| 8640ad0499 | |||
| e62c74b72c | |||
| 75454c6edc | |||
| 997dd5b6b3 | 
							
								
								
									
										32
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								README.md
									
									
									
									
									
								
							@@ -1,16 +1,30 @@
 | 
			
		||||
# simple_timer
 | 
			
		||||
# Just a Timer
 | 
			
		||||
 | 
			
		||||
A new Flutter project.
 | 
			
		||||
It's just a timer. You can start it, pause it, stop it, and start it again. It can also do laps.
 | 
			
		||||
 | 
			
		||||
## Features
 | 
			
		||||
 | 
			
		||||
- A timer
 | 
			
		||||
- Record lap times
 | 
			
		||||
- Persistent storage of timer data
 | 
			
		||||
- Keyboard shortcuts for quick control
 | 
			
		||||
 | 
			
		||||
## Screenshots
 | 
			
		||||
 | 
			
		||||

 | 
			
		||||
 | 
			
		||||
## Getting Started
 | 
			
		||||
 | 
			
		||||
This project is a starting point for a Flutter application.
 | 
			
		||||
1. Make sure you have Flutter installed on your machine
 | 
			
		||||
2. Clone this repository
 | 
			
		||||
3. Run `flutter pub get` to install dependencies
 | 
			
		||||
4. Launch the app with `flutter run`
 | 
			
		||||
 | 
			
		||||
A few resources to get you started if this is your first Flutter project:
 | 
			
		||||
## Platform Support
 | 
			
		||||
 | 
			
		||||
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
 | 
			
		||||
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
 | 
			
		||||
- Android
 | 
			
		||||
- Windows
 | 
			
		||||
 | 
			
		||||
For help getting started with Flutter development, view the
 | 
			
		||||
[online documentation](https://docs.flutter.dev/), which offers tutorials,
 | 
			
		||||
samples, guidance on mobile development, and a full API reference.
 | 
			
		||||
## License
 | 
			
		||||
 | 
			
		||||
This project is open source under the MIT License.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										377
									
								
								lib/main.dart
									
									
									
									
									
								
							
							
						
						
									
										377
									
								
								lib/main.dart
									
									
									
									
									
								
							@@ -1,6 +1,9 @@
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
import 'package:flutter/services.dart';
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
import 'dart:developer' as developer;
 | 
			
		||||
 | 
			
		||||
void main() {
 | 
			
		||||
  runApp(const MyApp());
 | 
			
		||||
@@ -12,11 +15,20 @@ class MyApp extends StatelessWidget {
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return MaterialApp(
 | 
			
		||||
      title: 'Simple Timer',
 | 
			
		||||
      theme: ThemeData(
 | 
			
		||||
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
 | 
			
		||||
      title: 'Just a timer',
 | 
			
		||||
      darkTheme: ThemeData(
 | 
			
		||||
        brightness: Brightness.dark,
 | 
			
		||||
        useMaterial3: true,
 | 
			
		||||
        scaffoldBackgroundColor: const Color(0xFF111827),
 | 
			
		||||
        colorSchemeSeed: const Color(0xFF111827),
 | 
			
		||||
        highlightColor: const Color(0xff1f2937),
 | 
			
		||||
        textTheme: TextTheme(
 | 
			
		||||
          bodyLarge: TextStyle(fontSize: 22),
 | 
			
		||||
          bodyMedium: TextStyle(fontSize: 20),
 | 
			
		||||
          bodySmall: TextStyle(fontSize: 17),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
      home: const MyHomePage(title: 'Simple Timer'),
 | 
			
		||||
      home: const MyHomePage(title: 'Just a timer'),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -28,6 +40,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 +67,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 using proper logging
 | 
			
		||||
      developer.log('Failed to save timer data: $e', name: 'simple_timer');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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 using proper logging
 | 
			
		||||
      developer.log('Failed to load timer data: $e', name: 'simple_timer');
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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,14 +223,17 @@ 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: 100), (timer) {
 | 
			
		||||
    _timer = Timer.periodic(const Duration(milliseconds: 16), (timer) {
 | 
			
		||||
      setState(() {
 | 
			
		||||
        _elapsedTime = DateTime.now().difference(_startTime!);
 | 
			
		||||
      });
 | 
			
		||||
@@ -65,6 +247,7 @@ class _MyHomePageState extends State<MyHomePage> {
 | 
			
		||||
  void _pauseTimer() {
 | 
			
		||||
    _timer?.cancel();
 | 
			
		||||
    _pauseTime = DateTime.now();
 | 
			
		||||
    _addEvent('pause');
 | 
			
		||||
    setState(() {
 | 
			
		||||
      _isRunning = false;
 | 
			
		||||
    });
 | 
			
		||||
@@ -72,64 +255,72 @@ 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 with fixed width on a single line, hiding zero hours/minutes
 | 
			
		||||
    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();
 | 
			
		||||
    // Use padLeft to ensure consistent width with leading zeros
 | 
			
		||||
    final secondsStr = seconds.toString().padLeft(2, '0');
 | 
			
		||||
    final msStr = milliseconds.toString().padLeft(3, '0');
 | 
			
		||||
    
 | 
			
		||||
    // Start with PT prefix
 | 
			
		||||
    String result = 'PT';
 | 
			
		||||
    
 | 
			
		||||
    // Only add parts that are non-zero (except if everything is zero, then show 0S)
 | 
			
		||||
    // Build output - only show non-zero hours and minutes
 | 
			
		||||
    StringBuffer result = StringBuffer();
 | 
			
		||||
    if (hours > 0) {
 | 
			
		||||
      result += '${hours}H';
 | 
			
		||||
      result.write('${hours.toString().padLeft(2, '0')}H ');
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    if (minutes > 0 || hours > 0) {
 | 
			
		||||
      result += '${minutes}M';
 | 
			
		||||
    if (hours > 0 || minutes > 0) {
 | 
			
		||||
      result.write('${minutes.toString().padLeft(2, '0')}M ');
 | 
			
		||||
    }
 | 
			
		||||
    // Always show seconds and milliseconds
 | 
			
		||||
    result.write('${secondsStr}S ${msStr}MS');
 | 
			
		||||
    
 | 
			
		||||
    // Always include seconds with a single decimal place (tenth of a second)
 | 
			
		||||
    result += '${seconds}.${formattedMs}S';
 | 
			
		||||
    
 | 
			
		||||
    return result;
 | 
			
		||||
    return result.toString();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void _handleKeyEvent(RawKeyEvent event) {
 | 
			
		||||
    if (event is RawKeyDownEvent) {
 | 
			
		||||
  // For lap recording - use the same format
 | 
			
		||||
  String _formatDurationForLap(Duration duration) {
 | 
			
		||||
    return _formatDuration(duration);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Update keyboard handler method to use modern KeyEvent API
 | 
			
		||||
  void _handleKeyEvent(KeyEvent event) {
 | 
			
		||||
    // Only process key down events to avoid duplicate triggers
 | 
			
		||||
    if (event is KeyDownEvent) {
 | 
			
		||||
      // Handle Space key
 | 
			
		||||
      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
 | 
			
		||||
      }
 | 
			
		||||
      // Handle Enter key
 | 
			
		||||
      else if (event.logicalKey == LogicalKeyboardKey.enter) {
 | 
			
		||||
        if (_isRunning) {
 | 
			
		||||
          _pauseTimer();
 | 
			
		||||
        } else {
 | 
			
		||||
@@ -139,12 +330,6 @@ class _MyHomePageState extends State<MyHomePage> {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void initState() {
 | 
			
		||||
    super.initState();
 | 
			
		||||
    _focusNode.requestFocus();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void dispose() {
 | 
			
		||||
    _timer?.cancel();
 | 
			
		||||
@@ -154,78 +339,84 @@ class _MyHomePageState extends State<MyHomePage> {
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Widget build(BuildContext context) {
 | 
			
		||||
    return RawKeyboardListener(
 | 
			
		||||
    // Use Focus directly to capture key events
 | 
			
		||||
    return Focus(
 | 
			
		||||
      focusNode: _focusNode,
 | 
			
		||||
      onKey: _handleKeyEvent,
 | 
			
		||||
      autofocus: true,
 | 
			
		||||
      onKeyEvent: (FocusNode node, KeyEvent event) {
 | 
			
		||||
        _handleKeyEvent(event);
 | 
			
		||||
        // Always return KeyEventResult.handled to indicate we've processed the event
 | 
			
		||||
        return KeyEventResult.handled;
 | 
			
		||||
      },
 | 
			
		||||
      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
 | 
			
		||||
        body: GestureDetector(
 | 
			
		||||
          // This ensures tapping anywhere gives focus back to our listener
 | 
			
		||||
          onTap: () => _focusNode.requestFocus(),
 | 
			
		||||
          behavior: HitTestBehavior.translucent,
 | 
			
		||||
          child: Center(
 | 
			
		||||
            child: Column(
 | 
			
		||||
              mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
              children: [
 | 
			
		||||
                const SizedBox(height: 10),
 | 
			
		||||
                // Use a fixed height container to prevent expanding and force horizontal scrolling if needed
 | 
			
		||||
                SizedBox(
 | 
			
		||||
                  height: 50,
 | 
			
		||||
                  child: SingleChildScrollView(
 | 
			
		||||
                    scrollDirection: Axis.horizontal,
 | 
			
		||||
                    child: Text(
 | 
			
		||||
                      _formatDuration(_elapsedTime),
 | 
			
		||||
                      style: Theme.of(context).textTheme.headlineMedium?.copyWith(
 | 
			
		||||
                        fontFamily: 'monospace', // Use monospaced font for fixed-width characters
 | 
			
		||||
                      ),
 | 
			
		||||
                      softWrap: false, // Prevent text wrapping
 | 
			
		||||
                      overflow: TextOverflow.visible, // Allow text to extend beyond bounds
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ),
 | 
			
		||||
              const SizedBox(height: 30),
 | 
			
		||||
              Row(
 | 
			
		||||
                mainAxisAlignment: MainAxisAlignment.center,
 | 
			
		||||
                children: [
 | 
			
		||||
                  ElevatedButton(
 | 
			
		||||
                    onPressed: _isRunning ? _pauseTimer : _startTimer,
 | 
			
		||||
                    child: Text(_isRunning ? 'Pause' : 'Start'),
 | 
			
		||||
                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: 20),
 | 
			
		||||
                if (_laps.isNotEmpty) ...[
 | 
			
		||||
                  Text(
 | 
			
		||||
                    'Laps:',
 | 
			
		||||
                    style: Theme.of(context).textTheme.titleLarge,
 | 
			
		||||
                  ),
 | 
			
		||||
                  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),
 | 
			
		||||
                  Expanded(
 | 
			
		||||
                    child: ListView.builder(
 | 
			
		||||
                      itemCount: _laps.length,
 | 
			
		||||
                      itemBuilder: (context, index) {
 | 
			
		||||
                        return ListTile(
 | 
			
		||||
                          title: Text('Lap ${(index + 1).toString().padLeft(3, '0')}: ${_laps[index]}'),
 | 
			
		||||
                        );
 | 
			
		||||
                      },
 | 
			
		||||
                    ),
 | 
			
		||||
                  ),
 | 
			
		||||
                ],
 | 
			
		||||
              ),
 | 
			
		||||
              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]}'),
 | 
			
		||||
                      );
 | 
			
		||||
                    },
 | 
			
		||||
                  ),
 | 
			
		||||
                ),
 | 
			
		||||
              ],
 | 
			
		||||
            ],
 | 
			
		||||
            ),
 | 
			
		||||
          ),
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,8 @@
 | 
			
		||||
import FlutterMacOS
 | 
			
		||||
import Foundation
 | 
			
		||||
 | 
			
		||||
import path_provider_foundation
 | 
			
		||||
 | 
			
		||||
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"
 | 
			
		||||
    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"
 | 
			
		||||
 
 | 
			
		||||
@@ -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:
 | 
			
		||||
 
 | 
			
		||||
@@ -23,8 +23,8 @@ echo "Creating a release..."
 | 
			
		||||
TOKEN="$GITEA_API_KEY"
 | 
			
		||||
GITEA="https://git.site.quack-lab.dev"
 | 
			
		||||
REPO="dave/flutter-simple-timer"
 | 
			
		||||
ZIP="calorie-counter-${TAG}.zip"
 | 
			
		||||
APK="calorie-counter-${TAG}.apk"
 | 
			
		||||
ZIP="simple-timer-${TAG}.zip"
 | 
			
		||||
APK="simple-timer-${TAG}.apk"
 | 
			
		||||
# Create a release
 | 
			
		||||
RELEASE_RESPONSE=$(curl -s -X POST \
 | 
			
		||||
  -H "Authorization: token $TOKEN" \
 | 
			
		||||
 
 | 
			
		||||
@@ -25,10 +25,10 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
 | 
			
		||||
  project.set_dart_entrypoint_arguments(std::move(command_line_arguments));
 | 
			
		||||
 | 
			
		||||
  FlutterWindow window(project);
 | 
			
		||||
  Win32Window::Size size(1920, 1080);
 | 
			
		||||
  Win32Window::Size size(1200, 800);
 | 
			
		||||
  Win32Window::Point origin((GetSystemMetrics(SM_CXSCREEN) - size.width) / 2,
 | 
			
		||||
                            (GetSystemMetrics(SM_CYSCREEN) - size.height) / 2);
 | 
			
		||||
  if (!window.Create(L"simple_timer", origin, size)) {
 | 
			
		||||
  if (!window.Create(L"Just a timer", origin, size)) {
 | 
			
		||||
    return EXIT_FAILURE;
 | 
			
		||||
  }
 | 
			
		||||
  window.SetQuitOnClose(true);
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user