From 0547da31dd14b67aa1556b7a7e3361c499d144e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9C=9C=E7=BD=90=E5=AD=90?= <51015673+YXX168@users.noreply.github.com> Date: Wed, 29 Jul 2026 12:52:33 +0800 Subject: [PATCH 01/14] fix: start rest timer after each completed set Track completed sets per exercise so rest starts between sets, while the final set completes the exercise without starting another timer. Existing boolean completion records remain compatible. --- lib/main.dart | 44 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index cba800b..57fb739 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1595,18 +1595,23 @@ class _WorkoutPageState extends State { Future _toggle(int di, int ei) async { final k = '${di}_$ei'; - final completing = !_done.containsKey(k); + final exercise = _plan[di].exercises[ei]; + final completedSets = _completedSets(di, ei); + final resetting = completedSets >= exercise.sets; + final nextCompletedSets = resetting ? 0 : completedSets + 1; setState(() { - if (!completing) { + if (resetting) { _done.remove(k); HapticFeedback.lightImpact(); } else { - _done[k] = true; + _done[k] = nextCompletedSets; HapticFeedback.mediumImpact(); } }); - if (completing && _autoRestTimer) { - _startRestTimer(_plan[di].exercises[ei]); + if (!resetting && + nextCompletedSets < exercise.sets && + _autoRestTimer) { + _startRestTimer(exercise); } final snapshot = Map.from(_done); final previous = _saveQueue; @@ -1683,11 +1688,19 @@ class _WorkoutPageState extends State { int _cnt(int d) { int c = 0; for (int i = 0; i < _plan[d].exercises.length; i++) { - if (_done.containsKey('${d}_$i')) c++; + if (_completedSets(d, i) >= _plan[d].exercises[i].sets) c++; } return c; } + int _completedSets(int dayIndex, int exerciseIndex) { + final value = _done['${dayIndex}_$exerciseIndex']; + final totalSets = _plan[dayIndex].exercises[exerciseIndex].sets; + if (value == true) return totalSets; + if (value is num) return value.toInt().clamp(0, totalSets) as int; + return 0; + } + @override Widget build(BuildContext context) { final t = ThemeInherited.of(context).theme; @@ -2035,7 +2048,7 @@ class _WorkoutPageState extends State { child: _exCard( e.value, e.key + 1, - _done.containsKey('${_day}_${e.key}'), + _completedSets(_day, e.key), t, () => _toggle(_day, e.key), parseRestSeconds(e.value.rest) == null @@ -2312,11 +2325,12 @@ class _WorkoutPageState extends State { Widget _exCard( Exercise ex, int num, - bool done, + int completedSets, WorkoutTheme t, VoidCallback tap, VoidCallback? startTimer, ) { + final done = completedSets >= ex.sets; return Padding( padding: const EdgeInsets.only(bottom: 6), child: PressScale( @@ -2479,6 +2493,20 @@ class _WorkoutPageState extends State { ], ), ), + if (!done && completedSets > 0) ...[ + const SizedBox(height: 7), + Text( + '已完成 $completedSets/${ex.sets} 组 · 点击完成下一组', + key: ValueKey( + 'exercise_set_progress_${_day}_${num - 1}', + ), + style: TextStyle( + fontSize: 10.5, + fontWeight: FontWeight.w700, + color: t.success, + ), + ), + ], ], ), ), From 2ded91ce37f1ec58b90fbb38f7e18f8e563ca2e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9C=9C=E7=BD=90=E5=AD=90?= <51015673+YXX168@users.noreply.github.com> Date: Wed, 29 Jul 2026 12:53:21 +0800 Subject: [PATCH 02/14] Update widget tests for exercise card behavior Verify set progress advances one set at a time and the final set does not start another rest timer. --- test/widget_test.dart | 47 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/test/widget_test.dart b/test/widget_test.dart index 8aa8688..79cf062 100755 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -28,7 +28,7 @@ void main() { expect(find.byType(MaterialApp), findsOneWidget); }); - testWidgets('tapping an exercise card toggles its completed state', ( + testWidgets('tapping an exercise card advances one set at a time', ( WidgetTester tester, ) async { await pumpApp(tester); @@ -50,13 +50,18 @@ void main() { await tester.tap(firstCard); await tester.pumpAndSettle(); - expect(find.text('1/$exerciseTotal'), findsOneWidget); + final firstExercise = workoutDays[activeDayIndex].exercises.first; + expect(find.text('0/$exerciseTotal'), findsOneWidget); + expect( + find.text('已完成 1/${firstExercise.sets} 组 · 点击完成下一组'), + findsOneWidget, + ); expect( find.descendant( of: firstCard, matching: find.text(workoutDays[activeDayIndex].exercises.first.note!), ), - findsNothing, + findsOneWidget, ); final preferences = await SharedPreferences.getInstance(); expect( @@ -64,7 +69,13 @@ void main() { contains('${activeDayIndex}_0'), ); - await tester.tap(find.byIcon(Icons.check).first); + for (var set = 1; set < firstExercise.sets; set++) { + await tester.tap(firstCard); + await tester.pumpAndSettle(); + } + expect(find.text('1/$exerciseTotal'), findsOneWidget); + + await tester.tap(firstCard); await tester.pumpAndSettle(); expect(find.text('0/$exerciseTotal'), findsOneWidget); }); @@ -86,7 +97,7 @@ void main() { }, ); - testWidgets('workout rest timer starts, extends and dismisses', ( + testWidgets('workout rest timer starts after a set, extends and dismisses', ( WidgetTester tester, ) async { await pumpApp(tester); @@ -112,4 +123,30 @@ void main() { await tester.pumpAndSettle(); expect(find.byKey(const Key('rest-timer-panel')), findsNothing); }); + + testWidgets('finishing the last set does not start another rest timer', ( + WidgetTester tester, + ) async { + await pumpApp(tester); + + await tester.tap(find.text('一')); + await tester.pumpAndSettle(); + final firstCard = find.byKey(const ValueKey('exercise_card_0_0')); + final sets = workoutDays.first.exercises.first.sets; + + for (var set = 0; set < sets; set++) { + await tester.tap(firstCard); + await tester.pump(const Duration(milliseconds: 250)); + if (set < sets - 1) { + expect(find.byKey(const Key('rest-timer-panel')), findsOneWidget); + tester + .widget(find.byKey(const Key('rest-timer-dismiss'))) + .onPressed!(); + await tester.pumpAndSettle(); + } + } + + expect(find.byKey(const Key('rest-timer-panel')), findsNothing); + expect(find.text('1/${workoutDays.first.exercises.length}'), findsOneWidget); + }); } From ef64966f02ae4c41601ce930eb4f7a5eec49c1e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9C=9C=E7=BD=90=E5=AD=90?= <51015673+YXX168@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:16:54 +0800 Subject: [PATCH 03/14] Implement CompletionPulse widget with animation Show a clear reset instruction on completed exercise cards and pulse the card until the user resets it. --- lib/main.dart | 94 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 3 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 57fb739..4f3420e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -611,6 +611,67 @@ class PressScale extends StatefulWidget { State createState() => _PressScaleState(); } +class CompletionPulse extends StatefulWidget { + final bool active; + final Widget child; + + const CompletionPulse({ + super.key, + required this.active, + required this.child, + }); + + @override + State createState() => _CompletionPulseState(); +} + +class _CompletionPulseState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + late final Animation _scale; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1100), + ); + _scale = Tween(begin: 1, end: 1.018).animate( + CurvedAnimation(parent: _controller, curve: Curves.easeInOut), + ); + _syncAnimation(); + } + + @override + void didUpdateWidget(covariant CompletionPulse oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.active != widget.active) _syncAnimation(); + } + + void _syncAnimation() { + if (widget.active) { + _controller.repeat(reverse: true); + } else { + _controller.stop(); + _controller.value = 0; + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) => ScaleTransition( + key: const Key('exercise-completion-pulse'), + scale: _scale, + child: widget.child, + ); +} + class _PressScaleState extends State with SingleTickerProviderStateMixin { late final AnimationController _c; @@ -2333,9 +2394,11 @@ class _WorkoutPageState extends State { final done = completedSets >= ex.sets; return Padding( padding: const EdgeInsets.only(bottom: 6), - child: PressScale( - onTap: tap, - child: Card( + child: CompletionPulse( + active: done, + child: PressScale( + onTap: tap, + child: Card( key: ValueKey('exercise_card_${_day}_${num - 1}'), color: done ? t.success.withOpacity(t.isDark ? 0.045 : 0.035) @@ -2506,6 +2569,30 @@ class _WorkoutPageState extends State { color: t.success, ), ), + ] else if (done) ...[ + const SizedBox(height: 7), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.refresh_rounded, + size: 13, + color: t.success, + ), + const SizedBox(width: 4), + Text( + '已完成 · 再点一次重置', + key: ValueKey( + 'exercise_reset_hint_${_day}_${num - 1}', + ), + style: TextStyle( + fontSize: 10.5, + fontWeight: FontWeight.w800, + color: t.success, + ), + ), + ], + ), ], ], ), @@ -2592,6 +2679,7 @@ class _WorkoutPageState extends State { ], ), ), + ), ), ), ); From 264cb2ad291ed813dda5de87bb914ae646572904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9C=9C=E7=BD=90=E5=AD=90?= <51015673+YXX168@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:17:21 +0800 Subject: [PATCH 04/14] Enhance widget test with completion checks Add assertions to verify exercise completion UI elements. --- test/widget_test.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/widget_test.dart b/test/widget_test.dart index 79cf062..3e68968 100755 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -74,6 +74,8 @@ void main() { await tester.pumpAndSettle(); } expect(find.text('1/$exerciseTotal'), findsOneWidget); + expect(find.text('已完成 · 再点一次重置'), findsOneWidget); + expect(find.byKey(const Key('exercise-completion-pulse')), findsWidgets); await tester.tap(firstCard); await tester.pumpAndSettle(); From fc6145b2a9144471188d757a686194d235d66003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9C=9C=E7=BD=90=E5=AD=90?= <51015673+YXX168@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:22:57 +0800 Subject: [PATCH 05/14] Refactor widget build methods for consistency --- lib/main.dart | 860 +++++++++++++++++++++++++------------------------- 1 file changed, 436 insertions(+), 424 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 4f3420e..817b9d3 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -598,9 +598,9 @@ class _FadeScaleEntryState extends State @override Widget build(BuildContext context) => FadeTransition( - opacity: _fade, - child: ScaleTransition(scale: _scale, child: widget.child), - ); + opacity: _fade, + child: ScaleTransition(scale: _scale, child: widget.child), + ); } class PressScale extends StatefulWidget { @@ -615,11 +615,7 @@ class CompletionPulse extends StatefulWidget { final bool active; final Widget child; - const CompletionPulse({ - super.key, - required this.active, - required this.child, - }); + const CompletionPulse({super.key, required this.active, required this.child}); @override State createState() => _CompletionPulseState(); @@ -637,9 +633,10 @@ class _CompletionPulseState extends State vsync: this, duration: const Duration(milliseconds: 1100), ); - _scale = Tween(begin: 1, end: 1.018).animate( - CurvedAnimation(parent: _controller, curve: Curves.easeInOut), - ); + _scale = Tween( + begin: 1, + end: 1.018, + ).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut)); _syncAnimation(); } @@ -666,10 +663,10 @@ class _CompletionPulseState extends State @override Widget build(BuildContext context) => ScaleTransition( - key: const Key('exercise-completion-pulse'), - scale: _scale, - child: widget.child, - ); + key: const Key('exercise-completion-pulse'), + scale: _scale, + child: widget.child, + ); } class _PressScaleState extends State @@ -965,11 +962,11 @@ class _ThemeStateState extends State { @override Widget build(BuildContext context) => ThemeInherited( - current: _mode, - theme: themes[_mode]!, - setTheme: setTheme, - child: widget.child, - ); + current: _mode, + theme: themes[_mode]!, + setTheme: setTheme, + child: widget.child, + ); } // ═══════════════════════════════════════════════════════════════════════════════ @@ -1028,10 +1025,11 @@ class RecompApp extends StatelessWidget { useMaterial3: true, fontFamily: 'Inter', fontFamilyFallback: const ['sans-serif'], - textTheme: (dark - ? ThemeData.dark().textTheme - : ThemeData.light().textTheme) - .apply(fontFamily: 'Inter'), + textTheme: + (dark + ? ThemeData.dark().textTheme + : ThemeData.light().textTheme) + .apply(fontFamily: 'Inter'), ), home: const MainPage(), ); @@ -1069,11 +1067,12 @@ class _MainPageState extends State with TickerProviderStateMixin { final p = _profile; final bmi = p != null && p.heightCm > 0 ? (p.weightKg / ((p.heightCm / 100) * (p.heightCm / 100))) - .toStringAsFixed(1) + .toStringAsFixed(1) : '--'; final ageStr = (p != null && p.age > 0) ? '${p.age}岁' : '--'; - final heightStr = - (p != null && p.heightCm > 0) ? '${p.heightCm.toInt()}cm' : '--'; + final heightStr = (p != null && p.heightCm > 0) + ? '${p.heightCm.toInt()}cm' + : '--'; final weightStr = (p != null && p.weightKg > 0) ? '${p.weightKg}kg' : '--'; final statusText = '$ageStr · $heightStr · $weightStr · BMI $bmi'; final pages = [ @@ -1165,15 +1164,16 @@ class _MainPageState extends State with TickerProviderStateMixin { transitionBuilder: (child, anim) => FadeTransition( opacity: anim, child: SlideTransition( - position: Tween( - begin: const Offset(0.02, 0), - end: Offset.zero, - ).animate( - CurvedAnimation( - parent: anim, - curve: Curves.easeOutCubic, - ), - ), + position: + Tween( + begin: const Offset(0.02, 0), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: anim, + curve: Curves.easeOutCubic, + ), + ), child: child, ), ), @@ -1371,8 +1371,9 @@ class _MainPageState extends State with TickerProviderStateMixin { }, child: Container( decoration: BoxDecoration( - color: - sel ? mt.primary.withOpacity(0.06) : null, + color: sel + ? mt.primary.withOpacity(0.06) + : null, borderRadius: BorderRadius.circular(14), border: Border.all( color: sel ? mt.primary : t.border, @@ -1532,8 +1533,9 @@ class _DaySegmentedNav extends StatelessWidget { duration: const Duration(milliseconds: 220), style: TextStyle( fontSize: 11, - fontWeight: - sel ? FontWeight.w900 : FontWeight.w700, + fontWeight: sel + ? FontWeight.w900 + : FontWeight.w700, color: sel ? Colors.white : t.text2, letterSpacing: -0.2, ), @@ -1544,8 +1546,9 @@ class _DaySegmentedNav extends StatelessWidget { duration: const Duration(milliseconds: 220), style: TextStyle( fontSize: 7.5, - fontWeight: - sel ? FontWeight.w700 : FontWeight.w500, + fontWeight: sel + ? FontWeight.w700 + : FontWeight.w500, color: sel ? Colors.white.withOpacity(0.86) : t.text4, @@ -1669,9 +1672,7 @@ class _WorkoutPageState extends State { HapticFeedback.mediumImpact(); } }); - if (!resetting && - nextCompletedSets < exercise.sets && - _autoRestTimer) { + if (!resetting && nextCompletedSets < exercise.sets && _autoRestTimer) { _startRestTimer(exercise); } final snapshot = Map.from(_done); @@ -1787,15 +1788,16 @@ class _WorkoutPageState extends State { transitionBuilder: (child, animation) => FadeTransition( opacity: animation, child: SlideTransition( - position: Tween( - begin: const Offset(0.02, 0), - end: Offset.zero, - ).animate( - CurvedAnimation( - parent: animation, - curve: Curves.easeOutCubic, - ), - ), + position: + Tween( + begin: const Offset(0.02, 0), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: animation, + curve: Curves.easeOutCubic, + ), + ), child: child, ), ), @@ -1940,46 +1942,46 @@ class _WorkoutPageState extends State { ), const SizedBox(height: 8), ...day.recoveryOptions!.asMap().entries.map( - (e) => FadeScaleEntry( - index: e.key + 2, - child: Card( - margin: const EdgeInsets.only(bottom: 8), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 12, - ), - child: Row( - children: [ - Container( - width: 10, - height: 10, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: t.primary, - boxShadow: [ - BoxShadow( - color: t.primary.withOpacity(0.4), - blurRadius: 6, - ), - ], - ), - ), - const SizedBox(width: 12), - Text( - e.value, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: t.text2, + (e) => FadeScaleEntry( + index: e.key + 2, + child: Card( + margin: const EdgeInsets.only(bottom: 8), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 12, + ), + child: Row( + children: [ + Container( + width: 10, + height: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: t.primary, + boxShadow: [ + BoxShadow( + color: t.primary.withOpacity(0.4), + blurRadius: 6, ), - ), - ], + ], + ), ), - ), + const SizedBox(width: 12), + Text( + e.value, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: t.text2, + ), + ), + ], ), ), ), + ), + ), ], ), ); @@ -2101,24 +2103,24 @@ class _WorkoutPageState extends State { ), const SizedBox(height: 14), ...day.exercises.asMap().entries.map( - (e) => FadeScaleEntry( - key: ValueKey('entry_${_day}_${e.key}'), - index: e.key, - delay: const Duration(milliseconds: 38), - child: RepaintBoundary( - child: _exCard( - e.value, - e.key + 1, - _completedSets(_day, e.key), - t, - () => _toggle(_day, e.key), - parseRestSeconds(e.value.rest) == null - ? null - : () => _startRestTimer(e.value), - ), - ), + (e) => FadeScaleEntry( + key: ValueKey('entry_${_day}_${e.key}'), + index: e.key, + delay: const Duration(milliseconds: 38), + child: RepaintBoundary( + child: _exCard( + e.value, + e.key + 1, + _completedSets(_day, e.key), + t, + () => _toggle(_day, e.key), + parseRestSeconds(e.value.rest) == null + ? null + : () => _startRestTimer(e.value), ), ), + ), + ), if (day.circuitNote != null) FadeScaleEntry( index: day.exercises.length + 2, @@ -2399,287 +2401,293 @@ class _WorkoutPageState extends State { child: PressScale( onTap: tap, child: Card( - key: ValueKey('exercise_card_${_day}_${num - 1}'), - color: done - ? t.success.withOpacity(t.isDark ? 0.045 : 0.035) - : (ex.isStar ? t.primary.withOpacity(0.025) : null), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(18), - side: done - ? BorderSide(color: t.success.withOpacity(0.22), width: 1) - : (ex.isStar - ? BorderSide( - color: t.primary.withOpacity(0.26), - width: 1.2, - ) - : BorderSide( - color: t.border.withOpacity(0.9), - width: 0.8, - )), - ), - child: AnimatedPadding( - duration: const Duration(milliseconds: 220), - curve: Curves.easeOutCubic, - padding: EdgeInsets.symmetric( - horizontal: 14, - vertical: done ? 11 : 14, + key: ValueKey('exercise_card_${_day}_${num - 1}'), + color: done + ? t.success.withOpacity(t.isDark ? 0.045 : 0.035) + : (ex.isStar ? t.primary.withOpacity(0.025) : null), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(18), + side: done + ? BorderSide(color: t.success.withOpacity(0.22), width: 1) + : (ex.isStar + ? BorderSide( + color: t.primary.withOpacity(0.26), + width: 1.2, + ) + : BorderSide( + color: t.border.withOpacity(0.9), + width: 0.8, + )), ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AnimatedContainer( - duration: const Duration(milliseconds: 220), - curve: Curves.easeOutCubic, - width: 28, - height: 28, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: done ? t.success : t.card, - border: Border.all( - color: done ? t.success : t.border, - width: 1.4, + child: AnimatedPadding( + duration: const Duration(milliseconds: 220), + curve: Curves.easeOutCubic, + padding: EdgeInsets.symmetric( + horizontal: 14, + vertical: done ? 11 : 14, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AnimatedContainer( + duration: const Duration(milliseconds: 220), + curve: Curves.easeOutCubic, + width: 28, + height: 28, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: done ? t.success : t.card, + border: Border.all( + color: done ? t.success : t.border, + width: 1.4, + ), + boxShadow: done + ? [ + BoxShadow( + color: t.success.withOpacity(0.28), + blurRadius: 10, + offset: const Offset(0, 3), + ), + ] + : null, ), - boxShadow: done - ? [ - BoxShadow( - color: t.success.withOpacity(0.28), - blurRadius: 10, - offset: const Offset(0, 3), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 200), + transitionBuilder: (c, a) => FadeTransition( + opacity: a, + child: ScaleTransition( + scale: Tween(begin: 0.8, end: 1.0).animate( + CurvedAnimation( + parent: a, + curve: Curves.easeOutBack, ), - ] - : null, - ), - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - transitionBuilder: (c, a) => FadeTransition( - opacity: a, - child: ScaleTransition( - scale: Tween(begin: 0.8, end: 1.0).animate( - CurvedAnimation(parent: a, curve: Curves.easeOutBack), + ), + child: c, ), - child: c, ), - ), - child: done - ? const Icon( - Icons.check, - color: Colors.white, - size: 14, - key: ValueKey('d'), - ) - : Center( - child: Text( - '$num', - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w700, - color: t.text4, + child: done + ? const Icon( + Icons.check, + color: Colors.white, + size: 14, + key: ValueKey('d'), + ) + : Center( + child: Text( + '$num', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w700, + color: t.text4, + ), + key: ValueKey('n$num'), ), - key: ValueKey('n$num'), ), - ), + ), ), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - ex.name, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w700, - color: done ? t.text4 : t.text1, - decoration: done ? TextDecoration.lineThrough : null, - decorationColor: t.text4, + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + ex.name, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: done ? t.text4 : t.text1, + decoration: done + ? TextDecoration.lineThrough + : null, + decorationColor: t.text4, + ), ), - ), - AnimatedSize( - duration: const Duration(milliseconds: 220), - curve: Curves.easeOutCubic, - alignment: Alignment.topCenter, - child: done - ? const SizedBox.shrink() - : Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 3), - Wrap( - spacing: 4, - runSpacing: 2, - children: [ - ...ex.muscles.map( - (m) => Container( - padding: const EdgeInsets.symmetric( - horizontal: 6, - vertical: 1, - ), - decoration: BoxDecoration( - color: t.primary.withOpacity(0.06), - borderRadius: BorderRadius.circular( - 4, + AnimatedSize( + duration: const Duration(milliseconds: 220), + curve: Curves.easeOutCubic, + alignment: Alignment.topCenter, + child: done + ? const SizedBox.shrink() + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 3), + Wrap( + spacing: 4, + runSpacing: 2, + children: [ + ...ex.muscles.map( + (m) => Container( + padding: const EdgeInsets.symmetric( + horizontal: 6, + vertical: 1, + ), + decoration: BoxDecoration( + color: t.primary.withOpacity( + 0.06, + ), + borderRadius: + BorderRadius.circular(4), + ), + child: Text( + m, + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.w600, + color: t.text3, + ), ), ), - child: Text( - m, + ), + if (ex.muscleTarget.isNotEmpty) + Text( + ex.muscleTarget, style: TextStyle( fontSize: 9, - fontWeight: FontWeight.w600, color: t.text3, ), ), - ), - ), - if (ex.muscleTarget.isNotEmpty) - Text( - ex.muscleTarget, + ], + ), + if (ex.note != null) + Padding( + padding: const EdgeInsets.only(top: 6), + child: Text( + ex.note!, style: TextStyle( - fontSize: 9, + fontSize: 10.5, color: t.text3, + height: 1.5, ), ), - ], - ), - if (ex.note != null) - Padding( - padding: const EdgeInsets.only(top: 6), - child: Text( - ex.note!, - style: TextStyle( - fontSize: 10.5, - color: t.text3, - height: 1.5, - ), ), - ), - ], - ), - ), - if (!done && completedSets > 0) ...[ - const SizedBox(height: 7), - Text( - '已完成 $completedSets/${ex.sets} 组 · 点击完成下一组', - key: ValueKey( - 'exercise_set_progress_${_day}_${num - 1}', - ), - style: TextStyle( - fontSize: 10.5, - fontWeight: FontWeight.w700, - color: t.success, - ), + ], + ), ), - ] else if (done) ...[ - const SizedBox(height: 7), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.refresh_rounded, - size: 13, + if (!done && completedSets > 0) ...[ + const SizedBox(height: 7), + Text( + '已完成 $completedSets/${ex.sets} 组 · 点击完成下一组', + key: ValueKey( + 'exercise_set_progress_${_day}_${num - 1}', + ), + style: TextStyle( + fontSize: 10.5, + fontWeight: FontWeight.w700, color: t.success, ), - const SizedBox(width: 4), - Text( - '已完成 · 再点一次重置', - key: ValueKey( - 'exercise_reset_hint_${_day}_${num - 1}', + ), + ] else if (done) ...[ + const SizedBox(height: 7), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.refresh_rounded, + size: 13, + color: t.success, + ), + const SizedBox(width: 4), + Text( + '已完成 · 再点一次重置', + key: ValueKey( + 'exercise_reset_hint_${_day}_${num - 1}', + ), + style: TextStyle( + fontSize: 10.5, + fontWeight: FontWeight.w800, + color: t.success, + ), ), + ], + ), + ], + ], + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text.rich( + TextSpan( + children: [ + TextSpan( + text: '${ex.sets}', style: TextStyle( - fontSize: 10.5, + fontSize: 17, fontWeight: FontWeight.w800, - color: t.success, + color: done ? t.text4 : t.primary, + fontFeatures: const [ + FontFeature.tabularFigures(), + ], + ), + ), + TextSpan( + text: ' ×', + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: done ? t.text4 : t.primary, ), ), ], ), - ], - ], - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text.rich( - TextSpan( - children: [ - TextSpan( - text: '${ex.sets}', - style: TextStyle( - fontSize: 17, - fontWeight: FontWeight.w800, - color: done ? t.text4 : t.primary, - fontFeatures: const [ - FontFeature.tabularFigures(), - ], - ), - ), - TextSpan( - text: ' ×', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w700, - color: done ? t.text4 : t.primary, - ), - ), - ], - ), - ), - Text( - ex.reps, - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.w600, - color: done ? t.text4 : t.text2, ), - ), - const SizedBox(height: 2), - if (startTimer == null) Text( - ex.rest, + ex.reps, style: TextStyle( - fontSize: 9, - fontWeight: FontWeight.w500, - color: done ? t.text4.withOpacity(0.72) : t.text4, + fontSize: 10, + fontWeight: FontWeight.w600, + color: done ? t.text4 : t.text2, ), - ) - else - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: startTimer, - child: Padding( - padding: const EdgeInsets.only(top: 2, bottom: 2), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.timer_outlined, - size: 10, - color: done - ? t.text4.withOpacity(0.72) - : t.primary, - ), - const SizedBox(width: 2), - Text( - ex.rest, - style: TextStyle( - fontSize: 9, - fontWeight: FontWeight.w600, + ), + const SizedBox(height: 2), + if (startTimer == null) + Text( + ex.rest, + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.w500, + color: done ? t.text4.withOpacity(0.72) : t.text4, + ), + ) + else + GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: startTimer, + child: Padding( + padding: const EdgeInsets.only(top: 2, bottom: 2), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.timer_outlined, + size: 10, color: done ? t.text4.withOpacity(0.72) : t.primary, ), - ), - ], + const SizedBox(width: 2), + Text( + ex.rest, + style: TextStyle( + fontSize: 9, + fontWeight: FontWeight.w600, + color: done + ? t.text4.withOpacity(0.72) + : t.primary, + ), + ), + ], + ), ), ), - ), - ], - ), - ], + ], + ), + ], + ), ), ), - ), ), ), ); @@ -3107,8 +3115,9 @@ class _RecordPageState extends State { '$day', style: TextStyle( fontSize: 10, - fontWeight: - isToday ? FontWeight.w800 : FontWeight.w600, + fontWeight: isToday + ? FontWeight.w800 + : FontWeight.w600, color: count == 0 ? (isToday ? t.primary : t.text4) : (intensity > 0.62 ? Colors.white : t.text1), @@ -3261,8 +3270,9 @@ class _RecordPageState extends State { cells.add(SizedBox(width: 4, height: 4)); } else { final rawCount = history[day.toString()]; - final count = - rawCount is int ? rawCount : int.tryParse('$rawCount') ?? 0; + final count = rawCount is int + ? rawCount + : int.tryParse('$rawCount') ?? 0; final intensity = count > 0 ? (count / 8).clamp(0.0, 1.0) : 0.0; cells.add( Container( @@ -3418,44 +3428,43 @@ class NutritionPage extends StatelessWidget { ), const SizedBox(height: 10), ...nutritionTips.asMap().entries.map( - (e) => FadeScaleEntry( - index: e.key + 5, - child: Card( - margin: const EdgeInsets.only(bottom: 6), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 12, + (e) => FadeScaleEntry( + index: e.key + 5, + child: Card( + margin: const EdgeInsets.only(bottom: 6), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 12, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 3, + height: 14, + margin: const EdgeInsets.only(right: 10, top: 2), + decoration: BoxDecoration( + color: t.primary, + borderRadius: BorderRadius.circular(2), + ), ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 3, - height: 14, - margin: - const EdgeInsets.only(right: 10, top: 2), - decoration: BoxDecoration( - color: t.primary, - borderRadius: BorderRadius.circular(2), - ), - ), - Expanded( - child: Text( - nutritionTips[e.key], - style: TextStyle( - fontSize: 12, - color: t.text2, - height: 1.6, - ), - ), + Expanded( + child: Text( + nutritionTips[e.key], + style: TextStyle( + fontSize: 12, + color: t.text2, + height: 1.6, ), - ], + ), ), - ), + ], ), ), ), + ), + ), const SizedBox(height: 8), ]), ), @@ -3751,32 +3760,32 @@ class ProgressionPage extends StatelessWidget { ('孤立动作', '侧平举 / 弯举 / 下压:每次 +0.5-1kg 或 +1-2次'), ('遇到瓶颈', '减重 10% 重新开始,或更换动作变式刺激新角度'), ].asMap().entries.map( - (i) => FadeScaleEntry( - index: i.key + 8, - child: Card( - margin: const EdgeInsets.only(bottom: 6), - child: ListTile( - dense: true, - title: Text( - i.value.$1, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w700, - color: t.text1, - ), - ), - subtitle: Text( - i.value.$2, - style: TextStyle( - fontSize: 11, - color: t.text3, - height: 1.5, - ), - ), + (i) => FadeScaleEntry( + index: i.key + 8, + child: Card( + margin: const EdgeInsets.only(bottom: 6), + child: ListTile( + dense: true, + title: Text( + i.value.$1, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + color: t.text1, + ), + ), + subtitle: Text( + i.value.$2, + style: TextStyle( + fontSize: 11, + color: t.text3, + height: 1.5, ), ), ), ), + ), + ), const SizedBox(height: 8), ]), ), @@ -3845,13 +3854,13 @@ class ThemePage extends StatelessWidget { ), ] : sel - ? [ - BoxShadow( - color: mt.primary.withOpacity(0.3), - blurRadius: 10, - ), - ] - : null, + ? [ + BoxShadow( + color: mt.primary.withOpacity(0.3), + blurRadius: 10, + ), + ] + : null, ), child: Padding( padding: const EdgeInsets.symmetric( @@ -3881,14 +3890,13 @@ class ThemePage extends StatelessWidget { ), ] : sel - ? [ - BoxShadow( - color: - mt.primary.withOpacity(0.3), - blurRadius: 10, - ), - ] - : null, + ? [ + BoxShadow( + color: mt.primary.withOpacity(0.3), + blurRadius: 10, + ), + ] + : null, ), child: AnimatedSwitcher( duration: const Duration(milliseconds: 200), @@ -4215,7 +4223,8 @@ class TrendChartPainter extends CustomPainter { final fillPath = Path(); final points = []; for (int i = 0; i < data.length; i++) { - final x = padding.left + + final x = + padding.left + (data.length == 1 ? chartW / 2 : (i / (data.length - 1)) * chartW); final y = padding.top + chartH - ((data[i].$2 - minV) / range) * chartH; points.add(Offset(x, y)); @@ -5078,12 +5087,12 @@ class _SettingsPageState extends State { MiniTrendChart( data: weights.length > 30 ? weights - .sublist(weights.length - 30) - .map((e) => ('${e.date.month}/${e.date.day}', e.weightKg)) - .toList() + .sublist(weights.length - 30) + .map((e) => ('${e.date.month}/${e.date.day}', e.weightKg)) + .toList() : weights - .map((e) => ('${e.date.month}/${e.date.day}', e.weightKg)) - .toList(), + .map((e) => ('${e.date.month}/${e.date.day}', e.weightKg)) + .toList(), theme: t, ), if (weights.isNotEmpty) ...[ @@ -5100,7 +5109,8 @@ class _SettingsPageState extends State { '变化: ${weights.last.weightKg > weights[weights.length - 2].weightKg ? "+" : ""}${(weights.last.weightKg - weights[weights.length - 2].weightKg).toStringAsFixed(1)}kg', style: TextStyle( fontSize: 11, - color: weights.last.weightKg > + color: + weights.last.weightKg > weights[weights.length - 2].weightKg ? t.warning : t.success, @@ -5192,7 +5202,9 @@ class _SettingsPageState extends State { : waistData, theme: t, ), - ...entries.reversed.take(8).map( + ...entries.reversed + .take(8) + .map( (entry) => Padding( padding: const EdgeInsets.only(top: 8), child: Row( From a99d1d46973fcc704cee761822b18947d2d53659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9C=9C=E7=BD=90=E5=AD=90?= <51015673+YXX168@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:23:55 +0800 Subject: [PATCH 06/14] Format code for better readability in widget_test.dart --- test/widget_test.dart | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/test/widget_test.dart b/test/widget_test.dart index 3e68968..2f3fb08 100755 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -40,8 +40,9 @@ void main() { await tester.tap(mondayTab); await tester.pumpAndSettle(); } - final activeDayIndex = - workoutDays[dayIndex].exercises.isEmpty ? 0 : dayIndex; + final activeDayIndex = workoutDays[dayIndex].exercises.isEmpty + ? 0 + : dayIndex; final exerciseTotal = workoutDays[activeDayIndex].exercises.length; final firstCard = find.byKey(ValueKey('exercise_card_${activeDayIndex}_0')); expect(firstCard, findsOneWidget); @@ -149,6 +150,9 @@ void main() { } expect(find.byKey(const Key('rest-timer-panel')), findsNothing); - expect(find.text('1/${workoutDays.first.exercises.length}'), findsOneWidget); + expect( + find.text('1/${workoutDays.first.exercises.length}'), + findsOneWidget, + ); }); } From e1e4e798b0d2e4bb633e7fe045fb28b319ee2e30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9C=9C=E7=BD=90=E5=AD=90?= <51015673+YXX168@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:44:17 +0800 Subject: [PATCH 07/14] Update formatting step in build workflow Use the workflow Flutter 3.41.6 toolchain to normalize formatting before analyze, test, and APK build so remaining failures reflect actual code issues. --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0143c11..8440d16 100755 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,8 +20,8 @@ jobs: cache: true - name: Get dependencies run: flutter pub get - - name: Check formatting - run: dart format --output=none --set-exit-if-changed lib test + - name: Format sources in Flutter 3.41.6 + run: dart format lib test - name: Analyze run: flutter analyze --no-fatal-infos - name: Test From 1ad50c4408e583ec557efb0a448308e881ca9e32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9C=9C=E7=BD=90=E5=AD=90?= <51015673+YXX168@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:52:11 +0800 Subject: [PATCH 08/14] fix: remove unnecessary set progress cast --- lib/main.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/main.dart b/lib/main.dart index 817b9d3..c3c698d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1759,7 +1759,7 @@ class _WorkoutPageState extends State { final value = _done['${dayIndex}_$exerciseIndex']; final totalSets = _plan[dayIndex].exercises[exerciseIndex].sets; if (value == true) return totalSets; - if (value is num) return value.toInt().clamp(0, totalSets) as int; + if (value is num) return value.toInt().clamp(0, totalSets); return 0; } From c32ca694b96d3ba55fbfd2591df67bb19374d92c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9C=9C=E7=BD=90=E5=AD=90?= <51015673+YXX168@users.noreply.github.com> Date: Wed, 29 Jul 2026 13:56:27 +0800 Subject: [PATCH 09/14] Update test to use pump with duration Use a bounded pump after completing the final set because the completed exercise intentionally keeps pulsing until reset. --- test/widget_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/widget_test.dart b/test/widget_test.dart index 2f3fb08..aeeab83 100755 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -72,7 +72,7 @@ void main() { for (var set = 1; set < firstExercise.sets; set++) { await tester.tap(firstCard); - await tester.pumpAndSettle(); + await tester.pump(const Duration(milliseconds: 350)); } expect(find.text('1/$exerciseTotal'), findsOneWidget); expect(find.text('已完成 · 再点一次重置'), findsOneWidget); From 972feba027417634c0ac5ce4f274ba69511cd113 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9C=9C=E7=BD=90=E5=AD=90?= <51015673+YXX168@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:03:39 +0800 Subject: [PATCH 10/14] Change formatting step to check for changes --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8440d16..0143c11 100755 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -20,8 +20,8 @@ jobs: cache: true - name: Get dependencies run: flutter pub get - - name: Format sources in Flutter 3.41.6 - run: dart format lib test + - name: Check formatting + run: dart format --output=none --set-exit-if-changed lib test - name: Analyze run: flutter analyze --no-fatal-infos - name: Test From b422d61af1e4ad3e3fe2e61882afb30aec0a3597 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9C=9C=E7=BD=90=E5=AD=90?= <51015673+YXX168@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:06:07 +0800 Subject: [PATCH 11/14] Enhance build workflow with formatting and permissions Added permissions for write access and automated formatting commit. --- .github/workflows/build.yml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0143c11..2a725e4 100755 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,8 @@ name: Validate and Build Flutter APK +permissions: + contents: write + on: push: branches: [main] @@ -20,8 +23,19 @@ jobs: cache: true - name: Get dependencies run: flutter pub get - - name: Check formatting - run: dart format --output=none --set-exit-if-changed lib test + - name: Format sources in Flutter 3.41.6 + run: dart format lib test + - name: Commit cloud-formatted sources + shell: bash + run: | + if git diff --quiet -- lib test; then + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add lib test + git commit -m "style: apply Flutter 3.41.6 formatting" + git push origin HEAD:YXX168-patch-1 - name: Analyze run: flutter analyze --no-fatal-infos - name: Test From 90a0f7dbfe29d14b6d505e13946232e628e04fdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9C=9C=E7=BD=90=E5=AD=90?= <51015673+YXX168@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:08:54 +0800 Subject: [PATCH 12/14] Update checkout action to use specific ref and fetch-depth --- .github/workflows/build.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2a725e4..225c8db 100755 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,6 +16,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + ref: YXX168-patch-1 + fetch-depth: 0 - uses: subosito/flutter-action@v2 with: flutter-version: '3.41.6' From 09633aa1c202057693f39a0d088394502e947fdd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 29 Jul 2026 06:09:39 +0000 Subject: [PATCH 13/14] style: apply Flutter 3.41.6 formatting --- lib/main.dart | 382 ++++++++++++++++++++---------------------- test/widget_test.dart | 5 +- 2 files changed, 187 insertions(+), 200 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index c3c698d..9f2d499 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -598,9 +598,9 @@ class _FadeScaleEntryState extends State @override Widget build(BuildContext context) => FadeTransition( - opacity: _fade, - child: ScaleTransition(scale: _scale, child: widget.child), - ); + opacity: _fade, + child: ScaleTransition(scale: _scale, child: widget.child), + ); } class PressScale extends StatefulWidget { @@ -663,10 +663,10 @@ class _CompletionPulseState extends State @override Widget build(BuildContext context) => ScaleTransition( - key: const Key('exercise-completion-pulse'), - scale: _scale, - child: widget.child, - ); + key: const Key('exercise-completion-pulse'), + scale: _scale, + child: widget.child, + ); } class _PressScaleState extends State @@ -962,11 +962,11 @@ class _ThemeStateState extends State { @override Widget build(BuildContext context) => ThemeInherited( - current: _mode, - theme: themes[_mode]!, - setTheme: setTheme, - child: widget.child, - ); + current: _mode, + theme: themes[_mode]!, + setTheme: setTheme, + child: widget.child, + ); } // ═══════════════════════════════════════════════════════════════════════════════ @@ -1025,11 +1025,10 @@ class RecompApp extends StatelessWidget { useMaterial3: true, fontFamily: 'Inter', fontFamilyFallback: const ['sans-serif'], - textTheme: - (dark - ? ThemeData.dark().textTheme - : ThemeData.light().textTheme) - .apply(fontFamily: 'Inter'), + textTheme: (dark + ? ThemeData.dark().textTheme + : ThemeData.light().textTheme) + .apply(fontFamily: 'Inter'), ), home: const MainPage(), ); @@ -1067,12 +1066,11 @@ class _MainPageState extends State with TickerProviderStateMixin { final p = _profile; final bmi = p != null && p.heightCm > 0 ? (p.weightKg / ((p.heightCm / 100) * (p.heightCm / 100))) - .toStringAsFixed(1) + .toStringAsFixed(1) : '--'; final ageStr = (p != null && p.age > 0) ? '${p.age}岁' : '--'; - final heightStr = (p != null && p.heightCm > 0) - ? '${p.heightCm.toInt()}cm' - : '--'; + final heightStr = + (p != null && p.heightCm > 0) ? '${p.heightCm.toInt()}cm' : '--'; final weightStr = (p != null && p.weightKg > 0) ? '${p.weightKg}kg' : '--'; final statusText = '$ageStr · $heightStr · $weightStr · BMI $bmi'; final pages = [ @@ -1164,16 +1162,15 @@ class _MainPageState extends State with TickerProviderStateMixin { transitionBuilder: (child, anim) => FadeTransition( opacity: anim, child: SlideTransition( - position: - Tween( - begin: const Offset(0.02, 0), - end: Offset.zero, - ).animate( - CurvedAnimation( - parent: anim, - curve: Curves.easeOutCubic, - ), - ), + position: Tween( + begin: const Offset(0.02, 0), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: anim, + curve: Curves.easeOutCubic, + ), + ), child: child, ), ), @@ -1371,9 +1368,8 @@ class _MainPageState extends State with TickerProviderStateMixin { }, child: Container( decoration: BoxDecoration( - color: sel - ? mt.primary.withOpacity(0.06) - : null, + color: + sel ? mt.primary.withOpacity(0.06) : null, borderRadius: BorderRadius.circular(14), border: Border.all( color: sel ? mt.primary : t.border, @@ -1533,9 +1529,8 @@ class _DaySegmentedNav extends StatelessWidget { duration: const Duration(milliseconds: 220), style: TextStyle( fontSize: 11, - fontWeight: sel - ? FontWeight.w900 - : FontWeight.w700, + fontWeight: + sel ? FontWeight.w900 : FontWeight.w700, color: sel ? Colors.white : t.text2, letterSpacing: -0.2, ), @@ -1546,9 +1541,8 @@ class _DaySegmentedNav extends StatelessWidget { duration: const Duration(milliseconds: 220), style: TextStyle( fontSize: 7.5, - fontWeight: sel - ? FontWeight.w700 - : FontWeight.w500, + fontWeight: + sel ? FontWeight.w700 : FontWeight.w500, color: sel ? Colors.white.withOpacity(0.86) : t.text4, @@ -1788,16 +1782,15 @@ class _WorkoutPageState extends State { transitionBuilder: (child, animation) => FadeTransition( opacity: animation, child: SlideTransition( - position: - Tween( - begin: const Offset(0.02, 0), - end: Offset.zero, - ).animate( - CurvedAnimation( - parent: animation, - curve: Curves.easeOutCubic, - ), - ), + position: Tween( + begin: const Offset(0.02, 0), + end: Offset.zero, + ).animate( + CurvedAnimation( + parent: animation, + curve: Curves.easeOutCubic, + ), + ), child: child, ), ), @@ -1942,46 +1935,46 @@ class _WorkoutPageState extends State { ), const SizedBox(height: 8), ...day.recoveryOptions!.asMap().entries.map( - (e) => FadeScaleEntry( - index: e.key + 2, - child: Card( - margin: const EdgeInsets.only(bottom: 8), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 12, - ), - child: Row( - children: [ - Container( - width: 10, - height: 10, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: t.primary, - boxShadow: [ - BoxShadow( - color: t.primary.withOpacity(0.4), - blurRadius: 6, - ), - ], - ), + (e) => FadeScaleEntry( + index: e.key + 2, + child: Card( + margin: const EdgeInsets.only(bottom: 8), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 12, ), - const SizedBox(width: 12), - Text( - e.value, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w600, - color: t.text2, - ), + child: Row( + children: [ + Container( + width: 10, + height: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: t.primary, + boxShadow: [ + BoxShadow( + color: t.primary.withOpacity(0.4), + blurRadius: 6, + ), + ], + ), + ), + const SizedBox(width: 12), + Text( + e.value, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: t.text2, + ), + ), + ], ), - ], + ), ), ), ), - ), - ), ], ), ); @@ -2103,24 +2096,24 @@ class _WorkoutPageState extends State { ), const SizedBox(height: 14), ...day.exercises.asMap().entries.map( - (e) => FadeScaleEntry( - key: ValueKey('entry_${_day}_${e.key}'), - index: e.key, - delay: const Duration(milliseconds: 38), - child: RepaintBoundary( - child: _exCard( - e.value, - e.key + 1, - _completedSets(_day, e.key), - t, - () => _toggle(_day, e.key), - parseRestSeconds(e.value.rest) == null - ? null - : () => _startRestTimer(e.value), + (e) => FadeScaleEntry( + key: ValueKey('entry_${_day}_${e.key}'), + index: e.key, + delay: const Duration(milliseconds: 38), + child: RepaintBoundary( + child: _exCard( + e.value, + e.key + 1, + _completedSets(_day, e.key), + t, + () => _toggle(_day, e.key), + parseRestSeconds(e.value.rest) == null + ? null + : () => _startRestTimer(e.value), + ), + ), ), ), - ), - ), if (day.circuitNote != null) FadeScaleEntry( index: day.exercises.length + 2, @@ -2410,14 +2403,14 @@ class _WorkoutPageState extends State { side: done ? BorderSide(color: t.success.withOpacity(0.22), width: 1) : (ex.isStar - ? BorderSide( - color: t.primary.withOpacity(0.26), - width: 1.2, - ) - : BorderSide( - color: t.border.withOpacity(0.9), - width: 0.8, - )), + ? BorderSide( + color: t.primary.withOpacity(0.26), + width: 1.2, + ) + : BorderSide( + color: t.border.withOpacity(0.9), + width: 0.8, + )), ), child: AnimatedPadding( duration: const Duration(milliseconds: 220), @@ -2496,9 +2489,8 @@ class _WorkoutPageState extends State { fontSize: 14, fontWeight: FontWeight.w700, color: done ? t.text4 : t.text1, - decoration: done - ? TextDecoration.lineThrough - : null, + decoration: + done ? TextDecoration.lineThrough : null, decorationColor: t.text4, ), ), @@ -3115,9 +3107,8 @@ class _RecordPageState extends State { '$day', style: TextStyle( fontSize: 10, - fontWeight: isToday - ? FontWeight.w800 - : FontWeight.w600, + fontWeight: + isToday ? FontWeight.w800 : FontWeight.w600, color: count == 0 ? (isToday ? t.primary : t.text4) : (intensity > 0.62 ? Colors.white : t.text1), @@ -3270,9 +3261,8 @@ class _RecordPageState extends State { cells.add(SizedBox(width: 4, height: 4)); } else { final rawCount = history[day.toString()]; - final count = rawCount is int - ? rawCount - : int.tryParse('$rawCount') ?? 0; + final count = + rawCount is int ? rawCount : int.tryParse('$rawCount') ?? 0; final intensity = count > 0 ? (count / 8).clamp(0.0, 1.0) : 0.0; cells.add( Container( @@ -3428,43 +3418,44 @@ class NutritionPage extends StatelessWidget { ), const SizedBox(height: 10), ...nutritionTips.asMap().entries.map( - (e) => FadeScaleEntry( - index: e.key + 5, - child: Card( - margin: const EdgeInsets.only(bottom: 6), - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 12, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 3, - height: 14, - margin: const EdgeInsets.only(right: 10, top: 2), - decoration: BoxDecoration( - color: t.primary, - borderRadius: BorderRadius.circular(2), - ), + (e) => FadeScaleEntry( + index: e.key + 5, + child: Card( + margin: const EdgeInsets.only(bottom: 6), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 14, + vertical: 12, ), - Expanded( - child: Text( - nutritionTips[e.key], - style: TextStyle( - fontSize: 12, - color: t.text2, - height: 1.6, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 3, + height: 14, + margin: + const EdgeInsets.only(right: 10, top: 2), + decoration: BoxDecoration( + color: t.primary, + borderRadius: BorderRadius.circular(2), + ), ), - ), + Expanded( + child: Text( + nutritionTips[e.key], + style: TextStyle( + fontSize: 12, + color: t.text2, + height: 1.6, + ), + ), + ), + ], ), - ], + ), ), ), ), - ), - ), const SizedBox(height: 8), ]), ), @@ -3760,32 +3751,32 @@ class ProgressionPage extends StatelessWidget { ('孤立动作', '侧平举 / 弯举 / 下压:每次 +0.5-1kg 或 +1-2次'), ('遇到瓶颈', '减重 10% 重新开始,或更换动作变式刺激新角度'), ].asMap().entries.map( - (i) => FadeScaleEntry( - index: i.key + 8, - child: Card( - margin: const EdgeInsets.only(bottom: 6), - child: ListTile( - dense: true, - title: Text( - i.value.$1, - style: TextStyle( - fontSize: 13, - fontWeight: FontWeight.w700, - color: t.text1, - ), - ), - subtitle: Text( - i.value.$2, - style: TextStyle( - fontSize: 11, - color: t.text3, - height: 1.5, + (i) => FadeScaleEntry( + index: i.key + 8, + child: Card( + margin: const EdgeInsets.only(bottom: 6), + child: ListTile( + dense: true, + title: Text( + i.value.$1, + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w700, + color: t.text1, + ), + ), + subtitle: Text( + i.value.$2, + style: TextStyle( + fontSize: 11, + color: t.text3, + height: 1.5, + ), + ), ), ), ), ), - ), - ), const SizedBox(height: 8), ]), ), @@ -3854,13 +3845,13 @@ class ThemePage extends StatelessWidget { ), ] : sel - ? [ - BoxShadow( - color: mt.primary.withOpacity(0.3), - blurRadius: 10, - ), - ] - : null, + ? [ + BoxShadow( + color: mt.primary.withOpacity(0.3), + blurRadius: 10, + ), + ] + : null, ), child: Padding( padding: const EdgeInsets.symmetric( @@ -3890,13 +3881,14 @@ class ThemePage extends StatelessWidget { ), ] : sel - ? [ - BoxShadow( - color: mt.primary.withOpacity(0.3), - blurRadius: 10, - ), - ] - : null, + ? [ + BoxShadow( + color: + mt.primary.withOpacity(0.3), + blurRadius: 10, + ), + ] + : null, ), child: AnimatedSwitcher( duration: const Duration(milliseconds: 200), @@ -4223,8 +4215,7 @@ class TrendChartPainter extends CustomPainter { final fillPath = Path(); final points = []; for (int i = 0; i < data.length; i++) { - final x = - padding.left + + final x = padding.left + (data.length == 1 ? chartW / 2 : (i / (data.length - 1)) * chartW); final y = padding.top + chartH - ((data[i].$2 - minV) / range) * chartH; points.add(Offset(x, y)); @@ -5087,12 +5078,12 @@ class _SettingsPageState extends State { MiniTrendChart( data: weights.length > 30 ? weights - .sublist(weights.length - 30) - .map((e) => ('${e.date.month}/${e.date.day}', e.weightKg)) - .toList() + .sublist(weights.length - 30) + .map((e) => ('${e.date.month}/${e.date.day}', e.weightKg)) + .toList() : weights - .map((e) => ('${e.date.month}/${e.date.day}', e.weightKg)) - .toList(), + .map((e) => ('${e.date.month}/${e.date.day}', e.weightKg)) + .toList(), theme: t, ), if (weights.isNotEmpty) ...[ @@ -5109,8 +5100,7 @@ class _SettingsPageState extends State { '变化: ${weights.last.weightKg > weights[weights.length - 2].weightKg ? "+" : ""}${(weights.last.weightKg - weights[weights.length - 2].weightKg).toStringAsFixed(1)}kg', style: TextStyle( fontSize: 11, - color: - weights.last.weightKg > + color: weights.last.weightKg > weights[weights.length - 2].weightKg ? t.warning : t.success, @@ -5202,9 +5192,7 @@ class _SettingsPageState extends State { : waistData, theme: t, ), - ...entries.reversed - .take(8) - .map( + ...entries.reversed.take(8).map( (entry) => Padding( padding: const EdgeInsets.only(top: 8), child: Row( diff --git a/test/widget_test.dart b/test/widget_test.dart index aeeab83..0542966 100755 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -40,9 +40,8 @@ void main() { await tester.tap(mondayTab); await tester.pumpAndSettle(); } - final activeDayIndex = workoutDays[dayIndex].exercises.isEmpty - ? 0 - : dayIndex; + final activeDayIndex = + workoutDays[dayIndex].exercises.isEmpty ? 0 : dayIndex; final exerciseTotal = workoutDays[activeDayIndex].exercises.length; final firstCard = find.byKey(ValueKey('exercise_card_${activeDayIndex}_0')); expect(firstCard, findsOneWidget); From b2fe823029616dc2fd01426646007f6e7942f124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=9C=9C=E7=BD=90=E5=AD=90?= <51015673+YXX168@users.noreply.github.com> Date: Wed, 29 Jul 2026 14:12:42 +0800 Subject: [PATCH 14/14] ci: enforce strict formatting after cloud normalization Removed permissions and commit steps, added formatting check. --- .github/workflows/build.yml | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 225c8db..0143c11 100755 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,8 +1,5 @@ name: Validate and Build Flutter APK -permissions: - contents: write - on: push: branches: [main] @@ -16,9 +13,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - with: - ref: YXX168-patch-1 - fetch-depth: 0 - uses: subosito/flutter-action@v2 with: flutter-version: '3.41.6' @@ -26,19 +20,8 @@ jobs: cache: true - name: Get dependencies run: flutter pub get - - name: Format sources in Flutter 3.41.6 - run: dart format lib test - - name: Commit cloud-formatted sources - shell: bash - run: | - if git diff --quiet -- lib test; then - exit 0 - fi - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add lib test - git commit -m "style: apply Flutter 3.41.6 formatting" - git push origin HEAD:YXX168-patch-1 + - name: Check formatting + run: dart format --output=none --set-exit-if-changed lib test - name: Analyze run: flutter analyze --no-fatal-infos - name: Test