db changes, seed changes, media view work for local images, and video prep, initial achievement work

This commit is contained in:
Joshua Burman 2025-01-02 13:29:13 -05:00
parent e78788d67a
commit 48f716cdb0
15 changed files with 2286 additions and 37 deletions

View File

@ -1,6 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <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.network" android:required="false" />
<uses-feature android:name="android.hardware.location.gps" android:required="false" /> <uses-feature android:name="android.hardware.location.gps" android:required="false" />
<uses-permission android:name="android.permission.INTERNET"/>
<application <application
android:label="sendtrain" android:label="sendtrain"
android:name="${applicationName}" android:name="${applicationName}"

View File

@ -35,7 +35,7 @@ class AppDatabase extends _$AppDatabase {
AppDatabase() : super(_openConnection()); AppDatabase() : super(_openConnection());
@override @override
int get schemaVersion => 9; int get schemaVersion => 10;
@override @override
MigrationStrategy get migration { MigrationStrategy get migration {
@ -141,7 +141,7 @@ class ObjectMediaItems extends Table {
dateTime().withDefault(Variable(DateTime.now()))(); dateTime().withDefault(Variable(DateTime.now()))();
} }
enum MediaType { youtube, image, location, localImage } enum MediaType { youtube, image, location, localImage, localVideo }
class MediaItems extends Table { class MediaItems extends Table {
IntColumn get id => integer().autoIncrement()(); IntColumn get id => integer().autoIncrement()();

View File

@ -1355,6 +1355,139 @@ final class Schema9 extends i0.VersionedSchema {
i1.GeneratedColumn<String> _column_25(String aliasedName) => i1.GeneratedColumn<String> _column_25(String aliasedName) =>
i1.GeneratedColumn<String>('reference', aliasedName, false, i1.GeneratedColumn<String>('reference', aliasedName, false,
type: i1.DriftSqlType.string); type: i1.DriftSqlType.string);
final class Schema10 extends i0.VersionedSchema {
Schema10({required super.database}) : super(version: 10);
@override
late final List<i1.DatabaseSchemaEntity> entities = [
sessions,
activities,
sessionActivities,
actions,
activityActions,
mediaItems,
objectMediaItems,
];
late final Shape11 sessions = Shape11(
source: i0.VersionedTable(
entityName: 'sessions',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_1,
_column_2,
_column_3,
_column_20,
_column_4,
_column_5,
],
attachedDatabase: database,
),
alias: null);
late final Shape1 activities = Shape1(
source: i0.VersionedTable(
entityName: 'activities',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_1,
_column_6,
_column_2,
_column_7,
_column_5,
],
attachedDatabase: database,
),
alias: null);
late final Shape9 sessionActivities = Shape9(
source: i0.VersionedTable(
entityName: 'session_activities',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_21,
_column_22,
_column_19,
_column_10,
_column_11,
_column_5,
],
attachedDatabase: database,
),
alias: null);
late final Shape3 actions = Shape3(
source: i0.VersionedTable(
entityName: 'actions',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_1,
_column_2,
_column_12,
_column_5,
],
attachedDatabase: database,
),
alias: null);
late final Shape10 activityActions = Shape10(
source: i0.VersionedTable(
entityName: 'activity_actions',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_22,
_column_23,
_column_19,
_column_5,
],
attachedDatabase: database,
),
alias: null);
late final Shape5 mediaItems = Shape5(
source: i0.VersionedTable(
entityName: 'media_items',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_1,
_column_2,
_column_25,
_column_6,
_column_5,
],
attachedDatabase: database,
),
alias: null);
late final Shape6 objectMediaItems = Shape6(
source: i0.VersionedTable(
entityName: 'object_media_items',
withoutRowId: false,
isStrict: false,
tableConstraints: [],
columns: [
_column_0,
_column_15,
_column_16,
_column_24,
_column_5,
],
attachedDatabase: database,
),
alias: null);
}
i0.MigrationStepWithVersion migrationSteps({ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2, required Future<void> Function(i1.Migrator m, Schema2 schema) from1To2,
required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3, required Future<void> Function(i1.Migrator m, Schema3 schema) from2To3,
@ -1364,6 +1497,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7, required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8, required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9, required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
}) { }) {
return (currentVersion, database) async { return (currentVersion, database) async {
switch (currentVersion) { switch (currentVersion) {
@ -1407,6 +1541,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema); final migrator = i1.Migrator(database, schema);
await from8To9(migrator, schema); await from8To9(migrator, schema);
return 9; return 9;
case 9:
final schema = Schema10(database: database);
final migrator = i1.Migrator(database, schema);
await from9To10(migrator, schema);
return 10;
default: default:
throw ArgumentError.value('Unknown migration from $currentVersion'); throw ArgumentError.value('Unknown migration from $currentVersion');
} }
@ -1422,6 +1561,7 @@ i1.OnUpgrade stepByStep({
required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7, required Future<void> Function(i1.Migrator m, Schema7 schema) from6To7,
required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8, required Future<void> Function(i1.Migrator m, Schema8 schema) from7To8,
required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9, required Future<void> Function(i1.Migrator m, Schema9 schema) from8To9,
required Future<void> Function(i1.Migrator m, Schema10 schema) from9To10,
}) => }) =>
i0.VersionedSchema.stepByStepHelper( i0.VersionedSchema.stepByStepHelper(
step: migrationSteps( step: migrationSteps(
@ -1433,4 +1573,5 @@ i1.OnUpgrade stepByStep({
from6To7: from6To7, from6To7: from6To7,
from7To8: from7To8, from7To8: from7To8,
from8To9: from8To9, from8To9: from8To9,
from9To10: from9To10,
)); ));

File diff suppressed because one or more lines are too long

View File

@ -66,6 +66,7 @@ Future<void> seedDb(AppDatabase database) async {
title: sessionValue[0], title: sessionValue[0],
content: sessionValue[1], content: sessionValue[1],
status: status, status: status,
address: sessionValue[2],
date: Value(DateTime.now()))) date: Value(DateTime.now())))
.then((sessionId) async { .then((sessionId) async {
// activities things // activities things
@ -175,14 +176,14 @@ Future<void> seedDb(AppDatabase database) async {
description: description:
'5155 Harvester Rd #1, Burlington, ON L7L 6V2, Canada', '5155 Harvester Rd #1, Burlington, ON L7L 6V2, Canada',
reference: reference:
'https://lh3.googleusercontent.com/places/ANXAkqHz0IeMrJnqjBwQJvYVHv9qSp0huWPCBcdeMZds66wpLofxGAIk3KrYFD2ShEZzqm1A-GO7BfmO3OtRdjSlnO6DAHgyDv_C_7w=s4800-w800', 'https://lh3.googleusercontent.com/places/ANXAkqHwtb5oRMGG3haJkaHeTxdTI1lQ17RgvkCXwzA1dGV53BXPbHrdXIs1mLC_-4exyRW8dbYhMOeiOCHJqGeVBx-dNtABZAl9tQA=s4800-w800',
type: MediaType.location)) type: MediaType.location))
.then((mediaId) async { .then((mediaId) async {
await database.into(database.objectMediaItems).insert( await database.into(database.objectMediaItems).insert(
ObjectMediaItemsCompanion.insert( ObjectMediaItemsCompanion.insert(
objectId: sessionId, objectId: sessionId,
mediaId: mediaId, mediaId: mediaId,
objectType: ObjectType.activities)); objectType: ObjectType.sessions));
}); });
}); });
} }

View File

@ -89,7 +89,7 @@ class _AppState extends State<App> {
NavigationDestination( NavigationDestination(
icon: Icon(Icons.sports), label: "Sessions"), icon: Icon(Icons.sports), label: "Sessions"),
NavigationDestination( NavigationDestination(
icon: Icon(Icons.landscape), label: "Activities"), icon: Icon(Icons.sports_gymnastics_rounded), label: "Activities"),
NavigationDestination( NavigationDestination(
icon: Icon(Icons.calendar_month_rounded), label: "Plan"), icon: Icon(Icons.calendar_month_rounded), label: "Plan"),
NavigationDestination( NavigationDestination(

View File

@ -1,14 +1,55 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/material.dart'; 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/database/database.dart';
import 'package:sendtrain/helpers/widget_helpers.dart'; import 'package:sendtrain/helpers/widget_helpers.dart';
import 'package:sendtrain/widgets/builders/dialogs.dart';
import 'package:video_player/video_player.dart';
class MediaCard extends StatelessWidget { class MediaCard extends StatefulWidget {
const MediaCard({super.key, required this.media}); const MediaCard({super.key, required this.media, this.callback});
final MediaItem media; final MediaItem media;
final Function? callback;
@override
State<MediaCard> createState() => _MediaCardState();
}
class _MediaCardState extends State<MediaCard> {
// late VideoPlayerController _controller;
late MediaItem media;
late Function? callback;
@override
void initState() {
super.initState();
media = widget.media;
callback = widget.callback;
// _controller = VideoPlayerController.asset(dataSource)
// ..initialize().then((_) {
// setState(() {}); //when your thumbnail will show.
// });
}
// @override
// void dispose() {
// super.dispose();
// _controller.dispose();
// }
// Future<VideoPlayerController> createVideoPlayer() async {
// final File file =
// await ImgB64Decoder.fileFromB64String(widget.encodedBytes);
// final VideoPlayerController controller = VideoPlayerController.file(file);
// await controller.initialize();
// await controller.setLooping(true);
// return controller;
// }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -20,7 +61,10 @@ class MediaCard extends StatelessWidget {
} else if (media.type == MediaType.localImage) { } else if (media.type == MediaType.localImage) {
image = Image.memory(base64Decode(media.reference)).image; image = Image.memory(base64Decode(media.reference)).image;
} else if (media.type == MediaType.youtube) { } else if (media.type == MediaType.youtube) {
image = NetworkImage('https://img.youtube.com/vi/${media.reference}/0.jpg'); 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 DecorationImage(image: image, fit: BoxFit.cover);
@ -37,6 +81,18 @@ class MediaCard extends StatelessWidget {
RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
shadowColor: const Color.fromARGB(0, 255, 255, 255), shadowColor: const Color.fromARGB(0, 255, 255, 255),
child: TextButton( child: TextButton(
onLongPress: () => showRemovalDialog(
'Media Removal',
'Would you like to permanently remove this media from the current session?',
context,
MediaItemsDao(Provider.of<AppDatabase>(context,
listen: false)),
media)
.then((result) {
if (callback != null) {
callback!();
}
}),
onPressed: () => showMediaDetailWidget(context, media), onPressed: () => showMediaDetailWidget(context, media),
child: const ListTile( child: const ListTile(
title: Text(''), title: Text(''),

View File

@ -25,6 +25,7 @@ class MediaDetails extends StatelessWidget {
media.description, media.description,
style: const TextStyle(fontSize: 20), style: const TextStyle(fontSize: 20),
)), )),
const Divider( const Divider(
indent: 20, indent: 20,
endIndent: 20, endIndent: 20,

View File

@ -47,7 +47,11 @@ class _SessionCardSmallState extends State<SessionCardSmall> {
// overlayColor: MaterialStateColor(Colors.deepPurple as int), // overlayColor: MaterialStateColor(Colors.deepPurple as int),
splashColor: Colors.deepPurple, splashColor: Colors.deepPurple,
borderRadius: const BorderRadius.all(Radius.elliptical(10, 10)), borderRadius: const BorderRadius.all(Radius.elliptical(10, 10)),
onLongPress: () => showMediaDetailWidget(context, sessionImage!), onLongPress: () {
if (sessionImage != null) {
showMediaDetailWidget(context, sessionImage);
}
},
onTap: () => onTap: () =>
showGenericDialog(SessionView(session: session), context), showGenericDialog(SessionView(session: session), context),
child: Container( child: Container(

View File

@ -1,11 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:drift/drift.dart' hide Column; import 'package:drift/drift.dart' hide Column;
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:mime/mime.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:sendtrain/daos/media_items_dao.dart'; import 'package:sendtrain/daos/media_items_dao.dart';
import 'package:sendtrain/daos/object_media_items_dao.dart'; import 'package:sendtrain/daos/object_media_items_dao.dart';
@ -193,13 +193,13 @@ class _SessionEditorState extends State<SessionEditor> {
borderSide: BorderSide.none, borderSide: BorderSide.none,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
), ),
labelText: 'Select Media (optional)', labelText: 'Media (optional)',
), ),
controller: sessionCreateController['media'], controller: sessionCreateController['media'],
onTap: () async { onTap: () async {
FilePickerResult? result = await FilePicker.platform FilePickerResult? result = await FilePicker.platform
.pickFiles( .pickFiles(
allowMultiple: true, type: FileType.media); allowMultiple: true, type: FileType.image);
if (result != null) { if (result != null) {
List<PlatformFile> files = result.files; List<PlatformFile> files = result.files;
@ -246,14 +246,21 @@ class _SessionEditorState extends State<SessionEditor> {
i++) { i++) {
PlatformFile file = PlatformFile file =
sessionPayload.files![i]; sessionPayload.files![i];
String? type = lookupMimeType(file.path!)!.split('/').first;
Uint8List fileBytes = Uint8List fileBytes =
await file.xFile.readAsBytes(); await file.xFile.readAsBytes();
MediaType mediaType = MediaType.localImage;
if (type == "video") {
mediaType = MediaType.localVideo;
}
await createSessionMedia( await createSessionMedia(
'Local Media', 'Local Media',
currentSessionId, currentSessionId,
file.name, file.name,
base64Encode(fileBytes), base64Encode(fileBytes),
MediaType.localImage); mediaType);
} }
} }

View File

@ -34,28 +34,42 @@ class SessionViewAchievements extends StatelessWidget {
final sessionActivities = snapshot.data!; final sessionActivities = snapshot.data!;
final achievements = getAchievements(sessionActivities); final achievements = getAchievements(sessionActivities);
Widget content;
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: () {},
));
} 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].toTitleCase()),
onPressed: () {
// remove achievements
},
));
},
);
}
return Column( return Column(
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.only(bottom: 10), padding: const EdgeInsets.only(bottom: 10),
child: SizedBox( child: SizedBox(height: 40, child: content)),
height: 40,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.fromLTRB(10, 0, 10, 0),
itemCount: achievements.length,
itemBuilder: (BuildContext context, int index) {
return Padding(
padding: const EdgeInsets.only(right: 5),
child: ActionChip(
visualDensity: VisualDensity.compact,
avatar:
const Icon(Icons.check_circle_outline),
label: Text(maxLines: 1, achievements[index].toTitleCase()),
onPressed: () {},
));
},
))),
], ],
); );
} else { } else {

View File

@ -4,22 +4,31 @@ import 'package:sendtrain/daos/media_items_dao.dart';
import 'package:sendtrain/database/database.dart'; import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/widgets/media/media_card.dart'; import 'package:sendtrain/widgets/media/media_card.dart';
class SessionViewMedia extends StatelessWidget { class SessionViewMedia extends StatefulWidget {
const SessionViewMedia({super.key, required this.session}); const SessionViewMedia({super.key, required this.session});
final Session session; final Session session;
@override
State<SessionViewMedia> createState() => _SessionViewMediaState();
}
class _SessionViewMediaState extends State<SessionViewMedia> {
void resetState() {
setState(() {});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FutureBuilder<List<MediaItem>>( return StreamBuilder<List<MediaItem>>(
future: MediaItemsDao(Provider.of<AppDatabase>(context)) stream: MediaItemsDao(Provider.of<AppDatabase>(context))
.fromSession(session.id), .fromSession(widget.session.id).asStream(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasData) { if (snapshot.hasData) {
final mediaItems = snapshot.data!; final mediaItems = snapshot.data!;
List<Widget> mediaCards = List.generate( List<Widget> mediaCards = List.generate(
mediaItems.length, (i) => MediaCard(media: mediaItems[i])); mediaItems.length, (i) => MediaCard(media: mediaItems[i], callback: resetState));
return Column( return Column(
children: [ children: [

View File

@ -48,6 +48,8 @@ dependencies:
file_picker: ^8.1.7 file_picker: ^8.1.7
http: ^1.2.2 http: ^1.2.2
uuid: ^4.5.1 uuid: ^4.5.1
mime: ^2.0.0
video_player: ^2.9.2
flutter_launcher_name: flutter_launcher_name:
name: "SendTrain" name: "SendTrain"

View File

@ -12,6 +12,7 @@ import 'schema_v6.dart' as v6;
import 'schema_v7.dart' as v7; import 'schema_v7.dart' as v7;
import 'schema_v8.dart' as v8; import 'schema_v8.dart' as v8;
import 'schema_v9.dart' as v9; import 'schema_v9.dart' as v9;
import 'schema_v10.dart' as v10;
class GeneratedHelper implements SchemaInstantiationHelper { class GeneratedHelper implements SchemaInstantiationHelper {
@override @override
@ -35,10 +36,12 @@ class GeneratedHelper implements SchemaInstantiationHelper {
return v8.DatabaseAtV8(db); return v8.DatabaseAtV8(db);
case 9: case 9:
return v9.DatabaseAtV9(db); return v9.DatabaseAtV9(db);
case 10:
return v10.DatabaseAtV10(db);
default: default:
throw MissingSchemaException(version, versions); throw MissingSchemaException(version, versions);
} }
} }
static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9]; static const versions = const [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
} }

File diff suppressed because it is too large Load Diff