screen-locker/stronglift_replacement/workout_app/lib/screens/home_screen.dart
Krzysztof kuhy Rudnicki 23d2173d9f Add comprehensive test suite, backup service, and linting to workout app
- Add sqflite_common_ffi + very_good_analysis; tighten analysis_options
- Add BackupService for JSON export/import of exercise state
- Add full test coverage: models, screens, services, widgets
- Add scripts/check_flutter_coverage.sh to enforce 100% line coverage
- Add docstrings to ExerciseState fields and storage service
- Minor fixes across screens, widgets, and sync/HTTP services

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VuiPt6GPWkxpLbJFrnfy8U
2026-06-28 08:11:43 +02:00

315 lines
9.7 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/// Home screen: auto-resumes an active session, shows done-today status.
library;
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:workout_app/models/exercise.dart';
import 'package:workout_app/screens/history_screen.dart';
import 'package:workout_app/screens/settings_screen.dart';
import 'package:workout_app/screens/workout_screen.dart';
import 'package:workout_app/services/http_server_service.dart';
import 'package:workout_app/services/storage_service.dart';
/// Home screen: auto-resumes active sessions and shows done-today status.
class HomeScreen extends StatefulWidget {
/// Creates a [HomeScreen].
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
late List<Exercise> _exercises;
String _nextType = 'A';
List<String> _serverAddresses = [];
bool _loading = true;
bool _doneToday = false;
Map<String, dynamic>? _savedSession;
/// True after the first load auto-navigated to an in-progress workout,
/// so returning from workout does not auto-navigate again.
bool _hasAutoResumed = false;
@override
void initState() {
super.initState();
unawaited(_load());
}
Future<void> _load() async {
final storage = StorageService.instance;
final nextType = await storage.getNextWorkoutType();
final exercises = await storage.getCurrentExercises(nextType);
final saved = await storage.loadActiveSession();
final addrs = await HttpServerService.instance.localAddresses;
final lastDate = await storage.getLastWorkoutDate();
final today = DateTime.now();
final doneToday =
lastDate != null &&
lastDate.year == today.year &&
lastDate.month == today.month &&
lastDate.day == today.day;
if (mounted) {
setState(() {
_nextType = nextType;
_exercises = exercises;
_serverAddresses = addrs;
_savedSession = saved;
_doneToday = doneToday;
_loading = false;
});
// Auto-resume active session on first load (app launch).
if (saved != null && !_hasAutoResumed) {
_hasAutoResumed = true;
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) unawaited(_openWorkout(resume: true));
});
}
}
}
Future<void> _openWorkout({bool resume = false}) async {
final storage = StorageService.instance;
Map<String, dynamic>? savedState;
var type = _nextType;
var exercises = _exercises;
if (resume && _savedSession != null) {
savedState = _savedSession;
final savedType = savedState!['workoutType'] as String? ?? _nextType;
type = savedType;
exercises = await storage.getCurrentExercises(savedType);
}
if (!mounted) return;
await Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => WorkoutScreen(
workoutType: type,
exercises: exercises,
savedState: savedState,
),
),
);
unawaited(_load());
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey.shade900,
appBar: AppBar(
backgroundColor: Colors.grey.shade800,
title: const Text(
'Workout Tracker',
style: TextStyle(color: Colors.white),
),
actions: [
IconButton(
icon: const Icon(Icons.history, color: Colors.white),
onPressed: () => Navigator.of(context).push(
MaterialPageRoute<void>(builder: (_) => const HistoryScreen()),
),
),
IconButton(
icon: const Icon(Icons.settings, color: Colors.white),
onPressed: () async {
await Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => const SettingsScreen(),
),
);
unawaited(_load());
},
),
],
),
body: _loading
? const Center(child: CircularProgressIndicator())
: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_WorkoutCard(
type: _nextType,
exercises: _exercises,
doneToday: _doneToday,
hasActiveSession: _savedSession != null,
onStart: _openWorkout,
onResume: () => _openWorkout(resume: true),
),
const SizedBox(height: 20),
_ServerAddressTile(addresses: _serverAddresses),
],
),
),
);
}
}
// ── Sub-widgets ──────────────────────────────────────────────────────────────
class _WorkoutCard extends StatelessWidget {
const _WorkoutCard({
required this.type,
required this.exercises,
required this.doneToday,
required this.hasActiveSession,
required this.onStart,
required this.onResume,
});
final String type;
final List<Exercise> exercises;
final bool doneToday;
final bool hasActiveSession;
final VoidCallback onStart;
final VoidCallback onResume;
@override
Widget build(BuildContext context) {
return Card(
color: Colors.grey.shade800,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (doneToday && !hasActiveSession) ...[
const Row(
children: [
Icon(Icons.check_circle, color: Colors.greenAccent, size: 18),
SizedBox(width: 8),
Text(
'Done for today!',
style: TextStyle(
color: Colors.greenAccent,
fontWeight: FontWeight.bold,
fontSize: 15,
),
),
],
),
const SizedBox(height: 6),
Text(
'Next: Workout $type — tomorrow',
style: const TextStyle(
color: Colors.white,
fontSize: 17,
fontWeight: FontWeight.bold,
),
),
] else ...[
Text(
hasActiveSession
? 'Workout $type in progress'
: 'Next: Workout $type',
style: TextStyle(
color: hasActiveSession ? Colors.orangeAccent : Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
],
const SizedBox(height: 10),
...exercises.map(
(e) => Text(
'${e.name} ${e.sets}×${e.reps}×${e.weight}kg',
style: const TextStyle(color: Colors.white70, fontSize: 13),
),
),
const SizedBox(height: 14),
if (hasActiveSession)
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange.shade800,
padding: const EdgeInsets.symmetric(vertical: 14),
),
onPressed: onResume,
child: const Text(
'Resume Workout',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
)
else if (!doneToday)
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.indigo,
padding: const EdgeInsets.symmetric(vertical: 14),
),
onPressed: onStart,
child: Text(
'Start Workout $type',
style: const TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
);
}
}
class _ServerAddressTile extends StatelessWidget {
const _ServerAddressTile({required this.addresses});
final List<String> addresses;
@override
Widget build(BuildContext context) {
final lines = addresses.isEmpty
? ['Server not started']
: addresses.map((ip) => '$ip:$kWorkoutServerPort').toList();
return Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: Colors.grey.shade800,
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'HTTP sync (no ADB needed)',
style: TextStyle(
color: Colors.white54,
fontSize: 11,
letterSpacing: 1.1,
),
),
const SizedBox(height: 4),
...lines.map(
(line) => Text(
line,
style: const TextStyle(
color: Colors.white70,
fontFamily: 'monospace',
fontSize: 13,
),
),
),
],
),
);
}
}