session crud #5

Merged
jshbrmn merged 22 commits from session-crud into main 2025-01-05 19:02:13 -08:00
27 changed files with 474 additions and 292 deletions
Showing only changes of commit 722a152130 - Show all commits

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

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

View File

@ -1,4 +1,6 @@
<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.gps" android:required="false" />
<application <application
android:label="sendtrain" android:label="sendtrain"
android:name="${applicationName}" android:name="${applicationName}"
@ -31,6 +33,8 @@
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
</application> </application>
<meta-data android:name="com.google.android.geo.API_KEY"
android:value="AIzaSyBCjMCEAyyNVpsnVYvZj6VL1mmB98Vd6AE" />
<!-- Required to query activities that can process text, see: <!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT. https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
@ -40,6 +44,7 @@
<intent> <intent>
<action android:name="android.intent.action.PROCESS_TEXT" /> <action android:name="android.intent.action.PROCESS_TEXT" />
<data android:mimeType="text/plain" /> <data android:mimeType="text/plain" />
</intent> </intent>
</queries> </queries>
</manifest> </manifest>

View File

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

View File

@ -2,6 +2,13 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <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> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>

View File

@ -6,16 +6,31 @@ import 'package:sendtrain/screens/activities_screen.dart';
import 'package:sendtrain/screens/sessions_screen.dart'; import 'package:sendtrain/screens/sessions_screen.dart';
// ignore: unused_import // ignore: unused_import
import 'package:sendtrain/database/seed.dart'; import 'package:sendtrain/database/seed.dart';
import 'package:sendtrain/widgets/session_creator.dart'; import 'package:sendtrain/widgets/sessions/session_creator.dart';
class SendTrain extends StatelessWidget { class SendTrain extends StatelessWidget {
const SendTrain({super.key}); const SendTrain({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final ThemeData themeData = ThemeData.dark(useMaterial3: true);
return MaterialApp( return MaterialApp(
title: "Sendtrain", title: "Sendtrain",
theme: ThemeData.dark(useMaterial3: true), theme: themeData.copyWith(
filledButtonTheme: FilledButtonThemeData(
style: FilledButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
floatingActionButtonTheme: FloatingActionButtonThemeData(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12))),
inputDecorationTheme: InputDecorationTheme(
fillColor: themeData.colorScheme.surface,
),
),
home: const App()); home: const App());
} }
} }
@ -84,16 +99,18 @@ class _AppState extends State<App> {
]), ]),
floatingActionButton: FloatingActionButton.extended( floatingActionButton: FloatingActionButton.extended(
onPressed: () { onPressed: () {
showAdaptiveDialog( showModalBottomSheet<void>(
barrierColor: Colors.black.withOpacity(0.5), context: context,
builder: (BuildContext context) => SessionCreator(), showDragHandle: true,
barrierDismissible: true, isScrollControlled: true,
barrierLabel: '', useSafeArea: true,
context: context); builder: (BuildContext context) {
return SessionCreator();
});
}, },
label: const Text('New Session'), label: const Text('New Session'),
icon: const Icon(Icons.add_chart), icon: const Icon(Icons.add_chart),
backgroundColor: Colors.deepPurple, // backgroundColor: Colors.deepPurple,
)); ));
} }
} }

View File

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

View File

@ -5,7 +5,7 @@ import 'package:sendtrain/daos/media_items_dao.dart';
import 'package:sendtrain/database/database.dart'; import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/extensions/string_extensions.dart'; import 'package:sendtrain/extensions/string_extensions.dart';
import 'package:sendtrain/models/activity_timer_model.dart'; import 'package:sendtrain/models/activity_timer_model.dart';
import 'package:sendtrain/widgets/activity_view.dart'; import 'package:sendtrain/widgets/activities/activity_view.dart';
class ActivityCard extends StatefulWidget { class ActivityCard extends StatefulWidget {
final Activity activity; final Activity activity;

View File

@ -5,10 +5,10 @@ import 'package:sendtrain/daos/actions_dao.dart';
import 'package:sendtrain/database/database.dart'; import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/extensions/string_extensions.dart'; import 'package:sendtrain/extensions/string_extensions.dart';
import 'package:sendtrain/models/activity_timer_model.dart'; import 'package:sendtrain/models/activity_timer_model.dart';
import 'package:sendtrain/widgets/activity_action_view.dart'; import 'package:sendtrain/widgets/activities/activity_action_view.dart';
import 'package:sendtrain/widgets/activity_view_categories.dart'; import 'package:sendtrain/widgets/activities/activity_view_categories.dart';
import 'package:sendtrain/widgets/activity_view_media.dart'; import 'package:sendtrain/widgets/activities/activity_view_media.dart';
import 'package:sendtrain/widgets/activity_view_types.dart'; import 'package:sendtrain/widgets/activities/activity_view_types.dart';
class ActivityView extends StatefulWidget { class ActivityView extends StatefulWidget {
const ActivityView( const ActivityView(

View File

@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/widgets/sessions/session_view.dart';
Future showSessionDialog(Session session, 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: SessionView(session: session)));
},
barrierDismissible: true,
barrierLabel: '',
context: parentContext,
pageBuilder: (context, animation1, animation2) {
return Container();
});
}
Future showRemovalDialog(title, content, context, dao, session) {
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: () => {dao.remove(session), Navigator.pop(context, 'OK')},
child: const Text('OK'),
),
],
),
);
}

View File

@ -0,0 +1,156 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:sendtrain/widgets/generic/elements/form_text_input.dart';
class FormSearchInput extends StatefulWidget {
const FormSearchInput({super.key, required this.sessionController});
final TextEditingController sessionController;
@override
State<FormSearchInput> createState() => _FormSearchInputState();
}
class _FormSearchInputState extends State<FormSearchInput> {
String? _currentQuery;
// The most recent suggestions received from the API.
late Iterable<Widget> _lastOptions = <Widget>[];
late final _Debounceable<Iterable<String>?, String> _debouncedSearch;
// Calls the "remote" API to search with the given query. Returns null when
// the call has been made obsolete.
Future<Iterable<String>?> _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 another search happened after this one, throw away these options.
if (_currentQuery != query) {
return null;
}
_currentQuery = null;
return options;
}
@override
void initState() {
super.initState();
_debouncedSearch = _debounce<Iterable<String>?, String>(_search);
}
@override
Widget build(BuildContext context) {
return SearchAnchor(
builder: (BuildContext context, SearchController controller) {
return FormTextInput(
controller: widget.sessionController,
title: 'Location (optional)',
icon: Icon(Icons.search_rounded),
maxLines: 2,
onTap: () {
controller.openView();
});
}, suggestionsBuilder:
(BuildContext context, SearchController controller) async {
final List<String>? options =
(await _debouncedSearch(controller.text))?.toList();
if (options == null) {
return _lastOptions;
}
_lastOptions = List<ListTile>.generate(options.length, (int index) {
final String item = options[index];
return ListTile(
title: Text(item),
onTap: () {
if (item.isNotEmpty) {
widget.sessionController.text = item;
}
controller.closeView(item);
// debugPrint('You just selected $item');
// Navigator.of(context).pop();
},
);
});
return _lastOptions;
});
}
}
const Duration fakeAPIDuration = Duration(seconds: 1);
const Duration debounceDuration = Duration(milliseconds: 500);
class _FakeAPI {
static const List<String> _kOptions = <String>[
'aardvark',
'bobcat',
'chameleon',
];
// Searches the options, but injects a fake "network" delay.
static Future<Iterable<String>> search(String query) async {
await Future<void>.delayed(fakeAPIDuration); // Fake 1 second delay.
if (query == '') {
return const Iterable<String>.empty();
}
return _kOptions.where((String option) {
return option.contains(query.toLowerCase());
});
}
}
typedef _Debounceable<S, T> = Future<S?> Function(T parameter);
/// 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();
try {
await debounceTimer!.future;
} on _CancelException {
return null;
}
return function(parameter);
};
}
// A wrapper around Timer used for debouncing.
class _DebounceTimer {
_DebounceTimer() {
_timer = Timer(debounceDuration, _onComplete);
}
late final Timer _timer;
final Completer<void> _completer = Completer<void>();
void _onComplete() {
_completer.complete();
}
Future<void> get future => _completer.future;
bool get isCompleted => _completer.isCompleted;
void cancel() {
_timer.cancel();
_completer.completeError(const _CancelException());
}
}
// An exception indicating that the timer was canceled.
class _CancelException implements Exception {
const _CancelException();
}

View File

@ -0,0 +1,52 @@
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});
final TextEditingController controller;
final String title;
final int? maxLines;
final int? minLines;
final Icon? icon;
final dynamic onTap;
@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 (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();
}
}));
}
}

View File

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

View File

@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:sendtrain/daos/sessions_dao.dart'; import 'package:sendtrain/daos/sessions_dao.dart';
import 'package:sendtrain/database/database.dart'; import 'package:sendtrain/database/database.dart';
import '../widgets/session_card.dart'; import '../widgets/sessions/session_card.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
class SessionsScreen extends StatefulWidget { class SessionsScreen extends StatefulWidget {

View File

@ -1,187 +0,0 @@
import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:sendtrain/daos/sessions_dao.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/widgets/session_view.dart';
class SessionCreator extends StatefulWidget {
const SessionCreator({super.key, this.data, this.session});
final Session? session;
final Map<String, dynamic>? data;
@override
State<SessionCreator> createState() => _SessionCreatorState();
}
class _SessionCreatorState extends State<SessionCreator> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final Map<String, TextEditingController> sessionCreateController = {
'name': TextEditingController(),
'content': TextEditingController(),
'status': TextEditingController(),
'date': TextEditingController(),
};
Future createSession(context) {
return SessionsDao(Provider.of<AppDatabase>(context, listen: false))
.createOrUpdate(SessionsCompanion(
title: Value(sessionCreateController['name']!.text),
content: Value(sessionCreateController['content']!.text),
status: Value(SessionStatus.pending),
date:
Value(DateTime.parse(sessionCreateController['date']!.text))));
}
@override
Widget build(BuildContext context) {
sessionCreateController['date']!.text =
DateFormat('yyyy-MM-dd').format(DateTime.now());
return SimpleDialog(
title: Text('Create Session', textAlign: TextAlign.center),
titlePadding: EdgeInsets.only(top: 17.5, bottom: 10),
contentPadding: EdgeInsets.only(left: 30, right: 30, bottom: 15),
children: [
Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Padding(
padding: EdgeInsets.only(top: 10, bottom: 10),
child: TextFormField(
controller: sessionCreateController['name'],
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'Enter session name',
),
validator: (String? value) {
if (value == null || value.isEmpty) {
return 'Please enter some text';
}
if (value.length <= 3 || value.length > 42) {
return 'Please enter between 3 and 42 characters';
}
return null;
},
)),
Padding(
padding: EdgeInsets.only(top: 10, bottom: 10),
child: TextFormField(
controller: sessionCreateController['content'],
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'Enter session description',
),
validator: (String? value) {
if (value == null || value.isEmpty) {
return 'Please enter some text';
}
if (value.length <= 3 || value.length > 256) {
return 'Please enter between 3 and 256 characters';
}
return null;
},
)),
Padding(
padding: EdgeInsets.only(top: 7.5, bottom: 7.5),
child: TextFormField(
readOnly: true,
decoration: const InputDecoration(
border: OutlineInputBorder(),
hintText: 'Enter a date for the session',
),
controller: sessionCreateController['date'],
onTap: () {
showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime.now()
.subtract(Duration(days: 365)),
lastDate:
DateTime.now().add(Duration(days: 365)))
.then((date) {
sessionCreateController['date']?.text =
DateFormat('yyyy-MM-dd').format(date!);
});
})),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => {
if (_formKey.currentState!.validate())
{
createSession(_formKey.currentContext)
.then((id) => {
SessionsDao(
Provider.of<AppDatabase>(
context,
listen: false))
.find(id)
.then(
(session) =>
showGeneralDialog(
barrierColor: Colors
.black
.withOpacity(0.5),
transitionDuration:
const Duration(
milliseconds:
220),
transitionBuilder:
(BuildContext
context,
Animation<
double>
animation,
Animation<
double>
secondaryAnimation,
Widget
child) {
Animation<Offset> custom = Tween<
Offset>(
begin:
const Offset(
0.0,
1.0),
end: const Offset(
0.0,
0.0))
.animate(
animation);
return SlideTransition(
position:
custom,
child: Dialog.fullscreen(
child: SessionView(
session:
session)));
},
barrierDismissible:
true,
barrierLabel: '',
context: context,
pageBuilder: (context,
animation1,
animation2) {
return Container();
}).whenComplete(() => Navigator.pop(context, 'Submit')),
)
})
}
},
child: Text('Submit'))
])
],
))
]);
}
}

View File

@ -6,7 +6,7 @@ import 'package:sendtrain/daos/media_items_dao.dart';
import 'package:sendtrain/daos/sessions_dao.dart'; import 'package:sendtrain/daos/sessions_dao.dart';
import 'package:sendtrain/database/database.dart' hide ActivityAction; import 'package:sendtrain/database/database.dart' hide ActivityAction;
import 'package:sendtrain/extensions/string_extensions.dart'; import 'package:sendtrain/extensions/string_extensions.dart';
import 'package:sendtrain/widgets/session_view.dart'; import 'package:sendtrain/widgets/builders/dialogs.dart';
class SessionCard extends StatefulWidget { class SessionCard extends StatefulWidget {
final int type; final int type;
@ -44,28 +44,7 @@ class _SessionCardState extends State<SessionCard> {
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
child: InkWell( child: InkWell(
splashColor: Colors.deepPurple, splashColor: Colors.deepPurple,
onTap: () => showGeneralDialog( onTap: () => showSessionDialog(session, context),
barrierColor: Colors.black.withOpacity(0.5),
transitionDuration: const Duration(milliseconds: 220),
transitionBuilder: (BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
Animation<Offset> custom = Tween<Offset>(
begin: const Offset(0.0, 1.0),
end: const Offset(0.0, 0.0))
.animate(animation);
return SlideTransition(
position: custom,
child: Dialog.fullscreen(
child: SessionView(session: session)));
},
barrierDismissible: true,
barrierLabel: '',
context: context,
pageBuilder: (context, animation1, animation2) {
return Container();
}),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
@ -94,35 +73,17 @@ class _SessionCardState extends State<SessionCard> {
alignment: Alignment.topCenter, alignment: Alignment.topCenter,
icon: Icon(Icons.close_rounded), icon: Icon(Icons.close_rounded),
onPressed: () { onPressed: () {
showAdaptiveDialog( showRemovalDialog(
context: context, 'Session Removal',
builder: (BuildContext context) => AlertDialog( 'Would you like to permanently remove this session?',
title: const Text('Session Removal'), context,
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: () => {
SessionsDao(Provider.of<AppDatabase>( SessionsDao(Provider.of<AppDatabase>(
context, context,
listen: false)) listen: false)),
.remove(session) session)
.then((result) { .then((result) {
setState(() {}); setState(() {});
}), });
Navigator.pop(context, 'OK')
},
child: const Text('OK'),
),
],
),
);
}, },
), ),
), ),
@ -147,28 +108,7 @@ class _SessionCardState extends State<SessionCard> {
splashColor: Colors.deepPurple, splashColor: Colors.deepPurple,
borderRadius: borderRadius:
const BorderRadius.all(Radius.elliptical(10, 10)), const BorderRadius.all(Radius.elliptical(10, 10)),
onTap: () => showGeneralDialog( onTap: () => showSessionDialog(session, context),
// barrierColor: Colors.black.withOpacity(0.5),
transitionDuration: const Duration(milliseconds: 220),
transitionBuilder: (BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
Widget child) {
Animation<Offset> custom = Tween<Offset>(
begin: const Offset(0.0, 1.0),
end: const Offset(0.0, 0.0))
.animate(animation);
return SlideTransition(
position: custom,
child: Dialog.fullscreen(
child: SessionView(session: session)));
},
barrierDismissible: true,
barrierLabel: '',
context: context,
pageBuilder: (context, animation1, animation2) {
return Container();
}),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
// color: const Color.fromARGB(47, 0, 0, 0), // color: const Color.fromARGB(47, 0, 0, 0),

View File

@ -0,0 +1,141 @@
import 'dart:async';
import 'package:drift/drift.dart' hide Column;
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:provider/provider.dart';
import 'package:sendtrain/daos/sessions_dao.dart';
import 'package:sendtrain/database/database.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';
class SessionCreator extends StatefulWidget {
const SessionCreator({super.key, this.data, this.session});
final Session? session;
final Map<String, dynamic>? data;
@override
State<SessionCreator> createState() => _SessionCreatorState();
}
class _SessionCreatorState extends State<SessionCreator> {
final GlobalKey<FormState> _formKey = GlobalKey<FormState>();
final Map<String, TextEditingController> sessionCreateController = {
'name': TextEditingController(),
'content': TextEditingController(),
'status': TextEditingController(),
'date': TextEditingController(),
'address': TextEditingController(),
'media': TextEditingController(),
};
Future createSession(context) {
return SessionsDao(Provider.of<AppDatabase>(context, listen: false))
.createOrUpdate(SessionsCompanion(
title: Value(sessionCreateController['name']!.text),
content: Value(sessionCreateController['content']!.text),
status: Value(SessionStatus.pending),
date: Value(DateTime.parse(sessionCreateController['date']!.text)),
address: Value(sessionCreateController['address']!.text)));
}
@override
Widget build(BuildContext context) {
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(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Padding(
padding: EdgeInsets.only(top: 10, bottom: 10),
child: Text('Create 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);
}
});
}),
// 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: 'Select Media (optional)',
// ),
// controller: sessionCreateController['media'],
// onTap: () async {
// FilePickerResult? result = await FilePicker.platform
// .pickFiles(allowMultiple: true);
// if (result != null) {
// List<File> files = result.paths
// .map((path) => File(path!))
// .toList();
// }
// })),
FormSearchInput(
sessionController: sessionCreateController['address']!),
Row(mainAxisAlignment: MainAxisAlignment.end, children: [
Padding(
padding: EdgeInsets.only(top: 10),
child: FilledButton(
onPressed: () => {
if (_formKey.currentState!.validate())
{
createSession(_formKey.currentContext)
.then((id) => {
SessionsDao(db).find(id).then(
(session) => showSessionDialog(
session,
_formKey
.currentContext!)),
Navigator.pop(
_formKey.currentContext!,
'Submit')
})
}
},
child: Text('Submit')))
]),
],
)));
}
}

View File

@ -7,9 +7,9 @@ import 'package:provider/provider.dart';
import 'package:sendtrain/daos/activities_dao.dart'; import 'package:sendtrain/daos/activities_dao.dart';
import 'package:sendtrain/database/database.dart'; import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/extensions/string_extensions.dart'; import 'package:sendtrain/extensions/string_extensions.dart';
import 'package:sendtrain/widgets/session_view_achievements.dart'; import 'package:sendtrain/widgets/sessions/session_view_achievements.dart';
import 'package:sendtrain/widgets/session_view_activities.dart'; import 'package:sendtrain/widgets/sessions/session_view_activities.dart';
import 'package:sendtrain/widgets/session_view_media.dart'; import 'package:sendtrain/widgets/sessions/session_view_media.dart';
class SessionView extends StatefulWidget { class SessionView extends StatefulWidget {
const SessionView({super.key, required this.session}); const SessionView({super.key, required this.session});

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:sendtrain/database/database.dart'; import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/widgets/activity_card.dart'; import 'package:sendtrain/widgets/activities/activity_card.dart';
class SessionViewActivities extends StatelessWidget { class SessionViewActivities extends StatelessWidget {
const SessionViewActivities({super.key, required this.activities }); const SessionViewActivities({super.key, required this.activities });

View File

@ -44,6 +44,8 @@ dependencies:
drift: ^2.22.1 drift: ^2.22.1
flutter_expandable_fab: ^2.3.0 flutter_expandable_fab: ^2.3.0
drift_flutter: ^0.2.2 drift_flutter: ^0.2.2
map_location_picker: ^1.2.7
file_picker: ^8.1.7
flutter_launcher_name: flutter_launcher_name:
name: "SendTrain" name: "SendTrain"