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 [Get it on Google Play 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) +[Promo Animation of WGER](https://github.com/wger-project/flutter) + ## Installation [Get it on Google Play 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) -[Promo Animation of WGER](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() ], ), ), - ), + ], ), ), );