added download functionality

This commit is contained in:
Myzel394 2022-08-15 20:07:15 +02:00
parent 351bd6fec9
commit a2c5dd8237
9 changed files with 163 additions and 40 deletions

View File

@ -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,

View File

@ -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<Uint8List> downloadFile(
final String table,
final String path,
) async {
static Future<Uint8List> 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<File> 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<void> deleteFile(final String path) async {
final response =
await supabase.from('memories').delete().eq('location', path).execute();

View File

@ -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<String> _IDs = <String>{};
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<void> callWithLoading(Future<void> Function() callback) async {
Future<void> callWithLoading(
Future<void> Function() callback, [
final String? id,
]) async {
setState(() {
_isLoading = true;
_IDs.add(id ?? _generalLoadingID);
});
try {
await callback();
} finally {
setState(() {
_isLoading = false;
_IDs.remove(id ?? _generalLoadingID);
});
}
}

View File

@ -98,7 +98,7 @@ class _MemoryViewState extends AuthRequiredState<MemoryView> {
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;

View File

@ -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<MemorySheet> with Loadable {
Future<void> deleteFile() async {
await FileManager.deleteFile(widget.memory.location);
widget.onMemoryDeleted();
if (mounted) {
Navigator.pop(context);
}
}
Future<void> 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<MemorySheet> 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,
),
],
),

View File

@ -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,
),
),
],

View File

@ -119,8 +119,9 @@ class _TimelinePageState extends State<TimelinePage> {
context: context,
backgroundColor: Colors.transparent,
isScrollControlled: true,
builder: (_) => MemorySheet(
builder: (sheetContext) => MemorySheet(
memory: timeline.currentMemory,
sheetContext: sheetContext,
onMemoryDeleted: timeline.removeEmptyDates,
),
);

View File

@ -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:

View File

@ -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: