diff --git a/lib/providers/trophies.dart b/lib/providers/trophies.dart index 183a8fde..19289af5 100644 --- a/lib/providers/trophies.dart +++ b/lib/providers/trophies.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2025 wger Team + * Copyright (c) 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -28,6 +28,38 @@ import 'base_provider.dart'; part 'trophies.g.dart'; +class TrophyState { + final _logger = Logger('TrophyState'); + + final List trophies; + final List userTrophies; + final List trophyProgression; + + TrophyState({ + this.trophies = const [], + this.userTrophies = const [], + this.trophyProgression = const [], + }); + + TrophyState copyWith({ + List? trophies, + List? userTrophies, + List? trophyProgression, + }) { + return TrophyState( + trophies: trophies ?? this.trophies, + userTrophies: userTrophies ?? this.userTrophies, + trophyProgression: trophyProgression ?? this.trophyProgression, + ); + } + + List get prTrophies => + userTrophies.where((t) => t.trophy.type == TrophyType.pr).toList(); + + List get nonPrTrophies => + userTrophies.where((t) => t.trophy.type != TrophyType.pr).toList(); +} + class TrophyRepository { final _logger = Logger('TrophyRepository'); @@ -96,40 +128,52 @@ TrophyRepository trophyRepository(Ref ref) { @Riverpod(keepAlive: true) final class TrophyStateNotifier extends _$TrophyStateNotifier { + final _logger = Logger('TrophyStateNotifier'); + @override - void build() {} + TrophyState build() { + return TrophyState(); + } + + Future fetchAll({String? language}) async { + await Future.wait([ + fetchTrophies(language: language), + fetchUserTrophies(language: language), + fetchTrophyProgression(language: language), + ]); + } /// Fetch all available trophies - Future> fetchTrophies() async { + Future> fetchTrophies({String? language}) async { + _logger.finer('Fetching trophies'); + final repo = ref.read(trophyRepositoryProvider); - return repo.fetchTrophies(); + final result = await repo.fetchTrophies(language: language); + state = state.copyWith(trophies: result); + return result; } - /// Fetch trophies awarded to the user. - /// Excludes hidden trophies as well as repeatable (PR) trophies since they are - /// handled separately + /// Fetch trophies awarded to the user, excludes hidden trophies Future> fetchUserTrophies({String? language}) async { - final repo = ref.read(trophyRepositoryProvider); - return repo.fetchUserTrophies( - filterQuery: {'trophy__is_hidden': 'false', 'trophy__is_repeatable': 'false'}, - language: language, - ); - } + _logger.finer('Fetching user trophies'); - /// Fetch PR trophies awarded to the user - Future> fetchUserPRTrophies({String? language}) async { final repo = ref.read(trophyRepositoryProvider); - return repo.fetchUserTrophies( - filterQuery: {'trophy__trophy_type': TrophyType.pr.name}, + final result = await repo.fetchUserTrophies( + filterQuery: {'trophy__is_hidden': 'false'}, //'trophy__is_repeatable': 'false' language: language, ); + state = state.copyWith(userTrophies: result); + return result; } /// Fetch trophy progression for the user Future> fetchTrophyProgression({String? language}) async { - final repo = ref.read(trophyRepositoryProvider); + _logger.finer('Fetching user trophy progression'); // Note that repeatable trophies are filtered out in the backend - return repo.fetchProgression(language: language); + final repo = ref.read(trophyRepositoryProvider); + final result = await repo.fetchProgression(language: language); + state = state.copyWith(trophyProgression: result); + return result; } } diff --git a/lib/providers/trophies.g.dart b/lib/providers/trophies.g.dart index 88f47d58..2f2a95e8 100644 --- a/lib/providers/trophies.g.dart +++ b/lib/providers/trophies.g.dart @@ -53,7 +53,8 @@ String _$trophyRepositoryHash() => r'0699f0c0f7f324f3ba9b21420d9845a3e3096b61'; @ProviderFor(TrophyStateNotifier) const trophyStateProvider = TrophyStateNotifierProvider._(); -final class TrophyStateNotifierProvider extends $NotifierProvider { +final class TrophyStateNotifierProvider + extends $NotifierProvider { const TrophyStateNotifierProvider._() : super( from: null, @@ -73,25 +74,31 @@ final class TrophyStateNotifierProvider extends $NotifierProvider TrophyStateNotifier(); /// {@macro riverpod.override_with_value} - Override overrideWithValue(void value) { + Override overrideWithValue(TrophyState value) { return $ProviderOverride( origin: this, - providerOverride: $SyncValueProvider(value), + providerOverride: $SyncValueProvider(value), ); } } -String _$trophyStateNotifierHash() => r'47b4babf337bbb8bf60142ebe59dc760fa08dce3'; +String _$trophyStateNotifierHash() => r'c80c732272cf843b698f28152f60b9a5f37ee449'; -abstract class _$TrophyStateNotifier extends $Notifier { - void build(); +abstract class _$TrophyStateNotifier extends $Notifier { + TrophyState build(); @$mustCallSuper @override void runBuild() { - build(); - final ref = this.ref as $Ref; + final created = build(); + final ref = this.ref as $Ref; final element = - ref.element as $ClassProviderElement, void, Object?, Object?>; - element.handleValue(ref, null); + ref.element + as $ClassProviderElement< + AnyNotifier, + TrophyState, + Object?, + Object? + >; + element.handleValue(ref, created); } } diff --git a/lib/screens/home_tabs_screen.dart b/lib/screens/home_tabs_screen.dart index ebb70398..10de168f 100644 --- a/lib/screens/home_tabs_screen.dart +++ b/lib/screens/home_tabs_screen.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -17,6 +17,7 @@ */ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; @@ -30,6 +31,7 @@ import 'package:wger/providers/gallery.dart'; import 'package:wger/providers/measurement.dart'; import 'package:wger/providers/nutrition.dart'; import 'package:wger/providers/routines.dart'; +import 'package:wger/providers/trophies.dart'; import 'package:wger/providers/user.dart'; import 'package:wger/screens/dashboard.dart'; import 'package:wger/screens/gallery_screen.dart'; @@ -37,7 +39,7 @@ import 'package:wger/screens/nutritional_plans_screen.dart'; import 'package:wger/screens/routine_list_screen.dart'; import 'package:wger/screens/weight_screen.dart'; -class HomeTabsScreen extends StatefulWidget { +class HomeTabsScreen extends ConsumerStatefulWidget { final _logger = Logger('HomeTabsScreen'); HomeTabsScreen(); @@ -48,25 +50,20 @@ class HomeTabsScreen extends StatefulWidget { _HomeTabsScreenState createState() => _HomeTabsScreenState(); } -class _HomeTabsScreenState extends State with SingleTickerProviderStateMixin { - late Future _initialData; +class _HomeTabsScreenState extends ConsumerState + with SingleTickerProviderStateMixin { + Future? _initialData; bool _errorHandled = false; int _selectedIndex = 0; bool _isWideScreen = false; - @override - void initState() { - super.initState(); - // Loading data here, since the build method can be called more than once - _initialData = _loadEntries(); - } - @override void didChangeDependencies() { super.didChangeDependencies(); final size = MediaQuery.sizeOf(context); _isWideScreen = size.width > MATERIAL_XS_BREAKPOINT; + _initialData ??= _loadEntries(); } void _onItemTapped(int index) { @@ -85,7 +82,9 @@ class _HomeTabsScreenState extends State with SingleTickerProvid /// Load initial data from the server Future _loadEntries() async { + final languageCode = Localizations.localeOf(context).languageCode; final authProvider = context.read(); + final trophyNotifier = ref.read(trophyStateProvider.notifier); if (!authProvider.dataInit) { final routinesProvider = context.read(); @@ -127,6 +126,7 @@ class _HomeTabsScreenState extends State with SingleTickerProvid // routinesProvider.fetchAndSetAllRoutinesFull(), weightProvider.fetchAndSetEntries(), measurementProvider.fetchAndSetAllCategoriesAndEntries(), + trophyNotifier.fetchAll(language: languageCode), ]); // diff --git a/lib/screens/routine_logs_screen.dart b/lib/screens/routine_logs_screen.dart index 93da5d5c..c9909b1f 100644 --- a/lib/screens/routine_logs_screen.dart +++ b/lib/screens/routine_logs_screen.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2025 wger Team + * Copyright (c) 2020 - 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. diff --git a/lib/widgets/dashboard/widgets/trophies.dart b/lib/widgets/dashboard/widgets/trophies.dart index 3e39fd45..e164e5d5 100644 --- a/lib/widgets/dashboard/widgets/trophies.dart +++ b/lib/widgets/dashboard/widgets/trophies.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2020 - 2025 wger Team + * Copyright (c) 2020 - 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -21,64 +21,45 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:wger/models/trophies/trophy.dart'; import 'package:wger/providers/trophies.dart'; import 'package:wger/screens/trophy_screen.dart'; -import 'package:wger/widgets/core/progress_indicator.dart'; class DashboardTrophiesWidget extends ConsumerWidget { const DashboardTrophiesWidget(); @override Widget build(BuildContext context, WidgetRef ref) { - final provider = ref.watch(trophyStateProvider.notifier); - final languageCode = Localizations.localeOf(context).languageCode; + final trophiesState = ref.read(trophyStateProvider); - return FutureBuilder( - future: provider.fetchUserTrophies(language: languageCode), - builder: (context, asyncSnapshot) { - if (asyncSnapshot.connectionState != ConnectionState.done) { - return const Card(child: BoxedProgressIndicator()); - } + return Card( + color: Colors.transparent, + shadowColor: Colors.transparent, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (trophiesState.nonPrTrophies.isEmpty) + Padding( + padding: const EdgeInsets.all(16.0), + child: Text('No trophies yet', style: Theme.of(context).textTheme.bodyMedium), + ) + else + SizedBox( + height: 140, + child: ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), + scrollDirection: Axis.horizontal, + itemCount: trophiesState.nonPrTrophies.length, + separatorBuilder: (context, index) => const SizedBox(width: 12), + itemBuilder: (context, index) { + final userTrophy = trophiesState.nonPrTrophies[index]; - final userTrophies = asyncSnapshot.data ?? []; - - return Card( - color: Colors.transparent, - shadowColor: Colors.transparent, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // ListTile( - // title: Text( - // 'Trophies', - // style: Theme.of(context).textTheme.headlineSmall, - // ), - // ), - if (userTrophies.isEmpty) - Padding( - padding: const EdgeInsets.all(16.0), - child: Text('No trophies yet', style: Theme.of(context).textTheme.bodyMedium), - ) - else - SizedBox( - height: 140, - child: ListView.separated( - padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0), - scrollDirection: Axis.horizontal, - itemCount: userTrophies.length, - separatorBuilder: (context, index) => const SizedBox(width: 12), - itemBuilder: (context, index) { - final userTrophy = userTrophies[index]; - - return SizedBox( - width: 220, - child: TrophyCard(trophy: userTrophy.trophy), - ); - }, - ), - ), - ], - ), - ); - }, + return SizedBox( + width: 220, + child: TrophyCard(trophy: userTrophy.trophy), + ); + }, + ), + ), + ], + ), ); } } diff --git a/lib/widgets/routines/gym_mode/log_page.dart b/lib/widgets/routines/gym_mode/log_page.dart index 10cf7e64..c3c99efb 100644 --- a/lib/widgets/routines/gym_mode/log_page.dart +++ b/lib/widgets/routines/gym_mode/log_page.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2020 - 2025 wger Team + * Copyright (c) 2020 - 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by diff --git a/lib/widgets/routines/gym_mode/summary.dart b/lib/widgets/routines/gym_mode/summary.dart index 146b15c5..e5097500 100644 --- a/lib/widgets/routines/gym_mode/summary.dart +++ b/lib/widgets/routines/gym_mode/summary.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2020 - 2025 wger Team + * Copyright (c) 2020 - 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -49,7 +49,6 @@ class WorkoutSummary extends ConsumerStatefulWidget { class _WorkoutSummaryState extends ConsumerState { late Future _initData; late Routine _routine; - late List _userPrTrophies; bool _didInit = false; @override @@ -76,11 +75,13 @@ class _WorkoutSummaryState extends ConsumerState { ); final trophyNotifier = ref.read(trophyStateProvider.notifier); - _userPrTrophies = await trophyNotifier.fetchUserPRTrophies(language: languageCode); + await trophyNotifier.fetchUserTrophies(language: languageCode); } @override Widget build(BuildContext context) { + final trophyState = ref.watch(trophyStateProvider); + return Column( children: [ NavigationHeader( @@ -102,10 +103,8 @@ class _WorkoutSummaryState extends ConsumerState { final apiSession = _routine.sessions.firstWhereOrNull( (s) => s.session.date.isSameDayAs(clock.now()), ); - final userTrophies = _userPrTrophies - .where( - (t) => t.contextData!.sessionId == apiSession?.session.id, - ) + final userTrophies = trophyState.prTrophies + .where((t) => t.contextData?.sessionId == apiSession?.session.id) .toList(); return WorkoutSessionStats( diff --git a/lib/widgets/routines/logs/day_logs_container.dart b/lib/widgets/routines/logs/day_logs_container.dart index 49d829b0..ed3ac3a4 100644 --- a/lib/widgets/routines/logs/day_logs_container.dart +++ b/lib/widgets/routines/logs/day_logs_container.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2020, 2025 wger Team + * Copyright (c) 2020 - 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -17,33 +17,59 @@ */ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logging/logging.dart'; import 'package:wger/helpers/date.dart'; import 'package:wger/helpers/errors.dart'; +import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/workouts/routine.dart'; +import 'package:wger/providers/trophies.dart'; +import '../gym_mode/summary.dart'; import 'exercise_log_chart.dart'; import 'muscle_groups.dart'; import 'session_info.dart'; -class DayLogWidget extends StatelessWidget { +class DayLogWidget extends ConsumerWidget { final DateTime _date; final Routine _routine; + final _logger = Logger('DayLogWidget'); - const DayLogWidget(this._date, this._routine); + DayLogWidget(this._date, this._routine); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final i18n = AppLocalizations.of(context); + final theme = Theme.of(context); + + final trophyState = ref.read(trophyStateProvider); + final sessionApi = _routine.sessions.firstWhere( (sessionApi) => sessionApi.session.date.isSameDayAs(_date), ); final exercises = sessionApi.exercises; + final prTrophies = trophyState.prTrophies + .where((t) => t.contextData?.sessionId == sessionApi.session.id) + .toList(); + + _logger.info(trophyState.prTrophies); + _logger.info(prTrophies); + return Column( spacing: 10, children: [ Card(child: SessionInfo(sessionApi.session)), + if (prTrophies.isNotEmpty) + SizedBox( + width: double.infinity, + child: InfoCard( + title: i18n.personalRecords, + value: prTrophies.length.toString(), + color: theme.colorScheme.tertiaryContainer, + ), + ), MuscleGroupsCard(sessionApi.logs), - Column( spacing: 10, children: [ @@ -66,7 +92,17 @@ class DayLogWidget extends StatelessWidget { (log) => Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(log.repTextNoNl(context)), + Row( + children: [ + if (prTrophies.any((t) => t.contextData?.logId == log.id)) + Icon( + Icons.emoji_events, + color: theme.colorScheme.primary, + size: 20, + ), + Text(log.repTextNoNl(context)), + ], + ), IconButton( icon: const Icon(Icons.delete), key: ValueKey('delete-log-${log.id}'), diff --git a/lib/widgets/routines/logs/log_overview_routine.dart b/lib/widgets/routines/logs/log_overview_routine.dart index dfcfc1aa..34f0be5a 100644 --- a/lib/widgets/routines/logs/log_overview_routine.dart +++ b/lib/widgets/routines/logs/log_overview_routine.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2025 wger Team + * Copyright (c) 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -18,20 +18,26 @@ import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:table_calendar/table_calendar.dart'; import 'package:wger/helpers/consts.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/workouts/routine.dart'; +import 'package:wger/providers/trophies.dart'; import 'package:wger/theme/theme.dart'; import 'package:wger/widgets/routines/logs/day_logs_container.dart'; -class WorkoutLogs extends StatelessWidget { +class WorkoutLogs extends ConsumerWidget { final Routine _routine; const WorkoutLogs(this._routine); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final languageCode = Localizations.localeOf(context).languageCode; + final trophyNotifier = ref.read(trophyStateProvider.notifier); + trophyNotifier.fetchUserTrophies(language: languageCode); + return ListView( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8), children: [ diff --git a/lib/widgets/trophies/trophies_overview.dart b/lib/widgets/trophies/trophies_overview.dart index 2b06f340..706040df 100644 --- a/lib/widgets/trophies/trophies_overview.dart +++ b/lib/widgets/trophies/trophies_overview.dart @@ -21,15 +21,13 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:wger/helpers/material.dart'; import 'package:wger/models/trophies/user_trophy_progression.dart'; import 'package:wger/providers/trophies.dart'; -import 'package:wger/widgets/core/progress_indicator.dart'; class TrophiesOverview extends ConsumerWidget { const TrophiesOverview({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final notifier = ref.watch(trophyStateProvider.notifier); - final languageCode = Localizations.localeOf(context).languageCode; + final trophyState = ref.watch(trophyStateProvider); // Responsive grid: determine columns based on screen width final width = MediaQuery.widthOf(context); @@ -44,52 +42,32 @@ class TrophiesOverview extends ConsumerWidget { crossAxisCount = 5; } - return FutureBuilder>( - future: notifier.fetchTrophyProgression(language: languageCode), - builder: (context, snapshot) { - if (snapshot.connectionState != ConnectionState.done) { - return const Card(child: BoxedProgressIndicator()); - } - - if (snapshot.hasError) { - return Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text('Error loading trophies', style: Theme.of(context).textTheme.bodyLarge), - ), - ); - } - - final userTrophyProgression = snapshot.data ?? []; - - // If empty, show placeholder - if (userTrophyProgression.isEmpty) { - return Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - 'No trophies yet', - style: Theme.of(context).textTheme.bodyLarge, - textAlign: TextAlign.center, - ), - ), - ); - } - - return RepaintBoundary( - child: GridView.builder( - padding: const EdgeInsets.all(12), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: crossAxisCount, - ), - key: const ValueKey('trophy-grid'), - itemCount: userTrophyProgression.length, - itemBuilder: (context, index) { - return _TrophyCardImage(userProgression: userTrophyProgression[index]); - }, + // If empty, show placeholder + if (trophyState.trophyProgression.isEmpty) { + return Center( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + 'No trophies yet', + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.center, ), - ); - }, + ), + ); + } + + return RepaintBoundary( + child: GridView.builder( + padding: const EdgeInsets.all(12), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + ), + key: const ValueKey('trophy-grid'), + itemCount: trophyState.trophyProgression.length, + itemBuilder: (context, index) { + return _TrophyCardImage(userProgression: trophyState.trophyProgression[index]); + }, + ), ); } } diff --git a/test/routine/routine_logs_screen_test.dart b/test/routine/routine_logs_screen_test.dart index c61bdf3a..1081171c 100644 --- a/test/routine/routine_logs_screen_test.dart +++ b/test/routine/routine_logs_screen_test.dart @@ -1,13 +1,13 @@ /* * This file is part of wger Workout Manager . - * Copyright (C) 2020, 2021 wger Team + * Copyright (c) 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * - * wger Workout Manager is distributed in the hope that it will be useful, + * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. @@ -20,6 +20,7 @@ import 'dart:io'; import 'package:clock/clock.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; @@ -27,14 +28,16 @@ import 'package:provider/provider.dart'; import 'package:wger/l10n/generated/app_localizations.dart'; import 'package:wger/models/workouts/routine.dart'; import 'package:wger/providers/routines.dart'; +import 'package:wger/providers/trophies.dart'; import 'package:wger/screens/routine_logs_screen.dart'; import 'package:wger/screens/routine_screen.dart'; import 'package:wger/widgets/routines/logs/log_overview_routine.dart'; import '../../test_data/routines.dart'; +import '../test_data/trophies.dart'; import 'routine_logs_screen_test.mocks.dart'; -@GenerateMocks([RoutinesProvider]) +@GenerateMocks([RoutinesProvider, TrophyRepository]) void main() { late Routine routine; final mockRoutinesProvider = MockRoutinesProvider(); @@ -49,25 +52,39 @@ void main() { Widget renderWidget({locale = 'en'}) { final key = GlobalKey(); - return ChangeNotifierProvider( - create: (context) => mockRoutinesProvider, - child: MaterialApp( - locale: Locale(locale), - localizationsDelegates: AppLocalizations.localizationsDelegates, - supportedLocales: AppLocalizations.supportedLocales, - navigatorKey: key, - home: TextButton( - onPressed: () => key.currentState!.push( - MaterialPageRoute( - settings: RouteSettings(arguments: routine.id), - builder: (_) => const WorkoutLogsScreen(), + // Arrange + final mockRepository = MockTrophyRepository(); + when( + mockRepository.fetchUserTrophies( + filterQuery: anyNamed('filterQuery'), + language: anyNamed('language'), + ), + ).thenAnswer((_) async => getUserTrophies()); + + return ProviderScope( + overrides: [ + trophyRepositoryProvider.overrideWithValue(mockRepository), + ], + child: ChangeNotifierProvider( + create: (context) => mockRoutinesProvider, + child: MaterialApp( + locale: Locale(locale), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + navigatorKey: key, + home: TextButton( + onPressed: () => key.currentState!.push( + MaterialPageRoute( + settings: RouteSettings(arguments: routine.id), + builder: (_) => const WorkoutLogsScreen(), + ), ), + child: const SizedBox(), ), - child: const SizedBox(), + routes: { + RoutineScreen.routeName: (ctx) => const WorkoutLogsScreen(), + }, ), - routes: { - RoutineScreen.routeName: (ctx) => const WorkoutLogsScreen(), - }, ), ); } diff --git a/test/routine/routine_logs_screen_test.mocks.dart b/test/routine/routine_logs_screen_test.mocks.dart index dca48e93..015095bb 100644 --- a/test/routine/routine_logs_screen_test.mocks.dart +++ b/test/routine/routine_logs_screen_test.mocks.dart @@ -9,6 +9,9 @@ import 'dart:ui' as _i17; import 'package:mockito/mockito.dart' as _i1; import 'package:mockito/src/dummies.dart' as _i16; import 'package:wger/models/exercises/exercise.dart' as _i15; +import 'package:wger/models/trophies/trophy.dart' as _i19; +import 'package:wger/models/trophies/user_trophy.dart' as _i20; +import 'package:wger/models/trophies/user_trophy_progression.dart' as _i21; import 'package:wger/models/workouts/base_config.dart' as _i9; import 'package:wger/models/workouts/day.dart' as _i6; import 'package:wger/models/workouts/day_data.dart' as _i14; @@ -21,6 +24,7 @@ import 'package:wger/models/workouts/slot_entry.dart' as _i8; import 'package:wger/models/workouts/weight_unit.dart' as _i3; import 'package:wger/providers/base_provider.dart' as _i2; import 'package:wger/providers/routines.dart' as _i12; +import 'package:wger/providers/trophies.dart' as _i18; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -592,3 +596,107 @@ class MockRoutinesProvider extends _i1.Mock implements _i12.RoutinesProvider { returnValueForMissingStub: null, ); } + +/// A class which mocks [TrophyRepository]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockTrophyRepository extends _i1.Mock implements _i18.TrophyRepository { + MockTrophyRepository() { + _i1.throwOnMissingStub(this); + } + + @override + _i2.WgerBaseProvider get base => + (super.noSuchMethod( + Invocation.getter(#base), + returnValue: _FakeWgerBaseProvider_0( + this, + Invocation.getter(#base), + ), + ) + as _i2.WgerBaseProvider); + + @override + String get trophiesPath => + (super.noSuchMethod( + Invocation.getter(#trophiesPath), + returnValue: _i16.dummyValue( + this, + Invocation.getter(#trophiesPath), + ), + ) + as String); + + @override + String get userTrophiesPath => + (super.noSuchMethod( + Invocation.getter(#userTrophiesPath), + returnValue: _i16.dummyValue( + this, + Invocation.getter(#userTrophiesPath), + ), + ) + as String); + + @override + String get userTrophyProgressionPath => + (super.noSuchMethod( + Invocation.getter(#userTrophyProgressionPath), + returnValue: _i16.dummyValue( + this, + Invocation.getter(#userTrophyProgressionPath), + ), + ) + as String); + + @override + _i13.Future> fetchTrophies({String? language}) => + (super.noSuchMethod( + Invocation.method(#fetchTrophies, [], {#language: language}), + returnValue: _i13.Future>.value(<_i19.Trophy>[]), + ) + as _i13.Future>); + + @override + _i13.Future> fetchUserTrophies({ + Map? filterQuery, + String? language, + }) => + (super.noSuchMethod( + Invocation.method(#fetchUserTrophies, [], { + #filterQuery: filterQuery, + #language: language, + }), + returnValue: _i13.Future>.value( + <_i20.UserTrophy>[], + ), + ) + as _i13.Future>); + + @override + _i13.Future> fetchProgression({ + Map? filterQuery, + String? language, + }) => + (super.noSuchMethod( + Invocation.method(#fetchProgression, [], { + #filterQuery: filterQuery, + #language: language, + }), + returnValue: _i13.Future>.value( + <_i21.UserTrophyProgression>[], + ), + ) + as _i13.Future>); + + @override + List<_i19.Trophy> filterByType( + List<_i19.Trophy>? list, + _i19.TrophyType? type, + ) => + (super.noSuchMethod( + Invocation.method(#filterByType, [list, type]), + returnValue: <_i19.Trophy>[], + ) + as List<_i19.Trophy>); +} diff --git a/test/trophies/widgets/dashboard_trophies_widget_test.dart b/test/trophies/widgets/dashboard_trophies_widget_test.dart index bf4209b4..687f13a0 100644 --- a/test/trophies/widgets/dashboard_trophies_widget_test.dart +++ b/test/trophies/widgets/dashboard_trophies_widget_test.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2025 wger Team + * Copyright (c) 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -19,33 +19,25 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; import 'package:network_image_mock/network_image_mock.dart'; import 'package:wger/providers/trophies.dart'; import 'package:wger/widgets/dashboard/widgets/trophies.dart'; import '../../test_data/trophies.dart'; -import 'dashboard_trophies_widget_test.mocks.dart'; -@GenerateMocks([TrophyRepository]) void main() { testWidgets('DashboardTrophiesWidget shows trophies', (WidgetTester tester) async { - // Arrange - final mockRepository = MockTrophyRepository(); - when( - mockRepository.fetchUserTrophies( - filterQuery: anyNamed('filterQuery'), - language: anyNamed('language'), - ), - ).thenAnswer((_) async => getUserTrophies()); - // Act await mockNetworkImagesFor(() async { await tester.pumpWidget( ProviderScope( overrides: [ - trophyRepositoryProvider.overrideWithValue(mockRepository), + trophyStateProvider.overrideWithValue( + TrophyState( + userTrophies: getUserTrophies(), + trophies: getTestTrophies(), + ), + ), ], child: const MaterialApp( home: Scaffold( diff --git a/test/trophies/widgets/dashboard_trophies_widget_test.mocks.dart b/test/trophies/widgets/dashboard_trophies_widget_test.mocks.dart deleted file mode 100644 index 41544f94..00000000 --- a/test/trophies/widgets/dashboard_trophies_widget_test.mocks.dart +++ /dev/null @@ -1,135 +0,0 @@ -// Mocks generated by Mockito 5.4.6 from annotations -// in wger/test/trophies/widgets/dashboard_trophies_widget_test.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i5; - -import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i4; -import 'package:wger/models/trophies/trophy.dart' as _i6; -import 'package:wger/models/trophies/user_trophy.dart' as _i7; -import 'package:wger/models/trophies/user_trophy_progression.dart' as _i8; -import 'package:wger/providers/base_provider.dart' as _i2; -import 'package:wger/providers/trophies.dart' as _i3; - -// ignore_for_file: type=lint -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: deprecated_member_use -// ignore_for_file: deprecated_member_use_from_same_package -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: must_be_immutable -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types -// ignore_for_file: subtype_of_sealed_class -// ignore_for_file: invalid_use_of_internal_member - -class _FakeWgerBaseProvider_0 extends _i1.SmartFake implements _i2.WgerBaseProvider { - _FakeWgerBaseProvider_0(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); -} - -/// A class which mocks [TrophyRepository]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockTrophyRepository extends _i1.Mock implements _i3.TrophyRepository { - MockTrophyRepository() { - _i1.throwOnMissingStub(this); - } - - @override - _i2.WgerBaseProvider get base => - (super.noSuchMethod( - Invocation.getter(#base), - returnValue: _FakeWgerBaseProvider_0( - this, - Invocation.getter(#base), - ), - ) - as _i2.WgerBaseProvider); - - @override - String get trophiesPath => - (super.noSuchMethod( - Invocation.getter(#trophiesPath), - returnValue: _i4.dummyValue( - this, - Invocation.getter(#trophiesPath), - ), - ) - as String); - - @override - String get userTrophiesPath => - (super.noSuchMethod( - Invocation.getter(#userTrophiesPath), - returnValue: _i4.dummyValue( - this, - Invocation.getter(#userTrophiesPath), - ), - ) - as String); - - @override - String get userTrophyProgressionPath => - (super.noSuchMethod( - Invocation.getter(#userTrophyProgressionPath), - returnValue: _i4.dummyValue( - this, - Invocation.getter(#userTrophyProgressionPath), - ), - ) - as String); - - @override - _i5.Future> fetchTrophies({String? language}) => - (super.noSuchMethod( - Invocation.method(#fetchTrophies, [], {#language: language}), - returnValue: _i5.Future>.value(<_i6.Trophy>[]), - ) - as _i5.Future>); - - @override - _i5.Future> fetchUserTrophies({ - Map? filterQuery, - String? language, - }) => - (super.noSuchMethod( - Invocation.method(#fetchUserTrophies, [], { - #filterQuery: filterQuery, - #language: language, - }), - returnValue: _i5.Future>.value( - <_i7.UserTrophy>[], - ), - ) - as _i5.Future>); - - @override - _i5.Future> fetchProgression({ - Map? filterQuery, - String? language, - }) => - (super.noSuchMethod( - Invocation.method(#fetchProgression, [], { - #filterQuery: filterQuery, - #language: language, - }), - returnValue: _i5.Future>.value( - <_i8.UserTrophyProgression>[], - ), - ) - as _i5.Future>); - - @override - List<_i6.Trophy> filterByType(List<_i6.Trophy>? list, _i6.TrophyType? type) => - (super.noSuchMethod( - Invocation.method(#filterByType, [list, type]), - returnValue: <_i6.Trophy>[], - ) - as List<_i6.Trophy>); -} diff --git a/test/trophies/widgets/trophies_overview_test.dart b/test/trophies/widgets/trophies_overview_test.dart index 2c6b71f5..0bc12cd5 100644 --- a/test/trophies/widgets/trophies_overview_test.dart +++ b/test/trophies/widgets/trophies_overview_test.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2025 wger Team + * Copyright (c) 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -19,33 +19,26 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; import 'package:network_image_mock/network_image_mock.dart'; import 'package:wger/providers/trophies.dart'; import 'package:wger/widgets/trophies/trophies_overview.dart'; import '../../test_data/trophies.dart'; -import 'dashboard_trophies_widget_test.mocks.dart'; -@GenerateMocks([TrophyRepository]) void main() { testWidgets('TrophiesOverview shows trophies', (WidgetTester tester) async { - // Arrange - final mockRepository = MockTrophyRepository(); - when( - mockRepository.fetchProgression( - filterQuery: anyNamed('filterQuery'), - language: anyNamed('language'), - ), - ).thenAnswer((_) async => getUserTrophyProgression()); - // Act await mockNetworkImagesFor(() async { await tester.pumpWidget( ProviderScope( overrides: [ - trophyRepositoryProvider.overrideWithValue(mockRepository), + trophyStateProvider.overrideWithValue( + TrophyState( + trophyProgression: getUserTrophyProgression(), + userTrophies: getUserTrophies(), + trophies: getTestTrophies(), + ), + ), ], child: const MaterialApp( home: Scaffold(body: TrophiesOverview()), diff --git a/test/trophies/widgets/trophies_overview_test.mocks.dart b/test/trophies/widgets/trophies_overview_test.mocks.dart deleted file mode 100644 index dc837fd8..00000000 --- a/test/trophies/widgets/trophies_overview_test.mocks.dart +++ /dev/null @@ -1,135 +0,0 @@ -// Mocks generated by Mockito 5.4.6 from annotations -// in wger/test/trophies/widgets/trophies_overview_test.dart. -// Do not manually edit this file. - -// ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i5; - -import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i4; -import 'package:wger/models/trophies/trophy.dart' as _i6; -import 'package:wger/models/trophies/user_trophy.dart' as _i7; -import 'package:wger/models/trophies/user_trophy_progression.dart' as _i8; -import 'package:wger/providers/base_provider.dart' as _i2; -import 'package:wger/providers/trophies.dart' as _i3; - -// ignore_for_file: type=lint -// ignore_for_file: avoid_redundant_argument_values -// ignore_for_file: avoid_setters_without_getters -// ignore_for_file: comment_references -// ignore_for_file: deprecated_member_use -// ignore_for_file: deprecated_member_use_from_same_package -// ignore_for_file: implementation_imports -// ignore_for_file: invalid_use_of_visible_for_testing_member -// ignore_for_file: must_be_immutable -// ignore_for_file: prefer_const_constructors -// ignore_for_file: unnecessary_parenthesis -// ignore_for_file: camel_case_types -// ignore_for_file: subtype_of_sealed_class -// ignore_for_file: invalid_use_of_internal_member - -class _FakeWgerBaseProvider_0 extends _i1.SmartFake implements _i2.WgerBaseProvider { - _FakeWgerBaseProvider_0(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); -} - -/// A class which mocks [TrophyRepository]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockTrophyRepository extends _i1.Mock implements _i3.TrophyRepository { - MockTrophyRepository() { - _i1.throwOnMissingStub(this); - } - - @override - _i2.WgerBaseProvider get base => - (super.noSuchMethod( - Invocation.getter(#base), - returnValue: _FakeWgerBaseProvider_0( - this, - Invocation.getter(#base), - ), - ) - as _i2.WgerBaseProvider); - - @override - String get trophiesPath => - (super.noSuchMethod( - Invocation.getter(#trophiesPath), - returnValue: _i4.dummyValue( - this, - Invocation.getter(#trophiesPath), - ), - ) - as String); - - @override - String get userTrophiesPath => - (super.noSuchMethod( - Invocation.getter(#userTrophiesPath), - returnValue: _i4.dummyValue( - this, - Invocation.getter(#userTrophiesPath), - ), - ) - as String); - - @override - String get userTrophyProgressionPath => - (super.noSuchMethod( - Invocation.getter(#userTrophyProgressionPath), - returnValue: _i4.dummyValue( - this, - Invocation.getter(#userTrophyProgressionPath), - ), - ) - as String); - - @override - _i5.Future> fetchTrophies({String? language}) => - (super.noSuchMethod( - Invocation.method(#fetchTrophies, [], {#language: language}), - returnValue: _i5.Future>.value(<_i6.Trophy>[]), - ) - as _i5.Future>); - - @override - _i5.Future> fetchUserTrophies({ - Map? filterQuery, - String? language, - }) => - (super.noSuchMethod( - Invocation.method(#fetchUserTrophies, [], { - #filterQuery: filterQuery, - #language: language, - }), - returnValue: _i5.Future>.value( - <_i7.UserTrophy>[], - ), - ) - as _i5.Future>); - - @override - _i5.Future> fetchProgression({ - Map? filterQuery, - String? language, - }) => - (super.noSuchMethod( - Invocation.method(#fetchProgression, [], { - #filterQuery: filterQuery, - #language: language, - }), - returnValue: _i5.Future>.value( - <_i8.UserTrophyProgression>[], - ), - ) - as _i5.Future>); - - @override - List<_i6.Trophy> filterByType(List<_i6.Trophy>? list, _i6.TrophyType? type) => - (super.noSuchMethod( - Invocation.method(#filterByType, [list, type]), - returnValue: <_i6.Trophy>[], - ) - as List<_i6.Trophy>); -} diff --git a/test/widgets/routines/gym_mode/log_page_test.dart b/test/widgets/routines/gym_mode/log_page_test.dart index 45526e7f..78e80858 100644 --- a/test/widgets/routines/gym_mode/log_page_test.dart +++ b/test/widgets/routines/gym_mode/log_page_test.dart @@ -1,6 +1,6 @@ /* * This file is part of wger Workout Manager . - * Copyright (c) 2025 - 2025 wger Team + * Copyright (c) 2025 - 2026 wger Team * * wger Workout Manager is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -189,15 +189,15 @@ void main() { testWidgets('save button calls addLog on RoutinesProvider', (tester) async { // Arrange - final notifier = container.read(gymStateProvider.notifier); + final gymNotifier = container.read(gymStateProvider.notifier); final routine = testdata.getTestRoutine(); - notifier.state = notifier.state.copyWith( + gymNotifier.state = gymNotifier.state.copyWith( dayId: routine.days.first.id, routine: routine, iteration: 1, ); - notifier.calculatePages(); - notifier.state = notifier.state.copyWith(currentPage: 2); + gymNotifier.calculatePages(); + gymNotifier.state = gymNotifier.state.copyWith(currentPage: 2); final mockRoutines = MockRoutinesProvider(); // Act @@ -229,10 +229,10 @@ void main() { expect(capturedLog!.repetitions, equals(7)); expect(capturedLog!.weight, equals(77)); - final currentSlotPage = notifier.state.getSlotEntryPageByIndex()!; + final currentSlotPage = gymNotifier.state.getSlotEntryPageByIndex()!; expect(capturedLog!.slotEntryId, equals(currentSlotPage.setConfigData!.slotEntryId)); - expect(capturedLog!.routineId, equals(notifier.state.routine.id)); - expect(capturedLog!.iteration, equals(notifier.state.iteration)); + expect(capturedLog!.routineId, equals(gymNotifier.state.routine.id)); + expect(capturedLog!.iteration, equals(gymNotifier.state.iteration)); }); }); }