From 1f2235be0679565154487ee0583c57d6c1f4d62e Mon Sep 17 00:00:00 2001
From: JustinBenito <83128918+JustinBenito@users.noreply.github.com>
Date: Sat, 1 Oct 2022 11:43:47 +0530
Subject: [PATCH 1/8] The UI modifications
The modifications of The UI.
---
auth_screen.dart | 399 ++++++++++++++++++++++++++
calendar.dart | 289 +++++++++++++++++++
categories.dart | 110 +++++++
entries_list.dart | 158 ++++++++++
forms.dart | 280 ++++++++++++++++++
home_tabs_screen.dart | 195 +++++++++++++
nutritional_plans_list.dart | 118 ++++++++
splash_screen.dart | 30 ++
theme.dart | 154 ++++++++++
widgets.dart | 558 ++++++++++++++++++++++++++++++++++++
workout_plans_list.dart | 120 ++++++++
11 files changed, 2411 insertions(+)
create mode 100644 auth_screen.dart
create mode 100644 calendar.dart
create mode 100644 categories.dart
create mode 100644 entries_list.dart
create mode 100644 forms.dart
create mode 100644 home_tabs_screen.dart
create mode 100644 nutritional_plans_list.dart
create mode 100644 splash_screen.dart
create mode 100644 theme.dart
create mode 100644 widgets.dart
create mode 100644 workout_plans_list.dart
diff --git a/auth_screen.dart b/auth_screen.dart
new file mode 100644
index 000000000..9488f6e81
--- /dev/null
+++ b/auth_screen.dart
@@ -0,0 +1,399 @@
+/*
+ * This file is part of wger Workout Manager .
+ * 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.
+ *
+ * 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 .
+ */
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:provider/provider.dart';
+import 'package:wger/exceptions/http_exception.dart';
+import 'package:wger/helpers/consts.dart';
+import 'package:wger/helpers/misc.dart';
+import 'package:wger/helpers/ui.dart';
+
+import '../providers/auth.dart';
+import '../theme/theme.dart';
+
+enum AuthMode {
+ Signup,
+ Login,
+}
+
+class AuthScreen extends StatelessWidget {
+ static const routeName = '/auth';
+
+ @override
+ Widget build(BuildContext context) {
+ final deviceSize = MediaQuery.of(context).size;
+ return Scaffold(
+ backgroundColor: Theme.of(context).primaryColor,
+ body: Stack(
+ children: [
+ SingleChildScrollView(
+ child: SizedBox(
+ height: deviceSize.height,
+ width: deviceSize.width,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ const Padding(padding: EdgeInsets.symmetric(vertical: 20)),
+ const Image(
+ image: AssetImage('assets/images/logo-white.png'),
+ width: 120,
+ ),
+ Container(
+ margin: const EdgeInsets.only(bottom: 20.0),
+ padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 94.0),
+ child: const Text(
+ 'WGER',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 50,
+ fontFamily: 'OpenSansBold',
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ const Flexible(
+ //flex: deviceSize.width > 600 ? 2 : 1,
+ child: AuthCard(),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class AuthCard extends StatefulWidget {
+ const AuthCard();
+
+ @override
+ _AuthCardState createState() => _AuthCardState();
+}
+
+class _AuthCardState extends State {
+ final GlobalKey _formKey = GlobalKey();
+
+ bool _canRegister = true;
+ AuthMode _authMode = AuthMode.Login;
+ bool _hideCustomServer = true;
+ final Map _authData = {
+ 'username': '',
+ 'email': '',
+ 'password': '',
+ 'serverUrl': '',
+ };
+ var _isLoading = false;
+ final _usernameController = TextEditingController();
+ final _passwordController = TextEditingController();
+ final _password2Controller = TextEditingController();
+ final _emailController = TextEditingController();
+ final _serverUrlController = TextEditingController(text: DEFAULT_SERVER);
+
+ @override
+ void initState() {
+ super.initState();
+ context.read().getServerUrlFromPrefs().then((value) {
+ _serverUrlController.text = value;
+ });
+
+ // Check if the API key is set
+ //
+ // If not, the user will not be able to register via the app
+ try {
+ final metadata = Provider.of(context, listen: false).metadata;
+ if (metadata.containsKey(MANIFEST_KEY_API) || metadata[MANIFEST_KEY_API] == '') {
+ _canRegister = false;
+ }
+ } on PlatformException {
+ _canRegister = false;
+ }
+ }
+
+ void _submit(BuildContext context) async {
+ if (!_formKey.currentState!.validate()) {
+ // Invalid!
+ return;
+ }
+ _formKey.currentState!.save();
+ setState(() {
+ _isLoading = true;
+ });
+
+ try {
+ // Login existing user
+ if (_authMode == AuthMode.Login) {
+ await Provider.of(context, listen: false)
+ .login(_authData['username']!, _authData['password']!, _authData['serverUrl']!);
+
+ // Register new user
+ } else {
+ await Provider.of(context, listen: false).register(
+ username: _authData['username']!,
+ password: _authData['password']!,
+ email: _authData['email']!,
+ serverUrl: _authData['serverUrl']!);
+ }
+
+ setState(() {
+ _isLoading = false;
+ });
+ } on WgerHttpException catch (error) {
+ showHttpExceptionErrorDialog(error, context);
+ setState(() {
+ _isLoading = false;
+ });
+ } catch (error) {
+ showErrorDialog(error, context);
+ setState(() {
+ _isLoading = false;
+ });
+ }
+ }
+
+ void _switchAuthMode() {
+ if (!_canRegister) {
+ launchURL(DEFAULT_SERVER, context);
+ return;
+ }
+
+ if (_authMode == AuthMode.Login) {
+ setState(() {
+ _authMode = AuthMode.Signup;
+ });
+ } else {
+ setState(() {
+ _authMode = AuthMode.Login;
+ });
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final deviceSize = MediaQuery.of(context).size;
+ return Card(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(10.0),
+ ),
+ elevation: 8.0,
+ child: Container(
+ width: deviceSize.width * 0.75,
+ padding: const EdgeInsets.all(16.0),
+ child: Form(
+ key: _formKey,
+ child: SingleChildScrollView(
+ child: AutofillGroup(
+ child: Column(
+ children: [
+ TextFormField(
+ key: const Key('inputUsername'),
+ decoration: InputDecoration(
+ prefixIcon: Icon(Icons.account_box_rounded),
+ labelText: AppLocalizations.of(context).username,
+ errorMaxLines: 2,
+ ),
+ autofillHints: const [AutofillHints.username],
+ controller: _usernameController,
+ textInputAction: TextInputAction.next,
+ keyboardType: TextInputType.emailAddress,
+ validator: (value) {
+ if (!RegExp(r'^[\w.@+-]+$').hasMatch(value!)) {
+ return AppLocalizations.of(context).usernameValidChars;
+ }
+
+ if (value.isEmpty) {
+ return AppLocalizations.of(context).invalidUsername;
+ }
+ return null;
+ },
+ onSaved: (value) {
+ _authData['username'] = value!;
+ },
+ ),
+ SizedBox(
+ height: 10,
+ ),
+ if (_authMode == AuthMode.Signup)
+ TextFormField(
+ key: const Key('inputEmail'),
+ decoration: InputDecoration(
+ prefixIcon: Icon(Icons.email),
+ labelText: AppLocalizations.of(context).email),
+ autofillHints: const [AutofillHints.email],
+ controller: _emailController,
+ keyboardType: TextInputType.emailAddress,
+ textInputAction: TextInputAction.next,
+
+ // Email is not required
+ validator: (value) {
+ if (value!.isNotEmpty && !value.contains('@')) {
+ return AppLocalizations.of(context).invalidEmail;
+ }
+ return null;
+ },
+ onSaved: (value) {
+ _authData['email'] = value!;
+ },
+ ),
+ SizedBox(
+ height: 10,
+ ),
+ TextFormField(
+ key: const Key('inputPassword'),
+ decoration: InputDecoration(
+ prefixIcon: Icon(Icons.security),
+ labelText: AppLocalizations.of(context).password),
+ autofillHints: const [AutofillHints.password],
+ obscureText: true,
+ controller: _passwordController,
+ textInputAction: TextInputAction.next,
+ validator: (value) {
+ if (value!.isEmpty || value.length < 8) {
+ return AppLocalizations.of(context).passwordTooShort;
+ }
+ return null;
+ },
+ onSaved: (value) {
+ _authData['password'] = value!;
+ },
+ ),
+ SizedBox(
+ height: 10,
+ ),
+ if (_authMode == AuthMode.Signup)
+ TextFormField(
+ key: const Key('inputPassword2'),
+ decoration: InputDecoration(
+ prefixIcon: Icon(Icons.security_sharp),
+ labelText: AppLocalizations.of(context).confirmPassword),
+ controller: _password2Controller,
+ enabled: _authMode == AuthMode.Signup,
+ obscureText: true,
+ validator: _authMode == AuthMode.Signup
+ ? (value) {
+ if (value != _passwordController.text) {
+ return AppLocalizations.of(context).passwordsDontMatch;
+ }
+ return null;
+ }
+ : null,
+ ),
+ // Off-stage widgets are kept in the tree, otherwise the server URL
+ // would not be saved to _authData
+ Offstage(
+ offstage: _hideCustomServer,
+ child: Row(
+ children: [
+ Flexible(
+ flex: 3,
+ child: TextFormField(
+ key: const Key('inputServer'),
+ decoration: InputDecoration(
+ labelText: AppLocalizations.of(context).customServerUrl,
+ helperText: AppLocalizations.of(context).customServerHint,
+ helperMaxLines: 4),
+ controller: _serverUrlController,
+ validator: (value) {
+ if (Uri.tryParse(value!) == null) {
+ return AppLocalizations.of(context).invalidUrl;
+ }
+
+ if (value.isEmpty || !value.contains('http')) {
+ return AppLocalizations.of(context).invalidUrl;
+ }
+ return null;
+ },
+ onSaved: (value) {
+ // Remove any trailing slash
+ if (value!.lastIndexOf('/') == (value.length - 1)) {
+ value = value.substring(0, value.lastIndexOf('/'));
+ }
+ _authData['serverUrl'] = value;
+ },
+ ),
+ ),
+ const SizedBox(
+ width: 20,
+ ),
+ Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ IconButton(
+ icon: const Icon(Icons.undo),
+ color: wgerSecondaryColor,
+ onPressed: () {
+ _serverUrlController.text = DEFAULT_SERVER;
+ },
+ ),
+ Text(AppLocalizations.of(context).reset)
+ ],
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(
+ height: 20,
+ ),
+ if (_isLoading)
+ const CircularProgressIndicator()
+ else
+ ElevatedButton(
+ key: const Key('actionButton'),
+ child: Text(_authMode == AuthMode.Login
+ ? AppLocalizations.of(context).login
+ : AppLocalizations.of(context).register),
+ onPressed: () {
+ return _submit(context);
+ },
+ ),
+ SizedBox(
+ height: 10,
+ ),
+ TextButton(
+ key: const Key('toggleActionButton'),
+ child: Text(
+ _authMode == AuthMode.Login
+ ? AppLocalizations.of(context).registerInstead.toUpperCase()
+ : AppLocalizations.of(context).loginInstead.toUpperCase(),
+ ),
+ onPressed: _switchAuthMode,
+ ),
+ TextButton(
+ child: Text(_hideCustomServer
+ ? AppLocalizations.of(context).useCustomServer
+ : AppLocalizations.of(context).useDefaultServer),
+ key: const Key('toggleCustomServerButton'),
+ onPressed: () {
+ setState(() {
+ _hideCustomServer = !_hideCustomServer;
+ });
+ },
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/calendar.dart b/calendar.dart
new file mode 100644
index 000000000..2a9deea03
--- /dev/null
+++ b/calendar.dart
@@ -0,0 +1,289 @@
+/*
+ * This file is part of wger Workout Manager .
+ * 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 .
+ */
+
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:provider/provider.dart';
+import 'package:table_calendar/table_calendar.dart';
+import 'package:wger/helpers/consts.dart';
+import 'package:wger/helpers/json.dart';
+import 'package:wger/helpers/misc.dart';
+import 'package:wger/models/workouts/session.dart';
+import 'package:wger/providers/body_weight.dart';
+import 'package:wger/providers/measurement.dart';
+import 'package:wger/providers/nutrition.dart';
+import 'package:wger/providers/workout_plans.dart';
+import 'package:wger/theme/theme.dart';
+
+/// Types of events
+enum EventType {
+ weight,
+ measurement,
+ session,
+ caloriesDiary,
+}
+
+/// An event in the dashboard calendar
+class Event {
+ final EventType _type;
+ final String _description;
+
+ Event(this._type, this._description);
+
+ String get description {
+ return _description;
+ }
+
+ EventType get type {
+ return _type;
+ }
+}
+
+class DashboardCalendarWidget extends StatefulWidget {
+ const DashboardCalendarWidget();
+
+ @override
+ _DashboardCalendarWidgetState createState() => _DashboardCalendarWidgetState();
+}
+
+class _DashboardCalendarWidgetState extends State
+ with TickerProviderStateMixin {
+ late Map> _events;
+ late final ValueNotifier> _selectedEvents;
+ RangeSelectionMode _rangeSelectionMode =
+ RangeSelectionMode.toggledOff; // Can be toggled on/off by longpressing a date
+ DateTime _focusedDay = DateTime.now();
+ DateTime? _selectedDay;
+ DateTime? _rangeStart;
+ DateTime? _rangeEnd;
+
+ @override
+ void initState() {
+ super.initState();
+
+ _events = >{};
+ _selectedDay = _focusedDay;
+ _selectedEvents = ValueNotifier(_getEventsForDay(_selectedDay!));
+ loadEvents();
+ }
+
+ void loadEvents() async {
+ // Process weight entries
+ final BodyWeightProvider weightProvider =
+ Provider.of(context, listen: false);
+ for (final entry in weightProvider.items) {
+ final date = DateFormatLists.format(entry.date);
+
+ if (!_events.containsKey(date)) {
+ _events[date] = [];
+ }
+
+ // Add events to lists
+ _events[date]!.add(Event(EventType.weight, '${entry.weight} kg'));
+ }
+
+ // Process measurements
+ final MeasurementProvider measurementProvider =
+ Provider.of(context, listen: false);
+ for (final category in measurementProvider.categories) {
+ for (final entry in category.entries) {
+ final date = DateFormatLists.format(entry.date);
+
+ if (!_events.containsKey(date)) {
+ _events[date] = [];
+ }
+
+ _events[date]!
+ .add(Event(EventType.measurement, '${category.name}: ${entry.value} ${category.unit}'));
+ }
+ }
+
+ // Process workout sessions
+ final WorkoutPlansProvider plans = Provider.of(context, listen: false);
+ await plans.fetchSessionData().then((entries) {
+ for (final entry in entries['results']) {
+ final session = WorkoutSession.fromJson(entry);
+ final date = DateFormatLists.format(session.date);
+ if (!_events.containsKey(date)) {
+ _events[date] = [];
+ }
+ var time = '';
+ time = '(${timeToString(session.timeStart)} - ${timeToString(session.timeEnd)})';
+
+ // Add events to lists
+ _events[date]!.add(Event(
+ EventType.session,
+ '${AppLocalizations.of(context).impression}: ${session.impressionAsString} $time',
+ ));
+ }
+ });
+
+ // Process nutritional plans
+ final NutritionPlansProvider nutritionProvider =
+ Provider.of(context, listen: false);
+ for (final plan in nutritionProvider.items) {
+ for (final entry in plan.logEntriesValues.entries) {
+ final date = DateFormatLists.format(entry.key);
+ if (!_events.containsKey(date)) {
+ _events[date] = [];
+ }
+
+ // Add events to lists
+ _events[date]!.add(Event(
+ EventType.caloriesDiary,
+ '${entry.value.energy.toStringAsFixed(0)} kcal',
+ ));
+ }
+ }
+
+ // Add initial selected day to events list
+ _selectedEvents.value = _getEventsForDay(_selectedDay!);
+ }
+
+ @override
+ void dispose() {
+ _selectedEvents.dispose();
+ super.dispose();
+ }
+
+ List _getEventsForDay(DateTime day) {
+ return _events[DateFormatLists.format(day)] ?? [];
+ }
+
+ List _getEventsForRange(DateTime start, DateTime end) {
+ final days = daysInRange(start, end);
+
+ return [
+ for (final d in days) ..._getEventsForDay(d),
+ ];
+ }
+
+ void _onDaySelected(DateTime selectedDay, DateTime focusedDay) {
+ if (!isSameDay(_selectedDay, selectedDay)) {
+ setState(() {
+ _selectedDay = selectedDay;
+ _focusedDay = focusedDay;
+ _rangeStart = null; // Important to clean those
+ _rangeEnd = null;
+ _rangeSelectionMode = RangeSelectionMode.toggledOff;
+ });
+
+ _selectedEvents.value = _getEventsForDay(selectedDay);
+ }
+ }
+
+ void _onRangeSelected(DateTime? start, DateTime? end, DateTime focusedDay) {
+ setState(() {
+ _selectedDay = null;
+ _focusedDay = focusedDay;
+ _rangeStart = start;
+ _rangeEnd = end;
+ _rangeSelectionMode = RangeSelectionMode.toggledOn;
+ });
+
+ // `start` or `end` could be null
+ if (start != null && end != null) {
+ _selectedEvents.value = _getEventsForRange(start, end);
+ } else if (start != null) {
+ _selectedEvents.value = _getEventsForDay(start);
+ } else if (end != null) {
+ _selectedEvents.value = _getEventsForDay(end);
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Card(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(15.0),
+ ),
+ elevation: 3,
+ child: Column(
+ children: [
+ ListTile(
+ title: Text(
+ AppLocalizations.of(context).calendar,
+ style: Theme.of(context).textTheme.headline4,
+ ),
+ leading: const Icon(
+ Icons.calendar_today_outlined,
+ color: wgerSecondaryColor,
+ ),
+ ),
+ TableCalendar(
+ locale: Localizations.localeOf(context).languageCode,
+ firstDay: DateTime.now().subtract(const Duration(days: 1000)),
+ lastDay: DateTime.now(),
+ focusedDay: _focusedDay,
+ selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
+ rangeStartDay: _rangeStart,
+ rangeEndDay: _rangeEnd,
+ calendarFormat: CalendarFormat.month,
+ availableGestures: AvailableGestures.horizontalSwipe,
+ availableCalendarFormats: const {
+ CalendarFormat.month: '',
+ },
+ rangeSelectionMode: _rangeSelectionMode,
+ eventLoader: _getEventsForDay,
+ startingDayOfWeek: StartingDayOfWeek.monday,
+ calendarStyle: wgerCalendarStyle,
+ onDaySelected: _onDaySelected,
+ onRangeSelected: _onRangeSelected,
+ onFormatChanged: (format) {},
+ onPageChanged: (focusedDay) {
+ _focusedDay = focusedDay;
+ },
+ ),
+ const SizedBox(height: 8.0),
+ ValueListenableBuilder>(
+ valueListenable: _selectedEvents,
+ builder: (context, value, _) => Column(
+ children: [
+ ...value
+ .map((event) => ListTile(
+ title: Text((() {
+ switch (event.type) {
+ case EventType.caloriesDiary:
+ return AppLocalizations.of(context).nutritionalDiary;
+
+ case EventType.session:
+ return AppLocalizations.of(context).workoutSession;
+
+ case EventType.weight:
+ return AppLocalizations.of(context).weight;
+
+ case EventType.measurement:
+ return AppLocalizations.of(context).measurement;
+ }
+ })()),
+ subtitle: Text(event.description),
+ //onTap: () => print('$event tapped!'),
+ ))
+ .toList()
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/categories.dart b/categories.dart
new file mode 100644
index 000000000..9b39586a0
--- /dev/null
+++ b/categories.dart
@@ -0,0 +1,110 @@
+/*
+ * This file is part of wger Workout Manager .
+ * 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 .
+ */
+
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:provider/provider.dart';
+import 'package:wger/providers/measurement.dart';
+import 'package:wger/screens/form_screen.dart';
+import 'package:wger/screens/measurement_entries_screen.dart';
+import 'package:wger/widgets/core/charts.dart';
+
+import 'forms.dart';
+
+class CategoriesList extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ final _provider = Provider.of(context, listen: false);
+
+ return ListView.builder(
+ padding: const EdgeInsets.all(10.0),
+ itemCount: _provider.categories.length,
+ itemBuilder: (context, index) {
+ final currentCategory = _provider.categories[index];
+
+ return Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Card(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(15.0),
+ ),
+ elevation: 3,
+ child: Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(top: 5),
+ child: Text(
+ currentCategory.name,
+ style: Theme.of(context).textTheme.headline6,
+ ),
+ ),
+ Container(
+ color: Colors.white,
+ padding: const EdgeInsets.all(10),
+ height: 220,
+ child: MeasurementChartWidget(
+ currentCategory.entries
+ .map((e) => MeasurementChartEntry(e.value, e.date))
+ .toList(),
+ unit: currentCategory.unit,
+ ),
+ ),
+ const Divider(),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: ElevatedButton(
+ style: ButtonStyle(
+ shape: MaterialStateProperty.all(
+ RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(18.0),
+ ))),
+ child: Text(AppLocalizations.of(context).goToDetailPage),
+ onPressed: () {
+ Navigator.pushNamed(
+ context,
+ MeasurementEntriesScreen.routeName,
+ arguments: currentCategory.id,
+ );
+ }),
+ ),
+ IconButton(
+ onPressed: () async {
+ await Navigator.pushNamed(
+ context,
+ FormScreen.routeName,
+ arguments: FormScreenArguments(
+ AppLocalizations.of(context).newEntry,
+ MeasurementEntryForm(currentCategory.id!),
+ ),
+ );
+ },
+ icon: const Icon(Icons.add),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ },
+ );
+ }
+}
diff --git a/entries_list.dart b/entries_list.dart
new file mode 100644
index 000000000..0b127aca4
--- /dev/null
+++ b/entries_list.dart
@@ -0,0 +1,158 @@
+/*
+ * This file is part of wger Workout Manager .
+ * 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 .
+ */
+
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:intl/intl.dart';
+import 'package:provider/provider.dart';
+import 'package:wger/providers/body_weight.dart';
+import 'package:wger/screens/form_screen.dart';
+import 'package:wger/screens/measurement_categories_screen.dart';
+import 'package:wger/theme/theme.dart';
+import 'package:wger/widgets/core/charts.dart';
+import 'package:wger/widgets/weight/forms.dart';
+
+class WeightEntriesList extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ final _weightProvider = Provider.of(context, listen: false);
+
+ return Column(
+ children: [
+ Container(
+ color: Theme.of(context).cardColor,
+ padding: const EdgeInsets.all(15),
+ height: 220,
+ child: MeasurementChartWidget(
+ _weightProvider.items.map((e) => MeasurementChartEntry(e.weight, e.date)).toList()),
+ ),
+ Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ SizedBox(),
+ ElevatedButton(
+ style: ButtonStyle(
+ shape: MaterialStateProperty.all(RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(18.0),
+ ))),
+ onPressed: () => Navigator.pushNamed(
+ context,
+ MeasurementCategoriesScreen.routeName,
+ ),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ Text(
+ AppLocalizations.of(context).measurements,
+ ),
+ const Icon(Icons.chevron_right)
+ ],
+ ),
+ ),
+ ],
+ ),
+ ),
+ Expanded(
+ child: ListView.builder(
+ padding: const EdgeInsets.all(10.0),
+ itemCount: _weightProvider.items.length,
+ itemBuilder: (context, index) {
+ final currentEntry = _weightProvider.items[index];
+ return Dismissible(
+ key: Key(currentEntry.id.toString()),
+ onDismissed: (direction) {
+ if (direction == DismissDirection.endToStart) {
+ // Delete entry from DB
+ _weightProvider.deleteEntry(currentEntry.id!);
+
+ // and inform the user
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(
+ AppLocalizations.of(context).successfullyDeleted,
+ textAlign: TextAlign.center,
+ ),
+ ),
+ );
+ }
+ },
+ confirmDismiss: (direction) async {
+ // Edit entry
+ if (direction == DismissDirection.startToEnd) {
+ Navigator.pushNamed(
+ context,
+ FormScreen.routeName,
+ arguments: FormScreenArguments(
+ AppLocalizations.of(context).edit,
+ WeightForm(currentEntry),
+ ),
+ );
+ return false;
+ }
+ return true;
+ },
+ secondaryBackground: Container(
+ color: Theme.of(context).errorColor,
+ alignment: Alignment.centerRight,
+ padding: const EdgeInsets.only(right: 20),
+ margin: const EdgeInsets.symmetric(
+ horizontal: 4,
+ vertical: 4,
+ ),
+ child: const Icon(
+ Icons.delete,
+ color: Colors.white,
+ ),
+ ),
+ background: Container(
+ color: wgerPrimaryButtonColor,
+ alignment: Alignment.centerLeft,
+ padding: const EdgeInsets.only(left: 20),
+ margin: const EdgeInsets.symmetric(
+ horizontal: 4,
+ vertical: 4,
+ ),
+ child: const Icon(
+ Icons.edit,
+ color: Colors.white,
+ ),
+ ),
+ child: Card(
+ child: ListTile(
+ title: Text(
+ '${currentEntry.weight} kg',
+ style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
+ ),
+ subtitle: Text(
+ DateFormat.yMd(Localizations.localeOf(context).languageCode)
+ .format(currentEntry.date),
+ style: TextStyle(
+ fontSize: 12, fontWeight: FontWeight.w200, fontFamily: 'OpenSans'),
+ ),
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/forms.dart b/forms.dart
new file mode 100644
index 000000000..1843c43b3
--- /dev/null
+++ b/forms.dart
@@ -0,0 +1,280 @@
+/*
+ * This file is part of wger Workout Manager .
+ * 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 .
+ */
+
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:provider/provider.dart';
+import 'package:wger/exceptions/http_exception.dart';
+import 'package:wger/helpers/json.dart';
+import 'package:wger/helpers/ui.dart';
+import 'package:wger/models/measurements/measurement_category.dart';
+import 'package:wger/models/measurements/measurement_entry.dart';
+import 'package:wger/providers/measurement.dart';
+
+class MeasurementCategoryForm extends StatelessWidget {
+ final _form = GlobalKey();
+ final nameController = TextEditingController();
+ final unitController = TextEditingController();
+
+ final Map categoryData = {'id': null, 'name': '', 'unit': ''};
+
+ MeasurementCategoryForm([MeasurementCategory? category]) {
+ //this._category = category ?? MeasurementCategory();
+ if (category != null) {
+ categoryData['id'] = category.id;
+ categoryData['unit'] = category.unit;
+ categoryData['name'] = category.name;
+ }
+
+ unitController.text = categoryData['unit']!;
+ nameController.text = categoryData['name']!;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Form(
+ key: _form,
+ child: Column(
+ children: [
+ // Name
+ TextFormField(
+ decoration: InputDecoration(
+ labelText: AppLocalizations.of(context).name,
+ helperText: AppLocalizations.of(context).measurementCategoriesHelpText,
+ ),
+ controller: nameController,
+ onSaved: (newValue) {
+ categoryData['name'] = newValue;
+ },
+ validator: (value) {
+ if (value!.isEmpty) {
+ return AppLocalizations.of(context).enterValue;
+ }
+ return null;
+ },
+ ),
+
+ // Unit
+ TextFormField(
+ decoration: InputDecoration(
+ labelText: AppLocalizations.of(context).unit,
+ helperText: AppLocalizations.of(context).measurementEntriesHelpText,
+ ),
+ controller: unitController,
+ onSaved: (newValue) {
+ categoryData['unit'] = newValue;
+ },
+ validator: (value) {
+ if (value!.isEmpty) {
+ return AppLocalizations.of(context).enterValue;
+ }
+ return null;
+ },
+ ),
+ SizedBox(
+ height: 10,
+ ),
+ 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 {
+ categoryData['id'] == null
+ ? await Provider.of(context, listen: false).addCategory(
+ MeasurementCategory(
+ id: categoryData['id'],
+ name: categoryData['name'],
+ unit: categoryData['unit'],
+ ),
+ )
+ : await Provider.of(context, listen: false).editCategory(
+ categoryData['id'], categoryData['name'], categoryData['unit']);
+ } on WgerHttpException catch (error) {
+ showHttpExceptionErrorDialog(error, context);
+ } catch (error) {
+ showErrorDialog(error, context);
+ }
+ Navigator.of(context).pop();
+ },
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class MeasurementEntryForm extends StatelessWidget {
+ final _form = GlobalKey();
+ final int _categoryId;
+ final _valueController = TextEditingController();
+ final _dateController = TextEditingController();
+ final _notesController = TextEditingController();
+
+ late final Map _entryData;
+
+ MeasurementEntryForm(this._categoryId, [MeasurementEntry? entry]) {
+ _entryData = {
+ 'id': null,
+ 'category': _categoryId,
+ 'date': DateTime.now(),
+ 'value': '',
+ 'notes': '',
+ };
+
+ if (entry != null) {
+ _entryData['id'] = entry.id;
+ _entryData['category'] = entry.category;
+ _entryData['value'] = entry.value;
+ _entryData['date'] = entry.date;
+ _entryData['notes'] = entry.notes;
+ }
+
+ _dateController.text = toDate(_entryData['date'])!;
+ _valueController.text = _entryData['value']!.toString();
+ _notesController.text = _entryData['notes']!;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Form(
+ key: _form,
+ child: Column(
+ children: [
+ TextFormField(
+ decoration: InputDecoration(labelText: AppLocalizations.of(context).date),
+ readOnly: true, // Hide text cursor
+ controller: _dateController,
+ onTap: () async {
+ // Stop keyboard from appearing
+ FocusScope.of(context).requestFocus(FocusNode());
+
+ // Show Date Picker Here
+ final pickedDate = await showDatePicker(
+ context: context,
+ initialDate: _entryData['date'],
+ firstDate: DateTime(DateTime.now().year - 10),
+ lastDate: DateTime.now(),
+
+ // TODO(x): we need to filter out dates that already have an entry
+ selectableDayPredicate: (day) {
+ // Always allow the current initial date
+ if (day == _entryData['date']) {
+ return true;
+ }
+ return true;
+ },
+ );
+
+ _dateController.text = toDate(pickedDate)!;
+ },
+ onSaved: (newValue) {
+ _entryData['date'] = DateTime.parse(newValue!);
+ },
+ validator: (value) {
+ if (value!.isEmpty) {
+ return AppLocalizations.of(context).enterValue;
+ }
+ return null;
+ },
+ ),
+ // Value
+ TextFormField(
+ decoration: InputDecoration(labelText: AppLocalizations.of(context).value),
+ controller: _valueController,
+ keyboardType: TextInputType.number,
+ validator: (value) {
+ if (value!.isEmpty) {
+ return AppLocalizations.of(context).enterValue;
+ }
+ try {
+ double.parse(value);
+ } catch (error) {
+ return AppLocalizations.of(context).enterValidNumber;
+ }
+ return null;
+ },
+ onSaved: (newValue) {
+ _entryData['value'] = double.parse(newValue!);
+ },
+ ),
+ // Value
+ TextFormField(
+ decoration: InputDecoration(labelText: AppLocalizations.of(context).notes),
+ controller: _notesController,
+ onSaved: (newValue) {
+ _entryData['notes'] = newValue;
+ },
+ validator: (value) {
+ const minLength = 0;
+ const maxLength = 100;
+ if (value!.isNotEmpty && (value.length < minLength || value.length > maxLength)) {
+ return AppLocalizations.of(context).enterCharacters(minLength, maxLength);
+ }
+ return null;
+ },
+ ),
+
+ 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 {
+ _entryData['id'] == null
+ ? await Provider.of(context, listen: false)
+ .addEntry(MeasurementEntry(
+ id: _entryData['id'],
+ category: _entryData['category'],
+ date: _entryData['date'],
+ value: _entryData['value'],
+ notes: _entryData['notes'],
+ ))
+ : await Provider.of(context, listen: false).editEntry(
+ _entryData['id'],
+ _entryData['category'],
+ _entryData['value'],
+ _entryData['notes'],
+ _entryData['date'],
+ );
+ } on WgerHttpException catch (error) {
+ showHttpExceptionErrorDialog(error, context);
+ } catch (error) {
+ showErrorDialog(error, context);
+ }
+ Navigator.of(context).pop();
+ },
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/home_tabs_screen.dart b/home_tabs_screen.dart
new file mode 100644
index 000000000..d61fa476f
--- /dev/null
+++ b/home_tabs_screen.dart
@@ -0,0 +1,195 @@
+/*
+ * This file is part of wger Workout Manager .
+ * 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.
+ *
+ * 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 .
+ */
+
+import 'dart:developer';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:font_awesome_flutter/font_awesome_flutter.dart';
+import 'package:provider/provider.dart';
+import 'package:rive/rive.dart';
+import 'package:wger/providers/auth.dart';
+import 'package:wger/providers/body_weight.dart';
+import 'package:wger/providers/exercises.dart';
+import 'package:wger/providers/gallery.dart';
+import 'package:wger/providers/measurement.dart';
+import 'package:wger/providers/nutrition.dart';
+import 'package:wger/providers/workout_plans.dart';
+import 'package:wger/screens/dashboard.dart';
+import 'package:wger/screens/gallery_screen.dart';
+import 'package:wger/screens/nutritional_plans_screen.dart';
+import 'package:wger/screens/weight_screen.dart';
+import 'package:wger/screens/workout_plans_screen.dart';
+import 'package:wger/theme/theme.dart';
+
+class HomeTabsScreen extends StatefulWidget {
+ static const routeName = '/dashboard2';
+
+ @override
+ _HomeTabsScreenState createState() => _HomeTabsScreenState();
+}
+
+class _HomeTabsScreenState extends State with SingleTickerProviderStateMixin {
+ late Future _initialData;
+ int _selectedIndex = 0;
+
+ @override
+ void initState() {
+ super.initState();
+ // Loading data here, since the build method can be called more than once
+ _initialData = _loadEntries();
+ }
+
+ void _onItemTapped(int index) {
+ setState(() {
+ _selectedIndex = index;
+ });
+ }
+
+ final _screenList = [
+ DashboardScreen(),
+ WorkoutPlansScreen(),
+ NutritionScreen(),
+ WeightScreen(),
+ const GalleryScreen(),
+ ];
+
+ /// Load initial data from the server
+ Future _loadEntries() async {
+ if (!Provider.of(context, listen: false).dataInit) {
+ Provider.of(context, listen: false).setServerVersion();
+
+ final workoutPlansProvider = Provider.of(context, listen: false);
+ final nutritionPlansProvider = Provider.of(context, listen: false);
+ final exercisesProvider = Provider.of(context, listen: false);
+ final galleryProvider = Provider.of(context, listen: false);
+ final weightProvider = Provider.of(context, listen: false);
+ final measurementProvider = Provider.of(context, listen: false);
+
+ // Base data
+ log('Loading base data');
+ await Future.wait([
+ workoutPlansProvider.fetchAndSetUnits(),
+ nutritionPlansProvider.fetchIngredientsFromCache(),
+ exercisesProvider.fetchAndSetExercises(),
+ ]);
+
+ // Plans, weight and gallery
+ log('Loading plans, weight, measurements and gallery');
+ await Future.wait([
+ galleryProvider.fetchAndSetGallery(),
+ nutritionPlansProvider.fetchAndSetAllPlansSparse(),
+ workoutPlansProvider.fetchAndSetAllPlansSparse(),
+ weightProvider.fetchAndSetEntries(),
+ measurementProvider.fetchAndSetAllCategoriesAndEntries(),
+ ]);
+
+ // Current nutritional plan
+ log('Loading current nutritional plan');
+ if (nutritionPlansProvider.currentPlan != null) {
+ final plan = nutritionPlansProvider.currentPlan!;
+ await nutritionPlansProvider.fetchAndSetPlanFull(plan.id!);
+ }
+
+ // Current workout plan
+ log('Loading current workout plan');
+ if (workoutPlansProvider.activePlan != null) {
+ final planId = workoutPlansProvider.activePlan!.id!;
+ await workoutPlansProvider.fetchAndSetWorkoutPlanFull(planId);
+ workoutPlansProvider.setCurrentPlan(planId);
+ }
+ }
+
+ Provider.of(context, listen: false).dataInit = true;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return FutureBuilder(
+ future: _initialData,
+ builder: (context, snapshot) {
+ if (snapshot.connectionState != ConnectionState.done) {
+ return Scaffold(
+ body: Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Center(
+ child: SizedBox(
+ height: 100,
+ child: RiveAnimation.asset(
+ 'assets/animations/wger_logo.riv',
+ animations: ['idle_loop2'],
+ ),
+ ),
+ ),
+ Text(
+ AppLocalizations.of(context).loadingText,
+ style: TextStyle(
+ fontSize: materialSizes['h5'],
+ fontFamily: 'OpenSansBold',
+ color: Colors.white,
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ } else {
+ return Scaffold(
+ body: _screenList.elementAt(_selectedIndex),
+ bottomNavigationBar: BottomNavigationBar(
+ items: [
+ BottomNavigationBarItem(
+ icon: const Icon(Icons.dashboard),
+ label: AppLocalizations.of(context).labelDashboard,
+ ),
+ BottomNavigationBarItem(
+ icon: const Icon(Icons.fitness_center),
+ label: AppLocalizations.of(context).labelBottomNavWorkout,
+ ),
+ BottomNavigationBarItem(
+ icon: const Icon(Icons.restaurant),
+ label: AppLocalizations.of(context).labelBottomNavNutrition,
+ ),
+ BottomNavigationBarItem(
+ icon: const FaIcon(
+ FontAwesomeIcons.weight,
+ size: 20,
+ ),
+ label: AppLocalizations.of(context).weight,
+ ),
+ BottomNavigationBarItem(
+ icon: const Icon(Icons.photo_library),
+ label: AppLocalizations.of(context).gallery,
+ ),
+ ],
+ type: BottomNavigationBarType.fixed,
+ currentIndex: _selectedIndex,
+ selectedItemColor: Colors.white,
+ unselectedItemColor: wgerPrimaryColorLight,
+ backgroundColor: wgerPrimaryColor,
+ onTap: _onItemTapped,
+ showUnselectedLabels: false,
+ ),
+ );
+ }
+ },
+ );
+ }
+}
diff --git a/nutritional_plans_list.dart b/nutritional_plans_list.dart
new file mode 100644
index 000000000..05b94d26d
--- /dev/null
+++ b/nutritional_plans_list.dart
@@ -0,0 +1,118 @@
+/*
+ * This file is part of wger Workout Manager .
+ * 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 .
+ */
+
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:intl/intl.dart';
+import 'package:wger/providers/nutrition.dart';
+import 'package:wger/screens/nutritional_plan_screen.dart';
+
+class NutritionalPlansList extends StatelessWidget {
+ final NutritionPlansProvider _nutritionProvider;
+ const NutritionalPlansList(this._nutritionProvider);
+
+ @override
+ Widget build(BuildContext context) {
+ return ListView.builder(
+ padding: const EdgeInsets.all(10.0),
+ itemCount: _nutritionProvider.items.length,
+ itemBuilder: (context, index) {
+ final currentPlan = _nutritionProvider.items[index];
+ return Dismissible(
+ key: Key(currentPlan.id.toString()),
+ confirmDismiss: (direction) async {
+ // Delete workout from DB
+ final bool? res = await showDialog(
+ context: context,
+ builder: (BuildContext contextDialog) {
+ return AlertDialog(
+ content: Text(
+ AppLocalizations.of(context).confirmDelete(currentPlan.description),
+ ),
+ actions: [
+ TextButton(
+ child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
+ onPressed: () => Navigator.of(contextDialog).pop(),
+ ),
+ TextButton(
+ child: Text(
+ AppLocalizations.of(context).delete,
+ style: TextStyle(color: Theme.of(context).errorColor),
+ ),
+ onPressed: () {
+ // Confirmed, delete the workout
+ _nutritionProvider.deletePlan(currentPlan.id!);
+
+ // Close the popup
+ Navigator.of(contextDialog).pop();
+
+ // and inform the user
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(
+ AppLocalizations.of(context).successfullyDeleted,
+ textAlign: TextAlign.center,
+ ),
+ ),
+ );
+ },
+ ),
+ ],
+ );
+ });
+ return res;
+ },
+ background: Container(
+ color: Theme.of(context).errorColor,
+ alignment: Alignment.centerRight,
+ padding: const EdgeInsets.only(right: 20),
+ margin: const EdgeInsets.symmetric(
+ horizontal: 4,
+ vertical: 4,
+ ),
+ child: const Icon(
+ Icons.delete,
+ color: Colors.white,
+ ),
+ ),
+ direction: DismissDirection.endToStart,
+ child: Card(
+ child: ListTile(
+ onTap: () {
+ Navigator.of(context).pushNamed(
+ NutritionalPlanScreen.routeName,
+ arguments: currentPlan,
+ );
+ },
+ title: Text(
+ currentPlan.description,
+ style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
+ ),
+ subtitle: Text(
+ DateFormat.yMd(
+ Localizations.localeOf(context).languageCode,
+ ).format(currentPlan.creationDate),
+ style: TextStyle(fontSize: 12, fontWeight: FontWeight.w200, fontFamily: 'OpenSans'),
+ ),
+ ),
+ ),
+ );
+ },
+ );
+ }
+}
diff --git a/splash_screen.dart b/splash_screen.dart
new file mode 100644
index 000000000..f018610f3
--- /dev/null
+++ b/splash_screen.dart
@@ -0,0 +1,30 @@
+/*
+ * This file is part of wger Workout Manager .
+ * 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 .
+ */
+
+import 'package:flutter/material.dart';
+
+class SplashScreen extends StatelessWidget {
+ @override
+ Widget build(BuildContext context) {
+ return const Scaffold(
+ body: Center(
+ child: Text('Loading...'),
+ ),
+ );
+ }
+}
diff --git a/theme.dart b/theme.dart
new file mode 100644
index 000000000..2d234e126
--- /dev/null
+++ b/theme.dart
@@ -0,0 +1,154 @@
+/*
+ * This file is part of wger Workout Manager .
+ * 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 .
+ */
+
+import 'package:charts_flutter/flutter.dart' as charts;
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:table_calendar/table_calendar.dart';
+
+const Color wgerPrimaryColor = Color(0xff2a4c7d);
+const Color wgerPrimaryButtonColor = Color(0xff266dd3);
+const Color wgerPrimaryColorLight = Color(0xff94B2DB);
+const Color wgerSecondaryColor = Color(0xffe63946);
+const Color wgerSecondaryColorLight = Color(0xffF6B4BA);
+const Color wgerTextMuted = Colors.black38;
+const Color wgerBackground = Color.fromARGB(255, 192, 230, 255);
+
+// Chart colors
+const charts.Color wgerChartPrimaryColor = charts.Color(r: 0x2a, g: 0x4c, b: 0x7d);
+const charts.Color wgerChartSecondaryColor = charts.Color(r: 0xe6, g: 0x39, b: 0x46);
+
+/// Original sizes for the material text theme
+/// https://api.flutter.dev/flutter/material/TextTheme-class.html
+const materialSizes = {
+ 'h1': 96.0,
+ 'h2': 60.0,
+ 'h3': 48.0,
+ 'h4': 34.0,
+ 'h5': 24.0,
+ 'h6': 20.0,
+};
+
+final ThemeData wgerTheme = ThemeData(
+ inputDecorationTheme: InputDecorationTheme(
+ iconColor: wgerSecondaryColor,
+ labelStyle: TextStyle(color: Colors.black),
+ ),
+
+ /*
+ * General stuff
+ */
+ primaryColor: wgerPrimaryColor,
+ scaffoldBackgroundColor: wgerBackground,
+
+ // This makes the visual density adapt to the platform that you run
+ // the app on. For desktop platforms, the controls will be smaller and
+ // closer together (more dense) than on mobile platforms.
+ visualDensity: VisualDensity.adaptivePlatformDensity,
+
+ // Show icons in the system's bar in light colors
+ appBarTheme: const AppBarTheme(
+ systemOverlayStyle: SystemUiOverlayStyle.dark,
+ color: wgerPrimaryColor,
+ ),
+
+ /*
+ * Text theme
+ */
+ textTheme: TextTheme(
+ headline1: const TextStyle(fontFamily: 'OpenSansLight', color: wgerPrimaryButtonColor),
+ headline2: const TextStyle(fontFamily: 'OpenSansLight', color: wgerPrimaryButtonColor),
+ headline3: TextStyle(
+ fontSize: materialSizes['h3']! * 0.8,
+ fontFamily: 'OpenSansBold',
+ color: wgerPrimaryButtonColor,
+ ),
+ headline4: TextStyle(
+ fontSize: materialSizes['h4']! * 0.8,
+ fontFamily: 'OpenSansBold',
+ color: wgerPrimaryButtonColor,
+ ),
+ headline5: TextStyle(
+ fontSize: materialSizes['h5'],
+ fontFamily: 'OpenSansBold',
+ color: wgerPrimaryButtonColor,
+ ),
+ headline6: TextStyle(
+ fontSize: materialSizes['h6']! * 0.8,
+ fontFamily: 'OpenSansBold',
+ color: wgerPrimaryButtonColor,
+ ),
+ ),
+
+ /*
+ * Button theme
+ */
+ textButtonTheme: TextButtonThemeData(
+ style: TextButton.styleFrom(
+ primary: wgerSecondaryColor,
+ ),
+ ),
+ outlinedButtonTheme: OutlinedButtonThemeData(
+ style: OutlinedButton.styleFrom(
+ primary: wgerSecondaryColor,
+ visualDensity: VisualDensity.compact,
+ side: const BorderSide(color: wgerSecondaryColor),
+ ),
+ ),
+ elevatedButtonTheme: ElevatedButtonThemeData(
+ style: ElevatedButton.styleFrom(
+ primary: wgerSecondaryColor,
+ ),
+ ),
+
+ /*
+ * Forms, etc.
+ */
+ sliderTheme: const SliderThemeData(
+ activeTrackColor: wgerSecondaryColor,
+ thumbColor: wgerPrimaryColor,
+ ),
+ colorScheme: ColorScheme.fromSwatch().copyWith(secondary: wgerSecondaryColor));
+
+const wgerCalendarStyle = CalendarStyle(
+// Use `CalendarStyle` to customize the UI
+ outsideDaysVisible: false,
+ todayDecoration: BoxDecoration(
+ color: Colors.amber,
+ shape: BoxShape.circle,
+ ),
+
+ markerDecoration: BoxDecoration(
+ color: Colors.black,
+ shape: BoxShape.circle,
+ ),
+ selectedDecoration: BoxDecoration(
+ color: wgerSecondaryColor,
+ shape: BoxShape.circle,
+ ),
+ rangeStartDecoration: BoxDecoration(
+ color: wgerSecondaryColor,
+ shape: BoxShape.circle,
+ ),
+ rangeEndDecoration: BoxDecoration(
+ color: wgerSecondaryColor,
+ shape: BoxShape.circle,
+ ),
+ rangeHighlightColor: wgerSecondaryColorLight,
+ weekendTextStyle: TextStyle(color: wgerSecondaryColor),
+);
diff --git a/widgets.dart b/widgets.dart
new file mode 100644
index 000000000..68c42252a
--- /dev/null
+++ b/widgets.dart
@@ -0,0 +1,558 @@
+/*
+ * This file is part of wger Workout Manager .
+ * 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 .
+ */
+
+import 'package:flutter/material.dart';
+import 'package:flutter/widgets.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:font_awesome_flutter/font_awesome_flutter.dart';
+import 'package:intl/intl.dart';
+import 'package:provider/provider.dart';
+import 'package:wger/models/nutrition/nutritional_plan.dart';
+import 'package:wger/models/workouts/workout_plan.dart';
+import 'package:wger/providers/body_weight.dart';
+import 'package:wger/providers/nutrition.dart';
+import 'package:wger/providers/workout_plans.dart';
+import 'package:wger/screens/form_screen.dart';
+import 'package:wger/screens/gym_mode.dart';
+import 'package:wger/screens/nutritional_plan_screen.dart';
+import 'package:wger/screens/weight_screen.dart';
+import 'package:wger/screens/workout_plan_screen.dart';
+import 'package:wger/theme/theme.dart';
+import 'package:wger/widgets/core/charts.dart';
+import 'package:wger/widgets/core/core.dart';
+import 'package:wger/widgets/nutrition/charts.dart';
+import 'package:wger/widgets/nutrition/forms.dart';
+import 'package:wger/widgets/weight/forms.dart';
+import 'package:wger/widgets/workouts/forms.dart';
+
+class DashboardNutritionWidget extends StatefulWidget {
+ @override
+ _DashboardNutritionWidgetState createState() => _DashboardNutritionWidgetState();
+}
+
+class _DashboardNutritionWidgetState extends State {
+ NutritionalPlan? _plan;
+ var _showDetail = false;
+ bool _hasContent = false;
+
+ @override
+ void initState() {
+ super.initState();
+ _plan = Provider.of(context, listen: false).currentPlan;
+ _hasContent = _plan != null;
+ }
+
+ List getContent() {
+ final List out = [];
+
+ if (!_hasContent) {
+ return out;
+ }
+
+ for (final meal in _plan!.meals) {
+ out.add(
+ Row(
+ children: [
+ Expanded(
+ child: Text(
+ meal.time!.format(context),
+ style: const TextStyle(fontWeight: FontWeight.bold),
+ //textAlign: TextAlign.left,
+ ),
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ MutedText(
+ '${AppLocalizations.of(context).energyShort} ${meal.nutritionalValues.energy.toStringAsFixed(0)}${AppLocalizations.of(context).kcal}'),
+ const MutedText(' / '),
+ MutedText(
+ '${AppLocalizations.of(context).proteinShort} ${meal.nutritionalValues.protein.toStringAsFixed(0)}${AppLocalizations.of(context).g}'),
+ const MutedText(' / '),
+ MutedText(
+ '${AppLocalizations.of(context).carbohydratesShort} ${meal.nutritionalValues.carbohydrates.toStringAsFixed(0)}${AppLocalizations.of(context).g}'),
+ const MutedText(' / '),
+ MutedText(
+ '${AppLocalizations.of(context).fatShort} ${meal.nutritionalValues.fat.toStringAsFixed(0)}${AppLocalizations.of(context).g} '),
+ ],
+ ),
+ IconButton(
+ icon: const Icon(Icons.history_edu),
+ color: wgerSecondaryColor,
+ onPressed: () {
+ Provider.of(context, listen: false).logMealToDiary(meal);
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(
+ AppLocalizations.of(context).mealLogged,
+ textAlign: TextAlign.center,
+ ),
+ ),
+ );
+ },
+ ),
+ ],
+ ),
+ );
+ out.add(const SizedBox(height: 5));
+
+ if (_showDetail) {
+ for (final item in meal.mealItems) {
+ out.add(
+ Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Flexible(
+ child: Text(
+ item.ingredientObj.name,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ const SizedBox(width: 5),
+ Text('${item.amount.toStringAsFixed(0)} ${AppLocalizations.of(context).g}'),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+ out.add(const SizedBox(height: 10));
+ }
+ out.add(const Divider());
+ }
+
+ return out;
+ }
+
+ Widget getTrailing() {
+ if (!_hasContent) {
+ return const Text('');
+ }
+
+ return _showDetail ? const Icon(Icons.expand_less) : const Icon(Icons.expand_more);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Card(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(15.0),
+ ),
+ elevation: 3,
+ child: Column(
+ children: [
+ ListTile(
+ title: Text(
+ _hasContent ? _plan!.description : AppLocalizations.of(context).nutritionalPlan,
+ style: Theme.of(context).textTheme.headline4,
+ ),
+ subtitle: Text(
+ _hasContent
+ ? DateFormat.yMd(Localizations.localeOf(context).languageCode)
+ .format(_plan!.creationDate)
+ : '',
+ ),
+ leading: const Icon(
+ Icons.restaurant,
+ color: wgerSecondaryColor,
+ ),
+ trailing: getTrailing(),
+ onTap: () {
+ setState(() {
+ _showDetail = !_showDetail;
+ });
+ },
+ ),
+ if (_hasContent)
+ Container(
+ padding: const EdgeInsets.only(left: 10),
+ child: Column(
+ children: [
+ ...getContent(),
+ Container(
+ padding: const EdgeInsets.all(15),
+ height: 180,
+ child: NutritionalPlanPieChartWidget(_plan!.nutritionalValues),
+ )
+ ],
+ ),
+ )
+ else
+ NothingFound(
+ AppLocalizations.of(context).noNutritionalPlans,
+ AppLocalizations.of(context).newNutritionalPlan,
+ PlanForm(),
+ ),
+ if (_hasContent)
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(4.0),
+ child: ElevatedButton(
+ style: ButtonStyle(
+ shape: MaterialStateProperty.all(
+ RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(18.0),
+ ))),
+ child: Text(AppLocalizations.of(context).logIngredient),
+ onPressed: () {
+ Navigator.pushNamed(
+ context,
+ FormScreen.routeName,
+ arguments: FormScreenArguments(
+ AppLocalizations.of(context).logIngredient,
+ IngredientLogForm(_plan!),
+ hasListView: true,
+ ),
+ );
+ },
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.all(4.0),
+ child: ElevatedButton(
+ style: ButtonStyle(
+ shape: MaterialStateProperty.all(
+ RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(18.0),
+ ))),
+ child: Text(AppLocalizations.of(context).goToDetailPage),
+ onPressed: () {
+ Navigator.of(context)
+ .pushNamed(NutritionalPlanScreen.routeName, arguments: _plan);
+ },
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+class DashboardWeightWidget extends StatefulWidget {
+ @override
+ _DashboardWeightWidgetState createState() => _DashboardWeightWidgetState();
+}
+
+class _DashboardWeightWidgetState extends State {
+ late BodyWeightProvider weightEntriesData;
+
+ @override
+ Widget build(BuildContext context) {
+ weightEntriesData = Provider.of(context, listen: false);
+
+ return Consumer(
+ builder: (context, workoutProvider, child) => Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Card(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(15.0),
+ ),
+ elevation: 3,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ ListTile(
+ title: Text(
+ AppLocalizations.of(context).weight,
+ style: Theme.of(context).textTheme.headline4,
+ ),
+ leading: const FaIcon(
+ FontAwesomeIcons.weight,
+ color: wgerSecondaryColor,
+ ),
+ trailing: IconButton(
+ icon: const Icon(Icons.add),
+ onPressed: () async {
+ Navigator.pushNamed(
+ context,
+ FormScreen.routeName,
+ arguments: FormScreenArguments(
+ AppLocalizations.of(context).newEntry,
+ WeightForm(),
+ ),
+ );
+ },
+ ),
+ ),
+ Column(
+ children: [
+ if (weightEntriesData.items.isNotEmpty)
+ Column(
+ children: [
+ Container(
+ decoration: BoxDecoration(borderRadius: BorderRadius.circular(12)),
+ padding: const EdgeInsets.all(15),
+ height: 180,
+ child: MeasurementChartWidget(weightEntriesData.items
+ .map((e) => MeasurementChartEntry(e.weight, e.date))
+ .toList()),
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(4.0),
+ child: ElevatedButton(
+ style: ButtonStyle(
+ shape:
+ MaterialStateProperty.all(
+ RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(18.0),
+ ))),
+ child: Text(AppLocalizations.of(context).goToDetailPage),
+ onPressed: () {
+ Navigator.of(context).pushNamed(WeightScreen.routeName);
+ }),
+ )
+ ],
+ ),
+ ],
+ )
+ else
+ NothingFound(
+ AppLocalizations.of(context).noWeightEntries,
+ AppLocalizations.of(context).newEntry,
+ WeightForm(),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ));
+ }
+}
+
+class DashboardWorkoutWidget extends StatefulWidget {
+ @override
+ _DashboardWorkoutWidgetState createState() => _DashboardWorkoutWidgetState();
+}
+
+class _DashboardWorkoutWidgetState extends State {
+ var _showDetail = false;
+ bool _hasContent = false;
+
+ WorkoutPlan? _workoutPlan;
+
+ @override
+ void initState() {
+ super.initState();
+ _workoutPlan = context.read().activePlan;
+ _hasContent = _workoutPlan != null;
+ }
+
+ Widget getTrailing() {
+ if (!_hasContent) {
+ return const Text('');
+ }
+
+ return _showDetail ? const Icon(Icons.expand_less) : const Icon(Icons.expand_more);
+ }
+
+ List getContent() {
+ final List out = [];
+
+ if (!_hasContent) {
+ return out;
+ }
+
+ for (final day in _workoutPlan!.days) {
+ out.add(SizedBox(
+ width: double.infinity,
+ child: Row(
+ children: [
+ Expanded(
+ child: Text(
+ day.description,
+ style: const TextStyle(fontWeight: FontWeight.bold),
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ Expanded(
+ child: MutedText(
+ day.getDaysText,
+ textAlign: TextAlign.right,
+ ),
+ ),
+ IconButton(
+ icon: const Icon(Icons.play_arrow),
+ color: wgerSecondaryColor,
+ onPressed: () {
+ Navigator.of(context).pushNamed(GymModeScreen.routeName, arguments: day);
+ },
+ ),
+ ],
+ ),
+ ));
+
+ for (final set in day.sets) {
+ out.add(SizedBox(
+ width: double.infinity,
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ ...set.settingsFiltered.map((s) {
+ return _showDetail
+ ? Column(
+ children: [
+ Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(s.exerciseObj.name),
+ const SizedBox(width: 10),
+ MutedText(set.getSmartRepr(s.exerciseObj).join('\n')),
+ ],
+ ),
+ const SizedBox(height: 10),
+ ],
+ )
+ : Container(decoration: BoxDecoration(borderRadius: BorderRadius.circular(12)));
+ }).toList(),
+ ],
+ ),
+ ));
+ }
+ out.add(const Divider());
+ }
+
+ return out;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Card(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(15.0),
+ ),
+ elevation: 3,
+ child: Column(
+ children: [
+ ListTile(
+ title: Text(
+ _hasContent ? _workoutPlan!.name : AppLocalizations.of(context).labelWorkoutPlan,
+ style: Theme.of(context).textTheme.headline4,
+ ),
+ subtitle: Text(
+ _hasContent
+ ? DateFormat.yMd(Localizations.localeOf(context).languageCode)
+ .format(_workoutPlan!.creationDate)
+ : '',
+ ),
+ leading: const Icon(
+ Icons.fitness_center_outlined,
+ color: wgerSecondaryColor,
+ ),
+ trailing: getTrailing(),
+ onTap: () {
+ setState(() {
+ _showDetail = !_showDetail;
+ });
+ },
+ ),
+ if (_hasContent)
+ Container(
+ decoration: BoxDecoration(borderRadius: BorderRadius.circular(12)),
+ padding: const EdgeInsets.only(left: 10),
+ child: Column(
+ children: [
+ ...getContent(),
+ ],
+ ),
+ )
+ else
+ NothingFound(
+ AppLocalizations.of(context).noWorkoutPlans,
+ AppLocalizations.of(context).newWorkout,
+ WorkoutForm(WorkoutPlan.empty()),
+ ),
+ if (_hasContent)
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ Padding(
+ padding: const EdgeInsets.all(4.0),
+ child: ElevatedButton(
+ style: ButtonStyle(
+ shape: MaterialStateProperty.all(
+ RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(18.0),
+ ))),
+ child: Text(AppLocalizations.of(context).goToDetailPage),
+ onPressed: () {
+ Navigator.of(context)
+ .pushNamed(WorkoutPlanScreen.routeName, arguments: _workoutPlan);
+ },
+ ),
+ )
+ ],
+ )
+ ],
+ ),
+ ));
+ }
+}
+
+class NothingFound extends StatelessWidget {
+ final String _title;
+ final String _titleForm;
+ final Widget _form;
+
+ const NothingFound(this._title, this._titleForm, this._form);
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox(
+ height: 150,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Text(_title),
+ IconButton(
+ iconSize: 30,
+ icon: const Icon(
+ Icons.add_box,
+ color: wgerSecondaryColor,
+ ),
+ onPressed: () async {
+ Navigator.pushNamed(
+ context,
+ FormScreen.routeName,
+ arguments: FormScreenArguments(
+ _titleForm,
+ _form,
+ ),
+ );
+ },
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/workout_plans_list.dart b/workout_plans_list.dart
new file mode 100644
index 000000000..d5e0a5360
--- /dev/null
+++ b/workout_plans_list.dart
@@ -0,0 +1,120 @@
+/*
+ * This file is part of wger Workout Manager .
+ * 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 .
+ */
+
+import 'package:flutter/material.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+import 'package:intl/intl.dart';
+import 'package:provider/provider.dart';
+import 'package:wger/providers/workout_plans.dart';
+import 'package:wger/screens/workout_plan_screen.dart';
+
+class WorkoutPlansList extends StatelessWidget {
+ final WorkoutPlansProvider _workoutProvider;
+
+ const WorkoutPlansList(this._workoutProvider);
+
+ @override
+ Widget build(BuildContext context) {
+ return ListView.builder(
+ padding: const EdgeInsets.all(10.0),
+ itemCount: _workoutProvider.items.length,
+ itemBuilder: (context, index) {
+ final currentWorkout = _workoutProvider.items[index];
+ return Dismissible(
+ key: Key(currentWorkout.id.toString()),
+ confirmDismiss: (direction) async {
+ // Delete workout from DB
+ final res = await showDialog(
+ context: context,
+ builder: (BuildContext contextDialog) {
+ return AlertDialog(
+ content: Text(
+ AppLocalizations.of(context).confirmDelete(currentWorkout.name),
+ ),
+ actions: [
+ TextButton(
+ child: Text(MaterialLocalizations.of(context).cancelButtonLabel),
+ onPressed: () => Navigator.of(contextDialog).pop(),
+ ),
+ TextButton(
+ child: Text(
+ AppLocalizations.of(context).delete,
+ style: TextStyle(color: Theme.of(context).errorColor),
+ ),
+ onPressed: () {
+ // Confirmed, delete the workout
+ Provider.of(context, listen: false)
+ .deleteWorkout(currentWorkout.id!);
+
+ // Close the popup
+ Navigator.of(contextDialog).pop();
+
+ // and inform the user
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(
+ AppLocalizations.of(context).successfullyDeleted,
+ textAlign: TextAlign.center,
+ ),
+ ),
+ );
+ },
+ ),
+ ],
+ );
+ });
+ return res;
+ },
+ background: Container(
+ color: Theme.of(context).errorColor,
+ alignment: Alignment.centerRight,
+ padding: const EdgeInsets.only(right: 20),
+ margin: const EdgeInsets.symmetric(
+ horizontal: 4,
+ vertical: 4,
+ ),
+ child: const Icon(
+ Icons.delete,
+ color: Colors.white,
+ ),
+ ),
+ direction: DismissDirection.endToStart,
+ child: Card(
+ child: ListTile(
+ onTap: () {
+ _workoutProvider.setCurrentPlan(currentWorkout.id!);
+
+ Navigator.of(context)
+ .pushNamed(WorkoutPlanScreen.routeName, arguments: currentWorkout);
+ },
+ title: Text(
+ currentWorkout.name,
+ style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
+ ),
+ subtitle: Text(
+ DateFormat.yMd(Localizations.localeOf(context).languageCode)
+ .format(currentWorkout.creationDate),
+ style: TextStyle(fontSize: 12, fontWeight: FontWeight.w200, fontFamily: 'OpenSans'),
+ ),
+ ),
+ ),
+ );
+ },
+ );
+ }
+}
From d9db4f7363f9e8e8b253e010df3847b12f36be65 Mon Sep 17 00:00:00 2001
From: JustinBenito <83128918+JustinBenito@users.noreply.github.com>
Date: Sat, 1 Oct 2022 14:32:58 +0530
Subject: [PATCH 2/8] Updated Readme to include promo
---
README.md | 3 +++
1 file changed, 3 insertions(+)
diff --git a/README.md b/README.md
index bf5697524..0ad876704 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,9 @@ If you want to contribute, hop on the Discord server and say hi!
+#Promo
+[![WGER-Workout Manager](https://img.youtube.com/vi/E_6RJjOzPKM/0.jpg)](https://www.youtube.com/watch?v=E_6RJjOzPKM)
+
## Installation
[
Date: Sat, 1 Oct 2022 14:35:53 +0530
Subject: [PATCH 3/8] New update
---
README.md | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 0ad876704..64767d1a3 100644
--- a/README.md
+++ b/README.md
@@ -15,9 +15,13 @@ If you want to contribute, hop on the Discord server and say hi!
-#Promo
+##Promo
[![WGER-Workout Manager](https://img.youtube.com/vi/E_6RJjOzPKM/0.jpg)](https://www.youtube.com/watch?v=E_6RJjOzPKM)
+[](https://github.com/wger-project/flutter)
+
## Installation
[
Date: Sat, 1 Oct 2022 15:20:48 +0530
Subject: [PATCH 4/8] updated video
---
README.md | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/README.md b/README.md
index 64767d1a3..934f4a2ba 100644
--- a/README.md
+++ b/README.md
@@ -16,11 +16,11 @@ If you want to contribute, hop on the Discord server and say hi!
##Promo
-[![WGER-Workout Manager](https://img.youtube.com/vi/E_6RJjOzPKM/0.jpg)](https://www.youtube.com/watch?v=E_6RJjOzPKM)
-[](https://github.com/wger-project/flutter)
+
+https://user-images.githubusercontent.com/83128918/193403621-30edc0f1-b9ca-4d45-91d8-1b8fe3dc8ba6.mp4
+
+
## Installation
[
Date: Sat, 1 Oct 2022 15:24:06 +0530
Subject: [PATCH 5/8] Updated Readme file with promo animation
---
README.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/README.md b/README.md
index 934f4a2ba..503d28b7d 100644
--- a/README.md
+++ b/README.md
@@ -15,7 +15,7 @@ If you want to contribute, hop on the Discord server and say hi!
-##Promo
+Promo
https://user-images.githubusercontent.com/83128918/193403621-30edc0f1-b9ca-4d45-91d8-1b8fe3dc8ba6.mp4
From 3222cb146e34b6603170b19f7512db0002f628a2 Mon Sep 17 00:00:00 2001
From: JustinBenito <83128918+JustinBenito@users.noreply.github.com>
Date: Mon, 17 Oct 2022 12:33:42 +0530
Subject: [PATCH 6/8] Updated Main.dart
Updated Main.dart to accomodate dark theme aswell as light theme
---
lib/main.dart | 191 +++++++++++++++++++++++++++++++++++++++++++++++++-
1 file changed, 190 insertions(+), 1 deletion(-)
diff --git a/lib/main.dart b/lib/main.dart
index 479f80987..3965a9d88 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -17,6 +17,7 @@
*/
import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
import 'package:wger/providers/base_provider.dart';
@@ -102,7 +103,195 @@ class MyApp extends StatelessWidget {
child: Consumer(
builder: (ctx, auth, _) => MaterialApp(
title: 'wger',
- theme: wgerTheme,
+ theme: ThemeData(
+ inputDecorationTheme: InputDecorationTheme(
+ iconColor: wgerSecondaryColor,
+ labelStyle: TextStyle(color: Colors.black),
+ ),
+
+ /*
+ * General stuff
+ */
+ primaryColor: wgerPrimaryColor,
+ scaffoldBackgroundColor: wgerBackground,
+
+ // This makes the visual density adapt to the platform that you run
+ // the app on. For desktop platforms, the controls will be smaller and
+ // closer together (more dense) than on mobile platforms.
+ visualDensity: VisualDensity.adaptivePlatformDensity,
+
+ // Show icons in the system's bar in light colors
+ appBarTheme: const AppBarTheme(
+ systemOverlayStyle: SystemUiOverlayStyle.dark,
+ color: wgerPrimaryColor,
+ titleTextStyle:
+ TextStyle(fontFamily: 'OpenSansBold', color: Colors.white, fontSize: 15),
+ ),
+ textTheme: TextTheme(
+ headline1: const TextStyle(
+ fontFamily: 'OpenSansLight', color: wgerPrimaryButtonColor, fontSize: 12),
+ headline2: const TextStyle(
+ fontFamily: 'OpenSans', color: wgerPrimaryButtonColor, fontSize: 15),
+ headline3: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.w600,
+ fontFamily: 'OpenSans',
+ color: wgerPrimaryButtonColor,
+ ),
+ headline4: TextStyle(
+ fontSize: materialSizes['h4']! * 0.8,
+ fontFamily: 'OpenSansBold',
+ color: wgerPrimaryButtonColor,
+ ),
+ headline5: TextStyle(
+ fontSize: 16,
+ fontFamily: 'OpenSansSemiBold',
+ color: wgerPrimaryButtonColor,
+ ),
+ headline6: TextStyle(
+ fontSize: materialSizes['h6']! * 0.8,
+ fontFamily: 'OpenSans',
+ color: wgerPrimaryButtonColor,
+ ),
+ subtitle1: TextStyle(
+ fontSize: 12,
+ fontWeight: FontWeight.w200,
+ fontFamily: 'OpenSans',
+ color: wgerPrimaryColorLight,
+ ),
+ subtitle2: TextStyle(
+ fontSize: 10,
+ fontWeight: FontWeight.w200,
+ fontFamily: 'OpenSansLight',
+ color: wgerPrimaryColorLight,
+ ),
+ ),
+
+ /*
+ * Button theme
+ */
+ textButtonTheme: TextButtonThemeData(
+ style: TextButton.styleFrom(
+ primary: wgerSecondaryColor,
+ ),
+ ),
+ outlinedButtonTheme: OutlinedButtonThemeData(
+ style: OutlinedButton.styleFrom(
+ primary: wgerSecondaryColor,
+ visualDensity: VisualDensity.compact,
+ side: const BorderSide(color: wgerSecondaryColor),
+ ),
+ ),
+ elevatedButtonTheme: ElevatedButtonThemeData(
+ style: ElevatedButton.styleFrom(
+ primary: wgerSecondaryColor,
+ ),
+ ),
+
+ /*
+ * Forms, etc.
+ */
+ sliderTheme: const SliderThemeData(
+ activeTrackColor: wgerSecondaryColor,
+ thumbColor: wgerPrimaryColor,
+ ),
+ colorScheme: ColorScheme.fromSwatch().copyWith(
+ secondary: wgerSecondaryColor,
+ brightness: Brightness.light,
+ )),
+ darkTheme: ThemeData(
+ dividerColor: wgerSecondaryColorLightDark,
+ bottomNavigationBarTheme: BottomNavigationBarThemeData(
+ selectedItemColor: Colors.white,
+ unselectedItemColor: wgerSecondaryColorLightDark,
+ backgroundColor: wgerPrimaryColorDark,
+ ),
+ focusColor: wgerSecondaryColorDark,
+ splashColor: wgerPrimaryColorLightDark,
+ primaryColor: wgerPrimaryColorDark,
+ primaryColorDark: wgerPrimaryButtonColorDark,
+ scaffoldBackgroundColor: wgerBackgroundDark,
+ cardColor: wgerPrimaryColorLightDark,
+ appBarTheme: AppBarTheme(
+ color: wgerPrimaryColorDark,
+ elevation: 10,
+ titleTextStyle: TextStyle(
+ fontFamily: 'OpenSansBold', color: wgerSecondaryColorLightDark, fontSize: 15)),
+ textTheme: TextTheme(
+ headline1:
+ const TextStyle(fontFamily: 'OpenSansLight', color: Colors.white, fontSize: 12),
+ headline2: const TextStyle(fontFamily: 'OpenSans', color: Colors.white, fontSize: 15),
+ headline3: TextStyle(
+ fontSize: 18,
+ fontWeight: FontWeight.w600,
+ fontFamily: 'OpenSans',
+ color: wgerSecondaryColorLightDark,
+ ),
+ headline4: TextStyle(
+ color: wgerSecondaryColorLightDark,
+ fontSize: 18,
+ fontWeight: FontWeight.w400,
+ fontFamily: 'OpenSansBold'),
+ headline5: TextStyle(
+ fontSize: 16,
+ fontFamily: 'OpenSansSemiBold',
+ color: wgerSecondaryColorLightDark,
+ ),
+ headline6: TextStyle(
+ fontSize: materialSizes['h6']! * 0.8,
+ fontFamily: 'OpenSans',
+ color: Colors.white,
+ ),
+ subtitle1: TextStyle(
+ fontSize: 12,
+ fontWeight: FontWeight.w200,
+ fontFamily: 'OpenSans',
+ color: Colors.white,
+ ),
+ subtitle2: TextStyle(
+ fontSize: 10,
+ fontWeight: FontWeight.w200,
+ fontFamily: 'OpenSansLight',
+ color: Colors.white,
+ ),
+ bodyText1: TextStyle(
+ color: darkmode ? wgerSecondaryColorLightDark : wgerSecondaryColorLight,
+ fontSize: 18,
+ fontWeight: FontWeight.w400,
+ ),
+ ),
+ inputDecorationTheme: InputDecorationTheme(
+ focusedBorder: UnderlineInputBorder(borderSide: BorderSide(color: Colors.white)),
+ iconColor: wgerSecondaryColorDark,
+ labelStyle: TextStyle(color: Colors.white),
+ ),
+ textButtonTheme: TextButtonThemeData(
+ style: TextButton.styleFrom(
+ primary: wgerSecondaryColorDark,
+ ),
+ ),
+ outlinedButtonTheme: OutlinedButtonThemeData(
+ style: OutlinedButton.styleFrom(
+ primary: wgerSecondaryColorDark,
+ visualDensity: VisualDensity.compact,
+ side: const BorderSide(color: wgerSecondaryColor),
+ ),
+ ),
+ elevatedButtonTheme: ElevatedButtonThemeData(
+ style: ElevatedButton.styleFrom(
+ primary: wgerSecondaryColorDark,
+ ),
+ ),
+ sliderTheme: const SliderThemeData(
+ activeTrackColor: wgerSecondaryColor,
+ thumbColor: wgerPrimaryColor,
+ ),
+ colorScheme: ColorScheme.fromSwatch().copyWith(
+ secondary: wgerSecondaryColorDark,
+ brightness: Brightness.dark,
+ ),
+ ),
+ themeMode: ThemeMode.system,
home: auth.isAuth
? FutureBuilder(
future: auth.applicationUpdateRequired(),
From 2e9abfd663fad2a5c7df8779cb151884dc5ed861 Mon Sep 17 00:00:00 2001
From: JustinBenito <83128918+JustinBenito@users.noreply.github.com>
Date: Mon, 17 Oct 2022 12:34:36 +0530
Subject: [PATCH 7/8] Updated Auth screen
Updated Auth screen to have dark mode and light mode
---
lib/main.dart | 681 +++++++++++++++++++++++++++++---------------------
1 file changed, 390 insertions(+), 291 deletions(-)
diff --git a/lib/main.dart b/lib/main.dart
index 3965a9d88..e4640325e 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -7,7 +7,7 @@
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
- * wger Workout Manager is distributed in the hope that it will be useful,
+ * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
@@ -20,311 +20,410 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
-import 'package:wger/providers/base_provider.dart';
-import 'package:wger/providers/body_weight.dart';
-import 'package:wger/providers/exercises.dart';
-import 'package:wger/providers/gallery.dart';
-import 'package:wger/providers/measurement.dart';
-import 'package:wger/providers/nutrition.dart';
-import 'package:wger/providers/workout_plans.dart';
-import 'package:wger/screens/auth_screen.dart';
-import 'package:wger/screens/dashboard.dart';
-import 'package:wger/screens/form_screen.dart';
-import 'package:wger/screens/gallery_screen.dart';
-import 'package:wger/screens/gym_mode.dart';
-import 'package:wger/screens/home_tabs_screen.dart';
-import 'package:wger/screens/measurement_categories_screen.dart';
-import 'package:wger/screens/measurement_entries_screen.dart';
-import 'package:wger/screens/nutritional_diary_screen.dart';
-import 'package:wger/screens/nutritional_plan_screen.dart';
-import 'package:wger/screens/nutritional_plans_screen.dart';
-import 'package:wger/screens/splash_screen.dart';
-import 'package:wger/screens/update_app_screen.dart';
-import 'package:wger/screens/weight_screen.dart';
-import 'package:wger/screens/workout_plan_screen.dart';
-import 'package:wger/screens/workout_plans_screen.dart';
-import 'package:wger/theme/theme.dart';
+import 'package:wger/exceptions/http_exception.dart';
+import 'package:wger/helpers/consts.dart';
+import 'package:wger/helpers/misc.dart';
+import 'package:wger/helpers/ui.dart';
-import 'providers/auth.dart';
+import '../providers/auth.dart';
+import '../theme/theme.dart';
-void main() {
- // Needs to be called before runApp
- WidgetsFlutterBinding.ensureInitialized();
-
- // Application
- runApp(MyApp());
+enum AuthMode {
+ Signup,
+ Login,
}
-class MyApp extends StatelessWidget {
- // This widget is the root of your application.
+class AuthScreen extends StatelessWidget {
+ static const routeName = '/auth';
+
@override
Widget build(BuildContext context) {
- return MultiProvider(
- providers: [
- ChangeNotifierProvider(
- create: (ctx) => AuthProvider(),
- ),
- ChangeNotifierProxyProvider(
- create: (context) =>
- ExercisesProvider(Provider.of(context, listen: false), []),
- update: (context, auth, previous) => previous ?? ExercisesProvider(auth, []),
- ),
- ChangeNotifierProxyProvider2(
- create: (context) => WorkoutPlansProvider(
- Provider.of(context, listen: false),
- Provider.of(context, listen: false),
- [],
- ),
- update: (context, auth, exercises, previous) =>
- previous ?? WorkoutPlansProvider(auth, exercises, []),
- ),
- ChangeNotifierProxyProvider(
- create: (context) =>
- NutritionPlansProvider(Provider.of(context, listen: false), []),
- update: (context, auth, previous) => previous ?? NutritionPlansProvider(auth, []),
- ),
- ChangeNotifierProxyProvider(
- create: (context) => MeasurementProvider(
- WgerBaseProvider(Provider.of(context, listen: false))),
- update: (context, base, previous) =>
- previous ?? MeasurementProvider(WgerBaseProvider(base)),
- ),
- ChangeNotifierProxyProvider(
- create: (context) =>
- BodyWeightProvider(Provider.of(context, listen: false), []),
- update: (context, auth, previous) => previous ?? BodyWeightProvider(auth, []),
- ),
- ChangeNotifierProxyProvider(
- create: (context) =>
- GalleryProvider(Provider.of(context, listen: false), []),
- update: (context, auth, previous) => previous ?? GalleryProvider(auth, []),
- ),
- ],
- child: Consumer(
- builder: (ctx, auth, _) => MaterialApp(
- title: 'wger',
- theme: ThemeData(
- inputDecorationTheme: InputDecorationTheme(
- iconColor: wgerSecondaryColor,
- labelStyle: TextStyle(color: Colors.black),
+ final deviceSize = MediaQuery.of(context).size;
+ return Scaffold(
+ backgroundColor: Theme.of(context).primaryColor,
+ body: Stack(
+ children: [
+ SingleChildScrollView(
+ child: SizedBox(
+ height: deviceSize.height,
+ width: deviceSize.width,
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.start,
+ crossAxisAlignment: CrossAxisAlignment.center,
+ children: [
+ const Padding(padding: EdgeInsets.symmetric(vertical: 20)),
+ const Image(
+ image: AssetImage('assets/images/logo-white.png'),
+ width: 120,
+ ),
+ Container(
+ margin: const EdgeInsets.only(bottom: 20.0),
+ padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 94.0),
+ child: const Text(
+ 'WGER',
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 50,
+ fontFamily: 'OpenSansBold',
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ const Flexible(
+ //flex: deviceSize.width > 600 ? 2 : 1,
+ child: AuthCard(),
+ ),
+ ],
),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+}
- /*
- * General stuff
- */
- primaryColor: wgerPrimaryColor,
- scaffoldBackgroundColor: wgerBackground,
+class AuthCard extends StatefulWidget {
+ const AuthCard();
- // This makes the visual density adapt to the platform that you run
- // the app on. For desktop platforms, the controls will be smaller and
- // closer together (more dense) than on mobile platforms.
- visualDensity: VisualDensity.adaptivePlatformDensity,
+ @override
+ _AuthCardState createState() => _AuthCardState();
+}
- // Show icons in the system's bar in light colors
- appBarTheme: const AppBarTheme(
- systemOverlayStyle: SystemUiOverlayStyle.dark,
- color: wgerPrimaryColor,
- titleTextStyle:
- TextStyle(fontFamily: 'OpenSansBold', color: Colors.white, fontSize: 15),
- ),
- textTheme: TextTheme(
- headline1: const TextStyle(
- fontFamily: 'OpenSansLight', color: wgerPrimaryButtonColor, fontSize: 12),
- headline2: const TextStyle(
- fontFamily: 'OpenSans', color: wgerPrimaryButtonColor, fontSize: 15),
- headline3: TextStyle(
- fontSize: 18,
- fontWeight: FontWeight.w600,
- fontFamily: 'OpenSans',
- color: wgerPrimaryButtonColor,
- ),
- headline4: TextStyle(
- fontSize: materialSizes['h4']! * 0.8,
- fontFamily: 'OpenSansBold',
- color: wgerPrimaryButtonColor,
- ),
- headline5: TextStyle(
- fontSize: 16,
- fontFamily: 'OpenSansSemiBold',
- color: wgerPrimaryButtonColor,
- ),
- headline6: TextStyle(
- fontSize: materialSizes['h6']! * 0.8,
- fontFamily: 'OpenSans',
- color: wgerPrimaryButtonColor,
- ),
- subtitle1: TextStyle(
- fontSize: 12,
- fontWeight: FontWeight.w200,
- fontFamily: 'OpenSans',
- color: wgerPrimaryColorLight,
- ),
- subtitle2: TextStyle(
- fontSize: 10,
- fontWeight: FontWeight.w200,
- fontFamily: 'OpenSansLight',
- color: wgerPrimaryColorLight,
- ),
- ),
+class _AuthCardState extends State {
+ final GlobalKey _formKey = GlobalKey();
- /*
- * Button theme
- */
- textButtonTheme: TextButtonThemeData(
- style: TextButton.styleFrom(
- primary: wgerSecondaryColor,
- ),
- ),
- outlinedButtonTheme: OutlinedButtonThemeData(
- style: OutlinedButton.styleFrom(
- primary: wgerSecondaryColor,
- visualDensity: VisualDensity.compact,
- side: const BorderSide(color: wgerSecondaryColor),
- ),
- ),
- elevatedButtonTheme: ElevatedButtonThemeData(
- style: ElevatedButton.styleFrom(
- primary: wgerSecondaryColor,
- ),
- ),
+ bool dark = false;
+ bool _canRegister = true;
+ AuthMode _authMode = AuthMode.Login;
+ bool _hideCustomServer = true;
+ final Map _authData = {
+ 'username': '',
+ 'email': '',
+ 'password': '',
+ 'serverUrl': '',
+ };
+ var _isLoading = false;
+ final _usernameController = TextEditingController();
+ final _passwordController = TextEditingController();
+ final _password2Controller = TextEditingController();
+ final _emailController = TextEditingController();
+ final _serverUrlController = TextEditingController(text: DEFAULT_SERVER);
- /*
- * Forms, etc.
- */
- sliderTheme: const SliderThemeData(
- activeTrackColor: wgerSecondaryColor,
- thumbColor: wgerPrimaryColor,
- ),
- colorScheme: ColorScheme.fromSwatch().copyWith(
- secondary: wgerSecondaryColor,
- brightness: Brightness.light,
- )),
- darkTheme: ThemeData(
- dividerColor: wgerSecondaryColorLightDark,
- bottomNavigationBarTheme: BottomNavigationBarThemeData(
- selectedItemColor: Colors.white,
- unselectedItemColor: wgerSecondaryColorLightDark,
- backgroundColor: wgerPrimaryColorDark,
- ),
- focusColor: wgerSecondaryColorDark,
- splashColor: wgerPrimaryColorLightDark,
- primaryColor: wgerPrimaryColorDark,
- primaryColorDark: wgerPrimaryButtonColorDark,
- scaffoldBackgroundColor: wgerBackgroundDark,
- cardColor: wgerPrimaryColorLightDark,
- appBarTheme: AppBarTheme(
- color: wgerPrimaryColorDark,
- elevation: 10,
- titleTextStyle: TextStyle(
- fontFamily: 'OpenSansBold', color: wgerSecondaryColorLightDark, fontSize: 15)),
- textTheme: TextTheme(
- headline1:
- const TextStyle(fontFamily: 'OpenSansLight', color: Colors.white, fontSize: 12),
- headline2: const TextStyle(fontFamily: 'OpenSans', color: Colors.white, fontSize: 15),
- headline3: TextStyle(
- fontSize: 18,
- fontWeight: FontWeight.w600,
- fontFamily: 'OpenSans',
- color: wgerSecondaryColorLightDark,
- ),
- headline4: TextStyle(
- color: wgerSecondaryColorLightDark,
- fontSize: 18,
- fontWeight: FontWeight.w400,
- fontFamily: 'OpenSansBold'),
- headline5: TextStyle(
- fontSize: 16,
- fontFamily: 'OpenSansSemiBold',
- color: wgerSecondaryColorLightDark,
- ),
- headline6: TextStyle(
- fontSize: materialSizes['h6']! * 0.8,
- fontFamily: 'OpenSans',
- color: Colors.white,
- ),
- subtitle1: TextStyle(
- fontSize: 12,
- fontWeight: FontWeight.w200,
- fontFamily: 'OpenSans',
- color: Colors.white,
- ),
- subtitle2: TextStyle(
- fontSize: 10,
- fontWeight: FontWeight.w200,
- fontFamily: 'OpenSansLight',
- color: Colors.white,
- ),
- bodyText1: TextStyle(
- color: darkmode ? wgerSecondaryColorLightDark : wgerSecondaryColorLight,
- fontSize: 18,
- fontWeight: FontWeight.w400,
- ),
- ),
- inputDecorationTheme: InputDecorationTheme(
- focusedBorder: UnderlineInputBorder(borderSide: BorderSide(color: Colors.white)),
- iconColor: wgerSecondaryColorDark,
- labelStyle: TextStyle(color: Colors.white),
- ),
- textButtonTheme: TextButtonThemeData(
- style: TextButton.styleFrom(
- primary: wgerSecondaryColorDark,
- ),
- ),
- outlinedButtonTheme: OutlinedButtonThemeData(
- style: OutlinedButton.styleFrom(
- primary: wgerSecondaryColorDark,
- visualDensity: VisualDensity.compact,
- side: const BorderSide(color: wgerSecondaryColor),
- ),
- ),
- elevatedButtonTheme: ElevatedButtonThemeData(
- style: ElevatedButton.styleFrom(
- primary: wgerSecondaryColorDark,
+ @override
+ void initState() {
+ super.initState();
+ context.read().getServerUrlFromPrefs().then((value) {
+ _serverUrlController.text = value;
+ });
+
+ // Check if the API key is set
+ //
+ // If not, the user will not be able to register via the app
+ try {
+ final metadata = Provider.of(context, listen: false).metadata;
+ if (metadata.containsKey(MANIFEST_KEY_API) || metadata[MANIFEST_KEY_API] == '') {
+ _canRegister = false;
+ }
+ } on PlatformException {
+ _canRegister = false;
+ }
+ }
+
+ void _submit(BuildContext context) async {
+ if (!_formKey.currentState!.validate()) {
+ // Invalid!
+ return;
+ }
+ _formKey.currentState!.save();
+ setState(() {
+ _isLoading = true;
+ });
+
+ try {
+ // Login existing user
+ if (_authMode == AuthMode.Login) {
+ await Provider.of(context, listen: false)
+ .login(_authData['username']!, _authData['password']!, _authData['serverUrl']!);
+
+ // Register new user
+ } else {
+ await Provider.of(context, listen: false).register(
+ username: _authData['username']!,
+ password: _authData['password']!,
+ email: _authData['email']!,
+ serverUrl: _authData['serverUrl']!);
+ }
+
+ setState(() {
+ _isLoading = false;
+ });
+ } on WgerHttpException catch (error) {
+ showHttpExceptionErrorDialog(error, context);
+ setState(() {
+ _isLoading = false;
+ });
+ } catch (error) {
+ showErrorDialog(error, context);
+ setState(() {
+ _isLoading = false;
+ });
+ }
+ }
+
+ void _switchAuthMode() {
+ if (!_canRegister) {
+ launchURL(DEFAULT_SERVER, context);
+ return;
+ }
+
+ if (_authMode == AuthMode.Login) {
+ setState(() {
+ _authMode = AuthMode.Signup;
+ });
+ } else {
+ setState(() {
+ _authMode = AuthMode.Login;
+ });
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final deviceSize = MediaQuery.of(context).size;
+ return Card(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(10.0),
+ ),
+ elevation: 8.0,
+ child: Container(
+ width: deviceSize.width * 0.75,
+ padding: const EdgeInsets.all(16.0),
+ child: Form(
+ key: _formKey,
+ child: SingleChildScrollView(
+ child: AutofillGroup(
+ child: Column(
+ children: [
+ TextFormField(
+ key: const Key('inputUsername'),
+ decoration: InputDecoration(
+ focusedBorder:
+ UnderlineInputBorder(borderSide: BorderSide(color: Colors.white)),
+ labelStyle: Theme.of(context).textTheme.headline6,
+ prefixIcon: Icon(Icons.account_box_rounded),
+ labelText: AppLocalizations.of(context).username,
+ errorMaxLines: 2,
+ ),
+ autofillHints: const [AutofillHints.username],
+ controller: _usernameController,
+ textInputAction: TextInputAction.next,
+ keyboardType: TextInputType.emailAddress,
+ validator: (value) {
+ if (!RegExp(r'^[\w.@+-]+$').hasMatch(value!)) {
+ return AppLocalizations.of(context).usernameValidChars;
+ }
+
+ if (value.isEmpty) {
+ return AppLocalizations.of(context).invalidUsername;
+ }
+ return null;
+ },
+ onSaved: (value) {
+ String? user = value;
+ user = user?.trim();
+ _authData['username'] = user!;
+ },
+ ),
+ SizedBox(
+ height: 10,
+ ),
+ if (_authMode == AuthMode.Signup)
+ TextFormField(
+ key: const Key('inputEmail'),
+ decoration: InputDecoration(
+ focusedBorder:
+ UnderlineInputBorder(borderSide: BorderSide(color: Colors.white)),
+ labelStyle: Theme.of(context).textTheme.headline6,
+ prefixIcon: Icon(Icons.email),
+ labelText: AppLocalizations.of(context).email),
+ autofillHints: const [AutofillHints.email],
+ controller: _emailController,
+ keyboardType: TextInputType.emailAddress,
+ textInputAction: TextInputAction.next,
+
+ // Email is not required
+ validator: (value) {
+ if (value!.isNotEmpty && !value.contains('@')) {
+ return AppLocalizations.of(context).invalidEmail;
+ }
+ return null;
+ },
+ onSaved: (value) {
+ _authData['email'] = value!;
+ },
+ ),
+ SizedBox(
+ height: 10,
+ ),
+ TextFormField(
+ key: const Key('inputPassword'),
+ decoration: InputDecoration(
+ focusedBorder:
+ UnderlineInputBorder(borderSide: BorderSide(color: Colors.white)),
+ labelStyle: Theme.of(context).textTheme.headline6,
+ prefixIcon: Icon(Icons.security),
+ labelText: AppLocalizations.of(context).password),
+ autofillHints: const [AutofillHints.password],
+ obscureText: true,
+ controller: _passwordController,
+ textInputAction: TextInputAction.next,
+ validator: (value) {
+ if (value!.isEmpty || value.length < 8) {
+ return AppLocalizations.of(context).passwordTooShort;
+ }
+ return null;
+ },
+ onSaved: (value) {
+ _authData['password'] = value!;
+ },
+ ),
+ SizedBox(
+ height: 10,
+ ),
+ if (_authMode == AuthMode.Signup)
+ TextFormField(
+ key: const Key('inputPassword2'),
+ decoration: InputDecoration(
+ focusedBorder:
+ UnderlineInputBorder(borderSide: BorderSide(color: Colors.white)),
+ labelStyle: Theme.of(context).textTheme.headline6,
+ prefixIcon: Icon(Icons.security_sharp),
+ labelText: AppLocalizations.of(context).confirmPassword),
+ controller: _password2Controller,
+ enabled: _authMode == AuthMode.Signup,
+ obscureText: true,
+ validator: _authMode == AuthMode.Signup
+ ? (value) {
+ if (value != _passwordController.text) {
+ return AppLocalizations.of(context).passwordsDontMatch;
+ }
+ return null;
+ }
+ : null,
+ ),
+ // Off-stage widgets are kept in the tree, otherwise the server URL
+ // would not be saved to _authData
+ Offstage(
+ offstage: _hideCustomServer,
+ child: Row(
+ children: [
+ Flexible(
+ flex: 3,
+ child: TextFormField(
+ key: const Key('inputServer'),
+ decoration: InputDecoration(
+ focusedBorder: UnderlineInputBorder(
+ borderSide: BorderSide(color: Colors.white)),
+ labelStyle: Theme.of(context).textTheme.headline6,
+ labelText: AppLocalizations.of(context).customServerUrl,
+ helperText: AppLocalizations.of(context).customServerHint,
+ helperStyle: Theme.of(context).textTheme.headline1,
+ helperMaxLines: 4),
+ controller: _serverUrlController,
+ validator: (value) {
+ if (Uri.tryParse(value!) == null) {
+ return AppLocalizations.of(context).invalidUrl;
+ }
+
+ if (value.isEmpty || !value.contains('http')) {
+ return AppLocalizations.of(context).invalidUrl;
+ }
+ return null;
+ },
+ onSaved: (value) {
+ // Remove any trailing slash
+ if (value!.lastIndexOf('/') == (value.length - 1)) {
+ value = value.substring(0, value.lastIndexOf('/'));
+ }
+ _authData['serverUrl'] = value;
+ },
+ ),
+ ),
+ const SizedBox(
+ width: 20,
+ ),
+ Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ IconButton(
+ icon: const Icon(Icons.undo),
+ color: wgerSecondaryColor,
+ onPressed: () {
+ _serverUrlController.text = DEFAULT_SERVER;
+ },
+ ),
+ Text(
+ AppLocalizations.of(context).reset,
+ style: Theme.of(context).textTheme.headline2,
+ )
+ ],
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(
+ height: 20,
+ ),
+ if (_isLoading)
+ const CircularProgressIndicator()
+ else
+ ElevatedButton(
+ key: const Key('actionButton'),
+ child: Text(_authMode == AuthMode.Login
+ ? AppLocalizations.of(context).login
+ : AppLocalizations.of(context).register),
+ onPressed: () {
+ return _submit(context);
+ },
+ ),
+ SizedBox(
+ height: 10,
+ ),
+ ElevatedButton(
+ style: ButtonStyle(
+ backgroundColor: MaterialStateProperty.all(Colors.white),
+ foregroundColor: MaterialStateProperty.all(wgerSecondaryColor),
+ ),
+ key: const Key('toggleActionButton'),
+ child: Text(
+ _authMode == AuthMode.Login
+ ? AppLocalizations.of(context).register.toUpperCase()
+ : AppLocalizations.of(context).login.toUpperCase(),
+ ),
+ onPressed: _switchAuthMode,
+ ),
+ SizedBox(
+ height: 10,
+ ),
+ TextButton(
+ style: ButtonStyle(
+ foregroundColor: MaterialStateProperty.all(wgerSecondaryColorDark),
+ ),
+ child: Text(_hideCustomServer
+ ? AppLocalizations.of(context).useCustomServer
+ : AppLocalizations.of(context).useDefaultServer),
+ key: const Key('toggleCustomServerButton'),
+ onPressed: () {
+ setState(() {
+ _hideCustomServer = !_hideCustomServer;
+ });
+ },
+ ),
+ ],
),
),
- sliderTheme: const SliderThemeData(
- activeTrackColor: wgerSecondaryColor,
- thumbColor: wgerPrimaryColor,
- ),
- colorScheme: ColorScheme.fromSwatch().copyWith(
- secondary: wgerSecondaryColorDark,
- brightness: Brightness.dark,
- ),
),
- themeMode: ThemeMode.system,
- home: auth.isAuth
- ? FutureBuilder(
- future: auth.applicationUpdateRequired(),
- builder: (ctx, snapshot) =>
- snapshot.connectionState == ConnectionState.done && snapshot.data == true
- ? UpdateAppScreen()
- : HomeTabsScreen(),
- )
- : FutureBuilder(
- future: auth.tryAutoLogin(),
- builder: (ctx, authResultSnapshot) =>
- authResultSnapshot.connectionState == ConnectionState.waiting
- ? SplashScreen()
- : AuthScreen(),
- ),
- routes: {
- DashboardScreen.routeName: (ctx) => DashboardScreen(),
- FormScreen.routeName: (ctx) => FormScreen(),
- GalleryScreen.routeName: (ctx) => const GalleryScreen(),
- GymModeScreen.routeName: (ctx) => GymModeScreen(),
- HomeTabsScreen.routeName: (ctx) => HomeTabsScreen(),
- MeasurementCategoriesScreen.routeName: (ctx) => MeasurementCategoriesScreen(),
- MeasurementEntriesScreen.routeName: (ctx) => MeasurementEntriesScreen(),
- NutritionScreen.routeName: (ctx) => NutritionScreen(),
- NutritionalDiaryScreen.routeName: (ctx) => NutritionalDiaryScreen(),
- NutritionalPlanScreen.routeName: (ctx) => NutritionalPlanScreen(),
- WeightScreen.routeName: (ctx) => WeightScreen(),
- WorkoutPlanScreen.routeName: (ctx) => WorkoutPlanScreen(),
- WorkoutPlansScreen.routeName: (ctx) => WorkoutPlansScreen(),
- },
- localizationsDelegates: AppLocalizations.localizationsDelegates,
- supportedLocales: AppLocalizations.supportedLocales,
- debugShowCheckedModeBanner: false,
),
),
);
From ca8e93c8793cb924a5f866bff24d84ef10efcd6a Mon Sep 17 00:00:00 2001
From: JustinBenito <83128918+JustinBenito@users.noreply.github.com>
Date: Mon, 17 Oct 2022 12:34:59 +0530
Subject: [PATCH 8/8] Updated Calendar
---
lib/main.dart | 596 ++++++++++++++++++++------------------------------
1 file changed, 233 insertions(+), 363 deletions(-)
diff --git a/lib/main.dart b/lib/main.dart
index e4640325e..d8df41a7c 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -7,7 +7,7 @@
* 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,
+ * 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.
@@ -17,413 +17,283 @@
*/
import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
import 'package:provider/provider.dart';
-import 'package:wger/exceptions/http_exception.dart';
+import 'package:table_calendar/table_calendar.dart';
import 'package:wger/helpers/consts.dart';
+import 'package:wger/helpers/json.dart';
import 'package:wger/helpers/misc.dart';
-import 'package:wger/helpers/ui.dart';
+import 'package:wger/models/workouts/session.dart';
+import 'package:wger/providers/body_weight.dart';
+import 'package:wger/providers/measurement.dart';
+import 'package:wger/providers/nutrition.dart';
+import 'package:wger/providers/workout_plans.dart';
+import 'package:wger/theme/theme.dart';
-import '../providers/auth.dart';
-import '../theme/theme.dart';
-
-enum AuthMode {
- Signup,
- Login,
+/// Types of events
+enum EventType {
+ weight,
+ measurement,
+ session,
+ caloriesDiary,
}
-class AuthScreen extends StatelessWidget {
- static const routeName = '/auth';
+/// An event in the dashboard calendar
+class Event {
+ final EventType _type;
+ final String _description;
- @override
- Widget build(BuildContext context) {
- final deviceSize = MediaQuery.of(context).size;
- return Scaffold(
- backgroundColor: Theme.of(context).primaryColor,
- body: Stack(
- children: [
- SingleChildScrollView(
- child: SizedBox(
- height: deviceSize.height,
- width: deviceSize.width,
- child: Column(
- mainAxisAlignment: MainAxisAlignment.start,
- crossAxisAlignment: CrossAxisAlignment.center,
- children: [
- const Padding(padding: EdgeInsets.symmetric(vertical: 20)),
- const Image(
- image: AssetImage('assets/images/logo-white.png'),
- width: 120,
- ),
- Container(
- margin: const EdgeInsets.only(bottom: 20.0),
- padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 94.0),
- child: const Text(
- 'WGER',
- style: TextStyle(
- color: Colors.white,
- fontSize: 50,
- fontFamily: 'OpenSansBold',
- fontWeight: FontWeight.bold,
- ),
- ),
- ),
- const Flexible(
- //flex: deviceSize.width > 600 ? 2 : 1,
- child: AuthCard(),
- ),
- ],
- ),
- ),
- ),
- ],
- ),
- );
+ Event(this._type, this._description);
+
+ String get description {
+ return _description;
+ }
+
+ EventType get type {
+ return _type;
}
}
-class AuthCard extends StatefulWidget {
- const AuthCard();
+class DashboardCalendarWidget extends StatefulWidget {
+ const DashboardCalendarWidget();
@override
- _AuthCardState createState() => _AuthCardState();
+ _DashboardCalendarWidgetState createState() => _DashboardCalendarWidgetState();
}
-class _AuthCardState extends State {
- final GlobalKey _formKey = GlobalKey();
-
- bool dark = false;
- bool _canRegister = true;
- AuthMode _authMode = AuthMode.Login;
- bool _hideCustomServer = true;
- final Map _authData = {
- 'username': '',
- 'email': '',
- 'password': '',
- 'serverUrl': '',
- };
- var _isLoading = false;
- final _usernameController = TextEditingController();
- final _passwordController = TextEditingController();
- final _password2Controller = TextEditingController();
- final _emailController = TextEditingController();
- final _serverUrlController = TextEditingController(text: DEFAULT_SERVER);
+class _DashboardCalendarWidgetState extends State
+ with TickerProviderStateMixin {
+ late Map> _events;
+ late final ValueNotifier> _selectedEvents;
+ RangeSelectionMode _rangeSelectionMode =
+ RangeSelectionMode.toggledOff; // Can be toggled on/off by longpressing a date
+ DateTime _focusedDay = DateTime.now();
+ DateTime? _selectedDay;
+ DateTime? _rangeStart;
+ DateTime? _rangeEnd;
@override
void initState() {
super.initState();
- context.read().getServerUrlFromPrefs().then((value) {
- _serverUrlController.text = value;
- });
- // Check if the API key is set
- //
- // If not, the user will not be able to register via the app
- try {
- final metadata = Provider.of(context, listen: false).metadata;
- if (metadata.containsKey(MANIFEST_KEY_API) || metadata[MANIFEST_KEY_API] == '') {
- _canRegister = false;
+ _events = >{};
+ _selectedDay = _focusedDay;
+ _selectedEvents = ValueNotifier(_getEventsForDay(_selectedDay!));
+ loadEvents();
+ }
+
+ void loadEvents() async {
+ // Process weight entries
+ final BodyWeightProvider weightProvider =
+ Provider.of(context, listen: false);
+ for (final entry in weightProvider.items) {
+ final date = DateFormatLists.format(entry.date);
+
+ if (!_events.containsKey(date)) {
+ _events[date] = [];
}
- } on PlatformException {
- _canRegister = false;
+
+ // Add events to lists
+ _events[date]!.add(Event(EventType.weight, '${entry.weight} kg'));
}
- }
- void _submit(BuildContext context) async {
- if (!_formKey.currentState!.validate()) {
- // Invalid!
- return;
+ // Process measurements
+ final MeasurementProvider measurementProvider =
+ Provider.of(context, listen: false);
+ for (final category in measurementProvider.categories) {
+ for (final entry in category.entries) {
+ final date = DateFormatLists.format(entry.date);
+
+ if (!_events.containsKey(date)) {
+ _events[date] = [];
+ }
+
+ _events[date]!
+ .add(Event(EventType.measurement, '${category.name}: ${entry.value} ${category.unit}'));
+ }
}
- _formKey.currentState!.save();
- setState(() {
- _isLoading = true;
+
+ // Process workout sessions
+ final WorkoutPlansProvider plans = Provider.of(context, listen: false);
+ await plans.fetchSessionData().then((entries) {
+ for (final entry in entries['results']) {
+ final session = WorkoutSession.fromJson(entry);
+ final date = DateFormatLists.format(session.date);
+ if (!_events.containsKey(date)) {
+ _events[date] = [];
+ }
+ var time = '';
+ time = '(${timeToString(session.timeStart)} - ${timeToString(session.timeEnd)})';
+
+ // Add events to lists
+ _events[date]!.add(Event(
+ EventType.session,
+ '${AppLocalizations.of(context).impression}: ${session.impressionAsString} $time',
+ ));
+ }
});
- try {
- // Login existing user
- if (_authMode == AuthMode.Login) {
- await Provider.of(context, listen: false)
- .login(_authData['username']!, _authData['password']!, _authData['serverUrl']!);
-
- // Register new user
- } else {
- await Provider.of(context, listen: false).register(
- username: _authData['username']!,
- password: _authData['password']!,
- email: _authData['email']!,
- serverUrl: _authData['serverUrl']!);
+ // Process nutritional plans
+ final NutritionPlansProvider nutritionProvider =
+ Provider.of(context, listen: false);
+ for (final plan in nutritionProvider.items) {
+ for (final entry in plan.logEntriesValues.entries) {
+ final date = DateFormatLists.format(entry.key);
+ if (!_events.containsKey(date)) {
+ _events[date] = [];
+ }
+
+ // Add events to lists
+ _events[date]!.add(Event(
+ EventType.caloriesDiary,
+ '${entry.value.energy.toStringAsFixed(0)} kcal',
+ ));
}
+ }
+ // Add initial selected day to events list
+ _selectedEvents.value = _getEventsForDay(_selectedDay!);
+ }
+
+ @override
+ void dispose() {
+ _selectedEvents.dispose();
+ super.dispose();
+ }
+
+ List _getEventsForDay(DateTime day) {
+ return _events[DateFormatLists.format(day)] ?? [];
+ }
+
+ List _getEventsForRange(DateTime start, DateTime end) {
+ final days = daysInRange(start, end);
+
+ return [
+ for (final d in days) ..._getEventsForDay(d),
+ ];
+ }
+
+ void _onDaySelected(DateTime selectedDay, DateTime focusedDay) {
+ if (!isSameDay(_selectedDay, selectedDay)) {
setState(() {
- _isLoading = false;
- });
- } on WgerHttpException catch (error) {
- showHttpExceptionErrorDialog(error, context);
- setState(() {
- _isLoading = false;
- });
- } catch (error) {
- showErrorDialog(error, context);
- setState(() {
- _isLoading = false;
+ _selectedDay = selectedDay;
+ _focusedDay = focusedDay;
+ _rangeStart = null; // Important to clean those
+ _rangeEnd = null;
+ _rangeSelectionMode = RangeSelectionMode.toggledOff;
});
+
+ _selectedEvents.value = _getEventsForDay(selectedDay);
}
}
- void _switchAuthMode() {
- if (!_canRegister) {
- launchURL(DEFAULT_SERVER, context);
- return;
- }
+ void _onRangeSelected(DateTime? start, DateTime? end, DateTime focusedDay) {
+ setState(() {
+ _selectedDay = null;
+ _focusedDay = focusedDay;
+ _rangeStart = start;
+ _rangeEnd = end;
+ _rangeSelectionMode = RangeSelectionMode.toggledOn;
+ });
- if (_authMode == AuthMode.Login) {
- setState(() {
- _authMode = AuthMode.Signup;
- });
- } else {
- setState(() {
- _authMode = AuthMode.Login;
- });
+ // `start` or `end` could be null
+ if (start != null && end != null) {
+ _selectedEvents.value = _getEventsForRange(start, end);
+ } else if (start != null) {
+ _selectedEvents.value = _getEventsForDay(start);
+ } else if (end != null) {
+ _selectedEvents.value = _getEventsForDay(end);
}
}
@override
Widget build(BuildContext context) {
- final deviceSize = MediaQuery.of(context).size;
- return Card(
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(10.0),
- ),
- elevation: 8.0,
- child: Container(
- width: deviceSize.width * 0.75,
- padding: const EdgeInsets.all(16.0),
- child: Form(
- key: _formKey,
- child: SingleChildScrollView(
- child: AutofillGroup(
- child: Column(
- children: [
- TextFormField(
- key: const Key('inputUsername'),
- decoration: InputDecoration(
- focusedBorder:
- UnderlineInputBorder(borderSide: BorderSide(color: Colors.white)),
- labelStyle: Theme.of(context).textTheme.headline6,
- prefixIcon: Icon(Icons.account_box_rounded),
- labelText: AppLocalizations.of(context).username,
- errorMaxLines: 2,
- ),
- autofillHints: const [AutofillHints.username],
- controller: _usernameController,
- textInputAction: TextInputAction.next,
- keyboardType: TextInputType.emailAddress,
- validator: (value) {
- if (!RegExp(r'^[\w.@+-]+$').hasMatch(value!)) {
- return AppLocalizations.of(context).usernameValidChars;
- }
-
- if (value.isEmpty) {
- return AppLocalizations.of(context).invalidUsername;
- }
- return null;
- },
- onSaved: (value) {
- String? user = value;
- user = user?.trim();
- _authData['username'] = user!;
- },
- ),
- SizedBox(
- height: 10,
- ),
- if (_authMode == AuthMode.Signup)
- TextFormField(
- key: const Key('inputEmail'),
- decoration: InputDecoration(
- focusedBorder:
- UnderlineInputBorder(borderSide: BorderSide(color: Colors.white)),
- labelStyle: Theme.of(context).textTheme.headline6,
- prefixIcon: Icon(Icons.email),
- labelText: AppLocalizations.of(context).email),
- autofillHints: const [AutofillHints.email],
- controller: _emailController,
- keyboardType: TextInputType.emailAddress,
- textInputAction: TextInputAction.next,
-
- // Email is not required
- validator: (value) {
- if (value!.isNotEmpty && !value.contains('@')) {
- return AppLocalizations.of(context).invalidEmail;
- }
- return null;
- },
- onSaved: (value) {
- _authData['email'] = value!;
- },
- ),
- SizedBox(
- height: 10,
- ),
- TextFormField(
- key: const Key('inputPassword'),
- decoration: InputDecoration(
- focusedBorder:
- UnderlineInputBorder(borderSide: BorderSide(color: Colors.white)),
- labelStyle: Theme.of(context).textTheme.headline6,
- prefixIcon: Icon(Icons.security),
- labelText: AppLocalizations.of(context).password),
- autofillHints: const [AutofillHints.password],
- obscureText: true,
- controller: _passwordController,
- textInputAction: TextInputAction.next,
- validator: (value) {
- if (value!.isEmpty || value.length < 8) {
- return AppLocalizations.of(context).passwordTooShort;
- }
- return null;
- },
- onSaved: (value) {
- _authData['password'] = value!;
- },
- ),
- SizedBox(
- height: 10,
+ return Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Card(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(15.0),
+ ),
+ elevation: 3,
+ child: Column(
+ children: [
+ ListTile(
+ title: Text(
+ AppLocalizations.of(context).calendar,
+ style: Theme.of(context).textTheme.headline4,
+ ),
+ leading: const Icon(
+ Icons.calendar_today_outlined,
+ color: wgerSecondaryColor,
+ ),
+ ),
+ TableCalendar(
+ locale: Localizations.localeOf(context).languageCode,
+ firstDay: DateTime.now().subtract(const Duration(days: 1000)),
+ lastDay: DateTime.now(),
+ focusedDay: _focusedDay,
+ selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
+ rangeStartDay: _rangeStart,
+ rangeEndDay: _rangeEnd,
+ calendarFormat: CalendarFormat.month,
+ availableGestures: AvailableGestures.horizontalSwipe,
+ availableCalendarFormats: const {
+ CalendarFormat.month: '',
+ },
+ daysOfWeekStyle: DaysOfWeekStyle(
+ weekdayStyle: TextStyle(
+ color: wgerSecondaryColor,
+ fontWeight: FontWeight.w200,
),
- if (_authMode == AuthMode.Signup)
- TextFormField(
- key: const Key('inputPassword2'),
- decoration: InputDecoration(
- focusedBorder:
- UnderlineInputBorder(borderSide: BorderSide(color: Colors.white)),
- labelStyle: Theme.of(context).textTheme.headline6,
- prefixIcon: Icon(Icons.security_sharp),
- labelText: AppLocalizations.of(context).confirmPassword),
- controller: _password2Controller,
- enabled: _authMode == AuthMode.Signup,
- obscureText: true,
- validator: _authMode == AuthMode.Signup
- ? (value) {
- if (value != _passwordController.text) {
- return AppLocalizations.of(context).passwordsDontMatch;
- }
- return null;
- }
- : null,
- ),
- // Off-stage widgets are kept in the tree, otherwise the server URL
- // would not be saved to _authData
- Offstage(
- offstage: _hideCustomServer,
- child: Row(
- children: [
- Flexible(
- flex: 3,
- child: TextFormField(
- key: const Key('inputServer'),
- decoration: InputDecoration(
- focusedBorder: UnderlineInputBorder(
- borderSide: BorderSide(color: Colors.white)),
- labelStyle: Theme.of(context).textTheme.headline6,
- labelText: AppLocalizations.of(context).customServerUrl,
- helperText: AppLocalizations.of(context).customServerHint,
- helperStyle: Theme.of(context).textTheme.headline1,
- helperMaxLines: 4),
- controller: _serverUrlController,
- validator: (value) {
- if (Uri.tryParse(value!) == null) {
- return AppLocalizations.of(context).invalidUrl;
- }
+ weekendStyle: TextStyle(
+ color: wgerSecondaryColor,
+ fontWeight: FontWeight.w200,
+ )),
+ rangeSelectionMode: _rangeSelectionMode,
+ eventLoader: _getEventsForDay,
+ startingDayOfWeek: StartingDayOfWeek.monday,
+ calendarStyle: wgerCalendarStyle,
+ headerStyle: HeaderStyle(
+ titleTextStyle: Theme.of(context).textTheme.bodyText1!,
+ ),
+ onDaySelected: _onDaySelected,
+ onRangeSelected: _onRangeSelected,
+ onFormatChanged: (format) {},
+ onPageChanged: (focusedDay) {
+ _focusedDay = focusedDay;
+ },
+ ),
+ const SizedBox(height: 8.0),
+ ValueListenableBuilder>(
+ valueListenable: _selectedEvents,
+ builder: (context, value, _) => Column(
+ children: [
+ ...value
+ .map((event) => ListTile(
+ title: Text((() {
+ switch (event.type) {
+ case EventType.caloriesDiary:
+ return AppLocalizations.of(context).nutritionalDiary;
- if (value.isEmpty || !value.contains('http')) {
- return AppLocalizations.of(context).invalidUrl;
- }
- return null;
- },
- onSaved: (value) {
- // Remove any trailing slash
- if (value!.lastIndexOf('/') == (value.length - 1)) {
- value = value.substring(0, value.lastIndexOf('/'));
+ case EventType.session:
+ return AppLocalizations.of(context).workoutSession;
+
+ case EventType.weight:
+ return AppLocalizations.of(context).weight;
+
+ case EventType.measurement:
+ return AppLocalizations.of(context).measurement;
}
- _authData['serverUrl'] = value;
- },
- ),
- ),
- const SizedBox(
- width: 20,
- ),
- Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- IconButton(
- icon: const Icon(Icons.undo),
- color: wgerSecondaryColor,
- onPressed: () {
- _serverUrlController.text = DEFAULT_SERVER;
- },
- ),
- Text(
- AppLocalizations.of(context).reset,
- style: Theme.of(context).textTheme.headline2,
- )
- ],
- ),
- ],
- ),
- ),
- const SizedBox(
- height: 20,
- ),
- if (_isLoading)
- const CircularProgressIndicator()
- else
- ElevatedButton(
- key: const Key('actionButton'),
- child: Text(_authMode == AuthMode.Login
- ? AppLocalizations.of(context).login
- : AppLocalizations.of(context).register),
- onPressed: () {
- return _submit(context);
- },
- ),
- SizedBox(
- height: 10,
- ),
- ElevatedButton(
- style: ButtonStyle(
- backgroundColor: MaterialStateProperty.all(Colors.white),
- foregroundColor: MaterialStateProperty.all(wgerSecondaryColor),
- ),
- key: const Key('toggleActionButton'),
- child: Text(
- _authMode == AuthMode.Login
- ? AppLocalizations.of(context).register.toUpperCase()
- : AppLocalizations.of(context).login.toUpperCase(),
- ),
- onPressed: _switchAuthMode,
- ),
- SizedBox(
- height: 10,
- ),
- TextButton(
- style: ButtonStyle(
- foregroundColor: MaterialStateProperty.all(wgerSecondaryColorDark),
- ),
- child: Text(_hideCustomServer
- ? AppLocalizations.of(context).useCustomServer
- : AppLocalizations.of(context).useDefaultServer),
- key: const Key('toggleCustomServerButton'),
- onPressed: () {
- setState(() {
- _hideCustomServer = !_hideCustomServer;
- });
- },
- ),
+ })()),
+ subtitle: Text(event.description),
+ //onTap: () => print('$event tapped!'),
+ ))
+ .toList()
],
),
),
- ),
+ ],
),
),
);