screen-locker/stronglift_replacement/workout_app/lib/widgets/rep_circle.dart
Krzysztof kuhy Rudnicki 23d2173d9f Add comprehensive test suite, backup service, and linting to workout app
- Add sqflite_common_ffi + very_good_analysis; tighten analysis_options
- Add BackupService for JSON export/import of exercise state
- Add full test coverage: models, screens, services, widgets
- Add scripts/check_flutter_coverage.sh to enforce 100% line coverage
- Add docstrings to ExerciseState fields and storage service
- Minor fixes across screens, widgets, and sync/HTTP services

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VuiPt6GPWkxpLbJFrnfy8U
2026-06-28 08:11:43 +02:00

126 lines
3.2 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/// Tappable circle widget representing one set of an exercise.
///
/// States:
/// neutral white, shows target reps, not yet acted upon
/// success green, shows target reps (full set done)
/// partial orange, shows how many reps were actually done (< target)
/// failed red, shows 0 (all reps deducted)
///
/// Interaction:
/// single tap → neutral→success, success→partial(-1 rep),
/// partial→partial(-1 rep), failed stays failed
/// long press → reset to neutral
library;
import 'package:flutter/material.dart';
/// Visual state of a [RepCircle].
enum RepCircleState {
/// Not yet tapped; shows target reps.
neutral,
/// All reps completed; green.
success,
/// Some reps completed; orange, shows actual count.
partial,
/// All reps deducted; red.
failed,
}
/// Tappable circle representing one working set of an exercise.
class RepCircle extends StatelessWidget {
/// Creates a [RepCircle].
const RepCircle({
required this.targetReps,
required this.doneReps,
required this.tapped,
required this.onTap,
required this.onLongPress,
super.key,
});
/// Number of reps the user is aiming for this set.
final int targetReps;
/// Reps currently registered (may be < targetReps after repeated taps).
final int doneReps;
/// Whether this circle has been tapped at all (neutral vs success).
final bool tapped;
/// Called on a single tap.
final VoidCallback onTap;
/// Called on a long press (resets to neutral).
final VoidCallback onLongPress;
RepCircleState get _state {
if (!tapped) return RepCircleState.neutral;
if (doneReps >= targetReps) return RepCircleState.success;
if (doneReps > 0) return RepCircleState.partial;
return RepCircleState.failed;
}
@override
Widget build(BuildContext context) {
final state = _state;
final Color bg;
final Color fg;
final String label;
switch (state) {
case RepCircleState.neutral:
bg = Colors.white;
fg = Colors.black87;
label = '$targetReps';
case RepCircleState.success:
bg = Colors.green;
fg = Colors.white;
label = '$targetReps';
case RepCircleState.partial:
bg = Colors.orange;
fg = Colors.white;
label = '$doneReps';
case RepCircleState.failed:
bg = Colors.red;
fg = Colors.white;
label = '0';
}
return GestureDetector(
onTap: onTap,
onLongPress: onLongPress,
child: Container(
width: 52,
height: 52,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: bg,
border: Border.all(
color: state == RepCircleState.neutral ? Colors.grey.shade400 : bg,
width: 2,
),
boxShadow: const [
BoxShadow(
color: Colors.black26,
blurRadius: 4,
offset: Offset(0, 2),
),
],
),
alignment: Alignment.center,
child: Text(
label,
style: TextStyle(
color: fg,
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
),
);
}
}