mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
Refactor widgets used in logs
We should try to reuse as many as possible in the end-of-gym-mode logs and in the routine log page
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:wger/helpers/json.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/workouts/log.dart';
|
||||
|
||||
part 'session.g.dart';
|
||||
@@ -78,6 +79,28 @@ class WorkoutSession {
|
||||
return endDate.difference(startDate);
|
||||
}
|
||||
|
||||
String durationTxt(BuildContext context) {
|
||||
final duration = this.duration;
|
||||
if (duration == null) {
|
||||
return '-/-';
|
||||
}
|
||||
return AppLocalizations.of(
|
||||
context,
|
||||
).durationHoursMinutes(duration.inHours, duration.inMinutes.remainder(60));
|
||||
}
|
||||
|
||||
String durationTxtWithStartEnd(BuildContext context) {
|
||||
final duration = this.duration;
|
||||
if (duration == null) {
|
||||
return '-/-';
|
||||
}
|
||||
|
||||
final startTime = MaterialLocalizations.of(context).formatTimeOfDay(timeStart!);
|
||||
final endTime = MaterialLocalizations.of(context).formatTimeOfDay(timeEnd!);
|
||||
|
||||
return '${durationTxt(context)} ($startTime - $endTime)';
|
||||
}
|
||||
|
||||
// Boilerplate
|
||||
factory WorkoutSession.fromJson(Map<String, dynamic> json) => _$WorkoutSessionFromJson(json);
|
||||
|
||||
|
||||
@@ -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) 2020, 2025 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -20,7 +20,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/providers/routines.dart';
|
||||
import 'package:wger/widgets/core/app_bar.dart';
|
||||
import 'package:wger/widgets/routines/workout_logs.dart';
|
||||
import 'package:wger/widgets/routines/logs/log_overview_routine.dart';
|
||||
|
||||
class WorkoutLogsScreen extends StatelessWidget {
|
||||
const WorkoutLogsScreen();
|
||||
|
||||
@@ -1,14 +1,28 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (c) 2020, 2025 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/helpers/date.dart';
|
||||
import 'package:wger/helpers/i18n.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/exercises/exercise.dart';
|
||||
import 'package:wger/models/workouts/log.dart';
|
||||
import 'package:wger/models/workouts/routine.dart';
|
||||
import 'package:wger/models/workouts/session_api.dart';
|
||||
import 'package:wger/providers/gym_state.dart';
|
||||
@@ -16,6 +30,9 @@ import 'package:wger/providers/routines.dart';
|
||||
import 'package:wger/widgets/core/progress_indicator.dart';
|
||||
import 'package:wger/widgets/routines/gym_mode/navigation.dart';
|
||||
|
||||
import '../logs/exercises_expansion_card.dart';
|
||||
import '../logs/muscle_groups.dart';
|
||||
|
||||
class ResultsWidget extends ConsumerStatefulWidget {
|
||||
final _logger = Logger('ResultsWidget');
|
||||
|
||||
@@ -64,7 +81,7 @@ class _ResultsWidgetState extends ConsumerState<ResultsWidget> {
|
||||
} else if (snapshot.hasError) {
|
||||
return Center(child: Text('Error: ${snapshot.error}: ${snapshot.stackTrace}'));
|
||||
} else if (snapshot.connectionState == ConnectionState.done) {
|
||||
return WorkoutStats(
|
||||
return WorkoutSessionStats(
|
||||
_routine.sessions.firstWhereOrNull(
|
||||
(s) => s.session.date.isSameDayAs(DateTime.now()),
|
||||
),
|
||||
@@ -81,11 +98,11 @@ class _ResultsWidgetState extends ConsumerState<ResultsWidget> {
|
||||
}
|
||||
}
|
||||
|
||||
class WorkoutStats extends ConsumerWidget {
|
||||
class WorkoutSessionStats extends ConsumerWidget {
|
||||
final _logger = Logger('WorkoutStats');
|
||||
final WorkoutSessionApi? _sessionApi;
|
||||
|
||||
WorkoutStats(this._sessionApi, {super.key});
|
||||
WorkoutSessionStats(this._sessionApi, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -151,105 +168,6 @@ class WorkoutStats extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class ExercisesCard extends StatelessWidget {
|
||||
final WorkoutSessionApi session;
|
||||
|
||||
const ExercisesCard(this.session, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final exercises = session.exercises;
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context).exercises,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
...exercises.map((exercise) {
|
||||
final logs = session.logs.where((log) => log.exerciseId == exercise.id).toList();
|
||||
return _ExerciseExpansionTile(exercise: exercise, logs: logs);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExerciseExpansionTile extends StatelessWidget {
|
||||
const _ExerciseExpansionTile({
|
||||
required this.exercise,
|
||||
required this.logs,
|
||||
});
|
||||
|
||||
final Exercise exercise;
|
||||
final List<Log> logs;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final languageCode = Localizations.localeOf(context).languageCode;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final topSet = logs.isEmpty
|
||||
? null
|
||||
: logs.reduce((a, b) => (a.weight ?? 0) > (b.weight ?? 0) ? a : b);
|
||||
final topSetWeight = topSet?.weight?.toStringAsFixed(0) ?? 'N/A';
|
||||
final topSetWeightUnit = topSet?.weightUnitObj != null
|
||||
? getServerStringTranslation(topSet!.weightUnitObj!.name, context)
|
||||
: '';
|
||||
return ExpansionTile(
|
||||
// leading: const Icon(Icons.fitness_center),
|
||||
title: Text(exercise.getTranslation(languageCode).name, style: theme.textTheme.titleMedium),
|
||||
subtitle: Text('Top set: $topSetWeight $topSetWeightUnit'),
|
||||
children: logs.map((log) => _SetDataRow(log: log)).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MuscleGroup {
|
||||
final String name;
|
||||
final double percentage;
|
||||
final Color color;
|
||||
|
||||
MuscleGroup(this.name, this.percentage, this.color);
|
||||
}
|
||||
|
||||
class _SetDataRow extends StatelessWidget {
|
||||
const _SetDataRow({required this.log});
|
||||
|
||||
final Log log;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final i18n = AppLocalizations.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
spacing: 5,
|
||||
children: [
|
||||
Text(
|
||||
log.repTextNoNl(context),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
// if (log.volume() > 0)
|
||||
// Text(
|
||||
// '${log.volume().toStringAsFixed(0)} ${getServerStringTranslation(log.weightUnitObj!.name, context)}',
|
||||
// style: theme.textTheme.bodyMedium,
|
||||
// ),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class InfoCard extends StatelessWidget {
|
||||
final String title;
|
||||
final String value;
|
||||
@@ -276,110 +194,3 @@ class InfoCard extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MuscleGroupsCard extends StatelessWidget {
|
||||
final List<Log> logs;
|
||||
|
||||
const MuscleGroupsCard(this.logs, {super.key});
|
||||
|
||||
List<MuscleGroup> _getMuscleGroups(BuildContext context) {
|
||||
final allMuscles = logs
|
||||
.expand((log) => [...log.exercise.muscles, ...log.exercise.musclesSecondary])
|
||||
.toList();
|
||||
if (allMuscles.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
final muscleCounts = allMuscles.groupListsBy((muscle) => muscle.nameTranslated(context));
|
||||
final total = allMuscles.length;
|
||||
|
||||
int colorIndex = 0;
|
||||
final colors = [
|
||||
Colors.blue,
|
||||
Colors.green,
|
||||
Colors.orange,
|
||||
Colors.purple,
|
||||
Colors.red,
|
||||
Colors.teal,
|
||||
Colors.deepOrange,
|
||||
Colors.indigo,
|
||||
Colors.pink,
|
||||
Colors.brown,
|
||||
Colors.cyan,
|
||||
Colors.lime,
|
||||
Colors.amber,
|
||||
Colors.lightGreen,
|
||||
Colors.deepPurple,
|
||||
];
|
||||
|
||||
return muscleCounts.entries.map((entry) {
|
||||
final percentage = (entry.value.length / total) * 100;
|
||||
final color = colors[colorIndex % colors.length];
|
||||
colorIndex++;
|
||||
return MuscleGroup(entry.key, percentage, color);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final muscles = _getMuscleGroups(context);
|
||||
final theme = Theme.of(context);
|
||||
final i18n = AppLocalizations.of(context);
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
i18n.muscles,
|
||||
style: theme.textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: PieChart(
|
||||
PieChartData(
|
||||
sections: muscles.map((muscle) {
|
||||
return PieChartSectionData(
|
||||
color: muscle.color,
|
||||
value: muscle.percentage,
|
||||
title: i18n.percentValue(muscle.percentage.toStringAsFixed(0)),
|
||||
radius: 50,
|
||||
titleStyle: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.onPrimary,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
sectionsSpace: 2,
|
||||
centerSpaceRadius: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 8,
|
||||
children: muscles.map((muscle) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
color: muscle.color,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(muscle.name),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,225 +0,0 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (C) 2020, 2021 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,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:wger/helpers/colors.dart';
|
||||
import 'package:wger/helpers/date.dart';
|
||||
import 'package:wger/helpers/errors.dart';
|
||||
import 'package:wger/helpers/misc.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/workouts/log.dart';
|
||||
import 'package:wger/models/workouts/routine.dart';
|
||||
import 'package:wger/models/workouts/session.dart';
|
||||
import 'package:wger/widgets/measurements/charts.dart';
|
||||
import 'package:wger/widgets/routines/charts.dart';
|
||||
import 'package:wger/widgets/routines/forms/session.dart';
|
||||
|
||||
class SessionInfo extends StatefulWidget {
|
||||
final WorkoutSession _session;
|
||||
|
||||
const SessionInfo(this._session);
|
||||
|
||||
@override
|
||||
State<SessionInfo> createState() => _SessionInfoState();
|
||||
}
|
||||
|
||||
class _SessionInfoState extends State<SessionInfo> {
|
||||
bool editMode = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = AppLocalizations.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(
|
||||
i18n.workoutSession,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
subtitle: Text(
|
||||
DateFormat.yMd(
|
||||
Localizations.localeOf(context).languageCode,
|
||||
).format(widget._session.date),
|
||||
),
|
||||
onTap: () => setState(() => editMode = !editMode),
|
||||
trailing: Icon(editMode ? Icons.edit_off : Icons.edit),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
if (editMode)
|
||||
SessionForm(
|
||||
widget._session.routineId!,
|
||||
onSaved: () => setState(() => editMode = false),
|
||||
session: widget._session,
|
||||
)
|
||||
else
|
||||
Column(
|
||||
children: [
|
||||
_buildInfoRow(
|
||||
context,
|
||||
i18n.timeStart,
|
||||
widget._session.timeStart != null
|
||||
? MaterialLocalizations.of(
|
||||
context,
|
||||
).formatTimeOfDay(widget._session.timeStart!)
|
||||
: '-/-',
|
||||
),
|
||||
_buildInfoRow(
|
||||
context,
|
||||
i18n.timeEnd,
|
||||
widget._session.timeEnd != null
|
||||
? MaterialLocalizations.of(context).formatTimeOfDay(widget._session.timeEnd!)
|
||||
: '-/-',
|
||||
),
|
||||
_buildInfoRow(
|
||||
context,
|
||||
i18n.impression,
|
||||
widget._session.impressionAsString,
|
||||
),
|
||||
_buildInfoRow(
|
||||
context,
|
||||
i18n.notes,
|
||||
widget._session.notes.isNotEmpty ? widget._session.notes : '-/-',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoRow(BuildContext context, String label, String value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'$label: ',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Expanded(child: Text(value)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ExerciseLogChart extends StatelessWidget {
|
||||
final Map<num, List<Log>> _logs;
|
||||
final DateTime _selectedDate;
|
||||
|
||||
const ExerciseLogChart(this._logs, this._selectedDate);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = generateChartColors(_logs.keys.length).iterator;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
LogChartWidgetFl(_logs, _selectedDate),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
..._logs.keys.map((reps) {
|
||||
colors.moveNext();
|
||||
|
||||
return Indicator(
|
||||
color: colors.current,
|
||||
text: formatNum(reps).toString(),
|
||||
isSquare: false,
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DayLogWidget extends StatelessWidget {
|
||||
final DateTime _date;
|
||||
final Routine _routine;
|
||||
|
||||
const DayLogWidget(this._date, this._routine);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sessionApi = _routine.sessions.firstWhere(
|
||||
(sessionApi) => sessionApi.session.date.isSameDayAs(_date),
|
||||
);
|
||||
final exercises = sessionApi.exercises;
|
||||
|
||||
return Card(
|
||||
child: Column(
|
||||
children: [
|
||||
SessionInfo(sessionApi.session),
|
||||
...exercises.map((exercise) {
|
||||
final translation = exercise.getTranslation(
|
||||
Localizations.localeOf(context).languageCode,
|
||||
);
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
translation.name,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
...sessionApi.logs
|
||||
.where((l) => l.exerciseId == exercise.id)
|
||||
.map(
|
||||
(log) => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(log.repTextNoNl(context)),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
key: ValueKey('delete-log-${log.id}'),
|
||||
onPressed: () {
|
||||
showDeleteDialog(context, translation.name, log);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: ExerciseLogChart(
|
||||
_routine.groupLogsByRepetition(
|
||||
logs: _routine.filterLogsByExercise(exercise.id!),
|
||||
filterNullReps: true,
|
||||
filterNullWeights: true,
|
||||
),
|
||||
_date,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
99
lib/widgets/routines/logs/day_logs_container.dart
Normal file
99
lib/widgets/routines/logs/day_logs_container.dart
Normal file
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (c) 2020, 2025 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:wger/helpers/date.dart';
|
||||
import 'package:wger/helpers/errors.dart';
|
||||
import 'package:wger/models/workouts/routine.dart';
|
||||
|
||||
import 'exercise_log_chart.dart';
|
||||
import 'muscle_groups.dart';
|
||||
import 'session_info.dart';
|
||||
|
||||
class DayLogWidget extends StatelessWidget {
|
||||
final DateTime _date;
|
||||
final Routine _routine;
|
||||
|
||||
const DayLogWidget(this._date, this._routine);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sessionApi = _routine.sessions.firstWhere(
|
||||
(sessionApi) => sessionApi.session.date.isSameDayAs(_date),
|
||||
);
|
||||
final exercises = sessionApi.exercises;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Card(child: SessionInfo(sessionApi.session)),
|
||||
MuscleGroupsCard(sessionApi.logs),
|
||||
|
||||
Column(
|
||||
children: [
|
||||
...exercises.map((exercise) {
|
||||
final translation = exercise.getTranslation(
|
||||
Localizations.localeOf(context).languageCode,
|
||||
);
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
translation.name,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
...sessionApi.logs
|
||||
.where((l) => l.exerciseId == exercise.id)
|
||||
.map(
|
||||
(log) => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(log.repTextNoNl(context)),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete),
|
||||
key: ValueKey('delete-log-${log.id}'),
|
||||
onPressed: () {
|
||||
showDeleteDialog(context, translation.name, log);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: ExerciseLogChart(
|
||||
_routine.groupLogsByRepetition(
|
||||
logs: _routine.filterLogsByExercise(exercise.id!),
|
||||
filterNullReps: true,
|
||||
filterNullWeights: true,
|
||||
),
|
||||
_date,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
61
lib/widgets/routines/logs/exercise_log_chart.dart
Normal file
61
lib/widgets/routines/logs/exercise_log_chart.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (c) 2020, 2025 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:wger/helpers/colors.dart';
|
||||
import 'package:wger/helpers/misc.dart';
|
||||
import 'package:wger/models/workouts/log.dart';
|
||||
import 'package:wger/widgets/measurements/charts.dart';
|
||||
import 'package:wger/widgets/routines/charts.dart';
|
||||
|
||||
class ExerciseLogChart extends StatelessWidget {
|
||||
final Map<num, List<Log>> _logs;
|
||||
final DateTime _selectedDate;
|
||||
|
||||
const ExerciseLogChart(this._logs, this._selectedDate);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colors = generateChartColors(_logs.keys.length).iterator;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
LogChartWidgetFl(_logs, _selectedDate),
|
||||
SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
..._logs.keys.map((reps) {
|
||||
colors.moveNext();
|
||||
|
||||
return Indicator(
|
||||
color: colors.current,
|
||||
text: formatNum(reps).toString(),
|
||||
isSquare: false,
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 15),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
115
lib/widgets/routines/logs/exercises_expansion_card.dart
Normal file
115
lib/widgets/routines/logs/exercises_expansion_card.dart
Normal file
@@ -0,0 +1,115 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (c) 2020, 2025 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:wger/helpers/i18n.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/exercises/exercise.dart';
|
||||
import 'package:wger/models/workouts/log.dart';
|
||||
import 'package:wger/models/workouts/session_api.dart';
|
||||
|
||||
class ExercisesCard extends StatelessWidget {
|
||||
final WorkoutSessionApi session;
|
||||
|
||||
const ExercisesCard(this.session, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final exercises = session.exercises;
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context).exercises,
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
...exercises.map((exercise) {
|
||||
final logs = session.logs.where((log) => log.exerciseId == exercise.id).toList();
|
||||
return _ExerciseExpansionTile(exercise: exercise, logs: logs);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExerciseExpansionTile extends StatelessWidget {
|
||||
const _ExerciseExpansionTile({
|
||||
required this.exercise,
|
||||
required this.logs,
|
||||
});
|
||||
|
||||
final Exercise exercise;
|
||||
final List<Log> logs;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final languageCode = Localizations.localeOf(context).languageCode;
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final topSet = logs.isEmpty
|
||||
? null
|
||||
: logs.reduce((a, b) => (a.weight ?? 0) > (b.weight ?? 0) ? a : b);
|
||||
final topSetWeight = topSet?.weight?.toStringAsFixed(0) ?? 'N/A';
|
||||
final topSetWeightUnit = topSet?.weightUnitObj != null
|
||||
? getServerStringTranslation(topSet!.weightUnitObj!.name, context)
|
||||
: '';
|
||||
return ExpansionTile(
|
||||
// leading: const Icon(Icons.fitness_center),
|
||||
title: Text(exercise.getTranslation(languageCode).name, style: theme.textTheme.titleMedium),
|
||||
subtitle: Text('Top set: $topSetWeight $topSetWeightUnit'),
|
||||
children: logs.map((log) => _SetDataRow(log: log)).toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SetDataRow extends StatelessWidget {
|
||||
const _SetDataRow({required this.log});
|
||||
|
||||
final Log log;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final i18n = AppLocalizations.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
spacing: 5,
|
||||
children: [
|
||||
Text(
|
||||
log.repTextNoNl(context),
|
||||
style: theme.textTheme.bodyMedium,
|
||||
),
|
||||
// if (log.volume() > 0)
|
||||
// Text(
|
||||
// '${log.volume().toStringAsFixed(0)} ${getServerStringTranslation(log.weightUnitObj!.name, context)}',
|
||||
// style: theme.textTheme.bodyMedium,
|
||||
// ),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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) 2020, 2025 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
@@ -23,7 +23,7 @@ import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/workouts/routine.dart';
|
||||
import 'package:wger/theme/theme.dart';
|
||||
import 'package:wger/widgets/routines/log.dart';
|
||||
import 'package:wger/widgets/routines/logs/day_logs_container.dart';
|
||||
|
||||
class WorkoutLogs extends StatelessWidget {
|
||||
final Routine _routine;
|
||||
@@ -39,14 +39,6 @@ class WorkoutLogs extends StatelessWidget {
|
||||
AppLocalizations.of(context).labelWorkoutLogs,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
Text(
|
||||
AppLocalizations.of(context).logHelpEntries,
|
||||
textAlign: TextAlign.justify,
|
||||
),
|
||||
Text(
|
||||
AppLocalizations.of(context).logHelpEntriesUnits,
|
||||
textAlign: TextAlign.justify,
|
||||
),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: WorkoutLogCalendar(_routine),
|
||||
@@ -141,16 +133,29 @@ class _WorkoutLogCalendarState extends State<WorkoutLogCalendar> {
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8.0),
|
||||
SizedBox(
|
||||
child: ValueListenableBuilder<List<DateTime>>(
|
||||
valueListenable: _selectedEvents,
|
||||
builder: (context, logEvents, _) {
|
||||
// At the moment there is only one "event" per day
|
||||
return logEvents.isNotEmpty
|
||||
? DayLogWidget(logEvents.first, widget._routine)
|
||||
: Container();
|
||||
},
|
||||
),
|
||||
ExpansionTile(
|
||||
showTrailingIcon: false,
|
||||
dense: true,
|
||||
title: const Align(alignment: Alignment.centerLeft, child: Icon(Icons.info_outline)),
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context).logHelpEntries,
|
||||
textAlign: TextAlign.justify,
|
||||
),
|
||||
Text(
|
||||
AppLocalizations.of(context).logHelpEntriesUnits,
|
||||
textAlign: TextAlign.justify,
|
||||
),
|
||||
],
|
||||
),
|
||||
ValueListenableBuilder<List<DateTime>>(
|
||||
valueListenable: _selectedEvents,
|
||||
builder: (context, logEvents, _) {
|
||||
// At the moment there is only one "event" per day
|
||||
return logEvents.isNotEmpty
|
||||
? DayLogWidget(logEvents.first, widget._routine)
|
||||
: Container();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
138
lib/widgets/routines/logs/muscle_groups.dart
Normal file
138
lib/widgets/routines/logs/muscle_groups.dart
Normal file
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (c) 2020, 2025 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fl_chart/fl_chart.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/workouts/log.dart';
|
||||
|
||||
class MuscleGroup {
|
||||
final String name;
|
||||
final double percentage;
|
||||
final Color color;
|
||||
|
||||
MuscleGroup(this.name, this.percentage, this.color);
|
||||
}
|
||||
|
||||
class MuscleGroupsCard extends StatelessWidget {
|
||||
final List<Log> logs;
|
||||
|
||||
const MuscleGroupsCard(this.logs, {super.key});
|
||||
|
||||
List<MuscleGroup> _getMuscleGroups(BuildContext context) {
|
||||
final allMuscles = logs
|
||||
.expand((log) => [...log.exercise.muscles, ...log.exercise.musclesSecondary])
|
||||
.toList();
|
||||
if (allMuscles.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
final muscleCounts = allMuscles.groupListsBy((muscle) => muscle.nameTranslated(context));
|
||||
final total = allMuscles.length;
|
||||
|
||||
int colorIndex = 0;
|
||||
final colors = [
|
||||
Colors.blue,
|
||||
Colors.green,
|
||||
Colors.orange,
|
||||
Colors.purple,
|
||||
Colors.red,
|
||||
Colors.teal,
|
||||
Colors.deepOrange,
|
||||
Colors.indigo,
|
||||
Colors.pink,
|
||||
Colors.brown,
|
||||
Colors.cyan,
|
||||
Colors.lime,
|
||||
Colors.amber,
|
||||
Colors.lightGreen,
|
||||
Colors.deepPurple,
|
||||
];
|
||||
|
||||
return muscleCounts.entries.map((entry) {
|
||||
final percentage = (entry.value.length / total) * 100;
|
||||
final color = colors[colorIndex % colors.length];
|
||||
colorIndex++;
|
||||
return MuscleGroup(entry.key, percentage, color);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final muscles = _getMuscleGroups(context);
|
||||
final theme = Theme.of(context);
|
||||
final i18n = AppLocalizations.of(context);
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
i18n.muscles,
|
||||
style: theme.textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
height: 200,
|
||||
child: PieChart(
|
||||
PieChartData(
|
||||
sections: muscles.map((muscle) {
|
||||
return PieChartSectionData(
|
||||
color: muscle.color,
|
||||
value: muscle.percentage,
|
||||
title: i18n.percentValue(muscle.percentage.toStringAsFixed(0)),
|
||||
radius: 50,
|
||||
titleStyle: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: theme.colorScheme.onPrimary,
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
sectionsSpace: 2,
|
||||
centerSpaceRadius: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Wrap(
|
||||
spacing: 16,
|
||||
runSpacing: 8,
|
||||
children: muscles.map((muscle) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
color: muscle.color,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(muscle.name),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
113
lib/widgets/routines/logs/session_info.dart
Normal file
113
lib/widgets/routines/logs/session_info.dart
Normal file
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* This file is part of wger Workout Manager <https://github.com/wger-project>.
|
||||
* Copyright (c) 2020, 2025 wger Team
|
||||
*
|
||||
* wger Workout Manager is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/models/workouts/session.dart';
|
||||
import 'package:wger/widgets/routines/forms/session.dart';
|
||||
|
||||
class SessionInfo extends StatefulWidget {
|
||||
final WorkoutSession _session;
|
||||
|
||||
const SessionInfo(this._session);
|
||||
|
||||
@override
|
||||
State<SessionInfo> createState() => _SessionInfoState();
|
||||
}
|
||||
|
||||
class _SessionInfoState extends State<SessionInfo> {
|
||||
bool editMode = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = AppLocalizations.of(context);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(
|
||||
i18n.workoutSession,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
subtitle: Text(
|
||||
DateFormat.yMd(
|
||||
Localizations.localeOf(context).languageCode,
|
||||
).format(widget._session.date),
|
||||
),
|
||||
onTap: () => setState(() => editMode = !editMode),
|
||||
trailing: Icon(editMode ? Icons.edit_off : Icons.edit),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
if (editMode)
|
||||
SessionForm(
|
||||
widget._session.routineId!,
|
||||
onSaved: () => setState(() => editMode = false),
|
||||
session: widget._session,
|
||||
)
|
||||
else
|
||||
Column(
|
||||
children: [
|
||||
SessionRow(
|
||||
label: i18n.impression,
|
||||
value: widget._session.impressionAsString,
|
||||
),
|
||||
SessionRow(
|
||||
label: i18n.duration,
|
||||
value: widget._session.durationTxtWithStartEnd(context),
|
||||
),
|
||||
SessionRow(
|
||||
label: i18n.notes,
|
||||
value: widget._session.notes.isNotEmpty ? widget._session.notes : '-/-',
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SessionRow extends StatelessWidget {
|
||||
const SessionRow({
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.value,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'$label: ',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold),
|
||||
),
|
||||
Expanded(child: Text(value)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ import 'package:wger/models/workouts/routine.dart';
|
||||
import 'package:wger/providers/routines.dart';
|
||||
import 'package:wger/screens/routine_logs_screen.dart';
|
||||
import 'package:wger/screens/routine_screen.dart';
|
||||
import 'package:wger/widgets/routines/workout_logs.dart';
|
||||
import 'package:wger/widgets/routines/logs/log_overview_routine.dart';
|
||||
|
||||
import '../../test_data/routines.dart';
|
||||
import 'routine_logs_screen_test.mocks.dart';
|
||||
|
||||
Reference in New Issue
Block a user