From 5977475021cc5d39c7f0b24a04561c9fecbb6f27 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Sat, 15 Aug 2026 21:39:57 +0200 Subject: [PATCH] Split the private widgets out of three more Dart screens exercise_tile.dart 319 -> 144 + exercise_tile_rows.dart github_mirror_screen.dart 441 -> 247 + github_mirror_screen_widgets.dart home_screen.dart 383 -> 260 + home_screen_cards.dart Same `part` pattern as history_screen: the private StatelessWidgets move, the State stays. home_screen lands 10 over -- its remaining methods all call setState, which an extension cannot, and its build() is one cohesive tree. 402 Dart tests; dart analyze clean; coverage gate passes. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01EveB813BQ8abAP68VSqZ5Y --- .../lib/screens/github_mirror_screen.dart | 198 +---------------- .../screens/github_mirror_screen_widgets.dart | 202 ++++++++++++++++++ .../workout_app/lib/screens/home_screen.dart | 127 +---------- .../lib/screens/home_screen_cards.dart | 130 +++++++++++ .../lib/widgets/exercise_tile.dart | 179 +--------------- .../lib/widgets/exercise_tile_rows.dart | 182 ++++++++++++++++ 6 files changed, 520 insertions(+), 498 deletions(-) create mode 100644 stronglift_replacement/workout_app/lib/screens/github_mirror_screen_widgets.dart create mode 100644 stronglift_replacement/workout_app/lib/screens/home_screen_cards.dart create mode 100644 stronglift_replacement/workout_app/lib/widgets/exercise_tile_rows.dart diff --git a/stronglift_replacement/workout_app/lib/screens/github_mirror_screen.dart b/stronglift_replacement/workout_app/lib/screens/github_mirror_screen.dart index 9e856e8..b240ba8 100644 --- a/stronglift_replacement/workout_app/lib/screens/github_mirror_screen.dart +++ b/stronglift_replacement/workout_app/lib/screens/github_mirror_screen.dart @@ -21,6 +21,8 @@ import 'package:workout_app/services/github_device_auth.dart'; import 'package:workout_app/services/sync_settings.dart'; import 'package:workout_app/ui/theme.dart'; +part 'github_mirror_screen_widgets.dart'; + /// How to style a [_SyncStatusBadge]. enum _SyncStatusKind { success, pending, error } @@ -243,199 +245,3 @@ class _GitHubMirrorScreenState extends State { } /// A visible, colored status pill for the GitHub sync connection state. -class _SyncStatusBadge extends StatelessWidget { - const _SyncStatusBadge({required this.text, required this.kind}); - - final String text; - final _SyncStatusKind kind; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final status = Theme.of(context).extension()!; - final color = switch (kind) { - _SyncStatusKind.success => status.success, - _SyncStatusKind.error => colorScheme.error, - _SyncStatusKind.pending => colorScheme.onSurfaceVariant, - }; - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - decoration: BoxDecoration( - color: color.withValues(alpha: 0.12), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: color.withValues(alpha: 0.4)), - ), - child: Row( - children: [ - if (kind == _SyncStatusKind.pending) - const SizedBox( - width: 14, - height: 14, - child: CircularProgressIndicator(strokeWidth: 2), - ) - else - Icon( - kind == _SyncStatusKind.success - ? Icons.check_circle - : Icons.error, - color: color, - size: 16, - ), - const SizedBox(width: 10), - Expanded( - child: Text( - text, - style: TextStyle( - color: color, - fontSize: AppTextSize.label, - fontWeight: FontWeight.w600, - ), - ), - ), - ], - ), - ); - } -} - -class _SyncTokenField extends StatelessWidget { - const _SyncTokenField({required this.controller, required this.onSave}); - - final TextEditingController controller; - final VoidCallback onSave; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: TextField( - controller: controller, - obscureText: true, - style: TextStyle(color: colorScheme.onSurface), - // filled/fillColor/border inherit from the shared - // inputDecorationTheme (theme.dart) — only the field-specific - // hint/padding need setting here. - decoration: InputDecoration( - hintText: 'GitHub PAT', - hintStyle: TextStyle(color: colorScheme.onSurfaceVariant), - contentPadding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 10, - ), - ), - ), - ), - const SizedBox(width: 8), - ElevatedButton(onPressed: onSave, child: const Text('Save')), - ], - ); - } -} - -/// Dialog shown during the device flow: displays the user code, opens the -/// verification page, and polls until authorized -- popping the token (or -/// null if cancelled / failed). -class _DeviceCodeDialog extends StatefulWidget { - const _DeviceCodeDialog({required this.device, required this.auth}); - - final DeviceCodeResponse device; - final GitHubDeviceAuth auth; - - @override - State<_DeviceCodeDialog> createState() => _DeviceCodeDialogState(); -} - -class _DeviceCodeDialogState extends State<_DeviceCodeDialog> { - String? _error; - - @override - void initState() { - super.initState(); - unawaited(_poll()); - } - - Future _poll() async { - try { - final token = await widget.auth.pollForToken(widget.device); - if (mounted) Navigator.of(context).pop(token); - } on Exception catch (e) { - if (mounted) setState(() => _error = '$e'); - } - } - - Future _openPage() async { - await Clipboard.setData(ClipboardData(text: widget.device.userCode)); - await launchUrl( - Uri.parse(widget.device.verificationUri), - mode: LaunchMode.externalApplication, - ); - } - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - return AlertDialog( - backgroundColor: colorScheme.surfaceContainerHigh, - title: Text( - 'Authorize on GitHub', - style: TextStyle(color: colorScheme.onSurface), - ), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'Enter this code on GitHub:', - style: TextStyle(color: colorScheme.onSurfaceVariant), - ), - const SizedBox(height: 8), - SelectableText( - widget.device.userCode, - style: TextStyle( - color: colorScheme.onSurface, - fontSize: AppTextSize.title, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 16), - if (_error == null) - Row( - children: [ - const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator(strokeWidth: 2), - ), - const SizedBox(width: 12), - Expanded( - child: Text( - 'Waiting for authorization…', - style: TextStyle(color: colorScheme.onSurfaceVariant), - ), - ), - ], - ) - else - Text(_error!, style: TextStyle(color: colorScheme.error)), - ], - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: Text( - 'Cancel', - style: TextStyle(color: colorScheme.onSurfaceVariant), - ), - ), - FilledButton.icon( - onPressed: _openPage, - icon: const Icon(Icons.open_in_new), - label: const Text('Open GitHub & copy code'), - ), - ], - ); - } -} diff --git a/stronglift_replacement/workout_app/lib/screens/github_mirror_screen_widgets.dart b/stronglift_replacement/workout_app/lib/screens/github_mirror_screen_widgets.dart new file mode 100644 index 0000000..7609736 --- /dev/null +++ b/stronglift_replacement/workout_app/lib/screens/github_mirror_screen_widgets.dart @@ -0,0 +1,202 @@ +// Status badge, token field and the device-code dialog. +// +// `part` file: these widgets are library-private and stay that way. +// See history_screen_charts.dart for the full reasoning. +part of 'github_mirror_screen.dart'; + +class _SyncStatusBadge extends StatelessWidget { + const _SyncStatusBadge({required this.text, required this.kind}); + + final String text; + final _SyncStatusKind kind; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final status = Theme.of(context).extension()!; + final color = switch (kind) { + _SyncStatusKind.success => status.success, + _SyncStatusKind.error => colorScheme.error, + _SyncStatusKind.pending => colorScheme.onSurfaceVariant, + }; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: color.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: color.withValues(alpha: 0.4)), + ), + child: Row( + children: [ + if (kind == _SyncStatusKind.pending) + const SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator(strokeWidth: 2), + ) + else + Icon( + kind == _SyncStatusKind.success + ? Icons.check_circle + : Icons.error, + color: color, + size: 16, + ), + const SizedBox(width: 10), + Expanded( + child: Text( + text, + style: TextStyle( + color: color, + fontSize: AppTextSize.label, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ); + } +} + +class _SyncTokenField extends StatelessWidget { + const _SyncTokenField({required this.controller, required this.onSave}); + + final TextEditingController controller; + final VoidCallback onSave; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: TextField( + controller: controller, + obscureText: true, + style: TextStyle(color: colorScheme.onSurface), + // filled/fillColor/border inherit from the shared + // inputDecorationTheme (theme.dart) — only the field-specific + // hint/padding need setting here. + decoration: InputDecoration( + hintText: 'GitHub PAT', + hintStyle: TextStyle(color: colorScheme.onSurfaceVariant), + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), + ), + ), + ), + const SizedBox(width: 8), + ElevatedButton(onPressed: onSave, child: const Text('Save')), + ], + ); + } +} + +/// Dialog shown during the device flow: displays the user code, opens the +/// verification page, and polls until authorized -- popping the token (or +/// null if cancelled / failed). +class _DeviceCodeDialog extends StatefulWidget { + const _DeviceCodeDialog({required this.device, required this.auth}); + + final DeviceCodeResponse device; + final GitHubDeviceAuth auth; + + @override + State<_DeviceCodeDialog> createState() => _DeviceCodeDialogState(); +} + +class _DeviceCodeDialogState extends State<_DeviceCodeDialog> { + String? _error; + + @override + void initState() { + super.initState(); + unawaited(_poll()); + } + + Future _poll() async { + try { + final token = await widget.auth.pollForToken(widget.device); + if (mounted) Navigator.of(context).pop(token); + } on Exception catch (e) { + if (mounted) setState(() => _error = '$e'); + } + } + + Future _openPage() async { + await Clipboard.setData(ClipboardData(text: widget.device.userCode)); + await launchUrl( + Uri.parse(widget.device.verificationUri), + mode: LaunchMode.externalApplication, + ); + } + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return AlertDialog( + backgroundColor: colorScheme.surfaceContainerHigh, + title: Text( + 'Authorize on GitHub', + style: TextStyle(color: colorScheme.onSurface), + ), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Enter this code on GitHub:', + style: TextStyle(color: colorScheme.onSurfaceVariant), + ), + const SizedBox(height: 8), + SelectableText( + widget.device.userCode, + style: TextStyle( + color: colorScheme.onSurface, + fontSize: AppTextSize.title, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 16), + if (_error == null) + Row( + children: [ + const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator(strokeWidth: 2), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + 'Waiting for authorization…', + style: TextStyle(color: colorScheme.onSurfaceVariant), + ), + ), + ], + ) + else + Text(_error!, style: TextStyle(color: colorScheme.error)), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text( + 'Cancel', + style: TextStyle(color: colorScheme.onSurfaceVariant), + ), + ), + FilledButton.icon( + onPressed: _openPage, + icon: const Icon(Icons.open_in_new), + label: const Text('Open GitHub & copy code'), + ), + ], + ); + } +} diff --git a/stronglift_replacement/workout_app/lib/screens/home_screen.dart b/stronglift_replacement/workout_app/lib/screens/home_screen.dart index e1d504b..0b53637 100644 --- a/stronglift_replacement/workout_app/lib/screens/home_screen.dart +++ b/stronglift_replacement/workout_app/lib/screens/home_screen.dart @@ -14,6 +14,8 @@ import 'package:workout_app/services/workout_sync_service.dart'; import 'package:workout_app/ui/theme.dart'; import 'package:workout_app/widgets/sync_status_card.dart'; +part 'home_screen_cards.dart'; + /// Home screen: auto-resumes active sessions and shows done-today status. class HomeScreen extends StatefulWidget { /// Creates a [HomeScreen]. @@ -256,128 +258,3 @@ class _HomeScreenState extends State { } // ── 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 exercises; - final bool doneToday; - final bool hasActiveSession; - final VoidCallback onStart; - final VoidCallback onResume; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final status = Theme.of(context).extension()!; - // Canonical button padding (rule 22): vertical 12, horizontal 24 — an - // exact 2x ratio, both on the 4px spacing scale (tokens.md's own example). - const buttonPadding = EdgeInsets.symmetric(horizontal: 24, vertical: 12); - return Card( - color: colorScheme.surfaceContainerHigh, - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (doneToday && !hasActiveSession) ...[ - Row( - children: [ - Icon(Icons.check_circle, color: status.success, size: 18), - const SizedBox(width: 8), - Text( - 'Done for today!', - style: TextStyle( - color: status.success, - fontWeight: FontWeight.bold, - fontSize: AppTextSize.body, - ), - ), - ], - ), - const SizedBox(height: 6), - Text( - 'Next: Workout $type — tomorrow', - style: TextStyle( - color: colorScheme.onSurface, - fontSize: AppTextSize.body, - fontWeight: FontWeight.bold, - ), - ), - ] else ...[ - Text( - hasActiveSession - ? 'Workout $type in progress' - : 'Next: Workout $type', - style: TextStyle( - color: hasActiveSession - ? status.warning - : colorScheme.onSurface, - fontSize: AppTextSize.subtitle, - fontWeight: FontWeight.bold, - ), - ), - ], - const SizedBox(height: 10), - ...exercises.map( - (e) => Text( - '${e.name} ${e.sets}×${e.reps}×${e.weight}kg', - style: TextStyle( - color: colorScheme.onSurfaceVariant, - fontSize: AppTextSize.label, - ), - ), - ), - const SizedBox(height: 14), - if (hasActiveSession) - SizedBox( - width: double.infinity, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: status.warning, - padding: buttonPadding, - ), - onPressed: onResume, - child: Text( - 'Resume Workout', - style: TextStyle( - color: colorScheme.onPrimary, - fontSize: AppTextSize.body, - fontWeight: FontWeight.bold, - ), - ), - ), - ) - else if (!doneToday) - SizedBox( - width: double.infinity, - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: colorScheme.primary, - padding: buttonPadding, - ), - onPressed: onStart, - child: Text( - 'Start Workout $type', - style: TextStyle( - color: colorScheme.onPrimary, - fontSize: AppTextSize.body, - fontWeight: FontWeight.bold, - ), - ), - ), - ), - ], - ), - ), - ); - } -} diff --git a/stronglift_replacement/workout_app/lib/screens/home_screen_cards.dart b/stronglift_replacement/workout_app/lib/screens/home_screen_cards.dart new file mode 100644 index 0000000..5c14e05 --- /dev/null +++ b/stronglift_replacement/workout_app/lib/screens/home_screen_cards.dart @@ -0,0 +1,130 @@ +// The workout and status cards on the home screen. +// +// `part` file: these widgets are library-private and stay that way. +// See history_screen_charts.dart for the full reasoning. +part of 'home_screen.dart'; + +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 exercises; + final bool doneToday; + final bool hasActiveSession; + final VoidCallback onStart; + final VoidCallback onResume; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final status = Theme.of(context).extension()!; + // Canonical button padding (rule 22): vertical 12, horizontal 24 — an + // exact 2x ratio, both on the 4px spacing scale (tokens.md's own example). + const buttonPadding = EdgeInsets.symmetric(horizontal: 24, vertical: 12); + return Card( + color: colorScheme.surfaceContainerHigh, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (doneToday && !hasActiveSession) ...[ + Row( + children: [ + Icon(Icons.check_circle, color: status.success, size: 18), + const SizedBox(width: 8), + Text( + 'Done for today!', + style: TextStyle( + color: status.success, + fontWeight: FontWeight.bold, + fontSize: AppTextSize.body, + ), + ), + ], + ), + const SizedBox(height: 6), + Text( + 'Next: Workout $type — tomorrow', + style: TextStyle( + color: colorScheme.onSurface, + fontSize: AppTextSize.body, + fontWeight: FontWeight.bold, + ), + ), + ] else ...[ + Text( + hasActiveSession + ? 'Workout $type in progress' + : 'Next: Workout $type', + style: TextStyle( + color: hasActiveSession + ? status.warning + : colorScheme.onSurface, + fontSize: AppTextSize.subtitle, + fontWeight: FontWeight.bold, + ), + ), + ], + const SizedBox(height: 10), + ...exercises.map( + (e) => Text( + '${e.name} ${e.sets}×${e.reps}×${e.weight}kg', + style: TextStyle( + color: colorScheme.onSurfaceVariant, + fontSize: AppTextSize.label, + ), + ), + ), + const SizedBox(height: 14), + if (hasActiveSession) + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: status.warning, + padding: buttonPadding, + ), + onPressed: onResume, + child: Text( + 'Resume Workout', + style: TextStyle( + color: colorScheme.onPrimary, + fontSize: AppTextSize.body, + fontWeight: FontWeight.bold, + ), + ), + ), + ) + else if (!doneToday) + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: colorScheme.primary, + padding: buttonPadding, + ), + onPressed: onStart, + child: Text( + 'Start Workout $type', + style: TextStyle( + color: colorScheme.onPrimary, + fontSize: AppTextSize.body, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/stronglift_replacement/workout_app/lib/widgets/exercise_tile.dart b/stronglift_replacement/workout_app/lib/widgets/exercise_tile.dart index 8b6c97e..b5526b2 100644 --- a/stronglift_replacement/workout_app/lib/widgets/exercise_tile.dart +++ b/stronglift_replacement/workout_app/lib/widgets/exercise_tile.dart @@ -6,6 +6,8 @@ import 'package:workout_app/models/exercise.dart'; import 'package:workout_app/ui/theme.dart'; import 'package:workout_app/widgets/rep_circle.dart'; +part 'exercise_tile_rows.dart'; + /// Card widget displaying warmup and working-set rep circles for one exercise. class ExerciseTile extends StatelessWidget { /// Creates an [ExerciseTile]. @@ -140,180 +142,3 @@ class ExerciseTile extends StatelessWidget { ); } } - -class _ThresholdRow extends StatelessWidget { - const _ThresholdRow({ - required this.successThreshold, - required this.failThreshold, - required this.onSuccessChanged, - required this.onFailChanged, - }); - - final int successThreshold; - final int failThreshold; - final ValueChanged onSuccessChanged; - final ValueChanged onFailChanged; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final status = Theme.of(context).extension()!; - final captionStyle = TextStyle( - color: colorScheme.onSurfaceVariant, - fontSize: AppTextSize.caption, - ); - return Row( - children: [ - Icon(Icons.trending_up, size: 13, color: status.success), - const SizedBox(width: 4), - Text('after', style: captionStyle), - const SizedBox(width: 6), - _MiniStepper( - value: successThreshold, - onChanged: onSuccessChanged, - ), - const SizedBox(width: 4), - Text('↑', style: captionStyle), - const Spacer(), - Icon(Icons.trending_down, size: 13, color: colorScheme.error), - const SizedBox(width: 4), - Text('after', style: captionStyle), - const SizedBox(width: 6), - _MiniStepper( - value: failThreshold, - onChanged: onFailChanged, - ), - const SizedBox(width: 4), - Text('↓', style: captionStyle), - ], - ); - } -} - -class _MiniStepper extends StatelessWidget { - const _MiniStepper({required this.value, required this.onChanged}); - - final int value; - final ValueChanged onChanged; - - static const _min = 1; - static const _max = 5; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - _btn( - context, - Icons.remove, - value > _min ? () => onChanged(value - 1) : null, - ), - SizedBox( - width: 22, - child: Text( - '$value', - textAlign: TextAlign.center, - style: TextStyle( - color: colorScheme.onSurface, - fontSize: AppTextSize.caption, - ), - ), - ), - _btn( - context, - Icons.add, - value < _max ? () => onChanged(value + 1) : null, - ), - ], - ); - } - - Widget _btn(BuildContext context, IconData icon, VoidCallback? onTap) { - final colorScheme = Theme.of(context).colorScheme; - return GestureDetector( - onTap: onTap, - child: Container( - width: 22, - height: 22, - decoration: BoxDecoration( - color: colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(4), - ), - alignment: Alignment.center, - child: Icon( - icon, - size: 12, - color: onTap != null - ? colorScheme.onSurface - : colorScheme.onSurfaceVariant, - ), - ), - ); - } -} - -class _WarmupRow extends StatelessWidget { - const _WarmupRow({ - required this.warmupWeight, - required this.tapped, - required this.onTap, - }); - - final double warmupWeight; - final bool tapped; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - final status = Theme.of(context).extension()!; - final mutedStyle = TextStyle( - color: colorScheme.onSurfaceVariant, - fontSize: AppTextSize.caption, - ); - return Row( - children: [ - Text('Warmup 1×5×', style: mutedStyle), - Text('${warmupWeight}kg', style: mutedStyle), - const SizedBox(width: 10), - GestureDetector( - onTap: tapped ? null : onTap, - child: AnimatedContainer( - duration: const Duration(milliseconds: 200), - width: 36, - height: 36, - decoration: BoxDecoration( - shape: BoxShape.circle, - // Tapped = a completed milestone, same semantic as every other - // "done" indicator in this app (rep circles, calendar days). - color: tapped ? status.success : Colors.transparent, - border: Border.all( - color: tapped ? status.success : colorScheme.outline, - width: 2, - ), - ), - alignment: Alignment.center, - child: Icon( - tapped ? Icons.check : Icons.fitness_center, - // on-fill on the filled circle; muted on the empty outline one. - color: tapped - ? colorScheme.onPrimary - : colorScheme.onSurfaceVariant, - size: 16, - ), - ), - ), - const SizedBox(width: 6), - Text( - tapped ? 'done' : 'optional', - style: TextStyle( - color: tapped ? status.success : colorScheme.onSurfaceVariant, - fontSize: AppTextSize.caption, - ), - ), - ], - ); - } -} diff --git a/stronglift_replacement/workout_app/lib/widgets/exercise_tile_rows.dart b/stronglift_replacement/workout_app/lib/widgets/exercise_tile_rows.dart new file mode 100644 index 0000000..b7a72a3 --- /dev/null +++ b/stronglift_replacement/workout_app/lib/widgets/exercise_tile_rows.dart @@ -0,0 +1,182 @@ +// Threshold row, mini stepper and warm-up row for ExerciseTile. +// +// `part` file: these widgets are library-private and stay that way. +// See history_screen_charts.dart for the full reasoning. +part of 'exercise_tile.dart'; + +class _ThresholdRow extends StatelessWidget { + const _ThresholdRow({ + required this.successThreshold, + required this.failThreshold, + required this.onSuccessChanged, + required this.onFailChanged, + }); + + final int successThreshold; + final int failThreshold; + final ValueChanged onSuccessChanged; + final ValueChanged onFailChanged; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final status = Theme.of(context).extension()!; + final captionStyle = TextStyle( + color: colorScheme.onSurfaceVariant, + fontSize: AppTextSize.caption, + ); + return Row( + children: [ + Icon(Icons.trending_up, size: 13, color: status.success), + const SizedBox(width: 4), + Text('after', style: captionStyle), + const SizedBox(width: 6), + _MiniStepper( + value: successThreshold, + onChanged: onSuccessChanged, + ), + const SizedBox(width: 4), + Text('↑', style: captionStyle), + const Spacer(), + Icon(Icons.trending_down, size: 13, color: colorScheme.error), + const SizedBox(width: 4), + Text('after', style: captionStyle), + const SizedBox(width: 6), + _MiniStepper( + value: failThreshold, + onChanged: onFailChanged, + ), + const SizedBox(width: 4), + Text('↓', style: captionStyle), + ], + ); + } +} + +class _MiniStepper extends StatelessWidget { + const _MiniStepper({required this.value, required this.onChanged}); + + final int value; + final ValueChanged onChanged; + + static const _min = 1; + static const _max = 5; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + _btn( + context, + Icons.remove, + value > _min ? () => onChanged(value - 1) : null, + ), + SizedBox( + width: 22, + child: Text( + '$value', + textAlign: TextAlign.center, + style: TextStyle( + color: colorScheme.onSurface, + fontSize: AppTextSize.caption, + ), + ), + ), + _btn( + context, + Icons.add, + value < _max ? () => onChanged(value + 1) : null, + ), + ], + ); + } + + Widget _btn(BuildContext context, IconData icon, VoidCallback? onTap) { + final colorScheme = Theme.of(context).colorScheme; + return GestureDetector( + onTap: onTap, + child: Container( + width: 22, + height: 22, + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(4), + ), + alignment: Alignment.center, + child: Icon( + icon, + size: 12, + color: onTap != null + ? colorScheme.onSurface + : colorScheme.onSurfaceVariant, + ), + ), + ); + } +} + +class _WarmupRow extends StatelessWidget { + const _WarmupRow({ + required this.warmupWeight, + required this.tapped, + required this.onTap, + }); + + final double warmupWeight; + final bool tapped; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final status = Theme.of(context).extension()!; + final mutedStyle = TextStyle( + color: colorScheme.onSurfaceVariant, + fontSize: AppTextSize.caption, + ); + return Row( + children: [ + Text('Warmup 1×5×', style: mutedStyle), + Text('${warmupWeight}kg', style: mutedStyle), + const SizedBox(width: 10), + GestureDetector( + onTap: tapped ? null : onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 36, + height: 36, + decoration: BoxDecoration( + shape: BoxShape.circle, + // Tapped = a completed milestone, same semantic as every other + // "done" indicator in this app (rep circles, calendar days). + color: tapped ? status.success : Colors.transparent, + border: Border.all( + color: tapped ? status.success : colorScheme.outline, + width: 2, + ), + ), + alignment: Alignment.center, + child: Icon( + tapped ? Icons.check : Icons.fitness_center, + // on-fill on the filled circle; muted on the empty outline one. + color: tapped + ? colorScheme.onPrimary + : colorScheme.onSurfaceVariant, + size: 16, + ), + ), + ), + const SizedBox(width: 6), + Text( + tapped ? 'done' : 'optional', + style: TextStyle( + color: tapped ? status.success : colorScheme.onSurfaceVariant, + fontSize: AppTextSize.caption, + ), + ), + ], + ); + } +}