diff --git a/lib/helpers/consts.dart b/lib/helpers/consts.dart index 81063c7f..a7a7902f 100644 --- a/lib/helpers/consts.dart +++ b/lib/helpers/consts.dart @@ -41,8 +41,10 @@ const DAYS_TO_CACHE = 20; /// Name of the submit button in forms const SUBMIT_BUTTON_KEY_NAME = 'submit-button'; -/// Local Preferences key for exercises +/// Local Preferences keys const PREFS_EXERCISES = 'exerciseData'; +const PREFS_EXERCISE_CACHE_VERSION = 'cacheVersion'; +const PREFS_INGREDIENTS = 'ingredientData'; const DEFAULT_ANIMATION_DURATION = Duration(milliseconds: 200); const DEFAULT_ANIMATION_CURVE = Curves.bounceIn; diff --git a/lib/models/exercises/comment.dart b/lib/models/exercises/comment.dart index e9461502..f55efa66 100644 --- a/lib/models/exercises/comment.dart +++ b/lib/models/exercises/comment.dart @@ -25,11 +25,15 @@ class Comment { @JsonKey(required: true) final int id; + @JsonKey(required: true, name: 'exercise') + final int exerciseId; + @JsonKey(required: true) final String comment; Comment({ required this.id, + required this.exerciseId, required this.comment, }); diff --git a/lib/models/exercises/comment.g.dart b/lib/models/exercises/comment.g.dart index 6195a276..de5e68ac 100644 --- a/lib/models/exercises/comment.g.dart +++ b/lib/models/exercises/comment.g.dart @@ -7,14 +7,16 @@ part of 'comment.dart'; // ************************************************************************** Comment _$CommentFromJson(Map json) { - $checkKeys(json, requiredKeys: const ['id', 'comment']); + $checkKeys(json, requiredKeys: const ['id', 'exercise', 'comment']); return Comment( id: json['id'] as int, + exerciseId: json['exercise'] as int, comment: json['comment'] as String, ); } Map _$CommentToJson(Comment instance) => { 'id': instance.id, + 'exercise': instance.exerciseId, 'comment': instance.comment, }; diff --git a/lib/models/exercises/exercise.dart b/lib/models/exercises/exercise.dart index d50236cc..ec4e38f6 100644 --- a/lib/models/exercises/exercise.dart +++ b/lib/models/exercises/exercise.dart @@ -18,6 +18,7 @@ import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; +import 'package:wger/models/exercises/alias.dart'; import 'package:wger/models/exercises/base.dart'; import 'package:wger/models/exercises/category.dart'; import 'package:wger/models/exercises/comment.dart'; @@ -60,6 +61,9 @@ class Exercise extends Equatable { @JsonKey(ignore: true) List tips = []; + @JsonKey(ignore: true) + List alias = []; + Exercise( {required this.id, required this.baseId, diff --git a/lib/providers/exercises.dart b/lib/providers/exercises.dart index 5f9c5eef..6aa2b493 100644 --- a/lib/providers/exercises.dart +++ b/lib/providers/exercises.dart @@ -26,8 +26,10 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:wger/exceptions/no_such_entry_exception.dart'; import 'package:wger/helpers/consts.dart'; +import 'package:wger/models/exercises/alias.dart'; import 'package:wger/models/exercises/base.dart'; import 'package:wger/models/exercises/category.dart'; +import 'package:wger/models/exercises/comment.dart'; import 'package:wger/models/exercises/equipment.dart'; import 'package:wger/models/exercises/exercise.dart'; import 'package:wger/models/exercises/language.dart'; @@ -38,14 +40,17 @@ class ExercisesProvider with ChangeNotifier { final WgerBaseProvider baseProvider; static const EXERCISE_CACHE_DAYS = 7; + static const CACHE_VERSION = 2; static const _exerciseInfoUrlPath = 'exerciseinfo'; static const _exerciseBaseUrlPath = 'exercise-base'; static const _exerciseUrlPath = 'exercise'; static const _exerciseSearchPath = 'exercise/search'; + static const _exerciseVariationsUrlPath = 'variation'; static const _exerciseCommentUrlPath = 'exercisecomment'; static const _exerciseImagesUrlPath = 'exerciseimage'; + static const _exerciseAliasUrlPath = 'exercisealias'; static const _categoriesUrlPath = 'exercisecategory'; static const _musclesUrlPath = 'muscle'; static const _equipmentUrlPath = 'equipment'; @@ -260,7 +265,7 @@ class ExercisesProvider with ChangeNotifier { baseProvider.makeUrl(_exerciseBaseUrlPath, id: exercise.baseId), ); final base = ExerciseBase.fromJson(exerciseBaseData); - setExerciseBaseData(base, [exercise]); + //setExerciseBaseData(base, [exercise]); /* final prefs = await SharedPreferences.getInstance(); @@ -288,21 +293,62 @@ class ExercisesProvider with ChangeNotifier { } } - /// Helper function that sets different objects such as category, etc. - ExerciseBase setExerciseBaseData(ExerciseBase base, List exercises) { - base.category = findCategoryById(base.categoryId); - base.muscles = base.musclesIds.map((e) => findMuscleById(e)).toList(); - base.musclesSecondary = base.musclesSecondaryIds.map((e) => findMuscleById(e)).toList(); - base.equipment = base.equipmentIds.map((e) => findEquipmentById(e)).toList(); + List mapBases(dynamic data, List exercises) { + try { + final bases = data.map((e) { + final base = ExerciseBase.fromJson(e); + base.category = findCategoryById(base.categoryId); + base.muscles = base.musclesIds.map((e) => findMuscleById(e)).toList(); + base.musclesSecondary = base.musclesSecondaryIds.map((e) => findMuscleById(e)).toList(); + base.equipment = base.equipmentIds.map((e) => findEquipmentById(e)).toList(); - exercises.forEach((e) { - e.base = base; - e.language = findLanguageById(e.languageId); + exercises.forEach((e) { + e.base = base; + e.language = findLanguageById(e.languageId); + }); + base.exercises = exercises; + return base; + }); + return bases.toList(); + } catch (e) { + print(e); + print('********************'); + throw e; + } + } + + List mapAliases(dynamic data, List exercises) { + List alias = data.map((e) => Alias.fromJson(e)).toList(); + alias.forEach((e) { + exercises.firstWhere((exercise) => exercise.id == e.exerciseId).alias.add(e); }); - base.exercises = []; - base.exercises = exercises; + return exercises; + } - return base; + List mapComments(dynamic data, List exercises) { + List comments = data.map((e) => Comment.fromJson(e)).toList(); + comments.forEach((e) { + exercises.firstWhere((exercise) => exercise.id == e.exerciseId).tips.add(e); + }); + return exercises; + } + + /// Checks the required cache version + /// + /// This is needed since the content of the exercise cache can change and we need + /// to invalidate it as a result + checkExerciseCacheVersion() async { + final prefs = await SharedPreferences.getInstance(); + if (prefs.containsKey(PREFS_EXERCISE_CACHE_VERSION)) { + final cacheVersion = prefs.getInt(PREFS_EXERCISE_CACHE_VERSION); + if (cacheVersion! != CACHE_VERSION) { + prefs.remove(PREFS_EXERCISES); + } + prefs.setInt(PREFS_EXERCISE_CACHE_VERSION, CACHE_VERSION); + } else { + prefs.remove(PREFS_EXERCISES); + prefs.setInt(PREFS_EXERCISE_CACHE_VERSION, CACHE_VERSION); + } } Future fetchAndSetExercises() async { @@ -311,6 +357,7 @@ class ExercisesProvider with ChangeNotifier { print(Intl.shortLocale(Intl.getCurrentLocale())); print('---------'); final prefs = await SharedPreferences.getInstance(); + await checkExerciseCacheVersion(); if (prefs.containsKey(PREFS_EXERCISES)) { final cacheData = json.decode(prefs.getString(PREFS_EXERCISES)!); @@ -319,14 +366,12 @@ class ExercisesProvider with ChangeNotifier { cacheData['muscles'].forEach((e) => _muscles.add(Muscle.fromJson(e))); cacheData['categories'].forEach((e) => _categories.add(ExerciseCategory.fromJson(e))); cacheData['languages'].forEach((e) => _languages.add(Language.fromJson(e))); - cacheData['exercise-translations'].forEach((e) => _exercises.add(Exercise.fromJson(e))); - cacheData['bases'].forEach((e) { - var base = setExerciseBaseData( - ExerciseBase.fromJson(e), - _exercises.where((element) => element.baseId == e['id']).toList(), - ); - _exerciseBases.add(base); - }); + + List exercises = + cacheData['exercise-translations'].map((e) => Exercise.fromJson(e)).toList(); + exercises = mapComments(cacheData['exercise-comments'], exercises); + _exercises = mapAliases(cacheData['exercise-alias'], exercises); + _exerciseBases = mapBases(cacheData['bases'], _exercises); _initFilters(); log("Read ${_exerciseBases.length} exercises from cache. Valid till ${cacheData['expiresIn']}"); return; @@ -334,44 +379,28 @@ class ExercisesProvider with ChangeNotifier { } // Load categories, muscles, equipment and languages - await Future.wait([ + final data = await Future.wait([ + baseProvider.fetch(baseProvider.makeUrl(_exerciseBaseUrlPath, query: {'limit': '1000'})), + baseProvider.fetch(baseProvider.makeUrl(_exerciseUrlPath, query: {'limit': '1000'})), + baseProvider.fetch(baseProvider.makeUrl(_exerciseCommentUrlPath, query: {'limit': '1000'})), + baseProvider.fetch(baseProvider.makeUrl(_exerciseAliasUrlPath, query: {'limit': '1000'})), fetchAndSetCategories(), fetchAndSetMuscles(), fetchAndSetEquipment(), fetchAndSetLanguages(), ]); + final exerciseBaseData = data[0]['results']; + final exerciseData = data[1]['results']; + final commentsData = data[2]['results']; + final aliasData = data[3]['results']; - final exerciseBaseData = await baseProvider.fetch( - baseProvider.makeUrl(_exerciseBaseUrlPath, query: {'limit': '1000'}), - ); - - final exerciseTranslationData = await baseProvider.fetch( - baseProvider.makeUrl( - _exerciseUrlPath, - query: {'limit': '1000'}, - ), - ); - - List exerciseTranslation = exerciseTranslationData['results'].map((e) { - return Exercise.fromJson(e); - }).toList(); - - for (var e in exerciseBaseData['results']) { - var base = setExerciseBaseData( - ExerciseBase.fromJson(e), - exerciseTranslation.where((element) => element.baseId == e['id']).toList(), - ); - _exerciseBases.add(base); - } + // Load exercise + List exercises = exerciseData.map((e) => Exercise.fromJson(e)).toList(); + exercises = mapComments(commentsData, exercises); + _exercises = mapAliases(aliasData, exercises); + _exerciseBases = mapBases(exerciseBaseData, _exercises); try { - List exerciseTranslations = []; - _exerciseBases.forEach((base) { - base.exercises.forEach((exercise) { - exerciseTranslations.add(exercise); - }); - }); - // Save the result to the cache final cacheData = { 'date': DateTime.now().toIso8601String(), @@ -380,8 +409,10 @@ class ExercisesProvider with ChangeNotifier { 'categories': _categories.map((e) => e.toJson()).toList(), 'muscles': _muscles.map((e) => e.toJson()).toList(), 'languages': _languages.map((e) => e.toJson()).toList(), - 'exercise-translations': exerciseTranslations.map((e) => e.toJson()).toList(), - 'bases': _exerciseBases.map((e) => e.toJson()).toList(), + 'bases': exerciseBaseData, + 'exercise-translations': exerciseData, + 'exercise-comments': commentsData, + 'exercise-alias': aliasData, }; log("Saved ${_exercises.length} exercises from cache. Valid till ${cacheData['expiresIn']}"); @@ -391,6 +422,8 @@ class ExercisesProvider with ChangeNotifier { } on MissingRequiredKeysException catch (error) { log(error.missingKeys.toString()); throw (error); + } catch (e) { + throw (e); } } diff --git a/lib/providers/nutrition.dart b/lib/providers/nutrition.dart index ba4757ee..945371d5 100644 --- a/lib/providers/nutrition.dart +++ b/lib/providers/nutrition.dart @@ -23,8 +23,9 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:wger/helpers/consts.dart'; import 'package:wger/exceptions/http_exception.dart'; +import 'package:wger/exceptions/no_such_entry_exception.dart'; +import 'package:wger/helpers/consts.dart'; import 'package:wger/models/nutrition/ingredient.dart'; import 'package:wger/models/nutrition/log.dart'; import 'package:wger/models/nutrition/meal.dart'; @@ -68,7 +69,10 @@ class NutritionPlansProvider extends WgerBaseProvider with ChangeNotifier { } NutritionalPlan findById(int id) { - return _plans.firstWhere((plan) => plan.id == id); + return _plans.firstWhere( + (plan) => plan.id == id, + orElse: () => throw NoSuchEntryException(), + ); } Meal? findMealById(int id) { @@ -120,7 +124,7 @@ class NutritionPlansProvider extends WgerBaseProvider with ChangeNotifier { NutritionalPlan plan; try { plan = findById(planId); - } on StateError catch (e) { + } on NoSuchEntryException catch (e) { plan = await fetchAndSetPlanSparse(planId); } @@ -280,8 +284,8 @@ class NutritionPlansProvider extends WgerBaseProvider with ChangeNotifier { Future fetchIngredientsFromCache() async { // Load exercises from cache, if available final prefs = await SharedPreferences.getInstance(); - if (prefs.containsKey('ingredientData')) { - final ingredientData = json.decode(prefs.getString('ingredientData')!); + if (prefs.containsKey(PREFS_INGREDIENTS)) { + final ingredientData = json.decode(prefs.getString(PREFS_INGREDIENTS)!); if (DateTime.parse(ingredientData['expiresIn']).isAfter(DateTime.now())) { ingredientData['ingredients'].forEach((e) => _ingredients.add(Ingredient.fromJson(e))); log("Read ${ingredientData['ingredients'].length} ingredients from cache. Valid till ${ingredientData['expiresIn']}"); @@ -295,7 +299,7 @@ class NutritionPlansProvider extends WgerBaseProvider with ChangeNotifier { 'expiresIn': DateTime.now().add(Duration(days: DAYS_TO_CACHE)).toIso8601String(), 'ingredients': [] }; - prefs.setString('ingredientData', json.encode(ingredientData)); + prefs.setString(PREFS_INGREDIENTS, json.encode(ingredientData)); return; }