From 5d39ae5088e6a7f12a042bebd01a1baedcd0b02a Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Sat, 20 Dec 2025 00:31:18 +0100 Subject: [PATCH] Start adding support for trophies --- lib/main.dart | 101 ++++++---- lib/models/trophies/trophy.dart | 66 +++++++ lib/models/trophies/trophy.g.dart | 72 +++++++ .../trophies/user_trophy_progression.dart | 64 ++++++ .../trophies/user_trophy_progression.g.dart | 63 ++++++ lib/models/workouts/session.g.dart | 22 ++- lib/providers/trophies.dart | 51 +++++ lib/providers/trophies.g.dart | 103 ++++++++++ lib/providers/wger_base_riverpod.dart | 31 +++ test/trophies/models/trophy_test.dart | 72 +++++++ .../models/user_trophy_progression_test.dart | 97 ++++++++++ test/trophies/trophies_provider_test.dart | 117 +++++++++++ .../trophies_provider_test.mocks.dart | 183 ++++++++++++++++++ 13 files changed, 998 insertions(+), 44 deletions(-) create mode 100644 lib/models/trophies/trophy.dart create mode 100644 lib/models/trophies/trophy.g.dart create mode 100644 lib/models/trophies/user_trophy_progression.dart create mode 100644 lib/models/trophies/user_trophy_progression.g.dart create mode 100644 lib/providers/trophies.dart create mode 100644 lib/providers/trophies.g.dart create mode 100644 lib/providers/wger_base_riverpod.dart create mode 100644 test/trophies/models/trophy_test.dart create mode 100644 test/trophies/models/user_trophy_progression_test.dart create mode 100644 test/trophies/trophies_provider_test.dart create mode 100644 test/trophies/trophies_provider_test.mocks.dart diff --git a/lib/main.dart b/lib/main.dart index 47761881..b36666c8 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'; @@ -129,7 +130,7 @@ void main() async { }; // Application - runApp(const riverpod.ProviderScope(child: MainApp())); + runApp(const MainApp()); } class MainApp extends StatelessWidget { @@ -217,46 +218,60 @@ 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(), + }, + 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 00000000..d58f5512 --- /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 00000000..aba73ac1 --- /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_progression.dart b/lib/models/trophies/user_trophy_progression.dart new file mode 100644 index 00000000..e84104a9 --- /dev/null +++ b/lib/models/trophies/user_trophy_progression.dart @@ -0,0 +1,64 @@ +/* + * 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: utcIso8601ToLocalDate) + final DateTime earnedAt; + + /// Progress towards earning the trophy (0-100%) + @JsonKey(required: true) + final num progress; + + @JsonKey(required: true, name: 'current_value', fromJson: stringToNumNull) + num? currentValue; + + @JsonKey(required: true, name: 'target_value', fromJson: stringToNumNull) + num? targetValue; + + @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 00000000..ac23b913 --- /dev/null +++ b/lib/models/trophies/user_trophy_progression.g.dart @@ -0,0 +1,63 @@ +/* + * 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 '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: utcIso8601ToLocalDate(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 ac78029d..9e3a52f7 100644 --- a/lib/models/workouts/session.g.dart +++ b/lib/models/workouts/session.g.dart @@ -1,3 +1,21 @@ +/* + * 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 'session.dart'; @@ -23,7 +41,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/providers/trophies.dart b/lib/providers/trophies.dart new file mode 100644 index 00000000..77a3a1bc --- /dev/null +++ b/lib/providers/trophies.dart @@ -0,0 +1,51 @@ +/* + * 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: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_progression.dart'; +import 'package:wger/providers/wger_base_riverpod.dart'; + +part 'trophies.g.dart'; + +@riverpod +Future> trophies(Ref ref) async { + const trophiesPath = 'trophy'; + + final baseProvider = ref.read(wgerBaseProvider); + + final trophyData = await baseProvider.fetchPaginated( + baseProvider.makeUrl(trophiesPath, query: {'limit': API_MAX_PAGE_SIZE}), + ); + + return trophyData.map((e) => Trophy.fromJson(e)).toList(); +} + +@riverpod +Future> trophyProgression(Ref ref) async { + const userTrophyProgressionPath = 'trophy/progress'; + + final baseProvider = ref.read(wgerBaseProvider); + + final trophyData = await baseProvider.fetchPaginated( + baseProvider.makeUrl(userTrophyProgressionPath, query: {'limit': API_MAX_PAGE_SIZE}), + ); + + return trophyData.map((e) => UserTrophyProgression.fromJson(e)).toList(); +} diff --git a/lib/providers/trophies.g.dart b/lib/providers/trophies.g.dart new file mode 100644 index 00000000..b9b825d5 --- /dev/null +++ b/lib/providers/trophies.g.dart @@ -0,0 +1,103 @@ +/* + * 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 'trophies.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning + +@ProviderFor(trophies) +const trophiesProvider = TrophiesProvider._(); + +final class TrophiesProvider + extends $FunctionalProvider>, List, FutureOr>> + with $FutureModifier>, $FutureProvider> { + const TrophiesProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'trophiesProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$trophiesHash(); + + @$internal + @override + $FutureProviderElement> $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr> create(Ref ref) { + return trophies(ref); + } +} + +String _$trophiesHash() => r'44dd5e9a820f4e37599daac2a49a9358386758a8'; + +@ProviderFor(trophyProgression) +const trophyProgressionProvider = TrophyProgressionProvider._(); + +final class TrophyProgressionProvider + extends + $FunctionalProvider< + AsyncValue>, + List, + FutureOr> + > + with + $FutureModifier>, + $FutureProvider> { + const TrophyProgressionProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'trophyProgressionProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$trophyProgressionHash(); + + @$internal + @override + $FutureProviderElement> $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr> create(Ref ref) { + return trophyProgression(ref); + } +} + +String _$trophyProgressionHash() => r'444caf04f3d0a7845e840d452e4b4a822b59df9b'; diff --git a/lib/providers/wger_base_riverpod.dart b/lib/providers/wger_base_riverpod.dart new file mode 100644 index 00000000..0c73cbd2 --- /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/test/trophies/models/trophy_test.dart b/test/trophies/models/trophy_test.dart new file mode 100644 index 00000000..3285b589 --- /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 00000000..ccbb1d06 --- /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/trophies_provider_test.dart b/test/trophies/trophies_provider_test.dart new file mode 100644 index 00000000..279afcc2 --- /dev/null +++ b/test/trophies/trophies_provider_test.dart @@ -0,0 +1,117 @@ +/* + * 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_riverpod/flutter_riverpod.dart'; +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 'package:wger/providers/wger_base_riverpod.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('Trophies providers', () { + test('trophies provider returns list of Trophy models', () 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 container = ProviderContainer.test( + overrides: [ + wgerBaseProvider.overrideWithValue(mockBase), + ], + ); + + // Act + final result = await container.read(trophiesProvider.future); + + // 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('trophyProgression provider returns list of UserTrophyProgression models', () 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.fetchPaginated(any)).thenAnswer((_) async => [progressionJson]); + when( + mockBase.makeUrl( + any, + id: anyNamed('id'), + objectMethod: anyNamed('objectMethod'), + query: anyNamed('query'), + ), + ).thenReturn(Uri.parse('https://example.org/user_progressions')); + final container = ProviderContainer.test( + overrides: [ + wgerBaseProvider.overrideWithValue(mockBase), + ], + ); + + // Act + final result = await container.read(trophyProgressionProvider.future); + + // 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.fetchPaginated(any)).called(1); + }); + }); +} diff --git a/test/trophies/trophies_provider_test.mocks.dart b/test/trophies/trophies_provider_test.mocks.dart new file mode 100644 index 00000000..dec2b616 --- /dev/null +++ b/test/trophies/trophies_provider_test.mocks.dart @@ -0,0 +1,183 @@ +/* + * 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/trophies/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}) => + (super.noSuchMethod( + Invocation.method(#getDefaultHeaders, [], { + #includeAuth: includeAuth, + }), + 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) => + (super.noSuchMethod( + Invocation.method(#fetch, [uri]), + returnValue: _i5.Future.value(), + ) + as _i5.Future); + + @override + _i5.Future> fetchPaginated(Uri? uri) => + (super.noSuchMethod( + Invocation.method(#fetchPaginated, [uri]), + 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>); +}