diff --git a/.vscode/settings.json b/.vscode/settings.json index 9619dd65..a318ce4a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,4 @@ { "dart.lineLength": 100, - "diffEditor.ignoreTrimWhitespace": true, + "diffEditor.ignoreTrimWhitespace": true } diff --git a/ios/Podfile b/ios/Podfile index 0b62d79e..fe628cb8 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -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' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 7f49fc33..6fd078dd 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -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 diff --git a/lib/helpers/consts.dart b/lib/helpers/consts.dart index d2e0760d..58e08d1f 100644 --- a/lib/helpers/consts.dart +++ b/lib/helpers/consts.dart @@ -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 } diff --git a/lib/helpers/gym_mode.dart b/lib/helpers/gym_mode.dart index ffad3e26..9a62f437 100644 --- a/lib/helpers/gym_mode.dart +++ b/lib/helpers/gym_mode.dart @@ -18,30 +18,35 @@ /// Calculates the number of plates needed to reach a specific weight List plateCalculator(num totalWeight, num barWeight, List plates) { - final List ans = []; + final List 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] diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 059a3bca..30de6e1b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -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", diff --git a/lib/main.dart b/lib/main.dart index dd2577f3..67c2de89 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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, diff --git a/lib/providers/plate_weights.dart b/lib/providers/plate_weights.dart new file mode 100644 index 00000000..51747d72 --- /dev/null +++ b/lib/providers/plate_weights.dart @@ -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((ref) { + return PlateCalculatorNotifier(); +}); + +class PlateCalculatorState { + final _logger = Logger('PlateWeightsState'); + + // https://en.wikipedia.org/wiki/Barbell#Bumper_plates + final Map 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 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 selectedPlates; + final List availablePlatesKg = const [0.5, 1, 1.25, 2, 2.5, 5, 10, 15, 20, 25]; + final List 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? selectedPlates, + }) : barWeight = barWeight ?? (isMetric ? DEFAULT_BAR_WEIGHT_KG : DEFAULT_BAR_WEIGHT_LB), + selectedPlates = + selectedPlates ?? (isMetric ? [...DEFAULT_KG_PLATES] : [...DEFAULT_LB_PLATES]); + + PlateCalculatorState.fromJson(Map plateData) + : useColors = plateData['useColors'] ?? true, + isMetric = plateData['isMetric'] ?? true, + selectedPlates = plateData['selectedPlates']?.cast() ?? [...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? selectedPlates, + }) { + return PlateCalculatorState( + useColors: useColors ?? this.useColors, + isMetric: isMetric ?? this.isMetric, + totalWeight: totalWeight ?? this.totalWeight, + barWeight: barWeight ?? this.barWeight, + selectedPlates: selectedPlates ?? this.selectedPlates, + ); + } + + Map toJson() { + return { + 'useColors': useColors, + 'isMetric': isMetric, + 'selectedPlates': selectedPlates, + 'barWeight': barWeight, + }; + } + + List get availablePlates { + return isMetric ? availablePlatesKg : availablePlatesLb; + } + + List get availableBarsWeights { + return isMetric ? availableBarWeightsKg : availableBarWeightsLb; + } + + List get platesList { + return plateCalculator(totalWeight, barWeight, selectedPlates); + } + + bool get hasPlates { + return platesList.isNotEmpty; + } + + Map 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 { + final _logger = Logger('PlateCalculatorNotifier'); + + late SharedPreferencesAsync prefs; + + PlateCalculatorNotifier({SharedPreferencesAsync? prefs}) : super(PlateCalculatorState()) { + this.prefs = prefs ?? PreferenceHelper.asyncPref; + _readDataFromSharedPrefs(); + } + + Future saveToSharedPrefs() async { + _logger.fine('Saving plate data to SharedPreferences'); + await prefs.setString(PREFS_KEY_PLATES, jsonEncode(state.toJson())); + } + + Future _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 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); + } +} diff --git a/lib/providers/user.dart b/lib/providers/user.dart index d8eabfd7..fc8959d1 100644 --- a/lib/providers/user.dart +++ b/lib/providers/user.dart @@ -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 _loadThemeMode() async { final prefsDarkMode = await prefs.getBool(PREFS_USER_DARK_THEME); diff --git a/lib/screens/configure_plates_screen.dart b/lib/screens/configure_plates_screen.dart new file mode 100644 index 00000000..d31f1a38 --- /dev/null +++ b/lib/screens/configure_plates_screen.dart @@ -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(), + ); + } +} diff --git a/lib/screens/gym_mode.dart b/lib/screens/gym_mode.dart index 0beace29..6137fa9a 100644 --- a/lib/screens/gym_mode.dart +++ b/lib/screens/gym_mode.dart @@ -47,6 +47,8 @@ class GymModeScreen extends StatelessWidget { .first; return Scaffold( + // backgroundColor: Theme.of(context).cardColor, + // primary: false, body: SafeArea( child: Consumer( builder: (context, value, child) => GymMode(dayDataGym, dayDataDisplay, args.iteration), diff --git a/lib/widgets/core/settings.dart b/lib/widgets/core/settings.dart index 7b0a3076..19802532 100644 --- a/lib/widgets/core/settings.dart +++ b/lib/widgets/core/settings.dart @@ -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), + ), ], ), ); diff --git a/lib/widgets/routines/gym_mode/exercise_overview.dart b/lib/widgets/routines/gym_mode/exercise_overview.dart index c4c25615..3dba0b8d 100644 --- a/lib/widgets/routines/gym_mode/exercise_overview.dart +++ b/lib/widgets/routines/gym_mode/exercise_overview.dart @@ -42,7 +42,6 @@ class ExerciseOverview extends StatelessWidget { _controller, exercisePages: _exercisePages, ), - const Divider(), Expanded( child: SingleChildScrollView( child: Padding( diff --git a/lib/widgets/routines/gym_mode/log_page.dart b/lib/widgets/routines/gym_mode/log_page.dart index 17ace6e7..7cd84b34 100644 --- a/lib/widgets/routines/gym_mode/log_page.dart +++ b/lib/widgets/routines/gym_mode/log_page.dart @@ -16,26 +16,29 @@ * along with this program. If not, see . */ 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 _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 { +class _LogPageState extends ConsumerState { final _form = GlobalKey(); final _repetitionsController = TextEditingController(); final _weightController = TextEditingController(); @@ -122,7 +122,9 @@ class _LogPageState extends State { 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 { 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 { ), 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 { 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 { onChanged: (v) => {}, ), ), + const SizedBox(width: 8), ], ), if (_detailed) @@ -258,6 +272,7 @@ class _LogPageState extends State { Flexible( child: WeightUnitInputWidget(widget._log.weightUnitId, onChanged: (v) => {}), ), + const SizedBox(width: 8), ], ), if (_detailed) @@ -272,6 +287,7 @@ class _LogPageState extends State { }, ), SwitchListTile( + dense: true, title: Text(AppLocalizations.of(context).setUnitsAndRir), value: _detailed, onChanged: (value) { @@ -280,7 +296,7 @@ class _LogPageState extends State { }); }, ), - ElevatedButton( + FilledButton( onPressed: _isSaving ? null : () async { @@ -327,103 +343,118 @@ class _LogPageState extends State { } 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 { 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), ], diff --git a/lib/widgets/routines/gym_mode/navigation.dart b/lib/widgets/routines/gym_mode/navigation.dart index 18f65436..2893fd1e 100644 --- a/lib/widgets/routines/gym_mode/navigation.dart +++ b/lib/widgets/routines/gym_mode/navigation.dart @@ -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(wgerPrimaryColor), ), diff --git a/lib/widgets/routines/gym_mode/session_page.dart b/lib/widgets/routines/gym_mode/session_page.dart index ce9c0f90..222b641c 100644 --- a/lib/widgets/routines/gym_mode/session_page.dart +++ b/lib/widgets/routines/gym_mode/session_page.dart @@ -59,7 +59,6 @@ class SessionPage extends StatelessWidget { _controller, exercisePages: _exercisePages, ), - const Divider(), Expanded(child: Container()), Padding( padding: const EdgeInsets.symmetric(horizontal: 15), diff --git a/lib/widgets/routines/gym_mode/start_page.dart b/lib/widgets/routines/gym_mode/start_page.dart index 1eb9030f..6afef502 100644 --- a/lib/widgets/routines/gym_mode/start_page.dart +++ b/lib/widgets/routines/gym_mode/start_page.dart @@ -21,7 +21,6 @@ class StartPage extends StatelessWidget { _controller, exercisePages: _exercisePages, ), - const Divider(), Expanded( child: ListView( children: [ diff --git a/lib/widgets/routines/plate_calculator.dart b/lib/widgets/routines/plate_calculator.dart new file mode 100644 index 00000000..d5a59fda --- /dev/null +++ b/lib/widgets/routines/plate_calculator.dart @@ -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 createState() => _AddPlateWeightsState(); +} + +class _AddPlateWeightsState extends ConsumerState { + @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(context); + + return Column( + mainAxisSize: MainAxisSize.max, + children: [ + Padding( + padding: const EdgeInsets.all(10), + child: DropdownMenu( + 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( + value: unit, + label: unit == WeightUnitEnum.kg ? i18n.kg : i18n.lb, + ); + }).toList(), + ), + ), + Padding( + padding: const EdgeInsets.all(10), + child: DropdownMenu( + 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( + 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), + ), + ], + ); + } +} diff --git a/test/providers/plate_calculator_test.dart b/test/providers/plate_calculator_test.dart new file mode 100644 index 00000000..1be3d070 --- /dev/null +++ b/test/providers/plate_calculator_test.dart @@ -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); + }); + }); +} diff --git a/test/providers/plate_calculator_test.mocks.dart b/test/providers/plate_calculator_test.mocks.dart new file mode 100644 index 00000000..31e23fea --- /dev/null +++ b/test/providers/plate_calculator_test.mocks.dart @@ -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> getKeys({Set? allowList}) => (super.noSuchMethod( + Invocation.method( + #getKeys, + [], + {#allowList: allowList}, + ), + returnValue: _i3.Future>.value({}), + ) as _i3.Future>); + + @override + _i3.Future> getAll({Set? allowList}) => (super.noSuchMethod( + Invocation.method( + #getAll, + [], + {#allowList: allowList}, + ), + returnValue: _i3.Future>.value({}), + ) as _i3.Future>); + + @override + _i3.Future getBool(String? key) => (super.noSuchMethod( + Invocation.method( + #getBool, + [key], + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future getInt(String? key) => (super.noSuchMethod( + Invocation.method( + #getInt, + [key], + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future getDouble(String? key) => (super.noSuchMethod( + Invocation.method( + #getDouble, + [key], + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future getString(String? key) => (super.noSuchMethod( + Invocation.method( + #getString, + [key], + ), + returnValue: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future?> getStringList(String? key) => (super.noSuchMethod( + Invocation.method( + #getStringList, + [key], + ), + returnValue: _i3.Future?>.value(), + ) as _i3.Future?>); + + @override + _i3.Future containsKey(String? key) => (super.noSuchMethod( + Invocation.method( + #containsKey, + [key], + ), + returnValue: _i3.Future.value(false), + ) as _i3.Future); + + @override + _i3.Future setBool( + String? key, + bool? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setBool, + [ + key, + value, + ], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future setInt( + String? key, + int? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setInt, + [ + key, + value, + ], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future setDouble( + String? key, + double? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setDouble, + [ + key, + value, + ], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future setString( + String? key, + String? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setString, + [ + key, + value, + ], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future setStringList( + String? key, + List? value, + ) => + (super.noSuchMethod( + Invocation.method( + #setStringList, + [ + key, + value, + ], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future remove(String? key) => (super.noSuchMethod( + Invocation.method( + #remove, + [key], + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); + + @override + _i3.Future clear({Set? allowList}) => (super.noSuchMethod( + Invocation.method( + #clear, + [], + {#allowList: allowList}, + ), + returnValue: _i3.Future.value(), + returnValueForMissingStub: _i3.Future.value(), + ) as _i3.Future); +} diff --git a/test/routine/gym_mode_screen_test.dart b/test/routine/gym_mode_screen_test.dart index 722c7b04..56ff274c 100644 --- a/test/routine/gym_mode_screen_test.dart +++ b/test/routine/gym_mode_screen_test.dart @@ -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( 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); diff --git a/test/widgets/routines/plate_calculator_test.dart b/test/widgets/routines/plate_calculator_test.dart new file mode 100644 index 00000000..50546d45 --- /dev/null +++ b/test/widgets/routines/plate_calculator_test.dart @@ -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 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); + }, + ); +}