From 422771075931e008404ff9d8f299fb655c7b2aa6 Mon Sep 17 00:00:00 2001 From: pavanpodila Date: Wed, 20 Mar 2024 13:01:55 +0530 Subject: [PATCH] adding vyuh_feature_system and vyuh_extension_script packages --- .github/workflows/dart.yml | 2 + .github/workflows/publish-dart.yml | 2 + apps/vyuh_demo/lib/main.dart | 2 +- apps/vyuh_demo/pubspec.lock | 28 +-- apps/vyuh_demo/pubspec.yaml | 2 +- .../vyuh_core/lib/feature_descriptor.dart | 6 +- .../system/vyuh_extension_script/.gitignore | 29 +++ .../system/vyuh_extension_script/.metadata | 10 + .../system/vyuh_extension_script/CHANGELOG.md | 5 + packages/system/vyuh_extension_script/LICENSE | 20 ++ .../system/vyuh_extension_script/README.md | 39 ++++ .../analysis_options.yaml | 4 + .../system/vyuh_extension_script/build.yaml | 22 ++ .../vyuh_extension_script/lib/js_script.dart | 34 +++ .../lib/js_script.g.dart | 13 ++ .../lib/script_extension.dart | 35 +++ .../lib/script_runtime.dart | 39 ++++ .../lib/vyuh_extension_script.dart | 4 + .../lib/web_script_runtime.dart | 10 + .../system/vyuh_extension_script/pubspec.yaml | 31 +++ .../test/vyuh_extension_script_test.dart | 1 + .../system/vyuh_feature_system/.gitignore | 30 +++ packages/system/vyuh_feature_system/.metadata | 10 + .../system/vyuh_feature_system/CHANGELOG.md | 14 ++ packages/system/vyuh_feature_system/LICENSE | 20 ++ packages/system/vyuh_feature_system/README.md | 39 ++++ .../system/vyuh_feature_system/build.yaml | 22 ++ .../lib/action/navigation.dart | 106 +++++++++ .../lib/action/navigation.g.dart | 31 +++ .../lib/condition/boolean.dart | 24 ++ .../lib/condition/boolean.g.dart | 12 + .../lib/content/accordion.dart | 158 +++++++++++++ .../lib/content/accordion.g.dart | 23 ++ .../lib/content/card/card.dart | 61 +++++ .../lib/content/card/card.g.dart | 29 +++ .../lib/content/card/default_layout.dart | 75 +++++++ .../lib/content/card/default_layout.g.dart | 12 + .../lib/content/card/list_item_layout.dart | 71 ++++++ .../lib/content/card/list_item_layout.g.dart | 12 + .../lib/content/conditional.dart | 119 ++++++++++ .../lib/content/conditional.g.dart | 23 ++ .../lib/content/divider.dart | 70 ++++++ .../lib/content/divider.g.dart | 12 + .../lib/content/empty.dart | 52 +++++ .../lib/content/empty.g.dart | 9 + .../lib/content/group/grid_layout.dart | 59 +++++ .../lib/content/group/grid_layout.g.dart | 13 ++ .../lib/content/group/group.dart | 73 ++++++ .../lib/content/group/group.g.dart | 20 ++ .../lib/content/index.dart | 13 ++ .../content/portable_text/invoke_action.dart | 16 ++ .../portable_text/invoke_action.g.dart | 14 ++ .../content/portable_text/portable_text.dart | 178 +++++++++++++++ .../portable_text/portable_text.g.dart | 18 ++ .../lib/content/reference.dart | 30 +++ .../lib/content/reference.g.dart | 20 ++ .../lib/content/route/conditional_route.dart | 112 ++++++++++ .../content/route/conditional_route.g.dart | 32 +++ .../lib/content/route/default_layout.dart | 47 ++++ .../lib/content/route/default_layout.g.dart | 10 + .../lib/content/route/default_route.dart | 19 ++ .../lib/content/route/dialog_layout.dart | 37 +++ .../lib/content/route/dialog_layout.g.dart | 19 ++ .../lib/content/route/route.dart | 141 ++++++++++++ .../lib/content/route/route.g.dart | 34 +++ .../lib/content/route/route_container.dart | 29 +++ .../lib/content/route/route_type.dart | 50 +++++ .../lib/content/route/route_type.g.dart | 30 +++ .../lib/content/route/sliver_layout.dart | 42 ++++ .../lib/content/route/sliver_layout.g.dart | 10 + .../lib/content/route/tabs.dart | 65 ++++++ .../lib/content/route/tabs.g.dart | 21 ++ .../lib/content/unknown.dart | 61 +++++ .../lib/content/web_view.dart | 52 +++++ .../vyuh_feature_system/lib/feature.dart | 211 ++++++++++++++++++ .../vyuh_feature_system/lib/ui/carousel.dart | 54 +++++ .../lib/ui/content_image.dart | 95 ++++++++ .../lib/ui/dialog_page.dart | 76 +++++++ .../lib/ui/press_effect.dart | 56 +++++ .../lib/vyuh_feature_system.dart | 10 + .../system/vyuh_feature_system/pubspec.yaml | 37 +++ .../test/vyuh_feature_essentials_test.dart | 1 + 82 files changed, 3158 insertions(+), 19 deletions(-) create mode 100644 packages/system/vyuh_extension_script/.gitignore create mode 100644 packages/system/vyuh_extension_script/.metadata create mode 100644 packages/system/vyuh_extension_script/CHANGELOG.md create mode 100644 packages/system/vyuh_extension_script/LICENSE create mode 100644 packages/system/vyuh_extension_script/README.md create mode 100644 packages/system/vyuh_extension_script/analysis_options.yaml create mode 100644 packages/system/vyuh_extension_script/build.yaml create mode 100644 packages/system/vyuh_extension_script/lib/js_script.dart create mode 100644 packages/system/vyuh_extension_script/lib/js_script.g.dart create mode 100644 packages/system/vyuh_extension_script/lib/script_extension.dart create mode 100644 packages/system/vyuh_extension_script/lib/script_runtime.dart create mode 100644 packages/system/vyuh_extension_script/lib/vyuh_extension_script.dart create mode 100644 packages/system/vyuh_extension_script/lib/web_script_runtime.dart create mode 100644 packages/system/vyuh_extension_script/pubspec.yaml create mode 100644 packages/system/vyuh_extension_script/test/vyuh_extension_script_test.dart create mode 100644 packages/system/vyuh_feature_system/.gitignore create mode 100644 packages/system/vyuh_feature_system/.metadata create mode 100644 packages/system/vyuh_feature_system/CHANGELOG.md create mode 100644 packages/system/vyuh_feature_system/LICENSE create mode 100644 packages/system/vyuh_feature_system/README.md create mode 100644 packages/system/vyuh_feature_system/build.yaml create mode 100644 packages/system/vyuh_feature_system/lib/action/navigation.dart create mode 100644 packages/system/vyuh_feature_system/lib/action/navigation.g.dart create mode 100644 packages/system/vyuh_feature_system/lib/condition/boolean.dart create mode 100644 packages/system/vyuh_feature_system/lib/condition/boolean.g.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/accordion.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/accordion.g.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/card/card.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/card/card.g.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/card/default_layout.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/card/default_layout.g.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/card/list_item_layout.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/card/list_item_layout.g.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/conditional.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/conditional.g.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/divider.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/divider.g.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/empty.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/empty.g.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/group/grid_layout.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/group/grid_layout.g.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/group/group.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/group/group.g.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/index.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/portable_text/invoke_action.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/portable_text/invoke_action.g.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/portable_text/portable_text.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/portable_text/portable_text.g.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/reference.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/reference.g.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/route/conditional_route.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/route/conditional_route.g.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/route/default_layout.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/route/default_layout.g.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/route/default_route.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/route/dialog_layout.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/route/dialog_layout.g.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/route/route.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/route/route.g.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/route/route_container.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/route/route_type.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/route/route_type.g.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/route/sliver_layout.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/route/sliver_layout.g.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/route/tabs.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/route/tabs.g.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/unknown.dart create mode 100644 packages/system/vyuh_feature_system/lib/content/web_view.dart create mode 100644 packages/system/vyuh_feature_system/lib/feature.dart create mode 100644 packages/system/vyuh_feature_system/lib/ui/carousel.dart create mode 100644 packages/system/vyuh_feature_system/lib/ui/content_image.dart create mode 100644 packages/system/vyuh_feature_system/lib/ui/dialog_page.dart create mode 100644 packages/system/vyuh_feature_system/lib/ui/press_effect.dart create mode 100644 packages/system/vyuh_feature_system/lib/vyuh_feature_system.dart create mode 100644 packages/system/vyuh_feature_system/pubspec.yaml create mode 100644 packages/system/vyuh_feature_system/test/vyuh_feature_essentials_test.dart diff --git a/.github/workflows/dart.yml b/.github/workflows/dart.yml index 9d7daaf0..25203c38 100644 --- a/.github/workflows/dart.yml +++ b/.github/workflows/dart.yml @@ -21,6 +21,8 @@ jobs: - packages/system/vyuh_cache - packages/system/vyuh_core - packages/system/vyuh_feature_developer + - packages/system/vyuh_feature_system + - packages/system/vyuh_extension_script - packages/sanity/sanity_client - packages/sanity/flutter_sanity_portable_text diff --git a/.github/workflows/publish-dart.yml b/.github/workflows/publish-dart.yml index ee4df49e..fa32df7c 100644 --- a/.github/workflows/publish-dart.yml +++ b/.github/workflows/publish-dart.yml @@ -16,6 +16,8 @@ jobs: - packages/system/vyuh_cache - packages/system/vyuh_core - packages/system/vyuh_feature_developer + - packages/system/vyuh_feature_system + - packages/system/vyuh_extension_script - packages/sanity/flutter_sanity_portable_text - packages/sanity/sanity_client diff --git a/apps/vyuh_demo/lib/main.dart b/apps/vyuh_demo/lib/main.dart index 329ada73..161b3cb5 100644 --- a/apps/vyuh_demo/lib/main.dart +++ b/apps/vyuh_demo/lib/main.dart @@ -9,7 +9,7 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); vc.runApp( - initialLocation: '/', + initialLocation: '/counter', features: [ developer.feature, sample.featureLauncher, diff --git a/apps/vyuh_demo/pubspec.lock b/apps/vyuh_demo/pubspec.lock index 4d536b9c..9d5356da 100644 --- a/apps/vyuh_demo/pubspec.lock +++ b/apps/vyuh_demo/pubspec.lock @@ -186,18 +186,18 @@ packages: dependency: transitive description: name: go_router - sha256: "170c46e237d6eb0e6e9f0e8b3f56101e14fb64f787016e42edd74c39cf8b176a" + sha256: "7ecb2f391edbca5473db591b48555a8912dde60edd0fb3013bd6743033b2d3f8" url: "https://pub.dev" source: hosted - version: "13.2.0" + version: "13.2.1" http: dependency: transitive description: name: http - sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" http_parser: dependency: transitive description: @@ -431,10 +431,10 @@ packages: dependency: transitive description: name: sqflite_common - sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5" + sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "2.5.4" stack_trace: dependency: transitive description: @@ -529,7 +529,7 @@ packages: path: "../../packages/system/vyuh_core" relative: true source: path - version: "1.0.0-beta.1" + version: "1.0.0-beta.2" vyuh_extension_content: dependency: "direct main" description: @@ -543,23 +543,23 @@ packages: path: "../../packages/system/vyuh_feature_developer" relative: true source: path - version: "1.0.0-beta.1" + version: "1.0.0-beta.2" web: dependency: transitive description: name: web - sha256: "4188706108906f002b3a293509234588823c8c979dc83304e229ff400c996b05" + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "0.4.2" + version: "0.5.1" win32: dependency: transitive description: name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + sha256: "8cb58b45c47dcb42ab3651533626161d6b67a2921917d8d429791f76972b3480" url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.3.0" xdg_directories: dependency: transitive description: @@ -569,5 +569,5 @@ packages: source: hosted version: "1.0.4" sdks: - dart: ">=3.3.0-279.1.beta <4.0.0" - flutter: ">=3.13.0" + dart: ">=3.3.0 <4.0.0" + flutter: ">=3.16.0" diff --git a/apps/vyuh_demo/pubspec.yaml b/apps/vyuh_demo/pubspec.yaml index fe811e25..338c7f75 100644 --- a/apps/vyuh_demo/pubspec.yaml +++ b/apps/vyuh_demo/pubspec.yaml @@ -15,7 +15,7 @@ dependencies: flutter_mobx: ^2.2.0+2 vyuh_core: ^1.0.0-beta.1 vyuh_extension_content: ^1.0.0-beta.1 - vyuh_feature_developer: ^1.0.0-beta.1 + vyuh_feature_developer: ^1.0.0-beta.2 feature_sample: ^1.0.0 dev_dependencies: diff --git a/packages/system/vyuh_core/lib/feature_descriptor.dart b/packages/system/vyuh_core/lib/feature_descriptor.dart index 870e08c5..a3a0534c 100644 --- a/packages/system/vyuh_core/lib/feature_descriptor.dart +++ b/packages/system/vyuh_core/lib/feature_descriptor.dart @@ -17,7 +17,7 @@ final class FeatureDescriptor { final List? extensions; final List? extensionBuilders; - final RouteBuilderFunction? _routes; + final RouteBuilderFunction _routes; RouteBuilderFunction? get routes => _routes; FeatureDescriptor({ @@ -26,10 +26,10 @@ final class FeatureDescriptor { this.description, this.icon, this.init, - RouteBuilderFunction? routes, + required RouteBuilderFunction routes, this.extensions, this.extensionBuilders, - }) : _routes = routes == null ? null : _runOnce(routes); + }) : _routes = _runOnce(routes); } FutureOr Function() _runOnce(FutureOr Function() fn) { diff --git a/packages/system/vyuh_extension_script/.gitignore b/packages/system/vyuh_extension_script/.gitignore new file mode 100644 index 00000000..ac5aa989 --- /dev/null +++ b/packages/system/vyuh_extension_script/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/packages/system/vyuh_extension_script/.metadata b/packages/system/vyuh_extension_script/.metadata new file mode 100644 index 00000000..b10e4879 --- /dev/null +++ b/packages/system/vyuh_extension_script/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "ba393198430278b6595976de84fe170f553cc728" + channel: "stable" + +project_type: package diff --git a/packages/system/vyuh_extension_script/CHANGELOG.md b/packages/system/vyuh_extension_script/CHANGELOG.md new file mode 100644 index 00000000..0941008d --- /dev/null +++ b/packages/system/vyuh_extension_script/CHANGELOG.md @@ -0,0 +1,5 @@ +## 1.0.0-beta.1 + +* Initial release +* Contains support for running JavaScript code inside a Dart context using `flutter_js` +* Provides extensibility via the `ScriptExtensionDescriptor` diff --git a/packages/system/vyuh_extension_script/LICENSE b/packages/system/vyuh_extension_script/LICENSE new file mode 100644 index 00000000..afac5a3f --- /dev/null +++ b/packages/system/vyuh_extension_script/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2024 Vyuh.tech + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/system/vyuh_extension_script/README.md b/packages/system/vyuh_extension_script/README.md new file mode 100644 index 00000000..02fe8eca --- /dev/null +++ b/packages/system/vyuh_extension_script/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/packages/system/vyuh_extension_script/analysis_options.yaml b/packages/system/vyuh_extension_script/analysis_options.yaml new file mode 100644 index 00000000..a5744c1c --- /dev/null +++ b/packages/system/vyuh_extension_script/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/system/vyuh_extension_script/build.yaml b/packages/system/vyuh_extension_script/build.yaml new file mode 100644 index 00000000..4cc2a58f --- /dev/null +++ b/packages/system/vyuh_extension_script/build.yaml @@ -0,0 +1,22 @@ +targets: + $default: + builders: + json_serializable: + options: + # Options configure how source code is generated for every + # `@JsonSerializable`-annotated class in the package. + # + # The default value for each is listed. + any_map: false + checked: false + constructor: '' + create_factory: true + create_field_map: false + create_per_field_to_json: false + create_to_json: false + disallow_unrecognized_keys: false + explicit_to_json: false + field_rename: none + generic_argument_factories: false + ignore_unannotated: false + include_if_null: true diff --git a/packages/system/vyuh_extension_script/lib/js_script.dart b/packages/system/vyuh_extension_script/lib/js_script.dart new file mode 100644 index 00000000..92dc0704 --- /dev/null +++ b/packages/system/vyuh_extension_script/lib/js_script.dart @@ -0,0 +1,34 @@ +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 'script_runtime.dart' + if (dart.library.html) 'web_script_runtime.dart' as rt; + +part 'js_script.g.dart'; + +@JsonSerializable() +final class JavaScriptAction extends ActionConfiguration { + static const schemaName = 'vyuh.action.javascript'; + + static final typeDescriptor = TypeDescriptor( + schemaType: JavaScriptAction.schemaName, + title: 'JavaScript Action', + fromJson: JavaScriptAction.fromJson, + ); + + @JsonKey(defaultValue: '') + final String script; + + JavaScriptAction({required this.script, super.isAsync}) + : super(schemaType: schemaName); + + factory JavaScriptAction.fromJson(Map json) => + _$JavaScriptActionFromJson(json); + + @override + void execute(BuildContext context) { + rt.ScriptRuntime.evaluate(script, context); + } +} diff --git a/packages/system/vyuh_extension_script/lib/js_script.g.dart b/packages/system/vyuh_extension_script/lib/js_script.g.dart new file mode 100644 index 00000000..1fc82683 --- /dev/null +++ b/packages/system/vyuh_extension_script/lib/js_script.g.dart @@ -0,0 +1,13 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'js_script.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +JavaScriptAction _$JavaScriptActionFromJson(Map json) => + JavaScriptAction( + script: json['script'] as String? ?? '', + isAsync: json['isAsync'] as bool? ?? false, + ); diff --git a/packages/system/vyuh_extension_script/lib/script_extension.dart b/packages/system/vyuh_extension_script/lib/script_extension.dart new file mode 100644 index 00000000..b979f215 --- /dev/null +++ b/packages/system/vyuh_extension_script/lib/script_extension.dart @@ -0,0 +1,35 @@ +import 'dart:async'; + +import 'package:vyuh_core/vyuh_core.dart'; + +import 'script_runtime.dart' if (dart.library.html) 'web_script_runtime.dart' + as rt; + +final class ScriptExtensionDescriptor extends FeatureExtensionDescriptor { + final String name; + + String get runtime => 'javascript'; + + final FutureOr Function(dynamic args) function; + + ScriptExtensionDescriptor({required this.name, required this.function}) + : super(title: 'ScriptExtension: $name'); +} + +final class ScriptExtensionBuilder extends FeatureExtensionBuilder { + ScriptExtensionBuilder() + : super( + extensionType: ScriptExtensionDescriptor, title: 'ScriptExtension'); + + @override + void build(List extensions) { + for (final extension in extensions.cast()) { + rt.ScriptRuntime.registerFunction(extension.name, extension.function); + } + } + + @override + void init() { + rt.ScriptRuntime.init(); + } +} diff --git a/packages/system/vyuh_extension_script/lib/script_runtime.dart b/packages/system/vyuh_extension_script/lib/script_runtime.dart new file mode 100644 index 00000000..9bc6fb26 --- /dev/null +++ b/packages/system/vyuh_extension_script/lib/script_runtime.dart @@ -0,0 +1,39 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_js/flutter_js.dart'; + +final class ScriptRuntime { + static JavascriptRuntime? _runtime; + + static BuildContext? get currentBuildContext => + _runtime?.localContext['buildContext']; + + static void init() { + _runtime ??= getJavascriptRuntime(); + } + + static void registerFunction(String name, Function(dynamic args) function) { + _runtime?.onMessage(name, function); + } + + static Future evaluate(String script, BuildContext context, + {bool isAsync = false}) async { + if (_runtime == null) { + return; + } + + try { + _runtime!.localContext['buildContext'] = context; + if (isAsync) { + final result = await _runtime!.evaluateAsync(script); + _runtime!.executePendingJob(); + await _runtime!.handlePromise(result); + } else { + _runtime!.evaluate(script); + } + } catch (e) { + debugPrint(e.toString()); + } finally { + _runtime!.localContext['buildContext'] = null; + } + } +} diff --git a/packages/system/vyuh_extension_script/lib/vyuh_extension_script.dart b/packages/system/vyuh_extension_script/lib/vyuh_extension_script.dart new file mode 100644 index 00000000..3c26a4c9 --- /dev/null +++ b/packages/system/vyuh_extension_script/lib/vyuh_extension_script.dart @@ -0,0 +1,4 @@ +library vyuh_extension_script; + +export 'script_extension.dart'; +export 'script_runtime.dart'; diff --git a/packages/system/vyuh_extension_script/lib/web_script_runtime.dart b/packages/system/vyuh_extension_script/lib/web_script_runtime.dart new file mode 100644 index 00000000..c620fc5a --- /dev/null +++ b/packages/system/vyuh_extension_script/lib/web_script_runtime.dart @@ -0,0 +1,10 @@ +import 'package:flutter/widgets.dart'; + +final class ScriptRuntime { + static void registerFunction(String name, Function(dynamic args) function) {} + + static Future evaluate(String script, BuildContext context, + {bool isAsync = false}) async {} + + static void init() {} +} diff --git a/packages/system/vyuh_extension_script/pubspec.yaml b/packages/system/vyuh_extension_script/pubspec.yaml new file mode 100644 index 00000000..b7a901fb --- /dev/null +++ b/packages/system/vyuh_extension_script/pubspec.yaml @@ -0,0 +1,31 @@ +name: vyuh_extension_script +description: A scripting extension for the Vyuh Framework +version: 1.0.0-beta.1 +homepage: https://vyuh.tech +repository: https://github.com/vyuh-tech/vyuh/tree/main/packages/system/vyuh_extension_script +issue_tracker: https://github.com/vyuh-tech/vyuh/issues + +environment: + sdk: '>=3.3.1 <4.0.0' + flutter: ">=3.16.0" + +dependencies: + flutter: + sdk: flutter + flutter_js: ^0.8.0 + vyuh_core: ^1.0.0-beta.1 + vyuh_extension_content: ^1.0.0-beta.1 + json_annotation: ^4.8.1 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 + json_serializable: ^6.7.1 + build_runner: ^2.4.6 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: diff --git a/packages/system/vyuh_extension_script/test/vyuh_extension_script_test.dart b/packages/system/vyuh_extension_script/test/vyuh_extension_script_test.dart new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/packages/system/vyuh_extension_script/test/vyuh_extension_script_test.dart @@ -0,0 +1 @@ + diff --git a/packages/system/vyuh_feature_system/.gitignore b/packages/system/vyuh_feature_system/.gitignore new file mode 100644 index 00000000..96486fd9 --- /dev/null +++ b/packages/system/vyuh_feature_system/.gitignore @@ -0,0 +1,30 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/packages/system/vyuh_feature_system/.metadata b/packages/system/vyuh_feature_system/.metadata new file mode 100644 index 00000000..a8883f7d --- /dev/null +++ b/packages/system/vyuh_feature_system/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "ff5b5b5fa6f35b717667719ddfdb1521d8bdd05a" + channel: "stable" + +project_type: package diff --git a/packages/system/vyuh_feature_system/CHANGELOG.md b/packages/system/vyuh_feature_system/CHANGELOG.md new file mode 100644 index 00000000..3e25ddb6 --- /dev/null +++ b/packages/system/vyuh_feature_system/CHANGELOG.md @@ -0,0 +1,14 @@ +## 1.0.0-beta.1 + +- Initial release. +- Contains the essential building blocks of any CMS-driven UI such as + - Card + - Group + - Route + - Conditional Route + - Portable Text + - Web View + - Unknown + - Divider + - Actions such as navigation +- Has example for adding more widgets such as `Accordion` diff --git a/packages/system/vyuh_feature_system/LICENSE b/packages/system/vyuh_feature_system/LICENSE new file mode 100644 index 00000000..afac5a3f --- /dev/null +++ b/packages/system/vyuh_feature_system/LICENSE @@ -0,0 +1,20 @@ +MIT License + +Copyright (c) 2024 Vyuh.tech + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/system/vyuh_feature_system/README.md b/packages/system/vyuh_feature_system/README.md new file mode 100644 index 00000000..a0a95a98 --- /dev/null +++ b/packages/system/vyuh_feature_system/README.md @@ -0,0 +1,39 @@ + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to start +using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/packages/system/vyuh_feature_system/build.yaml b/packages/system/vyuh_feature_system/build.yaml new file mode 100644 index 00000000..4cc2a58f --- /dev/null +++ b/packages/system/vyuh_feature_system/build.yaml @@ -0,0 +1,22 @@ +targets: + $default: + builders: + json_serializable: + options: + # Options configure how source code is generated for every + # `@JsonSerializable`-annotated class in the package. + # + # The default value for each is listed. + any_map: false + checked: false + constructor: '' + create_factory: true + create_field_map: false + create_per_field_to_json: false + create_to_json: false + disallow_unrecognized_keys: false + explicit_to_json: false + field_rename: none + generic_argument_factories: false + ignore_unannotated: false + include_if_null: true diff --git a/packages/system/vyuh_feature_system/lib/action/navigation.dart b/packages/system/vyuh_feature_system/lib/action/navigation.dart new file mode 100644 index 00000000..d3ce7025 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/action/navigation.dart @@ -0,0 +1,106 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.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/vyuh_feature_system.dart' as vf; +import 'package:vyuh_feature_system/vyuh_feature_system.dart'; + +part 'navigation.g.dart'; + +enum NavigationType { go, push } + +enum LinkType { url, route } + +@JsonSerializable() +final class NavigationAction extends ActionConfiguration { + static const schemaName = 'vyuh.action.navigation'; + static final typeDescriptor = TypeDescriptor( + schemaType: NavigationAction.schemaName, + title: 'Navigation Action', + fromJson: NavigationAction.fromJson, + ); + + final LinkType linkType; + final String? url; + final ObjectReference? route; + + final NavigationType navigationType; + + NavigationAction({ + this.navigationType = NavigationType.push, + this.linkType = LinkType.url, + this.route, + this.url, + super.title, + }) : super(schemaType: schemaName); + + factory NavigationAction.fromJson(Map json) => + _$NavigationActionFromJson(json); + + @override + Future execute(BuildContext context) async { + assert(url != null || route != null, 'One of url or route must be set.'); + + if (linkType == LinkType.route && route != null) { + return _performNavigation(context, routeId: route!.ref); + } + + final uri = Uri.parse(url!); + + if (uri.scheme.startsWith('http')) { + context.push('/__system_navigate__', extra: uri); + + return; + } + + final localRoute = vyuh.router.configuration.findMatch(uri.toString()); + var isLocal = localRoute.routes.any((route) => route is CMSRoute) == false; + + if (isLocal) { + navigationType.apply(context, uri.toString()); + return; + } + + _performNavigation(context, uri: uri); + } + + _performNavigation(BuildContext context, {Uri? uri, String? routeId}) async { + final state = Overlay.of(context); + final entry = OverlayEntry( + builder: (_) => vyuh.widgetBuilder.routeLoader(uri, routeId)); + state.insert(entry); + + try { + final routeResult = await vyuh.content.provider + .fetchRoute(path: uri?.toString(), routeId: routeId); + final route = await routeResult?.init(); + + final path = (route as vf.Route?)?.path; + if (path == null) { + throw ArgumentError( + 'Unable to determine path from route. Tried with uri: ${uri.toString()}, routeId: $routeId'); + } + + if (!context.mounted) { + return; + } + + navigationType.apply(context, path, route); + } catch (e) { + context.push('/__system_error__', extra: e); + } finally { + entry.remove(); + } + } +} + +extension on NavigationType { + void apply(BuildContext context, String path, [vf.Route? route]) { + if (this == NavigationType.push) { + context.push(path, extra: route); + } else { + context.go(path, extra: route); + } + } +} diff --git a/packages/system/vyuh_feature_system/lib/action/navigation.g.dart b/packages/system/vyuh_feature_system/lib/action/navigation.g.dart new file mode 100644 index 00000000..3074e953 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/action/navigation.g.dart @@ -0,0 +1,31 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'navigation.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +NavigationAction _$NavigationActionFromJson(Map json) => + NavigationAction( + navigationType: $enumDecodeNullable( + _$NavigationTypeEnumMap, json['navigationType']) ?? + NavigationType.push, + linkType: $enumDecodeNullable(_$LinkTypeEnumMap, json['linkType']) ?? + LinkType.url, + route: json['route'] == null + ? null + : ObjectReference.fromJson(json['route'] as Map), + url: json['url'] as String?, + title: json['title'] as String?, + ); + +const _$NavigationTypeEnumMap = { + NavigationType.go: 'go', + NavigationType.push: 'push', +}; + +const _$LinkTypeEnumMap = { + LinkType.url: 'url', + LinkType.route: 'route', +}; diff --git a/packages/system/vyuh_feature_system/lib/condition/boolean.dart b/packages/system/vyuh_feature_system/lib/condition/boolean.dart new file mode 100644 index 00000000..2f4e4300 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/condition/boolean.dart @@ -0,0 +1,24 @@ +import 'dart:async'; + +import 'package:json_annotation/json_annotation.dart'; +import 'package:vyuh_extension_content/vyuh_extension_content.dart'; + +part 'boolean.g.dart'; + +@JsonSerializable() +final class BooleanCondition extends ConditionConfiguration { + static const schemaName = 'vyuh.condition.boolean'; + + @JsonKey(defaultValue: false) + final bool value; + + BooleanCondition({this.value = false}) : super(schemaType: schemaName); + + factory BooleanCondition.fromJson(Map json) => + _$BooleanConditionFromJson(json); + + @override + Future execute() async { + return value.toString(); + } +} diff --git a/packages/system/vyuh_feature_system/lib/condition/boolean.g.dart b/packages/system/vyuh_feature_system/lib/condition/boolean.g.dart new file mode 100644 index 00000000..9616292a --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/condition/boolean.g.dart @@ -0,0 +1,12 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'boolean.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +BooleanCondition _$BooleanConditionFromJson(Map json) => + BooleanCondition( + value: json['value'] as bool? ?? false, + ); diff --git a/packages/system/vyuh_feature_system/lib/content/accordion.dart b/packages/system/vyuh_feature_system/lib/content/accordion.dart new file mode 100644 index 00000000..498079ee --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/accordion.dart @@ -0,0 +1,158 @@ +import 'package:collection/collection.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/vyuh_feature_system.dart'; + +part 'accordion.g.dart'; + +@JsonSerializable() +final class Accordion extends ContentItem { + static const schemaName = 'vyuh.accordion'; + + final String? title; + final String? description; + + @JsonKey(defaultValue: []) + final List items; + + Accordion({this.title, this.description, required this.items}) + : super(schemaType: schemaName); + + factory Accordion.fromJson(Map json) => + _$AccordionFromJson(json); +} + +@JsonSerializable() +final class AccordionItem { + @JsonKey(defaultValue: '') + final String title; + + final String? iconIdentifier; + + @JsonKey(fromJson: typeFromFirstOfListJson) + final ContentItem? content; + + AccordionItem({ + required this.title, + this.iconIdentifier, + required this.content, + }); + + factory AccordionItem.fromJson(Map json) => + _$AccordionItemFromJson(json); +} + +class AccordionDescriptor extends ContentDescriptor { + AccordionDescriptor({super.layouts}) + : super(schemaType: Accordion.schemaName, title: 'Accordion'); +} + +final class AccordionContentBuilder extends ContentBuilder { + AccordionContentBuilder() + : super( + content: TypeDescriptor( + schemaType: Accordion.schemaName, + title: 'Accordion', + fromJson: Accordion.fromJson), + defaultLayout: DefaultAccordionLayout(), + defaultLayoutDescriptor: DefaultAccordionLayout.typeDescriptor, + ); +} + +final class DefaultAccordionLayout extends LayoutConfiguration { + static const schemaName = '${Accordion.schemaName}.layout.default'; + static final typeDescriptor = TypeDescriptor( + schemaType: schemaName, + title: 'Default Accordion Layout', + fromJson: DefaultAccordionLayout.fromJson, + ); + + DefaultAccordionLayout() : super(schemaType: schemaName); + + factory DefaultAccordionLayout.fromJson(Map json) => + DefaultAccordionLayout(); + + @override + Widget build(BuildContext context, Accordion content) { + return DefaultAccordionView(content: content); + } +} + +class DefaultAccordionView extends StatefulWidget { + final Accordion content; + + const DefaultAccordionView({super.key, required this.content}); + + @override + State createState() => _DefaultAccordionViewState(); +} + +class _DefaultAccordionViewState extends State { + final List _expansions = []; + + @override + void initState() { + super.initState(); + + _expansions.addAll( + List.generate(widget.content.items.length, (index) => false)); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Padding( + padding: const EdgeInsets.all(4.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.content.title != null) + Padding( + padding: const EdgeInsets.all(4.0), + child: Text(widget.content.title!, + style: + theme.textTheme.titleMedium?.apply(fontWeightDelta: 2)), + ), + if (widget.content.description != null) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Text( + widget.content.description!, + style: theme.textTheme.bodyMedium + ?.apply(color: theme.colorScheme.secondary), + ), + ), + ExpansionPanelList( + expandedHeaderPadding: const EdgeInsets.symmetric(vertical: 4), + expansionCallback: (index, isExpanded) { + setState(() { + _expansions[index] = isExpanded; + }); + }, + children: widget.content.items + .mapIndexed((index, item) => ExpansionPanel( + canTapOnHeader: true, + isExpanded: _expansions[index], + headerBuilder: (_, isExpanded) => Padding( + padding: const EdgeInsets.all(8.0), + child: Text(item.title, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.apply(fontWeightDelta: isExpanded ? 3 : 0)), + ), + body: Padding( + padding: const EdgeInsets.all(8.0), + child: vyuh.content + .buildContent(context, item.content ?? Empty()), + ))) + .toList(growable: false), + ), + ], + ), + ); + } +} diff --git a/packages/system/vyuh_feature_system/lib/content/accordion.g.dart b/packages/system/vyuh_feature_system/lib/content/accordion.g.dart new file mode 100644 index 00000000..eb17467a --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/accordion.g.dart @@ -0,0 +1,23 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'accordion.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Accordion _$AccordionFromJson(Map json) => Accordion( + title: json['title'] as String?, + description: json['description'] as String?, + items: (json['items'] as List?) + ?.map((e) => AccordionItem.fromJson(e as Map)) + .toList() ?? + [], + ); + +AccordionItem _$AccordionItemFromJson(Map json) => + AccordionItem( + title: json['title'] as String? ?? '', + iconIdentifier: json['iconIdentifier'] as String?, + content: typeFromFirstOfListJson(json['content']), + ); diff --git a/packages/system/vyuh_feature_system/lib/content/card/card.dart b/packages/system/vyuh_feature_system/lib/content/card/card.dart new file mode 100644 index 00000000..479f8bbf --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/card/card.dart @@ -0,0 +1,61 @@ +import 'package:flutter_sanity_portable_text/flutter_sanity_portable_text.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:vyuh_core/vyuh_core.dart'; +import 'package:vyuh_extension_content/vyuh_extension_content.dart' as vx; +import 'package:vyuh_extension_content/vyuh_extension_content.dart'; +import 'package:vyuh_feature_system/content/card/default_layout.dart'; +import 'package:vyuh_feature_system/vyuh_feature_system.dart'; + +part 'card.g.dart'; + +@JsonSerializable() +class Card extends ContentItem implements PortableBlockItem { + static const schemaName = 'vyuh.card'; + + // Required to support Blocks inside Portable Text + @override + String get blockType => schemaName; + + final String? title; + final String? description; + final ImageReference? image; + final PortableTextContent? content; + + final vx.Action? action; + final vx.Action? secondaryAction; + final vx.Action? tertiaryAction; + + Card({ + required this.title, + required this.description, + this.content, + this.image, + this.action, + this.secondaryAction, + this.tertiaryAction, + super.layout, + }) : super(schemaType: Card.schemaName) { + setParent([ + if (content != null) content!, + ]); + } + + factory Card.fromJson(Map json) => _$CardFromJson(json); +} + +class CardDescriptor extends vx.ContentDescriptor { + CardDescriptor({super.layouts}) + : super(schemaType: Card.schemaName, title: 'Card'); +} + +final class CardContentBuilder extends vx.ContentBuilder { + CardContentBuilder() + : super( + content: TypeDescriptor( + schemaType: Card.schemaName, + title: 'Card', + fromJson: Card.fromJson), + defaultLayout: DefaultCardLayout(title: 'Default'), + defaultLayoutDescriptor: DefaultCardLayout.typeDescriptor, + ); +} 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 new file mode 100644 index 00000000..6f8c8817 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/card/card.g.dart @@ -0,0 +1,29 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'card.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Card _$CardFromJson(Map json) => Card( + title: json['title'] as String?, + description: json['description'] as String?, + content: json['content'] == null + ? null + : PortableTextContent.fromJson( + json['content'] as Map), + image: json['image'] == null + ? null + : ImageReference.fromJson(json['image'] as Map), + action: json['action'] == null + ? null + : Action.fromJson(json['action'] as Map), + secondaryAction: json['secondaryAction'] == null + ? null + : Action.fromJson(json['secondaryAction'] as Map), + tertiaryAction: json['tertiaryAction'] == null + ? null + : Action.fromJson(json['tertiaryAction'] as Map), + layout: typeFromFirstOfListJson(json['layout']), + ); 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 new file mode 100644 index 00000000..43ebc898 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/card/default_layout.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart' as f; +import 'package:flutter/material.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:vyuh_core/vyuh_core.dart'; +import 'package:vyuh_feature_system/vyuh_feature_system.dart' as e; + +part 'default_layout.g.dart'; + +@JsonSerializable() +class DefaultCardLayout extends LayoutConfiguration { + static const schemaName = '${e.Card.schemaName}.layout.default'; + static final typeDescriptor = TypeDescriptor( + schemaType: schemaName, + title: 'Default Card Layout', + fromJson: DefaultCardLayout.fromJson, + ); + + @JsonKey(defaultValue: '') + final String title; + + DefaultCardLayout({required this.title}) : super(schemaType: schemaName); + + factory DefaultCardLayout.fromJson(Map json) => + _$DefaultCardLayoutFromJson(json); + + @override + Widget build(BuildContext context, e.Card content) { + final theme = Theme.of(context); + + final blockLength = content.content?.blocks?.length; + final hasBlockContent = blockLength != null && blockLength > 0; + + return e.PressEffect( + onTap: content.action != null + ? (context) => content.action!.execute(context) + : null, + child: f.Card( + color: theme.cardColor, + clipBehavior: Clip.antiAlias, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (content.image?.asset?.ref != null) + Flexible( + child: e.ContentImage( + ref: content.image?.asset?.ref, + fit: BoxFit.cover, + ), + ), + f.Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (content.title != null) + Text( + content.title!, + style: theme.textTheme.titleMedium, + ), + if (content.description != null) Text(content.description!), + if (hasBlockContent) + Flexible( + child: vyuh.content + .buildContent(context, content.content!)), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/packages/system/vyuh_feature_system/lib/content/card/default_layout.g.dart b/packages/system/vyuh_feature_system/lib/content/card/default_layout.g.dart new file mode 100644 index 00000000..aef92291 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/card/default_layout.g.dart @@ -0,0 +1,12 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'default_layout.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +DefaultCardLayout _$DefaultCardLayoutFromJson(Map json) => + DefaultCardLayout( + title: json['title'] as String? ?? '', + ); 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 new file mode 100644 index 00000000..c0ec590f --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/card/list_item_layout.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:vyuh_core/vyuh_core.dart'; +import 'package:vyuh_feature_system/vyuh_feature_system.dart' as sys; + +part 'list_item_layout.g.dart'; + +@JsonSerializable() +class ListItemCardLayout extends LayoutConfiguration { + static const schemaName = '${sys.Card.schemaName}.layout.listItem'; + + @JsonKey(defaultValue: '') + final String title; + + ListItemCardLayout({required this.title}) : super(schemaType: schemaName); + + factory ListItemCardLayout.fromJson(Map json) => + _$ListItemCardLayoutFromJson(json); + + @override + Widget build(BuildContext context, sys.Card content) { + final theme = Theme.of(context); + + return sys.PressEffect( + onTap: (context) => content.action?.execute(context), + child: Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + 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.title != null) + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + content.title!, + style: theme.textTheme.bodyLarge, + ), + )), + 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, + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/packages/system/vyuh_feature_system/lib/content/card/list_item_layout.g.dart b/packages/system/vyuh_feature_system/lib/content/card/list_item_layout.g.dart new file mode 100644 index 00000000..c1570b42 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/card/list_item_layout.g.dart @@ -0,0 +1,12 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'list_item_layout.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ListItemCardLayout _$ListItemCardLayoutFromJson(Map json) => + ListItemCardLayout( + title: json['title'] as String? ?? '', + ); diff --git a/packages/system/vyuh_feature_system/lib/content/conditional.dart b/packages/system/vyuh_feature_system/lib/content/conditional.dart new file mode 100644 index 00000000..d02e4a31 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/conditional.dart @@ -0,0 +1,119 @@ +import 'dart:async'; + +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:mobx/mobx.dart'; +import 'package:vyuh_core/vyuh_core.dart'; +import 'package:vyuh_extension_content/vyuh_extension_content.dart'; + +part 'conditional.g.dart'; + +@JsonSerializable() +class Conditional extends ContentItem { + static const schemaName = 'vyuh.conditional'; + + @JsonKey(defaultValue: []) + final List? cases; + + final String? defaultCase; + final Condition? condition; + + Conditional({this.cases, this.condition, this.defaultCase}) + : super(schemaType: Conditional.schemaName); + + factory Conditional.fromJson(Map json) => + _$ConditionalFromJson(json); + + Future execute() async { + final value = (await condition?.execute()) ?? defaultCase; + + final caseItem = + cases?.firstWhereOrNull((element) => element.value == value); + return caseItem?.item; + } +} + +@JsonSerializable() +final class CaseItem { + final String? value; + + @JsonKey(fromJson: typeFromFirstOfListJson) + final ContentItem? item; + + CaseItem({this.value, this.item}); + + factory CaseItem.fromJson(Map json) => + _$CaseItemFromJson(json); +} + +class ConditionalDescriptor extends ContentDescriptor { + ConditionalDescriptor({super.layouts}) + : super(schemaType: Conditional.schemaName, title: 'Conditional'); +} + +final class ConditionalContentBuilder extends ContentBuilder { + ConditionalContentBuilder() + : super( + content: TypeDescriptor( + schemaType: Conditional.schemaName, + title: 'Conditional', + fromJson: Conditional.fromJson), + defaultLayout: DefaultConditionalLayout(), + defaultLayoutDescriptor: DefaultConditionalLayout.typeDescriptor, + ); +} + +final class DefaultConditionalLayout extends LayoutConfiguration { + static const schemaName = '${Conditional.schemaName}.layout.default'; + static final typeDescriptor = TypeDescriptor( + schemaType: schemaName, + title: 'Default Conditional Layout', + fromJson: DefaultConditionalLayout.fromJson, + ); + + DefaultConditionalLayout() : super(schemaType: schemaName); + + factory DefaultConditionalLayout.fromJson(Map json) => + DefaultConditionalLayout(); + + @override + Widget build(BuildContext context, Conditional content) => + _ConditionalBuilder(conditional: content); +} + +class _ConditionalBuilder extends StatefulWidget { + final Conditional conditional; + + const _ConditionalBuilder({required this.conditional}); + + @override + State<_ConditionalBuilder> createState() => _ConditionalBuilderState(); +} + +class _ConditionalBuilderState extends State<_ConditionalBuilder> { + late final ObservableFuture _future; + + @override + void initState() { + super.initState(); + _future = ObservableFuture(Future.value(widget.conditional.execute())); + } + + @override + Widget build(BuildContext context) { + return Observer(builder: (context) { + switch (_future.status) { + case FutureStatus.pending: + return vyuh.widgetBuilder.contentLoader(); + + case FutureStatus.rejected || FutureStatus.fulfilled: + final item = _future.value; + return item == null + ? Container() + : vyuh.content.buildContent(context, item); + } + }); + } +} diff --git a/packages/system/vyuh_feature_system/lib/content/conditional.g.dart b/packages/system/vyuh_feature_system/lib/content/conditional.g.dart new file mode 100644 index 00000000..53c5d49b --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/conditional.g.dart @@ -0,0 +1,23 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'conditional.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Conditional _$ConditionalFromJson(Map json) => Conditional( + cases: (json['cases'] as List?) + ?.map((e) => CaseItem.fromJson(e as Map)) + .toList() ?? + [], + condition: json['condition'] == null + ? null + : Condition.fromJson(json['condition'] as Map), + defaultCase: json['defaultCase'] as String?, + ); + +CaseItem _$CaseItemFromJson(Map json) => CaseItem( + value: json['value'] as String?, + item: typeFromFirstOfListJson(json['item']), + ); diff --git a/packages/system/vyuh_feature_system/lib/content/divider.dart b/packages/system/vyuh_feature_system/lib/content/divider.dart new file mode 100644 index 00000000..1305c589 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/divider.dart @@ -0,0 +1,70 @@ +import 'package:flutter/material.dart' as f; +import 'package:flutter/material.dart'; +import 'package:flutter_sanity_portable_text/flutter_sanity_portable_text.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:vyuh_core/vyuh_core.dart'; +import 'package:vyuh_extension_content/vyuh_extension_content.dart'; + +part 'divider.g.dart'; + +@JsonSerializable() +class Divider extends ContentItem implements PortableBlockItem { + static const schemaName = 'vyuh.divider'; + + final double? thickness; + final double? indent; + + Divider({this.thickness = 1, this.indent = 8}) + : super(schemaType: Divider.schemaName); + + factory Divider.fromJson(Map json) => + _$DividerFromJson(json); + + @override + String get blockType => Divider.schemaName; +} + +class DividerDescriptor extends ContentDescriptor { + DividerDescriptor() : super(schemaType: Divider.schemaName, title: 'Divider'); +} + +class DividerContentBuilder extends ContentBuilder { + DividerContentBuilder() + : super( + content: TypeDescriptor( + schemaType: Divider.schemaName, + title: 'Divider', + fromJson: Divider.fromJson), + defaultLayout: DefaultDividerLayout(), + defaultLayoutDescriptor: DefaultDividerLayout.typeDescriptor, + ); +} + +final class DefaultDividerLayout extends LayoutConfiguration { + static const schemaName = '${Divider.schemaName}.layout.default'; + static final typeDescriptor = TypeDescriptor( + schemaType: schemaName, + title: 'Default Divider Layout', + fromJson: DefaultDividerLayout.fromJson, + ); + + DefaultDividerLayout() + : super(schemaType: '${Divider.schemaName}.layout.default'); + + factory DefaultDividerLayout.fromJson(Map json) => + DefaultDividerLayout(); + + @override + Widget build(BuildContext context, Divider content) { + final child = f.Divider( + thickness: content.thickness, + ); + + return content.indent != null && content.indent! > 0 + ? Padding( + padding: EdgeInsets.all(content.indent!), + child: child, + ) + : child; + } +} diff --git a/packages/system/vyuh_feature_system/lib/content/divider.g.dart b/packages/system/vyuh_feature_system/lib/content/divider.g.dart new file mode 100644 index 00000000..cd945a3e --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/divider.g.dart @@ -0,0 +1,12 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'divider.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Divider _$DividerFromJson(Map json) => Divider( + thickness: (json['thickness'] as num?)?.toDouble() ?? 1, + indent: (json['indent'] as num?)?.toDouble() ?? 8, + ); diff --git a/packages/system/vyuh_feature_system/lib/content/empty.dart b/packages/system/vyuh_feature_system/lib/content/empty.dart new file mode 100644 index 00000000..39af5034 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/empty.dart @@ -0,0 +1,52 @@ +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'; + +part 'empty.g.dart'; + +const empty = SizedBox.shrink(); + +@JsonSerializable() +class Empty extends ContentItem { + static const schemaName = 'vyuh.empty'; + + Empty() : super(schemaType: Empty.schemaName); + + factory Empty.fromJson(Map json) => _$EmptyFromJson(json); +} + +class EmptyDescriptor extends ContentDescriptor { + EmptyDescriptor() : super(schemaType: Empty.schemaName, title: 'Empty'); +} + +class EmptyContentBuilder extends ContentBuilder { + EmptyContentBuilder() + : super( + content: TypeDescriptor( + schemaType: Empty.schemaName, + title: 'Empty', + fromJson: Empty.fromJson), + defaultLayout: DefaultEmptyLayout(), + defaultLayoutDescriptor: DefaultEmptyLayout.typeDescriptor, + ); +} + +final class DefaultEmptyLayout extends LayoutConfiguration { + static const schemaName = '${Empty.schemaName}.layout.default'; + static final typeDescriptor = TypeDescriptor( + schemaType: schemaName, + title: 'Default Empty Layout', + fromJson: DefaultEmptyLayout.fromJson, + ); + + DefaultEmptyLayout() : super(schemaType: schemaName); + + factory DefaultEmptyLayout.fromJson(Map json) => + DefaultEmptyLayout(); + + @override + Widget build(BuildContext context, Empty content) { + return empty; + } +} diff --git a/packages/system/vyuh_feature_system/lib/content/empty.g.dart b/packages/system/vyuh_feature_system/lib/content/empty.g.dart new file mode 100644 index 00000000..ce8b9c86 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/empty.g.dart @@ -0,0 +1,9 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'empty.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Empty _$EmptyFromJson(Map json) => Empty(); diff --git a/packages/system/vyuh_feature_system/lib/content/group/grid_layout.dart b/packages/system/vyuh_feature_system/lib/content/group/grid_layout.dart new file mode 100644 index 00000000..9ef77f2a --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/group/grid_layout.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:vyuh_core/vyuh_core.dart'; +import 'package:vyuh_feature_system/vyuh_feature_system.dart'; + +part 'grid_layout.g.dart'; + +@JsonSerializable() +final class GridGroupLayout extends LayoutConfiguration { + static const schemaName = '${Group.schemaName}.layout.grid'; + + @JsonKey(defaultValue: 2) + final int columns; + + @JsonKey(defaultValue: 1.0) + final double aspectRatio; + + GridGroupLayout({this.columns = 2, this.aspectRatio = 1.0}) + : super(schemaType: schemaName) { + assert(columns >= 2, 'Minimum of 2 columns is required'); + } + + factory GridGroupLayout.fromJson(Map json) => + _$GridGroupLayoutFromJson(json); + + @override + Widget build(BuildContext context, Group content) { + final theme = Theme.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (content.title != null) + Padding( + padding: const EdgeInsets.all(4.0), + child: Text(content.title!, style: theme.textTheme.titleMedium), + ), + if (content.description != null) + Padding( + padding: const EdgeInsets.only(left: 4.0, right: 4.0, bottom: 4.0), + child: Text(content.description!, style: theme.textTheme.bodySmall), + ), + GridView( + padding: EdgeInsets.zero, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: columns, + childAspectRatio: aspectRatio, + ), + children: content.items + .map((e) => vyuh.content.buildContent(context, e)) + .toList( + growable: false, + ), + ), + ], + ); + } +} diff --git a/packages/system/vyuh_feature_system/lib/content/group/grid_layout.g.dart b/packages/system/vyuh_feature_system/lib/content/group/grid_layout.g.dart new file mode 100644 index 00000000..f4a8037b --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/group/grid_layout.g.dart @@ -0,0 +1,13 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'grid_layout.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +GridGroupLayout _$GridGroupLayoutFromJson(Map json) => + GridGroupLayout( + columns: json['columns'] as int? ?? 2, + aspectRatio: (json['aspectRatio'] as num?)?.toDouble() ?? 1.0, + ); diff --git a/packages/system/vyuh_feature_system/lib/content/group/group.dart b/packages/system/vyuh_feature_system/lib/content/group/group.dart new file mode 100644 index 00000000..535527d9 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/group/group.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_sanity_portable_text/flutter_sanity_portable_text.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/vyuh_feature_system.dart'; + +export 'grid_layout.dart'; + +part 'group.g.dart'; + +@JsonSerializable() +class Group extends ContentItem implements PortableBlockItem, ContainerItem { + static const schemaName = 'vyuh.group'; + + @override + String get blockType => schemaName; + + final String? title; + + final String? description; + + @JsonKey(defaultValue: []) + final List items; + + Group({ + this.title, + this.description, + required this.items, + super.layout, + }) : super(schemaType: Group.schemaName) { + setParent(items); + } + + factory Group.fromJson(Map json) => _$GroupFromJson(json); +} + +class GroupDescriptor extends ContentDescriptor { + GroupDescriptor({super.layouts}) + : super(schemaType: Group.schemaName, title: 'Group'); +} + +class GroupContentBuilder extends ContentBuilder { + GroupContentBuilder() + : super( + content: TypeDescriptor( + schemaType: Group.schemaName, + title: 'Group', + fromJson: Group.fromJson), + defaultLayout: DefaultGroupLayout(), + defaultLayoutDescriptor: DefaultGroupLayout.typeDescriptor, + ); +} + +@JsonSerializable() +final class DefaultGroupLayout extends LayoutConfiguration { + static const schemaName = '${Group.schemaName}.layout.default'; + static final typeDescriptor = TypeDescriptor( + schemaType: schemaName, + title: 'Default Group Layout', + fromJson: DefaultGroupLayout.fromJson, + ); + + DefaultGroupLayout() : super(schemaType: schemaName); + + factory DefaultGroupLayout.fromJson(Map json) => + _$DefaultGroupLayoutFromJson(json); + + @override + Widget build(BuildContext context, Group content) { + return Carousel(content: content); + } +} diff --git a/packages/system/vyuh_feature_system/lib/content/group/group.g.dart b/packages/system/vyuh_feature_system/lib/content/group/group.g.dart new file mode 100644 index 00000000..6af2c11b --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/group/group.g.dart @@ -0,0 +1,20 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'group.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Group _$GroupFromJson(Map json) => Group( + title: json['title'] as String?, + description: json['description'] as String?, + items: (json['items'] as List?) + ?.map((e) => ContentItem.fromJson(e as Map)) + .toList() ?? + [], + layout: typeFromFirstOfListJson(json['layout']), + ); + +DefaultGroupLayout _$DefaultGroupLayoutFromJson(Map json) => + DefaultGroupLayout(); diff --git a/packages/system/vyuh_feature_system/lib/content/index.dart b/packages/system/vyuh_feature_system/lib/content/index.dart new file mode 100644 index 00000000..9d09a78c --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/index.dart @@ -0,0 +1,13 @@ +export 'accordion.dart'; +export 'card/card.dart'; +export 'conditional.dart'; +export 'divider.dart'; +export 'empty.dart'; +export 'group/group.dart'; +export 'portable_text/portable_text.dart'; +export 'reference.dart'; +export 'route/conditional_route.dart'; +export 'route/default_route.dart'; +export 'route/route.dart'; +export 'route/route_container.dart'; +export 'unknown.dart'; diff --git a/packages/system/vyuh_feature_system/lib/content/portable_text/invoke_action.dart b/packages/system/vyuh_feature_system/lib/content/portable_text/invoke_action.dart new file mode 100644 index 00000000..13dc37df --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/portable_text/invoke_action.dart @@ -0,0 +1,16 @@ +import 'package:flutter_sanity_portable_text/flutter_sanity_portable_text.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:vyuh_extension_content/vyuh_extension_content.dart'; + +part 'invoke_action.g.dart'; + +@JsonSerializable() +final class InvokeActionMarkDef extends MarkDef { + final Action action; + + InvokeActionMarkDef( + {required this.action, required super.key, required super.type}); + + factory InvokeActionMarkDef.fromJson(Map json) => + _$InvokeActionMarkDefFromJson(json); +} diff --git a/packages/system/vyuh_feature_system/lib/content/portable_text/invoke_action.g.dart b/packages/system/vyuh_feature_system/lib/content/portable_text/invoke_action.g.dart new file mode 100644 index 00000000..2ef1f681 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/portable_text/invoke_action.g.dart @@ -0,0 +1,14 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'invoke_action.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +InvokeActionMarkDef _$InvokeActionMarkDefFromJson(Map json) => + InvokeActionMarkDef( + action: Action.fromJson(json['action'] as Map), + key: json['_key'] as String, + type: json['_type'] as String, + ); diff --git a/packages/system/vyuh_feature_system/lib/content/portable_text/portable_text.dart b/packages/system/vyuh_feature_system/lib/content/portable_text/portable_text.dart new file mode 100644 index 00000000..d22cfd9d --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/portable_text/portable_text.dart @@ -0,0 +1,178 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_sanity_portable_text/flutter_sanity_portable_text.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:vyuh_core/vyuh_core.dart' as vc; +import 'package:vyuh_core/vyuh_core.dart'; +import 'package:vyuh_extension_content/vyuh_extension_content.dart'; + +part 'portable_text.g.dart'; + +@JsonSerializable() +class PortableTextContent extends ContentItem { + static const schemaName = 'vyuh.portableText'; + + @JsonKey(defaultValue: [], fromJson: blockItemsFromJson) + final List? blocks; + + PortableTextContent({ + this.blocks, + }) : super(schemaType: PortableTextContent.schemaName) { + final items = (blocks ?? []).whereType(); + setParent(items); + _setListItemIndexes(blocks ?? []); + } + + factory PortableTextContent.fromJson(Map json) => + _$PortableTextContentFromJson(json); + + static List? blockItemsFromJson(dynamic value) { + if (value is! List) { + return null; + } + + return value + .map((e) { + final type = vyuh.content.provider.schemaType(e); + final itemDescriptor = PortableTextContentBuilder.blockMap[type]; + + if (itemDescriptor == null) { + return kDebugMode + ? Unknown( + missingSchemaType: type, + description: + 'An $TypeDescriptor for this block-type has not been specified. Make sure to add that in your $PortableTextDescriptor.') + : null; + } + + return itemDescriptor.fromJson(e); + }) + .where((element) => element != null) + .cast() + .toList(growable: false); + } + + static void _setListItemIndexes(final List items) { + int? listStartIndex; + + for (var index = 0; index < items.length; index++) { + final item = items[index]; + switch (item) { + case TextBlockItem(): + if (item.listItem == null || item.listItem != ListItemType.number) { + listStartIndex = null; + continue; + } + + // Assign a starting index to the list only if previously + // we were not inside a list + listStartIndex ??= index; + item.listItemIndex = index - listStartIndex; + break; + + default: + listStartIndex = null; + continue; + } + } + } +} + +final class BlockItemDescriptor extends TypeDescriptor { + final BlockWidgetBuilder builder; + + BlockItemDescriptor({ + required super.schemaType, + required super.fromJson, + required this.builder, + }) : super(title: 'Block Item'); +} + +class PortableTextDescriptor extends ContentDescriptor { + final List? blocks; + final List? markDefs; + final Map? textStyleBuilders; + + PortableTextDescriptor({ + this.blocks, + this.markDefs, + this.textStyleBuilders, + }) : super( + schemaType: PortableTextContent.schemaName, title: 'Portable Text'); +} + +final class PortableTextContentBuilder + extends ContentBuilder { + static Map blockMap = {}; + + PortableTextContentBuilder() + : super( + content: TypeDescriptor( + schemaType: PortableTextContent.schemaName, + title: 'Portable Text', + fromJson: PortableTextContent.fromJson), + defaultLayout: DefaultPortableTextContentLayout(), + defaultLayoutDescriptor: + DefaultPortableTextContentLayout.typeDescriptor, + ); + + @override + void init(List descriptors) { + super.init(descriptors); + + final pDescriptors = descriptors.cast(); + + // Blocks + pDescriptors.expand((element) => element.blocks ?? []).fold(blockMap, + (previous, descriptor) { + previous[descriptor.schemaType] = descriptor; + return previous; + }); + + for (final entry in blockMap.entries) { + PortableTextConfig.shared.blocks[entry.key] = entry.value.builder; + } + + // MarkDefs + final markDefs = pDescriptors + .expand((element) => element.markDefs ?? []) + .map((e) => MapEntry(e.schemaType, e)); + + PortableTextConfig.shared.markDefs.addEntries(markDefs); + + // Styles + pDescriptors.fold(PortableTextConfig.shared.styles, (previous, element) { + previous.addAll(element.textStyleBuilders ?? {}); + return previous; + }); + } +} + +@JsonSerializable() +class DefaultPortableTextContentLayout + extends LayoutConfiguration { + static const schemaName = '${PortableTextContent.schemaName}.layout.default'; + static final typeDescriptor = TypeDescriptor( + schemaType: schemaName, + title: 'Default Potable Text Layout', + fromJson: DefaultPortableTextContentLayout.fromJson, + ); + + DefaultPortableTextContentLayout() : super(schemaType: schemaName); + + factory DefaultPortableTextContentLayout.fromJson( + Map json) => + _$DefaultPortableTextContentLayoutFromJson(json); + + @override + Widget build(BuildContext context, PortableTextContent content) { + if (content.parent is vc.RouteBase) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: PortableText(blocks: content.blocks ?? []), + ); + } + + return PortableText(blocks: content.blocks ?? []); + } +} diff --git a/packages/system/vyuh_feature_system/lib/content/portable_text/portable_text.g.dart b/packages/system/vyuh_feature_system/lib/content/portable_text/portable_text.g.dart new file mode 100644 index 00000000..5d858121 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/portable_text/portable_text.g.dart @@ -0,0 +1,18 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'portable_text.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +PortableTextContent _$PortableTextContentFromJson(Map json) => + PortableTextContent( + blocks: json['blocks'] == null + ? [] + : PortableTextContent.blockItemsFromJson(json['blocks']), + ); + +DefaultPortableTextContentLayout _$DefaultPortableTextContentLayoutFromJson( + Map json) => + DefaultPortableTextContentLayout(); diff --git a/packages/system/vyuh_feature_system/lib/content/reference.dart b/packages/system/vyuh_feature_system/lib/content/reference.dart new file mode 100644 index 00000000..c5435cc2 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/reference.dart @@ -0,0 +1,30 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:vyuh_core/vyuh_core.dart'; + +part 'reference.g.dart'; + +@JsonSerializable() +class ImageReference { + final ObjectReference? asset; + + ImageReference({ + required this.asset, + }); + + factory ImageReference.fromJson(final Map json) => + _$ImageReferenceFromJson(json); +} + +@JsonSerializable() +class ObjectReference { + @JsonKey(readValue: readValue) + final String type; + + @JsonKey(readValue: readValue) + final String ref; + + ObjectReference({required this.type, required this.ref}); + + factory ObjectReference.fromJson(Map json) => + _$ObjectReferenceFromJson(json); +} diff --git a/packages/system/vyuh_feature_system/lib/content/reference.g.dart b/packages/system/vyuh_feature_system/lib/content/reference.g.dart new file mode 100644 index 00000000..3e3bc40b --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/reference.g.dart @@ -0,0 +1,20 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'reference.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +ImageReference _$ImageReferenceFromJson(Map json) => + ImageReference( + asset: json['asset'] == null + ? null + : ObjectReference.fromJson(json['asset'] as Map), + ); + +ObjectReference _$ObjectReferenceFromJson(Map json) => + ObjectReference( + type: readValue(json, 'type') as String, + ref: readValue(json, 'ref') as String, + ); diff --git a/packages/system/vyuh_feature_system/lib/content/route/conditional_route.dart b/packages/system/vyuh_feature_system/lib/content/route/conditional_route.dart new file mode 100644 index 00000000..30a63ed2 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/route/conditional_route.dart @@ -0,0 +1,112 @@ +import 'package:collection/collection.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/vyuh_feature_system.dart' as vf; +import 'package:vyuh_feature_system/vyuh_feature_system.dart'; + +part 'conditional_route.g.dart'; + +@JsonSerializable() +final class CaseRouteItem { + final String? value; + + final ObjectReference? item; + + CaseRouteItem({this.value, this.item}); + + factory CaseRouteItem.fromJson(Map json) => + _$CaseRouteItemFromJson(json); +} + +@JsonSerializable() +final class ConditionalRoute extends RouteBase { + static const schemaName = 'vyuh.conditionalRoute'; + + @JsonKey(defaultValue: []) + final List? cases; + final String? defaultCase; + final Condition? condition; + + vf.Route? _initializedInstance; + + ConditionalRoute({ + this.condition, + this.cases, + this.defaultCase, + required super.title, + required super.path, + required super.createdAt, + required super.updatedAt, + required super.id, + }) : super(schemaType: ConditionalRoute.schemaName); + + factory ConditionalRoute.fromJson(Map json) => + _$ConditionalRouteFromJson(json); + + @override + Future init() async { + if (_initializedInstance != null) { + return _initializedInstance; + } + + final value = (await condition?.execute()) ?? defaultCase; + final caseItem = (cases ?? []).firstWhereOrNull((x) => x.value == value); + + final ref = caseItem?.item; + RouteBase? leafRoute; + + if (ref != null) { + final route = await vyuh.content.provider.fetchRoute(routeId: ref.ref); + leafRoute = await route?.init(); + } + + _initializedInstance = leafRoute as vf.Route?; + return _initializedInstance; + } + + @override + Future dispose() => Future.value(); +} + +class ConditionalRouteDescriptor extends ContentDescriptor { + ConditionalRouteDescriptor({super.layouts}) + : super( + schemaType: ConditionalRoute.schemaName, + title: 'Conditional Route'); +} + +final class ConditionalRouteBuilder extends ContentBuilder { + ConditionalRouteBuilder() + : super( + content: TypeDescriptor( + schemaType: ConditionalRoute.schemaName, + title: 'Conditional Route', + fromJson: ConditionalRoute.fromJson), + defaultLayout: DefaultConditionalRouteLayout(), + defaultLayoutDescriptor: DefaultConditionalRouteLayout.typeDescriptor, + ); +} + +class DefaultConditionalRouteLayout + extends LayoutConfiguration { + static const schemaName = '${ConditionalRoute.schemaName}.layout.default'; + static final typeDescriptor = TypeDescriptor( + schemaType: schemaName, + title: 'Default ConditionalRoute Layout', + fromJson: DefaultConditionalRouteLayout.fromJson, + ); + + DefaultConditionalRouteLayout() + : super(schemaType: '${ConditionalRoute.schemaName}.layout.default'); + + factory DefaultConditionalRouteLayout.fromJson(Map json) => + DefaultConditionalRouteLayout(); + + @override + Widget build(BuildContext context, ConditionalRoute content) { + // This should never be called + throw UnimplementedError(); + } +} diff --git a/packages/system/vyuh_feature_system/lib/content/route/conditional_route.g.dart b/packages/system/vyuh_feature_system/lib/content/route/conditional_route.g.dart new file mode 100644 index 00000000..722f256c --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/route/conditional_route.g.dart @@ -0,0 +1,32 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'conditional_route.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +CaseRouteItem _$CaseRouteItemFromJson(Map json) => + CaseRouteItem( + value: json['value'] as String?, + item: json['item'] == null + ? null + : ObjectReference.fromJson(json['item'] as Map), + ); + +ConditionalRoute _$ConditionalRouteFromJson(Map json) => + ConditionalRoute( + condition: json['condition'] == null + ? null + : Condition.fromJson(json['condition'] as Map), + cases: (json['cases'] as List?) + ?.map((e) => CaseRouteItem.fromJson(e as Map)) + .toList() ?? + [], + defaultCase: json['defaultCase'] as String?, + title: json['title'] as String, + path: json['path'] as String, + createdAt: DateTime.parse(readValue(json, 'createdAt') as String), + updatedAt: DateTime.parse(readValue(json, 'updatedAt') as String), + id: readValue(json, 'id') as String, + ); diff --git a/packages/system/vyuh_feature_system/lib/content/route/default_layout.dart b/packages/system/vyuh_feature_system/lib/content/route/default_layout.dart new file mode 100644 index 00000000..2b810414 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/route/default_layout.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:vyuh_core/vyuh_core.dart'; +import 'package:vyuh_feature_system/vyuh_feature_system.dart' as vf; + +part 'default_layout.g.dart'; + +@JsonSerializable() +final class DefaultRouteLayout extends LayoutConfiguration { + static const schemaName = '${vf.Route.schemaName}.layout.default'; + static final typeDescriptor = TypeDescriptor( + schemaType: schemaName, + title: 'Default Route Layout', + fromJson: DefaultRouteLayout.fromJson, + ); + + DefaultRouteLayout() : super(schemaType: schemaName); + + factory DefaultRouteLayout.fromJson(Map json) => + _$DefaultRouteLayoutFromJson(json); + + @override + Widget build(BuildContext context, vf.Route content) { + final theme = Theme.of(context); + final items = content.regions + .expand((element) => element.items) + .toList(growable: false); + + return vf.RouteContainer( + content: content, + child: Scaffold( + appBar: AppBar( + title: Text(content.title), + scrolledUnderElevation: 1, + shadowColor: theme.colorScheme.shadow, + ), + body: SafeArea( + child: ListView.builder( + itemCount: items.length, + itemBuilder: (_, index) { + return vyuh.content.buildContent(context, items[index]); + }), + ), + ), + ); + } +} diff --git a/packages/system/vyuh_feature_system/lib/content/route/default_layout.g.dart b/packages/system/vyuh_feature_system/lib/content/route/default_layout.g.dart new file mode 100644 index 00000000..0cedf5f7 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/route/default_layout.g.dart @@ -0,0 +1,10 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'default_layout.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +DefaultRouteLayout _$DefaultRouteLayoutFromJson(Map json) => + DefaultRouteLayout(); diff --git a/packages/system/vyuh_feature_system/lib/content/route/default_route.dart b/packages/system/vyuh_feature_system/lib/content/route/default_route.dart new file mode 100644 index 00000000..c3da9290 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/route/default_route.dart @@ -0,0 +1,19 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:vyuh_core/vyuh_core.dart' as vc; +import 'package:vyuh_core/vyuh_core.dart'; +import 'package:vyuh_feature_system/vyuh_feature_system.dart' as vf; + +Page defaultRoutePageBuilder( + BuildContext context, GoRouterState state) { + final route = state.extra as vf.Route?; + + if (route == null) { + return MaterialPage( + child: vyuh.content + .buildRoute(context, url: Uri.parse(state.matchedLocation)), + ); + } + + return route.createPage(context); +} diff --git a/packages/system/vyuh_feature_system/lib/content/route/dialog_layout.dart b/packages/system/vyuh_feature_system/lib/content/route/dialog_layout.dart new file mode 100644 index 00000000..82c0231e --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/route/dialog_layout.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:vyuh_core/vyuh_core.dart'; +import 'package:vyuh_feature_system/vyuh_feature_system.dart' as vf; + +part 'dialog_layout.g.dart'; + +enum DialogType { modalBottomSheet, dialog } + +@JsonSerializable() +final class DialogRouteLayout extends LayoutConfiguration { + static const schemaName = 'vyuh.route.layout.dialog'; + + @JsonKey(defaultValue: DialogType.dialog) + final DialogType dialogType; + + DialogRouteLayout({required this.dialogType}) : super(schemaType: schemaName); + + factory DialogRouteLayout.fromJson(Map json) => + _$DialogRouteLayoutFromJson(json); + + @override + Widget build(BuildContext context, vf.Route content) { + final items = content.regions + .expand((element) => element.items) + .toList(growable: false); + + return vf.RouteContainer( + content: content, + child: ListView.builder( + itemBuilder: (context, index) => + vyuh.content.buildContent(context, items[index]), + itemCount: items.length, + ), + ); + } +} diff --git a/packages/system/vyuh_feature_system/lib/content/route/dialog_layout.g.dart b/packages/system/vyuh_feature_system/lib/content/route/dialog_layout.g.dart new file mode 100644 index 00000000..90f86b3b --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/route/dialog_layout.g.dart @@ -0,0 +1,19 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'dialog_layout.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +DialogRouteLayout _$DialogRouteLayoutFromJson(Map json) => + DialogRouteLayout( + dialogType: + $enumDecodeNullable(_$DialogTypeEnumMap, json['dialogType']) ?? + DialogType.dialog, + ); + +const _$DialogTypeEnumMap = { + DialogType.modalBottomSheet: 'modalBottomSheet', + DialogType.dialog: 'dialog', +}; diff --git a/packages/system/vyuh_feature_system/lib/content/route/route.dart b/packages/system/vyuh_feature_system/lib/content/route/route.dart new file mode 100644 index 00000000..99550417 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/route/route.dart @@ -0,0 +1,141 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart' as flutter; +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/route/default_layout.dart'; + +part 'route.g.dart'; + +@JsonSerializable() +final class Route extends RouteBase { + static const schemaName = 'vyuh.route'; + + @JsonKey(fromJson: typeFromFirstOfListJson) + final RouteTypeConfiguration? routeType; + + @JsonKey(defaultValue: []) + final List regions; + + Route? _initializedInstance; + + Route({ + required super.title, + required this.routeType, + required super.path, + required this.regions, + required super.createdAt, + required super.updatedAt, + required super.id, + super.category, + super.layout, + this.initConfigurations, + }) : super(schemaType: Route.schemaName) { + setParent(regions.expand((element) => element.items)); + } + + @JsonKey(defaultValue: [], fromJson: initConfigurationsFromJson) + final List? initConfigurations; + + factory Route.fromJson(Map json) => _$RouteFromJson(json); + + static List? initConfigurationsFromJson( + dynamic json) => + listFromJson(json); + + flutter.Page createPage(flutter.BuildContext context) { + final child = kDebugMode + ? vyuh.content.buildRoute(context, routeId: id) + : vyuh.content.buildContent(context, this); + + return routeType?.create(child, this) ?? flutter.MaterialPage(child: child); + } + + static defaultPageBuilder({Uri? uri, String? routeId, Route? route}) {} + + @override + Future init() async { + if (_initializedInstance != null) { + return _initializedInstance; + } + + if (initConfigurations != null && initConfigurations!.isNotEmpty) { + for (final config in initConfigurations!) { + await config.init(this); + } + } + + _initializedInstance = this; + return _initializedInstance; + } + + @override + Future dispose() async { + if (initConfigurations != null && initConfigurations!.isNotEmpty) { + for (final config in initConfigurations!) { + await config.dispose(); + } + } + } +} + +@JsonSerializable() +final class Region { + final String identifier; + final String title; + + @JsonKey(defaultValue: []) + final List items; + + Region({ + required this.identifier, + required this.title, + required this.items, + }); + + factory Region.fromJson(Map json) => _$RegionFromJson(json); +} + +final class RouteDescriptor extends ContentDescriptor { + List>? initConfigurations; + List>? routeTypes; + + RouteDescriptor({ + this.initConfigurations, + this.routeTypes, + super.layouts, + }) : super(schemaType: Route.schemaName, title: 'Route'); +} + +final class RouteContentBuilder extends ContentBuilder { + RouteContentBuilder() + : super( + content: TypeDescriptor( + schemaType: Route.schemaName, + title: 'Route', + fromJson: Route.fromJson), + defaultLayout: DefaultRouteLayout(), + defaultLayoutDescriptor: DefaultRouteLayout.typeDescriptor, + ); + + @override + init(List descriptors) { + super.init(descriptors); + + final rtDescriptors = descriptors.cast(); + final initConfigs = rtDescriptors.expand((element) => + element.initConfigurations ?? + >[]); + + for (final config in initConfigs) { + vyuh.content.register(config); + } + + final routeTypes = rtDescriptors.expand((element) => + element.routeTypes ?? >[]); + + for (final config in routeTypes) { + vyuh.content.register(config); + } + } +} diff --git a/packages/system/vyuh_feature_system/lib/content/route/route.g.dart b/packages/system/vyuh_feature_system/lib/content/route/route.g.dart new file mode 100644 index 00000000..30118a29 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/route/route.g.dart @@ -0,0 +1,34 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'route.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +Route _$RouteFromJson(Map json) => Route( + title: json['title'] as String, + routeType: typeFromFirstOfListJson(json['routeType']), + path: json['path'] as String, + regions: (json['regions'] as List?) + ?.map((e) => Region.fromJson(e as Map)) + .toList() ?? + [], + createdAt: DateTime.parse(readValue(json, 'createdAt') as String), + updatedAt: DateTime.parse(readValue(json, 'updatedAt') as String), + id: readValue(json, 'id') as String, + category: json['category'] as String?, + layout: typeFromFirstOfListJson(json['layout']), + initConfigurations: json['initConfigurations'] == null + ? [] + : Route.initConfigurationsFromJson(json['initConfigurations']), + ); + +Region _$RegionFromJson(Map json) => Region( + identifier: json['identifier'] as String, + title: json['title'] as String, + items: (json['items'] as List?) + ?.map((e) => ContentItem.fromJson(e as Map)) + .toList() ?? + [], + ); diff --git a/packages/system/vyuh_feature_system/lib/content/route/route_container.dart b/packages/system/vyuh_feature_system/lib/content/route/route_container.dart new file mode 100644 index 00000000..6e48568d --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/route/route_container.dart @@ -0,0 +1,29 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:vyuh_core/vyuh_core.dart'; + +class RouteContainer extends StatefulWidget { + final RouteBase content; + + final Widget child; + + const RouteContainer({super.key, required this.content, required this.child}); + + @override + State createState() => _RouteContainerState(); +} + +class _RouteContainerState extends State { + @override + Widget build(BuildContext context) { + return widget.child; + } + + @override + void dispose() { + super.dispose(); + + unawaited(widget.content.dispose()); + } +} diff --git a/packages/system/vyuh_feature_system/lib/content/route/route_type.dart b/packages/system/vyuh_feature_system/lib/content/route/route_type.dart new file mode 100644 index 00000000..594d05d3 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/route/route_type.dart @@ -0,0 +1,50 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:vyuh_core/vyuh_core.dart' as vc; +import 'package:vyuh_extension_content/vyuh_extension_content.dart'; +import 'package:vyuh_feature_system/vyuh_feature_system.dart'; + +part 'route_type.g.dart'; + +enum PageBehavior { material, cupertino } + +enum DialogBehavior { modalBottomSheet, fullscreen } + +@JsonSerializable() +final class PageRouteType extends RouteTypeConfiguration { + static const schemaName = 'vyuh.route.page'; + + final PageBehavior behavior; + + PageRouteType({this.behavior = PageBehavior.material}) + : super(schemaType: PageRouteType.schemaName); + + factory PageRouteType.fromJson(Map json) => + _$PageRouteTypeFromJson(json); + + @override + Page create(Widget child, vc.RouteBase route) { + return behavior == PageBehavior.material + ? MaterialPage(child: child) + : CupertinoPage(child: child); + } +} + +@JsonSerializable() +final class DialogRouteType extends RouteTypeConfiguration { + static const schemaName = 'vyuh.route.dialog'; + + final DialogBehavior behavior; + + DialogRouteType({this.behavior = DialogBehavior.modalBottomSheet}) + : super(schemaType: DialogRouteType.schemaName); + + factory DialogRouteType.fromJson(Map json) => + _$DialogRouteTypeFromJson(json); + + @override + Page create(Widget child, vc.RouteBase route) { + return ModalDialogPage(builder: (context) => child); + } +} diff --git a/packages/system/vyuh_feature_system/lib/content/route/route_type.g.dart b/packages/system/vyuh_feature_system/lib/content/route/route_type.g.dart new file mode 100644 index 00000000..7dbc7422 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/route/route_type.g.dart @@ -0,0 +1,30 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'route_type.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +PageRouteType _$PageRouteTypeFromJson(Map json) => + PageRouteType( + behavior: $enumDecodeNullable(_$PageBehaviorEnumMap, json['behavior']) ?? + PageBehavior.material, + ); + +const _$PageBehaviorEnumMap = { + PageBehavior.material: 'material', + PageBehavior.cupertino: 'cupertino', +}; + +DialogRouteType _$DialogRouteTypeFromJson(Map json) => + DialogRouteType( + behavior: + $enumDecodeNullable(_$DialogBehaviorEnumMap, json['behavior']) ?? + DialogBehavior.modalBottomSheet, + ); + +const _$DialogBehaviorEnumMap = { + DialogBehavior.modalBottomSheet: 'modalBottomSheet', + DialogBehavior.fullscreen: 'fullscreen', +}; diff --git a/packages/system/vyuh_feature_system/lib/content/route/sliver_layout.dart b/packages/system/vyuh_feature_system/lib/content/route/sliver_layout.dart new file mode 100644 index 00000000..491f8fa8 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/route/sliver_layout.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:vyuh_core/vyuh_core.dart'; +import 'package:vyuh_feature_system/vyuh_feature_system.dart' as vf; + +part 'sliver_layout.g.dart'; + +@JsonSerializable() +final class SliverRouteLayout extends LayoutConfiguration { + static const schemaName = 'vyuh.route.layout.sliver'; + + SliverRouteLayout() : super(schemaType: schemaName); + + factory SliverRouteLayout.fromJson(Map json) => + _$SliverRouteLayoutFromJson(json); + + @override + Widget build(BuildContext context, vf.Route content) { + final items = content.regions + .expand((element) => element.items) + .toList(growable: false); + + return vf.RouteContainer( + content: content, + child: CustomScrollView( + primary: true, + slivers: [ + SliverAppBar( + centerTitle: true, + title: Text(content.title), + pinned: true, + ), + SliverList.builder( + itemBuilder: (context, index) => + vyuh.content.buildContent(context, items[index]), + itemCount: items.length, + ) + ], + ), + ); + } +} diff --git a/packages/system/vyuh_feature_system/lib/content/route/sliver_layout.g.dart b/packages/system/vyuh_feature_system/lib/content/route/sliver_layout.g.dart new file mode 100644 index 00000000..fc0fa076 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/route/sliver_layout.g.dart @@ -0,0 +1,10 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'sliver_layout.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +SliverRouteLayout _$SliverRouteLayoutFromJson(Map json) => + SliverRouteLayout(); diff --git a/packages/system/vyuh_feature_system/lib/content/route/tabs.dart b/packages/system/vyuh_feature_system/lib/content/route/tabs.dart new file mode 100644 index 00000000..5a09ade8 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/route/tabs.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:vyuh_core/vyuh_core.dart' as vt; +import 'package:vyuh_core/vyuh_core.dart'; +import 'package:vyuh_feature_system/vyuh_feature_system.dart'; + +part 'tabs.g.dart'; + +@JsonSerializable() +final class TabsRouteLayout extends LayoutConfiguration { + static const schemaName = 'vyuh.route.layout.tabs'; + + final List routes; + + TabsRouteLayout({required this.routes}) : super(schemaType: schemaName); + + factory TabsRouteLayout.fromJson(Map json) => + _$TabsRouteLayoutFromJson(json); + + @override + Widget build(BuildContext context, vt.RouteBase content) { + final layout = content.layout as TabsRouteLayout; + + return RouteContainer( + content: content, + child: DefaultTabController( + length: layout.routes.length, + child: Scaffold( + appBar: AppBar( + toolbarHeight: 0, + bottom: TabBar( + tabs: layout.routes + .map((e) => Tab(text: e.title)) + .toList(growable: false), + ), + ), + body: TabBarView( + children: layout.routes + .map((e) => + vyuh.content.buildRoute(context, routeId: e.route.ref)) + .toList(growable: false), + ), + ), + ), + ); + } +} + +@JsonSerializable() +final class LinkedRoute { + final String title; + final String? description; + final String identifier; + final ObjectReference route; + + LinkedRoute({ + required this.title, + required this.identifier, + required this.route, + this.description, + }); + + factory LinkedRoute.fromJson(Map json) => + _$LinkedRouteFromJson(json); +} diff --git a/packages/system/vyuh_feature_system/lib/content/route/tabs.g.dart b/packages/system/vyuh_feature_system/lib/content/route/tabs.g.dart new file mode 100644 index 00000000..96c5586f --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/route/tabs.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'tabs.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +TabsRouteLayout _$TabsRouteLayoutFromJson(Map json) => + TabsRouteLayout( + routes: (json['routes'] as List) + .map((e) => LinkedRoute.fromJson(e as Map)) + .toList(), + ); + +LinkedRoute _$LinkedRouteFromJson(Map json) => LinkedRoute( + title: json['title'] as String, + identifier: json['identifier'] as String, + route: ObjectReference.fromJson(json['route'] as Map), + description: json['description'] as String?, + ); diff --git a/packages/system/vyuh_feature_system/lib/content/unknown.dart b/packages/system/vyuh_feature_system/lib/content/unknown.dart new file mode 100644 index 00000000..bac07ac3 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/unknown.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:vyuh_core/vyuh_core.dart'; +import 'package:vyuh_extension_content/vyuh_extension_content.dart'; + +class UnknownDescriptor extends ContentDescriptor { + UnknownDescriptor() : super(schemaType: Unknown.schemaName, title: 'Unknown'); +} + +class UnknownContentBuilder extends ContentBuilder { + UnknownContentBuilder() + : super( + content: TypeDescriptor( + schemaType: Unknown.schemaName, + title: 'Unknown', + fromJson: Unknown.fromJson), + defaultLayout: DefaultUnknownLayout(), + defaultLayoutDescriptor: DefaultUnknownLayout.typeDescriptor, + ); +} + +final class DefaultUnknownLayout extends LayoutConfiguration { + static const schemaName = '${Unknown.schemaName}.layout.default'; + static final typeDescriptor = TypeDescriptor( + schemaType: schemaName, + title: 'Default Unknown Layout', + fromJson: DefaultUnknownLayout.fromJson); + + DefaultUnknownLayout() : super(schemaType: schemaName); + + factory DefaultUnknownLayout.fromJson(Map json) => + DefaultUnknownLayout(); + + @override + Widget build(BuildContext context, Unknown content) { + final theme = Theme.of(context); + return Container( + color: Colors.red.shade300, + padding: const EdgeInsets.all(8), + child: Column( + children: [ + Row( + children: [ + const Icon( + Icons.do_not_disturb_alt_rounded, + color: Colors.white54, + size: 32, + ), + const SizedBox(width: 10), + const Text('Missing schemaType:'), + const SizedBox(width: 4), + Text(content.missingSchemaType, + style: + theme.textTheme.bodyMedium?.apply(fontWeightDelta: 4)), + ], + ), + const SizedBox(height: 8), + Text(content.description), + ], + )); + } +} diff --git a/packages/system/vyuh_feature_system/lib/content/web_view.dart b/packages/system/vyuh_feature_system/lib/content/web_view.dart new file mode 100644 index 00000000..a0eae02b --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/content/web_view.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:webview_flutter/webview_flutter.dart'; + +class WebView extends StatefulWidget { + final Uri uri; + + const WebView({super.key, required this.uri}); + + @override + State createState() => _WebViewState(); +} + +class _WebViewState extends State { + late final WebViewController _controller; + int _progress = 0; + + @override + void initState() { + super.initState(); + _controller = WebViewController() + ..setUserAgent('vyuh') + ..setNavigationDelegate(NavigationDelegate(onProgress: (value) { + if (!mounted) { + return; + } + + setState(() { + _progress = value; + }); + })) + ..loadRequest(widget.uri); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(title: Text(widget.uri.toString())), + body: Stack( + children: [ + Positioned.fill(child: WebViewWidget(controller: _controller)), + if (_progress < 100) + Positioned( + left: 0, + right: 0, + top: 0, + child: LinearProgressIndicator( + value: _progress / 100.0, + )) + ], + )); + } +} diff --git a/packages/system/vyuh_feature_system/lib/feature.dart b/packages/system/vyuh_feature_system/lib/feature.dart new file mode 100644 index 00000000..47442bee --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/feature.dart @@ -0,0 +1,211 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_sanity_portable_text/flutter_sanity_portable_text.dart'; +import 'package:go_router/go_router.dart'; +import 'package:vyuh_core/vyuh_core.dart' as vt; +import 'package:vyuh_core/vyuh_core.dart'; +import 'package:vyuh_extension_content/vyuh_extension_content.dart'; +import 'package:vyuh_extension_script/js_script.dart'; +import 'package:vyuh_extension_script/vyuh_extension_script.dart'; +import 'package:vyuh_feature_system/content/card/list_item_layout.dart'; +import 'package:vyuh_feature_system/content/portable_text/invoke_action.dart'; +import 'package:vyuh_feature_system/content/route/default_layout.dart'; +import 'package:vyuh_feature_system/content/route/dialog_layout.dart'; +import 'package:vyuh_feature_system/content/route/route_type.dart'; +import 'package:vyuh_feature_system/content/route/sliver_layout.dart'; +import 'package:vyuh_feature_system/content/route/tabs.dart'; +import 'package:vyuh_feature_system/content/web_view.dart'; +import 'package:vyuh_feature_system/vyuh_feature_system.dart' as vf; +import 'package:vyuh_feature_system/vyuh_feature_system.dart'; + +final feature = FeatureDescriptor( + name: 'system', + title: 'System', + description: 'The core building blocks of the framework', + icon: Icons.hub, + routes: () => [ + GoRoute( + path: '/__system_error__', + pageBuilder: (context, state) { + return MaterialPage( + child: vyuh.widgetBuilder.errorView( + title: 'System error', + error: state.extra.toString(), + onRetry: () => vyuh.tracker.init(), + ), + ); + }, + ), + GoRoute( + path: '/__system_navigate__', + pageBuilder: (context, state) { + switch (state.extra) { + case Uri(): + return MaterialPage(child: WebView(uri: state.extra as Uri)); + } + + return const MaterialPage( + child: Scaffold(body: Center(child: Text('No route'))), + ); + }, + ), + ], + extensions: [ + ContentExtensionDescriptor( + contents: [ + RouteDescriptor(routeTypes: [ + TypeDescriptor( + schemaType: PageRouteType.schemaName, + title: 'Default Page Route', + fromJson: PageRouteType.fromJson, + ), + TypeDescriptor( + schemaType: DialogRouteType.schemaName, + title: 'Default Dialog Route', + fromJson: DialogRouteType.fromJson, + ), + ], layouts: [ + TypeDescriptor( + schemaType: DefaultRouteLayout.schemaName, + title: 'Default Layout', + fromJson: DefaultRouteLayout.fromJson, + ), + TypeDescriptor( + schemaType: TabsRouteLayout.schemaName, + title: 'Tab Layout', + fromJson: TabsRouteLayout.fromJson, + ), + TypeDescriptor( + schemaType: SliverRouteLayout.schemaName, + title: 'Sliver Layout', + fromJson: SliverRouteLayout.fromJson, + ), + TypeDescriptor( + schemaType: DialogRouteLayout.schemaName, + title: 'Dialog Layout', + fromJson: DialogRouteLayout.fromJson, + ), + ]), + CardDescriptor(layouts: [ + TypeDescriptor( + schemaType: ListItemCardLayout.schemaName, + title: 'List Item Layout', + fromJson: ListItemCardLayout.fromJson, + ), + ]), + GroupDescriptor(layouts: [ + TypeDescriptor( + schemaType: GridGroupLayout.schemaName, + title: 'Grid Layout', + fromJson: GridGroupLayout.fromJson, + ), + ]), + ConditionalDescriptor(), + ConditionalRouteDescriptor(), + UnknownDescriptor(), + EmptyDescriptor(), + PortableTextDescriptor( + markDefs: [ + MarkDefDescriptor( + schemaType: 'invokeAction', + fromJson: InvokeActionMarkDef.fromJson, + styleBuilder: (def, context, style) { + final theme = Theme.of(context); + + return style.copyWith( + color: theme.colorScheme.primary, + decorationColor: theme.colorScheme.inversePrimary, + decorationStyle: TextDecorationStyle.double, + decoration: TextDecoration.underline, + ); + }, + recognizerBuilder: (def, context) { + return TapGestureRecognizer() + ..onTap = () { + (def as InvokeActionMarkDef).action.execute(context); + }; + }, + ) + ], + blocks: [ + BlockItemDescriptor( + schemaType: Unknown.schemaName, + fromJson: Unknown.fromJson, + builder: (context, content) => + vyuh.content.buildContent(context, content as Unknown), + ), + BlockItemDescriptor( + schemaType: TextBlockItem.schemaName, + fromJson: TextBlockItem.fromJson, + builder: (_, content) => + PortableTextBlock(model: content as TextBlockItem), + ), + BlockItemDescriptor( + schemaType: vf.Card.schemaName, + fromJson: vf.Card.fromJson, + builder: (context, content) => + vyuh.content.buildContent(context, content as vf.Card), + ), + BlockItemDescriptor( + schemaType: Group.schemaName, + fromJson: Group.fromJson, + builder: (context, content) => + vyuh.content.buildContent(context, content as Group), + ), + BlockItemDescriptor( + schemaType: vf.Divider.schemaName, + fromJson: vf.Divider.fromJson, + builder: (context, content) => + vyuh.content.buildContent(context, content as vf.Divider), + ), + ], + ), + DividerDescriptor(), + AccordionDescriptor() + ], + contentBuilders: [ + RouteContentBuilder(), + CardContentBuilder(), + GroupContentBuilder(), + ConditionalContentBuilder(), + ConditionalRouteBuilder(), + UnknownContentBuilder(), + EmptyContentBuilder(), + PortableTextContentBuilder(), + DividerContentBuilder(), + AccordionContentBuilder(), + ], + conditions: [ + TypeDescriptor( + schemaType: BooleanCondition.schemaName, + title: 'Boolean Condition', + fromJson: BooleanCondition.fromJson, + ) + ], + actions: [ + NavigationAction.typeDescriptor, + JavaScriptAction.typeDescriptor, + ], + ), + ScriptExtensionDescriptor( + name: 'restart', + function: (args) { + return vyuh.tracker.init(); + }), + ScriptExtensionDescriptor( + name: 'switchTheme', + function: (args) { + final service = vyuh.di.get(); + service.changeTheme( + service.currentMode.value == ThemeMode.light + ? ThemeMode.dark + : ThemeMode.light, + ); + }, + ), + ], + extensionBuilders: [ + ScriptExtensionBuilder(), + ContentExtensionBuilder(), + ], +); diff --git a/packages/system/vyuh_feature_system/lib/ui/carousel.dart b/packages/system/vyuh_feature_system/lib/ui/carousel.dart new file mode 100644 index 00000000..7555ac48 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/ui/carousel.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:smooth_page_indicator/smooth_page_indicator.dart'; +import 'package:vyuh_core/vyuh_core.dart'; +import 'package:vyuh_feature_system/vyuh_feature_system.dart'; + +class Carousel extends StatefulWidget { + final Group content; + + const Carousel({super.key, required this.content}); + + @override + State createState() => _CarouselState(); +} + +class _CarouselState extends State { + final _controller = PageController(viewportFraction: 0.95); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + + return Column( + children: [ + SizedBox( + height: 200, + child: PageView( + padEnds: false, + controller: _controller, + pageSnapping: true, + scrollDirection: Axis.horizontal, + children: widget.content.items + .map((e) => vyuh.content.buildContent(context, e)) + .toList(growable: false), + ), + ), + SmoothPageIndicator( + controller: _controller, + onDotClicked: (index) => _controller.animateToPage(index, + curve: Curves.easeInOutCubic, + duration: const Duration(milliseconds: 300)), + count: widget.content.items.length, + effect: WormEffect( + offset: 20, + spacing: 4, + dotHeight: 3, + dotWidth: 12, + dotColor: theme.colorScheme.inversePrimary, + activeDotColor: theme.colorScheme.primary, + ), + ) + ], + ); + } +} diff --git a/packages/system/vyuh_feature_system/lib/ui/content_image.dart b/packages/system/vyuh_feature_system/lib/ui/content_image.dart new file mode 100644 index 00000000..e435c4fe --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/ui/content_image.dart @@ -0,0 +1,95 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:vyuh_core/vyuh_core.dart'; + +class ContentImage extends StatelessWidget { + final String? url; + final String? ref; + final double? width; + final double? height; + + final BoxFit fit; + final String? format; + + static const int nearestSize = 10; + + ContentImage({ + super.key, + this.url, + this.width, + this.height, + this.ref, + this.fit = BoxFit.cover, + this.format, + }) { + assert((url != null && ref == null) || (url == null && ref != null), + 'One of url or ref must be specified.'); + } + + @override + Widget build(final BuildContext context) { + final dpr = MediaQuery.devicePixelRatioOf(context).toInt(); + + return LayoutBuilder( + builder: (final BuildContext context, final BoxConstraints constraints) { + var roundedWidth = constraints.maxWidth.isFinite + ? constraints.maxWidth.roundedTo(nearestSize) + : constraints.minWidth > 0 + ? constraints.minWidth.roundedTo(nearestSize) + : null; + final roundedHeight = (roundedWidth == null) + ? null + : (constraints.maxHeight.isFinite + ? constraints.maxHeight.roundedTo(nearestSize) + : (constraints.minHeight > 0 + ? constraints.minHeight.roundedTo(nearestSize) + : null)); + + roundedWidth = (roundedWidth == null && roundedHeight == null) + ? MediaQuery.sizeOf(context).width.roundedTo(nearestSize) + : roundedWidth; + + final url = (ref != null) + ? vyuh.content.provider + .imageUrl( + ref!, + width: roundedWidth, + height: roundedHeight, + devicePixelRatio: dpr, + quality: dpr > 1 ? 90 : 50, + format: format, + ) + ?.toString() + : this.url; + + return CachedNetworkImage( + imageUrl: url ?? '', + placeholder: (final context, final __) => vyuh.widgetBuilder + .imagePlaceholder( + width: roundedWidth?.toDouble(), + height: roundedHeight?.toDouble()), + errorWidget: (final context, final __, final ___) => + vyuh.widgetBuilder.imagePlaceholder( + width: roundedWidth?.toDouble(), + height: roundedHeight?.toDouble()), + width: roundedWidth?.toDouble() ?? width, + height: roundedHeight?.toDouble() ?? height, + memCacheHeight: roundedHeight ?? width?.toInt(), + maxHeightDiskCache: roundedHeight ?? height?.toInt(), + fit: fit, + fadeOutDuration: Duration.zero, + fadeInDuration: Duration.zero, + ); + }, + ); + } +} + +extension on double { + int roundedTo(final int multiple) { + final value = toInt(); + final remainder = value % multiple; + + return remainder == 0 ? value : (value - remainder); + } +} diff --git a/packages/system/vyuh_feature_system/lib/ui/dialog_page.dart b/packages/system/vyuh_feature_system/lib/ui/dialog_page.dart new file mode 100644 index 00000000..59b0fd4b --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/ui/dialog_page.dart @@ -0,0 +1,76 @@ +import 'package:flutter/material.dart'; + +class ModalDialogPage extends Page { + final Offset? anchorPoint; + final Color? barrierColor; + final bool barrierDismissible; + final String? barrierLabel; + final bool useSafeArea; + final CapturedThemes? themes; + final WidgetBuilder builder; + + const ModalDialogPage({ + required this.builder, + this.anchorPoint, + this.barrierColor = Colors.transparent, + this.barrierDismissible = true, + this.barrierLabel, + this.useSafeArea = true, + this.themes, + super.key, + super.name, + super.arguments, + super.restorationId, + }); + + @override + Route createRoute(BuildContext context) => ModalBottomSheetRoute( + isScrollControlled: false, + isDismissible: barrierDismissible, + enableDrag: true, + showDragHandle: true, + settings: this, + builder: builder, + anchorPoint: anchorPoint, + barrierLabel: barrierLabel, + useSafeArea: useSafeArea, + capturedThemes: themes, + ); +} + +class DialogPage extends Page { + final Offset? anchorPoint; + final Color? barrierColor; + final bool barrierDismissible; + final String? barrierLabel; + final bool useSafeArea; + final CapturedThemes? themes; + final WidgetBuilder builder; + + const DialogPage({ + required this.builder, + this.anchorPoint, + this.barrierColor = Colors.transparent, + this.barrierDismissible = true, + this.barrierLabel, + this.useSafeArea = true, + this.themes, + super.key, + super.name, + super.arguments, + super.restorationId, + }); + + @override + Route createRoute(BuildContext context) => DialogRoute( + context: context, + barrierColor: barrierColor, + barrierDismissible: barrierDismissible, + settings: this, + builder: builder, + anchorPoint: anchorPoint, + barrierLabel: barrierLabel, + useSafeArea: useSafeArea, + themes: themes, + ); +} diff --git a/packages/system/vyuh_feature_system/lib/ui/press_effect.dart b/packages/system/vyuh_feature_system/lib/ui/press_effect.dart new file mode 100644 index 00000000..85368454 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/ui/press_effect.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_animate/flutter_animate.dart'; + +class PressEffect extends StatefulWidget { + final Widget child; + final Function(BuildContext context)? onTap; + final double scale; + + const PressEffect({ + super.key, + required this.child, + this.onTap, + this.scale = 0.95, + }); + + @override + State createState() => _PressEffectState(); +} + +class _PressEffectState extends State { + var _tapped = false; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: widget.onTap != null + ? () { + setState(() { + if (_tapped) { + return; + } + + _tapped = true; + }); + } + : null, + child: _tapped + ? widget.child.animate(onComplete: (_) { + setState(() { + _tapped = false; + + Future.delayed( + const Duration(milliseconds: 50), + () => widget.onTap?.call(context), + ); + }); + }).toggle( + duration: 25.ms, + delay: 50.ms, + builder: (_, value, child) { + return child.animate().scaleXY(end: value ? widget.scale : 1.0); + }) + : widget.child, + ); + } +} diff --git a/packages/system/vyuh_feature_system/lib/vyuh_feature_system.dart b/packages/system/vyuh_feature_system/lib/vyuh_feature_system.dart new file mode 100644 index 00000000..cb40e2e8 --- /dev/null +++ b/packages/system/vyuh_feature_system/lib/vyuh_feature_system.dart @@ -0,0 +1,10 @@ +library vyuh_feature_system; + +export 'action/navigation.dart'; +export 'condition/boolean.dart'; +export 'content/index.dart'; +export 'feature.dart'; +export 'ui/carousel.dart'; +export 'ui/content_image.dart'; +export 'ui/dialog_page.dart'; +export 'ui/press_effect.dart'; diff --git a/packages/system/vyuh_feature_system/pubspec.yaml b/packages/system/vyuh_feature_system/pubspec.yaml new file mode 100644 index 00000000..e5b7965c --- /dev/null +++ b/packages/system/vyuh_feature_system/pubspec.yaml @@ -0,0 +1,37 @@ +name: vyuh_feature_system +description: Essential building blocks for a CMS-driven UI +version: 1.0.0-beta.1 +homepage: https://vyuh.tech +repository: https://github.com/vyuh-tech/vyuh/tree/main/packages/system/vyuh_feature_system +issue_tracker: https://github.com/vyuh-tech/vyuh/issues + +environment: + sdk: '>=3.2.0 <4.0.0' + flutter: '>=3.16.0' + +dependencies: + flutter: + sdk: flutter + cached_network_image: ^3.3.1 + collection: ^1.18.0 + flutter_mobx: ^2.2.0+2 + json_annotation: ^4.8.1 + mobx: ^2.3.0+1 + smooth_page_indicator: ^1.1.0 + flutter_animate: ^4.3.0 + go_router: ^13.0.1 + webview_flutter: ^4.4.3 + logger: ^2.0.2+1 + flutter_sanity_portable_text: ^1.0.0-beta.2 + vyuh_core: ^1.0.0-beta.1 + vyuh_extension_content: ^1.0.0-beta.1 + vyuh_extension_script: ^1.0.0-beta.1 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 + json_serializable: ^6.7.1 + build_runner: ^2.4.6 + +flutter: diff --git a/packages/system/vyuh_feature_system/test/vyuh_feature_essentials_test.dart b/packages/system/vyuh_feature_system/test/vyuh_feature_essentials_test.dart new file mode 100644 index 00000000..ab73b3a2 --- /dev/null +++ b/packages/system/vyuh_feature_system/test/vyuh_feature_essentials_test.dart @@ -0,0 +1 @@ +void main() {}