mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
1094 lines
33 KiB
Dart
1094 lines
33 KiB
Dart
/*
|
||
* 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 'dart:async';
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
|
||
import 'package:flutter_html/flutter_html.dart';
|
||
import 'package:intl/intl.dart';
|
||
import 'package:provider/provider.dart';
|
||
import 'package:wger/helpers/consts.dart';
|
||
import 'package:wger/helpers/gym_mode.dart';
|
||
import 'package:wger/helpers/json.dart';
|
||
import 'package:wger/helpers/misc.dart';
|
||
import 'package:wger/helpers/ui.dart';
|
||
import 'package:wger/models/exercises/exercise.dart';
|
||
import 'package:wger/models/http_exception.dart';
|
||
import 'package:wger/models/workouts/day.dart';
|
||
import 'package:wger/models/workouts/log.dart';
|
||
import 'package:wger/models/workouts/session.dart';
|
||
import 'package:wger/models/workouts/set.dart';
|
||
import 'package:wger/models/workouts/setting.dart';
|
||
import 'package:wger/models/workouts/workout_plan.dart';
|
||
import 'package:wger/providers/exercises.dart';
|
||
import 'package:wger/providers/workout_plans.dart';
|
||
import 'package:wger/theme/theme.dart';
|
||
import 'package:wger/widgets/core/core.dart';
|
||
import 'package:wger/widgets/exercises/images.dart';
|
||
import 'package:wger/widgets/workouts/forms.dart';
|
||
|
||
class GymMode extends StatefulWidget {
|
||
final Day _workoutDay;
|
||
late TimeOfDay _start;
|
||
|
||
GymMode(this._workoutDay) {
|
||
_start = TimeOfDay.now();
|
||
}
|
||
|
||
@override
|
||
_GymModeState createState() => _GymModeState();
|
||
}
|
||
|
||
class _GymModeState extends State<GymMode> {
|
||
var _totalElements = 1;
|
||
|
||
/// Map with the first (navigation) page for each exercise
|
||
Map<String, int> _exercisePages = new Map();
|
||
|
||
PageController _controller = PageController(
|
||
initialPage: 0,
|
||
);
|
||
|
||
@override
|
||
void dispose() {
|
||
_controller.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
|
||
// Calculate amount of elements for progress indicator
|
||
for (var set in widget._workoutDay.sets) {
|
||
_totalElements = _totalElements + set.settingsComputed.length;
|
||
}
|
||
|
||
// Calculate the pages for the navigation
|
||
//
|
||
// This duplicates the code below in the getContent method, but it seems to
|
||
// be the easiest way
|
||
var currentPage = 1;
|
||
for (var set in widget._workoutDay.sets) {
|
||
var firstPage = true;
|
||
for (var setting in set.settingsComputed) {
|
||
final exercise =
|
||
Provider.of<ExercisesProvider>(context, listen: false).findById(setting.exerciseId);
|
||
|
||
if (firstPage) {
|
||
_exercisePages[exercise.name] = currentPage;
|
||
currentPage++;
|
||
}
|
||
|
||
// Log Page
|
||
currentPage++;
|
||
|
||
// Timer
|
||
currentPage++;
|
||
firstPage = false;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Returns the list of exercise overview, sets and pause pages
|
||
List<Widget> getContent() {
|
||
final exerciseProvider = Provider.of<ExercisesProvider>(context, listen: false);
|
||
final workoutProvider = Provider.of<WorkoutPlansProvider>(context, listen: false);
|
||
var currentElement = 1;
|
||
List<Widget> out = [];
|
||
|
||
for (var set in widget._workoutDay.sets) {
|
||
var firstPage = true;
|
||
for (var setting in set.settingsComputed) {
|
||
var ratioCompleted = currentElement / _totalElements;
|
||
final exercise = exerciseProvider.findById(setting.exerciseId);
|
||
currentElement++;
|
||
|
||
if (firstPage) {
|
||
out.add(ExerciseOverview(
|
||
_controller,
|
||
exercise,
|
||
ratioCompleted,
|
||
_exercisePages,
|
||
));
|
||
}
|
||
|
||
out.add(LogPage(
|
||
_controller,
|
||
setting,
|
||
set,
|
||
exercise,
|
||
workoutProvider.findById(widget._workoutDay.workoutId),
|
||
ratioCompleted,
|
||
_exercisePages,
|
||
));
|
||
out.add(TimerWidget(_controller, ratioCompleted, _exercisePages));
|
||
firstPage = false;
|
||
}
|
||
}
|
||
|
||
return out;
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return PageView(
|
||
controller: _controller,
|
||
children: [
|
||
StartPage(
|
||
_controller,
|
||
widget._workoutDay,
|
||
_exercisePages,
|
||
),
|
||
...getContent(),
|
||
SessionPage(
|
||
Provider.of<WorkoutPlansProvider>(context, listen: false)
|
||
.findById(widget._workoutDay.workoutId),
|
||
_controller,
|
||
widget._start,
|
||
_exercisePages,
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class StartPage extends StatelessWidget {
|
||
PageController _controller;
|
||
final Day _day;
|
||
Map<String, int> _exercisePages;
|
||
|
||
StartPage(this._controller, this._day, this._exercisePages);
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Column(
|
||
children: [
|
||
NavigationHeader(
|
||
AppLocalizations.of(context).todaysWorkout,
|
||
_controller,
|
||
exercisePages: _exercisePages,
|
||
),
|
||
Divider(),
|
||
Expanded(
|
||
child: ListView(
|
||
children: [
|
||
..._day.sets.map(
|
||
(set) {
|
||
return Column(
|
||
children: [
|
||
...set.settingsFiltered.map((s) {
|
||
return Column(
|
||
children: [
|
||
Text(
|
||
s.exerciseObj.name,
|
||
style: Theme.of(context).textTheme.headline6,
|
||
),
|
||
...set.getSmartRepr(s.exerciseObj).map((e) => Text(e)).toList(),
|
||
SizedBox(height: 15),
|
||
],
|
||
);
|
||
}).toList(),
|
||
],
|
||
);
|
||
},
|
||
).toList(),
|
||
],
|
||
),
|
||
),
|
||
ElevatedButton(
|
||
child: Text(AppLocalizations.of(context).start),
|
||
onPressed: () {
|
||
_controller.nextPage(duration: Duration(milliseconds: 200), curve: Curves.bounceIn);
|
||
},
|
||
),
|
||
NavigationFooter(
|
||
_controller,
|
||
0,
|
||
showPrevious: false,
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class LogPage extends StatefulWidget {
|
||
PageController _controller;
|
||
Setting _setting;
|
||
Set _set;
|
||
Exercise _exercise;
|
||
WorkoutPlan _workoutPlan;
|
||
final double _ratioCompleted;
|
||
Map<String, int> _exercisePages;
|
||
Log _log = Log.empty();
|
||
|
||
LogPage(
|
||
this._controller,
|
||
this._setting,
|
||
this._set,
|
||
this._exercise,
|
||
this._workoutPlan,
|
||
this._ratioCompleted,
|
||
this._exercisePages,
|
||
) {
|
||
_log.date = DateTime.now();
|
||
_log.workoutPlan = _workoutPlan.id!;
|
||
_log.exercise = _exercise;
|
||
_log.weightUnit = _setting.weightUnitObj;
|
||
_log.repetitionUnit = _setting.repetitionUnitObj;
|
||
_log.rir = _setting.rir;
|
||
}
|
||
|
||
@override
|
||
_LogPageState createState() => _LogPageState();
|
||
}
|
||
|
||
class _LogPageState extends State<LogPage> {
|
||
final _form = GlobalKey<FormState>();
|
||
String rirValue = Setting.DEFAULT_RIR;
|
||
final _repsController = TextEditingController();
|
||
final _weightController = TextEditingController();
|
||
var _detailed = false;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
|
||
if (widget._setting.reps != null) {
|
||
_repsController.text = widget._setting.reps.toString();
|
||
}
|
||
|
||
if (widget._setting.weight != null) {
|
||
_weightController.text = widget._setting.weight.toString();
|
||
}
|
||
}
|
||
|
||
Widget getRepsWidget() {
|
||
return Row(
|
||
children: [
|
||
IconButton(
|
||
icon: Icon(
|
||
Icons.remove,
|
||
color: Colors.black,
|
||
),
|
||
onPressed: () {
|
||
try {
|
||
int newValue = int.parse(_repsController.text) - 1;
|
||
if (newValue > 0) {
|
||
_repsController.text = newValue.toString();
|
||
}
|
||
} on FormatException catch (e) {}
|
||
},
|
||
),
|
||
Expanded(
|
||
child: TextFormField(
|
||
decoration: InputDecoration(
|
||
labelText: AppLocalizations.of(context).repetitions,
|
||
),
|
||
enabled: true,
|
||
controller: _repsController,
|
||
keyboardType: TextInputType.number,
|
||
onFieldSubmitted: (_) {},
|
||
onSaved: (newValue) {
|
||
widget._log.reps = int.parse(newValue!);
|
||
},
|
||
validator: (value) {
|
||
try {
|
||
int.parse(value!);
|
||
} catch (error) {
|
||
return AppLocalizations.of(context).enterValidNumber;
|
||
}
|
||
return null;
|
||
},
|
||
),
|
||
),
|
||
IconButton(
|
||
icon: Icon(
|
||
Icons.add,
|
||
color: Colors.black,
|
||
),
|
||
onPressed: () {
|
||
try {
|
||
int newValue = int.parse(_repsController.text) + 1;
|
||
_repsController.text = newValue.toString();
|
||
} on FormatException catch (e) {}
|
||
},
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget getWeightWidget() {
|
||
var minPlateWeight = 1.25;
|
||
return Row(
|
||
children: [
|
||
IconButton(
|
||
icon: Icon(
|
||
Icons.remove,
|
||
color: Colors.black,
|
||
),
|
||
onPressed: () {
|
||
try {
|
||
double newValue = double.parse(_weightController.text) - (2 * minPlateWeight);
|
||
if (newValue > 0) {
|
||
setState(() {
|
||
widget._log.weight = newValue;
|
||
_weightController.text = newValue.toString();
|
||
});
|
||
}
|
||
} on FormatException catch (e) {}
|
||
},
|
||
),
|
||
Expanded(
|
||
child: TextFormField(
|
||
decoration: InputDecoration(
|
||
labelText: AppLocalizations.of(context).weight,
|
||
),
|
||
controller: _weightController,
|
||
keyboardType: TextInputType.number,
|
||
onFieldSubmitted: (_) {},
|
||
onChanged: (value) {
|
||
try {
|
||
double.parse(value);
|
||
setState(() {
|
||
widget._log.weight = double.parse(value);
|
||
});
|
||
} on FormatException catch (e) {}
|
||
},
|
||
onSaved: (newValue) {
|
||
setState(() {
|
||
widget._log.weight = double.parse(newValue!);
|
||
});
|
||
},
|
||
validator: (value) {
|
||
try {
|
||
double.parse(value!);
|
||
} catch (error) {
|
||
return AppLocalizations.of(context).enterValidNumber;
|
||
}
|
||
return null;
|
||
},
|
||
),
|
||
),
|
||
IconButton(
|
||
icon: Icon(
|
||
Icons.add,
|
||
color: Colors.black,
|
||
),
|
||
onPressed: () {
|
||
try {
|
||
double newValue = double.parse(_weightController.text) + (2 * minPlateWeight);
|
||
setState(() {
|
||
widget._log.weight = newValue;
|
||
_weightController.text = newValue.toString();
|
||
});
|
||
} on FormatException catch (e) {}
|
||
},
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget getForm() {
|
||
return Form(
|
||
key: _form,
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Text(
|
||
AppLocalizations.of(context).newEntry,
|
||
style: Theme.of(context).textTheme.headline6,
|
||
textAlign: TextAlign.center,
|
||
),
|
||
if (!_detailed)
|
||
Row(
|
||
children: [
|
||
Flexible(child: getRepsWidget()),
|
||
SizedBox(width: 8),
|
||
Flexible(child: getWeightWidget()),
|
||
],
|
||
),
|
||
if (_detailed)
|
||
Row(
|
||
crossAxisAlignment: CrossAxisAlignment.end,
|
||
children: [
|
||
Flexible(child: getRepsWidget()),
|
||
SizedBox(width: 8),
|
||
Flexible(child: RepetitionUnitInputWidget(widget._log)),
|
||
],
|
||
),
|
||
if (_detailed)
|
||
Row(
|
||
crossAxisAlignment: CrossAxisAlignment.end,
|
||
children: [
|
||
Flexible(child: getWeightWidget()),
|
||
SizedBox(width: 8),
|
||
Flexible(child: WeightUnitInputWidget(widget._log))
|
||
],
|
||
),
|
||
if (_detailed) RiRInputWidget(widget._log),
|
||
SwitchListTile(
|
||
title: Text(AppLocalizations.of(context).setUnitsAndRir),
|
||
value: _detailed,
|
||
onChanged: (value) {
|
||
setState(() {
|
||
_detailed = !_detailed;
|
||
});
|
||
},
|
||
),
|
||
ElevatedButton(
|
||
child: Text(AppLocalizations.of(context).save),
|
||
onPressed: () async {
|
||
// Validate and save the current values to the weightEntry
|
||
final isValid = _form.currentState!.validate();
|
||
if (!isValid) {
|
||
return;
|
||
}
|
||
_form.currentState!.save();
|
||
|
||
// Save the entry on the server
|
||
try {
|
||
await Provider.of<WorkoutPlansProvider>(context, listen: false).addLog(widget._log);
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
duration: Duration(seconds: 2), // default is 4
|
||
content: Text(
|
||
AppLocalizations.of(context).successfullySaved,
|
||
textAlign: TextAlign.center,
|
||
),
|
||
),
|
||
);
|
||
widget._controller.nextPage(
|
||
duration: DEFAULT_ANIMATION_DURATION,
|
||
curve: DEFAULT_ANIMATION_CURVE,
|
||
);
|
||
} on WgerHttpException catch (error) {
|
||
showHttpExceptionErrorDialog(error, context);
|
||
} catch (error) {
|
||
showErrorDialog(error, context);
|
||
}
|
||
},
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget getPastLogs() {
|
||
return ListView(
|
||
children: [
|
||
Text(
|
||
AppLocalizations.of(context).labelWorkoutLogs,
|
||
style: Theme.of(context).textTheme.headline6,
|
||
textAlign: TextAlign.center,
|
||
),
|
||
...widget._workoutPlan.filterLogsByExercise(widget._exercise, unique: true).map((log) {
|
||
return ListTile(
|
||
title: Text(log.singleLogRepTextNoNl),
|
||
subtitle:
|
||
Text(DateFormat.yMd(Localizations.localeOf(context).languageCode).format(log.date)),
|
||
trailing: Icon(Icons.copy),
|
||
onTap: () {
|
||
setState(() {
|
||
// Text field
|
||
_repsController.text = log.reps.toString();
|
||
_weightController.text = log.weight.toString();
|
||
|
||
// Drop downs
|
||
widget._log.rir = log.rir;
|
||
widget._log.repetitionUnit = log.repetitionUnitObj;
|
||
widget._log.weightUnit = log.weightUnitObj;
|
||
});
|
||
},
|
||
contentPadding: EdgeInsets.symmetric(horizontal: 40),
|
||
);
|
||
}).toList(),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget getPlates() {
|
||
final plates = plateCalculator(
|
||
double.parse(_weightController.text),
|
||
BAR_WEIGHT,
|
||
AVAILABLE_PLATES,
|
||
);
|
||
final groupedPlates = groupPlates(plates);
|
||
|
||
return Column(
|
||
children: [
|
||
Text(
|
||
AppLocalizations.of(context).plateCalculator,
|
||
style: Theme.of(context).textTheme.headline6,
|
||
),
|
||
SizedBox(
|
||
height: 35,
|
||
child: plates.length > 0
|
||
? Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
...groupedPlates.keys
|
||
.map(
|
||
(key) => Row(
|
||
children: [
|
||
Text(groupedPlates[key].toString()),
|
||
Text('×'),
|
||
Container(
|
||
decoration: BoxDecoration(
|
||
color: wgerPrimaryColorLight,
|
||
shape: BoxShape.circle,
|
||
),
|
||
child: Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 3),
|
||
child: SizedBox(
|
||
height: 35,
|
||
width: 35,
|
||
child: Align(
|
||
alignment: Alignment.center,
|
||
child: Text(
|
||
key.toString(),
|
||
style: TextStyle(fontWeight: FontWeight.bold),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
SizedBox(width: 10),
|
||
],
|
||
),
|
||
)
|
||
.toList()
|
||
],
|
||
)
|
||
: MutedText(AppLocalizations.of(context).plateCalculatorNotDivisible),
|
||
),
|
||
SizedBox(height: 3),
|
||
],
|
||
);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Column(
|
||
children: [
|
||
NavigationHeader(
|
||
widget._exercise.name,
|
||
widget._controller,
|
||
exercisePages: widget._exercisePages,
|
||
),
|
||
Center(
|
||
child: Text(
|
||
widget._setting.singleSettingRepText,
|
||
style: Theme.of(context).textTheme.headline3,
|
||
textAlign: TextAlign.center,
|
||
),
|
||
),
|
||
if (widget._set.comment != '')
|
||
Text(
|
||
widget._set.comment,
|
||
textAlign: TextAlign.center,
|
||
),
|
||
SizedBox(height: 10),
|
||
Expanded(
|
||
child: (widget._workoutPlan.filterLogsByExercise(widget._exercise).length > 0)
|
||
? getPastLogs()
|
||
: Container()),
|
||
// Only show calculator for barbell
|
||
if (widget._log.exerciseObj.equipment.map((e) => e.id).contains(ID_EQUIPMENT_BARBELL))
|
||
getPlates(),
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||
child: Card(
|
||
color: wgerBackground,
|
||
child: getForm(),
|
||
),
|
||
),
|
||
NavigationFooter(widget._controller, widget._ratioCompleted),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class ExerciseOverview extends StatelessWidget {
|
||
final PageController _controller;
|
||
final Exercise _exercise;
|
||
final double _ratioCompleted;
|
||
Map<String, int> _exercisePages;
|
||
|
||
ExerciseOverview(
|
||
this._controller,
|
||
this._exercise,
|
||
this._ratioCompleted,
|
||
this._exercisePages,
|
||
);
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Column(
|
||
children: [
|
||
NavigationHeader(
|
||
_exercise.name,
|
||
_controller,
|
||
exercisePages: _exercisePages,
|
||
),
|
||
Divider(),
|
||
Expanded(
|
||
child: ListView(
|
||
padding: EdgeInsets.symmetric(horizontal: 15),
|
||
children: [
|
||
Text(
|
||
_exercise.categoryObj.name,
|
||
style: Theme.of(context).textTheme.headline6,
|
||
textAlign: TextAlign.center,
|
||
),
|
||
..._exercise.equipment
|
||
.map((e) => Text(
|
||
e.name,
|
||
style: Theme.of(context).textTheme.headline6,
|
||
textAlign: TextAlign.center,
|
||
))
|
||
.toList(),
|
||
if (_exercise.images.length > 0)
|
||
Container(
|
||
width: double.infinity,
|
||
height: 200,
|
||
child: ListView(
|
||
scrollDirection: Axis.horizontal,
|
||
children: [
|
||
..._exercise.images.map((e) => ExerciseImageWidget(image: e)).toList(),
|
||
],
|
||
),
|
||
),
|
||
Html(data: _exercise.description),
|
||
],
|
||
),
|
||
),
|
||
NavigationFooter(_controller, _ratioCompleted),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class SessionPage extends StatefulWidget {
|
||
WorkoutPlan _workoutPlan;
|
||
PageController _controller;
|
||
TimeOfDay _start;
|
||
Map<String, int> _exercisePages;
|
||
|
||
SessionPage(
|
||
this._workoutPlan,
|
||
this._controller,
|
||
this._start,
|
||
this._exercisePages,
|
||
);
|
||
|
||
@override
|
||
_SessionPageState createState() => _SessionPageState();
|
||
}
|
||
|
||
class _SessionPageState extends State<SessionPage> {
|
||
final _form = GlobalKey<FormState>();
|
||
final impressionController = TextEditingController();
|
||
final notesController = TextEditingController();
|
||
final timeStartController = TextEditingController();
|
||
final timeEndController = TextEditingController();
|
||
|
||
var _session = WorkoutSession();
|
||
|
||
/// Selected impression: bad, neutral, good
|
||
var selectedImpression = [false, true, false];
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
|
||
timeStartController.text = timeToString(widget._start)!;
|
||
timeEndController.text = timeToString(TimeOfDay.now())!;
|
||
_session.workoutId = widget._workoutPlan.id!;
|
||
_session.impression = DEFAULT_IMPRESSION;
|
||
_session.date = DateTime.now();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Column(
|
||
children: [
|
||
NavigationHeader(
|
||
AppLocalizations.of(context).workoutSession,
|
||
widget._controller,
|
||
exercisePages: widget._exercisePages,
|
||
),
|
||
Divider(),
|
||
Expanded(child: Container()),
|
||
Padding(
|
||
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||
child: Form(
|
||
key: _form,
|
||
child: Column(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
ToggleButtons(
|
||
children: <Widget>[
|
||
Icon(
|
||
Icons.sentiment_very_dissatisfied,
|
||
),
|
||
Icon(
|
||
Icons.sentiment_neutral,
|
||
),
|
||
Icon(
|
||
Icons.sentiment_very_satisfied,
|
||
),
|
||
],
|
||
renderBorder: false,
|
||
onPressed: (int index) {
|
||
setState(() {
|
||
for (int buttonIndex = 0;
|
||
buttonIndex < selectedImpression.length;
|
||
buttonIndex++) {
|
||
_session.impression = index + 1;
|
||
|
||
if (buttonIndex == index) {
|
||
selectedImpression[buttonIndex] = true;
|
||
} else {
|
||
selectedImpression[buttonIndex] = false;
|
||
}
|
||
}
|
||
});
|
||
},
|
||
isSelected: selectedImpression,
|
||
),
|
||
TextFormField(
|
||
decoration: InputDecoration(
|
||
labelText: AppLocalizations.of(context).notes,
|
||
),
|
||
maxLines: 3,
|
||
controller: notesController,
|
||
keyboardType: TextInputType.multiline,
|
||
onFieldSubmitted: (_) {},
|
||
onSaved: (newValue) {
|
||
_session.notes = newValue!;
|
||
},
|
||
),
|
||
Row(
|
||
children: [
|
||
Flexible(
|
||
child: TextFormField(
|
||
decoration: InputDecoration(
|
||
labelText: AppLocalizations.of(context).timeStart,
|
||
errorMaxLines: 2,
|
||
),
|
||
controller: timeStartController,
|
||
onFieldSubmitted: (_) {},
|
||
onTap: () async {
|
||
// Stop keyboard from appearing
|
||
FocusScope.of(context).requestFocus(new FocusNode());
|
||
|
||
// Open time picker
|
||
var pickedTime = await showTimePicker(
|
||
context: context,
|
||
initialTime: widget._start,
|
||
);
|
||
|
||
if (pickedTime != null) {
|
||
timeStartController.text = timeToString(pickedTime)!;
|
||
}
|
||
},
|
||
onSaved: (newValue) {
|
||
_session.timeStart = stringToTime(newValue);
|
||
},
|
||
validator: (_) {
|
||
TimeOfDay startTime = stringToTime(timeStartController.text);
|
||
TimeOfDay endTime = stringToTime(timeEndController.text);
|
||
if (startTime.isAfter(endTime)) {
|
||
return AppLocalizations.of(context).timeStartAhead;
|
||
}
|
||
return null;
|
||
}),
|
||
),
|
||
SizedBox(width: 10),
|
||
Flexible(
|
||
child: TextFormField(
|
||
decoration:
|
||
InputDecoration(labelText: AppLocalizations.of(context).timeEnd),
|
||
controller: timeEndController,
|
||
onFieldSubmitted: (_) {},
|
||
onTap: () async {
|
||
// Stop keyboard from appearing
|
||
FocusScope.of(context).requestFocus(new FocusNode());
|
||
|
||
// Open time picker
|
||
var pickedTime = await showTimePicker(
|
||
context: context,
|
||
initialTime: TimeOfDay.now(),
|
||
);
|
||
|
||
timeEndController.text = timeToString(pickedTime)!;
|
||
},
|
||
onSaved: (newValue) {
|
||
_session.timeEnd = stringToTime(newValue);
|
||
},
|
||
),
|
||
),
|
||
],
|
||
),
|
||
ElevatedButton(
|
||
child: Text(AppLocalizations.of(context).save),
|
||
onPressed: () async {
|
||
// Validate and save the current values to the weightEntry
|
||
final isValid = _form.currentState!.validate();
|
||
if (!isValid) {
|
||
return;
|
||
}
|
||
_form.currentState!.save();
|
||
|
||
// Save the entry on the server
|
||
try {
|
||
await Provider.of<WorkoutPlansProvider>(context, listen: false)
|
||
.addSession(_session);
|
||
Navigator.of(context).pop();
|
||
} on WgerHttpException catch (error) {
|
||
showHttpExceptionErrorDialog(error, context);
|
||
} catch (error) {
|
||
showErrorDialog(error, context);
|
||
}
|
||
},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
NavigationFooter(
|
||
widget._controller,
|
||
1,
|
||
showNext: false,
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class TimerWidget extends StatefulWidget {
|
||
final PageController _controller;
|
||
final double _ratioCompleted;
|
||
Map<String, int> _exercisePages;
|
||
|
||
TimerWidget(this._controller, this._ratioCompleted, this._exercisePages);
|
||
|
||
@override
|
||
_TimerWidgetState createState() => _TimerWidgetState();
|
||
}
|
||
|
||
class _TimerWidgetState extends State<TimerWidget> {
|
||
// See https://stackoverflow.com/questions/54610121/flutter-countdown-timer
|
||
|
||
Timer? _timer;
|
||
int _seconds = 1;
|
||
final _maxSeconds = 600;
|
||
DateTime today = new DateTime(2000, 1, 1, 0, 0, 0);
|
||
|
||
void startTimer() {
|
||
setState(() {
|
||
_seconds = 0;
|
||
});
|
||
|
||
_timer?.cancel();
|
||
|
||
const oneSecond = const Duration(seconds: 1);
|
||
_timer = new Timer.periodic(
|
||
oneSecond,
|
||
(Timer timer) {
|
||
if (_seconds == _maxSeconds) {
|
||
setState(() {
|
||
timer.cancel();
|
||
});
|
||
} else {
|
||
setState(() {
|
||
_seconds++;
|
||
});
|
||
}
|
||
},
|
||
);
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_timer?.cancel();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
void initState() {
|
||
// TODO: implement initState
|
||
super.initState();
|
||
startTimer();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Column(
|
||
children: [
|
||
NavigationHeader(
|
||
AppLocalizations.of(context).pause,
|
||
widget._controller,
|
||
exercisePages: widget._exercisePages,
|
||
),
|
||
Expanded(
|
||
child: Center(
|
||
child: Text(
|
||
DateFormat('m:ss').format(today.add(Duration(seconds: _seconds))),
|
||
style: Theme.of(context).textTheme.headline1!.copyWith(color: wgerPrimaryColor),
|
||
),
|
||
),
|
||
),
|
||
NavigationFooter(widget._controller, widget._ratioCompleted),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class NavigationFooter extends StatelessWidget {
|
||
final PageController _controller;
|
||
final double _ratioCompleted;
|
||
final bool showPrevious;
|
||
final bool showNext;
|
||
|
||
NavigationFooter(this._controller, this._ratioCompleted,
|
||
{this.showPrevious = true, this.showNext = true});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Row(
|
||
children: [
|
||
showPrevious
|
||
? IconButton(
|
||
icon: Icon(Icons.chevron_left),
|
||
onPressed: () {
|
||
_controller.previousPage(
|
||
duration: DEFAULT_ANIMATION_DURATION,
|
||
curve: DEFAULT_ANIMATION_CURVE,
|
||
);
|
||
},
|
||
)
|
||
: SizedBox(width: 48),
|
||
Expanded(
|
||
child: LinearProgressIndicator(
|
||
minHeight: 1.5,
|
||
value: _ratioCompleted,
|
||
valueColor: AlwaysStoppedAnimation<Color>(wgerPrimaryColor),
|
||
),
|
||
),
|
||
showNext
|
||
? IconButton(
|
||
icon: Icon(Icons.chevron_right),
|
||
onPressed: () {
|
||
_controller.nextPage(
|
||
duration: DEFAULT_ANIMATION_DURATION,
|
||
curve: DEFAULT_ANIMATION_CURVE,
|
||
);
|
||
},
|
||
)
|
||
: SizedBox(width: 48),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class NavigationHeader extends StatelessWidget {
|
||
final PageController _controller;
|
||
final String _title;
|
||
Map<String, int> exercisePages;
|
||
|
||
NavigationHeader(
|
||
this._title,
|
||
this._controller, {
|
||
required this.exercisePages,
|
||
});
|
||
|
||
Widget getDialog(BuildContext context) {
|
||
return AlertDialog(
|
||
title: Text(
|
||
AppLocalizations.of(context).jumpTo,
|
||
textAlign: TextAlign.center,
|
||
),
|
||
contentPadding: EdgeInsets.zero,
|
||
content: SingleChildScrollView(
|
||
child: Column(
|
||
children: [
|
||
...exercisePages.keys.map((e) {
|
||
return ListTile(
|
||
title: Text(e),
|
||
trailing: Icon(Icons.chevron_right),
|
||
onTap: () {
|
||
_controller.animateToPage(
|
||
exercisePages[e]!,
|
||
duration: DEFAULT_ANIMATION_DURATION,
|
||
curve: DEFAULT_ANIMATION_CURVE,
|
||
);
|
||
Navigator.of(context).pop();
|
||
},
|
||
);
|
||
}).toList(),
|
||
],
|
||
),
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
child: Text(MaterialLocalizations.of(context).closeButtonLabel),
|
||
onPressed: () {
|
||
Navigator.of(context).pop();
|
||
},
|
||
)
|
||
],
|
||
);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Row(
|
||
children: [
|
||
IconButton(
|
||
icon: Icon(Icons.close),
|
||
onPressed: () {
|
||
Navigator.of(context).pop();
|
||
},
|
||
),
|
||
Expanded(
|
||
child: Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||
child: Text(
|
||
_title,
|
||
style: Theme.of(context).textTheme.headline5,
|
||
textAlign: TextAlign.center,
|
||
),
|
||
),
|
||
),
|
||
IconButton(
|
||
icon: Icon(Icons.menu),
|
||
onPressed: () {
|
||
showDialog(
|
||
context: context,
|
||
builder: (ctx) => getDialog(context),
|
||
);
|
||
},
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|