mirror of
https://github.com/wger-project/flutter.git
synced 2026-02-18 00:17:48 +01:00
Load the user's workout plans
This commit is contained in:
BIN
assets/fonts/OpenSans-Bold.ttf
Normal file
BIN
assets/fonts/OpenSans-Bold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/OpenSans-Light.ttf
Normal file
BIN
assets/fonts/OpenSans-Light.ttf
Normal file
Binary file not shown.
@@ -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(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
13
lib/providers/workout_plan.dart
Normal file
13
lib/providers/workout_plan.dart
Normal 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,
|
||||
});
|
||||
}
|
||||
106
lib/providers/workout_plans.dart
Normal file
106
lib/providers/workout_plans.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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
14
lib/theme/theme.dart
Normal 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,
|
||||
);
|
||||
@@ -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();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
26
lib/widgets/workout_plans_list.dart
Normal file
26
lib/widgets/workout_plans_list.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
19
pubspec.yaml
19
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
|
||||
|
||||
Reference in New Issue
Block a user