diff --git a/.vscode/settings.json b/.vscode/settings.json index 9619dd65b..585ffee37 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,5 @@ { "dart.lineLength": 100, "diffEditor.ignoreTrimWhitespace": true, + "cmake.sourceDirectory": "/Users/ayush/flutter/linux", } diff --git a/AUTHORS.md b/AUTHORS.md index e8e7ac60a..fe3c5fe0d 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -13,6 +13,7 @@ - Thilina Herath - - Marko Milosevic - - Karthik Reddy (Axel) - +- Ayush Sourav Jagaty - - Ogundoyin Toluwani - - Nenza Nurfirmansyah - - Florian Schmitz - diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 36cfa724a..f7553e9de 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -121,4 +121,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 4e8f8b2be68aeea4c0d5beb6ff1e79fface1d048 -COCOAPODS: 1.16.0 +COCOAPODS: 1.15.2 diff --git a/lib/helpers/exercises/plate_configurator.dart b/lib/helpers/exercises/plate_configurator.dart new file mode 100644 index 000000000..37770bef8 --- /dev/null +++ b/lib/helpers/exercises/plate_configurator.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; + +class PlateConfiguration extends ChangeNotifier { + //olympic standard weights + List _plateWeights = [1.25, 2.5, 5, 10, 15, 20, 25]; + + List get plateWeights => _plateWeights; + + void setPlateWeights(List weights) { + _plateWeights = weights; + notifyListeners(); + } +} \ No newline at end of file diff --git a/lib/helpers/gym_mode.dart b/lib/helpers/gym_mode.dart index ffad3e261..7741147a0 100644 --- a/lib/helpers/gym_mode.dart +++ b/lib/helpers/gym_mode.dart @@ -19,7 +19,6 @@ /// Calculates the number of plates needed to reach a specific weight List plateCalculator(num totalWeight, num barWeight, List plates) { final List ans = []; - // Weight is less than the bar if (totalWeight < barWeight) { return []; diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 33108f7dd..6b89006a7 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -37,6 +37,7 @@ "@passwordTooShort": { "description": "Error message when the user a password that is too short" }, + "selectAvailablePlates": "Select Available Plates", "password": "Password", "@password": {}, "confirmPassword": "Confirm password", diff --git a/lib/main.dart b/lib/main.dart index ac4abfe1a..5c344a4dc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,10 +15,10 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ - import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:wger/core/locator.dart'; import 'package:wger/providers/add_exercise.dart'; import 'package:wger/providers/base_provider.dart'; @@ -27,6 +27,7 @@ import 'package:wger/providers/exercises.dart'; import 'package:wger/providers/gallery.dart'; import 'package:wger/providers/measurement.dart'; import 'package:wger/providers/nutrition.dart'; +import 'package:wger/providers/plate_weights.dart'; import 'package:wger/providers/user.dart'; import 'package:wger/providers/workout_plans.dart'; import 'package:wger/screens/add_exercise_screen.dart'; @@ -57,10 +58,8 @@ import 'providers/auth.dart'; void main() async { //zx.setLogEnabled(kDebugMode); - // Needs to be called before runApp WidgetsFlutterBinding.ensureInitialized(); - // Locator to initialize exerciseDB await ServiceLocator().configure(); // Application @@ -73,6 +72,7 @@ class MyApp extends StatelessWidget { Widget build(BuildContext context) { return MultiProvider( providers: [ + ChangeNotifierProvider(create: (context)=> PlateWeights()), ChangeNotifierProvider(create: (ctx) => AuthProvider()), ChangeNotifierProxyProvider( create: (context) => ExercisesProvider( diff --git a/lib/providers/plate_weights.dart b/lib/providers/plate_weights.dart new file mode 100644 index 000000000..47d2df487 --- /dev/null +++ b/lib/providers/plate_weights.dart @@ -0,0 +1,105 @@ +import 'dart:convert'; +import 'package:flutter/widgets.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:wger/helpers/gym_mode.dart'; + +class PlateWeights extends ChangeNotifier{ + bool isMetric = true; + bool plateChoiceExists = false; + bool loadedFromSharedPref = false; + num totalWeight = 0; + num barWeight = 20; + num convertTolbs = 2.205; + num totalWeightInKg = 0; + num barWeightInKg = 20; + List selectedWeights = []; + List kgWeights = [1.25, 2.5, 5, 10, 15, 20, 25]; + List lbsWeights = [2.5, 5, 10, 25, 35, 45]; + List customPlates = []; + late Map grouped; + List get data => selectedWeights; + set data(List newData){ + selectedWeights = newData; + //saving data to shared preference + saveIntoSharedPrefs(); + notifyListeners(); + } + Future saveIntoSharedPrefs() async{ + final pref = await SharedPreferences.getInstance(); + //converting List Weights to String + final String selectedPlates = jsonEncode(selectedWeights); + pref.setString('selectedPlates', selectedPlates); + notifyListeners(); + } + + void readPlates() async{ + final pref = await SharedPreferences.getInstance(); + final platePrefData = pref.getString('selectedPlates'); + if(platePrefData != null){ + try{ + final plateData = json.decode(platePrefData); + if(plateData is List){ + selectedWeights = plateData.cast(); + }else{ + throw const FormatException('Not a List'); + } + }catch(e){ + selectedWeights = []; + } + } + print('loaded'); + notifyListeners(); + } + + Future toggleSelection(num x) async{ + if(selectedWeights.contains(x)) { + selectedWeights.remove(x); + }else { + selectedWeights.add(x); + } + final prefs = await SharedPreferences.getInstance(); + prefs.setString('selectedPlates',jsonEncode(selectedWeights)); + notifyListeners(); + } + + void unitChange() { + if(isMetric==false) { + totalWeight = totalWeightInKg; + isMetric = true; + barWeight = barWeightInKg; + } else { + isMetric = false; + totalWeight = totalWeightInKg*2.205; + barWeight = barWeightInKg*2.205; + } + notifyListeners(); + } + + void clear() async{ + selectedWeights.clear(); + final prefs = await SharedPreferences.getInstance(); + prefs.setString('selectedPlates',jsonEncode(selectedWeights)); + notifyListeners(); + } + + void setWeight(num x) { + totalWeight = x; + totalWeightInKg=x; + notifyListeners(); + } + + void calculatePlates() { + selectedWeights.sort(); + customPlates = plateCalculator(totalWeight,barWeight,selectedWeights); + grouped = groupPlates(customPlates); + notifyListeners(); + } + + void resetPlates() async{ + selectedWeights = []; + final prefs = await SharedPreferences.getInstance(); + prefs.setString('selectedPlates',jsonEncode(selectedWeights)); + notifyListeners(); + } + +} \ No newline at end of file diff --git a/lib/providers/user.dart b/lib/providers/user.dart index cb547c49d..b89102a4d 100644 --- a/lib/providers/user.dart +++ b/lib/providers/user.dart @@ -19,6 +19,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:path/path.dart'; import 'package:wger/models/user/profile.dart'; import 'package:wger/providers/base_provider.dart'; @@ -35,7 +37,15 @@ class UserProvider with ChangeNotifier { void clear() { profile = null; } - + // change the unit of plates + void unitChange(){ + if(profile?.weightUnitStr == 'kg'){ + profile?.weightUnitStr = 'lb'; + }else{ + profile?.weightUnitStr = 'kg'; + } + ChangeNotifier(); + } /// Fetch the current user's profile Future fetchAndSetProfile() async { final userData = await baseProvider.fetch(baseProvider.makeUrl(PROFILE_URL)); diff --git a/lib/screens/add_plate_weights.dart b/lib/screens/add_plate_weights.dart new file mode 100644 index 000000000..3a430cadc --- /dev/null +++ b/lib/screens/add_plate_weights.dart @@ -0,0 +1,174 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:provider/provider.dart'; +import 'package:wger/providers/plate_weights.dart'; +import 'package:wger/providers/user.dart'; + +class AddPlateWeights extends StatefulWidget { + const AddPlateWeights({super.key}); + + @override + State createState() => _AddPlateWeightsState(); +} + +class _AddPlateWeightsState extends State with SingleTickerProviderStateMixin{ +late AnimationController _controller; + late Animation _animation; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_){ + Provider.of(context,listen: false).readPlates(); + }); + _controller = AnimationController( + vsync: this, + duration: Duration(seconds: 1), + ); + _animation = Tween( + begin: const Offset(-1.0, 0.0), // Start off-screen + end: const Offset(0.0, 0.0), // End at original position + ).animate(CurvedAnimation( + parent: _controller, + curve: Curves.easeInOut, + )); + + _controller.forward(); // Start the animation + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + bool unit = true; + return Consumer2 ( + builder:(context,plateProvider,userProvider,child)=> Scaffold ( + appBar: AppBar ( + title: const Text('Select Available Plates'), + ), + body: Column ( + children: [ + Row ( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('Preferred Unit'), + DropdownButton ( + onChanged: (newValue){ + plateProvider.clear(); + if(newValue=='kg') { + unit = true; + } else { + unit = false; + } + print(unit); + if(unit != userProvider.profile?.isMetric) { + userProvider.unitChange(); + //plateProvider.unitChange(); + _controller.reset(); + _controller.forward(); + } + }, + items: ['kg','lbs'].map((unit){ + return DropdownMenuItem ( + value: unit, + child: Text(unit), + ); + }).toList(), + ), + ], + ), + SingleChildScrollView ( + scrollDirection: Axis.horizontal, + child: Row ( + children: (userProvider.profile?.weightUnitStr == 'kg') + ? plateProvider.kgWeights.map((number) { + return SlideTransition( + position: _animation, + child: GestureDetector( + onTap: () => plateProvider.toggleSelection(number), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Container ( + margin: const EdgeInsets.all(8), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration ( + color: plateProvider.selectedWeights.contains(number) + ? const Color.fromARGB(255, 82, 226, 236) + : const Color.fromARGB(255, 97, 105, 101), + borderRadius: BorderRadius.circular(10), + ), + child: Text ( + '$number kg', // Add unit to text + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ); + }).toList() + : plateProvider.lbsWeights.map((number) { + return SlideTransition( + position: _animation, + child: GestureDetector( + onTap: () => plateProvider.toggleSelection(number), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Container( + margin: const EdgeInsets.all(8), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration ( + color: plateProvider.selectedWeights.contains(number) + ? const Color.fromARGB(255, 82, 226, 236) + : const Color.fromARGB(255, 97, 105, 101), + borderRadius: BorderRadius.circular(10), + ), + child: Text ( + '$number lbs', // Add unit to text + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ); + }).toList(), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + TextButton( + onPressed: (){ + plateProvider.saveIntoSharedPrefs(); + if(plateProvider.selectedWeights.isNotEmpty){ + plateProvider.plateChoiceExists=true; + plateProvider.calculatePlates(); + } + Navigator.pop(context); + }, + child: const Text('Done'), + ), + ElevatedButton( + onPressed: (){ + plateProvider.resetPlates(); + }, + child: const Text('Reset',style: TextStyle(color: Colors.red,fontWeight: FontWeight.bold)) + ), + ], + ), + ] + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/widgets/workouts/gym_mode.dart b/lib/widgets/workouts/gym_mode.dart index 27a878737..ab9cf84ba 100644 --- a/lib/widgets/workouts/gym_mode.dart +++ b/lib/widgets/workouts/gym_mode.dart @@ -16,12 +16,14 @@ * along with this program. If not, see . */ import 'dart:async'; +import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_html/flutter_html.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:wger/exceptions/http_exception.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/helpers/gym_mode.dart'; @@ -37,7 +39,9 @@ import 'package:wger/models/workouts/set.dart'; import 'package:wger/models/workouts/setting.dart'; import 'package:wger/models/workouts/workout_plan.dart'; import 'package:wger/providers/exercises.dart'; +import 'package:wger/providers/plate_weights.dart'; import 'package:wger/providers/workout_plans.dart'; +import 'package:wger/screens/add_plate_weights.dart'; import 'package:wger/theme/theme.dart'; import 'package:wger/widgets/core/core.dart'; import 'package:wger/widgets/exercises/images.dart'; @@ -260,15 +264,15 @@ class _LogPageState extends State { late FocusNode focusNode; @override - void initState() { + void initState(){ super.initState(); - + // WidgetsBinding.instance.addPostFrameCallback((_){ + // Provider.of(context,listen: false).readPlates(); + // }); focusNode = FocusNode(); - if (widget._setting.reps != null) { _repsController.text = widget._setting.reps.toString(); } - if (widget._setting.weight != null) { _weightController.text = widget._setting.weight.toString(); } @@ -335,66 +339,73 @@ class _LogPageState extends State { Widget getWeightWidget() { const minPlateWeight = 1.25; - return Row( - children: [ - IconButton( - icon: const Icon(Icons.remove, color: Colors.black), - onPressed: () { - try { - final double newValue = double.parse(_weightController.text) - (2 * minPlateWeight); - if (newValue > 0) { + return Consumer( + builder: (context,plateProvider, child) => + Row( + children: [ + IconButton( + icon: const Icon(Icons.remove, color: Colors.black), + onPressed: () { + try { + final double newValue = double.parse(_weightController.text) - (2 * minPlateWeight); + if (newValue > 0) { + plateProvider.setWeight(newValue); + plateProvider.calculatePlates(); + setState(() { + widget._log.weight = newValue; + _weightController.text = newValue.toString(); + }); + } + } on FormatException {} + }, + ), + Expanded( + child: TextFormField( + decoration: InputDecoration( + labelText: AppLocalizations.of(context).weight, + ), + controller: _weightController, + keyboardType: TextInputType.number, + onFieldSubmitted: (_) {}, + onChanged: (value) { + try { + double.parse(value); + setState(() { + widget._log.weight = double.parse(value); + }); + } on FormatException {} + }, + onSaved: (newValue) { setState(() { - widget._log.weight = newValue; - _weightController.text = newValue.toString(); + widget._log.weight = double.parse(newValue!); }); - } - } on FormatException {} - }, - ), - Expanded( - child: TextFormField( - decoration: InputDecoration( - labelText: AppLocalizations.of(context).weight, + }, + validator: (value) { + try { + double.parse(value!); + } catch (error) { + return AppLocalizations.of(context).enterValidNumber; + } + return null; + }, ), - controller: _weightController, - keyboardType: TextInputType.number, - onFieldSubmitted: (_) {}, - onChanged: (value) { + ), + IconButton( + icon: const Icon(Icons.add, color: Colors.black), + onPressed: () { try { - double.parse(value); + final double newValue = double.parse(_weightController.text) + (2 * minPlateWeight); + plateProvider.setWeight(newValue); + plateProvider.calculatePlates(); setState(() { - widget._log.weight = double.parse(value); + widget._log.weight = newValue; + _weightController.text = newValue.toString(); }); } on FormatException {} }, - onSaved: (newValue) { - setState(() { - widget._log.weight = double.parse(newValue!); - }); - }, - validator: (value) { - try { - double.parse(value!); - } catch (error) { - return AppLocalizations.of(context).enterValidNumber; - } - return null; - }, ), - ), - IconButton( - icon: const Icon(Icons.add, color: Colors.black), - onPressed: () { - try { - final double newValue = double.parse(_weightController.text) + (2 * minPlateWeight); - setState(() { - widget._log.weight = newValue; - _weightController.text = newValue.toString(); - }); - } on FormatException {} - }, - ), - ], + ], + ), ); } @@ -546,64 +557,68 @@ class _LogPageState extends State { } Widget getPlates() { - final plates = plateCalculator( - double.parse(_weightController.text), - BAR_WEIGHT, - AVAILABLE_PLATES, - ); - final groupedPlates = groupPlates(plates); - - 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( + print('get plates'); + final plates = plateCalculator(num.parse(_weightController.text),BAR_WEIGHT,AVAILABLE_PLATES); + Map groupedPlates; + groupedPlates = groupPlates(plates); + return Consumer( + builder: (context, plateProvider , child)=> + SingleChildScrollView( + child: Column( + children: [ + SizedBox ( + height: 35, + child: (plateProvider.plateChoiceExists?plateProvider.selectedWeights.isNotEmpty:plates.isNotEmpty) + ? Row( + mainAxisAlignment: MainAxisAlignment.center, 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, + IconButton( + onPressed: (){ + plateProvider.plateChoiceExists=false; + plateProvider.setWeight(num.parse(_weightController.text)); + Navigator.of(context).push(MaterialPageRoute(builder: (context)=>const AddPlateWeights())); + }, + icon: const Icon(Icons.settings), + ), + const Text ('Plates:-',style: TextStyle(),), + ...(plateProvider.plateChoiceExists?plateProvider.grouped:groupedPlates).keys.map( + (key) => Row ( + children: [ + Text(plateProvider.plateChoiceExists?(plateProvider.grouped[key].toString()):(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,), + ), + ), + ), ), ), + const SizedBox(width: 10), + ], ), ), + ], + ) + : MutedText ( + AppLocalizations.of(context).plateCalculatorNotDivisible, ), - ), - const SizedBox(width: 10), - ], - ), - ), - ], - ) - : MutedText( - AppLocalizations.of(context).plateCalculatorNotDivisible, - ), + ), + ], + ), ), - const SizedBox(height: 3), - ], ); } @@ -643,6 +658,8 @@ class _LogPageState extends State { } } + + class ExerciseOverview extends StatelessWidget { final PageController _controller; final Exercise _exerciseBase; diff --git a/test/nutrition/goldens/nutritional_plan_1_default_view.png b/test/nutrition/goldens/nutritional_plan_1_default_view.png index 09745b0c8..79ec2fc9c 100644 Binary files a/test/nutrition/goldens/nutritional_plan_1_default_view.png and b/test/nutrition/goldens/nutritional_plan_1_default_view.png differ diff --git a/test/nutrition/goldens/nutritional_plan_2_one_meal_with_ingredients.png b/test/nutrition/goldens/nutritional_plan_2_one_meal_with_ingredients.png index 09a8c964e..a44af8b9c 100644 Binary files a/test/nutrition/goldens/nutritional_plan_2_one_meal_with_ingredients.png and b/test/nutrition/goldens/nutritional_plan_2_one_meal_with_ingredients.png differ diff --git a/test/nutrition/goldens/nutritional_plan_3_both_meals_with_ingredients.png b/test/nutrition/goldens/nutritional_plan_3_both_meals_with_ingredients.png index 9ef95623b..3d24404c4 100644 Binary files a/test/nutrition/goldens/nutritional_plan_3_both_meals_with_ingredients.png and b/test/nutrition/goldens/nutritional_plan_3_both_meals_with_ingredients.png differ diff --git a/test/workout/gym_mode_screen_test.dart b/test/workout/gym_mode_screen_test.dart index 5175ef8da..cdc9fd11d 100644 --- a/test/workout/gym_mode_screen_test.dart +++ b/test/workout/gym_mode_screen_test.dart @@ -24,6 +24,7 @@ import 'package:mockito/mockito.dart'; import 'package:provider/provider.dart'; import 'package:wger/providers/base_provider.dart'; import 'package:wger/providers/exercises.dart'; +import 'package:wger/providers/plate_weights.dart'; import 'package:wger/providers/workout_plans.dart'; import 'package:wger/screens/gym_mode.dart'; import 'package:wger/screens/workout_plan_screen.dart'; @@ -78,10 +79,15 @@ void main() { when(mockExerciseProvider.findExerciseById(1)).thenReturn(bases[0]); when(mockExerciseProvider.findExerciseById(6)).thenReturn(bases[5]); - await tester.pumpWidget(createHomeScreen()); + await tester.pumpWidget(ChangeNotifierProvider( + create: (context) => PlateWeights(), + builder: (context, _) => createHomeScreen(), + )); + //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 // @@ -114,6 +120,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);