diff --git a/packages/system/vyuh_core/lib/plugin_types/di_plugin.dart b/packages/system/vyuh_core/lib/plugin_types/di/di_plugin.dart similarity index 100% rename from packages/system/vyuh_core/lib/plugin_types/di_plugin.dart rename to packages/system/vyuh_core/lib/plugin_types/di/di_plugin.dart diff --git a/packages/system/vyuh_core/lib/plugin_types/plugin_di_get_it.dart b/packages/system/vyuh_core/lib/plugin_types/di/plugin_di_get_it.dart similarity index 100% rename from packages/system/vyuh_core/lib/plugin_types/plugin_di_get_it.dart rename to packages/system/vyuh_core/lib/plugin_types/di/plugin_di_get_it.dart diff --git a/packages/system/vyuh_core/lib/plugin_types/console_logger_plugin.dart b/packages/system/vyuh_core/lib/plugin_types/logger/console_logger_plugin.dart similarity index 100% rename from packages/system/vyuh_core/lib/plugin_types/console_logger_plugin.dart rename to packages/system/vyuh_core/lib/plugin_types/logger/console_logger_plugin.dart diff --git a/packages/system/vyuh_core/lib/plugin_types/logger_plugin.dart b/packages/system/vyuh_core/lib/plugin_types/logger/logger_plugin.dart similarity index 100% rename from packages/system/vyuh_core/lib/plugin_types/logger_plugin.dart rename to packages/system/vyuh_core/lib/plugin_types/logger/logger_plugin.dart diff --git a/packages/system/vyuh_core/lib/plugin_types/network/http_network_plugin.dart b/packages/system/vyuh_core/lib/plugin_types/network/http_network_plugin.dart new file mode 100644 index 0000000..f843e17 --- /dev/null +++ b/packages/system/vyuh_core/lib/plugin_types/network/http_network_plugin.dart @@ -0,0 +1,60 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; +import 'package:vyuh_core/vyuh_core.dart'; + +final class HttpNetworkPlugin extends NetworkPlugin { + late Client _client; + var _initialized = false; + + HttpNetworkPlugin() + : super(name: 'vyuh.plugin.network.http', title: 'HTTP Network Plugin'); + + @override + Future init() async { + if (_initialized) { + return; + } + + _client = Client(); + _initialized = true; + } + + @override + Future dispose() async { + if (!_initialized) { + return; + } + + _client.close(); + _initialized = false; + } + + @override + Future get(Uri url, {Map? headers}) => + _client.get(url, headers: headers); + + @override + Future head(Uri url, {Map? headers}) => + _client.head(url, headers: headers); + + @override + Future post(Uri url, + {Map? headers, Object? body, Encoding? encoding}) => + _client.post(url, headers: headers, body: body, encoding: encoding); + + @override + Future put(Uri url, + {Map? headers, Object? body, Encoding? encoding}) => + _client.put(url, headers: headers, body: body, encoding: encoding); + + @override + Future delete(Uri url, + {Map? headers, Object? body, Encoding? encoding}) => + _client.delete(url, headers: headers, body: body, encoding: encoding); + + @override + Future patch(Uri url, + {Map? headers, Object? body, Encoding? encoding}) => + _client.patch(url, headers: headers, body: body, encoding: encoding); +} diff --git a/packages/system/vyuh_core/lib/plugin_types/network/network_plugin.dart b/packages/system/vyuh_core/lib/plugin_types/network/network_plugin.dart new file mode 100644 index 0000000..237dd1e --- /dev/null +++ b/packages/system/vyuh_core/lib/plugin_types/network/network_plugin.dart @@ -0,0 +1,25 @@ +import 'dart:convert'; + +import 'package:http/http.dart'; +import 'package:vyuh_core/vyuh_core.dart'; + +abstract base class NetworkPlugin extends Plugin { + NetworkPlugin({required super.name, required super.title}) + : super(pluginType: PluginType.network); + + Future get(Uri url, {Map? headers}); + + Future head(Uri url, {Map? headers}); + + Future post(Uri url, + {Map? headers, Object? body, Encoding? encoding}); + + Future put(Uri url, + {Map? headers, Object? body, Encoding? encoding}); + + Future delete(Uri url, + {Map? headers, Object? body, Encoding? encoding}); + + Future patch(Uri url, + {Map? headers, Object? body, Encoding? encoding}); +} diff --git a/packages/system/vyuh_core/lib/runtime/platform/default_platform.dart b/packages/system/vyuh_core/lib/runtime/platform/default_platform.dart index a08c652..69a7a6f 100644 --- a/packages/system/vyuh_core/lib/runtime/platform/default_platform.dart +++ b/packages/system/vyuh_core/lib/runtime/platform/default_platform.dart @@ -172,8 +172,8 @@ final class DefaultVyuhPlatform extends VyuhPlatform { debugLogDiagnostics: kDebugMode, observers: analytics.observers, errorBuilder: (_, state) => widgetBuilder.routeErrorView( - path: state.matchedLocation, title: 'Failed to load route', + subtitle: state.matchedLocation, error: state.error, onRetry: () { vyuh.tracker.init(tracker.currentState.value); @@ -230,6 +230,7 @@ extension on PluginType { AnalyticsPlugin(providers: [NoOpAnalyticsProvider()]), PluginType.content => NoOpContentPlugin(), PluginType.di => GetItDIPlugin(), + PluginType.network => HttpNetworkPlugin(), _ => null }; } diff --git a/packages/system/vyuh_core/lib/runtime/platform/default_platform_widget_builder.dart b/packages/system/vyuh_core/lib/runtime/platform/default_platform_widget_builder.dart index 164f533..9cecc5d 100644 --- a/packages/system/vyuh_core/lib/runtime/platform/default_platform_widget_builder.dart +++ b/packages/system/vyuh_core/lib/runtime/platform/default_platform_widget_builder.dart @@ -51,17 +51,15 @@ final defaultPlatformWidgetBuilder = PlatformWidgetBuilder( error: error, retryLabel: retryLabel, onRetry: onRetry, - showRestart: showRestart, ), routeErrorView: ({ - required path, required title, onRetry, retryLabel, subtitle, error, }) => - ErrorView( + ErrorViewScaffold( title: title, subtitle: subtitle, error: error, diff --git a/packages/system/vyuh_core/lib/runtime/platform/error_view.dart b/packages/system/vyuh_core/lib/runtime/platform/error_view.dart index a46e8ff..059408a 100644 --- a/packages/system/vyuh_core/lib/runtime/platform/error_view.dart +++ b/packages/system/vyuh_core/lib/runtime/platform/error_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:vyuh_core/vyuh_core.dart'; -class ErrorView extends StatelessWidget { +class ErrorViewScaffold extends StatelessWidget { final String title; final String? subtitle; final dynamic error; @@ -10,7 +10,7 @@ class ErrorView extends StatelessWidget { final String? retryLabel; - const ErrorView({ + const ErrorViewScaffold({ super.key, this.title = 'Something is not right!', this.subtitle, @@ -42,6 +42,7 @@ class ErrorView extends StatelessWidget { color: textColor, size: 64, ), + Text(title, textAlign: TextAlign.center), if (subtitle != null) Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), @@ -90,3 +91,76 @@ class ErrorView extends StatelessWidget { ); } } + +class ErrorView extends StatelessWidget { + final String title; + final String? subtitle; + final dynamic error; + final VoidCallback? onRetry; + final String? retryLabel; + + const ErrorView({ + super.key, + this.title = 'Something is not right!', + this.subtitle, + this.error, + this.onRetry, + this.retryLabel, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + final textColor = theme.colorScheme.onSurface; + return ConstrainedBox( + constraints: const BoxConstraints(minHeight: 100, maxHeight: 200), + child: Card( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Icon( + Icons.hide_source_rounded, + color: textColor, + size: 32, + ), + Text(title, textAlign: TextAlign.center), + if (subtitle != null) + Padding( + padding: const EdgeInsets.symmetric(vertical: 8.0), + child: Text(subtitle!, + textAlign: TextAlign.center, + style: + theme.textTheme.titleMedium?.apply(color: textColor)), + ), + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Column( + children: [ + if (error != null) + Text( + error.toString(), + style: theme.textTheme.bodyMedium?.apply( + fontFamily: 'Courier', + fontWeightDelta: 2, + fontSizeDelta: 1, + color: textColor, + ), + ), + ], + ), + )), + if (onRetry != null) + Padding( + padding: const EdgeInsets.only(left: 20.0, right: 20, top: 20), + child: FilledButton( + onPressed: onRetry, child: Text(retryLabel ?? 'Retry')), + ), + ], + ), + ), + ); + } +} diff --git a/packages/system/vyuh_core/lib/runtime/platform/framework_init_view.dart b/packages/system/vyuh_core/lib/runtime/platform/framework_init_view.dart index d920d56..c77abc8 100644 --- a/packages/system/vyuh_core/lib/runtime/platform/framework_init_view.dart +++ b/packages/system/vyuh_core/lib/runtime/platform/framework_init_view.dart @@ -43,14 +43,13 @@ class _FrameworkInitViewState extends State { final child = status == null || status == FutureStatus.pending ? vyuh.widgetBuilder.appLoader() - : vyuh.widgetBuilder.errorView( + : vyuh.widgetBuilder.routeErrorView( title: 'Failed to load app', error: vyuh.tracker.error, retryLabel: 'Try Again', onRetry: () { vyuh.tracker.init(); }, - showRestart: false, ); final pendingApp = MaterialApp(home: child); diff --git a/packages/system/vyuh_core/lib/runtime/platform/vyuh_platform.dart b/packages/system/vyuh_core/lib/runtime/platform/vyuh_platform.dart index f5bab6b..73ba46e 100644 --- a/packages/system/vyuh_core/lib/runtime/platform/vyuh_platform.dart +++ b/packages/system/vyuh_core/lib/runtime/platform/vyuh_platform.dart @@ -29,6 +29,7 @@ abstract class VyuhPlatform { PluginType.content, PluginType.di, PluginType.analytics, + PluginType.network, // PluginType.storage, ]; @@ -58,6 +59,8 @@ extension NamedPlugins on VyuhPlatform { AnalyticsPlugin get analytics => ensurePlugin(PluginType.analytics); + NetworkPlugin get network => ensurePlugin(PluginType.network); + T ensurePlugin(PluginType type, {bool mustExist = true}) { final plugin = getPlugin(type) as T; diff --git a/packages/system/vyuh_core/lib/runtime/platform_widget_builder.dart b/packages/system/vyuh_core/lib/runtime/platform_widget_builder.dart index 9b93f25..30b7436 100644 --- a/packages/system/vyuh_core/lib/runtime/platform_widget_builder.dart +++ b/packages/system/vyuh_core/lib/runtime/platform_widget_builder.dart @@ -7,7 +7,6 @@ typedef RouteLoader = Widget Function([Uri? url, String? routeId]); typedef ImagePlaceholderBuilder = Widget Function( {double? width, double? height}); typedef RouteErrorViewBuilder = Widget Function({ - required String path, required String title, String? retryLabel, VoidCallback? onRetry, diff --git a/packages/system/vyuh_core/lib/vyuh_core.dart b/packages/system/vyuh_core/lib/vyuh_core.dart index e4ae6a1..50c46ad 100644 --- a/packages/system/vyuh_core/lib/vyuh_core.dart +++ b/packages/system/vyuh_core/lib/vyuh_core.dart @@ -11,14 +11,16 @@ export 'feature_descriptor.dart'; export 'plugin_types/analytics/analytics_plugin.dart'; export 'plugin_types/analytics/analytics_provider.dart'; export 'plugin_types/analytics/noop_analytics_provider.dart'; -export 'plugin_types/console_logger_plugin.dart'; export 'plugin_types/content/content_plugin.dart'; export 'plugin_types/content/content_provider.dart'; export 'plugin_types/content/noop_content_provider.dart'; -export 'plugin_types/di_plugin.dart'; -export 'plugin_types/logger_plugin.dart'; +export 'plugin_types/di/di_plugin.dart'; +export 'plugin_types/di/plugin_di_get_it.dart'; +export 'plugin_types/logger/console_logger_plugin.dart'; +export 'plugin_types/logger/logger_plugin.dart'; +export 'plugin_types/network/http_network_plugin.dart'; +export 'plugin_types/network/network_plugin.dart'; export 'plugin_types/plugin.dart'; -export 'plugin_types/plugin_di_get_it.dart'; export 'runtime/cms_route.dart'; export 'runtime/init_tracker.dart'; export 'runtime/platform/default_platform_widget_builder.dart'; diff --git a/packages/system/vyuh_core/pubspec.yaml b/packages/system/vyuh_core/pubspec.yaml index a0856bc..4ab17d6 100644 --- a/packages/system/vyuh_core/pubspec.yaml +++ b/packages/system/vyuh_core/pubspec.yaml @@ -21,6 +21,7 @@ dependencies: get_it: ^7.6.4 logger: ^2.1.0 flutter_sanity_portable_text: ^1.0.0-beta.4 + http: ^1.2.1 dev_dependencies: flutter_test: diff --git a/packages/system/vyuh_extension_content/lib/content/action.dart b/packages/system/vyuh_extension_content/lib/content/action.dart index f187c45..cff00aa 100644 --- a/packages/system/vyuh_extension_content/lib/content/action.dart +++ b/packages/system/vyuh_extension_content/lib/content/action.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:vyuh_core/vyuh_core.dart'; @@ -14,9 +16,9 @@ final class Action { static configurationList(dynamic json) => listFromJson(json); - void execute(BuildContext context) { + FutureOr execute(BuildContext context) async { for (final config in configurations ?? []) { - config.execute(context); + await config.execute(context); } } @@ -37,5 +39,5 @@ abstract class ActionConfiguration { this.title, }); - void execute(BuildContext context); + FutureOr execute(BuildContext context); } diff --git a/packages/system/vyuh_extension_content/lib/content_extension_builder.dart b/packages/system/vyuh_extension_content/lib/content_extension_builder.dart index 162287e..538d38f 100644 --- a/packages/system/vyuh_extension_content/lib/content_extension_builder.dart +++ b/packages/system/vyuh_extension_content/lib/content_extension_builder.dart @@ -24,6 +24,7 @@ final class ContentExtensionBuilder extends ExtensionBuilder { .expand((element) => element.contents ?? []) .groupListsBy((element) => element.schemaType); + // Collect the builders for (final entry in contentBuilders.entries) { assert(entry.value.length == 1, 'There can be only one ContentBuilder for a content-type. We found ${entry.value.length} for ${entry.key}'); @@ -31,15 +32,22 @@ final class ContentExtensionBuilder extends ExtensionBuilder { _contentBuilderMap[entry.key] = entry.value.first; } + // Ensure every ContentDescriptor has a ContentBuilder for (final entry in contents.entries) { final schemaType = entry.key; - final descriptors = entry.value; final builder = _contentBuilderMap[schemaType]; assert(builder != null, 'Missing ContentBuilder for ContentDescriptor of schemaType: $schemaType'); + } + + // Setup the builders + for (final entry in _contentBuilderMap.entries) { + final schemaType = entry.key; + final builder = entry.value; + final descriptors = contents[schemaType] ?? []; - builder?.init(descriptors); + builder.init(descriptors); } _initTypeRegistrations( diff --git a/packages/system/vyuh_extension_content/lib/ui/route_builder.dart b/packages/system/vyuh_extension_content/lib/ui/route_builder.dart index e318e74..36e0888 100644 --- a/packages/system/vyuh_extension_content/lib/ui/route_builder.dart +++ b/packages/system/vyuh_extension_content/lib/ui/route_builder.dart @@ -73,7 +73,7 @@ class _RouteFutureBuilderState extends State { vyuh.analytics.reportError(exception); - return vyuh.widgetBuilder.errorView( + return vyuh.widgetBuilder.routeErrorView( title: 'Failed to load route from CMS', error: exception, onRetry: _refresh, @@ -88,7 +88,7 @@ class _RouteFutureBuilderState extends State { case FutureStatus.rejected: vyuh.analytics.reportError(_tracker.value?.error); - return vyuh.widgetBuilder.errorView( + return vyuh.widgetBuilder.routeErrorView( title: errorMsg, error: _tracker.value?.error, onRetry: _refresh, diff --git a/packages/system/vyuh_feature_system/lib/action/conditional_action.dart b/packages/system/vyuh_feature_system/lib/action/conditional_action.dart index a1dc8ce..a1d3322 100644 --- a/packages/system/vyuh_feature_system/lib/action/conditional_action.dart +++ b/packages/system/vyuh_feature_system/lib/action/conditional_action.dart @@ -29,7 +29,7 @@ class ConditionalAction extends ActionConfiguration { _$ConditionalActionFromJson(json); @override - void execute(flutter.BuildContext context) async { + Future execute(flutter.BuildContext context) async { final value = (await condition?.execute()) ?? defaultCase; if (context.mounted) { diff --git a/packages/system/vyuh_feature_system/lib/api_handler/simple_api_handler.dart b/packages/system/vyuh_feature_system/lib/api_handler/simple_api_handler.dart new file mode 100644 index 0000000..f686fa5 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/api_handler/simple_api_handler.dart @@ -0,0 +1,114 @@ +import 'dart:convert'; + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:json_path/json_path.dart'; +import 'package:vyuh_core/vyuh_core.dart'; +import 'package:vyuh_feature_system/content/card/list_item_layout.dart'; +import 'package:vyuh_feature_system/vyuh_feature_system.dart' as vf; +import 'package:vyuh_feature_system/vyuh_feature_system.dart'; + +part 'simple_api_handler.g.dart'; + +@JsonSerializable() +final class DisplayFieldMap { + final JSONPath? title; + final JSONPath? description; + final JSONPath? imageUrl; + + DisplayFieldMap({this.title, this.description, this.imageUrl}); + + factory DisplayFieldMap.fromJson(Map json) => + _$DisplayFieldMapFromJson(json); +} + +extension type const JSONPath(String path) { + factory JSONPath.fromJson(dynamic value) => JSONPath(value); +} + +@JsonSerializable() +final class SimpleAPIHandler extends APIHandler> { + static const schemaName = 'vyuh.apiContent.handler.simple'; + + final String? title; + final String url; + final Map? headers; + final List<(String name, String value)>? requestBody; + + final JSONPath rootField; + final DisplayFieldMap? fieldMap; + + static final typeDescriptor = TypeDescriptor( + schemaType: SimpleAPIHandler.schemaName, + fromJson: SimpleAPIHandler.fromJson, + title: 'Simple API Handler', + ); + + SimpleAPIHandler({ + this.title, + this.url = '', + this.headers, + this.requestBody, + this.rootField = const JSONPath(r'$'), + this.fieldMap, + }) : super(schemaType: schemaName); + + factory SimpleAPIHandler.fromJson(Map json) => + _$SimpleAPIHandlerFromJson(json); + + @override + Future?> invoke(BuildContext context) async { + final response = await vyuh.network.get(Uri.parse(url)); + final json = jsonDecode(response.body); + + final rootItem = JsonPath(rootField.path).read(json); + if (rootItem.singleOrNull == null) return null; + + final root = rootItem.single.value; + + return switch (root) { + List() => root.map((e) => _createCard(e)).toList(), + Map() => [_createCard(root.values.first)], + _ => null + }; + } + + vf.Card _createCard(Map json) { + final fields = { + 'title': fieldMap?.title, + 'description': fieldMap?.description, + 'imageUrl': fieldMap?.imageUrl, + }.entries.where((x) => x.value != null).map((e) { + final value = + JsonPath(e.value!.path).read(json).singleOrNull?.value as String?; + return MapEntry(e.key, value); + }).fold({}, (previousValue, element) { + previousValue[element.key] = element.value; + return previousValue; + }); + + return vf.Card( + title: fields['title'], + description: fields['description'], + imageUrl: + fields['imageUrl'] != null ? Uri.parse(fields['imageUrl']!) : null, + layout: ListItemCardLayout(title: 'List Item'), + ); + } + + @override + Widget buildData(BuildContext context, List? data) { + return data == null ? empty : _buildCardList(data); + } + + _buildCardList(List data) { + return LimitedBox( + maxHeight: 200, + child: ListView.builder( + itemCount: data.length, + itemBuilder: (context, index) => + vyuh.content.buildContent(context, data[index])), + ); + } +} diff --git a/packages/system/vyuh_feature_system/lib/api_handler/simple_api_handler.g.dart b/packages/system/vyuh_feature_system/lib/api_handler/simple_api_handler.g.dart new file mode 100644 index 0000000..cbf11f4 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/api_handler/simple_api_handler.g.dart @@ -0,0 +1,47 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'simple_api_handler.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +DisplayFieldMap _$DisplayFieldMapFromJson(Map json) => + DisplayFieldMap( + title: json['title'] == null ? null : JSONPath.fromJson(json['title']), + description: json['description'] == null + ? null + : JSONPath.fromJson(json['description']), + imageUrl: + json['imageUrl'] == null ? null : JSONPath.fromJson(json['imageUrl']), + ); + +SimpleAPIHandler _$SimpleAPIHandlerFromJson(Map json) => + SimpleAPIHandler( + title: json['title'] as String?, + url: json['url'] as String? ?? '', + headers: (json['headers'] as Map?)?.map( + (k, e) => MapEntry(k, e as String), + ), + requestBody: (json['requestBody'] as List?) + ?.map((e) => _$recordConvert( + e, + ($jsonValue) => ( + $jsonValue[r'$1'] as String, + $jsonValue[r'$2'] as String, + ), + )) + .toList(), + rootField: json['rootField'] == null + ? const JSONPath(r'$') + : JSONPath.fromJson(json['rootField']), + fieldMap: json['fieldMap'] == null + ? null + : DisplayFieldMap.fromJson(json['fieldMap'] as Map), + ); + +$Rec _$recordConvert<$Rec>( + Object? value, + $Rec Function(Map) convert, +) => + convert(value as Map); diff --git a/packages/system/vyuh_feature_system/lib/content/api_content.dart b/packages/system/vyuh_feature_system/lib/content/api_content.dart new file mode 100644 index 0000000..1d895ee --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/api_content.dart @@ -0,0 +1,123 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:vyuh_core/vyuh_core.dart'; +import 'package:vyuh_extension_content/vyuh_extension_content.dart'; +import 'package:vyuh_feature_system/content/empty.dart'; + +part 'api_content.g.dart'; + +@JsonSerializable() +final class APIContent extends ContentItem { + static const schemaName = 'vyuh.apiContent'; + + final bool showPending; + final bool showError; + + @JsonKey(fromJson: typeFromFirstOfListJson) + final APIHandler? handler; + + factory APIContent.fromJson(Map json) => + _$APIContentFromJson(json); + + APIContent({ + this.showError = true, + this.showPending = true, + this.handler, + }) : super(schemaType: APIContent.schemaName); +} + +abstract base class APIHandler { + final String schemaType; + + APIHandler({required this.schemaType}); + + Future invoke(BuildContext context); + + Widget buildData(BuildContext context, T? data); +} + +class APIContentDescriptor extends ContentDescriptor { + final List>? handlers; + + APIContentDescriptor({this.handlers}) + : super(schemaType: APIContent.schemaName, title: 'API Content'); +} + +final class APIContentBuilder extends ContentBuilder { + APIContentBuilder() + : super( + content: TypeDescriptor( + schemaType: APIContent.schemaName, + title: 'API Content', + fromJson: APIContent.fromJson, + ), + defaultLayout: DefaultAPIContentLayout(), + defaultLayoutDescriptor: DefaultAPIContentLayout.typeDescriptor, + ); + + @override + void init(List descriptors) { + super.init(descriptors); + + final apiHandlers = descriptors.cast().expand( + (element) => element.handlers ?? >[]); + + for (final handler in apiHandlers) { + vyuh.content.register(handler); + } + } +} + +final class DefaultAPIContentLayout extends LayoutConfiguration { + static const schemaName = '${APIContent.schemaName}.layout.default'; + static final typeDescriptor = TypeDescriptor( + schemaType: schemaName, + title: 'Default APIContent Layout', + fromJson: DefaultAPIContentLayout.fromJson, + ); + + DefaultAPIContentLayout() : super(schemaType: schemaName); + + factory DefaultAPIContentLayout.fromJson(Map json) => + DefaultAPIContentLayout(); + + @override + Widget build(BuildContext context, APIContent content) { + return FutureBuilder( + future: content.handler?.invoke(context), + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (content.showPending && + (snapshot.connectionState == ConnectionState.waiting || + snapshot.connectionState == ConnectionState.active)) { + // Showing a loading spinner during API call + return vyuh.widgetBuilder.contentLoader(); + } else if (snapshot.connectionState == ConnectionState.done) { + if (content.showError && snapshot.hasError) { + // Show error if API call resulted in an error + return vyuh.widgetBuilder.errorView( + title: 'API Error', + subtitle: + 'Handler in context was "${content.handler?.schemaType}"', + error: snapshot.error, + showRestart: false, + ); + } else { + // Show data when API call is successful + return content.handler?.buildData(context, snapshot.data) ?? empty; + } + } else { + // In case, the future is neither in progress nor done. + return kDebugMode + ? vyuh.widgetBuilder.errorView( + title: 'API Error', + error: + 'Something went wrong invoking the api with ${content.handler?.schemaType}', + showRestart: false, + ) + : empty; + } + }, + ); + } +} diff --git a/packages/system/vyuh_feature_system/lib/content/api_content.g.dart b/packages/system/vyuh_feature_system/lib/content/api_content.g.dart new file mode 100644 index 0000000..5231696 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/api_content.g.dart @@ -0,0 +1,13 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'api_content.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +APIContent _$APIContentFromJson(Map json) => APIContent( + showError: json['showError'] as bool? ?? true, + showPending: json['showPending'] as bool? ?? true, + handler: typeFromFirstOfListJson(json['handler']), + ); diff --git a/packages/system/vyuh_feature_system/lib/content/card/card.dart b/packages/system/vyuh_feature_system/lib/content/card/card.dart index 479f8bb..c11c3d9 100644 --- a/packages/system/vyuh_feature_system/lib/content/card/card.dart +++ b/packages/system/vyuh_feature_system/lib/content/card/card.dart @@ -19,6 +19,7 @@ class Card extends ContentItem implements PortableBlockItem { final String? title; final String? description; final ImageReference? image; + final Uri? imageUrl; final PortableTextContent? content; final vx.Action? action; @@ -30,6 +31,7 @@ class Card extends ContentItem implements PortableBlockItem { required this.description, this.content, this.image, + this.imageUrl, this.action, this.secondaryAction, this.tertiaryAction, diff --git a/packages/system/vyuh_feature_system/lib/content/card/card.g.dart b/packages/system/vyuh_feature_system/lib/content/card/card.g.dart index 6f8c881..032f2f0 100644 --- a/packages/system/vyuh_feature_system/lib/content/card/card.g.dart +++ b/packages/system/vyuh_feature_system/lib/content/card/card.g.dart @@ -16,6 +16,9 @@ Card _$CardFromJson(Map json) => Card( image: json['image'] == null ? null : ImageReference.fromJson(json['image'] as Map), + imageUrl: json['imageUrl'] == null + ? null + : Uri.parse(json['imageUrl'] as String), action: json['action'] == null ? null : Action.fromJson(json['action'] as Map), diff --git a/packages/system/vyuh_feature_system/lib/content/card/default_layout.dart b/packages/system/vyuh_feature_system/lib/content/card/default_layout.dart index 17a617a..73e45bb 100644 --- a/packages/system/vyuh_feature_system/lib/content/card/default_layout.dart +++ b/packages/system/vyuh_feature_system/lib/content/card/default_layout.dart @@ -41,9 +41,10 @@ class DefaultCardLayout extends LayoutConfiguration { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (content.image?.asset?.ref != null) + if (content.image?.asset?.ref != null || content.imageUrl != null) Flexible( child: e.ContentImage( + url: content.imageUrl?.toString(), ref: content.image?.asset?.ref, fit: BoxFit.contain, ), diff --git a/packages/system/vyuh_feature_system/lib/content/card/list_item_layout.dart b/packages/system/vyuh_feature_system/lib/content/card/list_item_layout.dart index c0ec590..a728f48 100644 --- a/packages/system/vyuh_feature_system/lib/content/card/list_item_layout.dart +++ b/packages/system/vyuh_feature_system/lib/content/card/list_item_layout.dart @@ -26,42 +26,45 @@ class ListItemCardLayout extends LayoutConfiguration { child: Card( child: Padding( padding: const EdgeInsets.all(8.0), - child: Column( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Row( - children: [ - if (content.image?.asset?.ref != null) - Container( - clipBehavior: Clip.antiAlias, - decoration: - BoxDecoration(borderRadius: BorderRadius.circular(8)), - height: 64, - width: 128, - child: sys.ContentImage( - ref: content.image?.asset?.ref, - fit: BoxFit.contain, + if (content.image?.asset?.ref != null || content.imageUrl != null) + Container( + clipBehavior: Clip.antiAlias, + decoration: + BoxDecoration(borderRadius: BorderRadius.circular(8)), + height: 64, + width: 92, + child: sys.ContentImage( + url: content.imageUrl?.toString(), + ref: content.image?.asset?.ref, + fit: BoxFit.fitWidth, + ), + ), + const SizedBox(width: 8), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (content.title != null) + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + content.title!, + style: theme.textTheme.bodyLarge, + ), ), - ), - if (content.title != null) - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Text( - content.title!, - style: theme.textTheme.bodyLarge, + if (content.description != null) + Text( + content.description!, + style: theme.textTheme.bodyMedium, ), - )), - const Icon(Icons.chevron_right_rounded) - ], - ), - if (content.description != null) - Padding( - padding: const EdgeInsets.symmetric(vertical: 4.0), - child: Text( - content.description!, - style: theme.textTheme.bodyMedium, - ), + ], ), + ), + if (content.action != null) + const Icon(Icons.chevron_right_rounded) ], ), ), diff --git a/packages/system/vyuh_feature_system/lib/content/index.dart b/packages/system/vyuh_feature_system/lib/content/index.dart index 9d09a78..262787c 100644 --- a/packages/system/vyuh_feature_system/lib/content/index.dart +++ b/packages/system/vyuh_feature_system/lib/content/index.dart @@ -1,4 +1,5 @@ export 'accordion.dart'; +export 'api_content.dart'; export 'card/card.dart'; export 'conditional.dart'; export 'divider.dart'; diff --git a/packages/system/vyuh_feature_system/lib/feature.dart b/packages/system/vyuh_feature_system/lib/feature.dart index 73833aa..c40d0a3 100644 --- a/packages/system/vyuh_feature_system/lib/feature.dart +++ b/packages/system/vyuh_feature_system/lib/feature.dart @@ -26,12 +26,11 @@ final feature = FeatureDescriptor( path: '/__system_error__', pageBuilder: (context, state) { return MaterialPage( - child: vyuh.widgetBuilder.errorView( + child: vyuh.widgetBuilder.routeErrorView( title: 'System error', error: state.extra.toString(), onRetry: () => vyuh.tracker.init(), retryLabel: 'Restart', - showRestart: false, ), ); }, @@ -156,7 +155,9 @@ final feature = FeatureDescriptor( ], ), DividerDescriptor(), - AccordionDescriptor() + APIContentDescriptor( + handlers: [SimpleAPIHandler.typeDescriptor], + ), ], contentBuilders: [ RouteContentBuilder(), @@ -169,6 +170,7 @@ final feature = FeatureDescriptor( PortableTextContentBuilder(), DividerContentBuilder(), AccordionContentBuilder(), + APIContentBuilder(), ], conditions: [ TypeDescriptor( diff --git a/packages/system/vyuh_feature_system/lib/vyuh_feature_system.dart b/packages/system/vyuh_feature_system/lib/vyuh_feature_system.dart index ce442e4..d1c0a8b 100644 --- a/packages/system/vyuh_feature_system/lib/vyuh_feature_system.dart +++ b/packages/system/vyuh_feature_system/lib/vyuh_feature_system.dart @@ -2,6 +2,7 @@ library vyuh_feature_system; export 'action/conditional_action.dart'; export 'action/navigation.dart'; +export 'api_handler/simple_api_handler.dart'; export 'condition/boolean.dart'; export 'content/index.dart'; export 'feature.dart'; diff --git a/packages/system/vyuh_feature_system/pubspec.yaml b/packages/system/vyuh_feature_system/pubspec.yaml index 18c70dd..6d2b2ce 100644 --- a/packages/system/vyuh_feature_system/pubspec.yaml +++ b/packages/system/vyuh_feature_system/pubspec.yaml @@ -26,6 +26,7 @@ dependencies: vyuh_core: ^1.0.0-beta.1 vyuh_extension_content: ^1.0.0-beta.1 vyuh_extension_script: ^1.0.0-beta.1 + json_path: ^0.7.1 dev_dependencies: flutter_test: