Compare commits

...

39 Commits
dao ... main

Author SHA1 Message Date
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
Joshua Burman
9f5fb0d1ad edited tones, added vibrate 2025-02-11 10:32:40 -05:00
Joshua Burman
23663f484b added sound during countdown, and upgraded minsdkversion 2025-02-10 17:08:13 -05:00
Joshua Burman
60bc571987 action display, and full display, started action editor 2025-01-24 16:15:22 -05:00
Joshua Burman
0cf62ec4b4 add new db fields to action 2025-01-07 18:41:50 -05:00
Joshua Burman
2288cba78e actvity card spacing 2025-01-07 15:56:51 -05:00
Joshua Burman
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
Joshua Burman
d9a5599a4b activity categories ui 2025-01-07 15:49:44 -05:00
Joshua Burman
0b0ca884bc genericizing add card, activity ui tweaks 2025-01-07 12:37:17 -05:00
Joshua Burman
8ec531c0ea activity read 2025-01-06 16:46:22 -05:00
Joshua Burman
6917754933 acivity description management, seed data mods 2025-01-06 14:47:14 -05:00
Joshua Burman
ebca90e69a action prep, activity association removal, activity ui tweaks 2025-01-06 10:10:08 -05:00
7ead6ba631 Merge pull request 'session crud' (#5) from session-crud into main
Reviewed-on: #5
2025-01-05 19:02:13 -08:00
Joshua Burman
a034c16160 imported excises, and modified seed/database to accomodate 2025-01-05 22:01:38 -05:00
Joshua Burman
acab37eb60 functional activity addition, bug to only remove activity action and date selection need to be fixed 2025-01-05 15:07:36 -05:00
Joshua Burman
fec4eaaf92 cleanup 2025-01-05 12:15:35 -05:00
Joshua Burman
ecc9aa3abc DRY up some search and places code 2025-01-05 00:45:27 -05:00
Joshua Burman
95701c73a6 proper list view clipping 2025-01-03 22:03:51 -05:00
Joshua Burman
2eb5d8f171 activity ui and search prep 2025-01-03 21:49:09 -05:00
Joshua Burman
6e0b1263ba default activity selector 2025-01-03 21:11:41 -05:00
Joshua Burman
9fc5fb5d22 media default and content 2025-01-03 20:37:45 -05:00
Joshua Burman
32826abcea achievement crud complete 2025-01-03 18:20:11 -05:00
Joshua Burman
2206720810 migration to move achievements to session, prep for achievement and media management 2025-01-02 19:50:29 -05:00
Joshua Burman
48f716cdb0 db changes, seed changes, media view work for local images, and video prep, initial achievement work 2025-01-02 13:29:13 -05:00
Joshua Burman
e78788d67a modified db for local images, added file list saving functionality 2025-01-01 23:42:46 -05:00
Joshua Burman
e36d2a837a migrate 7 2024-12-31 22:42:26 -05:00
Joshua Burman
10332ec8be media item and session images and location management, also refactoring and DRYing up code 2024-12-31 22:41:17 -05:00
Joshua Burman
5f628d6b48 location image search and population for new session 2024-12-30 01:54:51 -05:00
Joshua Burman
afe633e697 further refactoring and location search dev 2024-12-28 12:41:57 -05:00
Joshua Burman
8e0ec614a0 further refactoring, conversion to stream 2024-12-27 20:59:48 -05:00
Joshua Burman
fa374a5bc2 further import issues 2024-12-27 16:13:42 -05:00
Joshua Burman
26d9386812 import statement issues 2024-12-27 16:11:04 -05:00
Joshua Burman
722a152130 big refactor #1 2024-12-27 15:59:09 -05:00
Joshua Burman
cd8da31f4b basic session create and delete 2024-12-26 01:20:55 -05:00
Joshua Burman
029f037f90 session and activity delete 2024-12-24 22:38:25 -05:00
3c2f2e9bae Merge pull request 'DAO' (#4) from dao into main
Reviewed-on: #4
2024-12-24 14:00:49 -08:00
137 changed files with 107988 additions and 1357 deletions

1
android/app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1 @@
-keep class androidx.lifecycle.DefaultLifecycleObserver

View File

@ -1,4 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-feature android:name="android.hardware.location.network" android:required="false" />
<uses-feature android:name="android.hardware.location.gps" android:required="false" />
<uses-permission android:name="android.permission.INTERNET"/>
<application
android:label="sendtrain"
android:name="${applicationName}"
@ -17,12 +20,12 @@
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
@ -31,6 +34,8 @@
android:name="flutterEmbedding"
android:value="2" />
</application>
<meta-data android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyBCjMCEAyyNVpsnVYvZj6VL1mmB98Vd6AE" />
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
@ -38,8 +43,9 @@
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
<action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" />
</intent>
</queries>
</manifest>
</manifest>

Binary file not shown.

BIN
assets/audio/count_tone.mp3 Normal file

Binary file not shown.

22617
assets/exercises.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,6 @@
import UIKit
import Flutter
import GoogleMaps
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
@ -7,7 +8,8 @@ import Flutter
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GMSServices.provideAPIKey("AIzaSyBCjMCEAyyNVpsnVYvZj6VL1mmB98Vd6AE")
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
}

View File

@ -2,6 +2,13 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>remote-notification</string>
</array>
<key>NSPhotoLibraryUsageDescription</key>
<string>The Photo library is used when selecting a photo to upload as media for a Session</string>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>

View File

@ -15,7 +15,7 @@ class ActionsDao extends DatabaseAccessor<AppDatabase> with _$ActionsDaoMixin {
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(
[
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())
.map((e) => e.readTable(db.actions))
@ -32,4 +33,32 @@ class ActionsDao extends DatabaseAccessor<AppDatabase> with _$ActionsDaoMixin {
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

@ -4,7 +4,8 @@ import 'package:sendtrain/database/database.dart';
part 'activities_dao.g.dart';
@DriftAccessor(tables: [Activities])
class ActivitiesDao extends DatabaseAccessor<AppDatabase> with _$ActivitiesDaoMixin {
class ActivitiesDao extends DatabaseAccessor<AppDatabase>
with _$ActivitiesDaoMixin {
ActivitiesDao(super.db);
Future<List<Activity>> all() async {
@ -12,24 +13,77 @@ class ActivitiesDao extends DatabaseAccessor<AppDatabase> with _$ActivitiesDaoMi
}
Future<Activity> find(int id) async {
return await (select(activities)..where((activity) => activity.id.equals(id) )).getSingle();
return await (select(activities)
..where((activity) => activity.id.equals(id)))
.getSingle();
}
Future<List<Activity>> sessionActivities(int id) async {
final result = select(db.sessionActivities).join(
Future remove(Activity activity) => delete(activities).delete(activity);
Future<List<Activity>> contains(value) async {
return (select(activities)
..where((t) =>
t.title.contains(value) | t.description.contains(value) | t.category.contains(value)))
.get();
}
Future<List<Activity>> activitiesFromSession(int id) async {
final result = select(db.sessionActivities).join(
[
innerJoin(
db.activities,
db.activities.id
.equalsExp(db.sessionActivities.activityId),
db.activities.id.equalsExp(db.sessionActivities.activityId),
),
],
)..where(db.sessionActivities.sessionId.equals(id));
final activities = (await result.get())
.map((e) => e.readTable(db.activities))
.toList();
final activities =
(await result.get()).map((e) => e.readTable(db.activities)).toList();
return activities;
}
}
Stream<List<Activity>> watchSessionActivities(int id) {
final query = select(db.sessionActivities).join(
[
innerJoin(
db.activities,
db.activities.id.equalsExp(db.sessionActivities.activityId),
),
],
)..where(db.sessionActivities.sessionId.equals(id));
return query.watch().map((rows) {
final activities = (rows).map((e) => e.readTable(db.activities)).toList();
return activities;
});
}
// MultiSelectable<SessionActivity> _selectableSessionActivities(int id) {
// // return select(db.sessionActivities)..limit(1, offset: 1);
// // final query = select(db.sessionActivities).join(
// // [
// // innerJoin(
// // db.activities,
// // db.activities.id.equalsExp(db.sessionActivities.activityId),
// // ),
// // ],
// // )..where(db.sessionActivities.sessionId.equals(id));
// // return query;
// final query = select(db.sessionActivities)..where((row) => row.sessionId.equals(id));
// query.join(
// [
// innerJoin(
// db.activities,
// db.activities.id.equalsExp(db.sessionActivities.activityId),
// ),
// ],
// );
// return query;
// }
}

View File

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

View File

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

View File

@ -4,15 +4,21 @@ import 'package:sendtrain/database/database.dart';
part 'media_items_dao.g.dart';
@DriftAccessor(tables: [MediaItems])
class MediaItemsDao extends DatabaseAccessor<AppDatabase> with _$MediaItemsDaoMixin {
class MediaItemsDao extends DatabaseAccessor<AppDatabase>
with _$MediaItemsDaoMixin {
MediaItemsDao(super.db);
Future createOrUpdate(MediaItemsCompanion mediaItem) =>
into(mediaItems).insertOnConflictUpdate(mediaItem);
Future<List<MediaItem>> all() async {
return await select(mediaItems).get();
}
Future<MediaItem> find(int id) async {
return await (select(mediaItems)..where((mediaItem) => mediaItem.id.equals(id) )).getSingle();
return await (select(mediaItems)
..where((mediaItem) => mediaItem.id.equals(id)))
.getSingle();
}
Future<List<MediaItem>> fromActivity(Activity activity) async {
@ -24,18 +30,16 @@ class MediaItemsDao extends DatabaseAccessor<AppDatabase> with _$MediaItemsDaoMi
),
],
)
..where(
db.objectMediaItems.objectType.equals(ObjectType.activities.name))
..where(db.objectMediaItems.objectType.equals(ObjectType.activities.name))
..where(db.objectMediaItems.objectId.equals(activity.id));
final mediaItems = (await result.get())
.map((e) => e.readTable(db.mediaItems))
.toList();
final mediaItems =
(await result.get()).map((e) => e.readTable(db.mediaItems)).toList();
return mediaItems;
}
Future<List<MediaItem>> fromSession(Session session) async {
Future<List<MediaItem>> fromSession(int sessionId) async {
final result = select(db.objectMediaItems).join(
[
innerJoin(
@ -44,14 +48,37 @@ class MediaItemsDao extends DatabaseAccessor<AppDatabase> with _$MediaItemsDaoMi
),
],
)
..where(
db.objectMediaItems.objectType.equals(ObjectType.sessions.name))
..where(db.objectMediaItems.objectId.equals(session.id));
..where(db.objectMediaItems.objectType.equals(ObjectType.sessions.name))
..where(db.objectMediaItems.objectId.equals(sessionId));
final mediaItems = (await result.get())
.map((e) => e.readTable(db.mediaItems))
.toList();
final mediaItems =
(await result.get()).map((e) => e.readTable(db.mediaItems)).toList();
return mediaItems;
}
Stream<List<MediaItem>> watchSessionMediaItems(int id) {
final query = select(db.objectMediaItems).join(
[
innerJoin(
db.mediaItems,
db.mediaItems.id.equalsExp(db.objectMediaItems.mediaId),
),
],
)
..where(db.objectMediaItems.objectType.equals(ObjectType.sessions.name))
..where(db.objectMediaItems.objectId.equals(id));
return query.watch().map((rows) {
final mediaItems = (rows).map((e) => e.readTable(db.mediaItems)).toList();
return mediaItems;
});
}
Future remove(MediaItem mediaItem) => delete(mediaItems).delete(mediaItem);
Future removeAll(Iterable<int> mediaItemIds) {
return (delete(mediaItems)..where((table) => table.id.isIn(mediaItemIds)))
.go();
}
}

View File

@ -0,0 +1,19 @@
import 'package:drift/drift.dart';
import 'package:sendtrain/database/database.dart';
part 'object_media_items_dao.g.dart';
@DriftAccessor(tables: [ObjectMediaItems])
class ObjectMediaItemsDao extends DatabaseAccessor<AppDatabase> with _$ObjectMediaItemsDaoMixin {
ObjectMediaItemsDao(super.db);
Future createOrUpdate(ObjectMediaItemsCompanion objectMediaItem) => into(objectMediaItems).insertOnConflictUpdate(objectMediaItem);
Future<List<ObjectMediaItem>> all() async {
return await select(objectMediaItems).get();
}
Future<ObjectMediaItem> find(int id) async {
return await (select(objectMediaItems)..where((objectMediaItem) => objectMediaItem.id.equals(id) )).getSingle();
}
}

View File

@ -0,0 +1,10 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'object_media_items_dao.dart';
// ignore_for_file: type=lint
mixin _$ObjectMediaItemsDaoMixin on DatabaseAccessor<AppDatabase> {
$MediaItemsTable get mediaItems => attachedDatabase.mediaItems;
$ObjectMediaItemsTable get objectMediaItems =>
attachedDatabase.objectMediaItems;
}

View File

@ -4,15 +4,21 @@ import 'package:sendtrain/database/database.dart';
part 'session_activities_dao.g.dart';
@DriftAccessor(tables: [SessionActivities])
class SessionActivitiesDao extends DatabaseAccessor<AppDatabase> with _$SessionActivitiesDaoMixin {
class SessionActivitiesDao extends DatabaseAccessor<AppDatabase>
with _$SessionActivitiesDaoMixin {
SessionActivitiesDao(super.db);
Future createOrUpdate(SessionActivitiesCompanion sessionActivity) =>
into(sessionActivities).insertOnConflictUpdate(sessionActivity);
Future<List<SessionActivity>> all() async {
return await select(sessionActivities).get();
}
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 {
@ -21,4 +27,16 @@ class SessionActivitiesDao extends DatabaseAccessor<AppDatabase> with _$SessionA
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,11 +7,11 @@ part 'sessions_dao.g.dart';
class SessionsDao extends DatabaseAccessor<AppDatabase> with _$SessionsDaoMixin {
SessionsDao(super.db);
Future<List<Session>> all() async {
return await select(sessions).get();
}
Future<Session> find(int id) async {
return await (select(sessions)..where((session) => session.id.equals(id) )).getSingle();
}
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);
}

View File

@ -4,6 +4,7 @@ import 'package:sendtrain/daos/actions_dao.dart';
import 'package:sendtrain/daos/activities_dao.dart';
import 'package:sendtrain/daos/activity_actions_dao.dart';
import 'package:sendtrain/daos/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/sessions_dao.dart';
import 'package:sendtrain/database/seed.dart';
@ -22,6 +23,7 @@ part 'database.g.dart';
SessionsDao,
ActivitiesDao,
MediaItemsDao,
ObjectMediaItemsDao,
SessionActivitiesDao,
ActivityActionsDao,
ActionsDao
@ -33,7 +35,7 @@ class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection());
@override
int get schemaVersion => 4;
int get schemaVersion => 35;
@override
MigrationStrategy get migration {
@ -64,6 +66,8 @@ class Sessions extends Table {
TextColumn get title => text().withLength(min: 3, max: 32)();
TextColumn get content => text().named('body')();
TextColumn get status => textEnum<SessionStatus>()();
TextColumn get achievements => text().nullable()();
TextColumn get address => text().withLength(min: 3, max: 256).nullable()();
DateTimeColumn get date => dateTime().nullable()();
DateTimeColumn get createdAt =>
dateTime().withDefault(Variable(DateTime.now()))();
@ -71,11 +75,12 @@ class Sessions extends Table {
class SessionActivities extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get sessionId => integer().references(Sessions, #id)();
IntColumn get activityId => integer().references(Activities, #id)();
IntColumn get sessionId =>
integer().references(Sessions, #id, onDelete: KeyAction.cascade)();
IntColumn get activityId =>
integer().references(Activities, #id, onDelete: KeyAction.cascade)();
IntColumn get position => integer()();
TextColumn get results => text().nullable()();
TextColumn get achievements => text().nullable()();
DateTimeColumn get createdAt =>
dateTime().withDefault(Variable(DateTime.now()))();
}
@ -84,40 +89,105 @@ enum ActivityCategories { fundamentals, conditioning, advanced, custom, pro }
enum ActivityType {
strength,
power,
conditioning,
hypertrophy,
endurance,
stability,
mobility,
flexibility,
rehabilitation,
technical
stretching,
plyometrics,
strongman,
powerlifting,
cardio,
olympicWeightlifting
}
enum ActivityLevel { beginner, intermediate, expert }
enum ActivityMechanic { compound, isolation }
enum ActivityEquipment {
bodyOnly,
machine,
other,
foamRoll,
kettlebells,
dumbbell,
cable,
barbell,
bands,
medicineBall,
exerciseBall,
eZCurlBar
}
enum ActivityMuscle {
abdominals,
hamstrings,
calves,
shoulders,
adductors,
glutes,
quadriceps,
biceps,
forearms,
abductors,
triceps,
chest,
lowerBack,
traps,
middleBack,
lats,
neck
}
class Activities extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get title => text().withLength(min: 3, max: 32)();
TextColumn get type => textEnum<ActivityType>()();
TextColumn get description => text().named('body')();
TextColumn get category => textEnum<ActivityCategories>()();
TextColumn get title => text().withLength(min: 3, max: 100)();
TextColumn get type => textEnum<ActivityType>().nullable()();
TextColumn get description => text().named('body').nullable()();
TextColumn get category => textEnum<ActivityCategories>().nullable()();
// from exercises.json
TextColumn get force => text().nullable()();
TextColumn get level => textEnum<ActivityLevel>().nullable()();
TextColumn get mechanic => textEnum<ActivityMechanic>().nullable()();
TextColumn get equipment => textEnum<ActivityEquipment>().nullable()();
TextColumn get primaryMuscles => textEnum<ActivityMuscle>().nullable()();
TextColumn get secondaryMuscles => textEnum<ActivityMuscle>().nullable()();
DateTimeColumn get createdAt =>
dateTime().withDefault(Variable(DateTime.now()))();
}
class ActivityActions extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get activityId => integer().references(Activities, #id)();
IntColumn get actionId => integer().references(Actions, #id)();
IntColumn get activityId => integer().references(Activities, #id, onDelete: KeyAction.cascade)();
IntColumn get actionId =>
integer().references(Actions, #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 RepType { time, count }
enum ActionStatus { pending, started, paused, complete }
class Actions extends Table {
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')();
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()();
DateTimeColumn get createdAt =>
dateTime().withDefault(Variable(DateTime.now()))();
@ -133,18 +203,19 @@ class ObjectMediaItems extends Table {
IntColumn get id => integer().autoIncrement()();
IntColumn get objectId => integer()();
TextColumn get objectType => textEnum<ObjectType>()();
IntColumn get mediaId => integer().references(MediaItems, #id)();
IntColumn get mediaId =>
integer().references(MediaItems, #id, onDelete: KeyAction.cascade)();
DateTimeColumn get createdAt =>
dateTime().withDefault(Variable(DateTime.now()))();
}
enum MediaType { youtube, image }
enum MediaType { youtube, image, location, localImage, localVideo }
class MediaItems extends Table {
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 reference => text().withLength(min: 3, max: 256)();
TextColumn get reference => text()();
TextColumn get type => textEnum<MediaType>()();
DateTimeColumn get createdAt =>
dateTime().withDefault(Variable(DateTime.now()))();

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

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

@ -1,5 +1,7 @@
import 'dart:convert';
import 'dart:math';
import 'package:dart_casing/dart_casing.dart';
import 'package:flutter/services.dart' as root_bundle;
import 'package:drift/drift.dart';
import 'package:sendtrain/database/database.dart';
@ -7,24 +9,29 @@ Future<void> seedDb(AppDatabase database) async {
// seed data setup
final List<List> sessionValues = [
[
'Projecting @ Climbers Rock',
'Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.'
'Projecting',
'Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.',
'Climbers Rock Inc.'
],
[
'Moonboard @ Boardroom',
'Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.'
'Moonboard',
'Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.',
'Beta Bloc'
],
[
'Off-Wall Training',
'Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.'
'Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.',
'Climbers Rock Inc.'
],
[
'Climbing Outdoors',
'Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.'
'Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.',
'Gravity Hamilton'
],
[
'Volume Session @ Gravity',
'Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.'
'Volume Session',
'Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.',
'Up the Bloc'
],
];
@ -36,16 +43,106 @@ Future<void> seedDb(AppDatabase database) async {
['BgheYcxhrsw', MediaType.youtube]
];
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\": \"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 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\": \"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 totalActivities = 6;
final int totalActions = 5;
// final int totalActions = 5;
final int totalMedia = 5;
final random = Random();
final whitespaceRE = RegExp(r"(?! )\s+| \s+");
// we gotta build all the activities!
final jsondata =
await root_bundle.rootBundle.loadString('assets/exercises.json');
final exercises = json.decode(jsondata);
List<int> activityIds = [];
for (int i = 0; i < exercises.length; i++) {
var exercise = exercises[i];
var images = [];
if (exercise['images'] != null) {
for (int j = 0; j < exercise['images'].length; j++) {
var image = exercise['images'][j];
images.add(
"https://raw.githubusercontent.com/yuhonas/free-exercise-db/main/exercises/$image");
}
}
Map<Symbol, Value> payload = {
Symbol('title'): Value<String>(
exercise['name'].toString().trim().replaceAll(whitespaceRE, " ")),
Symbol('description'): Value<String>(json.encode(exercise['instructions']
.toString()
.trim()
.replaceAll(whitespaceRE, " "))),
Symbol('force'): Value<String>(exercise['force'] ?? "")
};
// well this fucking sucks
if (exercise['category'] != null) {
payload[Symbol('type')] = Value<ActivityType>(ActivityType.values
.firstWhere((e) =>
e.toString() ==
"ActivityType.${Casing.camelCase(exercise['category'])}"));
}
if (exercise['level'] != null) {
payload[Symbol('level')] = Value<ActivityLevel>(ActivityLevel.values
.firstWhere((e) =>
e.toString() ==
"ActivityLevel.${Casing.camelCase(exercise['level'])}"));
}
if (exercise['mechanic'] != null) {
payload[Symbol('mechanic')] = Value<ActivityMechanic>(
ActivityMechanic.values.firstWhere((e) =>
e.toString() ==
"ActivityMechanic.${Casing.camelCase(exercise['mechanic'])}"));
}
if (exercise['equipment'] != null) {
payload[Symbol('equipment')] = Value<ActivityEquipment>(
ActivityEquipment.values.firstWhere((e) =>
e.toString() ==
"ActivityEquipment.${Casing.camelCase(exercise['equipment'])}"));
}
if (exercise['primaryMuscles'].isNotEmpty) {
payload[Symbol('primaryMuscles')] = Value<ActivityMuscle>(
ActivityMuscle.values.firstWhere((e) =>
e.toString() ==
"ActivityMuscle.${Casing.camelCase(exercise['primaryMuscles'].first)}"));
}
if (exercise['secondaryMuscles'].isNotEmpty) {
payload[Symbol('secondaryMuscles')] = Value<ActivityMuscle>(
ActivityMuscle.values.firstWhere((e) =>
e.toString() ==
"ActivityMuscle.${Casing.camelCase(exercise['secondaryMuscles'].first)}"));
}
activityIds.add(await database
.into(database.activities)
.insert(Function.apply(ActivitiesCompanion.new, [], payload))
.then((activityId) async {
for (int m = 0; m < images.length; m++) {
final mediaItem = images[m];
await database
.into(database.mediaItems)
.insert(MediaItemsCompanion.insert(
title: exercise['name'],
description: exercise['name'],
reference: mediaItem,
type: MediaType.image))
.then((mediaId) async {
await database.into(database.objectMediaItems).insert(
ObjectMediaItemsCompanion.insert(
objectId: activityId,
mediaId: mediaId,
objectType: ObjectType.activities));
});
}
return activityId;
}));
}
// seed loop
for (int i = 0; i < totalSessions; i++) {
@ -61,87 +158,76 @@ Future<void> seedDb(AppDatabase database) async {
title: sessionValue[0],
content: sessionValue[1],
status: status,
address: Value(sessionValue[2]),
achievements: Value(
"[\"achievement 1\", \"achievement 2\", \"achievement 3\"]"),
date: Value(DateTime.now())))
.then((sessionId) async {
// activities things
for (int j = 0; j <= random.nextInt(totalActivities); j++) {
int activityId = random.nextInt(activityIds.length);
activityIds.removeAt(activityId);
await database
.into(database.activities)
.insert(ActivitiesCompanion.insert(
title: "Test activity $j",
type: ActivityType
.values[random.nextInt(ActivityType.values.length)],
description:
"$j Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.",
category: ActivityCategories
.values[random.nextInt(ActivityCategories.values.length)]))
.then((activityId) async {
// session activity relationships
await database
.into(database.sessionActivities)
.insert(SessionActivitiesCompanion.insert(
sessionId: sessionId,
activityId: activityId,
position: j,
results: Value("results json, will need to test"),
achievements: Value("comma, seperated, items"),
));
.into(database.sessionActivities)
.insert(SessionActivitiesCompanion.insert(
sessionId: sessionId,
activityId: activityId,
position: j,
results: Value("results json, will need to test"),
));
// actions
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.',
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 l = 0; l <= random.nextInt(totalMedia); l++) {
final mediaItem = mediaItems[random.nextInt(mediaItems.length)];
await database
.into(database.mediaItems)
.insert(MediaItemsCompanion.insert(
title: 'Media title $l',
description:
'Media description $l 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[0],
type: mediaItem[1]))
.then((mediaId) async {
await database.into(database.objectMediaItems).insert(
ObjectMediaItemsCompanion.insert(
objectId: actionId,
mediaId: mediaId,
objectType: ObjectType.actions));
});
}
});
}
for (int m = 0; m <= random.nextInt(totalMedia); m++) {
final mediaItem = mediaItems[random.nextInt(mediaItems.length)];
await database
.into(database.mediaItems)
.insert(MediaItemsCompanion.insert(
title: 'Media title $m',
description:
'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[0],
type: mediaItem[1]))
.then((mediaId) async {
await database.into(database.objectMediaItems).insert(
ObjectMediaItemsCompanion.insert(
objectId: activityId,
mediaId: mediaId,
objectType: ObjectType.activities));
});
}
});
// 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++) {
@ -162,6 +248,23 @@ Future<void> seedDb(AppDatabase database) async {
objectType: ObjectType.sessions));
});
}
await database
.into(database.mediaItems)
.insert(MediaItemsCompanion.insert(
title: 'Locations details',
description:
'5155 Harvester Rd #1, Burlington, ON L7L 6V2, Canada',
reference:
'https://lh3.googleusercontent.com/places/ANXAkqHwtb5oRMGG3haJkaHeTxdTI1lQ17RgvkCXwzA1dGV53BXPbHrdXIs1mLC_-4exyRW8dbYhMOeiOCHJqGeVBx-dNtABZAl9tQA=s4800-w800',
type: MediaType.location))
.then((mediaId) async {
await database.into(database.objectMediaItems).insert(
ObjectMediaItemsCompanion.insert(
objectId: sessionId,
mediaId: mediaId,
objectType: ObjectType.sessions));
});
});
}
}

View File

@ -0,0 +1,16 @@
import 'package:intl/intl.dart';
final DateFormat dateFormat = DateFormat('yyyy-MM-dd');
String formattedTime(int timeInSecond) {
int sec = timeInSecond % 60;
int min = (timeInSecond / 60).floor();
String minute = min.toString().length <= 1 ? "0$min" : "$min";
String second = sec.toString().length <= 1 ? "0$sec" : "$sec";
return "$minute:$second";
}
int toSeconds(int milliseconds) {
int sec = (milliseconds / 1000).floor();
return sec;
}

View File

@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
import 'package:sendtrain/database/database.dart';
ImageProvider findMediaByType(List<MediaItem> media, MediaType type) {
Iterable<MediaItem>? found = media.where((m) => m.type == type);
Image image;
if (found.isNotEmpty) {
image = Image.network(found.first.reference);
} else {
// Element is not found
image = Image.asset('assets/images/placeholder.jpg');
}
return image.image;
}

View File

@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/widgets/media/media_details.dart';
showMediaDetailWidget(BuildContext context, MediaItem media) {
showEditorSheet(context, MediaDetails(media: media));
}
showGenericSheet(BuildContext context, Widget widget,
[Color? backgroundColor]) {
backgroundColor ??= Theme.of(context).colorScheme.surfaceBright;
showModalBottomSheet<void>(
backgroundColor: backgroundColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10.0), topRight: Radius.circular(10.0)),
),
context: context,
showDragHandle: true,
isScrollControlled: true,
useSafeArea: true,
builder: (BuildContext context) {
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

@ -1,20 +1,38 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/helpers/widget_helpers.dart';
import 'package:sendtrain/models/activity_timer_model.dart';
import 'package:sendtrain/screens/activities_screen.dart';
import 'package:sendtrain/screens/sessions_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';
// ignore: unused_import
import 'package:sendtrain/database/seed.dart';
import 'package:sendtrain/widgets/sessions/session_editor.dart';
class SendTrain extends StatelessWidget {
const SendTrain({super.key});
@override
Widget build(BuildContext context) {
final ThemeData themeData = ThemeData.dark(useMaterial3: true);
return MaterialApp(
title: "Sendtrain",
theme: ThemeData.dark(useMaterial3: true),
theme: themeData.copyWith(
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
floatingActionButtonTheme: FloatingActionButtonThemeData(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12))),
inputDecorationTheme: InputDecorationTheme(
fillColor: themeData.colorScheme.surface,
),
),
home: const App());
}
}
@ -61,6 +79,10 @@ class _AppState extends State<App> {
alignment: Alignment.center,
child: const Text('In Progress...'),
),
Container(
alignment: Alignment.center,
child: const Text('Profile in Progress...'),
),
][currentPageIndex]),
bottomNavigationBar: NavigationBar(
onDestinationSelected: (int index) {
@ -73,32 +95,36 @@ class _AppState extends State<App> {
NavigationDestination(
icon: Icon(Icons.sports), label: "Sessions"),
NavigationDestination(
icon: Icon(Icons.landscape), label: "Activities"),
icon: Icon(Icons.sports_gymnastics_rounded),
label: "Activities"),
NavigationDestination(
icon: Icon(Icons.calendar_month_rounded), label: "Plan"),
NavigationDestination(
icon: Icon(Icons.group), label: "Team Send"),
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(
onPressed: () {
// Add your onPressed code here!
showEditorSheet(context, SessionEditor());
},
label: const Text('New Session'),
icon: const Icon(Icons.add_chart),
backgroundColor: Colors.deepPurple,
// backgroundColor: Colors.deepPurple,
));
}
}
void main() {
var db = AppDatabase();
runApp(MultiProvider(
providers: [
ChangeNotifierProvider(create: (context) => ActivityTimerModel()),
Provider<AppDatabase>(
create: (context) => AppDatabase(),
dispose: (context, db) => db.close()),
create: (context) => db, dispose: (context, db) => db.close()),
ChangeNotifierProvider(create: (context) => ActivityTimerModel()),
ChangeNotifierProvider(create: (context) => ActionTimer()),
],
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

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

View File

@ -0,0 +1,12 @@
class GooglePlaceModel {
final String placeId;
final String description;
final String address;
final List<dynamic>? imageReferences;
GooglePlaceModel(
{required this.placeId,
required this.description,
required this.address,
this.imageReferences});
}

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

@ -0,0 +1,68 @@
import 'dart:async';
typedef _Debounceable<S, T> = Future<S?> Function(T parameter);
class Debouncer {
Debouncer(this._duration, this._callback);
final Duration _duration;
final dynamic _callback;
late final _Debounceable<dynamic, String> _debouncedSearch = _debounce<dynamic, String>(_callback);
/// Returns a new function that is a debounced version of the given function.
///
/// This means that the original function will be called only after no calls
/// have been made for the given Duration.
_Debounceable<S, T> _debounce<S, T>(_Debounceable<S?, T> function) {
_DebounceTimer? debounceTimer;
return (T parameter) async {
if (debounceTimer != null && !debounceTimer!.isCompleted) {
debounceTimer!.cancel();
}
debounceTimer = _DebounceTimer(_duration);
try {
await debounceTimer!.future;
} on _CancelException {
return null;
}
return function(parameter);
};
}
process(data) {
return _debouncedSearch(data);
}
}
// A wrapper around Timer used for debouncing.
class _DebounceTimer {
final Duration debounceDuration;
_DebounceTimer(
this.debounceDuration
) {
_timer = Timer(debounceDuration, _onComplete);
}
late final Timer _timer;
final Completer<void> _completer = Completer<void>();
void _onComplete() {
_completer.complete();
}
Future<void> get future => _completer.future;
bool get isCompleted => _completer.isCompleted;
void cancel() {
_timer.cancel();
_completer.completeError(const _CancelException());
}
}
// An exception indicating that the timer was canceled.
class _CancelException implements Exception {
const _CancelException();
}

View File

@ -0,0 +1,43 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:sendtrain/daos/activities_dao.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/helpers/widget_helpers.dart';
import 'package:sendtrain/widgets/generic/elements/form_search_input.dart';
class ActivityFinderService {
final BuildContext context;
final ActivitiesDao dao;
ActivityFinderService(this.context)
: dao = ActivitiesDao(Provider.of<AppDatabase>(context, listen: false));
void finish() {}
Future<List<Suggestion>?> fetchSuggestions(String input) async {
List<Activity> activities = await dao.contains(input);
if (activities.isNotEmpty) {
return activities
.map<Suggestion>((activity) => Suggestion<Activity>(activity))
.toList();
} else {
return null;
}
}
Widget resultWidget(Activity activity, Function? callback) {
return ListTile(
title: Text(activity.title),
subtitle: Text(jsonToDescription(json.decode(activity.description ?? "")),
maxLines: 2, softWrap: true, overflow: TextOverflow.ellipsis),
onTap: () {
if (callback != null) {
callback();
}
},
);
}
}

View File

@ -0,0 +1,90 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart';
import 'package:sendtrain/models/google_place_model.dart';
import 'package:sendtrain/widgets/generic/elements/form_search_input.dart';
import 'package:uuid/uuid.dart';
class GooglePlacesService {
final sessionToken = Uuid().v4();
final apiKey = "AIzaSyBCjMCEAyyNVpsnVYvZj6VL1mmB98Vd6AE";
final client = Client();
void finish() {
client.close();
}
Future<List<Suggestion>?> fetchSuggestions(String input) async {
var headers = {
'Content-Type': 'application/json',
'X-Goog-Api-Key': apiKey,
"Access-Control-Allow-Origin": "*",
'X-Goog-FieldMask':
'places.displayName,places.id,places.formattedAddress,places.photos'
};
var request = Request('POST',
Uri.parse('https://places.googleapis.com/v1/places:searchText'));
request.body = json.encode({"textQuery": input});
request.headers.addAll(headers);
StreamedResponse response = await request.send();
if (response.statusCode == 200) {
final result = json.decode(await response.stream.bytesToString());
if (result.isNotEmpty) {
return result['places']
.map<Suggestion>((p) => Suggestion<GooglePlaceModel>(
GooglePlaceModel(
placeId: p['id'],
description: p['displayName']['text'],
address: p['formattedAddress'],
imageReferences: p['photos'])))
.toList();
} else {
return null;
}
} else {
throw Exception(response.reasonPhrase);
}
}
Future fetchPhoto(String name) async {
var headers = {
"Access-Control-Allow-Origin": "*",
};
var request = Request(
'GET',
Uri.parse(
'https://places.googleapis.com/v1/$name/media?key=$apiKey&maxWidthPx=800&skipHttpRedirect=true'));
request.headers.addAll(headers);
StreamedResponse response = await request.send();
if (response.statusCode == 200) {
final result = json.decode(await response.stream.bytesToString());
if (result.isNotEmpty) {
return result;
} else {
return null;
}
} else {
throw Exception(response.reasonPhrase);
}
}
Widget resultWidget(GooglePlaceModel place, Function? callback) {
return ListTile(
title: Text(place.description),
onTap: () async {
if (callback != null) {
callback();
}
},
);
}
}

View File

@ -0,0 +1,63 @@
import 'dart:convert';
import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:sendtrain/daos/sessions_dao.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/widgets/generic/elements/form_text_input.dart';
class AchievementEditor extends StatelessWidget {
AchievementEditor({super.key, required this.session, this.callback});
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final TextEditingController tec = TextEditingController();
final Session session;
final Function? callback;
@override
Widget build(
BuildContext context,
) {
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('Create Achievement',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleLarge)),
FormTextInput(controller: tec, title: 'Achievement', icon: Icon(Icons.military_tech_rounded)),
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
Padding(
padding: EdgeInsets.only(top: 10),
child: FilledButton(
child: Text('Submit'),
onPressed: () async {
session.achievements;
List achievements =
json.decode(session.achievements ?? "[]");
achievements.add(tec.text);
Session updatedSession = session.copyWith(
achievements:
Value<String>(json.encode(achievements)));
SessionsDao(Provider.of<AppDatabase>(context,
listen: false))
.replace(updatedSession);
Navigator.pop(_formKey.currentContext!, 'Submit');
if (callback != null) {
await callback!();
}
}))
])
])));
}
}

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import '../widgets/activity_type_filter.dart';
import 'activity_type_filter.dart';
class ActivitiesHeader extends StatefulWidget {
const ActivitiesHeader({super.key});

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

@ -0,0 +1,250 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.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/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 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 Function? callback;
final bool resetOnLoad;
// @override
// State<ActivityActionView> createState() => ActivityActionViewState();
// }
// class ActivityActionViewState extends State<ActivityActionView> {
// class ActivityActionView extends StatelessWidget {
// ActivityActionView({super.key, required this.actions});
// final List actions;
final ItemScrollController itemScrollController = ItemScrollController();
final ScrollOffsetController scrollOffsetController =
ScrollOffsetController();
final ItemPositionsListener itemPositionsListener =
ItemPositionsListener.create();
final ScrollOffsetListener scrollOffsetListener =
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
Widget build(BuildContext context) {
at = Provider.of<ActionTimer>(context, listen: false);
int actionCount = 0;
if (actions.isNotEmpty) {
at.setup(
ActionModel(
action: actions.first,
db: Provider.of<AppDatabase>(context)),
itemScrollController,
resetOnLoad);
// WidgetsBinding.instance.addPostFrameCallback((_) {
// if (itemScrollController.isAttached) {
// itemScrollController.scrollTo(
// index: at.currentAction.parentId != null
// ? at.currentAction.parentId!
// : at.currentAction.id,
// duration: Duration(milliseconds: 500),
// curve: Curves.easeInOutCubic);
// }
// });
return Expanded(
child: Column(children: [
Padding(
padding: const EdgeInsets.only(left: 10, right: 10),
child: Card(
clipBehavior: Clip.antiAlias,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10)),
),
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;
}
List<GestureDetector> content = [];
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;
for (int setItemNum = 0;
setItemNum < setItems.length;
setItemNum++) {
Item setItem = setItems[setItemNum];
content.add(gtBuild(at, setItem, actionCount++, itemNum,
order: (item as Set).setOrder));
}
}
if (itemNum == 0) {
return Card(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(0),
topRight: Radius.circular(0),
bottomLeft: Radius.circular(10),
bottomRight: Radius.circular(10)),
),
clipBehavior: Clip.antiAlias,
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));
}
}))
]));
} else {
return AddCardGeneric(
title: 'Add an Action!',
description:
'Click here to create an exercise template (sets and reps, etc) for your activity!',
action: () {
showEditorSheet(
context,
ActivityActionEditor(
session: session,
activity: activity,
callback: callback));
});
}
}
}

View File

@ -0,0 +1,115 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:provider/provider.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/extensions/string_extensions.dart';
import 'package:sendtrain/helpers/date_time_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/widgets/activities/activity_view.dart';
import 'package:sendtrain/widgets/builders/dialogs.dart';
import 'package:sendtrain/widgets/generic/elements/card_image.dart';
import 'package:sendtrain/widgets/generic/elements/generic_progress_indicator.dart';
class ActivityCard extends StatefulWidget {
final Activity activity;
final Session session;
final Function? callback;
const ActivityCard(
{super.key,
required this.activity,
required this.session,
this.callback});
@override
State<ActivityCard> createState() => ActivityCardState();
}
class ActivityCardState extends State<ActivityCard> {
@override
Widget build(BuildContext context) {
final ActivityTimerModel atm = Provider.of<ActivityTimerModel>(context);
return FutureBuilder<List<MediaItem>>(
future: MediaItemsDao(Provider.of<AppDatabase>(context))
.fromActivity(widget.activity),
builder: (context, snapshot) {
if (snapshot.hasData) {
List<MediaItem> mediaItems = snapshot.data!;
return Card.outlined(
color: atm.activity?.id == widget.activity.id
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceContainerLow,
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => showGenericDialog(
ActivityView(session: widget.session, activity: widget.activity), context),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
leading: CardImage(
image:
findMediaByType(mediaItems, MediaType.image)),
title: Consumer<ActivityTimerModel>(
builder: (context, atm, child) {
if (atm.activity?.id == widget.activity.id) {
return Text(
maxLines: 1,
overflow: TextOverflow.ellipsis,
"${widget.activity.title.toTitleCase()} (${formattedTime(atm.totalTime)})");
} else {
return Text(
maxLines: 1,
overflow: TextOverflow.ellipsis,
widget.activity.title.toTitleCase());
}
},
),
subtitle: Text(
overflow: TextOverflow.ellipsis,
maxLines: 2,
softWrap: true,
jsonToDescription(json
.decode(widget.activity.description ?? ""))),
contentPadding: EdgeInsets.only(left: 13),
trailing: Flex(
direction: Axis.vertical,
mainAxisSize: MainAxisSize.max,
children: [
IconButton(
padding: EdgeInsets.all(0),
alignment: Alignment.topCenter,
visualDensity: VisualDensity.compact,
icon: Icon(Icons.close_rounded),
onPressed: () {
showRemovalDialog(
'Activity Removal',
'Would you like to permanently remove this activity from the current session?',
context, () {
SessionActivitiesDao(
Provider.of<AppDatabase>(context,
listen: false))
.removeAssociation(widget.activity.id,
widget.session.id);
}).then((result) {
setState(() {});
});
},
)
])),
],
)),
);
} else {
return GenericProgressIndicator();
}
});
}
}

View File

@ -0,0 +1,287 @@
import 'dart:convert';
import 'package:flutter/material.dart' hide Action;
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
import 'package:provider/provider.dart';
import 'package:sendtrain/daos/actions_dao.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/extensions/string_extensions.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_view_categories.dart';
import 'package:sendtrain/widgets/activities/activity_view_media.dart';
import 'package:sendtrain/widgets/builders/dialogs.dart';
class ActivityView extends StatefulWidget {
const ActivityView(
{super.key, required this.session, required this.activity});
final Session session;
final Activity activity;
@override
State<ActivityView> createState() => _ActivityViewState();
}
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
Widget build(BuildContext context) {
final Activity activity = widget.activity;
final Session session = widget.session;
return FutureBuilder<List>(
future: ActionsDao(Provider.of<AppDatabase>(context))
.fromActivity(activity, session),
builder: (context, snapshot) {
if (snapshot.hasData) {
List<Action> actions = snapshot.data! as List<Action>;
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) {
return;
}
final bool shouldPop = await showBackDialog(context) ?? false;
if (context.mounted && shouldPop) {
Navigator.pop(context);
}
},
child: Scaffold(
floatingActionButtonLocation: ExpandableFab.location,
floatingActionButton: ExpandableFab(
key: _fabKey,
distance: 70,
type: ExpandableFabType.up,
overlayStyle: ExpandableFabOverlayStyle(
color: Colors.black.withValues(alpha: 0.5),
blur: 10,
),
onOpen: () {
// pause the activity on open
ActionTimer at =
Provider.of<ActionTimer>(context, listen: false);
if (at.started) at.pause();
},
children: [
// FloatingActionButton.extended(
// icon: const Icon(Icons.upload_outlined),
// label: Text('Upload Media'),
// onPressed: () {},
// ),
FloatingActionButton.extended(
icon: const Icon(Icons.done_all_outlined),
label: Text('Edit Action'),
onPressed: () {
showEditorSheet(
context,
ActivityActionEditor(
session: session,
activity: activity,
action: actions.first,
callback: resetState));
},
),
FloatingActionButton.extended(
icon: const Icon(Icons.note_add_outlined),
label: Text('Add Note'),
onPressed: () {},
),
FloatingActionButton.extended(
icon: const Icon(Icons.history_outlined),
label: Text('Restart'),
onPressed: () {},
),
FloatingActionButton.extended(
icon: const Icon(Icons.done_all_outlined),
label: Text('Done'),
onPressed: () {},
),
]),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AppBar(
titleSpacing: 0,
centerTitle: true,
title: const Text('Activity',
style: TextStyle(fontSize: 15)),
),
Padding(
padding: const EdgeInsets.only(
left: 15, right: 20, top: 15, bottom: 10),
child: Text(
maxLines: 1,
style: const TextStyle(
fontSize: 25,
fontWeight: FontWeight.bold),
activity.title.toTitleCase())),
SizedBox(
height: 40,
child: ListView(
scrollDirection: Axis.horizontal,
padding:
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: [
Expanded(
child: const Text(
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold),
'Actions')),
IconButton(
onPressed: () {
showGenericSheet(
context,
Column(children: [
ActivityActionView(
session: session,
activity: activity,
actions: actions,
callback: resetState,
resetOnLoad: false)
]),
Theme.of(context).colorScheme.surface);
},
icon: Icon(Icons.expand),
alignment: Alignment.bottomCenter,
)
])),
ActivityActionView(
session: session,
activity: activity,
actions: actions,
callback: resetState)
])));
// ] +
// action(actions, context)));
} else {
return Container(
alignment: Alignment.center,
child: SizedBox(
height: 50.0,
width: 50.0,
child: CircularProgressIndicator(),
));
}
});
}
}

View File

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

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:sendtrain/daos/media_items_dao.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/widgets/media_card.dart';
import 'package:sendtrain/widgets/media/media_card.dart';
class ActivityViewMedia extends StatelessWidget {
const ActivityViewMedia({super.key, required this.activity});

View File

@ -1,110 +0,0 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
import 'package:sendtrain/extensions/string_extensions.dart';
import 'package:sendtrain/models/activity_timer_model.dart';
class ActivityActionView extends StatefulWidget {
const ActivityActionView({super.key, required this.actions});
final List actions;
@override
State<ActivityActionView> createState() => ActivityActionViewState();
}
class ActivityActionViewState extends State<ActivityActionView> {
final ItemScrollController itemScrollController = ItemScrollController();
final ScrollOffsetController scrollOffsetController =
ScrollOffsetController();
final ItemPositionsListener itemPositionsListener =
ItemPositionsListener.create();
final ScrollOffsetListener scrollOffsetListener =
ScrollOffsetListener.create();
@override
Widget build(BuildContext context) {
ActivityTimerModel atm =
Provider.of<ActivityTimerModel>(context, listen: true);
List sets = json.decode(widget.actions[0].set);
// we need to set the scroll controller
// so we can update the selected item position
atm.setScrollController(itemScrollController);
return Expanded(
child: ScrollablePositionedList.builder(
padding: const EdgeInsets.fromLTRB(10, 0, 10, 20),
itemCount: sets.length,
itemScrollController: itemScrollController,
scrollOffsetController: scrollOffsetController,
itemPositionsListener: itemPositionsListener,
scrollOffsetListener: scrollOffsetListener,
itemBuilder: (BuildContext context, int setNum) {
List<GestureDetector> content = [];
List set = sets[setNum];
for (int actionNum = 0; actionNum < set.length; actionNum++) {
Map<String, dynamic> setItem = set[actionNum];
content.add(GestureDetector(
onTap: () {
atm.setAction(setNum, actionNum, 'manual');
atm.setActionCount();
itemScrollController.scrollTo(
index: setNum,
duration: Duration(milliseconds: 500),
curve: Curves.easeInOutCubic);
},
child: Row(children: [
Ink(
width: 70,
padding: const EdgeInsets.all(15),
color: atm.isCurrentItem(setNum, actionNum)
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.onPrimary,
child: Text(
textAlign: TextAlign.center,
'${setNum + 1}.${actionNum + 1} ')),
Expanded(
child: Ink(
padding: const EdgeInsets.all(15),
color: atm.isCurrentItem(setNum, actionNum)
? Theme.of(context).colorScheme.surfaceBright
: Theme.of(context).colorScheme.surfaceContainerLow,
child: Text(
textAlign: TextAlign.center,
'${setItem['name']}: ${setItem['amount']} ${setItem['type']}'.toTitleCase())))
])));
}
if (setNum == 0) {
return Card(
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(0),
topRight: Radius.circular(0),
bottomLeft: Radius.circular(10),
bottomRight: Radius.circular(10)),
),
clipBehavior: Clip.antiAlias,
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,150 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:sendtrain/daos/media_items_dao.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/extensions/string_extensions.dart';
import 'package:sendtrain/models/activity_timer_model.dart';
import 'package:sendtrain/widgets/activity_view.dart';
class ActivityCard extends StatefulWidget {
final Activity activity;
const ActivityCard({super.key, required this.activity});
@override
State<ActivityCard> createState() => ActivityCardState();
}
class ActivityCardState extends State<ActivityCard> {
String formattedTime(int timeInSecond) {
int sec = timeInSecond % 60;
int min = (timeInSecond / 60).floor();
String minute = min.toString().length <= 1 ? "0$min" : "$min";
String second = sec.toString().length <= 1 ? "0$sec" : "$sec";
return "$minute:$second";
}
@override
Widget build(BuildContext context) {
final ActivityTimerModel atm =
Provider.of<ActivityTimerModel>(context, listen: false);
return FutureBuilder<List<MediaItem>>(
future: MediaItemsDao(Provider.of<AppDatabase>(context))
.fromActivity(widget.activity),
builder: (context, snapshot) {
if (snapshot.hasData) {
List<MediaItem> mediaItems = snapshot.data!;
return Card(
color: atm.activity?.id == widget.activity.id
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceContainerLow,
clipBehavior: Clip.hardEdge,
child: InkWell(
onTap: () => showGeneralDialog(
barrierColor: Colors.black.withOpacity(0.5),
transitionDuration: const Duration(milliseconds: 220),
transitionBuilder: (BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
Animation<Offset> custom = Tween<Offset>(
begin: const Offset(0.0, 1.0),
end: const Offset(0.0, 0.0))
.animate(animation);
return SlideTransition(
position: custom,
child: Dialog.fullscreen(
child: ActivityView(activity: widget.activity)));
},
barrierDismissible: true,
barrierLabel: '',
context: context,
pageBuilder: (context, animation1, animation2) {
return Container();
}),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
// visualDensity: VisualDensity(horizontal: VisualDensity.maximumDensity),
leading: Padding(
padding: const EdgeInsets.fromLTRB(0, 0, 0, 0),
child: Container(
// padding: EdgeInsets.only(top: 5, bottom: 5),
width: 60,
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.fill,
image:
findMediaByType(mediaItems, 'image')),
// color: Colors.blue,
borderRadius: const BorderRadius.all(
Radius.elliptical(8, 8)),
),
)),
title: Consumer<ActivityTimerModel>(
builder: (context, atm, child) {
if (atm.activity?.id == widget.activity.id) {
return Text(
maxLines: 1,
"${widget.activity.title.toTitleCase()} (${formattedTime(atm.totalTime)})");
} else {
return Text(maxLines: 1, widget.activity.title.toTitleCase());
}
},
),
subtitle: Text(maxLines: 2, widget.activity.description),
trailing: IconButton(
visualDensity: VisualDensity.compact,
icon: Icon(Icons.close_rounded),
onPressed: () {
showAdaptiveDialog(
context: context,
builder: (BuildContext context) => AlertDialog(
title: const Text('Activity Removal'),
content: const Text(
'Would you like to permanently remove this activity from the current session?'),
actions: <Widget>[
TextButton(
onPressed: () =>
Navigator.pop(context, 'Cancel'),
child: const Text('Cancel'),
),
TextButton(
onPressed: () =>
Navigator.pop(context, 'OK'),
child: const Text('OK'),
),
],
),
);
},
)),
],
)),
);
} else {
return Container(
alignment: Alignment.center,
child: SizedBox(
height: 50.0,
width: 50.0,
child: CircularProgressIndicator(),
));
}
});
}
ImageProvider findMediaByType(List<MediaItem> media, String type) {
Iterable<MediaItem>? found = media.where((m) => m.type == MediaType.image);
if (found.isNotEmpty) {
return NetworkImage(found.first.reference);
} else {
// Element is not found
return const AssetImage('assets/images/placeholder.jpg');
}
}
}

View File

@ -1,196 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
import 'package:provider/provider.dart';
import 'package:sendtrain/daos/actions_dao.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/extensions/string_extensions.dart';
import 'package:sendtrain/models/activity_timer_model.dart';
import 'package:sendtrain/widgets/activity_action_view.dart';
import 'package:sendtrain/widgets/activity_view_categories.dart';
import 'package:sendtrain/widgets/activity_view_media.dart';
import 'package:sendtrain/widgets/activity_view_types.dart';
class ActivityView extends StatefulWidget {
const ActivityView(
{super.key, required this.activity});
final Activity activity;
@override
State<ActivityView> createState() => _ActivityViewState();
}
class _ActivityViewState extends State<ActivityView> {
@override
Widget build(BuildContext context) {
final Activity activity = widget.activity;
ActivityTimerModel atm =
Provider.of<ActivityTimerModel>(context, listen: false);
return FutureBuilder<List>(
future: ActionsDao(Provider.of<AppDatabase>(context))
.fromActivity(activity),
builder: (context, snapshot) {
if (snapshot.hasData) {
List actions = snapshot.data!;
atm.setup(activity, actions);
return Scaffold(
floatingActionButtonLocation: ExpandableFab.location,
floatingActionButton: ExpandableFab(
distance: 70,
type: ExpandableFabType.up,
overlayStyle: ExpandableFabOverlayStyle(
color: Colors.black.withOpacity(0.5),
blur: 10,
),
children: [
FloatingActionButton.extended(
icon: const Icon(Icons.history_outlined),
label: Text('Restart'),
onPressed: () {},
),
FloatingActionButton.extended(
icon: const Icon(Icons.done_all_outlined),
label: Text('Done'),
onPressed: () {},
),
FloatingActionButton.extended(
icon: const Icon(Icons.edit_outlined),
label: Text('Edit'),
onPressed: () {},
),
]),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AppBar(
titleSpacing: 0,
centerTitle: true,
title: const Text('Activity',
style: TextStyle(fontSize: 15)),
),
Padding(
padding: const EdgeInsets.only(
left: 15, right: 20, top: 15, bottom: 10),
child: Text(
maxLines: 1,
style: const TextStyle(
fontSize: 25, fontWeight: FontWeight.bold),
activity.title.toTitleCase())),
Padding(
padding: const EdgeInsets.fromLTRB(10, 0, 0, 10),
child: Flex(direction: Axis.horizontal, children: [
ActivityViewCategories(
categories: [activity.category]),
ActivityViewTypes(types: [activity.type])
])),
Padding(
padding: const EdgeInsets.only(
top: 0, bottom: 10, left: 15, right: 15),
child: Text(
textAlign: TextAlign.left,
style: const TextStyle(fontSize: 15),
activity.description)),
const Padding(
padding: EdgeInsets.fromLTRB(15, 20, 0, 10),
child: Text(
style: TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
'Media:')),
ActivityViewMedia(activity: activity),
const Padding(
padding: EdgeInsets.fromLTRB(15, 30, 0, 10),
child: Text(
textAlign: TextAlign.left,
style: TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
'Actions')),
Padding(
padding: const EdgeInsets.only(left: 10, right: 10),
child: Card(
clipBehavior: Clip.antiAlias,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(10),
topRight: Radius.circular(10)),
),
color: Theme.of(context).colorScheme.onPrimary,
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(
flex: 1,
child: Stack(
alignment: Alignment.center,
children: [
Container(
alignment: Alignment.center,
child: Consumer<ActivityTimerModel>(
builder: (context, atm, child) {
return Text(
style: const TextStyle(
fontSize: 20),
textAlign: TextAlign.center,
'${atm.actionCount} ${atm.currentAction['type']}'.toTitleCase());
},
),
),
Container(
alignment: Alignment.centerRight,
padding:
EdgeInsets.only(right: 15),
child:
Consumer<ActivityTimerModel>(
builder: (context, atm,
child) {
return Text(
style: const TextStyle(
fontSize: 12),
textAlign: TextAlign.right,
'${atm.currentAction['actionID'] + 1} of ${atm.totalActions()}');
})),
])),
]))),
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 {
return Container(
alignment: Alignment.center,
child: SizedBox(
height: 50.0,
width: 50.0,
child: CircularProgressIndicator(),
));
}
});
}
}

View File

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

View File

@ -0,0 +1,107 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:sendtrain/providers/action_timer.dart';
Future showGenericDialog(dynamic object, BuildContext parentContext) {
return showGeneralDialog(
barrierColor: Colors.black.withValues(alpha: 0.5),
transitionDuration: const Duration(milliseconds: 220),
transitionBuilder: (BuildContext context, Animation<double> animation,
Animation<double> secondaryAnimation, Widget child) {
Animation<Offset> custom = Tween<Offset>(
begin: const Offset(0.0, 1.0), end: const Offset(0.0, 0.0))
.animate(animation);
return SlideTransition(
position: custom, child: Dialog.fullscreen(child: object));
},
barrierDismissible: true,
barrierLabel: '',
context: parentContext,
pageBuilder: (context, animation1, animation2) {
return Container();
});
}
Future showCrudDialog(String title, String content, BuildContext context,
[Function? callback]) {
return showAdaptiveDialog(
context: context,
builder: (BuildContext context) => AlertDialog(
title: Text(title),
content: Text(content),
actions: <Widget>[
TextButton(
onPressed: () => {
Navigator.pop(context, 'Cancel'),
},
child: const Text('Cancel'),
),
TextButton(
onPressed: () => {
if (callback != null) {callback()},
Navigator.pop(context, 'OK')
},
child: const Text('OK'),
),
],
),
);
}
Future showRemovalDialog(String title, String content, BuildContext context,
[Function? callback]) {
return showCrudDialog(title, content, context, callback);
}
Future showUpdateDialog(String title, String content, BuildContext context,
[Function? 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,19 @@
import 'package:flutter/material.dart';
class CardContent extends StatelessWidget {
const CardContent({super.key, required this.content});
final String content;
@override
Widget build(BuildContext context) {
return ListTile(
contentPadding: const EdgeInsets.fromLTRB(15, 0, 15, 15),
title: Text(
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w300),
content),
);
}
}

View File

@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
enum SizeAxis { width, height }
class CardImage extends StatelessWidget {
const CardImage({super.key, required this.image, this.padding, this.size});
final ImageProvider<Object> image;
final EdgeInsets? padding;
final Map<SizeAxis, double>? size;
@override
Widget build(BuildContext context) {
return Padding(
padding: padding ?? const EdgeInsets.fromLTRB(0, 0, 0, 0),
child: Container(
width: size?[SizeAxis.width] ?? 60,
height: size?[SizeAxis.height] ?? 60,
decoration: BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
image: image,
onError: (error, stackTrace) => AssetImage('assets/images/placeholder.jpg')
// color: Colors.blue,
),
borderRadius: BorderRadius.all(Radius.elliptical(8, 8)),
),
));
}
}

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

@ -0,0 +1,116 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:sendtrain/services/functional/debouncer.dart';
import 'package:sendtrain/widgets/generic/elements/form_text_input.dart';
class Suggestion<T> {
T content;
Suggestion(this.content);
Widget resultWidget() {
return ListTile(
title: Text('test'),
);
}
}
// controller: manages the selected content
// service: manages the requests for the specific data to search against
// title: the title of the text input
// callback: the fuction called when a selection is made
class FormSearchInput extends StatefulWidget {
const FormSearchInput(
{super.key,
required this.controller,
required this.service,
required this.resultHandler,
this.title});
final String? title;
final TextEditingController controller;
final dynamic service;
final Function resultHandler;
@override
State<FormSearchInput> createState() => _FormSearchInputState();
}
class _FormSearchInputState extends State<FormSearchInput> {
String? _currentQuery;
late final service = widget.service;
late final resultHandler = widget.resultHandler;
// The most recent suggestions received from the API.
late Iterable<Widget> _lastOptions = <Widget>[];
late final Debouncer debouncer;
// @override
// initState() {
// service = widget.service;
// }
// Calls the "remote" API to search with the given query. Returns null when
// the call has been made obsolete.
Future<Iterable<Suggestion>?> _search(String query) async {
_currentQuery = query;
// In a real application, there should be some error handling here.
if (query.isNotEmpty && query.length > 3) {
final List<Suggestion>? suggestions =
await service.fetchSuggestions(_currentQuery!);
// If another search happened after this one, throw away these options.
if (_currentQuery != query) {
return null;
}
_currentQuery = null;
return suggestions?.map((suggestion) => suggestion);
} else {
return null;
}
}
@override
void initState() {
super.initState();
debouncer = Debouncer(Duration(milliseconds: 50), _search);
}
@override
Widget build(BuildContext context) {
return SearchAnchor(
isFullScreen: false,
builder: (BuildContext context, SearchController controller) {
return FormTextInput(
controller: widget.controller,
title: widget.title ?? "",
icon: Icon(Icons.search_rounded),
maxLines: 2,
requiresValidation: false,
onTap: () {
controller.openView();
});
},
suggestionsBuilder:
(BuildContext context, SearchController controller) async {
final List<Suggestion>? options =
(await debouncer.process(controller.text))?.toList();
if (options == null) {
return _lastOptions;
}
_lastOptions = List<ListTile>.generate(options.length, (int index) {
final Suggestion item = options[index];
final dynamic content = item.content;
return service.resultWidget(content, () {
resultHandler(content, service);
controller.closeView(null);
});
});
return _lastOptions;
});
}
}

View File

@ -0,0 +1,78 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
enum InputTypes { text, number }
class FormTextInput extends StatelessWidget {
const FormTextInput(
{super.key,
required this.controller,
required this.title,
this.icon,
this.maxLines,
this.minLines,
this.onTap,
this.requiresValidation = true,
this.type = InputTypes.text,
this.hint,
this.validations});
final TextEditingController controller;
final String title;
final int? maxLines;
final int? minLines;
final Icon? icon;
final dynamic onTap;
final bool requiresValidation;
final InputTypes type;
final String? hint;
final Function? validations;
@override
Widget build(BuildContext context) {
final Map params = {};
if (type == InputTypes.number) {
params['keyboardType'] = TextInputType.number;
params['inputFormatters'] = <TextInputFormatter>[
FilteringTextInputFormatter.digitsOnly
];
}
return Padding(
padding: EdgeInsets.only(top: 10, bottom: 10),
child: TextFormField(
keyboardType: params['keyboardType'] ?? TextInputType.text,
inputFormatters: params['inputFormatters'] ?? [],
minLines: minLines ?? 1,
maxLines: maxLines ?? 1,
controller: controller,
decoration: InputDecoration(
filled: true,
prefixIcon: icon ?? Icon(Icons.draw_rounded),
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(12)),
labelText: title,
hintText: hint ?? '',
),
validator: (String? value) {
if (requiresValidation == true) {
if (value == null || value.isEmpty) {
return 'Please enter some text';
}
if (validations != null) validations!(value);
// if (value.length < 3) {
// return 'Please enter a minimum of 3 characters';
// }
}
return null;
},
onTap: () {
if (onTap != null) {
onTap();
}
}));
}
}

View File

@ -0,0 +1,16 @@
import 'package:flutter/material.dart';
class GenericProgressIndicator extends StatelessWidget {
const GenericProgressIndicator({super.key});
@override
Widget build(BuildContext context) {
return Container(
alignment: Alignment.center,
child: SizedBox(
height: 50.0,
width: 50.0,
child: CircularProgressIndicator(),
));
}
}

View File

@ -0,0 +1,67 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:sendtrain/daos/media_items_dao.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/helpers/widget_helpers.dart';
import 'package:sendtrain/widgets/builders/dialogs.dart';
class MediaCard extends StatelessWidget {
const MediaCard(
{super.key, required this.media, this.callback, this.canDelete});
final MediaItem media;
final bool? canDelete;
final Function? callback;
@override
Widget build(BuildContext context) {
DecorationImage mediaImage(MediaItem media) {
dynamic image;
if (media.type == MediaType.image || media.type == MediaType.location) {
image = NetworkImage(media.reference);
} else if (media.type == MediaType.localImage) {
image = Image.memory(base64Decode(media.reference)).image;
} else if (media.type == MediaType.youtube) {
image =
NetworkImage('https://img.youtube.com/vi/${media.reference}/0.jpg');
} else if (media.type == MediaType.localVideo) {}
return DecorationImage(image: image, fit: BoxFit.cover);
}
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
image: mediaImage(media),
),
child: Card(
color: Colors.transparent,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
shadowColor: const Color.fromARGB(0, 255, 255, 255),
child: TextButton(
onLongPress: () {
if (canDelete == true) {
showRemovalDialog(
'Media Removal',
'Would you like to permanently remove this media from the current session?',
context, () {
MediaItemsDao(
Provider.of<AppDatabase>(context, listen: false))
.remove(media);
}).then((result) {
if (callback != null) {
callback!();
}
});
}
},
onPressed: () => showMediaDetailWidget(context, media),
child: const ListTile(
title: Text(''),
))));
}
}

View File

@ -0,0 +1,32 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:sendtrain/database/database.dart';
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
class MediaContent extends StatelessWidget {
const MediaContent({super.key, required this.media});
final MediaItem media;
@override
Widget build(BuildContext context) {
YoutubePlayerController controller = YoutubePlayerController(
initialVideoId: media.reference,
flags: const YoutubePlayerFlags(
autoPlay: false, mute: true, showLiveFullscreenButton: false));
if (media.type == MediaType.image || media.type == MediaType.location) {
return Image(image: NetworkImage(media.reference));
} else if (media.type == MediaType.localImage) {
return Image.memory(base64Decode(media.reference));
} else if (media.type == MediaType.youtube) {
return YoutubePlayer(
controller: controller,
aspectRatio: 16 / 9,
);
}
return const Image(image: AssetImage('assets/images/placeholder.jpg'));
}
}

View File

@ -0,0 +1,35 @@
import 'package:flutter/material.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/widgets/media/media_content.dart';
class MediaDetails extends StatelessWidget {
const MediaDetails({super.key, required this.media});
final MediaItem media;
@override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.fromLTRB(15, 0, 15, 15),
child: ListView(
shrinkWrap: true,
children: <Widget>[
ClipRRect(
borderRadius: BorderRadius.circular(10),
child: MediaContent(media: media),
),
const SizedBox(height: 15),
Padding(
padding: EdgeInsets.fromLTRB(15, 0, 15, 15),
child: Text(
media.description,
style: const TextStyle(fontSize: 20),
)),
const Divider(
indent: 20,
endIndent: 20,
)
]));
}
}

View File

@ -1,93 +0,0 @@
import 'package:flutter/material.dart';
import 'package:sendtrain/database/database.dart';
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
class MediaCard extends StatelessWidget {
const MediaCard({super.key, required this.media});
final MediaItem media;
@override
Widget build(BuildContext context) {
YoutubePlayerController controller = YoutubePlayerController(
initialVideoId: media.reference,
flags: const YoutubePlayerFlags(
autoPlay: false, mute: true, showLiveFullscreenButton: false));
DecorationImage mediaImage(MediaItem media) {
String image = '';
if (media.type == MediaType.image) {
image = media.reference;
} else if (media.type == MediaType.youtube) {
image = 'https://img.youtube.com/vi/${media.reference}/0.jpg';
}
return DecorationImage(image: NetworkImage(image), fit: BoxFit.cover);
}
Widget mediaItem(MediaItem media) {
if (media.type == MediaType.image) {
return Image(image: NetworkImage(media.reference));
} else if (media.type == MediaType.youtube) {
return YoutubePlayer(
controller: controller,
aspectRatio: 16 / 9,
);
}
return const Image(image: AssetImage('assets/images/placeholder.jpg'));
}
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
image: mediaImage(media),
),
child: Card(
color: Colors.transparent,
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
shadowColor: const Color.fromARGB(0, 255, 255, 255),
child: TextButton(
onPressed: () => showDialog<String>(
context: context,
builder: (BuildContext context) => Dialog.fullscreen(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
mediaItem(media),
const SizedBox(height: 15),
Text(
media.description,
style: const TextStyle(fontSize: 20),
),
const Divider(
indent: 20,
endIndent: 20,
color: Colors.deepPurple,
),
// const Text(
// 'Comments',
// style: TextStyle(fontSize: 20),
// ),
const SizedBox(height: 15),
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Close'),
),
],
),
),
),
),
child: const ListTile(
title: Text(''),
))));
}
}

View File

@ -3,8 +3,8 @@ import 'package:sendtrain/classes/activity_action.dart';
import 'package:sendtrain/database/database.dart' hide ActivityAction;
import 'package:sendtrain/models/activity_model.dart';
import '../widgets/activities_header.dart';
import '../widgets/activity_card.dart';
// import '../widgets/activities/activities_header.dart';
// import '../widgets/activities/activity_card.dart';
class ActivitiesScreen extends StatefulWidget {
const ActivitiesScreen({super.key});

View File

@ -1,31 +1,64 @@
// import 'package:drift/drift.dart' hide Column;
import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:sendtrain/daos/sessions_dao.dart';
import 'package:sendtrain/database/database.dart';
import '../widgets/session_card.dart';
import 'package:sendtrain/widgets/generic/elements/generic_progress_indicator.dart';
import 'package:sendtrain/widgets/sessions/session_card.dart';
import 'package:collection/collection.dart';
class SessionsScreen extends StatelessWidget {
class SessionsScreen extends StatefulWidget {
const SessionsScreen({super.key});
@override
State<SessionsScreen> createState() => _SessionsScreenState();
}
class _SessionsScreenState extends State<SessionsScreen> {
Widget getSessionCard(session) {
if (session != null) {
return SessionCard(session: session);
} else {
return Padding(
padding: EdgeInsets.all(15),
child: Icon(Icons.do_not_disturb_alt_outlined));
}
}
@override
Widget build(BuildContext context) {
return FutureBuilder<List<Session>>(
future: SessionsDao(Provider.of<AppDatabase>(context)).all(),
SessionsDao dao = SessionsDao(Provider.of<AppDatabase>(context));
return StreamBuilder<List<Session>>(
stream: dao.watch(),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data!.isNotEmpty) {
final sessions = snapshot.data!;
final pending = sessions.where((session) =>
session.status == SessionStatus.completed ||
session.status == SessionStatus.missed);
final upcoming = sessions.firstWhere(
final upcoming = sessions.firstWhereOrNull(
(session) => session.status == SessionStatus.pending);
final current = sessions.firstWhere(
final current = sessions.firstWhereOrNull(
(session) => session.status == SessionStatus.started);
if (current == null && upcoming != null) {
dao.createOrUpdate(SessionsCompanion(
id: Value(upcoming.id),
title: Value(upcoming.title),
content: Value(upcoming.content),
status: Value(SessionStatus.started),
address: Value(upcoming.address),
date: Value(upcoming.date)
));
}
List<Widget> previousSessions = List.generate(pending.length,
(i) => SessionCard(type: 1, session: pending.elementAt(i)));
Widget upcomingSession = SessionCard(session: upcoming);
Widget currentSession = SessionCard(session: current);
Widget upcomingSession = getSessionCard(upcoming);
Widget currentSession = getSessionCard(current);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
@ -63,13 +96,7 @@ class SessionsScreen extends StatelessWidget {
],
);
} else {
return Container(
alignment: Alignment.center,
child: SizedBox(
height: 50.0,
width: 50.0,
child: CircularProgressIndicator(),
));
return GenericProgressIndicator();
}
});
}

View File

@ -1,164 +0,0 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:sendtrain/database/database.dart' hide ActivityAction;
import 'package:sendtrain/extensions/string_extensions.dart';
import 'package:sendtrain/widgets/session_view.dart';
class SessionCard extends StatelessWidget {
final int type;
final Session session;
const SessionCard({super.key, this.type = 0, required this.session});
@override
Widget build(BuildContext context) {
initializeDateFormatting('en');
final DateFormat dateFormat = DateFormat('yyyy-MM-dd');
Color color = (session.status == SessionStatus.started)
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceContainerLow;
if (type == 0) {
return Card(
color: color,
margin: const EdgeInsets.fromLTRB(15, 15, 15, 0),
clipBehavior: Clip.hardEdge,
child: InkWell(
splashColor: Colors.deepPurple,
onTap: () => showGeneralDialog(
barrierColor: Colors.black.withOpacity(0.5),
transitionDuration: const Duration(milliseconds: 220),
transitionBuilder: (BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
Animation<Offset> custom = Tween<Offset>(
begin: const Offset(0.0, 1.0),
end: const Offset(0.0, 0.0))
.animate(animation);
return SlideTransition(
position: custom,
child: Dialog.fullscreen(child: SessionView(session: session)));
},
barrierDismissible: true,
barrierLabel: '',
context: context,
pageBuilder: (context, animation1, animation2) {
return Container();
}),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
leading: Padding(
padding: const EdgeInsets.fromLTRB(0, 8, 0, 0),
child: Container(
width: 60,
decoration: const BoxDecoration(
image: DecorationImage(
fit: BoxFit.cover,
image:
AssetImage('assets/images/placeholder.jpg')),
// color: Colors.blue,
borderRadius:
BorderRadius.all(Radius.elliptical(10, 10)),
),
)),
title: Text(maxLines: 1, session.title.toTitleCase()),
subtitle: Text(maxLines: 1, dateFormat.format(session.date as DateTime)),
trailing: IconButton(
visualDensity: VisualDensity.compact,
icon: Icon(Icons.close_rounded),
onPressed: () {
showAdaptiveDialog(
context: context,
builder: (BuildContext context) => AlertDialog(
title: const Text('Session Removal'),
content: const Text(
'Would you like to permanently remove this session?'),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.pop(context, 'Cancel'),
child: const Text('Cancel'),
),
TextButton(
onPressed: () => Navigator.pop(context, 'OK'),
child: const Text('OK'),
),
],
),
);
},
),
),
ListTile(
contentPadding: const EdgeInsets.fromLTRB(15, 0, 15, 15),
title: Text(
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontWeight: FontWeight.w300),
session.content),
),
],
)),
);
} else {
return Card(
color: color,
child: InkWell(
// overlayColor: MaterialStateColor(Colors.deepPurple as int),
splashColor: Colors.deepPurple,
borderRadius: const BorderRadius.all(Radius.elliptical(10, 10)),
onTap: () => showGeneralDialog(
// barrierColor: Colors.black.withOpacity(0.5),
transitionDuration: const Duration(milliseconds: 220),
transitionBuilder: (BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
Animation<Offset> custom = Tween<Offset>(
begin: const Offset(0.0, 1.0),
end: const Offset(0.0, 0.0))
.animate(animation);
return SlideTransition(
position: custom,
child:
Dialog.fullscreen(child: SessionView(session: session)));
},
barrierDismissible: true,
barrierLabel: '',
context: context,
pageBuilder: (context, animation1, animation2) {
return Container();
}),
child: Container(
decoration: BoxDecoration(
// color: const Color.fromARGB(47, 0, 0, 0),
borderRadius: BorderRadius.circular(10),
image: const DecorationImage(
colorFilter: ColorFilter.mode(
Color.fromARGB(220, 41, 39, 39),
BlendMode.hardLight),
image: AssetImage('assets/images/placeholder.jpg'),
fit: BoxFit.cover),
),
child: Align(
alignment: Alignment.center,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
title: Text(
maxLines: 3,
session.title.toTitleCase(),
textAlign: TextAlign.center),
subtitle: Text(
maxLines: 1,
dateFormat.format(session.date as DateTime),
textAlign: TextAlign.center),
),
])))));
}
}
}

View File

@ -1,106 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
import 'package:intl/intl.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:provider/provider.dart';
import 'package:sendtrain/daos/activities_dao.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/extensions/string_extensions.dart';
import 'package:sendtrain/widgets/session_view_achievements.dart';
import 'package:sendtrain/widgets/session_view_activities.dart';
import 'package:sendtrain/widgets/session_view_media.dart';
class SessionView extends StatelessWidget {
const SessionView({super.key, required this.session});
final Session session;
@override
Widget build(BuildContext context) {
initializeDateFormatting('en');
final DateFormat dateFormat = DateFormat('yyyy-MM-dd');
return FutureBuilder<List<Activity>>(
future: ActivitiesDao(Provider.of<AppDatabase>(context)).sessionActivities(session.id),
builder: (context, snapshot) {
if (snapshot.hasData) {
final activities = snapshot.data!;
return Scaffold(
floatingActionButtonLocation: ExpandableFab.location,
floatingActionButton: ExpandableFab(
distance: 70,
type: ExpandableFabType.up,
overlayStyle: ExpandableFabOverlayStyle(
color: Colors.black.withOpacity(0.5),
blur: 10,
),
children: [
FloatingActionButton.extended(
icon: const Icon(Icons.history_outlined),
label: Text('Restart'),
onPressed: () {},
),
FloatingActionButton.extended(
icon: const Icon(Icons.done_all_outlined),
label: Text('Done'),
onPressed: () {},
),
FloatingActionButton.extended(
icon: const Icon(Icons.edit_outlined),
label: Text('Edit'),
onPressed: () {},
),
]),
body: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
AppBar(
centerTitle: true,
title: Text(
'Session @ ${dateFormat.format(session.date as DateTime)}',
style: const TextStyle(fontSize: 15)),
),
Padding(
padding: const EdgeInsets.only(
left: 15, right: 20, top: 15, bottom: 10),
child: Text(
maxLines: 1,
style: const TextStyle(
fontSize: 25, fontWeight: FontWeight.bold),
session.title.toTitleCase())),
SessionViewAchievements(session: session),
Padding(
padding: const EdgeInsets.only(left: 15, right: 15),
child: Text(
style: const TextStyle(fontSize: 15),
session.content)),
const Padding(
padding: EdgeInsets.fromLTRB(15, 30, 0, 10),
child: Text(
style: TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
'Media:')),
SessionViewMedia(session: session),
const Padding(
padding: EdgeInsets.fromLTRB(15, 30, 0, 10),
child: Text(
style: TextStyle(
fontSize: 20, fontWeight: FontWeight.bold),
'Activites:')),
SessionViewActivities(
activities: activities),
],
));
} else {
return Container(
alignment: Alignment.center,
child: SizedBox(
height: 50.0,
width: 50.0,
child: CircularProgressIndicator(),
));
}
});
}
}

View File

@ -1,68 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:sendtrain/daos/session_activities_dao.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/extensions/string_extensions.dart';
class SessionViewAchievements extends StatelessWidget {
const SessionViewAchievements({super.key, required this.session});
final Session session;
List<String> getAchievements(List<SessionActivity> sessionActivities) {
List<String> achievements = [];
for (int i = 0; i < sessionActivities.length; i++) {
final SessionActivity sessionActivity = sessionActivities[i];
final List? saAchievments = sessionActivity.achievements?.split(',');
if (saAchievments != null) {
saAchievments.forEach((achievement) => achievements.add(achievement));
}
}
return achievements;
}
@override
Widget build(BuildContext context) {
return FutureBuilder<List<SessionActivity>>(
future: SessionActivitiesDao(Provider.of<AppDatabase>(context))
.fromSessionId(session.id),
builder: (context, snapshot) {
if (snapshot.hasData) {
final sessionActivities = snapshot.data!;
final achievements = getAchievements(sessionActivities);
return Column(
children: [
Padding(
padding: const EdgeInsets.only(bottom: 10),
child: SizedBox(
height: 40,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
itemCount: achievements.length,
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: const EdgeInsets.only(right: 5),
child: ActionChip(
visualDensity: VisualDensity.compact,
avatar:
const Icon(Icons.check_circle_outline),
label: Text(maxLines: 1, achievements[index].toTitleCase()),
onPressed: () {},
));
},
))),
],
);
} else {
return Padding(
padding: EdgeInsets.all(15),
child: CircularProgressIndicator());
}
});
}
}

View File

@ -1,23 +0,0 @@
import 'package:flutter/material.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/widgets/activity_card.dart';
class SessionViewActivities extends StatelessWidget {
const SessionViewActivities({super.key, required this.activities });
final List<Activity> activities;
@override
Widget build(BuildContext context) {
return Expanded(
child: ListView.builder(
// shrinkWrap: true,
padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
itemCount: activities.length,
itemBuilder: (BuildContext context, int index) {
return ActivityCard(
activity: activities[index]);
},
));
}
}

View File

@ -1,45 +0,0 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:sendtrain/daos/media_items_dao.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/widgets/media_card.dart';
class SessionViewMedia extends StatelessWidget {
const SessionViewMedia({super.key, required this.session});
final Session session;
@override
Widget build(BuildContext context) {
return FutureBuilder<List<MediaItem>>(
future: MediaItemsDao(Provider.of<AppDatabase>(context))
.fromSession(session),
builder: (context, snapshot) {
if (snapshot.hasData) {
final mediaItems = snapshot.data!;
List<Widget> mediaCards = List.generate(
mediaItems.length, (i) => MediaCard(media: mediaItems[i]));
return Column(
children: [
SizedBox(
width: double.infinity,
height: 100,
child: GridView.count(
padding: const EdgeInsets.fromLTRB(15, 0, 0, 0),
scrollDirection: Axis.horizontal,
crossAxisSpacing: 5,
mainAxisSpacing: 5,
crossAxisCount: 1,
children: mediaCards))
],
);
} else {
return Padding(
padding: EdgeInsets.all(15),
child: CircularProgressIndicator());
}
});
}
}

View File

@ -0,0 +1,69 @@
import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:sendtrain/daos/session_activities_dao.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/services/search/activity_finder_service.dart';
import 'package:sendtrain/widgets/generic/elements/form_search_input.dart';
class SessionActivitiesEditor extends StatelessWidget {
SessionActivitiesEditor({super.key, required this.session, this.callback});
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final TextEditingController tec = TextEditingController();
final Session session;
final Function? callback;
late final Activity selectedActivity;
@override
Widget build(
BuildContext context,
) {
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('Add Activity',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleLarge)),
FormSearchInput(
title: 'Find an Activity',
controller: tec,
service: ActivityFinderService(context),
resultHandler: (Activity content,
ActivityFinderService service) async {
tec.text = content.title;
selectedActivity = content;
}),
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
Padding(
padding: EdgeInsets.only(top: 10),
child: FilledButton(
child: Text('Submit'),
onPressed: () async {
final SessionActivitiesDao dao =
SessionActivitiesDao(
Provider.of<AppDatabase>(context, listen: false));
await dao.createOrUpdate(SessionActivitiesCompanion(
sessionId: Value(session.id),
activityId: Value(selectedActivity.id),
position: Value(0),
));
Navigator.pop(_formKey.currentContext!, 'Submit');
if (callback != null) {
await callback!();
}
}))
])
])));
}
}

View File

@ -0,0 +1,44 @@
import 'package:flutter/material.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:provider/provider.dart';
import 'package:sendtrain/daos/media_items_dao.dart';
import 'package:sendtrain/database/database.dart' hide ActivityAction;
import 'package:sendtrain/widgets/generic/elements/generic_progress_indicator.dart';
import 'package:sendtrain/widgets/sessions/session_card_full.dart';
import 'package:sendtrain/widgets/sessions/session_card_small.dart';
class SessionCard extends StatefulWidget {
final int type;
final Session session;
const SessionCard({super.key, this.type = 0, required this.session});
@override
State<SessionCard> createState() => _SessionCardState();
}
class _SessionCardState extends State<SessionCard> {
@override
Widget build(BuildContext context) {
final int type = widget.type;
final Session session = widget.session;
initializeDateFormatting('en');
return StreamBuilder<List<MediaItem>>(
stream: MediaItemsDao(Provider.of<AppDatabase>(context))
.watchSessionMediaItems(session.id),
builder: (context, snapshot) {
if (snapshot.hasData) {
List<MediaItem> mediaItems = snapshot.data!;
if (type == 0) {
return SessionCardFull(session: session, mediaItems: mediaItems);
} else {
return SessionCardSmall(session: session, mediaItems: mediaItems);
}
} else {
return GenericProgressIndicator();
}
});
}
}

View File

@ -0,0 +1,100 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:sendtrain/daos/sessions_dao.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/extensions/string_extensions.dart';
import 'package:sendtrain/helpers/date_time_helpers.dart';
import 'package:sendtrain/helpers/media_helpers.dart';
import 'package:sendtrain/helpers/widget_helpers.dart';
import 'package:sendtrain/widgets/builders/dialogs.dart';
import 'package:sendtrain/widgets/generic/elements/card_content.dart';
import 'package:sendtrain/widgets/generic/elements/card_image.dart';
import 'package:sendtrain/widgets/sessions/session_view.dart';
import 'package:collection/collection.dart';
class SessionCardFull extends StatefulWidget {
const SessionCardFull(
{super.key, required this.session, required this.mediaItems});
final Session session;
final List<MediaItem> mediaItems;
@override
State<SessionCardFull> createState() => _SessionCardFullState();
}
class _SessionCardFullState extends State<SessionCardFull> {
// late final List<MediaItem> mediaItems;
// late final MediaItem? sessionImage;
// late final Session session;
String sessionTitle(Session session) {
String title = session.title.toTitleCase();
if (session.address != null) title = "$title @ ${session.address}";
return title;
}
// @override
// initState() {
// super.initState();
// session = widget.session;
// mediaItems = widget.mediaItems;
// sessionImage = mediaItems
// .firstWhereOrNull((mediaItem) => mediaItem.type == MediaType.location);
// }
@override
Widget build(BuildContext context) {
final Session session = widget.session;
final List<MediaItem> mediaItems = widget.mediaItems;
final MediaItem? sessionImage = mediaItems
.firstWhereOrNull((mediaItem) => mediaItem.type == MediaType.location);
return Card.outlined(
color: (session.status == SessionStatus.started)
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceContainerLow,
margin: const EdgeInsets.fromLTRB(15, 15, 15, 0),
clipBehavior: Clip.hardEdge,
child: InkWell(
splashColor: Colors.deepPurple,
onLongPress: () => showMediaDetailWidget(context, sessionImage!),
onTap: () =>
showGenericDialog(SessionView(session: session), context),
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
contentPadding: EdgeInsets.only(left: 8),
leading: CardImage(
image: findMediaByType(mediaItems, MediaType.location),
padding: EdgeInsets.only(left: 5, top: 5)),
title: Text(maxLines: 1, sessionTitle(session)),
subtitle: Text(
maxLines: 1, dateFormat.format(session.date as DateTime)),
trailing: IconButton(
padding: EdgeInsets.all(0),
alignment: Alignment.topCenter,
icon: Icon(Icons.close_rounded),
onPressed: () {
showRemovalDialog(
'Session Removal',
'Would you like to permanently remove this session?',
context, () {
SessionsDao(
Provider.of<AppDatabase>(context, listen: false))
.remove(session);
}).then((result) {
setState(() {});
});
},
),
),
CardContent(content: session.content)
],
)),
);
}
}

View File

@ -0,0 +1,84 @@
import 'package:flutter/material.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/extensions/string_extensions.dart';
import 'package:sendtrain/helpers/date_time_helpers.dart';
import 'package:sendtrain/helpers/media_helpers.dart';
import 'package:sendtrain/helpers/widget_helpers.dart';
import 'package:sendtrain/widgets/builders/dialogs.dart';
import 'package:sendtrain/widgets/sessions/session_view.dart';
import 'package:collection/collection.dart';
class SessionCardSmall extends StatefulWidget {
const SessionCardSmall(
{super.key, required this.session, required this.mediaItems});
final Session session;
final List<MediaItem> mediaItems;
@override
State<SessionCardSmall> createState() => _SessionCardSmallState();
}
class _SessionCardSmallState extends State<SessionCardSmall> {
// late final List<MediaItem> mediaItems;
// late final MediaItem? sessionImage;
// late final Session session;
// @override
// initState() {
// super.initState();
// session = widget.session;
// mediaItems = widget.mediaItems;
// sessionImage = mediaItems.firstWhereOrNull((mediaItem) => mediaItem.type == MediaType.location);
// }
@override
Widget build(BuildContext context) {
final Session session = widget.session;
final List<MediaItem> mediaItems = widget.mediaItems;
final MediaItem? sessionImage = mediaItems
.firstWhereOrNull((mediaItem) => mediaItem.type == MediaType.location);
return Card(
color: (session.status == SessionStatus.started)
? Theme.of(context).colorScheme.primaryContainer
: Theme.of(context).colorScheme.surfaceContainerLow,
child: InkWell(
// overlayColor: MaterialStateColor(Colors.deepPurple as int),
splashColor: Colors.deepPurple,
borderRadius: const BorderRadius.all(Radius.elliptical(10, 10)),
onLongPress: () {
if (sessionImage != null) {
showMediaDetailWidget(context, sessionImage);
}
},
onTap: () =>
showGenericDialog(SessionView(session: session), context),
child: Container(
decoration: BoxDecoration(
// color: const Color.fromARGB(47, 0, 0, 0),
borderRadius: BorderRadius.circular(10),
image: DecorationImage(
colorFilter: ColorFilter.mode(
Color.fromARGB(220, 41, 39, 39), BlendMode.hardLight),
image: findMediaByType(mediaItems, MediaType.location),
fit: BoxFit.cover),
),
child: Align(
alignment: Alignment.center,
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
title: Text(
maxLines: 3,
session.title.toTitleCase(),
textAlign: TextAlign.center),
subtitle: Text(
maxLines: 1,
dateFormat.format(session.date as DateTime),
textAlign: TextAlign.center),
),
])))));
}
}

View File

@ -0,0 +1,318 @@
import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:drift/drift.dart' hide Column;
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:mime/mime.dart';
import 'package:provider/provider.dart';
import 'package:sendtrain/daos/media_items_dao.dart';
import 'package:sendtrain/daos/object_media_items_dao.dart';
import 'package:sendtrain/daos/sessions_dao.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/services/search/google_places_service.dart';
import 'package:sendtrain/widgets/builders/dialogs.dart';
import 'package:sendtrain/widgets/generic/elements/form_search_input.dart';
import 'package:sendtrain/widgets/generic/elements/form_text_input.dart';
import 'package:sendtrain/widgets/sessions/session_view.dart';
class SessionEditor extends StatefulWidget {
const SessionEditor({super.key, this.data, this.session, this.callback});
final Session? session;
final Map<String, dynamic>? data;
final Function? callback;
@override
State<SessionEditor> createState() => _SessionEditorState();
}
// used to pass the result of the found image back to current context...
class SessionPayload {
String? photoUri;
String? address;
List<PlatformFile>? files;
}
class _SessionEditorState extends State<SessionEditor> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
late AppDatabase db;
String editorType = 'Create';
final Map<String, TextEditingController> sessionCreateController = {
'name': TextEditingController(),
'content': TextEditingController(),
'status': TextEditingController(),
'date': TextEditingController(),
'address': TextEditingController(),
'media': TextEditingController(),
};
final SessionPayload sessionPayload = SessionPayload();
@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.session != null) {
editorType = 'Edit';
final Session session = widget.session!;
sessionCreateController['name']?.text = session.title;
sessionCreateController['content']?.text = session.content;
sessionCreateController['status']?.text = session.status.name;
sessionCreateController['date']?.text =
DateFormat('yyyy-MM-dd').format(session.date!);
if (session.address != null) {
sessionCreateController['address']?.text = session.address!;
}
}
}
Future createSession(context) async {
Map<Symbol, Value> payload = {
Symbol('title'): Value<String>(sessionCreateController['name']!.text),
Symbol('content'):
Value<String>(sessionCreateController['content']!.text),
// we want to maintain existing status during update
Symbol('status'): widget.session != null
? Value<SessionStatus>(widget.session!.status)
: Value<SessionStatus>(SessionStatus.pending),
Symbol('date'): Value<DateTime>(
DateTime.parse(sessionCreateController['date']!.text)),
};
// if a session exists we'll want to update it
// so the payload needs the session id
if (widget.session != null) {
payload[Symbol('id')] = Value<int>(widget.session!.id);
}
// optional params
if (sessionCreateController['address']!.text.isNotEmpty) {
payload[Symbol('address')] =
Value<String>(sessionCreateController['address']!.text);
}
return await SessionsDao(db)
.createOrUpdate(Function.apply(SessionsCompanion.new, [], payload));
}
Future deleteSessionMedia(int sessionId, MediaType mediaType) async {
List<MediaItem> deletedMedia =
(await MediaItemsDao(db).fromSession(sessionId))
.where((mediaItem) => mediaItem.type == mediaType)
.toList();
for (int i = 0; i < deletedMedia.length; i++) {
await MediaItemsDao(db).remove(deletedMedia[i]);
}
}
Future createSessionMedia(
title,
sessionId,
description,
reference,
mediaType,
) async {
// if (sessionPayload.photoUri != null) {
MediaItemsCompanion mediaItem = MediaItemsCompanion(
title: Value(title),
description: Value(description),
reference: Value(reference.toString()),
type: Value(mediaType));
return await MediaItemsDao(db).createOrUpdate(mediaItem).then((id) async {
ObjectMediaItemsCompanion omi = ObjectMediaItemsCompanion(
objectId: Value(sessionId),
objectType: Value(ObjectType.sessions),
mediaId: Value(id),
);
await ObjectMediaItemsDao(db).createOrUpdate(omi);
});
// }
}
@override
Widget build(BuildContext context) {
sessionCreateController['date']!.text =
DateFormat('yyyy-MM-dd').format(DateTime.now());
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 Session',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleLarge)),
FormTextInput(
controller: sessionCreateController['name']!,
title: 'Title'),
FormTextInput(
controller: sessionCreateController['content']!,
title: 'Description',
icon: Icon(Icons.description_rounded),
maxLines: 10),
FormTextInput(
controller: sessionCreateController['date']!,
title: 'Date',
icon: Icon(Icons.date_range_rounded),
onTap: () {
showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime.now(),
lastDate: DateTime.now().add(Duration(days: 365)))
.then((date) {
if (date != null) {
sessionCreateController['date']?.text =
DateFormat('yyyy-MM-dd').format(date);
}
});
}),
FormSearchInput(
title: 'Location (optional)',
controller: sessionCreateController['address']!,
service: GooglePlacesService(),
resultHandler: (content, service) async {
if (content.imageReferences != null) {
// get a random photo item from the returned result
Map<String, dynamic> photo = content.imageReferences![
Random().nextInt(content.imageReferences!.length)];
await service
.fetchPhoto(photo['name'])
.then((photoMap) {
sessionPayload.photoUri = photoMap['photoUri'];
});
}
sessionPayload.address = content.address;
sessionCreateController['address']!.text =
content.description;
service.finish();
}),
Padding(
padding: EdgeInsets.only(top: 10, bottom: 10),
child: TextFormField(
readOnly: true,
decoration: InputDecoration(
prefixIcon: Icon(Icons.image_rounded),
filled: true,
border: OutlineInputBorder(
borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(12),
),
labelText: 'Media (optional)',
),
controller: sessionCreateController['media'],
onTap: () async {
FilePickerResult? result = await FilePicker.platform
.pickFiles(
allowMultiple: true, type: FileType.image);
if (result != null) {
List<PlatformFile> files = result.files;
sessionCreateController['media']!.text =
files.map((file) => file.name).toString();
sessionPayload.files = files;
}
})),
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
Padding(
padding: EdgeInsets.only(top: 10),
child: FilledButton(
onPressed: () async => {
if (_formKey.currentState!.validate())
{
await createSession(_formKey.currentContext)
.then((sessionId) async {
int currentSessionId = sessionId;
// dirft weirdly doesn't return the proper id if
// an upsert ends up bein an update, so we'll
// set the id to the provided session for an update
if (widget.session != null) {
currentSessionId = widget.session!.id;
}
// if we've found a photo add it to media!
if (sessionPayload.photoUri != null) {
await deleteSessionMedia(
currentSessionId,
MediaType.location);
await createSessionMedia(
'Location Image',
currentSessionId,
sessionPayload.address,
sessionPayload.photoUri,
MediaType.location);
}
// if we've selected files to save, save them
if (sessionPayload.files != null) {
for (int i = 0;
i < sessionPayload.files!.length;
i++) {
PlatformFile file =
sessionPayload.files![i];
String? type =
lookupMimeType(file.path!)!
.split('/')
.first;
Uint8List fileBytes =
await file.xFile.readAsBytes();
MediaType mediaType =
MediaType.localImage;
if (type == "video") {
mediaType = MediaType.localVideo;
}
await createSessionMedia(
'Local Media',
currentSessionId,
file.name,
base64Encode(fileBytes),
mediaType);
}
}
// if session is null it's new so we show the dialog
// otherwise the dialog is already open, so no need
if (widget.session == null) {
SessionsDao(db)
.find(currentSessionId)
.then((session) =>
showGenericDialog(
SessionView(
session: session),
_formKey.currentContext!));
}
Navigator.pop(
_formKey.currentContext!, 'Submit');
if (widget.callback != null) {
await widget.callback!();
}
})
}
},
child: Text('Submit')))
]),
],
)));
}
}

Some files were not shown because too many files have changed in this diff Show More