From 5d39ae5088e6a7f12a042bebd01a1baedcd0b02a Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Sat, 20 Dec 2025 00:31:18 +0100 Subject: [PATCH 01/11] 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>); +} From b9fd061d3359dee34e7d7a1b08532e97173dbd12 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Sat, 20 Dec 2025 02:06:08 +0100 Subject: [PATCH 02/11] Add dashboard widget and rework the provider --- lib/models/trophies/user_trophy.dart | 57 ++++++++++++++ lib/models/trophies/user_trophy.g.dart | 53 +++++++++++++ lib/providers/trophies.dart | 73 ++++++++++++----- lib/providers/trophies.g.dart | 86 ++++++++++++--------- lib/screens/dashboard.dart | 5 +- lib/widgets/dashboard/widgets/trophies.dart | 65 ++++++++++++++++ test/trophies/trophies_provider_test.dart | 25 ++---- 7 files changed, 290 insertions(+), 74 deletions(-) create mode 100644 lib/models/trophies/user_trophy.dart create mode 100644 lib/models/trophies/user_trophy.g.dart create mode 100644 lib/widgets/dashboard/widgets/trophies.dart diff --git a/lib/models/trophies/user_trophy.dart b/lib/models/trophies/user_trophy.dart new file mode 100644 index 00000000..81cb51e3 --- /dev/null +++ b/lib/models/trophies/user_trophy.dart @@ -0,0 +1,57 @@ +/* + * 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'; + +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; + + UserTrophy({ + required this.id, + required this.trophy, + required this.earnedAt, + required this.progress, + required this.isNotified, + }); + + // 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 00000000..62d85ac3 --- /dev/null +++ b/lib/models/trophies/user_trophy.g.dart @@ -0,0 +1,53 @@ +/* + * 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.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +UserTrophy _$UserTrophyFromJson(Map json) { + $checkKeys( + json, + requiredKeys: const [ + 'id', + 'trophy', + 'earned_at', + 'progress', + 'is_notified', + ], + ); + 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, + ); +} + +Map _$UserTrophyToJson(UserTrophy instance) => { + 'id': instance.id, + 'trophy': instance.trophy, + 'earned_at': instance.earnedAt.toIso8601String(), + 'progress': instance.progress, + 'is_notified': instance.isNotified, +}; diff --git a/lib/providers/trophies.dart b/lib/providers/trophies.dart index 77a3a1bc..13ee1df7 100644 --- a/lib/providers/trophies.dart +++ b/lib/providers/trophies.dart @@ -19,33 +19,70 @@ 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'; -@riverpod -Future> trophies(Ref ref) async { - const trophiesPath = 'trophy'; +class TrophyRepository { + final WgerBaseProvider base; + final trophiesPath = 'trophy'; + final userTrophiesPath = 'user-trophy'; + final userTrophyProgressionPath = 'trophy/progress'; - final baseProvider = ref.read(wgerBaseProvider); + TrophyRepository(this.base); - final trophyData = await baseProvider.fetchPaginated( - baseProvider.makeUrl(trophiesPath, query: {'limit': API_MAX_PAGE_SIZE}), - ); + Future> fetchTrophies() async { + final url = base.makeUrl(trophiesPath, query: {'limit': API_MAX_PAGE_SIZE}); + final trophyData = await base.fetchPaginated(url); + return trophyData.map((e) => Trophy.fromJson(e)).toList(); + } - return trophyData.map((e) => Trophy.fromJson(e)).toList(); + Future> fetchUserTrophies() async { + final url = base.makeUrl(userTrophiesPath, query: {'limit': API_MAX_PAGE_SIZE}); + final trophyData = await base.fetchPaginated(url); + return trophyData.map((e) => UserTrophy.fromJson(e)).toList(); + } + + Future> fetchProgression() async { + final url = base.makeUrl(userTrophyProgressionPath, query: {'limit': API_MAX_PAGE_SIZE}); + final data = await base.fetchPaginated(url); + return data.map((e) => UserTrophyProgression.fromJson(e)).toList(); + } + + List filterByType(List list, TrophyType type) => + list.where((t) => t.type == type).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(); +TrophyRepository trophyRepository(Ref ref) { + final base = ref.read(wgerBaseProvider); + return TrophyRepository(base); +} + +@Riverpod(keepAlive: true) +final class TrophyStateNotifier extends _$TrophyStateNotifier { + @override + void build() {} + + /// Fetch all available trophies + Future> fetchTrophies() async { + final repo = ref.read(trophyRepositoryProvider); + return repo.fetchTrophies(); + } + + /// Fetch trophies awarded to the user + Future> fetchUserTrophies() async { + final repo = ref.read(trophyRepositoryProvider); + return repo.fetchUserTrophies(); + } + + /// Fetch trophy progression for the user + Future> fetchTrophyProgression() async { + final repo = ref.read(trophyRepositoryProvider); + return repo.fetchProgression(); + } } diff --git a/lib/providers/trophies.g.dart b/lib/providers/trophies.g.dart index b9b825d5..04df81d6 100644 --- a/lib/providers/trophies.g.dart +++ b/lib/providers/trophies.g.dart @@ -27,77 +27,89 @@ part of 'trophies.dart'; // GENERATED CODE - DO NOT MODIFY BY HAND // ignore_for_file: type=lint, type=warning -@ProviderFor(trophies) -const trophiesProvider = TrophiesProvider._(); +@ProviderFor(trophyRepository) +const trophyRepositoryProvider = TrophyRepositoryProvider._(); -final class TrophiesProvider - extends $FunctionalProvider>, List, FutureOr>> - with $FutureModifier>, $FutureProvider> { - const TrophiesProvider._() +final class TrophyRepositoryProvider + extends $FunctionalProvider + with $Provider { + const TrophyRepositoryProvider._() : super( from: null, argument: null, retry: null, - name: r'trophiesProvider', + name: r'trophyRepositoryProvider', isAutoDispose: true, dependencies: null, $allTransitiveDependencies: null, ); @override - String debugGetCreateSourceHash() => _$trophiesHash(); + String debugGetCreateSourceHash() => _$trophyRepositoryHash(); @$internal @override - $FutureProviderElement> $createElement( - $ProviderPointer pointer, - ) => $FutureProviderElement(pointer); + $ProviderElement $createElement($ProviderPointer pointer) => + $ProviderElement(pointer); @override - FutureOr> create(Ref ref) { - return trophies(ref); + TrophyRepository create(Ref ref) { + return trophyRepository(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(TrophyRepository value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); } } -String _$trophiesHash() => r'44dd5e9a820f4e37599daac2a49a9358386758a8'; +String _$trophyRepositoryHash() => r'0699f0c0f7f324f3ba9b21420d9845a3e3096b61'; -@ProviderFor(trophyProgression) -const trophyProgressionProvider = TrophyProgressionProvider._(); +@ProviderFor(TrophyStateNotifier) +const trophyStateProvider = TrophyStateNotifierProvider._(); -final class TrophyProgressionProvider - extends - $FunctionalProvider< - AsyncValue>, - List, - FutureOr> - > - with - $FutureModifier>, - $FutureProvider> { - const TrophyProgressionProvider._() +final class TrophyStateNotifierProvider extends $NotifierProvider { + const TrophyStateNotifierProvider._() : super( from: null, argument: null, retry: null, - name: r'trophyProgressionProvider', - isAutoDispose: true, + name: r'trophyStateProvider', + isAutoDispose: false, dependencies: null, $allTransitiveDependencies: null, ); @override - String debugGetCreateSourceHash() => _$trophyProgressionHash(); + String debugGetCreateSourceHash() => _$trophyStateNotifierHash(); @$internal @override - $FutureProviderElement> $createElement( - $ProviderPointer pointer, - ) => $FutureProviderElement(pointer); + TrophyStateNotifier create() => TrophyStateNotifier(); - @override - FutureOr> create(Ref ref) { - return trophyProgression(ref); + /// {@macro riverpod.override_with_value} + Override overrideWithValue(void value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); } } -String _$trophyProgressionHash() => r'444caf04f3d0a7845e840d452e4b4a822b59df9b'; +String _$trophyStateNotifierHash() => r'e5c8f2a9477b8f7e5efe4e9ba23765f951627a9f'; + +abstract class _$TrophyStateNotifier extends $Notifier { + void build(); + @$mustCallSuper + @override + void runBuild() { + build(); + final ref = this.ref as $Ref; + final element = + ref.element as $ClassProviderElement, void, Object?, Object?>; + element.handleValue(ref, null); + } +} diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index 3be5a4b4..0a714308 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/widgets/dashboard/widgets/trophies.dart b/lib/widgets/dashboard/widgets/trophies.dart new file mode 100644 index 00000000..9d2087c7 --- /dev/null +++ b/lib/widgets/dashboard/widgets/trophies.dart @@ -0,0 +1,65 @@ +/* + * This file is part of wger Workout Manager . + * 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 + * 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/providers/trophies.dart'; +import 'package:wger/widgets/core/progress_indicator.dart'; + +class DashboardTrophiesWidget extends ConsumerWidget { + const DashboardTrophiesWidget(); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final provider = ref.watch(trophyStateProvider.notifier); + + return FutureBuilder( + future: provider.fetchUserTrophies(), + builder: (context, asyncSnapshot) { + if (asyncSnapshot.connectionState != ConnectionState.done) { + return const Card( + child: BoxedProgressIndicator(), + ); + } + + return Card( + child: Column( + children: [ + ListTile( + title: Text( + 'Trophies', + style: Theme.of(context).textTheme.headlineSmall, + ), + ), + + ...(asyncSnapshot.data ?? []).map( + (userTrophy) => ListTile( + leading: CircleAvatar( + backgroundImage: NetworkImage(userTrophy.trophy.image), + ), + title: Text(userTrophy.trophy.name), + subtitle: Text(userTrophy.trophy.description, overflow: TextOverflow.ellipsis), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/test/trophies/trophies_provider_test.dart b/test/trophies/trophies_provider_test.dart index 279afcc2..5f159b94 100644 --- a/test/trophies/trophies_provider_test.dart +++ b/test/trophies/trophies_provider_test.dart @@ -16,13 +16,11 @@ * 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'; @@ -39,8 +37,8 @@ const trophyJson = { @GenerateMocks([WgerBaseProvider]) void main() { - group('Trophies providers', () { - test('trophies provider returns list of Trophy models', () async { + group('Trophy repository', () { + test('fetches list of trophies', () async { // Arrange final mockBase = MockWgerBaseProvider(); when(mockBase.fetchPaginated(any)).thenAnswer((_) async => [trophyJson]); @@ -52,15 +50,10 @@ void main() { query: anyNamed('query'), ), ).thenReturn(Uri.parse('https://example.org/trophies')); - - final container = ProviderContainer.test( - overrides: [ - wgerBaseProvider.overrideWithValue(mockBase), - ], - ); + final repository = TrophyRepository(mockBase); // Act - final result = await container.read(trophiesProvider.future); + final result = await repository.fetchTrophies(); // Assert expect(result, isA()); @@ -71,7 +64,7 @@ void main() { expect(trophy.type.toString(), contains('count')); }); - test('trophyProgression provider returns list of UserTrophyProgression models', () async { + test('fetches list of user trophy progression', () async { // Arrange final progressionJson = { 'trophy': trophyJson, @@ -93,14 +86,10 @@ void main() { query: anyNamed('query'), ), ).thenReturn(Uri.parse('https://example.org/user_progressions')); - final container = ProviderContainer.test( - overrides: [ - wgerBaseProvider.overrideWithValue(mockBase), - ], - ); + final repository = TrophyRepository(mockBase); // Act - final result = await container.read(trophyProgressionProvider.future); + final result = await repository.fetchProgression(); // Assert expect(result, isA()); From 9e4897f51605c518e066f2e504dc874608006adf Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Sat, 20 Dec 2025 17:55:22 +0100 Subject: [PATCH 03/11] Add smoke test for DashboardTrophiesWidget --- lib/widgets/dashboard/widgets/trophies.dart | 109 +++++++++++++--- test/test_data/trophies.dart | 76 +++++++++++ .../trophies_provider_test.dart | 0 .../trophies_provider_test.mocks.dart | 2 +- .../dashboard_trophies_widget_test.dart | 59 +++++++++ .../dashboard_trophies_widget_test.mocks.dart | 123 ++++++++++++++++++ .../gym_mode/log_page_test.mocks.dart | 18 --- 7 files changed, 349 insertions(+), 38 deletions(-) create mode 100644 test/test_data/trophies.dart rename test/trophies/{ => provider}/trophies_provider_test.dart (100%) rename test/trophies/{ => provider}/trophies_provider_test.mocks.dart (98%) create mode 100644 test/trophies/widgets/dashboard_trophies_widget_test.dart create mode 100644 test/trophies/widgets/dashboard_trophies_widget_test.mocks.dart diff --git a/lib/widgets/dashboard/widgets/trophies.dart b/lib/widgets/dashboard/widgets/trophies.dart index 9d2087c7..c17df4ae 100644 --- a/lib/widgets/dashboard/widgets/trophies.dart +++ b/lib/widgets/dashboard/widgets/trophies.dart @@ -18,6 +18,7 @@ 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/widgets/core/progress_indicator.dart'; @@ -32,30 +33,46 @@ class DashboardTrophiesWidget extends ConsumerWidget { future: provider.fetchUserTrophies(), builder: (context, asyncSnapshot) { if (asyncSnapshot.connectionState != ConnectionState.done) { - return const Card( - child: BoxedProgressIndicator(), - ); + return const Card(child: BoxedProgressIndicator()); } - return Card( - child: Column( - children: [ - ListTile( - title: Text( - 'Trophies', - style: Theme.of(context).textTheme.headlineSmall, - ), - ), + final userTrophies = asyncSnapshot.data ?? []; - ...(asyncSnapshot.data ?? []).map( - (userTrophy) => ListTile( - leading: CircleAvatar( - backgroundImage: NetworkImage(userTrophy.trophy.image), + return Card( + color: Colors.transparent, + shadowColor: Colors.transparent, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // ListTile( + // title: Text( + // 'Trophies', + // style: Theme.of(context).textTheme.headlineSmall, + // ), + // ), + if (userTrophies.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: userTrophies.length, + separatorBuilder: (context, index) => const SizedBox(width: 12), + itemBuilder: (context, index) { + final userTrophy = userTrophies[index]; + + return SizedBox( + width: 220, + child: TrophyCard(trophy: userTrophy.trophy), + ); + }, ), - title: Text(userTrophy.trophy.name), - subtitle: Text(userTrophy.trophy.description, overflow: TextOverflow.ellipsis), ), - ), ], ), ); @@ -63,3 +80,57 @@ class DashboardTrophiesWidget extends ConsumerWidget { ); } } + +class TrophyCard extends StatelessWidget { + const TrophyCard({ + super.key, + required this.trophy, + }); + + final Trophy trophy; + + @override + Widget build(BuildContext context) { + return Card.filled( + 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/test/test_data/trophies.dart b/test/test_data/trophies.dart new file mode 100644 index 00000000..698e26a5 --- /dev/null +++ b/test/test_data/trophies.dart @@ -0,0 +1,76 @@ +/* + * 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, + ), + ]; +} + +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/trophies_provider_test.dart b/test/trophies/provider/trophies_provider_test.dart similarity index 100% rename from test/trophies/trophies_provider_test.dart rename to test/trophies/provider/trophies_provider_test.dart diff --git a/test/trophies/trophies_provider_test.mocks.dart b/test/trophies/provider/trophies_provider_test.mocks.dart similarity index 98% rename from test/trophies/trophies_provider_test.mocks.dart rename to test/trophies/provider/trophies_provider_test.mocks.dart index dec2b616..bebf3d1d 100644 --- a/test/trophies/trophies_provider_test.mocks.dart +++ b/test/trophies/provider/trophies_provider_test.mocks.dart @@ -17,7 +17,7 @@ */ // Mocks generated by Mockito 5.4.6 from annotations -// in wger/test/trophies/trophies_provider_test.dart. +// in wger/test/trophies/provider/trophies_provider_test.dart. // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes 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 00000000..7c6a92b4 --- /dev/null +++ b/test/trophies/widgets/dashboard_trophies_widget_test.dart @@ -0,0 +1,59 @@ +/* + * 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:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.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'; +import 'dashboard_trophies_widget_test.mocks.dart'; + +@GenerateMocks([TrophyRepository]) +void main() { + testWidgets('DashboardTrophiesWidget shows trophies', (WidgetTester tester) async { + // Arrange + final mockRepository = MockTrophyRepository(); + when(mockRepository.fetchUserTrophies()).thenAnswer((_) async => getUserTrophies()); + + // Act + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + ProviderScope( + overrides: [ + trophyRepositoryProvider.overrideWithValue(mockRepository), + ], + child: const MaterialApp( + home: Scaffold( + body: DashboardTrophiesWidget(), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + // Assert + // expect(find.text('Trophies'), findsOneWidget); + expect(find.text('New Year, New Me'), findsOneWidget); + }); + }); +} diff --git a/test/trophies/widgets/dashboard_trophies_widget_test.mocks.dart b/test/trophies/widgets/dashboard_trophies_widget_test.mocks.dart new file mode 100644 index 00000000..1c8f3bde --- /dev/null +++ b/test/trophies/widgets/dashboard_trophies_widget_test.mocks.dart @@ -0,0 +1,123 @@ +// Mocks generated by Mockito 5.4.6 from annotations +// in wger/test/trophies/widgets/dashboard_trophies_widget_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i4; +import 'package:wger/models/trophies/trophy.dart' as _i6; +import 'package:wger/models/trophies/user_trophy.dart' as _i7; +import 'package:wger/models/trophies/user_trophy_progression.dart' as _i8; +import 'package:wger/providers/base_provider.dart' as _i2; +import 'package:wger/providers/trophies.dart' as _i3; + +// 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 _FakeWgerBaseProvider_0 extends _i1.SmartFake implements _i2.WgerBaseProvider { + _FakeWgerBaseProvider_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +/// A class which mocks [TrophyRepository]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTrophyRepository extends _i1.Mock implements _i3.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: _i4.dummyValue( + this, + Invocation.getter(#trophiesPath), + ), + ) + as String); + + @override + String get userTrophiesPath => + (super.noSuchMethod( + Invocation.getter(#userTrophiesPath), + returnValue: _i4.dummyValue( + this, + Invocation.getter(#userTrophiesPath), + ), + ) + as String); + + @override + String get userTrophyProgressionPath => + (super.noSuchMethod( + Invocation.getter(#userTrophyProgressionPath), + returnValue: _i4.dummyValue( + this, + Invocation.getter(#userTrophyProgressionPath), + ), + ) + as String); + + @override + _i5.Future> fetchTrophies() => + (super.noSuchMethod( + Invocation.method(#fetchTrophies, []), + returnValue: _i5.Future>.value(<_i6.Trophy>[]), + ) + as _i5.Future>); + + @override + _i5.Future> fetchUserTrophies() => + (super.noSuchMethod( + Invocation.method(#fetchUserTrophies, []), + returnValue: _i5.Future>.value( + <_i7.UserTrophy>[], + ), + ) + as _i5.Future>); + + @override + _i5.Future> fetchProgression() => + (super.noSuchMethod( + Invocation.method(#fetchProgression, []), + returnValue: _i5.Future>.value( + <_i8.UserTrophyProgression>[], + ), + ) + as _i5.Future>); + + @override + List<_i6.Trophy> filterByType(List<_i6.Trophy>? list, _i6.TrophyType? type) => + (super.noSuchMethod( + Invocation.method(#filterByType, [list, type]), + returnValue: <_i6.Trophy>[], + ) + as List<_i6.Trophy>); +} 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 500c61d8..e9105c48 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. From 36a3e7ef4aaa9f2cb696a1b10ee9a93d16eaef85 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Sat, 20 Dec 2025 19:53:55 +0100 Subject: [PATCH 04/11] Add overview page for the trophies --- lib/helpers/json.dart | 10 +- lib/l10n/app_en.arb | 1 + lib/main.dart | 2 + lib/models/trophies/user_trophy.g.dart | 18 -- .../trophies/user_trophy_progression.dart | 7 +- .../trophies/user_trophy_progression.g.dart | 22 +- lib/models/workouts/session.g.dart | 18 -- lib/models/workouts/slot_data.g.dart | 18 -- lib/providers/gym_state.g.dart | 18 -- lib/providers/trophies.dart | 12 +- lib/providers/trophies.g.dart | 18 -- lib/screens/trophy_screen.dart | 39 ++++ lib/widgets/dashboard/widgets/trophies.dart | 78 +++---- lib/widgets/trophies/trophies_overview.dart | 198 ++++++++++++++++++ test/core/validators_test.mocks.dart | 29 +-- test/test_data/trophies.dart | 9 + .../trophies_provider_test.mocks.dart | 18 -- .../widgets/trophies_overview_test.dart | 60 ++++++ 18 files changed, 387 insertions(+), 188 deletions(-) create mode 100644 lib/screens/trophy_screen.dart create mode 100644 lib/widgets/trophies/trophies_overview.dart create mode 100644 test/trophies/widgets/trophies_overview_test.dart diff --git a/lib/helpers/json.dart b/lib/helpers/json.dart index 91cd0756..b0202ca3 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 003c43ea..335df77c 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", diff --git a/lib/main.dart b/lib/main.dart index b36666c8..0eda236e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -58,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'; @@ -263,6 +264,7 @@ class MainApp extends StatelessWidget { 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/user_trophy.g.dart b/lib/models/trophies/user_trophy.g.dart index 62d85ac3..3208ebf6 100644 --- a/lib/models/trophies/user_trophy.g.dart +++ b/lib/models/trophies/user_trophy.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 'user_trophy.dart'; diff --git a/lib/models/trophies/user_trophy_progression.dart b/lib/models/trophies/user_trophy_progression.dart index e84104a9..6cf115ba 100644 --- a/lib/models/trophies/user_trophy_progression.dart +++ b/lib/models/trophies/user_trophy_progression.dart @@ -30,19 +30,22 @@ class UserTrophyProgression { @JsonKey(required: true, name: 'is_earned') final bool isEarned; - @JsonKey(required: true, name: 'earned_at', fromJson: utcIso8601ToLocalDate) - final DateTime earnedAt; + @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; diff --git a/lib/models/trophies/user_trophy_progression.g.dart b/lib/models/trophies/user_trophy_progression.g.dart index ac23b913..de2a49e7 100644 --- a/lib/models/trophies/user_trophy_progression.g.dart +++ b/lib/models/trophies/user_trophy_progression.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 'user_trophy_progression.dart'; @@ -42,7 +24,7 @@ UserTrophyProgression _$UserTrophyProgressionFromJson( return UserTrophyProgression( trophy: Trophy.fromJson(json['trophy'] as Map), isEarned: json['is_earned'] as bool, - earnedAt: utcIso8601ToLocalDate(json['earned_at'] as String), + 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?), @@ -55,7 +37,7 @@ Map _$UserTrophyProgressionToJson( ) => { 'trophy': instance.trophy, 'is_earned': instance.isEarned, - 'earned_at': instance.earnedAt.toIso8601String(), + 'earned_at': instance.earnedAt?.toIso8601String(), 'progress': instance.progress, 'current_value': instance.currentValue, 'target_value': instance.targetValue, diff --git a/lib/models/workouts/session.g.dart b/lib/models/workouts/session.g.dart index 9e3a52f7..bf061fa1 100644 --- a/lib/models/workouts/session.g.dart +++ b/lib/models/workouts/session.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 'session.dart'; diff --git a/lib/models/workouts/slot_data.g.dart b/lib/models/workouts/slot_data.g.dart index 589b2b99..756717e6 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/gym_state.g.dart b/lib/providers/gym_state.g.dart index 7596fa4b..1899e808 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'; diff --git a/lib/providers/trophies.dart b/lib/providers/trophies.dart index 13ee1df7..bf1104b2 100644 --- a/lib/providers/trophies.dart +++ b/lib/providers/trophies.dart @@ -48,9 +48,15 @@ class TrophyRepository { } Future> fetchProgression() async { - final url = base.makeUrl(userTrophyProgressionPath, query: {'limit': API_MAX_PAGE_SIZE}); - final data = await base.fetchPaginated(url); - return data.map((e) => UserTrophyProgression.fromJson(e)).toList(); + try { + final url = base.makeUrl(userTrophyProgressionPath); + final List data = await base.fetch(url); + return data.map((e) => UserTrophyProgression.fromJson(e)).toList(); + } catch (e, stck) { + print('Error fetching trophy progression: $e'); + print(stck); + return []; + } } List filterByType(List list, TrophyType type) => diff --git a/lib/providers/trophies.g.dart b/lib/providers/trophies.g.dart index 04df81d6..81c82726 100644 --- a/lib/providers/trophies.g.dart +++ b/lib/providers/trophies.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 'trophies.dart'; diff --git a/lib/screens/trophy_screen.dart b/lib/screens/trophy_screen.dart new file mode 100644 index 00000000..0cc3cc02 --- /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 index c17df4ae..b0e1bb62 100644 --- a/lib/widgets/dashboard/widgets/trophies.dart +++ b/lib/widgets/dashboard/widgets/trophies.dart @@ -20,6 +20,7 @@ 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'; import 'package:wger/widgets/core/progress_indicator.dart'; class DashboardTrophiesWidget extends ConsumerWidget { @@ -92,43 +93,48 @@ class TrophyCard extends StatelessWidget { @override Widget build(BuildContext context) { return Card.filled( - 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, - ), - ], + 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/trophies/trophies_overview.dart b/lib/widgets/trophies/trophies_overview.dart new file mode 100644 index 00000000..2db561e7 --- /dev/null +++ b/lib/widgets/trophies/trophies_overview.dart @@ -0,0 +1,198 @@ +/* + * 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: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'; +import 'package:wger/widgets/core/progress_indicator.dart'; + +class TrophiesOverview extends ConsumerWidget { + const TrophiesOverview({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final notifier = ref.watch(trophyStateProvider.notifier); + + // 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; + } + + return FutureBuilder>( + future: notifier.fetchTrophyProgression(), + builder: (context, snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return const Card(child: BoxedProgressIndicator()); + } + + if (snapshot.hasError) { + return Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text('Error loading trophies', style: Theme.of(context).textTheme.bodyLarge), + ), + ); + } + + final userTrophyProgression = snapshot.data ?? []; + + // If empty, show placeholder + if (userTrophyProgression.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: userTrophyProgression.length, + itemBuilder: (context, index) { + return _TrophyCardImage(userProgression: userTrophyProgression[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) + 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/validators_test.mocks.dart b/test/core/validators_test.mocks.dart index 8791d455..389f1556 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( diff --git a/test/test_data/trophies.dart b/test/test_data/trophies.dart index 698e26a5..8fc13d28 100644 --- a/test/test_data/trophies.dart +++ b/test/test_data/trophies.dart @@ -58,6 +58,15 @@ List getUserTrophyProgression() { targetValue: null, progressDisplay: null, ), + UserTrophyProgression( + trophy: trophies[1], + progress: 40, + isEarned: false, + earnedAt: null, + currentValue: 12, + targetValue: 30, + progressDisplay: '12 / 30', + ), ]; } diff --git a/test/trophies/provider/trophies_provider_test.mocks.dart b/test/trophies/provider/trophies_provider_test.mocks.dart index bebf3d1d..9830acb4 100644 --- a/test/trophies/provider/trophies_provider_test.mocks.dart +++ b/test/trophies/provider/trophies_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/trophies/provider/trophies_provider_test.dart. // Do not manually edit this file. diff --git a/test/trophies/widgets/trophies_overview_test.dart b/test/trophies/widgets/trophies_overview_test.dart new file mode 100644 index 00000000..7d7d65aa --- /dev/null +++ b/test/trophies/widgets/trophies_overview_test.dart @@ -0,0 +1,60 @@ +/* + * 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:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.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'; +import 'dashboard_trophies_widget_test.mocks.dart'; + +@GenerateMocks([TrophyRepository]) +void main() { + testWidgets('TrophiesOverview shows trophies', (WidgetTester tester) async { + // Arrange + final mockRepository = MockTrophyRepository(); + when(mockRepository.fetchProgression()).thenAnswer((_) async => getUserTrophyProgression()); + + // Act + await mockNetworkImagesFor(() async { + await tester.pumpWidget( + ProviderScope( + overrides: [ + trophyRepositoryProvider.overrideWithValue(mockRepository), + ], + 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); + }); + }); +} From 0774571fbc87c116ca19f59345f5eb1a68bcbd7a Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Sat, 20 Dec 2025 21:34:33 +0100 Subject: [PATCH 05/11] Fix test --- lib/providers/trophies.dart | 12 +++--------- test/trophies/provider/trophies_provider_test.dart | 13 +++---------- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/lib/providers/trophies.dart b/lib/providers/trophies.dart index bf1104b2..1da6eea3 100644 --- a/lib/providers/trophies.dart +++ b/lib/providers/trophies.dart @@ -48,15 +48,9 @@ class TrophyRepository { } Future> fetchProgression() async { - try { - final url = base.makeUrl(userTrophyProgressionPath); - final List data = await base.fetch(url); - return data.map((e) => UserTrophyProgression.fromJson(e)).toList(); - } catch (e, stck) { - print('Error fetching trophy progression: $e'); - print(stck); - return []; - } + final url = base.makeUrl(userTrophyProgressionPath); + final List data = await base.fetch(url); + return data.map((e) => UserTrophyProgression.fromJson(e)).toList(); } List filterByType(List list, TrophyType type) => diff --git a/test/trophies/provider/trophies_provider_test.dart b/test/trophies/provider/trophies_provider_test.dart index 5f159b94..456a806a 100644 --- a/test/trophies/provider/trophies_provider_test.dart +++ b/test/trophies/provider/trophies_provider_test.dart @@ -77,15 +77,8 @@ void main() { }; 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')); + when(mockBase.fetch(any)).thenAnswer((_) async => [progressionJson]); + when(mockBase.makeUrl(any)).thenReturn(Uri.parse('https://example.org/user_progressions')); final repository = TrophyRepository(mockBase); // Act @@ -100,7 +93,7 @@ void main() { expect(p.currentValue, 12.5); expect(p.progressDisplay, '12.5/100'); - verify(mockBase.fetchPaginated(any)).called(1); + verify(mockBase.fetch(any)).called(1); }); }); } From 536f18805b3abaa9cd4067d33bff805706d6aef7 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Sun, 21 Dec 2025 20:40:20 +0100 Subject: [PATCH 06/11] Load context data for PR trophies This allows us to show for which sets the user achieved new PRs --- lib/l10n/app_en.arb | 1 + lib/models/trophies/user_trophy.dart | 5 + lib/models/trophies/user_trophy.g.dart | 5 + .../trophies/user_trophy_context_data.dart | 75 ++++++++++ .../trophies/user_trophy_context_data.g.dart | 50 +++++++ lib/providers/gym_state.dart | 2 +- lib/providers/gym_state.g.dart | 2 +- lib/providers/trophies.dart | 65 +++++++-- lib/providers/trophies.g.dart | 2 +- lib/widgets/routines/gym_mode/summary.dart | 43 ++++-- .../logs/exercises_expansion_card.dart | 22 ++- test/core/validators_test.mocks.dart | 11 ++ .../dashboard_trophies_widget_test.dart | 5 +- .../dashboard_trophies_widget_test.mocks.dart | 16 ++- .../widgets/trophies_overview_test.dart | 4 +- .../widgets/trophies_overview_test.mocks.dart | 131 ++++++++++++++++++ 16 files changed, 397 insertions(+), 42 deletions(-) create mode 100644 lib/models/trophies/user_trophy_context_data.dart create mode 100644 lib/models/trophies/user_trophy_context_data.g.dart create mode 100644 test/trophies/widgets/trophies_overview_test.mocks.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 335df77c..efce357f 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -263,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/models/trophies/user_trophy.dart b/lib/models/trophies/user_trophy.dart index 81cb51e3..8e1e7a81 100644 --- a/lib/models/trophies/user_trophy.dart +++ b/lib/models/trophies/user_trophy.dart @@ -20,6 +20,7 @@ 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'; @@ -42,12 +43,16 @@ class UserTrophy { @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 diff --git a/lib/models/trophies/user_trophy.g.dart b/lib/models/trophies/user_trophy.g.dart index 3208ebf6..97d9da44 100644 --- a/lib/models/trophies/user_trophy.g.dart +++ b/lib/models/trophies/user_trophy.g.dart @@ -15,6 +15,7 @@ UserTrophy _$UserTrophyFromJson(Map json) { 'earned_at', 'progress', 'is_notified', + 'context_data', ], ); return UserTrophy( @@ -23,6 +24,9 @@ UserTrophy _$UserTrophyFromJson(Map json) { 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), ); } @@ -32,4 +36,5 @@ Map _$UserTrophyToJson(UserTrophy instance) => . + * 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 00000000..e1fcc63a --- /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/providers/gym_state.dart b/lib/providers/gym_state.dart index 949a753f..752f785d 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 1899e808..1ddf6b8b 100644 --- a/lib/providers/gym_state.g.dart +++ b/lib/providers/gym_state.g.dart @@ -40,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 index 1da6eea3..e172f5ec 100644 --- a/lib/providers/trophies.dart +++ b/lib/providers/trophies.dart @@ -16,6 +16,7 @@ * 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'; @@ -28,6 +29,8 @@ import 'base_provider.dart'; part 'trophies.g.dart'; class TrophyRepository { + final _logger = Logger('TrophyRepository'); + final WgerBaseProvider base; final trophiesPath = 'trophy'; final userTrophiesPath = 'user-trophy'; @@ -36,21 +39,43 @@ class TrophyRepository { TrophyRepository(this.base); Future> fetchTrophies() async { - final url = base.makeUrl(trophiesPath, query: {'limit': API_MAX_PAGE_SIZE}); - final trophyData = await base.fetchPaginated(url); - return trophyData.map((e) => Trophy.fromJson(e)).toList(); + try { + final url = base.makeUrl(trophiesPath, query: {'limit': API_MAX_PAGE_SIZE}); + final trophyData = await base.fetchPaginated(url); + return trophyData.map((e) => Trophy.fromJson(e)).toList(); + } catch (e, stk) { + _logger.warning('Error fetching trophies:', e, stk); + return []; + } } - Future> fetchUserTrophies() async { - final url = base.makeUrl(userTrophiesPath, query: {'limit': API_MAX_PAGE_SIZE}); - final trophyData = await base.fetchPaginated(url); - return trophyData.map((e) => UserTrophy.fromJson(e)).toList(); + Future> fetchUserTrophies({Map? filterQuery}) 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); + 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() async { - final url = base.makeUrl(userTrophyProgressionPath); - final List data = await base.fetch(url); - return data.map((e) => UserTrophyProgression.fromJson(e)).toList(); + Future> fetchProgression({Map? filterQuery}) async { + try { + final url = base.makeUrl(userTrophyProgressionPath, query: filterQuery); + final List data = await base.fetch(url); + 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) => @@ -74,15 +99,27 @@ final class TrophyStateNotifier extends _$TrophyStateNotifier { return repo.fetchTrophies(); } - /// Fetch trophies awarded to the user + /// Fetch trophies awarded to the user. + /// Excludes hidden trophies as well as repeatable (PR) trophies since they are + /// handled separately Future> fetchUserTrophies() async { final repo = ref.read(trophyRepositoryProvider); - return repo.fetchUserTrophies(); + return repo.fetchUserTrophies( + filterQuery: {'trophy__is_hidden': 'false', 'trophy__is_repeatable': 'false'}, + ); + } + + /// Fetch PR trophies awarded to the user + Future> fetchUserPRTrophies() async { + final repo = ref.read(trophyRepositoryProvider); + return repo.fetchUserTrophies(filterQuery: {'trophy__trophy_type': TrophyType.pr.name}); } /// Fetch trophy progression for the user Future> fetchTrophyProgression() async { final repo = ref.read(trophyRepositoryProvider); - return repo.fetchProgression(); + return repo.fetchProgression( + filterQuery: {'trophy__is_hidden': 'false', 'trophy__is_repeatable': 'false'}, + ); } } diff --git a/lib/providers/trophies.g.dart b/lib/providers/trophies.g.dart index 81c82726..4b413f7f 100644 --- a/lib/providers/trophies.g.dart +++ b/lib/providers/trophies.g.dart @@ -81,7 +81,7 @@ final class TrophyStateNotifierProvider extends $NotifierProvider r'e5c8f2a9477b8f7e5efe4e9ba23765f951627a9f'; +String _$trophyStateNotifierHash() => r'7f50ce352d980168dae169916a17d9b62b388d28'; abstract class _$TrophyStateNotifier extends $Notifier { void build(); diff --git a/lib/widgets/routines/gym_mode/summary.dart b/lib/widgets/routines/gym_mode/summary.dart index 439d7a0d..850af71a 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 - 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 @@ -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'; @@ -48,6 +50,7 @@ class WorkoutSummary extends ConsumerStatefulWidget { class _WorkoutSummaryState extends ConsumerState { late Future _initData; late Routine _routine; + late List _userPrTrophies; @override void initState() { @@ -62,6 +65,9 @@ class _WorkoutSummaryState extends ConsumerState { _routine = await context.read().fetchAndSetRoutineFull( gymState.routine.id!, ); + + final trophyNotifier = ref.read(trophyStateProvider.notifier); + _userPrTrophies = await trophyNotifier.fetchUserPRTrophies(); } @override @@ -82,10 +88,18 @@ class _WorkoutSummaryState extends ConsumerState { } else if (snapshot.hasError) { return Center(child: Text('Error: ${snapshot.error}: ${snapshot.stackTrace}')); } else if (snapshot.connectionState == ConnectionState.done) { + final apiSession = _routine.sessions.firstWhereOrNull( + (s) => s.session.date.isSameDayAs(clock.now()), + ); + final userTrophies = _userPrTrophies + .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 +116,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 +175,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/exercises_expansion_card.dart b/lib/widgets/routines/logs/exercises_expansion_card.dart index 0286418f..ea9634e7 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/test/core/validators_test.mocks.dart b/test/core/validators_test.mocks.dart index 389f1556..fb63573c 100644 --- a/test/core/validators_test.mocks.dart +++ b/test/core/validators_test.mocks.dart @@ -1107,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/trophies/widgets/dashboard_trophies_widget_test.dart b/test/trophies/widgets/dashboard_trophies_widget_test.dart index 7c6a92b4..9d7ae799 100644 --- a/test/trophies/widgets/dashboard_trophies_widget_test.dart +++ b/test/trophies/widgets/dashboard_trophies_widget_test.dart @@ -33,7 +33,9 @@ void main() { testWidgets('DashboardTrophiesWidget shows trophies', (WidgetTester tester) async { // Arrange final mockRepository = MockTrophyRepository(); - when(mockRepository.fetchUserTrophies()).thenAnswer((_) async => getUserTrophies()); + when( + mockRepository.fetchUserTrophies(filterQuery: anyNamed('filterQuery')), + ).thenAnswer((_) async => getUserTrophies()); // Act await mockNetworkImagesFor(() async { @@ -52,7 +54,6 @@ void main() { await tester.pumpAndSettle(); // Assert - // expect(find.text('Trophies'), findsOneWidget); expect(find.text('New Year, New Me'), findsOneWidget); }); }); diff --git a/test/trophies/widgets/dashboard_trophies_widget_test.mocks.dart b/test/trophies/widgets/dashboard_trophies_widget_test.mocks.dart index 1c8f3bde..47c030af 100644 --- a/test/trophies/widgets/dashboard_trophies_widget_test.mocks.dart +++ b/test/trophies/widgets/dashboard_trophies_widget_test.mocks.dart @@ -94,9 +94,13 @@ class MockTrophyRepository extends _i1.Mock implements _i3.TrophyRepository { as _i5.Future>); @override - _i5.Future> fetchUserTrophies() => + _i5.Future> fetchUserTrophies({ + Map? filterQuery, + }) => (super.noSuchMethod( - Invocation.method(#fetchUserTrophies, []), + Invocation.method(#fetchUserTrophies, [], { + #filterQuery: filterQuery, + }), returnValue: _i5.Future>.value( <_i7.UserTrophy>[], ), @@ -104,9 +108,13 @@ class MockTrophyRepository extends _i1.Mock implements _i3.TrophyRepository { as _i5.Future>); @override - _i5.Future> fetchProgression() => + _i5.Future> fetchProgression({ + Map? filterQuery, + }) => (super.noSuchMethod( - Invocation.method(#fetchProgression, []), + Invocation.method(#fetchProgression, [], { + #filterQuery: filterQuery, + }), returnValue: _i5.Future>.value( <_i8.UserTrophyProgression>[], ), diff --git a/test/trophies/widgets/trophies_overview_test.dart b/test/trophies/widgets/trophies_overview_test.dart index 7d7d65aa..7874f991 100644 --- a/test/trophies/widgets/trophies_overview_test.dart +++ b/test/trophies/widgets/trophies_overview_test.dart @@ -33,7 +33,9 @@ void main() { testWidgets('TrophiesOverview shows trophies', (WidgetTester tester) async { // Arrange final mockRepository = MockTrophyRepository(); - when(mockRepository.fetchProgression()).thenAnswer((_) async => getUserTrophyProgression()); + when( + mockRepository.fetchProgression(filterQuery: anyNamed('filterQuery')), + ).thenAnswer((_) async => getUserTrophyProgression()); // Act await mockNetworkImagesFor(() async { diff --git a/test/trophies/widgets/trophies_overview_test.mocks.dart b/test/trophies/widgets/trophies_overview_test.mocks.dart new file mode 100644 index 00000000..039b622a --- /dev/null +++ b/test/trophies/widgets/trophies_overview_test.mocks.dart @@ -0,0 +1,131 @@ +// Mocks generated by Mockito 5.4.6 from annotations +// in wger/test/trophies/widgets/trophies_overview_test.dart. +// Do not manually edit this file. + +// ignore_for_file: no_leading_underscores_for_library_prefixes +import 'dart:async' as _i5; + +import 'package:mockito/mockito.dart' as _i1; +import 'package:mockito/src/dummies.dart' as _i4; +import 'package:wger/models/trophies/trophy.dart' as _i6; +import 'package:wger/models/trophies/user_trophy.dart' as _i7; +import 'package:wger/models/trophies/user_trophy_progression.dart' as _i8; +import 'package:wger/providers/base_provider.dart' as _i2; +import 'package:wger/providers/trophies.dart' as _i3; + +// 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 _FakeWgerBaseProvider_0 extends _i1.SmartFake implements _i2.WgerBaseProvider { + _FakeWgerBaseProvider_0(Object parent, Invocation parentInvocation) + : super(parent, parentInvocation); +} + +/// A class which mocks [TrophyRepository]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTrophyRepository extends _i1.Mock implements _i3.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: _i4.dummyValue( + this, + Invocation.getter(#trophiesPath), + ), + ) + as String); + + @override + String get userTrophiesPath => + (super.noSuchMethod( + Invocation.getter(#userTrophiesPath), + returnValue: _i4.dummyValue( + this, + Invocation.getter(#userTrophiesPath), + ), + ) + as String); + + @override + String get userTrophyProgressionPath => + (super.noSuchMethod( + Invocation.getter(#userTrophyProgressionPath), + returnValue: _i4.dummyValue( + this, + Invocation.getter(#userTrophyProgressionPath), + ), + ) + as String); + + @override + _i5.Future> fetchTrophies() => + (super.noSuchMethod( + Invocation.method(#fetchTrophies, []), + returnValue: _i5.Future>.value(<_i6.Trophy>[]), + ) + as _i5.Future>); + + @override + _i5.Future> fetchUserTrophies({ + Map? filterQuery, + }) => + (super.noSuchMethod( + Invocation.method(#fetchUserTrophies, [], { + #filterQuery: filterQuery, + }), + returnValue: _i5.Future>.value( + <_i7.UserTrophy>[], + ), + ) + as _i5.Future>); + + @override + _i5.Future> fetchProgression({ + Map? filterQuery, + }) => + (super.noSuchMethod( + Invocation.method(#fetchProgression, [], { + #filterQuery: filterQuery, + }), + returnValue: _i5.Future>.value( + <_i8.UserTrophyProgression>[], + ), + ) + as _i5.Future>); + + @override + List<_i6.Trophy> filterByType(List<_i6.Trophy>? list, _i6.TrophyType? type) => + (super.noSuchMethod( + Invocation.method(#filterByType, [list, type]), + returnValue: <_i6.Trophy>[], + ) + as List<_i6.Trophy>); +} From 52892db9b11ff8fad6e4cae8e356cbad7ccd0448 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Mon, 22 Dec 2025 13:03:24 +0100 Subject: [PATCH 07/11] Allow passing the language to the fetch request This allows us to use the trophy translations from the server. --- lib/providers/base_provider.dart | 15 ++++---- lib/providers/trophies.dart | 36 ++++++++++++------- lib/providers/trophies.g.dart | 2 +- lib/widgets/dashboard/widgets/trophies.dart | 3 +- lib/widgets/routines/gym_mode/log_page.dart | 3 ++ lib/widgets/routines/gym_mode/summary.dart | 21 ++++++++--- lib/widgets/trophies/trophies_overview.dart | 3 +- test/core/settings_test.mocks.dart | 14 +++++--- .../contribute_exercise_image_test.mocks.dart | 14 +++++--- test/gallery/gallery_form_test.mocks.dart | 14 +++++--- test/gallery/gallery_screen_test.mocks.dart | 14 +++++--- .../measurement_provider_test.mocks.dart | 14 +++++--- .../nutritional_plan_screen_test.mocks.dart | 14 +++++--- .../nutritional_plans_screen_test.mocks.dart | 14 +++++--- .../routine/gym_mode/gym_mode_test.mocks.dart | 14 +++++--- test/routine/routine_screen_test.mocks.dart | 14 +++++--- .../routine/routines_provider_test.mocks.dart | 14 +++++--- test/routine/routines_screen_test.mocks.dart | 14 +++++--- .../trophies_provider_test.mocks.dart | 14 +++++--- .../dashboard_trophies_widget_test.dart | 5 ++- .../dashboard_trophies_widget_test.mocks.dart | 8 +++-- .../widgets/trophies_overview_test.dart | 5 ++- .../widgets/trophies_overview_test.mocks.dart | 8 +++-- test/user/provider_test.mocks.dart | 32 +++++------------ test/weight/weight_provider_test.mocks.dart | 14 +++++--- 25 files changed, 202 insertions(+), 121 deletions(-) diff --git a/lib/providers/base_provider.dart b/lib/providers/base_provider.dart index dee257d7..a1e11f54 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/trophies.dart b/lib/providers/trophies.dart index e172f5ec..183a8fde 100644 --- a/lib/providers/trophies.dart +++ b/lib/providers/trophies.dart @@ -38,10 +38,10 @@ class TrophyRepository { TrophyRepository(this.base); - Future> fetchTrophies() async { + Future> fetchTrophies({String? language}) async { try { final url = base.makeUrl(trophiesPath, query: {'limit': API_MAX_PAGE_SIZE}); - final trophyData = await base.fetchPaginated(url); + 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); @@ -49,7 +49,10 @@ class TrophyRepository { } } - Future> fetchUserTrophies({Map? filterQuery}) async { + Future> fetchUserTrophies({ + Map? filterQuery, + String? language, + }) async { final query = {'limit': API_MAX_PAGE_SIZE}; if (filterQuery != null) { query.addAll(filterQuery); @@ -57,7 +60,7 @@ class TrophyRepository { try { final url = base.makeUrl(userTrophiesPath, query: query); - final trophyData = await base.fetchPaginated(url); + 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:'); @@ -67,10 +70,13 @@ class TrophyRepository { } } - Future> fetchProgression({Map? filterQuery}) async { + Future> fetchProgression({ + Map? filterQuery, + String? language, + }) async { try { final url = base.makeUrl(userTrophyProgressionPath, query: filterQuery); - final List data = await base.fetch(url); + 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); @@ -102,24 +108,28 @@ final class TrophyStateNotifier extends _$TrophyStateNotifier { /// Fetch trophies awarded to the user. /// Excludes hidden trophies as well as repeatable (PR) trophies since they are /// handled separately - Future> fetchUserTrophies() async { + Future> fetchUserTrophies({String? language}) async { final repo = ref.read(trophyRepositoryProvider); return repo.fetchUserTrophies( filterQuery: {'trophy__is_hidden': 'false', 'trophy__is_repeatable': 'false'}, + language: language, ); } /// Fetch PR trophies awarded to the user - Future> fetchUserPRTrophies() async { + Future> fetchUserPRTrophies({String? language}) async { final repo = ref.read(trophyRepositoryProvider); - return repo.fetchUserTrophies(filterQuery: {'trophy__trophy_type': TrophyType.pr.name}); + return repo.fetchUserTrophies( + filterQuery: {'trophy__trophy_type': TrophyType.pr.name}, + language: language, + ); } /// Fetch trophy progression for the user - Future> fetchTrophyProgression() async { + Future> fetchTrophyProgression({String? language}) async { final repo = ref.read(trophyRepositoryProvider); - return repo.fetchProgression( - filterQuery: {'trophy__is_hidden': 'false', 'trophy__is_repeatable': 'false'}, - ); + + // Note that repeatable trophies are filtered out in the backend + return repo.fetchProgression(language: language); } } diff --git a/lib/providers/trophies.g.dart b/lib/providers/trophies.g.dart index 4b413f7f..88f47d58 100644 --- a/lib/providers/trophies.g.dart +++ b/lib/providers/trophies.g.dart @@ -81,7 +81,7 @@ final class TrophyStateNotifierProvider extends $NotifierProvider r'7f50ce352d980168dae169916a17d9b62b388d28'; +String _$trophyStateNotifierHash() => r'47b4babf337bbb8bf60142ebe59dc760fa08dce3'; abstract class _$TrophyStateNotifier extends $Notifier { void build(); diff --git a/lib/widgets/dashboard/widgets/trophies.dart b/lib/widgets/dashboard/widgets/trophies.dart index b0e1bb62..3e39fd45 100644 --- a/lib/widgets/dashboard/widgets/trophies.dart +++ b/lib/widgets/dashboard/widgets/trophies.dart @@ -29,9 +29,10 @@ class DashboardTrophiesWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final provider = ref.watch(trophyStateProvider.notifier); + final languageCode = Localizations.localeOf(context).languageCode; return FutureBuilder( - future: provider.fetchUserTrophies(), + future: provider.fetchUserTrophies(language: languageCode), builder: (context, asyncSnapshot) { if (asyncSnapshot.connectionState != ConnectionState.done) { return const Card(child: BoxedProgressIndicator()); diff --git a/lib/widgets/routines/gym_mode/log_page.dart b/lib/widgets/routines/gym_mode/log_page.dart index 70254266..10cf7e64 100644 --- a/lib/widgets/routines/gym_mode/log_page.dart +++ b/lib/widgets/routines/gym_mode/log_page.dart @@ -549,6 +549,9 @@ class _LogFormWidgetState extends ConsumerState { _weightController = TextEditingController(); WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) { + return; + } _syncControllersWithWidget(); }); } diff --git a/lib/widgets/routines/gym_mode/summary.dart b/lib/widgets/routines/gym_mode/summary.dart index 850af71a..146b15c5 100644 --- a/lib/widgets/routines/gym_mode/summary.dart +++ b/lib/widgets/routines/gym_mode/summary.dart @@ -38,7 +38,6 @@ import '../logs/muscle_groups.dart'; class WorkoutSummary extends ConsumerStatefulWidget { final _logger = Logger('WorkoutSummary'); - final PageController _controller; WorkoutSummary(this._controller); @@ -51,14 +50,24 @@ class _WorkoutSummaryState extends ConsumerState { late Future _initData; late Routine _routine; late List _userPrTrophies; + 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); @@ -67,7 +76,7 @@ class _WorkoutSummaryState extends ConsumerState { ); final trophyNotifier = ref.read(trophyStateProvider.notifier); - _userPrTrophies = await trophyNotifier.fetchUserPRTrophies(); + _userPrTrophies = await trophyNotifier.fetchUserPRTrophies(language: languageCode); } @override @@ -86,7 +95,9 @@ 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()), diff --git a/lib/widgets/trophies/trophies_overview.dart b/lib/widgets/trophies/trophies_overview.dart index 2db561e7..d763e946 100644 --- a/lib/widgets/trophies/trophies_overview.dart +++ b/lib/widgets/trophies/trophies_overview.dart @@ -29,6 +29,7 @@ class TrophiesOverview extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final notifier = ref.watch(trophyStateProvider.notifier); + final languageCode = Localizations.localeOf(context).languageCode; // Responsive grid: determine columns based on screen width final width = MediaQuery.widthOf(context); @@ -44,7 +45,7 @@ class TrophiesOverview extends ConsumerWidget { } return FutureBuilder>( - future: notifier.fetchTrophyProgression(), + future: notifier.fetchTrophyProgression(language: languageCode), builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { return const Card(child: BoxedProgressIndicator()); diff --git a/test/core/settings_test.mocks.dart b/test/core/settings_test.mocks.dart index db2394bd..e883c76d 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/exercises/contribute_exercise_image_test.mocks.dart b/test/exercises/contribute_exercise_image_test.mocks.dart index c3685ed3..c5c3b857 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 e14d7838..a531b6ae 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 d1ab1318..330d0c42 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 ef362a87..c8c22b45 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 2fe3c4ac..0ff6d292 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 c702d401..e42d7a17 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 6b3c882c..8259d2c0 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_screen_test.mocks.dart b/test/routine/routine_screen_test.mocks.dart index 99fe1643..8f05a817 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 06326506..ae751468 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 2607f048..1dafb2ca 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/trophies/provider/trophies_provider_test.mocks.dart b/test/trophies/provider/trophies_provider_test.mocks.dart index 9830acb4..79887735 100644 --- a/test/trophies/provider/trophies_provider_test.mocks.dart +++ b/test/trophies/provider/trophies_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/trophies/widgets/dashboard_trophies_widget_test.dart b/test/trophies/widgets/dashboard_trophies_widget_test.dart index 9d7ae799..bf4209b4 100644 --- a/test/trophies/widgets/dashboard_trophies_widget_test.dart +++ b/test/trophies/widgets/dashboard_trophies_widget_test.dart @@ -34,7 +34,10 @@ void main() { // Arrange final mockRepository = MockTrophyRepository(); when( - mockRepository.fetchUserTrophies(filterQuery: anyNamed('filterQuery')), + mockRepository.fetchUserTrophies( + filterQuery: anyNamed('filterQuery'), + language: anyNamed('language'), + ), ).thenAnswer((_) async => getUserTrophies()); // Act diff --git a/test/trophies/widgets/dashboard_trophies_widget_test.mocks.dart b/test/trophies/widgets/dashboard_trophies_widget_test.mocks.dart index 47c030af..41544f94 100644 --- a/test/trophies/widgets/dashboard_trophies_widget_test.mocks.dart +++ b/test/trophies/widgets/dashboard_trophies_widget_test.mocks.dart @@ -86,9 +86,9 @@ class MockTrophyRepository extends _i1.Mock implements _i3.TrophyRepository { as String); @override - _i5.Future> fetchTrophies() => + _i5.Future> fetchTrophies({String? language}) => (super.noSuchMethod( - Invocation.method(#fetchTrophies, []), + Invocation.method(#fetchTrophies, [], {#language: language}), returnValue: _i5.Future>.value(<_i6.Trophy>[]), ) as _i5.Future>); @@ -96,10 +96,12 @@ class MockTrophyRepository extends _i1.Mock implements _i3.TrophyRepository { @override _i5.Future> fetchUserTrophies({ Map? filterQuery, + String? language, }) => (super.noSuchMethod( Invocation.method(#fetchUserTrophies, [], { #filterQuery: filterQuery, + #language: language, }), returnValue: _i5.Future>.value( <_i7.UserTrophy>[], @@ -110,10 +112,12 @@ class MockTrophyRepository extends _i1.Mock implements _i3.TrophyRepository { @override _i5.Future> fetchProgression({ Map? filterQuery, + String? language, }) => (super.noSuchMethod( Invocation.method(#fetchProgression, [], { #filterQuery: filterQuery, + #language: language, }), returnValue: _i5.Future>.value( <_i8.UserTrophyProgression>[], diff --git a/test/trophies/widgets/trophies_overview_test.dart b/test/trophies/widgets/trophies_overview_test.dart index 7874f991..2c6b71f5 100644 --- a/test/trophies/widgets/trophies_overview_test.dart +++ b/test/trophies/widgets/trophies_overview_test.dart @@ -34,7 +34,10 @@ void main() { // Arrange final mockRepository = MockTrophyRepository(); when( - mockRepository.fetchProgression(filterQuery: anyNamed('filterQuery')), + mockRepository.fetchProgression( + filterQuery: anyNamed('filterQuery'), + language: anyNamed('language'), + ), ).thenAnswer((_) async => getUserTrophyProgression()); // Act diff --git a/test/trophies/widgets/trophies_overview_test.mocks.dart b/test/trophies/widgets/trophies_overview_test.mocks.dart index 039b622a..dc837fd8 100644 --- a/test/trophies/widgets/trophies_overview_test.mocks.dart +++ b/test/trophies/widgets/trophies_overview_test.mocks.dart @@ -86,9 +86,9 @@ class MockTrophyRepository extends _i1.Mock implements _i3.TrophyRepository { as String); @override - _i5.Future> fetchTrophies() => + _i5.Future> fetchTrophies({String? language}) => (super.noSuchMethod( - Invocation.method(#fetchTrophies, []), + Invocation.method(#fetchTrophies, [], {#language: language}), returnValue: _i5.Future>.value(<_i6.Trophy>[]), ) as _i5.Future>); @@ -96,10 +96,12 @@ class MockTrophyRepository extends _i1.Mock implements _i3.TrophyRepository { @override _i5.Future> fetchUserTrophies({ Map? filterQuery, + String? language, }) => (super.noSuchMethod( Invocation.method(#fetchUserTrophies, [], { #filterQuery: filterQuery, + #language: language, }), returnValue: _i5.Future>.value( <_i7.UserTrophy>[], @@ -110,10 +112,12 @@ class MockTrophyRepository extends _i1.Mock implements _i3.TrophyRepository { @override _i5.Future> fetchProgression({ Map? filterQuery, + String? language, }) => (super.noSuchMethod( Invocation.method(#fetchProgression, [], { #filterQuery: filterQuery, + #language: language, }), returnValue: _i5.Future>.value( <_i8.UserTrophyProgression>[], diff --git a/test/user/provider_test.mocks.dart b/test/user/provider_test.mocks.dart index b8fce543..6ff9bc85 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 4cd18bb5..bf00ffb1 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>); From 696b3333fb5045c76d46f194df839835e3ab0450 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Sat, 10 Jan 2026 12:57:52 +0100 Subject: [PATCH 08/11] Only show the progression bar for uncompleted trophies --- lib/widgets/trophies/trophies_overview.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/widgets/trophies/trophies_overview.dart b/lib/widgets/trophies/trophies_overview.dart index d763e946..2b06f340 100644 --- a/lib/widgets/trophies/trophies_overview.dart +++ b/lib/widgets/trophies/trophies_overview.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 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 @@ -158,7 +158,7 @@ class _TrophyCardImage extends StatelessWidget { ), const SizedBox(height: 8), - if (userProgression.trophy.isProgressive) + if (userProgression.trophy.isProgressive && !userProgression.isEarned) Tooltip( message: 'Progress: ${userProgression.progressDisplay}', child: SizedBox( From 352a4b1c49b8051a982fb40ebd9bd43c66e8ae33 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Sat, 10 Jan 2026 14:02:23 +0100 Subject: [PATCH 09/11] Also show the PR trophies in the regular log page --- lib/providers/trophies.dart | 82 ++++++++--- lib/providers/trophies.g.dart | 27 ++-- lib/screens/home_tabs_screen.dart | 22 +-- lib/screens/routine_logs_screen.dart | 4 +- lib/widgets/dashboard/widgets/trophies.dart | 83 +++++------ lib/widgets/routines/gym_mode/log_page.dart | 2 +- lib/widgets/routines/gym_mode/summary.dart | 13 +- .../routines/logs/day_logs_container.dart | 48 ++++++- .../routines/logs/log_overview_routine.dart | 14 +- lib/widgets/trophies/trophies_overview.dart | 74 ++++------ test/routine/routine_logs_screen_test.dart | 55 ++++--- .../routine_logs_screen_test.mocks.dart | 108 ++++++++++++++ .../dashboard_trophies_widget_test.dart | 22 +-- .../dashboard_trophies_widget_test.mocks.dart | 135 ------------------ .../widgets/trophies_overview_test.dart | 23 ++- .../widgets/trophies_overview_test.mocks.dart | 135 ------------------ .../routines/gym_mode/log_page_test.dart | 16 +-- 17 files changed, 377 insertions(+), 486 deletions(-) delete mode 100644 test/trophies/widgets/dashboard_trophies_widget_test.mocks.dart delete mode 100644 test/trophies/widgets/trophies_overview_test.mocks.dart diff --git a/lib/providers/trophies.dart b/lib/providers/trophies.dart index 183a8fde..19289af5 100644 --- a/lib/providers/trophies.dart +++ b/lib/providers/trophies.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 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 @@ -28,6 +28,38 @@ 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'); @@ -96,40 +128,52 @@ TrophyRepository trophyRepository(Ref ref) { @Riverpod(keepAlive: true) final class TrophyStateNotifier extends _$TrophyStateNotifier { + final _logger = Logger('TrophyStateNotifier'); + @override - void build() {} + 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() async { + Future> fetchTrophies({String? language}) async { + _logger.finer('Fetching trophies'); + final repo = ref.read(trophyRepositoryProvider); - return repo.fetchTrophies(); + final result = await repo.fetchTrophies(language: language); + state = state.copyWith(trophies: result); + return result; } - /// Fetch trophies awarded to the user. - /// Excludes hidden trophies as well as repeatable (PR) trophies since they are - /// handled separately + /// Fetch trophies awarded to the user, excludes hidden trophies Future> fetchUserTrophies({String? language}) async { - final repo = ref.read(trophyRepositoryProvider); - return repo.fetchUserTrophies( - filterQuery: {'trophy__is_hidden': 'false', 'trophy__is_repeatable': 'false'}, - language: language, - ); - } + _logger.finer('Fetching user trophies'); - /// Fetch PR trophies awarded to the user - Future> fetchUserPRTrophies({String? language}) async { final repo = ref.read(trophyRepositoryProvider); - return repo.fetchUserTrophies( - filterQuery: {'trophy__trophy_type': TrophyType.pr.name}, + 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 { - final repo = ref.read(trophyRepositoryProvider); + _logger.finer('Fetching user trophy progression'); // Note that repeatable trophies are filtered out in the backend - return repo.fetchProgression(language: language); + 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 index 88f47d58..2f2a95e8 100644 --- a/lib/providers/trophies.g.dart +++ b/lib/providers/trophies.g.dart @@ -53,7 +53,8 @@ String _$trophyRepositoryHash() => r'0699f0c0f7f324f3ba9b21420d9845a3e3096b61'; @ProviderFor(TrophyStateNotifier) const trophyStateProvider = TrophyStateNotifierProvider._(); -final class TrophyStateNotifierProvider extends $NotifierProvider { +final class TrophyStateNotifierProvider + extends $NotifierProvider { const TrophyStateNotifierProvider._() : super( from: null, @@ -73,25 +74,31 @@ final class TrophyStateNotifierProvider extends $NotifierProvider TrophyStateNotifier(); /// {@macro riverpod.override_with_value} - Override overrideWithValue(void value) { + Override overrideWithValue(TrophyState value) { return $ProviderOverride( origin: this, - providerOverride: $SyncValueProvider(value), + providerOverride: $SyncValueProvider(value), ); } } -String _$trophyStateNotifierHash() => r'47b4babf337bbb8bf60142ebe59dc760fa08dce3'; +String _$trophyStateNotifierHash() => r'c80c732272cf843b698f28152f60b9a5f37ee449'; -abstract class _$TrophyStateNotifier extends $Notifier { - void build(); +abstract class _$TrophyStateNotifier extends $Notifier { + TrophyState build(); @$mustCallSuper @override void runBuild() { - build(); - final ref = this.ref as $Ref; + final created = build(); + final ref = this.ref as $Ref; final element = - ref.element as $ClassProviderElement, void, Object?, Object?>; - element.handleValue(ref, null); + ref.element + as $ClassProviderElement< + AnyNotifier, + TrophyState, + Object?, + Object? + >; + element.handleValue(ref, created); } } diff --git a/lib/screens/home_tabs_screen.dart b/lib/screens/home_tabs_screen.dart index ebb70398..10de168f 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,25 +50,20 @@ class HomeTabsScreen extends StatefulWidget { _HomeTabsScreenState createState() => _HomeTabsScreenState(); } -class _HomeTabsScreenState extends State with SingleTickerProviderStateMixin { - late Future _initialData; +class _HomeTabsScreenState extends ConsumerState + with SingleTickerProviderStateMixin { + Future? _initialData; bool _errorHandled = false; int _selectedIndex = 0; bool _isWideScreen = false; - @override - void initState() { - super.initState(); - // Loading data here, since the build method can be called more than once - _initialData = _loadEntries(); - } - @override void didChangeDependencies() { super.didChangeDependencies(); final size = MediaQuery.sizeOf(context); _isWideScreen = size.width > MATERIAL_XS_BREAKPOINT; + _initialData ??= _loadEntries(); } void _onItemTapped(int index) { @@ -85,7 +82,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 +126,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 93da5d5c..c9909b1f 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/widgets/dashboard/widgets/trophies.dart b/lib/widgets/dashboard/widgets/trophies.dart index 3e39fd45..e164e5d5 100644 --- a/lib/widgets/dashboard/widgets/trophies.dart +++ b/lib/widgets/dashboard/widgets/trophies.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 @@ -21,64 +21,45 @@ 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'; -import 'package:wger/widgets/core/progress_indicator.dart'; class DashboardTrophiesWidget extends ConsumerWidget { const DashboardTrophiesWidget(); @override Widget build(BuildContext context, WidgetRef ref) { - final provider = ref.watch(trophyStateProvider.notifier); - final languageCode = Localizations.localeOf(context).languageCode; + final trophiesState = ref.read(trophyStateProvider); - return FutureBuilder( - future: provider.fetchUserTrophies(language: languageCode), - builder: (context, asyncSnapshot) { - if (asyncSnapshot.connectionState != ConnectionState.done) { - return const Card(child: BoxedProgressIndicator()); - } + 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]; - final userTrophies = asyncSnapshot.data ?? []; - - return Card( - color: Colors.transparent, - shadowColor: Colors.transparent, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // ListTile( - // title: Text( - // 'Trophies', - // style: Theme.of(context).textTheme.headlineSmall, - // ), - // ), - if (userTrophies.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: userTrophies.length, - separatorBuilder: (context, index) => const SizedBox(width: 12), - itemBuilder: (context, index) { - final userTrophy = userTrophies[index]; - - return SizedBox( - width: 220, - child: TrophyCard(trophy: userTrophy.trophy), - ); - }, - ), - ), - ], - ), - ); - }, + return SizedBox( + width: 220, + child: TrophyCard(trophy: userTrophy.trophy), + ); + }, + ), + ), + ], + ), ); } } diff --git a/lib/widgets/routines/gym_mode/log_page.dart b/lib/widgets/routines/gym_mode/log_page.dart index 10cf7e64..c3c99efb 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 diff --git a/lib/widgets/routines/gym_mode/summary.dart b/lib/widgets/routines/gym_mode/summary.dart index 146b15c5..e5097500 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 @@ -49,7 +49,6 @@ class WorkoutSummary extends ConsumerStatefulWidget { class _WorkoutSummaryState extends ConsumerState { late Future _initData; late Routine _routine; - late List _userPrTrophies; bool _didInit = false; @override @@ -76,11 +75,13 @@ class _WorkoutSummaryState extends ConsumerState { ); final trophyNotifier = ref.read(trophyStateProvider.notifier); - _userPrTrophies = await trophyNotifier.fetchUserPRTrophies(language: languageCode); + await trophyNotifier.fetchUserTrophies(language: languageCode); } @override Widget build(BuildContext context) { + final trophyState = ref.watch(trophyStateProvider); + return Column( children: [ NavigationHeader( @@ -102,10 +103,8 @@ class _WorkoutSummaryState extends ConsumerState { final apiSession = _routine.sessions.firstWhereOrNull( (s) => s.session.date.isSameDayAs(clock.now()), ); - final userTrophies = _userPrTrophies - .where( - (t) => t.contextData!.sessionId == apiSession?.session.id, - ) + final userTrophies = trophyState.prTrophies + .where((t) => t.contextData?.sessionId == apiSession?.session.id) .toList(); return WorkoutSessionStats( diff --git a/lib/widgets/routines/logs/day_logs_container.dart b/lib/widgets/routines/logs/day_logs_container.dart index 49d829b0..ed3ac3a4 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/log_overview_routine.dart b/lib/widgets/routines/logs/log_overview_routine.dart index dfcfc1aa..34f0be5a 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 index 2b06f340..706040df 100644 --- a/lib/widgets/trophies/trophies_overview.dart +++ b/lib/widgets/trophies/trophies_overview.dart @@ -21,15 +21,13 @@ 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'; -import 'package:wger/widgets/core/progress_indicator.dart'; class TrophiesOverview extends ConsumerWidget { const TrophiesOverview({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final notifier = ref.watch(trophyStateProvider.notifier); - final languageCode = Localizations.localeOf(context).languageCode; + final trophyState = ref.watch(trophyStateProvider); // Responsive grid: determine columns based on screen width final width = MediaQuery.widthOf(context); @@ -44,52 +42,32 @@ class TrophiesOverview extends ConsumerWidget { crossAxisCount = 5; } - return FutureBuilder>( - future: notifier.fetchTrophyProgression(language: languageCode), - builder: (context, snapshot) { - if (snapshot.connectionState != ConnectionState.done) { - return const Card(child: BoxedProgressIndicator()); - } - - if (snapshot.hasError) { - return Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text('Error loading trophies', style: Theme.of(context).textTheme.bodyLarge), - ), - ); - } - - final userTrophyProgression = snapshot.data ?? []; - - // If empty, show placeholder - if (userTrophyProgression.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: userTrophyProgression.length, - itemBuilder: (context, index) { - return _TrophyCardImage(userProgression: userTrophyProgression[index]); - }, + // 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]); + }, + ), ); } } diff --git a/test/routine/routine_logs_screen_test.dart b/test/routine/routine_logs_screen_test.dart index c61bdf3a..1081171c 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 dca48e93..015095bb 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/trophies/widgets/dashboard_trophies_widget_test.dart b/test/trophies/widgets/dashboard_trophies_widget_test.dart index bf4209b4..687f13a0 100644 --- a/test/trophies/widgets/dashboard_trophies_widget_test.dart +++ b/test/trophies/widgets/dashboard_trophies_widget_test.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 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 @@ -19,33 +19,25 @@ 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'; 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'; -import 'dashboard_trophies_widget_test.mocks.dart'; -@GenerateMocks([TrophyRepository]) void main() { testWidgets('DashboardTrophiesWidget shows trophies', (WidgetTester tester) async { - // Arrange - final mockRepository = MockTrophyRepository(); - when( - mockRepository.fetchUserTrophies( - filterQuery: anyNamed('filterQuery'), - language: anyNamed('language'), - ), - ).thenAnswer((_) async => getUserTrophies()); - // Act await mockNetworkImagesFor(() async { await tester.pumpWidget( ProviderScope( overrides: [ - trophyRepositoryProvider.overrideWithValue(mockRepository), + trophyStateProvider.overrideWithValue( + TrophyState( + userTrophies: getUserTrophies(), + trophies: getTestTrophies(), + ), + ), ], child: const MaterialApp( home: Scaffold( diff --git a/test/trophies/widgets/dashboard_trophies_widget_test.mocks.dart b/test/trophies/widgets/dashboard_trophies_widget_test.mocks.dart deleted file mode 100644 index 41544f94..00000000 --- a/test/trophies/widgets/dashboard_trophies_widget_test.mocks.dart +++ /dev/null @@ -1,135 +0,0 @@ -// Mocks generated by Mockito 5.4.6 from annotations -// in wger/test/trophies/widgets/dashboard_trophies_widget_test.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i5; - -import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i4; -import 'package:wger/models/trophies/trophy.dart' as _i6; -import 'package:wger/models/trophies/user_trophy.dart' as _i7; -import 'package:wger/models/trophies/user_trophy_progression.dart' as _i8; -import 'package:wger/providers/base_provider.dart' as _i2; -import 'package:wger/providers/trophies.dart' as _i3; - -// 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 _FakeWgerBaseProvider_0 extends _i1.SmartFake implements _i2.WgerBaseProvider { - _FakeWgerBaseProvider_0(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); -} - -/// A class which mocks [TrophyRepository]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockTrophyRepository extends _i1.Mock implements _i3.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: _i4.dummyValue( - this, - Invocation.getter(#trophiesPath), - ), - ) - as String); - - @override - String get userTrophiesPath => - (super.noSuchMethod( - Invocation.getter(#userTrophiesPath), - returnValue: _i4.dummyValue( - this, - Invocation.getter(#userTrophiesPath), - ), - ) - as String); - - @override - String get userTrophyProgressionPath => - (super.noSuchMethod( - Invocation.getter(#userTrophyProgressionPath), - returnValue: _i4.dummyValue( - this, - Invocation.getter(#userTrophyProgressionPath), - ), - ) - as String); - - @override - _i5.Future> fetchTrophies({String? language}) => - (super.noSuchMethod( - Invocation.method(#fetchTrophies, [], {#language: language}), - returnValue: _i5.Future>.value(<_i6.Trophy>[]), - ) - as _i5.Future>); - - @override - _i5.Future> fetchUserTrophies({ - Map? filterQuery, - String? language, - }) => - (super.noSuchMethod( - Invocation.method(#fetchUserTrophies, [], { - #filterQuery: filterQuery, - #language: language, - }), - returnValue: _i5.Future>.value( - <_i7.UserTrophy>[], - ), - ) - as _i5.Future>); - - @override - _i5.Future> fetchProgression({ - Map? filterQuery, - String? language, - }) => - (super.noSuchMethod( - Invocation.method(#fetchProgression, [], { - #filterQuery: filterQuery, - #language: language, - }), - returnValue: _i5.Future>.value( - <_i8.UserTrophyProgression>[], - ), - ) - as _i5.Future>); - - @override - List<_i6.Trophy> filterByType(List<_i6.Trophy>? list, _i6.TrophyType? type) => - (super.noSuchMethod( - Invocation.method(#filterByType, [list, type]), - returnValue: <_i6.Trophy>[], - ) - as List<_i6.Trophy>); -} diff --git a/test/trophies/widgets/trophies_overview_test.dart b/test/trophies/widgets/trophies_overview_test.dart index 2c6b71f5..0bc12cd5 100644 --- a/test/trophies/widgets/trophies_overview_test.dart +++ b/test/trophies/widgets/trophies_overview_test.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 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 @@ -19,33 +19,26 @@ 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'; 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'; -import 'dashboard_trophies_widget_test.mocks.dart'; -@GenerateMocks([TrophyRepository]) void main() { testWidgets('TrophiesOverview shows trophies', (WidgetTester tester) async { - // Arrange - final mockRepository = MockTrophyRepository(); - when( - mockRepository.fetchProgression( - filterQuery: anyNamed('filterQuery'), - language: anyNamed('language'), - ), - ).thenAnswer((_) async => getUserTrophyProgression()); - // Act await mockNetworkImagesFor(() async { await tester.pumpWidget( ProviderScope( overrides: [ - trophyRepositoryProvider.overrideWithValue(mockRepository), + trophyStateProvider.overrideWithValue( + TrophyState( + trophyProgression: getUserTrophyProgression(), + userTrophies: getUserTrophies(), + trophies: getTestTrophies(), + ), + ), ], child: const MaterialApp( home: Scaffold(body: TrophiesOverview()), diff --git a/test/trophies/widgets/trophies_overview_test.mocks.dart b/test/trophies/widgets/trophies_overview_test.mocks.dart deleted file mode 100644 index dc837fd8..00000000 --- a/test/trophies/widgets/trophies_overview_test.mocks.dart +++ /dev/null @@ -1,135 +0,0 @@ -// Mocks generated by Mockito 5.4.6 from annotations -// in wger/test/trophies/widgets/trophies_overview_test.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i5; - -import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i4; -import 'package:wger/models/trophies/trophy.dart' as _i6; -import 'package:wger/models/trophies/user_trophy.dart' as _i7; -import 'package:wger/models/trophies/user_trophy_progression.dart' as _i8; -import 'package:wger/providers/base_provider.dart' as _i2; -import 'package:wger/providers/trophies.dart' as _i3; - -// 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 _FakeWgerBaseProvider_0 extends _i1.SmartFake implements _i2.WgerBaseProvider { - _FakeWgerBaseProvider_0(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); -} - -/// A class which mocks [TrophyRepository]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockTrophyRepository extends _i1.Mock implements _i3.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: _i4.dummyValue( - this, - Invocation.getter(#trophiesPath), - ), - ) - as String); - - @override - String get userTrophiesPath => - (super.noSuchMethod( - Invocation.getter(#userTrophiesPath), - returnValue: _i4.dummyValue( - this, - Invocation.getter(#userTrophiesPath), - ), - ) - as String); - - @override - String get userTrophyProgressionPath => - (super.noSuchMethod( - Invocation.getter(#userTrophyProgressionPath), - returnValue: _i4.dummyValue( - this, - Invocation.getter(#userTrophyProgressionPath), - ), - ) - as String); - - @override - _i5.Future> fetchTrophies({String? language}) => - (super.noSuchMethod( - Invocation.method(#fetchTrophies, [], {#language: language}), - returnValue: _i5.Future>.value(<_i6.Trophy>[]), - ) - as _i5.Future>); - - @override - _i5.Future> fetchUserTrophies({ - Map? filterQuery, - String? language, - }) => - (super.noSuchMethod( - Invocation.method(#fetchUserTrophies, [], { - #filterQuery: filterQuery, - #language: language, - }), - returnValue: _i5.Future>.value( - <_i7.UserTrophy>[], - ), - ) - as _i5.Future>); - - @override - _i5.Future> fetchProgression({ - Map? filterQuery, - String? language, - }) => - (super.noSuchMethod( - Invocation.method(#fetchProgression, [], { - #filterQuery: filterQuery, - #language: language, - }), - returnValue: _i5.Future>.value( - <_i8.UserTrophyProgression>[], - ), - ) - as _i5.Future>); - - @override - List<_i6.Trophy> filterByType(List<_i6.Trophy>? list, _i6.TrophyType? type) => - (super.noSuchMethod( - Invocation.method(#filterByType, [list, type]), - returnValue: <_i6.Trophy>[], - ) - as List<_i6.Trophy>); -} diff --git a/test/widgets/routines/gym_mode/log_page_test.dart b/test/widgets/routines/gym_mode/log_page_test.dart index 45526e7f..78e80858 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 @@ -189,15 +189,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 +229,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)); }); }); } From ec153c51df7c28df3126b5dcf60223273354e306 Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Fri, 16 Jan 2026 17:09:05 +0100 Subject: [PATCH 10/11] i18n and other cleanup --- lib/l10n/app_en.arb | 11 ++++ lib/models/trophies/trophy.g.dart | 18 ------- lib/providers/base_provider.dart | 4 +- lib/providers/gym_state.g.dart | 9 ++-- lib/providers/trophies.g.dart | 11 ++-- lib/screens/trophy_screen.dart | 4 +- .../dashboard/widgets/nothing_found.dart | 41 ++++++++------- lib/widgets/dashboard/widgets/routines.dart | 6 +-- lib/widgets/dashboard/widgets/trophies.dart | 24 +++++++-- lib/widgets/dashboard/widgets/weight.dart | 6 +-- .../routines/logs/day_logs_container.dart | 7 +-- .../logs/exercises_expansion_card.dart | 5 +- lib/widgets/trophies/trophies_overview.dart | 4 +- test/core/settings_test.mocks.dart | 17 +++++- .../contribute_exercise_image_test.mocks.dart | 17 +++++- test/gallery/gallery_form_test.mocks.dart | 17 +++++- test/gallery/gallery_screen_test.mocks.dart | 17 +++++- .../measurement_provider_test.mocks.dart | 17 +++++- .../nutritional_plan_screen_test.mocks.dart | 17 +++++- .../nutritional_plans_screen_test.mocks.dart | 17 +++++- .../routine/gym_mode/gym_mode_test.mocks.dart | 17 +++++- test/routine/routine_screen_test.mocks.dart | 17 +++++- .../routine/routines_provider_test.mocks.dart | 17 +++++- test/routine/routines_screen_test.mocks.dart | 17 +++++- .../trophies_provider_test.mocks.dart | 17 +++++- .../dashboard_trophies_widget_test.dart | 52 ++++++++++++++----- .../widgets/trophies_overview_test.dart | 4 ++ test/user/provider_test.mocks.dart | 17 +++++- test/weight/weight_provider_test.mocks.dart | 17 +++++- 29 files changed, 330 insertions(+), 114 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index efce357f..7c116d7e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -233,6 +233,16 @@ "@comment": { "description": "Comment, additional information" }, + "topSet": "Top set: {value}", + "@topSet": { + "description": "Value is a representation of the set, like '10 x 80kg'", + "type": "text", + "placeholders": { + "value": { + "type": "String" + } + } + }, "impressionGood": "Good", "impressionNeutral": "Neutral", "impressionBad": "Bad", @@ -592,6 +602,7 @@ "@newEntry": { "description": "Title when adding a new entry such as a weight or log entry" }, + "noTrophies": "You have no trophies yet", "noWeightEntries": "You have no weight entries", "@noWeightEntries": { "description": "Message shown when the user has no logged weight entries" diff --git a/lib/models/trophies/trophy.g.dart b/lib/models/trophies/trophy.g.dart index aba73ac1..23eb2a47 100644 --- a/lib/models/trophies/trophy.g.dart +++ b/lib/models/trophies/trophy.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 'trophy.dart'; diff --git a/lib/providers/base_provider.dart b/lib/providers/base_provider.dart index 1571b12d..041871ae 100644 --- a/lib/providers/base_provider.dart +++ b/lib/providers/base_provider.dart @@ -73,7 +73,7 @@ class WgerBaseProvider { Uri uri, { int maxRetries = 3, Duration initialDelay = const Duration(milliseconds: 250), - String? language + String? language, }) async { int attempt = 0; final random = math.Random(); @@ -90,7 +90,7 @@ class WgerBaseProvider { while (true) { try { final response = await client - .get(uri, headers: getDefaultHeaders(includeAuth: true)) + .get(uri, headers: getDefaultHeaders(includeAuth: true, language: language)) .timeout(const Duration(seconds: 5)); if (response.statusCode >= 400) { diff --git a/lib/providers/gym_state.g.dart b/lib/providers/gym_state.g.dart index 1ddf6b8b..4239a549 100644 --- a/lib/providers/gym_state.g.dart +++ b/lib/providers/gym_state.g.dart @@ -10,10 +10,10 @@ part of 'gym_state.dart'; // ignore_for_file: type=lint, type=warning @ProviderFor(GymStateNotifier) -const gymStateProvider = GymStateNotifierProvider._(); +final gymStateProvider = GymStateNotifierProvider._(); final class GymStateNotifierProvider extends $NotifierProvider { - const GymStateNotifierProvider._() + GymStateNotifierProvider._() : super( from: null, argument: null, @@ -40,14 +40,13 @@ final class GymStateNotifierProvider extends $NotifierProvider r'8474afce33638bf67570fd64b64e9b5d171804d3'; +String _$gymStateNotifierHash() => r'3a0bb78e9f7e682ba93a40a73b170126b5eb5ca9'; abstract class _$GymStateNotifier extends $Notifier { GymModeState build(); @$mustCallSuper @override void runBuild() { - final created = build(); final ref = this.ref as $Ref; final element = ref.element @@ -57,6 +56,6 @@ abstract class _$GymStateNotifier extends $Notifier { Object?, Object? >; - element.handleValue(ref, created); + element.handleCreate(ref, build); } } diff --git a/lib/providers/trophies.g.dart b/lib/providers/trophies.g.dart index 2f2a95e8..3f1ffecd 100644 --- a/lib/providers/trophies.g.dart +++ b/lib/providers/trophies.g.dart @@ -10,12 +10,12 @@ part of 'trophies.dart'; // ignore_for_file: type=lint, type=warning @ProviderFor(trophyRepository) -const trophyRepositoryProvider = TrophyRepositoryProvider._(); +final trophyRepositoryProvider = TrophyRepositoryProvider._(); final class TrophyRepositoryProvider extends $FunctionalProvider with $Provider { - const TrophyRepositoryProvider._() + TrophyRepositoryProvider._() : super( from: null, argument: null, @@ -51,11 +51,11 @@ final class TrophyRepositoryProvider String _$trophyRepositoryHash() => r'0699f0c0f7f324f3ba9b21420d9845a3e3096b61'; @ProviderFor(TrophyStateNotifier) -const trophyStateProvider = TrophyStateNotifierProvider._(); +final trophyStateProvider = TrophyStateNotifierProvider._(); final class TrophyStateNotifierProvider extends $NotifierProvider { - const TrophyStateNotifierProvider._() + TrophyStateNotifierProvider._() : super( from: null, argument: null, @@ -89,7 +89,6 @@ abstract class _$TrophyStateNotifier extends $Notifier { @$mustCallSuper @override void runBuild() { - final created = build(); final ref = this.ref as $Ref; final element = ref.element @@ -99,6 +98,6 @@ abstract class _$TrophyStateNotifier extends $Notifier { Object?, Object? >; - element.handleValue(ref, created); + element.handleCreate(ref, build); } } diff --git a/lib/screens/trophy_screen.dart b/lib/screens/trophy_screen.dart index 0cc3cc02..5df5344b 100644 --- a/lib/screens/trophy_screen.dart +++ b/lib/screens/trophy_screen.dart @@ -31,9 +31,7 @@ class TrophyScreen extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: EmptyAppBar(AppLocalizations.of(context).trophies), - body: WidescreenWrapper( - child: TrophiesOverview(), - ), + body: const WidescreenWrapper(child: TrophiesOverview()), ); } } diff --git a/lib/widgets/dashboard/widgets/nothing_found.dart b/lib/widgets/dashboard/widgets/nothing_found.dart index 326d3417..86afc140 100644 --- a/lib/widgets/dashboard/widgets/nothing_found.dart +++ b/lib/widgets/dashboard/widgets/nothing_found.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. @@ -22,10 +22,10 @@ import 'package:wger/theme/theme.dart'; class NothingFound extends StatelessWidget { final String _title; - final String _titleForm; - final Widget _form; + final String? _titleForm; + final Widget? _form; - const NothingFound(this._title, this._titleForm, this._form); + const NothingFound(this._title, [this._titleForm, this._form]); @override Widget build(BuildContext context) { @@ -35,21 +35,22 @@ class NothingFound extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ Text(_title), - IconButton( - iconSize: 30, - icon: const Icon(Icons.add_box, color: wgerPrimaryButtonColor), - onPressed: () { - Navigator.pushNamed( - context, - FormScreen.routeName, - arguments: FormScreenArguments( - _titleForm, - hasListView: true, - _form, - ), - ); - }, - ), + if (_titleForm != null && _form != null) + IconButton( + iconSize: 30, + icon: const Icon(Icons.add_box, color: wgerPrimaryButtonColor), + onPressed: () { + Navigator.pushNamed( + context, + FormScreen.routeName, + arguments: FormScreenArguments( + _titleForm, + hasListView: true, + _form, + ), + ); + }, + ), ], ), ); diff --git a/lib/widgets/dashboard/widgets/routines.dart b/lib/widgets/dashboard/widgets/routines.dart index 5077d905..dba38045 100644 --- a/lib/widgets/dashboard/widgets/routines.dart +++ b/lib/widgets/dashboard/widgets/routines.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. @@ -58,7 +58,7 @@ class _DashboardRoutineWidgetState extends State { ), subtitle: Text( _hasContent - ? '${dateFormat.format(routine!.start)} - ${dateFormat.format(routine!.end)}' + ? '${dateFormat.format(routine!.start)} - ${dateFormat.format(routine.end)}' : '', ), leading: Icon( diff --git a/lib/widgets/dashboard/widgets/trophies.dart b/lib/widgets/dashboard/widgets/trophies.dart index e164e5d5..42e1cfd4 100644 --- a/lib/widgets/dashboard/widgets/trophies.dart +++ b/lib/widgets/dashboard/widgets/trophies.dart @@ -18,6 +18,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/trophies/trophy.dart'; import 'package:wger/providers/trophies.dart'; import 'package:wger/screens/trophy_screen.dart'; @@ -28,6 +29,7 @@ class DashboardTrophiesWidget extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final trophiesState = ref.read(trophyStateProvider); + final i18n = AppLocalizations.of(context); return Card( color: Colors.transparent, @@ -36,9 +38,25 @@ class DashboardTrophiesWidget extends ConsumerWidget { 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), + Card( + child: Column( + children: [ + ListTile( + title: Text( + i18n.trophies, + style: Theme.of(context).textTheme.headlineSmall, + ), + // leading: Icon(Icons.widgets_outlined), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + i18n.noTrophies, + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + ], + ), ) else SizedBox( diff --git a/lib/widgets/dashboard/widgets/weight.dart b/lib/widgets/dashboard/widgets/weight.dart index 35d407b2..d13e1419 100644 --- a/lib/widgets/dashboard/widgets/weight.dart +++ b/lib/widgets/dashboard/widgets/weight.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. @@ -42,7 +42,7 @@ class DashboardWeightWidget extends StatelessWidget { ); return Consumer( - builder: (context, _, __) => Card( + builder: (context, _, _) => Card( child: Column( mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/widgets/routines/logs/day_logs_container.dart b/lib/widgets/routines/logs/day_logs_container.dart index ed3ac3a4..506723d2 100644 --- a/lib/widgets/routines/logs/day_logs_container.dart +++ b/lib/widgets/routines/logs/day_logs_container.dart @@ -18,7 +18,6 @@ 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'; @@ -33,9 +32,8 @@ import 'session_info.dart'; class DayLogWidget extends ConsumerWidget { final DateTime _date; final Routine _routine; - final _logger = Logger('DayLogWidget'); - DayLogWidget(this._date, this._routine); + const DayLogWidget(this._date, this._routine); @override Widget build(BuildContext context, WidgetRef ref) { @@ -53,9 +51,6 @@ class DayLogWidget extends ConsumerWidget { .where((t) => t.contextData?.sessionId == sessionApi.session.id) .toList(); - _logger.info(trophyState.prTrophies); - _logger.info(prTrophies); - return Column( spacing: 10, children: [ diff --git a/lib/widgets/routines/logs/exercises_expansion_card.dart b/lib/widgets/routines/logs/exercises_expansion_card.dart index ea9634e7..0a1a3516 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 - 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 @@ -74,6 +74,7 @@ class _ExerciseExpansionTile extends StatelessWidget { Widget build(BuildContext context) { final languageCode = Localizations.localeOf(context).languageCode; final theme = Theme.of(context); + final i18n = AppLocalizations.of(context); final topSet = logs.isEmpty ? null @@ -85,7 +86,7 @@ class _ExerciseExpansionTile extends StatelessWidget { return ExpansionTile( // leading: const Icon(Icons.fitness_center), title: Text(exercise.getTranslation(languageCode).name, style: theme.textTheme.titleMedium), - subtitle: Text('Top set: $topSetWeight $topSetWeightUnit'), + subtitle: Text(i18n.topSet('$topSetWeight $topSetWeightUnit')), children: logs.map((log) => _SetDataRow(log: log, userPrTrophies: userPrTrophies)).toList(), ); } diff --git a/lib/widgets/trophies/trophies_overview.dart b/lib/widgets/trophies/trophies_overview.dart index 706040df..4f37f087 100644 --- a/lib/widgets/trophies/trophies_overview.dart +++ b/lib/widgets/trophies/trophies_overview.dart @@ -19,6 +19,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:wger/helpers/material.dart'; +import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/trophies/user_trophy_progression.dart'; import 'package:wger/providers/trophies.dart'; @@ -28,6 +29,7 @@ class TrophiesOverview extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final trophyState = ref.watch(trophyStateProvider); + final i18n = AppLocalizations.of(context); // Responsive grid: determine columns based on screen width final width = MediaQuery.widthOf(context); @@ -48,7 +50,7 @@ class TrophiesOverview extends ConsumerWidget { child: Padding( padding: const EdgeInsets.all(16.0), child: Text( - 'No trophies yet', + i18n.noTrophies, style: Theme.of(context).textTheme.bodyLarge, textAlign: TextAlign.center, ), diff --git a/test/core/settings_test.mocks.dart b/test/core/settings_test.mocks.dart index e883c76d..a6192cd8 100644 --- a/test/core/settings_test.mocks.dart +++ b/test/core/settings_test.mocks.dart @@ -1104,9 +1104,22 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider { as Uri); @override - _i18.Future fetch(Uri? uri, {String? language}) => + _i18.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + String? language, + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri], {#language: language}), + Invocation.method( + #fetch, + [uri], + { + #maxRetries: maxRetries, + #initialDelay: initialDelay, + #language: language, + }, + ), returnValue: _i18.Future.value(), ) as _i18.Future); diff --git a/test/exercises/contribute_exercise_image_test.mocks.dart b/test/exercises/contribute_exercise_image_test.mocks.dart index c5c3b857..58b8b260 100644 --- a/test/exercises/contribute_exercise_image_test.mocks.dart +++ b/test/exercises/contribute_exercise_image_test.mocks.dart @@ -406,9 +406,22 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider { as Uri); @override - _i14.Future fetch(Uri? uri, {String? language}) => + _i14.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + String? language, + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri], {#language: language}), + Invocation.method( + #fetch, + [uri], + { + #maxRetries: maxRetries, + #initialDelay: initialDelay, + #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 a531b6ae..5e048896 100644 --- a/test/gallery/gallery_form_test.mocks.dart +++ b/test/gallery/gallery_form_test.mocks.dart @@ -179,9 +179,22 @@ class MockGalleryProvider extends _i1.Mock implements _i4.GalleryProvider { as Uri); @override - _i6.Future fetch(Uri? uri, {String? language}) => + _i6.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + String? language, + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri], {#language: language}), + Invocation.method( + #fetch, + [uri], + { + #maxRetries: maxRetries, + #initialDelay: initialDelay, + #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 330d0c42..9b6edcbd 100644 --- a/test/gallery/gallery_screen_test.mocks.dart +++ b/test/gallery/gallery_screen_test.mocks.dart @@ -179,9 +179,22 @@ class MockGalleryProvider extends _i1.Mock implements _i4.GalleryProvider { as Uri); @override - _i6.Future fetch(Uri? uri, {String? language}) => + _i6.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + String? language, + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri], {#language: language}), + Invocation.method( + #fetch, + [uri], + { + #maxRetries: maxRetries, + #initialDelay: initialDelay, + #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 c8c22b45..b5127325 100644 --- a/test/measurements/measurement_provider_test.mocks.dart +++ b/test/measurements/measurement_provider_test.mocks.dart @@ -116,9 +116,22 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri, {String? language}) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + String? language, + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri], {#language: language}), + Invocation.method( + #fetch, + [uri], + { + #maxRetries: maxRetries, + #initialDelay: initialDelay, + #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 0ff6d292..0ed491a6 100644 --- a/test/nutrition/nutritional_plan_screen_test.mocks.dart +++ b/test/nutrition/nutritional_plan_screen_test.mocks.dart @@ -126,9 +126,22 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri, {String? language}) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + String? language, + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri], {#language: language}), + Invocation.method( + #fetch, + [uri], + { + #maxRetries: maxRetries, + #initialDelay: initialDelay, + #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 e42d7a17..7fa3dec2 100644 --- a/test/nutrition/nutritional_plans_screen_test.mocks.dart +++ b/test/nutrition/nutritional_plans_screen_test.mocks.dart @@ -361,9 +361,22 @@ class MockWgerBaseProvider extends _i1.Mock implements _i8.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri, {String? language}) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + String? language, + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri], {#language: language}), + Invocation.method( + #fetch, + [uri], + { + #maxRetries: maxRetries, + #initialDelay: initialDelay, + #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 8259d2c0..8d17cc4f 100644 --- a/test/routine/gym_mode/gym_mode_test.mocks.dart +++ b/test/routine/gym_mode/gym_mode_test.mocks.dart @@ -205,9 +205,22 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i20.Future fetch(Uri? uri, {String? language}) => + _i20.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + String? language, + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri], {#language: language}), + Invocation.method( + #fetch, + [uri], + { + #maxRetries: maxRetries, + #initialDelay: initialDelay, + #language: language, + }, + ), returnValue: _i20.Future.value(), ) as _i20.Future); diff --git a/test/routine/routine_screen_test.mocks.dart b/test/routine/routine_screen_test.mocks.dart index 8f05a817..8a11f7d1 100644 --- a/test/routine/routine_screen_test.mocks.dart +++ b/test/routine/routine_screen_test.mocks.dart @@ -116,9 +116,22 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri, {String? language}) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + String? language, + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri], {#language: language}), + Invocation.method( + #fetch, + [uri], + { + #maxRetries: maxRetries, + #initialDelay: initialDelay, + #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 ae751468..e70a0c48 100644 --- a/test/routine/routines_provider_test.mocks.dart +++ b/test/routine/routines_provider_test.mocks.dart @@ -155,9 +155,22 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i11.Future fetch(Uri? uri, {String? language}) => + _i11.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + String? language, + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri], {#language: language}), + Invocation.method( + #fetch, + [uri], + { + #maxRetries: maxRetries, + #initialDelay: initialDelay, + #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 1dafb2ca..5502f3a0 100644 --- a/test/routine/routines_screen_test.mocks.dart +++ b/test/routine/routines_screen_test.mocks.dart @@ -116,9 +116,22 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri, {String? language}) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + String? language, + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri], {#language: language}), + Invocation.method( + #fetch, + [uri], + { + #maxRetries: maxRetries, + #initialDelay: initialDelay, + #language: language, + }, + ), returnValue: _i5.Future.value(), ) as _i5.Future); diff --git a/test/trophies/provider/trophies_provider_test.mocks.dart b/test/trophies/provider/trophies_provider_test.mocks.dart index 79887735..15b59d6e 100644 --- a/test/trophies/provider/trophies_provider_test.mocks.dart +++ b/test/trophies/provider/trophies_provider_test.mocks.dart @@ -116,9 +116,22 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri, {String? language}) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + String? language, + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri], {#language: language}), + Invocation.method( + #fetch, + [uri], + { + #maxRetries: maxRetries, + #initialDelay: initialDelay, + #language: language, + }, + ), returnValue: _i5.Future.value(), ) as _i5.Future); diff --git a/test/trophies/widgets/dashboard_trophies_widget_test.dart b/test/trophies/widgets/dashboard_trophies_widget_test.dart index 687f13a0..d8549d92 100644 --- a/test/trophies/widgets/dashboard_trophies_widget_test.dart +++ b/test/trophies/widgets/dashboard_trophies_widget_test.dart @@ -18,20 +18,39 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_riverpod/misc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:network_image_mock/network_image_mock.dart'; +import 'package:wger/l10n/generated/app_localizations.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: [ + Future pumpOverview(WidgetTester tester, [List overrides = const []]) async { + await tester.pumpWidget( + ProviderScope( + overrides: overrides, + child: const MaterialApp( + locale: Locale('en'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: DashboardTrophiesWidget(), + ), + ), + ), + ); + } + + group('DashboardTrophiesWidget tests', () { + testWidgets('shows trophies', (WidgetTester tester) async { + // Act + await mockNetworkImagesFor(() async { + await pumpOverview( + tester, + [ trophyStateProvider.overrideWithValue( TrophyState( userTrophies: getUserTrophies(), @@ -39,17 +58,22 @@ void main() { ), ), ], - child: const MaterialApp( - home: Scaffold( - body: DashboardTrophiesWidget(), - ), - ), - ), - ); + ); + await tester.pumpAndSettle(); + + // Assert + expect(find.text('New Year, New Me'), findsOneWidget); + }); + }); + + testWidgets('handles empty results', (WidgetTester tester) async { + // Act + await pumpOverview(tester); await tester.pumpAndSettle(); // Assert - expect(find.text('New Year, New Me'), findsOneWidget); + expect(find.text('Trophies'), findsOneWidget); + expect(find.text('You have no trophies yet'), findsOneWidget); }); }); } diff --git a/test/trophies/widgets/trophies_overview_test.dart b/test/trophies/widgets/trophies_overview_test.dart index 0bc12cd5..1a843a21 100644 --- a/test/trophies/widgets/trophies_overview_test.dart +++ b/test/trophies/widgets/trophies_overview_test.dart @@ -20,6 +20,7 @@ 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/l10n/generated/app_localizations.dart'; import 'package:wger/providers/trophies.dart'; import 'package:wger/widgets/trophies/trophies_overview.dart'; @@ -41,6 +42,9 @@ void main() { ), ], child: const MaterialApp( + locale: Locale('en'), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, home: Scaffold(body: TrophiesOverview()), ), ), diff --git a/test/user/provider_test.mocks.dart b/test/user/provider_test.mocks.dart index 6ff9bc85..d16a2200 100644 --- a/test/user/provider_test.mocks.dart +++ b/test/user/provider_test.mocks.dart @@ -116,9 +116,22 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri, {String? language}) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + String? language, + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri], {#language: language}), + Invocation.method( + #fetch, + [uri], + { + #maxRetries: maxRetries, + #initialDelay: initialDelay, + #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 bf00ffb1..e1ff97ab 100644 --- a/test/weight/weight_provider_test.mocks.dart +++ b/test/weight/weight_provider_test.mocks.dart @@ -116,9 +116,22 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { as Uri); @override - _i5.Future fetch(Uri? uri, {String? language}) => + _i5.Future fetch( + Uri? uri, { + int? maxRetries = 3, + Duration? initialDelay = const Duration(milliseconds: 250), + String? language, + }) => (super.noSuchMethod( - Invocation.method(#fetch, [uri], {#language: language}), + Invocation.method( + #fetch, + [uri], + { + #maxRetries: maxRetries, + #initialDelay: initialDelay, + #language: language, + }, + ), returnValue: _i5.Future.value(), ) as _i5.Future); From f5a591a3b7e8f98c66a9a20e2245957bf11610fa Mon Sep 17 00:00:00 2001 From: Roland Geider Date: Sat, 17 Jan 2026 14:01:17 +0100 Subject: [PATCH 11/11] Add trophies to configurable dashboard lists --- lib/main.dart | 2 +- lib/providers/user.dart | 9 +- lib/screens/dashboard.dart | 17 +- .../core/settings/dashboard_visibility.dart | 2 + .../settings_dashboard_visibility_test.dart | 24 +- ...tings_dashboard_visibility_test.mocks.dart | 267 ++++-------------- test/core/validators_test.mocks.dart | 22 ++ test/user/provider_test.dart | 27 +- 8 files changed, 119 insertions(+), 251 deletions(-) diff --git a/lib/main.dart b/lib/main.dart index 4f2f1394..956c4874 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -266,7 +266,7 @@ class MainApp extends StatelessWidget { LogOverviewPage.routeName: (ctx) => const LogOverviewPage(), ConfigurePlatesScreen.routeName: (ctx) => const ConfigurePlatesScreen(), ConfigureDashboardWidgetsScreen.routeName: (ctx) => - const ConfigureDashboardWidgetsScreen(), + const ConfigureDashboardWidgetsScreen(), TrophyScreen.routeName: (ctx) => const TrophyScreen(), }, localizationsDelegates: AppLocalizations.localizationsDelegates, diff --git a/lib/providers/user.dart b/lib/providers/user.dart index e2c4c0a8..5e4004e7 100644 --- a/lib/providers/user.dart +++ b/lib/providers/user.dart @@ -28,6 +28,7 @@ import 'package:wger/models/user/profile.dart'; import 'package:wger/providers/base_provider.dart'; enum DashboardWidget { + trophies('trophies'), routines('routines'), nutrition('nutrition'), weight('weight'), @@ -124,7 +125,13 @@ class UserProvider with ChangeNotifier { // Add any missing widgets (e.g. newly added features) for (final widget in DashboardWidget.values) { if (!loaded.any((item) => item.widget == widget)) { - loaded.add(DashboardItem(widget)); + // Try to insert at the original position defined in the enum + // taking into account the current size of the list + var index = DashboardWidget.values.indexOf(widget); + if (index > loaded.length) { + index = loaded.length; + } + loaded.insert(index, DashboardItem(widget)); } } diff --git a/lib/screens/dashboard.dart b/lib/screens/dashboard.dart index 26a9aec2..51319722 100644 --- a/lib/screens/dashboard.dart +++ b/lib/screens/dashboard.dart @@ -18,6 +18,7 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:wger/helpers/material.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/providers/user.dart'; import 'package:wger/widgets/core/app_bar.dart'; @@ -25,10 +26,9 @@ import 'package:wger/widgets/dashboard/calendar.dart'; import 'package:wger/widgets/dashboard/widgets/measurements.dart'; import 'package:wger/widgets/dashboard/widgets/nutrition.dart'; import 'package:wger/widgets/dashboard/widgets/routines.dart'; +import 'package:wger/widgets/dashboard/widgets/trophies.dart'; import 'package:wger/widgets/dashboard/widgets/weight.dart'; -import '../widgets/dashboard/widgets/trophies.dart'; - class DashboardScreen extends StatelessWidget { const DashboardScreen({super.key}); @@ -46,6 +46,8 @@ class DashboardScreen extends StatelessWidget { return const DashboardCalendarWidget(); case DashboardWidget.nutrition: return const DashboardNutritionWidget(); + case DashboardWidget.trophies: + return const DashboardTrophiesWidget(); } /* child: Column( @@ -60,7 +62,7 @@ class DashboardScreen extends StatelessWidget { Widget build(BuildContext context) { final width = MediaQuery.sizeOf(context).width; final isMobile = width < MATERIAL_XS_BREAKPOINT; - inal user = Provider.of(context); + final user = Provider.of(context); late final int crossAxisCount; if (width < MATERIAL_XS_BREAKPOINT) { @@ -73,22 +75,21 @@ class DashboardScreen extends StatelessWidget { crossAxisCount = 4; } - - return Scaffold( appBar: MainAppBar(AppLocalizations.of(context).labelDashboard), body: Center( child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: MATERIAL_LG_BREAKPOINT.toDouble()), + constraints: const BoxConstraints(maxWidth: MATERIAL_LG_BREAKPOINT), child: isMobile ? ListView.builder( padding: const EdgeInsets.all(10), - itemBuilder: (context, index) => _getDashboardWidget(index), + itemBuilder: (context, index) => _getDashboardWidget(user.dashboardOrder[index]), itemCount: user.dashboardOrder.length, ) : GridView.builder( padding: const EdgeInsets.all(10), - itemBuilder: (context, index) => SingleChildScrollView(child: _getDashboardWidget(index)), + itemBuilder: (context, index) => + SingleChildScrollView(child: _getDashboardWidget(user.dashboardOrder[index])), itemCount: user.dashboardOrder.length, gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: crossAxisCount, diff --git a/lib/widgets/core/settings/dashboard_visibility.dart b/lib/widgets/core/settings/dashboard_visibility.dart index b7d110b8..a86b8d5f 100644 --- a/lib/widgets/core/settings/dashboard_visibility.dart +++ b/lib/widgets/core/settings/dashboard_visibility.dart @@ -40,6 +40,8 @@ class SettingsDashboardVisibility extends StatelessWidget { return i18n.calendar; case DashboardWidget.nutrition: return i18n.nutritionalPlans; + case DashboardWidget.trophies: + return i18n.trophies; } } diff --git a/test/core/settings_dashboard_visibility_test.dart b/test/core/settings_dashboard_visibility_test.dart index a171bbd4..1aedab97 100644 --- a/test/core/settings_dashboard_visibility_test.dart +++ b/test/core/settings_dashboard_visibility_test.dart @@ -29,10 +29,7 @@ import 'package:wger/widgets/core/settings/dashboard_visibility.dart'; import 'settings_dashboard_visibility_test.mocks.dart'; -@GenerateMocks([ - UserProvider, - WgerBaseProvider, -]) +@GenerateMocks([WgerBaseProvider]) void main() { late UserProvider userProvider; late MockWgerBaseProvider mockBaseProvider; @@ -98,24 +95,23 @@ void main() { await tester.pumpWidget(createWidget()); await tester.pumpAndSettle(); - // Initial order: routines, nutrition, weight... - expect(userProvider.dashboardOrder[0], DashboardWidget.routines); - expect(userProvider.dashboardOrder[1], DashboardWidget.nutrition); + // Initial order: trophies, routines, nutrition, weight... + expect(userProvider.dashboardOrder[0], DashboardWidget.trophies); + expect(userProvider.dashboardOrder[1], DashboardWidget.routines); - // Find drag handle for Routines (index 0) + // Find drag handle for Trophies (index 0) final handleFinder = find.byIcon(Icons.drag_handle); final firstHandle = handleFinder.at(0); - // final secondHandle = handleFinder.at(1); // Drag first item down await tester.drag(firstHandle, const Offset(0, 100)); // Drag down enough to swap await tester.pumpAndSettle(); // Verify order changed - // If swapped with second item (nutrition) and maybe third (weight) depending on height - // Based on running test: index 0 is nutrition, index 1 is weight. - expect(userProvider.dashboardOrder[0], DashboardWidget.nutrition); - expect(userProvider.dashboardOrder[1], DashboardWidget.weight); - expect(userProvider.dashboardOrder[2], DashboardWidget.routines); + // 100px drag seems to skip 2 items (trophies moves to index 2) + // [routines, nutrition, trophies, ...] + expect(userProvider.dashboardOrder[0], DashboardWidget.routines); + expect(userProvider.dashboardOrder[1], DashboardWidget.nutrition); + expect(userProvider.dashboardOrder[2], DashboardWidget.trophies); }); } diff --git a/test/core/settings_dashboard_visibility_test.mocks.dart b/test/core/settings_dashboard_visibility_test.mocks.dart index 01803e1b..bc9ac783 100644 --- a/test/core/settings_dashboard_visibility_test.mocks.dart +++ b/test/core/settings_dashboard_visibility_test.mocks.dart @@ -3,17 +3,12 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i9; -import 'dart:ui' as _i10; +import 'dart:async' as _i5; -import 'package:flutter/material.dart' as _i7; -import 'package:http/http.dart' as _i5; +import 'package:http/http.dart' as _i3; import 'package:mockito/mockito.dart' as _i1; -import 'package:shared_preferences/shared_preferences.dart' as _i3; -import 'package:wger/models/user/profile.dart' as _i8; -import 'package:wger/providers/auth.dart' as _i4; -import 'package:wger/providers/base_provider.dart' as _i2; -import 'package:wger/providers/user.dart' as _i6; +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 @@ -30,234 +25,67 @@ import 'package:wger/providers/user.dart' as _i6; // ignore_for_file: subtype_of_sealed_class // ignore_for_file: invalid_use_of_internal_member -class _FakeWgerBaseProvider_0 extends _i1.SmartFake implements _i2.WgerBaseProvider { - _FakeWgerBaseProvider_0(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); +class _FakeAuthProvider_0 extends _i1.SmartFake implements _i2.AuthProvider { + _FakeAuthProvider_0(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); } -class _FakeSharedPreferencesAsync_1 extends _i1.SmartFake implements _i3.SharedPreferencesAsync { - _FakeSharedPreferencesAsync_1(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 _FakeAuthProvider_2 extends _i1.SmartFake implements _i4.AuthProvider { - _FakeAuthProvider_2(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 _FakeClient_3 extends _i1.SmartFake implements _i5.Client { - _FakeClient_3(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); -} - -class _FakeUri_4 extends _i1.SmartFake implements Uri { - _FakeUri_4(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); -} - -class _FakeResponse_5 extends _i1.SmartFake implements _i5.Response { - _FakeResponse_5(Object parent, Invocation parentInvocation) : super(parent, parentInvocation); -} - -/// A class which mocks [UserProvider]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockUserProvider extends _i1.Mock implements _i6.UserProvider { - MockUserProvider() { - _i1.throwOnMissingStub(this); - } - - @override - _i7.ThemeMode get themeMode => - (super.noSuchMethod( - Invocation.getter(#themeMode), - returnValue: _i7.ThemeMode.system, - ) - as _i7.ThemeMode); - - @override - _i2.WgerBaseProvider get baseProvider => - (super.noSuchMethod( - Invocation.getter(#baseProvider), - returnValue: _FakeWgerBaseProvider_0( - this, - Invocation.getter(#baseProvider), - ), - ) - as _i2.WgerBaseProvider); - - @override - _i3.SharedPreferencesAsync get prefs => - (super.noSuchMethod( - Invocation.getter(#prefs), - returnValue: _FakeSharedPreferencesAsync_1( - this, - Invocation.getter(#prefs), - ), - ) - as _i3.SharedPreferencesAsync); - - @override - List<_i6.DashboardWidget> get dashboardOrder => - (super.noSuchMethod( - Invocation.getter(#dashboardOrder), - returnValue: <_i6.DashboardWidget>[], - ) - as List<_i6.DashboardWidget>); - - @override - set themeMode(_i7.ThemeMode? value) => super.noSuchMethod( - Invocation.setter(#themeMode, value), - returnValueForMissingStub: null, - ); - - @override - set prefs(_i3.SharedPreferencesAsync? value) => super.noSuchMethod( - Invocation.setter(#prefs, value), - returnValueForMissingStub: null, - ); - - @override - set profile(_i8.Profile? value) => super.noSuchMethod( - Invocation.setter(#profile, value), - returnValueForMissingStub: null, - ); - - @override - bool get hasListeners => - (super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false) as bool); - - @override - void clear() => super.noSuchMethod( - Invocation.method(#clear, []), - returnValueForMissingStub: null, - ); - - @override - bool isDashboardWidgetVisible(_i6.DashboardWidget? key) => - (super.noSuchMethod( - Invocation.method(#isDashboardWidgetVisible, [key]), - returnValue: false, - ) - as bool); - - @override - _i9.Future setDashboardWidgetVisible( - _i6.DashboardWidget? key, - bool? visible, - ) => - (super.noSuchMethod( - Invocation.method(#setDashboardWidgetVisible, [key, visible]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); - - @override - _i9.Future setDashboardOrder(int? oldIndex, int? newIndex) => - (super.noSuchMethod( - Invocation.method(#setDashboardOrder, [oldIndex, newIndex]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); - - @override - void setThemeMode(_i7.ThemeMode? mode) => super.noSuchMethod( - Invocation.method(#setThemeMode, [mode]), - returnValueForMissingStub: null, - ); - - @override - _i9.Future fetchAndSetProfile() => - (super.noSuchMethod( - Invocation.method(#fetchAndSetProfile, []), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); - - @override - _i9.Future saveProfile() => - (super.noSuchMethod( - Invocation.method(#saveProfile, []), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); - - @override - _i9.Future verifyEmail() => - (super.noSuchMethod( - Invocation.method(#verifyEmail, []), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); - - @override - void addListener(_i10.VoidCallback? listener) => super.noSuchMethod( - Invocation.method(#addListener, [listener]), - returnValueForMissingStub: null, - ); - - @override - void removeListener(_i10.VoidCallback? listener) => super.noSuchMethod( - Invocation.method(#removeListener, [listener]), - returnValueForMissingStub: null, - ); - - @override - void dispose() => super.noSuchMethod( - Invocation.method(#dispose, []), - returnValueForMissingStub: null, - ); - - @override - void notifyListeners() => super.noSuchMethod( - Invocation.method(#notifyListeners, []), - returnValueForMissingStub: null, - ); +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 _i2.WgerBaseProvider { +class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider { MockWgerBaseProvider() { _i1.throwOnMissingStub(this); } @override - _i4.AuthProvider get auth => + _i2.AuthProvider get auth => (super.noSuchMethod( Invocation.getter(#auth), - returnValue: _FakeAuthProvider_2(this, Invocation.getter(#auth)), + returnValue: _FakeAuthProvider_0(this, Invocation.getter(#auth)), ) - as _i4.AuthProvider); + as _i2.AuthProvider); @override - _i5.Client get client => + _i3.Client get client => (super.noSuchMethod( Invocation.getter(#client), - returnValue: _FakeClient_3(this, Invocation.getter(#client)), + returnValue: _FakeClient_1(this, Invocation.getter(#client)), ) - as _i5.Client); + as _i3.Client); @override - set auth(_i4.AuthProvider? value) => super.noSuchMethod( + set auth(_i2.AuthProvider? value) => super.noSuchMethod( Invocation.setter(#auth, value), returnValueForMissingStub: null, ); @override - set client(_i5.Client? value) => super.noSuchMethod( + set client(_i3.Client? value) => super.noSuchMethod( Invocation.setter(#client, value), returnValueForMissingStub: null, ); @override - Map getDefaultHeaders({bool? includeAuth = false}) => + Map getDefaultHeaders({ + bool? includeAuth = false, + String? language, + }) => (super.noSuchMethod( Invocation.method(#getDefaultHeaders, [], { #includeAuth: includeAuth, + #language: language, }), returnValue: {}, ) @@ -276,7 +104,7 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider { [path], {#id: id, #objectMethod: objectMethod, #query: query}, ), - returnValue: _FakeUri_4( + returnValue: _FakeUri_2( this, Invocation.method( #makeUrl, @@ -288,62 +116,67 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider { as Uri); @override - _i9.Future fetch( + _i5.Future fetch( Uri? uri, { int? maxRetries = 3, Duration? initialDelay = const Duration(milliseconds: 250), + String? language, }) => (super.noSuchMethod( Invocation.method( #fetch, [uri], - {#maxRetries: maxRetries, #initialDelay: initialDelay}, + { + #maxRetries: maxRetries, + #initialDelay: initialDelay, + #language: language, + }, ), - returnValue: _i9.Future.value(), + returnValue: _i5.Future.value(), ) - as _i9.Future); + as _i5.Future); @override - _i9.Future> fetchPaginated(Uri? uri) => + _i5.Future> fetchPaginated(Uri? uri, {String? language}) => (super.noSuchMethod( - Invocation.method(#fetchPaginated, [uri]), - returnValue: _i9.Future>.value([]), + Invocation.method(#fetchPaginated, [uri], {#language: language}), + returnValue: _i5.Future>.value([]), ) - as _i9.Future>); + as _i5.Future>); @override - _i9.Future> post(Map? data, Uri? uri) => + _i5.Future> post(Map? data, Uri? uri) => (super.noSuchMethod( Invocation.method(#post, [data, uri]), - returnValue: _i9.Future>.value( + returnValue: _i5.Future>.value( {}, ), ) - as _i9.Future>); + as _i5.Future>); @override - _i9.Future> patch( + _i5.Future> patch( Map? data, Uri? uri, ) => (super.noSuchMethod( Invocation.method(#patch, [data, uri]), - returnValue: _i9.Future>.value( + returnValue: _i5.Future>.value( {}, ), ) - as _i9.Future>); + as _i5.Future>); @override - _i9.Future<_i5.Response> deleteRequest(String? url, int? id) => + _i5.Future<_i3.Response> deleteRequest(String? url, int? id) => (super.noSuchMethod( Invocation.method(#deleteRequest, [url, id]), - returnValue: _i9.Future<_i5.Response>.value( - _FakeResponse_5( + returnValue: _i5.Future<_i3.Response>.value( + _FakeResponse_3( this, Invocation.method(#deleteRequest, [url, id]), ), ), ) - as _i9.Future<_i5.Response>); + as _i5.Future<_i3.Response>); } diff --git a/test/core/validators_test.mocks.dart b/test/core/validators_test.mocks.dart index f79e0354..b89be452 100644 --- a/test/core/validators_test.mocks.dart +++ b/test/core/validators_test.mocks.dart @@ -2207,6 +2207,17 @@ class MockAppLocalizations extends _i1.Mock implements _i2.AppLocalizations { ) as String); + @override + String get noTrophies => + (super.noSuchMethod( + Invocation.getter(#noTrophies), + returnValue: _i3.dummyValue( + this, + Invocation.getter(#noTrophies), + ), + ) + as String); + @override String get noWeightEntries => (super.noSuchMethod( @@ -3843,6 +3854,17 @@ class MockAppLocalizations extends _i1.Mock implements _i2.AppLocalizations { ) as String); + @override + String topSet(String? value) => + (super.noSuchMethod( + Invocation.method(#topSet, [value]), + returnValue: _i3.dummyValue( + this, + Invocation.method(#topSet, [value]), + ), + ) + as String); + @override String durationHoursMinutes(int? hours, int? minutes) => (super.noSuchMethod( diff --git a/test/user/provider_test.dart b/test/user/provider_test.dart index e12951f8..c5a9206e 100644 --- a/test/user/provider_test.dart +++ b/test/user/provider_test.dart @@ -106,11 +106,12 @@ void main() { group('dashboard config', () { test('initial config should be default (all visible, default order)', () { - expect(userProvider.dashboardOrder.length, 5); + expect(userProvider.dashboardOrder.length, 6); expect( userProvider.dashboardOrder, orderedEquals([ + DashboardWidget.trophies, DashboardWidget.routines, DashboardWidget.nutrition, DashboardWidget.weight, @@ -161,22 +162,28 @@ void main() { // act final newProvider = UserProvider(mockWgerBaseProvider, prefs: prefs); - await Future.delayed(const Duration(milliseconds: 50)); // wait for async prefs load + await Future.delayed(const Duration(milliseconds: 100)); // wait for async prefs load // assert - // The loaded ones come first - expect(newProvider.dashboardOrder[0], DashboardWidget.nutrition); - expect(newProvider.dashboardOrder[1], DashboardWidget.routines); + // Loaded: [nutrition, routines] + // Missing: trophies (0), weight (3), measurements (4), calendar (5) + // 1. trophies (index 0) inserted at 0 -> [trophies, nutrition, routines] + // 2. weight (index 3) inserted at 3 -> [trophies, nutrition, routines, weight] + + expect(newProvider.dashboardOrder[0], DashboardWidget.trophies); + expect(newProvider.dashboardOrder[1], DashboardWidget.nutrition); + expect(newProvider.dashboardOrder[2], DashboardWidget.routines); + expect(newProvider.dashboardOrder[3], DashboardWidget.weight); // Check visibility expect(newProvider.isDashboardWidgetVisible(DashboardWidget.nutrition), true); expect(newProvider.isDashboardWidgetVisible(DashboardWidget.routines), false); - // Remaining items are added after - expect(newProvider.dashboardOrder.length, 5); - - // Items not in the prefs are visible by default - expect(newProvider.isDashboardWidgetVisible(DashboardWidget.weight), true); + // Missing items should be visible by default + expect( + newProvider.isDashboardWidgetVisible(DashboardWidget.weight), + true, + ); }); }); }