Merge pull request #688 from AyushJagaty/plate-46-calc

Plate calculator
This commit is contained in:
Roland Geider
2025-05-27 20:01:48 +02:00
committed by GitHub
22 changed files with 1063 additions and 150 deletions

View File

@@ -1,4 +1,4 @@
{
"dart.lineLength": 100,
"diffEditor.ignoreTrimWhitespace": true,
"diffEditor.ignoreTrimWhitespace": true
}

View File

@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
platform :ios, '14.0'
platform :ios, '17.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

View File

@@ -22,23 +22,23 @@ PODS:
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- sqlite3 (3.49.1):
- sqlite3/common (= 3.49.1)
- sqlite3/common (3.49.1)
- sqlite3/dbstatvtab (3.49.1):
- sqlite3 (3.49.2):
- sqlite3/common (= 3.49.2)
- sqlite3/common (3.49.2)
- sqlite3/dbstatvtab (3.49.2):
- sqlite3/common
- sqlite3/fts5 (3.49.1):
- sqlite3/fts5 (3.49.2):
- sqlite3/common
- sqlite3/math (3.49.1):
- sqlite3/math (3.49.2):
- sqlite3/common
- sqlite3/perf-threadsafe (3.49.1):
- sqlite3/perf-threadsafe (3.49.2):
- sqlite3/common
- sqlite3/rtree (3.49.1):
- sqlite3/rtree (3.49.2):
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- FlutterMacOS
- sqlite3 (~> 3.49.1)
- sqlite3 (~> 3.49.2)
- sqlite3/dbstatvtab
- sqlite3/fts5
- sqlite3/math
@@ -101,10 +101,10 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/video_player_avfoundation/darwin"
SPEC CHECKSUMS:
camera_avfoundation: 04b44aeb14070126c6529e5ab82cc7c9fca107cf
camera_avfoundation: be3be85408cd4126f250386828e9b1dfa40ab436
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
flutter_keyboard_visibility: 4625131e43015dbbe759d9b20daaf77e0e3f6619
flutter_zxing: e741c4f3335db8910e5c396c4291cdfb320859dc
flutter_zxing: e8bcc43bd3056c70c271b732ed94e7a16fd62f93
image_picker_ios: 7fe1ff8e34c1790d6fff70a32484959f563a928a
integration_test: 4a889634ef21a45d28d50d622cf412dc6d9f586e
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
@@ -112,11 +112,11 @@ SPEC CHECKSUMS:
pointer_interceptor_ios: ec847ef8b0915778bed2b2cef636f4d177fa8eed
rive_common: dd421daaf9ae69f0125aa761dd96abd278399952
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7
sqlite3: fc1400008a9b3525f5914ed715a5d1af0b8f4983
sqlite3_flutter_libs: f6acaa2172e6bb3e2e70c771661905080e8ebcf2
sqlite3: 3c950dc86011117c307eb0b28c4a7bb449dce9f1
sqlite3_flutter_libs: 74334e3ef2dbdb7d37e50859bb45da43935779c4
url_launcher_ios: 694010445543906933d732453a59da0a173ae33d
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b
PODFILE CHECKSUM: 775997f741c536251164e3eacf6e34abf2eb7a17
PODFILE CHECKSUM: 5a367937f10bf0c459576e5e472a1159ee029c13
COCOAPODS: 1.16.2

View File

@@ -130,3 +130,5 @@ const LIBERAPAY_URL = 'https://liberapay.com/wger';
/// the milliseconds themselves can cause the application to crash since it runs
/// out of memory...
const double CHART_MILLISECOND_FACTOR = 100000.0;
enum WeightUnitEnum { kg, lb }

View File

@@ -18,30 +18,35 @@
/// Calculates the number of plates needed to reach a specific weight
List<num> plateCalculator(num totalWeight, num barWeight, List<num> plates) {
final List<num> ans = [];
final List<num> result = [];
final sortedPlates = List.of(plates)..sort();
// Weight is less than the bar
if (totalWeight < barWeight) {
return [];
}
if (sortedPlates.isEmpty) {
return [];
}
// Remove the bar and divide by two to get weight on each side
totalWeight = (totalWeight - barWeight) / 2;
// Weight can't be divided with the smallest plate
if (totalWeight % plates.first > 0) {
if (totalWeight % sortedPlates.first > 0) {
return [];
}
// Iterate through the plates, beginning with the biggest ones
for (final plate in plates.reversed) {
for (final plate in sortedPlates.reversed) {
while (totalWeight >= plate) {
totalWeight -= plate;
ans.add(plate);
result.add(plate);
}
}
return ans;
return result;
}
/// Groups a list of plates as calculated by [plateCalculator]

View File

@@ -37,6 +37,9 @@
"@passwordTooShort": {
"description": "Error message when the user a password that is too short"
},
"selectAvailablePlates": "Select available plates",
"barWeight": "Bar weight",
"useColors": "Use colors",
"password": "Password",
"@password": {},
"confirmPassword": "Confirm password",

View File

@@ -37,6 +37,7 @@ import 'package:wger/providers/routines.dart';
import 'package:wger/providers/user.dart';
import 'package:wger/screens/add_exercise_screen.dart';
import 'package:wger/screens/auth_screen.dart';
import 'package:wger/screens/configure_plates_screen.dart';
import 'package:wger/screens/dashboard.dart';
import 'package:wger/screens/exercise_screen.dart';
import 'package:wger/screens/exercises_screen.dart';
@@ -90,19 +91,20 @@ void main() async {
await PreferenceHelper.instance.migrationSupportFunctionForSharedPreferences();
// Catch errors from Flutter itself (widget build, layout, paint, etc.)
FlutterError.onError = (FlutterErrorDetails details) {
final stack = details.stack ?? StackTrace.empty;
if (kDebugMode) {
FlutterError.dumpErrorToConsole(details);
}
// Don't show the full error dialog for network image loading errors.
if (details.exception is NetworkImageLoadException) {
return;
}
showGeneralErrorDialog(details.exception, stack);
};
// FlutterError.onError = (FlutterErrorDetails details) {
// final stack = details.stack ?? StackTrace.empty;
// if (kDebugMode) {
// FlutterError.dumpErrorToConsole(details);
// }
//
// // Don't show the full error dialog for network image loading errors.
// if (details.exception is NetworkImageLoadException) {
// return;
// }
//
// // showGeneralErrorDialog(details.exception, stack);
// // throw details.exception;
// };
// Catch errors that happen outside of the Flutter framework (e.g., in async operations)
PlatformDispatcher.instance.onError = (error, stack) {
@@ -114,6 +116,7 @@ void main() async {
showHttpExceptionErrorDialog(error);
} else {
showGeneralErrorDialog(error, stack);
// throw error;
}
// Return true to indicate that the error has been handled.
@@ -242,6 +245,7 @@ class MainApp extends StatelessWidget {
AddExerciseScreen.routeName: (ctx) => const AddExerciseScreen(),
AboutPage.routeName: (ctx) => const AboutPage(),
SettingsPage.routeName: (ctx) => const SettingsPage(),
ConfigurePlatesScreen.routeName: (ctx) => const ConfigurePlatesScreen(),
},
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,

View File

@@ -0,0 +1,214 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:wger/helpers/consts.dart';
import 'package:wger/helpers/gym_mode.dart';
import 'package:wger/helpers/shared_preferences.dart';
const DEFAULT_KG_PLATES = [2.5, 5, 10, 15, 20, 25];
const DEFAULT_LB_PLATES = [2.5, 5, 10, 25, 35, 45];
const DEFAULT_BAR_WEIGHT_KG = 20;
const DEFAULT_BAR_WEIGHT_LB = 45;
const PREFS_KEY_PLATES = 'selectedPlates';
final plateCalculatorProvider =
StateNotifierProvider<PlateCalculatorNotifier, PlateCalculatorState>((ref) {
return PlateCalculatorNotifier();
});
class PlateCalculatorState {
final _logger = Logger('PlateWeightsState');
// https://en.wikipedia.org/wiki/Barbell#Bumper_plates
final Map<double, Color> plateColorMapKg = {
25: Colors.red,
20: Colors.blue,
15: Colors.yellow,
10: Colors.green,
5: Colors.white,
2.5: Colors.red,
2: Colors.blue,
1.25: Colors.yellow,
1: Colors.green,
0.5: Colors.white,
};
final Map<double, Color> plateColorMapLb = {
55: Colors.red,
45: Colors.blue,
35: Colors.yellow,
25: Colors.green,
10: Colors.white,
5: Colors.blue,
2.5: Colors.green,
1.25: Colors.white,
};
final bool useColors;
final bool isMetric;
final num totalWeight;
final num barWeight;
final List<num> selectedPlates;
final List<num> availablePlatesKg = const [0.5, 1, 1.25, 2, 2.5, 5, 10, 15, 20, 25];
final List<num> availablePlatesLb = const [2.5, 5, 10, 25, 35, 45];
final availableBarWeightsKg = [10, 15, 20];
final availableBarWeightsLb = [15, 20, 25, 33, 45];
PlateCalculatorState({
this.useColors = true,
num? barWeight,
this.isMetric = true,
this.totalWeight = 0,
List<num>? selectedPlates,
}) : barWeight = barWeight ?? (isMetric ? DEFAULT_BAR_WEIGHT_KG : DEFAULT_BAR_WEIGHT_LB),
selectedPlates =
selectedPlates ?? (isMetric ? [...DEFAULT_KG_PLATES] : [...DEFAULT_LB_PLATES]);
PlateCalculatorState.fromJson(Map<String, dynamic> plateData)
: useColors = plateData['useColors'] ?? true,
isMetric = plateData['isMetric'] ?? true,
selectedPlates = plateData['selectedPlates']?.cast<num>() ?? [...DEFAULT_KG_PLATES],
barWeight = plateData['barWeight'] ??
((plateData['isMetric'] ?? true) ? DEFAULT_BAR_WEIGHT_KG : DEFAULT_BAR_WEIGHT_LB),
totalWeight = 0;
PlateCalculatorState copyWith({
bool? useColors,
bool? isMetric,
num? totalWeight,
num? barWeight,
List<num>? selectedPlates,
}) {
return PlateCalculatorState(
useColors: useColors ?? this.useColors,
isMetric: isMetric ?? this.isMetric,
totalWeight: totalWeight ?? this.totalWeight,
barWeight: barWeight ?? this.barWeight,
selectedPlates: selectedPlates ?? this.selectedPlates,
);
}
Map<String, dynamic> toJson() {
return {
'useColors': useColors,
'isMetric': isMetric,
'selectedPlates': selectedPlates,
'barWeight': barWeight,
};
}
List<num> get availablePlates {
return isMetric ? availablePlatesKg : availablePlatesLb;
}
List<num> get availableBarsWeights {
return isMetric ? availableBarWeightsKg : availableBarWeightsLb;
}
List<num> get platesList {
return plateCalculator(totalWeight, barWeight, selectedPlates);
}
bool get hasPlates {
return platesList.isNotEmpty;
}
Map<num, int> get calculatePlates {
return groupPlates(plateCalculator(totalWeight, barWeight, selectedPlates));
}
Color getColor(num plate) {
if (!useColors) {
return Colors.grey;
}
if (isMetric) {
return plateColorMapKg[plate.toDouble()] ?? Colors.grey;
}
return plateColorMapLb[plate.toDouble()] ?? Colors.grey;
}
}
class PlateCalculatorNotifier extends StateNotifier<PlateCalculatorState> {
final _logger = Logger('PlateCalculatorNotifier');
late SharedPreferencesAsync prefs;
PlateCalculatorNotifier({SharedPreferencesAsync? prefs}) : super(PlateCalculatorState()) {
this.prefs = prefs ?? PreferenceHelper.asyncPref;
_readDataFromSharedPrefs();
}
Future<void> saveToSharedPrefs() async {
_logger.fine('Saving plate data to SharedPreferences');
await prefs.setString(PREFS_KEY_PLATES, jsonEncode(state.toJson()));
}
Future<void> _readDataFromSharedPrefs() async {
_logger.fine('Reading plate data from SharedPreferences');
final prefsData = await prefs.getString(PREFS_KEY_PLATES);
if (prefsData != null) {
try {
state = PlateCalculatorState.fromJson(json.decode(prefsData));
_logger.fine('Plate data loaded from SharedPreferences: ${state.toJson()}');
} catch (e) {
_logger.fine('Error decoding plate data from SharedPreferences: $e');
state = PlateCalculatorState();
}
}
}
Future<void> toggleSelection(num x) async {
final newSelectedPlates = List.of(state.selectedPlates);
if (newSelectedPlates.contains(x)) {
newSelectedPlates.remove(x);
} else {
newSelectedPlates.add(x);
}
state = state.copyWith(selectedPlates: newSelectedPlates);
await saveToSharedPrefs();
}
void setBarWeight(num x) {
_logger.fine('Setting bar weight to $x');
state = state.copyWith(barWeight: x);
}
void setUseColors(bool value) {
state = state.copyWith(useColors: value);
}
void unitChange({WeightUnitEnum? unit}) {
final WeightUnitEnum changeTo =
unit ?? (state.isMetric ? WeightUnitEnum.lb : WeightUnitEnum.kg);
if (changeTo == WeightUnitEnum.kg && !state.isMetric) {
state = state.copyWith(
isMetric: true,
totalWeight: 0,
barWeight: DEFAULT_BAR_WEIGHT_KG,
selectedPlates: [...DEFAULT_KG_PLATES],
);
}
if (changeTo == WeightUnitEnum.lb && state.isMetric) {
state = state.copyWith(
isMetric: false,
totalWeight: 0,
barWeight: DEFAULT_BAR_WEIGHT_LB,
selectedPlates: [...DEFAULT_LB_PLATES],
);
}
}
void setWeight(num x) {
_logger.fine('Setting weight to $x');
state = state.copyWith(totalWeight: x);
}
}

View File

@@ -45,6 +45,15 @@ class UserProvider with ChangeNotifier {
profile = null;
}
// // change the unit of plates
// void changeUnit({changeTo = 'kg'}) {
// if (changeTo == 'kg') {
// profile?.weightUnitStr = 'lb';
// } else {
// profile?.weightUnitStr = 'kg';
// }
// }
// Load theme mode from SharedPreferences
Future<void> _loadThemeMode() async {
final prefsDarkMode = await prefs.getBool(PREFS_USER_DARK_THEME);

View File

@@ -0,0 +1,19 @@
import 'package:flutter/material.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/widgets/routines/plate_calculator.dart';
class ConfigurePlatesScreen extends StatelessWidget {
static const routeName = '/ConfigureAvailablePlates';
const ConfigurePlatesScreen({super.key});
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context);
return Scaffold(
appBar: AppBar(title: Text(i18n.selectAvailablePlates)),
body: const ConfigureAvailablePlates(),
);
}
}

View File

@@ -47,6 +47,8 @@ class GymModeScreen extends StatelessWidget {
.first;
return Scaffold(
// backgroundColor: Theme.of(context).cardColor,
// primary: false,
body: SafeArea(
child: Consumer<RoutinesProvider>(
builder: (context, value, child) => GymMode(dayDataGym, dayDataDisplay, args.iteration),

View File

@@ -23,6 +23,7 @@ import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/providers/exercises.dart';
import 'package:wger/providers/nutrition.dart';
import 'package:wger/providers/user.dart';
import 'package:wger/screens/configure_plates_screen.dart';
class SettingsPage extends StatelessWidget {
static String routeName = '/SettingsPage';
@@ -111,6 +112,13 @@ class SettingsPage extends StatelessWidget {
}).toList(),
),
),
ListTile(
title: Text(i18n.selectAvailablePlates),
onTap: () {
Navigator.of(context).pushNamed(ConfigurePlatesScreen.routeName);
},
trailing: const Icon(Icons.chevron_right),
),
],
),
);

View File

@@ -42,7 +42,6 @@ class ExerciseOverview extends StatelessWidget {
_controller,
exercisePages: _exercisePages,
),
const Divider(),
Expanded(
child: SingleChildScrollView(
child: Padding(

View File

@@ -16,26 +16,29 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart' as provider;
import 'package:wger/exceptions/http_exception.dart';
import 'package:wger/helpers/consts.dart';
import 'package:wger/helpers/gym_mode.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/models/exercises/exercise.dart';
import 'package:wger/models/workouts/log.dart';
import 'package:wger/models/workouts/routine.dart';
import 'package:wger/models/workouts/set_config_data.dart';
import 'package:wger/models/workouts/slot_data.dart';
import 'package:wger/providers/plate_weights.dart';
import 'package:wger/providers/routines.dart';
import 'package:wger/screens/configure_plates_screen.dart';
import 'package:wger/widgets/core/core.dart';
import 'package:wger/widgets/core/progress_indicator.dart';
import 'package:wger/widgets/routines/forms/reps_unit.dart';
import 'package:wger/widgets/routines/forms/rir.dart';
import 'package:wger/widgets/routines/forms/weight_unit.dart';
import 'package:wger/widgets/routines/gym_mode/navigation.dart';
import 'package:wger/widgets/routines/plate_calculator.dart';
class LogPage extends StatefulWidget {
class LogPage extends ConsumerStatefulWidget {
final PageController _controller;
final SetConfigData _configData;
final SlotData _slotData;
@@ -43,8 +46,7 @@ class LogPage extends StatefulWidget {
final Routine _workoutPlan;
final double _ratioCompleted;
final Map<Exercise, int> _exercisePages;
late Log _log;
final int _iteration;
final Log _log;
LogPage(
this._controller,
@@ -54,18 +56,16 @@ class LogPage extends StatefulWidget {
this._workoutPlan,
this._ratioCompleted,
this._exercisePages,
this._iteration,
) {
_log = Log.fromSetConfigData(_configData);
_log.routineId = _workoutPlan.id!;
_log.iteration = _iteration;
}
int? iteration,
) : _log = Log.fromSetConfigData(_configData)
..routineId = _workoutPlan.id!
..iteration = iteration;
@override
_LogPageState createState() => _LogPageState();
}
class _LogPageState extends State<LogPage> {
class _LogPageState extends ConsumerState<LogPage> {
final _form = GlobalKey<FormState>();
final _repetitionsController = TextEditingController();
final _weightController = TextEditingController();
@@ -122,7 +122,9 @@ class _LogPageState extends State<LogPage> {
controller: _repetitionsController,
keyboardType: TextInputType.number,
focusNode: focusNode,
onFieldSubmitted: (_) {},
onFieldSubmitted: (_) {
// Placeholder for potential future logic
},
onSaved: (newValue) {
widget._log.repetitions = num.parse(newValue!);
focusNode.unfocus();
@@ -164,6 +166,9 @@ class _LogPageState extends State<LogPage> {
setState(() {
widget._log.weight = newValue;
_weightController.text = newValue.toString();
ref.read(plateCalculatorProvider.notifier).setWeight(
_weightController.text == '' ? 0 : double.parse(_weightController.text),
);
});
}
} on FormatException {}
@@ -176,12 +181,17 @@ class _LogPageState extends State<LogPage> {
),
controller: _weightController,
keyboardType: TextInputType.number,
onFieldSubmitted: (_) {},
onFieldSubmitted: (_) {
// Placeholder for potential future logic
},
onChanged: (value) {
try {
num.parse(value);
setState(() {
widget._log.weight = num.parse(value);
ref.read(plateCalculatorProvider.notifier).setWeight(
_weightController.text == '' ? 0 : double.parse(_weightController.text),
);
});
} on FormatException {}
},
@@ -208,6 +218,9 @@ class _LogPageState extends State<LogPage> {
setState(() {
widget._log.weight = newValue;
_weightController.text = newValue.toString();
ref.read(plateCalculatorProvider.notifier).setWeight(
_weightController.text == '' ? 0 : double.parse(_weightController.text),
);
});
} on FormatException {}
},
@@ -247,6 +260,7 @@ class _LogPageState extends State<LogPage> {
onChanged: (v) => {},
),
),
const SizedBox(width: 8),
],
),
if (_detailed)
@@ -258,6 +272,7 @@ class _LogPageState extends State<LogPage> {
Flexible(
child: WeightUnitInputWidget(widget._log.weightUnitId, onChanged: (v) => {}),
),
const SizedBox(width: 8),
],
),
if (_detailed)
@@ -272,6 +287,7 @@ class _LogPageState extends State<LogPage> {
},
),
SwitchListTile(
dense: true,
title: Text(AppLocalizations.of(context).setUnitsAndRir),
value: _detailed,
onChanged: (value) {
@@ -280,7 +296,7 @@ class _LogPageState extends State<LogPage> {
});
},
),
ElevatedButton(
FilledButton(
onPressed: _isSaving
? null
: () async {
@@ -327,103 +343,118 @@ class _LogPageState extends State<LogPage> {
}
Widget getPastLogs() {
return ListView(
children: [
Text(
AppLocalizations.of(context).labelWorkoutLogs,
style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center,
),
...widget._workoutPlan.filterLogsByExercise(widget._exercise.id!, unique: true).map((log) {
return ListTile(
title: Text(log.singleLogRepTextNoNl),
subtitle: Text(
DateFormat.yMd(Localizations.localeOf(context).languageCode).format(log.date),
),
trailing: const Icon(Icons.copy),
onTap: () {
setState(() {
// Text field
_repetitionsController.text = log.repetitions?.toString() ?? '';
_weightController.text = log.weight?.toString() ?? '';
return Container(
padding: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
// color: Theme.of(context).secondaryHeaderColor,
// color: Theme.of(context).splashColor,
// color: Theme.of(context).colorScheme.onInverseSurface,
// border: Border.all(color: Colors.black, width: 1),
),
child: ListView(
children: [
Text(
AppLocalizations.of(context).labelWorkoutLogs,
style: Theme.of(context).textTheme.titleMedium,
textAlign: TextAlign.center,
),
...widget._workoutPlan
.filterLogsByExercise(widget._exercise.id!, unique: true)
.map((log) {
return ListTile(
title: Text(log.singleLogRepTextNoNl),
subtitle: Text(
DateFormat.yMd(Localizations.localeOf(context).languageCode).format(log.date),
),
trailing: const Icon(Icons.copy),
onTap: () {
setState(() {
// Text field
_repetitionsController.text = log.repetitions?.toString() ?? '';
_weightController.text = log.weight?.toString() ?? '';
// Drop downs
widget._log.rir = log.rir;
widget._log.repetitionUnit = log.repetitionsUnitObj;
widget._log.weightUnit = log.weightUnitObj;
// Drop downs
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context).dataCopied),
));
});
},
contentPadding: const EdgeInsets.symmetric(horizontal: 40),
);
}),
],
widget._log.rir = log.rir;
widget._log.repetitionUnit = log.repetitionsUnitObj;
widget._log.weightUnit = log.weightUnitObj;
ScaffoldMessenger.of(context).hideCurrentSnackBar();
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(AppLocalizations.of(context).dataCopied),
));
});
},
contentPadding: const EdgeInsets.symmetric(horizontal: 40),
);
}),
],
),
);
}
Widget getPlates() {
final plates = plateCalculator(
double.parse(_weightController.text == '' ? '0' : _weightController.text),
BAR_WEIGHT,
AVAILABLE_PLATES,
);
final groupedPlates = groupPlates(plates);
final plateWeightsState = ref.watch(plateCalculatorProvider);
return Column(
children: [
Text(
AppLocalizations.of(context).plateCalculator,
style: Theme.of(context).textTheme.titleLarge,
),
SizedBox(
height: 35,
child: plates.isNotEmpty
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
...groupedPlates.keys.map(
(key) => Row(
children: [
Text(groupedPlates[key].toString()),
const Text('×'),
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
shape: BoxShape.circle,
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 3),
child: SizedBox(
height: 35,
width: 35,
child: Align(
alignment: Alignment.center,
child: Text(
key.toString(),
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
),
return Container(
color: Theme.of(context).colorScheme.onInverseSurface,
child: Column(
children: [
// Row(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Text(
// AppLocalizations.of(context).plateCalculator,
// style: Theme.of(context).textTheme.titleMedium,
// ),
// IconButton(
// onPressed: () {
// Navigator.of(context)
// .push(MaterialPageRoute(builder: (context) => const AddPlateWeights()));
// },
// icon: const Icon(Icons.settings, size: 16),
// ),
// ],
// ),
GestureDetector(
onTap: () {
Navigator.of(context).pushNamed(ConfigurePlatesScreen.routeName);
},
child: SizedBox(
child: plateWeightsState.hasPlates
? Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
...plateWeightsState.calculatePlates.entries.map(
(entry) => Row(
children: [
Text(entry.value.toString()),
const Text('×'),
PlateWeight(
value: entry.key,
size: 37,
padding: 2,
margin: 0,
color: ref.read(plateCalculatorProvider).getColor(entry.key),
),
),
const SizedBox(width: 10),
],
),
const SizedBox(width: 10),
],
),
],
)
: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
child: MutedText(
AppLocalizations.of(context).plateCalculatorNotDivisible,
textAlign: TextAlign.center,
),
),
],
)
: MutedText(
AppLocalizations.of(context).plateCalculatorNotDivisible,
),
),
const SizedBox(height: 3),
],
),
),
const SizedBox(height: 3),
],
),
);
}
@@ -436,27 +467,43 @@ class _LogPageState extends State<LogPage> {
widget._controller,
exercisePages: widget._exercisePages,
),
Center(
child: Text(
widget._configData.textRepr,
style: Theme.of(context).textTheme.headlineMedium,
textAlign: TextAlign.center,
Container(
color: Theme.of(context).colorScheme.onInverseSurface,
padding: const EdgeInsets.symmetric(vertical: 10),
child: Center(
child: Text(
widget._configData.textRepr,
style: Theme.of(context)
.textTheme
.headlineMedium
?.copyWith(color: Theme.of(context).colorScheme.primary),
textAlign: TextAlign.center,
),
),
),
if (widget._slotData.comment != '')
if (widget._slotData.comment.isNotEmpty)
Text(widget._slotData.comment, textAlign: TextAlign.center),
// Only show calculator for barbell
if (widget._log.exercise.equipment.map((e) => e.id).contains(ID_EQUIPMENT_BARBELL))
getPlates(),
const SizedBox(height: 10),
Expanded(
child: (widget._workoutPlan.filterLogsByExercise(widget._exercise.id!).isNotEmpty)
? getPastLogs()
: Container(),
),
// Only show calculator for barbell
if (widget._log.exercise.equipment.map((e) => e.id).contains(ID_EQUIPMENT_BARBELL))
getPlates(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),
child: Card(child: getForm()),
padding: const EdgeInsets.all(10),
child: Card(
color: Theme.of(context).colorScheme.inversePrimary,
// color: Theme.of(context).secondaryHeaderColor,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 5),
child: getForm(),
),
),
),
NavigationFooter(widget._controller, widget._ratioCompleted),
],

View File

@@ -52,7 +52,7 @@ class NavigationFooter extends StatelessWidget {
const SizedBox(width: 48),
Expanded(
child: LinearProgressIndicator(
minHeight: 1.5,
minHeight: 3,
value: _ratioCompleted,
valueColor: const AlwaysStoppedAnimation<Color>(wgerPrimaryColor),
),

View File

@@ -59,7 +59,6 @@ class SessionPage extends StatelessWidget {
_controller,
exercisePages: _exercisePages,
),
const Divider(),
Expanded(child: Container()),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 15),

View File

@@ -21,7 +21,6 @@ class StartPage extends StatelessWidget {
_controller,
exercisePages: _exercisePages,
),
const Divider(),
Expanded(
child: ListView(
children: [

View File

@@ -0,0 +1,166 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:wger/helpers/consts.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/providers/plate_weights.dart';
class PlateWeight extends StatelessWidget {
final num value;
final Color color;
final bool isSelected;
final double size;
final double padding;
final double margin;
const PlateWeight({
super.key,
required this.value,
required this.color,
this.isSelected = true,
this.size = 50,
this.padding = 8,
this.margin = 3,
});
@override
Widget build(BuildContext context) {
return SizedBox(
key: ValueKey('plateWeight-$value'),
height: size,
width: size,
child: Padding(
padding: EdgeInsets.all(padding),
child: Container(
alignment: Alignment.center,
margin: EdgeInsets.all(margin),
decoration: BoxDecoration(
color: isSelected ? color : Colors.black12,
shape: BoxShape.circle,
border: Border.all(color: Colors.black, width: isSelected ? 2 : 0),
),
child: Text(
value.toString(),
textAlign: TextAlign.center,
style: TextStyle(
color: isSelected ? Colors.black : Colors.black87,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
),
),
);
}
}
class ConfigureAvailablePlates extends ConsumerStatefulWidget {
const ConfigureAvailablePlates({super.key});
@override
ConsumerState<ConfigureAvailablePlates> createState() => _AddPlateWeightsState();
}
class _AddPlateWeightsState extends ConsumerState<ConfigureAvailablePlates> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(plateCalculatorProvider.notifier);
});
}
@override
Widget build(BuildContext context) {
final i18n = AppLocalizations.of(context);
final plateWeightsState = ref.watch(plateCalculatorProvider);
final plateWeightsNotifier = ref.read(plateCalculatorProvider.notifier);
// final userProvider = provider.Provider.of<UserProvider>(context);
return Column(
mainAxisSize: MainAxisSize.max,
children: [
Padding(
padding: const EdgeInsets.all(10),
child: DropdownMenu<WeightUnitEnum>(
key: const ValueKey('weightUnitDropdown'),
width: double.infinity,
initialSelection: plateWeightsState.isMetric ? WeightUnitEnum.kg : WeightUnitEnum.lb,
requestFocusOnTap: true,
label: Text(i18n.unit),
onSelected: (WeightUnitEnum? unit) {
if (unit == null) {
return;
}
plateWeightsNotifier.unitChange(unit: unit);
// userProvider.changeUnit(changeTo: unit.name);
// userProvider.saveProfile();
},
dropdownMenuEntries: WeightUnitEnum.values.map((unit) {
return DropdownMenuEntry<WeightUnitEnum>(
value: unit,
label: unit == WeightUnitEnum.kg ? i18n.kg : i18n.lb,
);
}).toList(),
),
),
Padding(
padding: const EdgeInsets.all(10),
child: DropdownMenu<num>(
key: const ValueKey('barWeightDropdown'),
width: double.infinity,
initialSelection: plateWeightsState.barWeight,
requestFocusOnTap: true,
label: Text(i18n.barWeight),
onSelected: (num? value) {
if (value == null) {
return;
}
plateWeightsNotifier.setBarWeight(value);
},
dropdownMenuEntries: plateWeightsState.availableBarsWeights.map((value) {
return DropdownMenuEntry<num>(
value: value,
label: value.toString(),
);
}).toList(),
),
),
SwitchListTile(
key: const ValueKey('useColorsSwitch'),
title: Text(i18n.useColors),
value: plateWeightsState.useColors,
onChanged: (state) => plateWeightsNotifier.setUseColors(state),
),
LayoutBuilder(
builder: (context, constraints) {
const double widthThreshold = 450.0;
final int crossAxisCount = constraints.maxWidth > widthThreshold ? 10 : 5;
return GridView.count(
crossAxisCount: crossAxisCount,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
children: plateWeightsState.availablePlates.map((number) {
final bool isSelected = plateWeightsState.selectedPlates.contains(number);
return GestureDetector(
onTap: () => plateWeightsNotifier.toggleSelection(number),
child: PlateWeight(
value: number,
isSelected: isSelected,
color: plateWeightsState.getColor(number),
),
);
}).toList(),
);
},
),
FilledButton(
onPressed: () {
plateWeightsNotifier.saveToSharedPrefs();
Navigator.pop(context);
},
child: Text(i18n.save),
),
],
);
}
}

View File

@@ -0,0 +1,123 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart'; // Added for annotations
import 'package:mockito/mockito.dart'; // Added for mockito
import 'package:shared_preferences/shared_preferences.dart';
import 'package:wger/helpers/consts.dart';
import 'package:wger/providers/plate_weights.dart';
import 'plate_calculator_test.mocks.dart';
@GenerateMocks([SharedPreferencesAsync])
void main() {
group('PlateWeightsNotifier', () {
late PlateCalculatorNotifier notifier;
late MockSharedPreferencesAsync mockPrefs;
setUp(() {
mockPrefs = MockSharedPreferencesAsync();
when(mockPrefs.getString(PREFS_KEY_PLATES)).thenAnswer((_) async => null);
when(mockPrefs.setString(any, any)).thenAnswer((_) async => true);
notifier = PlateCalculatorNotifier(prefs: mockPrefs);
});
test('toggleSelection adds and removes plates', () async {
// Test adding a plate
await notifier.toggleSelection(0.5);
expect(notifier.state.selectedPlates.contains(0.5), true);
// Test removing a plate
await notifier.toggleSelection(0.5);
expect(notifier.state.selectedPlates.contains(0.5), false);
});
test('unitChange updates state correctly', () {
// Change to imperial
notifier.setWeight(123);
notifier.unitChange(unit: WeightUnitEnum.lb);
expect(notifier.state.isMetric, false);
expect(notifier.state.totalWeight, 0);
expect(notifier.state.selectedPlates, DEFAULT_LB_PLATES);
expect(notifier.state.barWeight, DEFAULT_BAR_WEIGHT_LB);
expect(notifier.state.availablePlates, notifier.state.availablePlatesLb);
// Change back to metric
notifier.setWeight(123);
notifier.unitChange(unit: WeightUnitEnum.kg);
expect(notifier.state.isMetric, true);
expect(notifier.state.totalWeight, 0);
expect(notifier.state.selectedPlates, DEFAULT_KG_PLATES);
expect(notifier.state.barWeight, DEFAULT_BAR_WEIGHT_KG);
expect(notifier.state.availablePlates, notifier.state.availablePlatesKg);
});
test('setWeight updates totalWeight', () {
notifier.setWeight(100);
expect(notifier.state.totalWeight, 100);
});
});
group('PlateWeightsState', () {
test('copyWith creates a new instance with updated values', () {
final initialState = PlateCalculatorState();
final updatedState = initialState.copyWith(
isMetric: false,
totalWeight: 100,
barWeight: 15,
selectedPlates: [1, 2, 3],
);
expect(updatedState.isMetric, false);
expect(updatedState.barWeight, 15);
expect(updatedState.totalWeight, 100);
expect(updatedState.selectedPlates, [1, 2, 3]);
});
test('toJson returns correct map', () {
final state = PlateCalculatorState(isMetric: false, selectedPlates: [10, 20]);
final json = state.toJson();
expect(json['isMetric'], false);
expect(json['barWeight'], DEFAULT_BAR_WEIGHT_LB);
expect(json['selectedPlates'], [10, 20]);
});
test('barWeight returns correct default value based on isMetric', () {
final metricState = PlateCalculatorState(isMetric: true);
expect(metricState.barWeight, DEFAULT_BAR_WEIGHT_KG);
final imperialState = PlateCalculatorState(isMetric: false);
expect(imperialState.barWeight, DEFAULT_BAR_WEIGHT_LB);
});
test('availablePlates returns correct default list based on isMetric', () {
final metricState = PlateCalculatorState(isMetric: true);
expect(metricState.availablePlates, metricState.availablePlatesKg);
final imperialState = PlateCalculatorState(isMetric: false);
expect(imperialState.availablePlates, metricState.availablePlatesLb);
});
test('getColor returns correct color - 1', () {
final metricState = PlateCalculatorState(isMetric: true);
expect(metricState.getColor(20), Colors.blue);
expect(metricState.getColor(10), Colors.green);
expect(metricState.getColor(0.1), Colors.grey, reason: 'Fallback color');
final imperialState = PlateCalculatorState(isMetric: false);
expect(imperialState.getColor(45), Colors.blue);
expect(imperialState.getColor(35), Colors.yellow);
expect(imperialState.getColor(0.1), Colors.grey, reason: 'Fallback color');
});
test('getColor returns correct color - 2', () {
final metricState = PlateCalculatorState(isMetric: true, useColors: false);
expect(metricState.getColor(20), Colors.grey);
expect(metricState.getColor(10), Colors.grey);
final imperialState = PlateCalculatorState(isMetric: false, useColors: false);
expect(imperialState.getColor(45), Colors.grey);
expect(imperialState.getColor(25), Colors.grey);
});
});
}

View File

@@ -0,0 +1,213 @@
// Mocks generated by Mockito 5.4.6 from annotations
// in wger/test/providers/plate_calculator_test.dart.
// Do not manually edit this file.
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:async' as _i3;
import 'package:mockito/mockito.dart' as _i1;
import 'package:shared_preferences/src/shared_preferences_async.dart' as _i2;
// ignore_for_file: type=lint
// ignore_for_file: avoid_redundant_argument_values
// ignore_for_file: avoid_setters_without_getters
// ignore_for_file: comment_references
// ignore_for_file: deprecated_member_use
// ignore_for_file: deprecated_member_use_from_same_package
// ignore_for_file: implementation_imports
// ignore_for_file: invalid_use_of_visible_for_testing_member
// ignore_for_file: must_be_immutable
// ignore_for_file: prefer_const_constructors
// ignore_for_file: unnecessary_parenthesis
// ignore_for_file: camel_case_types
// ignore_for_file: subtype_of_sealed_class
/// A class which mocks [SharedPreferencesAsync].
///
/// See the documentation for Mockito's code generation for more information.
// ignore: must_be_immutable
class MockSharedPreferencesAsync extends _i1.Mock implements _i2.SharedPreferencesAsync {
MockSharedPreferencesAsync() {
_i1.throwOnMissingStub(this);
}
@override
_i3.Future<Set<String>> getKeys({Set<String>? allowList}) => (super.noSuchMethod(
Invocation.method(
#getKeys,
[],
{#allowList: allowList},
),
returnValue: _i3.Future<Set<String>>.value(<String>{}),
) as _i3.Future<Set<String>>);
@override
_i3.Future<Map<String, Object?>> getAll({Set<String>? allowList}) => (super.noSuchMethod(
Invocation.method(
#getAll,
[],
{#allowList: allowList},
),
returnValue: _i3.Future<Map<String, Object?>>.value(<String, Object?>{}),
) as _i3.Future<Map<String, Object?>>);
@override
_i3.Future<bool?> getBool(String? key) => (super.noSuchMethod(
Invocation.method(
#getBool,
[key],
),
returnValue: _i3.Future<bool?>.value(),
) as _i3.Future<bool?>);
@override
_i3.Future<int?> getInt(String? key) => (super.noSuchMethod(
Invocation.method(
#getInt,
[key],
),
returnValue: _i3.Future<int?>.value(),
) as _i3.Future<int?>);
@override
_i3.Future<double?> getDouble(String? key) => (super.noSuchMethod(
Invocation.method(
#getDouble,
[key],
),
returnValue: _i3.Future<double?>.value(),
) as _i3.Future<double?>);
@override
_i3.Future<String?> getString(String? key) => (super.noSuchMethod(
Invocation.method(
#getString,
[key],
),
returnValue: _i3.Future<String?>.value(),
) as _i3.Future<String?>);
@override
_i3.Future<List<String>?> getStringList(String? key) => (super.noSuchMethod(
Invocation.method(
#getStringList,
[key],
),
returnValue: _i3.Future<List<String>?>.value(),
) as _i3.Future<List<String>?>);
@override
_i3.Future<bool> containsKey(String? key) => (super.noSuchMethod(
Invocation.method(
#containsKey,
[key],
),
returnValue: _i3.Future<bool>.value(false),
) as _i3.Future<bool>);
@override
_i3.Future<void> setBool(
String? key,
bool? value,
) =>
(super.noSuchMethod(
Invocation.method(
#setBool,
[
key,
value,
],
),
returnValue: _i3.Future<void>.value(),
returnValueForMissingStub: _i3.Future<void>.value(),
) as _i3.Future<void>);
@override
_i3.Future<void> setInt(
String? key,
int? value,
) =>
(super.noSuchMethod(
Invocation.method(
#setInt,
[
key,
value,
],
),
returnValue: _i3.Future<void>.value(),
returnValueForMissingStub: _i3.Future<void>.value(),
) as _i3.Future<void>);
@override
_i3.Future<void> setDouble(
String? key,
double? value,
) =>
(super.noSuchMethod(
Invocation.method(
#setDouble,
[
key,
value,
],
),
returnValue: _i3.Future<void>.value(),
returnValueForMissingStub: _i3.Future<void>.value(),
) as _i3.Future<void>);
@override
_i3.Future<void> setString(
String? key,
String? value,
) =>
(super.noSuchMethod(
Invocation.method(
#setString,
[
key,
value,
],
),
returnValue: _i3.Future<void>.value(),
returnValueForMissingStub: _i3.Future<void>.value(),
) as _i3.Future<void>);
@override
_i3.Future<void> setStringList(
String? key,
List<String>? value,
) =>
(super.noSuchMethod(
Invocation.method(
#setStringList,
[
key,
value,
],
),
returnValue: _i3.Future<void>.value(),
returnValueForMissingStub: _i3.Future<void>.value(),
) as _i3.Future<void>);
@override
_i3.Future<void> remove(String? key) => (super.noSuchMethod(
Invocation.method(
#remove,
[key],
),
returnValue: _i3.Future<void>.value(),
returnValueForMissingStub: _i3.Future<void>.value(),
) as _i3.Future<void>);
@override
_i3.Future<void> clear({Set<String>? allowList}) => (super.noSuchMethod(
Invocation.method(
#clear,
[],
{#allowList: allowList},
),
returnValue: _i3.Future<void>.value(),
returnValueForMissingStub: _i3.Future<void>.value(),
) as _i3.Future<void>);
}

View File

@@ -22,6 +22,8 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:provider/provider.dart';
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
import 'package:wger/helpers/json.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/providers/base_provider.dart';
@@ -51,6 +53,10 @@ void main() {
final testRoutine = getTestRoutine();
final testExercises = getTestExercises();
setUp(() {
SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty();
});
Widget renderGymMode({locale = 'en'}) {
return ChangeNotifierProvider<RoutinesProvider>(
create: (context) => RoutinesProvider(
@@ -93,9 +99,11 @@ void main() {
)).thenReturn([]);
await tester.pumpWidget(renderGymMode());
//await tester.pumpWidget(createHomeScreen());
await tester.tap(find.byType(TextButton));
//print(find.byType(TextButton));
await tester.pumpAndSettle();
//await tester.ensureVisible(find.byKey(Key(key as String)));
//
// Start page
//
@@ -128,6 +136,7 @@ void main() {
expect(find.text('Bench press'), findsOneWidget);
expect(find.byType(LogPage), findsOneWidget);
expect(find.byType(Form), findsOneWidget);
// print(find.byType(Form));
expect(find.byType(ListTile), findsNWidgets(3), reason: 'Two logs and the switch tile');
expect(find.text('10 × 10 kg (1.5 RiR)'), findsOneWidget);
expect(find.text('12 × 10 kg (2 RiR)'), findsOneWidget);

View File

@@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences_platform_interface/in_memory_shared_preferences_async.dart';
import 'package:shared_preferences_platform_interface/shared_preferences_async_platform_interface.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/providers/plate_weights.dart';
import 'package:wger/widgets/routines/plate_calculator.dart';
Future<void> pumpWidget(
WidgetTester tester, {
PlateCalculatorNotifier? notifier,
}) async {
await tester.pumpWidget(
ProviderScope(
overrides: [
if (notifier != null) plateCalculatorProvider.overrideWith((ref) => notifier),
],
child: const MaterialApp(
locale: Locale('en'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
home: Scaffold(body: ConfigureAvailablePlates()),
),
),
);
}
void main() {
setUp(() {
SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty();
});
testWidgets('Smoke test for ConfigureAvailablePlates', (WidgetTester tester) async {
await pumpWidget(tester);
expect(find.text('Unit'), findsOneWidget);
expect(find.text('Bar weight'), findsOneWidget);
expect(find.byType(SwitchListTile), findsOneWidget);
expect(find.byType(FilledButton), findsOneWidget);
expect(find.byType(PlateWeight), findsWidgets);
});
testWidgets(
'ConfigureAvailablePlates interagiert korrekt mit echtem Notifier',
(WidgetTester tester) async {
// Arrange
final notifier = PlateCalculatorNotifier();
notifier.state = PlateCalculatorState(
isMetric: true,
barWeight: 20,
useColors: false,
selectedPlates: [1.25, 2.5],
);
await pumpWidget(tester, notifier: notifier);
// Correctly changes the unit
expect(notifier.state.isMetric, isTrue);
await tester.tap(find.byKey(const ValueKey('weightUnitDropdown')));
await tester.pumpAndSettle();
await tester.tap(find.text('lb').last);
await tester.pumpAndSettle();
expect(notifier.state.isMetric, isFalse);
// Correctly changes the bar weight
expect(notifier.state.barWeight, 45);
await tester.tap(find.byKey(const ValueKey('barWeightDropdown')));
await tester.pumpAndSettle();
final menuItem = find.ancestor(
of: find.text('25'),
matching: find.byType(InkWell),
);
expect(menuItem, findsOneWidget);
await tester.tap(menuItem);
await tester.pumpAndSettle();
expect(notifier.state.barWeight, 25);
// Correctly toggles the useColors switch
expect(notifier.state.useColors, isFalse);
await tester.tap(find.byKey(const ValueKey('useColorsSwitch')));
await tester.pumpAndSettle();
expect(notifier.state.useColors, isTrue);
// Correctly adds and removes plates
expect(notifier.state.selectedPlates.contains(5.0), isTrue);
await tester.tap(find.byKey(const ValueKey('plateWeight-5')));
await tester.pumpAndSettle();
expect(notifier.state.selectedPlates.contains(5.0), isFalse);
},
);
}