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 ff6d69155..fbd926514 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 b05ff8249..946b05e67 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'; @@ -130,6 +131,7 @@ class StacService { const StacAspectRatioParser(), const StacFittedBoxParser(), const StacLimitedBoxParser(), + const StacDynamicDataProviderParser(), const StacDynamicViewParser(), const StacDropdownMenuParser(), const StacClipRRectParser(), @@ -142,6 +144,7 @@ class StacService { const StacBackdropFilterParser(), const StacVerticalDividerParser(), const StacSelectableTextParser(), + const StacTemplateBuilderParser(), ]; static final _actionParsers = [ @@ -238,7 +241,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 @@ -295,7 +303,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..4fc9595c3 --- /dev/null +++ b/packages/stac/lib/src/parsers/widgets/stac_template_builder/stac_template_builder_parser.dart @@ -0,0 +1,141 @@ +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 7fce7f5b3..dd71459d5 100644 --- a/packages/stac/lib/src/parsers/widgets/widgets.dart +++ b/packages/stac/lib/src/parsers/widgets/widgets.dart @@ -26,6 +26,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'; @@ -82,6 +84,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..872085593 --- /dev/null +++ b/packages/stac/lib/src/utils/template_utils.dart @@ -0,0 +1,219 @@ +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).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)!; + 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).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)!; + 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_core/lib/foundation/specifications/widget_type.dart b/packages/stac_core/lib/foundation/specifications/widget_type.dart index 2503b3389..1eac9bdee 100644 --- a/packages/stac_core/lib/foundation/specifications/widget_type.dart +++ b/packages/stac_core/lib/foundation/specifications/widget_type.dart @@ -91,6 +91,9 @@ enum WidgetType { /// Divider widget divider, + /// Dynamic data provider widget + dynamicDataProvider, + /// Dynamic view widget dynamicView, @@ -266,6 +269,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 fe4061ee3..d6befb73a 100644 --- a/packages/stac_core/lib/widgets/widgets.dart +++ b/packages/stac_core/lib/widgets/widgets.dart @@ -29,6 +29,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'; @@ -88,6 +89,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';