From adf756a72a440587cfd6dff65c9f15f08492516a Mon Sep 17 00:00:00 2001 From: Mithul Nayagam <113115383+Potatomonsta@users.noreply.github.com> Date: Fri, 27 Feb 2026 12:54:32 +0530 Subject: [PATCH 1/4] feat: add DynamicDataProvider and TemplateBuilder - Add shared template utilities for nested data extraction and placeholder processing - Introduce DynamicDataProvider model/parser and DynamicDataScope for scoped fetched data - Introduce TemplateBuilder model/parser for data-driven child generation from lists or direct JSON - Wire DynamicDataProvider/TemplateBuilder into StacService resolution and parser registry - Refactor movie_app home_screen to use DynamicDataProvider + TemplateBuilder instead of raw JSON itemTemplate --- examples/movie_app/stac/home_screen.dart | 271 ++++++------------ .../Flutter/GeneratedPluginRegistrant.swift | 2 - examples/stac_gallery/pubspec.lock | 212 +++++++------- .../stac/lib/src/framework/stac_service.dart | 17 +- .../dynamic_data_scope.dart | 76 +++++ .../stac_dynamic_data_provider_parser.dart | 129 +++++++++ .../stac_dynamic_view_parser.dart | 184 +----------- .../stac_template_builder_parser.dart | 140 +++++++++ .../stac/lib/src/parsers/widgets/widgets.dart | 3 + .../stac/lib/src/utils/template_utils.dart | 201 +++++++++++++ .../lib/src/commands/init_command.dart | 3 +- .../lib/src/exceptions/auth_exception.dart | 4 +- .../lib/src/exceptions/build_exception.dart | 4 +- .../lib/src/models/project/project.g.dart | 72 ++--- .../src/models/project/subscription.g.dart | 47 +-- .../lib/src/models/project/ui_loads.dart | 8 +- .../lib/src/models/project/ui_loads.g.dart | 38 +-- .../lib/src/services/auth_service.dart | 14 +- .../lib/src/services/build_service.dart | 43 ++- .../lib/src/services/upgrade_service.dart | 41 +-- .../specifications/widget_type.dart | 6 + .../stac_dynamic_data_provider.dart | 92 ++++++ .../stac_dynamic_data_provider.g.dart | 34 +++ .../stac_template_builder.dart | 93 ++++++ .../stac_template_builder.g.dart | 33 +++ packages/stac_core/lib/widgets/widgets.dart | 2 + 26 files changed, 1163 insertions(+), 606 deletions(-) create mode 100644 packages/stac/lib/src/parsers/widgets/stac_dynamic_data_provider/dynamic_data_scope.dart create mode 100644 packages/stac/lib/src/parsers/widgets/stac_dynamic_data_provider/stac_dynamic_data_provider_parser.dart create mode 100644 packages/stac/lib/src/parsers/widgets/stac_template_builder/stac_template_builder_parser.dart create mode 100644 packages/stac/lib/src/utils/template_utils.dart create mode 100644 packages/stac_core/lib/widgets/dynamic_data_provider/stac_dynamic_data_provider.dart create mode 100644 packages/stac_core/lib/widgets/dynamic_data_provider/stac_dynamic_data_provider.g.dart create mode 100644 packages/stac_core/lib/widgets/template_builder/stac_template_builder.dart create mode 100644 packages/stac_core/lib/widgets/template_builder/stac_template_builder.g.dart diff --git a/examples/movie_app/stac/home_screen.dart b/examples/movie_app/stac/home_screen.dart index 42646a4fd..eccf13ed4 100644 --- a/examples/movie_app/stac/home_screen.dart +++ b/examples/movie_app/stac/home_screen.dart @@ -19,165 +19,40 @@ StacWidget homeScreen() { method: Method.get, ), ), - StacColumn( - children: [ - StacPadding( - padding: StacEdgeInsets.only( - left: 16, - right: 16, - top: 24, - bottom: 10, - ), - child: StacRow( - mainAxisAlignment: StacMainAxisAlignment.spaceBetween, - children: [ - StacText( - data: AppStrings.nowPlaying, - style: StacThemeData.textTheme.labelLarge, - ), - ], - ), - ), - StacSizedBox( - height: 164, - child: StacDynamicView( - request: StacNetworkRequest( - url: AppApi.getNowPlayingMoviesUrl(), - method: Method.get, - ), - targetPath: 'results', - template: _buildMovieListViewTemplate(), - ), - ), - ], + _buildMovieSection( + title: AppStrings.nowPlaying, + request: StacNetworkRequest( + url: AppApi.getNowPlayingMoviesUrl(), + method: Method.get, + ), ), - StacColumn( - children: [ - StacPadding( - padding: StacEdgeInsets.only( - left: 16, - right: 16, - top: 24, - bottom: 10, - ), - child: StacRow( - mainAxisAlignment: StacMainAxisAlignment.spaceBetween, - children: [ - StacText( - data: AppStrings.popularMovies, - style: StacThemeData.textTheme.labelLarge, - ), - ], - ), - ), - StacSizedBox( - height: 164, - child: StacDynamicView( - request: StacNetworkRequest( - url: AppApi.getPopularMoviesUrl(), - method: Method.get, - ), - targetPath: 'results', - template: _buildMovieListViewTemplate(), - ), - ), - ], + _buildMovieSection( + title: AppStrings.popularMovies, + request: StacNetworkRequest( + url: AppApi.getPopularMoviesUrl(), + method: Method.get, + ), ), - StacColumn( - children: [ - StacPadding( - padding: StacEdgeInsets.only( - left: 16, - right: 16, - top: 24, - bottom: 10, - ), - child: StacRow( - mainAxisAlignment: StacMainAxisAlignment.spaceBetween, - children: [ - StacText( - data: AppStrings.trendingMovies, - style: StacThemeData.textTheme.labelLarge, - ), - ], - ), - ), - StacSizedBox( - height: 164, - child: StacDynamicView( - request: StacNetworkRequest( - url: AppApi.getTrendingMoviesUrl(), - method: Method.get, - ), - targetPath: 'results', - template: _buildMovieListViewTemplate(), - ), - ), - ], + _buildMovieSection( + title: AppStrings.trendingMovies, + request: StacNetworkRequest( + url: AppApi.getTrendingMoviesUrl(), + method: Method.get, + ), ), - StacColumn( - children: [ - StacPadding( - padding: StacEdgeInsets.only( - left: 16, - right: 16, - top: 24, - bottom: 10, - ), - child: StacRow( - mainAxisAlignment: StacMainAxisAlignment.spaceBetween, - children: [ - StacText( - data: AppStrings.topRated, - style: StacThemeData.textTheme.labelLarge, - ), - ], - ), - ), - StacSizedBox( - height: 164, - child: StacDynamicView( - request: StacNetworkRequest( - url: AppApi.getTopRatedMoviesUrl(), - method: Method.get, - ), - targetPath: 'results', - template: _buildMovieListViewTemplate(), - ), - ), - ], + _buildMovieSection( + title: AppStrings.topRated, + request: StacNetworkRequest( + url: AppApi.getTopRatedMoviesUrl(), + method: Method.get, + ), ), - StacColumn( - children: [ - StacPadding( - padding: StacEdgeInsets.only( - left: 16, - right: 16, - top: 24, - bottom: 10, - ), - child: StacRow( - mainAxisAlignment: StacMainAxisAlignment.spaceBetween, - children: [ - StacText( - data: AppStrings.upcomingMovies, - style: StacThemeData.textTheme.labelLarge, - ), - ], - ), - ), - StacSizedBox( - height: 164, - child: StacDynamicView( - request: StacNetworkRequest( - url: AppApi.getUpcomingMoviesUrl(), - method: Method.get, - ), - targetPath: 'results', - template: _buildMovieListViewTemplate(), - ), - ), - ], + _buildMovieSection( + title: AppStrings.upcomingMovies, + request: StacNetworkRequest( + url: AppApi.getUpcomingMoviesUrl(), + method: Method.get, + ), ), StacSizedBox(height: 80), ], @@ -206,36 +81,64 @@ StacWidget homeScreen() { ); } -/// Helper function to build a ListView template with itemTemplate for movie lists. -/// Note: itemTemplate is a parser-specific feature handled by the dynamicView parser. -/// We construct the template as JSON to include itemTemplate. -StacWidget _buildMovieListViewTemplate() { - // Create template JSON with itemTemplate (parser-specific feature) - final templateJson = { - 'type': 'listView', - 'scrollDirection': 'horizontal', - 'shrinkWrap': true, - 'separator': StacSizedBox(width: 8).toJson(), - 'padding': StacEdgeInsets.only(left: 16).toJson(), - 'itemTemplate': StacGestureDetector( - onTap: StacSetValueAction( - values: [ - {'key': 'movie_id', 'value': '{{id}}'}, - ], - action: StacNavigator.pushStac('detail_screen'), - ), - child: StacClipRRect( - borderRadius: StacBorderRadius.all(6), - child: StacImage( - imageType: StacImageType.network, - src: '${AppApi.imageBaseUrl}/{{poster_path}}', - width: 108, +StacWidget _buildMovieSection({ + required String title, + required StacNetworkRequest request, +}) { + return StacDynamicDataProvider( + id: title, + request: request, + targetPath: 'results', + child: StacColumn( + children: [ + StacPadding( + padding: StacEdgeInsets.only( + left: 16, + right: 16, + top: 24, + bottom: 10, + ), + child: StacRow( + mainAxisAlignment: StacMainAxisAlignment.spaceBetween, + children: [ + StacText(data: title, style: StacThemeData.textTheme.labelLarge), + ], + ), + ), + StacSizedBox( height: 164, + child: StacTemplateBuilder( + providerId: title, + itemTemplate: _buildMoviePosterItem(), + child: StacListView( + scrollDirection: StacAxis.horizontal, + shrinkWrap: true, + separator: StacSizedBox(width: 8), + padding: StacEdgeInsets.only(left: 16), + ), + ), ), - ), - ).toJson(), - }; + ], + ), + ); +} - // Create a StacWidget with the JSON data - return StacWidget(jsonData: templateJson); +StacWidget _buildMoviePosterItem() { + return StacGestureDetector( + onTap: StacSetValueAction( + values: [ + {'key': 'movie_id', 'value': '{{id}}'}, + ], + action: StacNavigator.pushStac('detail_screen'), + ), + child: StacClipRRect( + borderRadius: StacBorderRadius.all(6), + child: StacImage( + imageType: StacImageType.network, + src: '${AppApi.imageBaseUrl}/{{poster_path}}', + width: 108, + height: 164, + ), + ), + ); } diff --git a/examples/stac_gallery/macos/Flutter/GeneratedPluginRegistrant.swift b/examples/stac_gallery/macos/Flutter/GeneratedPluginRegistrant.swift index 896f34b37..b2d096c82 100644 --- a/examples/stac_gallery/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/examples/stac_gallery/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,13 +5,11 @@ import FlutterMacOS import Foundation -import path_provider_foundation import shared_preferences_foundation import sqflite_darwin import webview_flutter_wkwebview func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin")) diff --git a/examples/stac_gallery/pubspec.lock b/examples/stac_gallery/pubspec.lock index 1ef347acb..9ccf5b278 100644 --- a/examples/stac_gallery/pubspec.lock +++ b/examples/stac_gallery/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: bloc - sha256: "52c10575f4445c61dd9e0cafcc6356fdd827c4c64dd7945ef3c4105f6b6ac189" + sha256: a48653a82055a900b88cd35f92429f068c5a8057ae9b136d197b3d56c57efb81 url: "https://pub.dev" source: hosted - version: "9.0.0" + version: "9.2.0" boolean_selector: dependency: transitive description: @@ -69,10 +69,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "8e928697a82be082206edb0b9c99c5a4ad6bc31c9e9b8b2f291ae65cd4a25daa" + sha256: bf05f6e12cfea92d3c09308d7bcdab1906cd8a179b023269eed00c071004b957 url: "https://pub.dev" source: hosted - version: "4.0.4" + version: "4.1.1" build_runner: dependency: "direct dev" description: @@ -93,10 +93,10 @@ packages: dependency: transitive description: name: built_value - sha256: "7931c90b84bc573fef103548e354258ae4c9d28d140e41961df6843c5d60d4d8" + sha256: "6ae8a6435a8c6520c7077b107e77f1fb4ba7009633259a4d49a8afd8e7efc5e9" url: "https://pub.dev" source: hosted - version: "8.12.3" + version: "8.12.4" cached_network_image: dependency: transitive description: @@ -133,10 +133,10 @@ packages: dependency: transitive description: name: checked_yaml - sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + sha256: "959525d3162f249993882720d52b7e0c833978df229be20702b33d48d91de70f" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.4" clock: dependency: transitive description: @@ -145,14 +145,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + code_assets: + dependency: transitive + description: + name: code_assets + sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + url: "https://pub.dev" + source: hosted + version: "1.0.0" code_builder: dependency: transitive description: name: code_builder - sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + sha256: "6a6cab2ba4680d6423f34a9b972a4c9a94ebe1b62ecec4e1a1f2cba91fd1319d" url: "https://pub.dev" source: hosted - version: "4.10.1" + version: "4.11.1" collection: dependency: transitive description: @@ -173,10 +181,10 @@ packages: dependency: transitive description: name: crypto - sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "3.0.7" cupertino_icons: dependency: "direct main" description: @@ -189,26 +197,26 @@ packages: dependency: transitive description: name: dart_style - sha256: "15a7db352c8fc6a4d2bc475ba901c25b39fe7157541da4c16eacce6f8be83e49" + sha256: "6f6b30cba0301e7b38f32bdc9a6bdae6f5921a55f0a1eb9450e1e6515645dbb2" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" dio: dependency: transitive description: name: dio - sha256: d90ee57923d1828ac14e492ca49440f65477f4bb1263575900be731a3dac66a9 + sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c url: "https://pub.dev" source: hosted - version: "5.9.0" + version: "5.9.2" dio_web_adapter: dependency: transitive description: name: dio_web_adapter - sha256: "7586e476d70caecaf1686d21eee7247ea43ef5c345eab9e0cc3583ff13378d78" + sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" fake_async: dependency: transitive description: @@ -221,10 +229,10 @@ packages: dependency: transitive description: name: ffi - sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" file: dependency: transitive description: @@ -320,14 +328,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + hooks: + dependency: transitive + description: + name: hooks + sha256: "7a08a0d684cb3b8fb604b78455d5d352f502b68079f7b80b831c62220ab0a4f6" + url: "https://pub.dev" + source: hosted + version: "1.0.1" http: dependency: transitive description: name: http - sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" + sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.6.0" http_multi_server: dependency: transitive description: @@ -356,34 +372,26 @@ packages: dependency: "direct main" description: name: json_annotation - sha256: "805fa86df56383000f640384b282ce0cb8431f1a7a2396de92fb66186d8c57df" + sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 url: "https://pub.dev" source: hosted - version: "4.10.0" - json_schema: - dependency: transitive - description: - name: json_schema - sha256: f37d9c3fdfe8c9aae55fdfd5af815d24ce63c3a0f6a2c1f0982c30f43643fa1a - url: "https://pub.dev" - source: hosted - version: "5.2.2" + version: "4.11.0" json_serializable: dependency: "direct dev" description: name: json_serializable - sha256: "93fba3ad139dab2b1ce59ecc6fdce6da46a42cdb6c4399ecda30f1e7e725760d" + sha256: "44729f5c45748e6748f6b9a57ab8f7e4336edc8ae41fc295070e3814e616a6c0" url: "https://pub.dev" source: hosted - version: "6.12.0" + version: "6.13.0" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0" + sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de" url: "https://pub.dev" source: hosted - version: "11.0.1" + version: "11.0.2" leak_tracker_flutter_testing: dependency: transitive description: @@ -412,10 +420,10 @@ packages: dependency: transitive description: name: logger - sha256: be4b23575aac7ebf01f225a241eb7f6b5641eeaf43c6a8613510fc2f8cf187d1 + sha256: a7967e31b703831a893bbc3c3dd11db08126fe5f369b5c648a36f821979f5be3 url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.6.2" logging: dependency: transitive description: @@ -456,6 +464,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + native_toolchain_c: + dependency: transitive + description: + name: native_toolchain_c + sha256: "89e83885ba09da5fdf2cdacc8002a712ca238c28b7f717910b34bcd27b0d03ac" + url: "https://pub.dev" + source: hosted + version: "0.17.4" nested: dependency: transitive description: @@ -464,6 +480,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + url: "https://pub.dev" + source: hosted + version: "9.3.0" octo_image: dependency: transitive description: @@ -508,18 +532,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 + sha256: f2c65e21139ce2c3dad46922be8272bb5963516045659e71bb16e151c93b580e url: "https://pub.dev" source: hosted - version: "2.2.17" + version: "2.2.22" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699" url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.6.0" path_provider_linux: dependency: transitive description: @@ -548,10 +572,10 @@ packages: dependency: transitive description: name: petitparser - sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" + sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675" url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "7.0.2" platform: dependency: transitive description: @@ -572,18 +596,18 @@ packages: dependency: transitive description: name: pool - sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d" url: "https://pub.dev" source: hosted - version: "1.5.1" + version: "1.5.2" provider: dependency: transitive description: name: provider - sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84" + sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272" url: "https://pub.dev" source: hosted - version: "6.1.5" + version: "6.1.5+1" pub_semver: dependency: transitive description: @@ -600,22 +624,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" - quiver: - dependency: transitive - description: - name: quiver - sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 - url: "https://pub.dev" - source: hosted - version: "3.2.2" - rfc_6901: - dependency: transitive - description: - name: rfc_6901 - sha256: "6a43b1858dca2febaf93e15639aa6b0c49ccdfd7647775f15a499f872b018154" - url: "https://pub.dev" - source: hosted - version: "0.2.1" rxdart: dependency: transitive description: @@ -636,10 +644,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "07d552dbe8e71ed720e5205e760438ff4ecfb76ec3b32ea664350e2ca4b0c43b" + sha256: "8374d6200ab33ac99031a852eba4c8eb2170c4bf20778b3e2c9eccb45384fb41" url: "https://pub.dev" source: hosted - version: "2.4.16" + version: "2.4.21" shared_preferences_foundation: dependency: transitive description: @@ -721,18 +729,10 @@ packages: dependency: transitive description: name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab" url: "https://pub.dev" source: hosted - version: "1.10.1" - sprintf: - dependency: transitive - description: - name: sprintf - sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.dev" - source: hosted - version: "7.0.0" + version: "1.10.2" sqflite: dependency: transitive description: @@ -745,18 +745,18 @@ packages: dependency: transitive description: name: sqflite_android - sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b" + sha256: ecd684501ebc2ae9a83536e8b15731642b9570dc8623e0073d227d0ee2bfea88 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2+2" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b" + sha256: "6ef422a4525ecc601db6c0a2233ff448c731307906e92cabc9ba292afaae16a6" url: "https://pub.dev" source: hosted - version: "2.5.5" + version: "2.5.6" sqflite_darwin: dependency: transitive description: @@ -844,10 +844,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "0669c70faae6270521ee4f05bffd2919892d42d1276e6c495be80174b6bc0ef6" + sha256: c254ade258ec8282947a0acbbc90b9575b4f19673533ee46f2f6e9b3aeefd7c0 url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.4.0" term_glyph: dependency: transitive description: @@ -872,30 +872,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" - uri: - dependency: transitive - description: - name: uri - sha256: "889eea21e953187c6099802b7b4cf5219ba8f3518f604a1033064d45b1b8268a" - url: "https://pub.dev" - source: hosted - version: "1.0.0" uuid: dependency: transitive description: name: uuid - sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489" url: "https://pub.dev" source: hosted - version: "4.5.1" + version: "4.5.3" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 url: "https://pub.dev" source: hosted - version: "1.1.18" + version: "1.1.19" vector_graphics_codec: dependency: transitive description: @@ -908,10 +900,10 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: "557a315b7d2a6dbb0aaaff84d857967ce6bdc96a63dc6ee2a57ce5a6ee5d3331" + sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" url: "https://pub.dev" source: hosted - version: "1.1.17" + version: "1.2.0" vector_math: dependency: transitive description: @@ -924,18 +916,18 @@ packages: dependency: transitive description: name: vm_service - sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" url: "https://pub.dev" source: hosted - version: "15.0.0" + version: "15.0.2" watcher: dependency: transitive description: name: watcher - sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.1" web: dependency: transitive description: @@ -972,26 +964,26 @@ packages: dependency: transitive description: name: webview_flutter_android - sha256: f6e6afef6e234801da77170f7a1847ded8450778caf2fe13979d140484be3678 + sha256: "2a03df01df2fd30b075d1e7f24c28aee593f2e5d5ac4c3c4283c5eda63717b24" url: "https://pub.dev" source: hosted - version: "4.7.0" + version: "4.10.13" webview_flutter_platform_interface: dependency: transitive description: name: webview_flutter_platform_interface - sha256: "7cb32b21825bd65569665c32bb00a34ded5779786d6201f5350979d2d529940d" + sha256: "63d26ee3aca7256a83ccb576a50272edd7cfc80573a4305caa98985feb493ee0" url: "https://pub.dev" source: hosted - version: "2.13.0" + version: "2.14.0" webview_flutter_wkwebview: dependency: transitive description: name: webview_flutter_wkwebview - sha256: a3d461fe3467014e05f3ac4962e5fdde2a4bf44c561cb53e9ae5c586600fdbc3 + sha256: fc0af89d403e1c053f03d023d97550412fa79f35332e2939514c82e6fe633198 url: "https://pub.dev" source: hosted - version: "3.22.0" + version: "3.23.8" xdg_directories: dependency: transitive description: @@ -1004,10 +996,10 @@ packages: dependency: transitive description: name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.6.1" yaml: dependency: transitive description: @@ -1017,5 +1009,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.9.0 <4.0.0" - flutter: ">=3.35.0" + dart: ">=3.10.3 <4.0.0" + flutter: ">=3.38.4" diff --git a/packages/stac/lib/src/framework/stac_service.dart b/packages/stac/lib/src/framework/stac_service.dart index 5a1cfbd9c..1f223d4f3 100644 --- a/packages/stac/lib/src/framework/stac_service.dart +++ b/packages/stac/lib/src/framework/stac_service.dart @@ -19,6 +19,7 @@ import 'package:stac/src/parsers/widgets/stac_row/stac_row_parser.dart'; import 'package:stac/src/parsers/widgets/stac_text/stac_text_parser.dart'; import 'package:stac/src/parsers/widgets/stac_tool_tip/stac_tool_tip_parser.dart'; import 'package:stac/src/services/stac_network_service.dart'; +import 'package:stac/src/utils/template_utils.dart'; import 'package:stac/src/utils/variable_resolver.dart'; import 'package:stac_core/stac_core.dart'; import 'package:stac_framework/stac_framework.dart'; @@ -127,6 +128,7 @@ class StacService { const StacAspectRatioParser(), const StacFittedBoxParser(), const StacLimitedBoxParser(), + const StacDynamicDataProviderParser(), const StacDynamicViewParser(), const StacDropdownMenuParser(), const StacClipRRectParser(), @@ -139,6 +141,7 @@ class StacService { const StacBackdropFilterParser(), const StacVerticalDividerParser(), const StacSelectableTextParser(), + const StacTemplateBuilderParser(), ]; static final _actionParsers = [ @@ -235,7 +238,12 @@ class StacService { ? json : resolveVariablesInJson(json, StacRegistry.instance); - final model = stacParser.getModel(resolvedJson); + // Second pass: resolve {{providerId.path}} from DynamicDataScope + final fullyResolved = widgetType == WidgetType.setValue.name + ? resolvedJson + : resolveDynamicDataInJson(resolvedJson, context); + + final model = stacParser.getModel(fullyResolved); return stacParser.parse(context, model); } catch (e, stackTrace) { // Log error with full context @@ -292,7 +300,12 @@ class StacService { ? widget.toJson() : resolveVariablesInJson(widget.toJson(), StacRegistry.instance); - final model = stacParser.getModel(resolvedJson); + // Second pass: resolve {{providerId.path}} from DynamicDataScope + final fullyResolved = widgetType == WidgetType.setValue.name + ? resolvedJson + : resolveDynamicDataInJson(resolvedJson, context); + + final model = stacParser.getModel(fullyResolved); return stacParser.parse(context, model); } catch (e, stackTrace) { _logError( diff --git a/packages/stac/lib/src/parsers/widgets/stac_dynamic_data_provider/dynamic_data_scope.dart b/packages/stac/lib/src/parsers/widgets/stac_dynamic_data_provider/dynamic_data_scope.dart new file mode 100644 index 000000000..de5f66c89 --- /dev/null +++ b/packages/stac/lib/src/parsers/widgets/stac_dynamic_data_provider/dynamic_data_scope.dart @@ -0,0 +1,76 @@ +import 'package:flutter/widgets.dart'; +import 'package:stac/src/utils/template_utils.dart'; +import 'package:stac_logger/stac_logger.dart'; + +/// InheritedWidget that exposes data from [DynamicDataProvider] ancestors +/// to their subtrees. +/// +/// Each scope holds a map of provider IDs to their fetched data. When +/// providers are nested, each scope merges the parent's data with its own, +/// making all ancestor providers accessible to any descendant. +class DynamicDataScope extends InheritedWidget { + const DynamicDataScope({ + super.key, + required super.child, + required this.dataMap, + }); + + /// Map of provider IDs to their extracted response data. + final Map dataMap; + + /// Returns the nearest [DynamicDataScope] from the widget tree, + /// or `null` if none is found. + static DynamicDataScope? of(BuildContext context) { + return context.dependOnInheritedWidgetOfExactType(); + } + + /// Returns the nearest [DynamicDataScope] without registering a dependency. + /// Useful for one-time reads that should not trigger rebuilds. + static DynamicDataScope? maybeOf(BuildContext context) { + return context + .getElementForInheritedWidgetOfExactType() + ?.widget + as DynamicDataScope?; + } + + /// Returns the data for a given [providerId], or `null` if not found. + dynamic getData(String providerId) { + return dataMap[providerId]; + } + + /// Resolves a `{{providerId.path.to.value}}` reference against the scope. + /// + /// Splits [expression] on the first `.` to get the provider ID, then + /// extracts the nested value from that provider's data using the + /// remaining path segments. + dynamic resolveExpression(String expression) { + final dotIndex = expression.indexOf('.'); + if (dotIndex == -1) { + return dataMap[expression]; + } + + final providerId = expression.substring(0, dotIndex); + final path = expression.substring(dotIndex + 1); + final providerData = dataMap[providerId]; + + if (providerData == null) { + Log.w('DynamicDataScope: No provider found with id "$providerId"'); + return null; + } + + return extractNestedData(providerData, path.split('.')); + } + + @override + bool updateShouldNotify(covariant DynamicDataScope oldWidget) { + return !_mapsEqual(dataMap, oldWidget.dataMap); + } + + static bool _mapsEqual(Map a, Map b) { + if (a.length != b.length) return false; + for (final key in a.keys) { + if (!b.containsKey(key) || a[key] != b[key]) return false; + } + return true; + } +} diff --git a/packages/stac/lib/src/parsers/widgets/stac_dynamic_data_provider/stac_dynamic_data_provider_parser.dart b/packages/stac/lib/src/parsers/widgets/stac_dynamic_data_provider/stac_dynamic_data_provider_parser.dart new file mode 100644 index 000000000..199166ba4 --- /dev/null +++ b/packages/stac/lib/src/parsers/widgets/stac_dynamic_data_provider/stac_dynamic_data_provider_parser.dart @@ -0,0 +1,129 @@ +import 'dart:convert'; + +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:stac/src/parsers/core/stac_widget_parser.dart'; +import 'package:stac/src/parsers/widgets/stac_dynamic_data_provider/dynamic_data_scope.dart'; +import 'package:stac/src/services/stac_network_service.dart'; +import 'package:stac/src/utils/template_utils.dart'; +import 'package:stac_core/stac_core.dart'; +import 'package:stac_framework/stac_framework.dart'; +import 'package:stac_logger/stac_logger.dart'; + +class StacDynamicDataProviderParser + extends StacParser { + const StacDynamicDataProviderParser(); + + @override + String get type => WidgetType.dynamicDataProvider.name; + + @override + StacDynamicDataProvider getModel(Map json) { + return StacDynamicDataProvider.fromJson(json); + } + + @override + Widget parse(BuildContext context, StacDynamicDataProvider model) { + return _DynamicDataProviderWidget(model: model); + } +} + +class _DynamicDataProviderWidget extends StatefulWidget { + const _DynamicDataProviderWidget({required this.model}); + + final StacDynamicDataProvider model; + + @override + State<_DynamicDataProviderWidget> createState() => + _DynamicDataProviderWidgetState(); +} + +class _DynamicDataProviderWidgetState + extends State<_DynamicDataProviderWidget> { + late Future _future; + + @override + void initState() { + super.initState(); + _future = _fetchData(); + } + + @override + void didUpdateWidget(covariant _DynamicDataProviderWidget oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.model.request != oldWidget.model.request) { + setState(() => _future = _fetchData()); + } + } + + Future _fetchData() async { + try { + return await StacNetworkService.request(context, widget.model.request); + } catch (e) { + Log.e('Error fetching dynamic data provider content: $e'); + rethrow; + } + } + + @override + Widget build(BuildContext context) { + return FutureBuilder( + future: _future, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return widget.model.loaderWidget.parse(context) ?? + const Center(child: CircularProgressIndicator()); + } else if (snapshot.hasError) { + Log.e(snapshot.error); + return widget.model.errorWidget.parse(context) ?? const SizedBox(); + } else if (snapshot.hasData) { + final response = snapshot.data; + if (response != null) { + try { + dynamic responseData; + if (response.data is String) { + responseData = jsonDecode(response.data); + } else if (response.data is Map) { + responseData = response.data; + } else { + responseData = response.data; + } + + final data = widget.model.targetPath?.isEmpty ?? true + ? responseData + : extractNestedData( + responseData, + widget.model.targetPath?.split('.') ?? [], + ); + + // Merge with any parent scope's data + final parentScope = DynamicDataScope.maybeOf(context); + final mergedDataMap = { + if (parentScope != null) ...parentScope.dataMap, + widget.model.id: data, + }; + + return DynamicDataScope( + dataMap: mergedDataMap, + child: Builder( + builder: (scopedContext) { + return widget.model.child.parse(scopedContext) ?? + const SizedBox(); + }, + ), + ); + } catch (e) { + Log.e('Error parsing DynamicDataProvider response: $e'); + return widget.model.errorWidget.parse(context) ?? + const SizedBox(); + } + } + return const SizedBox(); + } else { + return const SizedBox(); + } + }, + ); + } +} diff --git a/packages/stac/lib/src/parsers/widgets/stac_dynamic_view/stac_dynamic_view_parser.dart b/packages/stac/lib/src/parsers/widgets/stac_dynamic_view/stac_dynamic_view_parser.dart index aa72e3dcf..d3e50764b 100644 --- a/packages/stac/lib/src/parsers/widgets/stac_dynamic_view/stac_dynamic_view_parser.dart +++ b/packages/stac/lib/src/parsers/widgets/stac_dynamic_view/stac_dynamic_view_parser.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:stac/src/framework/framework.dart'; import 'package:stac/src/parsers/core/stac_widget_parser.dart'; import 'package:stac/src/services/stac_network_service.dart'; +import 'package:stac/src/utils/template_utils.dart'; import 'package:stac_core/stac_core.dart'; import 'package:stac_framework/stac_framework.dart'; import 'package:stac_logger/stac_logger.dart'; @@ -35,7 +36,6 @@ class StacDynamicViewParser extends StacParser { final response = snapshot.data; if (response != null) { try { - // Handle the response data based on its type dynamic responseData; if (response.data is String) { responseData = jsonDecode(response.data); @@ -47,7 +47,7 @@ class StacDynamicViewParser extends StacParser { final data = model.targetPath?.isEmpty ?? true ? responseData - : _extractNestedData( + : extractNestedData( responseData, model.targetPath?.split('.') ?? [], ); @@ -55,19 +55,16 @@ class StacDynamicViewParser extends StacParser { Log.d("data: $data"); if (data != null) { - // Check if data is an empty list and we have an empty template - if (_isEmptyList(data) && model.emptyTemplate != null) { + if (isEmptyList(data) && model.emptyTemplate != null) { Log.d("Data is empty list, using empty template"); return model.emptyTemplate.parse(context) ?? const SizedBox(); } - // Prepare data for template based on resultTarget final dataForTemplate = (model.resultTarget?.isNotEmpty ?? false) ? {model.resultTarget: data} : data; - // Apply the data to the template final renderedTemplate = _applyDataToTemplate( model.template ?? StacSizedBox(), dataForTemplate, @@ -101,70 +98,16 @@ class StacDynamicViewParser extends StacParser { } } - dynamic _extractNestedData(dynamic data, List keys) { - dynamic current = data; - final RegExp arrayKeyRegex = RegExp(r'(\w+)\[(\d+)\]'); - - for (final key in keys) { - Match? arrayMatch = arrayKeyRegex.firstMatch(key); - - if (arrayMatch != null) { - final String actualKey = arrayMatch.group(1)!; - final int index = int.parse(arrayMatch.group(2)!); - - if (current is Map && current.containsKey(actualKey)) { - dynamic potentialList = current[actualKey]; - if (potentialList is List) { - if (index >= 0 && index < potentialList.length) { - current = potentialList[index]; - } else { - return null; - } - } else { - return null; - } - } else { - return null; - } - } else { - if (current is Map && current.containsKey(key)) { - current = current[key]; - } else if (current is List) { - try { - int index = int.parse(key); - if (index >= 0 && index < current.length) { - current = current[index]; - } else { - return null; - } - } catch (e) { - return null; - } - } else { - return null; - } - } - } - if (current == null) { - return "null"; - } else { - return current; - } - } - Map _applyDataToTemplate( StacWidget currentTemplate, dynamic data, String resultTarget, ) { - // Deep copy template to avoid modifying the original Map resolvedTemplateJson = currentTemplate.toJson(); - // Check for list processing with itemTemplate if (resolvedTemplateJson.containsKey('itemTemplate')) { dynamic listForIteration; final String itemTemplateKey = 'itemTemplate'; - // Ensure itemTemplateActual is correctly typed. final itemTemplateActual = resolvedTemplateJson[itemTemplateKey] as Map; @@ -178,38 +121,21 @@ class StacDynamicViewParser extends StacParser { } if (listForIteration != null) { - // Check if the list is empty if (listForIteration is List && listForIteration.isEmpty) { Log.d( "List for iteration is empty, removing itemTemplate and children", ); resolvedTemplateJson.remove(itemTemplateKey); - // Clear children or set to empty list resolvedTemplateJson['children'] = []; return resolvedTemplateJson; } - resolvedTemplateJson.remove( - itemTemplateKey, - ); // Remove from outer template structure - final processedChildItems = >[]; - - for (final singleRawItem in listForIteration) { - // Removed unnecessary cast - if (singleRawItem is Map) { - final itemSpecificDataContext = resultTarget.isNotEmpty - ? {resultTarget: singleRawItem} - : singleRawItem; - - final processedChild = _applyDataToItem( - itemTemplateActual, - itemSpecificDataContext, - ); - processedChildItems.add(processedChild); - } else { - Log.w("Item in list is not a Map, skipping: $singleRawItem"); - } - } + resolvedTemplateJson.remove(itemTemplateKey); + final processedChildItems = processItemTemplate( + itemTemplate: itemTemplateActual, + listData: listForIteration as List, + resultTarget: resultTarget, + ); if (!resolvedTemplateJson.containsKey('children')) { resolvedTemplateJson['children'] = []; @@ -231,14 +157,11 @@ class StacDynamicViewParser extends StacParser { } } - // Process the (potentially modified) resolvedTemplate itself for any placeholders - // using the original overall dataContext. if (data is Map) { - // Ensure it's Map for _processTemplateRecursively final Map mapDataContext = Map.from( data, ); - _processTemplateRecursively(resolvedTemplateJson, mapDataContext); + processTemplateRecursively(resolvedTemplateJson, mapDataContext); } else { Log.d( "Overall dataContext is not a Map, skipping final placeholder processing for the main template structure. DataContext: $data", @@ -247,91 +170,4 @@ class StacDynamicViewParser extends StacParser { return resolvedTemplateJson; } - - Map _applyDataToItem( - Map template, - Map item, - ) { - final result = jsonDecode(jsonEncode(template)) as Map; - - // Process each key in the template - _processTemplateRecursively(result, item); - - return result; - } - - dynamic _processTemplateRecursively( - dynamic template, - Map data, - ) { - if (template is Map) { - for (final key in template.keys.toList()) { - final value = template[key]; - - if (value is String) { - // Check if the string contains any placeholders - if (value.contains('{{') && value.contains('}}')) { - // Process multiple placeholders in a single string - String processedValue = value; - final regex = RegExp(r'\{\{([^}]+)\}\}'); - final matches = regex.allMatches(value); - - for (final match in matches) { - final placeholder = match.group(0)!; - final dataKey = match.group(1)!.trim(); - final keys = dataKey.split('.'); - - // Extract the value from the data - final dataValue = _extractNestedData(data, keys); - - if (dataValue != null) { - processedValue = processedValue.replaceAll( - placeholder, - dataValue.toString(), - ); - } - } - - template[key] = processedValue; - } - } else if (value is Map || value is List) { - // Recursively process nested maps and lists - _processTemplateRecursively(value, data); - } - } - } else if (template is List) { - for (int i = 0; i < template.length; i++) { - _processTemplateRecursively(template[i], data); - } - } - return template; - } - - /// Helper method to check if the data represents an empty list. - /// This method checks various scenarios: - /// 1. Direct empty list - /// 2. Empty list at the target path (if resultTarget is specified) - /// 3. Empty list in nested data structures - bool _isEmptyList(dynamic data) { - // Direct empty list check - if (data is List && data.isEmpty) { - return true; - } - - // If data is a Map, check if it contains empty lists - if (data is Map) { - // Check all values in the map for empty lists - for (final value in data.values) { - if (value is List && value.isEmpty) { - return true; - } - // Recursively check nested maps - if (value is Map && _isEmptyList(value)) { - return true; - } - } - } - - return false; - } } diff --git a/packages/stac/lib/src/parsers/widgets/stac_template_builder/stac_template_builder_parser.dart b/packages/stac/lib/src/parsers/widgets/stac_template_builder/stac_template_builder_parser.dart new file mode 100644 index 000000000..a47bcfb12 --- /dev/null +++ b/packages/stac/lib/src/parsers/widgets/stac_template_builder/stac_template_builder_parser.dart @@ -0,0 +1,140 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:stac/src/framework/framework.dart'; +import 'package:stac/src/parsers/core/stac_widget_parser.dart'; +import 'package:stac/src/parsers/widgets/stac_dynamic_data_provider/dynamic_data_scope.dart'; +import 'package:stac/src/utils/template_utils.dart'; +import 'package:stac_core/stac_core.dart'; +import 'package:stac_framework/stac_framework.dart'; +import 'package:stac_logger/stac_logger.dart'; + +class StacTemplateBuilderParser extends StacParser { + const StacTemplateBuilderParser(); + + /// Widget types that accept a `children` array of [StacWidget] and are + /// valid as TemplateBuilder child targets. Others would silently drop + /// injected children. + static const Set _layoutWidgetTypesWithChildren = { + 'column', + 'row', + 'listView', + 'gridView', + 'stack', + 'wrap', + 'sliverList', + 'sliverGrid', + 'carouselView', + 'pageView', + 'tabBarView', + 'bottomNavigationView', + }; + + @override + String get type => WidgetType.templateBuilder.name; + + @override + StacTemplateBuilder getModel(Map json) { + return StacTemplateBuilder.fromJson(json); + } + + @override + Widget parse(BuildContext context, StacTemplateBuilder model) { + final listData = _resolveListData(context, model); + + if (listData == null) { + Log.w( + 'TemplateBuilder: No data resolved. ' + 'Provide either "data" or "providerId".', + ); + return model.child.parse(context) ?? const SizedBox(); + } + + if (listData.isEmpty) { + return model.emptyWidget.parse(context) ?? + model.child.parse(context) ?? + const SizedBox(); + } + + final itemTemplateJson = model.itemTemplate.toJson(); + final processedChildren = processItemTemplate( + itemTemplate: itemTemplateJson, + listData: listData, + ); + + // Deep copy the child JSON and inject the generated children + final childJson = + jsonDecode(jsonEncode(model.child.toJson())) as Map; + + final childType = childJson['type'] as String?; + if (childType == null || !_layoutWidgetTypesWithChildren.contains(childType)) { + throw FormatException( + 'TemplateBuilder child must be a layout widget that supports "children". ' + 'Got type: ${childType ?? "null"}. ' + 'Supported types: ${_layoutWidgetTypesWithChildren.join(", ")}.', + ); + } + + if (!childJson.containsKey('children')) { + childJson['children'] = []; + } + if (childJson['children'] is List) { + (childJson['children'] as List).addAll(processedChildren); + } else { + childJson['children'] = processedChildren; + } + + return Stac.fromJson(childJson, context) ?? const SizedBox(); + } + + List? _resolveListData( + BuildContext context, + StacTemplateBuilder model, + ) { + // Direct data takes priority + if (model.data != null) { + return model.data; + } + + // Fall back to DynamicDataScope lookup + if (model.providerId != null) { + final scope = DynamicDataScope.of(context); + if (scope == null) { + Log.w( + 'TemplateBuilder: No DynamicDataScope found in widget tree ' + 'for providerId "${model.providerId}".', + ); + return null; + } + + dynamic providerData = scope.getData(model.providerId!); + if (providerData == null) { + Log.w( + 'TemplateBuilder: No data found for ' + 'providerId "${model.providerId}".', + ); + return null; + } + + // Extract nested list via dataPath if specified + if (model.dataPath?.isNotEmpty ?? false) { + providerData = extractNestedData( + providerData, + model.dataPath!.split('.'), + ); + } + + if (providerData is List) { + return providerData; + } + + Log.w( + 'TemplateBuilder: Resolved data is not a List. ' + 'providerId="${model.providerId}", dataPath="${model.dataPath}".', + ); + return null; + } + + return null; + } +} diff --git a/packages/stac/lib/src/parsers/widgets/widgets.dart b/packages/stac/lib/src/parsers/widgets/widgets.dart index ab44d9fc6..9194b78bd 100644 --- a/packages/stac/lib/src/parsers/widgets/widgets.dart +++ b/packages/stac/lib/src/parsers/widgets/widgets.dart @@ -25,6 +25,8 @@ export 'package:stac/src/parsers/widgets/stac_default_tab_controller/stac_defaul export 'package:stac/src/parsers/widgets/stac_divider/stac_divider_parser.dart'; export 'package:stac/src/parsers/widgets/stac_drawer/stac_drawer_parser.dart'; export 'package:stac/src/parsers/widgets/stac_dropdown_menu/stac_dropdown_menu_parser.dart'; +export 'package:stac/src/parsers/widgets/stac_dynamic_data_provider/dynamic_data_scope.dart'; +export 'package:stac/src/parsers/widgets/stac_dynamic_data_provider/stac_dynamic_data_provider_parser.dart'; export 'package:stac/src/parsers/widgets/stac_dynamic_view/stac_dynamic_view_parser.dart'; export 'package:stac/src/parsers/widgets/stac_elevated_button/stac_elevated_button_parser.dart'; export 'package:stac/src/parsers/widgets/stac_expanded/stac_expanded_parser.dart'; @@ -79,6 +81,7 @@ export 'package:stac/src/parsers/widgets/stac_tab_bar/stac_tab_bar_parser.dart'; export 'package:stac/src/parsers/widgets/stac_tab_bar_view/stac_tab_bar_view_parser.dart'; export 'package:stac/src/parsers/widgets/stac_table/stac_table_parser.dart'; export 'package:stac/src/parsers/widgets/stac_table_cell/stac_table_cell_parser.dart'; +export 'package:stac/src/parsers/widgets/stac_template_builder/stac_template_builder_parser.dart'; export 'package:stac/src/parsers/widgets/stac_text_button/stac_text_button_parser.dart'; export 'package:stac/src/parsers/widgets/stac_text_field/stac_text_field_parser.dart'; export 'package:stac/src/parsers/widgets/stac_text_form_field/stac_text_form_field_parser.dart'; diff --git a/packages/stac/lib/src/utils/template_utils.dart b/packages/stac/lib/src/utils/template_utils.dart new file mode 100644 index 000000000..29767e867 --- /dev/null +++ b/packages/stac/lib/src/utils/template_utils.dart @@ -0,0 +1,201 @@ +import 'dart:convert'; + +import 'package:flutter/widgets.dart'; +import 'package:stac/src/parsers/widgets/stac_dynamic_data_provider/dynamic_data_scope.dart'; +import 'package:stac_logger/stac_logger.dart'; + +/// Extracts nested data from a dynamic structure using a list of keys. +/// +/// Supports dot-notation paths, array index access (e.g. `items[0]`), +/// and numeric keys for list index access. +dynamic extractNestedData(dynamic data, List keys) { + dynamic current = data; + final RegExp arrayKeyRegex = RegExp(r'(\w+)\[(\d+)\]'); + + for (final key in keys) { + Match? arrayMatch = arrayKeyRegex.firstMatch(key); + + if (arrayMatch != null) { + final String actualKey = arrayMatch.group(1)!; + final int index = int.parse(arrayMatch.group(2)!); + + if (current is Map && current.containsKey(actualKey)) { + dynamic potentialList = current[actualKey]; + if (potentialList is List) { + if (index >= 0 && index < potentialList.length) { + current = potentialList[index]; + } else { + return null; + } + } else { + return null; + } + } else { + return null; + } + } else { + if (current is Map && current.containsKey(key)) { + current = current[key]; + } else if (current is List) { + try { + int index = int.parse(key); + if (index >= 0 && index < current.length) { + current = current[index]; + } else { + return null; + } + } catch (e) { + return null; + } + } else { + return null; + } + } + } + if (current == null) { + return null; + } else { + return current; + } +} + +/// Recursively processes a template, replacing `{{placeholder}}` patterns +/// with values from the provided data map. +dynamic processTemplateRecursively( + dynamic template, + Map data, +) { + if (template is Map) { + for (final key in template.keys.toList()) { + final value = template[key]; + + if (value is String) { + if (value.contains('{{') && value.contains('}}')) { + String processedValue = value; + final regex = RegExp(r'\{\{([^}]+)\}\}'); + final matches = regex.allMatches(value); + + for (final match in matches) { + final placeholder = match.group(0)!; + final dataKey = match.group(1)!.trim(); + final keys = dataKey.split('.'); + + final dataValue = extractNestedData(data, keys); + + if (dataValue != null) { + processedValue = processedValue.replaceAll( + placeholder, + dataValue.toString(), + ); + } + } + + template[key] = processedValue; + } + } else if (value is Map || value is List) { + processTemplateRecursively(value, data); + } + } + } else if (template is List) { + for (int i = 0; i < template.length; i++) { + processTemplateRecursively(template[i], data); + } + } + return template; +} + +/// Applies data to a single item template by deep-copying the template +/// and processing all placeholders with the item's data. +Map applyDataToItem( + Map template, + Map item, +) { + final result = jsonDecode(jsonEncode(template)) as Map; + processTemplateRecursively(result, item); + return result; +} + +/// Checks if the data represents an empty list, including nested structures. +bool isEmptyList(dynamic data) { + if (data is List && data.isEmpty) { + return true; + } + + if (data is Map) { + for (final value in data.values) { + if (value is List && value.isEmpty) { + return true; + } + if (value is Map && isEmptyList(value)) { + return true; + } + } + } + + return false; +} + +/// Processes an itemTemplate against a list of data, producing a list of +/// rendered child widget JSON maps. +List> processItemTemplate({ + required Map itemTemplate, + required List listData, + String resultTarget = '', +}) { + final processedChildren = >[]; + + for (final singleRawItem in listData) { + if (singleRawItem is Map) { + final itemSpecificDataContext = resultTarget.isNotEmpty + ? {resultTarget: singleRawItem} + : singleRawItem; + + final processedChild = applyDataToItem( + itemTemplate, + itemSpecificDataContext, + ); + processedChildren.add(processedChild); + } else { + Log.w("Item in list is not a Map, skipping: $singleRawItem"); + } + } + + return processedChildren; +} + +/// Resolves remaining `{{providerId.path}}` placeholders in JSON using +/// data from the nearest [DynamicDataScope] in the widget tree. +/// +/// This is designed to run as a second pass after [resolveVariablesInJson], +/// picking up any `{{}}` patterns that weren't resolved from the global +/// registry (typically because they contain dots referencing provider data). +dynamic resolveDynamicDataInJson(dynamic json, BuildContext context) { + final scope = DynamicDataScope.maybeOf(context); + if (scope == null) return json; + + if (json is String) { + if (!json.contains('{{') || !json.contains('}}')) return json; + + final regex = RegExp(r'\{\{([^}]+)\}\}'); + String result = json; + final matches = regex.allMatches(json); + + for (final match in matches) { + final placeholder = match.group(0)!; + final expression = match.group(1)!.trim(); + + final resolved = scope.resolveExpression(expression); + if (resolved != null) { + result = result.replaceAll(placeholder, resolved.toString()); + } + } + return result; + } else if (json is Map) { + return json.map( + (key, value) => MapEntry(key, resolveDynamicDataInJson(value, context)), + ); + } else if (json is List) { + return json.map((item) => resolveDynamicDataInJson(item, context)).toList(); + } + return json; +} diff --git a/packages/stac_cli/lib/src/commands/init_command.dart b/packages/stac_cli/lib/src/commands/init_command.dart index 3b40a19e6..ef736ad8f 100644 --- a/packages/stac_cli/lib/src/commands/init_command.dart +++ b/packages/stac_cli/lib/src/commands/init_command.dart @@ -202,8 +202,7 @@ StacWidget helloWorld() { 'lib/default_stac_options.dart', ); - final dartConfig = - ''' + final dartConfig = ''' // This file is automatically generated by stac init. import 'package:stac/stac_core.dart'; diff --git a/packages/stac_cli/lib/src/exceptions/auth_exception.dart b/packages/stac_cli/lib/src/exceptions/auth_exception.dart index a106cdf12..f7a3a8a5d 100644 --- a/packages/stac_cli/lib/src/exceptions/auth_exception.dart +++ b/packages/stac_cli/lib/src/exceptions/auth_exception.dart @@ -8,11 +8,11 @@ class AuthException extends StacException { /// Exception thrown when user is not authenticated class NotAuthenticatedException extends AuthException { const NotAuthenticatedException() - : super('Not authenticated. Please run "stac login" first.'); + : super('Not authenticated. Please run "stac login" first.'); } /// Exception thrown when authentication fails class AuthenticationFailedException extends AuthException { const AuthenticationFailedException([String? reason]) - : super('Authentication failed${reason != null ? ': $reason' : ''}'); + : super('Authentication failed${reason != null ? ': $reason' : ''}'); } diff --git a/packages/stac_cli/lib/src/exceptions/build_exception.dart b/packages/stac_cli/lib/src/exceptions/build_exception.dart index a91958900..85f70be98 100644 --- a/packages/stac_cli/lib/src/exceptions/build_exception.dart +++ b/packages/stac_cli/lib/src/exceptions/build_exception.dart @@ -8,11 +8,11 @@ class BuildException extends StacException { /// Exception thrown when Dart to JSON conversion fails class ConversionException extends BuildException { const ConversionException(String message, {dynamic cause}) - : super('Dart to JSON conversion failed: $message', cause: cause); + : super('Dart to JSON conversion failed: $message', cause: cause); } /// Exception thrown when SDUI validation fails class ValidationException extends BuildException { const ValidationException(String message) - : super('SDUI validation failed: $message'); + : super('SDUI validation failed: $message'); } diff --git a/packages/stac_cli/lib/src/models/project/project.g.dart b/packages/stac_cli/lib/src/models/project/project.g.dart index 91bbad6d6..8a475b670 100644 --- a/packages/stac_cli/lib/src/models/project/project.g.dart +++ b/packages/stac_cli/lib/src/models/project/project.g.dart @@ -7,41 +7,41 @@ part of 'project.dart'; // ************************************************************************** Project _$ProjectFromJson(Map json) => Project( - id: json['id'] as String?, - name: json['name'] as String, - slug: json['slug'] as String?, - description: json['description'] as String?, - ownerId: json['ownerId'] as String, - createdAt: const FirestoreDateTime().fromJson(json['createdAt']), - updatedAt: const FirestoreDateTime().fromJson(json['updatedAt']), - defaultScreenId: json['defaultScreenId'] as String?, - isPublic: json['isPublic'] as bool, - tags: - (json['tags'] as List?)?.map((e) => e as String).toList() ?? - const [], - status: json['status'] as String?, - deletedAt: const FirestoreDateTimeNullable().fromJson(json['deletedAt']), - subscription: json['subscription'] == null - ? null - : Subscription.fromJson(json['subscription'] as Map), - uiLoads: json['uiLoads'] == null - ? null - : UiLoads.fromJson(json['uiLoads'] as Map), -); + id: json['id'] as String?, + name: json['name'] as String, + slug: json['slug'] as String?, + description: json['description'] as String?, + ownerId: json['ownerId'] as String, + createdAt: const FirestoreDateTime().fromJson(json['createdAt']), + updatedAt: const FirestoreDateTime().fromJson(json['updatedAt']), + defaultScreenId: json['defaultScreenId'] as String?, + isPublic: json['isPublic'] as bool, + tags: + (json['tags'] as List?)?.map((e) => e as String).toList() ?? + const [], + status: json['status'] as String?, + deletedAt: const FirestoreDateTimeNullable().fromJson(json['deletedAt']), + subscription: json['subscription'] == null + ? null + : Subscription.fromJson(json['subscription'] as Map), + uiLoads: json['uiLoads'] == null + ? null + : UiLoads.fromJson(json['uiLoads'] as Map), + ); Map _$ProjectToJson(Project instance) => { - 'id': instance.id, - 'name': instance.name, - 'slug': instance.slug, - 'description': instance.description, - 'ownerId': instance.ownerId, - 'createdAt': const FirestoreDateTime().toJson(instance.createdAt), - 'updatedAt': const FirestoreDateTime().toJson(instance.updatedAt), - 'defaultScreenId': instance.defaultScreenId, - 'isPublic': instance.isPublic, - 'tags': instance.tags, - 'status': instance.status, - 'deletedAt': const FirestoreDateTimeNullable().toJson(instance.deletedAt), - 'subscription': instance.subscription, - 'uiLoads': instance.uiLoads, -}; + 'id': instance.id, + 'name': instance.name, + 'slug': instance.slug, + 'description': instance.description, + 'ownerId': instance.ownerId, + 'createdAt': const FirestoreDateTime().toJson(instance.createdAt), + 'updatedAt': const FirestoreDateTime().toJson(instance.updatedAt), + 'defaultScreenId': instance.defaultScreenId, + 'isPublic': instance.isPublic, + 'tags': instance.tags, + 'status': instance.status, + 'deletedAt': const FirestoreDateTimeNullable().toJson(instance.deletedAt), + 'subscription': instance.subscription, + 'uiLoads': instance.uiLoads, + }; diff --git a/packages/stac_cli/lib/src/models/project/subscription.g.dart b/packages/stac_cli/lib/src/models/project/subscription.g.dart index 3ad4993ac..4256f381f 100644 --- a/packages/stac_cli/lib/src/models/project/subscription.g.dart +++ b/packages/stac_cli/lib/src/models/project/subscription.g.dart @@ -7,29 +7,30 @@ part of 'subscription.dart'; // ************************************************************************** Subscription _$SubscriptionFromJson(Map json) => Subscription( - subscriptionId: json['subscriptionId'] as String?, - productId: json['productId'] as String?, - customerId: json['customerId'] as String?, - status: $enumDecodeNullable(_$SubscriptionStatusEnumMap, json['status']), - environment: $enumDecodeNullable( - _$SubscriptionEnvironmentEnumMap, - json['environment'], - ), - currentPeriodStart: const FirestoreDateTimeNullable().fromJson( - json['currentPeriodStart'], - ), - currentPeriodEnd: const FirestoreDateTimeNullable().fromJson( - json['currentPeriodEnd'], - ), - lastRenewedAt: const FirestoreDateTimeNullable().fromJson( - json['lastRenewedAt'], - ), - updatedAt: const FirestoreDateTimeNullable().fromJson(json['updatedAt']), - cancelOnPeriodEnd: json['cancelOnPeriodEnd'] as bool?, - additionalUsageBillingEnabled: json['additionalUsageBillingEnabled'] as bool?, - spendLimitEnabled: json['spendLimitEnabled'] as bool?, - alertThreshold: (json['alertThreshold'] as num?)?.toDouble(), -); + subscriptionId: json['subscriptionId'] as String?, + productId: json['productId'] as String?, + customerId: json['customerId'] as String?, + status: $enumDecodeNullable(_$SubscriptionStatusEnumMap, json['status']), + environment: $enumDecodeNullable( + _$SubscriptionEnvironmentEnumMap, + json['environment'], + ), + currentPeriodStart: const FirestoreDateTimeNullable().fromJson( + json['currentPeriodStart'], + ), + currentPeriodEnd: const FirestoreDateTimeNullable().fromJson( + json['currentPeriodEnd'], + ), + lastRenewedAt: const FirestoreDateTimeNullable().fromJson( + json['lastRenewedAt'], + ), + updatedAt: const FirestoreDateTimeNullable().fromJson(json['updatedAt']), + cancelOnPeriodEnd: json['cancelOnPeriodEnd'] as bool?, + additionalUsageBillingEnabled: + json['additionalUsageBillingEnabled'] as bool?, + spendLimitEnabled: json['spendLimitEnabled'] as bool?, + alertThreshold: (json['alertThreshold'] as num?)?.toDouble(), + ); Map _$SubscriptionToJson(Subscription instance) => { diff --git a/packages/stac_cli/lib/src/models/project/ui_loads.dart b/packages/stac_cli/lib/src/models/project/ui_loads.dart index 115510824..77bd051c2 100644 --- a/packages/stac_cli/lib/src/models/project/ui_loads.dart +++ b/packages/stac_cli/lib/src/models/project/ui_loads.dart @@ -44,11 +44,11 @@ class UiLoads { } return UiLoads( - currentPeriodUiLoadCount: (data['currentPeriodUiLoadCount'] as num?) - ?.toInt(), + currentPeriodUiLoadCount: + (data['currentPeriodUiLoadCount'] as num?)?.toInt(), lastUiLoadCountFlushed: (data['lastUiLoadCountFlushed'] as num?)?.toInt(), - lastUiLoadsFlushedDelta: (data['lastUiLoadsFlushedDelta'] as num?) - ?.toInt(), + lastUiLoadsFlushedDelta: + (data['lastUiLoadsFlushedDelta'] as num?)?.toInt(), lastUiLoadsCountFlushedAt: DateTimeUtils.parseDateTime( data['lastUiLoadsCountFlushedAt'], ), diff --git a/packages/stac_cli/lib/src/models/project/ui_loads.g.dart b/packages/stac_cli/lib/src/models/project/ui_loads.g.dart index 8bb5bd02c..cb1db8d94 100644 --- a/packages/stac_cli/lib/src/models/project/ui_loads.g.dart +++ b/packages/stac_cli/lib/src/models/project/ui_loads.g.dart @@ -7,23 +7,25 @@ part of 'ui_loads.dart'; // ************************************************************************** UiLoads _$UiLoadsFromJson(Map json) => UiLoads( - currentPeriodUiLoadCount: (json['currentPeriodUiLoadCount'] as num?)?.toInt(), - lastUiLoadCountFlushed: (json['lastUiLoadCountFlushed'] as num?)?.toInt(), - lastUiLoadsFlushedDelta: (json['lastUiLoadsFlushedDelta'] as num?)?.toInt(), - lastUiLoadsCountFlushedAt: const FirestoreDateTimeNullable().fromJson( - json['lastUiLoadsCountFlushedAt'], - ), - lastUiLoadsUploadError: json['lastUiLoadsUploadError'] as String?, - lifetimeUiLoadCount: (json['lifetimeUiLoadCount'] as num?)?.toInt(), -); + currentPeriodUiLoadCount: + (json['currentPeriodUiLoadCount'] as num?)?.toInt(), + lastUiLoadCountFlushed: (json['lastUiLoadCountFlushed'] as num?)?.toInt(), + lastUiLoadsFlushedDelta: + (json['lastUiLoadsFlushedDelta'] as num?)?.toInt(), + lastUiLoadsCountFlushedAt: const FirestoreDateTimeNullable().fromJson( + json['lastUiLoadsCountFlushedAt'], + ), + lastUiLoadsUploadError: json['lastUiLoadsUploadError'] as String?, + lifetimeUiLoadCount: (json['lifetimeUiLoadCount'] as num?)?.toInt(), + ); Map _$UiLoadsToJson(UiLoads instance) => { - 'currentPeriodUiLoadCount': instance.currentPeriodUiLoadCount, - 'lastUiLoadCountFlushed': instance.lastUiLoadCountFlushed, - 'lastUiLoadsFlushedDelta': instance.lastUiLoadsFlushedDelta, - 'lastUiLoadsCountFlushedAt': const FirestoreDateTimeNullable().toJson( - instance.lastUiLoadsCountFlushedAt, - ), - 'lastUiLoadsUploadError': instance.lastUiLoadsUploadError, - 'lifetimeUiLoadCount': instance.lifetimeUiLoadCount, -}; + 'currentPeriodUiLoadCount': instance.currentPeriodUiLoadCount, + 'lastUiLoadCountFlushed': instance.lastUiLoadCountFlushed, + 'lastUiLoadsFlushedDelta': instance.lastUiLoadsFlushedDelta, + 'lastUiLoadsCountFlushedAt': const FirestoreDateTimeNullable().toJson( + instance.lastUiLoadsCountFlushedAt, + ), + 'lastUiLoadsUploadError': instance.lastUiLoadsUploadError, + 'lifetimeUiLoadCount': instance.lifetimeUiLoadCount, + }; diff --git a/packages/stac_cli/lib/src/services/auth_service.dart b/packages/stac_cli/lib/src/services/auth_service.dart index 5faf3ac6d..11ee09277 100644 --- a/packages/stac_cli/lib/src/services/auth_service.dart +++ b/packages/stac_cli/lib/src/services/auth_service.dart @@ -166,8 +166,7 @@ class AuthService { final authToken = AuthToken( accessToken: newIdToken, - refreshToken: - newRefreshToken ?? + refreshToken: newRefreshToken ?? refreshToken, // Keep old refresh token if new one not provided expiresAt: expiresAt, scopes: _scopes, @@ -279,10 +278,13 @@ class AuthService { await Process.run('xdg-open', [authUrl.toString()]); ConsoleLogger.info('Opening browser for authentication...'); } else if (Platform.isWindows) { - await Process.run('rundll32', [ - 'url.dll,FileProtocolHandler', - authUrl.toString(), - ], runInShell: false); + await Process.run( + 'rundll32', + [ + 'url.dll,FileProtocolHandler', + authUrl.toString(), + ], + runInShell: false); ConsoleLogger.info('Opening browser for authentication...'); } } catch (e) { diff --git a/packages/stac_cli/lib/src/services/build_service.dart b/packages/stac_cli/lib/src/services/build_service.dart index 8130b2f8d..35ecba394 100644 --- a/packages/stac_cli/lib/src/services/build_service.dart +++ b/packages/stac_cli/lib/src/services/build_service.dart @@ -194,19 +194,19 @@ class BuildService { final description = RegExp(r"description:\s*'([^']*)'").firstMatch(content)?.group(1) ?? - 'Stac'; + 'Stac'; final projectId = RegExp(r"projectId:\s*'([^']*)'").firstMatch(content)?.group(1) ?? - 'stac'; + 'stac'; final sourceDir = RegExp(r"sourceDir:\s*'([^']*)'").firstMatch(content)?.group(1) ?? - 'stac'; + 'stac'; final outputDir = RegExp(r"outputDir:\s*'([^']*)'").firstMatch(content)?.group(1) ?? - 'stac/.build'; + 'stac/.build'; return StacOptions( name: name, @@ -322,12 +322,10 @@ class BuildService { required StacDslArtifactType type, }) { final artifacts = []; - final annotationName = type == StacDslArtifactType.screen - ? 'StacScreen' - : 'StacThemeRef'; - final parameterName = type == StacDslArtifactType.screen - ? 'screenName' - : 'name'; + final annotationName = + type == StacDslArtifactType.screen ? 'StacScreen' : 'StacThemeRef'; + final parameterName = + type == StacDslArtifactType.screen ? 'screenName' : 'name'; final callablePrefixPattern = type == StacDslArtifactType.screen ? r'(?:StacWidget\s+)?' : r'(?:StacTheme\s+)?'; @@ -517,19 +515,18 @@ Future main(List args) async { ) async { try { // Execute Dart file in project context for proper dependency resolution - final result = - await Process.run( - 'dart', - ['run', path.basename(scriptFile.path)], - workingDirectory: projectDir, - runInShell: Platform - .isWindows, // Use shell on Windows for proper PATH resolution - ).timeout( - const Duration(seconds: 60), - onTimeout: () { - throw Exception('Script execution timed out after 60 seconds'); - }, - ); + final result = await Process.run( + 'dart', + ['run', path.basename(scriptFile.path)], + workingDirectory: projectDir, + runInShell: Platform + .isWindows, // Use shell on Windows for proper PATH resolution + ).timeout( + const Duration(seconds: 60), + onTimeout: () { + throw Exception('Script execution timed out after 60 seconds'); + }, + ); final stdout = result.stdout.toString(); final stderr = result.stderr.toString(); diff --git a/packages/stac_cli/lib/src/services/upgrade_service.dart b/packages/stac_cli/lib/src/services/upgrade_service.dart index 617aff8c5..69df0e85c 100644 --- a/packages/stac_cli/lib/src/services/upgrade_service.dart +++ b/packages/stac_cli/lib/src/services/upgrade_service.dart @@ -432,12 +432,15 @@ class UpgradeService { } Future _extractTarGz(String archivePath, String destDir) async { - final result = await Process.run('tar', [ - '-xzf', - archivePath, - '-C', - destDir, - ], runInShell: false); + final result = await Process.run( + 'tar', + [ + '-xzf', + archivePath, + '-C', + destDir, + ], + runInShell: false); if (result.exitCode != 0) { throw StacException('Failed to extract archive: ${result.stderr}'); } @@ -445,15 +448,18 @@ class UpgradeService { Future _extractZip(String archivePath, String destDir) async { // Use PowerShell to extract on Windows - final result = await Process.run('powershell', [ - '-Command', - 'Expand-Archive', - '-Path', - archivePath, - '-DestinationPath', - destDir, - '-Force', - ], runInShell: false); + final result = await Process.run( + 'powershell', + [ + '-Command', + 'Expand-Archive', + '-Path', + archivePath, + '-DestinationPath', + destDir, + '-Force', + ], + runInShell: false); if (result.exitCode != 0) { throw StacException('Failed to extract archive: ${result.stderr}'); } @@ -476,9 +482,8 @@ class UpgradeService { await File(filePath).openRead().forEach(sink.add); sink.close(); final hash = await sink.hash(); - final actual = hash.bytes - .map((b) => b.toRadixString(16).padLeft(2, '0')) - .join(); + final actual = + hash.bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); if (expected.toLowerCase() != actual.toLowerCase()) { throw StacException( diff --git a/packages/stac_core/lib/foundation/specifications/widget_type.dart b/packages/stac_core/lib/foundation/specifications/widget_type.dart index 4688463ce..1f7658d64 100644 --- a/packages/stac_core/lib/foundation/specifications/widget_type.dart +++ b/packages/stac_core/lib/foundation/specifications/widget_type.dart @@ -87,6 +87,9 @@ enum WidgetType { /// Divider widget divider, + /// Dynamic data provider widget + dynamicDataProvider, + /// Dynamic view widget dynamicView, @@ -252,6 +255,9 @@ enum WidgetType { /// Table cell widget tableCell, + /// Template builder widget + templateBuilder, + /// Text widget text, diff --git a/packages/stac_core/lib/widgets/dynamic_data_provider/stac_dynamic_data_provider.dart b/packages/stac_core/lib/widgets/dynamic_data_provider/stac_dynamic_data_provider.dart new file mode 100644 index 000000000..0367c53b8 --- /dev/null +++ b/packages/stac_core/lib/widgets/dynamic_data_provider/stac_dynamic_data_provider.dart @@ -0,0 +1,92 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:stac_core/actions/network_request/stac_network_request.dart'; +import 'package:stac_core/core/stac_widget.dart'; +import 'package:stac_core/foundation/specifications/widget_type.dart'; + +part 'stac_dynamic_data_provider.g.dart'; + +/// A Stac model that fetches data from a network request and exposes it +/// to its subtree via an InheritedWidget scope. +/// +/// Unlike [StacDynamicView], this widget separates data fetching from +/// template rendering. Child widgets can access the fetched data using +/// [TemplateBuilder] or `{{id.path}}` placeholder syntax. +/// +/// ```dart +/// StacDynamicDataProvider( +/// id: 'moviesData', +/// request: StacNetworkRequest(url: 'https://api.example.com/movies'), +/// targetPath: 'data', +/// loaderWidget: StacCircularProgressIndicator(), +/// child: StacColumn(children: [ +/// StacText(data: 'Total: {{moviesData.totalResults}}'), +/// StacTemplateBuilder( +/// providerId: 'moviesData', +/// dataPath: 'results', +/// itemTemplate: StacText(data: '{{title}}'), +/// child: StacListView(), +/// ), +/// ]), +/// ) +/// ``` +/// +/// ```json +/// { +/// "type": "dynamicDataProvider", +/// "id": "moviesData", +/// "request": { +/// "url": "https://api.example.com/movies", +/// "method": "GET" +/// }, +/// "targetPath": "data", +/// "loaderWidget": { +/// "type": "circularProgressIndicator" +/// }, +/// "child": { +/// "type": "column", +/// "children": [] +/// } +/// } +/// ``` +@JsonSerializable() +class StacDynamicDataProvider extends StacWidget { + /// Creates a [StacDynamicDataProvider] with the given properties. + const StacDynamicDataProvider({ + required this.id, + required this.request, + required this.child, + this.targetPath, + this.loaderWidget, + this.errorWidget, + }); + + /// Unique identifier used by descendant widgets to reference this + /// provider's data (e.g. via `{{id.path}}` or TemplateBuilder's providerId). + final String id; + + /// Configuration for the network request to fetch data. + final StacNetworkRequest request; + + /// Path within the fetched JSON data to extract before exposing to children. + final String? targetPath; + + /// The subtree that can access the fetched data. + final StacWidget child; + + /// Optional widget to display while the network request is in progress. + final StacWidget? loaderWidget; + + /// Optional widget to display if the network request fails. + final StacWidget? errorWidget; + + @override + String get type => WidgetType.dynamicDataProvider.name; + + /// Creates a [StacDynamicDataProvider] from a JSON map. + factory StacDynamicDataProvider.fromJson(Map json) => + _$StacDynamicDataProviderFromJson(json); + + /// Converts this [StacDynamicDataProvider] instance to a JSON map. + @override + Map toJson() => _$StacDynamicDataProviderToJson(this); +} diff --git a/packages/stac_core/lib/widgets/dynamic_data_provider/stac_dynamic_data_provider.g.dart b/packages/stac_core/lib/widgets/dynamic_data_provider/stac_dynamic_data_provider.g.dart new file mode 100644 index 000000000..577e491cc --- /dev/null +++ b/packages/stac_core/lib/widgets/dynamic_data_provider/stac_dynamic_data_provider.g.dart @@ -0,0 +1,34 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'stac_dynamic_data_provider.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +StacDynamicDataProvider _$StacDynamicDataProviderFromJson( + Map json, +) => StacDynamicDataProvider( + id: json['id'] as String, + request: StacNetworkRequest.fromJson(json['request'] as Map), + child: StacWidget.fromJson(json['child'] as Map), + targetPath: json['targetPath'] as String?, + loaderWidget: json['loaderWidget'] == null + ? null + : StacWidget.fromJson(json['loaderWidget'] as Map), + errorWidget: json['errorWidget'] == null + ? null + : StacWidget.fromJson(json['errorWidget'] as Map), +); + +Map _$StacDynamicDataProviderToJson( + StacDynamicDataProvider instance, +) => { + 'id': instance.id, + 'request': instance.request.toJson(), + 'targetPath': instance.targetPath, + 'child': instance.child.toJson(), + 'loaderWidget': instance.loaderWidget?.toJson(), + 'errorWidget': instance.errorWidget?.toJson(), + 'type': instance.type, +}; diff --git a/packages/stac_core/lib/widgets/template_builder/stac_template_builder.dart b/packages/stac_core/lib/widgets/template_builder/stac_template_builder.dart new file mode 100644 index 000000000..9ba1cfbd7 --- /dev/null +++ b/packages/stac_core/lib/widgets/template_builder/stac_template_builder.dart @@ -0,0 +1,93 @@ +import 'package:json_annotation/json_annotation.dart'; +import 'package:stac_core/core/stac_widget.dart'; +import 'package:stac_core/foundation/specifications/widget_type.dart'; + +part 'stac_template_builder.g.dart'; + +/// A Stac model that iterates over a list of data, applies an [itemTemplate] +/// to each item, and injects the resulting widgets as children of [child]. +/// +/// Data can be provided directly via [data], or looked up from a +/// [DynamicDataProvider] ancestor using [providerId] and [dataPath]. +/// +/// ```dart +/// StacTemplateBuilder( +/// providerId: 'moviesData', +/// dataPath: 'results', +/// itemTemplate: StacText(data: '{{title}}'), +/// child: StacListView(scrollDirection: Axis.horizontal), +/// ) +/// ``` +/// +/// With direct data: +/// ```dart +/// StacTemplateBuilder( +/// data: [ +/// {'name': 'Alice', 'role': 'Admin'}, +/// {'name': 'Bob', 'role': 'User'}, +/// ], +/// itemTemplate: StacListTile( +/// title: StacText(data: '{{name}} - {{role}}'), +/// ), +/// child: StacListView(), +/// ) +/// ``` +/// +/// ```json +/// { +/// "type": "templateBuilder", +/// "providerId": "moviesData", +/// "dataPath": "results", +/// "itemTemplate": { +/// "type": "text", +/// "data": "{{title}}" +/// }, +/// "child": { +/// "type": "listView", +/// "scrollDirection": "horizontal" +/// } +/// } +/// ``` +@JsonSerializable() +class StacTemplateBuilder extends StacWidget { + /// Creates a [StacTemplateBuilder] with the given properties. + const StacTemplateBuilder({ + this.data, + this.providerId, + this.dataPath, + required this.itemTemplate, + required this.child, + this.emptyWidget, + }); + + /// Direct list of JSON data objects to iterate over. + /// Takes priority over [providerId] if both are set. + final List? data; + + /// ID of a [DynamicDataProvider] ancestor to read data from. + final String? providerId; + + /// Dot-notation path within the provider's data to extract the list. + /// Only used when [providerId] is set. + final String? dataPath; + + /// Template widget applied to each item in the data list. + final StacWidget itemTemplate; + + /// Layout widget that receives the generated children. + final StacWidget child; + + /// Optional widget to display if the resolved data list is empty. + final StacWidget? emptyWidget; + + @override + String get type => WidgetType.templateBuilder.name; + + /// Creates a [StacTemplateBuilder] from a JSON map. + factory StacTemplateBuilder.fromJson(Map json) => + _$StacTemplateBuilderFromJson(json); + + /// Converts this [StacTemplateBuilder] instance to a JSON map. + @override + Map toJson() => _$StacTemplateBuilderToJson(this); +} diff --git a/packages/stac_core/lib/widgets/template_builder/stac_template_builder.g.dart b/packages/stac_core/lib/widgets/template_builder/stac_template_builder.g.dart new file mode 100644 index 000000000..4b675f6e7 --- /dev/null +++ b/packages/stac_core/lib/widgets/template_builder/stac_template_builder.g.dart @@ -0,0 +1,33 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'stac_template_builder.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +StacTemplateBuilder _$StacTemplateBuilderFromJson(Map json) => + StacTemplateBuilder( + data: json['data'] as List?, + providerId: json['providerId'] as String?, + dataPath: json['dataPath'] as String?, + itemTemplate: StacWidget.fromJson( + json['itemTemplate'] as Map, + ), + child: StacWidget.fromJson(json['child'] as Map), + emptyWidget: json['emptyWidget'] == null + ? null + : StacWidget.fromJson(json['emptyWidget'] as Map), + ); + +Map _$StacTemplateBuilderToJson( + StacTemplateBuilder instance, +) => { + 'data': instance.data, + 'providerId': instance.providerId, + 'dataPath': instance.dataPath, + 'itemTemplate': instance.itemTemplate.toJson(), + 'child': instance.child.toJson(), + 'emptyWidget': instance.emptyWidget?.toJson(), + 'type': instance.type, +}; diff --git a/packages/stac_core/lib/widgets/widgets.dart b/packages/stac_core/lib/widgets/widgets.dart index a44733e82..8a6563766 100644 --- a/packages/stac_core/lib/widgets/widgets.dart +++ b/packages/stac_core/lib/widgets/widgets.dart @@ -27,6 +27,7 @@ export 'default_tab_controller/stac_default_tab_controller.dart'; export 'divider/stac_divider.dart'; export 'drawer/stac_drawer.dart'; export 'dropdown_menu/stac_dropdown_menu.dart'; +export 'dynamic_data_provider/stac_dynamic_data_provider.dart'; export 'dynamic_view/stac_dynamic_view.dart'; export 'elevated_button/stac_elevated_button.dart'; export 'expanded/stac_expanded.dart'; @@ -84,6 +85,7 @@ export 'tab_bar_view/stac_tab_bar_view.dart'; export 'table/stac_table.dart'; export 'table_cell/stac_table_cell.dart'; export 'table_row/stac_table_row.dart'; +export 'template_builder/stac_template_builder.dart'; export 'text/stac_text.dart'; export 'text_button/stac_text_button.dart'; export 'text_field/stac_text_field.dart'; From f8257784c57e9a9be485aa0ed693ae9d2a9bc2bd Mon Sep 17 00:00:00 2001 From: Mithul Nayagam <113115383+Potatomonsta@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:19:17 +0530 Subject: [PATCH 2/4] refactor: improve code formatting and structure across multiple files - Adjusted formatting for better readability in `stac_template_builder_parser.dart`, `init_command.dart`, and various exception classes. - Enhanced JSON serialization/deserialization structure in `project.g.dart`, `subscription.g.dart`, and `ui_loads.g.dart`. - Refactored `auth_service.dart` and `build_service.dart` for consistent line breaks and improved clarity. - Updated `analysis_options.yaml` to switch linting from `flutter_lints` to `lints/recommended`. --- .../stac_template_builder_parser.dart | 3 +- .../lib/src/commands/init_command.dart | 3 +- .../lib/src/exceptions/auth_exception.dart | 4 +- .../lib/src/exceptions/build_exception.dart | 4 +- .../lib/src/models/project/project.g.dart | 72 +++++++++---------- .../src/models/project/subscription.g.dart | 47 ++++++------ .../lib/src/models/project/ui_loads.dart | 8 +-- .../lib/src/models/project/ui_loads.g.dart | 38 +++++----- .../lib/src/services/auth_service.dart | 14 ++-- .../lib/src/services/build_service.dart | 43 +++++------ .../lib/src/services/upgrade_service.dart | 41 +++++------ 11 files changed, 136 insertions(+), 141 deletions(-) diff --git a/packages/stac/lib/src/parsers/widgets/stac_template_builder/stac_template_builder_parser.dart b/packages/stac/lib/src/parsers/widgets/stac_template_builder/stac_template_builder_parser.dart index a47bcfb12..4fc9595c3 100644 --- a/packages/stac/lib/src/parsers/widgets/stac_template_builder/stac_template_builder_parser.dart +++ b/packages/stac/lib/src/parsers/widgets/stac_template_builder/stac_template_builder_parser.dart @@ -67,7 +67,8 @@ class StacTemplateBuilderParser extends StacParser { jsonDecode(jsonEncode(model.child.toJson())) as Map; final childType = childJson['type'] as String?; - if (childType == null || !_layoutWidgetTypesWithChildren.contains(childType)) { + if (childType == null || + !_layoutWidgetTypesWithChildren.contains(childType)) { throw FormatException( 'TemplateBuilder child must be a layout widget that supports "children". ' 'Got type: ${childType ?? "null"}. ' diff --git a/packages/stac_cli/lib/src/commands/init_command.dart b/packages/stac_cli/lib/src/commands/init_command.dart index ef736ad8f..3b40a19e6 100644 --- a/packages/stac_cli/lib/src/commands/init_command.dart +++ b/packages/stac_cli/lib/src/commands/init_command.dart @@ -202,7 +202,8 @@ StacWidget helloWorld() { 'lib/default_stac_options.dart', ); - final dartConfig = ''' + final dartConfig = + ''' // This file is automatically generated by stac init. import 'package:stac/stac_core.dart'; diff --git a/packages/stac_cli/lib/src/exceptions/auth_exception.dart b/packages/stac_cli/lib/src/exceptions/auth_exception.dart index f7a3a8a5d..a106cdf12 100644 --- a/packages/stac_cli/lib/src/exceptions/auth_exception.dart +++ b/packages/stac_cli/lib/src/exceptions/auth_exception.dart @@ -8,11 +8,11 @@ class AuthException extends StacException { /// Exception thrown when user is not authenticated class NotAuthenticatedException extends AuthException { const NotAuthenticatedException() - : super('Not authenticated. Please run "stac login" first.'); + : super('Not authenticated. Please run "stac login" first.'); } /// Exception thrown when authentication fails class AuthenticationFailedException extends AuthException { const AuthenticationFailedException([String? reason]) - : super('Authentication failed${reason != null ? ': $reason' : ''}'); + : super('Authentication failed${reason != null ? ': $reason' : ''}'); } diff --git a/packages/stac_cli/lib/src/exceptions/build_exception.dart b/packages/stac_cli/lib/src/exceptions/build_exception.dart index 85f70be98..a91958900 100644 --- a/packages/stac_cli/lib/src/exceptions/build_exception.dart +++ b/packages/stac_cli/lib/src/exceptions/build_exception.dart @@ -8,11 +8,11 @@ class BuildException extends StacException { /// Exception thrown when Dart to JSON conversion fails class ConversionException extends BuildException { const ConversionException(String message, {dynamic cause}) - : super('Dart to JSON conversion failed: $message', cause: cause); + : super('Dart to JSON conversion failed: $message', cause: cause); } /// Exception thrown when SDUI validation fails class ValidationException extends BuildException { const ValidationException(String message) - : super('SDUI validation failed: $message'); + : super('SDUI validation failed: $message'); } diff --git a/packages/stac_cli/lib/src/models/project/project.g.dart b/packages/stac_cli/lib/src/models/project/project.g.dart index 8a475b670..91bbad6d6 100644 --- a/packages/stac_cli/lib/src/models/project/project.g.dart +++ b/packages/stac_cli/lib/src/models/project/project.g.dart @@ -7,41 +7,41 @@ part of 'project.dart'; // ************************************************************************** Project _$ProjectFromJson(Map json) => Project( - id: json['id'] as String?, - name: json['name'] as String, - slug: json['slug'] as String?, - description: json['description'] as String?, - ownerId: json['ownerId'] as String, - createdAt: const FirestoreDateTime().fromJson(json['createdAt']), - updatedAt: const FirestoreDateTime().fromJson(json['updatedAt']), - defaultScreenId: json['defaultScreenId'] as String?, - isPublic: json['isPublic'] as bool, - tags: - (json['tags'] as List?)?.map((e) => e as String).toList() ?? - const [], - status: json['status'] as String?, - deletedAt: const FirestoreDateTimeNullable().fromJson(json['deletedAt']), - subscription: json['subscription'] == null - ? null - : Subscription.fromJson(json['subscription'] as Map), - uiLoads: json['uiLoads'] == null - ? null - : UiLoads.fromJson(json['uiLoads'] as Map), - ); + id: json['id'] as String?, + name: json['name'] as String, + slug: json['slug'] as String?, + description: json['description'] as String?, + ownerId: json['ownerId'] as String, + createdAt: const FirestoreDateTime().fromJson(json['createdAt']), + updatedAt: const FirestoreDateTime().fromJson(json['updatedAt']), + defaultScreenId: json['defaultScreenId'] as String?, + isPublic: json['isPublic'] as bool, + tags: + (json['tags'] as List?)?.map((e) => e as String).toList() ?? + const [], + status: json['status'] as String?, + deletedAt: const FirestoreDateTimeNullable().fromJson(json['deletedAt']), + subscription: json['subscription'] == null + ? null + : Subscription.fromJson(json['subscription'] as Map), + uiLoads: json['uiLoads'] == null + ? null + : UiLoads.fromJson(json['uiLoads'] as Map), +); Map _$ProjectToJson(Project instance) => { - 'id': instance.id, - 'name': instance.name, - 'slug': instance.slug, - 'description': instance.description, - 'ownerId': instance.ownerId, - 'createdAt': const FirestoreDateTime().toJson(instance.createdAt), - 'updatedAt': const FirestoreDateTime().toJson(instance.updatedAt), - 'defaultScreenId': instance.defaultScreenId, - 'isPublic': instance.isPublic, - 'tags': instance.tags, - 'status': instance.status, - 'deletedAt': const FirestoreDateTimeNullable().toJson(instance.deletedAt), - 'subscription': instance.subscription, - 'uiLoads': instance.uiLoads, - }; + 'id': instance.id, + 'name': instance.name, + 'slug': instance.slug, + 'description': instance.description, + 'ownerId': instance.ownerId, + 'createdAt': const FirestoreDateTime().toJson(instance.createdAt), + 'updatedAt': const FirestoreDateTime().toJson(instance.updatedAt), + 'defaultScreenId': instance.defaultScreenId, + 'isPublic': instance.isPublic, + 'tags': instance.tags, + 'status': instance.status, + 'deletedAt': const FirestoreDateTimeNullable().toJson(instance.deletedAt), + 'subscription': instance.subscription, + 'uiLoads': instance.uiLoads, +}; diff --git a/packages/stac_cli/lib/src/models/project/subscription.g.dart b/packages/stac_cli/lib/src/models/project/subscription.g.dart index 4256f381f..3ad4993ac 100644 --- a/packages/stac_cli/lib/src/models/project/subscription.g.dart +++ b/packages/stac_cli/lib/src/models/project/subscription.g.dart @@ -7,30 +7,29 @@ part of 'subscription.dart'; // ************************************************************************** Subscription _$SubscriptionFromJson(Map json) => Subscription( - subscriptionId: json['subscriptionId'] as String?, - productId: json['productId'] as String?, - customerId: json['customerId'] as String?, - status: $enumDecodeNullable(_$SubscriptionStatusEnumMap, json['status']), - environment: $enumDecodeNullable( - _$SubscriptionEnvironmentEnumMap, - json['environment'], - ), - currentPeriodStart: const FirestoreDateTimeNullable().fromJson( - json['currentPeriodStart'], - ), - currentPeriodEnd: const FirestoreDateTimeNullable().fromJson( - json['currentPeriodEnd'], - ), - lastRenewedAt: const FirestoreDateTimeNullable().fromJson( - json['lastRenewedAt'], - ), - updatedAt: const FirestoreDateTimeNullable().fromJson(json['updatedAt']), - cancelOnPeriodEnd: json['cancelOnPeriodEnd'] as bool?, - additionalUsageBillingEnabled: - json['additionalUsageBillingEnabled'] as bool?, - spendLimitEnabled: json['spendLimitEnabled'] as bool?, - alertThreshold: (json['alertThreshold'] as num?)?.toDouble(), - ); + subscriptionId: json['subscriptionId'] as String?, + productId: json['productId'] as String?, + customerId: json['customerId'] as String?, + status: $enumDecodeNullable(_$SubscriptionStatusEnumMap, json['status']), + environment: $enumDecodeNullable( + _$SubscriptionEnvironmentEnumMap, + json['environment'], + ), + currentPeriodStart: const FirestoreDateTimeNullable().fromJson( + json['currentPeriodStart'], + ), + currentPeriodEnd: const FirestoreDateTimeNullable().fromJson( + json['currentPeriodEnd'], + ), + lastRenewedAt: const FirestoreDateTimeNullable().fromJson( + json['lastRenewedAt'], + ), + updatedAt: const FirestoreDateTimeNullable().fromJson(json['updatedAt']), + cancelOnPeriodEnd: json['cancelOnPeriodEnd'] as bool?, + additionalUsageBillingEnabled: json['additionalUsageBillingEnabled'] as bool?, + spendLimitEnabled: json['spendLimitEnabled'] as bool?, + alertThreshold: (json['alertThreshold'] as num?)?.toDouble(), +); Map _$SubscriptionToJson(Subscription instance) => { diff --git a/packages/stac_cli/lib/src/models/project/ui_loads.dart b/packages/stac_cli/lib/src/models/project/ui_loads.dart index 77bd051c2..115510824 100644 --- a/packages/stac_cli/lib/src/models/project/ui_loads.dart +++ b/packages/stac_cli/lib/src/models/project/ui_loads.dart @@ -44,11 +44,11 @@ class UiLoads { } return UiLoads( - currentPeriodUiLoadCount: - (data['currentPeriodUiLoadCount'] as num?)?.toInt(), + currentPeriodUiLoadCount: (data['currentPeriodUiLoadCount'] as num?) + ?.toInt(), lastUiLoadCountFlushed: (data['lastUiLoadCountFlushed'] as num?)?.toInt(), - lastUiLoadsFlushedDelta: - (data['lastUiLoadsFlushedDelta'] as num?)?.toInt(), + lastUiLoadsFlushedDelta: (data['lastUiLoadsFlushedDelta'] as num?) + ?.toInt(), lastUiLoadsCountFlushedAt: DateTimeUtils.parseDateTime( data['lastUiLoadsCountFlushedAt'], ), diff --git a/packages/stac_cli/lib/src/models/project/ui_loads.g.dart b/packages/stac_cli/lib/src/models/project/ui_loads.g.dart index cb1db8d94..8bb5bd02c 100644 --- a/packages/stac_cli/lib/src/models/project/ui_loads.g.dart +++ b/packages/stac_cli/lib/src/models/project/ui_loads.g.dart @@ -7,25 +7,23 @@ part of 'ui_loads.dart'; // ************************************************************************** UiLoads _$UiLoadsFromJson(Map json) => UiLoads( - currentPeriodUiLoadCount: - (json['currentPeriodUiLoadCount'] as num?)?.toInt(), - lastUiLoadCountFlushed: (json['lastUiLoadCountFlushed'] as num?)?.toInt(), - lastUiLoadsFlushedDelta: - (json['lastUiLoadsFlushedDelta'] as num?)?.toInt(), - lastUiLoadsCountFlushedAt: const FirestoreDateTimeNullable().fromJson( - json['lastUiLoadsCountFlushedAt'], - ), - lastUiLoadsUploadError: json['lastUiLoadsUploadError'] as String?, - lifetimeUiLoadCount: (json['lifetimeUiLoadCount'] as num?)?.toInt(), - ); + currentPeriodUiLoadCount: (json['currentPeriodUiLoadCount'] as num?)?.toInt(), + lastUiLoadCountFlushed: (json['lastUiLoadCountFlushed'] as num?)?.toInt(), + lastUiLoadsFlushedDelta: (json['lastUiLoadsFlushedDelta'] as num?)?.toInt(), + lastUiLoadsCountFlushedAt: const FirestoreDateTimeNullable().fromJson( + json['lastUiLoadsCountFlushedAt'], + ), + lastUiLoadsUploadError: json['lastUiLoadsUploadError'] as String?, + lifetimeUiLoadCount: (json['lifetimeUiLoadCount'] as num?)?.toInt(), +); Map _$UiLoadsToJson(UiLoads instance) => { - 'currentPeriodUiLoadCount': instance.currentPeriodUiLoadCount, - 'lastUiLoadCountFlushed': instance.lastUiLoadCountFlushed, - 'lastUiLoadsFlushedDelta': instance.lastUiLoadsFlushedDelta, - 'lastUiLoadsCountFlushedAt': const FirestoreDateTimeNullable().toJson( - instance.lastUiLoadsCountFlushedAt, - ), - 'lastUiLoadsUploadError': instance.lastUiLoadsUploadError, - 'lifetimeUiLoadCount': instance.lifetimeUiLoadCount, - }; + 'currentPeriodUiLoadCount': instance.currentPeriodUiLoadCount, + 'lastUiLoadCountFlushed': instance.lastUiLoadCountFlushed, + 'lastUiLoadsFlushedDelta': instance.lastUiLoadsFlushedDelta, + 'lastUiLoadsCountFlushedAt': const FirestoreDateTimeNullable().toJson( + instance.lastUiLoadsCountFlushedAt, + ), + 'lastUiLoadsUploadError': instance.lastUiLoadsUploadError, + 'lifetimeUiLoadCount': instance.lifetimeUiLoadCount, +}; diff --git a/packages/stac_cli/lib/src/services/auth_service.dart b/packages/stac_cli/lib/src/services/auth_service.dart index 11ee09277..5faf3ac6d 100644 --- a/packages/stac_cli/lib/src/services/auth_service.dart +++ b/packages/stac_cli/lib/src/services/auth_service.dart @@ -166,7 +166,8 @@ class AuthService { final authToken = AuthToken( accessToken: newIdToken, - refreshToken: newRefreshToken ?? + refreshToken: + newRefreshToken ?? refreshToken, // Keep old refresh token if new one not provided expiresAt: expiresAt, scopes: _scopes, @@ -278,13 +279,10 @@ class AuthService { await Process.run('xdg-open', [authUrl.toString()]); ConsoleLogger.info('Opening browser for authentication...'); } else if (Platform.isWindows) { - await Process.run( - 'rundll32', - [ - 'url.dll,FileProtocolHandler', - authUrl.toString(), - ], - runInShell: false); + await Process.run('rundll32', [ + 'url.dll,FileProtocolHandler', + authUrl.toString(), + ], runInShell: false); ConsoleLogger.info('Opening browser for authentication...'); } } catch (e) { diff --git a/packages/stac_cli/lib/src/services/build_service.dart b/packages/stac_cli/lib/src/services/build_service.dart index 35ecba394..8130b2f8d 100644 --- a/packages/stac_cli/lib/src/services/build_service.dart +++ b/packages/stac_cli/lib/src/services/build_service.dart @@ -194,19 +194,19 @@ class BuildService { final description = RegExp(r"description:\s*'([^']*)'").firstMatch(content)?.group(1) ?? - 'Stac'; + 'Stac'; final projectId = RegExp(r"projectId:\s*'([^']*)'").firstMatch(content)?.group(1) ?? - 'stac'; + 'stac'; final sourceDir = RegExp(r"sourceDir:\s*'([^']*)'").firstMatch(content)?.group(1) ?? - 'stac'; + 'stac'; final outputDir = RegExp(r"outputDir:\s*'([^']*)'").firstMatch(content)?.group(1) ?? - 'stac/.build'; + 'stac/.build'; return StacOptions( name: name, @@ -322,10 +322,12 @@ class BuildService { required StacDslArtifactType type, }) { final artifacts = []; - final annotationName = - type == StacDslArtifactType.screen ? 'StacScreen' : 'StacThemeRef'; - final parameterName = - type == StacDslArtifactType.screen ? 'screenName' : 'name'; + final annotationName = type == StacDslArtifactType.screen + ? 'StacScreen' + : 'StacThemeRef'; + final parameterName = type == StacDslArtifactType.screen + ? 'screenName' + : 'name'; final callablePrefixPattern = type == StacDslArtifactType.screen ? r'(?:StacWidget\s+)?' : r'(?:StacTheme\s+)?'; @@ -515,18 +517,19 @@ Future main(List args) async { ) async { try { // Execute Dart file in project context for proper dependency resolution - final result = await Process.run( - 'dart', - ['run', path.basename(scriptFile.path)], - workingDirectory: projectDir, - runInShell: Platform - .isWindows, // Use shell on Windows for proper PATH resolution - ).timeout( - const Duration(seconds: 60), - onTimeout: () { - throw Exception('Script execution timed out after 60 seconds'); - }, - ); + final result = + await Process.run( + 'dart', + ['run', path.basename(scriptFile.path)], + workingDirectory: projectDir, + runInShell: Platform + .isWindows, // Use shell on Windows for proper PATH resolution + ).timeout( + const Duration(seconds: 60), + onTimeout: () { + throw Exception('Script execution timed out after 60 seconds'); + }, + ); final stdout = result.stdout.toString(); final stderr = result.stderr.toString(); diff --git a/packages/stac_cli/lib/src/services/upgrade_service.dart b/packages/stac_cli/lib/src/services/upgrade_service.dart index 69df0e85c..617aff8c5 100644 --- a/packages/stac_cli/lib/src/services/upgrade_service.dart +++ b/packages/stac_cli/lib/src/services/upgrade_service.dart @@ -432,15 +432,12 @@ class UpgradeService { } Future _extractTarGz(String archivePath, String destDir) async { - final result = await Process.run( - 'tar', - [ - '-xzf', - archivePath, - '-C', - destDir, - ], - runInShell: false); + final result = await Process.run('tar', [ + '-xzf', + archivePath, + '-C', + destDir, + ], runInShell: false); if (result.exitCode != 0) { throw StacException('Failed to extract archive: ${result.stderr}'); } @@ -448,18 +445,15 @@ class UpgradeService { Future _extractZip(String archivePath, String destDir) async { // Use PowerShell to extract on Windows - final result = await Process.run( - 'powershell', - [ - '-Command', - 'Expand-Archive', - '-Path', - archivePath, - '-DestinationPath', - destDir, - '-Force', - ], - runInShell: false); + final result = await Process.run('powershell', [ + '-Command', + 'Expand-Archive', + '-Path', + archivePath, + '-DestinationPath', + destDir, + '-Force', + ], runInShell: false); if (result.exitCode != 0) { throw StacException('Failed to extract archive: ${result.stderr}'); } @@ -482,8 +476,9 @@ class UpgradeService { await File(filePath).openRead().forEach(sink.add); sink.close(); final hash = await sink.hash(); - final actual = - hash.bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(); + final actual = hash.bytes + .map((b) => b.toRadixString(16).padLeft(2, '0')) + .join(); if (expected.toLowerCase() != actual.toLowerCase()) { throw StacException( From e9281f335790c5e8737ca15892aa15d7d8a4ed83 Mon Sep 17 00:00:00 2001 From: Mithul Nayagam <113115383+Potatomonsta@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:02:05 +0530 Subject: [PATCH 3/4] fix: enhance template processing for single placeholders - Updated `processTemplateRecursively` and `resolveDynamicDataInJson` to handle cases where only a single placeholder is present. - Improved nested data extraction and resolution logic for better accuracy in template processing. --- .../stac/lib/src/utils/template_utils.dart | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/stac/lib/src/utils/template_utils.dart b/packages/stac/lib/src/utils/template_utils.dart index 29767e867..872085593 100644 --- a/packages/stac/lib/src/utils/template_utils.dart +++ b/packages/stac/lib/src/utils/template_utils.dart @@ -73,7 +73,17 @@ dynamic processTemplateRecursively( if (value.contains('{{') && value.contains('}}')) { String processedValue = value; final regex = RegExp(r'\{\{([^}]+)\}\}'); - final matches = regex.allMatches(value); + final matches = regex.allMatches(value).toList(); + + if (matches.length == 1 && value.trim() == matches.first.group(0)) { + final dataKey = matches.first.group(1)!.trim(); + final keys = dataKey.split('.'); + final dataValue = extractNestedData(data, keys); + if (dataValue != null) { + template[key] = dataValue; + continue; + } + } for (final match in matches) { final placeholder = match.group(0)!; @@ -178,7 +188,15 @@ dynamic resolveDynamicDataInJson(dynamic json, BuildContext context) { final regex = RegExp(r'\{\{([^}]+)\}\}'); String result = json; - final matches = regex.allMatches(json); + final matches = regex.allMatches(json).toList(); + + if (matches.length == 1 && json.trim() == matches.first.group(0)) { + final expression = matches.first.group(1)!.trim(); + final resolved = scope.resolveExpression(expression); + if (resolved != null) { + return resolved; + } + } for (final match in matches) { final placeholder = match.group(0)!; From a2f4faaa86a311875d7f5bf46022e6a400d2c65f Mon Sep 17 00:00:00 2001 From: Mithul Nayagam <113115383+Potatomonsta@users.noreply.github.com> Date: Mon, 2 Mar 2026 17:05:36 +0530 Subject: [PATCH 4/4] chore: update package versions in pubspec.lock files to 1.4.0 - Bumped versions of `stac`, `stac_core`, `stac_framework`, and `stac_cli` to 1.4.0 across multiple example applications and the CLI package. --- examples/counter_example/pubspec.lock | 4 ++-- examples/movie_app/pubspec.lock | 4 ++-- examples/stac_gallery/pubspec.lock | 4 ++-- packages/stac_cli/pubspec.lock | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/counter_example/pubspec.lock b/examples/counter_example/pubspec.lock index 848df3a08..160130578 100644 --- a/examples/counter_example/pubspec.lock +++ b/examples/counter_example/pubspec.lock @@ -787,14 +787,14 @@ packages: path: "../../packages/stac" relative: true source: path - version: "1.3.1" + version: "1.4.0" stac_core: dependency: "direct overridden" description: path: "../../packages/stac_core" relative: true source: path - version: "1.3.0" + version: "1.4.0" stac_framework: dependency: "direct overridden" description: diff --git a/examples/movie_app/pubspec.lock b/examples/movie_app/pubspec.lock index 36aa62ce8..1c1ef626f 100644 --- a/examples/movie_app/pubspec.lock +++ b/examples/movie_app/pubspec.lock @@ -715,14 +715,14 @@ packages: path: "../../packages/stac" relative: true source: path - version: "1.3.1" + version: "1.4.0" stac_core: dependency: "direct overridden" description: path: "../../packages/stac_core" relative: true source: path - version: "1.3.0" + version: "1.4.0" stac_framework: dependency: "direct overridden" description: diff --git a/examples/stac_gallery/pubspec.lock b/examples/stac_gallery/pubspec.lock index 9ccf5b278..4ea0528ab 100644 --- a/examples/stac_gallery/pubspec.lock +++ b/examples/stac_gallery/pubspec.lock @@ -779,14 +779,14 @@ packages: path: "../../packages/stac" relative: true source: path - version: "1.3.1" + version: "1.4.0" stac_core: dependency: "direct overridden" description: path: "../../packages/stac_core" relative: true source: path - version: "1.3.0" + version: "1.4.0" stac_framework: dependency: "direct overridden" description: diff --git a/packages/stac_cli/pubspec.lock b/packages/stac_cli/pubspec.lock index fc7505afc..ea1f4bd3b 100644 --- a/packages/stac_cli/pubspec.lock +++ b/packages/stac_cli/pubspec.lock @@ -495,7 +495,7 @@ packages: path: "../stac_core" relative: true source: path - version: "1.3.0" + version: "1.4.0" stac_logger: dependency: "direct overridden" description: