Commit eb8ce58c authored by Administrator's avatar Administrator

A monster of a commit:

Email and Password entered via Form
... stored in credentials
... used to download via web scraping from Strava
Switched to provider
parent 50e8b0dd
......@@ -33,7 +33,7 @@ android {
defaultConfig {
applicationId "com.informatom.encrateia"
minSdkVersion 16
minSdkVersion 18
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
......
import 'package:flutter/material.dart';
import 'screens/dashboard.dart';
import 'package:encrateia/utils/db.dart';
void main() => runApp(MyApp());
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Db.create().connect();
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
......
......@@ -36,7 +36,7 @@ const tableActivity = SqfEntityTable(
SqfEntityField('name', DbType.text),
SqfEntityField('movingTime', DbType.integer),
SqfEntityField('type', DbType.text),
SqfEntityField('startDateTime', DbType.text),
SqfEntityField('startTime', DbType.text),
SqfEntityField('distance', DbType.integer),
SqfEntityFieldRelationship(
parentTable: tableAthlete,
......
This diff is collapsed.
// GENERATED CODE - DO NOT MODIFY BY HAND
// **************************************************************************
// SqfEntityFormGenerator
// **************************************************************************
/*
FORM WIDGETS OF YOUR MODEL WILL COME HERE SOON
*/
import 'package:encrateia/models/fit_download.dart';
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:encrateia/utils/db.dart';
import 'package:encrateia/model/model.dart';
import 'package:strava_flutter/strava.dart';
import 'package:encrateia/secrets/secrets.dart';
import 'package:strava_flutter/Models/activity.dart';
import 'package:strava_flutter/Models/activity.dart' as StravaActivity;
import 'package:encrateia/models/athlete.dart';
class Activity extends Model {
int id;
class Activity extends ChangeNotifier {
String state;
String path;
int stravaId;
String name;
Duration movingTime;
String type;
DateTime startDateTime;
int distance;
DbActivity db;
Activity();
String toString() => '$name $startDateTime';
Activity.fromStrava(SummaryActivity activity)
: stravaId = activity.id,
name = activity.name,
movingTime = Duration(seconds: activity.movingTime),
type = activity.type,
distance = activity.distance.toInt();
Activity.fromDb(DbActivity dbActivity)
: id = dbActivity.id,
stravaId = dbActivity.stravaId,
name = dbActivity.name,
movingTime = Duration(seconds: dbActivity.movingTime),
type = dbActivity.type,
distance = dbActivity.distance,
state = dbActivity.state;
Activity.fromStrava(StravaActivity.SummaryActivity summaryActivity) {
this.db
..name = summaryActivity.name
..movingTime = summaryActivity.movingTime
..type = summaryActivity.type
..distance = summaryActivity.distance.toInt();
}
download() async {
FitDownload.byId(stravaId.toString());
Activity.fromDb(DbActivity dbActivity) {
this
..db = dbActivity
..state = "fromDatabase";
}
persist() async {
await Db.create().connect();
String toString() => '$db.name $db.startTime';
var dbActivity = DbActivity(
stravaId: stravaId,
name: name,
movingTime: movingTime.inSeconds,
type: type,
distance: distance,
Duration movingDuration() => Duration(seconds: db.movingTime ?? 0);
DateTime startDateTime() => DateTime.parse(db.startTime);
download({Athlete athlete}) async {
int statusCode = await FitDownload.byId(
id: db.stravaId.toString(),
athlete: athlete,
);
await dbActivity.save();
this.state = "downloaded";
print("Download status code $statusCode.");
notifyListeners();
}
static Activity of(BuildContext context) => ScopedModel.of<Activity>(context);
static queryStrava() async {
Strava strava = Strava(true, secret);
final prompt = 'auto';
......@@ -67,15 +52,20 @@ class Activity extends Model {
prompt);
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
final startDate = now - 20 * 86400;
List<SummaryActivity> summaryActivities =
List<StravaActivity.SummaryActivity> summaryActivities =
await strava.getLoggedInAthleteActivities(now, startDate);
for (SummaryActivity summaryActivity in summaryActivities) {
Activity activity = Activity.fromStrava(summaryActivity);
activity.persist();
for (StravaActivity.SummaryActivity summaryActivity in summaryActivities) {
Activity.fromStrava(summaryActivity).db.save;
}
}
static Future<List<Activity>> all() async {
List<DbActivity> dbActivityList = await DbActivity().select().toList();
return dbActivityList
.map((dbActivity) => Activity.fromDb(dbActivity))
.toList();
}
}
import 'package:flutter/material.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:encrateia/utils/db.dart';
import 'package:encrateia/model/model.dart';
import 'package:strava_flutter/Models/detailedAthlete.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class Athlete extends Model {
int id;
String firstName;
String lastName;
class Athlete extends ChangeNotifier {
String state = "undefined";
String stravaUsername;
String photoPath;
int stravaId;
String email;
String password;
DbAthlete db;
Athlete();
String toString() => '$firstName $lastName ($stravaId)';
Athlete.fromDb(DbAthlete dbAthlete) {
this..db = dbAthlete
..state = "fromDatabase";
}
String toString() => '$db.firstName $db.lastName ($db.stravaId)';
updateFromStravaAthlete(DetailedAthlete athlete) {
firstName = athlete.firstname;
lastName = athlete.lastname;
db.firstName = athlete.firstname;
db.lastName = athlete.lastname;
db.stravaId = athlete.id;
db.stravaUsername = athlete.username;
db.photoPath = athlete.profile;
state = athlete.state;
stravaId = athlete.id;
stravaUsername = athlete.username;
photoPath = athlete.profile;
state = "unsaved";
notifyListeners();
}
......@@ -42,17 +43,20 @@ class Athlete extends Model {
return text;
}
persist() async {
await Db.create().connect();
store_credentials() async {
final storage = new FlutterSecureStorage();
await storage.write(key: "email", value: email);
await storage.write(key: "password", value: password);
}
var dbAthlete = DbAthlete(
firstName: firstName,
lastName: lastName,
stravaId: stravaId,
stravaUsername: stravaUsername,
photoPath: photoPath);
await dbAthlete.save();
Future<Athlete> read_credentials() async {
final storage = new FlutterSecureStorage();
email = await storage.read(key: "email");
password = await storage.read(key: "password");
}
static Athlete of(BuildContext context) => ScopedModel.of<Athlete>(context);
static Future<List<Athlete>> all() async {
List<DbAthlete> dbAthleteList = await DbAthlete().select().toList();
return dbAthleteList.map((dbAthlete) => Athlete.fromDb(dbAthlete)).toList();
}
}
import 'dart:io';
import 'package:http/http.dart' as http;
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:dio_cookie_manager/dio_cookie_manager.dart';
import 'package:cookie_jar/cookie_jar.dart';
import 'package:path_provider/path_provider.dart';
import 'package:encrateia/models/athlete.dart';
import 'package:html/parser.dart' show parse;
abstract class FitDownload {
static byId(String id) async {
static Future<int> byId({String id, Athlete athlete}) async {
String baseUri = "https://www.strava.com/";
String loginUri = baseUri + "login";
String sessionUri = baseUri + "session";
String dashboardUri = baseUri + "dashboard";
String exportUri = baseUri + 'activities/$id/export_original';
Directory appDocDir = await getApplicationDocumentsDirectory();
final uri = 'https://www.strava.com/activities/#{id}/export_original';
var rep = await http.get(uri);
var dio = Dio();
var cookieJar = CookieJar();
dio.interceptors.add(CookieManager(cookieJar));
if (rep.statusCode == 200) {
final file = await _localFile(id);
file.writeAsString(rep.body);
Map<String, String> headers = {
Headers.acceptHeader:
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
};
return rep.contentLength.toString() + " Bytes written";
} else {
return rep.statusCode.toString() + rep.reasonPhrase;
}
}
var homePageResponse = await dio.get(
loginUri,
options: Options(headers: headers),
);
var document = parse(homePageResponse.data);
var csrfParam =
document.querySelector('meta[name="csrf-param"]').attributes["content"];
var csrfToken =
document.querySelector('meta[name="csrf-token"]').attributes["content"];
Map<String, String> postData = {
"email": athlete.email,
"password": athlete.password,
"remember_me": "on",
csrfParam: csrfToken,
};
await dio.post(
sessionUri,
options: Options(headers: headers, validateStatus: (int status) => true),
data: postData,
);
static Future<File> _localFile(id) async {
final directory = await getApplicationDocumentsDirectory();
File file = File(directory.path + id + '.fit');
return file;
await dio.get(
dashboardUri,
options: Options(headers: headers, validateStatus: (int status) => true),
);
print("Started Download for $exportUri");
var downloadResponse =
await dio.download(exportUri, appDocDir.path + '/$id.fit');
print("Downloaded fit file for activity $id.");
return downloadResponse.statusCode;
}
}
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'edit_athlete.dart';
import 'package:scoped_model/scoped_model.dart';
import 'package:encrateia/models/athlete.dart';
import 'package:encrateia/model/model.dart';
import 'package:encrateia/utils/db.dart';
import 'list_activities_screen.dart';
class Dashboard extends StatefulWidget {
......@@ -15,12 +12,11 @@ class Dashboard extends StatefulWidget {
}
class _DashboardState extends State<Dashboard> {
Future<List<DbAthlete>> athletes;
Future<List<Athlete>> athletes;
@override
void initState() {
Db.create().connect();
athletes = DbAthlete().select().toList();
athletes = Athlete.all();
super.initState();
}
......@@ -28,7 +24,7 @@ class _DashboardState extends State<Dashboard> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Encrateia Dashboard")),
body: FutureBuilder<List<DbAthlete>>(
body: FutureBuilder<List<Athlete>>(
future: athletes,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
......@@ -81,9 +77,8 @@ class _DashboardState extends State<Dashboard> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ScopedModel<Athlete>(
model: Athlete(),
child: EditAthleteScreen(),
builder: (context) => EditAthleteScreen(
athlete: Athlete(),
),
),
);
......@@ -99,26 +94,42 @@ class _DashboardState extends State<Dashboard> {
return ListView(
padding: EdgeInsets.all(40),
children: <Widget>[
Text(
"Select the athlete to analyze:",
style: Theme.of(context).textTheme.title,
),
for (DbAthlete athlete in snapshot.data)
for (Athlete athlete in snapshot.data)
ListTile(
leading: Image.network(athlete.photoPath),
title: Text("${athlete.firstName} ${athlete.lastName}"),
subtitle: Text("${athlete.stravaId}"),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ListActivitiesScreen(
athlete: athlete,
),
leading: Image.network(athlete.db.photoPath),
title: Text(
"${athlete.db.firstName} ${athlete.db.lastName} - ${athlete.db.stravaId}"),
subtitle: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
RaisedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ListActivitiesScreen(
athlete: athlete,
),
),
);
},
child: Text("Analyze"),
),
);
},
)
RaisedButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) {
athlete.read_credentials();
return EditAthleteScreen(athlete: athlete);
}),
);
},
child: Icon(Icons.edit),
),
],
),
),
],
);
}
......
import 'package:flutter/material.dart';
import 'package:encrateia/models/athlete.dart';
import 'package:provider/provider.dart';
import 'strava/strava_get_user.dart';
import 'package:scoped_model/scoped_model.dart';
class EditAthleteScreen extends StatelessWidget {
final Athlete athlete;
const EditAthleteScreen({
Key key,
this.athlete,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Create Athlete'),
),
body: ScopedModel<Athlete>(
model: Athlete(),
child: ScopedModelDescendant<Athlete>(
builder: (context, _, athlete) => ListView(
padding: EdgeInsets.all(20),
children: <Widget>[
ListTile(
leading: Text("First Name"),
title: Text(athlete?.firstName ?? ""),
),
ListTile(
leading: Text("Last Name"),
title: Text(athlete?.lastName ?? ""),
),
ListTile(
leading: Text("Strava ID"),
title: Text(athlete?.stravaId.toString() ?? ""),
),
ListTile(
leading: Text("Strava Username"),
title: Text(athlete?.stravaUsername ?? ""),
),
appBar: AppBar(
title: Text('Create Athlete'),
),
body: ChangeNotifierProvider.value(
value: athlete,
child: Consumer<Athlete>(
builder: (context, athlete, _child) => ListView(
padding: EdgeInsets.all(20),
children: <Widget>[
ListTile(
leading: Text("First Name"),
title: Text(athlete.db.firstName ?? ""),
),
ListTile(
leading: Text("Last Name"),
title: Text(athlete.db.lastName ?? ""),
),
ListTile(
leading: Text("Strava ID"),
title: Text(athlete.db.stravaId.toString() ?? ""),
),
ListTile(
leading: Text("Strava Username"),
title: Text(athlete.db.stravaUsername ?? ""),
),
TextFormField(
decoration: InputDecoration(labelText: "Email"),
initialValue: athlete.email,
onChanged: (value) => athlete.email = value,
),
TextFormField(
decoration: InputDecoration(labelText: "Password"),
onChanged: (value) => athlete.password = value,
initialValue: athlete.password,
obscureText: true,
),
// Strava Connection Card
// Strava Connection Card
if (athlete.db.stravaId == null)
Card(
child: Column(
mainAxisSize: MainAxisSize.min,
......@@ -66,31 +85,36 @@ class EditAthleteScreen extends StatelessWidget {
),
),
// Cancel and Save Card
Padding(
padding: EdgeInsets.all(15),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
RaisedButton(
color: Theme.of(context).primaryColorDark,
textColor: Theme.of(context).primaryColorLight,
child: Text('Cancel', textScaleFactor: 1.5),
onPressed: null,
),
Container(width: 20.0),
RaisedButton(
color: Theme.of(context).primaryColorDark,
textColor: Theme.of(context).primaryColorLight,
child: Text('Save', textScaleFactor: 1.5),
onPressed: athlete.persist,
),
],
),
// Cancel and Save Card
Padding(
padding: EdgeInsets.all(15),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
RaisedButton(
color: Theme.of(context).primaryColorDark,
textColor: Theme.of(context).primaryColorLight,
child: Text('Cancel', textScaleFactor: 1.5),
onPressed: () => Navigator.of(context).pop(),
),
Container(width: 20.0),
RaisedButton(
color: Theme.of(context).primaryColorDark,
textColor: Theme.of(context).primaryColorLight,
child: Text('Save', textScaleFactor: 1.5),
onPressed: () {
athlete.db.save();
athlete.store_credentials();
Navigator.of(context).pop();
},
),
],
),
],
),
),
],
),
));
),
),
);
}
}
import 'package:encrateia/models/activity.dart';
import 'package:encrateia/models/athlete.dart';
import 'package:flutter/material.dart';
import 'package:encrateia/model/model.dart';
import 'package:encrateia/utils/db.dart';
class ListActivitiesScreen extends StatefulWidget {
final DbAthlete athlete;
ListActivitiesScreen({this.athlete});
final Athlete athlete;
const ListActivitiesScreen({
Key key,
this.athlete,
}) : super(key: key);
@override
_ListActivitiesScreenState createState() => _ListActivitiesScreenState();
}
class _ListActivitiesScreenState extends State<ListActivitiesScreen> {
Future<List<DbActivity>> activities;
Future<List<Activity>> activities;
@override
void initState() {
Db.create().connect();
activities = DbActivity().select().toList();
super.initState();
activities = Activity.all();
}
@override
......@@ -27,12 +29,12 @@ class _ListActivitiesScreenState extends State<ListActivitiesScreen> {
appBar: AppBar(
title: Text('Activities'),
),
body: FutureBuilder<List<DbActivity>>(
body: FutureBuilder<List<Activity>>(
future: activities,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done &&
snapshot.data.length > 0) {
final activities = snapshot.data.map((dbActivity) => Activity.fromDb(dbActivity));
final activities = snapshot.data.map((activity) => activity);
return ListView(
padding: EdgeInsets.all(20),
children: <Widget>[
......@@ -48,10 +50,10 @@ class _ListActivitiesScreenState extends State<ListActivitiesScreen> {
for (Activity activity in activities)
ListTile(
leading: Icon(Icons.directions_run),
title: Text("${activity.type} "
"${activity.stravaId}"),
subtitle: Text(activity.name),
trailing: stateIcon(activity),
title: Text("${activity.db.type} "
"${activity.db.stravaId}"),
subtitle: Text(activity.db.name ?? "Activity"),
trailing: stateIcon(activity, widget.athlete),
)
],
);
......@@ -76,16 +78,32 @@ class _ListActivitiesScreenState extends State<ListActivitiesScreen> {
);
}
static