diff --git a/lib/helpers/date_helpers.dart b/lib/helpers/date_helpers.dart deleted file mode 100644 index 8bea8b0..0000000 --- a/lib/helpers/date_helpers.dart +++ /dev/null @@ -1,3 +0,0 @@ -import 'package:intl/intl.dart'; - -final DateFormat dateFormat = DateFormat('yyyy-MM-dd'); \ No newline at end of file diff --git a/lib/helpers/date_time_helpers.dart b/lib/helpers/date_time_helpers.dart new file mode 100644 index 0000000..14422f3 --- /dev/null +++ b/lib/helpers/date_time_helpers.dart @@ -0,0 +1,11 @@ +import 'package:intl/intl.dart'; + +final DateFormat dateFormat = DateFormat('yyyy-MM-dd'); + +String formattedTime(int timeInSecond) { + int sec = timeInSecond % 60; + int min = (timeInSecond / 60).floor(); + String minute = min.toString().length <= 1 ? "0$min" : "$min"; + String second = sec.toString().length <= 1 ? "0$sec" : "$sec"; + return "$minute:$second"; +} diff --git a/lib/main.dart b/lib/main.dart index 4233cf7..ea69153 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -100,6 +100,9 @@ class _AppState extends State { floatingActionButton: FloatingActionButton.extended( onPressed: () { showModalBottomSheet( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10.0)), + ), context: context, showDragHandle: true, isScrollControlled: true, diff --git a/lib/widgets/activities/activity_card.dart b/lib/widgets/activities/activity_card.dart index 1eef5fe..c70a941 100644 --- a/lib/widgets/activities/activity_card.dart +++ b/lib/widgets/activities/activity_card.dart @@ -4,6 +4,7 @@ import 'package:sendtrain/daos/activities_dao.dart'; import 'package:sendtrain/daos/media_items_dao.dart'; import 'package:sendtrain/database/database.dart'; import 'package:sendtrain/extensions/string_extensions.dart'; +import 'package:sendtrain/helpers/date_time_helpers.dart'; import 'package:sendtrain/helpers/media_helpers.dart'; import 'package:sendtrain/models/activity_timer_model.dart'; import 'package:sendtrain/widgets/activities/activity_view.dart'; @@ -21,18 +22,10 @@ class ActivityCard extends StatefulWidget { } class ActivityCardState extends State { - String formattedTime(int timeInSecond) { - int sec = timeInSecond % 60; - int min = (timeInSecond / 60).floor(); - String minute = min.toString().length <= 1 ? "0$min" : "$min"; - String second = sec.toString().length <= 1 ? "0$sec" : "$sec"; - return "$minute:$second"; - } - @override Widget build(BuildContext context) { final ActivityTimerModel atm = - Provider.of(context, listen: false); + Provider.of(context); return FutureBuilder>( future: MediaItemsDao(Provider.of(context)) diff --git a/lib/widgets/builders/dialogs.dart b/lib/widgets/builders/dialogs.dart index d2b3bc9..be18107 100644 --- a/lib/widgets/builders/dialogs.dart +++ b/lib/widgets/builders/dialogs.dart @@ -20,7 +20,8 @@ Future showGenericDialog(dynamic object, BuildContext parentContext) { }); } -Future showRemovalDialog(title, content, context, dao, session) { +Future showRemovalDialog(String title, String content, BuildContext context, + dynamic dao, dynamic object) { return showAdaptiveDialog( context: context, builder: (BuildContext context) => AlertDialog( @@ -34,7 +35,7 @@ Future showRemovalDialog(title, content, context, dao, session) { child: const Text('Cancel'), ), TextButton( - onPressed: () => {dao.remove(session), Navigator.pop(context, 'OK')}, + onPressed: () => {dao.remove(object), Navigator.pop(context, 'OK')}, child: const Text('OK'), ), ], diff --git a/lib/widgets/generic/elements/card_image.dart b/lib/widgets/generic/elements/card_image.dart index 1b050c7..6eb6518 100644 --- a/lib/widgets/generic/elements/card_image.dart +++ b/lib/widgets/generic/elements/card_image.dart @@ -1,16 +1,22 @@ import 'package:flutter/material.dart'; +enum SizeAxis { width, height } + class CardImage extends StatelessWidget { - const CardImage({super.key, required this.image}); + const CardImage({super.key, required this.image, this.padding, this.size}); final ImageProvider image; + final EdgeInsets? padding; + + final Map? size; @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.fromLTRB(0, 0, 0, 0), + padding: padding ?? const EdgeInsets.fromLTRB(0, 0, 0, 0), child: Container( - width: 60, + width: size?[SizeAxis.width] ?? 60, + height: size?[SizeAxis.height] ?? 60, decoration: BoxDecoration( image: DecorationImage( fit: BoxFit.cover, diff --git a/lib/widgets/generic/elements/form_search_input.dart b/lib/widgets/generic/elements/form_search_input.dart index 8741d08..b8f9f38 100644 --- a/lib/widgets/generic/elements/form_search_input.dart +++ b/lib/widgets/generic/elements/form_search_input.dart @@ -1,7 +1,29 @@ import 'dart:async'; +import 'dart:convert'; import 'package:flutter/material.dart'; +import 'package:http/http.dart'; import 'package:sendtrain/widgets/generic/elements/form_text_input.dart'; +import 'package:uuid/uuid.dart'; + +class Suggestion { + final String placeId; + final String description; + final String address; + + Suggestion(this.placeId, this.description, this.address); + + @override + String toString() { + return 'Suggestion(description: $description, placeId: $placeId)'; + } + + Map toJson() => { + 'placeId': placeId, + 'name': description, + 'address': address, + }; +} class FormSearchInput extends StatefulWidget { const FormSearchInput({super.key, required this.sessionController}); @@ -26,15 +48,55 @@ class _FormSearchInputState extends State { _currentQuery = query; // In a real application, there should be some error handling here. - final Iterable options = await _FakeAPI.search(_currentQuery!); + // final Iterable options = await _FakeAPI.search(_currentQuery!); + if (query.isNotEmpty) { + final List? suggestions = + await fetchSuggestions(_currentQuery!, 'en'); - // If another search happened after this one, throw away these options. - if (_currentQuery != query) { + // If another search happened after this one, throw away these options. + if (_currentQuery != query) { + return null; + } + _currentQuery = null; + + return suggestions?.map((suggestion) => json.encode(suggestion)); + } else { return null; } - _currentQuery = null; + } - return options; + final sessionToken = Uuid().v4(); + final apiKey = "AIzaSyBCjMCEAyyNVpsnVYvZj6VL1mmB98Vd6AE"; + final client = Client(); + + Future?> fetchSuggestions(String input, String lang) async { + var headers = { + 'Content-Type': 'application/json', + 'X-Goog-Api-Key': apiKey, + "Access-Control-Allow-Origin": "*", + 'X-Goog-FieldMask': 'places.displayName,places.id,places.formattedAddress' + }; + var request = Request('POST', + Uri.parse('https://places.googleapis.com/v1/places:searchText')); + request.body = json.encode({"textQuery": input}); + request.headers.addAll(headers); + + StreamedResponse response = await request.send(); + + if (response.statusCode == 200) { + final result = json.decode(await response.stream.bytesToString()); + + if (result.isNotEmpty) { + return result['places'] + .map((p) => Suggestion( + p['id'], p['displayName']['text'], p['formattedAddress'])) + .toList(); + } else { + return null; + } + } else { + throw Exception(response.reasonPhrase); + } } @override @@ -46,64 +108,44 @@ class _FormSearchInputState extends State { @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(); - }, - ); + 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(json.decode(item)['name']), + onTap: () { + if (item.isNotEmpty) { + widget.sessionController.text = json.decode(item)['name']; + } + client.close(); + 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()); + return _lastOptions; }); } } +const Duration debounceDuration = Duration(milliseconds: 500); typedef _Debounceable = Future Function(T parameter); /// Returns a new function that is a debounced version of the given function. diff --git a/lib/widgets/media/media_card.dart b/lib/widgets/media/media_card.dart index a8f00f8..1126bd6 100644 --- a/lib/widgets/media/media_card.dart +++ b/lib/widgets/media/media_card.dart @@ -50,42 +50,42 @@ class MediaCard extends StatelessWidget { RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), shadowColor: const Color.fromARGB(0, 255, 255, 255), child: TextButton( - onPressed: () => showDialog( - context: context, - builder: (BuildContext context) => Dialog.fullscreen( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - mediaItem(media), - const SizedBox(height: 15), - Text( - media.description, - style: const TextStyle(fontSize: 20), - ), - const Divider( - indent: 20, - endIndent: 20, - color: Colors.deepPurple, - ), - // const Text( - // 'Comments', - // style: TextStyle(fontSize: 20), - // ), - const SizedBox(height: 15), - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: const Text('Close'), - ), - ], - ), - ), - ), + onPressed: () => showModalBottomSheet( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10.0)), ), + context: context, + showDragHandle: true, + isScrollControlled: true, + useSafeArea: true, + builder: (BuildContext context) { + return Padding( + padding: EdgeInsets.fromLTRB(15, 0, 15, 15), + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + ClipRRect( + borderRadius: BorderRadius.circular(10), + child: mediaItem(media), + ), + const SizedBox(height: 15), + Padding( + padding: EdgeInsets.fromLTRB(15, 0, 15, 15), + child: Text( + media.description, + style: const TextStyle(fontSize: 20), + )), + const Divider( + indent: 20, + endIndent: 20, + ) + ])); + // const Text( + // 'Comments', + // style: TextStyle(fontSize: 20), + // ), + }), child: const ListTile( title: Text(''), )))); diff --git a/lib/widgets/sessions/session_card_full.dart b/lib/widgets/sessions/session_card_full.dart index a35e614..eb80ae2 100644 --- a/lib/widgets/sessions/session_card_full.dart +++ b/lib/widgets/sessions/session_card_full.dart @@ -3,7 +3,7 @@ import 'package:provider/provider.dart'; import 'package:sendtrain/daos/sessions_dao.dart'; import 'package:sendtrain/database/database.dart'; import 'package:sendtrain/extensions/string_extensions.dart'; -import 'package:sendtrain/helpers/date_helpers.dart'; +import 'package:sendtrain/helpers/date_time_helpers.dart'; import 'package:sendtrain/helpers/media_helpers.dart'; import 'package:sendtrain/widgets/builders/dialogs.dart'; import 'package:sendtrain/widgets/generic/elements/card_content.dart'; @@ -22,6 +22,15 @@ class SessionCardFull extends StatefulWidget { } class _SessionCardFullState extends State { + + String sessionTitle(Session session) { + String title = session.title.toTitleCase(); + + if (session.address != null) title = "$title @ ${session.address}"; + + return title; + } + @override Widget build(BuildContext context) { final Session session = widget.session; @@ -42,8 +51,10 @@ class _SessionCardFullState extends State { children: [ ListTile( contentPadding: EdgeInsets.only(left: 8), - leading: CardImage(image: findMediaByType(mediaItems, 'image')), - title: Text(maxLines: 1, session.title.toTitleCase()), + leading: CardImage( + image: findMediaByType(mediaItems, 'image'), + padding: EdgeInsets.only(left: 5, top: 5)), + title: Text(maxLines: 1, sessionTitle(session)), subtitle: Text( maxLines: 1, dateFormat.format(session.date as DateTime)), trailing: IconButton( diff --git a/lib/widgets/sessions/session_card_small.dart b/lib/widgets/sessions/session_card_small.dart index 447f691..3609a93 100644 --- a/lib/widgets/sessions/session_card_small.dart +++ b/lib/widgets/sessions/session_card_small.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:sendtrain/database/database.dart'; import 'package:sendtrain/extensions/string_extensions.dart'; -import 'package:sendtrain/helpers/date_helpers.dart'; +import 'package:sendtrain/helpers/date_time_helpers.dart'; import 'package:sendtrain/helpers/media_helpers.dart'; import 'package:sendtrain/widgets/builders/dialogs.dart'; import 'package:sendtrain/widgets/sessions/session_view.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 62aacff..9a25cb3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,6 +46,8 @@ dependencies: drift_flutter: ^0.2.2 map_location_picker: ^1.2.7 file_picker: ^8.1.7 + http: ^1.2.2 + uuid: ^4.5.1 flutter_launcher_name: name: "SendTrain"