mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
Allow swapping exercises in the gym mode
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
import 'package:wger/helpers/date.dart';
|
||||
import 'package:wger/helpers/json.dart';
|
||||
import 'package:wger/models/exercises/exercise.dart';
|
||||
import 'package:wger/models/workouts/day.dart';
|
||||
import 'package:wger/models/workouts/day_data.dart';
|
||||
import 'package:wger/models/workouts/log.dart';
|
||||
@@ -176,4 +177,37 @@ class Routine {
|
||||
|
||||
return groupedLogs;
|
||||
}
|
||||
|
||||
void replaceExercise(int oldExerciseId, Exercise newExercise) {
|
||||
for (final session in sessions) {
|
||||
for (final log in session.logs) {
|
||||
if (log.exerciseId == oldExerciseId) {
|
||||
log.exerciseId = newExercise.id!;
|
||||
log.exercise = newExercise;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final day in dayData) {
|
||||
for (final slot in day.slots) {
|
||||
for (final config in slot.setConfigs) {
|
||||
if (config.exerciseId == oldExerciseId) {
|
||||
config.exerciseId = newExercise.id!;
|
||||
config.exercise = newExercise;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (final day in dayDataGym) {
|
||||
for (final slot in day.slots) {
|
||||
for (final config in slot.setConfigs) {
|
||||
if (config.exerciseId == oldExerciseId) {
|
||||
config.exerciseId = newExercise.id!;
|
||||
config.exercise = newExercise;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -135,6 +135,58 @@ class SetConfigData {
|
||||
}
|
||||
}
|
||||
|
||||
SetConfigData copyWith({
|
||||
int? exerciseId,
|
||||
int? slotEntryId,
|
||||
SlotEntryType? type,
|
||||
String? textRepr,
|
||||
num? nrOfSets,
|
||||
num? maxNrOfSets,
|
||||
num? weight,
|
||||
num? maxWeight,
|
||||
int? weightUnitId,
|
||||
num? weightRounding,
|
||||
num? repetitions,
|
||||
num? maxRepetitions,
|
||||
int? repetitionsUnitId,
|
||||
num? repetitionsRounding,
|
||||
num? rir,
|
||||
num? maxRir,
|
||||
num? rpe,
|
||||
num? restTime,
|
||||
num? maxRestTime,
|
||||
String? comment,
|
||||
Exercise? exercise,
|
||||
WeightUnit? weightUnit,
|
||||
RepetitionUnit? repetitionsUnit,
|
||||
}) {
|
||||
return SetConfigData(
|
||||
exerciseId: exerciseId ?? this.exerciseId,
|
||||
slotEntryId: slotEntryId ?? this.slotEntryId,
|
||||
type: type ?? this.type,
|
||||
textRepr: textRepr ?? this.textRepr,
|
||||
nrOfSets: nrOfSets ?? this.nrOfSets,
|
||||
maxNrOfSets: maxNrOfSets ?? this.maxNrOfSets,
|
||||
weight: weight ?? this.weight,
|
||||
maxWeight: maxWeight ?? this.maxWeight,
|
||||
weightUnitId: weightUnitId ?? this.weightUnitId,
|
||||
weightRounding: weightRounding ?? this.weightRounding,
|
||||
repetitions: repetitions ?? this.repetitions,
|
||||
maxRepetitions: maxRepetitions ?? this.maxRepetitions,
|
||||
repetitionsUnitId: repetitionsUnitId ?? this.repetitionsUnitId,
|
||||
repetitionsRounding: repetitionsRounding ?? this.repetitionsRounding,
|
||||
rir: rir ?? this.rir,
|
||||
maxRir: maxRir ?? this.maxRir,
|
||||
rpe: rpe ?? this.rpe,
|
||||
restTime: restTime ?? this.restTime,
|
||||
maxRestTime: maxRestTime ?? this.maxRestTime,
|
||||
comment: comment ?? this.comment,
|
||||
exercise: exercise ?? this.exercise,
|
||||
weightUnit: weightUnit ?? this.weightUnit,
|
||||
repetitionsUnit: repetitionsUnit ?? this.repetitionsUnit,
|
||||
);
|
||||
}
|
||||
|
||||
// Boilerplate
|
||||
factory SetConfigData.fromJson(Map<String, dynamic> json) => _$SetConfigDataFromJson(json);
|
||||
|
||||
|
||||
@@ -420,6 +420,43 @@ class GymStateNotifier extends _$GymStateNotifier {
|
||||
_logger.fine('Set logDone=$isDone for slot page UUID $uuid');
|
||||
}
|
||||
|
||||
void replaceExercises(
|
||||
String pageEntryUUID, {
|
||||
required int originalExerciseId,
|
||||
required Exercise newExercise,
|
||||
}) {
|
||||
final updatedPages = state.pages.map((page) {
|
||||
if (page.type != PageType.set) {
|
||||
return page;
|
||||
}
|
||||
|
||||
if (page.uuid != pageEntryUUID) {
|
||||
return page;
|
||||
}
|
||||
|
||||
final updatedSlotPages = page.slotPages.map((slotPage) {
|
||||
if (slotPage.setConfigData != null &&
|
||||
slotPage.setConfigData!.exercise.id == originalExerciseId) {
|
||||
final updatedSetConfigData = slotPage.setConfigData!.copyWith(
|
||||
exerciseId: newExercise.id,
|
||||
exercise: newExercise,
|
||||
);
|
||||
return slotPage.copyWith(setConfigData: updatedSetConfigData);
|
||||
}
|
||||
return slotPage;
|
||||
}).toList();
|
||||
|
||||
return page.copyWith(slotPages: updatedSlotPages);
|
||||
}).toList();
|
||||
|
||||
// TODO: this should not be done in-place!
|
||||
state.routine.replaceExercise(originalExerciseId, newExercise);
|
||||
state = state.copyWith(
|
||||
pages: updatedPages,
|
||||
);
|
||||
_logger.fine('Replaced exercise $originalExerciseId with ${newExercise.id}');
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_logger.fine('Clearing state');
|
||||
state = state.copyWith(
|
||||
|
||||
@@ -18,10 +18,8 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/l10n/generated/app_localizations.dart';
|
||||
import 'package:wger/providers/exercises.dart';
|
||||
import 'package:wger/providers/gym_state.dart';
|
||||
import 'package:wger/theme/theme.dart';
|
||||
import 'package:wger/widgets/routines/gym_mode/workout_menu.dart';
|
||||
@@ -87,8 +85,6 @@ class NavigationHeader extends ConsumerWidget {
|
||||
const NavigationHeader(this._title, this._controller, {this.showEndWorkoutButton = true});
|
||||
|
||||
Widget getDialog(BuildContext context, int totalPages, List<PageEntry> pages) {
|
||||
final exercisesProvider = context.read<ExercisesProvider>();
|
||||
|
||||
final endWorkoutButton = showEndWorkoutButton
|
||||
? TextButton(
|
||||
child: Text(AppLocalizations.of(context).endWorkout),
|
||||
|
||||
@@ -18,8 +18,10 @@
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
import 'package:wger/helpers/consts.dart';
|
||||
import 'package:wger/providers/gym_state.dart';
|
||||
import 'package:wger/widgets/exercises/autocompleter.dart';
|
||||
|
||||
class WorkoutMenu extends StatelessWidget {
|
||||
final PageController _controller;
|
||||
@@ -49,6 +51,14 @@ class WorkoutMenu extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'Swapping an exercise only affects the current workout, no changes are saved.',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -97,100 +107,180 @@ class NavigationTab extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class ProgressionTab extends ConsumerWidget {
|
||||
class ProgressionTab extends ConsumerStatefulWidget {
|
||||
final _logger = Logger('ProgressionTab');
|
||||
final PageController _controller;
|
||||
|
||||
const ProgressionTab(this._controller);
|
||||
ProgressionTab(this._controller, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
ConsumerState<ProgressionTab> createState() => _ProgressionTabState();
|
||||
}
|
||||
|
||||
class _ProgressionTabState extends ConsumerState<ProgressionTab> {
|
||||
String? showDetailsForPageId;
|
||||
_ProgressionTabState();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = ref.watch(gymStateProvider);
|
||||
final theme = Theme.of(context);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: Column(
|
||||
children: [
|
||||
...state.pages.where((page) => page.type == PageType.set).map((page) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...page.slotPages.where((slotPage) => slotPage.type == SlotPageType.log).map(
|
||||
(
|
||||
slotPage,
|
||||
) {
|
||||
final exercise = slotPage.setConfigData!.exercise
|
||||
.getTranslation(
|
||||
Localizations.localeOf(context).languageCode,
|
||||
)
|
||||
.name;
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||
child: Column(
|
||||
children: [
|
||||
...state.pages.where((page) => page.type == PageType.set).map((page) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...page.slotPages.where((slotPage) => slotPage.type == SlotPageType.log).map(
|
||||
(
|
||||
slotPage,
|
||||
) {
|
||||
final exercise = slotPage.setConfigData!.exercise
|
||||
.getTranslation(
|
||||
Localizations.localeOf(context).languageCode,
|
||||
)
|
||||
.name;
|
||||
|
||||
// Sets that are done are marked with a strikethrough
|
||||
final decoration = slotPage.logDone
|
||||
? TextDecoration.lineThrough
|
||||
: TextDecoration.none;
|
||||
// Sets that are done are marked with a strikethrough
|
||||
final decoration = slotPage.logDone
|
||||
? TextDecoration.lineThrough
|
||||
: TextDecoration.none;
|
||||
|
||||
// Sets that are done have a lighter color
|
||||
final color = slotPage.logDone
|
||||
? theme.colorScheme.onSurface.withValues(alpha: 0.6)
|
||||
: null;
|
||||
// Sets that are done have a lighter color
|
||||
final color = slotPage.logDone
|
||||
? theme.colorScheme.onSurface.withValues(alpha: 0.6)
|
||||
: null;
|
||||
|
||||
// he row for the current page is highlighted in bold
|
||||
final fontWeight = state.currentPage == slotPage.pageIndex
|
||||
? FontWeight.bold
|
||||
: null;
|
||||
// The row for the current page is highlighted in bold
|
||||
final fontWeight = state.currentPage == slotPage.pageIndex
|
||||
? FontWeight.bold
|
||||
: null;
|
||||
|
||||
return Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
if (slotPage.logDone) const TextSpan(text: '✅ '),
|
||||
TextSpan(
|
||||
text: '$exercise - ${slotPage.setConfigData!.textReprWithType}',
|
||||
style: theme.textTheme.bodyMedium!.copyWith(
|
||||
decoration: decoration,
|
||||
fontWeight: fontWeight,
|
||||
color: color,
|
||||
return Text.rich(
|
||||
TextSpan(
|
||||
children: [
|
||||
if (slotPage.logDone) const TextSpan(text: '✅ '),
|
||||
TextSpan(
|
||||
text: '$exercise - ${slotPage.setConfigData!.textReprWithType}',
|
||||
style: theme.textTheme.bodyMedium!.copyWith(
|
||||
decoration: decoration,
|
||||
fontWeight: fontWeight,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
if (showDetailsForPageId == page.uuid) ExerciseSwapWidget(page.uuid),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
//mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
if (showDetailsForPageId == page.uuid) {
|
||||
setState(() {
|
||||
widget._logger.fine('Hiding details');
|
||||
showDetailsForPageId = null;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
widget._logger.fine('Showing details for page ${page.uuid}');
|
||||
showDetailsForPageId = page.uuid;
|
||||
});
|
||||
}
|
||||
},
|
||||
icon: Icon(
|
||||
showDetailsForPageId == page.uuid
|
||||
? Icons.change_circle
|
||||
: Icons.change_circle_outlined,
|
||||
),
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
);
|
||||
},
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
//mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.swap_horiz, size: 18),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {},
|
||||
icon: const Icon(Icons.add, size: 18),
|
||||
),
|
||||
Expanded(child: Container()),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_controller.animateToPage(
|
||||
page.pageIndex,
|
||||
duration: DEFAULT_ANIMATION_DURATION,
|
||||
curve: DEFAULT_ANIMATION_CURVE,
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
// IconButton(
|
||||
// onPressed: () {},
|
||||
// icon: const Icon(Icons.add),
|
||||
// ),
|
||||
Expanded(child: Container()),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
widget._controller.animateToPage(
|
||||
page.pageIndex,
|
||||
duration: DEFAULT_ANIMATION_DURATION,
|
||||
curve: DEFAULT_ANIMATION_CURVE,
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
icon: const Icon(Icons.chevron_right),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ExerciseSwapWidget extends ConsumerWidget {
|
||||
final _logger = Logger('ExerciseSwapWidget');
|
||||
|
||||
final String pageUUID;
|
||||
|
||||
ExerciseSwapWidget(this.pageUUID, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final state = ref.watch(gymStateProvider);
|
||||
final gymProvider = ref.read(gymStateProvider.notifier);
|
||||
final page = state.pages.firstWhere((p) => p.uuid == pageUUID);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(5),
|
||||
child: Column(
|
||||
children: [
|
||||
...page.exercises.map((e) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Text(
|
||||
e.getTranslation('en').name,
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
),
|
||||
const Icon(Icons.swap_vert),
|
||||
ExerciseAutocompleter(
|
||||
onExerciseSelected: (exercise) {
|
||||
gymProvider.replaceExercises(
|
||||
page.uuid,
|
||||
originalExerciseId: e.id!,
|
||||
newExercise: exercise,
|
||||
);
|
||||
_logger.fine('Replaced exercise ${e.id} with ${exercise.id}');
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
],
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:wger/models/workouts/day_data.dart';
|
||||
import 'package:wger/providers/gym_state.dart';
|
||||
|
||||
import '../../test_data/routines.dart';
|
||||
@@ -27,22 +26,26 @@ void main() {
|
||||
group('GymStateNotifier.calculatePages', () {
|
||||
late GymStateNotifier notifier;
|
||||
late ProviderContainer container;
|
||||
late DayData day;
|
||||
|
||||
setUp(() {
|
||||
container = ProviderContainer.test();
|
||||
notifier = container.read(gymStateProvider.notifier);
|
||||
day = getTestRoutine().dayData.first;
|
||||
});
|
||||
|
||||
test(
|
||||
'Correctly generates pages: exercise and timer',
|
||||
() {
|
||||
// Arrange
|
||||
notifier.state = notifier.state.copyWith(showExercisePages: true, showTimerPages: true);
|
||||
notifier.state = notifier.state.copyWith(
|
||||
showExercisePages: true,
|
||||
showTimerPages: true,
|
||||
dayId: 1,
|
||||
iteration: 1,
|
||||
routine: getTestRoutine(),
|
||||
);
|
||||
|
||||
// Act
|
||||
notifier.calculatePages(day);
|
||||
notifier.calculatePages();
|
||||
|
||||
// Assert
|
||||
final pages = notifier.state.pages;
|
||||
@@ -61,10 +64,16 @@ void main() {
|
||||
|
||||
test('Correctly generates pages: no exercises and no timer', () {
|
||||
// Arrange
|
||||
notifier.state = notifier.state.copyWith(showExercisePages: false, showTimerPages: false);
|
||||
notifier.state = notifier.state.copyWith(
|
||||
showExercisePages: false,
|
||||
showTimerPages: false,
|
||||
dayId: 1,
|
||||
iteration: 1,
|
||||
routine: getTestRoutine(),
|
||||
);
|
||||
|
||||
// Act
|
||||
notifier.calculatePages(day);
|
||||
notifier.calculatePages();
|
||||
|
||||
// Assert
|
||||
final pages = notifier.state.pages;
|
||||
@@ -78,10 +87,16 @@ void main() {
|
||||
|
||||
test('Correctly generates pages: exercises and no timer', () {
|
||||
// Arrange
|
||||
notifier.state = notifier.state.copyWith(showExercisePages: true, showTimerPages: false);
|
||||
notifier.state = notifier.state.copyWith(
|
||||
showExercisePages: true,
|
||||
showTimerPages: false,
|
||||
dayId: 1,
|
||||
iteration: 1,
|
||||
routine: getTestRoutine(),
|
||||
);
|
||||
|
||||
// Act
|
||||
notifier.calculatePages(day);
|
||||
notifier.calculatePages();
|
||||
|
||||
// Assert
|
||||
final pages = notifier.state.pages;
|
||||
|
||||
Reference in New Issue
Block a user