Also show the PR trophies in the regular log page

This commit is contained in:
Roland Geider
2026-01-10 14:02:23 +01:00
parent 696b3333fb
commit 352a4b1c49
17 changed files with 377 additions and 486 deletions

View File

@@ -1,6 +1,6 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (c) 2025 wger Team
* Copyright (c) 2026 wger Team
*
* wger Workout Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@@ -28,6 +28,38 @@ import 'base_provider.dart';
part 'trophies.g.dart';
class TrophyState {
final _logger = Logger('TrophyState');
final List<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');
@@ -96,40 +128,52 @@ TrophyRepository trophyRepository(Ref ref) {
@Riverpod(keepAlive: true)
final class TrophyStateNotifier extends _$TrophyStateNotifier {
final _logger = Logger('TrophyStateNotifier');
@override
void build() {}
TrophyState build() {
return TrophyState();
}
Future<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() async {
Future<List<Trophy>> fetchTrophies({String? language}) async {
_logger.finer('Fetching trophies');
final repo = ref.read(trophyRepositoryProvider);
return repo.fetchTrophies();
final result = await repo.fetchTrophies(language: language);
state = state.copyWith(trophies: result);
return result;
}
/// Fetch trophies awarded to the user.
/// Excludes hidden trophies as well as repeatable (PR) trophies since they are
/// handled separately
/// Fetch trophies awarded to the user, excludes hidden trophies
Future<List<UserTrophy>> fetchUserTrophies({String? language}) async {
final repo = ref.read(trophyRepositoryProvider);
return repo.fetchUserTrophies(
filterQuery: {'trophy__is_hidden': 'false', 'trophy__is_repeatable': 'false'},
language: language,
);
}
_logger.finer('Fetching user trophies');
/// Fetch PR trophies awarded to the user
Future<List<UserTrophy>> fetchUserPRTrophies({String? language}) async {
final repo = ref.read(trophyRepositoryProvider);
return repo.fetchUserTrophies(
filterQuery: {'trophy__trophy_type': TrophyType.pr.name},
final result = await repo.fetchUserTrophies(
filterQuery: {'trophy__is_hidden': 'false'}, //'trophy__is_repeatable': 'false'
language: language,
);
state = state.copyWith(userTrophies: result);
return result;
}
/// Fetch trophy progression for the user
Future<List<UserTrophyProgression>> fetchTrophyProgression({String? language}) async {
final repo = ref.read(trophyRepositoryProvider);
_logger.finer('Fetching user trophy progression');
// Note that repeatable trophies are filtered out in the backend
return repo.fetchProgression(language: language);
final repo = ref.read(trophyRepositoryProvider);
final result = await repo.fetchProgression(language: language);
state = state.copyWith(trophyProgression: result);
return result;
}
}

View File

@@ -53,7 +53,8 @@ String _$trophyRepositoryHash() => r'0699f0c0f7f324f3ba9b21420d9845a3e3096b61';
@ProviderFor(TrophyStateNotifier)
const trophyStateProvider = TrophyStateNotifierProvider._();
final class TrophyStateNotifierProvider extends $NotifierProvider<TrophyStateNotifier, void> {
final class TrophyStateNotifierProvider
extends $NotifierProvider<TrophyStateNotifier, TrophyState> {
const TrophyStateNotifierProvider._()
: super(
from: null,
@@ -73,25 +74,31 @@ final class TrophyStateNotifierProvider extends $NotifierProvider<TrophyStateNot
TrophyStateNotifier create() => TrophyStateNotifier();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(void value) {
Override overrideWithValue(TrophyState value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<void>(value),
providerOverride: $SyncValueProvider<TrophyState>(value),
);
}
}
String _$trophyStateNotifierHash() => r'47b4babf337bbb8bf60142ebe59dc760fa08dce3';
String _$trophyStateNotifierHash() => r'c80c732272cf843b698f28152f60b9a5f37ee449';
abstract class _$TrophyStateNotifier extends $Notifier<void> {
void build();
abstract class _$TrophyStateNotifier extends $Notifier<TrophyState> {
TrophyState build();
@$mustCallSuper
@override
void runBuild() {
build();
final ref = this.ref as $Ref<void, void>;
final created = build();
final ref = this.ref as $Ref<TrophyState, TrophyState>;
final element =
ref.element as $ClassProviderElement<AnyNotifier<void, void>, void, Object?, Object?>;
element.handleValue(ref, null);
ref.element
as $ClassProviderElement<
AnyNotifier<TrophyState, TrophyState>,
TrophyState,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}

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

@@ -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
@@ -21,64 +21,45 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:wger/models/trophies/trophy.dart';
import 'package:wger/providers/trophies.dart';
import 'package:wger/screens/trophy_screen.dart';
import 'package:wger/widgets/core/progress_indicator.dart';
class DashboardTrophiesWidget extends ConsumerWidget {
const DashboardTrophiesWidget();
@override
Widget build(BuildContext context, WidgetRef ref) {
final provider = ref.watch(trophyStateProvider.notifier);
final languageCode = Localizations.localeOf(context).languageCode;
final trophiesState = ref.read(trophyStateProvider);
return FutureBuilder(
future: provider.fetchUserTrophies(language: languageCode),
builder: (context, asyncSnapshot) {
if (asyncSnapshot.connectionState != ConnectionState.done) {
return const Card(child: BoxedProgressIndicator());
}
return Card(
color: Colors.transparent,
shadowColor: Colors.transparent,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (trophiesState.nonPrTrophies.isEmpty)
Padding(
padding: const EdgeInsets.all(16.0),
child: Text('No trophies yet', style: Theme.of(context).textTheme.bodyMedium),
)
else
SizedBox(
height: 140,
child: ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
scrollDirection: Axis.horizontal,
itemCount: trophiesState.nonPrTrophies.length,
separatorBuilder: (context, index) => const SizedBox(width: 12),
itemBuilder: (context, index) {
final userTrophy = trophiesState.nonPrTrophies[index];
final userTrophies = asyncSnapshot.data ?? [];
return Card(
color: Colors.transparent,
shadowColor: Colors.transparent,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ListTile(
// title: Text(
// 'Trophies',
// style: Theme.of(context).textTheme.headlineSmall,
// ),
// ),
if (userTrophies.isEmpty)
Padding(
padding: const EdgeInsets.all(16.0),
child: Text('No trophies yet', style: Theme.of(context).textTheme.bodyMedium),
)
else
SizedBox(
height: 140,
child: ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0),
scrollDirection: Axis.horizontal,
itemCount: userTrophies.length,
separatorBuilder: (context, index) => const SizedBox(width: 12),
itemBuilder: (context, index) {
final userTrophy = userTrophies[index];
return SizedBox(
width: 220,
child: TrophyCard(trophy: userTrophy.trophy),
);
},
),
),
],
),
);
},
return SizedBox(
width: 220,
child: TrophyCard(trophy: userTrophy.trophy),
);
},
),
),
],
),
);
}
}

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

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
@@ -49,7 +49,6 @@ class WorkoutSummary extends ConsumerStatefulWidget {
class _WorkoutSummaryState extends ConsumerState<WorkoutSummary> {
late Future<void> _initData;
late Routine _routine;
late List<UserTrophy> _userPrTrophies;
bool _didInit = false;
@override
@@ -76,11 +75,13 @@ class _WorkoutSummaryState extends ConsumerState<WorkoutSummary> {
);
final trophyNotifier = ref.read(trophyStateProvider.notifier);
_userPrTrophies = await trophyNotifier.fetchUserPRTrophies(language: languageCode);
await trophyNotifier.fetchUserTrophies(language: languageCode);
}
@override
Widget build(BuildContext context) {
final trophyState = ref.watch(trophyStateProvider);
return Column(
children: [
NavigationHeader(
@@ -102,10 +103,8 @@ class _WorkoutSummaryState extends ConsumerState<WorkoutSummary> {
final apiSession = _routine.sessions.firstWhereOrNull(
(s) => s.session.date.isSameDayAs(clock.now()),
);
final userTrophies = _userPrTrophies
.where(
(t) => t.contextData!.sessionId == apiSession?.session.id,
)
final userTrophies = trophyState.prTrophies
.where((t) => t.contextData?.sessionId == apiSession?.session.id)
.toList();
return WorkoutSessionStats(

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,59 @@
*/
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:logging/logging.dart';
import 'package:wger/helpers/date.dart';
import 'package:wger/helpers/errors.dart';
import 'package:wger/l10n/generated/app_localizations.dart';
import 'package:wger/models/workouts/routine.dart';
import 'package:wger/providers/trophies.dart';
import '../gym_mode/summary.dart';
import 'exercise_log_chart.dart';
import 'muscle_groups.dart';
import 'session_info.dart';
class DayLogWidget extends StatelessWidget {
class DayLogWidget extends ConsumerWidget {
final DateTime _date;
final Routine _routine;
final _logger = Logger('DayLogWidget');
const DayLogWidget(this._date, this._routine);
DayLogWidget(this._date, this._routine);
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final i18n = AppLocalizations.of(context);
final theme = Theme.of(context);
final trophyState = ref.read(trophyStateProvider);
final sessionApi = _routine.sessions.firstWhere(
(sessionApi) => sessionApi.session.date.isSameDayAs(_date),
);
final exercises = sessionApi.exercises;
final prTrophies = trophyState.prTrophies
.where((t) => t.contextData?.sessionId == sessionApi.session.id)
.toList();
_logger.info(trophyState.prTrophies);
_logger.info(prTrophies);
return Column(
spacing: 10,
children: [
Card(child: SessionInfo(sessionApi.session)),
if (prTrophies.isNotEmpty)
SizedBox(
width: double.infinity,
child: InfoCard(
title: i18n.personalRecords,
value: prTrophies.length.toString(),
color: theme.colorScheme.tertiaryContainer,
),
),
MuscleGroupsCard(sessionApi.logs),
Column(
spacing: 10,
children: [
@@ -66,7 +92,17 @@ class DayLogWidget extends StatelessWidget {
(log) => Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(log.repTextNoNl(context)),
Row(
children: [
if (prTrophies.any((t) => t.contextData?.logId == log.id))
Icon(
Icons.emoji_events,
color: theme.colorScheme.primary,
size: 20,
),
Text(log.repTextNoNl(context)),
],
),
IconButton(
icon: const Icon(Icons.delete),
key: ValueKey('delete-log-${log.id}'),

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

@@ -21,15 +21,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:wger/helpers/material.dart';
import 'package:wger/models/trophies/user_trophy_progression.dart';
import 'package:wger/providers/trophies.dart';
import 'package:wger/widgets/core/progress_indicator.dart';
class TrophiesOverview extends ConsumerWidget {
const TrophiesOverview({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final notifier = ref.watch(trophyStateProvider.notifier);
final languageCode = Localizations.localeOf(context).languageCode;
final trophyState = ref.watch(trophyStateProvider);
// Responsive grid: determine columns based on screen width
final width = MediaQuery.widthOf(context);
@@ -44,52 +42,32 @@ class TrophiesOverview extends ConsumerWidget {
crossAxisCount = 5;
}
return FutureBuilder<List<UserTrophyProgression>>(
future: notifier.fetchTrophyProgression(language: languageCode),
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
return const Card(child: BoxedProgressIndicator());
}
if (snapshot.hasError) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text('Error loading trophies', style: Theme.of(context).textTheme.bodyLarge),
),
);
}
final userTrophyProgression = snapshot.data ?? [];
// If empty, show placeholder
if (userTrophyProgression.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'No trophies yet',
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
),
);
}
return RepaintBoundary(
child: GridView.builder(
padding: const EdgeInsets.all(12),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
),
key: const ValueKey('trophy-grid'),
itemCount: userTrophyProgression.length,
itemBuilder: (context, index) {
return _TrophyCardImage(userProgression: userTrophyProgression[index]);
},
// If empty, show placeholder
if (trophyState.trophyProgression.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'No trophies yet',
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
);
},
),
);
}
return RepaintBoundary(
child: GridView.builder(
padding: const EdgeInsets.all(12),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
),
key: const ValueKey('trophy-grid'),
itemCount: trophyState.trophyProgression.length,
itemBuilder: (context, index) {
return _TrophyCardImage(userProgression: trophyState.trophyProgression[index]);
},
),
);
}
}

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

@@ -1,6 +1,6 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (c) 2025 wger Team
* Copyright (c) 2026 wger Team
*
* wger Workout Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@@ -19,33 +19,25 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:network_image_mock/network_image_mock.dart';
import 'package:wger/providers/trophies.dart';
import 'package:wger/widgets/dashboard/widgets/trophies.dart';
import '../../test_data/trophies.dart';
import 'dashboard_trophies_widget_test.mocks.dart';
@GenerateMocks([TrophyRepository])
void main() {
testWidgets('DashboardTrophiesWidget shows trophies', (WidgetTester tester) async {
// Arrange
final mockRepository = MockTrophyRepository();
when(
mockRepository.fetchUserTrophies(
filterQuery: anyNamed('filterQuery'),
language: anyNamed('language'),
),
).thenAnswer((_) async => getUserTrophies());
// Act
await mockNetworkImagesFor(() async {
await tester.pumpWidget(
ProviderScope(
overrides: [
trophyRepositoryProvider.overrideWithValue(mockRepository),
trophyStateProvider.overrideWithValue(
TrophyState(
userTrophies: getUserTrophies(),
trophies: getTestTrophies(),
),
),
],
child: const MaterialApp(
home: Scaffold(

View File

@@ -1,135 +0,0 @@
// Mocks generated by Mockito 5.4.6 from annotations
// in wger/test/trophies/widgets/dashboard_trophies_widget_test.dart.
// Do not manually edit this file.
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:async' as _i5;
import 'package:mockito/mockito.dart' as _i1;
import 'package:mockito/src/dummies.dart' as _i4;
import 'package:wger/models/trophies/trophy.dart' as _i6;
import 'package:wger/models/trophies/user_trophy.dart' as _i7;
import 'package:wger/models/trophies/user_trophy_progression.dart' as _i8;
import 'package:wger/providers/base_provider.dart' as _i2;
import 'package:wger/providers/trophies.dart' as _i3;
// ignore_for_file: type=lint
// ignore_for_file: avoid_redundant_argument_values
// ignore_for_file: avoid_setters_without_getters
// ignore_for_file: comment_references
// ignore_for_file: deprecated_member_use
// ignore_for_file: deprecated_member_use_from_same_package
// ignore_for_file: implementation_imports
// ignore_for_file: invalid_use_of_visible_for_testing_member
// ignore_for_file: must_be_immutable
// ignore_for_file: prefer_const_constructors
// ignore_for_file: unnecessary_parenthesis
// ignore_for_file: camel_case_types
// ignore_for_file: subtype_of_sealed_class
// ignore_for_file: invalid_use_of_internal_member
class _FakeWgerBaseProvider_0 extends _i1.SmartFake implements _i2.WgerBaseProvider {
_FakeWgerBaseProvider_0(Object parent, Invocation parentInvocation)
: super(parent, parentInvocation);
}
/// A class which mocks [TrophyRepository].
///
/// See the documentation for Mockito's code generation for more information.
class MockTrophyRepository extends _i1.Mock implements _i3.TrophyRepository {
MockTrophyRepository() {
_i1.throwOnMissingStub(this);
}
@override
_i2.WgerBaseProvider get base =>
(super.noSuchMethod(
Invocation.getter(#base),
returnValue: _FakeWgerBaseProvider_0(
this,
Invocation.getter(#base),
),
)
as _i2.WgerBaseProvider);
@override
String get trophiesPath =>
(super.noSuchMethod(
Invocation.getter(#trophiesPath),
returnValue: _i4.dummyValue<String>(
this,
Invocation.getter(#trophiesPath),
),
)
as String);
@override
String get userTrophiesPath =>
(super.noSuchMethod(
Invocation.getter(#userTrophiesPath),
returnValue: _i4.dummyValue<String>(
this,
Invocation.getter(#userTrophiesPath),
),
)
as String);
@override
String get userTrophyProgressionPath =>
(super.noSuchMethod(
Invocation.getter(#userTrophyProgressionPath),
returnValue: _i4.dummyValue<String>(
this,
Invocation.getter(#userTrophyProgressionPath),
),
)
as String);
@override
_i5.Future<List<_i6.Trophy>> fetchTrophies({String? language}) =>
(super.noSuchMethod(
Invocation.method(#fetchTrophies, [], {#language: language}),
returnValue: _i5.Future<List<_i6.Trophy>>.value(<_i6.Trophy>[]),
)
as _i5.Future<List<_i6.Trophy>>);
@override
_i5.Future<List<_i7.UserTrophy>> fetchUserTrophies({
Map<String, String>? filterQuery,
String? language,
}) =>
(super.noSuchMethod(
Invocation.method(#fetchUserTrophies, [], {
#filterQuery: filterQuery,
#language: language,
}),
returnValue: _i5.Future<List<_i7.UserTrophy>>.value(
<_i7.UserTrophy>[],
),
)
as _i5.Future<List<_i7.UserTrophy>>);
@override
_i5.Future<List<_i8.UserTrophyProgression>> fetchProgression({
Map<String, String>? filterQuery,
String? language,
}) =>
(super.noSuchMethod(
Invocation.method(#fetchProgression, [], {
#filterQuery: filterQuery,
#language: language,
}),
returnValue: _i5.Future<List<_i8.UserTrophyProgression>>.value(
<_i8.UserTrophyProgression>[],
),
)
as _i5.Future<List<_i8.UserTrophyProgression>>);
@override
List<_i6.Trophy> filterByType(List<_i6.Trophy>? list, _i6.TrophyType? type) =>
(super.noSuchMethod(
Invocation.method(#filterByType, [list, type]),
returnValue: <_i6.Trophy>[],
)
as List<_i6.Trophy>);
}

View File

@@ -1,6 +1,6 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (c) 2025 wger Team
* Copyright (c) 2026 wger Team
*
* wger Workout Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@@ -19,33 +19,26 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:network_image_mock/network_image_mock.dart';
import 'package:wger/providers/trophies.dart';
import 'package:wger/widgets/trophies/trophies_overview.dart';
import '../../test_data/trophies.dart';
import 'dashboard_trophies_widget_test.mocks.dart';
@GenerateMocks([TrophyRepository])
void main() {
testWidgets('TrophiesOverview shows trophies', (WidgetTester tester) async {
// Arrange
final mockRepository = MockTrophyRepository();
when(
mockRepository.fetchProgression(
filterQuery: anyNamed('filterQuery'),
language: anyNamed('language'),
),
).thenAnswer((_) async => getUserTrophyProgression());
// Act
await mockNetworkImagesFor(() async {
await tester.pumpWidget(
ProviderScope(
overrides: [
trophyRepositoryProvider.overrideWithValue(mockRepository),
trophyStateProvider.overrideWithValue(
TrophyState(
trophyProgression: getUserTrophyProgression(),
userTrophies: getUserTrophies(),
trophies: getTestTrophies(),
),
),
],
child: const MaterialApp(
home: Scaffold(body: TrophiesOverview()),

View File

@@ -1,135 +0,0 @@
// Mocks generated by Mockito 5.4.6 from annotations
// in wger/test/trophies/widgets/trophies_overview_test.dart.
// Do not manually edit this file.
// ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:async' as _i5;
import 'package:mockito/mockito.dart' as _i1;
import 'package:mockito/src/dummies.dart' as _i4;
import 'package:wger/models/trophies/trophy.dart' as _i6;
import 'package:wger/models/trophies/user_trophy.dart' as _i7;
import 'package:wger/models/trophies/user_trophy_progression.dart' as _i8;
import 'package:wger/providers/base_provider.dart' as _i2;
import 'package:wger/providers/trophies.dart' as _i3;
// ignore_for_file: type=lint
// ignore_for_file: avoid_redundant_argument_values
// ignore_for_file: avoid_setters_without_getters
// ignore_for_file: comment_references
// ignore_for_file: deprecated_member_use
// ignore_for_file: deprecated_member_use_from_same_package
// ignore_for_file: implementation_imports
// ignore_for_file: invalid_use_of_visible_for_testing_member
// ignore_for_file: must_be_immutable
// ignore_for_file: prefer_const_constructors
// ignore_for_file: unnecessary_parenthesis
// ignore_for_file: camel_case_types
// ignore_for_file: subtype_of_sealed_class
// ignore_for_file: invalid_use_of_internal_member
class _FakeWgerBaseProvider_0 extends _i1.SmartFake implements _i2.WgerBaseProvider {
_FakeWgerBaseProvider_0(Object parent, Invocation parentInvocation)
: super(parent, parentInvocation);
}
/// A class which mocks [TrophyRepository].
///
/// See the documentation for Mockito's code generation for more information.
class MockTrophyRepository extends _i1.Mock implements _i3.TrophyRepository {
MockTrophyRepository() {
_i1.throwOnMissingStub(this);
}
@override
_i2.WgerBaseProvider get base =>
(super.noSuchMethod(
Invocation.getter(#base),
returnValue: _FakeWgerBaseProvider_0(
this,
Invocation.getter(#base),
),
)
as _i2.WgerBaseProvider);
@override
String get trophiesPath =>
(super.noSuchMethod(
Invocation.getter(#trophiesPath),
returnValue: _i4.dummyValue<String>(
this,
Invocation.getter(#trophiesPath),
),
)
as String);
@override
String get userTrophiesPath =>
(super.noSuchMethod(
Invocation.getter(#userTrophiesPath),
returnValue: _i4.dummyValue<String>(
this,
Invocation.getter(#userTrophiesPath),
),
)
as String);
@override
String get userTrophyProgressionPath =>
(super.noSuchMethod(
Invocation.getter(#userTrophyProgressionPath),
returnValue: _i4.dummyValue<String>(
this,
Invocation.getter(#userTrophyProgressionPath),
),
)
as String);
@override
_i5.Future<List<_i6.Trophy>> fetchTrophies({String? language}) =>
(super.noSuchMethod(
Invocation.method(#fetchTrophies, [], {#language: language}),
returnValue: _i5.Future<List<_i6.Trophy>>.value(<_i6.Trophy>[]),
)
as _i5.Future<List<_i6.Trophy>>);
@override
_i5.Future<List<_i7.UserTrophy>> fetchUserTrophies({
Map<String, String>? filterQuery,
String? language,
}) =>
(super.noSuchMethod(
Invocation.method(#fetchUserTrophies, [], {
#filterQuery: filterQuery,
#language: language,
}),
returnValue: _i5.Future<List<_i7.UserTrophy>>.value(
<_i7.UserTrophy>[],
),
)
as _i5.Future<List<_i7.UserTrophy>>);
@override
_i5.Future<List<_i8.UserTrophyProgression>> fetchProgression({
Map<String, String>? filterQuery,
String? language,
}) =>
(super.noSuchMethod(
Invocation.method(#fetchProgression, [], {
#filterQuery: filterQuery,
#language: language,
}),
returnValue: _i5.Future<List<_i8.UserTrophyProgression>>.value(
<_i8.UserTrophyProgression>[],
),
)
as _i5.Future<List<_i8.UserTrophyProgression>>);
@override
List<_i6.Trophy> filterByType(List<_i6.Trophy>? list, _i6.TrophyType? type) =>
(super.noSuchMethod(
Invocation.method(#filterByType, [list, type]),
returnValue: <_i6.Trophy>[],
)
as List<_i6.Trophy>);
}

View File

@@ -1,6 +1,6 @@
/*
* This file is part of wger Workout Manager <https://github.com/wger-project>.
* Copyright (c) 2025 - 2025 wger Team
* Copyright (c) 2025 - 2026 wger Team
*
* wger Workout Manager is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
@@ -189,15 +189,15 @@ void main() {
testWidgets('save button calls addLog on RoutinesProvider', (tester) async {
// Arrange
final notifier = container.read(gymStateProvider.notifier);
final gymNotifier = container.read(gymStateProvider.notifier);
final routine = testdata.getTestRoutine();
notifier.state = notifier.state.copyWith(
gymNotifier.state = gymNotifier.state.copyWith(
dayId: routine.days.first.id,
routine: routine,
iteration: 1,
);
notifier.calculatePages();
notifier.state = notifier.state.copyWith(currentPage: 2);
gymNotifier.calculatePages();
gymNotifier.state = gymNotifier.state.copyWith(currentPage: 2);
final mockRoutines = MockRoutinesProvider();
// Act
@@ -229,10 +229,10 @@ void main() {
expect(capturedLog!.repetitions, equals(7));
expect(capturedLog!.weight, equals(77));
final currentSlotPage = notifier.state.getSlotEntryPageByIndex()!;
final currentSlotPage = gymNotifier.state.getSlotEntryPageByIndex()!;
expect(capturedLog!.slotEntryId, equals(currentSlotPage.setConfigData!.slotEntryId));
expect(capturedLog!.routineId, equals(notifier.state.routine.id));
expect(capturedLog!.iteration, equals(notifier.state.iteration));
expect(capturedLog!.routineId, equals(gymNotifier.state.routine.id));
expect(capturedLog!.iteration, equals(gymNotifier.state.iteration));
});
});
}