Actually create exercise bases and translations

This commit is contained in:
Roland Geider
2022-02-20 14:01:58 +01:00
parent 5cebbb7c7f
commit 19a504dcfb
16 changed files with 208 additions and 91 deletions

View File

@@ -76,3 +76,6 @@ const ENERGY_CARBOHYDRATES = 4;
/// kcal per gram of fat (approx)
const ENERGY_FAT = 9;
/// Language ID for English (fallback)
const LANGUAGE_SHORT_ENGLISH = 'en';

View File

@@ -89,7 +89,8 @@ class MyApp extends StatelessWidget {
),
ChangeNotifierProxyProvider<AuthProvider, MeasurementProvider>(
create: (context) => MeasurementProvider(
WgerBaseProvider(Provider.of<AuthProvider>(context, listen: false))),
WgerBaseProvider(Provider.of<AuthProvider>(context, listen: false)),
),
update: (context, base, previous) =>
previous ?? MeasurementProvider(WgerBaseProvider(base)),
),
@@ -103,8 +104,12 @@ class MyApp extends StatelessWidget {
GalleryProvider(Provider.of<AuthProvider>(context, listen: false), []),
update: (context, auth, previous) => previous ?? GalleryProvider(auth, []),
),
ChangeNotifierProvider(
create: (_) => AddExerciseProvider(),
ChangeNotifierProxyProvider<AuthProvider, AddExerciseProvider>(
create: (context) => AddExerciseProvider(
WgerBaseProvider(Provider.of<AuthProvider>(context, listen: false)),
),
update: (context, base, previous) =>
previous ?? AddExerciseProvider(WgerBaseProvider(base)),
)
],
child: Consumer<AuthProvider>(
@@ -141,7 +146,7 @@ class MyApp extends StatelessWidget {
WorkoutPlanScreen.routeName: (ctx) => WorkoutPlanScreen(),
WorkoutPlansScreen.routeName: (ctx) => WorkoutPlansScreen(),
ExercisesScreen.routeName: (ctx) => const ExercisesScreen(),
ExerciseDetailScreen.routeName: (ctx) => ExerciseDetailScreen(),
ExerciseDetailScreen.routeName: (ctx) => const ExerciseDetailScreen(),
AddExerciseScreen.routeName: (ctx) => const AddExerciseScreen(),
},
localizationsDelegates: AppLocalizations.localizationsDelegates,

View File

@@ -18,6 +18,7 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:wger/helpers/consts.dart';
import 'package:wger/models/exercises/category.dart';
import 'package:wger/models/exercises/equipment.dart';
import 'package:wger/models/exercises/exercise.dart';
@@ -30,19 +31,19 @@ part 'base.g.dart';
@JsonSerializable(explicitToJson: true)
class ExerciseBase extends Equatable {
@JsonKey(required: true)
final int id;
final int? id;
@JsonKey(required: true)
final String uuid;
final String? uuid;
@JsonKey(required: true, name: 'variations')
final int? variationId;
@JsonKey(required: true, name: 'creation_date')
final DateTime creationDate;
final DateTime? creationDate;
@JsonKey(required: true, name: 'update_date')
final DateTime updateDate;
final DateTime? updateDate;
@JsonKey(required: true, name: 'category')
late int categoryId;
@@ -77,39 +78,54 @@ class ExerciseBase extends Equatable {
@JsonKey(ignore: true)
List<Video> videos = [];
ExerciseBase(
{required this.id,
required this.uuid,
required this.creationDate,
required this.updateDate,
this.variationId,
List<Muscle>? muscles,
List<Muscle>? musclesSecondary,
List<Equipment>? equipment,
List<ExerciseImage>? images,
ExerciseCategory? category}) {
ExerciseBase({
this.id,
this.uuid,
this.creationDate,
this.updateDate,
this.variationId,
List<Muscle>? muscles,
List<Muscle>? musclesSecondary,
List<Equipment>? equipment,
List<ExerciseImage>? images,
ExerciseCategory? category,
}) {
this.images = images ?? [];
this.equipment = equipment ?? [];
this.musclesSecondary = musclesSecondary ?? [];
this.muscles = muscles ?? [];
if (category != null) {
this.category = category;
categoryId = category.id;
}
if (muscles != null) {
this.muscles = muscles;
musclesIds = muscles.map((e) => e.id).toList();
}
if (musclesSecondary != null) {
this.musclesSecondary = musclesSecondary;
musclesSecondaryIds = musclesSecondary.map((e) => e.id).toList();
}
if (equipment != null) {
this.equipment = equipment;
equipmentIds = equipment.map((e) => e.id).toList();
}
}
/// Returns exercises for the given language
///
/// If no translation is found, English will be returned
///
/// TODO: don't hard code language code
/// TODO: don't just return the first exercise (this won't be necessary anymore
/// when we cleanup the database)
/// Note: we return the first translation as a fallback if we don't find a
/// translation in English. This is something that should never happen,
/// but we can't make sure that no local installation hasn't deleted
/// the entry in English.
Exercise getExercises(String language) {
return exercises.firstWhere(
(e) => e.language.shortName == language,
(e) => e.languageObj.shortName == language,
orElse: () => exercises.firstWhere(
(e) => e.language.shortName == 'en',
(e) => e.languageObj.shortName == LANGUAGE_SHORT_ENGLISH,
orElse: () => exercises.first,
),
);

View File

@@ -22,10 +22,14 @@ ExerciseBase _$ExerciseBaseFromJson(Map<String, dynamic> json) {
],
);
return ExerciseBase(
id: json['id'] as int,
uuid: json['uuid'] as String,
creationDate: DateTime.parse(json['creation_date'] as String),
updateDate: DateTime.parse(json['update_date'] as String),
id: json['id'] as int?,
uuid: json['uuid'] as String?,
creationDate: json['creation_date'] == null
? null
: DateTime.parse(json['creation_date'] as String),
updateDate: json['update_date'] == null
? null
: DateTime.parse(json['update_date'] as String),
variationId: json['variations'] as int?,
)
..categoryId = json['category'] as int
@@ -43,8 +47,8 @@ Map<String, dynamic> _$ExerciseBaseToJson(ExerciseBase instance) =>
'id': instance.id,
'uuid': instance.uuid,
'variations': instance.variationId,
'creation_date': instance.creationDate.toIso8601String(),
'update_date': instance.updateDate.toIso8601String(),
'creation_date': instance.creationDate?.toIso8601String(),
'update_date': instance.updateDate?.toIso8601String(),
'category': instance.categoryId,
'muscles': instance.musclesIds,
'muscles_secondary': instance.musclesSecondaryIds,

View File

@@ -26,29 +26,31 @@ import 'package:wger/models/exercises/equipment.dart';
import 'package:wger/models/exercises/image.dart';
import 'package:wger/models/exercises/language.dart';
import 'package:wger/models/exercises/muscle.dart';
import 'package:wger/models/exercises/video.dart';
part 'exercise.g.dart';
@JsonSerializable()
class Exercise extends Equatable {
@JsonKey(required: true)
final int id;
@JsonKey(required: true, name: 'exercise_base')
final int baseId;
final int? id;
@JsonKey(required: true)
final String uuid;
final String? uuid;
@JsonKey(required: true, name: 'language')
final int languageId;
late int languageId;
@JsonKey(ignore: true)
late Language language;
late Language languageObj;
@JsonKey(required: true, name: 'creation_date')
final DateTime creationDate;
final DateTime? creationDate;
@JsonKey(required: true, name: 'exercise_base')
late int? baseId;
@JsonKey(ignore: true)
late ExerciseBase baseObj;
@JsonKey(required: true)
final String name;
@@ -56,40 +58,48 @@ class Exercise extends Equatable {
@JsonKey(required: true)
final String description;
@JsonKey(ignore: true)
late ExerciseBase base;
@JsonKey(ignore: true)
List<Comment> tips = [];
@JsonKey(ignore: true)
List<Alias> alias = [];
Exercise(
{required this.id,
required this.baseId,
required this.uuid,
required this.creationDate,
required this.languageId,
required this.name,
required this.description,
base,
language}) {
Exercise({
this.id,
this.uuid,
this.creationDate,
required this.name,
required this.description,
base,
language,
}) {
if (base != null) {
this.base = base;
baseObj = base;
baseId = base.id;
}
if (language != null) {
this.language = language;
languageObj = language;
languageId = language.id;
}
}
ExerciseImage? get getMainImage => base.getMainImage;
ExerciseCategory get category => base.category;
List<ExerciseImage> get images => base.images;
List<Equipment> get equipment => base.equipment;
List<Muscle> get muscles => base.muscles;
List<Muscle> get musclesSecondary => base.musclesSecondary;
ExerciseImage? get getMainImage => baseObj.getMainImage;
ExerciseCategory get category => baseObj.category;
List<ExerciseImage> get images => baseObj.images;
List<Equipment> get equipment => baseObj.equipment;
List<Muscle> get muscles => baseObj.muscles;
List<Muscle> get musclesSecondary => baseObj.musclesSecondary;
set base(ExerciseBase base) {
baseObj = base;
baseId = base.id;
}
set language(Language language) {
languageObj = language;
languageId = language.id;
}
// Boilerplate
factory Exercise.fromJson(Map<String, dynamic> json) => _$ExerciseFromJson(json);
@@ -104,6 +114,6 @@ class Exercise extends Equatable {
creationDate,
name,
description,
base,
baseObj,
];
}

View File

@@ -11,31 +11,33 @@ Exercise _$ExerciseFromJson(Map<String, dynamic> json) {
json,
requiredKeys: const [
'id',
'exercise_base',
'uuid',
'language',
'creation_date',
'exercise_base',
'name',
'description'
],
);
return Exercise(
id: json['id'] as int,
baseId: json['exercise_base'] as int,
uuid: json['uuid'] as String,
creationDate: DateTime.parse(json['creation_date'] as String),
languageId: json['language'] as int,
id: json['id'] as int?,
uuid: json['uuid'] as String?,
creationDate: json['creation_date'] == null
? null
: DateTime.parse(json['creation_date'] as String),
name: json['name'] as String,
description: json['description'] as String,
);
)
..languageId = json['language'] as int
..baseId = json['exercise_base'] as int?;
}
Map<String, dynamic> _$ExerciseToJson(Exercise instance) => <String, dynamic>{
'id': instance.id,
'exercise_base': instance.baseId,
'uuid': instance.uuid,
'language': instance.languageId,
'creation_date': instance.creationDate.toIso8601String(),
'creation_date': instance.creationDate?.toIso8601String(),
'exercise_base': instance.baseId,
'name': instance.name,
'description': instance.description,
};

View File

@@ -88,7 +88,7 @@ class Log {
set exercise(Exercise exercise) {
exerciseObj = exercise;
exerciseId = exercise.id;
exerciseId = exercise.id!;
}
set weightUnit(WeightUnit weightUnit) {

View File

@@ -77,7 +77,7 @@ class Set {
this.order = order ?? 1;
this.comment = comment ?? '';
exercisesObj = exercises ?? [];
exercisesIds = exercisesObj.map((e) => e.id).toList();
exercisesIds = exercisesObj.map((e) => e.id!).toList();
this.settings = settings ?? [];
this.settingsComputed = settingsComputed ?? [];
if (day != null) {
@@ -104,7 +104,7 @@ class Set {
void addExercise(Exercise exercise) {
exercisesObj.add(exercise);
exercisesIds.add(exercise.id);
exercisesIds.add(exercise.id!);
}
void removeExercise(Exercise exercise) {

View File

@@ -93,7 +93,7 @@ class Setting {
set exercise(Exercise exercise) {
exerciseObj = exercise;
exerciseId = exercise.id;
exerciseId = exercise.id!;
}
set weightUnit(WeightUnit weightUnit) {

View File

@@ -5,10 +5,15 @@ import 'package:flutter/foundation.dart';
import 'package:wger/models/exercises/base.dart';
import 'package:wger/models/exercises/category.dart';
import 'package:wger/models/exercises/equipment.dart';
import 'package:wger/models/exercises/exercise.dart';
import 'package:wger/models/exercises/language.dart';
import 'package:wger/models/exercises/muscle.dart';
import 'base_provider.dart';
class AddExerciseProvider with ChangeNotifier {
final WgerBaseProvider baseProvider;
List<File> get exerciseImages => [..._exerciseImages];
List<File> _exerciseImages = [];
String? _nameEn;
@@ -24,6 +29,11 @@ class AddExerciseProvider with ChangeNotifier {
List<Muscle> _primaryMuscles = [];
List<Muscle> _secondaryMuscles = [];
AddExerciseProvider(this.baseProvider);
static const _exerciseBaseUrlPath = 'exercise-base';
static const _exerciseTranslationUrlPath = 'exercise-translation';
void clear() {
_exerciseImages = [];
_alternativeNamesEn = [];
@@ -45,6 +55,31 @@ class AddExerciseProvider with ChangeNotifier {
set targetArea(ExerciseCategory target) => _targetArea = target;
set language(Language language) => _language = language;
ExerciseBase get base {
return ExerciseBase(
category: _targetArea,
equipment: _equipment,
muscles: _primaryMuscles,
musclesSecondary: _secondaryMuscles,
);
}
Exercise get exerciseEn {
return Exercise(
name: _nameEn!,
description: _descriptionEn!,
language: const Language(id: 2, fullName: 'English', shortName: 'en'),
);
}
Exercise get exerciseTranslation {
return Exercise(
name: _nameTranslation!,
description: _descriptionTranslation!,
language: _language,
);
}
List<Muscle> get primaryMuscles => [..._primaryMuscles];
set primaryMuscles(List<Muscle> muscles) {
_primaryMuscles = muscles;
@@ -85,4 +120,46 @@ class AddExerciseProvider with ChangeNotifier {
log('Description: en/$_descriptionEn translation/$_descriptionTranslation');
log('Alternate names: en/$_alternativeNamesEn translation/$_alternativeNamesTranslation');
}
Future<void> addExercise() async {
printValues();
// Create the base
final base = await addExerciseBase();
// Set the variations
// ...
// Create the translations
final exerciseTranslationEn = exerciseEn;
exerciseTranslationEn.base = base;
addExerciseTranslation(exerciseTranslationEn);
final exerciseTranslationTranslation = exerciseTranslation;
exerciseTranslationTranslation.base = base;
addExerciseTranslation(exerciseTranslationTranslation);
// Create the images
// ...
}
Future<ExerciseBase> addExerciseBase() async {
final Uri postUri = baseProvider.makeUrl(_exerciseBaseUrlPath);
final Map<String, dynamic> newBaseMap = await baseProvider.post(base.toJson(), postUri);
final ExerciseBase newExerciseBase = ExerciseBase.fromJson(newBaseMap);
notifyListeners();
return newExerciseBase;
}
Future<Exercise> addExerciseTranslation(Exercise exercise) async {
final Uri postUri = baseProvider.makeUrl(_exerciseTranslationUrlPath);
final Map<String, dynamic> newTranslation = await baseProvider.post(exercise.toJson(), postUri);
final Exercise newExercise = Exercise.fromJson(newTranslation);
notifyListeners();
return newExercise;
}
}

View File

@@ -142,7 +142,7 @@ class ExercisesProvider with ChangeNotifier {
List<ExerciseBase> filteredItems = _exerciseBases;
if (filters!.searchTerm.length > 1) {
final exercises = await searchExercise(filters!.searchTerm);
filteredItems = exercises.map((e) => e.base).toList();
filteredItems = exercises.map((e) => e.baseObj).toList();
}
// Filter by exercise category and equipment (REPLACE WITH HTTP REQUEST)
@@ -195,7 +195,7 @@ class ExercisesProvider with ChangeNotifier {
List<Exercise> findExercisesByVariationId(int id, {int? exerciseIdToExclude, int? languageId}) {
var out = _exercises
.where(
(base) => base.base.variationId == id,
(base) => base.baseObj.variationId == id,
)
.toList();
@@ -401,7 +401,7 @@ class ExercisesProvider with ChangeNotifier {
for (var base in bases) {
final filteredExercises = exercises.where((e) => e.baseId == base.id);
for (final exercise in filteredExercises) {
exercise.base = base;
exercise.baseObj = base;
base.exercises.add(exercise);
out.add(exercise);
}
@@ -414,7 +414,7 @@ class ExercisesProvider with ChangeNotifier {
List<Exercise> mapLanguages(List<Exercise> exercises) {
for (final exercise in exercises) {
exercise.language = findLanguageById(exercise.languageId);
exercise.languageObj = findLanguageById(exercise.languageId);
}
return exercises;
}

View File

@@ -97,7 +97,7 @@ class _AddExerciseScreenState extends State<AddExerciseScreen> {
if (_keys[_currentStep].currentState?.validate() ?? false) {
_keys[_currentStep].currentState?.save();
context.read<AddExerciseProvider>().printValues();
context.read<AddExerciseProvider>().addExercise();
if (_currentStep != lastStepIndex) {
setState(() {

View File

@@ -70,7 +70,7 @@ class ExerciseDetail extends StatelessWidget {
List<Widget> getVariations(BuildContext context) {
final List<Widget> out = [];
if (_exercise.base.variationId == null) {
if (_exercise.baseObj.variationId == null) {
return out;
}
@@ -80,7 +80,7 @@ class ExerciseDetail extends StatelessWidget {
));
Provider.of<ExercisesProvider>(context, listen: false)
.findExercisesByVariationId(
_exercise.base.variationId!,
_exercise.baseObj.variationId!,
languageId: _exercise.languageId,
exerciseIdToExclude: _exercise.id,
)
@@ -220,10 +220,10 @@ class ExerciseDetail extends StatelessWidget {
final List<Widget> out = [];
out.add(Chip(
label: Text(_exercise.base.category.name),
label: Text(_exercise.baseObj.category.name),
));
if (_exercise.base.equipment.isNotEmpty) {
_exercise.base.equipment.map((e) => Chip(label: Text(e.name))).forEach((element) {
if (_exercise.baseObj.equipment.isNotEmpty) {
_exercise.baseObj.equipment.map((e) => Chip(label: Text(e.name))).forEach((element) {
out.add(element);
});
}
@@ -234,8 +234,8 @@ class ExerciseDetail extends StatelessWidget {
List<Widget> getVideos() {
// TODO: add carousel for the other videos
final List<Widget> out = [];
if (_exercise.base.videos.isNotEmpty) {
_exercise.base.videos.map((v) => ExerciseVideoWidget(video: v)).forEach((element) {
if (_exercise.baseObj.videos.isNotEmpty) {
_exercise.baseObj.videos.map((v) => ExerciseVideoWidget(video: v)).forEach((element) {
out.add(element);
});

View File

@@ -50,7 +50,7 @@ class ExerciseListTile extends StatelessWidget {
),
),
trailing:
Text('${exercise.language.shortName} base: ${exercise.baseId}, exId: ${exercise.id}'),
Text('${exercise.languageObj.shortName} base: ${exercise.baseId}, exId: ${exercise.id}'),
title: Text(
exercise.name,
//style: theme.textTheme.headline6,

View File

@@ -212,7 +212,7 @@ main() {
// assert
verifyNever(provider.baseProvider.fetch(tSearchByNameUri));
expect(provider.filteredExerciseBases, [data.getTestExercises()[0].base]);
expect(provider.filteredExerciseBases, [data.getTestExercises()[0].baseObj]);
});
test('A muscle is selected with no search term. Should not find results', () async {

View File

@@ -106,5 +106,5 @@ List<Exercise> getTestExercises() {
}
List<ExerciseBase> getTestExerciseBases() {
return getTestExercises().map((e) => e.base).toList();
return getTestExercises().map((e) => e.baseObj).toList();
}