mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 19:27:15 +01:00
Split home_screen.dart (260) and manual_workout_screen.dart (313)
Both by pattern B, both cut along the same line: whatever does not call setState can live in a part extension, whatever does must stay on the class. home_screen.dart 260 -> 228 home_screen_navigation.dart (42) takes `_openSyncSettings` and `_openWorkout` — the two routes the screen pushes, each ending in the same `unawaited(_load())` reload. `_load` and `_refreshSyncStatus` stay: both setState. manual_workout_screen.dart 313 -> 245 manual_workout_form_parts.dart (83) takes the form's non-interactive building blocks — `_budgetBanner`, `_section`, `_text` — plus the `_int`/`_str` field readers and the `_draft()` assembler they feed. The sport dropdown and RPE slider stay on the class; both setState on change. 402 tests pass, dart analyze clean, coverage still 100%. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y
This commit is contained in:
parent
ac7a7afa08
commit
b6414c7f1c
@ -15,6 +15,7 @@ import 'package:workout_app/ui/theme.dart';
|
||||
import 'package:workout_app/widgets/sync_status_card.dart';
|
||||
|
||||
part 'home_screen_cards.dart';
|
||||
part 'home_screen_navigation.dart';
|
||||
|
||||
/// Home screen: auto-resumes active sessions and shows done-today status.
|
||||
class HomeScreen extends StatefulWidget {
|
||||
@ -104,13 +105,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
}
|
||||
|
||||
/// Opens settings so the user can connect sync, then re-checks on return.
|
||||
Future<void> _openSyncSettings() async {
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(builder: (_) => const SettingsScreen()),
|
||||
);
|
||||
unawaited(_load());
|
||||
}
|
||||
|
||||
/// Runs a sync tick and folds the outcome into the status card.
|
||||
///
|
||||
/// Never throws: [WorkoutSyncService.syncNow] reports failures as a
|
||||
@ -159,32 +153,6 @@ class _HomeScreenState extends State<HomeScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
@ -0,0 +1,42 @@
|
||||
// Navigation off the home screen: settings, and starting/resuming a workout.
|
||||
//
|
||||
// A `part` so the extension keeps reaching the private `_HomeScreenState`
|
||||
// fields. Both methods are setState-free — `setState` is `@protected` and
|
||||
// cannot be called from an extension — and both end by reloading the screen.
|
||||
part of 'home_screen.dart';
|
||||
|
||||
/// The two routes the home screen pushes, and the reload each returns to.
|
||||
extension _HomeScreenNavigation on _HomeScreenState {
|
||||
Future<void> _openSyncSettings() async {
|
||||
await Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(builder: (_) => const SettingsScreen()),
|
||||
);
|
||||
unawaited(_load());
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
// The small, stateless pieces of the manual-workout form.
|
||||
//
|
||||
// A `part` so the extension keeps reaching `_fields` and `context`. All three
|
||||
// are setState-free — `setState` is `@protected` and unreachable from an
|
||||
// extension — so the interactive controls (the sport dropdown and the RPE
|
||||
// slider) stay on the state class.
|
||||
part of 'manual_workout_screen.dart';
|
||||
|
||||
/// The form's non-interactive building blocks.
|
||||
extension _ManualWorkoutFormParts on _ManualWorkoutScreenState {
|
||||
Widget _budgetBanner(ManualBudget budget) => Text(
|
||||
'Manual: ${budget.week}/$kManualWorkoutBudgetPer7Days this week · '
|
||||
'${budget.month}/$kManualWorkoutBudgetPer30Days this month',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontSize: AppTextSize.body,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _section(String title) => Padding(
|
||||
padding: const EdgeInsets.only(top: 16, bottom: 4),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontSize: AppTextSize.subtitle,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget _text(String key, String label, {int lines = 1}) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: TextField(
|
||||
key: Key('mw_$key'),
|
||||
controller: _fields[key],
|
||||
maxLines: lines,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||
// filled/fillColor/border inherit from the shared inputDecorationTheme
|
||||
// (theme.dart) — only the field-specific label needs setting here.
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
labelStyle: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
/// Reads [key]'s field as an int, treating blank or unparseable as 0.
|
||||
int _int(String key) => int.tryParse(_fields[key]!.text.trim()) ?? 0;
|
||||
|
||||
/// Reads [key]'s field verbatim.
|
||||
String _str(String key) => _fields[key]!.text;
|
||||
|
||||
ManualWorkoutDraft _draft() => ManualWorkoutDraft(
|
||||
sport: _sport,
|
||||
startTime: _str('start_time'),
|
||||
endTime: _str('end_time'),
|
||||
locationName: _str('location_name'),
|
||||
transportMethod: _str('transport_method'),
|
||||
cost: _str('cost'),
|
||||
rpe: _rpe.round(),
|
||||
wentWell: _str('went_well'),
|
||||
toImprove: _str('to_improve'),
|
||||
overallFeeling: _str('overall_feeling'),
|
||||
reservationPhone: _str('reservation_phone'),
|
||||
techniquesPracticed: _str('techniques_practiced'),
|
||||
warmUpMinutes: _str('warm_up_minutes'),
|
||||
painOrInjury: _str('pain_or_injury').trim().isEmpty
|
||||
? 'none'
|
||||
: _str('pain_or_injury'),
|
||||
matchesWon: _int('matches_won'),
|
||||
matchesLost: _int('matches_lost'),
|
||||
setsWon: _int('sets_won'),
|
||||
setsLost: _int('sets_lost'),
|
||||
racket: _str('racket'),
|
||||
balls: _str('balls'),
|
||||
activityTypeOther: _str('activity_type_other'),
|
||||
activityDetails: _str('activity_details'),
|
||||
equipment: _str('equipment'),
|
||||
);
|
||||
}
|
||||
@ -15,6 +15,8 @@ import 'package:workout_app/models/manual_workout.dart';
|
||||
import 'package:workout_app/services/workout_sync_service.dart';
|
||||
import 'package:workout_app/ui/theme.dart';
|
||||
|
||||
part 'manual_workout_form_parts.dart';
|
||||
|
||||
/// A screen that logs an off-app workout and syncs it to the PC.
|
||||
class ManualWorkoutScreen extends StatefulWidget {
|
||||
/// Creates the manual-workout form. [syncService]/[clock] are injectable for
|
||||
@ -93,37 +95,6 @@ class _ManualWorkoutScreenState extends State<ManualWorkoutScreen> {
|
||||
});
|
||||
}
|
||||
|
||||
int _int(String key) => int.tryParse(_fields[key]!.text.trim()) ?? 0;
|
||||
String _str(String key) => _fields[key]!.text;
|
||||
|
||||
ManualWorkoutDraft _draft() => ManualWorkoutDraft(
|
||||
sport: _sport,
|
||||
startTime: _str('start_time'),
|
||||
endTime: _str('end_time'),
|
||||
locationName: _str('location_name'),
|
||||
transportMethod: _str('transport_method'),
|
||||
cost: _str('cost'),
|
||||
rpe: _rpe.round(),
|
||||
wentWell: _str('went_well'),
|
||||
toImprove: _str('to_improve'),
|
||||
overallFeeling: _str('overall_feeling'),
|
||||
reservationPhone: _str('reservation_phone'),
|
||||
techniquesPracticed: _str('techniques_practiced'),
|
||||
warmUpMinutes: _str('warm_up_minutes'),
|
||||
painOrInjury: _str('pain_or_injury').trim().isEmpty
|
||||
? 'none'
|
||||
: _str('pain_or_injury'),
|
||||
matchesWon: _int('matches_won'),
|
||||
matchesLost: _int('matches_lost'),
|
||||
setsWon: _int('sets_won'),
|
||||
setsLost: _int('sets_lost'),
|
||||
racket: _str('racket'),
|
||||
balls: _str('balls'),
|
||||
activityTypeOther: _str('activity_type_other'),
|
||||
activityDetails: _str('activity_details'),
|
||||
equipment: _str('equipment'),
|
||||
);
|
||||
|
||||
Future<void> _submit() async {
|
||||
final draft = _draft();
|
||||
final error = validateManualWorkout(draft);
|
||||
@ -233,27 +204,6 @@ class _ManualWorkoutScreenState extends State<ManualWorkoutScreen> {
|
||||
];
|
||||
}
|
||||
|
||||
Widget _budgetBanner(ManualBudget budget) => Text(
|
||||
'Manual: ${budget.week}/$kManualWorkoutBudgetPer7Days this week · '
|
||||
'${budget.month}/$kManualWorkoutBudgetPer30Days this month',
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontSize: AppTextSize.body,
|
||||
),
|
||||
);
|
||||
|
||||
Widget _section(String title) => Padding(
|
||||
padding: const EdgeInsets.only(top: 16, bottom: 4),
|
||||
child: Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontSize: AppTextSize.subtitle,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Widget _sportDropdown() => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: DropdownButton<String>(
|
||||
@ -292,22 +242,4 @@ class _ManualWorkoutScreenState extends State<ManualWorkoutScreen> {
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Widget _text(String key, String label, {int lines = 1}) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: TextField(
|
||||
key: Key('mw_$key'),
|
||||
controller: _fields[key],
|
||||
maxLines: lines,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurface),
|
||||
// filled/fillColor/border inherit from the shared inputDecorationTheme
|
||||
// (theme.dart) — only the field-specific label needs setting here.
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
labelStyle: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user