diff --git a/lib/extensions/snackbar.dart b/lib/extensions/snackbar.dart index 26ad3ed..a261b46 100644 --- a/lib/extensions/snackbar.dart +++ b/lib/extensions/snackbar.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:quid_faciam_hodie/constants/values.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; @@ -9,12 +10,17 @@ extension ShowSnackBar on BuildContext { static ScaffoldFeatureController? pendingSnackBar; - ScaffoldFeatureController showSnackBar({ + ScaffoldFeatureController? showSnackBar({ required final String message, final Color backgroundColor = Colors.white, final Duration duration = const Duration(seconds: 4), final BuildContext? context, }) { + if (!isMaterial(context ?? this)) { + // Not implemented yet + return null; + } + pendingSnackBar?.close(); pendingSnackBar = null; diff --git a/lib/foreign_types/memory.dart b/lib/foreign_types/memory.dart index 943eb19..a747dcc 100644 --- a/lib/foreign_types/memory.dart +++ b/lib/foreign_types/memory.dart @@ -12,6 +12,7 @@ class Memory { final String filePath; final bool isPublic; final String userID; + final String annotation; final MemoryLocation? location; const Memory({ @@ -20,6 +21,7 @@ class Memory { required this.filePath, required this.isPublic, required this.userID, + required this.annotation, this.location, }); @@ -29,6 +31,7 @@ class Memory { filePath: jsonData['location'], isPublic: jsonData['is_public'], userID: jsonData['user_id'], + annotation: jsonData['annotation'], location: MemoryLocation.parse(jsonData), ); diff --git a/lib/locale/l10n/app_en.arb b/lib/locale/l10n/app_en.arb index 4c0ed0b..7864e7a 100644 --- a/lib/locale/l10n/app_en.arb +++ b/lib/locale/l10n/app_en.arb @@ -4,6 +4,7 @@ "generalError": "There was an error", "generalCancelButtonLabel": "Cancel", "generalContinueButtonLabel": "Continue", + "generalSaveButtonLabel": "Save", "generalUnderstoodButtonLabel": "OK", "generalLoadingLabel": "Loading...", @@ -52,6 +53,10 @@ "mainScreenHelpSheetTakePhotoExplanation": "Tap on the shutter button once to take a photo.", "mainScreenHelpSheetTakeVideoExplanation": "Hold down the shutter button to start recording a video. Leave the button to stop recording.", + "mainScreenAnnotationDialogTitle": "Add an annotation", + "mainScreenAnnotationDialogExplanation": "You can add an annotation to your memory", + "mainScreenAnnotationDialogAnnotationFieldLabel": "Annotation", + "recordingOverlayIsRecording": "Recording", @@ -151,6 +156,7 @@ "settingsScreenDeleteAccountConfirmLabel": "Delete Account now", "settingsScreenGeneralSectionTitle": "General", "settingsScreenGeneralSectionQualityLabel": "Quality", + "settingsScreenGeneralSectionAskForMemoryAnnotationsLabel": "Ask for memory annotations", "settingsScreenResetHelpSheetsLabel": "Reset Help Sheets", "settingsScreenResetHelpSheetsResetSuccessfully": "Help Sheets reset successfully.", diff --git a/lib/managers/file_manager.dart b/lib/managers/file_manager.dart index a2b90fa..3968a64 100644 --- a/lib/managers/file_manager.dart +++ b/lib/managers/file_manager.dart @@ -36,7 +36,8 @@ class FileManager { static uploadFile( final User user, final File file, { - LocationData? locationData, + final LocationData? locationData, + final Future? annotationGetterFuture, }) async { await GlobalValuesManager.waitForInitialization(); @@ -65,6 +66,15 @@ class FileManager { data['location_heading'] = locationData.heading!; } + if (annotationGetterFuture != null) { + final annotation = await annotationGetterFuture; + + if (annotation != null) { + // User has specified annotation + data['annotation'] = annotation; + } + } + final memoryResponse = await supabase.from('memories').insert(data).execute(); diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 407e3d8..268f35c 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:camera/camera.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:quid_faciam_hodie/constants/storage_keys.dart'; @@ -9,14 +10,20 @@ const secure = FlutterSecureStorage(); class Settings extends ChangeNotifier { ResolutionPreset _resolution = ResolutionPreset.max; + bool _askForMemoryAnnotations = false; - Settings({final ResolutionPreset resolution = ResolutionPreset.max}) - : _resolution = resolution; + Settings({ + final ResolutionPreset? resolution, + final bool? askForMemoryAnnotations, + }) : _resolution = resolution ?? ResolutionPreset.max, + _askForMemoryAnnotations = askForMemoryAnnotations ?? true; ResolutionPreset get resolution => _resolution; + bool get askForMemoryAnnotations => _askForMemoryAnnotations; Map toJSONData() => { 'resolution': _resolution.toString(), + 'askForMemoryAnnotations': _askForMemoryAnnotations ? 'true' : 'false', }; Future save() async { @@ -36,11 +43,23 @@ class Settings extends ChangeNotifier { } final data = jsonDecode(rawData); - final resolution = ResolutionPreset.values.firstWhere( + final resolution = ResolutionPreset.values.firstWhereOrNull( (preset) => preset.toString() == data['resolution'], ); + final askForMemoryAnnotations = () { + switch (data['askForMemoryAnnotations']) { + case 'true': + return true; + case 'false': + return false; + default: + return null; + } + }(); + return Settings( resolution: resolution, + askForMemoryAnnotations: askForMemoryAnnotations, ); } @@ -49,4 +68,10 @@ class Settings extends ChangeNotifier { notifyListeners(); save(); } + + void setAskForMemoryAnnotations(final bool askForMemoryAnnotations) { + _askForMemoryAnnotations = askForMemoryAnnotations; + notifyListeners(); + save(); + } } diff --git a/lib/screens/login_screen.dart b/lib/screens/login_screen.dart index 2cea031..6a1b44c 100644 --- a/lib/screens/login_screen.dart +++ b/lib/screens/login_screen.dart @@ -82,10 +82,9 @@ class _LoginScreenState extends AuthState with Loadable { await _signUp(); } catch (error) { if (mounted) { - if (isMaterial(context)) - context.showLongErrorSnackBar( - message: localizations.loginScreenLoginFailed, - ); + context.showLongErrorSnackBar( + message: localizations.loginScreenLoginFailed, + ); passwordController.clear(); } diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart index 8a3a88c..7febdb2 100644 --- a/lib/screens/main_screen.dart +++ b/lib/screens/main_screen.dart @@ -17,6 +17,7 @@ import 'package:quid_faciam_hodie/constants/values.dart'; import 'package:quid_faciam_hodie/extensions/snackbar.dart'; import 'package:quid_faciam_hodie/managers/file_manager.dart'; import 'package:quid_faciam_hodie/managers/global_values_manager.dart'; +import 'package:quid_faciam_hodie/screens/main_screen/annotation_dialog.dart'; import 'package:quid_faciam_hodie/screens/main_screen/camera_help_content.dart'; import 'package:quid_faciam_hodie/screens/main_screen/settings_button_overlay.dart'; import 'package:quid_faciam_hodie/utils/auth_required.dart'; @@ -188,6 +189,62 @@ class _MainScreenState extends AuthRequiredState with Loadable { }); } + Future _createAskAnnotationDialog() => showPlatformDialog( + barrierDismissible: true, + context: context, + builder: (dialogContext) => const AnnotationDialog(), + ); + + void _lockCamera() => setState(() { + lockCamera = true; + }); + + void _releaseCamera() => setState(() { + lockCamera = false; + }); + + void _showUploadingPhotoAnimation(final File file) => setState(() { + uploadingPhotoAnimation = file.readAsBytesSync(); + }); + + void _releaseUploadingPhotoAnimation() => setState(() { + uploadingPhotoAnimation = null; + }); + + Future getAnnotation() async { + final settings = GlobalValuesManager.settings!; + + if (settings.askForMemoryAnnotations) { + return _createAskAnnotationDialog(); + } else { + return ''; + } + } + + Future setFlashModeBeforeApplyingAction() async { + if (isTorchEnabled) { + await controller!.setFlashMode(FlashMode.torch); + } else { + await controller!.setFlashMode(FlashMode.off); + } + } + + Future getLocation([ + final File? fileToTag, + ]) async { + if (!(await Permission.location.isGranted)) { + return null; + } + + final locationData = await Location().getLocation(); + + if (fileToTag != null && Platform.isAndroid) { + await tagLocationToImage(fileToTag, locationData); + } + + return locationData; + } + Future takePhoto() async { final localizations = AppLocalizations.of(context)!; @@ -195,109 +252,94 @@ class _MainScreenState extends AuthRequiredState with Loadable { return; } - setState(() { - lockCamera = true; - }); + _lockCamera(); try { - if (isMaterial(context)) - context.showPendingSnackBar( - message: localizations.mainScreenTakePhotoActionTakingPhoto, - ); + context.showPendingSnackBar( + message: localizations.mainScreenTakePhotoActionTakingPhoto, + ); - if (isTorchEnabled) { - await controller!.setFlashMode(FlashMode.torch); - } else { - await controller!.setFlashMode(FlashMode.off); - } + await setFlashModeBeforeApplyingAction(); final file = File((await controller!.takePicture()).path); - setState(() { - uploadingPhotoAnimation = file.readAsBytesSync(); - }); + final annotationGetterFuture = getAnnotation(); + final locationData = await getLocation(file); - if (isMaterial(context)) - context.showPendingSnackBar( - message: localizations.mainScreenTakePhotoActionUploadingPhoto, - ); + _showUploadingPhotoAnimation(file); - LocationData? locationData; - - if (await Permission.location.isGranted) { - locationData = await Location().getLocation(); - - if (Platform.isAndroid) { - await tagLocationToImage(file, locationData); - } - } + context.showPendingSnackBar( + message: localizations.mainScreenTakePhotoActionUploadingPhoto, + ); try { - await FileManager.uploadFile(_user, file, locationData: locationData); + await FileManager.uploadFile( + _user, + file, + locationData: locationData, + annotationGetterFuture: annotationGetterFuture, + ); } catch (error) { - if (isMaterial(context)) - context.showErrorSnackBar(message: error.toString()); + context.showErrorSnackBar(message: error.toString()); + return; } - if (isMaterial(context)) - context.showSuccessSnackBar( - message: localizations.mainScreenUploadSuccess, - ); + context.showSuccessSnackBar( + message: localizations.mainScreenUploadSuccess, + ); } finally { - setState(() { - lockCamera = false; - uploadingPhotoAnimation = null; - }); + _releaseCamera(); + _releaseUploadingPhotoAnimation(); } } Future takeVideo() async { final localizations = AppLocalizations.of(context)!; - setState(() { - isRecording = false; - }); - if (!controller!.value.isRecordingVideo) { // Recording has already been stopped return; } setState(() { - lockCamera = true; + isRecording = false; }); + _lockCamera(); + try { - if (isMaterial(context)) - context.showPendingSnackBar( - message: localizations.mainScreenTakeVideoActionSaveVideo, - ); + context.showPendingSnackBar( + message: localizations.mainScreenTakeVideoActionSaveVideo, + ); final file = File((await controller!.stopVideoRecording()).path); - if (isMaterial(context)) - context.showPendingSnackBar( - message: localizations.mainScreenTakeVideoActionUploadingVideo, - ); + final annotationGetterFuture = getAnnotation(); + final locationData = await getLocation(); + + context.showPendingSnackBar( + message: localizations.mainScreenTakeVideoActionUploadingVideo, + ); try { - await FileManager.uploadFile(_user, file); + await FileManager.uploadFile( + _user, + file, + annotationGetterFuture: annotationGetterFuture, + locationData: locationData, + ); } catch (error) { - if (isMaterial(context)) { - context.showErrorSnackBar(message: error.toString()); - } + context.showErrorSnackBar(message: error.toString()); + return; } - if (isMaterial(context)) - context.showSuccessSnackBar( - message: localizations.mainScreenUploadSuccess, - ); + context.showSuccessSnackBar( + message: localizations.mainScreenUploadSuccess, + ); } finally { - setState(() { - lockCamera = false; - }); + _releaseCamera(); } } diff --git a/lib/screens/main_screen/annotation_dialog.dart b/lib/screens/main_screen/annotation_dialog.dart new file mode 100644 index 0000000..954253f --- /dev/null +++ b/lib/screens/main_screen/annotation_dialog.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; +import 'package:quid_faciam_hodie/constants/spacing.dart'; +import 'package:quid_faciam_hodie/utils/theme.dart'; + +class AnnotationDialog extends StatefulWidget { + const AnnotationDialog({Key? key}) : super(key: key); + + @override + State createState() => _AnnotationDialogState(); +} + +class _AnnotationDialogState extends State { + final TextEditingController controller = TextEditingController(); + + @override + void dispose() { + controller.dispose(); + + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final localizations = AppLocalizations.of(context)!; + + return PlatformAlertDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(MEDIUM_SPACE), + child: Column( + children: [ + Text( + localizations.mainScreenAnnotationDialogTitle, + style: getTitleTextStyle(context), + ), + const SizedBox(height: MEDIUM_SPACE), + Text( + localizations.mainScreenAnnotationDialogExplanation, + style: getBodyTextTextStyle(context), + ), + const SizedBox(height: MEDIUM_SPACE), + TextField( + controller: controller, + autofocus: true, + decoration: InputDecoration( + labelText: localizations + .mainScreenAnnotationDialogAnnotationFieldLabel, + ), + onSubmitted: (value) { + Navigator.of(context).pop(value); + }, + ), + ], + ), + ), + ], + ), + actions: [ + PlatformDialogAction( + child: Text(localizations.generalCancelButtonLabel), + onPressed: () => Navigator.pop(context), + ), + PlatformDialogAction( + child: Text(localizations.generalSaveButtonLabel), + onPressed: () => Navigator.pop(context, controller.text.trim()), + ), + ], + ); + } +} diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 52eb0e6..257a666 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -83,7 +83,7 @@ class _SettingsScreenState extends AuthRequiredState ); } - Widget getPicker() { + Widget getQualityPicker() { final settings = GlobalValuesManager.settings!; final resolutionTextMapping = getResolutionTextMapping(context); final items = ResolutionPreset.values @@ -125,6 +125,7 @@ class _SettingsScreenState extends AuthRequiredState @override Widget build(BuildContext context) { + final settings = GlobalValuesManager.settings!; final localizations = AppLocalizations.of(context)!; return PlatformScaffold( @@ -203,7 +204,15 @@ class _SettingsScreenState extends AuthRequiredState localizations .settingsScreenGeneralSectionQualityLabel, ), - title: getPicker(), + title: getQualityPicker(), + ), + SettingsTile.switchTile( + initialValue: settings.askForMemoryAnnotations, + onToggle: settings.setAskForMemoryAnnotations, + title: Text( + localizations + .settingsScreenGeneralSectionAskForMemoryAnnotationsLabel, + ), ), SettingsTile( leading: Icon(context.platformIcons.help), @@ -213,12 +222,10 @@ class _SettingsScreenState extends AuthRequiredState onPressed: (_) async { await UserHelpSheetsManager.deleteAll(); - if (isMaterial(context)) { - context.showSuccessSnackBar( - message: localizations - .settingsScreenResetHelpSheetsResetSuccessfully, - ); - } + context.showSuccessSnackBar( + message: localizations + .settingsScreenResetHelpSheetsResetSuccessfully, + ); }, ) ], diff --git a/lib/screens/timeline_screen/memory_sheet.dart b/lib/screens/timeline_screen/memory_sheet.dart index 88e4134..66f028a 100644 --- a/lib/screens/timeline_screen/memory_sheet.dart +++ b/lib/screens/timeline_screen/memory_sheet.dart @@ -65,13 +65,11 @@ class _MemorySheetState extends State with Loadable { Navigator.pop(context); - if (isMaterial(context)) - context.showSuccessSnackBar( - message: localizations.memorySheetSavedToGallery, - ); + context.showSuccessSnackBar( + message: localizations.memorySheetSavedToGallery, + ); } catch (error) { - if (isMaterial(context)) - context.showErrorSnackBar(message: localizations.generalError); + context.showErrorSnackBar(message: localizations.generalError); } } @@ -94,10 +92,9 @@ class _MemorySheetState extends State with Loadable { Navigator.pop(context); if (isNowPublic) { - if (isMaterial(context)) - context.showSuccessSnackBar( - message: localizations.memorySheetMemoryUpdatedToPublic, - ); + context.showSuccessSnackBar( + message: localizations.memorySheetMemoryUpdatedToPublic, + ); } else { if (isMaterial(context)) context.showSuccessSnackBar( @@ -105,8 +102,7 @@ class _MemorySheetState extends State with Loadable { ); } } catch (error) { - if (isMaterial(context)) - context.showErrorSnackBar(message: localizations.generalError); + context.showErrorSnackBar(message: localizations.generalError); } } diff --git a/lib/screens/timeline_screen/memory_view.dart b/lib/screens/timeline_screen/memory_view.dart index efc53ed..ccbde0a 100644 --- a/lib/screens/timeline_screen/memory_view.dart +++ b/lib/screens/timeline_screen/memory_view.dart @@ -1,12 +1,15 @@ +import 'dart:async'; import 'dart:typed_data'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_platform_widgets/flutter_platform_widgets.dart'; +import 'package:provider/provider.dart'; import 'package:quid_faciam_hodie/constants/spacing.dart'; import 'package:quid_faciam_hodie/enums.dart'; import 'package:quid_faciam_hodie/foreign_types/memory.dart'; +import 'package:quid_faciam_hodie/models/timeline.dart'; import 'package:quid_faciam_hodie/widgets/raw_memory_display.dart'; import 'package:video_player/video_player.dart'; @@ -37,6 +40,7 @@ class MemoryView extends StatefulWidget { class _MemoryViewState extends State { MemoryFetchStatus status = MemoryFetchStatus.downloading; Uint8List? data; + Timer? _nextMemoryTimer; @override void initState() { @@ -45,7 +49,16 @@ class _MemoryViewState extends State { loadMemoryFile(); } + @override + void dispose() { + _nextMemoryTimer?.cancel(); + + super.dispose(); + } + Future loadMemoryFile() async { + final timeline = context.read(); + setState(() { status = MemoryFetchStatus.downloading; }); @@ -75,6 +88,12 @@ class _MemoryViewState extends State { setState(() { status = MemoryFetchStatus.error; }); + + _nextMemoryTimer = Timer( + const Duration(seconds: 1), + timeline.nextMemory, + ); + return; } } diff --git a/lib/screens/timeline_screen/timeline_overlay.dart b/lib/screens/timeline_screen/timeline_overlay.dart index 4489bce..d9d14c0 100644 --- a/lib/screens/timeline_screen/timeline_overlay.dart +++ b/lib/screens/timeline_screen/timeline_overlay.dart @@ -51,44 +51,70 @@ class TimelineOverlay extends StatelessWidget { ), ), Positioned( - right: SMALL_SPACE, + left: 0, + right: 0, bottom: SMALL_SPACE * 2, child: Padding( - padding: const EdgeInsets.symmetric(horizontal: SMALL_SPACE), + padding: const EdgeInsets.symmetric(horizontal: MEDIUM_SPACE), child: AnimatedOpacity( duration: const Duration(milliseconds: 500), curve: Curves.linearToEaseOut, opacity: timeline.showOverlay ? 1.0 : 0.0, child: Row( + mainAxisAlignment: + (timeline.currentMemory.annotation.isNotEmpty) + ? MainAxisAlignment.spaceBetween + : MainAxisAlignment.end, children: [ - AnimatedOpacity( - opacity: timeline.currentMemory.isPublic ? 1.0 : 0.0, - duration: const Duration(milliseconds: 500), - curve: Curves.linearToEaseOut, - child: Icon( - Icons.public, - size: platformThemeData( + if (timeline.currentMemory.annotation.isNotEmpty) + Text( + timeline.currentMemory.annotation, + style: platformThemeData( context, - material: (data) => data.textTheme.bodyLarge!.fontSize, - cupertino: (data) => data.textTheme.textStyle.fontSize, + material: (data) => data.textTheme.titleSmall!.copyWith( + color: Colors.white, + ), + cupertino: (data) => + data.textTheme.navTitleTextStyle.copyWith( + color: Colors.white, + ), ), - color: Colors.white, ), + Row( + children: [ + AnimatedOpacity( + opacity: timeline.currentMemory.isPublic ? 1.0 : 0.0, + duration: const Duration(milliseconds: 500), + curve: Curves.linearToEaseOut, + child: Icon( + Icons.public, + size: platformThemeData( + context, + material: (data) => + data.textTheme.bodyLarge!.fontSize, + cupertino: (data) => + data.textTheme.textStyle.fontSize, + ), + color: Colors.white, + ), + ), + const SizedBox(width: SMALL_SPACE), + Text( + '$memoryIndex/$memoriesAmount', + style: platformThemeData( + context, + material: (data) => + data.textTheme.titleSmall!.copyWith( + color: Colors.white, + ), + cupertino: (data) => + data.textTheme.navTitleTextStyle.copyWith( + color: Colors.white, + ), + ), + ), + ], ), - const SizedBox(width: SMALL_SPACE), - Text( - '$memoryIndex/$memoriesAmount', - style: platformThemeData( - context, - material: (data) => data.textTheme.titleSmall!.copyWith( - color: Colors.white, - ), - cupertino: (data) => - data.textTheme.navTitleTextStyle.copyWith( - color: Colors.white, - ), - ), - ) ], ), ), diff --git a/lib/widgets/key_value_info.dart b/lib/widgets/key_value_info.dart index e6ba821..d282d5f 100644 --- a/lib/widgets/key_value_info.dart +++ b/lib/widgets/key_value_info.dart @@ -60,11 +60,9 @@ class KeyValueInfo extends StatelessWidget { HapticFeedback.lightImpact(); Clipboard.setData(ClipboardData(text: value)); - if (isMaterial(context)) { - context.showSuccessSnackBar( - message: 'Copied to clipboard!', - ); - } + context.showSuccessSnackBar( + message: 'Copied to clipboard!', + ); }, ) : null, diff --git a/lib/widgets/raw_memory_display.dart b/lib/widgets/raw_memory_display.dart index a4d40e9..9838e1d 100644 --- a/lib/widgets/raw_memory_display.dart +++ b/lib/widgets/raw_memory_display.dart @@ -43,12 +43,18 @@ class _RawMemoryDisplayState extends State { Future createTempVideo() async { final tempDirectory = await getTemporaryDirectory(); final path = '${tempDirectory.path}/${widget.filename ?? 'video.mp4'}'; + print("#" * 50); + print(widget.filename); + print(path); final file = File(path); + print(await file.exists()); + print(widget.data); + /* if (await file.exists()) { // File already exists, so just return it return file; - } + }*/ // File needs to be created await file.create(); diff --git a/pubspec.lock b/pubspec.lock index f7540e7..8ebcb8a 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -441,6 +441,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + morphing_text: + dependency: "direct main" + description: + name: morphing_text + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" nested: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f020eee..a937c61 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,6 +63,7 @@ dependencies: flutter_osm_plugin: ^0.39.0 url_launcher: ^6.1.5 apple_maps_flutter: ^1.2.0 + morphing_text: ^1.0.1 dev_dependencies: flutter_test: