Load the user's workout plans

This commit is contained in:
Roland Geider
2020-11-05 00:04:38 +01:00
parent 944f434757
commit 54018e17f4
12 changed files with 278 additions and 71 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -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<Auth>(
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<Auth, WorkoutPlans>(
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(),
),
),
),
);

View File

@@ -33,10 +33,10 @@ class Auth with ChangeNotifier {
// }
Future<void> _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: <String, String>{
'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<void> signIn(String email, String password) async {
return _authenticate(email, password, 'signInWithPassword');
Future<void> signIn(String username, String password) async {
return _authenticate(username, password, 'signInWithPassword');
}
Future<bool> tryAutoLogin() async {

View File

@@ -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,
});
}

View File

@@ -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<WorkoutPlan> _entries = [];
final String authToken;
WorkoutPlans(this.authToken, this._entries);
List<WorkoutPlan> get items {
return [..._entries];
}
WorkoutPlan findById(int id) {
return _entries.firstWhere((workoutPlan) => workoutPlan.id == id);
}
Future<void> fetchAndSetWorkouts() async {
final response = await http.get(
workoutPlansUrl,
headers: <String, String>{'Authorization': 'Token $authToken'},
);
final extractedData = json.decode(response.body) as Map<String, dynamic>;
final List<WorkoutPlan> 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<void> 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<void> 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<void> 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;
}
}

View File

@@ -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<AuthCard> {
});
try {
if (_authMode == AuthMode.Login) {
await Provider.of<Auth>(context, listen: false)
.signIn(_authData['email'], _authData['password']);
await Provider.of<Auth>(context, listen: false).signIn(
_authData['username'],
_authData['password'],
);
} else {
// await Provider.of<Auth>(context, listen: false)
// .signUp(_authData['email'], _authData['password']);
@@ -135,9 +138,9 @@ class _AuthCardState extends State<AuthCard> {
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<AuthCard> {
child: SingleChildScrollView(
child: Column(
children: <Widget>[
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<AuthCard> {
if (value != _passwordController.text) {
return 'Passwords do not match!';
}
return null;
}
: null,
),
@@ -244,7 +249,7 @@ class _AuthCardState extends State<AuthCard> {
),
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<AuthCard> {
onPressed: _switchAuthMode,
padding: EdgeInsets.symmetric(horizontal: 30.0, vertical: 4),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
textColor: const Color(0xff204a87),
textColor: wgerPrimaryColor,
),
],
),

View File

@@ -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<ScheduleScreen> {
var _isLoading = false;
var _isInit = true;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (_isInit) {
setState(() {
_isLoading = true;
});
Provider.of<WorkoutPlans>(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(),
);
}
}

14
lib/theme/theme.dart Normal file
View File

@@ -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,
);

View File

@@ -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<Auth>(context, listen: false).logout();
}),
leading: Icon(Icons.exit_to_app),
title: Text('Logout'),
onTap: () {
Navigator.of(context).pop();
Navigator.of(context).pushReplacementNamed('/');
Provider.of<Auth>(context, listen: false).logout();
},
),
],
),
);

View File

@@ -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<WorkoutPlans>(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),
),
),
);
}
}

View File

@@ -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