diff --git a/lib/extensions/snackbar.dart b/lib/extensions/snackbar.dart index e4529e1..5bee0d5 100644 --- a/lib/extensions/snackbar.dart +++ b/lib/extensions/snackbar.dart @@ -12,11 +12,12 @@ extension ShowSnackBar on BuildContext { required final String message, final Color backgroundColor = Colors.white, final Duration duration = const Duration(seconds: 4), + final BuildContext? context, }) { pendingSnackBar?.close(); pendingSnackBar = null; - return ScaffoldMessenger.of(this).showSnackBar( + return ScaffoldMessenger.of(context ?? this).showSnackBar( SnackBar( content: Text(message), backgroundColor: backgroundColor, @@ -25,14 +26,20 @@ extension ShowSnackBar on BuildContext { ); } - void showErrorSnackBar({required final String message}) { + void showErrorSnackBar({ + required final String message, + final BuildContext? context, + }) { showSnackBar( message: message, backgroundColor: Colors.red, ); } - void showSuccessSnackBar({required final String message}) { + void showSuccessSnackBar({ + required final String message, + final BuildContext? context, + }) { showSnackBar( message: message, duration: const Duration(milliseconds: 550), @@ -40,7 +47,10 @@ extension ShowSnackBar on BuildContext { ); } - void showPendingSnackBar({required final String message}) { + void showPendingSnackBar({ + required final String message, + final BuildContext? context, + }) { pendingSnackBar = showSnackBar( message: message, backgroundColor: Colors.yellow, diff --git a/lib/managers/file_manager.dart b/lib/managers/file_manager.dart index e368cd3..1f15f2d 100644 --- a/lib/managers/file_manager.dart +++ b/lib/managers/file_manager.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'dart:typed_data'; +import 'package:path_provider/path_provider.dart'; import 'package:share_location/enums.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:uuid/uuid.dart'; @@ -65,7 +66,7 @@ class FileManager { location.split('.').last == 'jpg' ? MemoryType.photo : MemoryType.video; try { - final file = await downloadFile('memories', location); + final file = await getFileData('memories', location); return [file, memoryType]; } catch (error) { @@ -73,13 +74,11 @@ class FileManager { } } - static Future downloadFile( - final String table, - final String path, - ) async { + static Future getFileData(final String table, final String path, + {final bool disableCache = false}) async { final key = '$table:$path'; - if (fileCache.containsKey(key)) { + if (!disableCache && fileCache.containsKey(key)) { return fileCache[key]!; } @@ -96,6 +95,30 @@ class FileManager { return data; } + static Future downloadFile( + final String table, + final String path, { + final bool disableDownloadCache = false, + final bool disableFileCache = false, + }) async { + final tempDirectory = await getTemporaryDirectory(); + final filename = '${tempDirectory.path}/$path'; + final file = File(filename); + + if (!disableFileCache && (await file.exists())) { + return file; + } + + final data = + await getFileData(table, path, disableCache: disableDownloadCache); + + // Create file + await file.create(recursive: true); + await file.writeAsBytes(data); + + return file; + } + static Future deleteFile(final String path) async { final response = await supabase.from('memories').delete().eq('location', path).execute(); diff --git a/lib/utils/loadable.dart b/lib/utils/loadable.dart index d05b056..496f5d6 100644 --- a/lib/utils/loadable.dart +++ b/lib/utils/loadable.dart @@ -1,20 +1,31 @@ -mixin Loadable { - bool _isLoading = false; +import 'package:uuid/uuid.dart'; - bool get isLoading => _isLoading; +const uuid = Uuid(); + +mixin Loadable { + static final String _generalLoadingID = '#_loadable-${uuid.v4()}'; + + final Set _IDs = {}; + + bool get isLoading => _IDs.contains(_generalLoadingID); + bool getIsLoadingSpecificID(final String id) => _IDs.contains(id); + bool getIsLoading(final String id) => isLoading || getIsLoadingSpecificID(id); void setState(void Function() callback); - Future callWithLoading(Future Function() callback) async { + Future callWithLoading( + Future Function() callback, [ + final String? id, + ]) async { setState(() { - _isLoading = true; + _IDs.add(id ?? _generalLoadingID); }); try { await callback(); } finally { setState(() { - _isLoading = false; + _IDs.remove(id ?? _generalLoadingID); }); } } diff --git a/lib/widgets/memory.dart b/lib/widgets/memory.dart index 24c65b4..cd5c95e 100644 --- a/lib/widgets/memory.dart +++ b/lib/widgets/memory.dart @@ -98,7 +98,7 @@ class _MemoryViewState extends AuthRequiredState { location.split('.').last == 'jpg' ? MemoryType.photo : MemoryType.video; try { - final fileData = await FileManager.downloadFile('memories', location); + final fileData = await FileManager.getFileData('memories', location); if (!mounted) { return; diff --git a/lib/widgets/memory_sheet.dart b/lib/widgets/memory_sheet.dart index d475e95..1d8e62f 100644 --- a/lib/widgets/memory_sheet.dart +++ b/lib/widgets/memory_sheet.dart @@ -1,5 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:gallery_saver/gallery_saver.dart'; import 'package:share_location/constants/spacing.dart'; +import 'package:share_location/enums.dart'; +import 'package:share_location/extensions/snackbar.dart'; import 'package:share_location/foreign_types/memory.dart'; import 'package:share_location/managers/file_manager.dart'; import 'package:share_location/utils/loadable.dart'; @@ -8,11 +12,13 @@ import 'package:share_location/widgets/modal_sheet.dart'; class MemorySheet extends StatefulWidget { final Memory memory; final VoidCallback onMemoryDeleted; + final BuildContext sheetContext; const MemorySheet({ Key? key, required this.memory, required this.onMemoryDeleted, + required this.sheetContext, }) : super(key: key); @override @@ -23,6 +29,55 @@ class _MemorySheetState extends State with Loadable { Future deleteFile() async { await FileManager.deleteFile(widget.memory.location); widget.onMemoryDeleted(); + + if (mounted) { + Navigator.pop(context); + } + } + + Future downloadFile() async { + try { + final file = + await FileManager.downloadFile('memories', widget.memory.location); + + if (!mounted) { + return; + } + + switch (widget.memory.type) { + case MemoryType.photo: + await GallerySaver.saveImage(file.path); + break; + case MemoryType.video: + await GallerySaver.saveVideo(file.path); + break; + } + + Navigator.pop(context); + + context.showSuccessSnackBar(message: 'File saved to Gallery!'); + } catch (error) { + context.showErrorSnackBar(message: 'There was an error'); + Fluttertoast.showToast( + msg: 'There was an error', + toastLength: Toast.LENGTH_SHORT, + backgroundColor: Colors.red, + textColor: Colors.white, + ); + } + } + + Widget buildLoadingIndicator() { + final theme = Theme.of(context); + + return SizedBox( + width: theme.textTheme.titleLarge!.fontSize, + height: theme.textTheme.titleLarge!.fontSize, + child: CircularProgressIndicator( + strokeWidth: 2, + color: theme.textTheme.bodyText1!.color, + ), + ); } @override @@ -38,18 +93,26 @@ class _MemorySheetState extends State with Loadable { ), const SizedBox(height: MEDIUM_SPACE), ListTile( - leading: Icon(Icons.delete_forever_sharp), - title: Text('Delete Memory'), - onTap: isLoading + leading: const Icon(Icons.download), + title: const Text('Download to Gallery'), + enabled: !getIsLoadingSpecificID('download'), + onTap: getIsLoadingSpecificID('download') ? null - : () async { - await callWithLoading(deleteFile); - - if (mounted) { - Navigator.pop(context); - } - }, - trailing: isLoading ? const CircularProgressIndicator() : null, + : () => callWithLoading(downloadFile, 'download'), + trailing: getIsLoadingSpecificID('download') + ? buildLoadingIndicator() + : null, + ), + ListTile( + leading: const Icon(Icons.delete_forever_sharp), + title: const Text('Delete Memory'), + enabled: !getIsLoadingSpecificID('delete'), + onTap: getIsLoadingSpecificID('delete') + ? null + : () => callWithLoading(deleteFile, 'delete'), + trailing: getIsLoadingSpecificID('delete') + ? buildLoadingIndicator() + : null, ), ], ), diff --git a/lib/widgets/modal_sheet.dart b/lib/widgets/modal_sheet.dart index dbf2626..9a0b572 100644 --- a/lib/widgets/modal_sheet.dart +++ b/lib/widgets/modal_sheet.dart @@ -19,18 +19,17 @@ class ModalSheet extends StatelessWidget { Padding( padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), - child: Container( - width: double.infinity, - padding: const EdgeInsets.all(MEDIUM_SPACE), - decoration: BoxDecoration( - color: theme.bottomSheetTheme.modalBackgroundColor ?? - theme.bottomAppBarColor, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(LARGE_SPACE), - topRight: Radius.circular(LARGE_SPACE), - ), + child: Material( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(LARGE_SPACE), + topRight: Radius.circular(LARGE_SPACE), + ), + color: theme.bottomSheetTheme.modalBackgroundColor ?? + theme.bottomAppBarColor, + child: Container( + padding: const EdgeInsets.all(MEDIUM_SPACE), + child: child, ), - child: child, ), ), ], diff --git a/lib/widgets/timeline_page.dart b/lib/widgets/timeline_page.dart index 8fe4b91..85f4335 100644 --- a/lib/widgets/timeline_page.dart +++ b/lib/widgets/timeline_page.dart @@ -119,8 +119,9 @@ class _TimelinePageState extends State { context: context, backgroundColor: Colors.transparent, isScrollControlled: true, - builder: (_) => MemorySheet( + builder: (sheetContext) => MemorySheet( memory: timeline.currentMemory, + sheetContext: sheetContext, onMemoryDeleted: timeline.removeEmptyDates, ), ); diff --git a/pubspec.lock b/pubspec.lock index d313794..44d56ce 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -205,6 +205,13 @@ packages: description: flutter source: sdk version: "0.0.0" + fluttertoast: + dependency: "direct main" + description: + name: fluttertoast + url: "https://pub.dartlang.org" + source: hosted + version: "8.0.9" functions_client: dependency: transitive description: @@ -212,6 +219,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.1-dev.5" + gallery_saver: + dependency: "direct main" + description: + name: gallery_saver + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.2" gotrue: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c89f180..1e492c1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -46,6 +46,8 @@ dependencies: property_change_notifier: ^0.3.0 path: ^1.8.1 provider: ^6.0.3 + gallery_saver: ^2.3.2 + fluttertoast: ^8.0.9 dev_dependencies: flutter_test: