diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro
new file mode 100644
index 0000000..116bc22
--- /dev/null
+++ b/android/app/proguard-rules.pro
@@ -0,0 +1 @@
+-keep class androidx.lifecycle.DefaultLifecycleObserver
\ No newline at end of file
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index a3b2bdf..568c3c4 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,4 +1,6 @@
+
+
+ android:name="io.flutter.embedding.android.NormalTheme"
+ android:resource="@style/NormalTheme"
+ />
-
-
+
+
-
-
+
+
+
-
+
\ No newline at end of file
diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift
index 70693e4..5428a20 100644
--- a/ios/Runner/AppDelegate.swift
+++ b/ios/Runner/AppDelegate.swift
@@ -1,5 +1,6 @@
import UIKit
import Flutter
+import GoogleMaps
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
@@ -7,7 +8,8 @@ import Flutter
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
+ GMSServices.provideAPIKey("AIzaSyBCjMCEAyyNVpsnVYvZj6VL1mmB98Vd6AE")
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
-}
+}
\ No newline at end of file
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 1940470..dc16cac 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -2,6 +2,13 @@
+ UIBackgroundModes
+
+ fetch
+ remote-notification
+
+ NSPhotoLibraryUsageDescription
+ The Photo library is used when selecting a photo to upload as media for a Session
CADisableMinimumFrameDurationOnPhone
CFBundleDevelopmentRegion
diff --git a/lib/main.dart b/lib/main.dart
index 98150a1..f9d2d90 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -6,16 +6,31 @@ import 'package:sendtrain/screens/activities_screen.dart';
import 'package:sendtrain/screens/sessions_screen.dart';
// ignore: unused_import
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 {
const SendTrain({super.key});
@override
Widget build(BuildContext context) {
+ final ThemeData themeData = ThemeData.dark(useMaterial3: true);
return MaterialApp(
title: "Sendtrain",
- theme: ThemeData.dark(useMaterial3: true),
+ theme: themeData.copyWith(
+ filledButtonTheme: FilledButtonThemeData(
+ style: FilledButton.styleFrom(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12),
+ ),
+ ),
+ ),
+ floatingActionButtonTheme: FloatingActionButtonThemeData(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12))),
+ inputDecorationTheme: InputDecorationTheme(
+ fillColor: themeData.colorScheme.surface,
+ ),
+ ),
home: const App());
}
}
@@ -84,16 +99,18 @@ class _AppState extends State {
]),
floatingActionButton: FloatingActionButton.extended(
onPressed: () {
- showAdaptiveDialog(
- barrierColor: Colors.black.withOpacity(0.5),
- builder: (BuildContext context) => SessionCreator(),
- barrierDismissible: true,
- barrierLabel: '',
- context: context);
+ showModalBottomSheet(
+ context: context,
+ showDragHandle: true,
+ isScrollControlled: true,
+ useSafeArea: true,
+ builder: (BuildContext context) {
+ return SessionCreator();
+ });
},
label: const Text('New Session'),
icon: const Icon(Icons.add_chart),
- backgroundColor: Colors.deepPurple,
+ // backgroundColor: Colors.deepPurple,
));
}
}
diff --git a/lib/widgets/activities_header.dart b/lib/widgets/activities/activities_header.dart
similarity index 95%
rename from lib/widgets/activities_header.dart
rename to lib/widgets/activities/activities_header.dart
index a5be690..6111f3b 100644
--- a/lib/widgets/activities_header.dart
+++ b/lib/widgets/activities/activities_header.dart
@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
-import '../widgets/activity_type_filter.dart';
+import 'activity_type_filter.dart';
class ActivitiesHeader extends StatefulWidget {
const ActivitiesHeader({super.key});
diff --git a/lib/widgets/activity_action_view.dart b/lib/widgets/activities/activity_action_view.dart
similarity index 100%
rename from lib/widgets/activity_action_view.dart
rename to lib/widgets/activities/activity_action_view.dart
diff --git a/lib/widgets/activity_card.dart b/lib/widgets/activities/activity_card.dart
similarity index 99%
rename from lib/widgets/activity_card.dart
rename to lib/widgets/activities/activity_card.dart
index f89fc2a..9669645 100644
--- a/lib/widgets/activity_card.dart
+++ b/lib/widgets/activities/activity_card.dart
@@ -5,7 +5,7 @@ import 'package:sendtrain/daos/media_items_dao.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/extensions/string_extensions.dart';
import 'package:sendtrain/models/activity_timer_model.dart';
-import 'package:sendtrain/widgets/activity_view.dart';
+import 'package:sendtrain/widgets/activities/activity_view.dart';
class ActivityCard extends StatefulWidget {
final Activity activity;
diff --git a/lib/widgets/activity_type_filter.dart b/lib/widgets/activities/activity_type_filter.dart
similarity index 100%
rename from lib/widgets/activity_type_filter.dart
rename to lib/widgets/activities/activity_type_filter.dart
diff --git a/lib/widgets/activity_view.dart b/lib/widgets/activities/activity_view.dart
similarity index 97%
rename from lib/widgets/activity_view.dart
rename to lib/widgets/activities/activity_view.dart
index c6d68da..b4d2340 100644
--- a/lib/widgets/activity_view.dart
+++ b/lib/widgets/activities/activity_view.dart
@@ -5,10 +5,10 @@ import 'package:sendtrain/daos/actions_dao.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/extensions/string_extensions.dart';
import 'package:sendtrain/models/activity_timer_model.dart';
-import 'package:sendtrain/widgets/activity_action_view.dart';
-import 'package:sendtrain/widgets/activity_view_categories.dart';
-import 'package:sendtrain/widgets/activity_view_media.dart';
-import 'package:sendtrain/widgets/activity_view_types.dart';
+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(
diff --git a/lib/widgets/activity_view_categories.dart b/lib/widgets/activities/activity_view_categories.dart
similarity index 100%
rename from lib/widgets/activity_view_categories.dart
rename to lib/widgets/activities/activity_view_categories.dart
diff --git a/lib/widgets/activity_view_media.dart b/lib/widgets/activities/activity_view_media.dart
similarity index 100%
rename from lib/widgets/activity_view_media.dart
rename to lib/widgets/activities/activity_view_media.dart
diff --git a/lib/widgets/activity_view_types.dart b/lib/widgets/activities/activity_view_types.dart
similarity index 100%
rename from lib/widgets/activity_view_types.dart
rename to lib/widgets/activities/activity_view_types.dart
diff --git a/lib/widgets/builders/dialogs.dart b/lib/widgets/builders/dialogs.dart
new file mode 100644
index 0000000..91bfadd
--- /dev/null
+++ b/lib/widgets/builders/dialogs.dart
@@ -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 animation,
+ Animation secondaryAnimation, Widget child) {
+ Animation custom = Tween(
+ 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: [
+ TextButton(
+ onPressed: () => {
+ Navigator.pop(context, 'Cancel'),
+ },
+ child: const Text('Cancel'),
+ ),
+ TextButton(
+ onPressed: () => {dao.remove(session), Navigator.pop(context, 'OK')},
+ child: const Text('OK'),
+ ),
+ ],
+ ),
+ );
+}
diff --git a/lib/widgets/generic/elements/form_search_input.dart b/lib/widgets/generic/elements/form_search_input.dart
new file mode 100644
index 0000000..8741d08
--- /dev/null
+++ b/lib/widgets/generic/elements/form_search_input.dart
@@ -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 createState() => _FormSearchInputState();
+}
+
+class _FormSearchInputState extends State {
+ String? _currentQuery;
+
+ // The most recent suggestions received from the API.
+ late Iterable _lastOptions = [];
+
+ late final _Debounceable?, String> _debouncedSearch;
+
+ // Calls the "remote" API to search with the given query. Returns null when
+ // the call has been made obsolete.
+ Future?> _search(String query) async {
+ _currentQuery = query;
+
+ // In a real application, there should be some error handling here.
+ final Iterable 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?, 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? options =
+ (await _debouncedSearch(controller.text))?.toList();
+ if (options == null) {
+ return _lastOptions;
+ }
+ _lastOptions = List.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 _kOptions = [
+ 'aardvark',
+ 'bobcat',
+ 'chameleon',
+ ];
+
+ // Searches the options, but injects a fake "network" delay.
+ static Future> search(String query) async {
+ await Future.delayed(fakeAPIDuration); // Fake 1 second delay.
+ if (query == '') {
+ return const Iterable.empty();
+ }
+ return _kOptions.where((String option) {
+ return option.contains(query.toLowerCase());
+ });
+ }
+}
+
+typedef _Debounceable = Future 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 _debounce(_Debounceable 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 _completer = Completer();
+
+ void _onComplete() {
+ _completer.complete();
+ }
+
+ Future 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();
+}
diff --git a/lib/widgets/generic/elements/form_text_input.dart b/lib/widgets/generic/elements/form_text_input.dart
new file mode 100644
index 0000000..6ad9b05
--- /dev/null
+++ b/lib/widgets/generic/elements/form_text_input.dart
@@ -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();
+ }
+ }));
+ }
+}
diff --git a/lib/widgets/media_card.dart b/lib/widgets/media/media_card.dart
similarity index 100%
rename from lib/widgets/media_card.dart
rename to lib/widgets/media/media_card.dart
diff --git a/lib/screens/activities_screen.dart b/lib/widgets/screens/activities_screen.dart
similarity index 94%
rename from lib/screens/activities_screen.dart
rename to lib/widgets/screens/activities_screen.dart
index b4fa536..3a616d9 100644
--- a/lib/screens/activities_screen.dart
+++ b/lib/widgets/screens/activities_screen.dart
@@ -3,8 +3,8 @@ import 'package:sendtrain/classes/activity_action.dart';
import 'package:sendtrain/database/database.dart' hide ActivityAction;
import 'package:sendtrain/models/activity_model.dart';
-import '../widgets/activities_header.dart';
-import '../widgets/activity_card.dart';
+import '../widgets/activities/activities_header.dart';
+import '../widgets/activities/activity_card.dart';
class ActivitiesScreen extends StatefulWidget {
const ActivitiesScreen({super.key});
diff --git a/lib/screens/sessions_screen.dart b/lib/widgets/screens/sessions_screen.dart
similarity index 98%
rename from lib/screens/sessions_screen.dart
rename to lib/widgets/screens/sessions_screen.dart
index 81dec32..0692b41 100644
--- a/lib/screens/sessions_screen.dart
+++ b/lib/widgets/screens/sessions_screen.dart
@@ -4,7 +4,7 @@ import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:sendtrain/daos/sessions_dao.dart';
import 'package:sendtrain/database/database.dart';
-import '../widgets/session_card.dart';
+import '../widgets/sessions/session_card.dart';
import 'package:collection/collection.dart';
class SessionsScreen extends StatefulWidget {
diff --git a/lib/widgets/session_creator.dart b/lib/widgets/session_creator.dart
deleted file mode 100644
index e6842a6..0000000
--- a/lib/widgets/session_creator.dart
+++ /dev/null
@@ -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? data;
-
- @override
- State createState() => _SessionCreatorState();
-}
-
-class _SessionCreatorState extends State {
- final GlobalKey _formKey = GlobalKey();
-
- final Map sessionCreateController = {
- 'name': TextEditingController(),
- 'content': TextEditingController(),
- 'status': TextEditingController(),
- 'date': TextEditingController(),
- };
-
- Future createSession(context) {
- return SessionsDao(Provider.of(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: [
- 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(
- 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 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'))
- ])
- ],
- ))
- ]);
- }
-}
diff --git a/lib/widgets/session_card.dart b/lib/widgets/sessions/session_card.dart
similarity index 61%
rename from lib/widgets/session_card.dart
rename to lib/widgets/sessions/session_card.dart
index 61d3137..14f5c60 100644
--- a/lib/widgets/session_card.dart
+++ b/lib/widgets/sessions/session_card.dart
@@ -6,7 +6,7 @@ import 'package:sendtrain/daos/media_items_dao.dart';
import 'package:sendtrain/daos/sessions_dao.dart';
import 'package:sendtrain/database/database.dart' hide ActivityAction;
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 {
final int type;
@@ -44,28 +44,7 @@ class _SessionCardState extends State {
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 animation,
- Animation secondaryAnimation,
- Widget child) {
- Animation custom = Tween(
- 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();
- }),
+ onTap: () => showSessionDialog(session, context),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
@@ -94,35 +73,17 @@ class _SessionCardState extends State {
alignment: Alignment.topCenter,
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: [
- TextButton(
- onPressed: () => {
- Navigator.pop(context, 'Cancel'),
- },
- child: const Text('Cancel'),
- ),
- TextButton(
- onPressed: () => {
- SessionsDao(Provider.of(
- context,
- listen: false))
- .remove(session)
- .then((result) {
- setState(() {});
- }),
- Navigator.pop(context, 'OK')
- },
- child: const Text('OK'),
- ),
- ],
- ),
- );
+ showRemovalDialog(
+ 'Session Removal',
+ 'Would you like to permanently remove this session?',
+ context,
+ SessionsDao(Provider.of(
+ context,
+ listen: false)),
+ session)
+ .then((result) {
+ setState(() {});
+ });
},
),
),
@@ -147,28 +108,7 @@ class _SessionCardState extends State {
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 animation,
- Animation secondaryAnimation,
- Widget child) {
- Animation custom = Tween(
- 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();
- }),
+ onTap: () => showSessionDialog(session, context),
child: Container(
decoration: BoxDecoration(
// color: const Color.fromARGB(47, 0, 0, 0),
diff --git a/lib/widgets/sessions/session_creator.dart b/lib/widgets/sessions/session_creator.dart
new file mode 100644
index 0000000..eb8fc61
--- /dev/null
+++ b/lib/widgets/sessions/session_creator.dart
@@ -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? data;
+
+ @override
+ State createState() => _SessionCreatorState();
+}
+
+class _SessionCreatorState extends State {
+ final GlobalKey _formKey = GlobalKey();
+
+ final Map sessionCreateController = {
+ 'name': TextEditingController(),
+ 'content': TextEditingController(),
+ 'status': TextEditingController(),
+ 'date': TextEditingController(),
+ 'address': TextEditingController(),
+ 'media': TextEditingController(),
+ };
+
+ Future createSession(context) {
+ return SessionsDao(Provider.of(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(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: [
+ 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 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')))
+ ]),
+ ],
+ )));
+ }
+}
diff --git a/lib/widgets/session_view.dart b/lib/widgets/sessions/session_view.dart
similarity index 95%
rename from lib/widgets/session_view.dart
rename to lib/widgets/sessions/session_view.dart
index f1bce4d..8c1c285 100644
--- a/lib/widgets/session_view.dart
+++ b/lib/widgets/sessions/session_view.dart
@@ -7,9 +7,9 @@ import 'package:provider/provider.dart';
import 'package:sendtrain/daos/activities_dao.dart';
import 'package:sendtrain/database/database.dart';
import 'package:sendtrain/extensions/string_extensions.dart';
-import 'package:sendtrain/widgets/session_view_achievements.dart';
-import 'package:sendtrain/widgets/session_view_activities.dart';
-import 'package:sendtrain/widgets/session_view_media.dart';
+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});
diff --git a/lib/widgets/session_view_achievements.dart b/lib/widgets/sessions/session_view_achievements.dart
similarity index 100%
rename from lib/widgets/session_view_achievements.dart
rename to lib/widgets/sessions/session_view_achievements.dart
diff --git a/lib/widgets/session_view_activities.dart b/lib/widgets/sessions/session_view_activities.dart
similarity index 90%
rename from lib/widgets/session_view_activities.dart
rename to lib/widgets/sessions/session_view_activities.dart
index 48a2607..8a673b9 100644
--- a/lib/widgets/session_view_activities.dart
+++ b/lib/widgets/sessions/session_view_activities.dart
@@ -1,6 +1,6 @@
import 'package:flutter/material.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 {
const SessionViewActivities({super.key, required this.activities });
diff --git a/lib/widgets/session_view_media.dart b/lib/widgets/sessions/session_view_media.dart
similarity index 100%
rename from lib/widgets/session_view_media.dart
rename to lib/widgets/sessions/session_view_media.dart
diff --git a/pubspec.yaml b/pubspec.yaml
index 26710c2..62aacff 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -44,6 +44,8 @@ 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
flutter_launcher_name:
name: "SendTrain"