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 {
|
class LogsRepsWidget extends ConsumerStatefulWidget {
|
||||||
final _logger = Logger('LogsRepsWidget');
|
|
||||||
|
|
||||||
final num valueChange;
|
final num valueChange;
|
||||||
|
|
||||||
LogsRepsWidget({
|
const LogsRepsWidget({
|
||||||
super.key,
|
super.key,
|
||||||
num? valueChange,
|
num? valueChange,
|
||||||
}) : valueChange = valueChange ?? 1;
|
}) : valueChange = valueChange ?? 1;
|
||||||
|
|
||||||
@override
|
@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 numberFormat = NumberFormat.decimalPattern(Localizations.localeOf(context).toString());
|
||||||
final i18n = AppLocalizations.of(context);
|
final i18n = AppLocalizations.of(context);
|
||||||
|
|
||||||
final logNotifier = ref.read(gymLogProvider.notifier);
|
final logNotifier = ref.read(gymLogProvider.notifier);
|
||||||
final log = ref.watch(gymLogProvider);
|
final log = ref.watch(gymLogProvider);
|
||||||
|
|
||||||
final currentReps = log?.repetitions;
|
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(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -239,7 +274,7 @@ class LogsRepsWidget extends ConsumerWidget {
|
|||||||
icon: const Icon(Icons.remove, color: Colors.black),
|
icon: const Icon(Icons.remove, color: Colors.black),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final base = currentReps ?? 0;
|
final base = currentReps ?? 0;
|
||||||
final newValue = base - valueChange;
|
final newValue = base - widget.valueChange;
|
||||||
if (newValue >= 0 && log != null) {
|
if (newValue >= 0 && log != null) {
|
||||||
logNotifier.setRepetitions(newValue);
|
logNotifier.setRepetitions(newValue);
|
||||||
}
|
}
|
||||||
@@ -251,8 +286,7 @@ class LogsRepsWidget extends ConsumerWidget {
|
|||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
decoration: InputDecoration(labelText: i18n.repetitions),
|
decoration: InputDecoration(labelText: i18n.repetitions),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
key: const ValueKey('reps-field'),
|
controller: _controller,
|
||||||
initialValue: repText,
|
|
||||||
keyboardType: textInputTypeDecimal,
|
keyboardType: textInputTypeDecimal,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
try {
|
try {
|
||||||
@@ -282,7 +316,7 @@ class LogsRepsWidget extends ConsumerWidget {
|
|||||||
icon: const Icon(Icons.add, color: Colors.black),
|
icon: const Icon(Icons.add, color: Colors.black),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final base = currentReps ?? 0;
|
final base = currentReps ?? 0;
|
||||||
final newValue = base + valueChange;
|
final newValue = base + widget.valueChange;
|
||||||
if (newValue >= 0 && log != null) {
|
if (newValue >= 0 && log != null) {
|
||||||
logNotifier.setRepetitions(newValue);
|
logNotifier.setRepetitions(newValue);
|
||||||
}
|
}
|
||||||
@@ -293,27 +327,62 @@ class LogsRepsWidget extends ConsumerWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LogsWeightWidget extends ConsumerWidget {
|
class LogsWeightWidget extends ConsumerStatefulWidget {
|
||||||
final _logger = Logger('LogsWeightWidget');
|
|
||||||
|
|
||||||
final num valueChange;
|
final num valueChange;
|
||||||
|
|
||||||
LogsWeightWidget({
|
const LogsWeightWidget({
|
||||||
super.key,
|
super.key,
|
||||||
num? valueChange,
|
num? valueChange,
|
||||||
}) : valueChange = valueChange ?? 1.25;
|
}) : valueChange = valueChange ?? 1.25;
|
||||||
|
|
||||||
@override
|
@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 numberFormat = NumberFormat.decimalPattern(Localizations.localeOf(context).toString());
|
||||||
final i18n = AppLocalizations.of(context);
|
final i18n = AppLocalizations.of(context);
|
||||||
|
|
||||||
final plateProvider = ref.read(plateCalculatorProvider.notifier);
|
final plateProvider = ref.read(plateCalculatorProvider.notifier);
|
||||||
final logProvider = ref.read(gymLogProvider.notifier);
|
final logProvider = ref.read(gymLogProvider.notifier);
|
||||||
final log = ref.watch(gymLogProvider);
|
final log = ref.watch(gymLogProvider);
|
||||||
|
|
||||||
final currentWeight = log?.weight;
|
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(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -322,7 +391,7 @@ class LogsWeightWidget extends ConsumerWidget {
|
|||||||
icon: const Icon(Icons.remove, color: Colors.black),
|
icon: const Icon(Icons.remove, color: Colors.black),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final base = currentWeight ?? 0;
|
final base = currentWeight ?? 0;
|
||||||
final newValue = base - valueChange;
|
final newValue = base - widget.valueChange;
|
||||||
if (newValue >= 0 && log != null) {
|
if (newValue >= 0 && log != null) {
|
||||||
logProvider.setWeight(newValue);
|
logProvider.setWeight(newValue);
|
||||||
}
|
}
|
||||||
@@ -332,9 +401,8 @@ class LogsWeightWidget extends ConsumerWidget {
|
|||||||
// Text field
|
// Text field
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
key: const ValueKey('weight-field'),
|
controller: _controller,
|
||||||
decoration: InputDecoration(labelText: i18n.weight),
|
decoration: InputDecoration(labelText: i18n.weight),
|
||||||
initialValue: weightText,
|
|
||||||
keyboardType: textInputTypeDecimal,
|
keyboardType: textInputTypeDecimal,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
try {
|
try {
|
||||||
@@ -365,7 +433,7 @@ class LogsWeightWidget extends ConsumerWidget {
|
|||||||
icon: const Icon(Icons.add, color: Colors.black),
|
icon: const Icon(Icons.add, color: Colors.black),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final base = currentWeight ?? 0;
|
final base = currentWeight ?? 0;
|
||||||
final newValue = base + valueChange;
|
final newValue = base + widget.valueChange;
|
||||||
if (log != null) {
|
if (log != null) {
|
||||||
logProvider.setWeight(newValue);
|
logProvider.setWeight(newValue);
|
||||||
}
|
}
|
||||||
@@ -408,9 +476,7 @@ class LogsPastLogsWidget extends ConsumerWidget {
|
|||||||
logProvider.setLog(pastLog);
|
logProvider.setLog(pastLog);
|
||||||
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
ScaffoldMessenger.of(context).hideCurrentSnackBar();
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(content: Text(AppLocalizations.of(context).dataCopied)),
|
||||||
content: Text(AppLocalizations.of(context).dataCopied),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 40),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 40),
|
||||||
|
|||||||
@@ -237,5 +237,125 @@ void main() {
|
|||||||
expect(capturedLog!.routineId, equals(notifier.state.routine.id));
|
expect(capturedLog!.routineId, equals(notifier.state.routine.id));
|
||||||
expect(capturedLog!.iteration, equals(notifier.state.iteration));
|
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