diff --git a/modules/ensemble/lib/framework/definition_providers/ensemble_provider.dart b/modules/ensemble/lib/framework/definition_providers/ensemble_provider.dart index 8a4cce43c..1c6516d2d 100644 --- a/modules/ensemble/lib/framework/definition_providers/ensemble_provider.dart +++ b/modules/ensemble/lib/framework/definition_providers/ensemble_provider.dart @@ -292,6 +292,11 @@ class AppModel { FirebaseFirestore db = FirebaseFirestore.instanceFor(app: app); return db.collection('apps').doc(appId).collection('artifacts'); } + CollectionReference> _getInternalArtifacts() { + final app = Ensemble().ensembleFirebaseApp!; + FirebaseFirestore db = FirebaseFirestore.instanceFor(app: app); + return db.collection('apps').doc(appId).collection('internal_artifacts'); + } /// App bundle for now only expects the theme, but we'll use this /// opportunity to also cache the home page Future getAppBundle() async { @@ -317,34 +322,49 @@ class AppModel { Map code = {}; Map output = {}; Map widgets = {}; + QuerySnapshot> snapshot = await _getInternalArtifacts() + .where('isArchived', isEqualTo: false) + .get(); + for (var doc in snapshot.docs) { + var type = doc.data()['type']; + var name = doc.data()['name']; + var content = doc.data()['content']; + if (type == ArtifactType.internal_widget.name) { + YamlMap yamlContent = await loadYaml(content); + widgets[name] = yamlContent["Widget"]; + } + if (type == ArtifactType.internal_script.name) { + code[name] = content; + } + } - YamlMap? resources = artifactCache[ArtifactType.resources.name]; - resources?.forEach((key, value) { - if (key == ResourceArtifactEntry.Widgets.name) { - if (value is YamlMap) { - widgets.addAll(value.value); - } - } else if (key == ResourceArtifactEntry.Scripts.name) { - if (value is YamlMap) { - //code will be in the format - - // Scripts: - // #apiUtils is the name of the code artifact - // apiUtils: |- - // function callAPI(name,payload) { - // ensemble.invokeAPI(name, payload); - // } - // #common is the name of the code artifact - // common: |- - // function sayHello() { - // return 'hello'; - // } - code.addAll(value.value); - } - } else { - // copy over non-Widgets - output[key] = value; - } - }); + // YamlMap? resources = artifactCache[ArtifactType.resources.name]; + // resources?.forEach((key, value) { + // if (key == ResourceArtifactEntry.Widgets.name) { + // if (value is YamlMap) { + // widgets.addAll(value.value); + // } + // } else if (key == ResourceArtifactEntry.Scripts.name) { + // if (value is YamlMap) { + // //code will be in the format - + // // Scripts: + // // #apiUtils is the name of the code artifact + // // apiUtils: |- + // // function callAPI(name,payload) { + // // ensemble.invokeAPI(name, payload); + // // } + // // #common is the name of the code artifact + // // common: |- + // // function sayHello() { + // // return 'hello'; + // // } + // code.addAll(value.value); + // } + // } else { + // // copy over non-Widgets + // output[key] = value; + // } + // }); // go through each imported App to include their widgets with proper namespace for (String appId in importCache.keys) { diff --git a/modules/ensemble/lib/framework/definition_providers/provider.dart b/modules/ensemble/lib/framework/definition_providers/provider.dart index 7ebb352d3..899300a4f 100644 --- a/modules/ensemble/lib/framework/definition_providers/provider.dart +++ b/modules/ensemble/lib/framework/definition_providers/provider.dart @@ -18,7 +18,9 @@ enum ArtifactType { i18n, resources, // global widgets/codes/APIs/ config, // app config - secrets + secrets, + internal_script, + internal_widget } // the root entries of the Resource artifact diff --git a/modules/ensemble/lib/framework/definition_providers/remote_provider.dart b/modules/ensemble/lib/framework/definition_providers/remote_provider.dart index 7675a714e..c7a701d07 100644 --- a/modules/ensemble/lib/framework/definition_providers/remote_provider.dart +++ b/modules/ensemble/lib/framework/definition_providers/remote_provider.dart @@ -1,9 +1,12 @@ import 'dart:async'; +import 'dart:convert'; import 'dart:ui'; import 'package:ensemble/ensemble.dart'; import 'package:ensemble/framework/definition_providers/provider.dart'; import 'package:ensemble/framework/widget/screen.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_i18n/flutter_i18n.dart'; import 'package:flutter_i18n/loaders/decoders/yaml_decode_strategy.dart'; import 'package:http/http.dart' as http; @@ -43,7 +46,7 @@ class RemoteDefinitionProvider extends FileDefinitionProvider { completer.complete(res); return completer.future; } - http.Response response = await http.get(Uri.parse('$path$screen.yaml')); + http.Response response = await http.get(Uri.parse('${path}screens/${screen}.yaml')); if (response.statusCode == 200) { dynamic res = ScreenDefinition(loadYaml(response.body)); if (cacheEnabled) { @@ -58,16 +61,30 @@ class RemoteDefinitionProvider extends FileDefinitionProvider { @override Future getAppBundle({bool? bypassCache = false}) async { - final env = await _readYamlFile('appConfig.yaml'); + dynamic env = await _readFileAsString('config/appConfig.json'); + if(env == null){ + env = await _readYamlFile('appConfig.yaml'); + } + else{ + env = json.decode(env); + } if (env != null) { appConfig = UserAppConfig( baseUrl: path, envVariables: env as Map, ); } + YamlMap? theme = await _readYamlFile('theme.yaml'); + if(theme == null) { + theme = await _readYamlFile('theme.ensemble'); + } + Map? resources = await getCombinedAppBundle(); + if (resources == null) { + resources = await _readYamlFile('resources.ensemble'); + } return AppBundle( - theme: await _readYamlFile('theme.ensemble'), - resources: await _readYamlFile('resources.ensemble')); + theme: theme, + resources: resources); } Future _readYamlFile(String file) async { @@ -82,6 +99,83 @@ class RemoteDefinitionProvider extends FileDefinitionProvider { return null; } + Future _readFileAsString(String file) async { + try { + http.Response response = await http.get(Uri.parse(path + file)); + if (response.statusCode == 200) { + return response.body; + } + } catch (error) { + // ignore + } + return null; + } + + Future getCombinedAppBundle() async { + Map code = {}; + Map output = {}; + Map widgets = {}; + + try { + // Get the manifest content + final manifestContent = + await http.get(Uri.parse(path + '.manifest.json')); + final Map manifestMap = json.decode(manifestContent.body); + + // Process App Widgets + try { + if (manifestMap['widgets'] != null) { + final List> widgetsList = + List>.from(manifestMap['widgets']); + + for (var widgetItem in widgetsList) { + try { + // Load the widget content in YamlMap + final widgetContent = + await _readYamlFile("widgets/${widgetItem["name"]}.yaml"); + if (widgetContent is YamlMap) { + widgets[widgetItem["name"]] = widgetContent["Widget"]; + } else { + debugPrint('Content in ${widgetItem["name"]} is not a YamlMap'); + } + } catch (e) { + // ignore error + } + } + } + } catch (e) { + debugPrint('Error processing widgets: $e'); + } + + // Process App Scripts + try { + if (manifestMap['scripts'] != null) { + final List> scriptsList = + List>.from(manifestMap['scripts']); + + for (var script in scriptsList) { + try { + // Load the script content in string + final scriptContent = await http.get(Uri.parse("${path}scripts/${script["name"]}.js")); + code[script["name"]] = scriptContent.body; + } catch (e) { + // ignore error + } + } + } + } catch (e) { + debugPrint('Error processing scripts: $e'); + } + + output[ResourceArtifactEntry.Widgets.name] = widgets; + output[ResourceArtifactEntry.Scripts.name] = code; + + return output; + } catch (e) { + return null; + } + } + @override UserAppConfig? getAppConfig() { return appConfig; diff --git a/modules/ensemble/lib/framework/model.dart b/modules/ensemble/lib/framework/model.dart index b4e4d6075..2f854eb44 100644 --- a/modules/ensemble/lib/framework/model.dart +++ b/modules/ensemble/lib/framework/model.dart @@ -39,7 +39,13 @@ class BackgroundImage { imageProvider = NetworkImage(_source); } } else { + final localSource = Utils.getLocalAssetFullPath(_source); + if(Utils.isUrl(localSource)){ + imageProvider = NetworkImage(localSource); + } + else{ imageProvider = AssetImage(Utils.getLocalAssetFullPath(_source)); + } } return DecorationImage( image: imageProvider, @@ -72,6 +78,17 @@ class BackgroundImage { fallbackWidget != null ? (_, __, ___) => fallbackWidget : null, ); } else { + final localSource = Utils.getLocalAssetFullPath(_source); + if(Utils.isUrl(localSource)){ + return CachedNetworkImage( + imageUrl: localSource, + fit: _fit, + alignment: _alignment, + errorWidget: + fallbackWidget != null ? (_, __, ___) => fallbackWidget : null, + ); + } + else{ return Image.asset( Utils.getLocalAssetFullPath(_source), fit: _fit, @@ -79,6 +96,7 @@ class BackgroundImage { errorBuilder: fallbackWidget != null ? (_, __, ___) => fallbackWidget : null, ); + } } } } diff --git a/modules/ensemble/lib/framework/secrets.dart b/modules/ensemble/lib/framework/secrets.dart index eced87243..d251468b3 100644 --- a/modules/ensemble/lib/framework/secrets.dart +++ b/modules/ensemble/lib/framework/secrets.dart @@ -7,6 +7,7 @@ import 'package:ensemble/framework/storage_manager.dart'; import 'package:ensemble_ts_interpreter/invokables/invokable.dart'; import 'package:flutter/services.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; +import 'package:http/http.dart' as http; import '../ensemble.dart'; @@ -42,6 +43,16 @@ class SecretsStore with Invokable { secrets![key] = value; }); } + else if(provider == 'remote') { + String path = + EnsembleConfigService.config["definitions"]?['remote']?["path"]; + final secretsString = + await http.get(Uri.parse('${path}/config/secrets.json')); + final Map appSecretsMap = json.decode(secretsString.body); + appSecretsMap["secrets"].forEach((key, value) { + secrets![key] = value; + }); + } } catch (_) {} // add secrets from env try { diff --git a/modules/ensemble/lib/util/utils.dart b/modules/ensemble/lib/util/utils.dart index 20a1a9d6b..297a2ca2b 100644 --- a/modules/ensemble/lib/util/utils.dart +++ b/modules/ensemble/lib/util/utils.dart @@ -1093,7 +1093,13 @@ static BoxDecoration? getBoxDecoration(dynamic style) { String path = EnsembleConfigService.config["definitions"]?['local']?["path"]; return '${path}/assets/${stripQueryParamsFromAsset(asset)}'; - } else { + } + else if (provider == 'remote'){ + String path = + EnsembleConfigService.config["definitions"]?['remote']?["path"]; + return '${path}/assets/${stripQueryParamsFromAsset(asset)}'; + } + else { return 'ensemble/assets/${stripQueryParamsFromAsset(asset)}'; } } catch (e) { diff --git a/modules/ensemble/lib/widget/image.dart b/modules/ensemble/lib/widget/image.dart index 34ca9e7d3..bc46cf893 100644 --- a/modules/ensemble/lib/widget/image.dart +++ b/modules/ensemble/lib/widget/image.dart @@ -261,14 +261,24 @@ class ImageState extends EWidgetState { fit: fit, errorBuilder: (context, error, stacktrace) => errorFallback()); } else { - // user might use env variables to switch between remote and local images. - // Assets might have additional token e.g. my-image.png?x=2343 - // so we need to strip them out - return Image.asset(Utils.getLocalAssetFullPath(widget._controller.source), - width: widget._controller.width?.toDouble(), - height: widget._controller.height?.toDouble(), - fit: fit, - errorBuilder: (context, error, stacktrace) => errorFallback()); + var localSource = Utils.getLocalAssetFullPath(widget._controller.source); + if (localSource.startsWith('https://') || + localSource.startsWith('http://')) { + return Image.network(localSource, + width: widget._controller.width?.toDouble(), + height: widget._controller.height?.toDouble(), + fit: fit, + errorBuilder: (context, error, stacktrace) => errorFallback()); + } else { + // user might use env variables to switch between remote and local images. + // Assets might have additional token e.g. my-image.png?x=2343 + // so we need to strip them out + return Image.asset(Utils.getLocalAssetFullPath(widget._controller.source), + width: widget._controller.width?.toDouble(), + height: widget._controller.height?.toDouble(), + fit: fit, + errorBuilder: (context, error, stacktrace) => errorFallback()); + } } } @@ -305,12 +315,29 @@ class ImageState extends EWidgetState { ), ); } - // attempt local assets - return SvgPicture.asset( - Utils.getLocalAssetFullPath(widget._controller.source), + + var localSource = Utils.getLocalAssetFullPath(widget._controller.source); + if (localSource.startsWith('https://') || + localSource.startsWith('http://')) { + return SvgPicture.network( + localSource, width: widget._controller.width?.toDouble(), height: widget._controller.height?.toDouble(), - fit: fit ?? BoxFit.contain); + fit: fit ?? BoxFit.contain, + placeholderBuilder: (_) => ColoredBoxPlaceholder( + color: widget._controller.placeholderColor, + width: widget._controller.width?.toDouble(), + height: widget._controller.height?.toDouble(), + ), + ); + } else { + // attempt local assets + return SvgPicture.asset( + Utils.getLocalAssetFullPath(widget._controller.source), + width: widget._controller.width?.toDouble(), + height: widget._controller.height?.toDouble(), + fit: fit ?? BoxFit.contain); + } } bool isSvg() { diff --git a/modules/ensemble/lib/widget/image_cropper.dart b/modules/ensemble/lib/widget/image_cropper.dart index aaffe559a..402abe614 100644 --- a/modules/ensemble/lib/widget/image_cropper.dart +++ b/modules/ensemble/lib/widget/image_cropper.dart @@ -313,6 +313,14 @@ class EnsembleImageCropperState extends EWidgetState fit: fit, ).image; } else { + final localSource = Utils.getLocalAssetFullPath(widget._controller.source); + if (Utils.isUrl(localSource)) { + return Image.network( + localSource, + width: widget._controller.width?.toDouble(), + height: widget._controller.height?.toDouble(), + ).image; + } return Image.asset( Utils.getLocalAssetFullPath(widget._controller.source), width: widget._controller.width?.toDouble(), @@ -336,8 +344,12 @@ class EnsembleImageCropperState extends EWidgetState return Svg(widget._controller.source, source: SvgSource.network); } // attempt local assets + final localSource = Utils.getLocalAssetFullPath(widget._controller.source); + if (Utils.isUrl(localSource)) { + return Svg(localSource, source: SvgSource.network); + } return Svg( - Utils.getLocalAssetFullPath(widget._controller.source), + localSource, source: SvgSource.asset, ); } diff --git a/modules/ensemble/lib/widget/lottie/native/lottiestate.dart b/modules/ensemble/lib/widget/lottie/native/lottiestate.dart index e49b505a4..bae2cdee7 100644 --- a/modules/ensemble/lib/widget/lottie/native/lottiestate.dart +++ b/modules/ensemble/lib/widget/lottie/native/lottiestate.dart @@ -98,6 +98,19 @@ class LottieState extends EWidgetState } // else attempt local asset else { + final localSource = Utils.getLocalAssetFullPath(widget.controller.source); + if (Utils.isUrl(localSource)) { + return Lottie.network(localSource, + controller: widget.controller.lottieController, + onLoaded: (composition) { + widget.controller.initializeLottieController(composition); + }, + width: widget.controller.width?.toDouble(), + height: widget.controller.height?.toDouble(), + repeat: widget.controller.repeat, + fit: fit, + errorBuilder: (context, error, stacktrace) => placeholderImage()); + } return Lottie.asset( Utils.getLocalAssetFullPath(widget.controller.source), controller: widget.controller.lottieController, diff --git a/modules/ensemble/lib/widget/lottie/web/lottiestate.dart b/modules/ensemble/lib/widget/lottie/web/lottiestate.dart index 8a311483e..01e5c3047 100644 --- a/modules/ensemble/lib/widget/lottie/web/lottiestate.dart +++ b/modules/ensemble/lib/widget/lottie/web/lottiestate.dart @@ -277,7 +277,21 @@ class LottieState extends EWidgetState ); } // else attempt local asset - + final localSource = Utils.getLocalAssetFullPath(widget.controller.source); + if (Utils.isUrl(localSource)) { + return Lottie.network( + localSource, + controller: widget.controller.lottieController, + onLoaded: (composition) { + widget.controller.initializeLottieController(composition); + }, + width: widget.controller.width?.toDouble(), + height: widget.controller.height?.toDouble(), + repeat: widget.controller.repeat, + fit: fit, + errorBuilder: (context, error, stacktrace) => placeholderImage(), + ); + } return Lottie.asset(Utils.getLocalAssetFullPath(widget.controller.source), controller: widget.controller.lottieController, onLoaded: (LottieComposition composition) { diff --git a/modules/ensemble/lib/widget/video.dart b/modules/ensemble/lib/widget/video.dart index 3958c8d18..01ecf72ff 100644 --- a/modules/ensemble/lib/widget/video.dart +++ b/modules/ensemble/lib/widget/video.dart @@ -132,6 +132,17 @@ class MyController extends WidgetController { notifyListeners(); }); } else { + final localSource = Utils.getLocalAssetFullPath(value); + if(Utils.isUrl(localSource)){ + _playerController = VideoPlayerController.networkUrl(Uri.parse(localSource)) + ..initialize().then((_) { + VideoPlayerValue value = _playerController!.value; + log(value.toString()); + setupPlayer(); + notifyListeners(); + }); + } + else{ _playerController = VideoPlayerController.asset(Utils.getLocalAssetFullPath(value)) ..initialize().then((_) { @@ -140,6 +151,7 @@ class MyController extends WidgetController { setupPlayer(); notifyListeners(); }); + } } _playerController!.addListener(() {