Merge pull request #1052 from wger-project/feature/trophies

Feature/trophies
This commit is contained in:
Roland Geider
2026-01-18 12:21:16 +01:00
committed by GitHub
56 changed files with 2539 additions and 445 deletions

View File

@@ -1,6 +1,6 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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.

View File

@@ -176,6 +176,7 @@
"slotEntryTypeTut": "Time under Tension",
"slotEntryTypeIso": "Isometric hold",
"slotEntryTypeJump": "Jump",
"trophies": "Trophies",
"routines": "Routines",
"newRoutine": "New routine",
"noRoutines": "You have no routines",
@@ -233,6 +234,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",
@@ -263,6 +274,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"
@@ -591,6 +603,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"

View File

@@ -1,6 +1,6 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (c) 2026 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
@@ -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/dashboard.dart';
@@ -58,6 +59,7 @@ import 'package:wger/screens/routine_screen.dart';
import 'package:wger/screens/settings_dashboard_widgets_screen.dart';
import 'package:wger/screens/settings_plates_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';
@@ -130,7 +132,7 @@ void main() async {
};
// Application
runApp(const riverpod.ProviderScope(child: MainApp()));
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
@@ -218,48 +220,63 @@ class MainApp extends StatelessWidget {
),
],
child: Consumer<AuthProvider>(
builder: (ctx, auth, _) => Consumer<UserProvider>(
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(),
ConfigureDashboardWidgetsScreen.routeName: (ctx) =>
const ConfigureDashboardWidgetsScreen(),
},
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
),
),
builder: (ctx, auth, _) {
final baseInstance = WgerBaseProvider(Provider.of(ctx, listen: false));
return Consumer<UserProvider>(
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(),
ConfigureDashboardWidgetsScreen.routeName: (ctx) =>
const ConfigureDashboardWidgetsScreen(),
TrophyScreen.routeName: (ctx) => const TrophyScreen(),
},
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
);
},
),
),
);
},
),
);
}

View File

@@ -0,0 +1,66 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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 <http://www.gnu.org/licenses/>.
*/
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<String, dynamic> json) => _$TrophyFromJson(json);
Map<String, dynamic> toJson() => _$TrophyToJson(this);
}

View File

@@ -0,0 +1,54 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'trophy.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Trophy _$TrophyFromJson(Map<String, dynamic> 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<String, dynamic> _$TrophyToJson(Trophy instance) => <String, dynamic>{
'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',
};

View File

@@ -0,0 +1,62 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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 <http://www.gnu.org/licenses/>.
*/
import 'package:json_annotation/json_annotation.dart';
import 'package:wger/helpers/json.dart';
import 'trophy.dart';
import 'user_trophy_context_data.dart';
part 'user_trophy.g.dart';
/// A trophy awarded to a user for achieving a specific milestone.
@JsonSerializable()
class UserTrophy {
@JsonKey(required: true)
final int id;
@JsonKey(required: true)
final Trophy trophy;
@JsonKey(required: true, name: 'earned_at', fromJson: utcIso8601ToLocalDate)
final DateTime earnedAt;
@JsonKey(required: true)
final num progress;
@JsonKey(required: true, name: 'is_notified')
final bool isNotified;
@JsonKey(required: true, name: 'context_data')
final ContextData? contextData;
UserTrophy({
required this.id,
required this.trophy,
required this.earnedAt,
required this.progress,
required this.isNotified,
this.contextData,
});
// Boilerplate
factory UserTrophy.fromJson(Map<String, dynamic> json) => _$UserTrophyFromJson(json);
Map<String, dynamic> toJson() => _$UserTrophyToJson(this);
}

View File

@@ -0,0 +1,40 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_trophy.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
UserTrophy _$UserTrophyFromJson(Map<String, dynamic> json) {
$checkKeys(
json,
requiredKeys: const [
'id',
'trophy',
'earned_at',
'progress',
'is_notified',
'context_data',
],
);
return UserTrophy(
id: (json['id'] as num).toInt(),
trophy: Trophy.fromJson(json['trophy'] as Map<String, dynamic>),
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<String, dynamic>),
);
}
Map<String, dynamic> _$UserTrophyToJson(UserTrophy instance) => <String, dynamic>{
'id': instance.id,
'trophy': instance.trophy,
'earned_at': instance.earnedAt.toIso8601String(),
'progress': instance.progress,
'is_notified': instance.isNotified,
'context_data': instance.contextData,
};

View File

@@ -0,0 +1,75 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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 <http://www.gnu.org/licenses/>.
*/
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<String, dynamic> json) => _$ContextDataFromJson(json);
Map<String, dynamic> toJson() => _$ContextDataToJson(this);
}

View File

@@ -0,0 +1,50 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_trophy_context_data.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ContextData _$ContextDataFromJson(Map<String, dynamic> 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<String, dynamic> _$ContextDataToJson(ContextData instance) => <String, dynamic>{
'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,
};

View File

@@ -0,0 +1,67 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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 <http://www.gnu.org/licenses/>.
*/
import 'package:json_annotation/json_annotation.dart';
import 'package:wger/helpers/json.dart';
import 'package:wger/models/trophies/trophy.dart';
part 'user_trophy_progression.g.dart';
@JsonSerializable()
class UserTrophyProgression {
@JsonKey(required: true)
final Trophy trophy;
@JsonKey(required: true, name: 'is_earned')
final bool isEarned;
@JsonKey(required: true, name: 'earned_at', fromJson: utcIso8601ToLocalDateNull)
final DateTime? earnedAt;
/// Progress towards earning the trophy (0-100%)
@JsonKey(required: true)
final num progress;
/// Current value towards the trophy goal (e.g., number of workouts completed)
@JsonKey(required: true, name: 'current_value', fromJson: stringToNumNull)
num? currentValue;
/// Target value to achieve the trophy goal
@JsonKey(required: true, name: 'target_value', fromJson: stringToNumNull)
num? targetValue;
/// Human-readable progress display (e.g., "3 / 10" or "51%")
@JsonKey(required: true, name: 'progress_display')
String? progressDisplay;
UserTrophyProgression({
required this.trophy,
required this.isEarned,
required this.earnedAt,
required this.progress,
required this.currentValue,
required this.targetValue,
this.progressDisplay,
});
// Boilerplate
factory UserTrophyProgression.fromJson(Map<String, dynamic> json) =>
_$UserTrophyProgressionFromJson(json);
Map<String, dynamic> toJson() => _$UserTrophyProgressionToJson(this);
}

View File

@@ -0,0 +1,45 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'user_trophy_progression.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
UserTrophyProgression _$UserTrophyProgressionFromJson(
Map<String, dynamic> 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<String, dynamic>),
isEarned: json['is_earned'] as bool,
earnedAt: utcIso8601ToLocalDateNull(json['earned_at'] as String?),
progress: json['progress'] as num,
currentValue: stringToNumNull(json['current_value'] as String?),
targetValue: stringToNumNull(json['target_value'] as String?),
progressDisplay: json['progress_display'] as String?,
);
}
Map<String, dynamic> _$UserTrophyProgressionToJson(
UserTrophyProgression instance,
) => <String, dynamic>{
'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,
};

View File

@@ -1,6 +1,6 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (c) 2026 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
@@ -45,7 +45,7 @@ class WgerBaseProvider {
this.client = client ?? http.Client();
}
Map<String, String> getDefaultHeaders({bool includeAuth = false}) {
Map<String, String> getDefaultHeaders({bool includeAuth = false, String? language}) {
final out = {
HttpHeaders.contentTypeHeader: 'application/json; charset=UTF-8',
HttpHeaders.userAgentHeader: auth.getAppNameHeader(),
@@ -55,6 +55,10 @@ class WgerBaseProvider {
out[HttpHeaders.authorizationHeader] = 'Token ${auth.token}';
}
if (language != null) {
out[HttpHeaders.acceptLanguageHeader] = language;
}
return out;
}
@@ -69,6 +73,7 @@ class WgerBaseProvider {
Uri uri, {
int maxRetries = 3,
Duration initialDelay = const Duration(milliseconds: 250),
String? language,
}) async {
int attempt = 0;
final random = math.Random();
@@ -85,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) {
@@ -114,13 +119,13 @@ class WgerBaseProvider {
}
/// Fetch and retrieve the overview list of objects, returns the JSON parsed response
Future<List<dynamic>> fetchPaginated(Uri uri) async {
Future<List<dynamic>> 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));

179
lib/providers/trophies.dart Normal file
View File

@@ -0,0 +1,179 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (c) 2026 wger Team
*
* wger Workout Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:wger/helpers/consts.dart';
import 'package:wger/models/trophies/trophy.dart';
import 'package:wger/models/trophies/user_trophy.dart';
import 'package:wger/models/trophies/user_trophy_progression.dart';
import 'package:wger/providers/wger_base_riverpod.dart';
import 'base_provider.dart';
part 'trophies.g.dart';
class TrophyState {
final _logger = Logger('TrophyState');
final List<Trophy> trophies;
final List<UserTrophy> userTrophies;
final List<UserTrophyProgression> trophyProgression;
TrophyState({
this.trophies = const [],
this.userTrophies = const [],
this.trophyProgression = const [],
});
TrophyState copyWith({
List<Trophy>? trophies,
List<UserTrophy>? userTrophies,
List<UserTrophyProgression>? trophyProgression,
}) {
return TrophyState(
trophies: trophies ?? this.trophies,
userTrophies: userTrophies ?? this.userTrophies,
trophyProgression: trophyProgression ?? this.trophyProgression,
);
}
List<UserTrophy> get prTrophies =>
userTrophies.where((t) => t.trophy.type == TrophyType.pr).toList();
List<UserTrophy> get nonPrTrophies =>
userTrophies.where((t) => t.trophy.type != TrophyType.pr).toList();
}
class TrophyRepository {
final _logger = Logger('TrophyRepository');
final WgerBaseProvider base;
final trophiesPath = 'trophy';
final userTrophiesPath = 'user-trophy';
final userTrophyProgressionPath = 'trophy/progress';
TrophyRepository(this.base);
Future<List<Trophy>> fetchTrophies({String? language}) async {
try {
final url = base.makeUrl(trophiesPath, query: {'limit': API_MAX_PAGE_SIZE});
final trophyData = await base.fetchPaginated(url, language: language);
return trophyData.map((e) => Trophy.fromJson(e)).toList();
} catch (e, stk) {
_logger.warning('Error fetching trophies:', e, stk);
return [];
}
}
Future<List<UserTrophy>> fetchUserTrophies({
Map<String, String>? filterQuery,
String? language,
}) async {
final query = {'limit': API_MAX_PAGE_SIZE};
if (filterQuery != null) {
query.addAll(filterQuery);
}
try {
final url = base.makeUrl(userTrophiesPath, query: query);
final trophyData = await base.fetchPaginated(url, language: language);
return trophyData.map((e) => UserTrophy.fromJson(e)).toList();
} catch (e, stk) {
_logger.warning('Error fetching user trophies:');
_logger.warning(e);
_logger.warning(stk);
return [];
}
}
Future<List<UserTrophyProgression>> fetchProgression({
Map<String, String>? filterQuery,
String? language,
}) async {
try {
final url = base.makeUrl(userTrophyProgressionPath, query: filterQuery);
final List<dynamic> data = await base.fetch(url, language: language);
return data.map((e) => UserTrophyProgression.fromJson(e)).toList();
} catch (e, stk) {
_logger.warning('Error fetching user trophy progression:', e, stk);
return [];
}
}
List<Trophy> filterByType(List<Trophy> list, TrophyType type) =>
list.where((t) => t.type == type).toList();
}
@riverpod
TrophyRepository trophyRepository(Ref ref) {
final base = ref.read(wgerBaseProvider);
return TrophyRepository(base);
}
@Riverpod(keepAlive: true)
final class TrophyStateNotifier extends _$TrophyStateNotifier {
final _logger = Logger('TrophyStateNotifier');
@override
TrophyState build() {
return TrophyState();
}
Future<void> fetchAll({String? language}) async {
await Future.wait([
fetchTrophies(language: language),
fetchUserTrophies(language: language),
fetchTrophyProgression(language: language),
]);
}
/// Fetch all available trophies
Future<List<Trophy>> fetchTrophies({String? language}) async {
_logger.finer('Fetching trophies');
final repo = ref.read(trophyRepositoryProvider);
final result = await repo.fetchTrophies(language: language);
state = state.copyWith(trophies: result);
return result;
}
/// Fetch trophies awarded to the user, excludes hidden trophies
Future<List<UserTrophy>> fetchUserTrophies({String? language}) async {
_logger.finer('Fetching user trophies');
final repo = ref.read(trophyRepositoryProvider);
final result = await repo.fetchUserTrophies(
filterQuery: {'trophy__is_hidden': 'false'}, //'trophy__is_repeatable': 'false'
language: language,
);
state = state.copyWith(userTrophies: result);
return result;
}
/// Fetch trophy progression for the user
Future<List<UserTrophyProgression>> fetchTrophyProgression({String? language}) async {
_logger.finer('Fetching user trophy progression');
// Note that repeatable trophies are filtered out in the backend
final repo = ref.read(trophyRepositoryProvider);
final result = await repo.fetchProgression(language: language);
state = state.copyWith(trophyProgression: result);
return result;
}
}

View File

@@ -0,0 +1,103 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'trophies.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(trophyRepository)
final trophyRepositoryProvider = TrophyRepositoryProvider._();
final class TrophyRepositoryProvider
extends $FunctionalProvider<TrophyRepository, TrophyRepository, TrophyRepository>
with $Provider<TrophyRepository> {
TrophyRepositoryProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'trophyRepositoryProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$trophyRepositoryHash();
@$internal
@override
$ProviderElement<TrophyRepository> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
TrophyRepository create(Ref ref) {
return trophyRepository(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(TrophyRepository value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<TrophyRepository>(value),
);
}
}
String _$trophyRepositoryHash() => r'0699f0c0f7f324f3ba9b21420d9845a3e3096b61';
@ProviderFor(TrophyStateNotifier)
final trophyStateProvider = TrophyStateNotifierProvider._();
final class TrophyStateNotifierProvider
extends $NotifierProvider<TrophyStateNotifier, TrophyState> {
TrophyStateNotifierProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'trophyStateProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$trophyStateNotifierHash();
@$internal
@override
TrophyStateNotifier create() => TrophyStateNotifier();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(TrophyState value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<TrophyState>(value),
);
}
}
String _$trophyStateNotifierHash() => r'c80c732272cf843b698f28152f60b9a5f37ee449';
abstract class _$TrophyStateNotifier extends $Notifier<TrophyState> {
TrophyState build();
@$mustCallSuper
@override
void runBuild() {
final ref = this.ref as $Ref<TrophyState, TrophyState>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<TrophyState, TrophyState>,
TrophyState,
Object?,
Object?
>;
element.handleCreate(ref, build);
}
}

View File

@@ -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));
}
}

View File

@@ -0,0 +1,31 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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 <http://www.gnu.org/licenses/>.
*/
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<WgerBaseProvider>((ref) {
throw UnimplementedError(
'Override wgerBaseProvider in a ProviderScope with your existing WgerBaseProvider instance',
);
});

View File

@@ -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,6 +26,7 @@ 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';
class DashboardScreen extends StatelessWidget {
@@ -44,22 +46,56 @@ class DashboardScreen extends StatelessWidget {
return const DashboardCalendarWidget();
case DashboardWidget.nutrition:
return const DashboardNutritionWidget();
case DashboardWidget.trophies:
return const DashboardTrophiesWidget();
}
}
@override
Widget build(BuildContext context) {
final user = Provider.of<UserProvider>(context);
return Scaffold(
appBar: MainAppBar(AppLocalizations.of(context).labelDashboard),
body: SingleChildScrollView(
padding: const EdgeInsets.all(10),
child: Column(
/*
child: Column(
children: user.dashboardOrder
.where((w) => user.isDashboardWidgetVisible(w))
.map(_getDashboardWidget)
.toList(),
*/
}
@override
Widget build(BuildContext context) {
final width = MediaQuery.sizeOf(context).width;
final isMobile = width < MATERIAL_XS_BREAKPOINT;
final user = Provider.of<UserProvider>(context);
late final int crossAxisCount;
if (width < MATERIAL_XS_BREAKPOINT) {
crossAxisCount = 1;
} else if (width < MATERIAL_MD_BREAKPOINT) {
crossAxisCount = 2;
} else if (width < MATERIAL_LG_BREAKPOINT) {
crossAxisCount = 3;
} else {
crossAxisCount = 4;
}
return Scaffold(
appBar: MainAppBar(AppLocalizations.of(context).labelDashboard),
body: Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: MATERIAL_LG_BREAKPOINT),
child: isMobile
? ListView.builder(
padding: const EdgeInsets.all(10),
itemBuilder: (context, index) => _getDashboardWidget(user.dashboardOrder[index]),
itemCount: user.dashboardOrder.length,
)
: GridView.builder(
padding: const EdgeInsets.all(10),
itemBuilder: (context, index) =>
SingleChildScrollView(child: _getDashboardWidget(user.dashboardOrder[index])),
itemCount: user.dashboardOrder.length,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
childAspectRatio: 0.7,
),
),
),
),
);

View File

@@ -1,6 +1,6 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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<HomeTabsScreen> with SingleTickerProviderStateMixin {
late Future<void> _initialData;
class _HomeTabsScreenState extends ConsumerState<HomeTabsScreen>
with SingleTickerProviderStateMixin {
Future<void>? _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<HomeTabsScreen> with SingleTickerProvid
/// Load initial data from the server
Future<void> _loadEntries() async {
final languageCode = Localizations.localeOf(context).languageCode;
final authProvider = context.read<AuthProvider>();
final trophyNotifier = ref.read(trophyStateProvider.notifier);
if (!authProvider.dataInit) {
final routinesProvider = context.read<RoutinesProvider>();
@@ -127,6 +126,7 @@ class _HomeTabsScreenState extends State<HomeTabsScreen> with SingleTickerProvid
// routinesProvider.fetchAndSetAllRoutinesFull(),
weightProvider.fetchAndSetEntries(),
measurementProvider.fetchAndSetAllCategoriesAndEntries(),
trophyNotifier.fetchAll(language: languageCode),
]);
//

View File

@@ -1,13 +1,13 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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.

View File

@@ -0,0 +1,37 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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 <http://www.gnu.org/licenses/>.
*/
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: const WidescreenWrapper(child: TrophiesOverview()),
);
}
}

View File

@@ -40,6 +40,8 @@ class SettingsDashboardVisibility extends StatelessWidget {
return i18n.calendar;
case DashboardWidget.nutrition:
return i18n.nutritionalPlans;
case DashboardWidget.trophies:
return i18n.trophies;
}
}

View File

@@ -1,13 +1,13 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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,
),
);
},
),
],
),
);

View File

@@ -1,13 +1,13 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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<DashboardRoutineWidget> {
),
subtitle: Text(
_hasContent
? '${dateFormat.format(routine!.start)} - ${dateFormat.format(routine!.end)}'
? '${dateFormat.format(routine!.start)} - ${dateFormat.format(routine.end)}'
: '',
),
leading: Icon(

View File

@@ -0,0 +1,142 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (c) 2020 - 2026 wger Team
*
* wger Workout Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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';
class DashboardTrophiesWidget extends ConsumerWidget {
const DashboardTrophiesWidget();
@override
Widget build(BuildContext context, WidgetRef ref) {
final trophiesState = ref.read(trophyStateProvider);
final i18n = AppLocalizations.of(context);
return Card(
color: Colors.transparent,
shadowColor: Colors.transparent,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (trophiesState.nonPrTrophies.isEmpty)
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(
height: 140,
child: ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
scrollDirection: Axis.horizontal,
itemCount: trophiesState.nonPrTrophies.length,
separatorBuilder: (context, index) => const SizedBox(width: 12),
itemBuilder: (context, index) {
final userTrophy = trophiesState.nonPrTrophies[index];
return SizedBox(
width: 220,
child: TrophyCard(trophy: userTrophy.trophy),
);
},
),
),
],
),
);
}
}
class TrophyCard extends StatelessWidget {
const TrophyCard({
super.key,
required this.trophy,
});
final Trophy trophy;
@override
Widget build(BuildContext context) {
return Card.filled(
child: InkWell(
onTap: () {
Navigator.of(context).pushNamed(TrophyScreen.routeName);
},
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
radius: 30,
backgroundImage: NetworkImage(trophy.image),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
trophy.name,
style: Theme.of(context).textTheme.titleMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
trophy.description,
style: Theme.of(context).textTheme.bodySmall,
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
],
),
),
],
),
],
),
),
),
);
}
}

View File

@@ -1,13 +1,13 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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<BodyWeightProvider>(
builder: (context, _, __) => Card(
builder: (context, _, _) => Card(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [

View File

@@ -1,6 +1,6 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (c) 2020, 2025 wger Team
* Copyright (c) 2020 - 2026 wger Team
*
* wger Workout Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@@ -24,10 +24,12 @@ import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import 'package:wger/helpers/date.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/models/trophies/user_trophy.dart';
import 'package:wger/models/workouts/routine.dart';
import 'package:wger/models/workouts/session_api.dart';
import 'package:wger/providers/gym_state.dart';
import 'package:wger/providers/routines.dart';
import 'package:wger/providers/trophies.dart';
import 'package:wger/widgets/core/progress_indicator.dart';
import 'package:wger/widgets/routines/gym_mode/navigation.dart';
@@ -36,7 +38,6 @@ import '../logs/muscle_groups.dart';
class WorkoutSummary extends ConsumerStatefulWidget {
final _logger = Logger('WorkoutSummary');
final PageController _controller;
WorkoutSummary(this._controller);
@@ -48,24 +49,39 @@ class WorkoutSummary extends ConsumerStatefulWidget {
class _WorkoutSummaryState extends ConsumerState<WorkoutSummary> {
late Future<void> _initData;
late Routine _routine;
bool _didInit = false;
@override
void initState() {
super.initState();
_initData = _reloadRoutineData();
}
Future<void> _reloadRoutineData() async {
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_didInit) {
final languageCode = Localizations.localeOf(context).languageCode;
_initData = _reloadRoutineData(languageCode);
_didInit = true;
}
}
Future<void> _reloadRoutineData(String languageCode) async {
widget._logger.fine('Loading routine data');
final gymState = ref.read(gymStateProvider);
_routine = await context.read<RoutinesProvider>().fetchAndSetRoutineFull(
gymState.routine.id!,
);
final trophyNotifier = ref.read(trophyStateProvider.notifier);
await trophyNotifier.fetchUserTrophies(language: languageCode);
}
@override
Widget build(BuildContext context) {
final trophyState = ref.watch(trophyStateProvider);
return Column(
children: [
NavigationHeader(
@@ -80,12 +96,20 @@ class _WorkoutSummaryState extends ConsumerState<WorkoutSummary> {
if (snapshot.connectionState == ConnectionState.waiting) {
return const BoxedProgressIndicator();
} else if (snapshot.hasError) {
return Center(child: Text('Error: ${snapshot.error}: ${snapshot.stackTrace}'));
widget._logger.warning(snapshot.error);
widget._logger.warning(snapshot.stackTrace);
return Center(child: Text('Error: ${snapshot.error}'));
} else if (snapshot.connectionState == ConnectionState.done) {
final apiSession = _routine.sessions.firstWhereOrNull(
(s) => s.session.date.isSameDayAs(clock.now()),
);
final userTrophies = trophyState.prTrophies
.where((t) => t.contextData?.sessionId == apiSession?.session.id)
.toList();
return WorkoutSessionStats(
_routine.sessions.firstWhereOrNull(
(s) => s.session.date.isSameDayAs(clock.now()),
),
apiSession,
userTrophies,
);
}
@@ -102,12 +126,14 @@ class _WorkoutSummaryState extends ConsumerState<WorkoutSummary> {
class WorkoutSessionStats extends ConsumerWidget {
final _logger = Logger('WorkoutSessionStats');
final WorkoutSessionApi? _sessionApi;
final List<UserTrophy> _userPrTrophies;
WorkoutSessionStats(this._sessionApi, {super.key});
WorkoutSessionStats(this._sessionApi, this._userPrTrophies, {super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final i18n = AppLocalizations.of(context);
final theme = Theme.of(context);
if (_sessionApi == null) {
return Center(
@@ -159,16 +185,19 @@ class WorkoutSessionStats extends ConsumerWidget {
),
],
),
// const SizedBox(height: 16),
// InfoCard(
// title: 'Personal Records',
// value: prCount.toString(),
// color: theme.colorScheme.tertiaryContainer,
// ),
if (_userPrTrophies.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 10),
child: InfoCard(
title: i18n.personalRecords,
value: _userPrTrophies.length.toString(),
color: theme.colorScheme.tertiaryContainer,
),
),
const SizedBox(height: 10),
MuscleGroupsCard(_sessionApi.logs),
const SizedBox(height: 10),
ExercisesCard(_sessionApi),
ExercisesCard(_sessionApi, _userPrTrophies),
FilledButton(
onPressed: () {
ref.read(gymStateProvider.notifier).clear();

View File

@@ -1,6 +1,6 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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,54 @@
*/
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.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;
const 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();
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 +87,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}'),

View File

@@ -1,6 +1,6 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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
@@ -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<UserTrophy> 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<UserTrophy> userPrTrophies;
final Exercise exercise;
final List<Log> logs;
@@ -66,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
@@ -77,21 +86,21 @@ 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'),
children: logs.map((log) => _SetDataRow(log: log)).toList(),
subtitle: Text(i18n.topSet('$topSetWeight $topSetWeightUnit')),
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<UserTrophy> 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 +112,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)}',

View File

@@ -1,13 +1,13 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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: [

View File

@@ -0,0 +1,179 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (c) 2025 - 2026 wger Team
*
* wger Workout Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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';
class TrophiesOverview extends ConsumerWidget {
const TrophiesOverview({super.key});
@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);
int crossAxisCount = 1;
if (width <= MATERIAL_XS_BREAKPOINT) {
crossAxisCount = 2;
} else if (width > MATERIAL_XS_BREAKPOINT && width < MATERIAL_MD_BREAKPOINT) {
crossAxisCount = 3;
} else if (width >= MATERIAL_MD_BREAKPOINT && width < MATERIAL_LG_BREAKPOINT) {
crossAxisCount = 4;
} else {
crossAxisCount = 5;
}
// If empty, show placeholder
if (trophyState.trophyProgression.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
i18n.noTrophies,
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
),
);
}
return RepaintBoundary(
child: GridView.builder(
padding: const EdgeInsets.all(12),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
),
key: const ValueKey('trophy-grid'),
itemCount: trophyState.trophyProgression.length,
itemBuilder: (context, index) {
return _TrophyCardImage(userProgression: trophyState.trophyProgression[index]);
},
),
);
}
}
class _TrophyCardImage extends StatelessWidget {
final UserTrophyProgression userProgression;
const _TrophyCardImage({required this.userProgression});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
final colorScheme = theme.colorScheme;
final double progress = (userProgression.progress.toDouble() / 100.0).clamp(0.0, 1.0);
return Opacity(
opacity: userProgression.isEarned ? 1.0 : 0.5,
child: Card(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.18),
width: userProgression.isEarned ? 1.2 : 0,
),
),
child: Stack(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
width: 70,
height: 70,
child: ClipOval(
child: Image.network(
userProgression.trophy.image,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) => Center(
child: Icon(Icons.emoji_events, size: 28, color: colorScheme.primary),
),
),
),
),
Text(
userProgression.trophy.name,
style: theme.textTheme.titleSmall?.copyWith(fontWeight: FontWeight.w600),
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
userProgression.trophy.description,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.textTheme.bodySmall?.color,
),
textAlign: TextAlign.center,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 8),
if (userProgression.trophy.isProgressive && !userProgression.isEarned)
Tooltip(
message: 'Progress: ${userProgression.progressDisplay}',
child: SizedBox(
height: 6,
child: ClipRRect(
borderRadius: BorderRadius.circular(6),
child: LinearProgressIndicator(
value: progress,
minHeight: 6,
valueColor: AlwaysStoppedAnimation(colorScheme.primary),
backgroundColor: colorScheme.onSurface.withAlpha((0.06 * 255).round()),
),
),
),
),
],
),
),
if (userProgression.isEarned)
Positioned(
top: 6,
right: 6,
child: Container(
width: 28,
height: 28,
decoration: const BoxDecoration(
color: Colors.green,
shape: BoxShape.circle,
),
child: const Icon(Icons.check, size: 16, color: Colors.white),
),
),
],
),
),
);
}
}

View File

@@ -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);
});
}

View File

@@ -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<void> setDashboardWidgetVisible(
_i6.DashboardWidget? key,
bool? visible,
) =>
(super.noSuchMethod(
Invocation.method(#setDashboardWidgetVisible, [key, visible]),
returnValue: _i9.Future<void>.value(),
returnValueForMissingStub: _i9.Future<void>.value(),
)
as _i9.Future<void>);
@override
_i9.Future<void> setDashboardOrder(int? oldIndex, int? newIndex) =>
(super.noSuchMethod(
Invocation.method(#setDashboardOrder, [oldIndex, newIndex]),
returnValue: _i9.Future<void>.value(),
returnValueForMissingStub: _i9.Future<void>.value(),
)
as _i9.Future<void>);
@override
void setThemeMode(_i7.ThemeMode? mode) => super.noSuchMethod(
Invocation.method(#setThemeMode, [mode]),
returnValueForMissingStub: null,
);
@override
_i9.Future<void> fetchAndSetProfile() =>
(super.noSuchMethod(
Invocation.method(#fetchAndSetProfile, []),
returnValue: _i9.Future<void>.value(),
returnValueForMissingStub: _i9.Future<void>.value(),
)
as _i9.Future<void>);
@override
_i9.Future<void> saveProfile() =>
(super.noSuchMethod(
Invocation.method(#saveProfile, []),
returnValue: _i9.Future<void>.value(),
returnValueForMissingStub: _i9.Future<void>.value(),
)
as _i9.Future<void>);
@override
_i9.Future<void> verifyEmail() =>
(super.noSuchMethod(
Invocation.method(#verifyEmail, []),
returnValue: _i9.Future<void>.value(),
returnValueForMissingStub: _i9.Future<void>.value(),
)
as _i9.Future<void>);
@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<String, String> getDefaultHeaders({bool? includeAuth = false}) =>
Map<String, String> getDefaultHeaders({
bool? includeAuth = false,
String? language,
}) =>
(super.noSuchMethod(
Invocation.method(#getDefaultHeaders, [], {
#includeAuth: includeAuth,
#language: language,
}),
returnValue: <String, String>{},
)
@@ -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<dynamic> fetch(
_i5.Future<dynamic> 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<dynamic>.value(),
returnValue: _i5.Future<dynamic>.value(),
)
as _i9.Future<dynamic>);
as _i5.Future<dynamic>);
@override
_i9.Future<List<dynamic>> fetchPaginated(Uri? uri) =>
_i5.Future<List<dynamic>> fetchPaginated(Uri? uri, {String? language}) =>
(super.noSuchMethod(
Invocation.method(#fetchPaginated, [uri]),
returnValue: _i9.Future<List<dynamic>>.value(<dynamic>[]),
Invocation.method(#fetchPaginated, [uri], {#language: language}),
returnValue: _i5.Future<List<dynamic>>.value(<dynamic>[]),
)
as _i9.Future<List<dynamic>>);
as _i5.Future<List<dynamic>>);
@override
_i9.Future<Map<String, dynamic>> post(Map<String, dynamic>? data, Uri? uri) =>
_i5.Future<Map<String, dynamic>> post(Map<String, dynamic>? data, Uri? uri) =>
(super.noSuchMethod(
Invocation.method(#post, [data, uri]),
returnValue: _i9.Future<Map<String, dynamic>>.value(
returnValue: _i5.Future<Map<String, dynamic>>.value(
<String, dynamic>{},
),
)
as _i9.Future<Map<String, dynamic>>);
as _i5.Future<Map<String, dynamic>>);
@override
_i9.Future<Map<String, dynamic>> patch(
_i5.Future<Map<String, dynamic>> patch(
Map<String, dynamic>? data,
Uri? uri,
) =>
(super.noSuchMethod(
Invocation.method(#patch, [data, uri]),
returnValue: _i9.Future<Map<String, dynamic>>.value(
returnValue: _i5.Future<Map<String, dynamic>>.value(
<String, dynamic>{},
),
)
as _i9.Future<Map<String, dynamic>>);
as _i5.Future<Map<String, dynamic>>);
@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>);
}

View File

@@ -1103,10 +1103,14 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider {
);
@override
Map<String, String> getDefaultHeaders({bool? includeAuth = false}) =>
Map<String, String> getDefaultHeaders({
bool? includeAuth = false,
String? language,
}) =>
(super.noSuchMethod(
Invocation.method(#getDefaultHeaders, [], {
#includeAuth: includeAuth,
#language: language,
}),
returnValue: <String, String>{},
)
@@ -1141,21 +1145,26 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider {
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: _i18.Future<dynamic>.value(),
)
as _i18.Future<dynamic>);
@override
_i18.Future<List<dynamic>> fetchPaginated(Uri? uri) =>
_i18.Future<List<dynamic>> fetchPaginated(Uri? uri, {String? language}) =>
(super.noSuchMethod(
Invocation.method(#fetchPaginated, [uri]),
Invocation.method(#fetchPaginated, [uri], {#language: language}),
returnValue: _i18.Future<List<dynamic>>.value(<dynamic>[]),
)
as _i18.Future<List<dynamic>>);

View File

@@ -778,6 +778,17 @@ class MockAppLocalizations extends _i1.Mock implements _i2.AppLocalizations {
)
as String);
@override
String get trophies =>
(super.noSuchMethod(
Invocation.getter(#trophies),
returnValue: _i3.dummyValue<String>(
this,
Invocation.getter(#trophies),
),
)
as String);
@override
String get routines =>
(super.noSuchMethod(
@@ -1107,6 +1118,17 @@ class MockAppLocalizations extends _i1.Mock implements _i2.AppLocalizations {
)
as String);
@override
String get personalRecords =>
(super.noSuchMethod(
Invocation.getter(#personalRecords),
returnValue: _i3.dummyValue<String>(
this,
Invocation.getter(#personalRecords),
),
)
as String);
@override
String get gymMode =>
(super.noSuchMethod(
@@ -2185,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<String>(
this,
Invocation.getter(#noTrophies),
),
)
as String);
@override
String get noWeightEntries =>
(super.noSuchMethod(
@@ -3821,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<String>(
this,
Invocation.method(#topSet, [value]),
),
)
as String);
@override
String durationHoursMinutes(int? hours, int? minutes) =>
(super.noSuchMethod(

View File

@@ -368,10 +368,14 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider {
);
@override
Map<String, String> getDefaultHeaders({bool? includeAuth = false}) =>
Map<String, String> getDefaultHeaders({
bool? includeAuth = false,
String? language,
}) =>
(super.noSuchMethod(
Invocation.method(#getDefaultHeaders, [], {
#includeAuth: includeAuth,
#language: language,
}),
returnValue: <String, String>{},
)
@@ -406,21 +410,26 @@ class MockWgerBaseProvider extends _i1.Mock implements _i2.WgerBaseProvider {
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: _i14.Future<dynamic>.value(),
)
as _i14.Future<dynamic>);
@override
_i14.Future<List<dynamic>> fetchPaginated(Uri? uri) =>
_i14.Future<List<dynamic>> fetchPaginated(Uri? uri, {String? language}) =>
(super.noSuchMethod(
Invocation.method(#fetchPaginated, [uri]),
Invocation.method(#fetchPaginated, [uri], {#language: language}),
returnValue: _i14.Future<List<dynamic>>.value(<dynamic>[]),
)
as _i14.Future<List<dynamic>>);

View File

@@ -141,10 +141,14 @@ class MockGalleryProvider extends _i1.Mock implements _i4.GalleryProvider {
as _i6.Future<void>);
@override
Map<String, String> getDefaultHeaders({bool? includeAuth = false}) =>
Map<String, String> getDefaultHeaders({
bool? includeAuth = false,
String? language,
}) =>
(super.noSuchMethod(
Invocation.method(#getDefaultHeaders, [], {
#includeAuth: includeAuth,
#language: language,
}),
returnValue: <String, String>{},
)
@@ -179,21 +183,26 @@ class MockGalleryProvider extends _i1.Mock implements _i4.GalleryProvider {
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: _i6.Future<dynamic>.value(),
)
as _i6.Future<dynamic>);
@override
_i6.Future<List<dynamic>> fetchPaginated(Uri? uri) =>
_i6.Future<List<dynamic>> fetchPaginated(Uri? uri, {String? language}) =>
(super.noSuchMethod(
Invocation.method(#fetchPaginated, [uri]),
Invocation.method(#fetchPaginated, [uri], {#language: language}),
returnValue: _i6.Future<List<dynamic>>.value(<dynamic>[]),
)
as _i6.Future<List<dynamic>>);

View File

@@ -141,10 +141,14 @@ class MockGalleryProvider extends _i1.Mock implements _i4.GalleryProvider {
as _i6.Future<void>);
@override
Map<String, String> getDefaultHeaders({bool? includeAuth = false}) =>
Map<String, String> getDefaultHeaders({
bool? includeAuth = false,
String? language,
}) =>
(super.noSuchMethod(
Invocation.method(#getDefaultHeaders, [], {
#includeAuth: includeAuth,
#language: language,
}),
returnValue: <String, String>{},
)
@@ -179,21 +183,26 @@ class MockGalleryProvider extends _i1.Mock implements _i4.GalleryProvider {
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: _i6.Future<dynamic>.value(),
)
as _i6.Future<dynamic>);
@override
_i6.Future<List<dynamic>> fetchPaginated(Uri? uri) =>
_i6.Future<List<dynamic>> fetchPaginated(Uri? uri, {String? language}) =>
(super.noSuchMethod(
Invocation.method(#fetchPaginated, [uri]),
Invocation.method(#fetchPaginated, [uri], {#language: language}),
returnValue: _i6.Future<List<dynamic>>.value(<dynamic>[]),
)
as _i6.Future<List<dynamic>>);

View File

@@ -78,10 +78,14 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider {
);
@override
Map<String, String> getDefaultHeaders({bool? includeAuth = false}) =>
Map<String, String> getDefaultHeaders({
bool? includeAuth = false,
String? language,
}) =>
(super.noSuchMethod(
Invocation.method(#getDefaultHeaders, [], {
#includeAuth: includeAuth,
#language: language,
}),
returnValue: <String, String>{},
)
@@ -116,21 +120,26 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider {
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: _i5.Future<dynamic>.value(),
)
as _i5.Future<dynamic>);
@override
_i5.Future<List<dynamic>> fetchPaginated(Uri? uri) =>
_i5.Future<List<dynamic>> fetchPaginated(Uri? uri, {String? language}) =>
(super.noSuchMethod(
Invocation.method(#fetchPaginated, [uri]),
Invocation.method(#fetchPaginated, [uri], {#language: language}),
returnValue: _i5.Future<List<dynamic>>.value(<dynamic>[]),
)
as _i5.Future<List<dynamic>>);

View File

@@ -88,10 +88,14 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider {
);
@override
Map<String, String> getDefaultHeaders({bool? includeAuth = false}) =>
Map<String, String> getDefaultHeaders({
bool? includeAuth = false,
String? language,
}) =>
(super.noSuchMethod(
Invocation.method(#getDefaultHeaders, [], {
#includeAuth: includeAuth,
#language: language,
}),
returnValue: <String, String>{},
)
@@ -126,21 +130,26 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider {
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: _i5.Future<dynamic>.value(),
)
as _i5.Future<dynamic>);
@override
_i5.Future<List<dynamic>> fetchPaginated(Uri? uri) =>
_i5.Future<List<dynamic>> fetchPaginated(Uri? uri, {String? language}) =>
(super.noSuchMethod(
Invocation.method(#fetchPaginated, [uri]),
Invocation.method(#fetchPaginated, [uri], {#language: language}),
returnValue: _i5.Future<List<dynamic>>.value(<dynamic>[]),
)
as _i5.Future<List<dynamic>>);

View File

@@ -323,10 +323,14 @@ class MockWgerBaseProvider extends _i1.Mock implements _i8.WgerBaseProvider {
);
@override
Map<String, String> getDefaultHeaders({bool? includeAuth = false}) =>
Map<String, String> getDefaultHeaders({
bool? includeAuth = false,
String? language,
}) =>
(super.noSuchMethod(
Invocation.method(#getDefaultHeaders, [], {
#includeAuth: includeAuth,
#language: language,
}),
returnValue: <String, String>{},
)
@@ -361,21 +365,26 @@ class MockWgerBaseProvider extends _i1.Mock implements _i8.WgerBaseProvider {
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: _i5.Future<dynamic>.value(),
)
as _i5.Future<dynamic>);
@override
_i5.Future<List<dynamic>> fetchPaginated(Uri? uri) =>
_i5.Future<List<dynamic>> fetchPaginated(Uri? uri, {String? language}) =>
(super.noSuchMethod(
Invocation.method(#fetchPaginated, [uri]),
Invocation.method(#fetchPaginated, [uri], {#language: language}),
returnValue: _i5.Future<List<dynamic>>.value(<dynamic>[]),
)
as _i5.Future<List<dynamic>>);

View File

@@ -167,10 +167,14 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider {
);
@override
Map<String, String> getDefaultHeaders({bool? includeAuth = false}) =>
Map<String, String> getDefaultHeaders({
bool? includeAuth = false,
String? language,
}) =>
(super.noSuchMethod(
Invocation.method(#getDefaultHeaders, [], {
#includeAuth: includeAuth,
#language: language,
}),
returnValue: <String, String>{},
)
@@ -205,21 +209,26 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider {
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: _i20.Future<dynamic>.value(),
)
as _i20.Future<dynamic>);
@override
_i20.Future<List<dynamic>> fetchPaginated(Uri? uri) =>
_i20.Future<List<dynamic>> fetchPaginated(Uri? uri, {String? language}) =>
(super.noSuchMethod(
Invocation.method(#fetchPaginated, [uri]),
Invocation.method(#fetchPaginated, [uri], {#language: language}),
returnValue: _i20.Future<List<dynamic>>.value(<dynamic>[]),
)
as _i20.Future<List<dynamic>>);

View File

@@ -1,13 +1,13 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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<NavigatorState>();
return ChangeNotifierProvider<RoutinesProvider>(
create: (context) => mockRoutinesProvider,
child: MaterialApp(
locale: Locale(locale),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
navigatorKey: key,
home: TextButton(
onPressed: () => key.currentState!.push(
MaterialPageRoute<void>(
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<RoutinesProvider>(
create: (context) => mockRoutinesProvider,
child: MaterialApp(
locale: Locale(locale),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
navigatorKey: key,
home: TextButton(
onPressed: () => key.currentState!.push(
MaterialPageRoute<void>(
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(),
},
),
);
}

View File

@@ -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<String>(
this,
Invocation.getter(#trophiesPath),
),
)
as String);
@override
String get userTrophiesPath =>
(super.noSuchMethod(
Invocation.getter(#userTrophiesPath),
returnValue: _i16.dummyValue<String>(
this,
Invocation.getter(#userTrophiesPath),
),
)
as String);
@override
String get userTrophyProgressionPath =>
(super.noSuchMethod(
Invocation.getter(#userTrophyProgressionPath),
returnValue: _i16.dummyValue<String>(
this,
Invocation.getter(#userTrophyProgressionPath),
),
)
as String);
@override
_i13.Future<List<_i19.Trophy>> fetchTrophies({String? language}) =>
(super.noSuchMethod(
Invocation.method(#fetchTrophies, [], {#language: language}),
returnValue: _i13.Future<List<_i19.Trophy>>.value(<_i19.Trophy>[]),
)
as _i13.Future<List<_i19.Trophy>>);
@override
_i13.Future<List<_i20.UserTrophy>> fetchUserTrophies({
Map<String, String>? filterQuery,
String? language,
}) =>
(super.noSuchMethod(
Invocation.method(#fetchUserTrophies, [], {
#filterQuery: filterQuery,
#language: language,
}),
returnValue: _i13.Future<List<_i20.UserTrophy>>.value(
<_i20.UserTrophy>[],
),
)
as _i13.Future<List<_i20.UserTrophy>>);
@override
_i13.Future<List<_i21.UserTrophyProgression>> fetchProgression({
Map<String, String>? filterQuery,
String? language,
}) =>
(super.noSuchMethod(
Invocation.method(#fetchProgression, [], {
#filterQuery: filterQuery,
#language: language,
}),
returnValue: _i13.Future<List<_i21.UserTrophyProgression>>.value(
<_i21.UserTrophyProgression>[],
),
)
as _i13.Future<List<_i21.UserTrophyProgression>>);
@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>);
}

View File

@@ -78,10 +78,14 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider {
);
@override
Map<String, String> getDefaultHeaders({bool? includeAuth = false}) =>
Map<String, String> getDefaultHeaders({
bool? includeAuth = false,
String? language,
}) =>
(super.noSuchMethod(
Invocation.method(#getDefaultHeaders, [], {
#includeAuth: includeAuth,
#language: language,
}),
returnValue: <String, String>{},
)
@@ -116,21 +120,26 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider {
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: _i5.Future<dynamic>.value(),
)
as _i5.Future<dynamic>);
@override
_i5.Future<List<dynamic>> fetchPaginated(Uri? uri) =>
_i5.Future<List<dynamic>> fetchPaginated(Uri? uri, {String? language}) =>
(super.noSuchMethod(
Invocation.method(#fetchPaginated, [uri]),
Invocation.method(#fetchPaginated, [uri], {#language: language}),
returnValue: _i5.Future<List<dynamic>>.value(<dynamic>[]),
)
as _i5.Future<List<dynamic>>);

View File

@@ -117,10 +117,14 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider {
);
@override
Map<String, String> getDefaultHeaders({bool? includeAuth = false}) =>
Map<String, String> getDefaultHeaders({
bool? includeAuth = false,
String? language,
}) =>
(super.noSuchMethod(
Invocation.method(#getDefaultHeaders, [], {
#includeAuth: includeAuth,
#language: language,
}),
returnValue: <String, String>{},
)
@@ -155,21 +159,26 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider {
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: _i11.Future<dynamic>.value(),
)
as _i11.Future<dynamic>);
@override
_i11.Future<List<dynamic>> fetchPaginated(Uri? uri) =>
_i11.Future<List<dynamic>> fetchPaginated(Uri? uri, {String? language}) =>
(super.noSuchMethod(
Invocation.method(#fetchPaginated, [uri]),
Invocation.method(#fetchPaginated, [uri], {#language: language}),
returnValue: _i11.Future<List<dynamic>>.value(<dynamic>[]),
)
as _i11.Future<List<dynamic>>);

View File

@@ -78,10 +78,14 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider {
);
@override
Map<String, String> getDefaultHeaders({bool? includeAuth = false}) =>
Map<String, String> getDefaultHeaders({
bool? includeAuth = false,
String? language,
}) =>
(super.noSuchMethod(
Invocation.method(#getDefaultHeaders, [], {
#includeAuth: includeAuth,
#language: language,
}),
returnValue: <String, String>{},
)
@@ -116,21 +120,26 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider {
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: _i5.Future<dynamic>.value(),
)
as _i5.Future<dynamic>);
@override
_i5.Future<List<dynamic>> fetchPaginated(Uri? uri) =>
_i5.Future<List<dynamic>> fetchPaginated(Uri? uri, {String? language}) =>
(super.noSuchMethod(
Invocation.method(#fetchPaginated, [uri]),
Invocation.method(#fetchPaginated, [uri], {#language: language}),
returnValue: _i5.Future<List<dynamic>>.value(<dynamic>[]),
)
as _i5.Future<List<dynamic>>);

View File

@@ -0,0 +1,85 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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 <http://www.gnu.org/licenses/>.
*/
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<Trophy> 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<UserTrophyProgression> getUserTrophyProgression() {
final trophies = getTestTrophies();
return [
UserTrophyProgression(
trophy: trophies[0],
progress: 100,
isEarned: true,
earnedAt: DateTime(2025, 12, 20),
currentValue: null,
targetValue: null,
progressDisplay: null,
),
UserTrophyProgression(
trophy: trophies[1],
progress: 40,
isEarned: false,
earnedAt: null,
currentValue: 12,
targetValue: 30,
progressDisplay: '12 / 30',
),
];
}
List<UserTrophy> getUserTrophies() {
final trophies = getTestTrophies();
return [
UserTrophy(
id: 4,
earnedAt: DateTime(2025, 12, 20),
isNotified: true,
progress: 100,
trophy: trophies[0],
),
];
}

View File

@@ -0,0 +1,72 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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 <http://www.gnu.org/licenses/>.
*/
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);
});
});
}

View File

@@ -0,0 +1,97 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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 <http://www.gnu.org/licenses/>.
*/
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');
});
});
}

View File

@@ -0,0 +1,99 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* 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 <http://www.gnu.org/licenses/>.
*/
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:wger/providers/base_provider.dart';
import 'package:wger/providers/trophies.dart';
import 'trophies_provider_test.mocks.dart';
const trophyJson = {
'id': 1,
'uuid': '550e8400-e29b-41d4-a716-446655440000',
'name': 'First Steps',
'description': 'Awarded for the first workout',
'image': 'https://example.org/trophy.png',
'trophy_type': 'count',
'is_hidden': false,
'is_progressive': true,
};
@GenerateMocks([WgerBaseProvider])
void main() {
group('Trophy repository', () {
test('fetches list of trophies', () async {
// Arrange
final mockBase = MockWgerBaseProvider();
when(mockBase.fetchPaginated(any)).thenAnswer((_) async => [trophyJson]);
when(
mockBase.makeUrl(
any,
id: anyNamed('id'),
objectMethod: anyNamed('objectMethod'),
query: anyNamed('query'),
),
).thenReturn(Uri.parse('https://example.org/trophies'));
final repository = TrophyRepository(mockBase);
// Act
final result = await repository.fetchTrophies();
// Assert
expect(result, isA<List>());
expect(result, hasLength(1));
final trophy = result.first;
expect(trophy.id, 1);
expect(trophy.name, 'First Steps');
expect(trophy.type.toString(), contains('count'));
});
test('fetches list of user trophy progression', () async {
// Arrange
final progressionJson = {
'trophy': trophyJson,
'is_earned': true,
'earned_at': '2020-01-02T15:04:05Z',
'progress': 42.5,
'current_value': '12.5',
'target_value': '100',
'progress_display': '12.5/100',
};
final mockBase = MockWgerBaseProvider();
when(mockBase.fetch(any)).thenAnswer((_) async => [progressionJson]);
when(mockBase.makeUrl(any)).thenReturn(Uri.parse('https://example.org/user_progressions'));
final repository = TrophyRepository(mockBase);
// Act
final result = await repository.fetchProgression();
// Assert
expect(result, isA<List>());
expect(result, hasLength(1));
final p = result.first;
expect(p.isEarned, isTrue);
expect(p.progress, 42.5);
expect(p.currentValue, 12.5);
expect(p.progressDisplay, '12.5/100');
verify(mockBase.fetch(any)).called(1);
});
});
}

View File

@@ -0,0 +1,182 @@
// Mocks generated by Mockito 5.4.6 from annotations
// in wger/test/trophies/provider/trophies_provider_test.dart.
// Do not manually edit this file.
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:async' as _i5;
import 'package:http/http.dart' as _i3;
import 'package:mockito/mockito.dart' as _i1;
import 'package:wger/providers/auth.dart' as _i2;
import 'package:wger/providers/base_provider.dart' as _i4;
// ignore_for_file: type=lint
// ignore_for_file: avoid_redundant_argument_values
// ignore_for_file: avoid_setters_without_getters
// ignore_for_file: comment_references
// ignore_for_file: deprecated_member_use
// ignore_for_file: deprecated_member_use_from_same_package
// ignore_for_file: implementation_imports
// ignore_for_file: invalid_use_of_visible_for_testing_member
// ignore_for_file: must_be_immutable
// ignore_for_file: prefer_const_constructors
// ignore_for_file: unnecessary_parenthesis
// ignore_for_file: camel_case_types
// ignore_for_file: subtype_of_sealed_class
// ignore_for_file: invalid_use_of_internal_member
class _FakeAuthProvider_0 extends _i1.SmartFake implements _i2.AuthProvider {
_FakeAuthProvider_0(Object parent, Invocation parentInvocation) : super(parent, parentInvocation);
}
class _FakeClient_1 extends _i1.SmartFake implements _i3.Client {
_FakeClient_1(Object parent, Invocation parentInvocation) : super(parent, parentInvocation);
}
class _FakeUri_2 extends _i1.SmartFake implements Uri {
_FakeUri_2(Object parent, Invocation parentInvocation) : super(parent, parentInvocation);
}
class _FakeResponse_3 extends _i1.SmartFake implements _i3.Response {
_FakeResponse_3(Object parent, Invocation parentInvocation) : super(parent, parentInvocation);
}
/// A class which mocks [WgerBaseProvider].
///
/// See the documentation for Mockito's code generation for more information.
class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider {
MockWgerBaseProvider() {
_i1.throwOnMissingStub(this);
}
@override
_i2.AuthProvider get auth =>
(super.noSuchMethod(
Invocation.getter(#auth),
returnValue: _FakeAuthProvider_0(this, Invocation.getter(#auth)),
)
as _i2.AuthProvider);
@override
_i3.Client get client =>
(super.noSuchMethod(
Invocation.getter(#client),
returnValue: _FakeClient_1(this, Invocation.getter(#client)),
)
as _i3.Client);
@override
set auth(_i2.AuthProvider? value) => super.noSuchMethod(
Invocation.setter(#auth, value),
returnValueForMissingStub: null,
);
@override
set client(_i3.Client? value) => super.noSuchMethod(
Invocation.setter(#client, value),
returnValueForMissingStub: null,
);
@override
Map<String, String> getDefaultHeaders({
bool? includeAuth = false,
String? language,
}) =>
(super.noSuchMethod(
Invocation.method(#getDefaultHeaders, [], {
#includeAuth: includeAuth,
#language: language,
}),
returnValue: <String, String>{},
)
as Map<String, String>);
@override
Uri makeUrl(
String? path, {
int? id,
String? objectMethod,
Map<String, dynamic>? 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<dynamic> fetch(
Uri? uri, {
int? maxRetries = 3,
Duration? initialDelay = const Duration(milliseconds: 250),
String? language,
}) =>
(super.noSuchMethod(
Invocation.method(
#fetch,
[uri],
{
#maxRetries: maxRetries,
#initialDelay: initialDelay,
#language: language,
},
),
returnValue: _i5.Future<dynamic>.value(),
)
as _i5.Future<dynamic>);
@override
_i5.Future<List<dynamic>> fetchPaginated(Uri? uri, {String? language}) =>
(super.noSuchMethod(
Invocation.method(#fetchPaginated, [uri], {#language: language}),
returnValue: _i5.Future<List<dynamic>>.value(<dynamic>[]),
)
as _i5.Future<List<dynamic>>);
@override
_i5.Future<Map<String, dynamic>> post(Map<String, dynamic>? data, Uri? uri) =>
(super.noSuchMethod(
Invocation.method(#post, [data, uri]),
returnValue: _i5.Future<Map<String, dynamic>>.value(
<String, dynamic>{},
),
)
as _i5.Future<Map<String, dynamic>>);
@override
_i5.Future<Map<String, dynamic>> patch(
Map<String, dynamic>? data,
Uri? uri,
) =>
(super.noSuchMethod(
Invocation.method(#patch, [data, uri]),
returnValue: _i5.Future<Map<String, dynamic>>.value(
<String, dynamic>{},
),
)
as _i5.Future<Map<String, dynamic>>);
@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>);
}

View File

@@ -0,0 +1,79 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (c) 2026 wger Team
*
* wger Workout Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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() {
Future<void> pumpOverview(WidgetTester tester, [List<Override> 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(),
trophies: getTestTrophies(),
),
),
],
);
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('Trophies'), findsOneWidget);
expect(find.text('You have no trophies yet'), findsOneWidget);
});
});
}

View File

@@ -0,0 +1,62 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (c) 2026 wger Team
*
* wger Workout Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
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';
import '../../test_data/trophies.dart';
void main() {
testWidgets('TrophiesOverview shows trophies', (WidgetTester tester) async {
// Act
await mockNetworkImagesFor(() async {
await tester.pumpWidget(
ProviderScope(
overrides: [
trophyStateProvider.overrideWithValue(
TrophyState(
trophyProgression: getUserTrophyProgression(),
userTrophies: getUserTrophies(),
trophies: getTestTrophies(),
),
),
],
child: const MaterialApp(
locale: Locale('en'),
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
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);
});
});
}

View File

@@ -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,
);
});
});
}

View File

@@ -78,10 +78,14 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider {
);
@override
Map<String, String> getDefaultHeaders({bool? includeAuth = false}) =>
Map<String, String> getDefaultHeaders({
bool? includeAuth = false,
String? language,
}) =>
(super.noSuchMethod(
Invocation.method(#getDefaultHeaders, [], {
#includeAuth: includeAuth,
#language: language,
}),
returnValue: <String, String>{},
)
@@ -116,21 +120,26 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider {
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: _i5.Future<dynamic>.value(),
)
as _i5.Future<dynamic>);
@override
_i5.Future<List<dynamic>> fetchPaginated(Uri? uri) =>
_i5.Future<List<dynamic>> fetchPaginated(Uri? uri, {String? language}) =>
(super.noSuchMethod(
Invocation.method(#fetchPaginated, [uri]),
Invocation.method(#fetchPaginated, [uri], {#language: language}),
returnValue: _i5.Future<List<dynamic>>.value(<dynamic>[]),
)
as _i5.Future<List<dynamic>>);

View File

@@ -78,10 +78,14 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider {
);
@override
Map<String, String> getDefaultHeaders({bool? includeAuth = false}) =>
Map<String, String> getDefaultHeaders({
bool? includeAuth = false,
String? language,
}) =>
(super.noSuchMethod(
Invocation.method(#getDefaultHeaders, [], {
#includeAuth: includeAuth,
#language: language,
}),
returnValue: <String, String>{},
)
@@ -116,21 +120,26 @@ class MockWgerBaseProvider extends _i1.Mock implements _i4.WgerBaseProvider {
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: _i5.Future<dynamic>.value(),
)
as _i5.Future<dynamic>);
@override
_i5.Future<List<dynamic>> fetchPaginated(Uri? uri) =>
_i5.Future<List<dynamic>> fetchPaginated(Uri? uri, {String? language}) =>
(super.noSuchMethod(
Invocation.method(#fetchPaginated, [uri]),
Invocation.method(#fetchPaginated, [uri], {#language: language}),
returnValue: _i5.Future<List<dynamic>>.value(<dynamic>[]),
)
as _i5.Future<List<dynamic>>);