diff --git a/lib/helpers/json.dart b/lib/helpers/json.dart index 91cd0756c..b0202ca3e 100644 --- a/lib/helpers/json.dart +++ b/lib/helpers/json.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -70,6 +70,14 @@ DateTime utcIso8601ToLocalDate(String dateTime) { return DateTime.parse(dateTime).toLocal(); } +DateTime? utcIso8601ToLocalDateNull(String? dateTime) { + if (dateTime == null) { + return null; + } + + return utcIso8601ToLocalDate(dateTime); +} + /* * Converts a time to a date object. * Needed e.g. when the wger api only sends a time but no date information. diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 003c43ea6..efce357f9 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -175,6 +175,7 @@ "slotEntryTypeTut": "Time under Tension", "slotEntryTypeIso": "Isometric hold", "slotEntryTypeJump": "Jump", + "trophies": "Trophies", "routines": "Routines", "newRoutine": "New routine", "noRoutines": "You have no routines", @@ -262,6 +263,7 @@ }, "selectExercises": "If you want to do a superset you can search for several exercises, they will be grouped together", "@selectExercises": {}, + "personalRecords": "Personal records", "gymMode": "Gym mode", "@gymMode": { "description": "Label when starting the gym mode" diff --git a/lib/main.dart b/lib/main.dart index 477618816..0eda236e4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -35,6 +35,7 @@ import 'package:wger/providers/measurement.dart'; import 'package:wger/providers/nutrition.dart'; import 'package:wger/providers/routines.dart'; import 'package:wger/providers/user.dart'; +import 'package:wger/providers/wger_base_riverpod.dart'; import 'package:wger/screens/add_exercise_screen.dart'; import 'package:wger/screens/auth_screen.dart'; import 'package:wger/screens/configure_plates_screen.dart'; @@ -57,6 +58,7 @@ import 'package:wger/screens/routine_list_screen.dart'; import 'package:wger/screens/routine_logs_screen.dart'; import 'package:wger/screens/routine_screen.dart'; import 'package:wger/screens/splash_screen.dart'; +import 'package:wger/screens/trophy_screen.dart'; import 'package:wger/screens/update_app_screen.dart'; import 'package:wger/screens/weight_screen.dart'; import 'package:wger/theme/theme.dart'; @@ -129,7 +131,7 @@ void main() async { }; // Application - runApp(const riverpod.ProviderScope(child: MainApp())); + runApp(const MainApp()); } class MainApp extends StatelessWidget { @@ -217,46 +219,61 @@ class MainApp extends StatelessWidget { ), ], child: Consumer( - builder: (ctx, auth, _) => Consumer( - builder: (ctx, user, _) => MaterialApp( - title: 'wger', - navigatorKey: navigatorKey, - theme: wgerLightTheme, - darkTheme: wgerDarkTheme, - highContrastTheme: wgerLightThemeHc, - highContrastDarkTheme: wgerDarkThemeHc, - themeMode: user.themeMode, - home: _getHomeScreen(auth), - routes: { - DashboardScreen.routeName: (ctx) => const DashboardScreen(), - FormScreen.routeName: (ctx) => const FormScreen(), - GalleryScreen.routeName: (ctx) => const GalleryScreen(), - GymModeScreen.routeName: (ctx) => const GymModeScreen(), - HomeTabsScreen.routeName: (ctx) => HomeTabsScreen(), - MeasurementCategoriesScreen.routeName: (ctx) => const MeasurementCategoriesScreen(), - MeasurementEntriesScreen.routeName: (ctx) => const MeasurementEntriesScreen(), - NutritionalPlansScreen.routeName: (ctx) => const NutritionalPlansScreen(), - NutritionalDiaryScreen.routeName: (ctx) => const NutritionalDiaryScreen(), - NutritionalPlanScreen.routeName: (ctx) => const NutritionalPlanScreen(), - LogMealsScreen.routeName: (ctx) => const LogMealsScreen(), - LogMealScreen.routeName: (ctx) => const LogMealScreen(), - WeightScreen.routeName: (ctx) => const WeightScreen(), - RoutineScreen.routeName: (ctx) => const RoutineScreen(), - RoutineEditScreen.routeName: (ctx) => const RoutineEditScreen(), - WorkoutLogsScreen.routeName: (ctx) => const WorkoutLogsScreen(), - RoutineListScreen.routeName: (ctx) => const RoutineListScreen(), - ExercisesScreen.routeName: (ctx) => const ExercisesScreen(), - ExerciseDetailScreen.routeName: (ctx) => const ExerciseDetailScreen(), - AddExerciseScreen.routeName: (ctx) => const AddExerciseScreen(), - AboutPage.routeName: (ctx) => const AboutPage(), - SettingsPage.routeName: (ctx) => const SettingsPage(), - LogOverviewPage.routeName: (ctx) => const LogOverviewPage(), - ConfigurePlatesScreen.routeName: (ctx) => const ConfigurePlatesScreen(), - }, - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: AppLocalizations.supportedLocales, - ), - ), + builder: (ctx, auth, _) { + final baseInstance = WgerBaseProvider(Provider.of(ctx, listen: false)); + + return Consumer( + builder: (ctx, user, _) => riverpod.ProviderScope( + overrides: [ + wgerBaseProvider.overrideWithValue(baseInstance), + ], + child: riverpod.Consumer( + builder: (rpCtx, ref, _) { + return MaterialApp( + title: 'wger', + navigatorKey: navigatorKey, + theme: wgerLightTheme, + darkTheme: wgerDarkTheme, + highContrastTheme: wgerLightThemeHc, + highContrastDarkTheme: wgerDarkThemeHc, + themeMode: user.themeMode, + home: _getHomeScreen(auth), + routes: { + DashboardScreen.routeName: (ctx) => const DashboardScreen(), + FormScreen.routeName: (ctx) => const FormScreen(), + GalleryScreen.routeName: (ctx) => const GalleryScreen(), + GymModeScreen.routeName: (ctx) => const GymModeScreen(), + HomeTabsScreen.routeName: (ctx) => HomeTabsScreen(), + MeasurementCategoriesScreen.routeName: (ctx) => + const MeasurementCategoriesScreen(), + MeasurementEntriesScreen.routeName: (ctx) => const MeasurementEntriesScreen(), + NutritionalPlansScreen.routeName: (ctx) => const NutritionalPlansScreen(), + NutritionalDiaryScreen.routeName: (ctx) => const NutritionalDiaryScreen(), + NutritionalPlanScreen.routeName: (ctx) => const NutritionalPlanScreen(), + LogMealsScreen.routeName: (ctx) => const LogMealsScreen(), + LogMealScreen.routeName: (ctx) => const LogMealScreen(), + WeightScreen.routeName: (ctx) => const WeightScreen(), + RoutineScreen.routeName: (ctx) => const RoutineScreen(), + RoutineEditScreen.routeName: (ctx) => const RoutineEditScreen(), + WorkoutLogsScreen.routeName: (ctx) => const WorkoutLogsScreen(), + RoutineListScreen.routeName: (ctx) => const RoutineListScreen(), + ExercisesScreen.routeName: (ctx) => const ExercisesScreen(), + ExerciseDetailScreen.routeName: (ctx) => const ExerciseDetailScreen(), + AddExerciseScreen.routeName: (ctx) => const AddExerciseScreen(), + AboutPage.routeName: (ctx) => const AboutPage(), + SettingsPage.routeName: (ctx) => const SettingsPage(), + LogOverviewPage.routeName: (ctx) => const LogOverviewPage(), + ConfigurePlatesScreen.routeName: (ctx) => const ConfigurePlatesScreen(), + TrophyScreen.routeName: (ctx) => const TrophyScreen(), + }, + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + ); + }, + ), + ), + ); + }, ), ); } diff --git a/lib/models/trophies/trophy.dart b/lib/models/trophies/trophy.dart new file mode 100644 index 000000000..d58f5512a --- /dev/null +++ b/lib/models/trophies/trophy.dart @@ -0,0 +1,66 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2025 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'package:json_annotation/json_annotation.dart'; + +part 'trophy.g.dart'; + +enum TrophyType { time, volume, count, sequence, date, pr, other } + +@JsonSerializable() +class Trophy { + @JsonKey(required: true) + final int id; + + @JsonKey(required: true) + final String uuid; + + @JsonKey(required: true) + final String name; + + @JsonKey(required: true) + final String description; + + @JsonKey(required: true) + final String image; + + @JsonKey(required: true, name: 'trophy_type') + final TrophyType type; + + @JsonKey(required: true, name: 'is_hidden') + final bool isHidden; + + @JsonKey(required: true, name: 'is_progressive') + final bool isProgressive; + + Trophy({ + required this.id, + required this.uuid, + required this.name, + required this.description, + required this.image, + required this.type, + required this.isHidden, + required this.isProgressive, + }); + + // Boilerplate + factory Trophy.fromJson(Map json) => _$TrophyFromJson(json); + + Map toJson() => _$TrophyToJson(this); +} diff --git a/lib/models/trophies/trophy.g.dart b/lib/models/trophies/trophy.g.dart new file mode 100644 index 000000000..aba73ac10 --- /dev/null +++ b/lib/models/trophies/trophy.g.dart @@ -0,0 +1,72 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2025 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'trophy.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Trophy _$TrophyFromJson(Map json) { + $checkKeys( + json, + requiredKeys: const [ + 'id', + 'uuid', + 'name', + 'description', + 'image', + 'trophy_type', + 'is_hidden', + 'is_progressive', + ], + ); + return Trophy( + id: (json['id'] as num).toInt(), + uuid: json['uuid'] as String, + name: json['name'] as String, + description: json['description'] as String, + image: json['image'] as String, + type: $enumDecode(_$TrophyTypeEnumMap, json['trophy_type']), + isHidden: json['is_hidden'] as bool, + isProgressive: json['is_progressive'] as bool, + ); +} + +Map _$TrophyToJson(Trophy instance) => { + 'id': instance.id, + 'uuid': instance.uuid, + 'name': instance.name, + 'description': instance.description, + 'image': instance.image, + 'trophy_type': _$TrophyTypeEnumMap[instance.type]!, + 'is_hidden': instance.isHidden, + 'is_progressive': instance.isProgressive, +}; + +const _$TrophyTypeEnumMap = { + TrophyType.time: 'time', + TrophyType.volume: 'volume', + TrophyType.count: 'count', + TrophyType.sequence: 'sequence', + TrophyType.date: 'date', + TrophyType.pr: 'pr', + TrophyType.other: 'other', +}; diff --git a/lib/models/trophies/user_trophy.dart b/lib/models/trophies/user_trophy.dart new file mode 100644 index 000000000..8e1e7a816 --- /dev/null +++ b/lib/models/trophies/user_trophy.dart @@ -0,0 +1,62 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2025 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'package:json_annotation/json_annotation.dart'; +import 'package:wger/helpers/json.dart'; + +import 'trophy.dart'; +import 'user_trophy_context_data.dart'; + +part 'user_trophy.g.dart'; + +/// A trophy awarded to a user for achieving a specific milestone. + +@JsonSerializable() +class UserTrophy { + @JsonKey(required: true) + final int id; + + @JsonKey(required: true) + final Trophy trophy; + + @JsonKey(required: true, name: 'earned_at', fromJson: utcIso8601ToLocalDate) + final DateTime earnedAt; + + @JsonKey(required: true) + final num progress; + + @JsonKey(required: true, name: 'is_notified') + final bool isNotified; + + @JsonKey(required: true, name: 'context_data') + final ContextData? contextData; + + UserTrophy({ + required this.id, + required this.trophy, + required this.earnedAt, + required this.progress, + required this.isNotified, + this.contextData, + }); + + // Boilerplate + factory UserTrophy.fromJson(Map json) => _$UserTrophyFromJson(json); + + Map toJson() => _$UserTrophyToJson(this); +} diff --git a/lib/models/trophies/user_trophy.g.dart b/lib/models/trophies/user_trophy.g.dart new file mode 100644 index 000000000..97d9da440 --- /dev/null +++ b/lib/models/trophies/user_trophy.g.dart @@ -0,0 +1,40 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_trophy.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +UserTrophy _$UserTrophyFromJson(Map json) { + $checkKeys( + json, + requiredKeys: const [ + 'id', + 'trophy', + 'earned_at', + 'progress', + 'is_notified', + 'context_data', + ], + ); + return UserTrophy( + id: (json['id'] as num).toInt(), + trophy: Trophy.fromJson(json['trophy'] as Map), + earnedAt: utcIso8601ToLocalDate(json['earned_at'] as String), + progress: json['progress'] as num, + isNotified: json['is_notified'] as bool, + contextData: json['context_data'] == null + ? null + : ContextData.fromJson(json['context_data'] as Map), + ); +} + +Map _$UserTrophyToJson(UserTrophy instance) => { + 'id': instance.id, + 'trophy': instance.trophy, + 'earned_at': instance.earnedAt.toIso8601String(), + 'progress': instance.progress, + 'is_notified': instance.isNotified, + 'context_data': instance.contextData, +}; diff --git a/lib/models/trophies/user_trophy_context_data.dart b/lib/models/trophies/user_trophy_context_data.dart new file mode 100644 index 000000000..d114e1e1b --- /dev/null +++ b/lib/models/trophies/user_trophy_context_data.dart @@ -0,0 +1,75 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2025 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'package:json_annotation/json_annotation.dart'; +import 'package:wger/helpers/json.dart'; + +part 'user_trophy_context_data.g.dart'; + +/// A trophy awarded to a user for achieving a specific milestone. + +@JsonSerializable() +class ContextData { + @JsonKey(required: true, name: 'log_id') + final int logId; + + @JsonKey(required: true, fromJson: utcIso8601ToLocalDate) + final DateTime date; + + @JsonKey(required: true, name: 'session_id') + final int sessionId; + + @JsonKey(required: true, name: 'exercise_id') + final int exerciseId; + + @JsonKey(required: true, name: 'repetitions_unit_id') + final int repetitionsUnitId; + + @JsonKey(required: true) + final num repetitions; + + @JsonKey(required: true, name: 'weight_unit_id') + final int weightUnitId; + + @JsonKey(required: true) + final num weight; + + @JsonKey(required: true) + final int? iteration; + + @JsonKey(required: true, name: 'one_rep_max_estimate') + final num oneRepMaxEstimate; + + ContextData({ + required this.logId, + required this.date, + required this.sessionId, + required this.exerciseId, + required this.repetitionsUnitId, + required this.repetitions, + required this.weightUnitId, + required this.weight, + this.iteration, + required this.oneRepMaxEstimate, + }); + + // Boilerplate + factory ContextData.fromJson(Map json) => _$ContextDataFromJson(json); + + Map toJson() => _$ContextDataToJson(this); +} diff --git a/lib/models/trophies/user_trophy_context_data.g.dart b/lib/models/trophies/user_trophy_context_data.g.dart new file mode 100644 index 000000000..e1fcc63a4 --- /dev/null +++ b/lib/models/trophies/user_trophy_context_data.g.dart @@ -0,0 +1,50 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_trophy_context_data.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ContextData _$ContextDataFromJson(Map json) { + $checkKeys( + json, + requiredKeys: const [ + 'log_id', + 'date', + 'session_id', + 'exercise_id', + 'repetitions_unit_id', + 'repetitions', + 'weight_unit_id', + 'weight', + 'iteration', + 'one_rep_max_estimate', + ], + ); + return ContextData( + logId: (json['log_id'] as num).toInt(), + date: utcIso8601ToLocalDate(json['date'] as String), + sessionId: (json['session_id'] as num).toInt(), + exerciseId: (json['exercise_id'] as num).toInt(), + repetitionsUnitId: (json['repetitions_unit_id'] as num).toInt(), + repetitions: json['repetitions'] as num, + weightUnitId: (json['weight_unit_id'] as num).toInt(), + weight: json['weight'] as num, + iteration: (json['iteration'] as num?)?.toInt(), + oneRepMaxEstimate: json['one_rep_max_estimate'] as num, + ); +} + +Map _$ContextDataToJson(ContextData instance) => { + 'log_id': instance.logId, + 'date': instance.date.toIso8601String(), + 'session_id': instance.sessionId, + 'exercise_id': instance.exerciseId, + 'repetitions_unit_id': instance.repetitionsUnitId, + 'repetitions': instance.repetitions, + 'weight_unit_id': instance.weightUnitId, + 'weight': instance.weight, + 'iteration': instance.iteration, + 'one_rep_max_estimate': instance.oneRepMaxEstimate, +}; diff --git a/lib/models/trophies/user_trophy_progression.dart b/lib/models/trophies/user_trophy_progression.dart new file mode 100644 index 000000000..6cf115ba2 --- /dev/null +++ b/lib/models/trophies/user_trophy_progression.dart @@ -0,0 +1,67 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2025 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'package:json_annotation/json_annotation.dart'; +import 'package:wger/helpers/json.dart'; +import 'package:wger/models/trophies/trophy.dart'; + +part 'user_trophy_progression.g.dart'; + +@JsonSerializable() +class UserTrophyProgression { + @JsonKey(required: true) + final Trophy trophy; + + @JsonKey(required: true, name: 'is_earned') + final bool isEarned; + + @JsonKey(required: true, name: 'earned_at', fromJson: utcIso8601ToLocalDateNull) + final DateTime? earnedAt; + + /// Progress towards earning the trophy (0-100%) + @JsonKey(required: true) + final num progress; + + /// Current value towards the trophy goal (e.g., number of workouts completed) + @JsonKey(required: true, name: 'current_value', fromJson: stringToNumNull) + num? currentValue; + + /// Target value to achieve the trophy goal + @JsonKey(required: true, name: 'target_value', fromJson: stringToNumNull) + num? targetValue; + + /// Human-readable progress display (e.g., "3 / 10" or "51%") + @JsonKey(required: true, name: 'progress_display') + String? progressDisplay; + + UserTrophyProgression({ + required this.trophy, + required this.isEarned, + required this.earnedAt, + required this.progress, + required this.currentValue, + required this.targetValue, + this.progressDisplay, + }); + + // Boilerplate + factory UserTrophyProgression.fromJson(Map json) => + _$UserTrophyProgressionFromJson(json); + + Map toJson() => _$UserTrophyProgressionToJson(this); +} diff --git a/lib/models/trophies/user_trophy_progression.g.dart b/lib/models/trophies/user_trophy_progression.g.dart new file mode 100644 index 000000000..de2a49e70 --- /dev/null +++ b/lib/models/trophies/user_trophy_progression.g.dart @@ -0,0 +1,45 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_trophy_progression.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +UserTrophyProgression _$UserTrophyProgressionFromJson( + Map json, +) { + $checkKeys( + json, + requiredKeys: const [ + 'trophy', + 'is_earned', + 'earned_at', + 'progress', + 'current_value', + 'target_value', + 'progress_display', + ], + ); + return UserTrophyProgression( + trophy: Trophy.fromJson(json['trophy'] as Map), + isEarned: json['is_earned'] as bool, + earnedAt: utcIso8601ToLocalDateNull(json['earned_at'] as String?), + progress: json['progress'] as num, + currentValue: stringToNumNull(json['current_value'] as String?), + targetValue: stringToNumNull(json['target_value'] as String?), + progressDisplay: json['progress_display'] as String?, + ); +} + +Map _$UserTrophyProgressionToJson( + UserTrophyProgression instance, +) => { + 'trophy': instance.trophy, + 'is_earned': instance.isEarned, + 'earned_at': instance.earnedAt?.toIso8601String(), + 'progress': instance.progress, + 'current_value': instance.currentValue, + 'target_value': instance.targetValue, + 'progress_display': instance.progressDisplay, +}; diff --git a/lib/models/workouts/session.g.dart b/lib/models/workouts/session.g.dart index ac78029df..bf061fa15 100644 --- a/lib/models/workouts/session.g.dart +++ b/lib/models/workouts/session.g.dart @@ -23,7 +23,9 @@ WorkoutSession _$WorkoutSessionFromJson(Map json) { id: (json['id'] as num?)?.toInt(), dayId: (json['day'] as num?)?.toInt(), routineId: (json['routine'] as num?)?.toInt(), - impression: json['impression'] == null ? 2 : int.parse(json['impression'] as String), + impression: json['impression'] == null + ? DEFAULT_IMPRESSION + : int.parse(json['impression'] as String), notes: json['notes'] as String? ?? '', timeStart: stringToTimeNull(json['time_start'] as String?), timeEnd: stringToTimeNull(json['time_end'] as String?), diff --git a/lib/models/workouts/slot_data.g.dart b/lib/models/workouts/slot_data.g.dart index 589b2b999..756717e67 100644 --- a/lib/models/workouts/slot_data.g.dart +++ b/lib/models/workouts/slot_data.g.dart @@ -1,21 +1,3 @@ -/* - * This file is part of wger Workout Manager . - * Copyright (c) 2025 wger Team - * - * wger Workout Manager is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - // GENERATED CODE - DO NOT MODIFY BY HAND part of 'slot_data.dart'; diff --git a/lib/providers/base_provider.dart b/lib/providers/base_provider.dart index dee257d75..a1e11f549 100644 --- a/lib/providers/base_provider.dart +++ b/lib/providers/base_provider.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -37,7 +37,7 @@ class WgerBaseProvider { this.client = client ?? http.Client(); } - Map getDefaultHeaders({bool includeAuth = false}) { + Map getDefaultHeaders({bool includeAuth = false, String? language}) { final out = { HttpHeaders.contentTypeHeader: 'application/json; charset=UTF-8', HttpHeaders.userAgentHeader: auth.getAppNameHeader(), @@ -47,6 +47,10 @@ class WgerBaseProvider { out[HttpHeaders.authorizationHeader] = 'Token ${auth.token}'; } + if (language != null) { + out[HttpHeaders.acceptLanguageHeader] = language; + } + return out; } @@ -56,8 +60,7 @@ class WgerBaseProvider { } /// Fetch and retrieve the overview list of objects, returns the JSON parsed response - Future fetch(Uri uri) async { - // Future | List> fetch(Uri uri) async { + Future fetch(Uri uri, {String? language}) async { // Send the request final response = await client.get( uri, @@ -74,13 +77,13 @@ class WgerBaseProvider { } /// Fetch and retrieve the overview list of objects, returns the JSON parsed response - Future> fetchPaginated(Uri uri) async { + Future> fetchPaginated(Uri uri, {String? language}) async { final out = []; var url = uri; var allPagesProcessed = false; while (!allPagesProcessed) { - final data = await fetch(url); + final data = await fetch(url, language: language); data['results'].forEach((e) => out.add(e)); diff --git a/lib/providers/gym_state.dart b/lib/providers/gym_state.dart index 949a753f8..752f785df 100644 --- a/lib/providers/gym_state.dart +++ b/lib/providers/gym_state.dart @@ -481,7 +481,7 @@ class GymStateNotifier extends _$GymStateNotifier { pages.add(PageEntry(type: PageType.workoutSummary, pageIndex: pageIndex + 1)); state = state.copyWith(pages: pages); - print(readPageStructure()); + // _logger.fine(readPageStructure()); _logger.finer('Initialized ${state.pages.length} pages'); } diff --git a/lib/providers/gym_state.g.dart b/lib/providers/gym_state.g.dart index 7596fa4be..1ddf6b8b1 100644 --- a/lib/providers/gym_state.g.dart +++ b/lib/providers/gym_state.g.dart @@ -1,21 +1,3 @@ -/* - * This file is part of wger Workout Manager . - * Copyright (c) 2025 wger Team - * - * wger Workout Manager is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - // GENERATED CODE - DO NOT MODIFY BY HAND part of 'gym_state.dart'; @@ -58,7 +40,7 @@ final class GymStateNotifierProvider extends $NotifierProvider r'4e1ac85de3c9f5c7dad4b0c5e6ad80ad36397610'; +String _$gymStateNotifierHash() => r'8474afce33638bf67570fd64b64e9b5d171804d3'; abstract class _$GymStateNotifier extends $Notifier { GymModeState build(); diff --git a/lib/providers/trophies.dart b/lib/providers/trophies.dart new file mode 100644 index 000000000..19289af58 --- /dev/null +++ b/lib/providers/trophies.dart @@ -0,0 +1,179 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2026 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'package:logging/logging.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:wger/helpers/consts.dart'; +import 'package:wger/models/trophies/trophy.dart'; +import 'package:wger/models/trophies/user_trophy.dart'; +import 'package:wger/models/trophies/user_trophy_progression.dart'; +import 'package:wger/providers/wger_base_riverpod.dart'; + +import 'base_provider.dart'; + +part 'trophies.g.dart'; + +class TrophyState { + final _logger = Logger('TrophyState'); + + final List trophies; + final List userTrophies; + final List trophyProgression; + + TrophyState({ + this.trophies = const [], + this.userTrophies = const [], + this.trophyProgression = const [], + }); + + TrophyState copyWith({ + List? trophies, + List? userTrophies, + List? trophyProgression, + }) { + return TrophyState( + trophies: trophies ?? this.trophies, + userTrophies: userTrophies ?? this.userTrophies, + trophyProgression: trophyProgression ?? this.trophyProgression, + ); + } + + List get prTrophies => + userTrophies.where((t) => t.trophy.type == TrophyType.pr).toList(); + + List get nonPrTrophies => + userTrophies.where((t) => t.trophy.type != TrophyType.pr).toList(); +} + +class TrophyRepository { + final _logger = Logger('TrophyRepository'); + + final WgerBaseProvider base; + final trophiesPath = 'trophy'; + final userTrophiesPath = 'user-trophy'; + final userTrophyProgressionPath = 'trophy/progress'; + + TrophyRepository(this.base); + + Future> fetchTrophies({String? language}) async { + try { + final url = base.makeUrl(trophiesPath, query: {'limit': API_MAX_PAGE_SIZE}); + final trophyData = await base.fetchPaginated(url, language: language); + return trophyData.map((e) => Trophy.fromJson(e)).toList(); + } catch (e, stk) { + _logger.warning('Error fetching trophies:', e, stk); + return []; + } + } + + Future> fetchUserTrophies({ + Map? filterQuery, + String? language, + }) async { + final query = {'limit': API_MAX_PAGE_SIZE}; + if (filterQuery != null) { + query.addAll(filterQuery); + } + + try { + final url = base.makeUrl(userTrophiesPath, query: query); + final trophyData = await base.fetchPaginated(url, language: language); + return trophyData.map((e) => UserTrophy.fromJson(e)).toList(); + } catch (e, stk) { + _logger.warning('Error fetching user trophies:'); + _logger.warning(e); + _logger.warning(stk); + return []; + } + } + + Future> fetchProgression({ + Map? filterQuery, + String? language, + }) async { + try { + final url = base.makeUrl(userTrophyProgressionPath, query: filterQuery); + final List data = await base.fetch(url, language: language); + return data.map((e) => UserTrophyProgression.fromJson(e)).toList(); + } catch (e, stk) { + _logger.warning('Error fetching user trophy progression:', e, stk); + return []; + } + } + + List filterByType(List list, TrophyType type) => + list.where((t) => t.type == type).toList(); +} + +@riverpod +TrophyRepository trophyRepository(Ref ref) { + final base = ref.read(wgerBaseProvider); + return TrophyRepository(base); +} + +@Riverpod(keepAlive: true) +final class TrophyStateNotifier extends _$TrophyStateNotifier { + final _logger = Logger('TrophyStateNotifier'); + + @override + TrophyState build() { + return TrophyState(); + } + + Future fetchAll({String? language}) async { + await Future.wait([ + fetchTrophies(language: language), + fetchUserTrophies(language: language), + fetchTrophyProgression(language: language), + ]); + } + + /// Fetch all available trophies + Future> fetchTrophies({String? language}) async { + _logger.finer('Fetching trophies'); + + final repo = ref.read(trophyRepositoryProvider); + final result = await repo.fetchTrophies(language: language); + state = state.copyWith(trophies: result); + return result; + } + + /// Fetch trophies awarded to the user, excludes hidden trophies + Future> fetchUserTrophies({String? language}) async { + _logger.finer('Fetching user trophies'); + + final repo = ref.read(trophyRepositoryProvider); + final result = await repo.fetchUserTrophies( + filterQuery: {'trophy__is_hidden': 'false'}, //'trophy__is_repeatable': 'false' + language: language, + ); + state = state.copyWith(userTrophies: result); + return result; + } + + /// Fetch trophy progression for the user + Future> fetchTrophyProgression({String? language}) async { + _logger.finer('Fetching user trophy progression'); + + // Note that repeatable trophies are filtered out in the backend + final repo = ref.read(trophyRepositoryProvider); + final result = await repo.fetchProgression(language: language); + state = state.copyWith(trophyProgression: result); + return result; + } +} diff --git a/lib/providers/trophies.g.dart b/lib/providers/trophies.g.dart new file mode 100644 index 000000000..2f2a95e8f --- /dev/null +++ b/lib/providers/trophies.g.dart @@ -0,0 +1,104 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'trophies.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(trophyRepository) +const trophyRepositoryProvider = TrophyRepositoryProvider._(); + +final class TrophyRepositoryProvider + extends $FunctionalProvider + with $Provider { + const TrophyRepositoryProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'trophyRepositoryProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$trophyRepositoryHash(); + + @$internal + @override + $ProviderElement $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); + + @override + TrophyRepository create(Ref ref) { + return trophyRepository(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(TrophyRepository value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$trophyRepositoryHash() => r'0699f0c0f7f324f3ba9b21420d9845a3e3096b61'; + +@ProviderFor(TrophyStateNotifier) +const trophyStateProvider = TrophyStateNotifierProvider._(); + +final class TrophyStateNotifierProvider + extends $NotifierProvider { + const TrophyStateNotifierProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'trophyStateProvider', + isAutoDispose: false, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$trophyStateNotifierHash(); + + @$internal + @override + TrophyStateNotifier create() => TrophyStateNotifier(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(TrophyState value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$trophyStateNotifierHash() => r'c80c732272cf843b698f28152f60b9a5f37ee449'; + +abstract class _$TrophyStateNotifier extends $Notifier { + TrophyState build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + TrophyState, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/lib/providers/wger_base_riverpod.dart b/lib/providers/wger_base_riverpod.dart new file mode 100644 index 000000000..0c73cbd23 --- /dev/null +++ b/lib/providers/wger_base_riverpod.dart @@ -0,0 +1,31 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2025 - 2025 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:wger/providers/auth.dart'; +import 'package:wger/providers/base_provider.dart'; + +/// Central provider that maps an existing [AuthProvider] (from the provider package) +/// to a [WgerBaseProvider] used by repositories. +/// +/// Usage: ref.watch(wgerBaseProvider(authProvider)) +final wgerBaseProvider = Provider((ref) { + throw UnimplementedError( + 'Override wgerBaseProvider in a ProviderScope with your existing WgerBaseProvider instance', + ); +}); diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index 3be5a4b4c..0a7143086 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2020 - 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -26,6 +26,8 @@ import 'package:wger/widgets/dashboard/widgets/nutrition.dart'; import 'package:wger/widgets/dashboard/widgets/routines.dart'; import 'package:wger/widgets/dashboard/widgets/weight.dart'; +import '../widgets/dashboard/widgets/trophies.dart'; + class DashboardScreen extends StatelessWidget { const DashboardScreen({super.key}); @@ -48,6 +50,7 @@ class DashboardScreen extends StatelessWidget { } final items = [ + const DashboardTrophiesWidget(), const DashboardRoutineWidget(), const DashboardNutritionWidget(), const DashboardWeightWidget(), diff --git a/lib/screens/home_tabs_screen.dart b/lib/screens/home_tabs_screen.dart index ebb703985..de6795cb8 100644 --- a/lib/screens/home_tabs_screen.dart +++ b/lib/screens/home_tabs_screen.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -17,6 +17,7 @@ */ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; @@ -30,6 +31,7 @@ import 'package:wger/providers/gallery.dart'; import 'package:wger/providers/measurement.dart'; import 'package:wger/providers/nutrition.dart'; import 'package:wger/providers/routines.dart'; +import 'package:wger/providers/trophies.dart'; import 'package:wger/providers/user.dart'; import 'package:wger/screens/dashboard.dart'; import 'package:wger/screens/gallery_screen.dart'; @@ -37,7 +39,7 @@ import 'package:wger/screens/nutritional_plans_screen.dart'; import 'package:wger/screens/routine_list_screen.dart'; import 'package:wger/screens/weight_screen.dart'; -class HomeTabsScreen extends StatefulWidget { +class HomeTabsScreen extends ConsumerStatefulWidget { final _logger = Logger('HomeTabsScreen'); HomeTabsScreen(); @@ -48,7 +50,8 @@ class HomeTabsScreen extends StatefulWidget { _HomeTabsScreenState createState() => _HomeTabsScreenState(); } -class _HomeTabsScreenState extends State with SingleTickerProviderStateMixin { +class _HomeTabsScreenState extends ConsumerState + with SingleTickerProviderStateMixin { late Future _initialData; bool _errorHandled = false; int _selectedIndex = 0; @@ -85,7 +88,9 @@ class _HomeTabsScreenState extends State with SingleTickerProvid /// Load initial data from the server Future _loadEntries() async { + final languageCode = Localizations.localeOf(context).languageCode; final authProvider = context.read(); + final trophyNotifier = ref.read(trophyStateProvider.notifier); if (!authProvider.dataInit) { final routinesProvider = context.read(); @@ -127,6 +132,7 @@ class _HomeTabsScreenState extends State with SingleTickerProvid // routinesProvider.fetchAndSetAllRoutinesFull(), weightProvider.fetchAndSetEntries(), measurementProvider.fetchAndSetAllCategoriesAndEntries(), + trophyNotifier.fetchAll(language: languageCode), ]); // diff --git a/lib/screens/routine_logs_screen.dart b/lib/screens/routine_logs_screen.dart index 93da5d5c1..c9909b1ff 100644 --- a/lib/screens/routine_logs_screen.dart +++ b/lib/screens/routine_logs_screen.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2025 wger Team + * Copyright (c) 2020 - 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. diff --git a/lib/screens/trophy_screen.dart b/lib/screens/trophy_screen.dart new file mode 100644 index 000000000..0cc3cc023 --- /dev/null +++ b/lib/screens/trophy_screen.dart @@ -0,0 +1,39 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2025 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * 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:wger/core/wide_screen_wrapper.dart'; +import 'package:wger/l10n/generated/app_localizations.dart'; +import 'package:wger/widgets/core/app_bar.dart'; +import 'package:wger/widgets/trophies/trophies_overview.dart'; + +class TrophyScreen extends StatelessWidget { + const TrophyScreen(); + + static const routeName = '/trophies'; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: EmptyAppBar(AppLocalizations.of(context).trophies), + body: WidescreenWrapper( + child: TrophiesOverview(), + ), + ); + } +} diff --git a/lib/widgets/dashboard/widgets/trophies.dart b/lib/widgets/dashboard/widgets/trophies.dart new file mode 100644 index 000000000..e164e5d5b --- /dev/null +++ b/lib/widgets/dashboard/widgets/trophies.dart @@ -0,0 +1,124 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2020 - 2026 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * 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_riverpod/flutter_riverpod.dart'; +import 'package:wger/models/trophies/trophy.dart'; +import 'package:wger/providers/trophies.dart'; +import 'package:wger/screens/trophy_screen.dart'; + +class DashboardTrophiesWidget extends ConsumerWidget { + const DashboardTrophiesWidget(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final trophiesState = ref.read(trophyStateProvider); + + return Card( + color: Colors.transparent, + shadowColor: Colors.transparent, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (trophiesState.nonPrTrophies.isEmpty) + Padding( + padding: const EdgeInsets.all(16.0), + child: Text('No trophies yet', style: Theme.of(context).textTheme.bodyMedium), + ) + else + SizedBox( + height: 140, + child: ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + scrollDirection: Axis.horizontal, + itemCount: trophiesState.nonPrTrophies.length, + separatorBuilder: (context, index) => const SizedBox(width: 12), + itemBuilder: (context, index) { + final userTrophy = trophiesState.nonPrTrophies[index]; + + return SizedBox( + width: 220, + child: TrophyCard(trophy: userTrophy.trophy), + ); + }, + ), + ), + ], + ), + ); + } +} + +class TrophyCard extends StatelessWidget { + const TrophyCard({ + super.key, + required this.trophy, + }); + + final Trophy trophy; + + @override + Widget build(BuildContext context) { + return Card.filled( + child: InkWell( + onTap: () { + Navigator.of(context).pushNamed(TrophyScreen.routeName); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + CircleAvatar( + radius: 30, + backgroundImage: NetworkImage(trophy.image), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + trophy.name, + style: Theme.of(context).textTheme.titleMedium, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + Text( + trophy.description, + style: Theme.of(context).textTheme.bodySmall, + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/widgets/routines/gym_mode/log_page.dart b/lib/widgets/routines/gym_mode/log_page.dart index 702542667..58ef1e4ec 100644 --- a/lib/widgets/routines/gym_mode/log_page.dart +++ b/lib/widgets/routines/gym_mode/log_page.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2020 - 2025 wger Team + * Copyright (c) 2020 - 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -30,6 +30,7 @@ import 'package:wger/models/workouts/slot_entry.dart'; import 'package:wger/providers/gym_state.dart'; import 'package:wger/providers/plate_weights.dart'; import 'package:wger/providers/routines.dart'; +import 'package:wger/providers/trophies.dart'; import 'package:wger/screens/configure_plates_screen.dart'; import 'package:wger/widgets/core/core.dart'; import 'package:wger/widgets/core/progress_indicator.dart'; @@ -549,6 +550,9 @@ class _LogFormWidgetState extends ConsumerState { _weightController = TextEditingController(); WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) { + return; + } _syncControllersWithWidget(); }); } @@ -778,6 +782,7 @@ class _LogFormWidgetState extends ConsumerState { try { final gymState = ref.read(gymStateProvider); final gymProvider = ref.read(gymStateProvider.notifier); + final trophyNotifier = ref.read(trophyStateProvider.notifier); await provider.Provider.of( context, @@ -786,6 +791,9 @@ class _LogFormWidgetState extends ConsumerState { final page = gymState.getSlotEntryPageByIndex()!; gymProvider.markSlotPageAsDone(page.uuid, isDone: true); + // + await trophyNotifier.fetchUserTrophies(); + if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( diff --git a/lib/widgets/routines/gym_mode/summary.dart b/lib/widgets/routines/gym_mode/summary.dart index 439d7a0d6..e50975008 100644 --- a/lib/widgets/routines/gym_mode/summary.dart +++ b/lib/widgets/routines/gym_mode/summary.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2020, 2025 wger Team + * Copyright (c) 2020 - 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -24,10 +24,12 @@ import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; import 'package:wger/helpers/date.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; +import 'package:wger/models/trophies/user_trophy.dart'; import 'package:wger/models/workouts/routine.dart'; import 'package:wger/models/workouts/session_api.dart'; import 'package:wger/providers/gym_state.dart'; import 'package:wger/providers/routines.dart'; +import 'package:wger/providers/trophies.dart'; import 'package:wger/widgets/core/progress_indicator.dart'; import 'package:wger/widgets/routines/gym_mode/navigation.dart'; @@ -36,7 +38,6 @@ import '../logs/muscle_groups.dart'; class WorkoutSummary extends ConsumerStatefulWidget { final _logger = Logger('WorkoutSummary'); - final PageController _controller; WorkoutSummary(this._controller); @@ -48,24 +49,39 @@ class WorkoutSummary extends ConsumerStatefulWidget { class _WorkoutSummaryState extends ConsumerState { late Future _initData; late Routine _routine; + bool _didInit = false; @override void initState() { super.initState(); - _initData = _reloadRoutineData(); } - Future _reloadRoutineData() async { + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (!_didInit) { + final languageCode = Localizations.localeOf(context).languageCode; + _initData = _reloadRoutineData(languageCode); + _didInit = true; + } + } + + Future _reloadRoutineData(String languageCode) async { widget._logger.fine('Loading routine data'); final gymState = ref.read(gymStateProvider); _routine = await context.read().fetchAndSetRoutineFull( gymState.routine.id!, ); + + final trophyNotifier = ref.read(trophyStateProvider.notifier); + await trophyNotifier.fetchUserTrophies(language: languageCode); } @override Widget build(BuildContext context) { + final trophyState = ref.watch(trophyStateProvider); + return Column( children: [ NavigationHeader( @@ -80,12 +96,20 @@ class _WorkoutSummaryState extends ConsumerState { if (snapshot.connectionState == ConnectionState.waiting) { return const BoxedProgressIndicator(); } else if (snapshot.hasError) { - return Center(child: Text('Error: ${snapshot.error}: ${snapshot.stackTrace}')); + widget._logger.warning(snapshot.error); + widget._logger.warning(snapshot.stackTrace); + return Center(child: Text('Error: ${snapshot.error}')); } else if (snapshot.connectionState == ConnectionState.done) { + final apiSession = _routine.sessions.firstWhereOrNull( + (s) => s.session.date.isSameDayAs(clock.now()), + ); + final userTrophies = trophyState.prTrophies + .where((t) => t.contextData?.sessionId == apiSession?.session.id) + .toList(); + return WorkoutSessionStats( - _routine.sessions.firstWhereOrNull( - (s) => s.session.date.isSameDayAs(clock.now()), - ), + apiSession, + userTrophies, ); } @@ -102,12 +126,14 @@ class _WorkoutSummaryState extends ConsumerState { class WorkoutSessionStats extends ConsumerWidget { final _logger = Logger('WorkoutSessionStats'); final WorkoutSessionApi? _sessionApi; + final List _userPrTrophies; - WorkoutSessionStats(this._sessionApi, {super.key}); + WorkoutSessionStats(this._sessionApi, this._userPrTrophies, {super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final i18n = AppLocalizations.of(context); + final theme = Theme.of(context); if (_sessionApi == null) { return Center( @@ -159,16 +185,19 @@ class WorkoutSessionStats extends ConsumerWidget { ), ], ), - // const SizedBox(height: 16), - // InfoCard( - // title: 'Personal Records', - // value: prCount.toString(), - // color: theme.colorScheme.tertiaryContainer, - // ), + if (_userPrTrophies.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 10), + child: InfoCard( + title: i18n.personalRecords, + value: _userPrTrophies.length.toString(), + color: theme.colorScheme.tertiaryContainer, + ), + ), const SizedBox(height: 10), MuscleGroupsCard(_sessionApi.logs), const SizedBox(height: 10), - ExercisesCard(_sessionApi), + ExercisesCard(_sessionApi, _userPrTrophies), FilledButton( onPressed: () { ref.read(gymStateProvider.notifier).clear(); diff --git a/lib/widgets/routines/logs/day_logs_container.dart b/lib/widgets/routines/logs/day_logs_container.dart index 49d829b06..ed3ac3a41 100644 --- a/lib/widgets/routines/logs/day_logs_container.dart +++ b/lib/widgets/routines/logs/day_logs_container.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2020, 2025 wger Team + * Copyright (c) 2020 - 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -17,33 +17,59 @@ */ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logging/logging.dart'; import 'package:wger/helpers/date.dart'; import 'package:wger/helpers/errors.dart'; +import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/workouts/routine.dart'; +import 'package:wger/providers/trophies.dart'; +import '../gym_mode/summary.dart'; import 'exercise_log_chart.dart'; import 'muscle_groups.dart'; import 'session_info.dart'; -class DayLogWidget extends StatelessWidget { +class DayLogWidget extends ConsumerWidget { final DateTime _date; final Routine _routine; + final _logger = Logger('DayLogWidget'); - const DayLogWidget(this._date, this._routine); + DayLogWidget(this._date, this._routine); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final i18n = AppLocalizations.of(context); + final theme = Theme.of(context); + + final trophyState = ref.read(trophyStateProvider); + final sessionApi = _routine.sessions.firstWhere( (sessionApi) => sessionApi.session.date.isSameDayAs(_date), ); final exercises = sessionApi.exercises; + final prTrophies = trophyState.prTrophies + .where((t) => t.contextData?.sessionId == sessionApi.session.id) + .toList(); + + _logger.info(trophyState.prTrophies); + _logger.info(prTrophies); + return Column( spacing: 10, children: [ Card(child: SessionInfo(sessionApi.session)), + if (prTrophies.isNotEmpty) + SizedBox( + width: double.infinity, + child: InfoCard( + title: i18n.personalRecords, + value: prTrophies.length.toString(), + color: theme.colorScheme.tertiaryContainer, + ), + ), MuscleGroupsCard(sessionApi.logs), - Column( spacing: 10, children: [ @@ -66,7 +92,17 @@ class DayLogWidget extends StatelessWidget { (log) => Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(log.repTextNoNl(context)), + Row( + children: [ + if (prTrophies.any((t) => t.contextData?.logId == log.id)) + Icon( + Icons.emoji_events, + color: theme.colorScheme.primary, + size: 20, + ), + Text(log.repTextNoNl(context)), + ], + ), IconButton( icon: const Icon(Icons.delete), key: ValueKey('delete-log-${log.id}'), diff --git a/lib/widgets/routines/logs/exercises_expansion_card.dart b/lib/widgets/routines/logs/exercises_expansion_card.dart index 0286418f2..ea9634e79 100644 --- a/lib/widgets/routines/logs/exercises_expansion_card.dart +++ b/lib/widgets/routines/logs/exercises_expansion_card.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2020, 2025 wger Team + * Copyright (c) 2020 - 2025 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -20,13 +20,15 @@ import 'package:flutter/material.dart'; import 'package:wger/helpers/i18n.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/exercises/exercise.dart'; +import 'package:wger/models/trophies/user_trophy.dart'; import 'package:wger/models/workouts/log.dart'; import 'package:wger/models/workouts/session_api.dart'; class ExercisesCard extends StatelessWidget { final WorkoutSessionApi session; + final List userPrTrophies; - const ExercisesCard(this.session, {super.key}); + const ExercisesCard(this.session, this.userPrTrophies, {super.key}); @override Widget build(BuildContext context) { @@ -44,7 +46,11 @@ class ExercisesCard extends StatelessWidget { const SizedBox(height: 16), ...exercises.map((exercise) { final logs = session.logs.where((log) => log.exerciseId == exercise.id).toList(); - return _ExerciseExpansionTile(exercise: exercise, logs: logs); + return _ExerciseExpansionTile( + exercise: exercise, + logs: logs, + userPrTrophies: userPrTrophies, + ); }), ], ), @@ -57,8 +63,10 @@ class _ExerciseExpansionTile extends StatelessWidget { const _ExerciseExpansionTile({ required this.exercise, required this.logs, + required this.userPrTrophies, }); + final List userPrTrophies; final Exercise exercise; final List logs; @@ -78,20 +86,20 @@ class _ExerciseExpansionTile extends StatelessWidget { // leading: const Icon(Icons.fitness_center), title: Text(exercise.getTranslation(languageCode).name, style: theme.textTheme.titleMedium), subtitle: Text('Top set: $topSetWeight $topSetWeightUnit'), - children: logs.map((log) => _SetDataRow(log: log)).toList(), + children: logs.map((log) => _SetDataRow(log: log, userPrTrophies: userPrTrophies)).toList(), ); } } class _SetDataRow extends StatelessWidget { - const _SetDataRow({required this.log}); + const _SetDataRow({required this.log, required this.userPrTrophies}); final Log log; + final List userPrTrophies; @override Widget build(BuildContext context) { final theme = Theme.of(context); - final i18n = AppLocalizations.of(context); return Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), @@ -103,6 +111,8 @@ class _SetDataRow extends StatelessWidget { log.repTextNoNl(context), style: theme.textTheme.bodyMedium, ), + if (userPrTrophies.any((trophy) => trophy.contextData?.logId == log.id)) + Icon(Icons.emoji_events, color: theme.colorScheme.primary, size: 20), // if (log.volume() > 0) // Text( // '${log.volume().toStringAsFixed(0)} ${getServerStringTranslation(log.weightUnitObj!.name, context)}', diff --git a/lib/widgets/routines/logs/log_overview_routine.dart b/lib/widgets/routines/logs/log_overview_routine.dart index dfcfc1aa4..34f0be5ad 100644 --- a/lib/widgets/routines/logs/log_overview_routine.dart +++ b/lib/widgets/routines/logs/log_overview_routine.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2025 wger Team + * Copyright (c) 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -18,20 +18,26 @@ import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:table_calendar/table_calendar.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/workouts/routine.dart'; +import 'package:wger/providers/trophies.dart'; import 'package:wger/theme/theme.dart'; import 'package:wger/widgets/routines/logs/day_logs_container.dart'; -class WorkoutLogs extends StatelessWidget { +class WorkoutLogs extends ConsumerWidget { final Routine _routine; const WorkoutLogs(this._routine); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final languageCode = Localizations.localeOf(context).languageCode; + final trophyNotifier = ref.read(trophyStateProvider.notifier); + trophyNotifier.fetchUserTrophies(language: languageCode); + return ListView( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8), children: [ diff --git a/lib/widgets/trophies/trophies_overview.dart b/lib/widgets/trophies/trophies_overview.dart new file mode 100644 index 000000000..706040dfa --- /dev/null +++ b/lib/widgets/trophies/trophies_overview.dart @@ -0,0 +1,177 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2025 - 2026 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * 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_riverpod/flutter_riverpod.dart'; +import 'package:wger/helpers/material.dart'; +import 'package:wger/models/trophies/user_trophy_progression.dart'; +import 'package:wger/providers/trophies.dart'; + +class TrophiesOverview extends ConsumerWidget { + const TrophiesOverview({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final trophyState = ref.watch(trophyStateProvider); + + // Responsive grid: determine columns based on screen width + final width = MediaQuery.widthOf(context); + int crossAxisCount = 1; + if (width <= MATERIAL_XS_BREAKPOINT) { + crossAxisCount = 2; + } else if (width > MATERIAL_XS_BREAKPOINT && width < MATERIAL_MD_BREAKPOINT) { + crossAxisCount = 3; + } else if (width >= MATERIAL_MD_BREAKPOINT && width < MATERIAL_LG_BREAKPOINT) { + crossAxisCount = 4; + } else { + crossAxisCount = 5; + } + + // If empty, show placeholder + if (trophyState.trophyProgression.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + 'No trophies yet', + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.center, + ), + ), + ); + } + + return RepaintBoundary( + child: GridView.builder( + padding: const EdgeInsets.all(12), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + ), + key: const ValueKey('trophy-grid'), + itemCount: trophyState.trophyProgression.length, + itemBuilder: (context, index) { + return _TrophyCardImage(userProgression: trophyState.trophyProgression[index]); + }, + ), + ); + } +} + +class _TrophyCardImage extends StatelessWidget { + final UserTrophyProgression userProgression; + + const _TrophyCardImage({required this.userProgression}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colorScheme = theme.colorScheme; + + final double progress = (userProgression.progress.toDouble() / 100.0).clamp(0.0, 1.0); + + return Opacity( + opacity: userProgression.isEarned ? 1.0 : 0.5, + child: Card( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: BorderSide( + color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.18), + width: userProgression.isEarned ? 1.2 : 0, + ), + ), + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: 70, + height: 70, + child: ClipOval( + child: Image.network( + userProgression.trophy.image, + fit: BoxFit.cover, + errorBuilder: (context, error, stackTrace) => Center( + child: Icon(Icons.emoji_events, size: 28, color: colorScheme.primary), + ), + ), + ), + ), + + Text( + userProgression.trophy.name, + style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 4), + + Text( + userProgression.trophy.description, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.textTheme.bodySmall?.color, + ), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + const SizedBox(height: 8), + + if (userProgression.trophy.isProgressive && !userProgression.isEarned) + Tooltip( + message: 'Progress: ${userProgression.progressDisplay}', + child: SizedBox( + height: 6, + child: ClipRRect( + borderRadius: BorderRadius.circular(6), + child: LinearProgressIndicator( + value: progress, + minHeight: 6, + valueColor: AlwaysStoppedAnimation(colorScheme.primary), + backgroundColor: colorScheme.onSurface.withAlpha((0.06 * 255).round()), + ), + ), + ), + ), + ], + ), + ), + if (userProgression.isEarned) + Positioned( + top: 6, + right: 6, + child: Container( + width: 28, + height: 28, + decoration: const BoxDecoration( + color: Colors.green, + shape: BoxShape.circle, + ), + child: const Icon(Icons.check, size: 16, color: Colors.white), + ), + ), + ], + ), + ), + ); + } +} diff --git a/test/core/settings_test.mocks.dart b/test/core/settings_test.mocks.dart index db2394bdf..e883c76de 100644 --- a/test/core/settings_test.mocks.dart +++ b/test/core/settings_test.mocks.dart @@ -1066,10 +1066,14 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider { ); @override - Map getDefaultHeaders({bool? includeAuth = false}) => + Map getDefaultHeaders({ + bool? includeAuth = false, + String? language, + }) => (super.noSuchMethod( Invocation.method(#getDefaultHeaders, [], { #includeAuth: includeAuth, + #language: language, }), returnValue: {}, ) @@ -1100,17 +1104,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider { as Uri); @override - _i18.Future fetch(Uri? uri) => + _i18.Future fetch(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method(#fetch, [uri], {#language: language}), returnValue: _i18.Future.value(), ) as _i18.Future); @override - _i18.Future> fetchPaginated(Uri? uri) => + _i18.Future> fetchPaginated(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetchPaginated, [uri]), + Invocation.method(#fetchPaginated, [uri], {#language: language}), returnValue: _i18.Future>.value([]), ) as _i18.Future>); diff --git a/test/core/validators_test.mocks.dart b/test/core/validators_test.mocks.dart index 8791d455c..fb63573cd 100644 --- a/test/core/validators_test.mocks.dart +++ b/test/core/validators_test.mocks.dart @@ -1,21 +1,3 @@ -/* - * This file is part of wger Workout Manager . - * Copyright (c) 2025 wger Team - * - * wger Workout Manager is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - // Mocks generated by Mockito 5.4.6 from annotations // in wger/test/core/validators_test.dart. // Do not manually edit this file. @@ -785,6 +767,17 @@ class MockAppLocalizations extends _i1.Mock implements _i2.AppLocalizations { ) as String); + @override + String get trophies => + (super.noSuchMethod( + Invocation.getter(#trophies), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#trophies), + ), + ) + as String); + @override String get routines => (super.noSuchMethod( @@ -1114,6 +1107,17 @@ class MockAppLocalizations extends _i1.Mock implements _i2.AppLocalizations { ) as String); + @override + String get personalRecords => + (super.noSuchMethod( + Invocation.getter(#personalRecords), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#personalRecords), + ), + ) + as String); + @override String get gymMode => (super.noSuchMethod( diff --git a/test/exercises/contribute_exercise_image_test.mocks.dart b/test/exercises/contribute_exercise_image_test.mocks.dart index c3685ed3e..c5c3b857c 100644 --- a/test/exercises/contribute_exercise_image_test.mocks.dart +++ b/test/exercises/contribute_exercise_image_test.mocks.dart @@ -368,10 +368,14 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider { ); @override - Map getDefaultHeaders({bool? includeAuth = false}) => + Map getDefaultHeaders({ + bool? includeAuth = false, + String? language, + }) => (super.noSuchMethod( Invocation.method(#getDefaultHeaders, [], { #includeAuth: includeAuth, + #language: language, }), returnValue: {}, ) @@ -402,17 +406,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider { as Uri); @override - _i14.Future fetch(Uri? uri) => + _i14.Future fetch(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method(#fetch, [uri], {#language: language}), returnValue: _i14.Future.value(), ) as _i14.Future); @override - _i14.Future> fetchPaginated(Uri? uri) => + _i14.Future> fetchPaginated(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetchPaginated, [uri]), + Invocation.method(#fetchPaginated, [uri], {#language: language}), returnValue: _i14.Future>.value([]), ) as _i14.Future>); diff --git a/test/gallery/gallery_form_test.mocks.dart b/test/gallery/gallery_form_test.mocks.dart index e14d78381..a531b6ae9 100644 --- a/test/gallery/gallery_form_test.mocks.dart +++ b/test/gallery/gallery_form_test.mocks.dart @@ -141,10 +141,14 @@ class MockGalleryProvider extends _i1.Mock implements _i4.GalleryProvider { as _i6.Future); @override - Map getDefaultHeaders({bool? includeAuth = false}) => + Map getDefaultHeaders({ + bool? includeAuth = false, + String? language, + }) => (super.noSuchMethod( Invocation.method(#getDefaultHeaders, [], { #includeAuth: includeAuth, + #language: language, }), returnValue: {}, ) @@ -175,17 +179,17 @@ class MockGalleryProvider extends _i1.Mock implements _i4.GalleryProvider { as Uri); @override - _i6.Future fetch(Uri? uri) => + _i6.Future fetch(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method(#fetch, [uri], {#language: language}), returnValue: _i6.Future.value(), ) as _i6.Future); @override - _i6.Future> fetchPaginated(Uri? uri) => + _i6.Future> fetchPaginated(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetchPaginated, [uri]), + Invocation.method(#fetchPaginated, [uri], {#language: language}), returnValue: _i6.Future>.value([]), ) as _i6.Future>); diff --git a/test/gallery/gallery_screen_test.mocks.dart b/test/gallery/gallery_screen_test.mocks.dart index d1ab1318c..330d0c420 100644 --- a/test/gallery/gallery_screen_test.mocks.dart +++ b/test/gallery/gallery_screen_test.mocks.dart @@ -141,10 +141,14 @@ class MockGalleryProvider extends _i1.Mock implements _i4.GalleryProvider { as _i6.Future); @override - Map getDefaultHeaders({bool? includeAuth = false}) => + Map getDefaultHeaders({ + bool? includeAuth = false, + String? language, + }) => (super.noSuchMethod( Invocation.method(#getDefaultHeaders, [], { #includeAuth: includeAuth, + #language: language, }), returnValue: {}, ) @@ -175,17 +179,17 @@ class MockGalleryProvider extends _i1.Mock implements _i4.GalleryProvider { as Uri); @override - _i6.Future fetch(Uri? uri) => + _i6.Future fetch(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method(#fetch, [uri], {#language: language}), returnValue: _i6.Future.value(), ) as _i6.Future); @override - _i6.Future> fetchPaginated(Uri? uri) => + _i6.Future> fetchPaginated(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetchPaginated, [uri]), + Invocation.method(#fetchPaginated, [uri], {#language: language}), returnValue: _i6.Future>.value([]), ) as _i6.Future>); diff --git a/test/measurements/measurement_provider_test.mocks.dart b/test/measurements/measurement_provider_test.mocks.dart index ef362a877..c8c22b455 100644 --- a/test/measurements/measurement_provider_test.mocks.dart +++ b/test/measurements/measurement_provider_test.mocks.dart @@ -78,10 +78,14 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { ); @override - Map getDefaultHeaders({bool? includeAuth = false}) => + Map getDefaultHeaders({ + bool? includeAuth = false, + String? language, + }) => (super.noSuchMethod( Invocation.method(#getDefaultHeaders, [], { #includeAuth: includeAuth, + #language: language, }), returnValue: {}, ) @@ -112,17 +116,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method(#fetch, [uri], {#language: language}), returnValue: _i5.Future.value(), ) as _i5.Future); @override - _i5.Future> fetchPaginated(Uri? uri) => + _i5.Future> fetchPaginated(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetchPaginated, [uri]), + Invocation.method(#fetchPaginated, [uri], {#language: language}), returnValue: _i5.Future>.value([]), ) as _i5.Future>); diff --git a/test/nutrition/nutritional_plan_screen_test.mocks.dart b/test/nutrition/nutritional_plan_screen_test.mocks.dart index 2fe3c4ac5..0ff6d2922 100644 --- a/test/nutrition/nutritional_plan_screen_test.mocks.dart +++ b/test/nutrition/nutritional_plan_screen_test.mocks.dart @@ -88,10 +88,14 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { ); @override - Map getDefaultHeaders({bool? includeAuth = false}) => + Map getDefaultHeaders({ + bool? includeAuth = false, + String? language, + }) => (super.noSuchMethod( Invocation.method(#getDefaultHeaders, [], { #includeAuth: includeAuth, + #language: language, }), returnValue: {}, ) @@ -122,17 +126,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method(#fetch, [uri], {#language: language}), returnValue: _i5.Future.value(), ) as _i5.Future); @override - _i5.Future> fetchPaginated(Uri? uri) => + _i5.Future> fetchPaginated(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetchPaginated, [uri]), + Invocation.method(#fetchPaginated, [uri], {#language: language}), returnValue: _i5.Future>.value([]), ) as _i5.Future>); diff --git a/test/nutrition/nutritional_plans_screen_test.mocks.dart b/test/nutrition/nutritional_plans_screen_test.mocks.dart index c702d4015..e42d7a17f 100644 --- a/test/nutrition/nutritional_plans_screen_test.mocks.dart +++ b/test/nutrition/nutritional_plans_screen_test.mocks.dart @@ -323,10 +323,14 @@ class MockWgerBaseProvider extends _i1.Mock implements _i8.WgerBaseProvider { ); @override - Map getDefaultHeaders({bool? includeAuth = false}) => + Map getDefaultHeaders({ + bool? includeAuth = false, + String? language, + }) => (super.noSuchMethod( Invocation.method(#getDefaultHeaders, [], { #includeAuth: includeAuth, + #language: language, }), returnValue: {}, ) @@ -357,17 +361,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i8.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method(#fetch, [uri], {#language: language}), returnValue: _i5.Future.value(), ) as _i5.Future); @override - _i5.Future> fetchPaginated(Uri? uri) => + _i5.Future> fetchPaginated(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetchPaginated, [uri]), + Invocation.method(#fetchPaginated, [uri], {#language: language}), returnValue: _i5.Future>.value([]), ) as _i5.Future>); diff --git a/test/routine/gym_mode/gym_mode_test.mocks.dart b/test/routine/gym_mode/gym_mode_test.mocks.dart index 6b3c882cc..8259d2c07 100644 --- a/test/routine/gym_mode/gym_mode_test.mocks.dart +++ b/test/routine/gym_mode/gym_mode_test.mocks.dart @@ -167,10 +167,14 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { ); @override - Map getDefaultHeaders({bool? includeAuth = false}) => + Map getDefaultHeaders({ + bool? includeAuth = false, + String? language, + }) => (super.noSuchMethod( Invocation.method(#getDefaultHeaders, [], { #includeAuth: includeAuth, + #language: language, }), returnValue: {}, ) @@ -201,17 +205,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i20.Future fetch(Uri? uri) => + _i20.Future fetch(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method(#fetch, [uri], {#language: language}), returnValue: _i20.Future.value(), ) as _i20.Future); @override - _i20.Future> fetchPaginated(Uri? uri) => + _i20.Future> fetchPaginated(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetchPaginated, [uri]), + Invocation.method(#fetchPaginated, [uri], {#language: language}), returnValue: _i20.Future>.value([]), ) as _i20.Future>); diff --git a/test/routine/routine_logs_screen_test.dart b/test/routine/routine_logs_screen_test.dart index c61bdf3a6..1081171c7 100644 --- a/test/routine/routine_logs_screen_test.dart +++ b/test/routine/routine_logs_screen_test.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -20,6 +20,7 @@ import 'dart:io'; import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; @@ -27,14 +28,16 @@ import 'package:provider/provider.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/workouts/routine.dart'; import 'package:wger/providers/routines.dart'; +import 'package:wger/providers/trophies.dart'; import 'package:wger/screens/routine_logs_screen.dart'; import 'package:wger/screens/routine_screen.dart'; import 'package:wger/widgets/routines/logs/log_overview_routine.dart'; import '../../test_data/routines.dart'; +import '../test_data/trophies.dart'; import 'routine_logs_screen_test.mocks.dart'; -@GenerateMocks([RoutinesProvider]) +@GenerateMocks([RoutinesProvider, TrophyRepository]) void main() { late Routine routine; final mockRoutinesProvider = MockRoutinesProvider(); @@ -49,25 +52,39 @@ void main() { Widget renderWidget({locale = 'en'}) { final key = GlobalKey(); - return ChangeNotifierProvider( - create: (context) => mockRoutinesProvider, - child: MaterialApp( - locale: Locale(locale), - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: AppLocalizations.supportedLocales, - navigatorKey: key, - home: TextButton( - onPressed: () => key.currentState!.push( - MaterialPageRoute( - settings: RouteSettings(arguments: routine.id), - builder: (_) => const WorkoutLogsScreen(), + // Arrange + final mockRepository = MockTrophyRepository(); + when( + mockRepository.fetchUserTrophies( + filterQuery: anyNamed('filterQuery'), + language: anyNamed('language'), + ), + ).thenAnswer((_) async => getUserTrophies()); + + return ProviderScope( + overrides: [ + trophyRepositoryProvider.overrideWithValue(mockRepository), + ], + child: ChangeNotifierProvider( + create: (context) => mockRoutinesProvider, + child: MaterialApp( + locale: Locale(locale), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + navigatorKey: key, + home: TextButton( + onPressed: () => key.currentState!.push( + MaterialPageRoute( + settings: RouteSettings(arguments: routine.id), + builder: (_) => const WorkoutLogsScreen(), + ), ), + child: const SizedBox(), ), - child: const SizedBox(), + routes: { + RoutineScreen.routeName: (ctx) => const WorkoutLogsScreen(), + }, ), - routes: { - RoutineScreen.routeName: (ctx) => const WorkoutLogsScreen(), - }, ), ); } diff --git a/test/routine/routine_logs_screen_test.mocks.dart b/test/routine/routine_logs_screen_test.mocks.dart index dca48e937..015095bb0 100644 --- a/test/routine/routine_logs_screen_test.mocks.dart +++ b/test/routine/routine_logs_screen_test.mocks.dart @@ -9,6 +9,9 @@ import 'dart:ui' as _i17; import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i16; import 'package:wger/models/exercises/exercise.dart' as _i15; +import 'package:wger/models/trophies/trophy.dart' as _i19; +import 'package:wger/models/trophies/user_trophy.dart' as _i20; +import 'package:wger/models/trophies/user_trophy_progression.dart' as _i21; import 'package:wger/models/workouts/base_config.dart' as _i9; import 'package:wger/models/workouts/day.dart' as _i6; import 'package:wger/models/workouts/day_data.dart' as _i14; @@ -21,6 +24,7 @@ import 'package:wger/models/workouts/slot_entry.dart' as _i8; import 'package:wger/models/workouts/weight_unit.dart' as _i3; import 'package:wger/providers/base_provider.dart' as _i2; import 'package:wger/providers/routines.dart' as _i12; +import 'package:wger/providers/trophies.dart' as _i18; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -592,3 +596,107 @@ class MockRoutinesProvider extends _i1.Mock implements _i12.RoutinesProvider { returnValueForMissingStub: null, ); } + +/// A class which mocks [TrophyRepository]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTrophyRepository extends _i1.Mock implements _i18.TrophyRepository { + MockTrophyRepository() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.WgerBaseProvider get base => + (super.noSuchMethod( + Invocation.getter(#base), + returnValue: _FakeWgerBaseProvider_0( + this, + Invocation.getter(#base), + ), + ) + as _i2.WgerBaseProvider); + + @override + String get trophiesPath => + (super.noSuchMethod( + Invocation.getter(#trophiesPath), + returnValue: _i16.dummyValue( + this, + Invocation.getter(#trophiesPath), + ), + ) + as String); + + @override + String get userTrophiesPath => + (super.noSuchMethod( + Invocation.getter(#userTrophiesPath), + returnValue: _i16.dummyValue( + this, + Invocation.getter(#userTrophiesPath), + ), + ) + as String); + + @override + String get userTrophyProgressionPath => + (super.noSuchMethod( + Invocation.getter(#userTrophyProgressionPath), + returnValue: _i16.dummyValue( + this, + Invocation.getter(#userTrophyProgressionPath), + ), + ) + as String); + + @override + _i13.Future> fetchTrophies({String? language}) => + (super.noSuchMethod( + Invocation.method(#fetchTrophies, [], {#language: language}), + returnValue: _i13.Future>.value(<_i19.Trophy>[]), + ) + as _i13.Future>); + + @override + _i13.Future> fetchUserTrophies({ + Map? filterQuery, + String? language, + }) => + (super.noSuchMethod( + Invocation.method(#fetchUserTrophies, [], { + #filterQuery: filterQuery, + #language: language, + }), + returnValue: _i13.Future>.value( + <_i20.UserTrophy>[], + ), + ) + as _i13.Future>); + + @override + _i13.Future> fetchProgression({ + Map? filterQuery, + String? language, + }) => + (super.noSuchMethod( + Invocation.method(#fetchProgression, [], { + #filterQuery: filterQuery, + #language: language, + }), + returnValue: _i13.Future>.value( + <_i21.UserTrophyProgression>[], + ), + ) + as _i13.Future>); + + @override + List<_i19.Trophy> filterByType( + List<_i19.Trophy>? list, + _i19.TrophyType? type, + ) => + (super.noSuchMethod( + Invocation.method(#filterByType, [list, type]), + returnValue: <_i19.Trophy>[], + ) + as List<_i19.Trophy>); +} diff --git a/test/routine/routine_screen_test.mocks.dart b/test/routine/routine_screen_test.mocks.dart index 99fe1643b..8f05a8179 100644 --- a/test/routine/routine_screen_test.mocks.dart +++ b/test/routine/routine_screen_test.mocks.dart @@ -78,10 +78,14 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { ); @override - Map getDefaultHeaders({bool? includeAuth = false}) => + Map getDefaultHeaders({ + bool? includeAuth = false, + String? language, + }) => (super.noSuchMethod( Invocation.method(#getDefaultHeaders, [], { #includeAuth: includeAuth, + #language: language, }), returnValue: {}, ) @@ -112,17 +116,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method(#fetch, [uri], {#language: language}), returnValue: _i5.Future.value(), ) as _i5.Future); @override - _i5.Future> fetchPaginated(Uri? uri) => + _i5.Future> fetchPaginated(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetchPaginated, [uri]), + Invocation.method(#fetchPaginated, [uri], {#language: language}), returnValue: _i5.Future>.value([]), ) as _i5.Future>); diff --git a/test/routine/routines_provider_test.mocks.dart b/test/routine/routines_provider_test.mocks.dart index 063265063..ae7514683 100644 --- a/test/routine/routines_provider_test.mocks.dart +++ b/test/routine/routines_provider_test.mocks.dart @@ -117,10 +117,14 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { ); @override - Map getDefaultHeaders({bool? includeAuth = false}) => + Map getDefaultHeaders({ + bool? includeAuth = false, + String? language, + }) => (super.noSuchMethod( Invocation.method(#getDefaultHeaders, [], { #includeAuth: includeAuth, + #language: language, }), returnValue: {}, ) @@ -151,17 +155,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i11.Future fetch(Uri? uri) => + _i11.Future fetch(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method(#fetch, [uri], {#language: language}), returnValue: _i11.Future.value(), ) as _i11.Future); @override - _i11.Future> fetchPaginated(Uri? uri) => + _i11.Future> fetchPaginated(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetchPaginated, [uri]), + Invocation.method(#fetchPaginated, [uri], {#language: language}), returnValue: _i11.Future>.value([]), ) as _i11.Future>); diff --git a/test/routine/routines_screen_test.mocks.dart b/test/routine/routines_screen_test.mocks.dart index 2607f048f..1dafb2ca2 100644 --- a/test/routine/routines_screen_test.mocks.dart +++ b/test/routine/routines_screen_test.mocks.dart @@ -78,10 +78,14 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { ); @override - Map getDefaultHeaders({bool? includeAuth = false}) => + Map getDefaultHeaders({ + bool? includeAuth = false, + String? language, + }) => (super.noSuchMethod( Invocation.method(#getDefaultHeaders, [], { #includeAuth: includeAuth, + #language: language, }), returnValue: {}, ) @@ -112,17 +116,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method(#fetch, [uri], {#language: language}), returnValue: _i5.Future.value(), ) as _i5.Future); @override - _i5.Future> fetchPaginated(Uri? uri) => + _i5.Future> fetchPaginated(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetchPaginated, [uri]), + Invocation.method(#fetchPaginated, [uri], {#language: language}), returnValue: _i5.Future>.value([]), ) as _i5.Future>); diff --git a/test/test_data/trophies.dart b/test/test_data/trophies.dart new file mode 100644 index 000000000..8fc13d286 --- /dev/null +++ b/test/test_data/trophies.dart @@ -0,0 +1,85 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2025 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'package:wger/models/trophies/trophy.dart'; +import 'package:wger/models/trophies/user_trophy.dart'; +import 'package:wger/models/trophies/user_trophy_progression.dart'; + +List getTestTrophies() { + return [ + Trophy( + id: 1, + uuid: '31a71d9a-bf26-4f18-b82f-afefe6f50df2', + name: 'New Year, New Me', + description: 'Work out on January 1st', + image: 'https://example.com/5362e55b-eaf1-4e34-9ef8-661538a3bdd9.png', + type: TrophyType.date, + isHidden: false, + isProgressive: false, + ), + Trophy( + id: 2, + uuid: 'b605b6a1-953d-41fb-87c9-a2f88b5f5907', + name: 'Unstoppable', + description: 'Maintain a 30-day workout streak', + image: 'https://example.com/b605b6a1-953d-41fb-87c9-a2f88b5f5907.png', + type: TrophyType.sequence, + isHidden: false, + isProgressive: true, + ), + ]; +} + +List getUserTrophyProgression() { + final trophies = getTestTrophies(); + + return [ + UserTrophyProgression( + trophy: trophies[0], + progress: 100, + isEarned: true, + earnedAt: DateTime(2025, 12, 20), + currentValue: null, + targetValue: null, + progressDisplay: null, + ), + UserTrophyProgression( + trophy: trophies[1], + progress: 40, + isEarned: false, + earnedAt: null, + currentValue: 12, + targetValue: 30, + progressDisplay: '12 / 30', + ), + ]; +} + +List getUserTrophies() { + final trophies = getTestTrophies(); + + return [ + UserTrophy( + id: 4, + earnedAt: DateTime(2025, 12, 20), + isNotified: true, + progress: 100, + trophy: trophies[0], + ), + ]; +} diff --git a/test/trophies/models/trophy_test.dart b/test/trophies/models/trophy_test.dart new file mode 100644 index 000000000..3285b589b --- /dev/null +++ b/test/trophies/models/trophy_test.dart @@ -0,0 +1,72 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2025 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'package:flutter_test/flutter_test.dart'; +import 'package:wger/models/trophies/trophy.dart'; + +void main() { + group('Trophy model', () { + final sampleJson = { + 'id': 1, + 'uuid': '550e8400-e29b-41d4-a716-446655440000', + 'name': 'First Steps', + 'description': 'Awarded for the first workout', + 'image': 'https://example.org/trophy.png', + 'trophy_type': 'count', + 'is_hidden': false, + 'is_progressive': true, + }; + + test('fromJson creates valid Trophy instance', () { + final trophy = Trophy.fromJson(sampleJson); + + expect(trophy.id, 1); + expect(trophy.uuid, '550e8400-e29b-41d4-a716-446655440000'); + expect(trophy.name, 'First Steps'); + expect(trophy.description, 'Awarded for the first workout'); + expect(trophy.image, 'https://example.org/trophy.png'); + expect(trophy.type, TrophyType.count); + expect(trophy.isHidden, isFalse); + expect(trophy.isProgressive, isTrue); + }); + + test('toJson returns expected map', () { + final trophy = Trophy( + id: 2, + uuid: '00000000-0000-0000-0000-000000000000', + name: 'Progressor', + description: 'Progressive trophy', + image: 'https://example.org/prog.png', + type: TrophyType.time, + isHidden: true, + isProgressive: false, + ); + + final json = trophy.toJson(); + + expect(json['id'], 2); + expect(json['uuid'], '00000000-0000-0000-0000-000000000000'); + expect(json['name'], 'Progressor'); + expect(json['description'], 'Progressive trophy'); + expect(json['image'], 'https://example.org/prog.png'); + expect(json['trophy_type'], 'time'); + expect(json['is_hidden'], true); + expect(json['is_progressive'], false); + }); + }); +} diff --git a/test/trophies/models/user_trophy_progression_test.dart b/test/trophies/models/user_trophy_progression_test.dart new file mode 100644 index 000000000..ccbb1d06a --- /dev/null +++ b/test/trophies/models/user_trophy_progression_test.dart @@ -0,0 +1,97 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2025 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'package:flutter_test/flutter_test.dart'; +import 'package:wger/models/trophies/trophy.dart'; +import 'package:wger/models/trophies/user_trophy_progression.dart'; + +void main() { + group('UserTrophyProgression model', () { + final trophyJson = { + 'id': 1, + 'uuid': '550e8400-e29b-41d4-a716-446655440000', + 'name': 'First Steps', + 'description': 'Awarded for the first workout', + 'image': 'https://example.org/trophy.png', + 'trophy_type': 'count', + 'is_hidden': false, + 'is_progressive': true, + }; + + final trophyProgressionJson = { + 'trophy': trophyJson, + 'is_earned': false, + 'earned_at': '2020-01-02T15:04:05Z', + 'progress': 42.5, + 'current_value': '12.5', + 'target_value': '100', + 'progress_display': '12.5/100', + }; + + test('fromJson creates valid UserTrophyProgression instance', () { + final utp = UserTrophyProgression.fromJson(trophyProgressionJson); + + expect(utp.trophy.id, 1); + expect(utp.trophy.uuid, '550e8400-e29b-41d4-a716-446655440000'); + expect(utp.isEarned, isFalse); + + final expectedEarnedAt = DateTime.parse('2020-01-02T15:04:05Z').toLocal(); + expect(utp.earnedAt, expectedEarnedAt); + + expect(utp.progress, 42.5); + expect(utp.currentValue, 12.5); + expect(utp.targetValue, 100); + expect(utp.progressDisplay, '12.5/100'); + }); + + test('toJson returns expected map', () { + final trophy = Trophy( + id: 2, + uuid: '00000000-0000-0000-0000-000000000000', + name: 'Progressor', + description: 'Progressive trophy', + image: 'https://example.org/prog.png', + type: TrophyType.time, + isHidden: true, + isProgressive: false, + ); + + final earnedAt = DateTime.parse('2020-01-02T15:04:05Z').toLocal(); + + final utp = UserTrophyProgression( + trophy: trophy, + isEarned: true, + earnedAt: earnedAt, + progress: 75, + currentValue: 75, + targetValue: 100, + progressDisplay: '75/100', + ); + + final json = utp.toJson(); + + expect(json['trophy'], same(trophy)); + expect(json['is_earned'], true); + expect(json['earned_at'], earnedAt.toIso8601String()); + expect(json['progress'], 75); + expect(json['current_value'], 75); + expect(json['target_value'], 100); + expect(json['progress_display'], '75/100'); + }); + }); +} diff --git a/test/trophies/provider/trophies_provider_test.dart b/test/trophies/provider/trophies_provider_test.dart new file mode 100644 index 000000000..456a806a5 --- /dev/null +++ b/test/trophies/provider/trophies_provider_test.dart @@ -0,0 +1,99 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2025 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; +import 'package:wger/providers/base_provider.dart'; +import 'package:wger/providers/trophies.dart'; + +import 'trophies_provider_test.mocks.dart'; + +const trophyJson = { + 'id': 1, + 'uuid': '550e8400-e29b-41d4-a716-446655440000', + 'name': 'First Steps', + 'description': 'Awarded for the first workout', + 'image': 'https://example.org/trophy.png', + 'trophy_type': 'count', + 'is_hidden': false, + 'is_progressive': true, +}; + +@GenerateMocks([WgerBaseProvider]) +void main() { + group('Trophy repository', () { + test('fetches list of trophies', () async { + // Arrange + final mockBase = MockWgerBaseProvider(); + when(mockBase.fetchPaginated(any)).thenAnswer((_) async => [trophyJson]); + when( + mockBase.makeUrl( + any, + id: anyNamed('id'), + objectMethod: anyNamed('objectMethod'), + query: anyNamed('query'), + ), + ).thenReturn(Uri.parse('https://example.org/trophies')); + final repository = TrophyRepository(mockBase); + + // Act + final result = await repository.fetchTrophies(); + + // Assert + expect(result, isA()); + expect(result, hasLength(1)); + final trophy = result.first; + expect(trophy.id, 1); + expect(trophy.name, 'First Steps'); + expect(trophy.type.toString(), contains('count')); + }); + + test('fetches list of user trophy progression', () async { + // Arrange + final progressionJson = { + 'trophy': trophyJson, + 'is_earned': true, + 'earned_at': '2020-01-02T15:04:05Z', + 'progress': 42.5, + 'current_value': '12.5', + 'target_value': '100', + 'progress_display': '12.5/100', + }; + + final mockBase = MockWgerBaseProvider(); + when(mockBase.fetch(any)).thenAnswer((_) async => [progressionJson]); + when(mockBase.makeUrl(any)).thenReturn(Uri.parse('https://example.org/user_progressions')); + final repository = TrophyRepository(mockBase); + + // Act + final result = await repository.fetchProgression(); + + // Assert + expect(result, isA()); + expect(result, hasLength(1)); + final p = result.first; + expect(p.isEarned, isTrue); + expect(p.progress, 42.5); + expect(p.currentValue, 12.5); + expect(p.progressDisplay, '12.5/100'); + + verify(mockBase.fetch(any)).called(1); + }); + }); +} diff --git a/test/trophies/provider/trophies_provider_test.mocks.dart b/test/trophies/provider/trophies_provider_test.mocks.dart new file mode 100644 index 000000000..79887735e --- /dev/null +++ b/test/trophies/provider/trophies_provider_test.mocks.dart @@ -0,0 +1,169 @@ +// Mocks generated by Mockito 5.4.6 from annotations +// in wger/test/trophies/provider/trophies_provider_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; + +import 'package:http/http.dart' as _i3; +import 'package:mockito/mockito.dart' as _i1; +import 'package:wger/providers/auth.dart' as _i2; +import 'package:wger/providers/base_provider.dart' as _i4; + +// 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 +// ignore_for_file: invalid_use_of_internal_member + +class _FakeAuthProvider_0 extends _i1.SmartFake implements _i2.AuthProvider { + _FakeAuthProvider_0(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +class _FakeClient_1 extends _i1.SmartFake implements _i3.Client { + _FakeClient_1(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +class _FakeUri_2 extends _i1.SmartFake implements Uri { + _FakeUri_2(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +class _FakeResponse_3 extends _i1.SmartFake implements _i3.Response { + _FakeResponse_3(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); +} + +/// A class which mocks [WgerBaseProvider]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { + MockWgerBaseProvider() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.AuthProvider get auth => + (super.noSuchMethod( + Invocation.getter(#auth), + returnValue: _FakeAuthProvider_0(this, Invocation.getter(#auth)), + ) + as _i2.AuthProvider); + + @override + _i3.Client get client => + (super.noSuchMethod( + Invocation.getter(#client), + returnValue: _FakeClient_1(this, Invocation.getter(#client)), + ) + as _i3.Client); + + @override + set auth(_i2.AuthProvider? value) => super.noSuchMethod( + Invocation.setter(#auth, value), + returnValueForMissingStub: null, + ); + + @override + set client(_i3.Client? value) => super.noSuchMethod( + Invocation.setter(#client, value), + returnValueForMissingStub: null, + ); + + @override + Map getDefaultHeaders({ + bool? includeAuth = false, + String? language, + }) => + (super.noSuchMethod( + Invocation.method(#getDefaultHeaders, [], { + #includeAuth: includeAuth, + #language: language, + }), + returnValue: {}, + ) + as Map); + + @override + Uri makeUrl( + String? path, { + int? id, + String? objectMethod, + Map? query, + }) => + (super.noSuchMethod( + Invocation.method( + #makeUrl, + [path], + {#id: id, #objectMethod: objectMethod, #query: query}, + ), + returnValue: _FakeUri_2( + this, + Invocation.method( + #makeUrl, + [path], + {#id: id, #objectMethod: objectMethod, #query: query}, + ), + ), + ) + as Uri); + + @override + _i5.Future fetch(Uri? uri, {String? language}) => + (super.noSuchMethod( + Invocation.method(#fetch, [uri], {#language: language}), + returnValue: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future> fetchPaginated(Uri? uri, {String? language}) => + (super.noSuchMethod( + Invocation.method(#fetchPaginated, [uri], {#language: language}), + returnValue: _i5.Future>.value([]), + ) + as _i5.Future>); + + @override + _i5.Future> post(Map? data, Uri? uri) => + (super.noSuchMethod( + Invocation.method(#post, [data, uri]), + returnValue: _i5.Future>.value( + {}, + ), + ) + as _i5.Future>); + + @override + _i5.Future> patch( + Map? data, + Uri? uri, + ) => + (super.noSuchMethod( + Invocation.method(#patch, [data, uri]), + returnValue: _i5.Future>.value( + {}, + ), + ) + as _i5.Future>); + + @override + _i5.Future<_i3.Response> deleteRequest(String? url, int? id) => + (super.noSuchMethod( + Invocation.method(#deleteRequest, [url, id]), + returnValue: _i5.Future<_i3.Response>.value( + _FakeResponse_3( + this, + Invocation.method(#deleteRequest, [url, id]), + ), + ), + ) + as _i5.Future<_i3.Response>); +} diff --git a/test/trophies/widgets/dashboard_trophies_widget_test.dart b/test/trophies/widgets/dashboard_trophies_widget_test.dart new file mode 100644 index 000000000..687f13a06 --- /dev/null +++ b/test/trophies/widgets/dashboard_trophies_widget_test.dart @@ -0,0 +1,55 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2026 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * 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_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:network_image_mock/network_image_mock.dart'; +import 'package:wger/providers/trophies.dart'; +import 'package:wger/widgets/dashboard/widgets/trophies.dart'; + +import '../../test_data/trophies.dart'; + +void main() { + testWidgets('DashboardTrophiesWidget shows trophies', (WidgetTester tester) async { + // Act + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + ProviderScope( + overrides: [ + trophyStateProvider.overrideWithValue( + TrophyState( + userTrophies: getUserTrophies(), + trophies: getTestTrophies(), + ), + ), + ], + child: const MaterialApp( + home: Scaffold( + body: DashboardTrophiesWidget(), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('New Year, New Me'), findsOneWidget); + }); + }); +} diff --git a/test/trophies/widgets/trophies_overview_test.dart b/test/trophies/widgets/trophies_overview_test.dart new file mode 100644 index 000000000..0bc12cd5e --- /dev/null +++ b/test/trophies/widgets/trophies_overview_test.dart @@ -0,0 +1,58 @@ +/* + * This file is part of wger Workout Manager . + * Copyright (c) 2026 wger Team + * + * wger Workout Manager is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * 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_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:network_image_mock/network_image_mock.dart'; +import 'package:wger/providers/trophies.dart'; +import 'package:wger/widgets/trophies/trophies_overview.dart'; + +import '../../test_data/trophies.dart'; + +void main() { + testWidgets('TrophiesOverview shows trophies', (WidgetTester tester) async { + // Act + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + ProviderScope( + overrides: [ + trophyStateProvider.overrideWithValue( + TrophyState( + trophyProgression: getUserTrophyProgression(), + userTrophies: getUserTrophies(), + trophies: getTestTrophies(), + ), + ), + ], + child: const MaterialApp( + home: Scaffold(body: TrophiesOverview()), + ), + ), + ); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('New Year, New Me'), findsOneWidget); + expect(find.text('Work out on January 1st'), findsOneWidget); + + expect(find.text('Unstoppable'), findsOneWidget); + expect(find.text('Maintain a 30-day workout streak'), findsOneWidget); + }); + }); +} diff --git a/test/user/provider_test.mocks.dart b/test/user/provider_test.mocks.dart index b8fce543f..6ff9bc854 100644 --- a/test/user/provider_test.mocks.dart +++ b/test/user/provider_test.mocks.dart @@ -1,21 +1,3 @@ -/* - * This file is part of wger Workout Manager . - * Copyright (c) 2025 wger Team - * - * wger Workout Manager is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - // Mocks generated by Mockito 5.4.6 from annotations // in wger/test/user/provider_test.dart. // Do not manually edit this file. @@ -96,10 +78,14 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { ); @override - Map getDefaultHeaders({bool? includeAuth = false}) => + Map getDefaultHeaders({ + bool? includeAuth = false, + String? language, + }) => (super.noSuchMethod( Invocation.method(#getDefaultHeaders, [], { #includeAuth: includeAuth, + #language: language, }), returnValue: {}, ) @@ -130,17 +116,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method(#fetch, [uri], {#language: language}), returnValue: _i5.Future.value(), ) as _i5.Future); @override - _i5.Future> fetchPaginated(Uri? uri) => + _i5.Future> fetchPaginated(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetchPaginated, [uri]), + Invocation.method(#fetchPaginated, [uri], {#language: language}), returnValue: _i5.Future>.value([]), ) as _i5.Future>); diff --git a/test/weight/weight_provider_test.mocks.dart b/test/weight/weight_provider_test.mocks.dart index 4cd18bb56..bf00ffb14 100644 --- a/test/weight/weight_provider_test.mocks.dart +++ b/test/weight/weight_provider_test.mocks.dart @@ -78,10 +78,14 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { ); @override - Map getDefaultHeaders({bool? includeAuth = false}) => + Map getDefaultHeaders({ + bool? includeAuth = false, + String? language, + }) => (super.noSuchMethod( Invocation.method(#getDefaultHeaders, [], { #includeAuth: includeAuth, + #language: language, }), returnValue: {}, ) @@ -112,17 +116,17 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri) => + _i5.Future fetch(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetch, [uri]), + Invocation.method(#fetch, [uri], {#language: language}), returnValue: _i5.Future.value(), ) as _i5.Future); @override - _i5.Future> fetchPaginated(Uri? uri) => + _i5.Future> fetchPaginated(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetchPaginated, [uri]), + Invocation.method(#fetchPaginated, [uri], {#language: language}), returnValue: _i5.Future>.value([]), ) as _i5.Future>); diff --git a/test/widgets/routines/gym_mode/log_page_test.dart b/test/widgets/routines/gym_mode/log_page_test.dart index 45526e7f4..e4b0db2af 100644 --- a/test/widgets/routines/gym_mode/log_page_test.dart +++ b/test/widgets/routines/gym_mode/log_page_test.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2025 - 2025 wger Team + * Copyright (c) 2025 - 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -33,13 +33,14 @@ import 'package:wger/models/workouts/slot_data.dart'; import 'package:wger/providers/exercises.dart'; import 'package:wger/providers/gym_state.dart'; import 'package:wger/providers/routines.dart'; +import 'package:wger/providers/trophies.dart'; import 'package:wger/widgets/routines/gym_mode/log_page.dart'; import '../../../../test_data/exercises.dart'; import '../../../../test_data/routines.dart' as testdata; import 'log_page_test.mocks.dart'; -@GenerateMocks([ExercisesProvider, RoutinesProvider]) +@GenerateMocks([ExercisesProvider, RoutinesProvider, TrophyRepository]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -50,7 +51,17 @@ void main() { setUp(() { SharedPreferencesAsyncPlatform.instance = InMemorySharedPreferencesAsync.empty(); testExercises = getTestExercises(); - container = ProviderContainer.test(); + final mockTrophyRepo = MockTrophyRepository(); + when( + mockTrophyRepo.fetchUserTrophies( + language: anyNamed('language'), + filterQuery: anyNamed('filterQuery'), + ), + ).thenAnswer((_) async => []); + + container = ProviderContainer.test( + overrides: [trophyRepositoryProvider.overrideWithValue(mockTrophyRepo)], + ); }); Future pumpLogPage(WidgetTester tester, {RoutinesProvider? routinesProvider}) async { @@ -189,15 +200,15 @@ void main() { testWidgets('save button calls addLog on RoutinesProvider', (tester) async { // Arrange - final notifier = container.read(gymStateProvider.notifier); + final gymNotifier = container.read(gymStateProvider.notifier); final routine = testdata.getTestRoutine(); - notifier.state = notifier.state.copyWith( + gymNotifier.state = gymNotifier.state.copyWith( dayId: routine.days.first.id, routine: routine, iteration: 1, ); - notifier.calculatePages(); - notifier.state = notifier.state.copyWith(currentPage: 2); + gymNotifier.calculatePages(); + gymNotifier.state = gymNotifier.state.copyWith(currentPage: 2); final mockRoutines = MockRoutinesProvider(); // Act @@ -229,10 +240,10 @@ void main() { expect(capturedLog!.repetitions, equals(7)); expect(capturedLog!.weight, equals(77)); - final currentSlotPage = notifier.state.getSlotEntryPageByIndex()!; + final currentSlotPage = gymNotifier.state.getSlotEntryPageByIndex()!; expect(capturedLog!.slotEntryId, equals(currentSlotPage.setConfigData!.slotEntryId)); - expect(capturedLog!.routineId, equals(notifier.state.routine.id)); - expect(capturedLog!.iteration, equals(notifier.state.iteration)); + expect(capturedLog!.routineId, equals(gymNotifier.state.routine.id)); + expect(capturedLog!.iteration, equals(gymNotifier.state.iteration)); }); }); } diff --git a/test/widgets/routines/gym_mode/log_page_test.mocks.dart b/test/widgets/routines/gym_mode/log_page_test.mocks.dart index 500c61d8d..359e485a6 100644 --- a/test/widgets/routines/gym_mode/log_page_test.mocks.dart +++ b/test/widgets/routines/gym_mode/log_page_test.mocks.dart @@ -1,21 +1,3 @@ -/* - * This file is part of wger Workout Manager . - * Copyright (c) 2025 wger Team - * - * wger Workout Manager is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - // Mocks generated by Mockito 5.4.6 from annotations // in wger/test/widgets/routines/gym_mode/log_page_test.dart. // Do not manually edit this file. @@ -32,6 +14,9 @@ import 'package:wger/models/exercises/equipment.dart' as _i6; import 'package:wger/models/exercises/exercise.dart' as _i4; import 'package:wger/models/exercises/language.dart' as _i8; import 'package:wger/models/exercises/muscle.dart' as _i7; +import 'package:wger/models/trophies/trophy.dart' as _i25; +import 'package:wger/models/trophies/user_trophy.dart' as _i26; +import 'package:wger/models/trophies/user_trophy_progression.dart' as _i27; import 'package:wger/models/workouts/base_config.dart' as _i15; import 'package:wger/models/workouts/day.dart' as _i12; import 'package:wger/models/workouts/day_data.dart' as _i22; @@ -45,6 +30,7 @@ import 'package:wger/models/workouts/weight_unit.dart' as _i9; import 'package:wger/providers/base_provider.dart' as _i2; import 'package:wger/providers/exercises.dart' as _i18; import 'package:wger/providers/routines.dart' as _i21; +import 'package:wger/providers/trophies.dart' as _i24; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -1067,3 +1053,107 @@ class MockRoutinesProvider extends _i1.Mock implements _i21.RoutinesProvider { returnValueForMissingStub: null, ); } + +/// A class which mocks [TrophyRepository]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTrophyRepository extends _i1.Mock implements _i24.TrophyRepository { + MockTrophyRepository() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.WgerBaseProvider get base => + (super.noSuchMethod( + Invocation.getter(#base), + returnValue: _FakeWgerBaseProvider_0( + this, + Invocation.getter(#base), + ), + ) + as _i2.WgerBaseProvider); + + @override + String get trophiesPath => + (super.noSuchMethod( + Invocation.getter(#trophiesPath), + returnValue: _i23.dummyValue( + this, + Invocation.getter(#trophiesPath), + ), + ) + as String); + + @override + String get userTrophiesPath => + (super.noSuchMethod( + Invocation.getter(#userTrophiesPath), + returnValue: _i23.dummyValue( + this, + Invocation.getter(#userTrophiesPath), + ), + ) + as String); + + @override + String get userTrophyProgressionPath => + (super.noSuchMethod( + Invocation.getter(#userTrophyProgressionPath), + returnValue: _i23.dummyValue( + this, + Invocation.getter(#userTrophyProgressionPath), + ), + ) + as String); + + @override + _i19.Future> fetchTrophies({String? language}) => + (super.noSuchMethod( + Invocation.method(#fetchTrophies, [], {#language: language}), + returnValue: _i19.Future>.value(<_i25.Trophy>[]), + ) + as _i19.Future>); + + @override + _i19.Future> fetchUserTrophies({ + Map? filterQuery, + String? language, + }) => + (super.noSuchMethod( + Invocation.method(#fetchUserTrophies, [], { + #filterQuery: filterQuery, + #language: language, + }), + returnValue: _i19.Future>.value( + <_i26.UserTrophy>[], + ), + ) + as _i19.Future>); + + @override + _i19.Future> fetchProgression({ + Map? filterQuery, + String? language, + }) => + (super.noSuchMethod( + Invocation.method(#fetchProgression, [], { + #filterQuery: filterQuery, + #language: language, + }), + returnValue: _i19.Future>.value( + <_i27.UserTrophyProgression>[], + ), + ) + as _i19.Future>); + + @override + List<_i25.Trophy> filterByType( + List<_i25.Trophy>? list, + _i25.TrophyType? type, + ) => + (super.noSuchMethod( + Invocation.method(#filterByType, [list, type]), + returnValue: <_i25.Trophy>[], + ) + as List<_i25.Trophy>); +}