Skip to content

Commit

Permalink
Merge branch 'main' into enhance/camera-frame
Browse files Browse the repository at this point in the history
  • Loading branch information
snehmehta authored Jan 15, 2025
2 parents 8cc4e00 + bfb7511 commit dddacc0
Show file tree
Hide file tree
Showing 54 changed files with 4,419 additions and 272 deletions.
49 changes: 49 additions & 0 deletions .github/workflows/starter-commands.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Test Commands in Starter

on:
pull_request:
branches:
- main

jobs:
test-modules:
runs-on: ubuntu-latest
timeout-minutes: 20
defaults:
run:
working-directory: starter

steps:
- name: Checkout repository
uses: actions/checkout@v2

- name: Set up Flutter SDK
uses: subosito/flutter-action@v2
with:
channel: 'stable'

- name: Set up Node.js
uses: actions/setup-node@v2

- name: Install dependencies
run: npm install

- name: Run hasCamera command
run: npm run hasCamera platform="ios,android" cameraDescription="Hello world"
continue-on-error: false

- name: Run hasFileManager command
run: npm run hasFileManager platform="ios,android" photoLibraryDescription="Hello" musicDescription="world"
continue-on-error: false

- name: Run hasContacts command
run: npm run hasContacts contactsDescription="Hello world" platform="ios,android"
continue-on-error: false

- name: Run hasConnect command
run: npm run hasConnect platform="ios,android" cameraDescription="Hello world" contactsDescription="Hello world"
continue-on-error: false

- name: Run hasLocation command
run: npm run hasLocation platform="ios,android" locationDescription="Hello world" alwaysUseLocationDescription="Hello world" inUseLocationDescription="Hello world"
continue-on-error: false
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ pubspec_overrides.yaml
/starter/ios/Flutter
starter/ios/Runner.xcodeproj/project.pbxproj
/node_modules
starter/node_modules
starter/package-lock.json

# FVM Version Cache
.fvm/
.fvm/
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.takeScreenshot
ActionType.saveFile,
ActionType.controlDeviceBackNavigation,
ActionType.closeApp,
Expand Down
142 changes: 129 additions & 13 deletions modules/ensemble/lib/action/misc_action.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:rate_my_app/rate_my_app.dart';
import 'package:share_plus/share_plus.dart';
import 'package:flutter/foundation.dart';
import 'package:mime/mime.dart';

class CopyToClipboardAction extends EnsembleAction {
CopyToClipboardAction(this._value,
Expand Down Expand Up @@ -57,29 +59,143 @@ class CopyToClipboardAction extends EnsembleAction {
}
}

/// Share a text (an optionally a title) to external Apps
/// Share text and files (an optionally a title) to external Apps
class ShareAction extends EnsembleAction {
ShareAction(this._text, {String? title}) : _title = title;
ShareAction(this._text, {String? title, dynamic files})
: _title = title,
_files = files;
String? _title;
dynamic _text;
dynamic _files;

factory ShareAction.from({Map? payload}) {
if (payload == null || payload['text'] == null) {
throw LanguageError("${ActionType.share.name} requires 'text'");
if (payload == null ||
(payload['text'] == null && payload['files'] == null)) {
throw LanguageError(
"${ActionType.share.name} requires 'text' or 'files'");
}
return ShareAction(payload['text'], title: payload['title']?.toString());

return ShareAction(
payload['text'],
title: payload['title']?.toString(),
files: payload['files'],
);
}

// Helper method to create XFile from file data
XFile? createXFile(dynamic file) {
final mimeType =
lookupMimeType(file["path"] ?? '', headerBytes: file["bytes"]) ??
'application/octet-stream';
try {
if (file is Map) {
// Handle file with path
if (file['path'] != null && file['path'].toString().isNotEmpty) {
final String path = file['path'].toString();
final String name = file['name']?.toString() ?? path.split('/').last;
return XFile(path, name: name, mimeType: mimeType);
}

// Handle file with bytes (web)
if (file.containsKey('bytes') && file['bytes'] != null) {
final String name = file['name']?.toString() ?? 'file';

return XFile.fromData(
file['bytes'],
name: name,
mimeType: mimeType,
);
}
} else if (file is String) {
// Handle simple file path string
return XFile(file, name: file.split('/').last, mimeType: mimeType);
}
} catch (e) {
debugPrint('Error creating XFile: $e');
}
return null;
}

@override
Future execute(BuildContext context, ScopeManager scopeManager) {
final box = context.findRenderObject() as RenderBox?;
Future<void> execute(BuildContext context, ScopeManager scopeManager) async {
try {
final box = context.findRenderObject() as RenderBox?;
if (box == null) return;

Share.share(
scopeManager.dataContext.eval(_text),
subject: Utils.optionalString(scopeManager.dataContext.eval(_title)),
sharePositionOrigin: box!.localToGlobal(Offset.zero) & box.size,
);
return Future.value(null);
final sharePositionOrigin = box.localToGlobal(Offset.zero) & box.size;
final evaluatedText =
scopeManager.dataContext.eval(_text)?.toString() ?? '';
final evaluatedTitle =
Utils.optionalString(scopeManager.dataContext.eval(_title));

// Handle file sharing
if (_files != null) {
final evaluatedFiles = scopeManager.dataContext.eval(_files);
if (evaluatedFiles != null) {
final filesList =
evaluatedFiles is List ? evaluatedFiles : [evaluatedFiles];
final List<XFile> xFiles = [];

// Create XFiles
for (var file in filesList) {
final xFile = createXFile(file);
if (xFile != null) {
xFiles.add(xFile);
}
}

// Share files if any were created successfully
if (xFiles.isNotEmpty) {
try {
final result = await Share.shareXFiles(
xFiles,
text: evaluatedText,
subject: evaluatedTitle ?? '',
sharePositionOrigin: sharePositionOrigin,
);

// Handle share result
if (result.status == ShareResultStatus.success) {
debugPrint('Share completed successfully: ${result.raw}');
} else {
debugPrint('Share completed with status: ${result.status}');
}
return;
} catch (e) {
debugPrint('Error sharing files: $e');
if (kIsWeb) {
// On web, fall back to sharing just the text
await Share.share(
evaluatedText,
subject: evaluatedTitle ?? '',
sharePositionOrigin: sharePositionOrigin,
);
return;
}
rethrow;
}
}
}
}

// Fall back to sharing just text if no files or file sharing failed
if (evaluatedText.isNotEmpty) {
final result = await Share.share(
evaluatedText,
subject: evaluatedTitle ?? '',
sharePositionOrigin: sharePositionOrigin,
);

if (result.status == ShareResultStatus.success) {
debugPrint('Text share completed successfully: ${result.raw}');
} else {
debugPrint('Text share completed with status: ${result.status}');
}
}
} catch (e) {
debugPrint('ShareAction failed: $e');
rethrow;
}
}
}

Expand Down
6 changes: 6 additions & 0 deletions modules/ensemble/lib/action/saveFile/download_stub.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// download_stub.dart
import 'dart:typed_data';

void downloadFileOnWeb(String fileName, Uint8List fileBytes) {
throw UnsupportedError('downloadFileOnWeb is not supported on this platform');
}
27 changes: 27 additions & 0 deletions modules/ensemble/lib/action/saveFile/download_web.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import 'dart:html' as html;

import 'package:flutter/foundation.dart';
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');
}
}
98 changes: 98 additions & 0 deletions modules/ensemble/lib/action/saveFile/save_file.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import 'dart:convert';
import 'dart:typed_data';

import 'package:ensemble/framework/action.dart';
import 'package:ensemble/framework/scope.dart';
import 'package:flutter/material.dart';
import 'package:ensemble/framework/error_handling.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import 'save_mobile.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
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.');
}

// Save the file to the storage system
await _saveFile(type!, fileName!, fileBytes);
} catch (e) {
throw Exception('Failed to save file: $e');
}
}

Future<void> _saveFile(
String type, String fileName, Uint8List fileBytes) async {
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);
}
}

/// 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'],
);
}
}
Loading

0 comments on commit dddacc0

Please sign in to comment.