mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
Merge pull request #688 from AyushJagaty/plate-46-calc
Plate calculator
This commit is contained in:
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"dart.lineLength": 100,
|
||||
"diffEditor.ignoreTrimWhitespace": true,
|
||||
"diffEditor.ignoreTrimWhitespace": true
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
214
lib/providers/plate_weights.dart
Normal file
214
lib/providers/plate_weights.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
19
lib/screens/configure_plates_screen.dart
Normal file
19
lib/screens/configure_plates_screen.dart
Normal 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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -42,7 +42,6 @@ class ExerciseOverview extends StatelessWidget {
|
||||
_controller,
|
||||
exercisePages: _exercisePages,
|
||||
),
|
||||
const Divider(),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
|
||||
@@ -59,7 +59,6 @@ class SessionPage extends StatelessWidget {
|
||||
_controller,
|
||||
exercisePages: _exercisePages,
|
||||
),
|
||||
const Divider(),
|
||||
Expanded(child: Container()),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
|
||||
@@ -21,7 +21,6 @@ class StartPage extends StatelessWidget {
|
||||
_controller,
|
||||
exercisePages: _exercisePages,
|
||||
),
|
||||
const Divider(),
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
|
||||
166
lib/widgets/routines/plate_calculator.dart
Normal file
166
lib/widgets/routines/plate_calculator.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
123
test/providers/plate_calculator_test.dart
Normal file
123
test/providers/plate_calculator_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
213
test/providers/plate_calculator_test.mocks.dart
Normal file
213
test/providers/plate_calculator_test.mocks.dart
Normal 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>);
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
92
test/widgets/routines/plate_calculator_test.dart
Normal file
92
test/widgets/routines/plate_calculator_test.dart
Normal 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);
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user