Start adding support for trophies

This commit is contained in:
Roland Geider
2025-12-20 00:31:18 +01:00
parent 0d0a5d8e02
commit 5d39ae5088
13 changed files with 998 additions and 44 deletions

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) 2025 wger Team
*
* wger Workout Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* wger Workout Manager is distributed in the hope that it will be useful,
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
@@ -35,6 +35,7 @@ import 'package:wger/providers/measurement.dart';
import 'package:wger/providers/nutrition.dart';
import 'package:wger/providers/routines.dart';
import 'package:wger/providers/user.dart';
import 'package:wger/providers/wger_base_riverpod.dart';
import 'package:wger/screens/add_exercise_screen.dart';
import 'package:wger/screens/auth_screen.dart';
import 'package:wger/screens/configure_plates_screen.dart';
@@ -129,7 +130,7 @@ void main() async {
};
// Application
runApp(const riverpod.ProviderScope(child: MainApp()));
runApp(const MainApp());
}
class MainApp extends StatelessWidget {
@@ -217,46 +218,60 @@ class MainApp extends StatelessWidget {
),
],
child: Consumer<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(),
},
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(),
},
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,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/>.
*/
// 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,64 @@
/*
* 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: utcIso8601ToLocalDate)
final DateTime earnedAt;
/// Progress towards earning the trophy (0-100%)
@JsonKey(required: true)
final num progress;
@JsonKey(required: true, name: 'current_value', fromJson: stringToNumNull)
num? currentValue;
@JsonKey(required: true, name: 'target_value', fromJson: stringToNumNull)
num? targetValue;
@JsonKey(required: true, name: 'progress_display')
String? progressDisplay;
UserTrophyProgression({
required this.trophy,
required this.isEarned,
required this.earnedAt,
required this.progress,
required this.currentValue,
required this.targetValue,
this.progressDisplay,
});
// Boilerplate
factory UserTrophyProgression.fromJson(Map<String, dynamic> json) =>
_$UserTrophyProgressionFromJson(json);
Map<String, dynamic> toJson() => _$UserTrophyProgressionToJson(this);
}

View File

@@ -0,0 +1,63 @@
/*
* 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/>.
*/
// 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: utcIso8601ToLocalDate(json['earned_at'] as String),
progress: json['progress'] as num,
currentValue: stringToNumNull(json['current_value'] as String?),
targetValue: stringToNumNull(json['target_value'] as String?),
progressDisplay: json['progress_display'] as String?,
);
}
Map<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,3 +1,21 @@
/*
* 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/>.
*/
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'session.dart';
@@ -23,7 +41,9 @@ WorkoutSession _$WorkoutSessionFromJson(Map<String, dynamic> json) {
id: (json['id'] as num?)?.toInt(),
dayId: (json['day'] as num?)?.toInt(),
routineId: (json['routine'] as num?)?.toInt(),
impression: json['impression'] == null ? 2 : int.parse(json['impression'] as String),
impression: json['impression'] == null
? DEFAULT_IMPRESSION
: int.parse(json['impression'] as String),
notes: json['notes'] as String? ?? '',
timeStart: stringToTimeNull(json['time_start'] as String?),
timeEnd: stringToTimeNull(json['time_end'] as String?),

View File

@@ -0,0 +1,51 @@
/*
* 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:riverpod_annotation/riverpod_annotation.dart';
import 'package:wger/helpers/consts.dart';
import 'package:wger/models/trophies/trophy.dart';
import 'package:wger/models/trophies/user_trophy_progression.dart';
import 'package:wger/providers/wger_base_riverpod.dart';
part 'trophies.g.dart';
@riverpod
Future<List<Trophy>> trophies(Ref ref) async {
const trophiesPath = 'trophy';
final baseProvider = ref.read(wgerBaseProvider);
final trophyData = await baseProvider.fetchPaginated(
baseProvider.makeUrl(trophiesPath, query: {'limit': API_MAX_PAGE_SIZE}),
);
return trophyData.map((e) => Trophy.fromJson(e)).toList();
}
@riverpod
Future<List<UserTrophyProgression>> trophyProgression(Ref ref) async {
const userTrophyProgressionPath = 'trophy/progress';
final baseProvider = ref.read(wgerBaseProvider);
final trophyData = await baseProvider.fetchPaginated(
baseProvider.makeUrl(userTrophyProgressionPath, query: {'limit': API_MAX_PAGE_SIZE}),
);
return trophyData.map((e) => UserTrophyProgression.fromJson(e)).toList();
}

View File

@@ -0,0 +1,103 @@
/*
* 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/>.
*/
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'trophies.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
@ProviderFor(trophies)
const trophiesProvider = TrophiesProvider._();
final class TrophiesProvider
extends $FunctionalProvider<AsyncValue<List<Trophy>>, List<Trophy>, FutureOr<List<Trophy>>>
with $FutureModifier<List<Trophy>>, $FutureProvider<List<Trophy>> {
const TrophiesProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'trophiesProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$trophiesHash();
@$internal
@override
$FutureProviderElement<List<Trophy>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<Trophy>> create(Ref ref) {
return trophies(ref);
}
}
String _$trophiesHash() => r'44dd5e9a820f4e37599daac2a49a9358386758a8';
@ProviderFor(trophyProgression)
const trophyProgressionProvider = TrophyProgressionProvider._();
final class TrophyProgressionProvider
extends
$FunctionalProvider<
AsyncValue<List<UserTrophyProgression>>,
List<UserTrophyProgression>,
FutureOr<List<UserTrophyProgression>>
>
with
$FutureModifier<List<UserTrophyProgression>>,
$FutureProvider<List<UserTrophyProgression>> {
const TrophyProgressionProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'trophyProgressionProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$trophyProgressionHash();
@$internal
@override
$FutureProviderElement<List<UserTrophyProgression>> $createElement(
$ProviderPointer pointer,
) => $FutureProviderElement(pointer);
@override
FutureOr<List<UserTrophyProgression>> create(Ref ref) {
return trophyProgression(ref);
}
}
String _$trophyProgressionHash() => r'444caf04f3d0a7845e840d452e4b4a822b59df9b';

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

@@ -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,117 @@
/*
* 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_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:wger/providers/base_provider.dart';
import 'package:wger/providers/trophies.dart';
import 'package:wger/providers/wger_base_riverpod.dart';
import 'trophies_provider_test.mocks.dart';
const trophyJson = {
'id': 1,
'uuid': '550e8400-e29b-41d4-a716-446655440000',
'name': 'First Steps',
'description': 'Awarded for the first workout',
'image': 'https://example.org/trophy.png',
'trophy_type': 'count',
'is_hidden': false,
'is_progressive': true,
};
@GenerateMocks([WgerBaseProvider])
void main() {
group('Trophies providers', () {
test('trophies provider returns list of Trophy models', () async {
// Arrange
final mockBase = MockWgerBaseProvider();
when(mockBase.fetchPaginated(any)).thenAnswer((_) async => [trophyJson]);
when(
mockBase.makeUrl(
any,
id: anyNamed('id'),
objectMethod: anyNamed('objectMethod'),
query: anyNamed('query'),
),
).thenReturn(Uri.parse('https://example.org/trophies'));
final container = ProviderContainer.test(
overrides: [
wgerBaseProvider.overrideWithValue(mockBase),
],
);
// Act
final result = await container.read(trophiesProvider.future);
// Assert
expect(result, isA<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('trophyProgression provider returns list of UserTrophyProgression models', () async {
// Arrange
final progressionJson = {
'trophy': trophyJson,
'is_earned': true,
'earned_at': '2020-01-02T15:04:05Z',
'progress': 42.5,
'current_value': '12.5',
'target_value': '100',
'progress_display': '12.5/100',
};
final mockBase = MockWgerBaseProvider();
when(mockBase.fetchPaginated(any)).thenAnswer((_) async => [progressionJson]);
when(
mockBase.makeUrl(
any,
id: anyNamed('id'),
objectMethod: anyNamed('objectMethod'),
query: anyNamed('query'),
),
).thenReturn(Uri.parse('https://example.org/user_progressions'));
final container = ProviderContainer.test(
overrides: [
wgerBaseProvider.overrideWithValue(mockBase),
],
);
// Act
final result = await container.read(trophyProgressionProvider.future);
// Assert
expect(result, isA<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.fetchPaginated(any)).called(1);
});
});
}

View File

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