From 323e3cd02a84c7649138a1010a546bc58f7f2022 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Wed, 22 Jul 2026 21:08:45 +0200 Subject: [PATCH] workout_app: warm-tint the unified-design-system palette Replaces the dead ColorScheme.fromSeed(Colors.indigo) (declared but never consumed by any screen/widget) with theme.dart: an explicit dark ColorScheme plus a real AppStatusColors extension, since this app genuinely uses success/warning status colors throughout (unlike dufs-cloud's app). Routes all 10 screen/widget files' ~280 hardcoded Colors.*/fontSize literals through it. Fixes the two flagged DESIGN_AUDIT_TODO violations: home_screen's Resume/Start buttons now use a real 2x horizontal:vertical padding ratio, and rep_circle's BoxShadow (the one shadow in an otherwise flat dark UI) is removed. --- .../workout_app/lib/main.dart | 9 +- .../lib/screens/history_screen.dart | 180 +++++++++-------- .../workout_app/lib/screens/home_screen.dart | 86 +++++---- .../lib/screens/manual_workout_screen.dart | 45 +++-- .../lib/screens/settings_screen.dart | 181 ++++++++++-------- .../lib/screens/workout_screen.dart | 68 +++---- .../workout_app/lib/ui/theme.dart | 164 ++++++++++++++++ .../workout_app/lib/widgets/break_banner.dart | 26 ++- .../lib/widgets/calendar_widget.dart | 38 ++-- .../lib/widgets/exercise_tile.dart | 145 ++++++++------ .../workout_app/lib/widgets/rep_circle.dart | 34 ++-- .../lib/widgets/workout_summary_dialog.dart | 21 +- .../test/screens/history_screen_test.dart | 67 ++++--- .../test/screens/home_screen_test.dart | 42 ++-- .../test/screens/settings_screen_test.dart | 58 +++--- .../test/screens/workout_screen_test.dart | 73 ++++--- .../workout_app/test/ui/theme_test.dart | 57 ++++++ .../test/widgets/break_banner_test.dart | 6 +- .../test/widgets/calendar_widget_test.dart | 6 +- .../test/widgets/exercise_tile_test.dart | 40 ++-- .../test/widgets/rep_circle_test.dart | 23 ++- .../widgets/workout_summary_dialog_test.dart | 41 +++- 22 files changed, 927 insertions(+), 483 deletions(-) create mode 100644 stronglift_replacement/workout_app/lib/ui/theme.dart create mode 100644 stronglift_replacement/workout_app/test/ui/theme_test.dart diff --git a/stronglift_replacement/workout_app/lib/main.dart b/stronglift_replacement/workout_app/lib/main.dart index bbc12c8..ec37061 100644 --- a/stronglift_replacement/workout_app/lib/main.dart +++ b/stronglift_replacement/workout_app/lib/main.dart @@ -5,6 +5,7 @@ import 'package:workout_app/screens/home_screen.dart'; import 'package:workout_app/services/backup_service.dart'; import 'package:workout_app/services/http_server_service.dart'; import 'package:workout_app/services/storage_service.dart'; +import 'package:workout_app/ui/theme.dart'; // coverage:ignore-start // App bootstrap: permission request, DB init, and a real listening socket — @@ -65,13 +66,7 @@ class _WorkoutAppState extends State with WidgetsBindingObserver { return MaterialApp( title: 'Workout Tracker', debugShowCheckedModeBanner: false, - theme: ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: Colors.indigo, - brightness: Brightness.dark, - ), - useMaterial3: true, - ), + theme: buildAppTheme(), home: const HomeScreen(), ); } diff --git a/stronglift_replacement/workout_app/lib/screens/history_screen.dart b/stronglift_replacement/workout_app/lib/screens/history_screen.dart index 26d9396..eeef908 100644 --- a/stronglift_replacement/workout_app/lib/screens/history_screen.dart +++ b/stronglift_replacement/workout_app/lib/screens/history_screen.dart @@ -13,6 +13,7 @@ import 'package:http/http.dart' as http; import 'package:workout_app/models/exercise.dart'; import 'package:workout_app/services/storage_service.dart'; import 'package:workout_app/services/workout_sync_service.dart'; +import 'package:workout_app/ui/theme.dart'; import 'package:workout_app/widgets/calendar_widget.dart'; const _kTotal = 'Total (all workouts)'; @@ -96,10 +97,9 @@ class _HistoryScreenState extends State { final payloads = await WorkoutSyncService( httpClient: widget.httpClient, ).readMergedWorkoutPayloads(); - final synced = payloads - .where((p) => _kSyncedOnlyKinds.contains(p['kind'])) - .toList() - ..sort((a, b) => '${b['date']}'.compareTo('${a['date']}')); + final synced = + payloads.where((p) => _kSyncedOnlyKinds.contains(p['kind'])).toList() + ..sort((a, b) => '${b['date']}'.compareTo('${a['date']}')); return synced; } @@ -202,20 +202,23 @@ class _HistoryScreenState extends State { final allNames = [_kTotal, ..._exerciseNames]; final isTotal = _selected == _kTotal; + final colorScheme = Theme.of(context).colorScheme; return Scaffold( - backgroundColor: Colors.grey.shade900, appBar: AppBar( - backgroundColor: Colors.grey.shade800, - title: const Text('Progress', style: TextStyle(color: Colors.white)), - iconTheme: const IconThemeData(color: Colors.white), + backgroundColor: colorScheme.surfaceContainerHigh, + title: Text( + 'Progress', + style: TextStyle(color: colorScheme.onSurface), + ), + iconTheme: IconThemeData(color: colorScheme.onSurface), ), body: _loading ? const Center(child: CircularProgressIndicator()) : _rows.isEmpty - ? const Center( + ? Center( child: Text( 'No workouts yet.', - style: TextStyle(color: Colors.white54), + style: TextStyle(color: colorScheme.onSurfaceVariant), ), ) : ListView( @@ -327,9 +330,9 @@ class _SectionLabel extends StatelessWidget { Widget build(BuildContext context) { return Text( text, - style: const TextStyle( - color: Colors.white54, - fontSize: 11, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: AppTextSize.caption, letterSpacing: 1.3, ), ); @@ -349,10 +352,11 @@ class _ExercisePicker extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return DropdownButton( value: selected, - dropdownColor: Colors.grey.shade800, - style: const TextStyle(color: Colors.white), + dropdownColor: colorScheme.surfaceContainerHigh, + style: TextStyle(color: colorScheme.onSurface), underline: const SizedBox(), isExpanded: true, items: names @@ -362,7 +366,9 @@ class _ExercisePicker extends StatelessWidget { child: Text( n, style: TextStyle( - color: n == _kTotal ? Colors.white70 : Colors.white, + color: n == _kTotal + ? colorScheme.onSurfaceVariant + : colorScheme.onSurface, fontStyle: n == _kTotal ? FontStyle.italic : FontStyle.normal, ), ), @@ -395,27 +401,29 @@ class _ProgressStatsCard extends StatelessWidget { final successLeft = state.successThreshold - state.successStreak; final failLeft = state.failThreshold - state.failStreak; + final colorScheme = Theme.of(context).colorScheme; + final status = Theme.of(context).extension()!; return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(8), + color: colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${state.name} — ${state.weight}kg', - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: colorScheme.onSurface, fontWeight: FontWeight.bold, - fontSize: 14, + fontSize: AppTextSize.label, ), ), const SizedBox(height: 8), _StreakRow( icon: Icons.trending_up, - color: Colors.greenAccent, + color: status.success, current: state.successStreak, threshold: state.successThreshold, leftLabel: '$successLeft more', @@ -429,7 +437,7 @@ class _ProgressStatsCard extends StatelessWidget { const SizedBox(height: 6), _StreakRow( icon: Icons.trending_down, - color: Colors.redAccent, + color: colorScheme.error, current: state.failStreak, threshold: state.failThreshold, leftLabel: '$failLeft more', @@ -463,6 +471,7 @@ class _StreakRow extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return Row( children: [ Icon(icon, color: color, size: 14), @@ -475,7 +484,7 @@ class _StreakRow extends StatelessWidget { margin: const EdgeInsets.only(right: 3), decoration: BoxDecoration( shape: BoxShape.circle, - color: i < current ? color : Colors.white24, + color: i < current ? color : colorScheme.surfaceContainerHighest, ), ), ), @@ -483,7 +492,10 @@ class _StreakRow extends StatelessWidget { Expanded( child: Text( '$leftLabel to $direction $actionLabel', - style: const TextStyle(color: Colors.white60, fontSize: 12), + style: TextStyle( + color: colorScheme.onSurfaceVariant, + fontSize: AppTextSize.caption, + ), ), ), ], @@ -498,20 +510,25 @@ class _WeightChart extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; if (points.length < 2) { return Container( height: 80, alignment: Alignment.center, - child: const Text( + child: Text( 'Not enough data for chart', - style: TextStyle(color: Colors.white38), + style: TextStyle(color: colorScheme.onSurfaceVariant), ), ); } return SizedBox( height: 140, child: CustomPaint( - painter: _ChartPainter(points), + painter: _ChartPainter( + points, + lineColor: colorScheme.primary, + labelColor: colorScheme.onSurfaceVariant, + ), size: Size.infinite, ), ); @@ -519,10 +536,19 @@ class _WeightChart extends StatelessWidget { } class _ChartPainter extends CustomPainter { - _ChartPainter(this.points); + _ChartPainter( + this.points, { + required this.lineColor, + required this.labelColor, + }); final List<(DateTime, double)> points; + // CustomPainter.paint() has no BuildContext, so the caller (which does) + // passes the themed colors in explicitly. + final Color lineColor; + final Color labelColor; + // Layout constants static const _topPad = 14.0; // room for top Y label static const _bottomPad = 22.0; // room for X-axis dates @@ -569,11 +595,11 @@ class _ChartPainter extends CustomPainter { : (1 - (w - minW) / wRange) * plotHeight + plotTop; final linePaint = Paint() - ..color = Colors.indigoAccent + ..color = lineColor ..strokeWidth = 2 ..style = PaintingStyle.stroke; final dotPaint = Paint() - ..color = Colors.indigoAccent + ..color = lineColor ..style = PaintingStyle.fill; final path = Path()..moveTo(xOf(points.first.$1), yOf(points.first.$2)); @@ -591,7 +617,7 @@ class _ChartPainter extends CustomPainter { tp ..text = TextSpan( text: text, - style: TextStyle(color: Colors.white54, fontSize: fontSize), + style: TextStyle(color: labelColor, fontSize: fontSize), ) ..layout() ..paint(canvas, offset); @@ -609,7 +635,7 @@ class _ChartPainter extends CustomPainter { tp ..text = TextSpan( text: label, - style: const TextStyle(color: Colors.white38, fontSize: 9), + style: TextStyle(color: labelColor, fontSize: 9), ) ..layout(); final cx = xOf(p.$1); @@ -618,6 +644,8 @@ class _ChartPainter extends CustomPainter { } } + // lineColor/labelColor come from the app's single fixed dark theme, so + // they never actually change at runtime — only points varies. @override bool shouldRepaint(_ChartPainter old) => old.points != points; } @@ -630,6 +658,7 @@ class _SyncedWorkoutTile extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; final kind = '${payload['kind']}'; final isRun = kind == 'runnerup_verified'; final label = isRun ? 'Run' : 'Manual'; @@ -639,17 +668,17 @@ class _SyncedWorkoutTile extends StatelessWidget { margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: isRun ? Colors.blue.shade800 : Colors.orange.shade900, - ), + color: colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(AppRadius.sm), + // Neutral — this distinguishes entry TYPE (run vs. manual), not a + // status/judgment, so it doesn't borrow a semantic status color. + border: Border.all(color: colorScheme.outline), ), child: Row( children: [ Icon( isRun ? Icons.directions_run : Icons.edit_note, - color: isRun ? Colors.blue.shade300 : Colors.orange.shade300, + color: colorScheme.onSurfaceVariant, size: 20, ), const SizedBox(width: 10), @@ -659,15 +688,18 @@ class _SyncedWorkoutTile extends StatelessWidget { children: [ Text( '${payload['date']} · $label', - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: colorScheme.onSurface, fontWeight: FontWeight.bold, ), ), if (detail.isNotEmpty) Text( detail, - style: TextStyle(color: Colors.grey.shade400, fontSize: 12), + style: TextStyle( + color: colorScheme.onSurfaceVariant, + fontSize: AppTextSize.caption, + ), ), ], ), @@ -692,26 +724,27 @@ class _AllSessionTile extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final status = Theme.of(context).extension()!; final succeeded = (row['succeeded'] as int) == 1; final type = row['workout_type'] as String; final date = row['date'] as String; final dur = _formatDuration(row['duration_seconds'] as int); + final statusColor = succeeded ? status.success : colorScheme.error; return Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: succeeded ? Colors.green.shade800 : Colors.red.shade900, - ), + color: colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(AppRadius.sm), + border: Border.all(color: statusColor), ), child: Row( children: [ Icon( succeeded ? Icons.check_circle : Icons.cancel, - color: succeeded ? Colors.greenAccent : Colors.redAccent, + color: statusColor, size: 18, ), const SizedBox(width: 10), @@ -721,14 +754,17 @@ class _AllSessionTile extends StatelessWidget { children: [ Text( 'Workout $type · $date', - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: colorScheme.onSurface, fontWeight: FontWeight.bold, ), ), Text( dur, - style: const TextStyle(color: Colors.white54, fontSize: 12), + style: TextStyle( + color: colorScheme.onSurfaceVariant, + fontSize: AppTextSize.caption, + ), ), ], ), @@ -753,6 +789,8 @@ class _ExerciseSessionTile extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final status = Theme.of(context).extension()!; final exData = session['exerciseData'] as Map; final succeeded = (exData['succeeded'] as bool?) == true; final date = session['date'] as String; @@ -763,16 +801,19 @@ class _ExerciseSessionTile extends StatelessWidget { final targetSets = exData['targetSets'] as int? ?? sets.length; final doneSets = sets.where((s) => s['succeeded'] == true).length; final repsSummary = sets.map((s) => '${s['doneReps']}').join(', '); + final statusColor = succeeded ? status.success : colorScheme.error; + final mutedStyle = TextStyle( + color: colorScheme.onSurfaceVariant, + fontSize: AppTextSize.caption, + ); return Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: succeeded ? Colors.green.shade800 : Colors.red.shade900, - ), + color: colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(AppRadius.sm), + border: Border.all(color: statusColor), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, @@ -781,7 +822,7 @@ class _ExerciseSessionTile extends StatelessWidget { padding: const EdgeInsets.only(top: 2), child: Icon( succeeded ? Icons.check_circle : Icons.cancel, - color: succeeded ? Colors.greenAccent : Colors.redAccent, + color: statusColor, size: 18, ), ), @@ -792,8 +833,8 @@ class _ExerciseSessionTile extends StatelessWidget { children: [ Text( date, - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: colorScheme.onSurface, fontWeight: FontWeight.bold, ), ), @@ -801,29 +842,14 @@ class _ExerciseSessionTile extends StatelessWidget { Text( '${weight ?? '?'}kg · $doneSets/$targetSets sets' ' · ${warmupDone ? '⬤ warmup' : '○ no warmup'}', - style: const TextStyle( - color: Colors.white70, - fontSize: 12, - ), + style: mutedStyle, ), if (repsSummary.isNotEmpty) ...[ const SizedBox(height: 2), - Text( - 'reps: $repsSummary', - style: const TextStyle( - color: Colors.white54, - fontSize: 11, - ), - ), + Text('reps: $repsSummary', style: mutedStyle), ], const SizedBox(height: 2), - Text( - 'workout: $dur', - style: const TextStyle( - color: Colors.white38, - fontSize: 11, - ), - ), + Text('workout: $dur', style: mutedStyle), ], ), ), diff --git a/stronglift_replacement/workout_app/lib/screens/home_screen.dart b/stronglift_replacement/workout_app/lib/screens/home_screen.dart index 8e22948..c0179fa 100644 --- a/stronglift_replacement/workout_app/lib/screens/home_screen.dart +++ b/stronglift_replacement/workout_app/lib/screens/home_screen.dart @@ -10,6 +10,7 @@ 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'; +import 'package:workout_app/ui/theme.dart'; /// Home screen: auto-resumes active sessions and shows done-today status. class HomeScreen extends StatefulWidget { @@ -100,17 +101,17 @@ class _HomeScreenState extends State { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return Scaffold( - backgroundColor: Colors.grey.shade900, appBar: AppBar( - backgroundColor: Colors.grey.shade800, - title: const Text( + backgroundColor: colorScheme.surfaceContainerHigh, + title: Text( 'Workout Tracker', - style: TextStyle(color: Colors.white), + style: TextStyle(color: colorScheme.onSurface), ), actions: [ IconButton( - icon: const Icon(Icons.edit_note, color: Colors.white), + icon: Icon(Icons.edit_note, color: colorScheme.onSurface), tooltip: 'Log manual workout', onPressed: () => Navigator.of(context).push( MaterialPageRoute( @@ -119,13 +120,13 @@ class _HomeScreenState extends State { ), ), IconButton( - icon: const Icon(Icons.history, color: Colors.white), + icon: Icon(Icons.history, color: colorScheme.onSurface), onPressed: () => Navigator.of(context).push( MaterialPageRoute(builder: (_) => const HistoryScreen()), ), ), IconButton( - icon: const Icon(Icons.settings, color: Colors.white), + icon: Icon(Icons.settings, color: colorScheme.onSurface), onPressed: () async { await Navigator.of(context).push( MaterialPageRoute( @@ -182,24 +183,29 @@ class _WorkoutCard extends StatelessWidget { @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: Colors.grey.shade800, + color: colorScheme.surfaceContainerHigh, child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (doneToday && !hasActiveSession) ...[ - const Row( + Row( children: [ - Icon(Icons.check_circle, color: Colors.greenAccent, size: 18), - SizedBox(width: 8), + Icon(Icons.check_circle, color: status.success, size: 18), + const SizedBox(width: 8), Text( 'Done for today!', style: TextStyle( - color: Colors.greenAccent, + color: status.success, fontWeight: FontWeight.bold, - fontSize: 15, + fontSize: AppTextSize.body, ), ), ], @@ -207,9 +213,9 @@ class _WorkoutCard extends StatelessWidget { const SizedBox(height: 6), Text( 'Next: Workout $type — tomorrow', - style: const TextStyle( - color: Colors.white, - fontSize: 17, + style: TextStyle( + color: colorScheme.onSurface, + fontSize: AppTextSize.body, fontWeight: FontWeight.bold, ), ), @@ -219,8 +225,10 @@ class _WorkoutCard extends StatelessWidget { ? 'Workout $type in progress' : 'Next: Workout $type', style: TextStyle( - color: hasActiveSession ? Colors.orangeAccent : Colors.white, - fontSize: 20, + color: hasActiveSession + ? status.warning + : colorScheme.onSurface, + fontSize: AppTextSize.subtitle, fontWeight: FontWeight.bold, ), ), @@ -229,7 +237,10 @@ class _WorkoutCard extends StatelessWidget { ...exercises.map( (e) => Text( '${e.name} ${e.sets}×${e.reps}×${e.weight}kg', - style: const TextStyle(color: Colors.white70, fontSize: 13), + style: TextStyle( + color: colorScheme.onSurfaceVariant, + fontSize: AppTextSize.label, + ), ), ), const SizedBox(height: 14), @@ -238,15 +249,15 @@ class _WorkoutCard extends StatelessWidget { width: double.infinity, child: ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: Colors.orange.shade800, - padding: const EdgeInsets.symmetric(vertical: 14), + backgroundColor: status.warning, + padding: buttonPadding, ), onPressed: onResume, - child: const Text( + child: Text( 'Resume Workout', style: TextStyle( - color: Colors.white, - fontSize: 16, + color: colorScheme.onPrimary, + fontSize: AppTextSize.body, fontWeight: FontWeight.bold, ), ), @@ -257,15 +268,15 @@ class _WorkoutCard extends StatelessWidget { width: double.infinity, child: ElevatedButton( style: ElevatedButton.styleFrom( - backgroundColor: Colors.indigo, - padding: const EdgeInsets.symmetric(vertical: 14), + backgroundColor: colorScheme.primary, + padding: buttonPadding, ), onPressed: onStart, child: Text( 'Start Workout $type', - style: const TextStyle( - color: Colors.white, - fontSize: 16, + style: TextStyle( + color: colorScheme.onPrimary, + fontSize: AppTextSize.body, fontWeight: FontWeight.bold, ), ), @@ -290,6 +301,7 @@ class ServerAddressTile extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; final lines = addresses.isEmpty ? ['Server not started'] : addresses.map((ip) => '$ip:$kWorkoutServerPort').toList(); @@ -297,17 +309,17 @@ class ServerAddressTile extends StatelessWidget { return Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(8), + color: colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( 'HTTP sync (no ADB needed)', style: TextStyle( - color: Colors.white54, - fontSize: 11, + color: colorScheme.onSurfaceVariant, + fontSize: AppTextSize.caption, letterSpacing: 1.1, ), ), @@ -315,10 +327,10 @@ class ServerAddressTile extends StatelessWidget { ...lines.map( (line) => Text( line, - style: const TextStyle( - color: Colors.white70, + style: TextStyle( + color: colorScheme.onSurfaceVariant, fontFamily: 'monospace', - fontSize: 13, + fontSize: AppTextSize.label, ), ), ), 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 6f805bc..e017f53 100644 --- a/stronglift_replacement/workout_app/lib/screens/manual_workout_screen.dart +++ b/stronglift_replacement/workout_app/lib/screens/manual_workout_screen.dart @@ -13,6 +13,7 @@ import 'package:crdt_sync/crdt_sync.dart'; import 'package:flutter/material.dart'; import 'package:workout_app/models/manual_workout.dart'; import 'package:workout_app/services/workout_sync_service.dart'; +import 'package:workout_app/ui/theme.dart'; /// A screen that logs an off-app workout and syncs it to the PC. class ManualWorkoutScreen extends StatefulWidget { @@ -148,15 +149,15 @@ class _ManualWorkoutScreenState extends State { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; final budget = _budget; final exhausted = budget?.exhausted ?? false; return Scaffold( - backgroundColor: Colors.grey.shade900, appBar: AppBar( - backgroundColor: Colors.grey.shade800, - title: const Text( + backgroundColor: colorScheme.surfaceContainerHigh, + title: Text( 'Log Manual Workout', - style: TextStyle(color: Colors.white), + style: TextStyle(color: colorScheme.onSurface), ), ), body: !_budgetLoaded @@ -166,11 +167,11 @@ class _ManualWorkoutScreenState extends State { children: [ _budgetBanner(budget!), if (exhausted) - const Padding( - padding: EdgeInsets.only(top: 8), + Padding( + padding: const EdgeInsets.only(top: 8), child: Text( 'Manual-workout budget exhausted for this window.', - style: TextStyle(color: Color(0xFFFF4444)), + style: TextStyle(color: colorScheme.error), ), ) else ...[ @@ -198,7 +199,7 @@ class _ManualWorkoutScreenState extends State { padding: const EdgeInsets.symmetric(vertical: 8), child: Text( _error!, - style: const TextStyle(color: Color(0xFFFF4444)), + style: TextStyle(color: colorScheme.error), ), ), Padding( @@ -235,16 +236,19 @@ class _ManualWorkoutScreenState extends State { Widget _budgetBanner(ManualBudget budget) => Text( 'Manual: ${budget.week}/$kManualWorkoutBudgetPer7Days this week · ' '${budget.month}/$kManualWorkoutBudgetPer30Days this month', - style: const TextStyle(color: Color(0xFF88CCFF), fontSize: 16), + 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: const TextStyle( - color: Color(0xFF88CCFF), - fontSize: 18, + style: TextStyle( + color: Theme.of(context).colorScheme.primary, + fontSize: AppTextSize.subtitle, fontWeight: FontWeight.bold, ), ), @@ -254,8 +258,8 @@ class _ManualWorkoutScreenState extends State { padding: const EdgeInsets.symmetric(vertical: 4), child: DropdownButton( value: _sport, - dropdownColor: Colors.grey.shade800, - style: const TextStyle(color: Colors.white), + dropdownColor: Theme.of(context).colorScheme.surfaceContainerHigh, + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), isExpanded: true, items: [ for (final entry in kSportLabels.entries) @@ -273,7 +277,7 @@ class _ManualWorkoutScreenState extends State { children: [ Text( 'RPE ${_rpe.round()}', - style: const TextStyle(color: Colors.white), + style: TextStyle(color: Theme.of(context).colorScheme.onSurface), ), Expanded( child: Slider( @@ -295,13 +299,14 @@ class _ManualWorkoutScreenState extends State { key: Key('mw_$key'), controller: _fields[key], maxLines: lines, - style: const TextStyle(color: Colors.white), + 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: Colors.grey.shade400), - filled: true, - fillColor: Colors.grey.shade800, - border: const OutlineInputBorder(), + labelStyle: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), ), ); diff --git a/stronglift_replacement/workout_app/lib/screens/settings_screen.dart b/stronglift_replacement/workout_app/lib/screens/settings_screen.dart index efe9b13..f8d93f4 100644 --- a/stronglift_replacement/workout_app/lib/screens/settings_screen.dart +++ b/stronglift_replacement/workout_app/lib/screens/settings_screen.dart @@ -13,6 +13,7 @@ import 'package:workout_app/models/workout_plan.dart'; import 'package:workout_app/services/github_device_auth.dart'; import 'package:workout_app/services/storage_service.dart'; import 'package:workout_app/services/sync_settings.dart'; +import 'package:workout_app/ui/theme.dart'; /// How to style a [_SyncStatusBadge]. enum _SyncStatusKind { success, pending, error } @@ -222,33 +223,31 @@ class _SettingsScreenState extends State { } Future _resetToDefaults() async { + final colorScheme = Theme.of(context).colorScheme; final ok = await showDialog( context: context, builder: (_) => AlertDialog( - backgroundColor: Colors.grey.shade900, - title: const Text( + backgroundColor: colorScheme.surfaceContainerHigh, + title: Text( 'Reset to defaults?', - style: TextStyle(color: Colors.white), + style: TextStyle(color: colorScheme.onSurface), ), - content: const Text( + content: Text( 'All weights and thresholds will be reset. ' 'Streak counters will be cleared.', - style: TextStyle(color: Colors.white70), + style: TextStyle(color: colorScheme.onSurfaceVariant), ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), - child: const Text( + child: Text( 'Cancel', - style: TextStyle(color: Colors.white70), + style: TextStyle(color: colorScheme.onSurfaceVariant), ), ), TextButton( onPressed: () => Navigator.pop(context, true), - child: const Text( - 'Reset', - style: TextStyle(color: Colors.redAccent), - ), + child: Text('Reset', style: TextStyle(color: colorScheme.error)), ), ], ), @@ -271,18 +270,21 @@ class _SettingsScreenState extends State { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return Scaffold( - backgroundColor: Colors.grey.shade900, appBar: AppBar( - backgroundColor: Colors.grey.shade800, - title: const Text('Settings', style: TextStyle(color: Colors.white)), - iconTheme: const IconThemeData(color: Colors.white), + backgroundColor: colorScheme.surfaceContainerHigh, + title: Text( + 'Settings', + style: TextStyle(color: colorScheme.onSurface), + ), + iconTheme: IconThemeData(color: colorScheme.onSurface), actions: [ TextButton( onPressed: _loading ? null : _resetToDefaults, - child: const Text( + child: Text( 'Reset defaults', - style: TextStyle(color: Colors.redAccent), + style: TextStyle(color: colorScheme.error), ), ), ], @@ -294,10 +296,13 @@ class _SettingsScreenState extends State { children: [ const _SectionHeader('WEIGHTS'), const SizedBox(height: 4), - const Text( + Text( 'Override current working weight. ' 'Resets streak counters. Rounded to 2.5 kg.', - style: TextStyle(color: Colors.white54, fontSize: 12), + style: TextStyle( + color: colorScheme.onSurfaceVariant, + fontSize: AppTextSize.caption, + ), ), const SizedBox(height: 12), ..._orderedNames.map((name) { @@ -312,10 +317,13 @@ class _SettingsScreenState extends State { const SizedBox(height: 20), const _SectionHeader('PROGRESSION THRESHOLDS'), const SizedBox(height: 4), - const Text( + Text( 'Consecutive successes (↑) or failures (↓) ' 'before weight changes.', - style: TextStyle(color: Colors.white54, fontSize: 12), + style: TextStyle( + color: colorScheme.onSurfaceVariant, + fontSize: AppTextSize.caption, + ), ), const SizedBox(height: 12), ..._orderedNames.map((name) { @@ -334,11 +342,14 @@ class _SettingsScreenState extends State { const SizedBox(height: 20), const _SectionHeader('GITHUB SYNC'), const SizedBox(height: 4), - const Text( + Text( 'Authorize in your browser -- no token to paste. Syncs to ' '$syncRepoOwner/$syncRepoName. Workouts push automatically ' 'on completion.', - style: TextStyle(color: Colors.white54, fontSize: 12), + style: TextStyle( + color: colorScheme.onSurfaceVariant, + fontSize: AppTextSize.caption, + ), ), const SizedBox(height: 12), if (_syncStatus != null) ...[ @@ -352,12 +363,15 @@ class _SettingsScreenState extends State { ), const SizedBox(height: 8), ExpansionTile( - title: const Text( + title: Text( 'Advanced: paste a token instead', - style: TextStyle(color: Colors.white70, fontSize: 13), + style: TextStyle( + color: colorScheme.onSurfaceVariant, + fontSize: AppTextSize.label, + ), ), - collapsedIconColor: Colors.white54, - iconColor: Colors.white54, + collapsedIconColor: colorScheme.onSurfaceVariant, + iconColor: colorScheme.onSurfaceVariant, tilePadding: EdgeInsets.zero, childrenPadding: const EdgeInsets.only(top: 8, bottom: 8), children: [ @@ -382,9 +396,9 @@ class _SectionHeader extends StatelessWidget { Widget build(BuildContext context) { return Text( text, - style: const TextStyle( - color: Colors.white54, - fontSize: 11, + style: TextStyle( + color: Theme.of(context).colorScheme.onSurfaceVariant, + fontSize: AppTextSize.caption, letterSpacing: 1.4, ), ); @@ -404,6 +418,7 @@ class _WeightRow extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return Padding( padding: const EdgeInsets.only(bottom: 10), child: Row( @@ -411,7 +426,10 @@ class _WeightRow extends StatelessWidget { Expanded( child: Text( name, - style: const TextStyle(color: Colors.white70, fontSize: 13), + style: TextStyle( + color: colorScheme.onSurfaceVariant, + fontSize: AppTextSize.label, + ), ), ), _StepperButton( @@ -426,9 +444,9 @@ class _WeightRow extends StatelessWidget { child: Text( '${weight}kg', textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.white, - fontSize: 14, + style: TextStyle( + color: colorScheme.onSurface, + fontSize: AppTextSize.label, fontWeight: FontWeight.bold, ), ), @@ -451,17 +469,18 @@ class _StepperButton extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return GestureDetector( onTap: onTap, child: Container( width: 36, height: 36, decoration: BoxDecoration( - color: Colors.grey.shade700, + color: colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(6), ), alignment: Alignment.center, - child: Icon(icon, color: Colors.white, size: 18), + child: Icon(icon, color: colorScheme.onSurface, size: 18), ), ); } @@ -478,10 +497,12 @@ class _SyncStatusBadge extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final status = Theme.of(context).extension()!; final color = switch (kind) { - _SyncStatusKind.success => Colors.greenAccent, - _SyncStatusKind.error => Colors.redAccent, - _SyncStatusKind.pending => Colors.white70, + _SyncStatusKind.success => status.success, + _SyncStatusKind.error => colorScheme.error, + _SyncStatusKind.pending => colorScheme.onSurfaceVariant, }; return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), @@ -512,7 +533,7 @@ class _SyncStatusBadge extends StatelessWidget { text, style: TextStyle( color: color, - fontSize: 13, + fontSize: AppTextSize.label, fontWeight: FontWeight.w600, ), ), @@ -531,6 +552,7 @@ class _SyncTokenField extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -538,16 +560,13 @@ class _SyncTokenField extends StatelessWidget { child: TextField( controller: controller, obscureText: true, - style: const TextStyle(color: Colors.white), + 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: Colors.grey.shade600), - filled: true, - fillColor: Colors.grey.shade800, - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - borderSide: BorderSide.none, - ), + hintStyle: TextStyle(color: colorScheme.onSurfaceVariant), contentPadding: const EdgeInsets.symmetric( horizontal: 12, vertical: 10, @@ -579,36 +598,38 @@ class _ExerciseThresholdCard extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final status = Theme.of(context).extension()!; return Container( margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(14), decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(8), + color: colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( name, - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: colorScheme.onSurface, fontWeight: FontWeight.bold, - fontSize: 14, + fontSize: AppTextSize.label, ), ), const SizedBox(height: 10), _ThresholdRow( label: '↑ Increase after N successes', value: successThreshold, - color: Colors.green, + color: status.success, onChanged: onSuccessChanged, ), const SizedBox(height: 6), _ThresholdRow( label: '↓ Decrease after N failures', value: failThreshold, - color: Colors.red, + color: colorScheme.error, onChanged: onFailChanged, ), ], @@ -632,12 +653,16 @@ class _ThresholdRow extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return Row( children: [ Expanded( child: Text( label, - style: const TextStyle(color: Colors.white70, fontSize: 12), + style: TextStyle( + color: colorScheme.onSurfaceVariant, + fontSize: AppTextSize.caption, + ), ), ), const SizedBox(width: 8), @@ -651,15 +676,20 @@ class _ThresholdRow extends StatelessWidget { height: 32, decoration: BoxDecoration( shape: BoxShape.circle, - color: i == value ? color : Colors.grey.shade700, + color: i == value + ? color + : colorScheme.surfaceContainerHighest, ), alignment: Alignment.center, child: Text( '$i', - style: const TextStyle( - color: Colors.white, + style: TextStyle( + // on-fill on the selected (filled) circle. + color: i == value + ? colorScheme.onPrimary + : colorScheme.onSurface, fontWeight: FontWeight.bold, - fontSize: 13, + fontSize: AppTextSize.label, ), ), ), @@ -711,57 +741,58 @@ class _DeviceCodeDialogState extends State<_DeviceCodeDialog> { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return AlertDialog( - backgroundColor: Colors.grey.shade900, - title: const Text( + backgroundColor: colorScheme.surfaceContainerHigh, + title: Text( 'Authorize on GitHub', - style: TextStyle(color: Colors.white), + style: TextStyle(color: colorScheme.onSurface), ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text( + Text( 'Enter this code on GitHub:', - style: TextStyle(color: Colors.white70), + style: TextStyle(color: colorScheme.onSurfaceVariant), ), const SizedBox(height: 8), SelectableText( widget.device.userCode, - style: const TextStyle( - color: Colors.white, - fontSize: 22, + style: TextStyle( + color: colorScheme.onSurface, + fontSize: AppTextSize.title, fontWeight: FontWeight.bold, ), ), const SizedBox(height: 16), if (_error == null) - const Row( + Row( children: [ - SizedBox( + const SizedBox( width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2), ), - SizedBox(width: 12), + const SizedBox(width: 12), Expanded( child: Text( 'Waiting for authorization…', - style: TextStyle(color: Colors.white70), + style: TextStyle(color: colorScheme.onSurfaceVariant), ), ), ], ) else - Text(_error!, style: const TextStyle(color: Colors.redAccent)), + Text(_error!, style: TextStyle(color: colorScheme.error)), ], ), actions: [ TextButton( onPressed: () => Navigator.of(context).pop(), - child: const Text( + child: Text( 'Cancel', - style: TextStyle(color: Colors.white70), + style: TextStyle(color: colorScheme.onSurfaceVariant), ), ), FilledButton.icon( diff --git a/stronglift_replacement/workout_app/lib/screens/workout_screen.dart b/stronglift_replacement/workout_app/lib/screens/workout_screen.dart index 3df90f6..c0ee22c 100644 --- a/stronglift_replacement/workout_app/lib/screens/workout_screen.dart +++ b/stronglift_replacement/workout_app/lib/screens/workout_screen.dart @@ -13,6 +13,7 @@ import 'package:workout_app/models/workout_session.dart'; import 'package:workout_app/services/storage_service.dart'; import 'package:workout_app/services/sync_service.dart'; import 'package:workout_app/services/workout_sync_service.dart'; +import 'package:workout_app/ui/theme.dart'; import 'package:workout_app/widgets/break_banner.dart'; import 'package:workout_app/widgets/exercise_tile.dart'; import 'package:workout_app/widgets/workout_summary_dialog.dart'; @@ -156,8 +157,8 @@ class _WorkoutScreenState extends State { 'breakDurationSecs': _breakDurationSecs, 'breakEndMs': _breakStartTime != null ? _breakStartTime! - .add(Duration(seconds: _breakDurationSecs)) - .millisecondsSinceEpoch + .add(Duration(seconds: _breakDurationSecs)) + .millisecondsSinceEpoch : 0, }); } @@ -193,8 +194,7 @@ class _WorkoutScreenState extends State { if (wasNotTapped) { _tapped[exIdx][setIdx] = true; } else { - _doneReps[exIdx][setIdx] = - (_doneReps[exIdx][setIdx] - 1).clamp(0, 999); + _doneReps[exIdx][setIdx] = (_doneReps[exIdx][setIdx] - 1).clamp(0, 999); _recomputeBreakIfNeeded(exIdx, setIdx); } }); @@ -284,8 +284,7 @@ class _WorkoutScreenState extends State { if (_breakForExIdx != exIdx || _breakForSetIdx != setIdx) return; if (_breakForSetIdx == -1) return; // warmup break, never recompute - final succeeded = - _doneReps[exIdx][setIdx] >= widget.exercises[exIdx].reps; + final succeeded = _doneReps[exIdx][setIdx] >= widget.exercises[exIdx].reps; final newDuration = succeeded ? _successBreakSecs : _failBreakSecs; if (newDuration == _breakDurationSecs) return; @@ -294,8 +293,9 @@ class _WorkoutScreenState extends State { _breakDurationSecs = newDuration; _breakRemaining = newRemaining; - _breakLabel = - succeeded ? 'Rest (3 min — well done!)' : 'Rest (5 min — keep going!)'; + _breakLabel = succeeded + ? 'Rest (3 min — well done!)' + : 'Rest (5 min — keep going!)'; } Future _onBreakFinished() async { @@ -347,28 +347,27 @@ class _WorkoutScreenState extends State { // ── Finish / Reset ───────────────────────────────────────────────────────── Future _confirmFinish() async { + final colorScheme = Theme.of(context).colorScheme; + final status = Theme.of(context).extension()!; final ok = await showDialog( context: context, builder: (_) => AlertDialog( - backgroundColor: Colors.grey.shade900, - title: const Text( + backgroundColor: colorScheme.surfaceContainerHigh, + title: Text( 'Finish workout?', - style: TextStyle(color: Colors.white), + style: TextStyle(color: colorScheme.onSurface), ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), - child: const Text( + child: Text( 'Cancel', - style: TextStyle(color: Colors.white70), + style: TextStyle(color: colorScheme.onSurfaceVariant), ), ), TextButton( onPressed: () => Navigator.pop(context, true), - child: const Text( - 'Finish', - style: TextStyle(color: Colors.greenAccent), - ), + child: Text('Finish', style: TextStyle(color: status.success)), ), ], ), @@ -377,32 +376,30 @@ class _WorkoutScreenState extends State { } Future _confirmReset() async { + final colorScheme = Theme.of(context).colorScheme; final ok = await showDialog( context: context, builder: (_) => AlertDialog( - backgroundColor: Colors.grey.shade900, - title: const Text( + backgroundColor: colorScheme.surfaceContainerHigh, + title: Text( 'Reset workout?', - style: TextStyle(color: Colors.white), + style: TextStyle(color: colorScheme.onSurface), ), - content: const Text( + content: Text( 'All progress will be lost.', - style: TextStyle(color: Colors.white70), + style: TextStyle(color: colorScheme.onSurfaceVariant), ), actions: [ TextButton( onPressed: () => Navigator.pop(context, false), - child: const Text( + child: Text( 'Cancel', - style: TextStyle(color: Colors.white70), + style: TextStyle(color: colorScheme.onSurfaceVariant), ), ), TextButton( onPressed: () => Navigator.pop(context, true), - child: const Text( - 'Reset', - style: TextStyle(color: Colors.redAccent), - ), + child: Text('Reset', style: TextStyle(color: colorScheme.error)), ), ], ), @@ -486,27 +483,28 @@ class _WorkoutScreenState extends State { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final status = Theme.of(context).extension()!; return PopScope( // Explicit `canPop: true` makes it clear this scope never blocks the back // button — a future reader must not assume the default silently. // ignore: avoid_redundant_argument_values canPop: true, child: Scaffold( - backgroundColor: Colors.grey.shade900, appBar: AppBar( automaticallyImplyLeading: false, - backgroundColor: Colors.grey.shade800, + backgroundColor: colorScheme.surfaceContainerHigh, title: Text( 'Workout ${widget.workoutType} · ${_formatDuration(_elapsed)}', - style: const TextStyle(color: Colors.white), + style: TextStyle(color: colorScheme.onSurface), ), actions: [ if (!_finished) TextButton( onPressed: _confirmReset, - child: const Text( + child: Text( 'Reset', - style: TextStyle(color: Colors.redAccent), + style: TextStyle(color: colorScheme.error), ), ), if (!_finished) @@ -515,7 +513,9 @@ class _WorkoutScreenState extends State { child: Text( 'Finish', style: TextStyle( - color: _allSetsCompleted ? Colors.greenAccent : Colors.grey, + color: _allSetsCompleted + ? status.success + : colorScheme.onSurfaceVariant, fontWeight: FontWeight.bold, ), ), diff --git a/stronglift_replacement/workout_app/lib/ui/theme.dart b/stronglift_replacement/workout_app/lib/ui/theme.dart new file mode 100644 index 0000000..a1c8172 --- /dev/null +++ b/stronglift_replacement/workout_app/lib/ui/theme.dart @@ -0,0 +1,164 @@ +/// The app's design tokens: one identical palette shared across every one +/// of kuhy's apps (Flutter, web, Python/Tkinter alike) — see the +/// `unified-design-system` skill (`~/.claude/skills/unified-design-system/`) +/// for the frozen token table this file implements. Built from an explicit +/// `ColorScheme`, not `ColorScheme.fromSeed`, because the shared palette is +/// hand-picked, not algorithmically derived from one seed color. +library; + +import 'package:flutter/material.dart'; + +/// Builds the app's single dark `ThemeData` from the shared token set. +ThemeData buildAppTheme() { + const colorScheme = ColorScheme.dark( + surface: Color(0xFF211D1B), // ink + surfaceContainerHighest: Color(0xFF38312E), // ink-raised-2 + surfaceContainerHigh: Color(0xFF2B2624), // ink-raised-1 + onSurface: Color(0xFFECEAE9), // text-on-dark + onSurfaceVariant: Color(0xFFAAA09A), // muted-on-dark + outline: Color(0xFF463E3A), // line-dark + primary: Color(0xFFB8862E), // accent + onPrimary: Color(0xFF211D1B), // on-fill + secondary: Color(0xFFB8862E), // accent + onSecondary: Color(0xFF211D1B), // on-fill + secondaryContainer: Color(0xFF463E3A), // line-dark + onSecondaryContainer: Color(0xFFB8862E), // accent + tertiary: Color(0xFFB8862E), // accent + onTertiary: Color(0xFF211D1B), // on-fill + tertiaryContainer: Color(0xFF463E3A), // line-dark + onTertiaryContainer: Color(0xFFB8862E), // accent + error: Color(0xFFE2585F), // danger + onError: Color(0xFF211D1B), // on-fill + ); + return ThemeData( + useMaterial3: true, + colorScheme: colorScheme, + scaffoldBackgroundColor: colorScheme.surface, + extensions: const [AppStatusColors.dark], + textTheme: const TextTheme( + bodyLarge: TextStyle(fontSize: AppTextSize.body), + bodyMedium: TextStyle(fontSize: AppTextSize.body), + titleLarge: TextStyle(fontSize: AppTextSize.title), + titleMedium: TextStyle(fontSize: AppTextSize.subtitle), + labelMedium: TextStyle(fontSize: AppTextSize.label), + labelSmall: TextStyle(fontSize: AppTextSize.caption), + ), + inputDecorationTheme: InputDecorationTheme( + filled: true, + fillColor: colorScheme.surfaceContainerHigh, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppRadius.sm), + borderSide: BorderSide(color: colorScheme.outline), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppRadius.sm), + borderSide: BorderSide(color: colorScheme.outline), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(AppRadius.sm), + borderSide: BorderSide(color: colorScheme.primary, width: 2), + ), + labelStyle: TextStyle(color: colorScheme.onSurfaceVariant), + ), + dividerTheme: DividerThemeData( + color: colorScheme.outline, + thickness: 1, + space: AppSpacing.md, + ), + ); +} + +/// Semantic status colors M3's [ColorScheme] has no role for (it only has +/// `error`). Success/warning are the shared `unified-design-system` values — +/// every status indicator (set completed, break running, budget pending) +/// should read these instead of an ad hoc `Colors.greenAccent`/`orangeAccent`. +@immutable +class AppStatusColors extends ThemeExtension { + /// Creates a status-color set. + const AppStatusColors({required this.success, required this.warning}); + + /// The shared dark-theme instance — success/warning from the unified + /// palette (danger already exists as `colorScheme.error`). + static const dark = AppStatusColors( + success: Color(0xFF8A9A3C), + warning: Color(0xFFE0A63C), + ); + + /// Positive/on-track status (e.g. a completed set, an earned skip). + final Color success; + + /// Caution/pending status (e.g. a running break, a near-exhausted budget). + final Color warning; + + @override + AppStatusColors copyWith({Color? success, Color? warning}) => AppStatusColors( + success: success ?? this.success, + warning: warning ?? this.warning, + ); + + @override + AppStatusColors lerp(AppStatusColors? other, double t) { + if (other is! AppStatusColors) return this; + return AppStatusColors( + success: Color.lerp(success, other.success, t) ?? success, + warning: Color.lerp(warning, other.warning, t) ?? warning, + ); + } +} + +/// Shared spacing scale (4px base) — round any new value to one of these +/// instead of introducing an off-scale literal. +abstract final class AppSpacing { + /// 4px. + static const double xs = 4; + + /// 8px. + static const double sm = 8; + + /// 16px. + static const double md = 16; + + /// 24px. + static const double lg = 24; + + /// 32px. + static const double xl = 32; + + /// 48px. + static const double xxl = 48; +} + +/// Shared corner-radius scale. Nested radii should be `outer - gap`, not a +/// fixed constant — compute per instance per safe-design-rules rule 24. +abstract final class AppRadius { + /// Buttons, inputs, chips. + static const double sm = 8; + + /// Cards. + static const double md = 12; + + /// Sheets, dialogs. + static const double lg = 16; +} + +/// Shared type scale (px). `body` is the floor for anything a user reads; +/// `label`/`caption` are for UI chrome only (timestamps, badges, tags). +abstract final class AppTextSize { + /// 12px — chrome only. + static const double caption = 12; + + /// 14px — chrome only. + static const double label = 14; + + /// 16px — the floor for actual reading content. + static const double body = 16; + + /// 20px. + static const double subtitle = 20; + + /// 24px. + static const double title = 24; + + /// 32px. + static const double display = 32; +} diff --git a/stronglift_replacement/workout_app/lib/widgets/break_banner.dart b/stronglift_replacement/workout_app/lib/widgets/break_banner.dart index 6da7088..5376e42 100644 --- a/stronglift_replacement/workout_app/lib/widgets/break_banner.dart +++ b/stronglift_replacement/workout_app/lib/widgets/break_banner.dart @@ -2,6 +2,7 @@ library; import 'package:flutter/material.dart'; +import 'package:workout_app/ui/theme.dart'; /// Banner widget showing a break countdown and a skip button. class BreakBanner extends StatelessWidget { @@ -30,8 +31,12 @@ class BreakBanner extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final status = Theme.of(context).extension()!; return Container( - color: Colors.indigo.shade900, + // Elevation via fill step (ink-raised-2), not a shadow — differentiates + // the banner from the page without a saturated attention-grabbing fill. + color: colorScheme.surfaceContainerHighest, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), child: Row( children: [ @@ -42,15 +47,19 @@ class BreakBanner extends StatelessWidget { children: [ Text( breakLabel, - style: const TextStyle(color: Colors.white70, fontSize: 12), + style: TextStyle( + color: colorScheme.onSurfaceVariant, + fontSize: AppTextSize.caption, + ), ), Text( _fmt(breakRemaining), - style: const TextStyle( - color: Colors.white, - fontSize: 26, + style: TextStyle( + // Warning (caution/pending) — this is a running countdown. + color: status.warning, + fontSize: AppTextSize.title, fontWeight: FontWeight.bold, - fontFeatures: [FontFeature.tabularFigures()], + fontFeatures: const [FontFeature.tabularFigures()], ), ), ], @@ -58,10 +67,7 @@ class BreakBanner extends StatelessWidget { ), TextButton( onPressed: onSkip, - child: const Text( - 'Skip', - style: TextStyle(color: Colors.white70), - ), + child: Text('Skip', style: TextStyle(color: colorScheme.primary)), ), ], ), diff --git a/stronglift_replacement/workout_app/lib/widgets/calendar_widget.dart b/stronglift_replacement/workout_app/lib/widgets/calendar_widget.dart index ea2bb11..5b7806e 100644 --- a/stronglift_replacement/workout_app/lib/widgets/calendar_widget.dart +++ b/stronglift_replacement/workout_app/lib/widgets/calendar_widget.dart @@ -2,6 +2,7 @@ library; import 'package:flutter/material.dart'; +import 'package:workout_app/ui/theme.dart'; /// Monthly calendar widget that highlights days with completed workouts. class WorkoutCalendar extends StatelessWidget { @@ -48,6 +49,8 @@ class WorkoutCalendar extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final status = Theme.of(context).extension()!; final year = month.year; final m = month.month; final daysInMonth = DateTime(year, m + 1, 0).day; @@ -59,8 +62,8 @@ class WorkoutCalendar extends StatelessWidget { return Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: Colors.grey.shade800, - borderRadius: BorderRadius.circular(8), + color: colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(AppRadius.sm), ), child: Column( children: [ @@ -69,21 +72,27 @@ class WorkoutCalendar extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ IconButton( - icon: const Icon(Icons.chevron_left, color: Colors.white70), + icon: Icon( + Icons.chevron_left, + color: colorScheme.onSurfaceVariant, + ), visualDensity: VisualDensity.compact, padding: EdgeInsets.zero, onPressed: onPrevMonth, ), Text( '${_monthNames[m - 1]} $year', - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: colorScheme.onSurface, fontWeight: FontWeight.bold, - fontSize: 14, + fontSize: AppTextSize.label, ), ), IconButton( - icon: const Icon(Icons.chevron_right, color: Colors.white70), + icon: Icon( + Icons.chevron_right, + color: colorScheme.onSurfaceVariant, + ), visualDensity: VisualDensity.compact, padding: EdgeInsets.zero, onPressed: onNextMonth, @@ -101,9 +110,9 @@ class WorkoutCalendar extends StatelessWidget { child: Text( h, textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.white38, - fontSize: 11, + style: TextStyle( + color: colorScheme.onSurfaceVariant, + fontSize: AppTextSize.caption, fontWeight: FontWeight.bold, ), ), @@ -131,15 +140,18 @@ class WorkoutCalendar extends StatelessWidget { decoration: worked ? BoxDecoration( shape: BoxShape.circle, - color: Colors.green.shade700, + color: status.success, ) : null, alignment: Alignment.center, child: Text( '$day', style: TextStyle( - color: worked ? Colors.white : Colors.white38, - fontSize: 12, + // on-fill (dark) on the success circle, muted otherwise. + color: worked + ? colorScheme.onPrimary + : colorScheme.onSurfaceVariant, + fontSize: AppTextSize.caption, fontWeight: worked ? FontWeight.bold : FontWeight.normal, ), ), diff --git a/stronglift_replacement/workout_app/lib/widgets/exercise_tile.dart b/stronglift_replacement/workout_app/lib/widgets/exercise_tile.dart index ba65cb5..8b6c97e 100644 --- a/stronglift_replacement/workout_app/lib/widgets/exercise_tile.dart +++ b/stronglift_replacement/workout_app/lib/widgets/exercise_tile.dart @@ -3,6 +3,7 @@ library; import 'package:flutter/material.dart'; import 'package:workout_app/models/exercise.dart'; +import 'package:workout_app/ui/theme.dart'; import 'package:workout_app/widgets/rep_circle.dart'; /// Card widget displaying warmup and working-set rep circles for one exercise. @@ -59,10 +60,20 @@ class ExerciseTile extends StatelessWidget { @override Widget build(BuildContext context) { - var headerColor = Colors.grey.shade800; + final colorScheme = Theme.of(context).colorScheme; + final status = Theme.of(context).extension()!; + var headerColor = colorScheme.surfaceContainerHigh; if (_allCompleted) { - headerColor = _allSucceeded ? Colors.green.shade800 : Colors.red.shade900; + headerColor = _allSucceeded ? status.success : colorScheme.error; } + // A filled success/danger card needs on-fill text throughout (tokens.md: + // one on-fill value for all four fills, never a per-fill judgment call). + final onHeader = _allCompleted + ? colorScheme.onPrimary + : colorScheme.onSurface; + final onHeaderMuted = _allCompleted + ? colorScheme.onPrimary + : colorScheme.onSurfaceVariant; return Card( color: headerColor, @@ -76,16 +87,19 @@ class ExerciseTile extends StatelessWidget { Expanded( child: Text( exercise.name, - style: const TextStyle( - color: Colors.white, + style: TextStyle( + color: onHeader, fontWeight: FontWeight.bold, - fontSize: 15, + fontSize: AppTextSize.body, ), ), ), Text( '${exercise.sets}×${exercise.reps}×${exercise.weight}kg', - style: const TextStyle(color: Colors.white70, fontSize: 13), + style: TextStyle( + color: onHeaderMuted, + fontSize: AppTextSize.label, + ), ), ], ), @@ -112,7 +126,8 @@ class ExerciseTile extends StatelessWidget { ), ), ), - const Divider(color: Colors.white12, height: 20), + // Color inherited from the shared dividerTheme (line-dark). + const Divider(height: 20), _ThresholdRow( successThreshold: successThreshold, failThreshold: failThreshold, @@ -141,41 +156,35 @@ class _ThresholdRow extends StatelessWidget { @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: [ - const Icon(Icons.trending_up, size: 13, color: Colors.greenAccent), + Icon(Icons.trending_up, size: 13, color: status.success), const SizedBox(width: 4), - const Text( - 'after', - style: TextStyle(color: Colors.white38, fontSize: 11), - ), + Text('after', style: captionStyle), const SizedBox(width: 6), _MiniStepper( value: successThreshold, onChanged: onSuccessChanged, ), const SizedBox(width: 4), - const Text( - '↑', - style: TextStyle(color: Colors.white38, fontSize: 11), - ), + Text('↑', style: captionStyle), const Spacer(), - const Icon(Icons.trending_down, size: 13, color: Colors.redAccent), + Icon(Icons.trending_down, size: 13, color: colorScheme.error), const SizedBox(width: 4), - const Text( - 'after', - style: TextStyle(color: Colors.white38, fontSize: 11), - ), + Text('after', style: captionStyle), const SizedBox(width: 6), _MiniStepper( value: failThreshold, onChanged: onFailChanged, ), const SizedBox(width: 4), - const Text( - '↓', - style: TextStyle(color: Colors.white38, fontSize: 11), - ), + Text('↓', style: captionStyle), ], ); } @@ -192,40 +201,57 @@ class _MiniStepper extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; return Row( mainAxisSize: MainAxisSize.min, children: [ - _btn(Icons.remove, value > _min ? () => onChanged(value - 1) : null), + _btn( + context, + Icons.remove, + value > _min ? () => onChanged(value - 1) : null, + ), SizedBox( width: 22, child: Text( '$value', textAlign: TextAlign.center, - style: const TextStyle(color: Colors.white, fontSize: 12), + style: TextStyle( + color: colorScheme.onSurface, + fontSize: AppTextSize.caption, + ), ), ), - _btn(Icons.add, value < _max ? () => onChanged(value + 1) : null), + _btn( + context, + Icons.add, + value < _max ? () => onChanged(value + 1) : null, + ), ], ); } - Widget _btn(IconData icon, VoidCallback? onTap) => GestureDetector( - onTap: onTap, - child: Container( - width: 22, - height: 22, - decoration: BoxDecoration( - color: Colors.grey.shade700, - borderRadius: BorderRadius.circular(4), + 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, + ), ), - alignment: Alignment.center, - child: Icon( - icon, - size: 12, - color: onTap != null ? Colors.white : Colors.white24, - ), - ), - ); + ); + } } class _WarmupRow extends StatelessWidget { @@ -241,16 +267,16 @@ class _WarmupRow extends StatelessWidget { @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: [ - const Text( - 'Warmup 1×5×', - style: TextStyle(color: Colors.white54, fontSize: 12), - ), - Text( - '${warmupWeight}kg', - style: const TextStyle(color: Colors.white54, fontSize: 12), - ), + Text('Warmup 1×5×', style: mutedStyle), + Text('${warmupWeight}kg', style: mutedStyle), const SizedBox(width: 10), GestureDetector( onTap: tapped ? null : onTap, @@ -260,16 +286,21 @@ class _WarmupRow extends StatelessWidget { height: 36, decoration: BoxDecoration( shape: BoxShape.circle, - color: tapped ? Colors.teal : Colors.transparent, + // 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 ? Colors.teal : Colors.white38, + color: tapped ? status.success : colorScheme.outline, width: 2, ), ), alignment: Alignment.center, child: Icon( tapped ? Icons.check : Icons.fitness_center, - color: tapped ? Colors.white : Colors.white38, + // on-fill on the filled circle; muted on the empty outline one. + color: tapped + ? colorScheme.onPrimary + : colorScheme.onSurfaceVariant, size: 16, ), ), @@ -278,8 +309,8 @@ class _WarmupRow extends StatelessWidget { Text( tapped ? 'done' : 'optional', style: TextStyle( - color: tapped ? Colors.tealAccent : Colors.white30, - fontSize: 11, + color: tapped ? status.success : colorScheme.onSurfaceVariant, + fontSize: AppTextSize.caption, ), ), ], diff --git a/stronglift_replacement/workout_app/lib/widgets/rep_circle.dart b/stronglift_replacement/workout_app/lib/widgets/rep_circle.dart index f80d2b5..00b9d49 100644 --- a/stronglift_replacement/workout_app/lib/widgets/rep_circle.dart +++ b/stronglift_replacement/workout_app/lib/widgets/rep_circle.dart @@ -13,6 +13,7 @@ library; import 'package:flutter/material.dart'; +import 'package:workout_app/ui/theme.dart'; /// Visual state of a [RepCircle]. enum RepCircleState { @@ -65,27 +66,29 @@ class RepCircle extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final status = Theme.of(context).extension()!; final state = _state; + // on-fill: this circle is a solid filled surface — text on it always + // needs dark (ink) text, never the page's near-white text-on-dark. + final fg = colorScheme.onPrimary; final Color bg; - final Color fg; final String label; switch (state) { case RepCircleState.neutral: - bg = Colors.white; - fg = Colors.black87; + // Not yet acted upon — a near-white fill (not pure Colors.white) + // still reads as "the bright/blank one" against the dark page. + bg = colorScheme.onSurface; label = '$targetReps'; case RepCircleState.success: - bg = Colors.green; - fg = Colors.white; + bg = status.success; label = '$targetReps'; case RepCircleState.partial: - bg = Colors.orange; - fg = Colors.white; + bg = status.warning; label = '$doneReps'; case RepCircleState.failed: - bg = Colors.red; - fg = Colors.white; + bg = colorScheme.error; label = '0'; } @@ -99,16 +102,11 @@ class RepCircle extends StatelessWidget { shape: BoxShape.circle, color: bg, border: Border.all( - color: state == RepCircleState.neutral ? Colors.grey.shade400 : bg, + color: state == RepCircleState.neutral ? colorScheme.outline : bg, width: 2, ), - boxShadow: const [ - BoxShadow( - color: Colors.black26, - blurRadius: 4, - offset: Offset(0, 2), - ), - ], + // No shadow (rule 26: dark surfaces never shadow) — the fill + // color already differentiates this circle from the page. ), alignment: Alignment.center, child: Text( @@ -116,7 +114,7 @@ class RepCircle extends StatelessWidget { style: TextStyle( color: fg, fontWeight: FontWeight.bold, - fontSize: 16, + fontSize: AppTextSize.body, ), ), ), diff --git a/stronglift_replacement/workout_app/lib/widgets/workout_summary_dialog.dart b/stronglift_replacement/workout_app/lib/widgets/workout_summary_dialog.dart index 0eb777d..3af1dc3 100644 --- a/stronglift_replacement/workout_app/lib/widgets/workout_summary_dialog.dart +++ b/stronglift_replacement/workout_app/lib/widgets/workout_summary_dialog.dart @@ -4,6 +4,7 @@ library; import 'package:flutter/material.dart'; import 'package:workout_app/models/workout_session.dart'; import 'package:workout_app/services/sync_service.dart'; +import 'package:workout_app/ui/theme.dart'; /// Dialog that summarises a completed workout and reports the sync status. class WorkoutSummaryDialog extends StatelessWidget { @@ -28,13 +29,15 @@ class WorkoutSummaryDialog extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + final status = Theme.of(context).extension()!; final succeeded = session.fullySucceeded; return AlertDialog( - backgroundColor: Colors.grey.shade900, + backgroundColor: colorScheme.surfaceContainerHigh, title: Text( succeeded ? 'Workout Complete! 💪' : 'Workout Done', style: TextStyle( - color: succeeded ? Colors.greenAccent : Colors.orangeAccent, + color: succeeded ? status.success : status.warning, fontWeight: FontWeight.bold, ), ), @@ -44,14 +47,14 @@ class WorkoutSummaryDialog extends StatelessWidget { children: [ Text( 'Duration: ${_fmt(session.duration)}', - style: const TextStyle(color: Colors.white70), + style: TextStyle(color: colorScheme.onSurfaceVariant), ), const SizedBox(height: 8), ...session.exercises.map( (e) => Text( '${e.exercise.name}: ${e.succeeded ? "✓" : "✗"}', style: TextStyle( - color: e.succeeded ? Colors.greenAccent : Colors.redAccent, + color: e.succeeded ? status.success : colorScheme.error, ), ), ), @@ -61,8 +64,10 @@ class WorkoutSummaryDialog extends StatelessWidget { ? 'Saved to ${syncResult.path}' : 'Sync failed: ${syncResult.error}', style: TextStyle( - color: syncResult.success ? Colors.white54 : Colors.redAccent, - fontSize: 12, + color: syncResult.success + ? colorScheme.onSurfaceVariant + : colorScheme.error, + fontSize: AppTextSize.caption, ), ), ], @@ -72,9 +77,9 @@ class WorkoutSummaryDialog extends StatelessWidget { onPressed: () { Navigator.of(context).popUntil((r) => r.isFirst); }, - child: const Text( + child: Text( 'Back to Home', - style: TextStyle(color: Colors.white), + style: TextStyle(color: colorScheme.primary), ), ), ], diff --git a/stronglift_replacement/workout_app/test/screens/history_screen_test.dart b/stronglift_replacement/workout_app/test/screens/history_screen_test.dart index bd20f38..38de324 100644 --- a/stronglift_replacement/workout_app/test/screens/history_screen_test.dart +++ b/stronglift_replacement/workout_app/test/screens/history_screen_test.dart @@ -9,6 +9,7 @@ import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:workout_app/models/workout_plan.dart'; import 'package:workout_app/screens/history_screen.dart'; import 'package:workout_app/services/storage_service.dart'; +import 'package:workout_app/ui/theme.dart'; import '../fake_secure_storage.dart'; @@ -35,8 +36,10 @@ void main() { await tester.pump(); } - Widget _wrap({http.Client? httpClient}) => - MaterialApp(home: HistoryScreen(httpClient: httpClient)); + Widget _wrap({http.Client? httpClient}) => MaterialApp( + theme: buildAppTheme(), + home: HistoryScreen(httpClient: httpClient), + ); /// A GitHub mock serving one PC device log holding a run and a manual. http.Client _syncMock() { @@ -72,9 +75,12 @@ void main() { return MockClient((req) async { final path = req.url.path; if (path.endsWith('screen-locker-sync/devices')) { - return http.Response(jsonEncode([ - {'name': 'pc', 'type': 'dir'}, - ]), 200); + return http.Response( + jsonEncode([ + {'name': 'pc', 'type': 'dir'}, + ]), + 200, + ); } if (path.contains('pc/log.json')) { return http.Response(file(log), 200); @@ -136,8 +142,9 @@ void main() { expect(find.textContaining('Workout A'), findsWidgets); }); - testWidgets('exercise picker shows "Total (all workouts)" initially', - (tester) async { + testWidgets('exercise picker shows "Total (all workouts)" initially', ( + tester, + ) async { final json = jsonEncode({ 'exercises': [ { @@ -181,8 +188,9 @@ void main() { expect(find.byIcon(Icons.cancel), findsWidgets); }); - testWidgets('session duration over 1 hour formats with h prefix', - (tester) async { + testWidgets('session duration over 1 hour formats with h prefix', ( + tester, + ) async { final json = jsonEncode({'exercises': []}); await _seed(tester, json, duration: 3700); await _pump(tester, _wrap()); @@ -248,7 +256,9 @@ void main() { }); } - testWidgets('selecting an exercise shows the drill-down view', (tester) async { + testWidgets('selecting an exercise shows the drill-down view', ( + tester, + ) async { // Use a real progression-tracked exercise so getExerciseState is non-null // and the streak stats card renders. Three distinct weights on three dates // give the weight chart >= 2 points with a real range (painter fully runs). @@ -260,7 +270,13 @@ void main() { ); await _seed( tester, - _exerciseSessionJson(name, 22.5, ok: false, warmup: false, withSets: false), + _exerciseSessionJson( + name, + 22.5, + ok: false, + warmup: false, + withSets: false, + ), date: '2024-06-08', succeeded: false, ); @@ -308,23 +324,24 @@ void main() { expect(find.textContaining('reps:'), findsWidgets); }); - testWidgets('chart handles two points on the same date (tRange == 0)', - (tester) async { + testWidgets('chart handles two points on the same date (tRange == 0)', ( + tester, + ) async { // Two sessions on the SAME date give the total chart two points that share // a timestamp, exercising the painter's tRange == 0 (centre-x) branch. String vol(double w) => jsonEncode({ - 'exercises': [ - { - 'name': 'Squat', - 'targetSets': 3, - 'targetReps': 5, - 'targetWeight': w, - 'warmupDone': false, - 'succeeded': true, - 'sets': >[], - }, - ], - }); + 'exercises': [ + { + 'name': 'Squat', + 'targetSets': 3, + 'targetReps': 5, + 'targetWeight': w, + 'warmupDone': false, + 'succeeded': true, + 'sets': >[], + }, + ], + }); await _seed(tester, vol(20), date: '2024-06-01'); await _seed(tester, vol(40), date: '2024-06-01'); await _pump(tester, _wrap()); diff --git a/stronglift_replacement/workout_app/test/screens/home_screen_test.dart b/stronglift_replacement/workout_app/test/screens/home_screen_test.dart index 6016324..9ef81b6 100644 --- a/stronglift_replacement/workout_app/test/screens/home_screen_test.dart +++ b/stronglift_replacement/workout_app/test/screens/home_screen_test.dart @@ -5,6 +5,7 @@ import 'package:workout_app/models/workout_plan.dart'; import 'package:workout_app/screens/home_screen.dart'; import 'package:workout_app/screens/workout_screen.dart'; import 'package:workout_app/services/storage_service.dart'; +import 'package:workout_app/ui/theme.dart'; import '../fake_secure_storage.dart'; @@ -29,7 +30,8 @@ void main() { await tester.pump(); } - Widget _wrap() => const MaterialApp(home: HomeScreen()); + Widget _wrap() => + MaterialApp(theme: buildAppTheme(), home: const HomeScreen()); testWidgets('shows Workout Tracker app bar', (tester) async { await _pump(tester, _wrap()); @@ -62,8 +64,9 @@ void main() { expect(find.text('Settings'), findsOneWidget); }); - testWidgets('"Done for today" message shows after saving a session today', - (tester) async { + testWidgets('"Done for today" message shows after saving a session today', ( + tester, + ) async { final today = DateTime.now(); final dateStr = '${today.year}-${today.month.toString().padLeft(2, '0')}' @@ -88,7 +91,9 @@ void main() { expect(find.text('HTTP sync (no ADB needed)'), findsOneWidget); }); - testWidgets('manual-workout icon navigates to the manual form', (tester) async { + testWidgets('manual-workout icon navigates to the manual form', ( + tester, + ) async { installFakeSecureStorage(); // ManualWorkoutScreen loads its sync budget await _pump(tester, _wrap()); await tester.tap(find.byIcon(Icons.edit_note)); @@ -97,8 +102,9 @@ void main() { expect(find.text('Log Manual Workout'), findsOneWidget); }); - testWidgets('starting a workout navigates to the workout screen', - (tester) async { + testWidgets('starting a workout navigates to the workout screen', ( + tester, + ) async { await _pump(tester, _wrap()); await tester.tap(find.text('Start Workout A')); await tester.pump(); @@ -106,8 +112,9 @@ void main() { expect(find.textContaining('Workout A'), findsWidgets); }); - testWidgets('an active session auto-resumes into the workout screen', - (tester) async { + testWidgets('an active session auto-resumes into the workout screen', ( + tester, + ) async { await tester.runAsync( () => StorageService.instance.saveActiveSession({ 'workoutType': 'A', @@ -121,8 +128,9 @@ void main() { expect(find.textContaining('Workout A'), findsWidgets); }); - testWidgets('active session shows Resume, returns (98) and re-enters (153)', - (tester) async { + testWidgets('active session shows Resume, returns (98) and re-enters (153)', ( + tester, + ) async { // A fully-formed active session so WorkoutScreen restores cleanly (its // _restoreFromSaved expects startTimeMs + per-exercise tapped/doneReps). await tester.runAsync( @@ -130,7 +138,9 @@ void main() { 'workoutType': 'A', 'startTimeMs': DateTime.now().millisecondsSinceEpoch, 'tapped': [for (final e in workoutA) List.filled(e.sets, false)], - 'doneReps': [for (final e in workoutA) List.filled(e.sets, e.reps)], + 'doneReps': [ + for (final e in workoutA) List.filled(e.sets, e.reps), + ], 'warmupTapped': List.filled(workoutA.length, false), }), ); @@ -166,8 +176,9 @@ void main() { expect(find.byType(WorkoutScreen), findsOneWidget); }); - testWidgets('returning from settings reloads the home screen', - (tester) async { + testWidgets('returning from settings reloads the home screen', ( + tester, + ) async { installFakeSecureStorage(); // SettingsScreen reads the sync token on init await _pump(tester, _wrap()); await tester.tap(find.byIcon(Icons.settings)); @@ -185,8 +196,9 @@ void main() { expect(find.text('Workout Tracker'), findsOneWidget); }); - testWidgets('ServerAddressTile shows a placeholder when no addresses', - (tester) async { + testWidgets('ServerAddressTile shows a placeholder when no addresses', ( + tester, + ) async { await tester.pumpWidget( const MaterialApp( home: Scaffold(body: ServerAddressTile(addresses: [])), diff --git a/stronglift_replacement/workout_app/test/screens/settings_screen_test.dart b/stronglift_replacement/workout_app/test/screens/settings_screen_test.dart index dda29a2..58e85a9 100644 --- a/stronglift_replacement/workout_app/test/screens/settings_screen_test.dart +++ b/stronglift_replacement/workout_app/test/screens/settings_screen_test.dart @@ -16,6 +16,7 @@ import 'package:workout_app/services/sync_settings.dart'; import 'package:workout_app/models/workout_plan.dart'; import 'package:workout_app/screens/settings_screen.dart'; import 'package:workout_app/services/storage_service.dart'; +import 'package:workout_app/ui/theme.dart'; import '../fake_secure_storage.dart'; @@ -59,8 +60,10 @@ void main() { await tester.pump(); } - Widget _wrap({http.Client? httpClient}) => - MaterialApp(home: SettingsScreen(httpClient: httpClient)); + Widget _wrap({http.Client? httpClient}) => MaterialApp( + theme: buildAppTheme(), + home: SettingsScreen(httpClient: httpClient), + ); /// Drains the device flow's real `Future.delayed` poll (GitHubDeviceAuth /// injects no test delay, so under `runAsync` it is a genuine Timer, not @@ -94,14 +97,17 @@ void main() { expect(find.text('Settings'), findsOneWidget); }); - testWidgets('shows WEIGHTS and PROGRESSION THRESHOLDS sections', - (tester) async { + testWidgets('shows WEIGHTS and PROGRESSION THRESHOLDS sections', ( + tester, + ) async { await _pump(tester, _wrap()); expect(find.text('WEIGHTS'), findsOneWidget); expect(find.text('PROGRESSION THRESHOLDS'), findsOneWidget); }); - testWidgets('shows all exercise names from both workout plans', (tester) async { + testWidgets('shows all exercise names from both workout plans', ( + tester, + ) async { await _pump(tester, _wrap()); for (final ex in [...workoutA, ...workoutB]) { expect(find.text(ex.name), findsWidgets); @@ -169,8 +175,8 @@ void main() { testWidgets('Reset dialog confirms and resets data', (tester) async { await tester.runAsync( - () => StorageService.instance - .setExerciseWeight(workoutA.first.name, 99.0), + () => + StorageService.instance.setExerciseWeight(workoutA.first.name, 99.0), ); await _pump(tester, _wrap()); @@ -273,11 +279,10 @@ void main() { await tester.tap(find.text('Connect GitHub')); await _pumpUntil( tester, - () => - find - .textContaining('Could not start device flow') - .evaluate() - .isNotEmpty, + () => find + .textContaining('Could not start device flow') + .evaluate() + .isNotEmpty, ); expect( @@ -328,11 +333,10 @@ void main() { await _pumpUntil( tester, - () => - find - .textContaining('Connected and verified via GitHub') - .evaluate() - .isNotEmpty, + () => find + .textContaining('Connected and verified via GitHub') + .evaluate() + .isNotEmpty, ); expect( @@ -437,9 +441,12 @@ void main() { final failLabel = find.text('↓ Decrease after N failures').first; await tester.ensureVisible(failLabel); await tester.pumpAndSettle(); - final failRow = - find.ancestor(of: failLabel, matching: find.byType(Row)).first; - final failFour = find.descendant(of: failRow, matching: find.text('4')).first; + final failRow = find + .ancestor(of: failLabel, matching: find.byType(Row)) + .first; + final failFour = find + .descendant(of: failRow, matching: find.text('4')) + .first; await tester.ensureVisible(failFour); await tester.pumpAndSettle(); await tester.runAsync(() async { @@ -513,8 +520,10 @@ void main() { await tester.tap(find.text('Connect GitHub')); await _pumpUntil( tester, - () => - find.textContaining('could not save the token').evaluate().isNotEmpty, + () => find + .textContaining('could not save the token') + .evaluate() + .isNotEmpty, ); expect( find.textContaining('could not save the token'), @@ -606,10 +615,7 @@ void main() { await _pumpUntil( tester, () => - find - .textContaining('Connected and verified') - .evaluate() - .isNotEmpty, + find.textContaining('Connected and verified').evaluate().isNotEmpty, ); expect(find.textContaining('Connected and verified'), findsOneWidget); }); diff --git a/stronglift_replacement/workout_app/test/screens/workout_screen_test.dart b/stronglift_replacement/workout_app/test/screens/workout_screen_test.dart index 7d88ee5..47ae208 100644 --- a/stronglift_replacement/workout_app/test/screens/workout_screen_test.dart +++ b/stronglift_replacement/workout_app/test/screens/workout_screen_test.dart @@ -5,6 +5,7 @@ import 'package:workout_app/models/exercise.dart'; import 'package:workout_app/models/workout_plan.dart'; import 'package:workout_app/screens/workout_screen.dart'; import 'package:workout_app/services/storage_service.dart'; +import 'package:workout_app/ui/theme.dart'; import 'package:workout_app/widgets/break_banner.dart'; import 'package:workout_app/widgets/exercise_tile.dart'; import 'package:workout_app/widgets/rep_circle.dart'; @@ -15,18 +16,18 @@ import '../fake_secure_storage.dart'; /// A saved state with every set of the two [_exercises] tapped as done. Map _completeSaved() => { - 'workoutType': 'A', - 'startTimeMs': DateTime.now().millisecondsSinceEpoch, - 'tapped': [ - [true, true, true], - [true, true, true], - ], - 'doneReps': [ - [5, 5, 5], - [5, 5, 5], - ], - 'warmupTapped': [false, false], - }; + 'workoutType': 'A', + 'startTimeMs': DateTime.now().millisecondsSinceEpoch, + 'tapped': [ + [true, true, true], + [true, true, true], + ], + 'doneReps': [ + [5, 5, 5], + [5, 5, 5], + ], + 'warmupTapped': [false, false], +}; const _exercises = [ Exercise(name: 'Squat', sets: 3, reps: 5, weight: 20.0), @@ -37,14 +38,14 @@ Widget _wrap({ String type = 'A', List exercises = _exercises, Map? savedState, -}) => - MaterialApp( - home: WorkoutScreen( - workoutType: type, - exercises: exercises, - savedState: savedState, - ), - ); +}) => MaterialApp( + theme: buildAppTheme(), + home: WorkoutScreen( + workoutType: type, + exercises: exercises, + savedState: savedState, + ), +); void main() { setUpAll(() { @@ -89,7 +90,9 @@ void main() { expect(find.text('Finish'), findsOneWidget); }); - testWidgets('Finish button is disabled when not all sets done', (tester) async { + testWidgets('Finish button is disabled when not all sets done', ( + tester, + ) async { await _pump(tester, _wrap()); final finishButton = tester.widget( find.widgetWithText(TextButton, 'Finish'), @@ -121,8 +124,9 @@ void main() { final now = DateTime.now(); final saved = { 'workoutType': 'A', - 'startTimeMs': - now.subtract(const Duration(minutes: 10)).millisecondsSinceEpoch, + 'startTimeMs': now + .subtract(const Duration(minutes: 10)) + .millisecondsSinceEpoch, 'tapped': [ [true, true, true], [true, true, true], @@ -200,7 +204,9 @@ void main() { await tester.pump(); } - testWidgets('tapping a set starts a break which Skip cancels', (tester) async { + testWidgets('tapping a set starts a break which Skip cancels', ( + tester, + ) async { await _pump(tester, _wrap()); await _tapReal(tester, find.byType(RepCircle).first); expect(find.byType(BreakBanner), findsOneWidget); @@ -217,8 +223,9 @@ void main() { expect(find.textContaining('Warmup rest'), findsOneWidget); }); - testWidgets('re-tapping a set decrements reps and recomputes the break', - (tester) async { + testWidgets('re-tapping a set decrements reps and recomputes the break', ( + tester, + ) async { await _pump(tester, _wrap()); final circle = find.byType(RepCircle).first; await _tapReal(tester, circle); // done=5 -> success break @@ -228,8 +235,9 @@ void main() { expect(find.textContaining('keep going'), findsOneWidget); }); - testWidgets('long-pressing a set resets it and cancels its break', - (tester) async { + testWidgets('long-pressing a set resets it and cancels its break', ( + tester, + ) async { await _pump(tester, _wrap()); final circle = find.byType(RepCircle).first; await _tapReal(tester, circle); @@ -239,8 +247,9 @@ void main() { expect(find.byType(BreakBanner), findsNothing); }); - testWidgets('finishing a completed workout saves and shows the summary', - (tester) async { + testWidgets('finishing a completed workout saves and shows the summary', ( + tester, + ) async { await _pump(tester, _wrap(savedState: _completeSaved())); // Drive the whole flow on the real loop: _confirmFinish's showDialog await @@ -299,7 +308,9 @@ void main() { expect(session, isNull); // reset cleared the persisted session }); - testWidgets('changing a threshold in the workout persists it', (tester) async { + testWidgets('changing a threshold in the workout persists it', ( + tester, + ) async { // A real exercise has a progression-state row, so _onThresholdChanged's // state-update branch runs (and the write actually lands). final name = workoutA.first.name; diff --git a/stronglift_replacement/workout_app/test/ui/theme_test.dart b/stronglift_replacement/workout_app/test/ui/theme_test.dart new file mode 100644 index 0000000..e59338e --- /dev/null +++ b/stronglift_replacement/workout_app/test/ui/theme_test.dart @@ -0,0 +1,57 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:workout_app/ui/theme.dart'; + +void main() { + group('buildAppTheme', () { + test('uses the shared palette and 16px body floor', () { + final theme = buildAppTheme(); + + expect(theme.colorScheme.brightness, Brightness.dark); + expect(theme.colorScheme.primary, const Color(0xFFB8862E)); + expect(theme.colorScheme.onPrimary, const Color(0xFF211D1B)); + expect(theme.textTheme.bodyLarge!.fontSize, AppTextSize.body); + expect(theme.textTheme.bodyMedium!.fontSize, AppTextSize.body); + }); + + test('registers AppStatusColors', () { + final theme = buildAppTheme(); + expect(theme.extension(), AppStatusColors.dark); + }); + }); + + group('AppStatusColors', () { + const colors = AppStatusColors.dark; + + test('copyWith overrides only the given fields', () { + final overridden = colors.copyWith(success: Colors.red); + expect(overridden.success, Colors.red); + expect(overridden.warning, colors.warning); + }); + + test('copyWith with no args returns equivalent values', () { + final same = colors.copyWith(); + expect(same.success, colors.success); + expect(same.warning, colors.warning); + }); + + test('lerp at t=0 returns this', () { + const other = AppStatusColors(success: Colors.red, warning: Colors.blue); + final result = colors.lerp(other, 0); + expect(result.success, colors.success); + expect(result.warning, colors.warning); + }); + + test('lerp at t=1 matches Color.lerp at t=1', () { + const other = AppStatusColors(success: Colors.red, warning: Colors.blue); + final result = colors.lerp(other, 1); + expect(result.success, Color.lerp(colors.success, other.success, 1)); + expect(result.warning, Color.lerp(colors.warning, other.warning, 1)); + }); + + test('lerp with a non-AppStatusColors other returns this unchanged', () { + final result = colors.lerp(null, 0.5); + expect(result, same(colors)); + }); + }); +} diff --git a/stronglift_replacement/workout_app/test/widgets/break_banner_test.dart b/stronglift_replacement/workout_app/test/widgets/break_banner_test.dart index 86342ec..f4e3065 100644 --- a/stronglift_replacement/workout_app/test/widgets/break_banner_test.dart +++ b/stronglift_replacement/workout_app/test/widgets/break_banner_test.dart @@ -1,8 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:workout_app/ui/theme.dart'; import 'package:workout_app/widgets/break_banner.dart'; -Widget _wrap(Widget child) => MaterialApp(home: Scaffold(body: child)); +Widget _wrap(Widget child) => MaterialApp( + theme: buildAppTheme(), + home: Scaffold(body: child), +); void main() { group('BreakBanner', () { diff --git a/stronglift_replacement/workout_app/test/widgets/calendar_widget_test.dart b/stronglift_replacement/workout_app/test/widgets/calendar_widget_test.dart index b7b0adc..8114896 100644 --- a/stronglift_replacement/workout_app/test/widgets/calendar_widget_test.dart +++ b/stronglift_replacement/workout_app/test/widgets/calendar_widget_test.dart @@ -1,8 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:workout_app/ui/theme.dart'; import 'package:workout_app/widgets/calendar_widget.dart'; -Widget _wrap(Widget child) => MaterialApp(home: Scaffold(body: child)); +Widget _wrap(Widget child) => MaterialApp( + theme: buildAppTheme(), + home: Scaffold(body: child), +); void main() { group('WorkoutCalendar', () { diff --git a/stronglift_replacement/workout_app/test/widgets/exercise_tile_test.dart b/stronglift_replacement/workout_app/test/widgets/exercise_tile_test.dart index 7709120..d82a406 100644 --- a/stronglift_replacement/workout_app/test/widgets/exercise_tile_test.dart +++ b/stronglift_replacement/workout_app/test/widgets/exercise_tile_test.dart @@ -1,10 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:workout_app/ui/theme.dart'; import 'package:workout_app/models/exercise.dart'; import 'package:workout_app/widgets/exercise_tile.dart'; import 'package:workout_app/widgets/rep_circle.dart'; -Widget _wrap(Widget child) => MaterialApp(home: Scaffold(body: child)); +Widget _wrap(Widget child) => MaterialApp( + theme: buildAppTheme(), + home: Scaffold(body: child), +); const _exercise = Exercise(name: 'Squat', sets: 3, reps: 5, weight: 20.0); @@ -18,19 +22,18 @@ ExerciseTile _tile({ void Function(int)? onLongPressCircle, VoidCallback? onTapWarmup, void Function(int, int)? onThresholdChanged, -}) => - ExerciseTile( - exercise: _exercise, - tapped: tapped ?? [false, false, false], - doneReps: doneReps ?? [5, 5, 5], - warmupTapped: warmupTapped, - successThreshold: successThreshold, - failThreshold: failThreshold, - onTapCircle: onTapCircle ?? (_) {}, - onLongPressCircle: onLongPressCircle ?? (_) {}, - onTapWarmup: onTapWarmup ?? () {}, - onThresholdChanged: onThresholdChanged ?? (_, __) {}, - ); +}) => ExerciseTile( + exercise: _exercise, + tapped: tapped ?? [false, false, false], + doneReps: doneReps ?? [5, 5, 5], + warmupTapped: warmupTapped, + successThreshold: successThreshold, + failThreshold: failThreshold, + onTapCircle: onTapCircle ?? (_) {}, + onLongPressCircle: onLongPressCircle ?? (_) {}, + onTapWarmup: onTapWarmup ?? () {}, + onThresholdChanged: onThresholdChanged ?? (_, __) {}, +); void main() { group('ExerciseTile', () { @@ -43,7 +46,10 @@ void main() { testWidgets('shows warmup weight', (tester) async { await tester.pumpWidget(_wrap(_tile())); // warmupWeight for 20kg squat = 10kg (50%) - expect(find.textContaining('${_exercise.warmupWeight}kg'), findsOneWidget); + expect( + find.textContaining('${_exercise.warmupWeight}kg'), + findsOneWidget, + ); }); testWidgets('calls onTapCircle when set circle tapped', (tester) async { @@ -87,7 +93,9 @@ void main() { expect(find.byType(ExerciseTile), findsOneWidget); }); - testWidgets('header is red when all sets tapped but some failed', (tester) async { + testWidgets('header is red when all sets tapped but some failed', ( + tester, + ) async { await tester.pumpWidget( _wrap( _tile( diff --git a/stronglift_replacement/workout_app/test/widgets/rep_circle_test.dart b/stronglift_replacement/workout_app/test/widgets/rep_circle_test.dart index ad0d95f..f8825f8 100644 --- a/stronglift_replacement/workout_app/test/widgets/rep_circle_test.dart +++ b/stronglift_replacement/workout_app/test/widgets/rep_circle_test.dart @@ -1,12 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:workout_app/ui/theme.dart'; import 'package:workout_app/widgets/rep_circle.dart'; -Widget _wrap(Widget child) => MaterialApp(home: Scaffold(body: child)); +Widget _wrap(Widget child) => MaterialApp( + theme: buildAppTheme(), + home: Scaffold(body: child), +); void main() { group('RepCircle states', () { - testWidgets('neutral shows target reps and white background', (tester) async { + testWidgets('neutral shows target reps and white background', ( + tester, + ) async { var tapped = false; await tester.pumpWidget( _wrap( @@ -25,7 +31,9 @@ void main() { expect(tapped, isTrue); }); - testWidgets('success state (tapped, doneReps == targetReps)', (tester) async { + testWidgets('success state (tapped, doneReps == targetReps)', ( + tester, + ) async { await tester.pumpWidget( _wrap( RepCircle( @@ -40,7 +48,9 @@ void main() { expect(find.text('5'), findsOneWidget); }); - testWidgets('partial state (tapped, 0 < doneReps < targetReps)', (tester) async { + testWidgets('partial state (tapped, 0 < doneReps < targetReps)', ( + tester, + ) async { await tester.pumpWidget( _wrap( RepCircle( @@ -90,7 +100,10 @@ void main() { group('RepCircleState enum', () { test('all values are distinct', () { - expect(RepCircleState.values.toSet().length, RepCircleState.values.length); + expect( + RepCircleState.values.toSet().length, + RepCircleState.values.length, + ); }); }); } diff --git a/stronglift_replacement/workout_app/test/widgets/workout_summary_dialog_test.dart b/stronglift_replacement/workout_app/test/widgets/workout_summary_dialog_test.dart index a5465f3..111b6dd 100644 --- a/stronglift_replacement/workout_app/test/widgets/workout_summary_dialog_test.dart +++ b/stronglift_replacement/workout_app/test/widgets/workout_summary_dialog_test.dart @@ -5,9 +5,13 @@ import 'package:workout_app/models/exercise_result.dart'; import 'package:workout_app/models/set_result.dart'; import 'package:workout_app/models/workout_session.dart'; import 'package:workout_app/services/sync_service.dart'; +import 'package:workout_app/ui/theme.dart'; import 'package:workout_app/widgets/workout_summary_dialog.dart'; -WorkoutSession _session({bool allSucceeded = true, Duration duration = const Duration(minutes: 45, seconds: 30)}) { +WorkoutSession _session({ + bool allSucceeded = true, + Duration duration = const Duration(minutes: 45, seconds: 30), +}) { final start = DateTime(2024, 6, 1, 9); final end = start.add(duration); return WorkoutSession( @@ -32,9 +36,12 @@ WorkoutSession _session({bool allSucceeded = true, Duration duration = const Dur void main() { group('WorkoutSummaryDialog', () { - testWidgets('shows "Workout Complete!" when fully succeeded', (tester) async { + testWidgets('shows "Workout Complete!" when fully succeeded', ( + tester, + ) async { await tester.pumpWidget( MaterialApp( + theme: buildAppTheme(), home: WorkoutSummaryDialog( session: _session(), syncResult: const SyncResult( @@ -47,9 +54,12 @@ void main() { expect(find.textContaining('Workout Complete'), findsOneWidget); }); - testWidgets('shows "Workout Done" when not fully succeeded', (tester) async { + testWidgets('shows "Workout Done" when not fully succeeded', ( + tester, + ) async { await tester.pumpWidget( MaterialApp( + theme: buildAppTheme(), home: WorkoutSummaryDialog( session: _session(allSucceeded: false), syncResult: const SyncResult( @@ -63,9 +73,12 @@ void main() { expect(find.text('Workout Done'), findsOneWidget); }); - testWidgets('shows duration in mm m ss s format for sub-hour workout', (tester) async { + testWidgets('shows duration in mm m ss s format for sub-hour workout', ( + tester, + ) async { await tester.pumpWidget( MaterialApp( + theme: buildAppTheme(), home: WorkoutSummaryDialog( session: _session(), syncResult: const SyncResult(success: true, path: '/p'), @@ -78,8 +91,11 @@ void main() { testWidgets('shows hours in duration for long workouts', (tester) async { await tester.pumpWidget( MaterialApp( + theme: buildAppTheme(), home: WorkoutSummaryDialog( - session: _session(duration: const Duration(hours: 1, minutes: 5, seconds: 3)), + session: _session( + duration: const Duration(hours: 1, minutes: 5, seconds: 3), + ), syncResult: const SyncResult(success: true, path: '/p'), ), ), @@ -87,9 +103,12 @@ void main() { expect(find.textContaining('1h'), findsOneWidget); }); - testWidgets('shows exercise name with check mark on success', (tester) async { + testWidgets('shows exercise name with check mark on success', ( + tester, + ) async { await tester.pumpWidget( MaterialApp( + theme: buildAppTheme(), home: WorkoutSummaryDialog( session: _session(), syncResult: const SyncResult(success: true, path: '/p'), @@ -102,9 +121,14 @@ void main() { testWidgets('shows exercise name with cross on failure', (tester) async { await tester.pumpWidget( MaterialApp( + theme: buildAppTheme(), home: WorkoutSummaryDialog( session: _session(allSucceeded: false), - syncResult: const SyncResult(success: false, path: null, error: 'err'), + syncResult: const SyncResult( + success: false, + path: null, + error: 'err', + ), ), ), ); @@ -114,6 +138,7 @@ void main() { testWidgets('shows saved path on sync success', (tester) async { await tester.pumpWidget( MaterialApp( + theme: buildAppTheme(), home: WorkoutSummaryDialog( session: _session(), syncResult: const SyncResult( @@ -132,6 +157,7 @@ void main() { testWidgets('shows error message on sync failure', (tester) async { await tester.pumpWidget( MaterialApp( + theme: buildAppTheme(), home: WorkoutSummaryDialog( session: _session(allSucceeded: false), syncResult: const SyncResult( @@ -151,6 +177,7 @@ void main() { testWidgets('"Back to Home" button pops to first route', (tester) async { await tester.pumpWidget( MaterialApp( + theme: buildAppTheme(), home: Scaffold( body: WorkoutSummaryDialog( session: _session(),