16 Commits

Author SHA1 Message Date
de22d62432 migrating... 2025-04-15 15:29:55 -04:00
bdc4fee8c2 Merge pull request 'edited tones, added vibrate' (#8) from action-things into main
Reviewed-on: #8
2025-02-11 08:41:00 -08:00
8dcffcca11 Merge branch 'main' into action-things 2025-02-11 08:40:40 -08:00
2eda797375 Merge pull request 'action-things' (#7) from action-things into main
Reviewed-on: #7
2025-02-11 07:34:18 -08:00
9f5fb0d1ad edited tones, added vibrate 2025-02-11 10:32:40 -05:00
23663f484b added sound during countdown, and upgraded minsdkversion 2025-02-10 17:08:13 -05:00
60bc571987 action display, and full display, started action editor 2025-01-24 16:15:22 -05:00
0cf62ec4b4 add new db fields to action 2025-01-07 18:41:50 -05:00
2288cba78e actvity card spacing 2025-01-07 15:56:51 -05:00
1027439848 remove activity junk 2025-01-07 15:55:30 -05:00
6012a1541e Merge pull request 'activity things' (#6) from activity-things into main
Reviewed-on: #6
2025-01-07 12:50:19 -08:00
d9a5599a4b activity categories ui 2025-01-07 15:49:44 -05:00
0b0ca884bc genericizing add card, activity ui tweaks 2025-01-07 12:37:17 -05:00
8ec531c0ea activity read 2025-01-06 16:46:22 -05:00
6917754933 acivity description management, seed data mods 2025-01-06 14:47:14 -05:00
ebca90e69a action prep, activity association removal, activity ui tweaks 2025-01-06 10:10:08 -05:00
78 changed files with 65818 additions and 2271 deletions

Binary file not shown.

BIN
assets/audio/count_tone.mp3 Normal file

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
import 'package:drift/drift.dart';
import 'package:sendtrain/database/database.dart';
part 'action_sets_dao.g.dart';
@DriftAccessor(tables: [ActionSets])
class ActionSetsDao extends DatabaseAccessor<AppDatabase>
with _$ActionSetsDaoMixin {
ActionSetsDao(super.db);
// Future<Session> find(int id) => (select(sessions)..where((session) => session.id.equals(id) )).getSingle();
// Stream<Session> watchSession(int id) => (select(sessions)..where((session) => session.id.equals(id) )).watchSingle();
// Future<List<Session>> all() => select(sessions).get();
// Stream<List<Session>> watch() => select(sessions).watch();
// Future createOrUpdate(SessionsCompanion session) => into(sessions).insertOnConflictUpdate(session);
// Future replace(Session session) => update(sessions).replace(session);
// Future remove(Session session) => delete(sessions).delete(session);
Future<List<ActionSet>> fromSession(Session session) async {
return await (select(actionSets)
..where((actionSet) => actionSet.sessionId.equals(session.id))..orderBy([(t) => OrderingTerm.asc(actionSets.position)]))
.get();
}
}

View File

@ -0,0 +1,10 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'action_sets_dao.dart';
// ignore_for_file: type=lint
mixin _$ActionSetsDaoMixin on DatabaseAccessor<AppDatabase> {
$ActivitiesTable get activities => attachedDatabase.activities;
$SessionsTable get sessions => attachedDatabase.sessions;
$ActionSetsTable get actionSets => attachedDatabase.actionSets;
}

View File

@ -12,10 +12,10 @@ class ActionsDao extends DatabaseAccessor<AppDatabase> with _$ActionsDaoMixin {
} }
Future<Action> find(int id) async { Future<Action> find(int id) async {
return await (select(actions)..where((action) => action.id.equals(id) )).getSingle(); return await (select(actions)..where((action) => action.id.equals(id))).getSingle();
} }
Future<List<Action>> fromActivity(Activity activity) async { Future<List<Action>> fromActivity(Activity activity, Session session) async {
final result = select(db.activityActions).join( final result = select(db.activityActions).join(
[ [
innerJoin( innerJoin(
@ -24,7 +24,8 @@ class ActionsDao extends DatabaseAccessor<AppDatabase> with _$ActionsDaoMixin {
), ),
], ],
) )
..where(db.activityActions.activityId.equals(activity.id)); ..where(db.activityActions.activityId.equals(activity.id))
..where(db.activityActions.sessionId.equals(session.id));
final actions = (await result.get()) final actions = (await result.get())
.map((e) => e.readTable(db.actions)) .map((e) => e.readTable(db.actions))
@ -32,4 +33,32 @@ class ActionsDao extends DatabaseAccessor<AppDatabase> with _$ActionsDaoMixin {
return actions; return actions;
} }
Stream<List<Action>> watchActivityActions(Activity activity, Session session) {
final result = select(db.activityActions).join(
[
innerJoin(
db.actions,
db.actions.id.equalsExp(db.activityActions.actionId),
),
],
)
..where(db.activityActions.activityId.equals(activity.id))
..where(db.activityActions.sessionId.equals(session.id));
// final actions = result.watch().map((rows) {
// return rows.map((row) {
// row.readTable(db.actions);
// }).toList();
// });
final actions = (result.watch()).map((rows) {
return rows.map((row) => row.readTable(db.actions)).toList();
});
return actions;
}
Future createOrUpdate(ActionsCompanion action) => into(actions).insertOnConflictUpdate(action);
Future replace(Action action) => update(actions).replace(action);
} }

View File

@ -12,6 +12,7 @@ class ActivityActionsDao extends DatabaseAccessor<AppDatabase> with _$ActivityAc
Future insert(ActivityAction activityAction) => into(activityActions).insert(activityAction); Future insert(ActivityAction activityAction) => into(activityActions).insert(activityAction);
Future replace(ActivityAction activityAction) => update(activityActions).replace(activityAction); Future replace(ActivityAction activityAction) => update(activityActions).replace(activityAction);
Future remove(ActivityAction activityAction) => delete(activityActions).delete(activityAction); Future remove(ActivityAction activityAction) => delete(activityActions).delete(activityAction);
Future createOrUpdate(ActivityActionsCompanion activityAction) => into(activityActions).insertOnConflictUpdate(activityAction);
// Future<List<ActivityAction>> all() async { // Future<List<ActivityAction>> all() async {
// return await select(activityActions).get(); // return await select(activityActions).get();

View File

@ -6,5 +6,6 @@ part of 'activity_actions_dao.dart';
mixin _$ActivityActionsDaoMixin on DatabaseAccessor<AppDatabase> { mixin _$ActivityActionsDaoMixin on DatabaseAccessor<AppDatabase> {
$ActivitiesTable get activities => attachedDatabase.activities; $ActivitiesTable get activities => attachedDatabase.activities;
$ActionsTable get actions => attachedDatabase.actions; $ActionsTable get actions => attachedDatabase.actions;
$SessionsTable get sessions => attachedDatabase.sessions;
$ActivityActionsTable get activityActions => attachedDatabase.activityActions; $ActivityActionsTable get activityActions => attachedDatabase.activityActions;
} }

View File

@ -4,17 +4,21 @@ import 'package:sendtrain/database/database.dart';
part 'session_activities_dao.g.dart'; part 'session_activities_dao.g.dart';
@DriftAccessor(tables: [SessionActivities]) @DriftAccessor(tables: [SessionActivities])
class SessionActivitiesDao extends DatabaseAccessor<AppDatabase> with _$SessionActivitiesDaoMixin { class SessionActivitiesDao extends DatabaseAccessor<AppDatabase>
with _$SessionActivitiesDaoMixin {
SessionActivitiesDao(super.db); SessionActivitiesDao(super.db);
Future createOrUpdate(SessionActivitiesCompanion sessionActivity) => into(sessionActivities).insertOnConflictUpdate(sessionActivity); Future createOrUpdate(SessionActivitiesCompanion sessionActivity) =>
into(sessionActivities).insertOnConflictUpdate(sessionActivity);
Future<List<SessionActivity>> all() async { Future<List<SessionActivity>> all() async {
return await select(sessionActivities).get(); return await select(sessionActivities).get();
} }
Future<SessionActivity> find(int id) async { Future<SessionActivity> find(int id) async {
return await (select(sessionActivities)..where((sessionActivity) => sessionActivity.id.equals(id) )).getSingle(); return await (select(sessionActivities)
..where((sessionActivity) => sessionActivity.id.equals(id)))
.getSingle();
} }
Future<List<SessionActivity>> fromSessionId(int id) async { Future<List<SessionActivity>> fromSessionId(int id) async {
@ -23,4 +27,16 @@ class SessionActivitiesDao extends DatabaseAccessor<AppDatabase> with _$SessionA
return result.get(); return result.get();
} }
}
Future remove(SessionActivity sessionActivity) =>
delete(sessionActivities).delete(sessionActivity);
Future removeAssociation(int activityId, int sessionId) {
return (delete(sessionActivities)
..where((t) =>
t.sessionId.equals(sessionId) & t.activityId.equals(activityId)))
.go();
}
}
// return (delete(todos)..where((t) => t.id.isSmallerThanValue(10))).go();

View File

@ -7,6 +7,7 @@ import 'package:sendtrain/daos/media_items_dao.dart';
import 'package:sendtrain/daos/object_media_items_dao.dart'; import 'package:sendtrain/daos/object_media_items_dao.dart';
import 'package:sendtrain/daos/session_activities_dao.dart'; import 'package:sendtrain/daos/session_activities_dao.dart';
import 'package:sendtrain/daos/sessions_dao.dart'; import 'package:sendtrain/daos/sessions_dao.dart';
import 'package:sendtrain/daos/action_sets_dao.dart';
import 'package:sendtrain/database/seed.dart'; import 'package:sendtrain/database/seed.dart';
part 'database.g.dart'; part 'database.g.dart';
@ -14,6 +15,8 @@ part 'database.g.dart';
@DriftDatabase(tables: [ @DriftDatabase(tables: [
Sessions, Sessions,
SessionActivities, SessionActivities,
// SessionSets,
ActionSets,
Activities, Activities,
ActivityActions, ActivityActions,
Actions, Actions,
@ -25,6 +28,8 @@ part 'database.g.dart';
MediaItemsDao, MediaItemsDao,
ObjectMediaItemsDao, ObjectMediaItemsDao,
SessionActivitiesDao, SessionActivitiesDao,
// SessionSetsDao,
ActionSetsDao,
ActivityActionsDao, ActivityActionsDao,
ActionsDao ActionsDao
]) ])
@ -35,7 +40,7 @@ class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection()); AppDatabase() : super(_openConnection());
@override @override
int get schemaVersion => 19; int get schemaVersion => 40;
@override @override
MigrationStrategy get migration { MigrationStrategy get migration {
@ -73,6 +78,26 @@ class Sessions extends Table {
dateTime().withDefault(Variable(DateTime.now()))(); dateTime().withDefault(Variable(DateTime.now()))();
} }
class ActionSets extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get name => text().withLength(min: 3, max: 32)();
IntColumn get reps => integer()();
IntColumn get activityId =>
integer().references(Activities, #id, onDelete: KeyAction.cascade)();
IntColumn get restBeforeSet => integer().nullable()();
IntColumn get restBetweenReps => integer().nullable()();
IntColumn get restAfterSet => integer().nullable()();
TextColumn get repType => textEnum<RepType>()();
IntColumn get repLength => integer().nullable()();
TextColumn get setWeights => text().nullable()();
BoolColumn get isAlternating => boolean().withDefault(Variable(false))();
TextColumn get tempo => text().withLength(min: 6, max: 36).nullable()();
IntColumn get sessionId => integer().references(Sessions, #id, onDelete: KeyAction.cascade)();
IntColumn get position => integer()();
DateTimeColumn get createdAt =>
dateTime().withDefault(Variable(DateTime.now()))();
}
class SessionActivities extends Table { class SessionActivities extends Table {
IntColumn get id => integer().autoIncrement()(); IntColumn get id => integer().autoIncrement()();
IntColumn get sessionId => IntColumn get sessionId =>
@ -85,6 +110,16 @@ class SessionActivities extends Table {
dateTime().withDefault(Variable(DateTime.now()))(); dateTime().withDefault(Variable(DateTime.now()))();
} }
// class SessionSets extends Table {
// IntColumn get id => integer().autoIncrement()();
// IntColumn get setId =>
// integer().references(ActionSets, #id, onDelete: KeyAction.cascade)();
// IntColumn get sessionId => integer().references(Sessions, #id, onDelete: KeyAction.cascade)();
// IntColumn get position => integer()();
// DateTimeColumn get createdAt =>
// dateTime().withDefault(Variable(DateTime.now()))();
// }
enum ActivityCategories { fundamentals, conditioning, advanced, custom, pro } enum ActivityCategories { fundamentals, conditioning, advanced, custom, pro }
enum ActivityType { enum ActivityType {
@ -113,7 +148,8 @@ enum ActivityEquipment {
bands, bands,
medicineBall, medicineBall,
exerciseBall, exerciseBall,
eZCurlBar eZCurlBar,
crimpBlock,
} }
enum ActivityMuscle { enum ActivityMuscle {
@ -155,19 +191,39 @@ class Activities extends Table {
class ActivityActions extends Table { class ActivityActions extends Table {
IntColumn get id => integer().autoIncrement()(); IntColumn get id => integer().autoIncrement()();
IntColumn get activityId => IntColumn get activityId => integer().references(Activities, #id, onDelete: KeyAction.cascade)();
integer().references(Activities, #id, onDelete: KeyAction.cascade)();
IntColumn get actionId => IntColumn get actionId =>
integer().references(Actions, #id, onDelete: KeyAction.cascade)(); integer().references(Actions, #id, onDelete: KeyAction.cascade)();
IntColumn get sessionId => integer().references(Sessions, #id, onDelete: KeyAction.cascade)();
IntColumn get position => integer()(); IntColumn get position => integer()();
DateTimeColumn get createdAt => DateTimeColumn get createdAt =>
dateTime().withDefault(Variable(DateTime.now()))(); dateTime().withDefault(Variable(DateTime.now()))();
} }
enum RepType { time, count }
enum ActionStatus { pending, started, paused, complete }
class Actions extends Table { class Actions extends Table {
IntColumn get id => integer().autoIncrement()(); IntColumn get id => integer().autoIncrement()();
TextColumn get title => text().withLength(min: 3, max: 32)(); TextColumn get title => text().withLength(min: 3, max: 64)();
TextColumn get description => text().named('body')(); TextColumn get description => text().named('body')();
IntColumn get totalSets => integer()();
TextColumn get totalReps => text().withLength(min: 1, max: 32)();
IntColumn get restBeforeSets => integer().nullable()();
IntColumn get restBetweenSets => integer().nullable()();
IntColumn get restBetweenReps => integer().nullable()();
IntColumn get restAfterSets => integer().nullable()();
TextColumn get repType => textEnum<RepType>()();
IntColumn get repLength => integer().nullable()();
TextColumn get repWeights => text().nullable()();
TextColumn get setWeights => text().nullable()();
BoolColumn get isAlternating => boolean().withDefault(Variable(false))();
TextColumn get tempo => text().withLength(min: 6, max: 36).nullable()();
TextColumn get status =>
textEnum<ActionStatus>().withDefault(Variable('pending'))();
TextColumn get state => text().withDefault(Variable(
"{\"currentSet\": 0, \"currentRep\": 0, \"currentActionType\": 0, \"currentTime\": 0, \"currentAction\": 0}"))();
TextColumn get set => text()(); TextColumn get set => text()();
DateTimeColumn get createdAt => DateTimeColumn get createdAt =>
dateTime().withDefault(Variable(DateTime.now()))(); dateTime().withDefault(Variable(DateTime.now()))();
@ -193,7 +249,7 @@ enum MediaType { youtube, image, location, localImage, localVideo }
class MediaItems extends Table { class MediaItems extends Table {
IntColumn get id => integer().autoIncrement()(); IntColumn get id => integer().autoIncrement()();
TextColumn get title => text().withLength(min: 3, max: 32)(); TextColumn get title => text().withLength(min: 3, max: 64)();
TextColumn get description => text().named('body')(); TextColumn get description => text().named('body')();
TextColumn get reference => text()(); TextColumn get reference => text()();
TextColumn get type => textEnum<MediaType>()(); TextColumn get type => textEnum<MediaType>()();

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -43,17 +43,17 @@ Future<void> seedDb(AppDatabase database) async {
['BgheYcxhrsw', MediaType.youtube] ['BgheYcxhrsw', MediaType.youtube]
]; ];
final List<String> actionTypes = [ // final List<String> actionTypes = [
"[[{\"actionID\": 0, \"name\": \"1, 3, 5\", \"type\": \"repititions\", \"amount\": 1, \"weight\": 0}, {\"actionID\": 1, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 300}], [{\"actionID\": 2, \"name\": \"1, 3, 5\", \"type\": \"repititions\", \"amount\": 1, \"weight\": 0}, {\"actionID\": 3, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 300}], [{\"actionID\": 4, \"name\": \"1, 3, 5\", \"type\": \"repititions\", \"amount\": 1, \"weight\": 0}, {\"actionID\": 5, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 300}]]", // "[[{\"actionID\": 0, \"name\": \"1, 3, 5\", \"type\": \"repititions\", \"amount\": 1, \"weight\": 0}, {\"actionID\": 1, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 300}], [{\"actionID\": 2, \"name\": \"1, 3, 5\", \"type\": \"repititions\", \"amount\": 1, \"weight\": 0}, {\"actionID\": 3, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 300}], [{\"actionID\": 4, \"name\": \"1, 3, 5\", \"type\": \"repititions\", \"amount\": 1, \"weight\": 0}, {\"actionID\": 5, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 300}]]",
"[[{\"actionID\": 0, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weight\": 80}, {\"actionID\": 1, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 5}, {\"actionID\": 2, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weights\": 80}, {\"actionID\": 3, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 300}], [{\"actionID\": 4, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weight\": 80}, {\"actionID\": 5, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 5}, {\"actionID\": 6, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weights\": 80}, {\"actionID\": 7, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 300}], [{\"actionID\": 8, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weight\": 80}, {\"actionID\": 9, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 5}, {\"actionID\": 10, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weights\": 80}, {\"actionID\": 11, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 300}], [{\"actionID\": 12, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weight\": 80}, {\"actionID\": 13, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 5}, {\"actionID\": 14, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weights\": 80}, {\"actionID\": 15, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 300}], [{\"actionID\": 16, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weight\": 80}, {\"actionID\": 17, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 5}, {\"actionID\": 18, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weights\": 80}, {\"actionID\": 19, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 300}]]" // "[[{\"actionID\": 0, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weight\": 80}, {\"actionID\": 1, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 5}, {\"actionID\": 2, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weights\": 80}, {\"actionID\": 3, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 300}], [{\"actionID\": 4, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weight\": 80}, {\"actionID\": 5, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 5}, {\"actionID\": 6, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weights\": 80}, {\"actionID\": 7, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 300}], [{\"actionID\": 8, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weight\": 80}, {\"actionID\": 9, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 5}, {\"actionID\": 10, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weights\": 80}, {\"actionID\": 11, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 300}], [{\"actionID\": 12, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weight\": 80}, {\"actionID\": 13, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 5}, {\"actionID\": 14, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weights\": 80}, {\"actionID\": 15, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 300}], [{\"actionID\": 16, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weight\": 80}, {\"actionID\": 17, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 5}, {\"actionID\": 18, \"name\": \"Long Pulls\", \"type\": \"seconds\", \"amount\": 5, \"weights\": 80}, {\"actionID\": 19, \"name\": \"Rest\", \"type\": \"seconds\", \"amount\": 300}]]"
]; // ];
final int totalSessions = 15; final int totalSessions = 15;
final int totalActivities = 6; final int totalActivities = 6;
final int totalActions = 5; // final int totalActions = 5;
final int totalMedia = 5; final int totalMedia = 5;
final random = Random(); final random = Random();
final whitespaceRE = RegExp(r"(?! )\s+| \s+");
// we gotta build all the activities! // we gotta build all the activities!
final jsondata = final jsondata =
await root_bundle.rootBundle.loadString('assets/exercises.json'); await root_bundle.rootBundle.loadString('assets/exercises.json');
@ -66,14 +66,15 @@ Future<void> seedDb(AppDatabase database) async {
if (exercise['images'] != null) { if (exercise['images'] != null) {
for (int j = 0; j < exercise['images'].length; j++) { for (int j = 0; j < exercise['images'].length; j++) {
var image = exercise['images'][j]; var image = exercise['images'][j];
images.add( images.add(image);
"https://raw.githubusercontent.com/yuhonas/free-exercise-db/main/exercises/$image");
} }
} }
Map<Symbol, Value> payload = { Map<Symbol, Value> payload = {
Symbol('title'): Value<String>(exercise['name']), Symbol('title'): Value<String>(
Symbol('description'): Value<String>(exercise['instructions'].toString()), exercise['name'].toString().trim().replaceAll(whitespaceRE, " ")),
Symbol('description'):
Value<String>(json.encode(exercise['instructions'])),
Symbol('force'): Value<String>(exercise['force'] ?? "") Symbol('force'): Value<String>(exercise['force'] ?? "")
}; };
@ -124,9 +125,8 @@ Future<void> seedDb(AppDatabase database) async {
await database await database
.into(database.mediaItems) .into(database.mediaItems)
.insert(MediaItemsCompanion.insert( .insert(MediaItemsCompanion.insert(
title: 'Media title $m', title: exercise['name'],
description: description: exercise['name'],
'Media description $m Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.',
reference: mediaItem, reference: mediaItem,
type: MediaType.image)) type: MediaType.image))
.then((mediaId) async { .then((mediaId) async {
@ -160,37 +160,100 @@ Future<void> seedDb(AppDatabase database) async {
"[\"achievement 1\", \"achievement 2\", \"achievement 3\"]"), "[\"achievement 1\", \"achievement 2\", \"achievement 3\"]"),
date: Value(DateTime.now()))) date: Value(DateTime.now())))
.then((sessionId) async { .then((sessionId) async {
// activities things //session actions
for (int j = 0; j <= random.nextInt(totalActivities); j++) { int activityId = random.nextInt(activityIds.length);
int activityId = random.nextInt(activityIds.length);
activityIds.removeAt(activityId);
await database for (int i = 0; i < 5; i += 1) {
.into(database.sessionActivities) int restBefore = 0;
.insert(SessionActivitiesCompanion.insert( int restAfter = 300000;
sessionId: sessionId,
activityId: activityId,
position: j,
results: Value("results json, will need to test"),
));
// actions if (i == 0) {
for (int k = 0; k <= random.nextInt(totalActions); k++) { restBefore = 30000;
await database
.into(database.actions)
.insert(ActionsCompanion.insert(
title: 'Test action $k',
description:
'$k Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.',
set: actionTypes[random.nextInt(actionTypes.length)]))
.then((actionId) async {
// add activity action association
await database.into(database.activityActions).insert(
ActivityActionsCompanion.insert(
activityId: activityId, actionId: actionId, position: k));
});
} }
await database.into(database.actionSets).insert(
ActionSetsCompanion.insert(
name: 'test set',
reps: 5,
activityId: activityId,
repType: RepType.time,
isAlternating: Value(true),
restBeforeSet: Value(restBefore),
restAfterSet: Value(restAfter),
restBetweenReps: Value(10000),
repLength: Value(10000),
setWeights: Value('[100]'),
tempo: Value('[3000,2000,1000]'),
sessionId: sessionId,
position: i));
} }
// SessionSetsCompanion.insert()
// activities things
// for (int j = 0; j <= random.nextInt(totalActivities); j++) {
// int activityId = random.nextInt(activityIds.length);
// activityIds.removeAt(activityId);
// await database
// .into(database.sessionActivities)
// .insert(SessionActivitiesCompanion.insert(
// sessionId: sessionId,
// activityId: activityId,
// position: j,
// results: Value("results json, will need to test"),
// ));
// // actions
// // await database
// // .into(database.actions)
// // .insert(ActionsCompanion.insert(
// // title: 'Test action',
// // description:
// // 'Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.',
// // totalSets: 5,
// // totalReps: "[1]",
// // restBeforeSets: Value(30000),
// // restBetweenSets: Value(300000),
// // restBetweenReps: Value(15000),
// // restAfterSets: Value(300000),
// // repType: RepType.time,
// // repLength: Value(10000),
// // repWeights: Value("[110]"),
// // setWeights: Value("[1]"),
// // isAlternating: Value(true),
// // set: actionTypes[random.nextInt(actionTypes.length)]))
// // .then((actionId) async {
// // // add activity action association
// // await database.into(database.activityActions).insert(
// // ActivityActionsCompanion.insert(
// // activityId: activityId, actionId: actionId, sessionId: sessionId, position: 0));
// // });
// // for (int k = 0; k <= random.nextInt(totalActions); k++) {
// // await database
// // .into(database.actions)
// // .insert(ActionsCompanion.insert(
// // title: 'Test action $k',
// // description:
// // '$k Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.',
// // totalSets: 5,
// // totalReps: "[1]",
// // restBeforeSets: Value(30000),
// // restBetweenSets: Value(300000),
// // restBetweenReps: Value(15000),
// // restAfterSets: Value(300000),
// // repType: RepType.time,
// // repLength: Value(10000),
// // repWeights: Value("[110]"),
// // setWeights: Value("[1]"),
// // isAlternating: Value(true),
// // set: actionTypes[random.nextInt(actionTypes.length)]))
// // .then((actionId) async {
// // // add activity action association
// // await database.into(database.activityActions).insert(
// // ActivityActionsCompanion.insert(
// // activityId: activityId, actionId: actionId, position: k));
// // });
// // }
// }
for (int n = 0; n <= random.nextInt(totalMedia); n++) { for (int n = 0; n <= random.nextInt(totalMedia); n++) {
final mediaItem = mediaItems[random.nextInt(mediaItems.length)]; final mediaItem = mediaItems[random.nextInt(mediaItems.length)];

View File

@ -9,3 +9,8 @@ String formattedTime(int timeInSecond) {
String second = sec.toString().length <= 1 ? "0$sec" : "$sec"; String second = sec.toString().length <= 1 ? "0$sec" : "$sec";
return "$minute:$second"; return "$minute:$second";
} }
int toSeconds(int milliseconds) {
int sec = (milliseconds / 1000).floor();
return sec;
}

View File

@ -6,7 +6,23 @@ ImageProvider findMediaByType(List<MediaItem> media, MediaType type) {
Image image; Image image;
if (found.isNotEmpty) { if (found.isNotEmpty) {
image = Image.network(found.first.reference); image = Image.network('https://test.com/image.jpg', loadingBuilder:
(BuildContext context, Widget child, ImageChunkEvent? loadingProgress) {
print('loading');
print(loadingProgress);
if (loadingProgress == null) return child;
return Center(
child: CircularProgressIndicator(
value: loadingProgress.expectedTotalBytes != null
? loadingProgress.cumulativeBytesLoaded /
loadingProgress.expectedTotalBytes!
: null,
),
);
}, errorBuilder: (context, error, stackTrace) {
print('error');
return Image.asset('assets/images/placeholder.jpg');
});
} else { } else {
// Element is not found // Element is not found
image = Image.asset('assets/images/placeholder.jpg'); image = Image.asset('assets/images/placeholder.jpg');

View File

@ -6,10 +6,15 @@ showMediaDetailWidget(BuildContext context, MediaItem media) {
showEditorSheet(context, MediaDetails(media: media)); showEditorSheet(context, MediaDetails(media: media));
} }
showEditorSheet(BuildContext context, Widget widget) { showGenericSheet(BuildContext context, Widget widget,
[Color? backgroundColor]) {
backgroundColor ??= Theme.of(context).colorScheme.surfaceBright;
showModalBottomSheet<void>( showModalBottomSheet<void>(
backgroundColor: backgroundColor,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(topLeft: Radius.circular(10.0), topRight: Radius.circular(10.0)), borderRadius: BorderRadius.only(
topLeft: Radius.circular(10.0), topRight: Radius.circular(10.0)),
), ),
context: context, context: context,
showDragHandle: true, showDragHandle: true,
@ -19,3 +24,39 @@ showEditorSheet(BuildContext context, Widget widget) {
return widget; return widget;
}); });
} }
showEditorSheet(BuildContext context, Widget widget) {
showGenericSheet(context, widget);
}
String jsonToDescription(List text) {
String content = '';
for (int i = 0; i < text.length; i++) {
if (content.isEmpty) {
content = text[i];
} else {
content = "$content\n\n${text[i]}";
}
}
return content;
}
Widget formItemWrapper(Widget content,
[EdgeInsets padding = const EdgeInsets.fromLTRB(0, 0, 0, 0)]) {
return Expanded(child: Padding(padding: padding, child: content));
}
List<DropdownMenuEntry> numericDropDownItems(String type, int itemLimit) {
final List<DropdownMenuEntry> items = [];
// String entryName = type;
for (int i = 0; i < itemLimit; i++) {
// if (i != 0) entryName = "${type}s";
items.add(DropdownMenuEntry(value: i + 1, label: "${i + 1}"));
}
return items;
}

View File

@ -3,7 +3,8 @@ import 'package:provider/provider.dart';
import 'package:sendtrain/database/database.dart'; import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/helpers/widget_helpers.dart'; import 'package:sendtrain/helpers/widget_helpers.dart';
import 'package:sendtrain/models/activity_timer_model.dart'; import 'package:sendtrain/models/activity_timer_model.dart';
import 'package:sendtrain/widgets/screens/activities_screen.dart'; import 'package:sendtrain/providers/action_timer.dart';
// import 'package:sendtrain/widgets/screens/activities_screen.dart';
import 'package:sendtrain/widgets/screens/sessions_screen.dart'; import 'package:sendtrain/widgets/screens/sessions_screen.dart';
// ignore: unused_import // ignore: unused_import
import 'package:sendtrain/database/seed.dart'; import 'package:sendtrain/database/seed.dart';
@ -65,7 +66,7 @@ class _AppState extends State<App> {
padding: const EdgeInsets.fromLTRB(0, 50, 0, 0), padding: const EdgeInsets.fromLTRB(0, 50, 0, 0),
child: <Widget>[ child: <Widget>[
SessionsScreen(), SessionsScreen(),
const ActivitiesScreen(), // const ActivitiesScreen(),
Container( Container(
alignment: Alignment.center, alignment: Alignment.center,
child: const Text('In Progress...'), child: const Text('In Progress...'),
@ -78,6 +79,10 @@ class _AppState extends State<App> {
alignment: Alignment.center, alignment: Alignment.center,
child: const Text('In Progress...'), child: const Text('In Progress...'),
), ),
Container(
alignment: Alignment.center,
child: const Text('Profile in Progress...'),
),
][currentPageIndex]), ][currentPageIndex]),
bottomNavigationBar: NavigationBar( bottomNavigationBar: NavigationBar(
onDestinationSelected: (int index) { onDestinationSelected: (int index) {
@ -89,15 +94,17 @@ class _AppState extends State<App> {
destinations: const <Widget>[ destinations: const <Widget>[
NavigationDestination( NavigationDestination(
icon: Icon(Icons.sports), label: "Sessions"), icon: Icon(Icons.sports), label: "Sessions"),
NavigationDestination( // NavigationDestination(
icon: Icon(Icons.sports_gymnastics_rounded), // icon: Icon(Icons.sports_gymnastics_rounded),
label: "Activities"), // label: "Activities"),
NavigationDestination( NavigationDestination(
icon: Icon(Icons.calendar_month_rounded), label: "Plan"), icon: Icon(Icons.calendar_month_rounded), label: "Plan"),
NavigationDestination( NavigationDestination(
icon: Icon(Icons.group), label: "Team Send"), icon: Icon(Icons.group), label: "Team Send"),
NavigationDestination( NavigationDestination(
icon: Icon(Icons.analytics), label: "Progress") icon: Icon(Icons.analytics), label: "Progress"),
NavigationDestination(
icon: Icon(Icons.account_circle_rounded), label: "Profile"),
]), ]),
floatingActionButton: FloatingActionButton.extended( floatingActionButton: FloatingActionButton.extended(
onPressed: () { onPressed: () {
@ -111,12 +118,13 @@ class _AppState extends State<App> {
} }
void main() { void main() {
var db = AppDatabase();
runApp(MultiProvider( runApp(MultiProvider(
providers: [ providers: [
ChangeNotifierProvider(create: (context) => ActivityTimerModel()),
Provider<AppDatabase>( Provider<AppDatabase>(
create: (context) => AppDatabase(), create: (context) => db, dispose: (context, db) => db.close()),
dispose: (context, db) => db.close()), ChangeNotifierProvider(create: (context) => ActivityTimerModel()),
ChangeNotifierProvider(create: (context) => ActionTimer()),
], ],
child: const SendTrain(), child: const SendTrain(),
)); ));

View File

@ -0,0 +1,260 @@
import 'dart:convert';
import 'package:sendtrain/daos/actions_dao.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/helpers/date_time_helpers.dart';
class ActionModel {
final ActionsDao dao;
List<Item> items;
Action action;
ActionModel({required this.action, required AppDatabase db})
: dao = ActionsDao(db),
items = _generateItems(action);
int get id => action.id;
ActionStatus get status => action.status;
Map get state => json.decode(action.state);
List<Set> get sets => items.whereType<Set>().toList();
List<Item> get allItems => _flattenedItems();
int get totalTime {
int time = 0;
for (int i = 0; i < allItems.length; i++) {
Item item = allItems[i];
time += item.time ?? 0;
}
return toSeconds(time);
}
List<Item> _flattenedItems() {
List<Item> items = [];
for (int i = 0; i < this.items.length; i++) {
Item item = this.items[i];
if (item.runtimeType == Set) {
Set setItem = item as Set;
for (int j = 0; j < setItem.items.length; j++) {
items.add(setItem.items[j]);
}
} else {
items.add(item);
}
}
return items;
}
static List<Item> _generateItems(Action action) {
int totalItems = 0;
int setItems = 0;
List<Item> items = [];
final List setReps = json.decode(action.totalReps);
if (action.restBeforeSets != null) {
items.add(Rest(
id: totalItems,
position: totalItems,
action: action,
time: action.restBeforeSets!,
name: 'prepare'));
}
for (int i = 0; i < action.totalSets; i++) {
final int totalReps;
if (setReps.length == 1) {
totalReps = setReps.first;
} else {
totalReps = setReps[i];
}
totalItems += 1;
items.add(Set(
id: totalItems,
setOrder: setItems++,
position: totalItems,
action: action,
totalReps: totalReps));
if (action.restBetweenSets != null && i < action.totalSets - 1) {
totalItems += 1;
items.add(Rest(
id: totalItems,
position: totalItems,
action: action,
time: action.restBetweenSets!,
name: 'rest'));
}
}
if (action.restAfterSets != null && totalItems != items.length) {
totalItems += 1;
items.add(Rest(
id: totalItems,
position: totalItems,
action: action,
time: action.restAfterSets!,
name: 'cooldown'));
}
return items;
}
Future<Action> updateStatus(ActionStatus status) async {
Action newAction = action.copyWith(id: action.id, status: status);
await dao.createOrUpdate(newAction.toCompanion(true));
action = newAction;
return newAction;
}
Future<Action> updateState(String state) async {
Action newAction = action.copyWith(id: action.id, state: state);
await dao.createOrUpdate(newAction.toCompanion(true));
action = newAction;
return newAction;
}
}
class Item {
final int id;
final Action action;
int position;
List<Item> items = [];
dynamic value;
final String name;
int? parentId;
int? time;
Item(
{required this.id,
required this.position,
required this.action,
this.parentId,
this.time})
: name = action.title;
RepType get valueType => action.repType;
String get humanValueType => valueType == RepType.time ? 'seconds' : 'reps';
}
class Set extends Item {
final int totalReps;
int? setOrder;
Set(
{required super.id,
required super.action,
required super.position,
required this.totalReps,
this.setOrder}) {
items = _generateItems(action, id, totalReps);
}
int? get weightMultiplyer =>
action.setWeights != null ? json.decode(action.setWeights!)[id] : null;
List<Reps> get reps => items.whereType<Reps>().toList();
static List<Item> _generateItems(action, id, totalReps) {
List<Item> items = [];
// add item for exercise
int position = 0;
if (action.repType == RepType.time) {
for (int i = 0; i < totalReps; i++) {
position = position > 0 ? position + 1 : position;
// don't show a rest before first rep
if (i > 0) {
items.add(Rest(
id: position,
position: position,
parentId: id,
action: action,
time: action.restBetweenReps,
name: 'rest'));
}
items.add(Reps(
id: ++position, position: position, parentId: id, action: action));
if (action.isAlternating) {
items.add(Rest(
id: ++position,
position: position,
parentId: id,
action: action,
time: action.restBetweenReps,
name: 'alternate'));
items.add(Reps(
id: ++position,
position: position,
parentId: id,
action: action));
}
}
} else {
items.add(Reps(id: id, position: position, action: action));
if (action.isAlternating) {
items.add(Rest(
id: ++position,
position: position,
parentId: id,
action: action,
time: action.restBetweenReps,
name: 'alternate'));
items.add(Reps(id: id, position: ++position, action: action));
}
}
return items;
}
}
class Reps extends Item {
Reps(
{required super.id,
required super.position,
required super.action,
super.parentId});
@override
dynamic get value => type == RepType.time ? time : count;
RepType get type => action.repType;
@override
int? get time => toSeconds(action.repLength!);
int? get count => getReps(id, json.decode(action.totalReps));
int? get weight =>
action.repWeights != null ? json.decode(action.repWeights!)[id] : null;
static int getReps(setId, reps) {
if (reps.length > 1) {
return reps[setId];
} else {
return reps.first;
}
}
}
class Rest extends Item {
@override
String name;
Rest(
{required super.id,
required super.position,
required super.action,
super.parentId,
required super.time,
required this.name});
// @override
// String get name => 'Rest';
@override
int get value => toSeconds(time ?? 0);
@override
RepType get valueType => RepType.time;
}

View File

@ -19,9 +19,9 @@ class ActivityTimerModel with ChangeNotifier {
int get actionCount => _actionCounter; int get actionCount => _actionCounter;
int get currentActionNum => _currentActionNum; int get currentActionNum => _currentActionNum;
dynamic get currentAction => currentSet[_currentActionNum]; dynamic get currentAction => currentSet.isNotEmpty ? currentSet[_currentActionNum] : {};
int get currentSetNum => _currentSetNum; int get currentSetNum => _currentSetNum;
dynamic get currentSet => _sets[_currentSetNum]; dynamic get currentSet => _sets.isNotEmpty ? _sets[_currentSetNum] : {};
Activity? get activity => _activity; Activity? get activity => _activity;
List get sets => _sets; List get sets => _sets;
Timer? get periodicTimer => _periodicTimer; Timer? get periodicTimer => _periodicTimer;
@ -36,7 +36,7 @@ class ActivityTimerModel with ChangeNotifier {
_isc = null; _isc = null;
_activity = activity; _activity = activity;
// only one action for now // only one action for now
_sets = json.decode(actions[0].set); _sets = actions.isNotEmpty ? json.decode(actions[0].set) : [];
// _actions = actions; // _actions = actions;
_currentActionNum = 0; _currentActionNum = 0;
_currentSetNum = 0; _currentSetNum = 0;
@ -92,7 +92,7 @@ class ActivityTimerModel with ChangeNotifier {
} }
void setActionCount() { void setActionCount() {
_actionCounter = currentAction['amount']; _actionCounter = currentAction.isNotEmpty ? currentAction['amount'] : 0;
} }
void pause() { void pause() {

View File

@ -0,0 +1,223 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_sound/flutter_sound.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/models/action_model.dart';
import 'package:vibration/vibration.dart';
class ActionTimer with ChangeNotifier {
ActionModel? actionModel;
double _progress = 0;
int _currentTime = 0;
final List<ItemScrollController> _scrollControllers = [];
final FlutterSoundPlayer _mPlayer = FlutterSoundPlayer();
ActionTimer();
Map get state => actionModel?.state ?? _stateConstructor();
ActionStatus get status => actionModel?.status ?? ActionStatus.pending;
bool get started => status == ActionStatus.started;
bool get paused => status == ActionStatus.paused;
bool get pending => status == ActionStatus.pending;
bool get complete => status == ActionStatus.complete;
bool get available => paused | pending;
List<Set> get sets => actionModel!.sets;
List<Item> get items => actionModel!.items;
Set get currentSet => sets[state['currentSet']];
Reps get currentRep => currentSet.reps[state['currentRep']];
Item get currentAction => allActions[state['currentAction']];
int get currentTime => _currentTime;
dynamic get currentValue => currentAction.valueType == RepType.time
? currentTime
: currentAction.value;
List<Item> get allActions => actionModel?.allItems ?? [];
String get repType =>
actionModel!.action.repType == RepType.time ? 'Seconds' : 'Reps';
int? get repLength => currentRep.value;
int? get repCount => currentRep.count;
dynamic get repValue =>
actionModel!.action.repType == RepType.time ? repLength : repCount;
double get progress => _progress;
int get totalTime => actionModel!.totalTime;
Timer? _periodicTimer;
Map _stateConstructor() {
return {
'currentSet': 0,
'currentRep': 0,
'currentTime': 0,
'currentAction': 0
};
}
void setup(ActionModel actionModel, ItemScrollController scrollController,
[bool resetOnLoad = true]) async {
_scrollControllers.clear();
_scrollControllers.add(scrollController);
if (resetOnLoad) {
if (this.actionModel == actionModel) {
reset();
_scrollControllers.add(scrollController);
}
this.actionModel = actionModel;
setAction(currentAction.id);
}
}
Future pause() async =>
await actionModel?.updateStatus(ActionStatus.paused).whenComplete(() {
_periodicTimer?.cancel();
notifyListeners();
// _mPlayer.stopPlayer();
// Be careful : you must `close` the audio session when you have finished with it.
});
Future start() async {
await actionModel!.updateStatus(ActionStatus.started);
await _mPlayer.openPlayer();
Uint8List? countTone;
Uint8List? finishTone;
await rootBundle
.load('assets/audio/count_tone.mp3')
.then((data) => countTone = data.buffer.asUint8List());
await rootBundle
.load('assets/audio/count_finish.mp3')
.then((data) => finishTone = data.buffer.asUint8List());
// start timer
if (_periodicTimer == null || _periodicTimer!.isActive == false) {
_periodicTimer =
Timer.periodic(const Duration(seconds: 1), (Timer timer) async {
switch (currentAction.valueType) {
case RepType.count:
break;
case RepType.time:
_currentTime--;
if (_currentTime <= 3 && _currentTime != 0) {
await _mPlayer
.startPlayer(fromDataBuffer: countTone, codec: Codec.mp3)
.then((duration) async {
if (await Vibration.hasVibrator()) {
Vibration.vibrate(duration: 250);
}
});
}
if (_currentTime == 0) {
// move to next action
await _mPlayer
.startPlayer(fromDataBuffer: finishTone, codec: Codec.mp3)
.then((duration) async {
if (await Vibration.hasVibrator()) {
Vibration.vibrate(duration: 250);
}
});
await setAction(state['currentAction'] + 1);
}
await updateProgress();
notifyListeners();
}
});
}
notifyListeners();
}
Future close() async => await actionModel!
.updateStatus(ActionStatus.complete)
.whenComplete(() async {
_periodicTimer!.cancel();
_mPlayer.closePlayer();
notifyListeners();
});
Future reset() async {
await actionModel?.updateStatus(ActionStatus.pending);
await actionModel?.updateState(json.encode(_stateConstructor()));
_periodicTimer?.cancel();
_progress = 0;
_scrollControllers.clear();
_mPlayer.closePlayer();
notifyListeners();
}
Future clear() async {
await reset();
}
double timeUsed() {
Iterable<Item> usedItems = allActions.getRange(0, state['currentAction']);
return usedItems.fold(0.0, (p, c) => p + c.value!);
}
double totalComplete() {
Iterable<Item> usedItems = allActions.getRange(0, state['currentAction']);
return usedItems.length / allActions.length;
}
updateProgress() {
double repUsed = (currentAction.value - currentTime) / currentAction.value;
_progress =
totalComplete() + ((repUsed < 0 ? 0 : repUsed) / allActions.length);
notifyListeners();
}
setAction(int actionNum, [bool isManual = false]) async {
if (actionNum < allActions.length) {
Item item = allActions[actionNum];
Map newState = state;
newState['currentAction'] = actionNum;
newState['currentSet'] = item.parentId;
newState['currentRep'] = item.id;
newState['currentTime'] = _currentTime = item.value!;
await actionModel!
.updateState(json.encode(newState))
.whenComplete(() async {
// if manual select, pause next action
if (isManual) {
await pause();
await updateProgress();
}
int index = currentAction.parentId != null
? currentAction.parentId!
: currentAction.id;
if (_scrollControllers.isNotEmpty) {
for (int i = 0; i < _scrollControllers.length; i++) {
ItemScrollController sc = _scrollControllers[i];
sc.scrollTo(
index: index,
duration: Duration(milliseconds: 500),
curve: Curves.easeInOutCubic);
}
}
// _scrollController?.scrollTo(
// index: index,
// duration: Duration(milliseconds: 500),
// curve: Curves.easeInOutCubic);
});
} else {
await actionModel?.updateStatus(ActionStatus.complete).whenComplete(() {
_periodicTimer?.cancel();
notifyListeners();
});
}
notifyListeners();
}
}

View File

@ -1,8 +1,10 @@
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:sendtrain/daos/activities_dao.dart'; import 'package:sendtrain/daos/activities_dao.dart';
import 'package:sendtrain/database/database.dart'; import 'package:sendtrain/database/database.dart';
// import 'package:sendtrain/widgets/activities/activity_card.dart'; import 'package:sendtrain/helpers/widget_helpers.dart';
import 'package:sendtrain/widgets/generic/elements/form_search_input.dart'; import 'package:sendtrain/widgets/generic/elements/form_search_input.dart';
class ActivityFinderService { class ActivityFinderService {
@ -27,10 +29,10 @@ class ActivityFinderService {
} }
Widget resultWidget(Activity activity, Function? callback) { Widget resultWidget(Activity activity, Function? callback) {
// return ActivityCard(activity: activity, callback: callback);
return ListTile( return ListTile(
title: Text(activity.title), title: Text(activity.title),
subtitle: Text(activity.description ?? ""), subtitle: Text(jsonToDescription(json.decode(activity.description ?? "")),
maxLines: 2, softWrap: true, overflow: TextOverflow.ellipsis),
onTap: () { onTap: () {
if (callback != null) { if (callback != null) {
callback(); callback();

View File

@ -0,0 +1,305 @@
import 'dart:convert';
import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart' hide Action;
import 'package:provider/provider.dart';
import 'package:sendtrain/daos/actions_dao.dart';
import 'package:sendtrain/daos/activity_actions_dao.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/helpers/widget_helpers.dart';
import 'package:sendtrain/widgets/generic/elements/form_drop_down.dart';
import 'package:sendtrain/widgets/generic/elements/form_text_input.dart';
class ActivityActionEditor extends StatefulWidget {
const ActivityActionEditor(
{super.key,
required this.session,
required this.activity,
this.action,
this.callback});
final Session session;
final Activity activity;
final Action? action;
final Function? callback;
@override
State<ActivityActionEditor> createState() => _ActivityActionEditorState();
}
class _ActivityActionEditorState extends State<ActivityActionEditor> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final Map<String, TextEditingController> actionEditController = {
'sets': TextEditingController(),
'reps': TextEditingController(),
'weight': TextEditingController(),
'repLength': TextEditingController(),
'preparation': TextEditingController(),
'setRest': TextEditingController(),
'repRest': TextEditingController(),
'cooldown': TextEditingController(),
'type': TextEditingController(),
'alternating': TextEditingController(),
};
late final AppDatabase db;
bool isAlternating = false;
bool isTimed = false;
String editorType = 'Create';
@override
void initState() {
super.initState();
db = Provider.of<AppDatabase>(context, listen: false);
// if we're editing a session, we'll want to populate it with the appropriate values
if (widget.action != null) {
final Action action = widget.action!;
editorType = 'Edit';
isAlternating = action.isAlternating;
isTimed = action.repType == RepType.time ? true : false;
actionEditController['sets']?.text = action.totalSets.toString();
actionEditController['reps']?.text =
json.decode(action.totalReps)[0].toString();
actionEditController['weight']?.text =
json.decode(action.repWeights ?? "")[0].toString();
actionEditController['repLength']?.text =
((action.repLength ?? 0) ~/ 1000).toString();
actionEditController['preparation']?.text =
((action.restBeforeSets ?? 0) ~/ 1000).toString();
actionEditController['setRest']?.text =
((action.restBetweenSets ?? 0) ~/ 1000).toString();
actionEditController['repRest']?.text =
((action.restBetweenReps ?? 0) ~/ 1000).toString();
actionEditController['cooldown']?.text =
((action.restAfterSets ?? 0) ~/ 1000).toString();
actionEditController['isTimed']?.text = isTimed.toString();
actionEditController['alternating']?.text = isAlternating.toString();
}
}
@override
Widget build(BuildContext context) {
if (widget.action != null) {
editorType = 'Edit';
}
return Padding(
padding: EdgeInsets.fromLTRB(15, 0, 15, 15),
child: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Padding(
padding: EdgeInsets.only(top: 10, bottom: 10),
child: Text('$editorType Action',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleLarge)),
Row(children: [
formItemWrapper(
CheckboxListTile(
title: Text("Reps alternate? (eg. Left/Right Hand)"),
value: isAlternating,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.all(Radius.circular(10.0)),
),
onChanged: (bool? value) {
setState(() {
isAlternating = value!;
});
},
),
EdgeInsets.fromLTRB(10, 10, 10, 10)),
]),
Row(children: [
formItemWrapper(
CheckboxListTile(
title: Text("Are reps timed?"),
value: isTimed,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.all(Radius.circular(10.0)),
),
onChanged: (bool? value) {
setState(() {
isTimed = value!;
});
},
),
EdgeInsets.fromLTRB(10, 10, 10, 15))
]),
Row(children: [
FormDropDown(
title: 'Sets',
entries: numericDropDownItems('Set', 50),
controller: actionEditController['sets']!),
FormDropDown(
title: 'Reps',
entries: numericDropDownItems('Rep', 100),
controller: actionEditController['reps']!,
)
]),
Row(children: [
formItemWrapper(
FormTextInput(
type: InputTypes.number,
controller: actionEditController['preparation']!,
title: 'Preparation (sec)',
hint: 'time before start',
requiresValidation: false),
EdgeInsets.fromLTRB(10, 5, 10, 0)),
formItemWrapper(
FormTextInput(
type: InputTypes.number,
controller: actionEditController['cooldown']!,
title: 'Cooldown (sec)',
hint: 'rest after completion',
requiresValidation: false),
EdgeInsets.fromLTRB(10, 5, 10, 0)),
]),
Row(children: [
formItemWrapper(
FormTextInput(
type: InputTypes.number,
controller: actionEditController['setRest']!,
title: 'Set Rest (sec)',
hint: 'Rest between sets',
requiresValidation: false),
EdgeInsets.only(left: 10, right: 10)),
formItemWrapper(
FormTextInput(
type: InputTypes.number,
controller: actionEditController['repRest']!,
title: 'Rep Rest (sec)',
hint: 'Rest between reps',
requiresValidation: false),
EdgeInsets.only(left: 10, right: 10)),
]),
Row(children: [
formItemWrapper(
FormTextInput(
type: InputTypes.number,
controller: actionEditController['repLength']!,
title: 'Rep Length (sec)',
hint: 'Total rep time (not required)',
requiresValidation: false),
EdgeInsets.only(left: 10, right: 10)),
formItemWrapper(
FormTextInput(
type: InputTypes.number,
controller: actionEditController['weight']!,
title: 'Weight',
hint: 'Weight for reps',
requiresValidation: false),
EdgeInsets.only(left: 10, right: 10)),
]),
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
Padding(
padding: EdgeInsets.only(top: 10, right: 10),
child: FilledButton(
onPressed: () async {
if (_formKey.currentState!.validate()) {
if (widget.action != null) {
Action newAction = widget.action!.copyWith(
totalSets: int.parse(
actionEditController['sets']!.text),
totalReps: json.encode([
int.parse(
actionEditController['reps']!.text)
]),
repLength: Value<int>(int.parse(
actionEditController['repLength']!
.text) *
1000),
restBeforeSets: Value<int>(int.parse(
actionEditController['preparation']!
.text) *
1000),
restBetweenSets: Value<int>(int.parse(
actionEditController['setRest']!
.text) *
1000),
restBetweenReps: Value<int>(int.parse(
actionEditController['repRest']!
.text) *
1000),
restAfterSets: Value<int>(int.parse(
actionEditController['cooldown']!
.text) *
1000),
repType: int.parse(actionEditController[
'repLength']!
.text) >
0
? RepType.time
: RepType.count,
repWeights: Value<String>(json.encode([
int.parse(
actionEditController['weight']!.text)
])),
// setWeights: Value<String>(json.encode([actionEditController['setWeights']!.text])),
isAlternating: isAlternating,
);
// var result = await ActionsDao(db).createOrUpdate(
// newAction.toCompanion(true));
await ActionsDao(db).replace(newAction);
} else {
// create action
await ActionsDao(db)
.createOrUpdate(ActionsCompanion(
title: Value('rep'),
description: Value('exercise action'),
totalSets: Value(int.parse(
actionEditController['sets']!
.text)),
totalReps: Value(json.encode(
[int.parse(actionEditController['reps']!.text)])),
repLength: Value<int>(
int.parse(actionEditController['repLength']!.text) *
1000),
restBeforeSets: Value<int>(
int.parse(actionEditController['preparation']!.text) *
1000),
restBetweenSets: Value<int>(
int.parse(actionEditController['setRest']!.text) *
1000),
restBetweenReps:
Value<int>(int.parse(actionEditController['repRest']!.text) * 1000),
restAfterSets: Value<int>(int.parse(actionEditController['cooldown']!.text) * 1000),
repType: Value(int.parse(actionEditController['repLength']!.text) > 0 ? RepType.time : RepType.count),
repWeights: Value<String>(json.encode([int.parse(actionEditController['weight']!.text)])),
// setWeights: Value<String>(json.encode([actionEditController['setWeights']!.text])),
isAlternating: Value<bool>(isAlternating),
// repType: RepType.values.firstWhere((e) => e.toString() == "RepType.${actionEditController['repType']!.text}"),
set: Value("")))
.then((actionId) {
ActivityActionsDao(db).createOrUpdate(
ActivityActionsCompanion(
activityId:
Value(widget.activity.id),
sessionId: Value(widget.session.id),
actionId: Value(actionId),
position: Value(0)));
});
}
Navigator.pop(
_formKey.currentContext!, 'Submit');
if (widget.callback != null) {
await widget.callback!();
}
}
},
child: Text('Submit')))
])
])));
}
}

View File

@ -1,20 +1,38 @@
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/extensions/string_extensions.dart'; import 'package:sendtrain/extensions/string_extensions.dart';
import 'package:sendtrain/models/activity_timer_model.dart'; import 'package:sendtrain/helpers/widget_helpers.dart';
import 'package:sendtrain/models/action_model.dart';
import 'package:sendtrain/providers/action_timer.dart';
import 'package:sendtrain/widgets/activities/activity_action_editor.dart';
import 'package:sendtrain/widgets/generic/elements/add_card_generic.dart';
class ActivityActionView extends StatefulWidget { // class ActivityActionView extends StatefulWidget {
const ActivityActionView({super.key, required this.actions}); class ActivityActionView extends StatelessWidget {
ActivityActionView(
{super.key,
required this.session,
required this.activity,
required this.actions,
this.callback,
this.resetOnLoad = true});
final Session session;
final Activity activity;
final List actions; final List actions;
final Function? callback;
final bool resetOnLoad;
@override // @override
State<ActivityActionView> createState() => ActivityActionViewState(); // State<ActivityActionView> createState() => ActivityActionViewState();
} // }
class ActivityActionViewState extends State<ActivityActionView> { // class ActivityActionViewState extends State<ActivityActionView> {
// class ActivityActionView extends StatelessWidget {
// ActivityActionView({super.key, required this.actions});
// final List actions;
final ItemScrollController itemScrollController = ItemScrollController(); final ItemScrollController itemScrollController = ItemScrollController();
final ScrollOffsetController scrollOffsetController = final ScrollOffsetController scrollOffsetController =
ScrollOffsetController(); ScrollOffsetController();
@ -23,88 +41,210 @@ class ActivityActionViewState extends State<ActivityActionView> {
final ScrollOffsetListener scrollOffsetListener = final ScrollOffsetListener scrollOffsetListener =
ScrollOffsetListener.create(); ScrollOffsetListener.create();
late final ActionTimer at;
// int actionCount = 0;
GestureDetector gtBuild(
ActionTimer at, Item item, int actionNum, int selectedIndex,
{int? order}) {
// default, for rests
String setItemRef = '-';
// non rests decimal reference to item
if (order != null) {
setItemRef = '${order + 1}.${item.position + 1}';
}
return GestureDetector(onTap: () {
at.setAction(actionNum, true);
}, child: Consumer<ActionTimer>(builder: (context, at, child) {
return Row(children: [
Ink(
width: 70,
padding: const EdgeInsets.all(15),
color: item == at.currentAction
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.onPrimary,
child: Text(textAlign: TextAlign.center, setItemRef)),
Expanded(
child: Ink(
padding: const EdgeInsets.all(15),
color: item == at.currentAction
? Theme.of(context).colorScheme.surfaceBright
: Theme.of(context).colorScheme.surfaceContainerLow,
child: Text(
textAlign: TextAlign.center,
'${item.name}: ${item.value} ${item.humanValueType}'
.toTitleCase())))
]);
}));
}
// @override
// void initState() {
// super.initState();
// at = Provider.of<ActionTimer>(context, listen: false);
// }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
ActivityTimerModel atm = at = Provider.of<ActionTimer>(context, listen: false);
Provider.of<ActivityTimerModel>(context, listen: true); int actionCount = 0;
List sets = json.decode(widget.actions[0].set); if (actions.isNotEmpty) {
at.setup(
ActionModel(
action: actions.first,
db: Provider.of<AppDatabase>(context)),
itemScrollController,
resetOnLoad);
// we need to set the scroll controller // WidgetsBinding.instance.addPostFrameCallback((_) {
// so we can update the selected item position // if (itemScrollController.isAttached) {
atm.setScrollController(itemScrollController); // itemScrollController.scrollTo(
// index: at.currentAction.parentId != null
// ? at.currentAction.parentId!
// : at.currentAction.id,
// duration: Duration(milliseconds: 500),
// curve: Curves.easeInOutCubic);
// }
// });
return Expanded( return Expanded(
child: ScrollablePositionedList.builder( child: Column(children: [
padding: const EdgeInsets.fromLTRB(10, 0, 10, 20), Padding(
itemCount: sets.length, padding: const EdgeInsets.only(left: 10, right: 10),
itemScrollController: itemScrollController, child: Card(
scrollOffsetController: scrollOffsetController, clipBehavior: Clip.antiAlias,
itemPositionsListener: itemPositionsListener, shape: const RoundedRectangleBorder(
scrollOffsetListener: scrollOffsetListener, borderRadius: BorderRadius.only(
itemBuilder: (BuildContext context, int setNum) { topLeft: Radius.circular(10),
List<GestureDetector> content = []; topRight: Radius.circular(10)),
List set = sets[setNum]; ),
color: Theme.of(context).colorScheme.onPrimary,
child: Row(children: [
Ink(
width: 70,
color: Theme.of(context).colorScheme.primaryContainer,
child: Consumer<ActionTimer>(
builder: (context, at, child) {
return IconButton(
alignment: AlignmentDirectional.center,
icon: at.available
? const Icon(Icons.play_arrow_rounded)
: const Icon(Icons.pause_rounded),
onPressed: () => {
if (at.started)
{at.pause()}
else if (at.available || at.complete)
{at.start()}
});
},
)),
Expanded(
flex: 1,
child: Stack(alignment: Alignment.center, children: [
Container(
alignment: Alignment.center,
child: Consumer<ActionTimer>(
builder: (context, at, child) {
return Text(
style: const TextStyle(fontSize: 20),
textAlign: TextAlign.center,
'${at.currentValue} ${at.currentAction.humanValueType}'
.toTitleCase());
},
),
),
Container(
alignment: Alignment.centerRight,
padding: EdgeInsets.only(right: 15),
child: Consumer<ActionTimer>(
builder: (context, at, child) {
return Text(
style: const TextStyle(fontSize: 12),
textAlign: TextAlign.right,
'${at.state['currentAction'] + 1} of ${at.allActions.length}');
})),
])),
]))),
Padding(
padding: EdgeInsets.only(left: 14, right: 14),
child: Consumer<ActionTimer>(builder: (context, at, child) {
return LinearProgressIndicator(
value: at.progress,
semanticsLabel: 'Activity Progress',
);
})),
Expanded(
child: ScrollablePositionedList.builder(
padding: const EdgeInsets.fromLTRB(10, 0, 10, 20),
itemCount: at.items.length,
// initialScrollIndex: at.currentAction.parentId != null
// ? at.currentAction.parentId!
// : at.currentAction.id,
itemScrollController: itemScrollController,
scrollOffsetController: scrollOffsetController,
itemPositionsListener: itemPositionsListener,
scrollOffsetListener: scrollOffsetListener,
itemBuilder: (BuildContext context, int itemNum) {
if (itemNum == 0) {
actionCount = 0;
}
for (int actionNum = 0; actionNum < set.length; actionNum++) { List<GestureDetector> content = [];
Map<String, dynamic> setItem = set[actionNum]; Item item = at.items[itemNum];
if (item.runtimeType == Rest) {
content.add(gtBuild(at, item, actionCount++, itemNum));
} else if (item.runtimeType == Set) {
List<Item> setItems = item.items;
content.add(GestureDetector( for (int setItemNum = 0;
onTap: () { setItemNum < setItems.length;
atm.setAction(setNum, actionNum, 'manual'); setItemNum++) {
atm.setActionCount(); Item setItem = setItems[setItemNum];
content.add(gtBuild(at, setItem, actionCount++, itemNum,
order: (item as Set).setOrder));
}
}
itemScrollController.scrollTo( if (itemNum == 0) {
index: setNum, return Card(
duration: Duration(milliseconds: 500), shape: const RoundedRectangleBorder(
curve: Curves.easeInOutCubic); borderRadius: BorderRadius.only(
}, topLeft: Radius.circular(0),
child: Row(children: [ topRight: Radius.circular(0),
Ink( bottomLeft: Radius.circular(10),
width: 70, bottomRight: Radius.circular(10)),
padding: const EdgeInsets.all(15), ),
color: atm.isCurrentItem(setNum, actionNum) clipBehavior: Clip.antiAlias,
? Theme.of(context).colorScheme.primaryContainer child: Column(children: content));
: Theme.of(context).colorScheme.onPrimary, } else {
child: Text( return Card(
textAlign: TextAlign.center, shape: const RoundedRectangleBorder(
'${setNum + 1}.${actionNum + 1} ')), borderRadius: BorderRadius.only(
Expanded( topLeft: Radius.circular(10),
child: Ink( topRight: Radius.circular(10),
padding: const EdgeInsets.all(15), bottomLeft: Radius.circular(10),
color: atm.isCurrentItem(setNum, actionNum) bottomRight: Radius.circular(10)),
? Theme.of(context).colorScheme.surfaceBright ),
: Theme.of(context).colorScheme.surfaceContainerLow, clipBehavior: Clip.antiAlias,
child: Text( child: Column(children: content));
textAlign: TextAlign.center, }
'${setItem['name']}: ${setItem['amount']} ${setItem['type']}'.toTitleCase()))) }))
]))); ]));
} } else {
return AddCardGeneric(
if (setNum == 0) { title: 'Add an Action!',
return Card( description:
shape: const RoundedRectangleBorder( 'Click here to create an exercise template (sets and reps, etc) for your activity!',
borderRadius: BorderRadius.only( action: () {
topLeft: Radius.circular(0), showEditorSheet(
topRight: Radius.circular(0), context,
bottomLeft: Radius.circular(10), ActivityActionEditor(
bottomRight: Radius.circular(10)), session: session,
), activity: activity,
clipBehavior: Clip.antiAlias, callback: callback));
child: Column(children: content)); });
} else { }
return Card(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10),
bottomLeft: Radius.circular(10),
bottomRight: Radius.circular(10)),
),
clipBehavior: Clip.antiAlias,
child: Column(children: content));
}
// return Column(children: contents);
},
));
} }
} }

View File

@ -1,11 +1,14 @@
import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:sendtrain/daos/activities_dao.dart';
import 'package:sendtrain/daos/media_items_dao.dart'; import 'package:sendtrain/daos/media_items_dao.dart';
import 'package:sendtrain/daos/session_activities_dao.dart';
import 'package:sendtrain/database/database.dart'; import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/extensions/string_extensions.dart'; import 'package:sendtrain/extensions/string_extensions.dart';
import 'package:sendtrain/helpers/date_time_helpers.dart'; import 'package:sendtrain/helpers/date_time_helpers.dart';
import 'package:sendtrain/helpers/media_helpers.dart'; import 'package:sendtrain/helpers/media_helpers.dart';
import 'package:sendtrain/helpers/widget_helpers.dart';
import 'package:sendtrain/models/activity_timer_model.dart'; import 'package:sendtrain/models/activity_timer_model.dart';
import 'package:sendtrain/widgets/activities/activity_view.dart'; import 'package:sendtrain/widgets/activities/activity_view.dart';
import 'package:sendtrain/widgets/builders/dialogs.dart'; import 'package:sendtrain/widgets/builders/dialogs.dart';
@ -14,9 +17,14 @@ import 'package:sendtrain/widgets/generic/elements/generic_progress_indicator.da
class ActivityCard extends StatefulWidget { class ActivityCard extends StatefulWidget {
final Activity activity; final Activity activity;
final Session session;
final Function? callback; final Function? callback;
const ActivityCard({super.key, required this.activity, this.callback}); const ActivityCard(
{super.key,
required this.activity,
required this.session,
this.callback});
@override @override
State<ActivityCard> createState() => ActivityCardState(); State<ActivityCard> createState() => ActivityCardState();
@ -41,7 +49,7 @@ class ActivityCardState extends State<ActivityCard> {
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
child: InkWell( child: InkWell(
onTap: () => showGenericDialog( onTap: () => showGenericDialog(
ActivityView(activity: widget.activity), context), ActivityView(session: widget.session, activity: widget.activity), context),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
@ -54,16 +62,22 @@ class ActivityCardState extends State<ActivityCard> {
if (atm.activity?.id == widget.activity.id) { if (atm.activity?.id == widget.activity.id) {
return Text( return Text(
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis,
"${widget.activity.title.toTitleCase()} (${formattedTime(atm.totalTime)})"); "${widget.activity.title.toTitleCase()} (${formattedTime(atm.totalTime)})");
} else { } else {
return Text( return Text(
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis,
widget.activity.title.toTitleCase()); widget.activity.title.toTitleCase());
} }
}, },
), ),
subtitle: subtitle: Text(
Text(maxLines: 2, widget.activity.description ?? ""), overflow: TextOverflow.ellipsis,
maxLines: 2,
softWrap: true,
jsonToDescription(json
.decode(widget.activity.description ?? ""))),
contentPadding: EdgeInsets.only(left: 13), contentPadding: EdgeInsets.only(left: 13),
trailing: Flex( trailing: Flex(
direction: Axis.vertical, direction: Axis.vertical,
@ -79,10 +93,11 @@ class ActivityCardState extends State<ActivityCard> {
'Activity Removal', 'Activity Removal',
'Would you like to permanently remove this activity from the current session?', 'Would you like to permanently remove this activity from the current session?',
context, () { context, () {
ActivitiesDao(Provider.of<AppDatabase>( SessionActivitiesDao(
context, Provider.of<AppDatabase>(context,
listen: false)) listen: false))
.remove(widget.activity); .removeAssociation(widget.activity.id,
widget.session.id);
}).then((result) { }).then((result) {
setState(() {}); setState(() {});
}); });

View File

@ -1,18 +1,23 @@
import 'package:flutter/material.dart'; import 'dart:convert';
import 'package:flutter/material.dart' hide Action;
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart'; import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:sendtrain/daos/actions_dao.dart'; import 'package:sendtrain/daos/actions_dao.dart';
import 'package:sendtrain/database/database.dart'; import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/extensions/string_extensions.dart'; import 'package:sendtrain/extensions/string_extensions.dart';
import 'package:sendtrain/models/activity_timer_model.dart'; import 'package:sendtrain/helpers/widget_helpers.dart';
import 'package:sendtrain/providers/action_timer.dart';
import 'package:sendtrain/widgets/activities/activity_action_editor.dart';
import 'package:sendtrain/widgets/activities/activity_action_view.dart'; import 'package:sendtrain/widgets/activities/activity_action_view.dart';
import 'package:sendtrain/widgets/activities/activity_view_categories.dart'; import 'package:sendtrain/widgets/activities/activity_view_categories.dart';
import 'package:sendtrain/widgets/activities/activity_view_media.dart'; import 'package:sendtrain/widgets/activities/activity_view_media.dart';
import 'package:sendtrain/widgets/activities/activity_view_types.dart'; import 'package:sendtrain/widgets/builders/dialogs.dart';
class ActivityView extends StatefulWidget { class ActivityView extends StatefulWidget {
const ActivityView( const ActivityView(
{super.key, required this.activity}); {super.key, required this.session, required this.activity});
final Session session;
final Activity activity; final Activity activity;
@override @override
@ -20,173 +25,254 @@ class ActivityView extends StatefulWidget {
} }
class _ActivityViewState extends State<ActivityView> { class _ActivityViewState extends State<ActivityView> {
final _fabKey = GlobalKey<ExpandableFabState>();
void resetState() async {
final state = _fabKey.currentState;
if (state != null && state.isOpen) {
state.toggle();
}
setState(() {});
}
List<ActivityMuscle> activityMuscle(Activity activity) {
List<ActivityMuscle> muscles = [];
if (activity.primaryMuscles != null) {
muscles.add(activity.primaryMuscles!);
}
if (activity.secondaryMuscles != null) {
muscles.add(activity.secondaryMuscles!);
}
return muscles;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Activity activity = widget.activity; final Activity activity = widget.activity;
ActivityTimerModel atm = final Session session = widget.session;
Provider.of<ActivityTimerModel>(context, listen: false);
return FutureBuilder<List>( return FutureBuilder<List>(
future: ActionsDao(Provider.of<AppDatabase>(context)) future: ActionsDao(Provider.of<AppDatabase>(context))
.fromActivity(activity), .fromActivity(activity, session),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
List actions = snapshot.data!; List<Action> actions = snapshot.data! as List<Action>;
atm.setup(activity, actions);
return Scaffold( return PopScope(
floatingActionButtonLocation: ExpandableFab.location, canPop: false,
floatingActionButton: ExpandableFab( onPopInvokedWithResult: (didPop, result) async {
distance: 70, if (didPop) {
type: ExpandableFabType.up, return;
overlayStyle: ExpandableFabOverlayStyle( }
color: Colors.black.withOpacity(0.5), final bool shouldPop = await showBackDialog(context) ?? false;
blur: 10, if (context.mounted && shouldPop) {
), Navigator.pop(context);
children: [ }
FloatingActionButton.extended( },
icon: const Icon(Icons.upload_outlined), child: Scaffold(
label: Text('Upload Media'), floatingActionButtonLocation: ExpandableFab.location,
onPressed: () {}, floatingActionButton: ExpandableFab(
), key: _fabKey,
FloatingActionButton.extended( distance: 70,
icon: const Icon(Icons.note_add_outlined), type: ExpandableFabType.up,
label: Text('Add Note'), overlayStyle: ExpandableFabOverlayStyle(
onPressed: () {}, color: Colors.black.withValues(alpha: 0.5),
), blur: 10,
FloatingActionButton.extended( ),
icon: const Icon(Icons.history_outlined), onOpen: () {
label: Text('Restart'), // pause the activity on open
onPressed: () {}, ActionTimer at =
), Provider.of<ActionTimer>(context, listen: false);
FloatingActionButton.extended( if (at.started) at.pause();
icon: const Icon(Icons.done_all_outlined), },
label: Text('Done'), children: [
onPressed: () {}, // FloatingActionButton.extended(
), // icon: const Icon(Icons.upload_outlined),
]), // label: Text('Upload Media'),
body: Column( // onPressed: () {},
crossAxisAlignment: CrossAxisAlignment.start, // ),
children: [ FloatingActionButton.extended(
AppBar( icon: const Icon(Icons.done_all_outlined),
titleSpacing: 0, label: Text('Edit Action'),
centerTitle: true, onPressed: () {
title: const Text('Activity', showEditorSheet(
style: TextStyle(fontSize: 15)), context,
), ActivityActionEditor(
Padding( session: session,
padding: const EdgeInsets.only( activity: activity,
left: 15, right: 20, top: 15, bottom: 10), action: actions.first,
child: Text( callback: resetState));
maxLines: 1, },
style: const TextStyle( ),
fontSize: 25, fontWeight: FontWeight.bold), FloatingActionButton.extended(
activity.title.toTitleCase())), icon: const Icon(Icons.note_add_outlined),
Padding( label: Text('Add Note'),
padding: const EdgeInsets.fromLTRB(10, 0, 0, 10), onPressed: () {},
child: Flex(direction: Axis.horizontal, children: [ ),
ActivityViewCategories( FloatingActionButton.extended(
categories: activity.category != null ? [activity.category!] : []), icon: const Icon(Icons.history_outlined),
ActivityViewTypes(types: [activity.type!]) label: Text('Restart'),
])), onPressed: () {},
Padding( ),
padding: const EdgeInsets.only( FloatingActionButton.extended(
top: 0, bottom: 10, left: 15, right: 15), icon: const Icon(Icons.done_all_outlined),
child: Text( label: Text('Done'),
textAlign: TextAlign.left, onPressed: () {},
style: const TextStyle(fontSize: 15), ),
activity.description ?? "")), ]),
const Padding( body: Column(
padding: EdgeInsets.fromLTRB(15, 20, 0, 10), crossAxisAlignment: CrossAxisAlignment.start,
child: Text( children: [
style: TextStyle( AppBar(
fontSize: 20, fontWeight: FontWeight.bold), titleSpacing: 0,
'Media:')), centerTitle: true,
ActivityViewMedia(activity: activity), title: const Text('Activity',
const Padding( style: TextStyle(fontSize: 15)),
padding: EdgeInsets.fromLTRB(15, 30, 0, 10), ),
child: Text( Padding(
textAlign: TextAlign.left, padding: const EdgeInsets.only(
style: TextStyle( left: 15, right: 20, top: 15, bottom: 10),
fontSize: 20, fontWeight: FontWeight.bold), child: Text(
'Actions')), maxLines: 1,
Padding( style: const TextStyle(
padding: const EdgeInsets.only(left: 10, right: 10), fontSize: 25,
child: Card( fontWeight: FontWeight.bold),
clipBehavior: Clip.antiAlias, activity.title.toTitleCase())),
shape: const RoundedRectangleBorder( SizedBox(
borderRadius: BorderRadius.only( height: 40,
topLeft: Radius.circular(10), child: ListView(
topRight: Radius.circular(10)), scrollDirection: Axis.horizontal,
), padding:
color: Theme.of(context).colorScheme.onPrimary, const EdgeInsets.fromLTRB(10, 0, 10, 0),
shrinkWrap: true,
children: [
ActivityViewCategories<List<ActivityLevel>>(
icon: Icon(Icons.stairs_rounded),
text: "Activity Level",
object: activity.level != null
? [activity.level!]
: []),
// ActivityViewCategories<List<ActivityMechanic>>(
// icon: Icon(Icons.),
// text: 'Activity Mechanic',
// object: activity.mechanic != null
// ? [activity.mechanic!]
// : []),
ActivityViewCategories<
List<ActivityEquipment>>(
icon:
Icon(Icons.fitness_center_rounded),
text: 'Equipment Used',
object: activity.equipment != null
? [activity.equipment!]
: []),
ActivityViewCategories<List<ActivityType>>(
icon: Icon(Icons.type_specimen_rounded),
text: 'Activity Type',
object: activity.type != null
? [activity.type!]
: []),
ActivityViewCategories<
List<ActivityMuscle>>(
icon: Icon(Icons.person),
text: 'Muscles used',
object: activityMuscle(activity))
])),
Padding(
padding: const EdgeInsets.only(
top: 10, bottom: 0, left: 15, right: 15),
child: Text(
maxLines: 4,
overflow: TextOverflow.ellipsis,
// softWrap: true,
textAlign: TextAlign.left,
style: const TextStyle(fontSize: 15),
jsonToDescription([
json.decode(activity.description ?? "")[0]
]))),
Padding(
padding: EdgeInsets.only(right: 15),
child: Align(
alignment: Alignment.topRight,
child: TextButton(
style: ButtonStyle(
textStyle:
WidgetStateProperty.all<TextStyle>(
TextStyle(
fontWeight:
FontWeight.normal)),
shape: WidgetStateProperty.all<
RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(10.0),
))),
onPressed: () {
showGenericSheet(
context,
Padding(
padding: EdgeInsets.all(15),
child: Text(
style:
TextStyle(fontSize: 18),
jsonToDescription(json.decode(
activity.description ??
"")))));
},
child: Text(
"read more",
textAlign: TextAlign.right,
style: TextStyle(fontSize: 12),
),
))),
const Padding(
padding: EdgeInsets.fromLTRB(15, 10, 0, 10),
child: Text(
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold),
'Media:')),
ActivityViewMedia(activity: activity),
Padding(
padding: const EdgeInsets.fromLTRB(15, 20, 5, 0),
child: Row(children: [ child: Row(children: [
Ink(
width: 70,
color: Theme.of(context)
.colorScheme
.primaryContainer,
child: Consumer<ActivityTimerModel>(
builder: (context, atm, child) {
return IconButton(
alignment:
AlignmentDirectional.center,
icon: atm.isActive
? const Icon(
Icons.pause_rounded)
: const Icon(
Icons.play_arrow_rounded),
onPressed: () => {
atm.isActive
? atm.pause()
: atm.start()
});
},
)),
Expanded( Expanded(
flex: 1, child: const Text(
child: Stack( textAlign: TextAlign.left,
alignment: Alignment.center, style: TextStyle(
children: [ fontSize: 20,
Container( fontWeight: FontWeight.bold),
alignment: Alignment.center, 'Actions')),
child: Consumer<ActivityTimerModel>( IconButton(
builder: (context, atm, child) { onPressed: () {
return Text( showGenericSheet(
style: const TextStyle( context,
fontSize: 20), Column(children: [
textAlign: TextAlign.center, ActivityActionView(
'${atm.actionCount} ${atm.currentAction['type']}'.toTitleCase()); session: session,
}, activity: activity,
), actions: actions,
), callback: resetState,
Container( resetOnLoad: false)
alignment: Alignment.centerRight, ]),
padding: Theme.of(context).colorScheme.surface);
EdgeInsets.only(right: 15), },
child: icon: Icon(Icons.expand),
Consumer<ActivityTimerModel>( alignment: Alignment.bottomCenter,
builder: (context, atm, )
child) { ])),
return Text( ActivityActionView(
style: const TextStyle( session: session,
fontSize: 12), activity: activity,
textAlign: TextAlign.right, actions: actions,
'${atm.currentAction['actionID'] + 1} of ${atm.totalActions()}'); callback: resetState)
})), ])));
])), // ] +
]))), // action(actions, context)));
Padding(
padding: EdgeInsets.only(left: 14, right: 14),
child: Consumer<ActivityTimerModel>(
builder: (context, atm, child) {
return LinearProgressIndicator(
value: atm.progress,
semanticsLabel: 'Activity Progress',
);
})),
ActivityActionView(actions: actions),
]));
} else { } else {
return Container( return Container(
alignment: Alignment.center, alignment: Alignment.center,

View File

@ -1,30 +1,35 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/extensions/string_extensions.dart'; import 'package:sendtrain/extensions/string_extensions.dart';
class ActivityViewCategories extends StatelessWidget { class ActivityViewCategories<T extends List<Enum>> extends StatelessWidget {
const ActivityViewCategories({super.key, required this.categories}); const ActivityViewCategories(
{super.key,
required this.object,
required this.icon,
required this.text});
final List<ActivityCategories> categories; final T object;
final Icon icon;
final String text;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return ListView.builder(
height: 40, shrinkWrap: true,
child: ListView.builder( scrollDirection: Axis.horizontal,
shrinkWrap: true, // padding: const EdgeInsets.only(right: 10, left: 10),
scrollDirection: Axis.horizontal, itemCount: object.length,
padding: const EdgeInsets.only(right: 10), itemBuilder: (BuildContext context, int index) {
itemCount: categories.length, return Padding(
itemBuilder: (BuildContext context, int index) { padding: EdgeInsets.only(right: 5),
return ActionChip( child: ActionChip(
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
avatar: const Icon(Icons.category_rounded), avatar: icon,
label: Text(maxLines: 1, categories[index].name.toTitleCase()), label: Text(maxLines: 1, object[index].name.toTitleCase()),
tooltip: "Activity Category", tooltip: text,
onPressed: () {}, onPressed: () {},
); ));
}, },
)); );
} }
} }

View File

@ -1,8 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:sendtrain/providers/action_timer.dart';
Future showGenericDialog(dynamic object, BuildContext parentContext) { Future showGenericDialog(dynamic object, BuildContext parentContext) {
return showGeneralDialog( return showGeneralDialog(
barrierColor: Colors.black.withOpacity(0.5), barrierColor: Colors.black.withValues(alpha: 0.5),
transitionDuration: const Duration(milliseconds: 220), transitionDuration: const Duration(milliseconds: 220),
transitionBuilder: (BuildContext context, Animation<double> animation, transitionBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) { Animation<double> secondaryAnimation, Widget child) {
@ -55,3 +57,51 @@ Future showUpdateDialog(String title, String content, BuildContext context,
[Function? callback]) { [Function? callback]) {
return showCrudDialog(title, content, context, callback); return showCrudDialog(title, content, context, callback);
} }
// TODO - factor out, this should be more generic
Future<bool?> showBackDialog(BuildContext context) async {
ActionTimer at = Provider.of<ActionTimer>(context, listen: false);
if (at.pending || at.complete) {
await at.clear();
return true;
} else {
return await showDialog<bool>(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Are you sure?'),
content: const Text(
'Leaving will stop the current activity. Are you sure you want to leave?',
),
actions: <Widget>[
TextButton(
style: TextButton.styleFrom(
textStyle: Theme.of(context).textTheme.labelLarge,
),
child: const Text('Nevermind'),
onPressed: () async {
Navigator.pop(context, false);
},
),
TextButton(
style: TextButton.styleFrom(
textStyle: Theme.of(context).textTheme.labelLarge,
),
child: const Text('Leave'),
onPressed: () async {
ActionTimer at =
Provider.of<ActionTimer>(context, listen: false);
await at.clear();
if (context.mounted) {
Navigator.pop(context, true);
}
},
),
],
);
},
);
}
}

View File

@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
class AddCardGeneric extends StatelessWidget {
const AddCardGeneric(
{super.key, required this.title, required this.description, this.action});
final String title;
final String description;
final Function? action;
@override
Widget build(BuildContext context) {
return Expanded(
child: ListView(
padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
children: [
Card.outlined(
child: InkWell(
customBorder: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
onTap: () {
if (action != null) {
action!();
}
},
child: ListTile(
contentPadding:
EdgeInsets.only(top: 5, left: 15, right: 5, bottom: 5),
autofocus: true,
leading: Icon(Icons.add_box_rounded),
title: Text(title),
subtitle: Text(description),
)))
]));
}
}

View File

@ -0,0 +1,32 @@
import 'package:flutter/material.dart';
import 'package:sendtrain/helpers/widget_helpers.dart';
class FormDropDown extends StatelessWidget {
const FormDropDown(
{super.key,
required this.title,
required this.entries,
required this.controller});
final List<DropdownMenuEntry> entries;
final String title;
final TextEditingController controller;
@override
Widget build(BuildContext context) {
return formItemWrapper(
DropdownMenu(
leadingIcon: Icon(Icons.select_all_rounded),
initialSelection: controller.text,
controller: controller,
expandedInsets: EdgeInsets.zero,
inputDecorationTheme: InputDecorationTheme(
filled: true,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(12))),
label: Text(title),
dropdownMenuEntries: entries),
EdgeInsets.fromLTRB(10, 5, 10, 5));
}
}

View File

@ -57,8 +57,7 @@ class _FormSearchInputState extends State<FormSearchInput> {
_currentQuery = query; _currentQuery = query;
// In a real application, there should be some error handling here. // In a real application, there should be some error handling here.
// final Iterable<String> options = await _FakeAPI.search(_currentQuery!); if (query.isNotEmpty && query.length > 3) {
if (query.isNotEmpty) {
final List<Suggestion>? suggestions = final List<Suggestion>? suggestions =
await service.fetchSuggestions(_currentQuery!); await service.fetchSuggestions(_currentQuery!);
@ -83,33 +82,35 @@ class _FormSearchInputState extends State<FormSearchInput> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SearchAnchor( return SearchAnchor(
isFullScreen: false,
builder: (BuildContext context, SearchController controller) { builder: (BuildContext context, SearchController controller) {
return FormTextInput( return FormTextInput(
controller: widget.controller, controller: widget.controller,
title: widget.title ?? "", title: widget.title ?? "",
icon: Icon(Icons.search_rounded), icon: Icon(Icons.search_rounded),
maxLines: 2, maxLines: 2,
requiresValidation: false, requiresValidation: false,
onTap: () { onTap: () {
controller.openView(); controller.openView();
}); });
}, suggestionsBuilder: },
suggestionsBuilder:
(BuildContext context, SearchController controller) async { (BuildContext context, SearchController controller) async {
final List<Suggestion>? options = final List<Suggestion>? options =
(await debouncer.process(controller.text))?.toList(); (await debouncer.process(controller.text))?.toList();
if (options == null) { if (options == null) {
return _lastOptions; return _lastOptions;
} }
_lastOptions = List<ListTile>.generate(options.length, (int index) { _lastOptions = List<ListTile>.generate(options.length, (int index) {
final Suggestion item = options[index]; final Suggestion item = options[index];
final dynamic content = item.content; final dynamic content = item.content;
return service.resultWidget(content, () { return service.resultWidget(content, () {
resultHandler(content, service); resultHandler(content, service);
controller.closeView(null); controller.closeView(null);
}); });
}); });
return _lastOptions; return _lastOptions;
}); });
} }
} }

View File

@ -1,4 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
enum InputTypes { text, number }
class FormTextInput extends StatelessWidget { class FormTextInput extends StatelessWidget {
const FormTextInput( const FormTextInput(
@ -9,7 +12,10 @@ class FormTextInput extends StatelessWidget {
this.maxLines, this.maxLines,
this.minLines, this.minLines,
this.onTap, this.onTap,
this.requiresValidation=true}); this.requiresValidation = true,
this.type = InputTypes.text,
this.hint,
this.validations});
final TextEditingController controller; final TextEditingController controller;
final String title; final String title;
@ -18,12 +24,25 @@ class FormTextInput extends StatelessWidget {
final Icon? icon; final Icon? icon;
final dynamic onTap; final dynamic onTap;
final bool requiresValidation; final bool requiresValidation;
final InputTypes type;
final String? hint;
final Function? validations;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final Map params = {};
if (type == InputTypes.number) {
params['keyboardType'] = TextInputType.number;
params['inputFormatters'] = <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly
];
}
return Padding( return Padding(
padding: EdgeInsets.only(top: 10, bottom: 10), padding: EdgeInsets.only(top: 10, bottom: 10),
child: TextFormField( child: TextFormField(
keyboardType: params['keyboardType'] ?? TextInputType.text,
inputFormatters: params['inputFormatters'] ?? [],
minLines: minLines ?? 1, minLines: minLines ?? 1,
maxLines: maxLines ?? 1, maxLines: maxLines ?? 1,
controller: controller, controller: controller,
@ -34,6 +53,7 @@ class FormTextInput extends StatelessWidget {
borderSide: BorderSide.none, borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(12)), borderRadius: BorderRadius.circular(12)),
labelText: title, labelText: title,
hintText: hint ?? '',
), ),
validator: (String? value) { validator: (String? value) {
if (requiresValidation == true) { if (requiresValidation == true) {
@ -41,9 +61,11 @@ class FormTextInput extends StatelessWidget {
return 'Please enter some text'; return 'Please enter some text';
} }
if (value.length < 3) { if (validations != null) validations!(value);
return 'Please enter a minimum of 3 characters';
} // if (value.length < 3) {
// return 'Please enter a minimum of 3 characters';
// }
} }
return null; return null;
}, },

View File

@ -1,6 +1,4 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@ -8,29 +6,56 @@ import 'package:sendtrain/daos/media_items_dao.dart';
import 'package:sendtrain/database/database.dart'; import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/helpers/widget_helpers.dart'; import 'package:sendtrain/helpers/widget_helpers.dart';
import 'package:sendtrain/widgets/builders/dialogs.dart'; import 'package:sendtrain/widgets/builders/dialogs.dart';
import 'package:video_player/video_player.dart';
class MediaCard extends StatelessWidget { class MediaCard extends StatelessWidget {
const MediaCard({super.key, required this.media, this.callback}); const MediaCard(
{super.key, required this.media, this.callback, this.canDelete});
final MediaItem media; final MediaItem media;
final bool? canDelete;
final Function? callback; final Function? callback;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
DecorationImage mediaImage(MediaItem media) { mediaImage(MediaItem media) {
dynamic image; Image image = Image.asset('assets/images/placeholder.jpg');
if (media.type == MediaType.image || media.type == MediaType.location) { if (media.type == MediaType.image || media.type == MediaType.location) {
image = NetworkImage(media.reference); image = Image.network(media.reference, loadingBuilder:
(BuildContext context, Widget child,
ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child;
return Text('WTF!!!!');
// return Center(
// child: CircularProgressIndicator(
// value: loadingProgress.expectedTotalBytes != null
// ? loadingProgress.cumulativeBytesLoaded /
// loadingProgress.expectedTotalBytes!
// : null,
// ),
// );
});
} else if (media.type == MediaType.localImage) { } else if (media.type == MediaType.localImage) {
image = Image.memory(base64Decode(media.reference)).image; image = Image.memory(base64Decode(media.reference));
} else if (media.type == MediaType.youtube) { } else if (media.type == MediaType.youtube) {
image = image =
NetworkImage('https://img.youtube.com/vi/${media.reference}/0.jpg'); Image.network('https://img.youtube.com/vi/${media.reference}/0.jpg',
} else if (media.type == MediaType.localVideo) {} loadingBuilder: (BuildContext context, Widget child,
ImageChunkEvent? loadingProgress) {
if (loadingProgress == null) return child;
return Text('WTF!!!!');
// return Center(
// child: CircularProgressIndicator(
// value: loadingProgress.expectedTotalBytes != null
// ? loadingProgress.cumulativeBytesLoaded /
// loadingProgress.expectedTotalBytes!
// : null,
// ),
// );
});
} //else if (media.type == MediaType.localVideo) {}
return DecorationImage(image: image, fit: BoxFit.cover); return DecorationImage(image: image.image, fit: BoxFit.cover);
} }
return Container( return Container(
@ -44,7 +69,9 @@ class MediaCard extends StatelessWidget {
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
shadowColor: const Color.fromARGB(0, 255, 255, 255), shadowColor: const Color.fromARGB(0, 255, 255, 255),
child: TextButton( child: TextButton(
onLongPress: () => showRemovalDialog( onLongPress: () {
if (canDelete == true) {
showRemovalDialog(
'Media Removal', 'Media Removal',
'Would you like to permanently remove this media from the current session?', 'Would you like to permanently remove this media from the current session?',
context, () { context, () {
@ -55,7 +82,9 @@ class MediaCard extends StatelessWidget {
if (callback != null) { if (callback != null) {
callback!(); callback!();
} }
}), });
}
},
onPressed: () => showMediaDetailWidget(context, media), onPressed: () => showMediaDetailWidget(context, media),
child: const ListTile( child: const ListTile(
title: Text(''), title: Text(''),

View File

@ -13,6 +13,7 @@ import 'package:sendtrain/widgets/generic/elements/generic_progress_indicator.da
import 'package:sendtrain/widgets/sessions/session_activities_editor.dart'; import 'package:sendtrain/widgets/sessions/session_activities_editor.dart';
import 'package:sendtrain/widgets/sessions/session_editor.dart'; import 'package:sendtrain/widgets/sessions/session_editor.dart';
import 'package:sendtrain/widgets/sessions/session_view_achievements.dart'; import 'package:sendtrain/widgets/sessions/session_view_achievements.dart';
import 'package:sendtrain/widgets/sessions/session_view_actions.dart';
import 'package:sendtrain/widgets/sessions/session_view_activities.dart'; import 'package:sendtrain/widgets/sessions/session_view_activities.dart';
import 'package:sendtrain/widgets/sessions/session_view_media.dart'; import 'package:sendtrain/widgets/sessions/session_view_media.dart';
@ -78,12 +79,12 @@ class _SessionViewState extends State<SessionView> {
distance: 70, distance: 70,
type: ExpandableFabType.up, type: ExpandableFabType.up,
overlayStyle: ExpandableFabOverlayStyle( overlayStyle: ExpandableFabOverlayStyle(
color: Colors.black.withOpacity(0.5), color: Colors.black.withValues(alpha: 0.5),
blur: 10, blur: 10,
), ),
children: [ children: [
FloatingActionButton.extended( FloatingActionButton.extended(
icon: const Icon(Icons.edit_outlined), icon: const Icon(Icons.military_tech_rounded),
label: Text('Add Achievement'), label: Text('Add Achievement'),
onPressed: () { onPressed: () {
showEditorSheet( showEditorSheet(
@ -93,7 +94,7 @@ class _SessionViewState extends State<SessionView> {
}, },
), ),
FloatingActionButton.extended( FloatingActionButton.extended(
icon: const Icon(Icons.edit_outlined), icon: const Icon(Icons.sports_gymnastics_rounded),
label: Text('Add Activity'), label: Text('Add Activity'),
onPressed: () { onPressed: () {
showEditorSheet( showEditorSheet(
@ -104,7 +105,7 @@ class _SessionViewState extends State<SessionView> {
), ),
FloatingActionButton.extended( FloatingActionButton.extended(
icon: const Icon(Icons.edit_outlined), icon: const Icon(Icons.edit_outlined),
label: Text('Edit'), label: Text('Edit Session'),
onPressed: () { onPressed: () {
showEditorSheet( showEditorSheet(
context, context,
@ -114,7 +115,7 @@ class _SessionViewState extends State<SessionView> {
), ),
FloatingActionButton.extended( FloatingActionButton.extended(
icon: const Icon(Icons.history_outlined), icon: const Icon(Icons.history_outlined),
label: Text('Restart'), label: Text('Restart Session'),
onPressed: () { onPressed: () {
Session newSession = Session newSession =
session.copyWith(status: SessionStatus.pending); session.copyWith(status: SessionStatus.pending);
@ -131,7 +132,7 @@ class _SessionViewState extends State<SessionView> {
), ),
FloatingActionButton.extended( FloatingActionButton.extended(
icon: const Icon(Icons.done_all_outlined), icon: const Icon(Icons.done_all_outlined),
label: Text('Done'), label: Text('Finish Session'),
onPressed: () { onPressed: () {
Session newSession = Session newSession =
session.copyWith(status: SessionStatus.completed); session.copyWith(status: SessionStatus.completed);
@ -178,13 +179,20 @@ class _SessionViewState extends State<SessionView> {
fontSize: 20, fontWeight: FontWeight.bold), fontSize: 20, fontWeight: FontWeight.bold),
'Media:')), 'Media:')),
SessionViewMedia(session: session), SessionViewMedia(session: session),
// const Padding(
// padding: EdgeInsets.fromLTRB(15, 30, 0, 10),
// child: Text(
// style: TextStyle(
// fontSize: 20, fontWeight: FontWeight.bold),
// 'Activites:')),
// SessionViewActivities(session: session),
const Padding( const Padding(
padding: EdgeInsets.fromLTRB(15, 30, 0, 10), padding: EdgeInsets.fromLTRB(15, 30, 0, 10),
child: Text( child: Text(
style: TextStyle( style: TextStyle(
fontSize: 20, fontWeight: FontWeight.bold), fontSize: 20, fontWeight: FontWeight.bold),
'Activites:')), 'Actions:')),
SessionViewActivities(session: session), SessionViewActions(session: session)
], ],
)); ));
} else { } else {

View File

@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:sendtrain/daos/action_sets_dao.dart';
import 'package:sendtrain/database/database.dart';
class SessionViewActions extends StatelessWidget {
SessionViewActions({super.key, required this.session});
final Session session;
// final List actions;
final ItemScrollController itemScrollController = ItemScrollController();
final ScrollOffsetController scrollOffsetController =
ScrollOffsetController();
final ItemPositionsListener itemPositionsListener =
ItemPositionsListener.create();
final ScrollOffsetListener scrollOffsetListener =
ScrollOffsetListener.create();
@override
Widget build(BuildContext context) {
return FutureBuilder<List>(
future: ActionSetsDao(Provider.of<AppDatabase>(context))
.fromSession(session),
builder: (context, snapshot) {
if (snapshot.hasData) {
List<ActionSet> actionSets = snapshot.data! as List<ActionSet>;
return Text(actionSets.first.name);
} else {
return Container(
alignment: Alignment.center,
child: SizedBox(
height: 50.0,
width: 50.0,
child: CircularProgressIndicator(),
));
}
});
}
}

View File

@ -4,6 +4,7 @@ import 'package:sendtrain/daos/activities_dao.dart';
import 'package:sendtrain/database/database.dart'; import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/helpers/widget_helpers.dart'; import 'package:sendtrain/helpers/widget_helpers.dart';
import 'package:sendtrain/widgets/activities/activity_card.dart'; import 'package:sendtrain/widgets/activities/activity_card.dart';
import 'package:sendtrain/widgets/generic/elements/add_card_generic.dart';
import 'package:sendtrain/widgets/generic/elements/generic_progress_indicator.dart'; import 'package:sendtrain/widgets/generic/elements/generic_progress_indicator.dart';
import 'package:sendtrain/widgets/sessions/session_activities_editor.dart'; import 'package:sendtrain/widgets/sessions/session_activities_editor.dart';
@ -32,36 +33,21 @@ class _SessionViewActivitiesState extends State<SessionViewActivities> {
padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
itemCount: activities.length, itemCount: activities.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
return ActivityCard(activity: activities[index]); return ActivityCard(
activity: activities[index], session: widget.session);
}, },
)); ));
} else { } else {
return Expanded( return AddCardGeneric(
child: ListView( title: 'Add an Activity!',
padding: const EdgeInsets.fromLTRB(10, 0, 10, 0), description:
children: [ 'Here you can associate one or more activities that you can follow along with during your session.',
Card.outlined( action: () {
child: InkWell( showEditorSheet(
customBorder: RoundedRectangleBorder( context,
borderRadius: BorderRadius.circular(10), SessionActivitiesEditor(
), session: widget.session, callback: () {}));
onTap: () { });
showEditorSheet(
context,
SessionActivitiesEditor(
session: widget.session,
callback: () {}));
},
child: ListTile(
contentPadding: EdgeInsets.only(
top: 5, left: 15, right: 5, bottom: 5),
autofocus: true,
leading: Icon(Icons.add_box_rounded),
title: Text('Add an Activity!'),
subtitle: Text(
'Here you can associate one or more activities that you can follow along with during your session.'),
)))
]));
} }
} else { } else {
return GenericProgressIndicator(); return GenericProgressIndicator();

View File

@ -32,7 +32,7 @@ class _SessionViewMediaState extends State<SessionViewMedia> {
List<Widget> content; List<Widget> content;
if (mediaItems.isNotEmpty) { if (mediaItems.isNotEmpty) {
List<Widget> mediaCards = List.generate(mediaItems.length, List<Widget> mediaCards = List.generate(mediaItems.length,
(i) => MediaCard(media: mediaItems[i], callback: resetState)); (i) => MediaCard(media: mediaItems[i], callback: resetState, canDelete: true));
content = mediaCards; content = mediaCards;
} else { } else {
content = [ content = [

View File

@ -51,6 +51,9 @@ dependencies:
mime: ^2.0.0 mime: ^2.0.0
video_player: ^2.9.2 video_player: ^2.9.2
dart_casing: ^3.0.1 dart_casing: ^3.0.1
collection: ^1.18.0
flutter_sound: ^9.23.1
vibration: ^3.1.2
flutter_launcher_name: flutter_launcher_name:
name: "SendTrain" name: "SendTrain"
@ -86,6 +89,7 @@ flutter:
# - images/a_dot_burr.jpeg # - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg # - images/a_dot_ham.jpeg
assets: assets:
- assets/audio/
- assets/images/ - assets/images/
- assets/exercises.json - assets/exercises.json

View File

@ -22,6 +22,26 @@ import 'schema_v16.dart' as v16;
import 'schema_v17.dart' as v17; import 'schema_v17.dart' as v17;
import 'schema_v18.dart' as v18; import 'schema_v18.dart' as v18;
import 'schema_v19.dart' as v19; import 'schema_v19.dart' as v19;
import 'schema_v20.dart' as v20;
import 'schema_v21.dart' as v21;
import 'schema_v22.dart' as v22;
import 'schema_v23.dart' as v23;
import 'schema_v24.dart' as v24;
import 'schema_v25.dart' as v25;
import 'schema_v26.dart' as v26;
import 'schema_v27.dart' as v27;
import 'schema_v28.dart' as v28;
import 'schema_v29.dart' as v29;
import 'schema_v30.dart' as v30;
import 'schema_v31.dart' as v31;
import 'schema_v32.dart' as v32;
import 'schema_v33.dart' as v33;
import 'schema_v34.dart' as v34;
import 'schema_v35.dart' as v35;
import 'schema_v36.dart' as v36;
import 'schema_v37.dart' as v37;
import 'schema_v39.dart' as v39;
import 'schema_v40.dart' as v40;
class GeneratedHelper implements SchemaInstantiationHelper { class GeneratedHelper implements SchemaInstantiationHelper {
@override @override
@ -65,6 +85,46 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v18.DatabaseAtV18(db); return v18.DatabaseAtV18(db);
case 19: case 19:
return v19.DatabaseAtV19(db); return v19.DatabaseAtV19(db);
case 20:
return v20.DatabaseAtV20(db);
case 21:
return v21.DatabaseAtV21(db);
case 22:
return v22.DatabaseAtV22(db);
case 23:
return v23.DatabaseAtV23(db);
case 24:
return v24.DatabaseAtV24(db);
case 25:
return v25.DatabaseAtV25(db);
case 26:
return v26.DatabaseAtV26(db);
case 27:
return v27.DatabaseAtV27(db);
case 28:
return v28.DatabaseAtV28(db);
case 29:
return v29.DatabaseAtV29(db);
case 30:
return v30.DatabaseAtV30(db);
case 31:
return v31.DatabaseAtV31(db);
case 32:
return v32.DatabaseAtV32(db);
case 33:
return v33.DatabaseAtV33(db);
case 34:
return v34.DatabaseAtV34(db);
case 35:
return v35.DatabaseAtV35(db);
case 36:
return v36.DatabaseAtV36(db);
case 37:
return v37.DatabaseAtV37(db);
case 39:
return v39.DatabaseAtV39(db);
case 40:
return v40.DatabaseAtV40(db);
default: default:
throw MissingSchemaException(version, versions); throw MissingSchemaException(version, versions);
} }
@ -89,6 +149,26 @@ class GeneratedHelper implements SchemaInstantiationHelper {
16, 16,
17, 17,
18, 18,
19 19,
20,
21,
22,
23,
24,
25,
26,
27,
28,
29,
30,
31,
32,
33,
34,
35,
36,
37,
39,
40
]; ];
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff