From b6414c7f1c3842c797f3b4dd7a7f368973824d69 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sat, 15 Aug 2026 22:30:27 +0200 Subject: [PATCH] Split home_screen.dart (260) and manual_workout_screen.dart (313) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y --- .../workout_app/lib/screens/home_screen.dart | 34 +------- .../lib/screens/home_screen_navigation.dart | 42 ++++++++++ .../screens/manual_workout_form_parts.dart | 83 +++++++++++++++++++ .../lib/screens/manual_workout_screen.dart | 72 +--------------- 4 files changed, 128 insertions(+), 103 deletions(-) create mode 100644 stronglift_replacement/workout_app/lib/screens/home_screen_navigation.dart create mode 100644 stronglift_replacement/workout_app/lib/screens/manual_workout_form_parts.dart diff --git a/stronglift_replacement/workout_app/lib/screens/home_screen.dart b/stronglift_replacement/workout_app/lib/screens/home_screen.dart index 0b53637..2e0a8dc 100644 --- a/stronglift_replacement/workout_app/lib/screens/home_screen.dart +++ b/stronglift_replacement/workout_app/lib/screens/home_screen.dart @@ -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 { } /// Opens settings so the user can connect sync, then re-checks on return. - Future _openSyncSettings() async { - await Navigator.of(context).push( - MaterialPageRoute(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 { } } - Future _openWorkout({bool resume = false}) async { - final storage = StorageService.instance; - Map? 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( - builder: (_) => WorkoutScreen( - workoutType: type, - exercises: exercises, - savedState: savedState, - ), - ), - ); - unawaited(_load()); - } - @override Widget build(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; diff --git a/stronglift_replacement/workout_app/lib/screens/home_screen_navigation.dart b/stronglift_replacement/workout_app/lib/screens/home_screen_navigation.dart new file mode 100644 index 0000000..1cac381 --- /dev/null +++ b/stronglift_replacement/workout_app/lib/screens/home_screen_navigation.dart @@ -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 _openSyncSettings() async { + await Navigator.of(context).push( + MaterialPageRoute(builder: (_) => const SettingsScreen()), + ); + unawaited(_load()); + } + + Future _openWorkout({bool resume = false}) async { + final storage = StorageService.instance; + Map? 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( + builder: (_) => WorkoutScreen( + workoutType: type, + exercises: exercises, + savedState: savedState, + ), + ), + ); + unawaited(_load()); + } +} diff --git a/stronglift_replacement/workout_app/lib/screens/manual_workout_form_parts.dart b/stronglift_replacement/workout_app/lib/screens/manual_workout_form_parts.dart new file mode 100644 index 0000000..d0665c7 --- /dev/null +++ b/stronglift_replacement/workout_app/lib/screens/manual_workout_form_parts.dart @@ -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'), + ); +} diff --git a/stronglift_replacement/workout_app/lib/screens/manual_workout_screen.dart b/stronglift_replacement/workout_app/lib/screens/manual_workout_screen.dart index e017f53..ebc1b44 100644 --- a/stronglift_replacement/workout_app/lib/screens/manual_workout_screen.dart +++ b/stronglift_replacement/workout_app/lib/screens/manual_workout_screen.dart @@ -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 { }); } - 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 _submit() async { final draft = _draft(); final error = validateManualWorkout(draft); @@ -233,27 +204,6 @@ class _ManualWorkoutScreenState extends State { ]; } - 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( @@ -292,22 +242,4 @@ class _ManualWorkoutScreenState extends State { ], ), ); - - 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, - ), - ), - ), - ); }