diff --git a/assets/fonts/OpenSans-Bold.ttf b/assets/fonts/OpenSans-Bold.ttf new file mode 100644 index 00000000..1811cd63 Binary files /dev/null and b/assets/fonts/OpenSans-Bold.ttf differ diff --git a/assets/fonts/OpenSans-Light.ttf b/assets/fonts/OpenSans-Light.ttf new file mode 100644 index 00000000..bf6b3693 Binary files /dev/null and b/assets/fonts/OpenSans-Light.ttf differ diff --git a/lib/main.dart b/lib/main.dart index 5bf7c02c..0898ff80 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:wger/providers/workout_plans.dart'; import 'package:wger/screens/auth_screen.dart'; import 'package:wger/screens/home_tabs_screen.dart'; import 'package:wger/screens/splash_screen.dart'; +import 'package:wger/theme/theme.dart'; import 'providers/auth.dart'; @@ -17,27 +19,33 @@ class MyApp extends StatelessWidget { return ChangeNotifierProvider( create: (_) => Auth(), child: Consumer( - builder: (ctx, auth, _) => MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - primaryColor: Color(0xff2a4c7d), - accentColor: Colors.amber, - - // 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, + builder: (ctx, auth, _) => MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (ctx) => Auth(), + ), + ChangeNotifierProxyProvider( + create: null, // TODO: Create is required but it can be null?? + update: (context, value, previous) => WorkoutPlans( + auth.token, + previous == null ? [] : previous.items, + ), + ) + ], + child: MaterialApp( + title: 'Flutter Demo', + theme: wgerTheme, + home: auth.isAuth + ? HomeTabsScreen() + : FutureBuilder( + future: auth.tryAutoLogin(), + builder: (ctx, authResultSnapshot) => + authResultSnapshot.connectionState == + ConnectionState.waiting + ? SplashScreen() + : AuthScreen(), + ), ), - home: auth.isAuth - ? HomeTabsScreen() - : FutureBuilder( - future: auth.tryAutoLogin(), - builder: (ctx, authResultSnapshot) => - authResultSnapshot.connectionState == - ConnectionState.waiting - ? SplashScreen() - : AuthScreen(), - ), ), ), ); diff --git a/lib/providers/auth.dart b/lib/providers/auth.dart index 54e37bd8..6de71820 100644 --- a/lib/providers/auth.dart +++ b/lib/providers/auth.dart @@ -33,10 +33,10 @@ class Auth with ChangeNotifier { // } Future _authenticate( - String email, String password, String urlSegment) async { + String username, String password, String urlSegment) async { // The android emulator uses var url = 'http://10.0.2.2:8000/api/v2/login/'; - print(email); + print(username); print(password); try { @@ -45,7 +45,7 @@ class Auth with ChangeNotifier { headers: { 'Content-Type': 'application/json; charset=UTF-8', }, - body: json.encode({'username': email, 'password': password}), + body: json.encode({'username': username, 'password': password}), ); final responseData = json.decode(response.body); print(response.statusCode); @@ -86,8 +86,8 @@ class Auth with ChangeNotifier { return _authenticate(email, password, 'signUp'); } - Future signIn(String email, String password) async { - return _authenticate(email, password, 'signInWithPassword'); + Future signIn(String username, String password) async { + return _authenticate(username, password, 'signInWithPassword'); } Future tryAutoLogin() async { diff --git a/lib/providers/workout_plan.dart b/lib/providers/workout_plan.dart new file mode 100644 index 00000000..f1b164c0 --- /dev/null +++ b/lib/providers/workout_plan.dart @@ -0,0 +1,13 @@ +import 'package:flutter/foundation.dart'; + +class WorkoutPlan with ChangeNotifier { + final int id; + final DateTime creation_date; + final String description; + + WorkoutPlan({ + @required this.id, + @required this.description, + @required this.creation_date, + }); +} diff --git a/lib/providers/workout_plans.dart b/lib/providers/workout_plans.dart new file mode 100644 index 00000000..182ad44e --- /dev/null +++ b/lib/providers/workout_plans.dart @@ -0,0 +1,106 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:wger/providers/workout_plan.dart'; + +class WorkoutPlans with ChangeNotifier { + static const workoutPlansUrl = 'http://10.0.2.2:8000/api/v2/workout/'; + List _entries = []; + final String authToken; + + WorkoutPlans(this.authToken, this._entries); + + List get items { + return [..._entries]; + } + + WorkoutPlan findById(int id) { + return _entries.firstWhere((workoutPlan) => workoutPlan.id == id); + } + + Future fetchAndSetWorkouts() async { + final response = await http.get( + workoutPlansUrl, + headers: {'Authorization': 'Token $authToken'}, + ); + final extractedData = json.decode(response.body) as Map; + + final List loadedWorkoutPlans = []; + if (loadedWorkoutPlans == null) { + return; + } + + try { + for (final entry in extractedData['results']) { + loadedWorkoutPlans.add(WorkoutPlan( + id: entry['id'], + description: entry['comment'], + creation_date: DateTime.parse(entry['creation_date']), + )); + } + + _entries = loadedWorkoutPlans; + notifyListeners(); + } catch (error) { + throw (error); + } + } + + Future addProduct(WorkoutPlan product) async { + final productsUrl = + 'https://flutter-shop-a2335.firebaseio.com/products.json?auth=$authToken'; + try { + final response = await http.post( + productsUrl, + body: json.encode( + { + 'description': product.description, + }, + ), + ); + final newProduct = WorkoutPlan( + id: json.decode(response.body)['name'], + creation_date: json.decode(response.body)['creation_date'], + description: product.description, + ); + _entries.add(newProduct); + notifyListeners(); + } catch (error) { + print(error); + throw error; + } + } + + Future updateProduct(String id, WorkoutPlan newProduct) async { + final prodIndex = _entries.indexWhere((element) => element.id == id); + if (prodIndex >= 0) { + final url = + 'https://flutter-shop-a2335.firebaseio.com/products/$id.json?auth=$authToken'; + await http.patch(url, + body: json.encode({ + 'description': newProduct.description, + })); + _entries[prodIndex] = newProduct; + notifyListeners(); + } + } + + Future deleteProduct(String id) async { + final url = + 'https://flutter-shop-a2335.firebaseio.com/products/$id.json?auth=$authToken'; + final existingProductIndex = + _entries.indexWhere((element) => element.id == id); + var existingProduct = _entries[existingProductIndex]; + _entries.removeAt(existingProductIndex); + notifyListeners(); + + final response = await http.delete(url); + if (response.statusCode >= 400) { + _entries.insert(existingProductIndex, existingProduct); + notifyListeners(); + //throw HttpException(); + } + existingProduct = null; + } +} diff --git a/lib/screens/auth_screen.dart b/lib/screens/auth_screen.dart index a3d26a33..d482e3d3 100644 --- a/lib/screens/auth_screen.dart +++ b/lib/screens/auth_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; +import 'package:wger/theme/theme.dart'; import '../models/http_exception.dart'; import '../providers/auth.dart'; @@ -50,8 +51,8 @@ class AuthScreen extends StatelessWidget { style: TextStyle( color: Theme.of(context).accentColor, fontSize: 50, - fontFamily: 'Anton', - fontWeight: FontWeight.normal, + fontFamily: 'OpenSansBold', + fontWeight: FontWeight.bold, ), ), ), @@ -119,8 +120,10 @@ class _AuthCardState extends State { }); try { if (_authMode == AuthMode.Login) { - await Provider.of(context, listen: false) - .signIn(_authData['email'], _authData['password']); + await Provider.of(context, listen: false).signIn( + _authData['username'], + _authData['password'], + ); } else { // await Provider.of(context, listen: false) // .signUp(_authData['email'], _authData['password']); @@ -135,9 +138,9 @@ class _AuthCardState extends State { errorMessage = error.errors['password']; } _showErrorDialog(errorMessage); - } finally { - var errorMessage = 'Could not authenticate you. Please try again later'; - _showErrorDialog(errorMessage); + //} finally { + // var errorMessage = 'Could not authenticate you. Please try again later'; + // _showErrorDialog(errorMessage); } setState(() { @@ -176,41 +179,42 @@ class _AuthCardState extends State { child: SingleChildScrollView( child: Column( children: [ + TextFormField( + decoration: InputDecoration(labelText: 'Username'), + keyboardType: TextInputType.emailAddress, + validator: (value) { + if (value.isEmpty) { + return 'Invalid Username!'; + } + return null; + }, + onSaved: (value) { + _authData['username'] = value; + }, + ), if (_authMode == AuthMode.Signup) TextFormField( - decoration: InputDecoration(labelText: 'Username'), + decoration: InputDecoration(labelText: 'E-Mail'), keyboardType: TextInputType.emailAddress, validator: (value) { - if (value.isEmpty) { - return 'Invalid Username!'; + if (value.isEmpty || !value.contains('@')) { + return 'Invalid email!'; } return null; }, onSaved: (value) { - _authData['username'] = value; + _authData['email'] = value; }, ), - TextFormField( - decoration: InputDecoration(labelText: 'E-Mail'), - keyboardType: TextInputType.emailAddress, - validator: (value) { - //if (value.isEmpty || !value.contains('@')) { - // return 'Invalid email!'; - //} - return null; - }, - onSaved: (value) { - _authData['email'] = value; - }, - ), TextFormField( decoration: InputDecoration(labelText: 'Password'), obscureText: true, controller: _passwordController, validator: (value) { - if (value.isEmpty || value.length < 5) { + if (value.isEmpty || value.length < 8) { return 'Password is too short!'; } + return null; }, onSaved: (value) { _authData['password'] = value; @@ -226,6 +230,7 @@ class _AuthCardState extends State { if (value != _passwordController.text) { return 'Passwords do not match!'; } + return null; } : null, ), @@ -244,7 +249,7 @@ class _AuthCardState extends State { ), padding: EdgeInsets.symmetric(horizontal: 30.0, vertical: 8.0), - color: const Color(0xff204a87), + color: wgerPrimaryColor, textColor: Theme.of(context).primaryTextTheme.button.color, ), FlatButton( @@ -253,7 +258,7 @@ class _AuthCardState extends State { onPressed: _switchAuthMode, padding: EdgeInsets.symmetric(horizontal: 30.0, vertical: 4), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - textColor: const Color(0xff204a87), + textColor: wgerPrimaryColor, ), ], ), diff --git a/lib/screens/schedule_screen.dart b/lib/screens/schedule_screen.dart index eb00215c..798e0009 100644 --- a/lib/screens/schedule_screen.dart +++ b/lib/screens/schedule_screen.dart @@ -1,7 +1,33 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:wger/providers/workout_plans.dart'; import 'package:wger/widgets/app_drawer.dart'; +import 'package:wger/widgets/workout_plans_list.dart'; + +class ScheduleScreen extends StatefulWidget { + @override + _ScheduleScreenState createState() => _ScheduleScreenState(); +} + +class _ScheduleScreenState extends State { + var _isLoading = false; + var _isInit = true; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_isInit) { + setState(() { + _isLoading = true; + }); + + Provider.of(context).fetchAndSetWorkouts().then((value) { + _isLoading = false; + }); + } + _isInit = false; + } -class ScheduleScreen extends StatelessWidget { Widget getAppBar() { return AppBar( title: Text('Workouts'), @@ -19,6 +45,15 @@ class ScheduleScreen extends StatelessWidget { return Scaffold( appBar: getAppBar(), drawer: AppDrawer(), + floatingActionButton: FloatingActionButton( + onPressed: () {}, + child: const Icon(Icons.add), + ), + body: _isLoading + ? Center( + child: CircularProgressIndicator(), + ) + : WorkoutPlansList(), ); } } diff --git a/lib/theme/theme.dart b/lib/theme/theme.dart new file mode 100644 index 00000000..bb9a54e2 --- /dev/null +++ b/lib/theme/theme.dart @@ -0,0 +1,14 @@ +import 'package:flutter/material.dart'; + +const Color wgerPrimaryColor = Color(0xff2a4c7d); +const Color wgerSecondaryColor = Color(0xffe63946); + +final ThemeData wgerTheme = ThemeData( + primaryColor: wgerPrimaryColor, + accentColor: wgerSecondaryColor, + //fontFamily: 'OpenSansLight', + // 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, +); diff --git a/lib/widgets/app_drawer.dart b/lib/widgets/app_drawer.dart index 7ed74986..44ac793f 100644 --- a/lib/widgets/app_drawer.dart +++ b/lib/widgets/app_drawer.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:wger/providers/auth.dart'; class AppDrawer extends StatelessWidget { @override @@ -36,13 +38,14 @@ class AppDrawer extends StatelessWidget { ), Divider(), ListTile( - leading: Icon(Icons.exit_to_app), - title: Text('Logout'), - onTap: () { - Navigator.of(context).pop(); - Navigator.of(context).pushReplacementNamed('/'); - //Provider.of(context, listen: false).logout(); - }), + leading: Icon(Icons.exit_to_app), + title: Text('Logout'), + onTap: () { + Navigator.of(context).pop(); + Navigator.of(context).pushReplacementNamed('/'); + Provider.of(context, listen: false).logout(); + }, + ), ], ), ); diff --git a/lib/widgets/workout_plans_list.dart b/lib/widgets/workout_plans_list.dart new file mode 100644 index 00000000..4fe55dd0 --- /dev/null +++ b/lib/widgets/workout_plans_list.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:provider/provider.dart'; +import 'package:wger/providers/workout_plans.dart'; + +class WorkoutPlansList extends StatelessWidget { + @override + Widget build(BuildContext context) { + final workoutPlansData = Provider.of(context); + return ListView.builder( + padding: const EdgeInsets.all(10.0), + itemCount: workoutPlansData.items.length, + itemBuilder: (context, index) => Card( + child: ListTile( + leading: FlutterLogo(size: 56.0), + title: Text( + DateFormat('dd.MM.yyyy') + .format(workoutPlansData.items[index].creation_date) + .toString(), + ), + subtitle: Text(workoutPlansData.items[index].description), + ), + ), + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index ee6a2757..429c5d50 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -71,17 +71,14 @@ flutter: # "family" key with the font family name, and a "fonts" key with a # list giving the asset and other descriptors for the font. For # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 + fonts: + - family: OpenSansLight + fonts: + - asset: assets/fonts/OpenSans-Light.ttf + - family: OpenSansBold + fonts: + - asset: assets/fonts/OpenSans-Bold.ttf + weight: 700 # # For details regarding fonts from package dependencies, # see https://flutter.dev/custom-fonts/#from-packages