From 7d457fa48ec1aa49a842d4087a1c962d35b2534d Mon Sep 17 00:00:00 2001 From: unknown Date: Tue, 31 Dec 2024 22:26:16 +0500 Subject: [PATCH 1/4] Added saveFile action --- .../ensemble/lib/action/action_invokable.dart | 1 + .../ensemble/lib/action/save_to_gallery.dart | 133 ++++++++++++++++++ modules/ensemble/lib/framework/action.dart | 20 +-- 3 files changed, 145 insertions(+), 9 deletions(-) create mode 100644 modules/ensemble/lib/action/save_to_gallery.dart diff --git a/modules/ensemble/lib/action/action_invokable.dart b/modules/ensemble/lib/action/action_invokable.dart index dea90b0de..42dd8fb98 100644 --- a/modules/ensemble/lib/action/action_invokable.dart +++ b/modules/ensemble/lib/action/action_invokable.dart @@ -39,6 +39,7 @@ abstract class ActionInvokable with Invokable { ActionType.dismissDialog, ActionType.closeAllDialogs, ActionType.executeActionGroup, + ActionType.saveFile, ]); } diff --git a/modules/ensemble/lib/action/save_to_gallery.dart b/modules/ensemble/lib/action/save_to_gallery.dart new file mode 100644 index 000000000..9d8e2cf16 --- /dev/null +++ b/modules/ensemble/lib/action/save_to_gallery.dart @@ -0,0 +1,133 @@ +import 'dart:convert'; +import 'dart:typed_data'; +import 'dart:io'; + +import 'package:ensemble/framework/action.dart'; +import 'package:ensemble/framework/scope.dart'; +import 'package:flutter/material.dart'; +import 'package:image_gallery_saver/image_gallery_saver.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:ensemble/framework/error_handling.dart'; +import 'package:dio/dio.dart'; + +/// Custom action to save files (images and documents) in platform-specific accessible directories +class SaveToFileSystemAction extends EnsembleAction { + final String? fileName; + final dynamic blobData; + final String? source; // Optional source for URL if blobData is not available + + SaveToFileSystemAction({ + required this.fileName, + this.blobData, + this.source, + }); + + factory SaveToFileSystemAction.from({Map? payload}) { + if (payload == null || payload['fileName'] == null) { + throw LanguageError('${ActionType.saveFile.name} requires fileName.'); + } + + return SaveToFileSystemAction( + fileName: payload['fileName'], + blobData: payload['blobData'], + source: payload['source'], + ); + } + + @override + Future execute(BuildContext context, ScopeManager scopeManager) async { + try { + if (fileName == null) { + throw Exception('Missing required parameter: fileName.'); + } + + Uint8List? fileBytes; + + // If blobData is provided, process it + if (blobData != null) { + // Handle base64 blob or binary data + if (blobData is String) { + fileBytes = base64Decode(blobData); // Decode base64 + } else if (blobData is List) { + fileBytes = Uint8List.fromList(blobData); + } else { + throw Exception( + 'Invalid blob data format. Must be base64 or List.'); + } + } else if (source != null) { + // If blobData is not available, check for source (network URL) + Dio dio = Dio(); + var response = await dio.get(source!, + options: Options(responseType: ResponseType.bytes)); + fileBytes = Uint8List.fromList(response.data); + } else { + throw Exception('Missing blobData and source.'); + } + + // Determine file type based on file extension + final fileExtension = fileName!.split('.').last.toLowerCase(); + if (['jpg', 'jpeg', 'png', 'gif', 'bmp'].contains(fileExtension)) { + // Save images to DCIM/Pictures + await _saveImageToDCIM(fileName!, fileBytes!); + } else { + // Save documents to Documents folder + await _saveDocumentToDocumentsFolder(fileName!, fileBytes!); + } + } catch (e) { + print('Error saving file: $e'); + throw Exception('Failed to save file: $e'); + } + } + + /// Save images to DCIM/Pictures folder + Future _saveImageToDCIM(String fileName, Uint8List fileBytes) async { + try { + // Get DCIM directory + final directory = Directory('/storage/emulated/0/DCIM/Pictures'); + if (!await directory.exists()) { + await directory.create(recursive: true); + } + + // Save file + final filePath = '${directory.path}/$fileName'; + final file = File(filePath); + await file.writeAsBytes(fileBytes); + + print('Image saved to DCIM/Pictures: $filePath'); + } catch (e) { + print('Error saving image to DCIM/Pictures: $e'); + throw Exception('Failed to save image: $e'); + } + } + + /// Save documents to Documents folder + Future _saveDocumentToDocumentsFolder( + String fileName, Uint8List fileBytes) async { + try { + // Get Documents directory + final directory = Directory('/storage/emulated/0/Documents'); + if (!await directory.exists()) { + await directory.create(recursive: true); + } + + // Save file + final filePath = '${directory.path}/$fileName'; + final file = File(filePath); + await file.writeAsBytes(fileBytes); + + print('Document saved to Documents folder: $filePath'); + } catch (e) { + print('Error saving document to Documents folder: $e'); + throw Exception('Failed to save document: $e'); + } + } + + /// Factory method to construct the action from JSON + static SaveToFileSystemAction fromJson(Map json) { + return SaveToFileSystemAction( + fileName: json['fileName'], + blobData: json['blobData'], + source: json['source'], + ); + } +} diff --git a/modules/ensemble/lib/framework/action.dart b/modules/ensemble/lib/framework/action.dart index 54fac1dba..cb90a2209 100644 --- a/modules/ensemble/lib/framework/action.dart +++ b/modules/ensemble/lib/framework/action.dart @@ -18,6 +18,7 @@ import 'package:ensemble/action/change_locale_actions.dart'; import 'package:ensemble/action/misc_action.dart'; import 'package:ensemble/action/navigation_action.dart'; import 'package:ensemble/action/notification_actions.dart'; +import 'package:ensemble/action/save_to_gallery.dart'; import 'package:ensemble/action/phone_contact_action.dart'; import 'package:ensemble/action/sign_in_out_action.dart'; import 'package:ensemble/action/toast_actions.dart'; @@ -57,18 +58,16 @@ class ShowCameraAction extends EnsembleAction { EnsembleAction? onClose; EnsembleAction? onCapture; EnsembleAction? onError; - factory ShowCameraAction.fromYaml({Invokable? initiator, Map? payload}) { return ShowCameraAction( - initiator: initiator, - options: Utils.getMap(payload?['options']), - id: Utils.optionalString(payload?['id']), - onComplete: EnsembleAction.from(payload?['onComplete']), - onClose: EnsembleAction.from(payload?['onClose']), - onCapture: EnsembleAction.from(payload?['onCapture']), - onError: EnsembleAction.from(payload?['onError']) - ); + initiator: initiator, + options: Utils.getMap(payload?['options']), + id: Utils.optionalString(payload?['id']), + onComplete: EnsembleAction.from(payload?['onComplete']), + onClose: EnsembleAction.from(payload?['onClose']), + onCapture: EnsembleAction.from(payload?['onCapture']), + onError: EnsembleAction.from(payload?['onError'])); } } @@ -1052,6 +1051,7 @@ enum ActionType { bluetoothDisconnect, bluetoothSubscribeCharacteristic, bluetoothUnsubscribeCharacteristic, + saveFile } /// payload representing an Action to do (navigateToScreen, InvokeAPI, ..) @@ -1171,6 +1171,8 @@ abstract class EnsembleAction { return CopyToClipboardAction.from(payload: payload); } else if (actionType == ActionType.share) { return ShareAction.from(payload: payload); + } else if (actionType == ActionType.saveFile) { + return SaveToFileSystemAction.from(payload: payload); } else if (actionType == ActionType.rateApp) { return RateAppAction.from(payload: payload); } else if (actionType == ActionType.getDeviceToken) { From 0535da8c0c8da90c1f125590b9f293146f87bca3 Mon Sep 17 00:00:00 2001 From: unknown Date: Wed, 1 Jan 2025 01:56:54 +0500 Subject: [PATCH 2/4] Renamed Action & Added type property --- .../lib/action/{save_to_gallery.dart => save_file.dart} | 8 ++++++-- modules/ensemble/lib/framework/action.dart | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) rename modules/ensemble/lib/action/{save_to_gallery.dart => save_file.dart} (95%) diff --git a/modules/ensemble/lib/action/save_to_gallery.dart b/modules/ensemble/lib/action/save_file.dart similarity index 95% rename from modules/ensemble/lib/action/save_to_gallery.dart rename to modules/ensemble/lib/action/save_file.dart index 9d8e2cf16..2aed3a817 100644 --- a/modules/ensemble/lib/action/save_to_gallery.dart +++ b/modules/ensemble/lib/action/save_file.dart @@ -15,11 +15,13 @@ class SaveToFileSystemAction extends EnsembleAction { final String? fileName; final dynamic blobData; final String? source; // Optional source for URL if blobData is not available + final String? type; // file type SaveToFileSystemAction({ required this.fileName, this.blobData, this.source, + this.type, }); factory SaveToFileSystemAction.from({Map? payload}) { @@ -31,6 +33,7 @@ class SaveToFileSystemAction extends EnsembleAction { fileName: payload['fileName'], blobData: payload['blobData'], source: payload['source'], + type: payload['type'], ); } @@ -66,10 +69,11 @@ class SaveToFileSystemAction extends EnsembleAction { // Determine file type based on file extension final fileExtension = fileName!.split('.').last.toLowerCase(); - if (['jpg', 'jpeg', 'png', 'gif', 'bmp'].contains(fileExtension)) { + // if (['jpg', 'jpeg', 'png', 'gif', 'bmp'].contains(fileExtension)) { + if (type == 'image') { // Save images to DCIM/Pictures await _saveImageToDCIM(fileName!, fileBytes!); - } else { + } else if (type == 'document') { // Save documents to Documents folder await _saveDocumentToDocumentsFolder(fileName!, fileBytes!); } diff --git a/modules/ensemble/lib/framework/action.dart b/modules/ensemble/lib/framework/action.dart index cb90a2209..ceac4d078 100644 --- a/modules/ensemble/lib/framework/action.dart +++ b/modules/ensemble/lib/framework/action.dart @@ -18,7 +18,7 @@ import 'package:ensemble/action/change_locale_actions.dart'; import 'package:ensemble/action/misc_action.dart'; import 'package:ensemble/action/navigation_action.dart'; import 'package:ensemble/action/notification_actions.dart'; -import 'package:ensemble/action/save_to_gallery.dart'; +import 'package:ensemble/action/save_file.dart'; import 'package:ensemble/action/phone_contact_action.dart'; import 'package:ensemble/action/sign_in_out_action.dart'; import 'package:ensemble/action/toast_actions.dart'; From b4df9b33c4666e846914cdbfc454fd983106261c Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 6 Jan 2025 12:45:36 +0500 Subject: [PATCH 3/4] updated for ios & web --- modules/ensemble/lib/action/save_file.dart | 111 +++++++++++++++------ modules/ensemble/pubspec.yaml | 2 + 2 files changed, 84 insertions(+), 29 deletions(-) diff --git a/modules/ensemble/lib/action/save_file.dart b/modules/ensemble/lib/action/save_file.dart index 2aed3a817..e5c5c9109 100644 --- a/modules/ensemble/lib/action/save_file.dart +++ b/modules/ensemble/lib/action/save_file.dart @@ -8,7 +8,9 @@ import 'package:flutter/material.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:path_provider/path_provider.dart'; import 'package:ensemble/framework/error_handling.dart'; +import 'package:flutter/foundation.dart'; import 'package:dio/dio.dart'; +import 'dart:html' as html; /// Custom action to save files (images and documents) in platform-specific accessible directories class SaveToFileSystemAction extends EnsembleAction { @@ -67,65 +69,116 @@ class SaveToFileSystemAction extends EnsembleAction { throw Exception('Missing blobData and source.'); } - // Determine file type based on file extension - final fileExtension = fileName!.split('.').last.toLowerCase(); - // if (['jpg', 'jpeg', 'png', 'gif', 'bmp'].contains(fileExtension)) { if (type == 'image') { - // Save images to DCIM/Pictures - await _saveImageToDCIM(fileName!, fileBytes!); + // Save images to Default Image Path + await _saveImageToDCIM(fileName!, fileBytes); } else if (type == 'document') { // Save documents to Documents folder - await _saveDocumentToDocumentsFolder(fileName!, fileBytes!); + await _saveDocumentToDocumentsFolder(fileName!, fileBytes); } } catch (e) { - print('Error saving file: $e'); throw Exception('Failed to save file: $e'); } } - /// Save images to DCIM/Pictures folder Future _saveImageToDCIM(String fileName, Uint8List fileBytes) async { try { - // Get DCIM directory - final directory = Directory('/storage/emulated/0/DCIM/Pictures'); - if (!await directory.exists()) { - await directory.create(recursive: true); + final result = await ImageGallerySaver.saveImage( + fileBytes, + name: fileName, + ); + if (result['isSuccess']) { + debugPrint('Image saved to gallery: $result'); + } else { + throw Exception('Failed to save image to gallery.'); } - - // Save file - final filePath = '${directory.path}/$fileName'; - final file = File(filePath); - await file.writeAsBytes(fileBytes); - - print('Image saved to DCIM/Pictures: $filePath'); } catch (e) { - print('Error saving image to DCIM/Pictures: $e'); throw Exception('Failed to save image: $e'); } } - /// Save documents to Documents folder + /// Save documents to the default "Documents" directory Future _saveDocumentToDocumentsFolder( String fileName, Uint8List fileBytes) async { try { - // Get Documents directory - final directory = Directory('/storage/emulated/0/Documents'); - if (!await directory.exists()) { - await directory.create(recursive: true); + String filePath; + + if (Platform.isAndroid) { + // Get the default "Documents" directory on Android + Directory? directory = Directory('/storage/emulated/0/Documents'); + if (!directory.existsSync()) { + directory.createSync( + recursive: true); // Create the directory if it doesn't exist + } + filePath = '${directory.path}/$fileName'; + } else if (Platform.isIOS) { + // On iOS, use the app-specific Documents directory + final directory = await getApplicationDocumentsDirectory(); + filePath = '${directory.path}/$fileName'; + + // Optionally, use a share intent to let users save the file to their desired location + } else if (kIsWeb) { + _downloadFileOnWeb(fileName, fileBytes); + return; + } else { + throw UnsupportedError('Platform not supported'); } - // Save file - final filePath = '${directory.path}/$fileName'; + // Write the file to the determined path final file = File(filePath); await file.writeAsBytes(fileBytes); - print('Document saved to Documents folder: $filePath'); + debugPrint('Document saved to: $filePath'); + + // Notify Android's media store to make it visible + if (Platform.isAndroid) { + await _notifyFileSystem(filePath); + } } catch (e) { - print('Error saving document to Documents folder: $e'); throw Exception('Failed to save document: $e'); } } + /// Notify the Android media store about the new file + Future _notifyFileSystem(String filePath) async { + try { + await Process.run('am', [ + 'broadcast', + '-a', + 'android.intent.action.MEDIA_SCANNER_SCAN_FILE', + '-d', + 'file://$filePath', + ]); + } catch (e) { + debugPrint('Failed to notify media store: $e'); + } + } + + Future _downloadFileOnWeb(String fileName, Uint8List fileBytes) async { + try { + // Convert Uint8List to a Blob + final blob = html.Blob([fileBytes]); + + // Create an object URL for the Blob + final url = html.Url.createObjectUrlFromBlob(blob); + + // Create a download anchor element + final anchor = html.AnchorElement(href: url) + ..target = 'blank' // Open in a new tab if needed + ..download = fileName; // Set the download file name + + // Trigger the download + anchor.click(); + + // Revoke the object URL to free resources + html.Url.revokeObjectUrl(url); + + debugPrint('File downloaded: $fileName'); + } catch (e) { + throw Exception('Failed to download file: $e'); + } + } + /// Factory method to construct the action from JSON static SaveToFileSystemAction fromJson(Map json) { return SaveToFileSystemAction( diff --git a/modules/ensemble/pubspec.yaml b/modules/ensemble/pubspec.yaml index a0fe6a2d6..4dcfb29f6 100644 --- a/modules/ensemble/pubspec.yaml +++ b/modules/ensemble/pubspec.yaml @@ -93,6 +93,8 @@ dependencies: shared_preferences: ^2.1.1 workmanager: ^0.5.1 flutter_local_notifications: ^17.2.3 + dio: ^5.7.0 + image_gallery_saver: ^2.0.3 flutter_i18n: ^0.35.1 pointer_interceptor: ^0.9.3+4 flutter_secure_storage: ^9.2.2 From 9d63558bf4f8ae454e4bba5e4fdfdb6757dbcc72 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 6 Jan 2025 18:17:16 +0500 Subject: [PATCH 4/4] Updated save file action --- modules/ensemble/lib/action/save_file.dart | 51 ++++++++-------------- modules/ensemble/pubspec.yaml | 1 - 2 files changed, 19 insertions(+), 33 deletions(-) diff --git a/modules/ensemble/lib/action/save_file.dart b/modules/ensemble/lib/action/save_file.dart index e5c5c9109..b45c858e0 100644 --- a/modules/ensemble/lib/action/save_file.dart +++ b/modules/ensemble/lib/action/save_file.dart @@ -9,7 +9,7 @@ import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:path_provider/path_provider.dart'; import 'package:ensemble/framework/error_handling.dart'; import 'package:flutter/foundation.dart'; -import 'package:dio/dio.dart'; +import 'package:http/http.dart' as http; import 'dart:html' as html; /// Custom action to save files (images and documents) in platform-specific accessible directories @@ -61,10 +61,13 @@ class SaveToFileSystemAction extends EnsembleAction { } } else if (source != null) { // If blobData is not available, check for source (network URL) - Dio dio = Dio(); - var response = await dio.get(source!, - options: Options(responseType: ResponseType.bytes)); - fileBytes = Uint8List.fromList(response.data); + final response = await http.get(Uri.parse(source!)); + if (response.statusCode == 200) { + fileBytes = Uint8List.fromList(response.bodyBytes); + } else { + throw Exception( + 'Failed to download file: HTTP ${response.statusCode}'); + } } else { throw Exception('Missing blobData and source.'); } @@ -83,14 +86,18 @@ class SaveToFileSystemAction extends EnsembleAction { Future _saveImageToDCIM(String fileName, Uint8List fileBytes) async { try { - final result = await ImageGallerySaver.saveImage( - fileBytes, - name: fileName, - ); - if (result['isSuccess']) { - debugPrint('Image saved to gallery: $result'); + if (kIsWeb) { + _downloadFileOnWeb(fileName, fileBytes); } else { - throw Exception('Failed to save image to gallery.'); + final result = await ImageGallerySaver.saveImage( + fileBytes, + name: fileName, + ); + if (result['isSuccess']) { + debugPrint('Image saved to gallery: $result'); + } else { + throw Exception('Failed to save image to gallery.'); + } } } catch (e) { throw Exception('Failed to save image: $e'); @@ -129,31 +136,11 @@ class SaveToFileSystemAction extends EnsembleAction { await file.writeAsBytes(fileBytes); debugPrint('Document saved to: $filePath'); - - // Notify Android's media store to make it visible - if (Platform.isAndroid) { - await _notifyFileSystem(filePath); - } } catch (e) { throw Exception('Failed to save document: $e'); } } - /// Notify the Android media store about the new file - Future _notifyFileSystem(String filePath) async { - try { - await Process.run('am', [ - 'broadcast', - '-a', - 'android.intent.action.MEDIA_SCANNER_SCAN_FILE', - '-d', - 'file://$filePath', - ]); - } catch (e) { - debugPrint('Failed to notify media store: $e'); - } - } - Future _downloadFileOnWeb(String fileName, Uint8List fileBytes) async { try { // Convert Uint8List to a Blob diff --git a/modules/ensemble/pubspec.yaml b/modules/ensemble/pubspec.yaml index 4dcfb29f6..2f1e83ef5 100644 --- a/modules/ensemble/pubspec.yaml +++ b/modules/ensemble/pubspec.yaml @@ -93,7 +93,6 @@ dependencies: shared_preferences: ^2.1.1 workmanager: ^0.5.1 flutter_local_notifications: ^17.2.3 - dio: ^5.7.0 image_gallery_saver: ^2.0.3 flutter_i18n: ^0.35.1 pointer_interceptor: ^0.9.3+4