Skip to content

Commit

Permalink
Merge pull request #1799 from EnsembleUI/save-to-gallery-action
Browse files Browse the repository at this point in the history
added `saveFile` action
  • Loading branch information
mehsaandev authored Jan 7, 2025
2 parents 89f4885 + 5eaff75 commit d6a7f4c
Show file tree
Hide file tree
Showing 4 changed files with 183 additions and 0 deletions.
1 change: 1 addition & 0 deletions modules/ensemble/lib/action/action_invokable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ abstract class ActionInvokable with Invokable {
ActionType.dismissDialog,
ActionType.closeAllDialogs,
ActionType.executeActionGroup,
ActionType.saveFile,
ActionType.controlDeviceBackNavigation,
]);
}
Expand Down
177 changes: 177 additions & 0 deletions modules/ensemble/lib/action/save_file.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
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:flutter/foundation.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
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}) {
if (payload == null || payload['fileName'] == null) {
throw LanguageError('${ActionType.saveFile.name} requires fileName.');
}

return SaveToFileSystemAction(
fileName: payload['fileName'],
blobData: payload['blobData'],
source: payload['source'],
type: payload['type'],
);
}

@override
Future<void> 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<int>) {
fileBytes = Uint8List.fromList(blobData);
} else {
throw Exception(
'Invalid blob data format. Must be base64 or List<int>.');
}
} else if (source != null) {
// If blobData is not available, check for source (network URL)
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.');
}

if (type == 'image') {
// Save images to Default Image Path
await _saveImageToDCIM(fileName!, fileBytes);
} else if (type == 'document') {
// Save documents to Documents folder
await _saveDocumentToDocumentsFolder(fileName!, fileBytes);
}
} catch (e) {
throw Exception('Failed to save file: $e');
}
}

Future<void> _saveImageToDCIM(String fileName, Uint8List fileBytes) async {
try {
if (kIsWeb) {
_downloadFileOnWeb(fileName, fileBytes);
} else {
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');
}
}

/// Save documents to the default "Documents" directory
Future<void> _saveDocumentToDocumentsFolder(
String fileName, Uint8List fileBytes) async {
try {
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');
}

// Write the file to the determined path
final file = File(filePath);
await file.writeAsBytes(fileBytes);

debugPrint('Document saved to: $filePath');
} catch (e) {
throw Exception('Failed to save document: $e');
}
}

Future<void> _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<String, dynamic> json) {
return SaveToFileSystemAction(
fileName: json['fileName'],
blobData: json['blobData'],
source: json['source'],
);
}
}
4 changes: 4 additions & 0 deletions modules/ensemble/lib/framework/action.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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_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';
Expand Down Expand Up @@ -1055,6 +1056,7 @@ enum ActionType {
bluetoothDisconnect,
bluetoothSubscribeCharacteristic,
bluetoothUnsubscribeCharacteristic,
saveFile
controlDeviceBackNavigation
}

Expand Down Expand Up @@ -1175,6 +1177,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.controlDeviceBackNavigation) {
return ControlBackNavigation.from(payload: payload);
} else if (actionType == ActionType.rateApp) {
Expand Down
1 change: 1 addition & 0 deletions modules/ensemble/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ dependencies:
shared_preferences: ^2.1.1
workmanager: ^0.5.1
flutter_local_notifications: ^17.2.3
image_gallery_saver: ^2.0.3
flutter_i18n: ^0.35.1
pointer_interceptor: ^0.9.3+4
flutter_secure_storage: ^9.2.2
Expand Down

0 comments on commit d6a7f4c

Please sign in to comment.