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