Compare commits
1 Commits
7ead6ba631
...
local-noti
Author | SHA1 | Date | |
---|---|---|---|
98077e7d57 |
@ -9,10 +9,6 @@
|
||||
# packages, and plugins designed to encourage good coding practices.
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
analyzer:
|
||||
exclude:
|
||||
- "**/*.g.dart"
|
||||
|
||||
linter:
|
||||
# The lint rules applied to this project can be customized in the
|
||||
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||
|
1
android/app/proguard-rules.pro
vendored
1
android/app/proguard-rules.pro
vendored
@ -1 +0,0 @@
|
||||
-keep class androidx.lifecycle.DefaultLifecycleObserver
|
@ -1,7 +1,4 @@
|
||||
<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}"
|
||||
@ -24,8 +21,8 @@
|
||||
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.
|
||||
@ -34,8 +31,6 @@
|
||||
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.
|
||||
@ -43,9 +38,8 @@
|
||||
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>
|
22617
assets/exercises.json
22617
assets/exercises.json
File diff suppressed because it is too large
Load Diff
@ -1,9 +0,0 @@
|
||||
targets:
|
||||
$default:
|
||||
builders:
|
||||
drift_dev:
|
||||
options:
|
||||
schema_dir: lib/database/drift_schemas/
|
||||
databases:
|
||||
# Required: A name for the database and it's path
|
||||
sendtrain: lib/database/database.dart
|
@ -1,6 +1,5 @@
|
||||
import UIKit
|
||||
import Flutter
|
||||
import GoogleMaps
|
||||
|
||||
@UIApplicationMain
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
@ -8,7 +7,6 @@ import GoogleMaps
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
GMSServices.provideAPIKey("AIzaSyBCjMCEAyyNVpsnVYvZj6VL1mmB98Vd6AE")
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
@ -2,13 +2,6 @@
|
||||
<!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>
|
||||
|
@ -1,35 +0,0 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:sendtrain/database/database.dart';
|
||||
|
||||
part 'actions_dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [Actions])
|
||||
class ActionsDao extends DatabaseAccessor<AppDatabase> with _$ActionsDaoMixin {
|
||||
ActionsDao(super.db);
|
||||
|
||||
Future<List<Action>> all() async {
|
||||
return await select(actions).get();
|
||||
}
|
||||
|
||||
Future<Action> find(int id) async {
|
||||
return await (select(actions)..where((action) => action.id.equals(id) )).getSingle();
|
||||
}
|
||||
|
||||
Future<List<Action>> fromActivity(Activity activity) async {
|
||||
final result = select(db.activityActions).join(
|
||||
[
|
||||
innerJoin(
|
||||
db.actions,
|
||||
db.actions.id.equalsExp(db.activityActions.actionId),
|
||||
),
|
||||
],
|
||||
)
|
||||
..where(db.activityActions.activityId.equals(activity.id));
|
||||
|
||||
final actions = (await result.get())
|
||||
.map((e) => e.readTable(db.actions))
|
||||
.toList();
|
||||
|
||||
return actions;
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'actions_dao.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
mixin _$ActionsDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||
$ActionsTable get actions => attachedDatabase.actions;
|
||||
}
|
@ -1,89 +0,0 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:sendtrain/database/database.dart';
|
||||
|
||||
part 'activities_dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [Activities])
|
||||
class ActivitiesDao extends DatabaseAccessor<AppDatabase>
|
||||
with _$ActivitiesDaoMixin {
|
||||
ActivitiesDao(super.db);
|
||||
|
||||
Future<List<Activity>> all() async {
|
||||
return await select(activities).get();
|
||||
}
|
||||
|
||||
Future<Activity> find(int id) async {
|
||||
return await (select(activities)
|
||||
..where((activity) => activity.id.equals(id)))
|
||||
.getSingle();
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
],
|
||||
)..where(db.sessionActivities.sessionId.equals(id));
|
||||
|
||||
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;
|
||||
// }
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'activities_dao.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
mixin _$ActivitiesDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||
$ActivitiesTable get activities => attachedDatabase.activities;
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:sendtrain/database/database.dart';
|
||||
|
||||
part 'activity_actions_dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [ActivityActions])
|
||||
class ActivityActionsDao extends DatabaseAccessor<AppDatabase> with _$ActivityActionsDaoMixin {
|
||||
ActivityActionsDao(super.db);
|
||||
|
||||
Future<List<ActivityAction>> all() => select(activityActions).get();
|
||||
Stream<List<ActivityAction>> watch() => select(activityActions).watch();
|
||||
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<List<ActivityAction>> all() async {
|
||||
// return await select(activityActions).get();
|
||||
// }
|
||||
|
||||
Future<ActivityAction> find(int id) async {
|
||||
return await (select(activityActions)..where((activityAction) => activityAction.id.equals(id) )).getSingle();
|
||||
}
|
||||
|
||||
Future<List<ActivityAction>> fromActivityId(int id) async {
|
||||
final result = db.managers.activityActions
|
||||
.filter((activityAction) => activityAction.activityId.id(id));
|
||||
|
||||
return result.get();
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'activity_actions_dao.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
mixin _$ActivityActionsDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||
$ActivitiesTable get activities => attachedDatabase.activities;
|
||||
$ActionsTable get actions => attachedDatabase.actions;
|
||||
$ActivityActionsTable get activityActions => attachedDatabase.activityActions;
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:sendtrain/database/database.dart';
|
||||
|
||||
part 'media_items_dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [MediaItems])
|
||||
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();
|
||||
}
|
||||
|
||||
Future<List<MediaItem>> fromActivity(Activity activity) async {
|
||||
final result = select(db.objectMediaItems).join(
|
||||
[
|
||||
innerJoin(
|
||||
db.mediaItems,
|
||||
db.mediaItems.id.equalsExp(db.objectMediaItems.mediaId),
|
||||
),
|
||||
],
|
||||
)
|
||||
..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();
|
||||
|
||||
return mediaItems;
|
||||
}
|
||||
|
||||
Future<List<MediaItem>> fromSession(int sessionId) async {
|
||||
final result = 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(sessionId));
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'media_items_dao.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
mixin _$MediaItemsDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||
$MediaItemsTable get mediaItems => attachedDatabase.mediaItems;
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
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();
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
// 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;
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:sendtrain/database/database.dart';
|
||||
|
||||
part 'session_activities_dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [SessionActivities])
|
||||
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();
|
||||
}
|
||||
|
||||
Future<List<SessionActivity>> fromSessionId(int id) async {
|
||||
final result = db.managers.sessionActivities
|
||||
.filter((sessionActivity) => sessionActivity.sessionId.id(id));
|
||||
|
||||
return result.get();
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'session_activities_dao.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
mixin _$SessionActivitiesDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||
$SessionsTable get sessions => attachedDatabase.sessions;
|
||||
$ActivitiesTable get activities => attachedDatabase.activities;
|
||||
$SessionActivitiesTable get sessionActivities =>
|
||||
attachedDatabase.sessionActivities;
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:sendtrain/database/database.dart';
|
||||
|
||||
part 'sessions_dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [Sessions])
|
||||
class SessionsDao extends DatabaseAccessor<AppDatabase> with _$SessionsDaoMixin {
|
||||
SessionsDao(super.db);
|
||||
|
||||
Future<Session> find(int id) => (select(sessions)..where((session) => session.id.equals(id) )).getSingle();
|
||||
Stream<Session> watchSession(int id) => (select(sessions)..where((session) => session.id.equals(id) )).watchSingle();
|
||||
Future<List<Session>> all() => select(sessions).get();
|
||||
Stream<List<Session>> watch() => select(sessions).watch();
|
||||
Future createOrUpdate(SessionsCompanion session) => into(sessions).insertOnConflictUpdate(session);
|
||||
Future replace(Session session) => update(sessions).replace(session);
|
||||
Future remove(Session session) => delete(sessions).delete(session);
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'sessions_dao.dart';
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
mixin _$SessionsDaoMixin on DatabaseAccessor<AppDatabase> {
|
||||
$SessionsTable get sessions => attachedDatabase.sessions;
|
||||
}
|
121
lib/database.dart
Normal file
121
lib/database.dart
Normal file
@ -0,0 +1,121 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_flutter/drift_flutter.dart';
|
||||
|
||||
part 'database.g.dart';
|
||||
|
||||
|
||||
enum SessionStatus {
|
||||
pending,
|
||||
started,
|
||||
completed,
|
||||
missed
|
||||
}
|
||||
|
||||
class Sessions extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get title => text().withLength(min: 3, max: 32)();
|
||||
TextColumn get content => text().named('body')();
|
||||
TextColumn get status => textEnum<SessionStatus>()();
|
||||
DateTimeColumn get date => dateTime().nullable()();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(Variable(DateTime.now()))();
|
||||
}
|
||||
|
||||
class SessionActivities extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
IntColumn get sessionId => integer().references(Sessions, #id)();
|
||||
IntColumn get activityId => integer().references(Activities, #id)();
|
||||
TextColumn get results => text().nullable()();
|
||||
TextColumn get achievements => text().nullable()();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(Variable(DateTime.now()))();
|
||||
}
|
||||
|
||||
enum ActivityCategories {
|
||||
fundamentals,
|
||||
conditioning,
|
||||
advanced,
|
||||
custom,
|
||||
pro
|
||||
}
|
||||
|
||||
enum ActivityType {
|
||||
strength,
|
||||
power,
|
||||
conditioning,
|
||||
hypertrophy,
|
||||
endurance,
|
||||
stability,
|
||||
mobility,
|
||||
flexibility,
|
||||
rehabilitation,
|
||||
technical
|
||||
}
|
||||
|
||||
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>()();
|
||||
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)();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(Variable(DateTime.now()))();
|
||||
}
|
||||
|
||||
class Actions extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get title => text().withLength(min: 3, max: 32)();
|
||||
TextColumn get description => text().named('body')();
|
||||
TextColumn get set => text()();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(Variable(DateTime.now()))();
|
||||
}
|
||||
|
||||
class ObjectMediaItems extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
IntColumn get objectId => integer().references(Actions, #id)();
|
||||
IntColumn get mediaId => integer().references(MediaItems, #id)();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(Variable(DateTime.now()))();
|
||||
}
|
||||
|
||||
enum MediaType {
|
||||
youtube,
|
||||
image
|
||||
}
|
||||
class MediaItems extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get title => text().withLength(min: 3, max: 32)();
|
||||
TextColumn get description => text().named('body')();
|
||||
TextColumn get reference => text().withLength(min: 3, max: 32)();
|
||||
TextColumn get type => textEnum<MediaType>()();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(Variable(DateTime.now()))();
|
||||
}
|
||||
|
||||
@DriftDatabase(tables: [
|
||||
Sessions,
|
||||
SessionActivities,
|
||||
Activities,
|
||||
ActivityActions,
|
||||
Actions,
|
||||
ObjectMediaItems,
|
||||
MediaItems
|
||||
])
|
||||
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
// After generating code, this class needs to define a `schemaVersion` getter
|
||||
// and a constructor telling drift where the database should be stored.
|
||||
// These are described in the getting started guide: https://drift.simonbinder.eu/setup/
|
||||
AppDatabase() : super(_openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
|
||||
static QueryExecutor _openConnection() {
|
||||
// `driftDatabase` from `package:drift_flutter` stores the database in
|
||||
// `getApplicationDocumentsDirectory()`.
|
||||
return driftDatabase(name: 'sendtrain');
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
@ -1,202 +0,0 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift_flutter/drift_flutter.dart';
|
||||
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';
|
||||
|
||||
part 'database.g.dart';
|
||||
|
||||
@DriftDatabase(tables: [
|
||||
Sessions,
|
||||
SessionActivities,
|
||||
Activities,
|
||||
ActivityActions,
|
||||
Actions,
|
||||
ObjectMediaItems,
|
||||
MediaItems
|
||||
], daos: [
|
||||
SessionsDao,
|
||||
ActivitiesDao,
|
||||
MediaItemsDao,
|
||||
ObjectMediaItemsDao,
|
||||
SessionActivitiesDao,
|
||||
ActivityActionsDao,
|
||||
ActionsDao
|
||||
])
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
// After generating code, this class needs to define a `schemaVersion` getter
|
||||
// and a constructor telling drift where the database should be stored.
|
||||
// These are described in the getting started guide: https://drift.simonbinder.eu/setup/
|
||||
AppDatabase() : super(_openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 19;
|
||||
|
||||
@override
|
||||
MigrationStrategy get migration {
|
||||
return MigrationStrategy(
|
||||
onCreate: (m) async {
|
||||
await m.createAll().then((r) async {
|
||||
await seedDb(this);
|
||||
}); // create all tables
|
||||
},
|
||||
beforeOpen: (details) async {
|
||||
/// Enable foreign_keys
|
||||
await customStatement('PRAGMA foreign_keys = ON');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
static QueryExecutor _openConnection() {
|
||||
// `driftDatabase` from `package:drift_flutter` stores the database in
|
||||
// `getApplicationDocumentsDirectory()`.
|
||||
return driftDatabase(name: 'sendtrain');
|
||||
}
|
||||
}
|
||||
|
||||
enum SessionStatus { pending, started, completed, missed }
|
||||
|
||||
class Sessions extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
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()))();
|
||||
}
|
||||
|
||||
class SessionActivities extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
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()();
|
||||
DateTimeColumn get createdAt =>
|
||||
dateTime().withDefault(Variable(DateTime.now()))();
|
||||
}
|
||||
|
||||
enum ActivityCategories { fundamentals, conditioning, advanced, custom, pro }
|
||||
|
||||
enum ActivityType {
|
||||
strength,
|
||||
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: 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, onDelete: KeyAction.cascade)();
|
||||
IntColumn get actionId =>
|
||||
integer().references(Actions, #id, onDelete: KeyAction.cascade)();
|
||||
IntColumn get position => integer()();
|
||||
DateTimeColumn get createdAt =>
|
||||
dateTime().withDefault(Variable(DateTime.now()))();
|
||||
}
|
||||
|
||||
class Actions extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get title => text().withLength(min: 3, max: 32)();
|
||||
TextColumn get description => text().named('body')();
|
||||
TextColumn get set => text()();
|
||||
DateTimeColumn get createdAt =>
|
||||
dateTime().withDefault(Variable(DateTime.now()))();
|
||||
}
|
||||
|
||||
enum ObjectType {
|
||||
actions,
|
||||
activities,
|
||||
sessions,
|
||||
}
|
||||
|
||||
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, onDelete: KeyAction.cascade)();
|
||||
DateTimeColumn get createdAt =>
|
||||
dateTime().withDefault(Variable(DateTime.now()))();
|
||||
}
|
||||
|
||||
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 description => text().named('body')();
|
||||
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 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
@ -1,232 +0,0 @@
|
||||
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';
|
||||
|
||||
Future<void> seedDb(AppDatabase database) async {
|
||||
// seed data setup
|
||||
final List<List> sessionValues = [
|
||||
[
|
||||
'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',
|
||||
'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.',
|
||||
'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.',
|
||||
'Gravity Hamilton'
|
||||
],
|
||||
[
|
||||
'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'
|
||||
],
|
||||
];
|
||||
|
||||
final List<List> mediaItems = [
|
||||
[
|
||||
'https://www.climbing.com/wp-content/uploads/2022/06/campus-board-e1655470701154.jpeg',
|
||||
MediaType.image
|
||||
],
|
||||
['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 int totalSessions = 15;
|
||||
final int totalActivities = 6;
|
||||
final int totalActions = 5;
|
||||
final int totalMedia = 5;
|
||||
final random = Random();
|
||||
|
||||
// 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']),
|
||||
Symbol('description'): Value<String>(exercise['instructions'].toString()),
|
||||
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: '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,
|
||||
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++) {
|
||||
// session things
|
||||
var status = SessionStatus.completed;
|
||||
if (i == 0) status = SessionStatus.started;
|
||||
if (i == 1) status = SessionStatus.pending;
|
||||
|
||||
final sessionValue = sessionValues[random.nextInt(sessionValues.length)];
|
||||
await database
|
||||
.into(database.sessions)
|
||||
.insert(SessionsCompanion.insert(
|
||||
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.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 n = 0; n <= random.nextInt(totalMedia); n++) {
|
||||
final mediaItem = mediaItems[random.nextInt(mediaItems.length)];
|
||||
await database
|
||||
.into(database.mediaItems)
|
||||
.insert(MediaItemsCompanion.insert(
|
||||
title: 'Media title $n',
|
||||
description:
|
||||
'Media description $n 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: sessionId,
|
||||
mediaId: mediaId,
|
||||
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));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -1,84 +0,0 @@
|
||||
List<String> exceptions = [
|
||||
'a',
|
||||
'abaft',
|
||||
'about',
|
||||
'above',
|
||||
'afore',
|
||||
'after',
|
||||
'along',
|
||||
'amid',
|
||||
'among',
|
||||
'an',
|
||||
'apud',
|
||||
'as',
|
||||
'aside',
|
||||
'at',
|
||||
'atop',
|
||||
'below',
|
||||
'but',
|
||||
'by',
|
||||
'circa',
|
||||
'down',
|
||||
'for',
|
||||
'from',
|
||||
'given',
|
||||
'in',
|
||||
'into',
|
||||
'lest',
|
||||
'like',
|
||||
'mid',
|
||||
'midst',
|
||||
'minus',
|
||||
'near',
|
||||
'next',
|
||||
'of',
|
||||
'off',
|
||||
'on',
|
||||
'onto',
|
||||
'out',
|
||||
'over',
|
||||
'pace',
|
||||
'past',
|
||||
'per',
|
||||
'plus',
|
||||
'pro',
|
||||
'qua',
|
||||
'round',
|
||||
'sans',
|
||||
'save',
|
||||
'since',
|
||||
'than',
|
||||
'thru',
|
||||
'till',
|
||||
'times',
|
||||
'to',
|
||||
'under',
|
||||
'until',
|
||||
'unto',
|
||||
'up',
|
||||
'upon',
|
||||
'via',
|
||||
'vice',
|
||||
'with',
|
||||
'worth',
|
||||
'the","and',
|
||||
'nor',
|
||||
'or',
|
||||
'yet',
|
||||
'so'
|
||||
];
|
||||
|
||||
extension TitleCase on String {
|
||||
String toTitleCase() {
|
||||
return toLowerCase().replaceAllMapped(
|
||||
RegExp(
|
||||
r'[A-Z]{2,}(?=[A-Z][a-z]+[0-9]*|\b)|[A-Z]?[a-z]+[0-9]*|[A-Z]|[0-9]+'),
|
||||
(Match match) {
|
||||
|
||||
// if (exceptions.contains(match[0])) {
|
||||
// return match[0]!;
|
||||
// }
|
||||
return "${match[0]![0].toUpperCase()}${match[0]!.substring(1).toLowerCase()}";
|
||||
}).replaceAll(RegExp(r'(_|-)+'), ' ');
|
||||
}
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
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";
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
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));
|
||||
}
|
||||
|
||||
showEditorSheet(BuildContext context, Widget widget) {
|
||||
showModalBottomSheet<void>(
|
||||
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;
|
||||
});
|
||||
}
|
124
lib/main.dart
124
lib/main.dart
@ -1,37 +1,19 @@
|
||||
import 'package:drift/drift.dart';
|
||||
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/database.dart';
|
||||
import 'package:sendtrain/models/activity_timer_model.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';
|
||||
import 'package:sendtrain/screens/activities_screen.dart';
|
||||
import 'package:sendtrain/screens/sessions_screen.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.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,
|
||||
),
|
||||
),
|
||||
theme: ThemeData.dark(useMaterial3: true),
|
||||
home: const App());
|
||||
}
|
||||
}
|
||||
@ -90,8 +72,7 @@ class _AppState extends State<App> {
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.sports), label: "Sessions"),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.sports_gymnastics_rounded),
|
||||
label: "Activities"),
|
||||
icon: Icon(Icons.landscape), label: "Activities"),
|
||||
NavigationDestination(
|
||||
icon: Icon(Icons.calendar_month_rounded), label: "Plan"),
|
||||
NavigationDestination(
|
||||
@ -101,23 +82,96 @@ class _AppState extends State<App> {
|
||||
]),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
showEditorSheet(context, SessionEditor());
|
||||
// Add your onPressed code here!
|
||||
},
|
||||
label: const Text('New Session'),
|
||||
icon: const Icon(Icons.add_chart),
|
||||
// backgroundColor: Colors.deepPurple,
|
||||
backgroundColor: Colors.deepPurple,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
void main() {
|
||||
runApp(MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider(create: (context) => ActivityTimerModel()),
|
||||
Provider<AppDatabase>(
|
||||
create: (context) => AppDatabase(),
|
||||
dispose: (context, db) => db.close()),
|
||||
],
|
||||
// final database = AppDatabase();
|
||||
|
||||
// database.into(database.sessions).insert(SessionsCompanion.insert(
|
||||
// title: 'Projecting @ Climbers Rock',
|
||||
// content: 'Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.',
|
||||
// status: SessionStatus.started,
|
||||
// date: Value(DateTime.now())));
|
||||
|
||||
// database.into(database.sessions).insert(SessionsCompanion.insert(
|
||||
// title: 'Moonboard @ Boardroom',
|
||||
// content: 'Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.',
|
||||
// status: SessionStatus.pending,
|
||||
// date: Value(DateTime.now())));
|
||||
|
||||
// database.into(database.sessions).insert(SessionsCompanion.insert(
|
||||
// title: 'Moonboard @ Boardroom',
|
||||
// content: 'Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.',
|
||||
// status: SessionStatus.completed,
|
||||
// date: Value(DateTime.now())));
|
||||
|
||||
// database.into(database.sessions).insert(SessionsCompanion.insert(
|
||||
// title: 'Projecting @ Climbers Rock',
|
||||
// content: 'Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.',
|
||||
// status: SessionStatus.completed,
|
||||
// date: Value(DateTime.now())));
|
||||
|
||||
// database.into(database.sessions).insert(SessionsCompanion.insert(
|
||||
// title: 'Off-Wall Training',
|
||||
// content: 'Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.',
|
||||
// status: SessionStatus.missed,
|
||||
// date: Value(DateTime.now())));
|
||||
|
||||
// database.into(database.sessions).insert(SessionsCompanion.insert(
|
||||
// title: 'Off-Wall Training',
|
||||
// content: 'Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.',
|
||||
// status: SessionStatus.completed,
|
||||
// date: Value(DateTime.now())));
|
||||
|
||||
// database.into(database.activities).insert(ActivitiesCompanion.insert(
|
||||
// title: "test activity",
|
||||
// type: ActivityType.technical,
|
||||
// description: "test training activity",
|
||||
// category: ActivityCategories.fundamentals));
|
||||
|
||||
// database
|
||||
// .into(database.sessionActivities)
|
||||
// .insert(SessionActivitiesCompanion.insert(
|
||||
// sessionId: 1,
|
||||
// activityId: 1,
|
||||
// results: Value("results json, will need to test"),
|
||||
// achievements: Value("comma, seperated, items"),
|
||||
// ));
|
||||
|
||||
// database.into(database.actions).insert(ActionsCompanion.insert(
|
||||
// title: "test action title",
|
||||
// description: "teste action description",
|
||||
// set: "not sure how the json will work yet",
|
||||
// ));
|
||||
|
||||
// database
|
||||
// .into(database.activityActions)
|
||||
// .insert(ActivityActionsCompanion.insert(
|
||||
// activityId: 1,
|
||||
// actionId: 1,
|
||||
// ));
|
||||
|
||||
// database.into(database.mediaItems).insert(MediaItemsCompanion.insert(
|
||||
// title: "test youtube media item",
|
||||
// description: "this is a test youtube item",
|
||||
// reference: "sZVAEy9UmoY",
|
||||
// type: MediaType.youtube));
|
||||
|
||||
// database
|
||||
// .into(database.objectMediaItems)
|
||||
// .insert(ObjectMediaItemsCompanion.insert(objectId: 1, mediaId: 1));
|
||||
|
||||
runApp(
|
||||
ChangeNotifierProvider(
|
||||
create: (context) => ActivityTimerModel(),
|
||||
child: const SendTrain(),
|
||||
));
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -1,15 +1,13 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
import 'package:sendtrain/database/database.dart';
|
||||
import 'package:sendtrain/models/activity_model.dart';
|
||||
|
||||
class ActivityTimerModel with ChangeNotifier {
|
||||
int _actionCounter = 0;
|
||||
Activity? _activity;
|
||||
ActivityModel? _activity;
|
||||
List _sets = [];
|
||||
// List _actions = [];
|
||||
int _currentActionNum = 0;
|
||||
int _currentSetNum = 0;
|
||||
Timer? _periodicTimer;
|
||||
@ -22,22 +20,20 @@ class ActivityTimerModel with ChangeNotifier {
|
||||
dynamic get currentAction => currentSet[_currentActionNum];
|
||||
int get currentSetNum => _currentSetNum;
|
||||
dynamic get currentSet => _sets[_currentSetNum];
|
||||
Activity? get activity => _activity;
|
||||
ActivityModel? get activity => _activity;
|
||||
List get sets => _sets;
|
||||
Timer? get periodicTimer => _periodicTimer;
|
||||
bool get isActive => _isActive();
|
||||
double get progress => _progress;
|
||||
int get totalTime => _totalTime;
|
||||
|
||||
void setup(Activity activity, List actions) {
|
||||
void setup(ActivityModel activity) {
|
||||
if (_activity == null || activity.id != _activity?.id) {
|
||||
_periodicTimer?.cancel();
|
||||
_progress = 0;
|
||||
_isc = null;
|
||||
_activity = activity;
|
||||
// only one action for now
|
||||
_sets = json.decode(actions[0].set);
|
||||
// _actions = actions;
|
||||
_sets = activity.actions[0].items();
|
||||
_currentActionNum = 0;
|
||||
_currentSetNum = 0;
|
||||
setActionCount();
|
||||
|
@ -1,12 +0,0 @@
|
||||
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});
|
||||
}
|
@ -1,10 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
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/activities_header.dart';
|
||||
// import '../widgets/activities/activity_card.dart';
|
||||
import '../widgets/activities_header.dart';
|
||||
import '../widgets/activity_card.dart';
|
||||
|
||||
class ActivitiesScreen extends StatefulWidget {
|
||||
const ActivitiesScreen({super.key});
|
||||
@ -14,7 +13,6 @@ class ActivitiesScreen extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ActivitiesScreenState extends State<ActivitiesScreen> {
|
||||
final Activity? activity = null;
|
||||
|
||||
final data = ActivityModel(
|
||||
id: 1,
|
||||
@ -42,24 +40,23 @@ class _ActivitiesScreenState extends State<ActivitiesScreen> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text("N/A");
|
||||
// List<Widget> activities = List.generate(10, (i) => ActivityCard(activity: data, data: activity));
|
||||
List<Widget> activities = List.generate(10, (i) => ActivityCard(activity: data));
|
||||
|
||||
// return Padding(
|
||||
// padding: const EdgeInsets.fromLTRB(10, 15, 10, 0),
|
||||
// child: Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: <Widget>[
|
||||
// const ActivitiesHeader(),
|
||||
// Expanded(
|
||||
// child: GridView.count(
|
||||
// primary: false,
|
||||
// padding: const EdgeInsets.fromLTRB(0, 0, 0, 0),
|
||||
// crossAxisSpacing: 10,
|
||||
// mainAxisSpacing: 10,
|
||||
// crossAxisCount: 2,
|
||||
// children: activities,
|
||||
// ))
|
||||
// ]));
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 15, 10, 0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const ActivitiesHeader(),
|
||||
Expanded(
|
||||
child: GridView.count(
|
||||
primary: false,
|
||||
padding: const EdgeInsets.fromLTRB(0, 0, 0, 0),
|
||||
crossAxisSpacing: 10,
|
||||
mainAxisSpacing: 10,
|
||||
crossAxisCount: 2,
|
||||
children: activities,
|
||||
))
|
||||
]));
|
||||
}
|
||||
}
|
113
lib/screens/sessions_screen.dart
Normal file
113
lib/screens/sessions_screen.dart
Normal file
@ -0,0 +1,113 @@
|
||||
import 'package:drift/drift.dart' hide Column;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sendtrain/database.dart';
|
||||
import '../widgets/session_card.dart';
|
||||
|
||||
class SessionsScreen extends StatelessWidget {
|
||||
final AppDatabase database = AppDatabase();
|
||||
SessionsScreen({super.key});
|
||||
|
||||
Future<List<Session>> getSessions() async {
|
||||
// database.managers.sessions.filter((session) => session.status(SessionStatus.pending));
|
||||
return await database.managers.sessions.get();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<List<Session>>(
|
||||
future: getSessions(),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final sessions = snapshot.data!;
|
||||
final pending = sessions.where((session) => session.status == SessionStatus.completed || session.status == SessionStatus.missed);
|
||||
final upcoming = sessions.firstWhere((session) => session.status == SessionStatus.pending);
|
||||
final current = sessions.firstWhere((session) => session.status == SessionStatus.started);
|
||||
|
||||
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);
|
||||
|
||||
database.close();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const Padding(
|
||||
padding: EdgeInsets.fromLTRB(15, 5, 0, 0),
|
||||
child: Text(
|
||||
style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
|
||||
'Current:')),
|
||||
currentSession,
|
||||
const Padding(
|
||||
padding: EdgeInsets.fromLTRB(15, 30, 0, 0),
|
||||
child: Text(
|
||||
style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
|
||||
'Upcoming:')),
|
||||
upcomingSession,
|
||||
const Padding(
|
||||
padding: EdgeInsets.fromLTRB(15, 30, 0, 0),
|
||||
child: Text(
|
||||
style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
|
||||
'Previous:')),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 160,
|
||||
child: GridView.count(
|
||||
padding: const EdgeInsets.fromLTRB(15, 10, 0, 0),
|
||||
scrollDirection: Axis.horizontal,
|
||||
crossAxisSpacing: 5,
|
||||
mainAxisSpacing: 5,
|
||||
crossAxisCount: 1,
|
||||
children: previousSessions))
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return const CircularProgressIndicator();
|
||||
}
|
||||
});
|
||||
|
||||
// List<Widget> previousSessions = List.generate(
|
||||
// 10, (i) => SessionCard(state: 1, type: 1, session: _sessions.first));
|
||||
// Widget upcomingSession = SessionCard(state: 2, session: _sessions.first);
|
||||
// Widget currentSession = SessionCard(session: _sessions.first);
|
||||
|
||||
// return Column(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
// children: <Widget>[
|
||||
// const Padding(
|
||||
// padding: EdgeInsets.fromLTRB(15, 5, 0, 0),
|
||||
// child: Text(
|
||||
// style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
|
||||
// 'Current:')),
|
||||
// currentSession,
|
||||
// const Padding(
|
||||
// padding: EdgeInsets.fromLTRB(15, 30, 0, 0),
|
||||
// child: Text(
|
||||
// style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
|
||||
// 'Upcoming:')),
|
||||
// upcomingSession,
|
||||
// const Padding(
|
||||
// padding: EdgeInsets.fromLTRB(15, 30, 0, 0),
|
||||
// child: Text(
|
||||
// style: TextStyle(fontSize: 25, fontWeight: FontWeight.bold),
|
||||
// 'Previous:')),
|
||||
// SizedBox(
|
||||
// width: double.infinity,
|
||||
// height: 160,
|
||||
// child: GridView.count(
|
||||
// padding: const EdgeInsets.fromLTRB(15, 10, 0, 0),
|
||||
// scrollDirection: Axis.horizontal,
|
||||
// crossAxisSpacing: 5,
|
||||
// mainAxisSpacing: 5,
|
||||
// crossAxisCount: 1,
|
||||
// children: previousSessions))
|
||||
// // Flexible(
|
||||
// // child: ListView(
|
||||
// // scrollDirection: Axis.vertical,
|
||||
// // children: previousSessions,
|
||||
// // )),
|
||||
// ],
|
||||
// );
|
||||
}
|
||||
}
|
@ -1,68 +0,0 @@
|
||||
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();
|
||||
}
|
@ -1,41 +0,0 @@
|
||||
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/widgets/activities/activity_card.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 ActivityCard(activity: activity, callback: callback);
|
||||
return ListTile(
|
||||
title: Text(activity.title),
|
||||
subtitle: Text(activity.description ?? ""),
|
||||
onTap: () {
|
||||
if (callback != null) {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
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();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,63 +0,0 @@
|
||||
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!();
|
||||
}
|
||||
}))
|
||||
])
|
||||
])));
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:sendtrain/daos/activities_dao.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/helpers/date_time_helpers.dart';
|
||||
import 'package:sendtrain/helpers/media_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 Function? callback;
|
||||
|
||||
const ActivityCard({super.key, required this.activity, 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(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,
|
||||
"${widget.activity.title.toTitleCase()} (${formattedTime(atm.totalTime)})");
|
||||
} else {
|
||||
return Text(
|
||||
maxLines: 1,
|
||||
widget.activity.title.toTitleCase());
|
||||
}
|
||||
},
|
||||
),
|
||||
subtitle:
|
||||
Text(maxLines: 2, 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, () {
|
||||
ActivitiesDao(Provider.of<AppDatabase>(
|
||||
context,
|
||||
listen: false))
|
||||
.remove(widget.activity);
|
||||
}).then((result) {
|
||||
setState(() {});
|
||||
});
|
||||
},
|
||||
)
|
||||
])),
|
||||
],
|
||||
)),
|
||||
);
|
||||
} else {
|
||||
return GenericProgressIndicator();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,201 +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/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/activities/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.upload_outlined),
|
||||
label: Text('Upload Media'),
|
||||
onPressed: () {},
|
||||
),
|
||||
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())),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(10, 0, 0, 10),
|
||||
child: Flex(direction: Axis.horizontal, children: [
|
||||
ActivityViewCategories(
|
||||
categories: activity.category != null ? [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(),
|
||||
));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -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: () {},
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
@ -1,51 +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/media_card.dart';
|
||||
|
||||
class ActivityViewMedia extends StatelessWidget {
|
||||
const ActivityViewMedia({super.key, required this.activity});
|
||||
|
||||
final Activity activity;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<List<MediaItem>>(
|
||||
future: MediaItemsDao(Provider.of<AppDatabase>(context)).fromActivity(activity),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
List<MediaItem> mediaItems = snapshot.data!;
|
||||
List<MediaCard> mediaCards = [];
|
||||
|
||||
for (int i = 0; i < mediaItems.length; i++) {
|
||||
mediaCards.add(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 Container(
|
||||
alignment: Alignment.center,
|
||||
child: SizedBox(
|
||||
height: 50.0,
|
||||
width: 50.0,
|
||||
child: CircularProgressIndicator(),
|
||||
));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sendtrain/database/database.dart';
|
||||
import 'package:sendtrain/extensions/string_extensions.dart';
|
||||
|
||||
class ActivityViewTypes extends StatelessWidget {
|
||||
const ActivityViewTypes({super.key, required this.types});
|
||||
|
||||
final List<ActivityType> types;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 40,
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.only(right: 10),
|
||||
itemCount: types.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
return ActionChip(
|
||||
visualDensity: VisualDensity.compact,
|
||||
avatar: const Icon(Icons.fitness_center_rounded),
|
||||
label: Text(maxLines: 1, types[index].name.toTitleCase()),
|
||||
tooltip: "Activity Type",
|
||||
onPressed: () {},
|
||||
);
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'activity_type_filter.dart';
|
||||
import '../widgets/activity_type_filter.dart';
|
||||
|
||||
class ActivitiesHeader extends StatefulWidget {
|
||||
const ActivitiesHeader({super.key});
|
@ -1,14 +1,12 @@
|
||||
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/classes/activity_action.dart';
|
||||
import 'package:sendtrain/models/activity_timer_model.dart';
|
||||
|
||||
class ActivityActionView extends StatefulWidget {
|
||||
const ActivityActionView({super.key, required this.actions});
|
||||
final List actions;
|
||||
const ActivityActionView({super.key, required this.action});
|
||||
final ActivityAction action;
|
||||
|
||||
@override
|
||||
State<ActivityActionView> createState() => ActivityActionViewState();
|
||||
@ -27,7 +25,7 @@ class ActivityActionViewState extends State<ActivityActionView> {
|
||||
Widget build(BuildContext context) {
|
||||
ActivityTimerModel atm =
|
||||
Provider.of<ActivityTimerModel>(context, listen: true);
|
||||
List sets = json.decode(widget.actions[0].set);
|
||||
List<List<Map<String, dynamic>>> sets = atm.activity!.actions[0].items();
|
||||
|
||||
// we need to set the scroll controller
|
||||
// so we can update the selected item position
|
||||
@ -36,14 +34,14 @@ class ActivityActionViewState extends State<ActivityActionView> {
|
||||
return Expanded(
|
||||
child: ScrollablePositionedList.builder(
|
||||
padding: const EdgeInsets.fromLTRB(10, 0, 10, 20),
|
||||
itemCount: sets.length,
|
||||
itemCount: widget.action.activityActionSet.total,
|
||||
itemScrollController: itemScrollController,
|
||||
scrollOffsetController: scrollOffsetController,
|
||||
itemPositionsListener: itemPositionsListener,
|
||||
scrollOffsetListener: scrollOffsetListener,
|
||||
itemBuilder: (BuildContext context, int setNum) {
|
||||
List<GestureDetector> content = [];
|
||||
List set = sets[setNum];
|
||||
List<Map<String, dynamic>> set = sets[setNum];
|
||||
|
||||
for (int actionNum = 0; actionNum < set.length; actionNum++) {
|
||||
Map<String, dynamic> setItem = set[actionNum];
|
||||
@ -76,7 +74,7 @@ class ActivityActionViewState extends State<ActivityActionView> {
|
||||
: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
child: Text(
|
||||
textAlign: TextAlign.center,
|
||||
'${setItem['name']}: ${setItem['amount']} ${setItem['type']}'.toTitleCase())))
|
||||
'${setItem['name']}: ${setItem['amount']} ${setItem['type']}')))
|
||||
])));
|
||||
}
|
||||
|
127
lib/widgets/activity_card.dart
Normal file
127
lib/widgets/activity_card.dart
Normal file
@ -0,0 +1,127 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:sendtrain/classes/media.dart';
|
||||
import 'package:sendtrain/models/activity_model.dart';
|
||||
import 'package:sendtrain/models/activity_timer_model.dart';
|
||||
import 'package:sendtrain/widgets/activity_view.dart';
|
||||
|
||||
class ActivityCard extends StatefulWidget {
|
||||
final ActivityModel 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 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(
|
||||
leading: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(0, 0, 0, 0),
|
||||
child: Container(
|
||||
width: 60,
|
||||
decoration: BoxDecoration(
|
||||
image: DecorationImage(
|
||||
fit: BoxFit.cover,
|
||||
image: findMediaByType(
|
||||
widget.activity.actions[0].media, 'image')),
|
||||
// color: Colors.blue,
|
||||
borderRadius:
|
||||
const BorderRadius.all(Radius.elliptical(10, 10)),
|
||||
),
|
||||
)),
|
||||
title: Consumer<ActivityTimerModel>(
|
||||
builder: (context, atm, child) {
|
||||
if (atm.activity?.id == widget.activity.id) {
|
||||
return Text(
|
||||
maxLines: 1,
|
||||
"${widget.activity.title} (${formattedTime(atm.totalTime)})");
|
||||
} else {
|
||||
return Text(maxLines: 1, widget.activity.title);
|
||||
}
|
||||
},
|
||||
),
|
||||
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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
)),
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
|
||||
ImageProvider findMediaByType(List<Media>? media, String type) {
|
||||
var found = media?.where((m) => m.type == 'image');
|
||||
|
||||
if (found != null) {
|
||||
return NetworkImage(found.first.reference);
|
||||
} else {
|
||||
// Element is not found
|
||||
return const AssetImage('assets/images/placeholder.jpg');
|
||||
}
|
||||
}
|
||||
}
|
214
lib/widgets/activity_view.dart
Normal file
214
lib/widgets/activity_view.dart
Normal file
@ -0,0 +1,214 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_expandable_fab/flutter_expandable_fab.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:sendtrain/classes/activity_action.dart';
|
||||
import 'package:sendtrain/classes/media.dart';
|
||||
import 'package:sendtrain/models/activity_model.dart';
|
||||
import 'package:sendtrain/models/activity_timer_model.dart';
|
||||
import 'package:sendtrain/widgets/activity_action_view.dart';
|
||||
import 'package:sendtrain/widgets/media_card.dart';
|
||||
|
||||
class ActivityView extends StatefulWidget {
|
||||
const ActivityView({super.key, required this.activity});
|
||||
final ActivityModel activity;
|
||||
|
||||
@override
|
||||
State<ActivityView> createState() => _ActivityViewState();
|
||||
}
|
||||
|
||||
class _ActivityViewState extends State<ActivityView> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ActivityModel activity = widget.activity;
|
||||
ActivityTimerModel atm =
|
||||
Provider.of<ActivityTimerModel>(context, listen: false);
|
||||
|
||||
atm.setup(activity);
|
||||
|
||||
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(
|
||||
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)),
|
||||
ActivityViewCategories(categories: activity.categories),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 0, bottom: 20, left: 15, right: 15),
|
||||
child: Text(
|
||||
textAlign: TextAlign.left,
|
||||
style: const TextStyle(fontSize: 15),
|
||||
activity.description)),
|
||||
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']}');
|
||||
},
|
||||
),
|
||||
),
|
||||
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(action: activity.actions[0]),
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
class ActivityViewCategories extends StatelessWidget {
|
||||
const ActivityViewCategories({super.key, this.categories});
|
||||
|
||||
final List<String>? categories;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
scrollDirection: Axis.horizontal,
|
||||
padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
|
||||
itemCount: categories?.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, '${categories?[index]}'),
|
||||
onPressed: () {},
|
||||
));
|
||||
},
|
||||
))),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ActivityViewMedia extends StatelessWidget {
|
||||
const ActivityViewMedia({super.key, this.activity});
|
||||
|
||||
final ActivityModel? activity;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Media> media = [];
|
||||
|
||||
for (ActivityAction action in activity!.actions) {
|
||||
if (action.media!.isNotEmpty) {
|
||||
media.addAll(action.media as Iterable<Media>);
|
||||
}
|
||||
}
|
||||
|
||||
List<Widget> mediaCards =
|
||||
List.generate(media.length, (i) => MediaCard(media: media[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))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Future showGenericDialog(dynamic object, BuildContext parentContext) {
|
||||
return 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: 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);
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
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),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
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)),
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
@ -1,115 +0,0 @@
|
||||
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.
|
||||
// final Iterable<String> options = await _FakeAPI.search(_currentQuery!);
|
||||
if (query.isNotEmpty) {
|
||||
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(
|
||||
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;
|
||||
});
|
||||
}
|
||||
}
|
@ -1,56 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
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});
|
||||
|
||||
final TextEditingController controller;
|
||||
final String title;
|
||||
final int? maxLines;
|
||||
final int? minLines;
|
||||
final Icon? icon;
|
||||
final dynamic onTap;
|
||||
final bool requiresValidation;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: 10, bottom: 10),
|
||||
child: TextFormField(
|
||||
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,
|
||||
),
|
||||
validator: (String? value) {
|
||||
if (requiresValidation == true) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return 'Please enter some text';
|
||||
}
|
||||
|
||||
if (value.length < 3) {
|
||||
return 'Please enter a minimum of 3 characters';
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
onTap: () {
|
||||
if (onTap != null) {
|
||||
onTap();
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
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(),
|
||||
));
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:typed_data';
|
||||
|
||||
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';
|
||||
import 'package:video_player/video_player.dart';
|
||||
|
||||
class MediaCard extends StatelessWidget {
|
||||
const MediaCard({super.key, required this.media, this.callback});
|
||||
|
||||
final MediaItem media;
|
||||
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: () => 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(''),
|
||||
))));
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
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'));
|
||||
}
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
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,
|
||||
)
|
||||
]));
|
||||
}
|
||||
}
|
93
lib/widgets/media_card.dart
Normal file
93
lib/widgets/media_card.dart
Normal file
@ -0,0 +1,93 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:sendtrain/classes/media.dart';
|
||||
import 'package:youtube_player_flutter/youtube_player_flutter.dart';
|
||||
|
||||
class MediaCard extends StatelessWidget {
|
||||
const MediaCard({super.key, required this.media});
|
||||
|
||||
final Media media;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
YoutubePlayerController controller = YoutubePlayerController(
|
||||
initialVideoId: media.reference,
|
||||
flags: const YoutubePlayerFlags(
|
||||
autoPlay: false, mute: true, showLiveFullscreenButton: false));
|
||||
|
||||
DecorationImage mediaImage(Media media) {
|
||||
String image = '';
|
||||
|
||||
if (media.type == "image") {
|
||||
image = media.reference;
|
||||
} else if (media.type == "youtube") {
|
||||
image = 'https://img.youtube.com/vi/${media.reference}/0.jpg';
|
||||
}
|
||||
|
||||
return DecorationImage(image: NetworkImage(image), fit: BoxFit.cover);
|
||||
}
|
||||
|
||||
Widget mediaItem(Media media) {
|
||||
if (media.type == "image") {
|
||||
return Image(image: NetworkImage(media.reference));
|
||||
} else if (media.type == "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(''),
|
||||
))));
|
||||
}
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
// 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 '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 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) {
|
||||
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.firstWhereOrNull(
|
||||
(session) => session.status == SessionStatus.pending);
|
||||
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 = getSessionCard(upcoming);
|
||||
Widget currentSession = getSessionCard(current);
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
const Padding(
|
||||
padding: EdgeInsets.fromLTRB(15, 5, 0, 0),
|
||||
child: Text(
|
||||
style: TextStyle(
|
||||
fontSize: 25, fontWeight: FontWeight.bold),
|
||||
'Current:')),
|
||||
currentSession,
|
||||
const Padding(
|
||||
padding: EdgeInsets.fromLTRB(15, 30, 0, 0),
|
||||
child: Text(
|
||||
style: TextStyle(
|
||||
fontSize: 25, fontWeight: FontWeight.bold),
|
||||
'Upcoming:')),
|
||||
upcomingSession,
|
||||
const Padding(
|
||||
padding: EdgeInsets.fromLTRB(15, 30, 0, 0),
|
||||
child: Text(
|
||||
style: TextStyle(
|
||||
fontSize: 25, fontWeight: FontWeight.bold),
|
||||
'Previous:')),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 160,
|
||||
child: GridView.count(
|
||||
padding: const EdgeInsets.fromLTRB(15, 10, 0, 0),
|
||||
scrollDirection: Axis.horizontal,
|
||||
crossAxisSpacing: 5,
|
||||
mainAxisSpacing: 5,
|
||||
crossAxisCount: 1,
|
||||
children: previousSessions))
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return GenericProgressIndicator();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
316
lib/widgets/session_card.dart
Normal file
316
lib/widgets/session_card.dart
Normal file
@ -0,0 +1,316 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:intl/date_symbol_data_local.dart';
|
||||
import 'package:sendtrain/classes/activity_action.dart';
|
||||
import 'package:sendtrain/classes/media.dart';
|
||||
import 'package:sendtrain/database.dart' hide ActivityAction;
|
||||
import 'package:sendtrain/models/activity_model.dart';
|
||||
import 'package:sendtrain/models/session_model.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;
|
||||
|
||||
// place holder until we can retrieve real data
|
||||
final data = SessionModel(
|
||||
id: 1,
|
||||
title: "Projecting @ Climbers Rock",
|
||||
content:
|
||||
"Beta pully beta beta pinch one arm crimpy. Futuristic pinch, dyno dynamic drop knee climb. Climbing ondra slopey onsight beta ondra power endurance.",
|
||||
date: DateTime.now(),
|
||||
activities: [
|
||||
ActivityModel(
|
||||
id: 1,
|
||||
title: 'Campus Board',
|
||||
type: 'fundamental',
|
||||
categories: ['strength', 'power'],
|
||||
description:
|
||||
"Campus board session, focusing on explosiveness and contact strength.",
|
||||
actions: [
|
||||
ActivityAction(
|
||||
id: 1,
|
||||
title: '1, 3, 5',
|
||||
description:
|
||||
'Move between the first, third, and fifth rungs, alternating hands. Rest and alternate sides, to start',
|
||||
media: [
|
||||
Media(
|
||||
id: 1,
|
||||
reference:
|
||||
'https://www.climbing.com/wp-content/uploads/2022/06/campus-board-e1655470701154.jpeg',
|
||||
type: 'image',
|
||||
description: 'Campus board movement'),
|
||||
Media(
|
||||
id: 1,
|
||||
reference: '7ACyeOP-Hxo',
|
||||
type: 'youtube',
|
||||
description: 'How to campus board')
|
||||
],
|
||||
activityActionSet: Set(
|
||||
type: 'drop_set',
|
||||
total: 3,
|
||||
rest: 300000,
|
||||
reps: Reps(
|
||||
type: 'repititions',
|
||||
tempo: [0],
|
||||
amounts: [1, 1, 1],
|
||||
weights: [0],
|
||||
rest: 20000))),
|
||||
],
|
||||
resources: ['https://www.youtube.com/watch?v=bLz0xp1PEm4']),
|
||||
ActivityModel(
|
||||
id: 2,
|
||||
title: 'Projecting',
|
||||
type: 'fundamental',
|
||||
categories: ['technique', 'conditioning'],
|
||||
description:
|
||||
"Session focused on attempting a climb at or beyond your perceived limit.",
|
||||
actions: [
|
||||
ActivityAction(
|
||||
id: 1,
|
||||
title: 'Attempt Climb',
|
||||
description:
|
||||
'Attempt your selected climb, if you fall off early in the climb, attempt again. 1 repitition equals roughly doing all the moves, not necessarily in 1 attempt.',
|
||||
media: [
|
||||
Media(
|
||||
id: 1,
|
||||
reference:
|
||||
'https://www.climbing.com/wp-content/uploads/2022/07/Fixed-44.jpg',
|
||||
type: 'image',
|
||||
description: 'Projecting a climb'),
|
||||
Media(
|
||||
id: 1,
|
||||
reference: 'BgheYcxhrsw',
|
||||
type: 'youtube',
|
||||
description: 'How to project climbs')
|
||||
],
|
||||
activityActionSet: Set(
|
||||
type: 'standard',
|
||||
total: 10,
|
||||
rest: 300000,
|
||||
reps: Reps(
|
||||
type: 'repititions',
|
||||
tempo: [0],
|
||||
amounts: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
weights: [0],
|
||||
rest: 0))),
|
||||
],
|
||||
resources: ['https://www.youtube.com/watch?v=dyAvbUvY_PU']),
|
||||
ActivityModel(
|
||||
id: 3,
|
||||
title: 'Long Block Pulls',
|
||||
type: 'Hypertrophy',
|
||||
categories: ['Strength', 'Power'],
|
||||
description:
|
||||
"Block pull on a edge of a specific size and time to induce a hypertrophic effect on the formarms.",
|
||||
actions: [
|
||||
ActivityAction(
|
||||
id: 1,
|
||||
title: 'Long Pulls',
|
||||
description:
|
||||
'Select your desired weight to pull, add it to the block. You should aim for an effort level of 8-9 when reaching the end of the set time, going to failure will result in significantly extended recovery time.',
|
||||
media: [
|
||||
Media(
|
||||
id: 1,
|
||||
reference:
|
||||
'https://trailandcrag.com/sites/default/files/inline-images/05-min_3.jpg',
|
||||
type: 'image',
|
||||
description: 'Block pull example'),
|
||||
Media(
|
||||
id: 1,
|
||||
reference: 'sZVAEy9UmoY',
|
||||
type: 'youtube',
|
||||
description:
|
||||
'Principals of Grip gains, and related protocols')
|
||||
],
|
||||
activityActionSet: Set(
|
||||
type: 'alternating',
|
||||
total: 5,
|
||||
rest: 5000,
|
||||
reps: Reps(
|
||||
type: 'seconds',
|
||||
tempo: [0],
|
||||
amounts: [5, 5, 5, 5, 5],
|
||||
weights: [80, 80, 80, 80, 80],
|
||||
rest: 5000))),
|
||||
],
|
||||
resources: ['https://www.youtube.com/watch?v=dyAvbUvY_PU']),
|
||||
],
|
||||
achievements: [
|
||||
'got 1 3 5 first time!',
|
||||
'no pain in elbow',
|
||||
'life is pain',
|
||||
'new PR for pullups'
|
||||
],
|
||||
media: [
|
||||
Media(
|
||||
id: 1,
|
||||
reference: 'TwS8ycTY5cc',
|
||||
type: 'youtube',
|
||||
description: 'Attempting crux move'),
|
||||
Media(
|
||||
id: 1,
|
||||
reference:
|
||||
'https://static.wixstatic.com/media/c83481_1dd473ad49524ae5a95d993ba10e0a50~mv2.jpg/v1/fill/w_640,h_426,al_c,q_80,usm_0.66_1.00_0.01,enc_auto/c83481_1dd473ad49524ae5a95d993ba10e0a50~mv2.jpg',
|
||||
type: 'image',
|
||||
description: 'Struggling on deadpoints'),
|
||||
Media(
|
||||
id: 1,
|
||||
reference: 'TwS8ycTY5cc',
|
||||
type: 'youtube',
|
||||
description: 'Attempting crux move')
|
||||
]);
|
||||
|
||||
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(data: data, 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),
|
||||
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(data: data, 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,
|
||||
textAlign: TextAlign.center),
|
||||
subtitle: Text(
|
||||
maxLines: 1,
|
||||
dateFormat.format(session.date as DateTime),
|
||||
textAlign: TextAlign.center),
|
||||
),
|
||||
])))));
|
||||
}
|
||||
}
|
||||
}
|
165
lib/widgets/session_view.dart
Normal file
165
lib/widgets/session_view.dart
Normal file
@ -0,0 +1,165 @@
|
||||
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:sendtrain/classes/media.dart';
|
||||
import 'package:sendtrain/database.dart';
|
||||
import 'package:sendtrain/models/activity_model.dart';
|
||||
import 'package:sendtrain/models/session_model.dart';
|
||||
import 'package:sendtrain/widgets/activity_card.dart';
|
||||
import 'package:sendtrain/widgets/media_card.dart';
|
||||
|
||||
class SessionView extends StatelessWidget {
|
||||
const SessionView({super.key, required this.data, required this.session});
|
||||
|
||||
final SessionModel data;
|
||||
final Session session;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
initializeDateFormatting('en');
|
||||
final DateFormat dateFormat = DateFormat('yyyy-MM-dd');
|
||||
|
||||
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)),
|
||||
SessionViewAchievements(achievements: data.achievements),
|
||||
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(media: data.media),
|
||||
const Padding(
|
||||
padding: EdgeInsets.fromLTRB(15, 30, 0, 10),
|
||||
child: Text(
|
||||
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
|
||||
'Activites:')),
|
||||
SessionViewActivities(activities: data.activities),
|
||||
],
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class SessionViewActivities extends StatelessWidget {
|
||||
const SessionViewActivities({super.key, this.activities});
|
||||
|
||||
final List<ActivityModel>? 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]);
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
class SessionViewAchievements extends StatelessWidget {
|
||||
const SessionViewAchievements({super.key, this.achievements});
|
||||
|
||||
final List<String>? achievements;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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]}'),
|
||||
onPressed: () {},
|
||||
));
|
||||
},
|
||||
))),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SessionViewMedia extends StatelessWidget {
|
||||
const SessionViewMedia({super.key, this.media});
|
||||
|
||||
final List<Media>? media;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
List<Widget> mediaCards = List.generate((media != null) ? media!.length : 0,
|
||||
(i) => MediaCard(media: media![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))
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,69 +0,0 @@
|
||||
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!();
|
||||
}
|
||||
}))
|
||||
])
|
||||
])));
|
||||
}
|
||||
}
|
@ -1,44 +0,0 @@
|
||||
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();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,100 +0,0 @@
|
||||
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)
|
||||
],
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
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),
|
||||
),
|
||||
])))));
|
||||
}
|
||||
}
|
@ -1,318 +0,0 @@
|
||||
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')))
|
||||
]),
|
||||
],
|
||||
)));
|
||||
}
|
||||
}
|
@ -1,195 +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/sessions_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/widgets/achievements/achievement_editor.dart';
|
||||
import 'package:sendtrain/widgets/generic/elements/generic_progress_indicator.dart';
|
||||
import 'package:sendtrain/widgets/sessions/session_activities_editor.dart';
|
||||
import 'package:sendtrain/widgets/sessions/session_editor.dart';
|
||||
import 'package:sendtrain/widgets/sessions/session_view_achievements.dart';
|
||||
import 'package:sendtrain/widgets/sessions/session_view_activities.dart';
|
||||
import 'package:sendtrain/widgets/sessions/session_view_media.dart';
|
||||
|
||||
class SessionView extends StatefulWidget {
|
||||
const SessionView({super.key, required this.session});
|
||||
|
||||
final Session session;
|
||||
|
||||
@override
|
||||
State<SessionView> createState() => _SessionViewState();
|
||||
}
|
||||
|
||||
class _SessionViewState extends State<SessionView> {
|
||||
final _fabKey = GlobalKey<ExpandableFabState>();
|
||||
late Session session;
|
||||
late DateFormat dateFormat;
|
||||
|
||||
String title() {
|
||||
String title = session.title.toTitleCase();
|
||||
|
||||
if (session.address != null) {
|
||||
title = "$title @ ${session.address}";
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
void resetState() async {
|
||||
Session updatedSession =
|
||||
await SessionsDao(Provider.of<AppDatabase>(context, listen: false))
|
||||
.find(session.id);
|
||||
|
||||
final state = _fabKey.currentState;
|
||||
if (state != null && state.isOpen) {
|
||||
state.toggle();
|
||||
}
|
||||
|
||||
setState(() {
|
||||
session = updatedSession;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
initState() {
|
||||
super.initState();
|
||||
initializeDateFormatting('en');
|
||||
dateFormat = DateFormat('yyyy-MM-dd');
|
||||
session = widget.session;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder<Session>(
|
||||
initialData: session,
|
||||
stream: SessionsDao(Provider.of<AppDatabase>(context))
|
||||
.watchSession(session.id),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
return Scaffold(
|
||||
floatingActionButtonLocation: ExpandableFab.location,
|
||||
floatingActionButton: ExpandableFab(
|
||||
key: _fabKey,
|
||||
distance: 70,
|
||||
type: ExpandableFabType.up,
|
||||
overlayStyle: ExpandableFabOverlayStyle(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
blur: 10,
|
||||
),
|
||||
children: [
|
||||
FloatingActionButton.extended(
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
label: Text('Add Achievement'),
|
||||
onPressed: () {
|
||||
showEditorSheet(
|
||||
context,
|
||||
AchievementEditor(
|
||||
session: session, callback: resetState));
|
||||
},
|
||||
),
|
||||
FloatingActionButton.extended(
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
label: Text('Add Activity'),
|
||||
onPressed: () {
|
||||
showEditorSheet(
|
||||
context,
|
||||
SessionActivitiesEditor(
|
||||
session: session, callback: resetState));
|
||||
},
|
||||
),
|
||||
FloatingActionButton.extended(
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
label: Text('Edit'),
|
||||
onPressed: () {
|
||||
showEditorSheet(
|
||||
context,
|
||||
SessionEditor(
|
||||
session: session, callback: resetState));
|
||||
},
|
||||
),
|
||||
FloatingActionButton.extended(
|
||||
icon: const Icon(Icons.history_outlined),
|
||||
label: Text('Restart'),
|
||||
onPressed: () {
|
||||
Session newSession =
|
||||
session.copyWith(status: SessionStatus.pending);
|
||||
|
||||
SessionsDao(Provider.of<AppDatabase>(context,
|
||||
listen: false))
|
||||
.replace(newSession);
|
||||
|
||||
final state = _fabKey.currentState;
|
||||
if (state != null) {
|
||||
state.toggle();
|
||||
}
|
||||
},
|
||||
),
|
||||
FloatingActionButton.extended(
|
||||
icon: const Icon(Icons.done_all_outlined),
|
||||
label: Text('Done'),
|
||||
onPressed: () {
|
||||
Session newSession =
|
||||
session.copyWith(status: SessionStatus.completed);
|
||||
|
||||
SessionsDao(Provider.of<AppDatabase>(context,
|
||||
listen: false))
|
||||
.replace(newSession);
|
||||
|
||||
final state = _fabKey.currentState;
|
||||
if (state != null) {
|
||||
state.toggle();
|
||||
}
|
||||
},
|
||||
),
|
||||
]),
|
||||
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),
|
||||
title())),
|
||||
SessionViewAchievements(
|
||||
session: session, callback: resetState),
|
||||
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(session: session),
|
||||
],
|
||||
));
|
||||
} else {
|
||||
return GenericProgressIndicator();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
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/extensions/string_extensions.dart';
|
||||
import 'package:sendtrain/helpers/widget_helpers.dart';
|
||||
import 'package:sendtrain/widgets/achievements/achievement_editor.dart';
|
||||
import 'package:sendtrain/widgets/builders/dialogs.dart';
|
||||
|
||||
class SessionViewAchievements extends StatelessWidget {
|
||||
const SessionViewAchievements(
|
||||
{super.key, required this.session, this.callback});
|
||||
|
||||
final Session session;
|
||||
final Function? callback;
|
||||
|
||||
Session updateAchievements(int index, List achievements) {
|
||||
achievements.removeAt(index);
|
||||
return session.copyWith(
|
||||
achievements: Value<String>(json.encode(achievements)));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget content;
|
||||
final AppDatabase db = Provider.of<AppDatabase>(context, listen: false);
|
||||
List achievements = json.decode(session.achievements ?? "[]");
|
||||
|
||||
if (achievements.isEmpty) {
|
||||
content = Padding(
|
||||
padding: const EdgeInsets.only(left: 10, right: 5),
|
||||
child: ActionChip(
|
||||
visualDensity: VisualDensity.compact,
|
||||
avatar: const Icon(Icons.check_circle_outline),
|
||||
label: Text(maxLines: 1, 'Add Achievements!'),
|
||||
onPressed: () {
|
||||
showEditorSheet(context,
|
||||
AchievementEditor(session: session, callback: callback));
|
||||
},
|
||||
));
|
||||
} else {
|
||||
content = 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].toString().toTitleCase()),
|
||||
onPressed: () async {
|
||||
// remove the achievement at index
|
||||
// then update session
|
||||
Session newSession =
|
||||
updateAchievements(index, achievements);
|
||||
await showUpdateDialog(
|
||||
'Achievement Removal',
|
||||
'Would you like to remove this achievement?',
|
||||
context, () {
|
||||
SessionsDao(db).replace(newSession);
|
||||
if (callback != null) {
|
||||
callback!();
|
||||
}
|
||||
});
|
||||
}));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: SizedBox(height: 40, child: content)),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
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/activities/activity_card.dart';
|
||||
import 'package:sendtrain/widgets/generic/elements/generic_progress_indicator.dart';
|
||||
import 'package:sendtrain/widgets/sessions/session_activities_editor.dart';
|
||||
|
||||
class SessionViewActivities extends StatefulWidget {
|
||||
const SessionViewActivities({super.key, required this.session});
|
||||
|
||||
final Session session;
|
||||
|
||||
@override
|
||||
State<SessionViewActivities> createState() => _SessionViewActivitiesState();
|
||||
}
|
||||
|
||||
class _SessionViewActivitiesState extends State<SessionViewActivities> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return StreamBuilder<List<Activity>>(
|
||||
stream: ActivitiesDao(Provider.of<AppDatabase>(context))
|
||||
.watchSessionActivities(widget.session.id),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final activities = snapshot.data!;
|
||||
if (activities.isNotEmpty) {
|
||||
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]);
|
||||
},
|
||||
));
|
||||
} else {
|
||||
return Expanded(
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
|
||||
children: [
|
||||
Card.outlined(
|
||||
child: InkWell(
|
||||
customBorder: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
onTap: () {
|
||||
showEditorSheet(
|
||||
context,
|
||||
SessionActivitiesEditor(
|
||||
session: widget.session,
|
||||
callback: () {}));
|
||||
},
|
||||
child: ListTile(
|
||||
contentPadding: EdgeInsets.only(
|
||||
top: 5, left: 15, right: 5, bottom: 5),
|
||||
autofocus: true,
|
||||
leading: Icon(Icons.add_box_rounded),
|
||||
title: Text('Add an Activity!'),
|
||||
subtitle: Text(
|
||||
'Here you can associate one or more activities that you can follow along with during your session.'),
|
||||
)))
|
||||
]));
|
||||
}
|
||||
} else {
|
||||
return GenericProgressIndicator();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -1,72 +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/helpers/widget_helpers.dart';
|
||||
import 'package:sendtrain/widgets/media/media_card.dart';
|
||||
import 'package:sendtrain/widgets/sessions/session_editor.dart';
|
||||
|
||||
class SessionViewMedia extends StatefulWidget {
|
||||
const SessionViewMedia({super.key, required this.session});
|
||||
|
||||
final Session session;
|
||||
|
||||
@override
|
||||
State<SessionViewMedia> createState() => _SessionViewMediaState();
|
||||
}
|
||||
|
||||
class _SessionViewMediaState extends State<SessionViewMedia> {
|
||||
void resetState() {
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<List<MediaItem>>(
|
||||
future: MediaItemsDao(Provider.of<AppDatabase>(context))
|
||||
.fromSession(widget.session.id),
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.hasData) {
|
||||
final mediaItems = snapshot.data!;
|
||||
|
||||
List<Widget> content;
|
||||
if (mediaItems.isNotEmpty) {
|
||||
List<Widget> mediaCards = List.generate(mediaItems.length,
|
||||
(i) => MediaCard(media: mediaItems[i], callback: resetState));
|
||||
content = mediaCards;
|
||||
} else {
|
||||
content = [
|
||||
FloatingActionButton(
|
||||
onPressed: () {
|
||||
showEditorSheet(
|
||||
context,
|
||||
SessionEditor(
|
||||
session: widget.session, callback: resetState));
|
||||
},
|
||||
mini: true,
|
||||
child: Icon(Icons.add_a_photo_rounded))
|
||||
];
|
||||
}
|
||||
|
||||
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: content))
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return Padding(
|
||||
padding: EdgeInsets.all(15),
|
||||
child: CircularProgressIndicator());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
10
pubspec.yaml
10
pubspec.yaml
@ -44,13 +44,7 @@ dependencies:
|
||||
drift: ^2.22.1
|
||||
flutter_expandable_fab: ^2.3.0
|
||||
drift_flutter: ^0.2.2
|
||||
map_location_picker: ^1.2.7
|
||||
file_picker: ^8.1.7
|
||||
http: ^1.2.2
|
||||
uuid: ^4.5.1
|
||||
mime: ^2.0.0
|
||||
video_player: ^2.9.2
|
||||
dart_casing: ^3.0.1
|
||||
flutter_local_notifications: ^18.0.1
|
||||
|
||||
flutter_launcher_name:
|
||||
name: "SendTrain"
|
||||
@ -68,7 +62,6 @@ dev_dependencies:
|
||||
build_runner: ^2.4.13
|
||||
json_serializable: ^6.9.0
|
||||
drift_dev: ^2.22.1
|
||||
test: ^1.25.7
|
||||
|
||||
# For information on the generic Dart part of this file, see the
|
||||
# following page: https://dart.dev/tools/pub/pubspec
|
||||
@ -87,7 +80,6 @@ flutter:
|
||||
# - images/a_dot_ham.jpeg
|
||||
assets:
|
||||
- assets/images/
|
||||
- assets/exercises.json
|
||||
|
||||
# An image asset can refer to one or more resolution-specific "variants", see
|
||||
# https://flutter.dev/assets-and-images/#resolution-aware
|
||||
|
@ -1,94 +0,0 @@
|
||||
// dart format width=80
|
||||
// GENERATED CODE, DO NOT EDIT BY HAND.
|
||||
// ignore_for_file: type=lint
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/internal/migrations.dart';
|
||||
import 'schema_v1.dart' as v1;
|
||||
import 'schema_v2.dart' as v2;
|
||||
import 'schema_v3.dart' as v3;
|
||||
import 'schema_v4.dart' as v4;
|
||||
import 'schema_v5.dart' as v5;
|
||||
import 'schema_v6.dart' as v6;
|
||||
import 'schema_v7.dart' as v7;
|
||||
import 'schema_v8.dart' as v8;
|
||||
import 'schema_v9.dart' as v9;
|
||||
import 'schema_v10.dart' as v10;
|
||||
import 'schema_v11.dart' as v11;
|
||||
import 'schema_v12.dart' as v12;
|
||||
import 'schema_v13.dart' as v13;
|
||||
import 'schema_v14.dart' as v14;
|
||||
import 'schema_v15.dart' as v15;
|
||||
import 'schema_v16.dart' as v16;
|
||||
import 'schema_v17.dart' as v17;
|
||||
import 'schema_v18.dart' as v18;
|
||||
import 'schema_v19.dart' as v19;
|
||||
|
||||
class GeneratedHelper implements SchemaInstantiationHelper {
|
||||
@override
|
||||
GeneratedDatabase databaseForVersion(QueryExecutor db, int version) {
|
||||
switch (version) {
|
||||
case 1:
|
||||
return v1.DatabaseAtV1(db);
|
||||
case 2:
|
||||
return v2.DatabaseAtV2(db);
|
||||
case 3:
|
||||
return v3.DatabaseAtV3(db);
|
||||
case 4:
|
||||
return v4.DatabaseAtV4(db);
|
||||
case 5:
|
||||
return v5.DatabaseAtV5(db);
|
||||
case 6:
|
||||
return v6.DatabaseAtV6(db);
|
||||
case 7:
|
||||
return v7.DatabaseAtV7(db);
|
||||
case 8:
|
||||
return v8.DatabaseAtV8(db);
|
||||
case 9:
|
||||
return v9.DatabaseAtV9(db);
|
||||
case 10:
|
||||
return v10.DatabaseAtV10(db);
|
||||
case 11:
|
||||
return v11.DatabaseAtV11(db);
|
||||
case 12:
|
||||
return v12.DatabaseAtV12(db);
|
||||
case 13:
|
||||
return v13.DatabaseAtV13(db);
|
||||
case 14:
|
||||
return v14.DatabaseAtV14(db);
|
||||
case 15:
|
||||
return v15.DatabaseAtV15(db);
|
||||
case 16:
|
||||
return v16.DatabaseAtV16(db);
|
||||
case 17:
|
||||
return v17.DatabaseAtV17(db);
|
||||
case 18:
|
||||
return v18.DatabaseAtV18(db);
|
||||
case 19:
|
||||
return v19.DatabaseAtV19(db);
|
||||
default:
|
||||
throw MissingSchemaException(version, versions);
|
||||
}
|
||||
}
|
||||
|
||||
static const versions = const [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19
|
||||
];
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user