media item and session images and location management, also refactoring and DRYing up code

This commit is contained in:
Joshua Burman
2024-12-31 22:41:17 -05:00
parent 5f628d6b48
commit 10332ec8be
17 changed files with 328 additions and 129 deletions

View File

@ -5,10 +5,12 @@ 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(
@ -22,6 +24,9 @@ class SessionCardFull extends StatefulWidget {
}
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();
@ -32,10 +37,16 @@ class _SessionCardFullState extends State<SessionCardFull> {
}
@override
Widget build(BuildContext context) {
final Session session = widget.session;
final List<MediaItem> mediaItems = widget.mediaItems;
initState() {
super.initState();
session = widget.session;
mediaItems = widget.mediaItems;
sessionImage = mediaItems
.firstWhereOrNull((mediaItem) => mediaItem.type == MediaType.location);
}
@override
Widget build(BuildContext context) {
return Card(
color: (session.status == SessionStatus.started)
? Theme.of(context).colorScheme.primaryContainer
@ -44,6 +55,7 @@ class _SessionCardFullState extends State<SessionCardFull> {
clipBehavior: Clip.hardEdge,
child: InkWell(
splashColor: Colors.deepPurple,
onLongPress: () => showMediaDetailWidget(context, sessionImage!),
onTap: () =>
showGenericDialog(SessionView(session: session), context),
child: Column(
@ -52,7 +64,7 @@ class _SessionCardFullState extends State<SessionCardFull> {
ListTile(
contentPadding: EdgeInsets.only(left: 8),
leading: CardImage(
image: findMediaByType(mediaItems, 'image'),
image: findMediaByType(mediaItems, MediaType.location),
padding: EdgeInsets.only(left: 5, top: 5)),
title: Text(maxLines: 1, sessionTitle(session)),
subtitle: Text(

View File

@ -3,8 +3,10 @@ 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(
@ -18,11 +20,20 @@ class SessionCardSmall extends StatefulWidget {
}
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;
return Card(
color: (session.status == SessionStatus.started)
? Theme.of(context).colorScheme.primaryContainer
@ -31,7 +42,9 @@ class _SessionCardSmallState extends State<SessionCardSmall> {
// overlayColor: MaterialStateColor(Colors.deepPurple as int),
splashColor: Colors.deepPurple,
borderRadius: const BorderRadius.all(Radius.elliptical(10, 10)),
onTap: () => showGenericDialog(SessionView(session: session), context),
onLongPress: () => showMediaDetailWidget(context, sessionImage!),
onTap: () =>
showGenericDialog(SessionView(session: session), context),
child: Container(
decoration: BoxDecoration(
// color: const Color.fromARGB(47, 0, 0, 0),
@ -39,7 +52,7 @@ class _SessionCardSmallState extends State<SessionCardSmall> {
image: DecorationImage(
colorFilter: ColorFilter.mode(
Color.fromARGB(220, 41, 39, 39), BlendMode.hardLight),
image: findMediaByType(mediaItems, 'image'),
image: findMediaByType(mediaItems, MediaType.location),
fit: BoxFit.cover),
),
child: Align(

View File

@ -1,5 +1,4 @@
import 'dart:async';
import 'dart:math';
import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart';
@ -14,14 +13,15 @@ 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 SessionCreator extends StatefulWidget {
const SessionCreator({super.key, this.data, this.session});
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<SessionCreator> createState() => _SessionCreatorState();
State<SessionEditor> createState() => _SessionEditorState();
}
// used to pass the result of the found image back to current context...
@ -30,8 +30,10 @@ class SessionPayload {
String? address;
}
class _SessionCreatorState extends State<SessionCreator> {
class _SessionEditorState extends State<SessionEditor> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
late AppDatabase db;
String editorType = 'Create';
final Map<String, TextEditingController> sessionCreateController = {
'name': TextEditingController(),
@ -44,45 +46,78 @@ class _SessionCreatorState extends State<SessionCreator> {
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),
Symbol('status'): Value<SessionStatus>(SessionStatus.pending),
// 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(Provider.of<AppDatabase>(context, listen: false))
return await SessionsDao(db)
.createOrUpdate(Function.apply(SessionsCompanion.new, [], payload));
}
Future createSessionMedia(context, sessionId) async {
List<MediaItem> deletedMedia =
await MediaItemsDao(db).fromSession(sessionId)
..where((mediaItem) => mediaItem.type == MediaType.location);
await MediaItemsDao(db).removeAll(deletedMedia.map((m) => m.id));
if (sessionPayload.photoUri != null) {
MediaItemsCompanion mediaItem = MediaItemsCompanion(
title: Value('Location Image'),
description: Value(sessionPayload.address!),
reference: Value(sessionPayload.photoUri!),
type: Value(MediaType.image));
type: Value(MediaType.location));
return await MediaItemsDao(
Provider.of<AppDatabase>(context, listen: false))
.createOrUpdate(mediaItem).then((id) async {
ObjectMediaItemsCompanion omi = ObjectMediaItemsCompanion(
objectId: Value(sessionId),
objectType: Value(ObjectType.sessions),
mediaId: Value(id),
);
return await MediaItemsDao(db).createOrUpdate(mediaItem).then((id) async {
ObjectMediaItemsCompanion omi = ObjectMediaItemsCompanion(
objectId: Value(sessionId),
objectType: Value(ObjectType.sessions),
mediaId: Value(id),
);
await ObjectMediaItemsDao(Provider.of<AppDatabase>(context, listen: false)).createOrUpdate(omi);
});
await ObjectMediaItemsDao(db).createOrUpdate(omi);
});
}
}
@ -91,8 +126,6 @@ class _SessionCreatorState extends State<SessionCreator> {
sessionCreateController['date']!.text =
DateFormat('yyyy-MM-dd').format(DateTime.now());
AppDatabase db = Provider.of<AppDatabase>(context, listen: false);
return Padding(
padding: EdgeInsets.fromLTRB(15, 0, 15, 15),
child: Form(
@ -103,7 +136,7 @@ class _SessionCreatorState extends State<SessionCreator> {
children: <Widget>[
Padding(
padding: EdgeInsets.only(top: 10, bottom: 10),
child: Text('Create Session',
child: Text('$editorType Session',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.titleLarge)),
FormTextInput(
@ -166,19 +199,43 @@ class _SessionCreatorState extends State<SessionCreator> {
if (_formKey.currentState!.validate())
{
await createSession(_formKey.currentContext)
.then((id) async => {
await createSessionMedia(
_formKey.currentContext, id),
SessionsDao(db).find(id).then(
(session) => showGenericDialog(
SessionView(
session: session),
_formKey
.currentContext!)),
Navigator.pop(
_formKey.currentContext!,
'Submit')
})
.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 createSessionMedia(
_formKey.currentContext,
currentSessionId);
}
// 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!(widget.session!.id);
}
})
}
},
child: Text('Submit')))

View File

@ -5,9 +5,11 @@ import 'package:intl/date_symbol_data_local.dart';
import 'package:provider/provider.dart';
import 'package:sendtrain/daos/activities_dao.dart';
import 'package:sendtrain/daos/sessions_dao.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/extensions/string_extensions.dart';
import 'package:sendtrain/widgets/generic/elements/generic_progress_indicator.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';
@ -22,13 +24,45 @@ class SessionView extends StatefulWidget {
}
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(int sessionId) async {
Session updatedSession =
await SessionsDao(Provider.of<AppDatabase>(context, listen: false))
.find(sessionId);
final state = _fabKey.currentState;
if (state != null) {
state.toggle();
}
setState(() {
session = updatedSession;
});
}
@override
initState() {
super.initState();
initializeDateFormatting('en');
dateFormat = DateFormat('yyyy-MM-dd');
session = widget.session;
}
@override
Widget build(BuildContext context) {
final Session session = widget.session;
initializeDateFormatting('en');
final DateFormat dateFormat = DateFormat('yyyy-MM-dd');
return StreamBuilder<List<Activity>>(
stream: ActivitiesDao(Provider.of<AppDatabase>(context))
.watchSessionActivities(session.id),
@ -39,6 +73,7 @@ class _SessionViewState extends State<SessionView> {
return Scaffold(
floatingActionButtonLocation: ExpandableFab.location,
floatingActionButton: ExpandableFab(
key: _fabKey,
distance: 70,
type: ExpandableFabType.up,
overlayStyle: ExpandableFabOverlayStyle(
@ -49,17 +84,55 @@ class _SessionViewState extends State<SessionView> {
FloatingActionButton.extended(
icon: const Icon(Icons.history_outlined),
label: Text('Restart'),
onPressed: () {},
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: () {},
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();
}
},
),
FloatingActionButton.extended(
icon: const Icon(Icons.edit_outlined),
label: Text('Edit'),
onPressed: () {},
onPressed: () {
showModalBottomSheet<void>(
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.all(Radius.circular(10.0)),
),
context: context,
showDragHandle: true,
isScrollControlled: true,
useSafeArea: true,
builder: (BuildContext context) {
return SessionEditor(
session: session, callback: resetState);
});
},
),
]),
body: Column(
@ -78,7 +151,7 @@ class _SessionViewState extends State<SessionView> {
maxLines: 1,
style: const TextStyle(
fontSize: 25, fontWeight: FontWeight.bold),
session.title.toTitleCase())),
title())),
SessionViewAchievements(session: session),
Padding(
padding: const EdgeInsets.only(left: 15, right: 15),

View File

@ -13,7 +13,7 @@ class SessionViewMedia extends StatelessWidget {
Widget build(BuildContext context) {
return FutureBuilder<List<MediaItem>>(
future: MediaItemsDao(Provider.of<AppDatabase>(context))
.fromSession(session),
.fromSession(session.id),
builder: (context, snapshot) {
if (snapshot.hasData) {
final mediaItems = snapshot.data!;