From 722a15213059b0058a1416f0d901fb02feb4c68f Mon Sep 17 00:00:00 2001 From: Joshua Burman Date: Fri, 27 Dec 2024 15:59:09 -0500 Subject: [PATCH] big refactor #1 --- android/app/proguard-rules.pro | 1 + android/app/src/main/AndroidManifest.xml | 21 +- ios/Runner/AppDelegate.swift | 4 +- ios/Runner/Info.plist | 7 + lib/main.dart | 35 +++- .../{ => activities}/activities_header.dart | 2 +- .../activity_action_view.dart | 0 .../{ => activities}/activity_card.dart | 2 +- .../activity_type_filter.dart | 0 .../{ => activities}/activity_view.dart | 8 +- .../activity_view_categories.dart | 0 .../{ => activities}/activity_view_media.dart | 0 .../{ => activities}/activity_view_types.dart | 0 lib/widgets/builders/dialogs.dart | 46 +++++ .../generic/elements/form_search_input.dart | 156 +++++++++++++++ .../generic/elements/form_text_input.dart | 52 +++++ lib/widgets/{ => media}/media_card.dart | 0 .../screens/activities_screen.dart | 4 +- .../screens/sessions_screen.dart | 2 +- lib/widgets/session_creator.dart | 187 ------------------ lib/widgets/{ => sessions}/session_card.dart | 88 ++------- lib/widgets/sessions/session_creator.dart | 141 +++++++++++++ lib/widgets/{ => sessions}/session_view.dart | 6 +- .../session_view_achievements.dart | 0 .../session_view_activities.dart | 2 +- .../{ => sessions}/session_view_media.dart | 0 pubspec.yaml | 2 + 27 files changed, 474 insertions(+), 292 deletions(-) create mode 100644 android/app/proguard-rules.pro rename lib/widgets/{ => activities}/activities_header.dart (95%) rename lib/widgets/{ => activities}/activity_action_view.dart (100%) rename lib/widgets/{ => activities}/activity_card.dart (99%) rename lib/widgets/{ => activities}/activity_type_filter.dart (100%) rename lib/widgets/{ => activities}/activity_view.dart (97%) rename lib/widgets/{ => activities}/activity_view_categories.dart (100%) rename lib/widgets/{ => activities}/activity_view_media.dart (100%) rename lib/widgets/{ => activities}/activity_view_types.dart (100%) create mode 100644 lib/widgets/builders/dialogs.dart create mode 100644 lib/widgets/generic/elements/form_search_input.dart create mode 100644 lib/widgets/generic/elements/form_text_input.dart rename lib/widgets/{ => media}/media_card.dart (100%) rename lib/{ => widgets}/screens/activities_screen.dart (94%) rename lib/{ => widgets}/screens/sessions_screen.dart (98%) delete mode 100644 lib/widgets/session_creator.dart rename lib/widgets/{ => sessions}/session_card.dart (61%) create mode 100644 lib/widgets/sessions/session_creator.dart rename lib/widgets/{ => sessions}/session_view.dart (95%) rename lib/widgets/{ => sessions}/session_view_achievements.dart (100%) rename lib/widgets/{ => sessions}/session_view_activities.dart (90%) rename lib/widgets/{ => sessions}/session_view_media.dart (100%) 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"