Skip to content

Commit

Permalink
Merge branch 'main' into enable-modules-with-scripts
Browse files Browse the repository at this point in the history
  • Loading branch information
sharjeelyunus authored Jan 8, 2025
2 parents ecdf74c + 76d0d99 commit b33a7ca
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 1 deletion.
2 changes: 2 additions & 0 deletions modules/ensemble/lib/action/action_invokable.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ abstract class ActionInvokable with Invokable {
ActionType.dismissDialog,
ActionType.closeAllDialogs,
ActionType.executeActionGroup,
ActionType.saveFile,
ActionType.controlDeviceBackNavigation,
ActionType.closeApp,
]);
}

Expand Down
24 changes: 24 additions & 0 deletions modules/ensemble/lib/action/close_app.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import 'package:ensemble/framework/action.dart';
import 'package:ensemble/framework/scope.dart';
import 'package:ensemble/util/utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:ensemble_ts_interpreter/invokables/invokable.dart';

class CloseAppAction extends EnsembleAction {
CloseAppAction({
super.initiator,
});

factory CloseAppAction.from({Invokable? initiator, Map? payload}) {
return CloseAppAction(
initiator: initiator,
);
}

@override
Future execute(BuildContext context, ScopeManager scopeManager) {
SystemNavigator.pop();
return Future.value(null);
}
}
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'],
);
}
}
10 changes: 9 additions & 1 deletion modules/ensemble/lib/framework/action.dart
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ 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';
import 'package:ensemble/action/disable_hardware_navigation.dart';
import 'package:ensemble/action/close_app.dart';
import 'package:ensemble/ensemble.dart';
import 'package:ensemble/framework/data_context.dart';
import 'package:ensemble/framework/error_handling.dart';
Expand Down Expand Up @@ -1055,7 +1057,9 @@ enum ActionType {
bluetoothDisconnect,
bluetoothSubscribeCharacteristic,
bluetoothUnsubscribeCharacteristic,
controlDeviceBackNavigation
controlDeviceBackNavigation,
closeApp,
saveFile
}

/// payload representing an Action to do (navigateToScreen, InvokeAPI, ..)
Expand Down Expand Up @@ -1175,12 +1179,16 @@ 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) {
return RateAppAction.from(payload: payload);
} else if (actionType == ActionType.getDeviceToken) {
return GetDeviceTokenAction.fromMap(payload: payload);
} else if (actionType == ActionType.closeApp) {
return CloseAppAction();
} else if (actionType == ActionType.openPlaidLink) {
return PlaidLinkAction.fromYaml(initiator: initiator, payload: payload);
} else if (actionType == ActionType.openAppSettings) {
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 b33a7ca

Please sign in to comment.