mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
Convert the reps and weight log widgets back to stateful.
This solves some problems with the keyboard focus changing or the quick-add buttons for reps and weight. Also, add some tests for these.
This commit is contained in:
@@ -211,26 +211,61 @@ class LogsPlatesWidget extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class LogsRepsWidget extends ConsumerWidget {
|
||||
final _logger = Logger('LogsRepsWidget');
|
||||
|
||||
class LogsRepsWidget extends ConsumerStatefulWidget {
|
||||
final num valueChange;
|
||||
|
||||
LogsRepsWidget({
|
||||
const LogsRepsWidget({
|
||||
super.key,
|
||||
num? valueChange,
|
||||
}) : valueChange = valueChange ?? 1;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ConsumerState<LogsRepsWidget> createState() => _LogsRepsWidgetState();
|
||||
}
|
||||
|
||||
class _LogsRepsWidgetState extends ConsumerState<LogsRepsWidget> {
|
||||
final _logger = Logger('LogsRepsWidget');
|
||||
late TextEditingController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final numberFormat = NumberFormat.decimalPattern(Localizations.localeOf(context).toString());
|
||||
final i18n = AppLocalizations.of(context);
|
||||
|
||||
final logNotifier = ref.read(gymLogProvider.notifier);
|
||||
final log = ref.watch(gymLogProvider);
|
||||
|
||||
final currentReps = log?.repetitions;
|
||||
final repText = currentReps != null ? numberFormat.format(currentReps) : '';
|
||||
final currentInput = numberFormat.tryParse(_controller.text);
|
||||
|
||||
// Sync from provider to controller if needed
|
||||
if (currentReps != null) {
|
||||
// Update if values differ, but allow invalid input while typing (unless empty/initial)
|
||||
if (currentInput != currentReps && (currentInput != null || _controller.text.isEmpty)) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
_controller.text = numberFormat.format(currentReps);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (_controller.text.isNotEmpty) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
_controller.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
@@ -239,7 +274,7 @@ class LogsRepsWidget extends ConsumerWidget {
|
||||
icon: const Icon(Icons.remove, color: Colors.black),
|
||||
onPressed: () {
|
||||
final base = currentReps ?? 0;
|
||||
final newValue = base - valueChange;
|
||||
final newValue = base - widget.valueChange;
|
||||
if (newValue >= 0 && log != null) {
|
||||
logNotifier.setRepetitions(newValue);
|
||||
}
|
||||
@@ -251,8 +286,7 @@ class LogsRepsWidget extends ConsumerWidget {
|
||||
child: TextFormField(
|
||||
decoration: InputDecoration(labelText: i18n.repetitions),
|
||||
enabled: true,
|
||||
key: const ValueKey('reps-field'),
|
||||
initialValue: repText,
|
||||
controller: _controller,
|
||||
keyboardType: textInputTypeDecimal,
|
||||
onChanged: (value) {
|
||||
try {
|
||||
@@ -282,7 +316,7 @@ class LogsRepsWidget extends ConsumerWidget {
|
||||
icon: const Icon(Icons.add, color: Colors.black),
|
||||
onPressed: () {
|
||||
final base = currentReps ?? 0;
|
||||
final newValue = base + valueChange;
|
||||
final newValue = base + widget.valueChange;
|
||||
if (newValue >= 0 && log != null) {
|
||||
logNotifier.setRepetitions(newValue);
|
||||
}
|
||||
@@ -293,27 +327,62 @@ class LogsRepsWidget extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class LogsWeightWidget extends ConsumerWidget {
|
||||
final _logger = Logger('LogsWeightWidget');
|
||||
|
||||
class LogsWeightWidget extends ConsumerStatefulWidget {
|
||||
final num valueChange;
|
||||
|
||||
LogsWeightWidget({
|
||||
const LogsWeightWidget({
|
||||
super.key,
|
||||
num? valueChange,
|
||||
}) : valueChange = valueChange ?? 1.25;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ConsumerState<LogsWeightWidget> createState() => _LogsWeightWidgetState();
|
||||
}
|
||||
|
||||
class _LogsWeightWidgetState extends ConsumerState<LogsWeightWidget> {
|
||||
final _logger = Logger('LogsWeightWidget');
|
||||
late TextEditingController _controller;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = TextEditingController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final numberFormat = NumberFormat.decimalPattern(Localizations.localeOf(context).toString());
|
||||
final i18n = AppLocalizations.of(context);
|
||||
|
||||
final plateProvider = ref.read(plateCalculatorProvider.notifier);
|
||||
final logProvider = ref.read(gymLogProvider.notifier);
|
||||
final log = ref.watch(gymLogProvider);
|
||||
|
||||
final currentWeight = log?.weight;
|
||||
final weightText = currentWeight != null ? numberFormat.format(currentWeight) : '';
|
||||
final currentInput = numberFormat.tryParse(_controller.text);
|
||||
|
||||
// Sync from provider to controller if needed
|
||||
if (currentWeight != null) {
|
||||
// Update if values differ, but allow invalid input while typing (unless empty/initial)
|
||||
if (currentInput != currentWeight && (currentInput != null || _controller.text.isEmpty)) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
_controller.text = numberFormat.format(currentWeight);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (_controller.text.isNotEmpty) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
_controller.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
@@ -322,7 +391,7 @@ class LogsWeightWidget extends ConsumerWidget {
|
||||
icon: const Icon(Icons.remove, color: Colors.black),
|
||||
onPressed: () {
|
||||
final base = currentWeight ?? 0;
|
||||
final newValue = base - valueChange;
|
||||
final newValue = base - widget.valueChange;
|
||||
if (newValue >= 0 && log != null) {
|
||||
logProvider.setWeight(newValue);
|
||||
}
|
||||
@@ -332,9 +401,8 @@ class LogsWeightWidget extends ConsumerWidget {
|
||||
// Text field
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
key: const ValueKey('weight-field'),
|
||||
controller: _controller,
|
||||
decoration: InputDecoration(labelText: i18n.weight),
|
||||
initialValue: weightText,
|
||||
keyboardType: textInputTypeDecimal,
|
||||
onChanged: (value) {
|
||||
try {
|
||||
@@ -365,7 +433,7 @@ class LogsWeightWidget extends ConsumerWidget {
|
||||
icon: const Icon(Icons.add, color: Colors.black),
|
||||
onPressed: () {
|
||||
final base = currentWeight ?? 0;
|
||||
final newValue = base + valueChange;
|
||||
final newValue = base + widget.valueChange;
|
||||
if (log != null) {
|
||||
logProvider.setWeight(newValue);
|
||||
}
|
||||
@@ -408,9 +476,7 @@ class LogsPastLogsWidget extends ConsumerWidget {
|
||||
logProvider.setLog(pastLog);
|
||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context).dataCopied),
|
||||
),
|
||||
SnackBar(content: Text(AppLocalizations.of(context).dataCopied)),
|
||||
);
|
||||
},
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 40),
|
||||
|
||||
@@ -237,5 +237,125 @@ void main() {
|
||||
expect(capturedLog!.routineId, equals(notifier.state.routine.id));
|
||||
expect(capturedLog!.iteration, equals(notifier.state.iteration));
|
||||
});
|
||||
|
||||
testWidgets('LogsRepsWidget quick buttons update values', (tester) async {
|
||||
// Arrange
|
||||
final notifier = container.read(gymStateProvider.notifier);
|
||||
final routine = testdata.getTestRoutine();
|
||||
routine.dayDataGym[0].slots[0].setConfigs[0].repetitions = 0;
|
||||
notifier.state = notifier.state.copyWith(
|
||||
dayId: routine.days.first.id,
|
||||
routine: routine,
|
||||
iteration: 1,
|
||||
);
|
||||
notifier.calculatePages();
|
||||
notifier.setCurrentPage(2);
|
||||
notifier.state = notifier.state.copyWith(currentPage: 2);
|
||||
final mockRoutines = MockRoutinesProvider();
|
||||
await pumpLogPage(tester, routinesProvider: mockRoutines);
|
||||
|
||||
// Act
|
||||
final repsWidgetFinder = find.byKey(const ValueKey('logs-reps-widget'));
|
||||
expect(repsWidgetFinder, findsOneWidget);
|
||||
final addBtn = find.descendant(
|
||||
of: repsWidgetFinder,
|
||||
matching: find.byIcon(Icons.add),
|
||||
);
|
||||
final removeBtn = find.descendant(
|
||||
of: repsWidgetFinder,
|
||||
matching: find.byIcon(Icons.remove),
|
||||
);
|
||||
|
||||
// Assert
|
||||
// Increment 0 -> 1
|
||||
await tester.tap(addBtn);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.descendant(of: repsWidgetFinder, matching: find.text('1')),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// Increment 1 -> 2
|
||||
await tester.tap(addBtn);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.descendant(of: repsWidgetFinder, matching: find.text('2')),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// Decrement 2 -> 1
|
||||
await tester.tap(removeBtn);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.descendant(of: repsWidgetFinder, matching: find.text('1')),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('LogsWeightWidget quick buttons update values', (tester) async {
|
||||
// Arrange
|
||||
final notifier = container.read(gymStateProvider.notifier);
|
||||
final routine = testdata.getTestRoutine();
|
||||
routine.dayDataGym[0].slots[0].setConfigs[0].weight = 0;
|
||||
notifier.state = notifier.state.copyWith(
|
||||
dayId: routine.days.first.id,
|
||||
routine: routine,
|
||||
iteration: 1,
|
||||
);
|
||||
notifier.calculatePages();
|
||||
notifier.setCurrentPage(2);
|
||||
notifier.state = notifier.state.copyWith(currentPage: 2);
|
||||
final mockRoutines = MockRoutinesProvider();
|
||||
await pumpLogPage(tester, routinesProvider: mockRoutines);
|
||||
|
||||
// Act
|
||||
final weightWidgetFinder = find.byKey(const ValueKey('logs-weight-widget'));
|
||||
expect(weightWidgetFinder, findsOneWidget);
|
||||
final addBtn = find.descendant(
|
||||
of: weightWidgetFinder,
|
||||
matching: find.byIcon(Icons.add),
|
||||
);
|
||||
final removeBtn = find.descendant(
|
||||
of: weightWidgetFinder,
|
||||
matching: find.byIcon(Icons.remove),
|
||||
);
|
||||
|
||||
// Assert
|
||||
// Increment 0 -> 1.25
|
||||
await tester.tap(addBtn);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.descendant(of: weightWidgetFinder, matching: find.text('1.25')),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// Increment 1.25 -> 2.5
|
||||
await tester.tap(addBtn);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.descendant(of: weightWidgetFinder, matching: find.text('2.5')),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// Decrement 2.5 -> 1.25
|
||||
await tester.tap(removeBtn);
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
await tester.pumpAndSettle();
|
||||
expect(
|
||||
find.descendant(of: weightWidgetFinder, matching: find.text('1.25')),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user