diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..cdb1e774 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: CI + +on: + push: + pull_request: + +jobs: + check: + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v7 + - uses: subosito/flutter-action@v2 + with: + channel: stable + flutter-version: 3.47.7 + cache: true + - run: flutter pub get + - run: dart format --set-exit-if-changed --output=none lib test tool + - run: flutter analyze + - run: flutter test + - name: Build the example for the web (WebAssembly) + working-directory: example + run: flutter build web --wasm diff --git a/CHANGELOG.md b/CHANGELOG.md index dde3003f..93ee31fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,77 @@ +## 7.0.0 + +### Breaking changes + +- Require Dart 3.13 and Flutter 3.47. +- Migrate to codelessly_api 5.0.0, json_annotation and json_serializable, + replacing the forked codegen that no longer builds on current Dart. +- Remove the `staggerDownloadQueue` argument of `CodelesslyConfig.copyWith`, + which had no effect, and the font parser's command-line `main()`. + +### New + +- Material Symbols render, including fill, weight, grade and optical size. + Cupertino icons render from the cupertino_icons font. Two-tone Material + icons load again. +- `DebugLogger` and `ErrorLogger` are exported; `DataManager`'s public + constructor already required an `ErrorLogger`. +- `AuthManager` takes a `claimsPropagationTimeout`. +- `CodelesslyConfig.useHostTheme` and `maxTextScaleFactor` let layouts + follow the app's theme and, within a bound, the system text size. +- Web views reload when their bound URL changes. +- Placeholder nodes deserialize (codelessly_api 5). + +### Fixes + +- Authentication no longer hangs when project claims never arrive or the + token refresh fails, and works on Windows (it decoded the wrong string). +- Initialization no longer hangs when the publish model stream errors. +- Cloud database variables keep one Firestore listener each instead of + accumulating them, so earlier queries can't overwrite newer results. +- Failed onLoad API calls and non-blocking actions are reported instead of + surfacing as uncaught errors. +- Call Function actions now wait for the app's function (unless marked + non-blocking) and report its errors; before, the next action started + immediately and async errors went uncaught. +- `CodelesslyWidget` handles removing an explicit context or controller, + and a missing breakpoint, without throwing. +- PageView, TabBar and Accordion edge cases that threw are fixed; + Accordion respects `isExpanded` and `enabled`, and TabBar `showDivider`. +- Malformed links and failed SVG downloads no longer throw. +- The web view works in WebAssembly builds, and embedded video no longer + pulls native web view plugins into web builds; the package is fully + WebAssembly-compatible. +- The auth token and auth response body are no longer logged, and library + code logs through `DebugLogger` instead of `print`. + +### Other changes + +- Require Dart 3.13 and Flutter 3.47. +- Migrate to codelessly_api 5.0.0, json_annotation and json_serializable, + replacing the forked codegen that no longer builds on current Dart. +- Upgrade dependencies, including Firebase, google_fonts, equatable 3 and + json_path. +- Render the new API values: FilledButton and FilledButton.tonal buttons, + RoundedSuperellipseBorder shapes, TextAlign start/end, the + `endContained` FAB location, `webSearch`/`twitter` keyboards, and the + `onUnfocus`/`onUserInteractionIfError` autovalidate modes. +- Render the new AppBar `foregroundColor`/`scrolledUnderElevation` and + TabBar `tabAlignment`/`dividerHeight` properties. +- Render the other parameters added in codelessly_api 5: Radio side, + inner radius and background, Slider value indicator modes, secondary + track and padding, ListTile minimum height and title alignment, + CircularProgressIndicator stroke alignment and track gap, ExpansionTile + trailing icon visibility and shapes, Text soft wrapping and scaling, and + PageView implicit scrolling. +- Render the gapped slider track and the drop and rounded rectangle value + indicators; these previously fell back to other shapes. +- Replace deprecated Flutter APIs: scrollCacheExtent, RadioGroup, + ShowValueIndicator.onDrag, Switch activeThumbColor and the new Color + component accessors. +- Fix a LateInitializationError when a node widget is built without a + CodelesslyContext above it. +- Fix cache writes whose failures escaped the error handler. + ## 6.2.0 - Create dynamic settings architecture. diff --git a/README.md b/README.md index 8706efa8..4502679e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![Codelessly Logo](packages/Codelessly-Logo-Text.png) +![Codelessly Logo](https://raw.githubusercontent.com/Codelessly/CodelesslySDK/master/packages/Codelessly-Logo-Text.png) # Codelessly CloudUI™ SDK @@ -6,7 +6,7 @@ > ### Dynamic UI and real-time updates for Flutter apps -![Codelessly Publish UI](packages/Codelessly-Cover.png) +![Codelessly Publish UI](https://raw.githubusercontent.com/Codelessly/CodelesslySDK/master/packages/Codelessly-Cover.png) Supercharge your Flutter apps with dynamic UI and real-time updates. Build and publish UI without code! @@ -60,10 +60,10 @@ CodelesslyWidget( **How to obtain a Layout ID** 1. In the Codelessly Editor, select the **canvas** of your layout. -2. Press the **Preview Icon** in the toolbar. ![CloudUI Preview Icon](packages/preview_icon.png) +2. Press the **Preview Icon** in the toolbar. ![CloudUI Preview Icon](https://raw.githubusercontent.com/Codelessly/CodelesslySDK/master/packages/preview_icon.png) 3. Copy the **layoutID**. -![Codelessly Widget Code](packages/codelessly_widget_code.png) +![Codelessly Widget Code](https://raw.githubusercontent.com/Codelessly/CodelesslySDK/master/packages/codelessly_widget_code.png) ### Complete Example @@ -73,7 +73,7 @@ Here is a complete example: import 'package:codelessly_sdk/codelessly_sdk.dart'; class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); + const MyApp({super.key}); @override Widget build(BuildContext context) { @@ -114,7 +114,7 @@ CodelesslyWidget( Use the `${}` templating syntax in input fields to link data from the Codelessly editor to layouts as shown below. -![Data](packages/ui_with_data_linking.png) +![Data](https://raw.githubusercontent.com/Codelessly/CodelesslySDK/master/packages/ui_with_data_linking.png) To link the title of a text widget to the `title` variable in the Codelessly editor, put `${title}` in the text widget’s text field. @@ -144,7 +144,7 @@ the `data` is updated. This how it looks with populated data: -![Data](packages/ui_with_populated_data.png) +![Data](https://raw.githubusercontent.com/Codelessly/CodelesslySDK/master/packages/ui_with_populated_data.png) ## Functions @@ -156,7 +156,7 @@ CodelesslyWidget( functions: { 'functionName': (context, reference, params) { // TODO: Implement your function here - }), + }, }, ), ``` @@ -165,7 +165,7 @@ CodelesslyWidget( Here, we tell the button to call the native `onNextButtonClicked` function when pressed. -![Defining call function action](packages/defining_call_function_action.png) +![Defining call function action](https://raw.githubusercontent.com/Codelessly/CodelesslySDK/master/packages/defining_call_function_action.png) In the Codelessly Editor, you can easily add an Action to a widget. Use the `Call Function` action to invoke `onNextButtonClicked`. @@ -187,10 +187,10 @@ to all `CodelesslyWidget`s. ```dart CodelesslyWidget( - layoutID: '0R0Qe7wgeAJMnj3MGW4l', - isPreview: kDebugMode, + layoutID: LAYOUT_ID, + publishSource: kDebugMode ? PublishSource.preview : PublishSource.publish, config: CodelesslyConfig( - authToken: 'LCVyNTxyLCVxQXh3WDc5MFowLjApQXJfWyNdSnlAQjphLyN1', + authToken: AUTH_TOKEN, ), ) ``` @@ -198,11 +198,11 @@ CodelesslyWidget( - `layoutID`: The ID of the published canvas. The ID can be found in Quick Preview or under `Publish > Settings > Published Layouts`. -![Codelessly Published Layout ID](packages/codelessly_published_layout_id.png) +![Codelessly Published Layout ID](https://raw.githubusercontent.com/Codelessly/CodelesslySDK/master/packages/codelessly_published_layout_id.png) -- `isPreview`: Whether the layout is in preview or production mode. Preview mode is meant for debugging the layout and - syncs with the changes made in the editor. Widgets in production mode are only updated when published using the - Publish button. +- `publishSource`: Whether the layout loads from preview or production. Preview mode is meant for debugging the layout + and syncs with the changes made in the editor. Widgets in production mode are only updated when published using the + Publish button. Defaults to the global config's setting. - `config`: An optional `CodelesslyConfig` that holds the information required to authenticate your layout from the server. @@ -211,7 +211,8 @@ lets you embed layouts from other projects with different auth tokens. ## Environment Configuration -The CodelesslyWidget supports **Preview** and **Published** environments via the `isPreview` boolean. +The CodelesslyWidget supports **Preview** and **Published** environments, set globally with `isPreview` or per widget +with `publishSource`. ```dart // Global config. @@ -223,7 +224,8 @@ Codelessly.instance.initialize( // Widget level. Overrides global settings for this widget. CodelesslyWidget( - isPreview: true + layoutID: LAYOUT_ID, + publishSource: PublishSource.preview, ); ``` @@ -275,7 +277,7 @@ Codelessly.instance.initialize( authToken: AUTH_TOKEN, isPreview: kDebugMode, preload: true, - automaticallyCollectCrashReports: true, + automaticallySendCrashReports: true, ), ); ``` @@ -285,8 +287,12 @@ CodelesslyConfig supports the following configuration capabilities. * `isPreview`: Global enable or disable preview-mode setting. * `preload`: Preload layouts to improve performance. When layouts are preloaded, they load instantly and behave like local widgets. `true` by default. -* `automaticallyCollectCrashReports`: Report SDK crashes to Codelessly. `true` by default. You can optionally disable - this behavior. +* `automaticallySendCrashReports`: Report SDK crashes to Codelessly. `false` by default. Only the error and stack trace + are sent, no device data. +* `useHostTheme`: Render layouts with your app's `Theme` instead of a default `ThemeData`. Layouts are styled in the + editor, so they use a default theme by default to look exactly as designed. +* `maxTextScaleFactor`: The largest system text scale layouts follow. `1` by default, which keeps layouts at their + designed size; raise it to let text grow with the user's accessibility settings. ### Global Data and Functions @@ -296,8 +302,8 @@ Data and functions registered in the global `Codelessly.instance` are available Codelessly.instance.initialize( data: {'username': 'Sample User', 'paid': false}, functions: { - 'openPurchasePage': CodelesslyFunction( - (context) => Navigator.pushNamed(context, "PurchasePage")), + 'openPurchasePage': (context, reference, params) => + Navigator.pushNamed(context, 'PurchasePage'), }, ); ``` diff --git a/analysis_options.yaml b/analysis_options.yaml index fc71dc38..2d41ad3f 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -13,6 +13,7 @@ analyzer: exclude: - lib/**/*.pb*.dart - lib/**/*.g.dart + - build/** errors: # treat missing required parameters as a warning (not a hint) missing_required_param: warning @@ -20,6 +21,9 @@ analyzer: missing_return: warning # allow having TODOs in the code todo: ignore + # Models are mutable by design. Equatable 3 marks itself @immutable, + # which would flag every one of them. + must_be_immutable: ignore linter: rules: diff --git a/build.yaml b/build.yaml index 21c94274..4d315687 100644 --- a/build.yaml +++ b/build.yaml @@ -4,7 +4,7 @@ targets: exclude: - example/** builders: - codelessly_json_serializable: + json_serializable: options: # The other properties are the same as default (https://pub.dev/packages/json_serializable#build-configuration). # explicit_to_json must be true for nested properties to work (like a Paint inside a Frame, toJson must be called in both) diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml index ae08714c..f78ea2dc 100644 --- a/example/analysis_options.yaml +++ b/example/analysis_options.yaml @@ -7,6 +7,15 @@ # The following line activates a set of recommended lints for Flutter apps, # packages, and plugins designed to encourage good coding practices. +analyzer: + exclude: + - build/** + - android/** + - ios/** + - web/** + - windows/** + - macos/** + - linux/** include: package:flutter_lints/flutter.yaml linter: diff --git a/example/linux/flutter/generated_plugin_registrant.cc b/example/linux/flutter/generated_plugin_registrant.cc index fcafa493..f6f23bfe 100644 --- a/example/linux/flutter/generated_plugin_registrant.cc +++ b/example/linux/flutter/generated_plugin_registrant.cc @@ -6,17 +6,9 @@ #include "generated_plugin_registrant.h" -#include -#include #include void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) irondash_engine_context_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "IrondashEngineContextPlugin"); - irondash_engine_context_plugin_register_with_registrar(irondash_engine_context_registrar); - g_autoptr(FlPluginRegistrar) super_native_extensions_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "SuperNativeExtensionsPlugin"); - super_native_extensions_plugin_register_with_registrar(super_native_extensions_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/example/linux/flutter/generated_plugins.cmake b/example/linux/flutter/generated_plugins.cmake index f6cb52d0..df8d2f72 100644 --- a/example/linux/flutter/generated_plugins.cmake +++ b/example/linux/flutter/generated_plugins.cmake @@ -3,12 +3,11 @@ # list(APPEND FLUTTER_PLUGIN_LIST - irondash_engine_context - super_native_extensions url_launcher_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/example/macos/Flutter/GeneratedPluginRegistrant.swift b/example/macos/Flutter/GeneratedPluginRegistrant.swift index c4dc4aa7..6bfb9205 100644 --- a/example/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/example/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,41 +6,17 @@ import FlutterMacOS import Foundation import cloud_firestore -import cloud_functions -import connectivity_plus -import device_info_plus -import file_picker -import firebase_analytics import firebase_auth import firebase_core -import firebase_crashlytics -import firebase_database -import firebase_storage -import google_sign_in_ios -import irondash_engine_context -import path_provider_foundation import shared_preferences_foundation -import super_native_extensions import url_launcher_macos import webview_flutter_wkwebview func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) - FLTFirebaseFunctionsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFunctionsPlugin")) - ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) - DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) - FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) - FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) - FLTFirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCrashlyticsPlugin")) - FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin")) - FLTFirebaseStoragePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseStoragePlugin")) - FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin")) - IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin")) - PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) - SuperNativeExtensionsPlugin.register(with: registry.registrar(forPlugin: "SuperNativeExtensionsPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "WebViewFlutterPlugin")) } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 2d4e8793..ce8e8ac5 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -6,8 +6,8 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: - sdk: ">=3.6.0 <4.0.0" - flutter: ">=3.24.0" + sdk: ^3.13.0 + flutter: ">=3.47.0" dependencies: flutter: @@ -19,7 +19,7 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^5.0.0 + flutter_lints: ^6.0.0 flutter: uses-material-design: true diff --git a/example/windows/flutter/generated_plugin_registrant.cc b/example/windows/flutter/generated_plugin_registrant.cc index c437ccec..a3c4d169 100644 --- a/example/windows/flutter/generated_plugin_registrant.cc +++ b/example/windows/flutter/generated_plugin_registrant.cc @@ -7,29 +7,17 @@ #include "generated_plugin_registrant.h" #include -#include #include #include -#include -#include -#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { CloudFirestorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("CloudFirestorePluginCApi")); - ConnectivityPlusWindowsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); FirebaseAuthPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); FirebaseCorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); - FirebaseStoragePluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FirebaseStoragePluginCApi")); - IrondashEngineContextPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("IrondashEngineContextPluginCApi")); - SuperNativeExtensionsPluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("SuperNativeExtensionsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/example/windows/flutter/generated_plugins.cmake b/example/windows/flutter/generated_plugins.cmake index 932bb087..9cebacbc 100644 --- a/example/windows/flutter/generated_plugins.cmake +++ b/example/windows/flutter/generated_plugins.cmake @@ -4,16 +4,13 @@ list(APPEND FLUTTER_PLUGIN_LIST cloud_firestore - connectivity_plus firebase_auth firebase_core - firebase_storage - irondash_engine_context - super_native_extensions url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/lib/codelessly_sdk.dart b/lib/codelessly_sdk.dart index dfe64f0d..0fb7c904 100644 --- a/lib/codelessly_sdk.dart +++ b/lib/codelessly_sdk.dart @@ -15,6 +15,8 @@ export 'src/data/network_data_repository.dart'; export 'src/data/web_data_repository.dart'; export 'src/functions/functions.dart'; export 'src/functions/functions_repository.dart'; +export 'src/logging/debug_logger.dart'; +export 'src/logging/error_logger.dart'; export 'src/model/asset_model.dart'; export 'src/model/auth_data.dart'; export 'src/model/custom_component.dart'; @@ -30,6 +32,7 @@ export 'src/ui/codelessly_context.dart'; export 'src/ui/codelessly_widget.dart'; export 'src/ui/codelessly_widget_controller.dart'; export 'src/ui/stripe_painter.dart'; +export 'src/utils/cupertino_icons_by_codepoint.dart'; export 'src/utils/extensions.dart'; export 'src/utils/flutter_icons_data_map.dart'; export 'src/utils/flutter_icons_names_map.dart'; diff --git a/lib/src/auth/auth_manager.dart b/lib/src/auth/auth_manager.dart index fc1ea3d1..38e5076d 100644 --- a/lib/src/auth/auth_manager.dart +++ b/lib/src/auth/auth_manager.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:io'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/foundation.dart'; @@ -8,8 +7,6 @@ import 'package:http/http.dart' as http; import 'package:jwt_decoder/jwt_decoder.dart'; import '../../codelessly_sdk.dart'; -import '../logging/debug_logger.dart'; -import '../logging/error_logger.dart'; /// An implementation that uses Firebase auth to authenticate users and manage /// authentication state. Handles anonymous authentication, token verification, @@ -67,8 +64,9 @@ class AuthManager { required this.cacheManager, required this.firebaseAuth, required this.client, - AuthData? authData, - }) : _authData = authData { + this._authData, + this.claimsPropagationTimeout = const Duration(seconds: 30), + }) { if (_authData != null) { _authStreamController.add(_authData); } @@ -80,15 +78,20 @@ class AuthManager { final Stopwatch stopwatch = Stopwatch()..start(); if (firebaseAuth.currentUser == null) { - DebugLogger.instance - .printInfo('Authenticating anonymously...', name: name); + DebugLogger.instance.printInfo( + 'Authenticating anonymously...', + name: name, + ); await firebaseAuth.signInAnonymously(); - DebugLogger.instance - .printInfo('Anonymous authentication successful!', name: name); + DebugLogger.instance.printInfo( + 'Anonymous authentication successful!', + name: name, + ); } else { DebugLogger.instance.printInfo( - 'Already authenticated ${firebaseAuth.currentUser!.isAnonymous ? 'anonymously' : 'with email ${firebaseAuth.currentUser!.email}'}', - name: name); + 'Already authenticated ${firebaseAuth.currentUser!.isAnonymous ? 'anonymously' : 'with email ${firebaseAuth.currentUser!.email}'}', + name: name, + ); } // This step is mandatory to compare claims for auth. If re-auth is @@ -119,12 +122,14 @@ class AuthManager { } else { if (cachedAuthData.authToken != config.authToken) { DebugLogger.instance.printInfo( - 'Auth token mismatch. Cache invalidated.', - name: name); + 'Auth token mismatch. Cache invalidated.', + name: name, + ); } else { DebugLogger.instance.printInfo( - 'Project ID is not in user claims. Cache invalidated.', - name: name); + 'Project ID is not in user claims. Cache invalidated.', + name: name, + ); } await cacheManager.clearAll(); await cacheManager.deleteAllByteData(); @@ -137,8 +142,10 @@ class AuthManager { await cacheManager.deleteAllByteData(); } } else { - DebugLogger.instance - .printInfo('Auth data is not cached in cache manager.', name: name); + DebugLogger.instance.printInfo( + 'Auth data is not cached in cache manager.', + name: name, + ); } // We verify the auth token. If it the user is NOT already authenticated @@ -154,13 +161,15 @@ class AuthManager { if (!isAuthenticated()) { DebugLogger.instance.printInfo( - 'Token is not authenticated. Authenticating...', - name: name); + 'Token is not authenticated. Authenticating...', + name: name, + ); await authenticate(); } else { DebugLogger.instance.printInfo( - 'Token already authenticated! Verifying in the background...', - name: name); + 'Token already authenticated! Verifying in the background...', + name: name, + ); authenticate().catchError((error) { // Error handling needs to be done here because this will not be caught // by SDK initialization. @@ -170,8 +179,9 @@ class AuthManager { stopwatch.stop(); DebugLogger.instance.printInfo( - 'Auth manager initialized took ${stopwatch.elapsedMilliseconds}ms or ${stopwatch.elapsed.inSeconds}s', - name: name); + 'Auth manager initialized took ${stopwatch.elapsedMilliseconds}ms or ${stopwatch.elapsed.inSeconds}s', + name: name, + ); } /// Disposes this instance of the [AuthManager]. @@ -189,7 +199,7 @@ class AuthManager { DebugLogger.instance.printFunction('reset()', name: name); DebugLogger.instance.printInfo('Invalidating...', name: name); _authData = null; - _authStreamController.add(_authData); + if (!_disposed) _authStreamController.add(_authData); _idTokenChangeListener?.cancel(); } @@ -209,8 +219,8 @@ class AuthManager { /// configured in the [CodelesslyConfig] is returned. PublishSource getBestPublishSource(CodelesslyConfig config) => authData?.isTemplate == true - ? PublishSource.template - : config.publishSource; + ? PublishSource.template + : config.publishSource; /// A helper method to check if the user has access to a project given their /// [result] from [getIdTokenResult] and the [projectId] they're trying to @@ -227,8 +237,9 @@ class AuthManager { if (result == null) { DebugLogger.instance.printInfo( - 'Id token result is null. Cannot check claims for project id [$projectId]', - name: name); + 'Id token result is null. Cannot check claims for project id [$projectId]', + name: name, + ); return false; } @@ -237,7 +248,8 @@ class AuthManager { // https://github.com/firebase/flutterfire/issues/11768#issuecomment-1888363494 // A temporary fix for windows platform until the issue is resolved. if (!kIsWeb && defaultTargetPlatform == TargetPlatform.windows) { - claims = JwtDecoder.decode(result.toString()); + final String? token = result.token; + claims = token == null ? {} : JwtDecoder.decode(token); } else { claims = result.claims ?? {}; } @@ -245,31 +257,40 @@ class AuthManager { DebugLogger.instance.printInfo('User claims: $claims', name: name); if (claims.isEmpty) { DebugLogger.instance.printInfo( - 'User claims is null. Cannot check claims for project id [$projectId]', - name: name); + 'User claims is null. Cannot check claims for project id [$projectId]', + name: name, + ); return false; } if (!claims.containsKey('project_ids')) { DebugLogger.instance.printInfo( - 'User claims does not contain project_ids. Cannot check claims for project id [$projectId]', - name: name); + 'User claims does not contain project_ids. Cannot check claims for project id [$projectId]', + name: name, + ); return false; } if (claims['project_ids'] is! List) { DebugLogger.instance.printInfo( - 'User claims project_ids is not a list. Cannot check claims for project id [$projectId]', - name: name); + 'User claims project_ids is not a list. Cannot check claims for project id [$projectId]', + name: name, + ); return false; } final projectIds = claims['project_ids'] as List; - DebugLogger.instance - .printInfo('User project id claims: $projectIds', name: name); + DebugLogger.instance.printInfo( + 'User project id claims: $projectIds', + name: name, + ); return projectIds.contains(projectId); } + /// How long [postAuthSuccess] waits for the project claim to appear on the + /// user's token before failing authentication. + final Duration claimsPropagationTimeout; + /// This method is called after successful authentication of the token. /// It checks if the user has access to the project via token claims and if /// not, it listens to Firebase Auth user changes until the claim appears @@ -292,8 +313,9 @@ class AuthManager { // Check if the user already has access to the project. if (checkClaimsForProject(_idTokenResult, authData.projectId)) { DebugLogger.instance.printInfo( - 'User has access to project since the claim exists already. Completing Firebase Auth process.', - name: name); + 'User has access to project since the claim exists already. Completing Firebase Auth process.', + name: name, + ); return; } @@ -301,54 +323,72 @@ class AuthManager { _idTokenResult = await firebaseAuth.currentUser?.getIdTokenResult(true); if (checkClaimsForProject(_idTokenResult, authData.projectId)) { DebugLogger.instance.printInfo( - 'User has access to project since the claim exists already after force-refreshing. Completing Firebase Auth process.', - name: name); + 'User has access to project since the claim exists already after force-refreshing. Completing Firebase Auth process.', + name: name, + ); return; } DebugLogger.instance.printInfo( - 'Listening & waiting for Firebase Auth state changes to proceed since user token does not have desired claims for project access...', - name: name); + 'Listening & waiting for Firebase Auth state changes to proceed since user token does not have desired claims for project access...', + name: name, + ); // Create a completer to handle the completion of the Firebase Auth process. final Completer completer = Completer(); // Listen to Firebase Auth state changes. - _idTokenChangeListener = - firebaseAuth.userChanges().listen((User? event) async { - // If the completer is already completed, return. - // We check this because getIdTokenResult may take a while to complete - // and this userChanges() event may have already triggered a new event. - if (completer.isCompleted) return; + _idTokenChangeListener = firebaseAuth.userChanges().listen( + (User? event) async { + // If the completer is already completed, return. + // We check this because getIdTokenResult may take a while to complete + // and this userChanges() event may have already triggered a new event. + if (completer.isCompleted) return; - DebugLogger.instance.printInfo( + DebugLogger.instance.printInfo( 'Firebase Auth state changed. Checking claims...', - name: name); - _idTokenResult = await event?.getIdTokenResult(true); + name: name, + ); + try { + _idTokenResult = await event?.getIdTokenResult(true); + } catch (e, stackTrace) { + if (!completer.isCompleted) completer.completeError(e, stackTrace); + return; + } - // If the completer is already completed, return. - // We check this because getIdTokenResult may take a while to complete - // and this userChanges() event may have already triggered a new event. - if (completer.isCompleted) return; + // If the completer is already completed, return. + // We check this because getIdTokenResult may take a while to complete + // and this userChanges() event may have already triggered a new event. + if (completer.isCompleted) return; - // Check if the user has access to the project. - if (checkClaimsForProject(_idTokenResult, authData.projectId)) { - DebugLogger.instance.printInfo( + // Check if the user has access to the project. + if (checkClaimsForProject(_idTokenResult, authData.projectId)) { + DebugLogger.instance.printInfo( 'User has access to project since the claim exists. Completing Firebase Auth process.', - name: name); - completer.complete(event); - } - }); - - // Wait for the completer to complete. - await completer.future; + name: name, + ); + completer.complete(event); + } + }, + onError: (Object e, StackTrace stackTrace) { + if (!completer.isCompleted) completer.completeError(e, stackTrace); + }, + ); - // Cancel the listener for Firebase Auth state changes. - _idTokenChangeListener?.cancel(); + // Wait for the claim to arrive, but not forever: a failure here must + // surface as an authentication error instead of hanging initialization. + try { + await completer.future.timeout(claimsPropagationTimeout); + } finally { + // Cancel the listener for Firebase Auth state changes. + await _idTokenChangeListener?.cancel(); + _idTokenChangeListener = null; + } DebugLogger.instance.printInfo( - 'Auth state changed successfully as expected. Firebase Auth complete.', - name: name); + 'Auth state changed successfully as expected. Firebase Auth complete.', + name: name, + ); } /// Performs a handshake with the server to authenticate the token. @@ -358,8 +398,12 @@ class AuthManager { try { DebugLogger.instance.printInfo('Authenticating token...', name: name); + final String? userToken = _idTokenResult?.token; + if (userToken == null) { + throw StateError('Cannot authenticate without a Firebase ID token.'); + } final AuthData? authData = await verifyProjectAuthToken( - userToken: _idTokenResult!.token!, + userToken: userToken, config: config, client: client, postSuccess: postAuthSuccess, @@ -367,8 +411,9 @@ class AuthManager { if (_disposed) { DebugLogger.instance.printInfo( - 'Auth manager was disposed. Aborting authentication.', - name: name); + 'Auth manager was disposed. Aborting authentication.', + name: name, + ); return; } @@ -378,30 +423,41 @@ class AuthManager { await cacheManager.store('auth', _authData!.toJson()); DebugLogger.instance.printInfo('Stored auth data in cache', name: name); - DebugLogger.instance - .printInfo('Authentication successfully!', name: name); + DebugLogger.instance.printInfo( + 'Authentication successfully!', + name: name, + ); } else { _authData = null; _authStreamController.add(_authData); await cacheManager.delete('auth'); - DebugLogger.instance - .printInfo('Failed to authenticate token.', name: name); + DebugLogger.instance.printInfo( + 'Failed to authenticate token.', + name: name, + ); - ErrorLogger.instance.captureException('Authentication failed', - message: 'Failed to authenticate token', type: 'auth_failed'); + ErrorLogger.instance.captureException( + 'Authentication failed', + message: 'Failed to authenticate token', + type: 'auth_failed', + ); return; } - } on SocketException catch (e, str) { - ErrorLogger.instance.captureException(e, - message: 'Network error during authentication', - type: 'network_error', - stackTrace: str); + } on http.ClientException catch (e, str) { + ErrorLogger.instance.captureException( + e, + message: 'Network error during authentication', + type: 'network_error', + stackTrace: str, + ); rethrow; } catch (e, str) { - ErrorLogger.instance.captureException(e, - message: 'Failed to authenticate token', - type: 'auth_error', - stackTrace: str); + ErrorLogger.instance.captureException( + e, + message: 'Failed to authenticate token', + type: 'auth_error', + stackTrace: str, + ); rethrow; } } @@ -427,7 +483,7 @@ class AuthManager { required Future Function(AuthData authData) postSuccess, }) async { DebugLogger.instance.printFunction( - 'verifyProjectAuthToken(authToken: ${config.authToken}, slug: ${config.slug})', + 'verifyProjectAuthToken(slug: ${config.slug})', name: name, ); @@ -435,7 +491,8 @@ class AuthManager { // Make a POST request to the server to verify the token. final http.Response result = await client.post( Uri.parse( - '${config.firebaseCloudFunctionsBaseURL}/api/verifyProjectAuthToken'), + '${config.firebaseCloudFunctionsBaseURL}/api/verifyProjectAuthToken', + ), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer $userToken', @@ -451,8 +508,9 @@ class AuthManager { // successful. if (result.statusCode == 200) { DebugLogger.instance.printInfo( - 'Successful auth token verification response received.', - name: name); + 'Successful auth token verification response received.', + name: name, + ); final jsonBody = jsonDecode(result.body); @@ -463,21 +521,20 @@ class AuthManager { // success. await postSuccess(authData); - DebugLogger.instance - .printInfo('Auth token response:\n${result.body}', name: name); - return authData; } else { // If the status code of the response is not 200, the authentication failed. // Log the status code, reason, and body of the response. DebugLogger.instance.printInfo( - 'Failed to authenticate token.\nStatus Code: ${result.statusCode}.\nReason: ${result.reasonPhrase}\nBody: ${result.body}', - name: name); + 'Failed to authenticate token.\nStatus Code: ${result.statusCode}.\nReason: ${result.reasonPhrase}', + name: name, + ); } } catch (e, stacktrace) { DebugLogger.instance.printInfo( - 'Error trying to authenticate token.\nError: $e', - name: name); + 'Error trying to authenticate token.\nError: $e', + name: name, + ); DebugLogger.instance.printInfo('$stacktrace', name: name); } diff --git a/lib/src/cache/codelessly_cache_manager.dart b/lib/src/cache/codelessly_cache_manager.dart index a1cb0785..cf2c4363 100644 --- a/lib/src/cache/codelessly_cache_manager.dart +++ b/lib/src/cache/codelessly_cache_manager.dart @@ -5,8 +5,6 @@ import 'package:flutter/foundation.dart'; import 'package:hive_ce_flutter/hive_flutter.dart'; import '../../codelessly_sdk.dart'; -import '../logging/debug_logger.dart'; -import '../logging/error_logger.dart'; /// Handles caching of any data that needs to be cached via Hive. class CodelesslyCacheManager extends CacheManager { @@ -32,9 +30,7 @@ class CodelesslyCacheManager extends CacheManager { final Stopwatch stopwatch = Stopwatch()..start(); try { await Hive.initFlutter('codelessly_sdk'); - box = await Hive.openBox( - 'cache_${config.uniqueID.replaceAll('/', '')}', - ); + box = await Hive.openBox('cache_${config.uniqueID.replaceAll('/', '')}'); filesBox = await Hive.openBox( 'cache_files_${config.uniqueID.replaceAll('/', '')}', ); @@ -79,10 +75,20 @@ class CodelesslyCacheManager extends CacheManager { } @override - void dispose() async { + void dispose() { DebugLogger.instance.printFunction('dispose()', name: name); - box.close(); - filesBox.close(); + unawaited( + Future.wait([box.close(), filesBox.close()]) + .catchError((Object e, StackTrace stackTrace) { + ErrorLogger.instance.captureException( + e, + message: 'Failed to close cache', + type: 'cache_close_failed', + stackTrace: stackTrace, + ); + return []; + }), + ); } @override @@ -90,9 +96,9 @@ class CodelesslyCacheManager extends CacheManager { DebugLogger.instance.printFunction('store(key: $key)', name: name); try { if (value is String || value is num || value is bool || value is List) { - return box.put(key, value); + return await box.put(key, value); } - return box.put(key, jsonEncode(value)); + return await box.put(key, jsonEncode(value)); } catch (e, str) { ErrorLogger.instance.captureException( e, @@ -144,8 +150,10 @@ class CodelesslyCacheManager extends CacheManager { DebugLogger.instance.printInfo('Deleting all byte data...', name: name); try { filesBox.clear(); - DebugLogger.instance - .printInfo('Cache bytes deleted successfully!', name: name); + DebugLogger.instance.printInfo( + 'Cache bytes deleted successfully!', + name: name, + ); } catch (e, str) { ErrorLogger.instance.captureException( e, @@ -236,13 +244,17 @@ class CodelesslyCacheManager extends CacheManager { } final String fileName = path.split('/').last; if (!excludedFileNames.contains(fileName)) { - DebugLogger.instance - .printInfo('\t\tDeleting file: $path', name: name); + DebugLogger.instance.printInfo( + '\t\tDeleting file: $path', + name: name, + ); await filesBox.delete(path); - DebugLogger.instance - .printInfo('\t\tSuccessfully deleted.', name: name); + DebugLogger.instance.printInfo( + '\t\tSuccessfully deleted.', + name: name, + ); purgedFiles++; } diff --git a/lib/src/codelessly.dart b/lib/src/codelessly.dart index c922c6e9..6cfccad4 100644 --- a/lib/src/codelessly.dart +++ b/lib/src/codelessly.dart @@ -9,8 +9,6 @@ import 'package:flutter/widgets.dart'; import 'package:http/http.dart'; import '../codelessly_sdk.dart'; -import 'logging/debug_logger.dart'; -import 'logging/error_logger.dart'; import 'logging/stat_tracker.dart'; import 'utils/codelessly_http_client.dart'; @@ -21,7 +19,9 @@ typedef NavigationListener = void Function( ); typedef BreakpointsListener = void Function( - BuildContext context, Breakpoint breakpoint); + BuildContext context, + Breakpoint breakpoint, +); /// The entry point for accessing the Codelessly SDK. /// @@ -106,10 +106,10 @@ class Codelessly { /// A helper getter to retrieve the active [DataManager] based on the /// [PublishSource] provided by the [CodelesslyConfig]. DataManager get dataManager => switch (config!.publishSource) { - PublishSource.publish => publishDataManager, - PublishSource.preview => previewDataManager, - PublishSource.template => templateDataManager, - }; + PublishSource.publish => publishDataManager, + PublishSource.preview => previewDataManager, + PublishSource.template => templateDataManager, + }; /// Returns the local instance of the Firebase SDK that this instance of the /// Codelessly SDK is using. @@ -229,8 +229,9 @@ class Codelessly { void dispose({bool sealCache = true}) { DebugLogger.instance.printFunction('dispose()', name: name); DebugLogger.instance.printInfo( - 'Disposing SDK. ${sealCache ? 'Sealing cache.' : 'Keeping cache open.'}', - name: name); + 'Disposing SDK. ${sealCache ? 'Sealing cache.' : 'Keeping cache open.'}', + name: name, + ); status = CStatus.empty(); _statusStreamController.close(); @@ -262,8 +263,9 @@ class Codelessly { FutureOr reset({bool clearCache = false}) async { DebugLogger.instance.printFunction('reset()', name: name); DebugLogger.instance.printInfo( - 'Resetting SDK. ${clearCache ? 'Clearing cache.' : 'Keeping cache.'}', - name: name); + 'Resetting SDK. ${clearCache ? 'Clearing cache.' : 'Keeping cache.'}', + name: name, + ); _cacheManager?.reset(); _publishDataManager?.reset(); _previewDataManager?.reset(); @@ -393,8 +395,9 @@ class Codelessly { final FirebaseOptions firebaseOptions = config.firebaseOptions; DebugLogger.instance.printInfo( - 'Initializing Firebase instance with project ID: [${firebaseOptions.projectId}] and Firebase instance name [$firebaseInstanceName]', - name: name); + 'Initializing Firebase instance with project ID: [${firebaseOptions.projectId}] and Firebase instance name [$firebaseInstanceName]', + name: name, + ); final Stopwatch stopwatch = Stopwatch()..start(); try { @@ -413,8 +416,9 @@ class Codelessly { } finally { if (_firebaseApp != null) { DebugLogger.instance.printInfo( - 'Firebase default app not initialized. Call Firebase.initializeApp() before initializing the CodelesslySDK.', - name: name); + 'Firebase default app not initialized. Call Firebase.initializeApp() before initializing the CodelesslySDK.', + name: name, + ); } } @@ -425,33 +429,40 @@ class Codelessly { _didInitializeFirebase = true; DebugLogger.instance.printInfo( - 'Codelessly successfully initialized the default Firebase app.', - name: name); + 'Codelessly successfully initialized the default Firebase app.', + name: name, + ); return; } DebugLogger.instance.printInfo( - 'A default Firebase app was found already registered. Checking for an existing [$firebaseInstanceName] Firebase app.', - name: name); + 'A default Firebase app was found already registered. Checking for an existing [$firebaseInstanceName] Firebase app.', + name: name, + ); FirebaseApp? existingApp; // Check if an existing Firebase app instance can be reused. - existingApp = Firebase.apps - .firstWhereOrNull((app) => app.name == firebaseInstanceName); + existingApp = Firebase.apps.firstWhereOrNull( + (app) => app.name == firebaseInstanceName, + ); if (existingApp != null) { DebugLogger.instance.printInfo( - 'Found an existing Firebase app instance with name: [$firebaseInstanceName]. Using it.', - name: name); + 'Found an existing Firebase app instance with name: [$firebaseInstanceName]. Using it.', + name: name, + ); _firebaseApp = existingApp; } else { // Create a new Firebase app instance if none exists DebugLogger.instance.printInfo( - 'No existing Firebase app instance found with name: [$firebaseInstanceName]. Registering a new one.', - name: name); + 'No existing Firebase app instance found with name: [$firebaseInstanceName]. Registering a new one.', + name: name, + ); _firebaseApp = await Firebase.initializeApp( - name: firebaseInstanceName, options: firebaseOptions); + name: firebaseInstanceName, + options: firebaseOptions, + ); } // Initialize Firestore and FirebaseAuth instances. @@ -461,8 +472,9 @@ class Codelessly { _didInitializeFirebase = true; DebugLogger.instance.printInfo( - 'Firebase instance initialized successfully [${_firebaseApp?.name}].', - name: name); + 'Firebase instance initialized successfully [${_firebaseApp?.name}].', + name: name, + ); } catch (e, str) { ErrorLogger.instance.captureException( e, @@ -474,8 +486,9 @@ class Codelessly { stopwatch.stop(); DebugLogger.instance.printInfo( - 'Firebase initialized in ${stopwatch.elapsed.inMilliseconds}ms or ${stopwatch.elapsed.inSeconds}s', - name: name); + 'Firebase initialized in ${stopwatch.elapsed.inMilliseconds}ms or ${stopwatch.elapsed.inSeconds}s', + name: name, + ); } /// Initializes the internal Firestore instance used by this SDK and @@ -518,9 +531,7 @@ class Codelessly { }) async { DebugLogger.instance.initialize( name: name, - config: DebugLoggerConfig( - debugLog: config?.debugLog ?? false, - ), + config: DebugLoggerConfig(debugLog: config?.debugLog ?? false), ); DebugLogger.instance.printFunction('initialize()', name: name); @@ -571,7 +582,8 @@ class Codelessly { _cacheManager = cacheManager ?? CodelesslyCacheManager(config: _config!); // Create the auth manager. - _authManager = authManager ?? + _authManager = + authManager ?? AuthManager( config: _config!, cacheManager: _cacheManager!, @@ -580,7 +592,8 @@ class Codelessly { ); // Create the publish data manager. - _publishDataManager = publishDataManager ?? + _publishDataManager = + publishDataManager ?? DataManager( 'Publish', config: _config!.copyWith(isPreview: false), @@ -598,7 +611,8 @@ class Codelessly { ); // Create the preview data manager. - _previewDataManager = previewDataManager ?? + _previewDataManager = + previewDataManager ?? DataManager( 'Preview', config: _config!.copyWith(isPreview: true), @@ -617,7 +631,8 @@ class Codelessly { // Create the template data manager. This is always automatically // created. - _templateDataManager = templateDataManager ?? + _templateDataManager = + templateDataManager ?? DataManager( 'Template', config: _config!.copyWith( @@ -639,10 +654,7 @@ class Codelessly { status = CStatus.loading(CLoadingState.createdManagers); - DebugLogger.instance.printInfo( - 'Initializing cache manager', - name: name, - ); + DebugLogger.instance.printInfo('Initializing cache manager', name: name); await _cacheManager!.init(); status = CStatus.loading(CLoadingState.initializedCache); @@ -659,7 +671,8 @@ class Codelessly { StatTracker.instance.init( projectId: projectId, serverUrl: Uri.parse( - '${_config!.firebaseCloudFunctionsBaseURL}/api/trackStatsRequest'), + '${_config!.firebaseCloudFunctionsBaseURL}/api/trackStatsRequest', + ), ); } @@ -682,10 +695,7 @@ class Codelessly { await dataManager.init(layoutID: null); } - DebugLogger.instance.printInfo( - 'Data manager initialized.', - name: name, - ); + DebugLogger.instance.printInfo('Data manager initialized.', name: name); status = CStatus.loading(CLoadingState.initializedDataManagers); } else { if (!initializeDataManagers) { @@ -706,41 +716,48 @@ class Codelessly { 'Since a slug was provided & data manager finished, authenticating in the background...', name: name, ); - _authManager!.init().then((_) async { - if (_authManager!.authData == null) return; - - if (_authManager?.authData?.projectId case String projectId) { - StatTracker.instance.init( - projectId: projectId, - serverUrl: Uri.parse( - '${_config!.firebaseCloudFunctionsBaseURL}/api/trackStatsRequest'), - ); - } - - DebugLogger.instance.printInfo( - '[POST-INIT] Background authentication succeeded. Initializing layout storage.', - name: name, - ); - await dataManager - .onPublishModelLoaded(_authManager!.authData!.projectId); - - DebugLogger.instance.printInfo( - '[POST-INIT] Layout storage initialized. Listening to publish model.', - name: name, - ); - dataManager.listenToPublishModel(_authManager!.authData!.projectId); - }).catchError((e, str) { - DebugLogger.instance.printInfo( - '[POST-INIT] Background authentication failed.', - name: name, - ); - ErrorLogger.instance.captureException( - e, - message: 'Failed to authenticate', - type: 'authentication_failed', - stackTrace: str, - ); - }); + _authManager! + .init() + .then((_) async { + if (_authManager!.authData == null) return; + + if (_authManager?.authData?.projectId case String projectId) { + StatTracker.instance.init( + projectId: projectId, + serverUrl: Uri.parse( + '${_config!.firebaseCloudFunctionsBaseURL}/api/trackStatsRequest', + ), + ); + } + + DebugLogger.instance.printInfo( + '[POST-INIT] Background authentication succeeded. Initializing layout storage.', + name: name, + ); + await dataManager.onPublishModelLoaded( + _authManager!.authData!.projectId, + ); + + DebugLogger.instance.printInfo( + '[POST-INIT] Layout storage initialized. Listening to publish model.', + name: name, + ); + dataManager.listenToPublishModel( + _authManager!.authData!.projectId, + ); + }) + .catchError((e, str) { + DebugLogger.instance.printInfo( + '[POST-INIT] Background authentication failed.', + name: name, + ); + ErrorLogger.instance.captureException( + e, + message: 'Failed to authenticate', + type: 'authentication_failed', + stackTrace: str, + ); + }); status = CStatus.loading(CLoadingState.initializedSlug); } diff --git a/lib/src/codelessly_config.dart b/lib/src/codelessly_config.dart index 534d763f..6328051b 100644 --- a/lib/src/codelessly_config.dart +++ b/lib/src/codelessly_config.dart @@ -6,7 +6,7 @@ import 'firebase_options_prod.dart'; import 'model/publish_source.dart'; /// Holds initialization configuration options for the SDK. -class CodelesslyConfig with EquatableMixin { +class CodelesslyConfig with Equatable { /// The SDK auth token used to authenticate the SDK with the Codelessly /// servers. final String authToken; @@ -84,6 +84,23 @@ class CodelesslyConfig with EquatableMixin { /// The Firebase project ID for this instance. final String firebaseProjectId; + /// Whether layouts render with the app's [Theme] instead of a default + /// [ThemeData]. + /// + /// Layouts are styled explicitly in the Codelessly editor, so by default + /// they render with a default theme to look exactly as designed. Enable this + /// to let theme-derived defaults (color scheme, typography, brightness) + /// follow the app. + final bool useHostTheme; + + /// The largest text scale factor layouts follow from the platform's text + /// size setting. + /// + /// Layouts are designed at fixed sizes, so the default of 1 ignores the + /// setting. Raise it to let text grow for accessibility, at the risk of + /// text overflowing fixed-size boxes. + final double maxTextScaleFactor; + /// Creates a new instance of [CodelesslyConfig]. /// /// [authToken] is the token required to authenticate and initialize the SDK. @@ -108,10 +125,14 @@ class CodelesslyConfig with EquatableMixin { this.firebaseProjectId = CodelesslyConstants.kProdFirebaseProjectId, PublishSource? publishSource, this.debugLog = false, - }) : firebaseOptions = - firebaseOptions ?? DefaultFirebaseOptionsProd.currentPlatform, - publishSource = publishSource ?? - (isPreview ? PublishSource.preview : PublishSource.publish); + this.useHostTheme = false, + this.maxTextScaleFactor = 1, + }) : assert(maxTextScaleFactor >= 1), + firebaseOptions = + firebaseOptions ?? DefaultFirebaseOptionsProd.currentPlatform, + publishSource = + publishSource ?? + (isPreview ? PublishSource.preview : PublishSource.publish); /// Creates a new instance of [CodelesslyConfig] with the provided optional /// parameters. @@ -121,7 +142,6 @@ class CodelesslyConfig with EquatableMixin { bool? automaticallySendCrashReports, bool? isPreview, bool? preload, - bool? staggerDownloadQueue, FirebaseOptions? firebaseOptions, String? firebaseCloudFunctionsBaseURL, String? baseURL, @@ -129,39 +149,47 @@ class CodelesslyConfig with EquatableMixin { String? clientType, String? firebaseProjectId, PublishSource? publishSource, - }) => - CodelesslyConfig( - authToken: authToken ?? this.authToken, - slug: slug ?? this.slug, - automaticallySendCrashReports: - automaticallySendCrashReports ?? this.automaticallySendCrashReports, - isPreview: isPreview ?? this.isPreview, - preload: preload ?? this.preload, - firebaseOptions: firebaseOptions ?? this.firebaseOptions, - firebaseCloudFunctionsBaseURL: - firebaseCloudFunctionsBaseURL ?? this.firebaseCloudFunctionsBaseURL, - baseURL: baseURL ?? this.baseURL, - firebaseInstanceName: firebaseInstanceName ?? this.firebaseInstanceName, - clientType: clientType ?? this.clientType, - firebaseProjectId: firebaseProjectId ?? this.firebaseProjectId, - publishSource: publishSource ?? this.publishSource, - ); + bool? debugLog, + bool? useHostTheme, + double? maxTextScaleFactor, + }) => CodelesslyConfig( + authToken: authToken ?? this.authToken, + slug: slug ?? this.slug, + automaticallySendCrashReports: + automaticallySendCrashReports ?? this.automaticallySendCrashReports, + isPreview: isPreview ?? this.isPreview, + preload: preload ?? this.preload, + firebaseOptions: firebaseOptions ?? this.firebaseOptions, + firebaseCloudFunctionsBaseURL: + firebaseCloudFunctionsBaseURL ?? this.firebaseCloudFunctionsBaseURL, + baseURL: baseURL ?? this.baseURL, + firebaseInstanceName: firebaseInstanceName ?? this.firebaseInstanceName, + clientType: clientType ?? this.clientType, + firebaseProjectId: firebaseProjectId ?? this.firebaseProjectId, + publishSource: publishSource ?? this.publishSource, + debugLog: debugLog ?? this.debugLog, + useHostTheme: useHostTheme ?? this.useHostTheme, + maxTextScaleFactor: maxTextScaleFactor ?? this.maxTextScaleFactor, + ); @override List get props => [ - authToken, - slug, - automaticallySendCrashReports, - isPreview, - preload, - firebaseOptions, - firebaseCloudFunctionsBaseURL, - baseURL, - firebaseInstanceName, - clientType, - firebaseProjectId, - publishSource, - ]; + authToken, + slug, + automaticallySendCrashReports, + isPreview, + preload, + firebaseOptions, + firebaseCloudFunctionsBaseURL, + baseURL, + firebaseInstanceName, + clientType, + firebaseProjectId, + publishSource, + debugLog, + useHostTheme, + maxTextScaleFactor, + ]; } enum CLoadingState { diff --git a/lib/src/data/cloud_database.dart b/lib/src/data/cloud_database.dart index d002d8ac..8416ffc6 100644 --- a/lib/src/data/cloud_database.dart +++ b/lib/src/data/cloud_database.dart @@ -5,12 +5,9 @@ import 'package:codelessly_api/codelessly_api.dart'; import 'package:flutter/widgets.dart'; import '../../codelessly_sdk.dart'; -import '../logging/debug_logger.dart'; import '../logging/stat_tracker.dart'; import '../utils/constants.dart'; -const String _label = 'Cloud Database'; - abstract final class PrivateDocumentFields { PrivateDocumentFields._(); @@ -61,7 +58,7 @@ abstract class CloudDatabase extends ChangeNotifier { /// /// It will throw an [AssertionError] if the [identifier] is empty. CloudDatabase(this.identifier, this.publishSource) - : assert(identifier.isNotEmpty, 'identifier cannot be empty'); + : assert(identifier.isNotEmpty, 'identifier cannot be empty'); /// Adds a document to the cloud storage at the given [path]. /// @@ -197,20 +194,38 @@ class FirestoreCloudDatabase extends CloudDatabase { static const String name = 'CloudDatabase'; /// Reference to the root document for this session. - late final DocumentReference> rootRef = - firestore.collection(_rootCollection).doc(identifier); + late final DocumentReference> rootRef = firestore + .collection(_rootCollection) + .doc(identifier); String get _rootCollection => switch (publishSource) { - PublishSource.publish => 'data', - PublishSource.preview => 'preview_data', - PublishSource.template => 'template_data', - }; + PublishSource.publish => 'data', + PublishSource.preview => 'preview_data', + PublishSource.template => 'template_data', + }; /// Reference to the Firestore instance. final FirebaseFirestore firestore; - /// Subscriptions to the streams that are being listened to. - final List _subscriptions = []; + /// Active cloud streams keyed by the id of the variable they update. A + /// variable only follows its latest query, so starting a new stream for it + /// cancels the previous one. + final Map _subscriptions = {}; + + void _replaceSubscription( + Observable variable, + StreamSubscription subscription, + ) { + _subscriptions.remove(variable.value.id)?.cancel(); + _subscriptions[variable.value.id] = subscription; + } + + void _cancelSubscriptions() { + for (final subscription in _subscriptions.values) { + subscription.cancel(); + } + _subscriptions.clear(); + } /// Creates a [FirestoreCloudDatabase] with the given [identifier] and /// [firestore] instance. @@ -244,8 +259,10 @@ class FirestoreCloudDatabase extends CloudDatabase { await rootRef.set({'project': identifier}); StatTracker.instance.track(StatType.write, 'cloudDatabase/init'); - DebugLogger.instance - .printInfo('Done initializing for $identifier', name: name); + DebugLogger.instance.printInfo( + 'Done initializing for $identifier', + name: name, + ); } /// Gives a collection reference for the given [path]. @@ -278,8 +295,9 @@ class FirestoreCloudDatabase extends CloudDatabase { } else { // this means the ref represents a collection, so we add a collection // with the first part of the path as the id. - final String id = - pathParts.isNotEmpty ? pathParts.removeAt(0) : 'default'; + final String id = pathParts.isNotEmpty + ? pathParts.removeAt(0) + : 'default'; ref = ref.doc(part).collection(id); } } @@ -299,7 +317,9 @@ class FirestoreCloudDatabase extends CloudDatabase { /// [documentId] is the id of the document. If [documentId] is null or empty, /// then "default" is used as the document id. DocumentReference> getDocPath( - String path, String? documentId) { + String path, + String? documentId, + ) { // Use default if null. documentId ??= 'default'; @@ -418,7 +438,9 @@ class FirestoreCloudDatabase extends CloudDatabase { @override Future> getDocumentData( - String path, String documentId) async { + String path, + String documentId, + ) async { final docRef = getDocPath(path, documentId); final snapshot = await docRef.get(); @@ -435,7 +457,8 @@ class FirestoreCloudDatabase extends CloudDatabase { StatTracker.instance.track(StatType.read, 'cloudDatabase/streamDocument'); return snapshot.data()?.let( - (value) => sanitizeCloudDataForUse(value, docId: snapshot.id)) ?? + (value) => sanitizeCloudDataForUse(value, docId: snapshot.id), + ) ?? {}; }); } @@ -478,8 +501,9 @@ class FirestoreCloudDatabase extends CloudDatabase { // Set the variable with success state. variable.set( - variable.value - .copyWith(value: CloudDatabaseVariableUtils.success(data)), + variable.value.copyWith( + value: CloudDatabaseVariableUtils.success(data), + ), ); }, onError: (error) { @@ -496,20 +520,17 @@ class FirestoreCloudDatabase extends CloudDatabase { }, ); - // Add subscription to the list of subscriptions. - _subscriptions.add(subscription); + _replaceSubscription(variable, subscription); } @override void reset() { - // Cancel all subscriptions. - _subscriptions.forEach((sub) => sub.cancel()); + _cancelSubscriptions(); } @override void dispose() { - // Cancel all subscriptions. - _subscriptions.forEach((sub) => sub.cancel()); + _cancelSubscriptions(); super.dispose(); } @@ -564,8 +585,9 @@ class FirestoreCloudDatabase extends CloudDatabase { ); // Set the variable with success state. variable.set( - variable.value - .copyWith(value: CloudDatabaseVariableUtils.success(docs)), + variable.value.copyWith( + value: CloudDatabaseVariableUtils.success(docs), + ), ); }, onError: (error) { @@ -582,8 +604,7 @@ class FirestoreCloudDatabase extends CloudDatabase { }, ); - // Add subscription to the list of subscriptions. - _subscriptions.add(subscription); + _replaceSubscription(variable, subscription); } } @@ -599,10 +620,12 @@ Map sanitizeCloudDataForUse( // breaks reference and allows to modify the data. data = {...data}; - data[SDKConstants.createdAt] = - deserializeCosmicValue(data[SDKConstants.createdAt]); - data[SDKConstants.updatedAt] = - deserializeCosmicValue(data[SDKConstants.updatedAt]); + data[SDKConstants.createdAt] = deserializeCosmicValue( + data[SDKConstants.createdAt], + ); + data[SDKConstants.updatedAt] = deserializeCosmicValue( + data[SDKConstants.updatedAt], + ); data[SDKConstants.id] = docId; // Sort private fields to the bottom. @@ -641,8 +664,9 @@ Map sanitizeCloudDataToSend( late final DateTime now = DateTime.now().toUtc(); if (autoChangeUpdatedAt) { - data[SDKConstants.createdAt] = - serializedCosmicValue(data[SDKConstants.createdAt] ?? now); + data[SDKConstants.createdAt] = serializedCosmicValue( + data[SDKConstants.createdAt] ?? now, + ); data[SDKConstants.updatedAt] = serializedCosmicValue(now); } @@ -672,10 +696,9 @@ String? serializedCosmicValue(Object? value) { Timestamp timestamp => timestamp.toDate().toUtc().toIso8601String(), DateTime dateTime => dateTime.toUtc().toIso8601String(), String string => DateTime.tryParse(string)?.toUtc().toIso8601String(), - int millisecondsSinceEpoch => - DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch) - .toUtc() - .toIso8601String(), + int millisecondsSinceEpoch => DateTime.fromMillisecondsSinceEpoch( + millisecondsSinceEpoch, + ).toUtc().toIso8601String(), _ => null, }; } @@ -685,8 +708,9 @@ DateTime? deserializeCosmicValue(Object? value) { Timestamp timestamp => timestamp.toDate().toUtc(), DateTime dateTime => dateTime.toUtc(), String string => DateTime.tryParse(string)?.toUtc(), - int millisecondsSinceEpoch => - DateTime.fromMillisecondsSinceEpoch(millisecondsSinceEpoch).toUtc(), + int millisecondsSinceEpoch => DateTime.fromMillisecondsSinceEpoch( + millisecondsSinceEpoch, + ).toUtc(), _ => null, }; } diff --git a/lib/src/data/data_manager.dart b/lib/src/data/data_manager.dart index 2023d38b..3c6b99ff 100644 --- a/lib/src/data/data_manager.dart +++ b/lib/src/data/data_manager.dart @@ -7,8 +7,6 @@ import 'package:hive_ce_flutter/hive_flutter.dart'; import 'package:logging/logging.dart'; import '../../codelessly_sdk.dart'; -import '../logging/debug_logger.dart'; -import '../logging/error_logger.dart'; import '../logging/stat_tracker.dart'; /// Orchestrates the data flow for the SDK. @@ -118,8 +116,8 @@ class DataManager { required this.localDataRepository, required this.firebaseFirestore, required this.errorLogger, - SDKPublishModel? publishModel, - }) : _publishModel = publishModel; + this._publishModel, + }); /// Initializes the instance. /// @@ -152,8 +150,10 @@ class DataManager { _publishModel ??= cachedModel; if (_publishModel != null) { - DebugLogger.instance - .printInfo('Publish model is cached locally. Emitting.', name: name); + DebugLogger.instance.printInfo( + 'Publish model is cached locally. Emitting.', + name: name, + ); await emitPublishModel(); loadFontsFromPublishModel(); @@ -185,29 +185,44 @@ class DataManager { ); } - final publishBundleFuture = fetchCompletePublishBundle( - slug: slug, - source: config.publishSource, - ).then((success) { - if (success) { - DebugLogger.instance.printInfo( - '[slug] Complete publish model from slug is downloaded in background. Emitting.', - name: name, - ); - - loadFontsFromPublishModel(); - } else { - errorLogger.captureException( - 'Failed to download complete publish bundle', - message: - 'Failed to download complete publish bundle for slug $slug.', - type: 'bundle_download_failed', - ); - } - }); + final publishBundleFuture = + fetchCompletePublishBundle( + slug: slug, + source: config.publishSource, + ).then((success) { + if (success) { + DebugLogger.instance.printInfo( + '[slug] Complete publish model from slug is downloaded in background. Emitting.', + name: name, + ); + + loadFontsFromPublishModel(); + } else { + errorLogger.captureException( + 'Failed to download complete publish bundle', + message: + 'Failed to download complete publish bundle for slug $slug.', + type: 'bundle_download_failed', + ); + } + }); if (_publishModel == null) { await publishBundleFuture; + } else { + // Background refresh: report failures instead of leaving them as + // unhandled async errors. + unawaited( + publishBundleFuture.catchError((Object e, StackTrace st) { + errorLogger.captureException( + e, + message: + 'Failed to refresh publish bundle for slug $slug in background.', + type: 'bundle_download_failed', + stackTrace: st, + ); + }), + ); } _logTime(stopwatch); @@ -342,8 +357,14 @@ class DataManager { // Listen the publish model document. // It's either going to be fetched for the first time if it doesn't exist // in cache, or it's going to be updated with new data. - final Future firstPublishEvent = - listenToPublishModel(authData.projectId); + final Future firstPublishEvent = listenToPublishModel( + authData.projectId, + ); + if (_publishModel != null) { + // The cached model is used and the first event is not awaited. Its + // errors are already reported by the stream listener. + firstPublishEvent.ignore(); + } // If the publish model is still null, then we need to wait for the first // publish model to arrive from the server via the stream above. @@ -548,10 +569,7 @@ class DataManager { _localDatabase?.reset(); _localDatabase = await initializeLocalStorage(projectId: projectId); didChange = true; - DebugLogger.instance.printInfo( - 'Local storage initialized.', - name: name, - ); + DebugLogger.instance.printInfo('Local storage initialized.', name: name); } else { DebugLogger.instance.printInfo( 'Local storage already initialized correctly. Skipping.', @@ -559,12 +577,14 @@ class DataManager { ); } - final bool shouldInitCloudStorage = _cloudDatabase == null || + final bool shouldInitCloudStorage = + _cloudDatabase == null || (cloudDatabase!.publishSource != config.publishSource || cloudDatabase!.identifier != projectId); bool isAuthenticated = false; if (shouldInitCloudStorage) { - isAuthenticated = authManager.isAuthenticated() && + isAuthenticated = + authManager.isAuthenticated() && authManager.hasCloudStorageAccess(projectId); } if (shouldInitCloudStorage && isAuthenticated) { @@ -576,10 +596,7 @@ class DataManager { _cloudDatabase = await initializeCloudStorage(projectId: projectId); _publishModelStreamController.add(_publishModel); didChange = true; - DebugLogger.instance.printInfo( - 'Cloud storage initialized.', - name: name, - ); + DebugLogger.instance.printInfo('Cloud storage initialized.', name: name); } else { if (shouldInitCloudStorage) { DebugLogger.instance.printInfo( @@ -597,8 +614,9 @@ class DataManager { return didChange; } - Future initializeLocalStorage( - {required String projectId}) async { + Future initializeLocalStorage({ + required String projectId, + }) async { final Box box = await Hive.openBox(projectId); return HiveLocalDatabase(box, identifier: projectId); } @@ -607,8 +625,9 @@ class DataManager { // [DataManager] by delegating this initialization to the creator of // [DataManager], maybe through a callback so that DataManager can decide // exactly when to initialize this. - Future initializeCloudStorage( - {required String projectId}) async { + Future initializeCloudStorage({ + required String projectId, + }) async { final instance = FirestoreCloudDatabase( projectId, config.publishSource, @@ -645,10 +664,7 @@ class DataManager { name: name, ); } else { - DebugLogger.instance.printInfo( - 'No fonts to load.', - name: name, - ); + DebugLogger.instance.printInfo('No fonts to load.', name: name); } for (final SDKPublishFont font in _publishModel!.fonts.values) { @@ -670,87 +686,91 @@ class DataManager { final Completer completer = Completer(); _publishModelDocumentListener?.cancel(); - _publishModelDocumentListener = networkDataRepository - .streamPublishModel( - projectID: projectId, - source: config.publishSource, - ) - .listen((SDKPublishModel? serverModel) { - if (serverModel == null) return; - - DebugLogger.instance.printInfo( - 'Publish model stream event received.', - name: name, - ); - - final bool isFirstEvent = !completer.isCompleted; - - // If the completer has not completed yet, it needs to be - // completed with the first available publish model form the server. - if (isFirstEvent) { - DebugLogger.instance.printInfo( - 'Completing publish model stream completer since this is the first event.', - name: name, - ); - completer.complete(serverModel); - } - - // If the publish model is null, meaning no publish model was previously - // cached, then this is the first available publish model we have that - // just arrived from the server. - // - // We can skip publish model comparison as there is nothing to compare - // to yet. - // - // Since it is null, it will be hydrated with a value outside of this - // listener when the completer completes. The publish model is emitted - // once that happens, therefore we don't need to emit it here, nor - // compare. - if (_publishModel == null) { - DebugLogger.instance.printInfo( - 'Publish model is null during init and received the first publish model from the server. Skipping comparison in stream.', - name: name, - ); - return; - } - - if (config.slug != null) { - DebugLogger.instance.printInfo( - 'Initialized using the slug, this event is not essential for initial loading.', - name: name, - ); - } else if (isFirstEvent) { - DebugLogger.instance.printInfo( - 'Publish model during init was not null, and we received a new publish model from the server. Comparing...', - name: name, - ); - } else { - DebugLogger.instance.printInfo( - 'Received a new publish model from the server. Comparing...', - name: name, - ); - } - final SDKPublishModel localModel = _publishModel!; - - // Comparison should always be a background process. - processPublishDifference( - serverModel: serverModel, - localModel: localModel, - ); - - DebugLogger.instance.printInfo( - 'Publish model comparison complete.', - name: name, - ); - }) - ..onError((error, str) { - ErrorLogger.instance.captureException( - error, - message: 'Failed to listen to publish model', - type: 'publish_model_stream_failed', - stackTrace: str, - ); - }); + _publishModelDocumentListener = + networkDataRepository + .streamPublishModel( + projectID: projectId, + source: config.publishSource, + ) + .listen((SDKPublishModel? serverModel) { + if (serverModel == null) return; + + DebugLogger.instance.printInfo( + 'Publish model stream event received.', + name: name, + ); + + final bool isFirstEvent = !completer.isCompleted; + + // If the completer has not completed yet, it needs to be + // completed with the first available publish model form the server. + if (isFirstEvent) { + DebugLogger.instance.printInfo( + 'Completing publish model stream completer since this is the first event.', + name: name, + ); + completer.complete(serverModel); + } + + // If the publish model is null, meaning no publish model was previously + // cached, then this is the first available publish model we have that + // just arrived from the server. + // + // We can skip publish model comparison as there is nothing to compare + // to yet. + // + // Since it is null, it will be hydrated with a value outside of this + // listener when the completer completes. The publish model is emitted + // once that happens, therefore we don't need to emit it here, nor + // compare. + if (_publishModel == null) { + DebugLogger.instance.printInfo( + 'Publish model is null during init and received the first publish model from the server. Skipping comparison in stream.', + name: name, + ); + return; + } + + if (config.slug != null) { + DebugLogger.instance.printInfo( + 'Initialized using the slug, this event is not essential for initial loading.', + name: name, + ); + } else if (isFirstEvent) { + DebugLogger.instance.printInfo( + 'Publish model during init was not null, and we received a new publish model from the server. Comparing...', + name: name, + ); + } else { + DebugLogger.instance.printInfo( + 'Received a new publish model from the server. Comparing...', + name: name, + ); + } + final SDKPublishModel localModel = _publishModel!; + + // Comparison should always be a background process. + processPublishDifference( + serverModel: serverModel, + localModel: localModel, + ); + + DebugLogger.instance.printInfo( + 'Publish model comparison complete.', + name: name, + ); + }) + ..onError((error, str) { + ErrorLogger.instance.captureException( + error, + message: 'Failed to listen to publish model', + type: 'publish_model_stream_failed', + stackTrace: str, + ); + // Without this, initialization would wait forever for a first event + // that never comes, e.g. on a Firestore permission error. + if (!completer.isCompleted) completer.completeError(error, str); + }); return completer.future; } @@ -857,8 +877,8 @@ class DataManager { final bool entryChanged = localModel.entryLayoutId != serverModel.entryLayoutId || - localModel.entryCanvasId != serverModel.entryCanvasId || - localModel.entryPageId != serverModel.entryPageId; + localModel.entryCanvasId != serverModel.entryCanvasId || + localModel.entryPageId != serverModel.entryPageId; final bool didLayoutIDMapChange = const MapEquality().equals( localModel.layoutIDMap, @@ -866,17 +886,24 @@ class DataManager { ); if (didLayoutIDMapChange) { localModel = localModel.copyWith(layoutIDMap: serverModel.layoutIDMap); - DebugLogger.instance - .printInfo('DIFF: Layout ID map changed. Updating...', name: name); + DebugLogger.instance.printInfo( + 'DIFF: Layout ID map changed. Updating...', + name: name, + ); } - final bool disabledLayoutsChanged = !(const ListEquality() - .equals(localModel.disabledLayouts, serverModel.disabledLayouts)); + final bool disabledLayoutsChanged = !(const ListEquality().equals( + localModel.disabledLayouts, + serverModel.disabledLayouts, + )); if (disabledLayoutsChanged) { - localModel = - localModel.copyWith(disabledLayouts: serverModel.disabledLayouts); - DebugLogger.instance - .printInfo('DIFF: Disabled layouts changed. Updating...', name: name); + localModel = localModel.copyWith( + disabledLayouts: serverModel.disabledLayouts, + ); + DebugLogger.instance.printInfo( + 'DIFF: Disabled layouts changed. Updating...', + name: name, + ); } if (layoutUpdates.isEmpty && @@ -888,11 +915,14 @@ class DataManager { !entryChanged && !didLayoutIDMapChange && !disabledLayoutsChanged) { - DebugLogger.instance - .printInfo('DIFF: No updates to process.', name: name); + DebugLogger.instance.printInfo( + 'DIFF: No updates to process.', + name: name, + ); return; } else { - DebugLogger.instance.printInfo('''DIFF: Processing updates: + DebugLogger.instance.printInfo( + '''DIFF: Processing updates: | ${layoutUpdates.length} layout updates. | ${fontUpdates.length} font updates. | ${apiUpdates.length} api updates. @@ -902,7 +932,8 @@ class DataManager { | ${entryChanged ? 1 : 0} entry id update${entryChanged ? '' : 's'}. | ${didLayoutIDMapChange ? 'Layout ID map changed.' : 'No layout ID map changes.'} | ${disabledLayoutsChanged ? 'Disabled layout IDs changed.' : 'No disabled layout IDs changed.'}''', - name: name); + name: name, + ); } for (final String layoutID in layoutUpdates.keys) { @@ -910,8 +941,10 @@ class DataManager { switch (updateType) { case UpdateType.delete: - DebugLogger.instance - .printInfo('DIFF: Deleting layout [$layoutID]...', name: name); + DebugLogger.instance.printInfo( + 'DIFF: Deleting layout [$layoutID]...', + name: name, + ); localModel.layouts.remove(layoutID); localDataRepository.deletePublishLayout( layoutID: layoutID, @@ -919,14 +952,16 @@ class DataManager { ); case UpdateType.add: case UpdateType.update: - DebugLogger.instance - .printInfo('DIFF: Downloading layout [$layoutID]...', name: name); - final SDKPublishLayout? layout = - await networkDataRepository.downloadLayoutModel( - projectID: authManager.authData!.projectId, - layoutID: layoutID, - source: config.publishSource, + DebugLogger.instance.printInfo( + 'DIFF: Downloading layout [$layoutID]...', + name: name, ); + final SDKPublishLayout? layout = await networkDataRepository + .downloadLayoutModel( + projectID: authManager.authData!.projectId, + layoutID: layoutID, + source: config.publishSource, + ); if (layout != null) { localModel.layouts[layoutID] = layout; } @@ -938,8 +973,10 @@ class DataManager { switch (updateType) { case UpdateType.delete: - DebugLogger.instance - .printInfo('DIFF: Deleting font [$fontID]...', name: name); + DebugLogger.instance.printInfo( + 'DIFF: Deleting font [$fontID]...', + name: name, + ); localModel.fonts.remove(fontID); localDataRepository.deleteFontBytes( fontID: fontID, @@ -947,14 +984,16 @@ class DataManager { ); case UpdateType.add: case UpdateType.update: - DebugLogger.instance - .printInfo('DIFF: Downloading font [$fontID]...', name: name); - final SDKPublishFont? font = - await networkDataRepository.downloadFontModel( - projectID: authManager.authData!.projectId, - fontID: fontID, - source: config.publishSource, + DebugLogger.instance.printInfo( + 'DIFF: Downloading font [$fontID]...', + name: name, ); + final SDKPublishFont? font = await networkDataRepository + .downloadFontModel( + projectID: authManager.authData!.projectId, + fontID: fontID, + source: config.publishSource, + ); if (font != null) { localModel.fonts[fontID] = font; @@ -968,8 +1007,10 @@ class DataManager { switch (updateType) { case UpdateType.delete: - DebugLogger.instance - .printInfo('DIFF: Deleting api [$apiId]...', name: name); + DebugLogger.instance.printInfo( + 'DIFF: Deleting api [$apiId]...', + name: name, + ); localModel.apis.remove(apiId); localDataRepository.deletePublishApi( apiId: apiId, @@ -977,8 +1018,10 @@ class DataManager { ); case UpdateType.add: case UpdateType.update: - DebugLogger.instance - .printInfo('DIFF: Downloading api [$apiId]...', name: name); + DebugLogger.instance.printInfo( + 'DIFF: Downloading api [$apiId]...', + name: name, + ); final HttpApiData? api = await networkDataRepository.downloadApi( projectID: authManager.authData!.projectId, apiId: apiId, @@ -996,8 +1039,9 @@ class DataManager { switch (updateType) { case UpdateType.delete: DebugLogger.instance.printInfo( - 'DIFF: Deleting variables for layout [$layoutId]...', - name: name); + 'DIFF: Deleting variables for layout [$layoutId]...', + name: name, + ); localModel.variables.remove(layoutId); localDataRepository.deletePublishVariables( layoutID: layoutId, @@ -1006,14 +1050,15 @@ class DataManager { case UpdateType.add: case UpdateType.update: DebugLogger.instance.printInfo( - 'DIFF: Downloading variables for layout [$layoutId]...', - name: name); + 'DIFF: Downloading variables for layout [$layoutId]...', + name: name, + ); final SDKLayoutVariables? layoutVariables = await networkDataRepository.downloadLayoutVariables( - projectID: authManager.authData!.projectId, - layoutID: layoutId, - source: config.publishSource, - ); + projectID: authManager.authData!.projectId, + layoutID: layoutId, + source: config.publishSource, + ); if (layoutVariables != null) { localModel.variables[layoutId] = layoutVariables; } @@ -1026,8 +1071,9 @@ class DataManager { switch (updateType) { case UpdateType.delete: DebugLogger.instance.printInfo( - 'DIFF: Deleting conditions for layout [$layoutID]...', - name: name); + 'DIFF: Deleting conditions for layout [$layoutID]...', + name: name, + ); localModel.variables.remove(layoutID); localDataRepository.deletePublishConditions( layoutID: layoutID, @@ -1036,22 +1082,25 @@ class DataManager { case UpdateType.add: case UpdateType.update: DebugLogger.instance.printInfo( - 'DIFF: Downloading conditions for layout [$layoutID]...', - name: name); + 'DIFF: Downloading conditions for layout [$layoutID]...', + name: name, + ); final SDKLayoutConditions? layoutConditions = await networkDataRepository.downloadLayoutConditions( - projectID: authManager.authData!.projectId, - layoutID: layoutID, - source: config.publishSource, - ); + projectID: authManager.authData!.projectId, + layoutID: layoutID, + source: config.publishSource, + ); if (layoutConditions != null) { localModel.conditions[layoutID] = layoutConditions; } } } - DebugLogger.instance - .printInfo('DIFF: Done processing updates.', name: name); + DebugLogger.instance.printInfo( + 'DIFF: Done processing updates.', + name: name, + ); _publishModel = localModel.copyWith( updates: serverModel.updates, entryPageId: serverModel.entryPageId, @@ -1262,13 +1311,15 @@ class DataManager { }) async { if (_publishModel != null && queuingDone) { DebugLogger.instance.printInfo( - '[queueLayout] No longer queuing. Downloading layout [$layoutID] immediately...', - name: name); + '[queueLayout] No longer queuing. Downloading layout [$layoutID] immediately...', + name: name, + ); try { await getOrFetchPopulatedLayout(layoutID: layoutID); DebugLogger.instance.printInfo( - '[queueLayout] Layout [$layoutID] download complete.', - name: name); + '[queueLayout] Layout [$layoutID] download complete.', + name: name, + ); } catch (e, str) { DebugLogger.instance.log( '[queueLayout] Layout [$layoutID] failed to download immediately.\nError: $e', @@ -1289,32 +1340,37 @@ class DataManager { if (prioritize) { if (_downloadQueue.first == layoutID) { DebugLogger.instance.printInfo( - '[queueLayout] Layout [$layoutID] is already at the front of the queue. Skipping.', - name: name); + '[queueLayout] Layout [$layoutID] is already at the front of the queue. Skipping.', + name: name, + ); return; } else { DebugLogger.instance.printInfo( - '[queueLayout] Layout [$layoutID] is already in the queue. Moving it to the front to prioritize it.', - name: name); + '[queueLayout] Layout [$layoutID] is already in the queue. Moving it to the front to prioritize it.', + name: name, + ); _downloadQueue.remove(layoutID); } } else { DebugLogger.instance.printInfo( - '[queueLayout] Layout [$layoutID] is already in the queue. Skipping.', - name: name); + '[queueLayout] Layout [$layoutID] is already in the queue. Skipping.', + name: name, + ); return; } } if (prioritize) { DebugLogger.instance.printInfo( - '[queueLayout] Prioritizing this layout. Inserting [$layoutID] to the front of the queue.', - name: name); + '[queueLayout] Prioritizing this layout. Inserting [$layoutID] to the front of the queue.', + name: name, + ); _downloadQueue.insert(0, layoutID); } else { DebugLogger.instance.printInfo( - '[queueLayout] Adding [$layoutID] to the back of the queue.', - name: name); + '[queueLayout] Adding [$layoutID] to the back of the queue.', + name: name, + ); _downloadQueue.add(layoutID); } } @@ -1342,9 +1398,7 @@ class DataManager { /// /// Will return `true` if the layout and its associated fonts were fetched /// successfully, `false` otherwise. - Future getOrFetchPopulatedLayout({ - required String layoutID, - }) async { + Future getOrFetchPopulatedLayout({required String layoutID}) async { DebugLogger.instance.printFunction( 'getOrFetchPopulatedLayout(layoutID: $layoutID)', name: name, @@ -1352,10 +1406,7 @@ class DataManager { final SDKPublishModel? model = _publishModel; final AuthData? auth = authManager.authData; - assert( - model != null, - 'Data manager has not been initialized yet.', - ); + assert(model != null, 'Data manager has not been initialized yet.'); // If the user is not authenticated, then they get whatever is cached and // nothing else. @@ -1369,13 +1420,15 @@ class DataManager { SDKPublishLayout? layout; if (model!.layouts.containsKey(layoutID)) { DebugLogger.instance.printInfo( - '\tLayout [$layoutID] is already cached. Skipping download.', - name: name); + '\tLayout [$layoutID] is already cached. Skipping download.', + name: name, + ); layout = model.layouts[layoutID]; } else { DebugLogger.instance.printInfo( - '\tLayout [$layoutID] is not cached. Downloading...', - name: name); + '\tLayout [$layoutID] is not cached. Downloading...', + name: name, + ); layout = await networkDataRepository.downloadLayoutModel( layoutID: layoutID, projectID: auth.projectId, @@ -1383,13 +1436,15 @@ class DataManager { ); if (layout == null) { DebugLogger.instance.printInfo( - '\tLayout [$layoutID] could not be downloaded.', - name: name); + '\tLayout [$layoutID] could not be downloaded.', + name: name, + ); return false; } else { DebugLogger.instance.printInfo( - '\tLayout [$layoutID] downloaded successfully.', - name: name); + '\tLayout [$layoutID] downloaded successfully.', + name: name, + ); } model.layouts[layoutID] = layout; @@ -1400,43 +1455,50 @@ class DataManager { // Process APIs if (model.updates.layoutApis.containsKey(layoutID)) { DebugLogger.instance.printInfo( - '\tLayout [$layoutID] has ${model.updates.layoutApis[layoutID]!.length} apis.', - name: name); + '\tLayout [$layoutID] has ${model.updates.layoutApis[layoutID]!.length} apis.', + name: name, + ); final Set> apiModels = getOrFetchApis( apiIds: model.updates.layoutApis[layoutID]!, ); - final List results = - await Future.wait(apiModels.map((future) async { - try { - return await future; - } catch (e) { - DebugLogger.instance.log( - 'Error while fetching apis.\nError: $e', - category: DebugCategory.error, - name: name, - level: Level.WARNING, - ); - return null; - } - })); + final List results = await Future.wait( + apiModels.map((future) async { + try { + return await future; + } catch (e) { + DebugLogger.instance.log( + 'Error while fetching apis.\nError: $e', + category: DebugCategory.error, + name: name, + level: Level.WARNING, + ); + return null; + } + }), + ); for (final api in results) { if (api == null) continue; model.apis[api.id] = api; } } else { - DebugLogger.instance - .printInfo('\tLayout [$layoutID] has no apis.', name: name); + DebugLogger.instance.printInfo( + '\tLayout [$layoutID] has no apis.', + name: name, + ); } // Process Variables if (model.updates.variables.containsKey(layoutID)) { - DebugLogger.instance - .printInfo('\tLayout [$layoutID] has variables.', name: name); + DebugLogger.instance.printInfo( + '\tLayout [$layoutID] has variables.', + name: name, + ); try { - final SDKLayoutVariables? variables = - await getOrFetchVariables(layoutID); + final SDKLayoutVariables? variables = await getOrFetchVariables( + layoutID, + ); if (variables != null) { model.variables[layoutID] = variables; } @@ -1449,17 +1511,22 @@ class DataManager { ); } } else { - DebugLogger.instance - .printInfo('Layout [$layoutID] has no variables.', name: name); + DebugLogger.instance.printInfo( + 'Layout [$layoutID] has no variables.', + name: name, + ); } // Process Conditions if (model.updates.conditions.containsKey(layoutID)) { - DebugLogger.instance - .printInfo('\tLayout [$layoutID] has conditions.', name: name); + DebugLogger.instance.printInfo( + '\tLayout [$layoutID] has conditions.', + name: name, + ); try { - final SDKLayoutConditions? conditions = - await getOrFetchConditions(layoutID); + final SDKLayoutConditions? conditions = await getOrFetchConditions( + layoutID, + ); if (conditions != null) { model.conditions[layoutID] = conditions; } @@ -1472,54 +1539,64 @@ class DataManager { ); } } else { - DebugLogger.instance - .printInfo('\tLayout [$layoutID] has no conditions.', name: name); + DebugLogger.instance.printInfo( + '\tLayout [$layoutID] has no conditions.', + name: name, + ); } await emitPublishModel(); savePublishModel(); - DebugLogger.instance - .printInfo('\tLayout [$layoutID] ready, time for fonts...', name: name); + DebugLogger.instance.printInfo( + '\tLayout [$layoutID] ready, time for fonts...', + name: name, + ); // Process Fonts if (model.updates.layoutFonts.containsKey(layoutID)) { DebugLogger.instance.printInfo( - '\tLayout [$layoutID] has ${model.updates.layoutFonts[layoutID]!.length} fonts.', - name: name); + '\tLayout [$layoutID] has ${model.updates.layoutFonts[layoutID]!.length} fonts.', + name: name, + ); // Download or load fonts in the background. - getOrFetchFontModels( - fontIDs: model.updates.layoutFonts[layoutID]!, - ).then((Set fontModels) async { - DebugLogger.instance.printInfo( - '\tFound ${fontModels.length} fonts to fetch for layout [$layoutID].', - name: name); - - for (final SDKPublishFont fontModel in fontModels) { - DebugLogger.instance.printInfo( - '\t\tFontModel [${fontModel.id}] ready. Fetching bytes & loading...', - name: name); - - model.fonts[fontModel.id] = fontModel; - - await getOrFetchFontBytesAndSaveAndLoad(fontModel).then((_) { + getOrFetchFontModels(fontIDs: model.updates.layoutFonts[layoutID]!) + .then((Set fontModels) async { DebugLogger.instance.printInfo( - '\t\tFontModel [${fontModel.id}] loaded. Done!\n', - name: name); + '\tFound ${fontModels.length} fonts to fetch for layout [$layoutID].', + name: name, + ); + + for (final SDKPublishFont fontModel in fontModels) { + DebugLogger.instance.printInfo( + '\t\tFontModel [${fontModel.id}] ready. Fetching bytes & loading...', + name: name, + ); + + model.fonts[fontModel.id] = fontModel; + + await getOrFetchFontBytesAndSaveAndLoad(fontModel).then((_) { + DebugLogger.instance.printInfo( + '\t\tFontModel [${fontModel.id}] loaded. Done!\n', + name: name, + ); + }); + } + }) + .catchError((error, stack) { + DebugLogger.instance.log( + 'Error while fetching fonts.\nError: $error', + category: DebugCategory.error, + name: name, + level: Level.WARNING, + ); }); - } - }).catchError((error, stack) { - DebugLogger.instance.log( - 'Error while fetching fonts.\nError: $error', - category: DebugCategory.error, - name: name, - level: Level.WARNING, - ); - }); } else { - DebugLogger.instance - .printInfo('\tLayout [$layoutID] has no fonts.', name: name); + DebugLogger.instance.printInfo( + '\tLayout [$layoutID] has no fonts.', + name: name, + ); } StatTracker.instance.track(StatType.populatedLayoutDownload, layoutID); @@ -1554,14 +1631,16 @@ class DataManager { } finally { stopwatch.stop(); DebugLogger.instance.printInfo( - 'Publish bundle download stopwatch done in ${stopwatch.elapsedMilliseconds}ms or ${stopwatch.elapsed.inSeconds}s.', - name: name); + 'Publish bundle download stopwatch done in ${stopwatch.elapsedMilliseconds}ms or ${stopwatch.elapsed.inSeconds}s.', + name: name, + ); } if (model != null) { DebugLogger.instance.printInfo( - 'Successfully downloaded complete publish bundle. Emitting it.', - name: name); + 'Successfully downloaded complete publish bundle. Emitting it.', + name: name, + ); _publishModel = model; await emitPublishModel(); savePublishModel(); @@ -1569,8 +1648,9 @@ class DataManager { } DebugLogger.instance.printInfo( - 'Failed to download complete publish bundle in ${stopwatch.elapsedMilliseconds}ms or ${stopwatch.elapsed.inSeconds}s.', - name: name); + 'Failed to download complete publish bundle in ${stopwatch.elapsedMilliseconds}ms or ${stopwatch.elapsed.inSeconds}s.', + name: name, + ); return false; } @@ -1586,16 +1666,18 @@ class DataManager { /// Loads a [font] with its associated [fontBytes] into the Flutter engine. Future loadFont(SDKPublishFont font, Uint8List fontBytes) async { DebugLogger.instance.printInfo( - '[FLUTTER] Loading font [${font.id}](${font.fullFontName}) into Flutter framework.', - name: name); + '[FLUTTER] Loading font [${font.id}](${font.fullFontName}) into Flutter framework.', + name: name, + ); final FontLoader fontLoader = FontLoader(font.fullFontName); - fontLoader.addFont(Future.value(ByteData.view(fontBytes.buffer))); + fontLoader.addFont(Future.value(ByteData.sublistView(fontBytes))); return fontLoader.load().then((_) { DebugLogger.instance.printInfo( - '[FLUTTER] Successfully loaded font [${font.id}](${font.fullFontName}) into Flutter framework.', - name: name); + '[FLUTTER] Successfully loaded font [${font.id}](${font.fullFontName}) into Flutter framework.', + name: name, + ); }); } @@ -1603,34 +1685,38 @@ class DataManager { /// them. Future getOrFetchFontBytesAndSave(SDKPublishFont font) async { DebugLogger.instance.printInfo( - '\t\tChecking bytes for [${font.id}](${font.fullFontName}).', - name: name); + '\t\tChecking bytes for [${font.id}](${font.fullFontName}).', + name: name, + ); final Uint8List? fontBytes = localDataRepository.fetchFontBytes( fontID: font.id, source: config.publishSource, ); if (fontBytes != null) { - DebugLogger.instance - .printInfo('\t\tFont [${font.id}] bytes already cached.', name: name); + DebugLogger.instance.printInfo( + '\t\tFont [${font.id}] bytes already cached.', + name: name, + ); return Future.value(fontBytes); } else { DebugLogger.instance.printInfo( - '\t\tFont [${font.id}] bytes not cached. Downloading...', - name: name); + '\t\tFont [${font.id}] bytes not cached. Downloading...', + name: name, + ); return downloadFontBytesAndSave(font); } } /// Downloads the bytes of a [font] and saves them to cache. Future downloadFontBytesAndSave(SDKPublishFont font) async => - networkDataRepository.downloadFontBytes(url: font.url).then( - (Uint8List? fontBytes) { - // Save the font bytes to cache. - if (fontBytes == null) return null; - saveFontBytes(font, fontBytes); - return fontBytes; - }, - ); + networkDataRepository.downloadFontBytes(url: font.url).then(( + Uint8List? fontBytes, + ) { + // Save the font bytes to cache. + if (fontBytes == null) return null; + saveFontBytes(font, fontBytes); + return fontBytes; + }); /// Gets all [SDKPublishFont] models of a given set of [fontIDs] either from /// local cache or downloads them from the network. @@ -1648,19 +1734,20 @@ class DataManager { if (auth == null) continue; try { - final SDKPublishFont? downloadedFont = - await networkDataRepository.downloadFontModel( - projectID: auth.projectId, - fontID: fontID, - source: config.publishSource, - ); + final SDKPublishFont? downloadedFont = await networkDataRepository + .downloadFontModel( + projectID: auth.projectId, + fontID: fontID, + source: config.publishSource, + ); if (downloadedFont != null) { fonts.add(downloadedFont); } } catch (e) { DebugLogger.instance.printInfo( - '\t\tFont [$fontID] could not be downloaded.', - name: name); + '\t\tFont [$fontID] could not be downloaded.', + name: name, + ); } } } @@ -1669,9 +1756,7 @@ class DataManager { /// Gets all [SDKPublishFont] models of a given set of [fontIDs] either from /// local cache or downloads them from the network. - Set> getOrFetchApis({ - required Set apiIds, - }) { + Set> getOrFetchApis({required Set apiIds}) { final AuthData? auth = authManager.authData; final Set> apis = {}; @@ -1682,12 +1767,12 @@ class DataManager { } else { if (auth == null) continue; - final Future apiFuture = - networkDataRepository.downloadApi( - projectID: auth.projectId, - apiId: apiId, - source: config.publishSource, - ); + final Future apiFuture = networkDataRepository + .downloadApi( + projectID: auth.projectId, + apiId: apiId, + source: config.publishSource, + ); apis.add(apiFuture); } } diff --git a/lib/src/data/firebase_data_repository.dart b/lib/src/data/firebase_data_repository.dart index 5af8079e..6c46ceef 100644 --- a/lib/src/data/firebase_data_repository.dart +++ b/lib/src/data/firebase_data_repository.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:cloud_firestore/cloud_firestore.dart'; import '../../codelessly_sdk.dart'; -import '../logging/error_logger.dart'; import '../logging/stat_tracker.dart'; /// Handles the data flow of [SDKPublishModel] from the server. @@ -13,18 +12,16 @@ class FirebaseDataRepository extends NetworkDataRepository { /// Creates a new [FirebaseDataRepository] instance with the given [firestore] /// instance. - FirebaseDataRepository({ - required this.firestore, - required super.config, - }); + FirebaseDataRepository({required this.firestore, required super.config}); @override Stream streamPublishModel({ required String projectID, required PublishSource source, }) { - final publishModelDoc = - firestore.collection(source.serverPath).doc(projectID); + final publishModelDoc = firestore + .collection(source.serverPath) + .doc(projectID); return publishModelDoc.snapshots().map((event) { StatTracker.instance.track( @@ -43,9 +40,10 @@ class FirebaseDataRepository extends NetworkDataRepository { ); return null; } - final SDKPublishModel model = SDKPublishModel.fromJson( - {...data, 'id': event.id}, - ); + final SDKPublishModel model = SDKPublishModel.fromJson({ + ...data, + 'id': event.id, + }); return model; }); } @@ -90,9 +88,10 @@ class FirebaseDataRepository extends NetworkDataRepository { return null; } - final SDKPublishLayout layout = SDKPublishLayout.fromJson( - {...data, 'id': value.id}, - ); + final SDKPublishLayout layout = SDKPublishLayout.fromJson({ + ...data, + 'id': value.id, + }); return layout; }); } @@ -127,9 +126,10 @@ class FirebaseDataRepository extends NetworkDataRepository { return null; } - final SDKPublishFont font = SDKPublishFont.fromJson( - {...data, 'id': value.id}, - ); + final SDKPublishFont font = SDKPublishFont.fromJson({ + ...data, + 'id': value.id, + }); return font; }); } @@ -164,9 +164,7 @@ class FirebaseDataRepository extends NetworkDataRepository { return null; } - final HttpApiData api = HttpApiData.fromJson( - {...data, 'id': value.id}, - ); + final HttpApiData api = HttpApiData.fromJson({...data, 'id': value.id}); return api; }); } @@ -202,9 +200,10 @@ class FirebaseDataRepository extends NetworkDataRepository { return null; } - final SDKLayoutVariables layoutVariables = SDKLayoutVariables.fromJson( - {...data, 'id': value.id}, - ); + final SDKLayoutVariables layoutVariables = SDKLayoutVariables.fromJson({ + ...data, + 'id': value.id, + }); return layoutVariables; }); } diff --git a/lib/src/data/local_data_repository.dart b/lib/src/data/local_data_repository.dart index 43e7320f..9d042f55 100644 --- a/lib/src/data/local_data_repository.dart +++ b/lib/src/data/local_data_repository.dart @@ -17,9 +17,7 @@ class LocalDataRepository { LocalDataRepository({required this.cacheManager}); /// Returns the [SDKPublishModel] from the cache. - SDKPublishModel? fetchPublishModel({ - required PublishSource source, - }) { + SDKPublishModel? fetchPublishModel({required PublishSource source}) { try { return cacheManager.get( source.modelCacheKey, @@ -46,60 +44,46 @@ class LocalDataRepository { Future savePublishModel({ required SDKPublishModel model, required PublishSource source, - }) => - cacheManager.store( - source.modelCacheKey, - model.toFullJson(), - ); + }) => cacheManager.store(source.modelCacheKey, model.toFullJson()); /// Stores the given [bytes] of a [fontID] in the cache. Future saveFontBytes({ required String fontID, required Uint8List bytes, required PublishSource source, - }) => - cacheManager - .storeBytes( - source.fontsCacheKey, - fontID, - bytes, - ) - .catchError((e, str) { - debugPrintStack(label: '$e', stackTrace: str); - }); + }) => cacheManager.storeBytes(source.fontsCacheKey, fontID, bytes).catchError( + (e, str) { + debugPrintStack(label: '$e', stackTrace: str); + }, + ); /// Deletes a given layout associated with a [layoutID] from the cache. void deletePublishLayout({ required String layoutID, required PublishSource source, - }) => - cacheManager.delete(source.modelCacheKey); + }) => cacheManager.delete(source.modelCacheKey); /// Deletes a given api associated with a [apiId] from the cache. void deletePublishApi({ required String apiId, required PublishSource source, - }) => - cacheManager.delete(source.apisCacheKey); + }) => cacheManager.delete(source.apisCacheKey); /// Deletes a given api associated with a [apiId] from the cache. void deletePublishVariables({ required String layoutID, required PublishSource source, - }) => - cacheManager.delete(source.variablesCacheKey); + }) => cacheManager.delete(source.variablesCacheKey); /// Deletes a given api associated with a [apiId] from the cache. void deletePublishConditions({ required String layoutID, required PublishSource source, - }) => - cacheManager.delete(source.conditionsCacheKey); + }) => cacheManager.delete(source.conditionsCacheKey); /// Deletes the stored bytes of a given [fontID] from the cache. void deleteFontBytes({ required String fontID, required PublishSource source, - }) => - cacheManager.deleteBytes(source.fontsCacheKey, fontID); + }) => cacheManager.deleteBytes(source.fontsCacheKey, fontID); } diff --git a/lib/src/data/local_database.dart b/lib/src/data/local_database.dart index 6e59f193..8a184158 100644 --- a/lib/src/data/local_database.dart +++ b/lib/src/data/local_database.dart @@ -1,10 +1,11 @@ import 'dart:async'; -import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:hive_ce_flutter/hive_flutter.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../logging/debug_logger.dart'; +import '../logging/error_logger.dart'; /// Allows access to local storage on the device. Implementations of this class /// should be able to store and retrieve data from the device's local storage. @@ -145,13 +146,23 @@ class HiveLocalDatabase extends LocalDatabase { } @override - void dispose() async { + void dispose() { DebugLogger.instance.printFunction('dispose()', name: LocalDatabase.name); // Cancel the subscription to the storage updates. _subscription?.cancel(); - // Close the box itself. - await _box.close(); + // Close the box itself. ChangeNotifier.dispose is synchronous, so report + // close failures instead of leaving them unhandled. + unawaited( + _box.close().catchError((Object e, StackTrace stackTrace) { + ErrorLogger.instance.captureException( + e, + message: 'Failed to close local database', + type: 'local_database_close_failed', + stackTrace: stackTrace, + ); + }), + ); super.dispose(); } @@ -205,9 +216,9 @@ class SharedPreferencesLocalDatabase extends LocalDatabase { .getKeys() .where((key) => key.startsWith(_prefix)) .fold>({}, (map, key) { - map[key.substring(_prefix.length)] = _prefsInstance!.get(key); - return map; - }); + map[key.substring(_prefix.length)] = _prefsInstance!.get(key); + return map; + }); } @override @@ -231,7 +242,7 @@ class SharedPreferencesLocalDatabase extends LocalDatabase { } else { throw ArgumentError('Unsupported value type: ${value.runtimeType}'); } - + // Notify listeners after value changes notifyListeners(); _notifiers[key]?.notify(); @@ -264,9 +275,9 @@ class SharedPreferencesLocalDatabase extends LocalDatabase { @override Listenable getNotifier(String? key) { if (key == null) return this; - + if (_notifiers.containsKey(key)) return _notifiers[key]!; - + final notifier = _StorageListenable._(key); _notifiers[key] = notifier; return notifier; diff --git a/lib/src/data/network_data_repository.dart b/lib/src/data/network_data_repository.dart index 2abba2fa..c2ea86bf 100644 --- a/lib/src/data/network_data_repository.dart +++ b/lib/src/data/network_data_repository.dart @@ -6,8 +6,6 @@ import 'package:http/http.dart' as http; import 'package:logging/logging.dart'; import '../../codelessly_sdk.dart'; -import '../logging/debug_logger.dart'; -import '../logging/error_logger.dart'; import '../logging/stat_tracker.dart'; /// An abstract class that represents the operations that a [DataManager] will @@ -20,9 +18,7 @@ abstract class NetworkDataRepository { final CodelesslyConfig config; /// Creates a new instance of [NetworkDataRepository]. - NetworkDataRepository({ - required this.config, - }); + NetworkDataRepository({required this.config}); /// Calls a cloud function that searches for the project associated with the /// given unique slug and returns a completely populated [SDKPublishModel] @@ -44,10 +40,14 @@ abstract class NetworkDataRepository { final http.Response result = await http.get(Uri.parse(url)); if (result.statusCode != 200) { - DebugLogger.instance - .printInfo('Error downloading publish bundle.', name: name); - DebugLogger.instance - .printInfo('Status code: ${result.statusCode}', name: name); + DebugLogger.instance.printInfo( + 'Error downloading publish bundle.', + name: name, + ); + DebugLogger.instance.printInfo( + 'Status code: ${result.statusCode}', + name: name, + ); DebugLogger.instance.printInfo('Message: ${result.body}', name: name); ErrorLogger.instance.captureException( @@ -61,14 +61,16 @@ abstract class NetworkDataRepository { StatTracker.instance.track(StatType.bundleDownload); - final Map modelDoc = - jsonDecode(utf8.decode(result.bodyBytes)); + final Map modelDoc = jsonDecode( + utf8.decode(result.bodyBytes), + ); final SDKPublishModel model = SDKPublishModel.fromJson(modelDoc); DebugLogger.instance.printInfo( - 'Finished downloading publish bundle with slug: $slug and source: $source.', - name: name); + 'Finished downloading publish bundle with slug: $slug and source: $source.', + name: name, + ); return model; } catch (e, str) { DebugLogger.instance.log( @@ -141,15 +143,10 @@ abstract class NetworkDataRepository { required String projectID, required Set fontIDs, required PublishSource source, - }) => - { - for (final fontID in fontIDs) - downloadFontModel( - projectID: projectID, - fontID: fontID, - source: source, - ) - }; + }) => { + for (final fontID in fontIDs) + downloadFontModel(projectID: projectID, fontID: fontID, source: source), + }; /// Fetches the relevant [SDKPublishLayout]s from the server based on the /// configurations of the implementation. @@ -157,20 +154,21 @@ abstract class NetworkDataRepository { required String projectID, required Set layoutIDs, required PublishSource source, - }) => - { - for (final layoutID in layoutIDs) - downloadLayoutModel( - projectID: projectID, - layoutID: layoutID, - source: source, - ) - }; + }) => { + for (final layoutID in layoutIDs) + downloadLayoutModel( + projectID: projectID, + layoutID: layoutID, + source: source, + ), + }; /// Downloads the bytes of a font from the server given a [url]. Future downloadFontBytes({required String url}) async { - DebugLogger.instance - .printFunction('downloadFontBytes(url: $url)', name: name); + DebugLogger.instance.printFunction( + 'downloadFontBytes(url: $url)', + name: name, + ); try { final http.Response response = await http.get(Uri.parse(url)); if (response.statusCode != 200) return null; diff --git a/lib/src/data/web_data_repository.dart b/lib/src/data/web_data_repository.dart index 209e8f5d..4610db7d 100644 --- a/lib/src/data/web_data_repository.dart +++ b/lib/src/data/web_data_repository.dart @@ -4,7 +4,6 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import '../../codelessly_sdk.dart'; -import '../logging/error_logger.dart'; /// A [NetworkDataRepository] implementation that utilizes the Firebase Cloud /// Functions to retrieve the relevant data. @@ -13,10 +12,7 @@ class WebDataRepository extends NetworkDataRepository { final http.Client client; /// Creates a [WebDataRepository] instance. - WebDataRepository({ - required super.config, - required this.client, - }); + WebDataRepository({required super.config, required this.client}); @override Stream streamPublishModel({ @@ -26,12 +22,10 @@ class WebDataRepository extends NetworkDataRepository { try { final http.Response result = await client.post( Uri.parse( - '${config.firebaseCloudFunctionsBaseURL}/api/getPublishModelRequest'), + '${config.firebaseCloudFunctionsBaseURL}/api/getPublishModelRequest', + ), headers: {'Content-Type': 'application/json'}, - body: jsonEncode({ - 'projectID': projectID, - 'source': source.serverPath, - }), + body: jsonEncode({'projectID': projectID, 'source': source.serverPath}), ); if (result.statusCode != 200) { @@ -68,7 +62,8 @@ class WebDataRepository extends NetworkDataRepository { try { final http.Response result = await client.post( Uri.parse( - '${config.firebaseCloudFunctionsBaseURL}/api/getLayoutModelRequest'), + '${config.firebaseCloudFunctionsBaseURL}/api/getLayoutModelRequest', + ), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'projectID': projectID, @@ -114,7 +109,8 @@ class WebDataRepository extends NetworkDataRepository { try { final http.Response result = await client.post( Uri.parse( - '${config.firebaseCloudFunctionsBaseURL}/api/getFontModelRequest'), + '${config.firebaseCloudFunctionsBaseURL}/api/getFontModelRequest', + ), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'projectID': projectID, @@ -157,7 +153,8 @@ class WebDataRepository extends NetworkDataRepository { try { final http.Response result = await client.post( Uri.parse( - '${config.firebaseCloudFunctionsBaseURL}/api/getPublishedApiRequest'), + '${config.firebaseCloudFunctionsBaseURL}/api/getPublishedApiRequest', + ), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'projectID': projectID, @@ -200,7 +197,8 @@ class WebDataRepository extends NetworkDataRepository { try { final http.Response result = await client.post( Uri.parse( - '${config.firebaseCloudFunctionsBaseURL}/api/getPublishedLayoutVariablesRequest'), + '${config.firebaseCloudFunctionsBaseURL}/api/getPublishedLayoutVariablesRequest', + ), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'projectID': projectID, @@ -221,8 +219,10 @@ class WebDataRepository extends NetworkDataRepository { } final Map modelDoc = jsonDecode(result.body); - final SDKLayoutVariables variables = - SDKLayoutVariables.fromJson({...modelDoc, 'id': layoutID}); + final SDKLayoutVariables variables = SDKLayoutVariables.fromJson({ + ...modelDoc, + 'id': layoutID, + }); return variables; } catch (e, str) { @@ -246,7 +246,8 @@ class WebDataRepository extends NetworkDataRepository { try { final http.Response result = await client.post( Uri.parse( - '${config.firebaseCloudFunctionsBaseURL}/api/getPublishedLayoutConditionsRequest'), + '${config.firebaseCloudFunctionsBaseURL}/api/getPublishedLayoutConditionsRequest', + ), headers: {'Content-Type': 'application/json'}, body: jsonEncode({ 'projectID': projectID, @@ -267,8 +268,10 @@ class WebDataRepository extends NetworkDataRepository { } final Map modelDoc = jsonDecode(result.body); - final SDKLayoutConditions conditions = - SDKLayoutConditions.fromJson({...modelDoc, 'id': layoutID}); + final SDKLayoutConditions conditions = SDKLayoutConditions.fromJson({ + ...modelDoc, + 'id': layoutID, + }); return conditions; } catch (e, str) { diff --git a/lib/src/firebase_options_prod.dart b/lib/src/firebase_options_prod.dart index 3bd53948..e65dbc55 100644 --- a/lib/src/firebase_options_prod.dart +++ b/lib/src/firebase_options_prod.dart @@ -56,8 +56,7 @@ class DefaultFirebaseOptionsProd { projectId: 'codeless-app', databaseURL: 'https://codeless-app-default-rtdb.firebaseio.com', storageBucket: 'codeless-app.appspot.com', - iosClientId: - '1008464357540-quq84tmv8e0b7qo3jo2r3148pd9snfni.apps.googleusercontent.com', + iosClientId: '1008464357540-quq84tmv8e0b7qo3jo2r3148pd9snfni.apps.googleusercontent.com', iosBundleId: 'com.codelessly', ); @@ -68,8 +67,7 @@ class DefaultFirebaseOptionsProd { projectId: 'codeless-app', databaseURL: 'https://codeless-app-default-rtdb.firebaseio.com', storageBucket: 'codeless-app.appspot.com', - iosClientId: - '1008464357540-quq84tmv8e0b7qo3jo2r3148pd9snfni.apps.googleusercontent.com', + iosClientId: '1008464357540-quq84tmv8e0b7qo3jo2r3148pd9snfni.apps.googleusercontent.com', iosBundleId: 'com.codelessly', ); diff --git a/lib/src/functions/functions_repository.dart b/lib/src/functions/functions_repository.dart index 8ec65101..98d99936 100644 --- a/lib/src/functions/functions_repository.dart +++ b/lib/src/functions/functions_repository.dart @@ -13,7 +13,6 @@ import 'package:url_launcher/url_launcher.dart'; import '../../codelessly_sdk.dart'; import '../logging/stat_tracker.dart'; import '../ui/codelessly_dialog_widget.dart'; -import '../logging/debug_logger.dart'; /// Enum representing the types of API requests. enum ApiRequestType { @@ -96,10 +95,15 @@ class FunctionsRepository { setVariant(context, action as SetVariantAction, notify: notify); return true; case ActionType.setVariable: - return setVariableFromAction(context, action as SetVariableAction, - notify: notify); + return setVariableFromAction( + context, + action as SetVariableAction, + notify: notify, + ); case ActionType.callFunction: - callFunction(context, action as CallFunctionAction); + final result = callFunction(context, action as CallFunctionAction); + // Blocking actions wait for the app's function to finish. + if (result is Future) return result.then((_) => true); return true; case ActionType.callApi: return makeApiRequestFromAction(action as ApiCallAction, context); @@ -107,10 +111,14 @@ class FunctionsRepository { return setStorageFromAction(context, action as SetStorageAction); case ActionType.setCloudStorage: return setCloudDatabaseFromAction( - context, action as SetCloudStorageAction); + context, + action as SetCloudStorageAction, + ); case ActionType.loadFromCloudStorage: return loadFromStorageAction( - context, action as LoadFromCloudStorageAction); + context, + action as LoadFromCloudStorageAction, + ); } } @@ -119,8 +127,11 @@ class FunctionsRepository { BuildContext context, [ Observable? variable, ]) { - final Map apis = - context.read().dataManager.publishModel!.apis; + final Map apis = context + .read() + .dataManager + .publishModel! + .apis; final HttpApiData? apiData = apis[action.apiId]; @@ -154,8 +165,9 @@ class FunctionsRepository { } // fetch default value from api data. final param = apiData.variables.findByNameOrNull(name); - final paramValue = - param != null && param.value.isNotEmpty ? param.value : ''; + final paramValue = param != null && param.value.isNotEmpty + ? param.value + : ''; params[name] = paramValue; } @@ -173,15 +185,21 @@ class FunctionsRepository { ); } - static Map _generateMapFromPairs(List pairs, - Map parameters, ScopedValues scopedValues) { + static Map _generateMapFromPairs( + List pairs, + Map parameters, + ScopedValues scopedValues, + ) { return pairs .where((pair) => pair.isUsed && pair.key.isNotEmpty) .toList() .asMap() - .map((key, pair) => MapEntry( + .map( + (key, pair) => MapEntry( _applyApiInputs(pair.key, parameters, scopedValues), - _applyApiInputs(pair.value, parameters, scopedValues))); + _applyApiInputs(pair.value, parameters, scopedValues), + ), + ); } static String _applyApiInputs( @@ -244,8 +262,10 @@ class FunctionsRepository { }) async { final ScopedValues effectiveScopedValues = scopedValues ?? ScopedValues.of(context); - final parsedParams = - substituteVariablesInMap(action.params, effectiveScopedValues); + final parsedParams = substituteVariablesInMap( + action.params, + effectiveScopedValues, + ); DebugLogger.instance.printInfo( 'Performing navigation action with params: $parsedParams', @@ -261,11 +281,16 @@ class FunctionsRepository { await Navigator.maybePop(context, parsedParams); } else { // Check if a layout exists for the action's [destinationId]. - final String? layoutId = effectiveCodelessly - .dataManager.publishModel?.layouts[action.destinationId]?.id ?? + final String? layoutId = + effectiveCodelessly + .dataManager + .publishModel + ?.layouts[action.destinationId] + ?.id ?? effectiveCodelessly.dataManager.publishModel?.layouts.values .firstWhereOrNull( - (layout) => layout.canvasIds.contains(action.destinationId)) + (layout) => layout.canvasIds.contains(action.destinationId), + ) ?.id; DebugLogger.instance.printInfo( @@ -303,9 +328,8 @@ class FunctionsRepository { context, MaterialPageRoute( settings: RouteSettings(arguments: parsedParams), - builder: (context) => CodelesslyWidget( - controller: effectiveController, - ), + builder: (context) => + CodelesslyWidget(controller: effectiveController), ), ); // Notify navigation listeners that this screen has been navigated back @@ -323,9 +347,8 @@ class FunctionsRepository { context, MaterialPageRoute( settings: RouteSettings(arguments: parsedParams), - builder: (context) => CodelesslyWidget( - controller: effectiveController, - ), + builder: (context) => + CodelesslyWidget(controller: effectiveController), ), ); @@ -362,7 +385,8 @@ class FunctionsRepository { // Check if a layout exists for the action's [destinationId]. final String? layoutId = codelessly.dataManager.publishModel?.layouts.values .firstWhereOrNull( - (layout) => layout.canvasIds.contains(action.destinationId)) + (layout) => layout.canvasIds.contains(action.destinationId), + ) ?.id; DebugLogger.instance.printInfo( @@ -395,10 +419,8 @@ class FunctionsRepository { routeSettings: RouteSettings(arguments: parsedParams), builder: (context) => CodelesslyDialogWidget( showCloseButton: action.showCloseButton, - builder: (context) => CodelesslyWidget( - codelessly: codelessly, - layoutID: layoutId, - ), + builder: (context) => + CodelesslyWidget(codelessly: codelessly, layoutID: layoutId), ), ); if (context.mounted) { @@ -416,6 +438,12 @@ class FunctionsRepository { nullSubstitutionMode: NullSubstitutionMode.emptyString, scopedValues: ScopedValues.of(context), ).trim(); + openLink(url); + } + + /// Opens [url] with the platform, logging instead of throwing when it is + /// malformed or cannot be launched. + static void openLink(String url) { final Uri? uri = Uri.tryParse(url); if (uri == null || url.isEmpty) { DebugLogger.instance.printInfo( @@ -424,11 +452,19 @@ class FunctionsRepository { ); return; } - DebugLogger.instance.printInfo( - 'Launching URL: $uri', - name: name, + DebugLogger.instance.printInfo('Launching URL: $uri', name: name); + unawaited( + launchUrl(uri).then( + (_) {}, + onError: (Object e, StackTrace stackTrace) => + ErrorLogger.instance.captureException( + e, + message: 'Failed to launch $url', + type: 'launch_url_failed', + stackTrace: stackTrace, + ), + ), ); - launchUrl(uri); } static Future makeApiRequest({ @@ -440,8 +476,10 @@ class FunctionsRepository { bool useCloudFunctionForWeb = false, Observable? variable, }) async { - assert(variable == null || variable.value.type.isMap, - 'Provided variable for api call must be of type map. Found ${variable.value.type}'); + assert( + variable == null || variable.value.type.isMap, + 'Provided variable for api call must be of type map. Found ${variable.value.type}', + ); printApiDetails(method: method, url: url, headers: headers, body: body); @@ -493,11 +531,17 @@ class FunctionsRepository { final Uri uri = Uri.parse(url); response = switch (method) { HttpMethod.get => await client.get(uri, headers: headers), - HttpMethod.post => - await client.post(uri, headers: headers, body: body), - HttpMethod.delete => - await client.delete(uri, headers: headers, body: body), - HttpMethod.put => await client.put(uri, headers: headers, body: body) + HttpMethod.post => await client.post( + uri, + headers: headers, + body: body, + ), + HttpMethod.delete => await client.delete( + uri, + headers: headers, + body: body, + ), + HttpMethod.put => await client.put(uri, headers: headers, body: body), }; } @@ -520,21 +564,11 @@ class FunctionsRepository { return response; } catch (error, stackTrace) { - DebugLogger.instance.printInfo( - error.toString(), - name: name, - ); - DebugLogger.instance.printInfo( - stackTrace.toString(), - name: name, - ); + DebugLogger.instance.printInfo(error.toString(), name: name); + DebugLogger.instance.printInfo(stackTrace.toString(), name: name); if (variable != null) { variable.value = variable.value.copyWith( - value: ApiResponseVariableUtils.error( - url, - error, - data: existingData, - ), + value: ApiResponseVariableUtils.error(url, error, data: existingData), ); DebugLogger.instance.printInfo( '${variable.value.name} updated with error state.', @@ -561,18 +595,12 @@ class FunctionsRepository { '--------------------------------------------------------------------', name: name, ); - DebugLogger.instance.printInfo( - 'API Request:', - name: name, - ); + DebugLogger.instance.printInfo('API Request:', name: name); DebugLogger.instance.printInfo( '--------------------------------------------------------------------', name: name, ); - DebugLogger.instance.printInfo( - '${method.shortName} $url', - name: name, - ); + DebugLogger.instance.printInfo('${method.shortName} $url', name: name); DebugLogger.instance.printInfo( 'Headers: ${headers.isEmpty ? 'None' : ''}', name: name, @@ -582,10 +610,7 @@ class FunctionsRepository { const JsonEncoder.withIndent(' ').convert(headers), name: name, ); - DebugLogger.instance.printInfo( - '', - name: name, - ); + DebugLogger.instance.printInfo('', name: name); } DebugLogger.instance.printInfo( 'Body: ${body == null || body.toString().trim().isEmpty ? 'None' : ''}', @@ -599,10 +624,7 @@ class FunctionsRepository { name: name, ); } catch (e) { - DebugLogger.instance.printInfo( - body.toString(), - name: name, - ); + DebugLogger.instance.printInfo(body.toString(), name: name); } } DebugLogger.instance.printInfo( @@ -649,7 +671,9 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. } static Future submitToNewsletter( - BuildContext context, SubmitAction action) async { + BuildContext context, + SubmitAction action, + ) async { final CodelesslyContext payload = context.read(); // Get primary linked text field ID. final String primaryTextField = action.primaryTextField; @@ -657,7 +681,8 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. final List primaryFieldValues = payload.nodeValues[primaryTextField]?.value ?? []; // Email ID would be primary field's input value. - final String emailID = primaryFieldValues + final String emailID = + primaryFieldValues .firstWhereOrNull((value) => value.name == 'inputValue') ?.value .typedValue() ?? @@ -686,12 +711,14 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. final List lastNameFieldValues = payload.nodeValues[lastNameField]?.value ?? []; // Get first and last names. - final String firstName = firstNameFieldValues + final String firstName = + firstNameFieldValues .firstWhereOrNull((value) => value.name == 'inputValue') ?.value .typedValue() ?? ''; - final String lastName = lastNameFieldValues + final String lastName = + lastNameFieldValues .firstWhereOrNull((value) => value.name == 'inputValue') ?.value .typedValue() ?? @@ -709,15 +736,15 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. 'email_address': emailID, 'status': 'subscribed', 'merge_fields': mergeFields, - } - ] + }, + ], }; // Mailchimp's endpoint to submit data. final String url = 'https://${action.dataCenter}.api.mailchimp.com/3.0/lists/${action.listID}'; // Headers to authenticate request. final Map headers = { - 'Authorization': 'auth ${action.apiKey}' + 'Authorization': 'auth ${action.apiKey}', }; // Submit data to Mailchimp. return makeApiRequest( @@ -770,8 +797,8 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. V Function()? syncValue, bool notify = true, }) { - final CodelesslyContext codelesslyContext = - context.read(); + final CodelesslyContext codelesslyContext = context + .read(); final List values = codelesslyContext.nodeValues[action.nodeID]!.value; // Get new value. @@ -789,15 +816,18 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. } if (newValue != null) { // Get old value by name. - final ValueModel oldValue = - values.firstWhere((value) => value.name == newValue!.name); + final ValueModel oldValue = values.firstWhere( + (value) => value.name == newValue!.name, + ); // Replace old value with new. final List updateValues = [...values] ..remove(oldValue) ..add(newValue); // Update values list. - codelesslyContext.nodeValues[action.nodeID]! - .set(updateValues, notify: notify); + codelesslyContext.nodeValues[action.nodeID]!.set( + updateValues, + notify: notify, + ); } } @@ -810,11 +840,14 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. // Get node's values. final List values = payload.nodeValues[action.nodeID]!.value; // Get new variant value. - final ValueModel newValue = - StringValue(name: 'currentVariantId', value: action.variantID); + final ValueModel newValue = StringValue( + name: 'currentVariantId', + value: action.variantID, + ); // Get old value by name. - final ValueModel oldValue = - values.firstWhere((value) => value.name == newValue.name); + final ValueModel oldValue = values.firstWhere( + (value) => value.name == newValue.name, + ); // Replace old value with new. final List updateValues = [...values] ..remove(oldValue) @@ -832,10 +865,11 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. bool notify = true, }) { final ScopedValues scopedValues = ScopedValues.of(context); - final CodelesslyContext codelesslyContext = - context.read(); - final variableNotifier = - codelesslyContext.findVariableByName(action.variable.name); + final CodelesslyContext codelesslyContext = context + .read(); + final variableNotifier = codelesslyContext.findVariableByName( + action.variable.name, + ); if (variableNotifier == null) return false; String newValue = PropertyValueDelegate.substituteVariables( @@ -850,29 +884,39 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. VariableType.text => newValue, VariableType.image => newValue, VariableType.color => ColorRGBA.fromHex(newValue), - VariableType.integer => _performIntOperation(action.numberOperation, - currentValue?.toInt(), newValue, scopedValues), - VariableType.decimal => _performDecimalOperation(action.numberOperation, - currentValue?.toDouble(), newValue, scopedValues), - VariableType.boolean => action.toggled - ? !(bool.tryParse(currentValue.toString()) ?? false) - : bool.tryParse(newValue), + VariableType.integer => _performIntOperation( + action.numberOperation, + currentValue?.toInt(), + newValue, + scopedValues, + ), + VariableType.decimal => _performDecimalOperation( + action.numberOperation, + currentValue?.toDouble(), + newValue, + scopedValues, + ), + VariableType.boolean => + action.toggled + ? !(bool.tryParse(currentValue.toString()) ?? false) + : bool.tryParse(newValue), VariableType.list => _performListOperation( - action, - currentValue?.toList(), - newValue, - scopedValues, - ), + action, + currentValue?.toList(), + newValue, + scopedValues, + ), VariableType.map => _performMapOperation( - action, - currentValue?.toMap(), - newValue, - scopedValues, - ), + action, + currentValue?.toMap(), + newValue, + scopedValues, + ), }; - final VariableData updatedVariable = - variableNotifier.value.copyWith(value: updatedValue); + final VariableData updatedVariable = variableNotifier.value.copyWith( + value: updatedValue, + ); variableNotifier.set(updatedVariable, notify: notify); return true; @@ -915,8 +959,8 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. if (match == null) return false; - final ValueNotifier? propertyNotifier = - payload.findVariableByName(match.name); + final ValueNotifier? propertyNotifier = payload + .findVariableByName(match.name); if (propertyNotifier == null) return false; @@ -948,8 +992,9 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. if (payload.nodeValues[node.id] == null) return false; final List values = payload.nodeValues[node.id]?.value ?? []; - final ValueModel? valueModel = - values.firstWhereOrNull((val) => val.name == property); + final ValueModel? valueModel = values.firstWhereOrNull( + (val) => val.name == property, + ); if (valueModel == null) return false; @@ -979,12 +1024,7 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. property: property, value: value.toString(), ) || - setNodeValue( - context, - node: node, - property: property, - value: value, - ); + setNodeValue(context, node: node, property: property, value: value); } /// Triggers actions on given [node] or [reactions] with given [type]. @@ -998,50 +1038,96 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. dynamic value, List? reactions, }) async { - final filteredReactions = (reactions ?? node?.reactions ?? []) - .where((reaction) => reaction.trigger.type == type); + final filteredReactions = (reactions ?? node?.reactions ?? []).where( + (reaction) => reaction.trigger.type == type, + ); if (filteredReactions.isEmpty) return false; for (final reaction in filteredReactions) { if (!context.mounted) continue; - final future = FunctionsRepository.performAction( - context, - reaction.action, - internalValue: value, - ); + final FutureOr future; + try { + future = FunctionsRepository.performAction( + context, + reaction.action, + internalValue: value, + ); + } catch (e, stackTrace) { + _reportActionError(reaction, e, stackTrace); + return true; + } if (!reaction.action.nonBlocking) { // Await only if this action is not a non-blocking. It must not be - // awaited if it is non-blocking. - await future; + // awaited if it is non-blocking. A failed action stops the chain, + // since later actions usually depend on it. + try { + await future; + } catch (e, stackTrace) { + _reportActionError(reaction, e, stackTrace); + return true; + } } else { DebugLogger.instance.printInfo( 'Skipping awaiting for action ${reaction.name} as it is non-blocking...', name: name, ); + if (future is Future) { + unawaited( + future.then( + (_) {}, + onError: (Object e, StackTrace stackTrace) => + _reportActionError(reaction, e, stackTrace), + ), + ); + } } } return true; } - static void callFunction(BuildContext context, CallFunctionAction action) { + static void _reportActionError( + Reaction reaction, + Object error, + StackTrace stackTrace, + ) { + ErrorLogger.instance.captureException( + error, + message: 'Action ${reaction.name} failed', + type: 'action_failed', + stackTrace: stackTrace, + ); + } + + /// Calls the app's function named by [action], returning its result so + /// callers can wait for it and see its errors. + static FutureOr callFunction( + BuildContext context, + CallFunctionAction action, + ) { final ScopedValues scopedValues = ScopedValues.of(context); - final CodelesslyContext codelesslyContext = - context.read(); + final CodelesslyContext codelesslyContext = context + .read(); final CodelesslyFunction? function = codelesslyContext.functions[action.name]; // Substitute variables in params. - final Map parsedParams = - substituteVariablesInMap(action.params, scopedValues); + final Map parsedParams = substituteVariablesInMap( + action.params, + scopedValues, + ); DebugLogger.instance.printInfo( 'Calling function ${action.name}(${parsedParams.entries.map((e) => '${e.key}: ${e.value}').join(', ')}).', name: name, ); - function?.call(context, codelesslyContext, Map.unmodifiable(parsedParams)); + return function?.call( + context, + codelesslyContext, + Map.unmodifiable(parsedParams), + ); } static Future setStorageFromAction( @@ -1063,23 +1149,14 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. try { final LocalDatabase? storage = scopedValues.localStorage; if (storage == null) { - DebugLogger.instance.printInfo( - 'Storage is null.', - name: name, - ); + DebugLogger.instance.printInfo('Storage is null.', name: name); return false; } await storage.clear(); return true; } catch (error, stackTrace) { - DebugLogger.instance.printInfo( - error.toString(), - name: name, - ); - DebugLogger.instance.printInfo( - stackTrace.toString(), - name: name, - ); + DebugLogger.instance.printInfo(error.toString(), name: name); + DebugLogger.instance.printInfo(stackTrace.toString(), name: name); return false; } } @@ -1092,10 +1169,7 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. final LocalDatabase? storage = scopedValues.localStorage; if (storage == null) { - DebugLogger.instance.printInfo( - 'Storage is null.', - name: name, - ); + DebugLogger.instance.printInfo('Storage is null.', name: name); return false; } @@ -1141,25 +1215,34 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. VariableType.text => newValue, VariableType.image => newValue, VariableType.color => ColorRGBA.fromHex(newValue), - VariableType.integer => _performIntOperation(action.numberOperation, - currentValue?.toInt(), newValue, scopedValues), - VariableType.decimal => _performDecimalOperation(action.numberOperation, - currentValue?.toDouble(), newValue, scopedValues), - VariableType.boolean => action.toggled - ? !(bool.tryParse(currentValue.toString()) ?? false) - : bool.tryParse(newValue), + VariableType.integer => _performIntOperation( + action.numberOperation, + currentValue?.toInt(), + newValue, + scopedValues, + ), + VariableType.decimal => _performDecimalOperation( + action.numberOperation, + currentValue?.toDouble(), + newValue, + scopedValues, + ), + VariableType.boolean => + action.toggled + ? !(bool.tryParse(currentValue.toString()) ?? false) + : bool.tryParse(newValue), VariableType.list => _performListOperation( - action, - currentValue?.toList(), - newValue, - scopedValues, - ), + action, + currentValue?.toList(), + newValue, + scopedValues, + ), VariableType.map => _performMapOperation( - action, - currentValue?.toMap(), - newValue, - scopedValues, - ), + action, + currentValue?.toMap(), + newValue, + scopedValues, + ), }; if (match == null || pointer == null) { @@ -1177,10 +1260,10 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. final defaultValue = action.variableType.isList ? [] : action.variableType.isMap - ? {} - : null; + ? {} + : null; Map storageData = { - match.name: storage.get(match.name, defaultValue: defaultValue) + match.name: storage.get(match.name, defaultValue: defaultValue), }; final result = pointer.write(storageData, value); @@ -1196,14 +1279,8 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. return true; } catch (error, stackTrace) { - DebugLogger.instance.printInfo( - error.toString(), - name: name, - ); - DebugLogger.instance.printInfo( - stackTrace.toString(), - name: name, - ); + DebugLogger.instance.printInfo(error.toString(), name: name); + DebugLogger.instance.printInfo(stackTrace.toString(), name: name); return false; } } @@ -1211,14 +1288,13 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. /// Performs remove operation on given storage [action] and returns `true` if /// the operation was successful, `false` otherwise. static Future _removeFromStorage( - SetStorageAction action, ScopedValues scopedValues) async { + SetStorageAction action, + ScopedValues scopedValues, + ) async { try { final LocalDatabase? storage = scopedValues.localStorage; if (storage == null) { - DebugLogger.instance.printInfo( - 'Storage is null.', - name: name, - ); + DebugLogger.instance.printInfo('Storage is null.', name: name); return false; } @@ -1260,14 +1336,8 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. return true; } catch (error, stackTrace) { - DebugLogger.instance.printInfo( - error.toString(), - name: name, - ); - DebugLogger.instance.printInfo( - stackTrace.toString(), - name: name, - ); + DebugLogger.instance.printInfo(error.toString(), name: name); + DebugLogger.instance.printInfo(stackTrace.toString(), name: name); return false; } } @@ -1364,10 +1434,14 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. ) async { final ScopedValues scopedValues = ScopedValues.of(context); return await switch (action.subAction) { - AddDocumentSubAction addDocumentSubAction => - addDocumentToCloud(addDocumentSubAction, scopedValues), - UpdateDocumentSubAction updateDocumentSubAction => - updateDocumentOnCloud(updateDocumentSubAction, scopedValues), + AddDocumentSubAction addDocumentSubAction => addDocumentToCloud( + addDocumentSubAction, + scopedValues, + ), + UpdateDocumentSubAction updateDocumentSubAction => updateDocumentOnCloud( + updateDocumentSubAction, + scopedValues, + ), RemoveDocumentSubAction removeDocumentSubAction => removeDocumentFromCloud(removeDocumentSubAction, scopedValues), }; @@ -1381,10 +1455,7 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. final CloudDatabase? cloudDatabase = scopedValues.cloudDatabase; if (cloudDatabase == null) { - DebugLogger.instance.printInfo( - 'Cloud storage is null.', - name: name, - ); + DebugLogger.instance.printInfo('Cloud storage is null.', name: name); return false; } @@ -1438,14 +1509,8 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. skipCreationIfDocumentExists: subAction.skipCreationIfDocumentExists, ); } catch (error, stackTrace) { - DebugLogger.instance.printInfo( - error.toString(), - name: name, - ); - DebugLogger.instance.printInfo( - stackTrace.toString(), - name: name, - ); + DebugLogger.instance.printInfo(error.toString(), name: name); + DebugLogger.instance.printInfo(stackTrace.toString(), name: name); return false; } } @@ -1457,10 +1522,7 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. final CloudDatabase? cloudDatabase = scopedValues.cloudDatabase; if (cloudDatabase == null) { - DebugLogger.instance.printInfo( - 'Cloud storage is null.', - name: name, - ); + DebugLogger.instance.printInfo('Cloud storage is null.', name: name); return false; } @@ -1507,7 +1569,9 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. final match = VariableMatch.parse(storageKey.wrapWithVariableSyntax()); final docData = await cloudDatabase.getDocumentData( - subAction.path, evaluatedDocumentId); + subAction.path, + evaluatedDocumentId, + ); final Object? currentValue; JsonPointer? pointer; @@ -1531,28 +1595,34 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. VariableType.text => newValue, VariableType.image => newValue, VariableType.color => ColorRGBA.fromHex(newValue), - VariableType.integer => _performIntOperation(subAction.numberOperation, - currentValue?.toInt(), newValue, scopedValues), + VariableType.integer => _performIntOperation( + subAction.numberOperation, + currentValue?.toInt(), + newValue, + scopedValues, + ), VariableType.decimal => _performDecimalOperation( - subAction.numberOperation, - currentValue?.toDouble(), - newValue, - scopedValues), - VariableType.boolean => subAction.toggled - ? !(bool.tryParse(currentValue.toString()) ?? false) - : bool.tryParse(newValue), + subAction.numberOperation, + currentValue?.toDouble(), + newValue, + scopedValues, + ), + VariableType.boolean => + subAction.toggled + ? !(bool.tryParse(currentValue.toString()) ?? false) + : bool.tryParse(newValue), VariableType.list => _performListOperation( - subAction, - currentValue?.toList(), - newValue, - scopedValues, - ), + subAction, + currentValue?.toList(), + newValue, + scopedValues, + ), VariableType.map => _performMapOperation( - subAction, - currentValue?.toMap(), - newValue, - scopedValues, - ), + subAction, + currentValue?.toMap(), + newValue, + scopedValues, + ), }; if (match == null || pointer == null) { @@ -1575,8 +1645,8 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. final defaultValue = subAction.variableType.isList ? [] : subAction.variableType.isMap - ? {} - : null; + ? {} + : null; Map storageData = { match.name: docData[match.name] ?? defaultValue, }; @@ -1599,14 +1669,8 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. value: {match.name: docData[match.name]}, ); } catch (error, stackTrace) { - DebugLogger.instance.printInfo( - error.toString(), - name: name, - ); - DebugLogger.instance.printInfo( - stackTrace.toString(), - name: name, - ); + DebugLogger.instance.printInfo(error.toString(), name: name); + DebugLogger.instance.printInfo(stackTrace.toString(), name: name); return false; } } @@ -1619,10 +1683,7 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. final CloudDatabase? cloudDatabase = scopedValues.cloudDatabase; if (cloudDatabase == null) { - DebugLogger.instance.printInfo( - 'Cloud storage is null.', - name: name, - ); + DebugLogger.instance.printInfo('Cloud storage is null.', name: name); return false; } @@ -1642,14 +1703,8 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. evaluatedDocumentId, ); } catch (error, stackTrace) { - DebugLogger.instance.printInfo( - error.toString(), - name: name, - ); - DebugLogger.instance.printInfo( - stackTrace.toString(), - name: name, - ); + DebugLogger.instance.printInfo(error.toString(), name: name); + DebugLogger.instance.printInfo(stackTrace.toString(), name: name); return false; } } @@ -1703,7 +1758,10 @@ ${response.body.contains('{') ? const JsonEncoder.withIndent(' ').convert(json. name: name, ); cloudDatabase.streamDocumentToVariable( - evaluatedPath, evaluatedDocumentId, variable); + evaluatedPath, + evaluatedDocumentId, + variable, + ); } else { DebugLogger.instance.printInfo( 'Streaming collection from cloud storage: $evaluatedPath', diff --git a/lib/src/logging/debug_logger.dart b/lib/src/logging/debug_logger.dart index 81b2b8ce..88432fee 100644 --- a/lib/src/logging/debug_logger.dart +++ b/lib/src/logging/debug_logger.dart @@ -74,8 +74,12 @@ class DebugLogger { /// /// Filters messages based on the configuration settings such as enabled and /// disabled categories, and highlights. - void log(Object? message, - {String? category, String? name, Level level = Level.INFO}) { + void log( + Object? message, { + String? category, + String? name, + Level level = Level.INFO, + }) { try { // Check if debug logging is enabled if (!config.debugLog) { @@ -121,8 +125,12 @@ class DebugLogger { } /// Prints a log message to the console using the [Logger] library. - void _log(Object? message, - {String? category, String? name, Level level = Level.INFO}) { + void _log( + Object? message, { + String? category, + String? name, + Level level = Level.INFO, + }) { StringBuffer logMessage = StringBuffer(); if (name?.isNotEmpty ?? false) { logMessage.write('[$name] '); @@ -149,8 +157,12 @@ class DebugLogger { /// } /// ``` void printRebuild(String name) { - log('Rebuild', - category: DebugCategory.rebuild, name: name, level: Level.FINEST); + log( + 'Rebuild', + category: DebugCategory.rebuild, + name: name, + level: Level.FINEST, + ); } /// [Level.FINEST] Specialized logging function used for tracing user actions within an application. @@ -168,8 +180,12 @@ class DebugLogger { /// ) /// ``` void printAction(Object? message, {String? name}) { - log(message, - category: DebugCategory.action, name: name, level: Level.FINEST); + log( + message, + category: DebugCategory.action, + name: name, + level: Level.FINEST, + ); } /// [Level.FINEST] Logs detailed information about function executions, including function name and passed parameters. @@ -191,8 +207,12 @@ class DebugLogger { /// } /// ``` void printFunction(Object? message, {required String name}) { - log(message, - category: DebugCategory.function, name: name, level: Level.FINEST); + log( + message, + category: DebugCategory.function, + name: name, + level: Level.FINEST, + ); } /// [Level.INFO] Default log method. @@ -262,19 +282,23 @@ class DebugLoggerConfig { bool isHighlighted({String? category, String? name}) { if (highlights.isEmpty) return false; - return highlights.any((String highlight) => highlight.contains('-') - ? (highlight == - [category, name] - .where((String? item) => item != null) - .join('-')) - : (highlight == category || highlight == name)); + return highlights.any( + (String highlight) => highlight.contains('-') + ? (highlight == + [ + category, + name, + ].where((String? item) => item != null).join('-')) + : (highlight == category || highlight == name), + ); } /// Sets a highlight for a specific category and name. DebugLoggerConfig setHighlight({String? category, String? name}) { - String highlight = [category, name] - .where((String? item) => item != null) - .join('-'); + String highlight = [ + category, + name, + ].where((String? item) => item != null).join('-'); if (highlight.isNotEmpty) { highlights.add(highlight); } diff --git a/lib/src/logging/error_logger.dart b/lib/src/logging/error_logger.dart index 243a174c..5b3678d5 100644 --- a/lib/src/logging/error_logger.dart +++ b/lib/src/logging/error_logger.dart @@ -1,4 +1,5 @@ import 'dart:async'; + import 'package:logging/logging.dart'; import '../logging/debug_logger.dart'; diff --git a/lib/src/logging/stat_tracker.dart b/lib/src/logging/stat_tracker.dart index 306c30e2..7778c04b 100644 --- a/lib/src/logging/stat_tracker.dart +++ b/lib/src/logging/stat_tracker.dart @@ -60,36 +60,30 @@ class StatTracker { } /// Sends the batch of stats to the server with retry logic. - Future sendBatch() => debouncer.run( - () async { - int maxRetries = 3; - int baseDelaySeconds = 2; - - for (int attempt = 0; attempt < maxRetries; attempt++) { - try { - final response = await client.post( - serverUrl!, - headers: {'Content-Type': 'application/json'}, - body: jsonEncode({ - 'projectId': projectId, - 'stats': statBatch, - }), - ); - - if (response.statusCode == 200) { - statBatch.clear(); - return; // Exit the function if successful - } - } catch (e) { - // Handle exception silently - } - await Future.delayed(Duration( - seconds: - baseDelaySeconds * (1 << attempt))); // Exponential backoff - } - }, - forceRunAfter: 20, - ); + Future sendBatch() => debouncer.run(() async { + int maxRetries = 3; + int baseDelaySeconds = 2; + + for (int attempt = 0; attempt < maxRetries; attempt++) { + try { + final response = await client.post( + serverUrl!, + headers: {'Content-Type': 'application/json'}, + body: jsonEncode({'projectId': projectId, 'stats': statBatch}), + ); + + if (response.statusCode == 200) { + statBatch.clear(); + return; // Exit the function if successful + } + } catch (e) { + // Handle exception silently + } + await Future.delayed( + Duration(seconds: baseDelaySeconds * (1 << attempt)), + ); // Exponential backoff + } + }, forceRunAfter: 20); /// Tracks a stat with optional sublabel and count /// diff --git a/lib/src/model/asset_model.dart b/lib/src/model/asset_model.dart index 199a5d90..4ae94cb6 100644 --- a/lib/src/model/asset_model.dart +++ b/lib/src/model/asset_model.dart @@ -1,12 +1,13 @@ -import 'package:codelessly_json_annotation/codelessly_json_annotation.dart'; +import 'package:json_annotation/json_annotation.dart'; import 'package:equatable/equatable.dart'; +import 'package:codelessly_api/codelessly_api.dart'; part 'asset_model.g.dart'; /// Defines an asset that is utilized throughout node data as an id and /// looked up for information about the asset based on that id. @JsonSerializable() -class AssetModel with EquatableMixin { +class AssetModel with Equatable { /// The id of the asset. final String id; @@ -72,12 +73,12 @@ class AssetModel with EquatableMixin { @override List get props => [ - id, - name, - url, - blurHash, - sourceWidth, - sourceHeight, - createdAt, - ]; + id, + name, + url, + blurHash, + sourceWidth, + sourceHeight, + createdAt, + ]; } diff --git a/lib/src/model/asset_model.g.dart b/lib/src/model/asset_model.g.dart index f2023df7..7b95731f 100644 --- a/lib/src/model/asset_model.g.dart +++ b/lib/src/model/asset_model.g.dart @@ -7,37 +7,24 @@ part of 'asset_model.dart'; // ************************************************************************** AssetModel _$AssetModelFromJson(Map json) => AssetModel( - id: json['id'] as String, - name: json['name'] as String, - url: json['url'] as String, - blurHash: json['blurHash'] as String? ?? '', - sourceWidth: (json['sourceWidth'] as num).toDouble(), - sourceHeight: (json['sourceHeight'] as num).toDouble(), - createdAt: const DateTimeConverter() - .fromJson((json['createdAt'] as num?)?.toInt()), - ); + id: json['id'] as String, + name: json['name'] as String, + url: json['url'] as String, + blurHash: json['blurHash'] as String? ?? '', + sourceWidth: (json['sourceWidth'] as num).toDouble(), + sourceHeight: (json['sourceHeight'] as num).toDouble(), + createdAt: const DateTimeConverter().fromJson( + (json['createdAt'] as num?)?.toInt(), + ), +); -Map _$AssetModelToJson(AssetModel instance) { - final val = { - 'id': instance.id, - 'name': instance.name, - 'url': instance.url, - 'sourceWidth': instance.sourceWidth, - 'sourceHeight': instance.sourceHeight, - }; - - void writeNotNull( - String key, dynamic value, dynamic jsonValue, dynamic defaultValue) { - final bool serialize = - shouldSerialize(key, value, jsonValue, defaultValue, true); - - if (serialize) { - val[key] = jsonValue; - } - } - - writeNotNull('createdAt', instance.createdAt, - const DateTimeConverter().toJson(instance.createdAt), null); - writeNotNull('blurHash', instance.blurHash, instance.blurHash, ''); - return val; -} +Map _$AssetModelToJson(AssetModel instance) => + { + 'id': instance.id, + 'name': instance.name, + 'url': instance.url, + 'sourceWidth': instance.sourceWidth, + 'sourceHeight': instance.sourceHeight, + 'createdAt': ?const DateTimeConverter().toJson(instance.createdAt), + 'blurHash': instance.blurHash, + }; diff --git a/lib/src/model/auth_data.dart b/lib/src/model/auth_data.dart index 87e40bc4..bfff75e1 100644 --- a/lib/src/model/auth_data.dart +++ b/lib/src/model/auth_data.dart @@ -1,4 +1,5 @@ -import 'package:codelessly_json_annotation/codelessly_json_annotation.dart'; +import 'package:json_annotation/json_annotation.dart'; +import 'package:codelessly_api/codelessly_api.dart'; import '../../codelessly_sdk.dart'; @@ -58,10 +59,10 @@ class AuthData extends PrivacyBase { @override List get props => [ - ...super.props, - authToken, - projectId, - timestamp, - isTemplate, - ]; + ...super.props, + authToken, + projectId, + timestamp, + isTemplate, + ]; } diff --git a/lib/src/model/auth_data.g.dart b/lib/src/model/auth_data.g.dart index 5ad023cd..be53b09d 100644 --- a/lib/src/model/auth_data.g.dart +++ b/lib/src/model/auth_data.g.dart @@ -7,41 +7,27 @@ part of 'auth_data.dart'; // ************************************************************************** AuthData _$AuthDataFromJson(Map json) => AuthData( - authToken: json['authToken'] as String, - projectId: json['projectId'] as String, - timestamp: const DateTimeConverter() - .fromJson((json['timestamp'] as num?)?.toInt()), - isTemplate: json['isTemplate'] as bool? ?? false, - owner: json['owner'] as String, - teams: (json['teams'] as List?)?.map((e) => e as String).toSet(), - users: (json['users'] as List?)?.map((e) => e as String).toSet(), - roles: (json['roles'] as Map).map( - (k, e) => MapEntry(k as String, Role.fromJson(e as String)), - ), - ); + authToken: json['authToken'] as String, + projectId: json['projectId'] as String, + timestamp: const DateTimeConverter().fromJson( + (json['timestamp'] as num?)?.toInt(), + ), + isTemplate: json['isTemplate'] as bool? ?? false, + owner: json['owner'] as String, + teams: (json['teams'] as List?)?.map((e) => e as String).toSet(), + users: (json['users'] as List?)?.map((e) => e as String).toSet(), + roles: (json['roles'] as Map).map( + (k, e) => MapEntry(k as String, Role.fromJson(e as String)), + ), +); -Map _$AuthDataToJson(AuthData instance) { - final val = { - 'owner': instance.owner, - 'users': instance.users.toList(), - 'roles': instance.roles.map((k, e) => MapEntry(k, e.toJson())), - 'teams': instance.teams.toList(), - 'authToken': instance.authToken, - 'projectId': instance.projectId, - }; - - void writeNotNull( - String key, dynamic value, dynamic jsonValue, dynamic defaultValue) { - final bool serialize = - shouldSerialize(key, value, jsonValue, defaultValue, true); - - if (serialize) { - val[key] = jsonValue; - } - } - - writeNotNull('isTemplate', instance.isTemplate, instance.isTemplate, false); - writeNotNull('timestamp', instance.timestamp, - const DateTimeConverter().toJson(instance.timestamp), null); - return val; -} +Map _$AuthDataToJson(AuthData instance) => { + 'owner': instance.owner, + 'users': instance.users.toList(), + 'roles': instance.roles.map((k, e) => MapEntry(k, e.toJson())), + 'teams': instance.teams.toList(), + 'authToken': instance.authToken, + 'projectId': instance.projectId, + 'isTemplate': instance.isTemplate, + 'timestamp': ?const DateTimeConverter().toJson(instance.timestamp), +}; diff --git a/lib/src/model/custom_component.dart b/lib/src/model/custom_component.dart index 88263ef8..d7b01d0e 100644 --- a/lib/src/model/custom_component.dart +++ b/lib/src/model/custom_component.dart @@ -3,14 +3,14 @@ // license that can be found in the LICENSE.md file. import 'package:codelessly_api/codelessly_api.dart'; -import 'package:codelessly_json_annotation/codelessly_json_annotation.dart'; +import 'package:json_annotation/json_annotation.dart'; import 'package:equatable/equatable.dart'; part 'custom_component.g.dart'; /// A model to hold custom component data. @JsonSerializable() -class CustomComponent with EquatableMixin, SerializableMixin { +class CustomComponent with Equatable, SerializableMixin { /// Unique identifier for the component. final String id; @@ -53,9 +53,9 @@ class CustomComponent with EquatableMixin, SerializableMixin { required this.projectId, required this.pageId, required int? version, - }) : createdAt = createdAt ?? DateTime.now(), - // parentId = parentId ?? getTopMostParentIDs(data.nodes).first, - version = version ?? 1; + }) : createdAt = createdAt ?? DateTime.now(), + // parentId = parentId ?? getTopMostParentIDs(data.nodes).first, + version = version ?? 1; /// Duplicate this [CustomComponent] instance with the given data overrides. CustomComponent copyWith({ @@ -68,18 +68,17 @@ class CustomComponent with EquatableMixin, SerializableMixin { String? pageId, String? projectId, int? version, - }) => - CustomComponent( - id: id ?? this.id, - name: name ?? this.name, - data: data ?? this.data, - createdAt: DateTime.now(), - previewUrl: previewUrl ?? this.previewUrl, - blurhash: blurhash ?? this.blurhash, - projectId: projectId ?? this.projectId, - pageId: pageId ?? this.pageId, - version: version ?? this.version, - ); + }) => CustomComponent( + id: id ?? this.id, + name: name ?? this.name, + data: data ?? this.data, + createdAt: DateTime.now(), + previewUrl: previewUrl ?? this.previewUrl, + blurhash: blurhash ?? this.blurhash, + projectId: projectId ?? this.projectId, + pageId: pageId ?? this.pageId, + version: version ?? this.version, + ); /// Factory constructor for creating a new instance of this class from /// a JSON object. @@ -91,27 +90,27 @@ class CustomComponent with EquatableMixin, SerializableMixin { @override List get props => [ - id, - name, - data, - createdAt, - previewUrl, - blurhash, - projectId, - pageId, - version, - ]; + id, + name, + data, + createdAt, + previewUrl, + blurhash, + projectId, + pageId, + version, + ]; void setParentAsInstance() => data.setParentAsInstance( - componentId: id, - componentSchema: schema, - componentVersion: version, - ); + componentId: id, + componentSchema: schema, + componentVersion: version, + ); } /// Holds component's node data and containing rect size data. @JsonSerializable() -class ComponentData with EquatableMixin, SerializableMixin { +class ComponentData with Equatable, SerializableMixin { /// Width of the containing rect for [nodes]. final double width; @@ -163,14 +162,14 @@ class ComponentData with EquatableMixin, SerializableMixin { @override List get props => [ - width, - height, - nodes, - variables, - conditions, - parentId, - originalParentId, - ]; + width, + height, + nodes, + variables, + conditions, + parentId, + originalParentId, + ]; @override Map toJson() => _$ComponentDataToJson(this); @@ -189,16 +188,15 @@ class ComponentData with EquatableMixin, SerializableMixin { Map>? conditions, String? parentId, String? originalParentId, - }) => - ComponentData( - width: width ?? this.width, - height: height ?? this.height, - nodes: nodes ?? this.nodes, - variables: variables ?? this.variables, - conditions: conditions ?? this.conditions, - parentId: parentId ?? this.parentId, - originalParentId: originalParentId ?? this.originalParentId, - ); + }) => ComponentData( + width: width ?? this.width, + height: height ?? this.height, + nodes: nodes ?? this.nodes, + variables: variables ?? this.variables, + conditions: conditions ?? this.conditions, + parentId: parentId ?? this.parentId, + originalParentId: originalParentId ?? this.originalParentId, + ); void setParentAsInstance({ required String componentId, diff --git a/lib/src/model/custom_component.g.dart b/lib/src/model/custom_component.g.dart index d2acbba7..3c122a1d 100644 --- a/lib/src/model/custom_component.g.dart +++ b/lib/src/model/custom_component.g.dart @@ -7,102 +7,77 @@ part of 'custom_component.dart'; // ************************************************************************** CustomComponent _$CustomComponentFromJson(Map json) => CustomComponent( - id: json['id'] as String, - name: json['name'] as String? ?? '', - data: ComponentData.fromJson( - Map.from(json['data'] as Map)), - createdAt: const DateTimeConverter() - .fromJson((json['createdAt'] as num?)?.toInt()), - previewUrl: json['previewUrl'] as String?, - blurhash: json['blurhash'] as String? ?? '', - projectId: json['projectId'] as String, - pageId: json['pageId'] as String, - version: (json['version'] as num?)?.toInt(), - ); + id: json['id'] as String, + name: json['name'] as String? ?? '', + data: ComponentData.fromJson(Map.from(json['data'] as Map)), + createdAt: const DateTimeConverter().fromJson( + (json['createdAt'] as num?)?.toInt(), + ), + previewUrl: json['previewUrl'] as String?, + blurhash: json['blurhash'] as String? ?? '', + projectId: json['projectId'] as String, + pageId: json['pageId'] as String, + version: (json['version'] as num?)?.toInt(), +); -Map _$CustomComponentToJson(CustomComponent instance) { - final val = { - 'id': instance.id, - }; - - void writeNotNull( - String key, dynamic value, dynamic jsonValue, dynamic defaultValue) { - final bool serialize = - shouldSerialize(key, value, jsonValue, defaultValue, true); - - if (serialize) { - val[key] = jsonValue; - } - } - - writeNotNull('name', instance.name, instance.name, ''); - val['data'] = instance.data.toJson(); - writeNotNull('createdAt', instance.createdAt, - const DateTimeConverter().toJson(instance.createdAt), null); - writeNotNull('previewUrl', instance.previewUrl, instance.previewUrl, null); - writeNotNull('blurhash', instance.blurhash, instance.blurhash, ''); - val['projectId'] = instance.projectId; - val['pageId'] = instance.pageId; - val['version'] = instance.version; - return val; -} +Map _$CustomComponentToJson(CustomComponent instance) => + { + 'id': instance.id, + 'name': instance.name, + 'data': instance.data.toJson(), + 'createdAt': ?const DateTimeConverter().toJson(instance.createdAt), + 'previewUrl': ?instance.previewUrl, + 'blurhash': instance.blurhash, + 'projectId': instance.projectId, + 'pageId': instance.pageId, + 'version': instance.version, + }; ComponentData _$ComponentDataFromJson(Map json) => ComponentData( - width: (json['width'] as num).toDouble(), - height: (json['height'] as num).toDouble(), - nodes: Map.from(json['nodes'] as Map), - variables: (json['variables'] as Map?)?.map( - (k, e) => MapEntry( - k as String, - (e as List) - .map((e) => VariableData.fromJson( - Map.from(e as Map))) - .toSet()), - ) ?? - {}, - conditions: (json['conditions'] as Map?)?.map( - (k, e) => MapEntry( - k as String, - (e as List) - .map((e) => BaseCondition.fromJson( - Map.from(e as Map))) - .toSet()), - ) ?? - {}, - parentId: json['parentId'] as String, - originalParentId: json['originalParentId'] as String, - ); - -Map _$ComponentDataToJson(ComponentData instance) { - final val = { - 'width': instance.width, - 'height': instance.height, - 'nodes': instance.nodes, - }; - - void writeNotNull( - String key, dynamic value, dynamic jsonValue, dynamic defaultValue) { - final bool serialize = - shouldSerialize(key, value, jsonValue, defaultValue, true); - - if (serialize) { - val[key] = jsonValue; - } - } + width: (json['width'] as num).toDouble(), + height: (json['height'] as num).toDouble(), + nodes: Map.from(json['nodes'] as Map), + variables: + (json['variables'] as Map?)?.map( + (k, e) => MapEntry( + k as String, + (e as List) + .map( + (e) => + VariableData.fromJson(Map.from(e as Map)), + ) + .toSet(), + ), + ) ?? + const {}, + conditions: + (json['conditions'] as Map?)?.map( + (k, e) => MapEntry( + k as String, + (e as List) + .map( + (e) => + BaseCondition.fromJson(Map.from(e as Map)), + ) + .toSet(), + ), + ) ?? + const {}, + parentId: json['parentId'] as String, + originalParentId: json['originalParentId'] as String, +); - writeNotNull( - 'variables', - instance.variables, - instance.variables - .map((k, e) => MapEntry(k, e.map((e) => e.toJson()).toList())), - const {}); - writeNotNull( - 'conditions', - instance.conditions, - instance.conditions - .map((k, e) => MapEntry(k, e.map((e) => e.toJson()).toList())), - const {}); - val['parentId'] = instance.parentId; - val['originalParentId'] = instance.originalParentId; - return val; -} +Map _$ComponentDataToJson(ComponentData instance) => + { + 'width': instance.width, + 'height': instance.height, + 'nodes': instance.nodes, + 'variables': instance.variables.map( + (k, e) => MapEntry(k, e.map((e) => e.toJson()).toList()), + ), + 'conditions': instance.conditions.map( + (k, e) => MapEntry(k, e.map((e) => e.toJson()).toList()), + ), + 'parentId': instance.parentId, + 'originalParentId': instance.originalParentId, + }; diff --git a/lib/src/model/font_family.dart b/lib/src/model/font_family.dart index 7b9e2499..88b2617b 100644 --- a/lib/src/model/font_family.dart +++ b/lib/src/model/font_family.dart @@ -3,7 +3,7 @@ // license that can be found in the LICENSE.md file. import 'package:codelessly_api/codelessly_api.dart'; -import 'package:codelessly_json_annotation/codelessly_json_annotation.dart'; +import 'package:json_annotation/json_annotation.dart'; import 'package:equatable/equatable.dart'; import '../../codelessly_sdk.dart'; @@ -44,8 +44,8 @@ class FontFamilyModel extends PrivacyBase { // Privacy required super.privacy, - }) : id = id ?? name, - super.private(); + }) : id = id ?? name, + super.private(); /// Factory constructor for creating a new [FontFamilyModel] instance from /// JSON data. @@ -100,14 +100,19 @@ class FontVariantModel extends Equatable { /// A factory constructor that creates an instance of [FontVariantModel] by /// parsing given [name]. - factory FontVariantModel.fromName(String name, - {String fontURL = '', String previewURL = ''}) { + factory FontVariantModel.fromName( + String name, { + String fontURL = '', + String previewURL = '', + }) { FontWeightNumeric fileFontWeight = FontWeightNumeric.w400; String fileFontStyle = 'Normal'; // Convert 'extra-bold' or 'extra bold' into 'extrabold'. - final String modifiedName = - name.toLowerCase().replaceAll('-', '').replaceAll(' ', ''); + final String modifiedName = name + .toLowerCase() + .replaceAll('-', '') + .replaceAll(' ', ''); // Check font weight. if (modifiedName.contains('thin')) { diff --git a/lib/src/model/font_family.g.dart b/lib/src/model/font_family.g.dart index 07ac8516..867099cb 100644 --- a/lib/src/model/font_family.g.dart +++ b/lib/src/model/font_family.g.dart @@ -7,20 +7,21 @@ part of 'font_family.dart'; // ************************************************************************** FontFamilyModel _$FontFamilyModelFromJson(Map json) => FontFamilyModel( - id: json['id'] as String?, - name: json['name'] as String, - fontVariants: (json['fontVariants'] as List) - .map((e) => - FontVariantModel.fromJson(Map.from(e as Map))) - .toList(), - owner: json['owner'] as String, - teams: (json['teams'] as List?)?.map((e) => e as String).toSet(), - users: (json['users'] as List?)?.map((e) => e as String).toSet(), - roles: (json['roles'] as Map).map( - (k, e) => MapEntry(k as String, Role.fromJson(e as String)), - ), - public: json['public'] as bool?, - ); + id: json['id'] as String?, + name: json['name'] as String, + fontVariants: (json['fontVariants'] as List) + .map( + (e) => FontVariantModel.fromJson(Map.from(e as Map)), + ) + .toList(), + owner: json['owner'] as String, + teams: (json['teams'] as List?)?.map((e) => e as String).toSet(), + users: (json['users'] as List?)?.map((e) => e as String).toSet(), + roles: (json['roles'] as Map).map( + (k, e) => MapEntry(k as String, Role.fromJson(e as String)), + ), + public: json['public'] as bool?, +); Map _$FontFamilyModelToJson(FontFamilyModel instance) => { @@ -35,35 +36,23 @@ Map _$FontFamilyModelToJson(FontFamilyModel instance) => }; FontVariantModel _$FontVariantModelFromJson(Map json) => FontVariantModel( - name: json['name'] as String? ?? 'regular', - weight: $enumDecodeNullable(_$FontWeightNumericEnumMap, json['weight']) ?? - FontWeightNumeric.w400, - style: json['style'] as String? ?? 'Normal', - fontURL: json['fontURL'] as String? ?? '', - previewURL: json['previewURL'] as String? ?? '', - ); + name: json['name'] as String? ?? 'regular', + weight: + $enumDecodeNullable(_$FontWeightNumericEnumMap, json['weight']) ?? + FontWeightNumeric.w400, + style: json['style'] as String? ?? 'Normal', + fontURL: json['fontURL'] as String? ?? '', + previewURL: json['previewURL'] as String? ?? '', +); -Map _$FontVariantModelToJson(FontVariantModel instance) { - final val = {}; - - void writeNotNull( - String key, dynamic value, dynamic jsonValue, dynamic defaultValue) { - final bool serialize = - shouldSerialize(key, value, jsonValue, defaultValue, true); - - if (serialize) { - val[key] = jsonValue; - } - } - - writeNotNull('name', instance.name, instance.name, 'regular'); - writeNotNull('weight', instance.weight, - _$FontWeightNumericEnumMap[instance.weight]!, FontWeightNumeric.w400); - writeNotNull('style', instance.style, instance.style, 'Normal'); - writeNotNull('fontURL', instance.fontURL, instance.fontURL, ''); - writeNotNull('previewURL', instance.previewURL, instance.previewURL, ''); - return val; -} +Map _$FontVariantModelToJson(FontVariantModel instance) => + { + 'name': instance.name, + 'weight': _$FontWeightNumericEnumMap[instance.weight]!, + 'style': instance.style, + 'fontURL': instance.fontURL, + 'previewURL': instance.previewURL, + }; const _$FontWeightNumericEnumMap = { FontWeightNumeric.w100: 'w100', diff --git a/lib/src/model/font_file_data.dart b/lib/src/model/font_file_data.dart index e8d91bb7..5e851c86 100644 --- a/lib/src/model/font_file_data.dart +++ b/lib/src/model/font_file_data.dart @@ -18,6 +18,11 @@ class FontFileData extends Equatable { }); @override - List get props => - [fileName, fileExtension, familyName, style, bytes]; + List get props => [ + fileName, + fileExtension, + familyName, + style, + bytes, + ]; } diff --git a/lib/src/model/model_http_request.dart b/lib/src/model/model_http_request.dart index add4ecfe..59b9dd7c 100644 --- a/lib/src/model/model_http_request.dart +++ b/lib/src/model/model_http_request.dart @@ -1,17 +1,12 @@ import 'package:codelessly_api/codelessly_api.dart'; -import 'package:codelessly_json_annotation/codelessly_json_annotation.dart'; +import 'package:json_annotation/json_annotation.dart'; import 'package:equatable/equatable.dart'; import '../../codelessly_sdk.dart'; part 'model_http_request.g.dart'; -enum HttpMethod { - get, - post, - put, - delete, -} +enum HttpMethod { get, post, put, delete } enum RequestBodyType { text, form } @@ -98,9 +93,9 @@ class HttpApiData extends PrivacyBase { required super.users, required super.roles, required super.public, - }) : variables = List.of(variables), - lastUpdated = lastUpdated ?? DateTime.now(), - created = created ?? DateTime.now(); + }) : variables = List.of(variables), + lastUpdated = lastUpdated ?? DateTime.now(), + created = created ?? DateTime.now(); HttpApiData.private({ required this.directory, @@ -123,10 +118,10 @@ class HttpApiData extends PrivacyBase { // Privacy required super.privacy, - }) : variables = List.of(variables), - lastUpdated = lastUpdated ?? DateTime.now(), - created = created ?? DateTime.now(), - super.private(); + }) : variables = List.of(variables), + lastUpdated = lastUpdated ?? DateTime.now(), + created = created ?? DateTime.now(), + super.private(); HttpApiData copyWith({ HttpMethod? method, @@ -150,28 +145,27 @@ class HttpApiData extends PrivacyBase { // Privacy PrivacyBase? privacy, - }) => - HttpApiData.private( - method: method ?? this.method, - url: url ?? this.url, - headers: headers ?? this.headers, - queryParams: queryParams ?? this.queryParams, - body: body ?? this.body, - formFields: formFields ?? this.formFields, - variables: variables ?? this.variables, - name: name ?? this.name, - project: project ?? this.project, - id: id ?? this.id, - isDeleted: isDeleted ?? this.isDeleted, - lastUpdated: lastUpdated ?? this.lastUpdated, - created: created ?? this.created, - bodyType: bodyType ?? this.bodyType, - isDraft: isDraft ?? this.isDraft, - directory: directory ?? this.directory, - requestBodyContentType: - requestBodyContentType ?? this.requestBodyContentType, - privacy: privacy ?? this, - ); + }) => HttpApiData.private( + method: method ?? this.method, + url: url ?? this.url, + headers: headers ?? this.headers, + queryParams: queryParams ?? this.queryParams, + body: body ?? this.body, + formFields: formFields ?? this.formFields, + variables: variables ?? this.variables, + name: name ?? this.name, + project: project ?? this.project, + id: id ?? this.id, + isDeleted: isDeleted ?? this.isDeleted, + lastUpdated: lastUpdated ?? this.lastUpdated, + created: created ?? this.created, + bodyType: bodyType ?? this.bodyType, + isDraft: isDraft ?? this.isDraft, + directory: directory ?? this.directory, + requestBodyContentType: + requestBodyContentType ?? this.requestBodyContentType, + privacy: privacy ?? this, + ); factory HttpApiData.fromJson(Map json) => _$HttpApiDataFromJson(json); diff --git a/lib/src/model/model_http_request.g.dart b/lib/src/model/model_http_request.g.dart index 281d6c2d..45313e60 100644 --- a/lib/src/model/model_http_request.g.dart +++ b/lib/src/model/model_http_request.g.dart @@ -7,112 +7,96 @@ part of 'model_http_request.dart'; // ************************************************************************** HttpApiData _$HttpApiDataFromJson(Map json) => HttpApiData( - directory: json['directory'] as String, - method: $enumDecodeNullable(_$HttpMethodEnumMap, json['method']) ?? - HttpMethod.get, - url: json['url'] as String? ?? '', - headers: (json['headers'] as List?) - ?.map((e) => HttpKeyValuePair.fromJson( - Map.from(e as Map))) - .toList() ?? - [], - queryParams: (json['queryParams'] as List?) - ?.map((e) => HttpKeyValuePair.fromJson( - Map.from(e as Map))) - .toList() ?? - [], - formFields: (json['formFields'] as List?) - ?.map((e) => HttpKeyValuePair.fromJson( - Map.from(e as Map))) - .toList() ?? - [], - body: json['body'] as String?, - bodyType: - $enumDecodeNullable(_$RequestBodyTypeEnumMap, json['bodyType']) ?? - RequestBodyType.text, - name: json['name'] as String, - id: json['id'] as String? ?? '', - project: json['project'] as String? ?? '', - variables: (json['variables'] as List?) - ?.map((e) => - VariableData.fromJson(Map.from(e as Map))) - .toList() ?? - [], - isDeleted: json['deleted'] as bool? ?? false, - lastUpdated: const DateTimeConverter() - .fromJson((json['lastUpdated'] as num?)?.toInt()), - requestBodyContentType: $enumDecodeNullable( - _$RequestBodyTextTypeEnumMap, json['requestBodyContentType']) ?? - RequestBodyTextType.json, - created: const DateTimeConverter() - .fromJson((json['created'] as num?)?.toInt()), - owner: json['owner'] as String, - teams: (json['teams'] as List?)?.map((e) => e as String).toSet(), - users: (json['users'] as List?)?.map((e) => e as String).toSet(), - roles: (json['roles'] as Map).map( - (k, e) => MapEntry(k as String, Role.fromJson(e as String)), - ), - public: json['public'] as bool?, - ); + directory: json['directory'] as String, + method: + $enumDecodeNullable(_$HttpMethodEnumMap, json['method']) ?? + HttpMethod.get, + url: json['url'] as String? ?? '', + headers: + (json['headers'] as List?) + ?.map( + (e) => + HttpKeyValuePair.fromJson(Map.from(e as Map)), + ) + .toList() ?? + const [], + queryParams: + (json['queryParams'] as List?) + ?.map( + (e) => + HttpKeyValuePair.fromJson(Map.from(e as Map)), + ) + .toList() ?? + const [], + formFields: + (json['formFields'] as List?) + ?.map( + (e) => + HttpKeyValuePair.fromJson(Map.from(e as Map)), + ) + .toList() ?? + const [], + body: json['body'] as String?, + bodyType: + $enumDecodeNullable(_$RequestBodyTypeEnumMap, json['bodyType']) ?? + RequestBodyType.text, + name: json['name'] as String, + id: json['id'] as String? ?? '', + project: json['project'] as String? ?? '', + variables: + (json['variables'] as List?) + ?.map( + (e) => VariableData.fromJson(Map.from(e as Map)), + ) + .toList() ?? + const [], + isDeleted: json['deleted'] as bool? ?? false, + lastUpdated: const DateTimeConverter().fromJson( + (json['lastUpdated'] as num?)?.toInt(), + ), + requestBodyContentType: + $enumDecodeNullable( + _$RequestBodyTextTypeEnumMap, + json['requestBodyContentType'], + ) ?? + RequestBodyTextType.json, + created: const DateTimeConverter().fromJson( + (json['created'] as num?)?.toInt(), + ), + owner: json['owner'] as String, + teams: (json['teams'] as List?)?.map((e) => e as String).toSet(), + users: (json['users'] as List?)?.map((e) => e as String).toSet(), + roles: (json['roles'] as Map).map( + (k, e) => MapEntry(k as String, Role.fromJson(e as String)), + ), + public: json['public'] as bool?, +); -Map _$HttpApiDataToJson(HttpApiData instance) { - final val = { - 'owner': instance.owner, - 'users': instance.users.toList(), - 'roles': instance.roles.map((k, e) => MapEntry(k, e.toJson())), - 'teams': instance.teams.toList(), - 'public': instance.public, - }; - - void writeNotNull( - String key, dynamic value, dynamic jsonValue, dynamic defaultValue) { - final bool serialize = - shouldSerialize(key, value, jsonValue, defaultValue, true); - - if (serialize) { - val[key] = jsonValue; - } - } - - writeNotNull('id', instance.id, instance.id, ''); - val['name'] = instance.name; - writeNotNull('project', instance.project, instance.project, ''); - writeNotNull('method', instance.method, _$HttpMethodEnumMap[instance.method]!, - HttpMethod.get); - writeNotNull('url', instance.url, instance.url, ''); - writeNotNull( - 'headers', - instance.headers, - instance.headers.map((e) => e.toJson()).toList(), - const []); - writeNotNull( - 'queryParams', - instance.queryParams, - instance.queryParams.map((e) => e.toJson()).toList(), - const []); - writeNotNull( - 'formFields', - instance.formFields, - instance.formFields.map((e) => e.toJson()).toList(), - const []); - writeNotNull('body', instance.body, instance.body, null); - writeNotNull('bodyType', instance.bodyType, - _$RequestBodyTypeEnumMap[instance.bodyType]!, RequestBodyType.text); - writeNotNull('deleted', instance.isDeleted, instance.isDeleted, false); - writeNotNull('lastUpdated', instance.lastUpdated, - const DateTimeConverter().toJson(instance.lastUpdated), null); - writeNotNull( - 'requestBodyContentType', - instance.requestBodyContentType, - _$RequestBodyTextTypeEnumMap[instance.requestBodyContentType]!, - RequestBodyTextType.json); - writeNotNull('variables', instance.variables, - instance.variables.map((e) => e.toJson()).toList(), const []); - writeNotNull('created', instance.created, - const DateTimeConverter().toJson(instance.created), null); - val['directory'] = instance.directory; - return val; -} +Map _$HttpApiDataToJson(HttpApiData instance) => + { + 'owner': instance.owner, + 'users': instance.users.toList(), + 'roles': instance.roles.map((k, e) => MapEntry(k, e.toJson())), + 'teams': instance.teams.toList(), + 'public': instance.public, + 'id': instance.id, + 'name': instance.name, + 'project': instance.project, + 'method': _$HttpMethodEnumMap[instance.method]!, + 'url': instance.url, + 'headers': instance.headers.map((e) => e.toJson()).toList(), + 'queryParams': instance.queryParams.map((e) => e.toJson()).toList(), + 'formFields': instance.formFields.map((e) => e.toJson()).toList(), + 'body': ?instance.body, + 'bodyType': _$RequestBodyTypeEnumMap[instance.bodyType]!, + 'deleted': instance.isDeleted, + 'lastUpdated': ?const DateTimeConverter().toJson(instance.lastUpdated), + 'requestBodyContentType': + _$RequestBodyTextTypeEnumMap[instance.requestBodyContentType]!, + 'variables': instance.variables.map((e) => e.toJson()).toList(), + 'created': ?const DateTimeConverter().toJson(instance.created), + 'directory': instance.directory, + }; const _$HttpMethodEnumMap = { HttpMethod.get: 'get', @@ -134,27 +118,14 @@ const _$RequestBodyTextTypeEnumMap = { }; HttpKeyValuePair _$HttpKeyValuePairFromJson(Map json) => HttpKeyValuePair( - key: json['key'] as String, - value: json['value'] as String, - isUsed: json['isUsed'] as bool? ?? true, - ); - -Map _$HttpKeyValuePairToJson(HttpKeyValuePair instance) { - final val = { - 'key': instance.key, - 'value': instance.value, - }; - - void writeNotNull( - String key, dynamic value, dynamic jsonValue, dynamic defaultValue) { - final bool serialize = - shouldSerialize(key, value, jsonValue, defaultValue, true); - - if (serialize) { - val[key] = jsonValue; - } - } + key: json['key'] as String, + value: json['value'] as String, + isUsed: json['isUsed'] as bool? ?? true, +); - writeNotNull('isUsed', instance.isUsed, instance.isUsed, true); - return val; -} +Map _$HttpKeyValuePairToJson(HttpKeyValuePair instance) => + { + 'key': instance.key, + 'value': instance.value, + 'isUsed': instance.isUsed, + }; diff --git a/lib/src/model/privacy_base.dart b/lib/src/model/privacy_base.dart index be897184..59389d38 100644 --- a/lib/src/model/privacy_base.dart +++ b/lib/src/model/privacy_base.dart @@ -1,5 +1,5 @@ import 'package:codelessly_api/codelessly_api.dart'; -import 'package:codelessly_json_annotation/codelessly_json_annotation.dart'; +import 'package:json_annotation/json_annotation.dart'; import 'package:equatable/equatable.dart'; part 'privacy_base.g.dart'; @@ -16,10 +16,10 @@ enum Role { /// ----------------- POWER HIERARCHY ----------------- bool isHigherLevelThan(Role other) => switch (this) { - admin => other != admin, - editor => other == viewer, - viewer => false, - }; + admin => other != admin, + editor => other == viewer, + viewer => false, + }; bool isHigherOrEqualLevelThan(Role other) => isHigherLevelThan(other) || this == other; @@ -37,40 +37,40 @@ enum Role { /// Only admins can modify project metadata. bool get canManageProject => switch (this) { - admin => true, - editor || viewer => false, - }; + admin => true, + editor || viewer => false, + }; /// Only admins and editors can edit projects. bool get canEditProject => switch (this) { - admin || editor => true, - viewer => false, - }; + admin || editor => true, + viewer => false, + }; /// Only admins and editors can manage team members. bool get canManageTeam => switch (this) { - admin || editor => true, - viewer => false, - }; + admin || editor => true, + viewer => false, + }; /// Anyone can manage project members. bool get canManageProjectMembers => true; factory Role.fromJson(String value) => switch (value) { - 'owner' || 'admin' => admin, - 'editor' => editor, - _ => viewer, - }; + 'owner' || 'admin' => admin, + 'editor' => editor, + _ => viewer, + }; String toJson() => switch (this) { - Role.admin => 'owner', - _ => name, - }; + Role.admin => 'owner', + _ => name, + }; } /// Represents the privacy controls of a given model that this mixin /// is applied to. -abstract class PrivacyBase with SerializableMixin, EquatableMixin { +abstract class PrivacyBase with SerializableMixin, Equatable { /// The owner of the document. The owner is the user who is responsible for /// the document and has the highest level of access to it. In terms of /// permissions, it's equivalent to the [Role.admin] role, however this owner @@ -98,14 +98,13 @@ abstract class PrivacyBase with SerializableMixin, EquatableMixin { required Set users, required Map roles, required bool public, - }) => - _PrivacyBaseImpl( - owner: owner, - teams: teams, - users: users, - roles: roles, - public: public, - ); + }) => _PrivacyBaseImpl( + owner: owner, + teams: teams, + users: users, + roles: roles, + public: public, + ); /// An owner ID must always exist in the roles map. It's an error /// if it doesn't. @@ -134,19 +133,19 @@ abstract class PrivacyBase with SerializableMixin, EquatableMixin { required Set? users, required this.roles, required bool? public, - }) : teams = teams ?? const {}, - users = users ?? const {}, - public = public ?? false; + }) : teams = teams ?? const {}, + users = users ?? const {}, + public = public ?? false; /// Creates a new [PrivacyBase] derived from another [PrivacyBase]. PrivacyBase.private({required PrivacyBase privacy, bool? public}) - : this( - owner: privacy.owner, - teams: privacy.teams, - users: privacy.users, - roles: privacy.roles, - public: public ?? privacy.public, - ); + : this( + owner: privacy.owner, + teams: privacy.teams, + users: privacy.users, + roles: privacy.roles, + public: public ?? privacy.public, + ); /// While this PrivacyBase object is extended by other models, calling /// toJson on those models will return data in addition to these core @@ -162,12 +161,12 @@ abstract class PrivacyBase with SerializableMixin, EquatableMixin { /// } @JsonKey(includeFromJson: false, includeToJson: false) PrivacyBase get privacy => _PrivacyBaseImpl( - owner: owner, - teams: teams, - users: users, - roles: roles, - public: public, - ); + owner: owner, + teams: teams, + users: users, + roles: roles, + public: public, + ); @override List get props => [users, roles, teams, public]; diff --git a/lib/src/model/privacy_base.g.dart b/lib/src/model/privacy_base.g.dart index e8b67980..ce32f96b 100644 --- a/lib/src/model/privacy_base.g.dart +++ b/lib/src/model/privacy_base.g.dart @@ -7,14 +7,14 @@ part of 'privacy_base.dart'; // ************************************************************************** _PrivacyBaseImpl _$PrivacyBaseImplFromJson(Map json) => _PrivacyBaseImpl( - owner: json['owner'] as String, - teams: (json['teams'] as List?)?.map((e) => e as String).toSet(), - users: (json['users'] as List?)?.map((e) => e as String).toSet(), - roles: (json['roles'] as Map).map( - (k, e) => MapEntry(k as String, Role.fromJson(e as String)), - ), - public: json['public'] as bool?, - ); + owner: json['owner'] as String, + teams: (json['teams'] as List?)?.map((e) => e as String).toSet(), + users: (json['users'] as List?)?.map((e) => e as String).toSet(), + roles: (json['roles'] as Map).map( + (k, e) => MapEntry(k as String, Role.fromJson(e as String)), + ), + public: json['public'] as bool?, +); Map _$PrivacyBaseImplToJson(_PrivacyBaseImpl instance) => { diff --git a/lib/src/model/publish_source.dart b/lib/src/model/publish_source.dart index 6056c316..ee37b741 100644 --- a/lib/src/model/publish_source.dart +++ b/lib/src/model/publish_source.dart @@ -20,57 +20,57 @@ enum PublishSource { /// The path to the data collection. String get rootDataCollection => switch (this) { - PublishSource.publish => 'data', - PublishSource.preview => 'preview_data', - PublishSource.template => 'template_data', - }; + PublishSource.publish => 'data', + PublishSource.preview => 'preview_data', + PublishSource.template => 'template_data', + }; /// The path to the publish model collection. String get serverPath => switch (this) { - PublishSource.publish => 'publish', - PublishSource.preview => 'publish_preview', - PublishSource.template => 'templates', - }; + PublishSource.publish => 'publish', + PublishSource.preview => 'publish_preview', + PublishSource.template => 'templates', + }; /// The path to the mirrored or opposite collection. String get mirroredServerPath => switch (this) { - PublishSource.publish => 'publish_preview', - PublishSource.preview => 'publish', - PublishSource.template => 'publish', - }; + PublishSource.publish => 'publish_preview', + PublishSource.preview => 'publish', + PublishSource.template => 'publish', + }; /// The cache key for the published model. String get modelCacheKey => switch (this) { - PublishSource.publish => 'publish_layout_model', - PublishSource.preview => 'preview_layout_model', - PublishSource.template => 'template_layout_model', - }; + PublishSource.publish => 'publish_layout_model', + PublishSource.preview => 'preview_layout_model', + PublishSource.template => 'template_layout_model', + }; /// The cache key for the font files. String get fontsCacheKey => switch (this) { - PublishSource.publish => 'publish_fonts', - PublishSource.preview => 'preview_fonts', - PublishSource.template => 'template_fonts', - }; + PublishSource.publish => 'publish_fonts', + PublishSource.preview => 'preview_fonts', + PublishSource.template => 'template_fonts', + }; /// The cache key for the APIs. String get apisCacheKey => switch (this) { - PublishSource.publish => 'publish_apis', - PublishSource.preview => 'preview_apis', - PublishSource.template => 'template_apis', - }; + PublishSource.publish => 'publish_apis', + PublishSource.preview => 'preview_apis', + PublishSource.template => 'template_apis', + }; /// The cache key for the APIs. String get variablesCacheKey => switch (this) { - PublishSource.publish => 'publish_variables', - PublishSource.preview => 'preview_variables', - PublishSource.template => 'template_variables', - }; + PublishSource.publish => 'publish_variables', + PublishSource.preview => 'preview_variables', + PublishSource.template => 'template_variables', + }; /// The cache key for the APIs. String get conditionsCacheKey => switch (this) { - PublishSource.publish => 'publish_conditions', - PublishSource.preview => 'preview_conditions', - PublishSource.template => 'template_conditions', - }; + PublishSource.publish => 'publish_conditions', + PublishSource.preview => 'preview_conditions', + PublishSource.template => 'template_conditions', + }; } diff --git a/lib/src/model/sdk_publish_model.dart b/lib/src/model/sdk_publish_model.dart index 6076e351..19e5b7c3 100644 --- a/lib/src/model/sdk_publish_model.dart +++ b/lib/src/model/sdk_publish_model.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:codelessly_api/codelessly_api.dart'; -import 'package:codelessly_json_annotation/codelessly_json_annotation.dart'; +import 'package:json_annotation/json_annotation.dart'; import 'package:equatable/equatable.dart'; import 'model_http_request.dart'; @@ -16,7 +16,7 @@ part 'sdk_publish_model.g.dart'; /// The [dataMap] is the actual data map that holds the data. typedef LookupMapRecord = ({ Map lookupMap, - Map dataMap + Map dataMap, }); /// An extension type that allows for automatic correction of custom ids to @@ -171,20 +171,20 @@ class SDKPublishModel extends PrivacyBase { final List disabledLayouts; static Map serializeLookupMapFonts( - LookupMap map) => - map.dataMap.map((key, value) => MapEntry(key, value.toJson())); + LookupMap map, + ) => map.dataMap.map((key, value) => MapEntry(key, value.toJson())); static Map serializeLookupMapLayouts( - LookupMap map) => - map.dataMap.map((key, value) => MapEntry(key, value.toJson())); + LookupMap map, + ) => map.dataMap.map((key, value) => MapEntry(key, value.toJson())); static Map serializeLookupMapConditions( - LookupMap map) => - map.dataMap.map((key, value) => MapEntry(key, value.toJson())); + LookupMap map, + ) => map.dataMap.map((key, value) => MapEntry(key, value.toJson())); static Map serializeLookupMapVariables( - LookupMap map) => - map.dataMap.map((key, value) => MapEntry(key, value.toJson())); + LookupMap map, + ) => map.dataMap.map((key, value) => MapEntry(key, value.toJson())); static Object? readLookupMap(Map json, String key) { return {'layoutIDMap': json['layoutIDMap'], key: json[key]}; @@ -203,8 +203,8 @@ class SDKPublishModel extends PrivacyBase { lookupMap: layoutIDMap, dataMap: { for (final item in data.entries) - item.key: SDKPublishLayout.fromJson(item.value) - } + item.key: SDKPublishLayout.fromJson(item.value), + }, )); } @@ -215,8 +215,8 @@ class SDKPublishModel extends PrivacyBase { lookupMap: layoutIDMap, dataMap: { for (final item in data.entries) - item.key: SDKPublishFont.fromJson(item.value) - } + item.key: SDKPublishFont.fromJson(item.value), + }, )); } @@ -228,8 +228,8 @@ class SDKPublishModel extends PrivacyBase { lookupMap: layoutIDMap, dataMap: { for (final item in data.entries) - item.key: SDKLayoutVariables.fromJson(item.value) - } + item.key: SDKLayoutVariables.fromJson(item.value), + }, )); } @@ -241,8 +241,8 @@ class SDKPublishModel extends PrivacyBase { lookupMap: layoutIDMap, dataMap: { for (final item in data.entries) - item.key: SDKLayoutConditions.fromJson(item.value) - } + item.key: SDKLayoutConditions.fromJson(item.value), + }, )); } @@ -271,28 +271,36 @@ class SDKPublishModel extends PrivacyBase { required super.users, required super.roles, required super.public, - }) : layoutIDMap = layoutIDMap ?? {}, - pages = pages ?? [], - updates = updates ?? SDKPublishUpdates(), - layouts = layouts ?? - LookupMap( - (lookupMap: layoutIDMap ?? {}, dataMap: {}), - ), - fonts = fonts ?? - LookupMap( - (lookupMap: layoutIDMap ?? {}, dataMap: {}), - ), - variables = variables ?? - LookupMap( - (lookupMap: layoutIDMap ?? {}, dataMap: {}), - ), - conditions = conditions ?? - LookupMap( - (lookupMap: layoutIDMap ?? {}, dataMap: {}), - ), - apis = apis ?? {}, - lastUpdated = lastUpdated ?? DateTime.now(), - disabledLayouts = disabledLayouts ?? []; + }) : layoutIDMap = layoutIDMap ?? {}, + pages = pages ?? [], + updates = updates ?? SDKPublishUpdates(), + layouts = + layouts ?? + LookupMap(( + lookupMap: layoutIDMap ?? {}, + dataMap: {}, + )), + fonts = + fonts ?? + LookupMap(( + lookupMap: layoutIDMap ?? {}, + dataMap: {}, + )), + variables = + variables ?? + LookupMap(( + lookupMap: layoutIDMap ?? {}, + dataMap: {}, + )), + conditions = + conditions ?? + LookupMap(( + lookupMap: layoutIDMap ?? {}, + dataMap: {}, + )), + apis = apis ?? {}, + lastUpdated = lastUpdated ?? DateTime.now(), + disabledLayouts = disabledLayouts ?? []; /// Creates a new instance of [SDKPublishModel] with privacy inherited from /// another [PrivacyBase]. @@ -316,25 +324,29 @@ class SDKPublishModel extends PrivacyBase { // Privacy required super.privacy, - }) : layoutIDMap = layoutIDMap ?? {}, - pages = pages ?? [], - updates = updates ?? SDKPublishUpdates(), - layouts = LookupMap( - (lookupMap: layoutIDMap ?? {}, dataMap: layouts ?? {}), - ), - fonts = LookupMap( - (lookupMap: layoutIDMap ?? {}, dataMap: fonts ?? {}), - ), - variables = LookupMap( - (lookupMap: layoutIDMap ?? {}, dataMap: variables ?? {}), - ), - conditions = LookupMap( - (lookupMap: layoutIDMap ?? {}, dataMap: conditions ?? {}), - ), - lastUpdated = lastUpdated ?? DateTime.now(), - apis = apis ?? {}, - disabledLayouts = disabledLayouts ?? [], - super.private(); + }) : layoutIDMap = layoutIDMap ?? {}, + pages = pages ?? [], + updates = updates ?? SDKPublishUpdates(), + layouts = LookupMap(( + lookupMap: layoutIDMap ?? {}, + dataMap: layouts ?? {}, + )), + fonts = LookupMap(( + lookupMap: layoutIDMap ?? {}, + dataMap: fonts ?? {}, + )), + variables = LookupMap(( + lookupMap: layoutIDMap ?? {}, + dataMap: variables ?? {}, + )), + conditions = LookupMap(( + lookupMap: layoutIDMap ?? {}, + dataMap: conditions ?? {}, + )), + lastUpdated = lastUpdated ?? DateTime.now(), + apis = apis ?? {}, + disabledLayouts = disabledLayouts ?? [], + super.private(); /// Creates a new instance of [SDKPublishModel] from a JSON map. factory SDKPublishModel.fromJson(Map json) { @@ -396,22 +408,22 @@ class SDKPublishModel extends PrivacyBase { @override List get props => [ - ...super.props, - projectId, - fonts, - pages, - updates, - layoutIDMap, - layouts, - apis, - variables, - conditions, - entryLayoutId, - entryPageId, - entryCanvasId, - lastUpdated, - disabledLayouts, - ]; + ...super.props, + projectId, + fonts, + pages, + updates, + layoutIDMap, + layouts, + apis, + variables, + conditions, + entryLayoutId, + entryPageId, + entryCanvasId, + lastUpdated, + disabledLayouts, + ]; } @JsonSerializable() @@ -426,7 +438,7 @@ class SDKPublishLayout extends PrivacyBase { final String projectId; /// A list of the nodes that makes up this layout. - /// CanvasID -> Node> + /// `CanvasID -> Node>` @CanvasesMapConverter() final Map> canvases; @@ -461,9 +473,9 @@ class SDKPublishLayout extends PrivacyBase { required super.users, required super.roles, required super.public, - }) : breakpoints = breakpoints ?? [], - canvasIds = canvases.keys.toSet(), - hidden = hidden ?? false; + }) : breakpoints = breakpoints ?? [], + canvasIds = canvases.keys.toSet(), + hidden = hidden ?? false; /// Creates a new instance of [SDKPublishLayout] with privacy inherited from /// another [PrivacyBase]. @@ -478,10 +490,10 @@ class SDKPublishLayout extends PrivacyBase { // Privacy required super.privacy, - }) : breakpoints = breakpoints ?? [], - canvasIds = canvases.keys.toSet(), - hidden = hidden ?? false, - super.private(); + }) : breakpoints = breakpoints ?? [], + canvasIds = canvases.keys.toSet(), + hidden = hidden ?? false, + super.private(); /// Returns true if the layout is expired. @JsonKey(includeFromJson: false, includeToJson: false) @@ -524,15 +536,15 @@ class SDKPublishLayout extends PrivacyBase { @override List get props => [ - ...super.props, - id, - pageId, - projectId, - canvases, - lastUpdated, - breakpoints, - hidden, - ]; + ...super.props, + id, + pageId, + projectId, + canvases, + lastUpdated, + breakpoints, + hidden, + ]; } /// Represents a single variation of a common font. @@ -581,17 +593,17 @@ class SDKPublishFont extends PrivacyBase { /// Creates a new instance of [SDKPublishFont] with privacy inherited from /// another [PrivacyBase]. - SDKPublishFont.private( - {String? id, - required this.url, - required this.family, - required this.weight, - this.style, + SDKPublishFont.private({ + String? id, + required this.url, + required this.family, + required this.weight, + this.style, - // Privacy - required super.privacy}) - : id = id ?? family, - super.private(); + // Privacy + required super.privacy, + }) : id = id ?? family, + super.private(); /// Creates a copy of this instance with the provided parameters. SDKPublishFont copyWith({ @@ -623,27 +635,17 @@ class SDKPublishFont extends PrivacyBase { Map toJson() => _$SDKPublishFontToJson(this); @override - List get props => [ - ...super.props, - url, - family, - weight, - style, - ]; + List get props => [...super.props, url, family, weight, style]; } /// Defines an interpretation of what kind of update has been made to a given /// layout or font. /// /// This is used inside the data managers directly. -enum UpdateType { - delete, - add, - update, -} +enum UpdateType { delete, add, update } @JsonSerializable() -class SDKPublishUpdates with EquatableMixin { +class SDKPublishUpdates with Equatable { /// The last time the fonts collection received an update. @DateTimeMapConverter() final Map fonts; @@ -716,14 +718,14 @@ class SDKPublishUpdates with EquatableMixin { @override List get props => [ - fonts, - layouts, - apis, - variables, - conditions, - layoutFonts, - layoutApis, - ]; + fonts, + layouts, + apis, + variables, + conditions, + layoutFonts, + layoutApis, + ]; /// Creates a new instance of [SDKPublishUpdates] from a JSON map. factory SDKPublishUpdates.fromJson(Map json) => diff --git a/lib/src/model/sdk_publish_model.g.dart b/lib/src/model/sdk_publish_model.g.dart index 4a7b6156..2e10e382 100644 --- a/lib/src/model/sdk_publish_model.g.dart +++ b/lib/src/model/sdk_publish_model.g.dart @@ -7,260 +7,223 @@ part of 'sdk_publish_model.dart'; // ************************************************************************** SDKPublishModel _$SDKPublishModelFromJson(Map json) => SDKPublishModel( - projectId: json['projectId'] as String, - updates: json['updates'] == null - ? null - : SDKPublishUpdates.fromJson( - Map.from(json['updates'] as Map)), - pages: - (json['pages'] as List?)?.map((e) => e as String).toList(), - layoutIDMap: (json['layoutIDMap'] as Map?)?.map( - (k, e) => MapEntry(k as String, e as String), - ), - fonts: SDKPublishModel.deserializeFonts( - SDKPublishModel.readLookupMap(json, 'fonts') as Map), - layouts: SDKPublishModel.deserializeLayouts( - SDKPublishModel.readLookupMap(json, 'layouts') as Map), - variables: SDKPublishModel.deserializeVariables( - SDKPublishModel.readLookupMap(json, 'variables') as Map), - conditions: SDKPublishModel.deserializeConditions( - SDKPublishModel.readLookupMap(json, 'conditions') as Map), - apis: (json['apis'] as Map?)?.map( - (k, e) => MapEntry(k as String, - HttpApiData.fromJson(Map.from(e as Map))), - ), - entryLayoutId: json['entryLayoutId'] as String?, - entryPageId: json['entryPageId'] as String?, - entryCanvasId: json['entryCanvasId'] as String?, - lastUpdated: const DateTimeConverter() - .fromJson((json['lastUpdated'] as num?)?.toInt()), - disabledLayouts: (json['disabledLayouts'] as List?) - ?.map((e) => e as String) - .toList(), - owner: json['owner'] as String, - teams: (json['teams'] as List?)?.map((e) => e as String).toSet(), - users: (json['users'] as List?)?.map((e) => e as String).toSet(), - roles: (json['roles'] as Map).map( - (k, e) => MapEntry(k as String, Role.fromJson(e as String)), - ), - public: json['public'] as bool?, - ); + projectId: json['projectId'] as String, + updates: json['updates'] == null + ? null + : SDKPublishUpdates.fromJson( + Map.from(json['updates'] as Map), + ), + pages: (json['pages'] as List?)?.map((e) => e as String).toList(), + layoutIDMap: (json['layoutIDMap'] as Map?)?.map( + (k, e) => MapEntry(k as String, e as String), + ), + fonts: SDKPublishModel.deserializeFonts( + SDKPublishModel.readLookupMap(json, 'fonts') as Map, + ), + layouts: SDKPublishModel.deserializeLayouts( + SDKPublishModel.readLookupMap(json, 'layouts') as Map, + ), + variables: SDKPublishModel.deserializeVariables( + SDKPublishModel.readLookupMap(json, 'variables') as Map, + ), + conditions: SDKPublishModel.deserializeConditions( + SDKPublishModel.readLookupMap(json, 'conditions') as Map, + ), + apis: (json['apis'] as Map?)?.map( + (k, e) => MapEntry( + k as String, + HttpApiData.fromJson(Map.from(e as Map)), + ), + ), + entryLayoutId: json['entryLayoutId'] as String?, + entryPageId: json['entryPageId'] as String?, + entryCanvasId: json['entryCanvasId'] as String?, + lastUpdated: const DateTimeConverter().fromJson( + (json['lastUpdated'] as num?)?.toInt(), + ), + disabledLayouts: (json['disabledLayouts'] as List?) + ?.map((e) => e as String) + .toList(), + owner: json['owner'] as String, + teams: (json['teams'] as List?)?.map((e) => e as String).toSet(), + users: (json['users'] as List?)?.map((e) => e as String).toSet(), + roles: (json['roles'] as Map).map( + (k, e) => MapEntry(k as String, Role.fromJson(e as String)), + ), + public: json['public'] as bool?, +); -Map _$SDKPublishModelToJson(SDKPublishModel instance) { - final val = { - 'owner': instance.owner, - 'users': instance.users.toList(), - 'roles': instance.roles.map((k, e) => MapEntry(k, e.toJson())), - 'teams': instance.teams.toList(), - 'public': instance.public, - 'projectId': instance.projectId, - 'fonts': SDKPublishModel.serializeLookupMapFonts(instance.fonts), - 'layouts': SDKPublishModel.serializeLookupMapLayouts(instance.layouts), - 'pages': instance.pages, - 'updates': instance.updates.toJson(), - 'apis': instance.apis.map((k, e) => MapEntry(k, e.toJson())), - 'variables': - SDKPublishModel.serializeLookupMapVariables(instance.variables), - 'conditions': - SDKPublishModel.serializeLookupMapConditions(instance.conditions), - }; - - void writeNotNull( - String key, dynamic value, dynamic jsonValue, dynamic defaultValue) { - final bool serialize = - shouldSerialize(key, value, jsonValue, defaultValue, true); - - if (serialize) { - val[key] = jsonValue; - } - } - - writeNotNull( - 'entryLayoutId', instance.entryLayoutId, instance.entryLayoutId, null); - writeNotNull('entryPageId', instance.entryPageId, instance.entryPageId, null); - writeNotNull( - 'entryCanvasId', instance.entryCanvasId, instance.entryCanvasId, null); - writeNotNull('lastUpdated', instance.lastUpdated, - const DateTimeConverter().toJson(instance.lastUpdated), null); - val['layoutIDMap'] = instance.layoutIDMap; - val['disabledLayouts'] = instance.disabledLayouts; - return val; -} +Map _$SDKPublishModelToJson( + SDKPublishModel instance, +) => { + 'owner': instance.owner, + 'users': instance.users.toList(), + 'roles': instance.roles.map((k, e) => MapEntry(k, e.toJson())), + 'teams': instance.teams.toList(), + 'public': instance.public, + 'projectId': instance.projectId, + 'fonts': SDKPublishModel.serializeLookupMapFonts(instance.fonts), + 'layouts': SDKPublishModel.serializeLookupMapLayouts(instance.layouts), + 'pages': instance.pages, + 'updates': instance.updates.toJson(), + 'apis': instance.apis.map((k, e) => MapEntry(k, e.toJson())), + 'variables': SDKPublishModel.serializeLookupMapVariables(instance.variables), + 'conditions': SDKPublishModel.serializeLookupMapConditions( + instance.conditions, + ), + 'entryLayoutId': ?instance.entryLayoutId, + 'entryPageId': ?instance.entryPageId, + 'entryCanvasId': ?instance.entryCanvasId, + 'lastUpdated': ?const DateTimeConverter().toJson(instance.lastUpdated), + 'layoutIDMap': instance.layoutIDMap, + 'disabledLayouts': instance.disabledLayouts, +}; SDKPublishLayout _$SDKPublishLayoutFromJson(Map json) => SDKPublishLayout( - id: json['id'] as String, - pageId: json['pageId'] as String, - projectId: json['projectId'] as String, - canvases: const CanvasesMapConverter() - .fromJson(json['canvases'] as Map), - lastUpdated: const DateTimeConverter() - .fromJson((json['lastUpdated'] as num?)?.toInt()), - breakpoints: (json['breakpoints'] as List?) - ?.map((e) => Breakpoint.fromJson(e as Map)) - .toList(), - hidden: json['hidden'] as bool?, - owner: json['owner'] as String, - teams: (json['teams'] as List?)?.map((e) => e as String).toSet(), - users: (json['users'] as List?)?.map((e) => e as String).toSet(), - roles: (json['roles'] as Map).map( - (k, e) => MapEntry(k as String, Role.fromJson(e as String)), - ), - public: json['public'] as bool?, - ); - -Map _$SDKPublishLayoutToJson(SDKPublishLayout instance) { - final val = { - 'owner': instance.owner, - 'users': instance.users.toList(), - 'roles': instance.roles.map((k, e) => MapEntry(k, e.toJson())), - 'teams': instance.teams.toList(), - 'public': instance.public, - 'id': instance.id, - 'pageId': instance.pageId, - 'projectId': instance.projectId, - 'canvases': const CanvasesMapConverter().toJson(instance.canvases), - 'breakpoints': instance.breakpoints.map((e) => e.toJson()).toList(), - }; - - void writeNotNull( - String key, dynamic value, dynamic jsonValue, dynamic defaultValue) { - final bool serialize = - shouldSerialize(key, value, jsonValue, defaultValue, true); + id: json['id'] as String, + pageId: json['pageId'] as String, + projectId: json['projectId'] as String, + canvases: const CanvasesMapConverter().fromJson( + json['canvases'] as Map, + ), + lastUpdated: const DateTimeConverter().fromJson( + (json['lastUpdated'] as num?)?.toInt(), + ), + breakpoints: (json['breakpoints'] as List?) + ?.map((e) => Breakpoint.fromJson(e as Map)) + .toList(), + hidden: json['hidden'] as bool?, + owner: json['owner'] as String, + teams: (json['teams'] as List?)?.map((e) => e as String).toSet(), + users: (json['users'] as List?)?.map((e) => e as String).toSet(), + roles: (json['roles'] as Map).map( + (k, e) => MapEntry(k as String, Role.fromJson(e as String)), + ), + public: json['public'] as bool?, +); - if (serialize) { - val[key] = jsonValue; - } - } - - writeNotNull('lastUpdated', instance.lastUpdated, - const DateTimeConverter().toJson(instance.lastUpdated), null); - val['hidden'] = instance.hidden; - return val; -} +Map _$SDKPublishLayoutToJson(SDKPublishLayout instance) => + { + 'owner': instance.owner, + 'users': instance.users.toList(), + 'roles': instance.roles.map((k, e) => MapEntry(k, e.toJson())), + 'teams': instance.teams.toList(), + 'public': instance.public, + 'id': instance.id, + 'pageId': instance.pageId, + 'projectId': instance.projectId, + 'canvases': const CanvasesMapConverter().toJson(instance.canvases), + 'breakpoints': instance.breakpoints.map((e) => e.toJson()).toList(), + 'lastUpdated': ?const DateTimeConverter().toJson(instance.lastUpdated), + 'hidden': instance.hidden, + }; SDKPublishFont _$SDKPublishFontFromJson(Map json) => SDKPublishFont( - id: json['id'] as String?, - url: json['url'] as String, - family: json['family'] as String, - weight: json['weight'] as String, - style: json['style'] as String?, - owner: json['owner'] as String, - teams: (json['teams'] as List?)?.map((e) => e as String).toSet(), - users: (json['users'] as List?)?.map((e) => e as String).toSet(), - roles: (json['roles'] as Map).map( - (k, e) => MapEntry(k as String, Role.fromJson(e as String)), - ), - public: json['public'] as bool?, - ); - -Map _$SDKPublishFontToJson(SDKPublishFont instance) { - final val = { - 'owner': instance.owner, - 'users': instance.users.toList(), - 'roles': instance.roles.map((k, e) => MapEntry(k, e.toJson())), - 'teams': instance.teams.toList(), - 'public': instance.public, - 'id': instance.id, - 'url': instance.url, - 'family': instance.family, - 'weight': instance.weight, - }; - - void writeNotNull( - String key, dynamic value, dynamic jsonValue, dynamic defaultValue) { - final bool serialize = - shouldSerialize(key, value, jsonValue, defaultValue, true); + id: json['id'] as String?, + url: json['url'] as String, + family: json['family'] as String, + weight: json['weight'] as String, + style: json['style'] as String?, + owner: json['owner'] as String, + teams: (json['teams'] as List?)?.map((e) => e as String).toSet(), + users: (json['users'] as List?)?.map((e) => e as String).toSet(), + roles: (json['roles'] as Map).map( + (k, e) => MapEntry(k as String, Role.fromJson(e as String)), + ), + public: json['public'] as bool?, +); - if (serialize) { - val[key] = jsonValue; - } - } - - writeNotNull('style', instance.style, instance.style, null); - return val; -} +Map _$SDKPublishFontToJson(SDKPublishFont instance) => + { + 'owner': instance.owner, + 'users': instance.users.toList(), + 'roles': instance.roles.map((k, e) => MapEntry(k, e.toJson())), + 'teams': instance.teams.toList(), + 'public': instance.public, + 'id': instance.id, + 'url': instance.url, + 'family': instance.family, + 'weight': instance.weight, + 'style': ?instance.style, + }; SDKPublishUpdates _$SDKPublishUpdatesFromJson(Map json) => SDKPublishUpdates( - fonts: json['fonts'] == null - ? const {} - : const DateTimeMapConverter() - .fromJson(json['fonts'] as Map), - layouts: json['layouts'] == null - ? const {} - : const DateTimeMapConverter() - .fromJson(json['layouts'] as Map), - apis: json['apis'] == null - ? const {} - : const DateTimeMapConverter() - .fromJson(json['apis'] as Map), - variables: json['variables'] == null - ? const {} - : const DateTimeMapConverter() - .fromJson(json['variables'] as Map), - conditions: json['conditions'] == null - ? const {} - : const DateTimeMapConverter() - .fromJson(json['conditions'] as Map), - layoutFonts: (json['layoutFonts'] as Map?)?.map( - (k, e) => MapEntry(k as String, - (e as List).map((e) => e as String).toSet()), - ) ?? - {}, - layoutApis: (json['layoutApis'] as Map?)?.map( - (k, e) => MapEntry(k as String, - (e as List).map((e) => e as String).toSet()), - ) ?? - {}, - ); - -Map _$SDKPublishUpdatesToJson(SDKPublishUpdates instance) { - final val = {}; - - void writeNotNull( - String key, dynamic value, dynamic jsonValue, dynamic defaultValue) { - final bool serialize = - shouldSerialize(key, value, jsonValue, defaultValue, true); - - if (serialize) { - val[key] = jsonValue; - } - } + fonts: json['fonts'] == null + ? const {} + : const DateTimeMapConverter().fromJson( + json['fonts'] as Map, + ), + layouts: json['layouts'] == null + ? const {} + : const DateTimeMapConverter().fromJson( + json['layouts'] as Map, + ), + apis: json['apis'] == null + ? const {} + : const DateTimeMapConverter().fromJson( + json['apis'] as Map, + ), + variables: json['variables'] == null + ? const {} + : const DateTimeMapConverter().fromJson( + json['variables'] as Map, + ), + conditions: json['conditions'] == null + ? const {} + : const DateTimeMapConverter().fromJson( + json['conditions'] as Map, + ), + layoutFonts: + (json['layoutFonts'] as Map?)?.map( + (k, e) => MapEntry( + k as String, + (e as List).map((e) => e as String).toSet(), + ), + ) ?? + const {}, + layoutApis: + (json['layoutApis'] as Map?)?.map( + (k, e) => MapEntry( + k as String, + (e as List).map((e) => e as String).toSet(), + ), + ) ?? + const {}, +); - writeNotNull('fonts', instance.fonts, - const DateTimeMapConverter().toJson(instance.fonts), const {}); - writeNotNull('layouts', instance.layouts, - const DateTimeMapConverter().toJson(instance.layouts), const {}); - writeNotNull('apis', instance.apis, - const DateTimeMapConverter().toJson(instance.apis), const {}); - writeNotNull('variables', instance.variables, - const DateTimeMapConverter().toJson(instance.variables), const {}); - writeNotNull('conditions', instance.conditions, - const DateTimeMapConverter().toJson(instance.conditions), const {}); - writeNotNull('layoutFonts', instance.layoutFonts, - instance.layoutFonts.map((k, e) => MapEntry(k, e.toList())), const {}); - writeNotNull('layoutApis', instance.layoutApis, - instance.layoutApis.map((k, e) => MapEntry(k, e.toList())), const {}); - return val; -} +Map _$SDKPublishUpdatesToJson( + SDKPublishUpdates instance, +) => { + 'fonts': const DateTimeMapConverter().toJson(instance.fonts), + 'layouts': const DateTimeMapConverter().toJson(instance.layouts), + 'apis': const DateTimeMapConverter().toJson(instance.apis), + 'variables': const DateTimeMapConverter().toJson(instance.variables), + 'conditions': const DateTimeMapConverter().toJson(instance.conditions), + 'layoutFonts': instance.layoutFonts.map((k, e) => MapEntry(k, e.toList())), + 'layoutApis': instance.layoutApis.map((k, e) => MapEntry(k, e.toList())), +}; SDKLayoutVariables _$SDKLayoutVariablesFromJson(Map json) => SDKLayoutVariables( - id: json['id'] as String, - variables: (json['variables'] as Map).map( + id: json['id'] as String, + variables: (json['variables'] as Map).map( + (k, e) => MapEntry( + k as String, + (e as Map).map( (k, e) => MapEntry( - k as String, - (e as Map).map( - (k, e) => MapEntry(k as String, - VariableData.fromJson(Map.from(e as Map))), - )), - ), - owner: json['owner'] as String, - teams: (json['teams'] as List?)?.map((e) => e as String).toSet(), - users: (json['users'] as List?)?.map((e) => e as String).toSet(), - roles: (json['roles'] as Map).map( - (k, e) => MapEntry(k as String, Role.fromJson(e as String)), + k as String, + VariableData.fromJson(Map.from(e as Map)), + ), ), - public: json['public'] as bool?, - ); + ), + ), + owner: json['owner'] as String, + teams: (json['teams'] as List?)?.map((e) => e as String).toSet(), + users: (json['users'] as List?)?.map((e) => e as String).toSet(), + roles: (json['roles'] as Map).map( + (k, e) => MapEntry(k as String, Role.fromJson(e as String)), + ), + public: json['public'] as bool?, +); Map _$SDKLayoutVariablesToJson(SDKLayoutVariables instance) => { @@ -270,8 +233,9 @@ Map _$SDKLayoutVariablesToJson(SDKLayoutVariables instance) => 'teams': instance.teams.toList(), 'public': instance.public, 'id': instance.id, - 'variables': instance.variables - .map((k, e) => MapEntry(k, e.map((k, e) => MapEntry(k, e.toJson())))), + 'variables': instance.variables.map( + (k, e) => MapEntry(k, e.map((k, e) => MapEntry(k, e.toJson()))), + ), }; SDKLayoutConditions _$SDKLayoutConditionsFromJson(Map json) => @@ -279,11 +243,14 @@ SDKLayoutConditions _$SDKLayoutConditionsFromJson(Map json) => id: json['id'] as String, conditions: (json['conditions'] as Map).map( (k, e) => MapEntry( - k as String, - (e as Map).map( - (k, e) => MapEntry(k as String, - BaseCondition.fromJson(Map.from(e as Map))), - )), + k as String, + (e as Map).map( + (k, e) => MapEntry( + k as String, + BaseCondition.fromJson(Map.from(e as Map)), + ), + ), + ), ), owner: json['owner'] as String, teams: (json['teams'] as List?)?.map((e) => e as String).toSet(), @@ -295,14 +262,15 @@ SDKLayoutConditions _$SDKLayoutConditionsFromJson(Map json) => ); Map _$SDKLayoutConditionsToJson( - SDKLayoutConditions instance) => - { - 'owner': instance.owner, - 'users': instance.users.toList(), - 'roles': instance.roles.map((k, e) => MapEntry(k, e.toJson())), - 'teams': instance.teams.toList(), - 'public': instance.public, - 'id': instance.id, - 'conditions': instance.conditions - .map((k, e) => MapEntry(k, e.map((k, e) => MapEntry(k, e.toJson())))), - }; + SDKLayoutConditions instance, +) => { + 'owner': instance.owner, + 'users': instance.users.toList(), + 'roles': instance.roles.map((k, e) => MapEntry(k, e.toJson())), + 'teams': instance.teams.toList(), + 'public': instance.public, + 'id': instance.id, + 'conditions': instance.conditions.map( + (k, e) => MapEntry(k, e.map((k, e) => MapEntry(k, e.toJson()))), + ), +}; diff --git a/lib/src/transformers/defaults/default_properties.dart b/lib/src/transformers/defaults/default_properties.dart index 045047d9..0e688793 100644 --- a/lib/src/transformers/defaults/default_properties.dart +++ b/lib/src/transformers/defaults/default_properties.dart @@ -5,100 +5,100 @@ import '../../../codelessly_sdk.dart'; // TODO: InkWells. ListTileProperties get defaultListTileProperties => ListTileProperties( - isThreeLine: ListTileDefaults.isThreeLine, - dense: ListTileDefaults.dense, - visualDensity: ListTileDefaults.visualDensity, - shape: ListTileDefaults.shape, - selectedColor: ListTileDefaults.selectedColor.toColorRGBA(), - iconColor: ListTileDefaults.iconColor.toColorRGBA(), - textColor: ListTileDefaults.textColor.toColorRGBA(), - contentPadding: ListTileDefaults.contentPadding, - enabled: ListTileDefaults.enabled, - selected: ListTileDefaults.selected, - focusColor: ListTileDefaults.focusColor.toColorRGBA(), - hoverColor: ListTileDefaults.hoverColor.toColorRGBA(), - autofocus: ListTileDefaults.autofocus, - cornerRadius: ListTileDefaults.cornerRadius, - tileColor: ListTileDefaults.tileColor.toColorRGBA(), - selectedTileColor: ListTileDefaults.selectedTileColor.toColorRGBA(), - enableFeedback: ListTileDefaults.enableFeedback, - horizontalTitleGap: ListTileDefaults.horizontalTitleGap, - minVerticalPadding: ListTileDefaults.minVerticalPadding, - minLeadingWidth: ListTileDefaults.minLeadingWidth, - ); + isThreeLine: ListTileDefaults.isThreeLine, + dense: ListTileDefaults.dense, + visualDensity: ListTileDefaults.visualDensity, + shape: ListTileDefaults.shape, + selectedColor: ListTileDefaults.selectedColor.toColorRGBA(), + iconColor: ListTileDefaults.iconColor.toColorRGBA(), + textColor: ListTileDefaults.textColor.toColorRGBA(), + contentPadding: ListTileDefaults.contentPadding, + enabled: ListTileDefaults.enabled, + selected: ListTileDefaults.selected, + focusColor: ListTileDefaults.focusColor.toColorRGBA(), + hoverColor: ListTileDefaults.hoverColor.toColorRGBA(), + autofocus: ListTileDefaults.autofocus, + cornerRadius: ListTileDefaults.cornerRadius, + tileColor: ListTileDefaults.tileColor.toColorRGBA(), + selectedTileColor: ListTileDefaults.selectedTileColor.toColorRGBA(), + enableFeedback: ListTileDefaults.enableFeedback, + horizontalTitleGap: ListTileDefaults.horizontalTitleGap, + minVerticalPadding: ListTileDefaults.minVerticalPadding, + minLeadingWidth: ListTileDefaults.minLeadingWidth, +); TextFieldProperties get defaultTextFieldProperties => TextFieldProperties( - autoCorrect: TextFieldDefaults.autoCorrect, - autoFocus: TextFieldDefaults.autoFocus, - enableInteractiveSelection: TextFieldDefaults.enableInteractiveSelection, - enabled: TextFieldDefaults.enabled, - obscureText: TextFieldDefaults.obscureText, - readOnly: TextFieldDefaults.readOnly, - showCursor: TextFieldDefaults.showCursor, - keyboardType: TextFieldDefaults.keyboardType, - selectionHeightStyle: TextFieldDefaults.selectionHeightStyle, - selectionWidthStyle: TextFieldDefaults.selectionWidthStyle, - textAlign: TextFieldDefaults.textAlign, - textAlignVertical: TextFieldDefaults.textAlignVertical, - cursorColor: TextFieldDefaults.cursorColor.toColorRGB(), - cursorHeight: TextFieldDefaults.cursorHeight, - cursorWidth: TextFieldDefaults.cursorWidth, - cursorRadius: TextFieldDefaults.cursorRadius, - maxLines: TextFieldDefaults.maxLines, - obscuringCharacter: TextFieldDefaults.obscuringCharacter, - inputStyle: TextFieldDefaults.style, - decoration: TextFieldDefaults.decoration, - expands: false, - ); + autoCorrect: TextFieldDefaults.autoCorrect, + autoFocus: TextFieldDefaults.autoFocus, + enableInteractiveSelection: TextFieldDefaults.enableInteractiveSelection, + enabled: TextFieldDefaults.enabled, + obscureText: TextFieldDefaults.obscureText, + readOnly: TextFieldDefaults.readOnly, + showCursor: TextFieldDefaults.showCursor, + keyboardType: TextFieldDefaults.keyboardType, + selectionHeightStyle: TextFieldDefaults.selectionHeightStyle, + selectionWidthStyle: TextFieldDefaults.selectionWidthStyle, + textAlign: TextFieldDefaults.textAlign, + textAlignVertical: TextFieldDefaults.textAlignVertical, + cursorColor: TextFieldDefaults.cursorColor.toColorRGB(), + cursorHeight: TextFieldDefaults.cursorHeight, + cursorWidth: TextFieldDefaults.cursorWidth, + cursorRadius: TextFieldDefaults.cursorRadius, + maxLines: TextFieldDefaults.maxLines, + obscuringCharacter: TextFieldDefaults.obscuringCharacter, + inputStyle: TextFieldDefaults.style, + decoration: TextFieldDefaults.decoration, + expands: false, +); ButtonProperties get defaultButtonProperties => ButtonProperties( - buttonType: ButtonTypeEnum.elevated, - buttonColor: ButtonDefaults.buttonColor.toColorRGBA(), - shadowColor: ButtonDefaults.shadowColor.toColorRGBA(), - label: ButtonDefaults.label, - elevation: ButtonDefaults.elevation, - cornerRadius: ButtonDefaults.cornerRadius, - enabled: ButtonDefaults.enabled, - labelAlignment: ButtonDefaults.labelAlignment, - placement: ButtonDefaults.placement, - gap: ButtonDefaults.gap, - labelStyle: ButtonDefaults.labelStyle, - icon: ButtonDefaults.icon, - ); + buttonType: ButtonTypeEnum.elevated, + buttonColor: ButtonDefaults.buttonColor.toColorRGBA(), + shadowColor: ButtonDefaults.shadowColor.toColorRGBA(), + label: ButtonDefaults.label, + elevation: ButtonDefaults.elevation, + cornerRadius: ButtonDefaults.cornerRadius, + enabled: ButtonDefaults.enabled, + labelAlignment: ButtonDefaults.labelAlignment, + placement: ButtonDefaults.placement, + gap: ButtonDefaults.gap, + labelStyle: ButtonDefaults.labelStyle, + icon: ButtonDefaults.icon, +); CheckboxProperties get defaultCheckboxProperties => CheckboxProperties( - checkColor: CheckBoxDefaults.checkColor.toColorRGBA(), - activeColor: CheckBoxDefaults.activeColor.toColorRGBA(), - hoverColor: CheckBoxDefaults.hoverColor.toColorRGBA(), - focusColor: CheckBoxDefaults.focusColor.toColorRGBA(), - tristate: CheckBoxDefaults.tristate, - borderColor: CheckBoxDefaults.borderColor.toColorRGBA(), - borderWidth: CheckBoxDefaults.borderWidth, - cornerRadius: CheckBoxDefaults.cornerRadius, - splashRadius: CheckBoxDefaults.splashRadius, - autofocus: CheckBoxDefaults.autofocus, - ); + checkColor: CheckBoxDefaults.checkColor.toColorRGBA(), + activeColor: CheckBoxDefaults.activeColor.toColorRGBA(), + hoverColor: CheckBoxDefaults.hoverColor.toColorRGBA(), + focusColor: CheckBoxDefaults.focusColor.toColorRGBA(), + tristate: CheckBoxDefaults.tristate, + borderColor: CheckBoxDefaults.borderColor.toColorRGBA(), + borderWidth: CheckBoxDefaults.borderWidth, + cornerRadius: CheckBoxDefaults.cornerRadius, + splashRadius: CheckBoxDefaults.splashRadius, + autofocus: CheckBoxDefaults.autofocus, +); SwitchProperties get defaultSwitchProperties => SwitchProperties( - activeTrackColor: SwitchDefaults.activeTrackColor.toColorRGBA(), - inactiveTrackColor: SwitchDefaults.inactiveTrackColor.toColorRGBA(), - activeThumbColor: SwitchDefaults.activeThumbColor.toColorRGBA(), - inactiveThumbColor: SwitchDefaults.inactiveThumbColor.toColorRGBA(), - hoverColor: SwitchDefaults.hoverColor.toColorRGBA(), - focusColor: SwitchDefaults.focusColor.toColorRGBA(), - splashRadius: SwitchDefaults.splashRadius, - autofocus: SwitchDefaults.autofocus, - ); + activeTrackColor: SwitchDefaults.activeTrackColor.toColorRGBA(), + inactiveTrackColor: SwitchDefaults.inactiveTrackColor.toColorRGBA(), + activeThumbColor: SwitchDefaults.activeThumbColor.toColorRGBA(), + inactiveThumbColor: SwitchDefaults.inactiveThumbColor.toColorRGBA(), + hoverColor: SwitchDefaults.hoverColor.toColorRGBA(), + focusColor: SwitchDefaults.focusColor.toColorRGBA(), + splashRadius: SwitchDefaults.splashRadius, + autofocus: SwitchDefaults.autofocus, +); RadioProperties get defaultRadioProperties => RadioProperties( - activeColor: RadioDefaults.activeColor.toColorRGBA(), - inactiveColor: RadioDefaults.inactiveColor.toColorRGBA(), - hoverColor: RadioDefaults.hoverColor.toColorRGBA(), - focusColor: RadioDefaults.focusColor.toColorRGBA(), - splashRadius: RadioDefaults.splashRadius, - autofocus: RadioDefaults.autofocus, - toggleable: RadioDefaults.toggleable, - ); + activeColor: RadioDefaults.activeColor.toColorRGBA(), + inactiveColor: RadioDefaults.inactiveColor.toColorRGBA(), + hoverColor: RadioDefaults.hoverColor.toColorRGBA(), + focusColor: RadioDefaults.focusColor.toColorRGBA(), + splashRadius: RadioDefaults.splashRadius, + autofocus: RadioDefaults.autofocus, + toggleable: RadioDefaults.toggleable, +); M2NavigationBarProperties get defaultM2FixedNavigationBarProperties => M2NavigationBarProperties( @@ -107,15 +107,17 @@ M2NavigationBarProperties get defaultM2FixedNavigationBarProperties => navigationBarType: M2NavigationBarDefaults.navigationBarType, landscapeLayout: M2NavigationBarDefaults.landscapeLayout, selectedLabelStyle: TextProp.general( - fontSize: M2NavigationBarDefaults.labelFontSize, - fills: [M2NavigationBarDefaults.selectedLabelColorDark.toPaint()]), + fontSize: M2NavigationBarDefaults.labelFontSize, + fills: [M2NavigationBarDefaults.selectedLabelColorDark.toPaint()], + ), unselectedLabelStyle: TextProp.general( - fontSize: M2NavigationBarDefaults.labelFontSize, - fills: [M2NavigationBarDefaults.unselectedLabelColorDark.toPaint()]), - selectedIconColor: - M2NavigationBarDefaults.selectedIconColor.toColorRGBA(), - unselectedIconColor: - M2NavigationBarDefaults.unselectedIconColor.toColorRGBA(), + fontSize: M2NavigationBarDefaults.labelFontSize, + fills: [M2NavigationBarDefaults.unselectedLabelColorDark.toPaint()], + ), + selectedIconColor: M2NavigationBarDefaults.selectedIconColor + .toColorRGBA(), + unselectedIconColor: M2NavigationBarDefaults.unselectedIconColor + .toColorRGBA(), unselectedIconSize: M2NavigationBarDefaults.unselectedIconSize, selectedIconSize: M2NavigationBarDefaults.selectedIconSize, labelBehavior: M2NavigationBarDefaults.labelBehavior, @@ -129,15 +131,17 @@ M2NavigationBarProperties get defaultM2ShiftingNavigationBarProperties => navigationBarType: M2NavigationBarDefaults.navigationBarType, landscapeLayout: M2NavigationBarDefaults.landscapeLayout, selectedLabelStyle: TextProp.general( - fontSize: M2NavigationBarDefaults.labelFontSize, - fills: [M2NavigationBarDefaults.selectedLabelColorLight.toPaint()]), + fontSize: M2NavigationBarDefaults.labelFontSize, + fills: [M2NavigationBarDefaults.selectedLabelColorLight.toPaint()], + ), unselectedLabelStyle: TextProp.general( - fontSize: M2NavigationBarDefaults.labelFontSize, - fills: [M2NavigationBarDefaults.unselectedLabelColorLight.toPaint()]), - selectedIconColor: - M2NavigationBarDefaults.selectedIconColor.toColorRGBA(), - unselectedIconColor: - M2NavigationBarDefaults.unselectedIconColor.toColorRGBA(), + fontSize: M2NavigationBarDefaults.labelFontSize, + fills: [M2NavigationBarDefaults.unselectedLabelColorLight.toPaint()], + ), + selectedIconColor: M2NavigationBarDefaults.selectedIconColor + .toColorRGBA(), + unselectedIconColor: M2NavigationBarDefaults.unselectedIconColor + .toColorRGBA(), unselectedIconSize: M2NavigationBarDefaults.unselectedIconSize, selectedIconSize: M2NavigationBarDefaults.selectedIconSize, labelBehavior: M2NavigationBarDefaults.labelBehavior, @@ -150,10 +154,10 @@ M3NavigationBarProperties get defaultM3NavigationBarProperties => elevation: M3NavigationBarDefaults.elevation, selectedLabelStyle: M3NavigationBarDefaults.selectedLabelStyle, unselectedLabelStyle: M3NavigationBarDefaults.unselectedLabelStyle, - unselectedIconColor: - M3NavigationBarDefaults.unselectedIconColor.toColorRGBA(), - selectedIconColor: - M3NavigationBarDefaults.selectedIconColor.toColorRGBA(), + unselectedIconColor: M3NavigationBarDefaults.unselectedIconColor + .toColorRGBA(), + selectedIconColor: M3NavigationBarDefaults.selectedIconColor + .toColorRGBA(), unselectedIconSize: M3NavigationBarDefaults.unselectedIconSize, selectedIconSize: M3NavigationBarDefaults.selectedIconSize, labelBehavior: M3NavigationBarDefaults.labelBehavior, @@ -162,61 +166,57 @@ M3NavigationBarProperties get defaultM3NavigationBarProperties => ); SliderProperties get defaultSliderProperties => SliderProperties( - activeTrackColor: SliderDefaults.activeTrackColor.toColorRGBA(), - inactiveTrackColor: SliderDefaults.inactiveTrackColor.toColorRGBA(), - overlayColor: SliderDefaults.overlayColor.toColorRGBA(), - thumbColor: SliderDefaults.thumbColor.toColorRGBA(), - autofocus: SliderDefaults.autofocus, - min: SliderDefaults.min, - max: SliderDefaults.max, - trackHeight: SliderDefaults.trackHeight, - label: SliderDefaults.label, - isDiscrete: SliderDefaults.isDiscrete, - showLabel: SliderDefaults.showLabel, - activeTickMarkColor: SliderDefaults.activeTickMarkColor.toColorRGBA(), - inactiveTickMarkColor: SliderDefaults.inactiveTickMarkColor.toColorRGBA(), - valueIndicatorColor: SliderDefaults.valueIndicatorColor.toColorRGBA(), - valueIndicatorTextColor: - SliderDefaults.valueIndicatorTextColor.toColorRGBA(), - valueIndicatorFontSize: SliderDefaults.valueIndicatorFontSize, - allowFractionalPoints: SliderDefaults.allowFractionalPoints, - thumbRadius: SliderDefaults.thumbRadius, - showThumb: SliderDefaults.showThumb, - trackShape: SliderDefaults.trackShape, - tickMarkRadius: SliderDefaults.tickMarkRadius, - valueIndicatorShape: SliderDefaults.valueIndicatorShape, - overlayRadius: SliderDefaults.overlayRadius, - deriveOverlayColorFromThumb: SliderDefaults.deriveOverlayColorFromThumb, - ); + activeTrackColor: SliderDefaults.activeTrackColor.toColorRGBA(), + inactiveTrackColor: SliderDefaults.inactiveTrackColor.toColorRGBA(), + overlayColor: SliderDefaults.overlayColor.toColorRGBA(), + thumbColor: SliderDefaults.thumbColor.toColorRGBA(), + autofocus: SliderDefaults.autofocus, + min: SliderDefaults.min, + max: SliderDefaults.max, + trackHeight: SliderDefaults.trackHeight, + label: SliderDefaults.label, + isDiscrete: SliderDefaults.isDiscrete, + showLabel: SliderDefaults.showLabel, + activeTickMarkColor: SliderDefaults.activeTickMarkColor.toColorRGBA(), + inactiveTickMarkColor: SliderDefaults.inactiveTickMarkColor.toColorRGBA(), + valueIndicatorColor: SliderDefaults.valueIndicatorColor.toColorRGBA(), + valueIndicatorTextColor: SliderDefaults.valueIndicatorTextColor.toColorRGBA(), + valueIndicatorFontSize: SliderDefaults.valueIndicatorFontSize, + allowFractionalPoints: SliderDefaults.allowFractionalPoints, + thumbRadius: SliderDefaults.thumbRadius, + showThumb: SliderDefaults.showThumb, + trackShape: SliderDefaults.trackShape, + tickMarkRadius: SliderDefaults.tickMarkRadius, + valueIndicatorShape: SliderDefaults.valueIndicatorShape, + overlayRadius: SliderDefaults.overlayRadius, + deriveOverlayColorFromThumb: SliderDefaults.deriveOverlayColorFromThumb, +); -ExpansionTileProperties get defaultExpansionTileProperties => - ExpansionTileProperties( - backgroundColor: ExpansionTileDefaults.backgroundColor.toColorRGBA(), - collapsedBackgroundColor: - ExpansionTileDefaults.collapsedBackgroundColor.toColorRGBA(), - initiallyExpanded: ExpansionTileDefaults.initiallyExpanded, - maintainState: ExpansionTileDefaults.maintainState, - tilePadding: ExpansionTileDefaults.tilePadding, - expandedAlignment: ExpansionTileDefaults.expandedAlignment, - expandedCrossAxisAlignment: - ExpansionTileDefaults.expandedCrossAxisAlignment, - childrenPadding: ExpansionTileDefaults.childrenPadding, - iconColor: ExpansionTileDefaults.iconColor.toColorRGBA(), - collapsedIconColor: - ExpansionTileDefaults.collapsedIconColor.toColorRGBA(), - textColor: ExpansionTileDefaults.textColor.toColorRGBA(), - collapsedTextColor: - ExpansionTileDefaults.collapsedTextColor.toColorRGBA(), - controlAffinity: ExpansionTileDefaults.controlAffinity, - visualDensity: ExpansionTileDefaults.visualDensity, - ); +ExpansionTileProperties +get defaultExpansionTileProperties => ExpansionTileProperties( + backgroundColor: ExpansionTileDefaults.backgroundColor.toColorRGBA(), + collapsedBackgroundColor: ExpansionTileDefaults.collapsedBackgroundColor + .toColorRGBA(), + initiallyExpanded: ExpansionTileDefaults.initiallyExpanded, + maintainState: ExpansionTileDefaults.maintainState, + tilePadding: ExpansionTileDefaults.tilePadding, + expandedAlignment: ExpansionTileDefaults.expandedAlignment, + expandedCrossAxisAlignment: ExpansionTileDefaults.expandedCrossAxisAlignment, + childrenPadding: ExpansionTileDefaults.childrenPadding, + iconColor: ExpansionTileDefaults.iconColor.toColorRGBA(), + collapsedIconColor: ExpansionTileDefaults.collapsedIconColor.toColorRGBA(), + textColor: ExpansionTileDefaults.textColor.toColorRGBA(), + collapsedTextColor: ExpansionTileDefaults.collapsedTextColor.toColorRGBA(), + controlAffinity: ExpansionTileDefaults.controlAffinity, + visualDensity: ExpansionTileDefaults.visualDensity, +); FloatingActionButtonProperties get defaultFloatingActionButtonProperties => FloatingActionButtonProperties( - backgroundColor: - FloatingActionButtonDefaults.backgroundColor.toColorRGBA(), - foregroundColor: - FloatingActionButtonDefaults.foregroundColor.toColorRGBA(), + backgroundColor: FloatingActionButtonDefaults.backgroundColor + .toColorRGBA(), + foregroundColor: FloatingActionButtonDefaults.foregroundColor + .toColorRGBA(), label: FloatingActionButtonDefaults.label, type: FloatingActionButtonDefaults.type, elevation: FloatingActionButtonDefaults.elevation, @@ -233,47 +233,47 @@ FloatingActionButtonProperties get defaultFloatingActionButtonProperties => ); DividerProperties get defaultDividerProperties => DividerProperties( - color: DividerDefaults.color.toColorRGBA(), - thickness: DividerDefaults.thickness, - indent: DividerDefaults.indent, - endIndent: DividerDefaults.endIndent, - ); + color: DividerDefaults.color.toColorRGBA(), + thickness: DividerDefaults.thickness, + indent: DividerDefaults.indent, + endIndent: DividerDefaults.endIndent, +); CupertinoLoadingIndicatorProperties - get defaultCupertinoLoadingIndicatorProperties => - CupertinoLoadingIndicatorProperties( - color: CupertinoLoadingIndicatorDefaults.color.toColorRGBA(), - radius: CupertinoLoadingIndicatorDefaults.radius, - ); +get defaultCupertinoLoadingIndicatorProperties => + CupertinoLoadingIndicatorProperties( + color: CupertinoLoadingIndicatorDefaults.color.toColorRGBA(), + radius: CupertinoLoadingIndicatorDefaults.radius, + ); MaterialLoadingIndicatorProperties - get defaultMaterialLoadingIndicatorProperties => - MaterialLoadingIndicatorProperties( - color: MaterialLoadingIndicatorDefaults.color.toColorRGBA(), - strokeWidth: MaterialLoadingIndicatorDefaults.strokeWidth, - ); +get defaultMaterialLoadingIndicatorProperties => + MaterialLoadingIndicatorProperties( + color: MaterialLoadingIndicatorDefaults.color.toColorRGBA(), + strokeWidth: MaterialLoadingIndicatorDefaults.strokeWidth, + ); ProgressBarProperties get defaultProgressBarProperties => ProgressBarProperties( - maxValue: ProgressBarDefaults.maxValue, - backgroundColor: ProgressBarDefaults.backgroundColor.toColorRGBA(), - progressColor: ProgressBarDefaults.progressColor.toColorRGBA(), - animate: ProgressBarDefaults.animate, - animationDurationInMillis: ProgressBarDefaults.animationDurationInMillis, - cornerRadius: ProgressBarDefaults.cornerRadius, - ); + maxValue: ProgressBarDefaults.maxValue, + backgroundColor: ProgressBarDefaults.backgroundColor.toColorRGBA(), + progressColor: ProgressBarDefaults.progressColor.toColorRGBA(), + animate: ProgressBarDefaults.animate, + animationDurationInMillis: ProgressBarDefaults.animationDurationInMillis, + cornerRadius: ProgressBarDefaults.cornerRadius, +); TabBarProperties get defaultTabBarProperties => TabBarProperties( - dividerColor: TabBarDefaults.dividerColor.toColorRGBA(), - indicatorColor: TabBarDefaults.indicatorColor.toColorRGBA(), - indicatorWeight: TabBarDefaults.indicatorWeight, - labelColor: TabBarDefaults.labelColor.toColorRGBA(), - labelPadding: TabBarDefaults.labelPadding, - labelStyle: TabBarDefaults.labelStyle, - unselectedLabelColor: TabBarDefaults.unselectedLabelColor.toColorRGBA(), - unselectedLabelStyle: TabBarDefaults.unselectedLabelStyle, - indicatorSize: TabBarDefaults.indicatorSize, - indicatorPadding: TabBarDefaults.indicatorPadding, - tabItemDirection: TabBarDefaults.tabItemDirection, - gap: TabBarDefaults.gap, - showDivider: false, - ); + dividerColor: TabBarDefaults.dividerColor.toColorRGBA(), + indicatorColor: TabBarDefaults.indicatorColor.toColorRGBA(), + indicatorWeight: TabBarDefaults.indicatorWeight, + labelColor: TabBarDefaults.labelColor.toColorRGBA(), + labelPadding: TabBarDefaults.labelPadding, + labelStyle: TabBarDefaults.labelStyle, + unselectedLabelColor: TabBarDefaults.unselectedLabelColor.toColorRGBA(), + unselectedLabelStyle: TabBarDefaults.unselectedLabelStyle, + indicatorSize: TabBarDefaults.indicatorSize, + indicatorPadding: TabBarDefaults.indicatorPadding, + tabItemDirection: TabBarDefaults.tabItemDirection, + gap: TabBarDefaults.gap, + showDivider: false, +); diff --git a/lib/src/transformers/defaults/theme_defaults.dart b/lib/src/transformers/defaults/theme_defaults.dart index 27919670..ee5be900 100644 --- a/lib/src/transformers/defaults/theme_defaults.dart +++ b/lib/src/transformers/defaults/theme_defaults.dart @@ -59,8 +59,9 @@ class ButtonDefaults { static const Color shadowColor = Color(0xFFA5A5A5); static const String label = 'Button'; static const double elevation = 0; - static const CornerRadius cornerRadius = - CornerRadius.all(RadiusModel.circular(4)); + static const CornerRadius cornerRadius = CornerRadius.all( + RadiusModel.circular(4), + ); static const bool enabled = true; static const TextAlignHorizontalEnum labelAlignment = TextAlignHorizontalEnum.center; @@ -95,10 +96,8 @@ class TextFieldDefaults { static const int maxLines = 1; static const String obscuringCharacter = '•'; - static TextProp get style => TextProp.general( - fontSize: 14, - fills: [PaintModel.blackPaint], - ); + static TextProp get style => + TextProp.general(fontSize: 14, fills: [PaintModel.blackPaint]); static InputDecorationModel get decoration => InputDecorationModel(); static const Color fillColor = Color(0xffeeeeee); @@ -116,8 +115,9 @@ class CheckBoxDefaults { static const bool autofocus = false; static const bool tristate = false; static const double borderWidth = 1.5; - static const CornerRadius cornerRadius = - CornerRadius.all(RadiusModel.circular(3)); + static const CornerRadius cornerRadius = CornerRadius.all( + RadiusModel.circular(3), + ); CheckBoxDefaults._(); } @@ -150,8 +150,9 @@ class RadioDefaults { class AppBarDefaults { static const bool centerTitle = false; static const double elevation = 0; - static const MultiSourceIconModel leading = - MultiSourceIconModel.icon(color: null); + static const MultiSourceIconModel leading = MultiSourceIconModel.icon( + color: null, + ); static const bool automaticallyImplyLeading = false; static TextProp get titleStyle => TextProp.general(fontSize: 18); @@ -191,13 +192,13 @@ class M3NavigationBarDefaults { static const double elevation = 0; static TextProp get selectedLabelStyle => TextProp( - fontSize: 13, - fontName: const FontName( - family: 'Roboto', - style: 'Normal', - weight: FontWeightNumeric.w600, - ), - ); + fontSize: 13, + fontName: const FontName( + family: 'Roboto', + style: 'Normal', + weight: FontWeightNumeric.w600, + ), + ); static TextProp get unselectedLabelStyle => TextProp(fontSize: 13); static const Color unselectedIconColor = Colors.black; @@ -271,8 +272,10 @@ class FloatingActionButtonDefaults { static const double highlightElevation = 12; static const double hoverElevation = 8; static const FloatingActionButtonType type = FloatingActionButtonType.regular; - static const MultiSourceIconModel icon = - MultiSourceIconModel.icon(size: 24, color: null); + static const MultiSourceIconModel icon = MultiSourceIconModel.icon( + size: 24, + color: null, + ); static const String label = 'Button'; static TextProp get labelStyle => @@ -306,8 +309,9 @@ class ProgressBarDefaults { static const double maxValue = 100; static const bool animate = true; static const int animationDurationInMillis = 300; - static const CornerRadius cornerRadius = - CornerRadius.all(RadiusModel.circular(8)); + static const CornerRadius cornerRadius = CornerRadius.all( + RadiusModel.circular(8), + ); } class TabBarDefaults { @@ -326,7 +330,9 @@ class TabBarDefaults { static const EdgeInsetsModel indicatorPadding = EdgeInsetsModel.zero; static const AxisC tabItemDirection = AxisC.horizontal; static const double gap = 10; - static const EdgeInsetsModel labelPadding = - EdgeInsetsModel.symmetric(horizontal: 16, vertical: 12); + static const EdgeInsetsModel labelPadding = EdgeInsetsModel.symmetric( + horizontal: 16, + vertical: 12, + ); static const Color dividerColor = Colors.grey; } diff --git a/lib/src/transformers/node_transformer.dart b/lib/src/transformers/node_transformer.dart index ff919774..84befc50 100644 --- a/lib/src/transformers/node_transformer.dart +++ b/lib/src/transformers/node_transformer.dart @@ -5,7 +5,7 @@ import 'package:flutter/widgets.dart'; import '../../codelessly_sdk.dart'; /// A class that holds information about how to build a node. -abstract class BuildSettings with EquatableMixin { +abstract class BuildSettings with Equatable { /// Whether to internally handle the node's opacity. final bool withOpacity; @@ -56,18 +56,18 @@ abstract class BuildSettings with EquatableMixin { @override List get props => [ - withOpacity, - withVisibility, - withBlendModes, - withRotation, - withMargins, - withConstraints, - withReactions, - withAlignment, - isPreview, - useInk, - obscureImages, - ]; + withOpacity, + withVisibility, + withBlendModes, + withRotation, + withMargins, + withConstraints, + withReactions, + withAlignment, + isPreview, + useInk, + obscureImages, + ]; } /// A class that holds information about how to build a node that is to be @@ -153,13 +153,13 @@ class WidgetBuildSettings extends BuildSettings { @override List get props => [ - ...super.props, - debugLabel, - nullSubstitutionMode, - replaceVariablesWithSymbols, - buildRawWidget, - brightness, - ]; + ...super.props, + debugLabel, + nullSubstitutionMode, + replaceVariablesWithSymbols, + buildRawWidget, + brightness, + ]; } /// This is the base class for all transformers. @@ -175,8 +175,12 @@ class WidgetBuildSettings extends BuildSettings { /// [WidgetNodeTransformerManager]. /// /// Codegen uses different `O`, `BX`, and `BS` types. -abstract class NodeTypeTransformer { +abstract class NodeTypeTransformer< + N extends BaseNode, + O, + BX, + BS extends BuildSettings +> { /// Creates a new instance of the transformer. /// /// [getNode] is a function that returns a node by its id. @@ -192,11 +196,7 @@ abstract class NodeTypeTransformer /// Creates a new instance of the transformer. NodeWidgetTransformer(GetNode getNode, this.manager) - : super(getNode, manager); + : super(getNode, manager); } diff --git a/lib/src/transformers/node_transformer_manager.dart b/lib/src/transformers/node_transformer_manager.dart index ce9ee296..57948e2d 100644 --- a/lib/src/transformers/node_transformer_manager.dart +++ b/lib/src/transformers/node_transformer_manager.dart @@ -32,8 +32,12 @@ import '../../codelessly_sdk.dart'; /// `NT` is the node transformer type, probably a [NodeWidgetTransformer]. /// /// Codegen uses different O, BC, BS, and NT types for its transformers. -abstract class NodeTransformerManager { +abstract class NodeTransformerManager< + O, + BC, + BS extends BuildSettings, + NT extends NodeTypeTransformer +> { final GetNode getNode; NodeTransformerManager(this.getNode); @@ -44,10 +48,7 @@ abstract class NodeTransformerManager { @override Widget buildWidget( - AccordionNode node, BuildContext context, WidgetBuildSettings settings) { + AccordionNode node, + BuildContext context, + WidgetBuildSettings settings, + ) { return PassiveAccordionWidget( node: node, - topWidget: manager.buildWidgetFromNode(getNode(node.children[0]), context, - settings: settings), - bottomWidget: manager.buildWidgetFromNode( - getNode(node.children[1]), context, - settings: settings), + isInitiallyExpanded: node.properties.isExpanded, + topWidget: _buildChild(node, 0, context, settings), + bottomWidget: _buildChild(node, 1, context, settings), + ); + } + + /// Builds the accordion's [index]th child, or nothing while the accordion + /// is still missing it. + Widget _buildChild( + AccordionNode node, + int index, + BuildContext context, + WidgetBuildSettings settings, + ) { + final String? childId = node.children.elementAtOrNull(index); + if (childId == null) return const SizedBox.shrink(); + return manager.buildWidgetFromNode( + getNode(childId), + context, + settings: settings, ); } @@ -40,12 +58,9 @@ class PassiveAccordionTransformer extends NodeWidgetTransformer { ); return PassiveAccordionWidget( enabled: enabled, - topWidget: title ?? - manager.buildWidgetFromNode(getNode(node.children[0]), context, - settings: settings), - bottomWidget: content ?? - manager.buildWidgetFromNode(getNode(node.children[1]), context, - settings: settings), + isInitiallyExpanded: node.properties.isExpanded, + topWidget: title ?? _buildChild(node, 0, context, settings), + bottomWidget: content ?? _buildChild(node, 1, context, settings), node: node, ); } @@ -104,7 +119,7 @@ class _AccordionWidgetState extends State { height: 48, child: TextButton( onPressed: () { - if (widget.enabled) return; + if (!widget.enabled) return; setState(() => isExpanded = !isExpanded); }, child: Icon( diff --git a/lib/src/transformers/node_transformers/passive_app_bar_transformer.dart b/lib/src/transformers/node_transformers/passive_app_bar_transformer.dart index d4f2d32b..21227ab2 100644 --- a/lib/src/transformers/node_transformers/passive_app_bar_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_app_bar_transformer.dart @@ -38,7 +38,8 @@ class PassiveAppBarTransformer extends NodeWidgetTransformer { ), List variablesOverrides = const [], }) { - final Widget? leading = node.properties.leading.icon.show && + final Widget? leading = + node.properties.leading.icon.show && !node.properties.automaticallyImplyLeading ? retrieveIconWidget( node.properties.leading.icon, @@ -85,14 +86,17 @@ class PassiveAppBarTransformer extends NodeWidgetTransformer { backgroundColor: node.properties.backgroundColor.toFlutterColor(), elevation: node.properties.elevation, foregroundColor: + node.properties.foregroundColor?.toFlutterColor() ?? node.properties.titleStyle.fills.firstOrNull?.toFlutterColor(), + scrolledUnderElevation: node.properties.scrolledUnderElevation, shadowColor: node.properties.shadowColor.toFlutterColor(), actions: [ for (final item in node.properties.actions.whereType()) buildIconOrButton( context, - icon: retrieveIconWidget(item.icon, item.icon.size) ?? + icon: + retrieveIconWidget(item.icon, item.icon.size) ?? const SizedBox.shrink(), size: item.icon.size, tooltip: item.tooltip, @@ -110,9 +114,7 @@ class PassiveAppBarTransformer extends NodeWidgetTransformer { required List reactions, }) { final bool hasReactions = reactions - .where( - (reaction) => reaction.trigger.type == TriggerType.click, - ) + .where((reaction) => reaction.trigger.type == TriggerType.click) .isNotEmpty; if (hasReactions) { return IconButton( @@ -128,7 +130,10 @@ class PassiveAppBarTransformer extends NodeWidgetTransformer { void onTriggerAction(BuildContext context, List reactions) async { final bool executed = await FunctionsRepository.triggerAction( - context, reactions: reactions, TriggerType.click); + context, + reactions: reactions, + TriggerType.click, + ); // only navigate back by default if no actions are defined on the leading // icon. @@ -151,10 +156,7 @@ class PassiveAppBarTransformer extends NodeWidgetTransformer { basicBoxLocal: NodeBox(0, 0, width, height), properties: props, ); - return PassiveAppBarWidget( - node: node, - settings: settings, - ); + return PassiveAppBarWidget(node: node, settings: settings); } Widget buildPreview( @@ -172,17 +174,22 @@ class PassiveAppBarTransformer extends NodeWidgetTransformer { properties: properties ?? node!.properties, id: '', name: 'AppBar', - basicBoxLocal: - NodeBox(0, 0, width ?? 250, height ?? kDefaultAppBarHeight), - retainedOuterBoxLocal: - NodeBox(0, 0, width ?? 250, height ?? kDefaultAppBarHeight), + basicBoxLocal: NodeBox( + 0, + 0, + width ?? 250, + height ?? kDefaultAppBarHeight, + ), + retainedOuterBoxLocal: NodeBox( + 0, + 0, + width ?? 250, + height ?? kDefaultAppBarHeight, + ), ); return Theme( data: Theme.of(context).copyWith(platform: TargetPlatform.android), - child: PassiveAppBarWidget( - node: previewNode, - settings: settings, - ), + child: PassiveAppBarWidget(node: previewNode, settings: settings), ); } } @@ -217,9 +224,7 @@ class PassiveAppBarWidget extends StatelessWidget required List reactions, }) { final bool hasReactions = reactions - .where( - (reaction) => reaction.trigger.type == TriggerType.click, - ) + .where((reaction) => reaction.trigger.type == TriggerType.click) .isNotEmpty; if (hasReactions && onTriggerAction != null) { return IconButton( @@ -235,7 +240,8 @@ class PassiveAppBarWidget extends StatelessWidget @override Widget build(BuildContext context) { - final Widget? leading = node.properties.leading.icon.show && + final Widget? leading = + node.properties.leading.icon.show && !node.properties.automaticallyImplyLeading ? retrieveIconWidget(node.properties.leading.icon, null) : null; @@ -281,7 +287,9 @@ class PassiveAppBarWidget extends StatelessWidget : false, title: title, foregroundColor: + node.properties.foregroundColor?.toFlutterColor() ?? node.properties.titleStyle.fills.firstOrNull?.toFlutterColor(), + scrolledUnderElevation: node.properties.scrolledUnderElevation, titleSpacing: node.properties.titleSpacing, shadowColor: node.properties.shadowColor.toFlutterColor(), actions: [ @@ -289,7 +297,8 @@ class PassiveAppBarWidget extends StatelessWidget in node.properties.actions.whereType()) buildIconOrButton( context, - icon: retrieveIconWidget(item.icon, item.icon.size) ?? + icon: + retrieveIconWidget(item.icon, item.icon.size) ?? const SizedBox.shrink(), size: item.icon.size, tooltip: item.tooltip, diff --git a/lib/src/transformers/node_transformers/passive_button_transformer.dart b/lib/src/transformers/node_transformers/passive_button_transformer.dart index 36fd4c81..0e944da9 100644 --- a/lib/src/transformers/node_transformers/passive_button_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_button_transformer.dart @@ -14,10 +14,7 @@ class PassiveButtonTransformer extends NodeWidgetTransformer { BuildContext context, WidgetBuildSettings settings, ) { - return buildButtonFromNode( - node, - settings: settings, - ); + return buildButtonFromNode(node, settings: settings); } Widget buildFromProps( @@ -86,19 +83,26 @@ class PassiveButtonWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final ScopedValues scopedValues = - ScopedValues.of(context, variablesOverrides: variablesOverrides); + final ScopedValues scopedValues = ScopedValues.of( + context, + variablesOverrides: variablesOverrides, + ); final ButtonStyle buttonStyle = createMasterButtonStyle( node, elevation: elevation, scopedValues: scopedValues, ); - final double effectiveIconSize = - min(node.properties.icon.size ?? 24, node.basicBoxLocal.height); - Widget? iconWidget = - retrieveIconWidget(node.properties.icon, effectiveIconSize); + final double effectiveIconSize = min( + node.properties.icon.size ?? 24, + node.basicBoxLocal.height, + ); + Widget? iconWidget = retrieveIconWidget( + node.properties.icon, + effectiveIconSize, + ); - final bool enabled = PropertyValueDelegate.getPropertyValue( + final bool enabled = + PropertyValueDelegate.getPropertyValue( node, 'enabled', scopedValues: scopedValues, @@ -113,10 +117,8 @@ class PassiveButtonWidget extends StatelessWidget { final List spans = node.properties.label .splitMap( variablePathRegex, - onMatch: (match) => VariableSpan( - variable: match[0]!, - style: const TextStyle(), - ), + onMatch: (match) => + VariableSpan(variable: match[0]!, style: const TextStyle()), onNonMatch: (text) => TextSpan(text: text), ) .toList(); @@ -125,7 +127,8 @@ class PassiveButtonWidget extends StatelessWidget { textAlign: node.properties.labelAlignment.flutterTextAlignment, ); } else { - final String value = PropertyValueDelegate.getPropertyValue( + final String value = + PropertyValueDelegate.getPropertyValue( node, 'label', scopedValues: scopedValues, @@ -164,7 +167,59 @@ class PassiveButtonWidget extends StatelessWidget { if (node.properties.icon.show) SizedBox(width: node.properties.gap), iconWidget, - } + }, + ], + ) + : label, + ); + case ButtonTypeEnum.filled: + buttonWidget = FilledButton( + onPressed: enabled ? () => onPressed?.call(context) : null, + onLongPress: enabled ? () => onLongPress?.call(context) : null, + style: buttonStyle, + child: iconWidget != null + ? Row( + mainAxisSize: MainAxisSize.min, + verticalDirection: VerticalDirection.down, + children: [ + if (node.properties.placement == + IconPlacementEnum.start) ...{ + iconWidget, + if (node.properties.icon.show) + SizedBox(width: node.properties.gap), + }, + Flexible(child: label), + if (node.properties.placement == IconPlacementEnum.end) ...{ + if (node.properties.icon.show) + SizedBox(width: node.properties.gap), + iconWidget, + }, + ], + ) + : label, + ); + case ButtonTypeEnum.filledTonal: + buttonWidget = FilledButton.tonal( + onPressed: enabled ? () => onPressed?.call(context) : null, + onLongPress: enabled ? () => onLongPress?.call(context) : null, + style: buttonStyle, + child: iconWidget != null + ? Row( + mainAxisSize: MainAxisSize.min, + verticalDirection: VerticalDirection.down, + children: [ + if (node.properties.placement == + IconPlacementEnum.start) ...{ + iconWidget, + if (node.properties.icon.show) + SizedBox(width: node.properties.gap), + }, + Flexible(child: label), + if (node.properties.placement == IconPlacementEnum.end) ...{ + if (node.properties.icon.show) + SizedBox(width: node.properties.gap), + iconWidget, + }, ], ) : label, @@ -188,7 +243,7 @@ class PassiveButtonWidget extends StatelessWidget { if (node.properties.placement == IconPlacementEnum.end) ...{ SizedBox(width: node.properties.gap), iconWidget, - } + }, ], ) : label, @@ -212,7 +267,7 @@ class PassiveButtonWidget extends StatelessWidget { if (node.properties.placement == IconPlacementEnum.end) ...{ SizedBox(width: node.properties.gap), iconWidget, - } + }, ], ) : label, diff --git a/lib/src/transformers/node_transformers/passive_canvas_transformer.dart b/lib/src/transformers/node_transformers/passive_canvas_transformer.dart index f5de87b8..f6c1415c 100644 --- a/lib/src/transformers/node_transformers/passive_canvas_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_canvas_transformer.dart @@ -7,7 +7,6 @@ import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import '../../../codelessly_sdk.dart'; -import '../../logging/debug_logger.dart'; class PassiveCanvasTransformer extends NodeWidgetTransformer { static const String name = 'PassiveCanvasWidget'; @@ -25,16 +24,16 @@ class PassiveCanvasTransformer extends NodeWidgetTransformer { WidgetBuildSettings settings, ) { if (node.properties.topAppBarPlaceholderId == null) return null; - SinglePlaceholderNode appBarPlaceholderNode = - getNode(node.properties.topAppBarPlaceholderId!) - as SinglePlaceholderNode; + SinglePlaceholderNode appBarPlaceholderNode = getNode( + node.properties.topAppBarPlaceholderId!, + ) as SinglePlaceholderNode; final childId = appBarPlaceholderNode.childrenOrEmpty.firstOrNull; if (childId == null) return null; final BaseNode appBarNode = getNode(childId); - final NodeWidgetTransformer transformer = - manager.getTransformerByNode(appBarNode); + final NodeWidgetTransformer transformer = manager + .getTransformerByNode(appBarNode); if (transformer is PassiveAppBarTransformer) { final Widget appBarChild = manager.buildWidgetFromNode( @@ -57,14 +56,13 @@ class PassiveCanvasTransformer extends NodeWidgetTransformer { final Widget appBarChild = manager.buildWidgetFromNode( appBarNode, context, - settings: settings.copyWith( - brightness: node.properties.brightness, - ), + settings: settings.copyWith(brightness: node.properties.brightness), ); return PreferredSize( - preferredSize: - Size.fromHeight(appBarPlaceholderNode.outerBoxLocal.height), + preferredSize: Size.fromHeight( + appBarPlaceholderNode.outerBoxLocal.height, + ), child: SafeArea(child: appBarChild), ); } @@ -76,14 +74,15 @@ class PassiveCanvasTransformer extends NodeWidgetTransformer { WidgetBuildSettings settings, ) { if (node.properties.navigationBarPlaceholderId == null) return null; - SinglePlaceholderNode navigationBarPlaceholderNode = - getNode(node.properties.navigationBarPlaceholderId!) - as SinglePlaceholderNode; + SinglePlaceholderNode navigationBarPlaceholderNode = getNode( + node.properties.navigationBarPlaceholderId!, + ) as SinglePlaceholderNode; if (navigationBarPlaceholderNode.childrenOrEmpty.isEmpty) return null; - final navigationBarNode = - getNode(navigationBarPlaceholderNode.childrenOrEmpty.first); + final navigationBarNode = getNode( + navigationBarPlaceholderNode.childrenOrEmpty.first, + ); Widget navBar = manager.buildWidgetByID( node.properties.navigationBarPlaceholderId!, @@ -152,7 +151,8 @@ class PassiveCanvasTransformer extends NodeWidgetTransformer { ); } - final bool needsAScaffold = appBar != null || + final bool needsAScaffold = + appBar != null || floatingActionButton != null || bottomNavigationBar != null; @@ -182,10 +182,10 @@ class PassiveCanvasTransformer extends NodeWidgetTransformer { } return manager.getTransformer().buildRectangle( - node, - children: [scaffold], - settings: settings, - ); + node, + children: [scaffold], + settings: settings, + ); } Widget _wrapInScaffoldForAutoScale({ @@ -221,8 +221,9 @@ class PassiveCanvasTransformer extends NodeWidgetTransformer { Align( alignment: Alignment.topCenter, child: ConstrainedBox( - constraints: - BoxConstraints(maxWidth: node.resolvedConstraints.maxWidth!), + constraints: BoxConstraints( + maxWidth: node.resolvedConstraints.maxWidth!, + ), child: body, ), ), @@ -242,7 +243,8 @@ class PassiveCanvasTransformer extends NodeWidgetTransformer { ); } - final bool needsAScaffold = appBar != null || + final bool needsAScaffold = + appBar != null || floatingActionButton != null || bottomNavigationBar != null; @@ -294,18 +296,19 @@ class PassiveCanvasTransformer extends NodeWidgetTransformer { } return manager.getTransformer().buildRectangle( - node, - children: [scaffold], - settings: settings, - ); + node, + children: [scaffold], + settings: settings, + ); } void onFaBPressed(BuildContext context, CanvasNode node) { FunctionsRepository.triggerAction( - context, - node: node, - TriggerType.click, - reactions: node.properties.floatingActionButton?.reactions); + context, + node: node, + TriggerType.click, + reactions: node.properties.floatingActionButton?.reactions, + ); } /// This would not work if the canvas is expected to have 2 different forms. @@ -335,15 +338,15 @@ class PassiveCanvasTransformer extends NodeWidgetTransformer { settings: settings, builder: (context) => switch (node.scaleMode) { ScaleMode.responsive => _wrapInScaffoldForResponsive( - context: context, - node: node, - settings: settings, - ), + context: context, + node: node, + settings: settings, + ), ScaleMode.autoScale => _wrapInScaffoldForAutoScale( - context: context, - node: node, - settings: settings, - ) + context: context, + node: node, + settings: settings, + ), }, ); } @@ -356,7 +359,7 @@ class PassiveCanvasTransformer extends NodeWidgetTransformer { 'fill-${fill.id}', scopedValues: ScopedValues.of(context), ) ?? - fill + fill, ]; if (fills.length == 1 && fills[0].type == PaintType.solid && @@ -405,8 +408,11 @@ class _PassiveCanvasWidgetState extends State { // Listen for navigation events to update the system UI brightness back // again, such as if this canvas was navigated away from, but then the view // pops and goes back to this canvas. - codelessly?.addNavigationListener('canvas-${widget.node.id}', - (event, layoutId, canvasId) { + codelessly?.addNavigationListener('canvas-${widget.node.id}', ( + event, + layoutId, + canvasId, + ) { if (canvasId == widget.node.id) { updateSystemBrightness(context); } @@ -446,13 +452,15 @@ class _PassiveCanvasWidgetState extends State { List collectOnLoadActions() => widget.node.reactions .whereTriggerType(TriggerType.load) .map((e) => e.action) - .where((action) => - action.enabled && - // Because calling api is handled in LayoutBuilder. - action.type != ActionType.callApi && - action.type != ActionType.navigation && - action.type != ActionType.submit && - action.type != ActionType.link) + .where( + (action) => + action.enabled && + // Because calling api is handled in LayoutBuilder. + action.type != ActionType.callApi && + action.type != ActionType.navigation && + action.type != ActionType.submit && + action.type != ActionType.link, + ) .toList(); void triggerOnLoadActions(BuildContext context) { @@ -489,12 +497,13 @@ class _PassiveCanvasWidgetState extends State { @override Widget build(BuildContext context) { - final Brightness brightness = - widget.node.properties.brightness.toFlutterBrightness(context); + final Brightness brightness = widget.node.properties.brightness + .toFlutterBrightness(context); return AnnotatedRegion( value: SystemUiOverlayStyle( - statusBarColor: - brightness == Brightness.light ? Colors.black : Colors.white, + statusBarColor: brightness == Brightness.light + ? Colors.black + : Colors.white, statusBarIconBrightness: brightness, statusBarBrightness: brightness, ), diff --git a/lib/src/transformers/node_transformers/passive_checkbox_transformer.dart b/lib/src/transformers/node_transformers/passive_checkbox_transformer.dart index 17a73c04..b9895742 100644 --- a/lib/src/transformers/node_transformers/passive_checkbox_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_checkbox_transformer.dart @@ -14,15 +14,9 @@ class PassiveCheckboxTransformer extends NodeWidgetTransformer { WidgetBuildSettings settings, ) { if (settings.isPreview) { - return PreviewCheckboxWidget( - node: node, - settings: settings, - ); + return PreviewCheckboxWidget(node: node, settings: settings); } else { - return PassiveCheckboxWidget( - node: node, - settings: settings, - ); + return PassiveCheckboxWidget(node: node, settings: settings); } } } @@ -72,15 +66,24 @@ class PassiveCheckboxWidget extends StatelessWidget { void onChanged(BuildContext context, bool? internalValue) { NodeStateProvider.setState(context, internalValue); - FunctionsRepository.setPropertyValue(context, - node: node, property: 'value', value: internalValue); + FunctionsRepository.setPropertyValue( + context, + node: node, + property: 'value', + value: internalValue, + ); FunctionsRepository.triggerAction( - context, node: node, TriggerType.changed, value: internalValue); + context, + node: node, + TriggerType.changed, + value: internalValue, + ); } @override Widget build(BuildContext context) { - final bool? value = PropertyValueDelegate.getPropertyValue( + final bool? value = + PropertyValueDelegate.getPropertyValue( node, 'value', scopedValues: ScopedValues.of( @@ -123,7 +126,8 @@ class TransformerCheckbox extends StatefulWidget { class _TransformerCheckboxState extends State { @override Widget build(BuildContext context) { - final double scale = widget.node.basicBoxLocal.width / + final double scale = + widget.node.basicBoxLocal.width / (widget.node.properties.compact ? Checkbox.width : kCheckboxDefaultSize); @@ -134,7 +138,8 @@ class _TransformerCheckboxState extends State { scale: scale, child: Checkbox( key: ValueKey( - '${widget.node.id}-${IndexedItemProvider.maybeOf(context)?.index ?? ''}'), + '${widget.node.id}-${IndexedItemProvider.maybeOf(context)?.index ?? ''}', + ), value: widget.node.properties.tristate ? widget.value : (widget.value ?? false), diff --git a/lib/src/transformers/node_transformers/passive_divider_transformer.dart b/lib/src/transformers/node_transformers/passive_divider_transformer.dart index db27b3ef..b9b87422 100644 --- a/lib/src/transformers/node_transformers/passive_divider_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_divider_transformer.dart @@ -1,6 +1,5 @@ import 'package:codelessly_api/codelessly_api.dart'; import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; import '../../../codelessly_sdk.dart'; @@ -21,8 +20,9 @@ class PassiveDividerTransformer extends NodeWidgetTransformer { required DividerProperties props, required double height, required double width, - WidgetBuildSettings settings = - const WidgetBuildSettings(debugLabel: 'buildFromProps'), + WidgetBuildSettings settings = const WidgetBuildSettings( + debugLabel: 'buildFromProps', + ), }) { final node = DividerNode( id: '', @@ -40,8 +40,9 @@ class PassiveDividerTransformer extends NodeWidgetTransformer { double? height, double? width, ValueChanged? onChanged, - WidgetBuildSettings settings = - const WidgetBuildSettings(debugLabel: 'buildPreview'), + WidgetBuildSettings settings = const WidgetBuildSettings( + debugLabel: 'buildPreview', + ), }) { final previewNode = DividerNode( properties: properties ?? node?.properties ?? DividerProperties(), @@ -59,8 +60,9 @@ class PassiveDividerTransformer extends NodeWidgetTransformer { Widget buildFromNode( DividerNode node, { - WidgetBuildSettings settings = - const WidgetBuildSettings(debugLabel: 'buildFromNode'), + WidgetBuildSettings settings = const WidgetBuildSettings( + debugLabel: 'buildFromNode', + ), }) { return PassiveDividerWidget( node: node, @@ -76,7 +78,7 @@ class PassiveDividerTransformer extends NodeWidgetTransformer { void onAction(Reaction reaction) { switch (reaction.action.type) { case ActionType.link: - launchUrl(Uri.parse((reaction.action as LinkAction).url)); + FunctionsRepository.openLink((reaction.action as LinkAction).url); default: break; } diff --git a/lib/src/transformers/node_transformers/passive_dropdown_transformer.dart b/lib/src/transformers/node_transformers/passive_dropdown_transformer.dart index 0acfdafe..8d6ffccd 100644 --- a/lib/src/transformers/node_transformers/passive_dropdown_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_dropdown_transformer.dart @@ -48,17 +48,29 @@ class PassiveDropdownTransformer extends NodeWidgetTransformer { return buildDropdownFromNode(context, node, settings); } - void onTap(context, DropdownNode node) => + void onTap(BuildContext context, DropdownNode node) => FunctionsRepository.triggerAction(context, node: node, TriggerType.click); - void onChanged(BuildContext context, DropdownNode node, int index, - Object internalValue) { + void onChanged( + BuildContext context, + DropdownNode node, + int index, + Object internalValue, + ) { NodeStateProvider.setState(context, internalValue); - FunctionsRepository.setPropertyValue(context, - node: node, property: 'value', value: internalValue); + FunctionsRepository.setPropertyValue( + context, + node: node, + property: 'value', + value: internalValue, + ); FunctionsRepository.triggerAction( - context, node: node, TriggerType.changed, value: internalValue); + context, + node: node, + TriggerType.changed, + value: internalValue, + ); } } @@ -83,39 +95,42 @@ class PassiveDropdownWidget extends StatelessWidget { for (final (index, value) in items.indexed) DropdownMenuItem( value: value, - alignment: node.properties.itemAlignment.flutterAlignmentGeometry ?? + alignment: + node.properties.itemAlignment.flutterAlignmentGeometry ?? AlignmentDirectional.centerStart, child: ChangeNotifierProvider.value( value: context.read(), child: IndexedItemProvider( item: IndexedItem(index, value), - child: Builder(builder: (context) { - String label = '$value'; - if (node.properties.useDataSource) { - String? labelText = - PropertyValueDelegate.getPropertyValue( - node, - 'itemLabel', - scopedValues: ScopedValues.of(context), - ); - labelText ??= node.properties.itemLabel; - label = PropertyValueDelegate.substituteVariables( - labelText, - nullSubstitutionMode: settings.nullSubstitutionMode, - scopedValues: ScopedValues.of(context), + child: Builder( + builder: (context) { + String label = '$value'; + if (node.properties.useDataSource) { + String? labelText = + PropertyValueDelegate.getPropertyValue( + node, + 'itemLabel', + scopedValues: ScopedValues.of(context), + ); + labelText ??= node.properties.itemLabel; + label = PropertyValueDelegate.substituteVariables( + labelText, + nullSubstitutionMode: settings.nullSubstitutionMode, + scopedValues: ScopedValues.of(context), + ); + } + return Text( + label, + style: TextUtils.retrieveTextStyleFromProp( + node.properties.itemTextStyle, + ), + overflow: TextOverflow.ellipsis, ); - } - return Text( - label, - style: TextUtils.retrieveTextStyleFromProp( - node.properties.itemTextStyle, - ), - overflow: TextOverflow.ellipsis, - ); - }), + }, + ), ), ), - ) + ), ]; } @@ -124,41 +139,45 @@ class PassiveDropdownWidget extends StatelessWidget { for (final (index, value) in items.indexed) DropdownMenuItem( value: Object, - alignment: node.properties.itemAlignment.flutterAlignmentGeometry ?? + alignment: + node.properties.itemAlignment.flutterAlignmentGeometry ?? AlignmentDirectional.centerStart, child: IndexedItemProvider( item: IndexedItem(index, value), - child: Builder(builder: (context) { - String label = '$value'; - if (node.properties.useDataSource) { - String? labelText = - PropertyValueDelegate.getPropertyValue( - node, - 'itemLabel', - scopedValues: ScopedValues.of(context), - ); - labelText ??= node.properties.itemLabel; - label = PropertyValueDelegate.substituteVariables( - labelText, - nullSubstitutionMode: settings.nullSubstitutionMode, - scopedValues: ScopedValues.of(context), - ); - } - return Container( - constraints: BoxConstraints( + child: Builder( + builder: (context) { + String label = '$value'; + if (node.properties.useDataSource) { + String? labelText = + PropertyValueDelegate.getPropertyValue( + node, + 'itemLabel', + scopedValues: ScopedValues.of(context), + ); + labelText ??= node.properties.itemLabel; + label = PropertyValueDelegate.substituteVariables( + labelText, + nullSubstitutionMode: settings.nullSubstitutionMode, + scopedValues: ScopedValues.of(context), + ); + } + return Container( + constraints: BoxConstraints( maxWidth: - node.basicBoxLocal.width - node.properties.iconSize), - child: Text( - label, - style: TextUtils.retrieveTextStyleFromProp( - node.properties.selectedItemTextStyle, + node.basicBoxLocal.width - node.properties.iconSize, ), - overflow: TextOverflow.ellipsis, - ), - ); - }), + child: Text( + label, + style: TextUtils.retrieveTextStyleFromProp( + node.properties.selectedItemTextStyle, + ), + overflow: TextOverflow.ellipsis, + ), + ); + }, + ), ), - ) + ), ]; } @@ -167,14 +186,15 @@ class PassiveDropdownWidget extends StatelessWidget { final ScopedValues scopedValues = ScopedValues.of(context); final List items = node.properties.useDataSource ? PropertyValueDelegate.getPropertyValue( - node, - 'data', - scopedValues: scopedValues, - ) ?? - [] + node, + 'data', + scopedValues: scopedValues, + ) ?? + [] : node.properties.items; - Object? value = PropertyValueDelegate.getPropertyValue( + Object? value = + PropertyValueDelegate.getPropertyValue( node, 'value', scopedValues: scopedValues, @@ -184,59 +204,68 @@ class PassiveDropdownWidget extends StatelessWidget { return AdaptiveNodeBox( node: node, child: Theme( - data: ThemeData( + data: Theme.of(context).copyWith( hoverColor: node.properties.hoverColor?.toFlutterColor(), splashColor: node.properties.splashColor?.toFlutterColor(), ), - child: Builder(builder: (context) { - final child = DropdownButton( - value: items.isEmpty || items.count(value) != 1 ? null : value, - isDense: node.properties.dense, - isExpanded: node.properties.expanded, - autofocus: node.properties.autoFocus, - enableFeedback: node.properties.enableFeedback, - alignment: node.properties.selectedItemAlignment - .flutterAlignmentGeometry ?? - Alignment.centerLeft, - hint: node.properties.hint.isNotEmpty - ? Text( - node.properties.hint, - style: TextUtils.retrieveTextStyleFromProp( - node.properties.hintStyle, - ), - overflow: TextOverflow.ellipsis, - ) - : null, - iconDisabledColor: - node.properties.iconDisabledColor.toFlutterColor(), - iconEnabledColor: node.properties.iconEnabledColor.toFlutterColor(), - iconSize: node.properties.iconSize, - icon: retrieveIconWidget( - node.properties.icon, node.properties.iconSize), - dropdownColor: node.properties.dropdownColor.toFlutterColor(), - focusColor: node.properties.focusColor.toFlutterColor(), - elevation: node.properties.elevation, - borderRadius: node.properties.borderRadius.borderRadius, - onTap: onTap, - padding: node.padding.flutterEdgeInsets, - onChanged: node.properties.enabled - ? (value) { - if (value == null) return; - final index = items.indexOf(value); - onChanged?.call(index, value); - } - : null, - underline: - node.properties.underline ? null : const SizedBox.shrink(), - items: buildItems(context, items), - selectedItemBuilder: (context) => - selectedItemBuilder(context, items), - ); - if (node.horizontalFit.isWrap) { - return IntrinsicWidth(child: child); - } - return child; - }), + child: Builder( + builder: (context) { + final child = DropdownButton( + value: items.isEmpty || items.count(value) != 1 ? null : value, + isDense: node.properties.dense, + isExpanded: node.properties.expanded, + autofocus: node.properties.autoFocus, + enableFeedback: node.properties.enableFeedback, + alignment: + node + .properties + .selectedItemAlignment + .flutterAlignmentGeometry ?? + Alignment.centerLeft, + hint: node.properties.hint.isNotEmpty + ? Text( + node.properties.hint, + style: TextUtils.retrieveTextStyleFromProp( + node.properties.hintStyle, + ), + overflow: TextOverflow.ellipsis, + ) + : null, + iconDisabledColor: node.properties.iconDisabledColor + .toFlutterColor(), + iconEnabledColor: node.properties.iconEnabledColor + .toFlutterColor(), + iconSize: node.properties.iconSize, + icon: retrieveIconWidget( + node.properties.icon, + node.properties.iconSize, + ), + dropdownColor: node.properties.dropdownColor.toFlutterColor(), + focusColor: node.properties.focusColor.toFlutterColor(), + elevation: node.properties.elevation, + borderRadius: node.properties.borderRadius.borderRadius, + onTap: onTap, + padding: node.padding.flutterEdgeInsets, + onChanged: node.properties.enabled + ? (value) { + if (value == null) return; + final index = items.indexOf(value); + onChanged?.call(index, value); + } + : null, + underline: node.properties.underline + ? null + : const SizedBox.shrink(), + items: buildItems(context, items), + selectedItemBuilder: (context) => + selectedItemBuilder(context, items), + ); + if (node.horizontalFit.isWrap) { + return IntrinsicWidth(child: child); + } + return child; + }, + ), ), ); } diff --git a/lib/src/transformers/node_transformers/passive_embedded_video_transformer.dart b/lib/src/transformers/node_transformers/passive_embedded_video_transformer.dart index bdc3d52d..75d1bc98 100644 --- a/lib/src/transformers/node_transformers/passive_embedded_video_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_embedded_video_transformer.dart @@ -4,11 +4,9 @@ import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:provider/provider.dart'; import 'package:webview_flutter/webview_flutter.dart'; -import 'package:webview_flutter_android/webview_flutter_android.dart'; -import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; import '../../../codelessly_sdk.dart'; -import '../web/web_webview_platform.dart'; +import 'embedded_video_controller.dart'; class PassiveEmbeddedVideoTransformer extends NodeWidgetTransformer { @@ -31,11 +29,14 @@ class PassiveEmbeddedVideoTransformer String url = '', EmbeddedVideoSource source = EmbeddedVideoSource.youtube, required String baseUrl, - WidgetBuildSettings settings = - const WidgetBuildSettings(debugLabel: 'buildPreview', isPreview: true), + WidgetBuildSettings settings = const WidgetBuildSettings( + debugLabel: 'buildPreview', + isPreview: true, + ), }) { final previewNode = EmbeddedVideoNode( - properties: properties ?? + properties: + properties ?? node?.properties ?? (source == EmbeddedVideoSource.youtube ? EmbeddedYoutubeVideoProperties(url: url) @@ -94,55 +95,16 @@ class _PassiveEmbeddedVideoWidgetState void initState() { super.initState(); if (!isPlatformSupportedForWebView || widget.settings.isPreview) { - print('Unsupported platform: $defaultTargetPlatform for embedded ' - 'video.'); + DebugLogger.instance.printInfo( + 'Unsupported platform: $defaultTargetPlatform for embedded video.', + name: 'EmbeddedVideo', + ); return; } - final PlatformWebViewControllerCreationParams params; - if (kIsWeb) { - // WebView on web only supports loadRequest. Any other method invocation - // on the controller will result in an exception. Be aware!! - WebViewPlatform.instance = WebWebViewPlatform(); - _controller = WebViewController(); - } else { - if (WebViewPlatform.instance is WebKitWebViewPlatform) { - params = WebKitWebViewControllerCreationParams( - allowsInlineMediaPlayback: true, - mediaTypesRequiringUserAction: { - if (!widget.node.properties.autoPlay) ...{ - PlaybackMediaTypes.audio, - PlaybackMediaTypes.video, - }, - }, - ); - } else if (WebViewPlatform.instance is AndroidWebViewPlatform) { - params = AndroidWebViewControllerCreationParams(); - } else { - params = const PlatformWebViewControllerCreationParams(); - } - - _controller = WebViewController.fromPlatformCreationParams(params); - _controller.setJavaScriptMode(JavaScriptMode.unrestricted); - } - // _controller.setUserAgent('chrome'); - if (_controller.platform is AndroidWebViewController) { - print('setting android config'); - final controller = _controller.platform as AndroidWebViewController; - controller.setMediaPlaybackRequiresUserGesture( - !widget.node.properties.autoPlay); - // Using this user-agent string to force the video to play in the webview - // on Android. This is a hack, but it works. - controller.setUserAgent( - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36'); - } else if (_controller.platform is WebKitWebViewController) { - print('setting webkit config'); - final controller = _controller.platform as WebKitWebViewController; - if (widget.node.properties.source == EmbeddedVideoSource.youtube) { - controller.setUserAgent( - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36'); - // controller.setUserAgent('chrome'); - } - } + _controller = createEmbeddedVideoController( + autoPlay: widget.node.properties.autoPlay, + isYoutube: widget.node.properties.source == EmbeddedVideoSource.youtube, + ); final config = context.read().config!; @@ -163,7 +125,7 @@ class _PassiveEmbeddedVideoWidgetState baseUrl: config.baseURL, ); } - print('Loading $embedUrl'); + DebugLogger.instance.printInfo('Loading $embedUrl', name: 'EmbeddedVideo'); _controller.loadRequest(Uri.parse(embedUrl)); } @@ -174,10 +136,14 @@ class _PassiveEmbeddedVideoWidgetState switch (widget.node.properties.source) { case EmbeddedVideoSource.youtube: videoWidget = buildEmbeddedYoutubeVideo( - context, widget.node.properties as EmbeddedYoutubeVideoProperties); + context, + widget.node.properties as EmbeddedYoutubeVideoProperties, + ); case EmbeddedVideoSource.vimeo: videoWidget = buildEmbeddedVimeoVideo( - context, widget.node.properties as EmbeddedVimeoVideoProperties); + context, + widget.node.properties as EmbeddedVimeoVideoProperties, + ); } return AdaptiveNodeBox(node: widget.node, child: videoWidget); @@ -188,10 +154,7 @@ class _PassiveEmbeddedVideoWidgetState EmbeddedYoutubeVideoProperties properties, ) { if (isPlatformSupportedForWebView && !widget.settings.isPreview) { - return WebViewWidget( - controller: _controller, - key: ValueKey(properties), - ); + return WebViewWidget(controller: _controller, key: ValueKey(properties)); } if (properties.metadata != null) { return AdaptiveNodeBox( @@ -207,9 +170,10 @@ class _PassiveEmbeddedVideoWidgetState child: Container( decoration: BoxDecoration( image: const DecorationImage( - image: AssetImage('assets/images/image_youtube_video.png'), - fit: BoxFit.cover, - alignment: Alignment.topCenter), + image: AssetImage('assets/images/image_youtube_video.png'), + fit: BoxFit.cover, + alignment: Alignment.topCenter, + ), borderRadius: BorderRadius.circular(4), border: Border.all(color: Theme.of(context).colorScheme.secondary), ), @@ -230,18 +194,18 @@ class _PassiveEmbeddedVideoWidgetState } Widget buildEmbeddedVimeoVideo( - BuildContext context, EmbeddedVimeoVideoProperties properties) { + BuildContext context, + EmbeddedVimeoVideoProperties properties, + ) { if (isPlatformSupportedForWebView && !widget.settings.isPreview) { - return WebViewWidget( - key: ValueKey(properties), - controller: _controller, - ); + return WebViewWidget(key: ValueKey(properties), controller: _controller); } if (properties.metadata != null) { return AdaptiveNodeBox( node: widget.node, - child: - Center(child: VimeoVideoPreviewUI(metadata: properties.metadata!)), + child: Center( + child: VimeoVideoPreviewUI(metadata: properties.metadata!), + ), ); } @@ -252,9 +216,10 @@ class _PassiveEmbeddedVideoWidgetState child: Container( decoration: BoxDecoration( image: const DecorationImage( - image: AssetImage('assets/images/image_vimeo_video.png'), - fit: BoxFit.cover, - alignment: Alignment.topCenter), + image: AssetImage('assets/images/image_vimeo_video.png'), + fit: BoxFit.cover, + alignment: Alignment.topCenter, + ), borderRadius: BorderRadius.circular(4), border: Border.all(color: Theme.of(context).colorScheme.secondary), ), @@ -305,24 +270,26 @@ class YoutubeVideoPreviewUI extends StatelessWidget { child: Stack( children: [ Positioned.fill( - child: Builder(builder: (context) { - if (isEmpty) { - return Container( - color: Colors.black, - alignment: Alignment.center, - child: const Text( - 'Video Unavailable', - style: TextStyle(color: Colors.grey), - ), + child: Builder( + builder: (context) { + if (isEmpty) { + return Container( + color: Colors.black, + alignment: Alignment.center, + child: const Text( + 'Video Unavailable', + style: TextStyle(color: Colors.grey), + ), + ); + } + return Image.network( + metadata.cloudThumbnailUrl.isNotEmpty + ? metadata.cloudThumbnailUrl + : metadata.thumbnailUrl, + fit: BoxFit.cover, ); - } - return Image.network( - metadata.cloudThumbnailUrl.isNotEmpty - ? metadata.cloudThumbnailUrl - : metadata.thumbnailUrl, - fit: BoxFit.cover, - ); - }), + }, + ), ), Positioned( left: 0, @@ -406,24 +373,26 @@ class VimeoVideoPreviewUI extends StatelessWidget { child: Stack( children: [ Positioned.fill( - child: Builder(builder: (context) { - if (isEmpty) { - return Container( - color: Colors.black, - alignment: Alignment.center, - child: const Text( - 'Video Unavailable', - style: TextStyle(color: Colors.grey), - ), + child: Builder( + builder: (context) { + if (isEmpty) { + return Container( + color: Colors.black, + alignment: Alignment.center, + child: const Text( + 'Video Unavailable', + style: TextStyle(color: Colors.grey), + ), + ); + } + return Image.network( + metadata.cloudThumbnailUrl.isNotEmpty + ? metadata.cloudThumbnailUrl + : metadata.thumbnailUrl, + fit: BoxFit.cover, ); - } - return Image.network( - metadata.cloudThumbnailUrl.isNotEmpty - ? metadata.cloudThumbnailUrl - : metadata.thumbnailUrl, - fit: BoxFit.cover, - ); - }), + }, + ), ), Positioned( left: 0, @@ -434,8 +403,10 @@ class VimeoVideoPreviewUI extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - padding: - const EdgeInsets.symmetric(horizontal: 10, vertical: 6), + padding: const EdgeInsets.symmetric( + horizontal: 10, + vertical: 6, + ), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.7), borderRadius: BorderRadius.circular(6), @@ -453,8 +424,10 @@ class VimeoVideoPreviewUI extends StatelessWidget { ), const SizedBox(height: 6), Container( - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.7), borderRadius: BorderRadius.circular(6), @@ -485,10 +458,7 @@ class VimeoVideoPreviewUI extends StatelessWidget { color: Colors.black.withValues(alpha: 0.8), borderRadius: BorderRadius.circular(4), ), - child: const Icon( - Icons.play_arrow, - color: Colors.white, - ), + child: const Icon(Icons.play_arrow, color: Colors.white), ), ), Positioned( @@ -505,9 +475,7 @@ class VimeoVideoPreviewUI extends StatelessWidget { child: Container( height: 10, decoration: BoxDecoration( - border: Border.all( - color: Colors.grey, - ), + border: Border.all(color: Colors.grey), ), ), ), @@ -518,11 +486,7 @@ class VimeoVideoPreviewUI extends StatelessWidget { color: Colors.white, ), const SizedBox(width: 4), - const Icon( - Icons.settings, - size: 14, - color: Colors.white, - ), + const Icon(Icons.settings, size: 14, color: Colors.white), const SizedBox(width: 4), Image.asset( 'assets/images/vimeo_logo.png', diff --git a/lib/src/transformers/node_transformers/passive_expansion_tile_transformer.dart b/lib/src/transformers/node_transformers/passive_expansion_tile_transformer.dart index dc265043..57f2341b 100644 --- a/lib/src/transformers/node_transformers/passive_expansion_tile_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_expansion_tile_transformer.dart @@ -69,14 +69,16 @@ class PassiveExpansionTileTransformer ) : null; - final ExpansionTileNode previewNode = node ?? + final ExpansionTileNode previewNode = + node ?? ExpansionTileNode( id: 'expansion_tile', name: 'Expansion Tile', basicBoxLocal: NodeBox(0, 0, width, height), retainedOuterBoxLocal: NodeBox(0, 0, width, height), verticalFit: SizeFit.shrinkWrap, - properties: (props ?? + properties: + (props ?? node?.properties ?? ExpansionTileProperties( controlAffinity: controlAffinity, @@ -99,11 +101,8 @@ class PassiveExpansionTileTransformer trailingNode: trailingNode, initiallyExpanded: initiallyExpanded, getNode: getNode, - buildWidgetFromNode: (node, context) => manager.buildWidgetFromNode( - node, - context, - settings: settings, - ), + buildWidgetFromNode: (node, context) => + manager.buildWidgetFromNode(node, context, settings: settings), ); } @@ -153,16 +152,18 @@ class PassiveExpansionTileWidget extends StatelessWidget { /// Defines whether the ExpansionTile should render its internal rotating /// chevron icon on the leading part. bool shouldBuildLeading() { - final effectiveAffinity = - node.properties.effectiveAffinity(node.properties.controlAffinity); + final effectiveAffinity = node.properties.effectiveAffinity( + node.properties.controlAffinity, + ); return effectiveAffinity == ListTileControlAffinityC.leading; } /// Defines whether the ExpansionTile should render its internal trailing /// icon on the trailing part. bool shouldBuildTrailing() { - final effectiveAffinity = - node.properties.effectiveAffinity(node.properties.controlAffinity); + final effectiveAffinity = node.properties.effectiveAffinity( + node.properties.controlAffinity, + ); return effectiveAffinity == ListTileControlAffinityC.trailing; } @@ -235,29 +236,42 @@ class PassiveExpansionTileWidget extends StatelessWidget { child: Theme( data: Theme.of(context).copyWith( visualDensity: node.properties.visualDensity.flutterVisualDensity, - dividerColor: - node.properties.showDividers ? null : Colors.transparent, + dividerColor: node.properties.showDividers + ? null + : Colors.transparent, ), child: ExpansionTile( key: ValueKey(node.id), initiallyExpanded: initiallyExpanded ?? node.properties.initiallyExpanded, maintainState: node.properties.maintainState, + showTrailingIcon: node.properties.showTrailingIcon, + shape: node.properties.shape == null + ? null + : getShape( + shape: node.properties.shape!, + radius: node.properties.cornerRadius, + ), + collapsedShape: node.properties.collapsedShape == null + ? null + : getShape( + shape: node.properties.collapsedShape!, + radius: node.properties.cornerRadius, + ), tilePadding: node.properties.tilePadding?.flutterEdgeInsets, expandedCrossAxisAlignment: node.properties.expandedCrossAxisAlignment.flutterAxis, - expandedAlignment: - node.properties.expandedAlignment.flutterAlignment, + expandedAlignment: node.properties.expandedAlignment.flutterAlignment, childrenPadding: node.properties.childrenPadding?.flutterEdgeInsets, backgroundColor: node.properties.backgroundColor?.toFlutterColor(), - collapsedBackgroundColor: - node.properties.collapsedBackgroundColor?.toFlutterColor(), + collapsedBackgroundColor: node.properties.collapsedBackgroundColor + ?.toFlutterColor(), iconColor: node.properties.iconColor?.toFlutterColor(), textColor: node.properties.textColor?.toFlutterColor(), - collapsedTextColor: - node.properties.collapsedTextColor?.toFlutterColor(), - collapsedIconColor: - node.properties.collapsedIconColor?.toFlutterColor(), + collapsedTextColor: node.properties.collapsedTextColor + ?.toFlutterColor(), + collapsedIconColor: node.properties.collapsedIconColor + ?.toFlutterColor(), controlAffinity: node.properties .effectiveAffinity(node.properties.controlAffinity) .flutterControlAffinity, @@ -274,30 +288,25 @@ class PassiveExpansionTileWidget extends StatelessWidget { leading: leading == null ? null : leading is SinglePlaceholderNode && - leading.children.isEmpty && - shouldBuildLeading() - ? null - : StrictNodeBox( - node: leading, - child: Stack( - children: [ - buildWidgetFromNode(leading, context), - ], - ), - ), + leading.children.isEmpty && + shouldBuildLeading() + ? null + : StrictNodeBox( + node: leading, + child: Stack( + children: [buildWidgetFromNode(leading, context)], + ), + ), trailing: trailing == null ? null : trailing is SinglePlaceholderNode && - trailing.children.isEmpty && - shouldBuildTrailing() - ? null - : StrictNodeBox( - node: trailing, - child: buildWidgetFromNode( - trailing, - context, - ), - ), + trailing.children.isEmpty && + shouldBuildTrailing() + ? null + : StrictNodeBox( + node: trailing, + child: buildWidgetFromNode(trailing, context), + ), children: children .map((id) => buildWidgetFromNode(getNode(id), context)) .toList(), diff --git a/lib/src/transformers/node_transformers/passive_external_component_transformer.dart b/lib/src/transformers/node_transformers/passive_external_component_transformer.dart index 66c7923e..9d02e27b 100644 --- a/lib/src/transformers/node_transformers/passive_external_component_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_external_component_transformer.dart @@ -30,8 +30,8 @@ class PassiveExternalComponentWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final CodelesslyContext codelesslyContext = - context.watch(); + final CodelesslyContext codelesslyContext = context + .watch(); final WidgetBuilder? builder = codelesslyContext.externalComponentBuilders[node.properties.builderID]; diff --git a/lib/src/transformers/node_transformers/passive_floating_action_button_transformer.dart b/lib/src/transformers/node_transformers/passive_floating_action_button_transformer.dart index fdf7cda0..bc111069 100644 --- a/lib/src/transformers/node_transformers/passive_floating_action_button_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_floating_action_button_transformer.dart @@ -21,23 +21,28 @@ class PassiveFloatingActionButtonTransformer required FloatingActionButtonProperties props, double? width, double? height, - WidgetBuildSettings settings = - const WidgetBuildSettings(debugLabel: 'buildFromProps'), + WidgetBuildSettings settings = const WidgetBuildSettings( + debugLabel: 'buildFromProps', + ), }) { final node = FloatingActionButtonNode( id: '', name: 'FAB', - basicBoxLocal: - NodeBox(0, 0, width ?? props.type.size, height ?? props.type.size), - retainedOuterBoxLocal: - NodeBox(0, 0, width ?? props.type.size, props.type.size), + basicBoxLocal: NodeBox( + 0, + 0, + width ?? props.type.size, + height ?? props.type.size, + ), + retainedOuterBoxLocal: NodeBox( + 0, + 0, + width ?? props.type.size, + props.type.size, + ), properties: props, ); - return buildFromNode( - context, - node, - settings: settings, - ); + return buildFromNode(context, node, settings: settings); } Widget buildPreview({ @@ -45,8 +50,9 @@ class PassiveFloatingActionButtonTransformer FloatingActionButtonNode? node, double? width, VoidCallback? onPressed, - WidgetBuildSettings settings = - const WidgetBuildSettings(debugLabel: 'buildPreview'), + WidgetBuildSettings settings = const WidgetBuildSettings( + debugLabel: 'buildPreview', + ), }) { properties = properties ?? node?.properties ?? FloatingActionButtonProperties(); @@ -54,10 +60,18 @@ class PassiveFloatingActionButtonTransformer properties: properties, id: '', name: 'FAB', - basicBoxLocal: - NodeBox(0, 0, width ?? properties.type.size, properties.type.size), - retainedOuterBoxLocal: - NodeBox(0, 0, width ?? properties.type.size, properties.type.size), + basicBoxLocal: NodeBox( + 0, + 0, + width ?? properties.type.size, + properties.type.size, + ), + retainedOuterBoxLocal: NodeBox( + 0, + 0, + width ?? properties.type.size, + properties.type.size, + ), ); return PassiveFloatingActionButtonWidget( node: previewNode, @@ -69,8 +83,9 @@ class PassiveFloatingActionButtonTransformer Widget buildFromNode( BuildContext context, FloatingActionButtonNode node, { - WidgetBuildSettings settings = - const WidgetBuildSettings(debugLabel: 'buildFromNode'), + WidgetBuildSettings settings = const WidgetBuildSettings( + debugLabel: 'buildFromNode', + ), }) { return PassiveFloatingActionButtonWidget( node: node, @@ -80,8 +95,11 @@ class PassiveFloatingActionButtonTransformer } void onPressed(BuildContext context, List reactions) => - FunctionsRepository.triggerAction(context, TriggerType.click, - reactions: reactions); + FunctionsRepository.triggerAction( + context, + TriggerType.click, + reactions: reactions, + ); } class PassiveFloatingActionButtonWidget extends StatelessWidget { @@ -104,12 +122,7 @@ class PassiveFloatingActionButtonWidget extends StatelessWidget { return SizedBox( width: node.middleBoxLocal.width, height: node.middleBoxLocal.height, - child: buildFAB( - node.id, - fab, - onPressed: onPressed, - elevation: elevation, - ), + child: buildFAB(node.id, fab, onPressed: onPressed, elevation: elevation), ); } @@ -173,8 +186,10 @@ class PassiveFloatingActionButtonWidget extends StatelessWidget { return FloatingActionButton.extended( heroTag: null, onPressed: onPressed, - extendedPadding: - const EdgeInsets.symmetric(vertical: 2, horizontal: 10), + extendedPadding: const EdgeInsets.symmetric( + vertical: 2, + horizontal: 10, + ), backgroundColor: fab.backgroundColor.toFlutterColor(), foregroundColor: fab.foregroundColor.toFlutterColor(), elevation: elevation ?? fab.elevation, @@ -185,8 +200,9 @@ class PassiveFloatingActionButtonWidget extends StatelessWidget { hoverColor: fab.hoverColor?.toFlutterColor(), splashColor: fab.splashColor?.toFlutterColor(), extendedIconLabelSpacing: fab.extendedIconLabelSpacing, - extendedTextStyle: - TextUtils.retrieveTextStyleFromProp(fab.labelStyle), + extendedTextStyle: TextUtils.retrieveTextStyleFromProp( + fab.labelStyle, + ), shape: shape, icon: !fab.icon.show ? null : getFABIcon(fab), label: Text(fab.label), diff --git a/lib/src/transformers/node_transformers/passive_grid_view_transformer.dart b/lib/src/transformers/node_transformers/passive_grid_view_transformer.dart index 77fe753e..47b4829c 100644 --- a/lib/src/transformers/node_transformers/passive_grid_view_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_grid_view_transformer.dart @@ -4,10 +4,10 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:codelessly_api/codelessly_api.dart'; import 'package:firebase_ui_firestore/firebase_ui_firestore.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:provider/provider.dart'; import '../../../codelessly_sdk.dart'; -import '../../logging/error_logger.dart'; import '../../ui/draggable_scroll_configuration.dart'; class PassiveGridViewTransformer extends NodeWidgetTransformer { @@ -15,7 +15,10 @@ class PassiveGridViewTransformer extends NodeWidgetTransformer { @override Widget buildWidget( - GridViewNode node, BuildContext context, WidgetBuildSettings settings) { + GridViewNode node, + BuildContext context, + WidgetBuildSettings settings, + ) { return PassiveGridViewWidget( node: node, manager: manager, @@ -37,7 +40,9 @@ class PassiveGridViewWidget extends StatelessWidget { }); Query> constructQuery( - BuildContext context, Codelessly codelessly) { + BuildContext context, + Codelessly codelessly, + ) { final PublishSource source = codelessly.config?.publishSource ?? PublishSource.preview; @@ -104,7 +109,8 @@ class PassiveGridViewWidget extends StatelessWidget { return GridViewBuilder( primary: node.primary, gridDelegate: node.properties.gridDelegate, - shrinkWrap: node.properties.itemCount != null && + shrinkWrap: + node.properties.itemCount != null && (node.scrollDirection == AxisC.horizontal ? node.isHorizontalWrap : node.isVerticalWrap), @@ -112,8 +118,9 @@ class PassiveGridViewWidget extends StatelessWidget { padding: node.padding.flutterEdgeInsets, keyboardDismissBehavior: node.keyboardDismissBehavior.flutterKeyboardDismissBehavior, - physics: - node.physics.flutterScrollPhysics(node.shouldAlwaysScroll), + physics: node.physics.flutterScrollPhysics( + node.shouldAlwaysScroll, + ), scrollDirection: node.scrollDirection.flutterAxis, cacheExtent: node.properties.cacheExtent, reverse: node.reverse, @@ -124,10 +131,7 @@ class PassiveGridViewWidget extends StatelessWidget { final data = doc?.data(); return IndexedItemProvider( key: ValueKey(index), - item: IndexedItem( - index, - {if (id != null) 'id': id, ...?data}, - ), + item: IndexedItem(index, {'id': ?id, ...?data}), child: Builder( builder: (context) { // This builder is important to pass a context that has @@ -150,7 +154,8 @@ class PassiveGridViewWidget extends StatelessWidget { ); } - final List? data = PropertyValueDelegate.getVariableValueFromPath( + final List? data = + PropertyValueDelegate.getVariableValueFromPath( node.variables['data'] ?? '', scopedValues: ScopedValues.of(context), ) ?? @@ -168,7 +173,8 @@ class PassiveGridViewWidget extends StatelessWidget { child: GridViewBuilder( primary: node.primary, gridDelegate: node.properties.gridDelegate, - shrinkWrap: node.properties.itemCount != null && + shrinkWrap: + node.properties.itemCount != null && (node.scrollDirection == AxisC.horizontal ? node.isHorizontalWrap : node.isVerticalWrap), @@ -244,7 +250,9 @@ class GridViewBuilder extends StatelessWidget { padding: padding, physics: physics, scrollDirection: scrollDirection, - cacheExtent: cacheExtent, + scrollCacheExtent: cacheExtent == null + ? null + : ScrollCacheExtent.pixels(cacheExtent!), reverse: reverse, clipBehavior: clipBehavior, itemBuilder: itemBuilder!, diff --git a/lib/src/transformers/node_transformers/passive_icon_transformer.dart b/lib/src/transformers/node_transformers/passive_icon_transformer.dart index aac7b5c0..f927bc36 100644 --- a/lib/src/transformers/node_transformers/passive_icon_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_icon_transformer.dart @@ -37,6 +37,8 @@ class PassiveIconWidget extends StatelessWidget { Widget build(BuildContext context) { return retrieveIconWidget(node.properties.icon, node.basicBoxLocal.width) ?? SizedBox( - width: node.basicBoxLocal.width, height: node.basicBoxLocal.height); + width: node.basicBoxLocal.width, + height: node.basicBoxLocal.height, + ); } } diff --git a/lib/src/transformers/node_transformers/passive_list_tile_transformer.dart b/lib/src/transformers/node_transformers/passive_list_tile_transformer.dart index 449ae819..af8d6a8f 100644 --- a/lib/src/transformers/node_transformers/passive_list_tile_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_list_tile_transformer.dart @@ -29,12 +29,18 @@ class PassiveListTileTransformer extends NodeWidgetTransformer { } void onTap(BuildContext context, List reactions) => - FunctionsRepository.triggerAction(context, TriggerType.click, - reactions: reactions); + FunctionsRepository.triggerAction( + context, + TriggerType.click, + reactions: reactions, + ); void onLongPress(BuildContext context, List reactions) => - FunctionsRepository.triggerAction(context, TriggerType.longPress, - reactions: reactions); + FunctionsRepository.triggerAction( + context, + TriggerType.longPress, + reactions: reactions, + ); Widget buildPreview( BuildContext context, { @@ -51,7 +57,8 @@ class PassiveListTileTransformer extends NodeWidgetTransformer { replaceVariablesWithSymbols: true, ), }) { - final previewNode = node ?? + final previewNode = + node ?? ListTileNode( id: 'list_tile', name: 'List Tile', @@ -128,7 +135,7 @@ class PassiveListTileTransformer extends NodeWidgetTransformer { end: titleText.length, fills: [PaintModel.blackPaint], fontSize: 16, - ) + ), ], ); @@ -146,7 +153,7 @@ class PassiveListTileTransformer extends NodeWidgetTransformer { end: subtitleText.length, fills: [PaintModel.blackPaint], fontSize: 14, - ) + ), ], ); @@ -156,12 +163,7 @@ class PassiveListTileTransformer extends NodeWidgetTransformer { basicBoxLocal: NodeBox(0, 0, width, height), retainedOuterBoxLocal: NodeBox(0, 0, width, height), properties: properties, - children: [ - leading.id, - title.id, - subtitle.id, - trailing.id, - ], + children: [leading.id, title.id, subtitle.id, trailing.id], leading: leading.id, title: title.id, subtitle: subtitle.id, @@ -278,8 +280,8 @@ class PassiveListTileWidget extends StatelessWidget { isThreeLine: node.properties.isThreeLine, shape: getShapeFromMixin(node.properties), selected: node.properties.selected, - selectedTileColor: - node.properties.selectedTileColor?.toFlutterColor(), + selectedTileColor: node.properties.selectedTileColor + ?.toFlutterColor(), hoverColor: node.properties.hoverColor?.toFlutterColor(), focusColor: node.properties.focusColor?.toFlutterColor(), enabled: node.properties.enabled, @@ -292,38 +294,40 @@ class PassiveListTileWidget extends StatelessWidget { horizontalTitleGap: node.properties.horizontalTitleGap, minVerticalPadding: node.properties.minVerticalPadding, minLeadingWidth: node.properties.minLeadingWidth, + minTileHeight: node.properties.minTileHeight, + titleAlignment: node.properties.titleAlignment?.toFlutter(), title: title == null ? null : titleWidget ?? - SizedBox( - width: title.basicBoxLocal.width, - height: title.basicBoxLocal.height, - child: buildWidgetFromNode(title, context), - ), + SizedBox( + width: title.basicBoxLocal.width, + height: title.basicBoxLocal.height, + child: buildWidgetFromNode(title, context), + ), subtitle: subtitle == null ? null : subtitleWidget ?? - SizedBox( - width: subtitle.basicBoxLocal.width, - height: subtitle.basicBoxLocal.height, - child: buildWidgetFromNode(subtitle, context), - ), + SizedBox( + width: subtitle.basicBoxLocal.width, + height: subtitle.basicBoxLocal.height, + child: buildWidgetFromNode(subtitle, context), + ), leading: leading == null ? null : leadingWidget ?? - SizedBox( - width: leading.basicBoxLocal.width, - height: leading.basicBoxLocal.height, - child: buildWidgetFromNode(leading, context), - ), + SizedBox( + width: leading.basicBoxLocal.width, + height: leading.basicBoxLocal.height, + child: buildWidgetFromNode(leading, context), + ), trailing: trailing == null ? null : trailingWidget ?? - SizedBox( - width: trailing.basicBoxLocal.width, - height: trailing.basicBoxLocal.height, - child: buildWidgetFromNode(trailing, context), - ), + SizedBox( + width: trailing.basicBoxLocal.width, + height: trailing.basicBoxLocal.height, + child: buildWidgetFromNode(trailing, context), + ), ), ), ); diff --git a/lib/src/transformers/node_transformers/passive_list_view_transformer.dart b/lib/src/transformers/node_transformers/passive_list_view_transformer.dart index 0943c7e6..787a78ba 100644 --- a/lib/src/transformers/node_transformers/passive_list_view_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_list_view_transformer.dart @@ -4,10 +4,10 @@ import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:codelessly_api/codelessly_api.dart'; import 'package:firebase_ui_firestore/firebase_ui_firestore.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:provider/provider.dart'; import '../../../codelessly_sdk.dart'; -import '../../logging/error_logger.dart'; import '../../ui/draggable_scroll_configuration.dart'; class PassiveListViewTransformer extends NodeWidgetTransformer { @@ -15,7 +15,10 @@ class PassiveListViewTransformer extends NodeWidgetTransformer { @override Widget buildWidget( - ListViewNode node, BuildContext context, WidgetBuildSettings settings) { + ListViewNode node, + BuildContext context, + WidgetBuildSettings settings, + ) { return PassiveListViewWidget( node: node, manager: manager, @@ -37,7 +40,9 @@ class PassiveListViewWidget extends StatelessWidget { }); Query> constructQuery( - BuildContext context, Codelessly codelessly) { + BuildContext context, + Codelessly codelessly, + ) { final PublishSource source = codelessly.config?.publishSource ?? PublishSource.preview; @@ -112,8 +117,9 @@ class PassiveListViewWidget extends StatelessWidget { padding: node.padding.flutterEdgeInsets, keyboardDismissBehavior: node.keyboardDismissBehavior.flutterKeyboardDismissBehavior, - physics: - node.physics.flutterScrollPhysics(node.shouldAlwaysScroll), + physics: node.physics.flutterScrollPhysics( + node.shouldAlwaysScroll, + ), scrollDirection: node.scrollDirection.flutterAxis, cacheExtent: node.properties.cacheExtent, reverse: node.reverse, @@ -121,9 +127,9 @@ class PassiveListViewWidget extends StatelessWidget { clipBehavior: node.clipsContent ? Clip.hardEdge : Clip.none, separatedBuilder: node.properties.hasSeparator ? (context, index) => ListViewItemSeparator( - scrollDirection: node.scrollDirection, - properties: node.properties, - ) + scrollDirection: node.scrollDirection, + properties: node.properties, + ) : null, itemBuilder: (context, index) { final doc = docs.elementAtOrNull(index); @@ -131,22 +137,21 @@ class PassiveListViewWidget extends StatelessWidget { final data = doc?.data(); return IndexedItemProvider( key: ValueKey(index), - item: IndexedItem( - index, - {if (id != null) 'id': id, ...?data}, + item: IndexedItem(index, {'id': ?id, ...?data}), + child: Builder( + builder: (context) { + // This builder is important to pass a context that has + // the IndexedItemProvider. + return KeyedSubtree( + key: ValueKey(index), + child: manager.buildWidgetByID( + itemNode, + context, + settings: settings, + ), + ); + }, ), - child: Builder(builder: (context) { - // This builder is important to pass a context that has - // the IndexedItemProvider. - return KeyedSubtree( - key: ValueKey(index), - child: manager.buildWidgetByID( - itemNode, - context, - settings: settings, - ), - ); - }), ); }, ); @@ -155,7 +160,8 @@ class PassiveListViewWidget extends StatelessWidget { ); } - final List? data = PropertyValueDelegate.getVariableValueFromPath( + final List? data = + PropertyValueDelegate.getVariableValueFromPath( node.variables['data'] ?? '', scopedValues: ScopedValues.of(context), ) ?? @@ -180,9 +186,9 @@ class PassiveListViewWidget extends StatelessWidget { final bool shrinkWrap = parentScrollable != null ? true : node.properties.itemCount != null && - (node.scrollDirection == AxisC.horizontal - ? node.isHorizontalWrap - : node.isVerticalWrap); + (node.scrollDirection == AxisC.horizontal + ? node.isHorizontalWrap + : node.isVerticalWrap); return AdaptiveNodeBox( node: node, @@ -199,28 +205,31 @@ class PassiveListViewWidget extends StatelessWidget { reverse: node.reverse, itemExtent: node.properties.itemExtent, clipBehavior: node.clipsContent ? Clip.hardEdge : Clip.none, - separatedBuilder: (node.properties.itemCount != null || data != null) && + separatedBuilder: + (node.properties.itemCount != null || data != null) && node.properties.hasSeparator ? (context, index) => ListViewItemSeparator( - scrollDirection: node.scrollDirection, - properties: node.properties, - ) + scrollDirection: node.scrollDirection, + properties: node.properties, + ) : null, itemBuilder: (context, index) => IndexedItemProvider( key: ValueKey(index), item: IndexedItem(index, data?.elementAtOrNull(index)), - child: Builder(builder: (context) { - // This builder is important to pass a context that has - // the IndexedItemProvider. - return KeyedSubtree( - key: ValueKey(index), - child: manager.buildWidgetByID( - itemNode, - context, - settings: settings, - ), - ); - }), + child: Builder( + builder: (context) { + // This builder is important to pass a context that has + // the IndexedItemProvider. + return KeyedSubtree( + key: ValueKey(index), + child: manager.buildWidgetByID( + itemNode, + context, + settings: settings, + ), + ); + }, + ), ), ), ); @@ -272,7 +281,9 @@ class ListViewBuilder extends StatelessWidget { padding: padding, physics: physics, scrollDirection: scrollDirection, - cacheExtent: cacheExtent, + scrollCacheExtent: cacheExtent == null + ? null + : ScrollCacheExtent.pixels(cacheExtent!), reverse: reverse, clipBehavior: clipBehavior, itemBuilder: itemBuilder!, @@ -288,7 +299,9 @@ class ListViewBuilder extends StatelessWidget { padding: padding, physics: physics, scrollDirection: scrollDirection, - cacheExtent: cacheExtent, + scrollCacheExtent: cacheExtent == null + ? null + : ScrollCacheExtent.pixels(cacheExtent!), reverse: reverse, itemExtent: itemExtent, clipBehavior: clipBehavior, @@ -333,10 +346,12 @@ class ListViewItemSeparator extends StatelessWidget { ); case ListItemSeparator.space: return SizedBox( - width: - scrollDirection.isHorizontal ? properties.separatorSpacing : null, - height: - scrollDirection.isVertical ? properties.separatorSpacing : null, + width: scrollDirection.isHorizontal + ? properties.separatorSpacing + : null, + height: scrollDirection.isVertical + ? properties.separatorSpacing + : null, ); } } diff --git a/lib/src/transformers/node_transformers/passive_loading_indicator_transformer.dart b/lib/src/transformers/node_transformers/passive_loading_indicator_transformer.dart index 39dfb175..33eede2d 100644 --- a/lib/src/transformers/node_transformers/passive_loading_indicator_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_loading_indicator_transformer.dart @@ -1,7 +1,6 @@ import 'package:codelessly_api/codelessly_api.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; import '../../../codelessly_sdk.dart'; @@ -24,8 +23,9 @@ class PassiveLoadingIndicatorTransformer required double height, required double width, bool animate = true, - WidgetBuildSettings settings = - const WidgetBuildSettings(debugLabel: 'buildFromProps'), + WidgetBuildSettings settings = const WidgetBuildSettings( + debugLabel: 'buildFromProps', + ), }) { final node = LoadingIndicatorNode( id: '', @@ -44,8 +44,9 @@ class PassiveLoadingIndicatorTransformer double? width, ValueChanged? onChanged, bool animate = false, - WidgetBuildSettings settings = - const WidgetBuildSettings(debugLabel: 'buildPreview'), + WidgetBuildSettings settings = const WidgetBuildSettings( + debugLabel: 'buildPreview', + ), }) { final previewNode = LoadingIndicatorNode( properties: node?.properties ?? properties!, @@ -65,8 +66,9 @@ class PassiveLoadingIndicatorTransformer Widget buildFromNode( LoadingIndicatorNode node, { bool animate = true, - WidgetBuildSettings settings = - const WidgetBuildSettings(debugLabel: 'buildFromNode'), + WidgetBuildSettings settings = const WidgetBuildSettings( + debugLabel: 'buildFromNode', + ), }) { return PassiveLoadingIndicatorWidget( node: node, @@ -83,7 +85,7 @@ class PassiveLoadingIndicatorTransformer void onAction(Reaction reaction) { switch (reaction.action.type) { case ActionType.link: - launchUrl(Uri.parse((reaction.action as LinkAction).url)); + FunctionsRepository.openLink((reaction.action as LinkAction).url); default: break; } @@ -109,8 +111,8 @@ class PassiveLoadingIndicatorWidget extends StatelessWidget { @override Widget build(BuildContext context) { if (node.properties.isMaterial) { - final MaterialLoadingIndicatorProperties props = - node.properties.asMaterial(); + final MaterialLoadingIndicatorProperties props = node.properties + .asMaterial(); return Center( child: SizedBox.square( @@ -121,12 +123,14 @@ class PassiveLoadingIndicatorWidget extends StatelessWidget { color: props.color.toFlutterColor(), backgroundColor: props.backgroundColor.toFlutterColor(), strokeCap: props.strokeCap.flutterStrokeCap, + strokeAlign: props.strokeAlign, + trackGap: props.trackGap, ), ), ); } else if (node.properties.isCupertino) { - final CupertinoLoadingIndicatorProperties props = - node.properties.asCupertino(); + final CupertinoLoadingIndicatorProperties props = node.properties + .asCupertino(); return CupertinoActivityIndicator( radius: (node.basicBoxLocal.width ~/ 2).toDouble(), color: props.color.toFlutterColor(), diff --git a/lib/src/transformers/node_transformers/passive_navigation_bar_transformer.dart b/lib/src/transformers/node_transformers/passive_navigation_bar_transformer.dart index 4e178567..8a1090bc 100644 --- a/lib/src/transformers/node_transformers/passive_navigation_bar_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_navigation_bar_transformer.dart @@ -25,8 +25,11 @@ class PassiveNavigationBarTransformer void onChanged(NavigationBarNode node, BuildContext context, int index) { NodeStateProvider.setState(context, index); final item = node.properties.items[index]; - FunctionsRepository.triggerAction(context, TriggerType.changed, - reactions: item.reactions); + FunctionsRepository.triggerAction( + context, + TriggerType.changed, + reactions: item.reactions, + ); } Widget buildFromProps( @@ -34,8 +37,9 @@ class PassiveNavigationBarTransformer double width = 250, double height = kBottomNavigationBarHeight, required NavigationBarProperties props, - WidgetBuildSettings settings = - const WidgetBuildSettings(debugLabel: 'buildFromProps'), + WidgetBuildSettings settings = const WidgetBuildSettings( + debugLabel: 'buildFromProps', + ), }) { final node = NavigationBarNode( id: '', @@ -57,17 +61,26 @@ class PassiveNavigationBarTransformer double? width, int currentIndex = 0, ValueChanged? onChanged, - WidgetBuildSettings settings = - const WidgetBuildSettings(debugLabel: 'buildPreview'), + WidgetBuildSettings settings = const WidgetBuildSettings( + debugLabel: 'buildPreview', + ), }) { final previewNode = NavigationBarNode( properties: properties ?? node!.properties, id: '', name: 'NavigationBar', - basicBoxLocal: - NodeBox(0, 0, width ?? 300, height ?? kBottomNavigationBarHeight), - retainedOuterBoxLocal: - NodeBox(0, 0, width ?? 300, height ?? kBottomNavigationBarHeight), + basicBoxLocal: NodeBox( + 0, + 0, + width ?? 300, + height ?? kBottomNavigationBarHeight, + ), + retainedOuterBoxLocal: NodeBox( + 0, + 0, + width ?? 300, + height ?? kBottomNavigationBarHeight, + ), currentIndex: currentIndex, verticalFit: SizeFit.shrinkWrap, ); @@ -109,7 +122,8 @@ class _PassiveNavigationBarWidgetState @override void initState() { - currentIndex = context.getNodeValue(widget.node.id, 'currentIndex') ?? + currentIndex = + context.getNodeValue(widget.node.id, 'currentIndex') ?? widget.node.currentIndex; super.initState(); } @@ -152,10 +166,7 @@ class _PassiveNavigationBarWidgetState child: navBar, ); } - navBar = AdaptiveNodeBox( - node: widget.node, - child: navBar, - ); + navBar = AdaptiveNodeBox(node: widget.node, child: navBar); // BottomAppBars have a hard requirement for a scaffold, and scaffolds // have a hard requirement for a size. @@ -200,16 +211,18 @@ class _PassiveNavigationBarWidgetState labelTextStyle: WidgetStateProperty.resolveWith((states) { if (states.contains(WidgetState.selected)) { return TextUtils.retrieveTextStyleFromProp( - style.selectedLabelStyle); + style.selectedLabelStyle, + ); } return TextUtils.retrieveTextStyleFromProp( - style.unselectedLabelStyle); + style.unselectedLabelStyle, + ); }), ), child: MediaQuery( - data: MediaQuery.of(context).copyWith( - padding: widget.applyBottomPadding ? null : EdgeInsets.zero, - ), + data: MediaQuery.of( + context, + ).copyWith(padding: widget.applyBottomPadding ? null : EdgeInsets.zero), child: NavigationBar( selectedIndex: currentIndex, onDestinationSelected: onChanged, @@ -218,7 +231,7 @@ class _PassiveNavigationBarWidgetState height: node.basicBoxLocal.height, labelBehavior: style.labelBehavior.flutterLabelBehavior, destinations: [ - for (final itemModel in style.items) getM3Item(itemModel, style) + for (final itemModel in style.items) getM3Item(itemModel, style), ], ), ), @@ -233,10 +246,13 @@ class _PassiveNavigationBarWidgetState final M2NavigationBarProperties style = node.properties as M2NavigationBarProperties; - final Color? selectedLabelColor = - style.selectedLabelStyle.fills.firstOrNull?.toFlutterColor(); - final Color? unselectedLabelColor = - style.unselectedLabelStyle.fills.firstOrNull?.toFlutterColor(); + final Color? selectedLabelColor = style.selectedLabelStyle.fills.firstOrNull + ?.toFlutterColor(); + final Color? unselectedLabelColor = style + .unselectedLabelStyle + .fills + .firstOrNull + ?.toFlutterColor(); final IconThemeData selectedIconTheme = IconThemeData( color: style.selectedIconColor?.toFlutterColor(), @@ -249,9 +265,11 @@ class _PassiveNavigationBarWidgetState ); final TextStyle selectedLabelStyle = TextUtils.retrieveTextStyleFromProp( - style.selectedLabelStyle.copyWith(fills: [])); + style.selectedLabelStyle.copyWith(fills: []), + ); final unselectedLabelStyle = TextUtils.retrieveTextStyleFromProp( - style.unselectedLabelStyle.copyWith(fills: [])); + style.unselectedLabelStyle.copyWith(fills: []), + ); final double elevation = style.makeNotched ? 0 : style.elevation; @@ -276,9 +294,9 @@ class _PassiveNavigationBarWidgetState /// /// We don't want that. We disable it here. return MediaQuery( - data: MediaQuery.of(context).copyWith( - padding: widget.applyBottomPadding ? null : EdgeInsets.zero, - ), + data: MediaQuery.of( + context, + ).copyWith(padding: widget.applyBottomPadding ? null : EdgeInsets.zero), child: BottomNavigationBar( currentIndex: currentIndex, onTap: onChanged, @@ -296,7 +314,7 @@ class _PassiveNavigationBarWidgetState showUnselectedLabels: style.labelBehavior.showForUnselected(), iconSize: style.unselectedIconSize, items: [ - for (final itemModel in style.items) getM2Item(itemModel, style) + for (final itemModel in style.items) getM2Item(itemModel, style), ], ), ); @@ -314,12 +332,12 @@ class _PassiveNavigationBarWidgetState final Widget? selectedIconDj = itemModel.differSelectedIcon && itemModel.selectedIcon != null - ? retrieveNavBarItemIconWidget( - itemModel.selectedIcon!, - null, - // style.selectedIconSize, - ) - : null; + ? retrieveNavBarItemIconWidget( + itemModel.selectedIcon!, + null, + // style.selectedIconSize, + ) + : null; return BottomNavigationBarItem( icon: unselectedIconDj, activeIcon: selectedIconDj, @@ -328,9 +346,11 @@ class _PassiveNavigationBarWidgetState // bottom navigation bar when it is shifting type and color per item // is not provided. // Detailed explanation: https://codelessly.slack.com/archives/C018XK7431R/p1659077643469429 - backgroundColor: itemModel.backgroundColor?.toFlutterColor() ?? + backgroundColor: + itemModel.backgroundColor?.toFlutterColor() ?? style.backgroundColor?.toFlutterColor().orNullIf( - style.navigationBarType != M2NavigationBarType.shifting), + style.navigationBarType != M2NavigationBarType.shifting, + ), label: itemModel.label, tooltip: itemModel.tooltip.orNullIf(itemModel.label == itemModel.tooltip), ); @@ -348,12 +368,12 @@ class _PassiveNavigationBarWidgetState final Widget? selectedIconDj = itemModel.differSelectedIcon && itemModel.selectedIcon != null - ? retrieveNavBarItemIconWidget( - itemModel.selectedIcon!, - null, - // style.selectedIconSize, - ) - : null; + ? retrieveNavBarItemIconWidget( + itemModel.selectedIcon!, + null, + // style.selectedIconSize, + ) + : null; return NavigationDestination( icon: unselectedIcon, selectedIcon: selectedIconDj, diff --git a/lib/src/transformers/node_transformers/passive_page_view_transformer.dart b/lib/src/transformers/node_transformers/passive_page_view_transformer.dart index 74784d1c..03a34edb 100644 --- a/lib/src/transformers/node_transformers/passive_page_view_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_page_view_transformer.dart @@ -11,7 +11,10 @@ class PassivePageViewTransformer extends NodeWidgetTransformer { @override Widget buildWidget( - PageViewNode node, BuildContext context, WidgetBuildSettings settings) { + PageViewNode node, + BuildContext context, + WidgetBuildSettings settings, + ) { return PassivePageViewWidget( node: node, manager: manager, @@ -21,14 +24,26 @@ class PassivePageViewTransformer extends NodeWidgetTransformer { } void onPageChanged(BuildContext context, int index, PageViewNode node) { - FunctionsRepository.setNodeValue(context, - node: node, property: 'indexValue', value: index); + FunctionsRepository.setNodeValue( + context, + node: node, + property: 'indexValue', + value: index, + ); - FunctionsRepository.setPropertyVariable(context, - node: node, property: 'indexValue', value: '$index'); + FunctionsRepository.setPropertyVariable( + context, + node: node, + property: 'indexValue', + value: '$index', + ); - FunctionsRepository.triggerAction(context, TriggerType.changed, - node: node, value: index); + FunctionsRepository.triggerAction( + context, + TriggerType.changed, + node: node, + value: index, + ); } } @@ -69,7 +84,7 @@ class _PassivePageViewWidgetState extends State { super.didUpdateWidget(oldWidget); if (widget.node.properties.initialPage != oldWidget.node.properties.initialPage) { - controller.jumpToPage(widget.node.properties.initialPage); + _jumpToPage(widget.node.properties.initialPage); } final ScopedValues scopedValues = ScopedValues.of(context); @@ -81,17 +96,29 @@ class _PassivePageViewWidgetState extends State { if (widget.node.variables['indexValue'] != null) { final int? currentPropertyValue = PropertyValueDelegate.getPropertyValue( - widget.node, - 'indexValue', - scopedValues: scopedValues, - ); + widget.node, + 'indexValue', + scopedValues: scopedValues, + ); if (currentPropertyValue != null && controller.page?.round() != currentPropertyValue) { - controller.jumpToPage(currentPropertyValue); + _jumpToPage(currentPropertyValue); } } } + /// Jumps once the controller is attached; didUpdateWidget can run before + /// the page view has laid out, when jumpToPage would throw. + void _jumpToPage(int page) { + if (controller.hasClients) { + controller.jumpToPage(page); + return; + } + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted && controller.hasClients) controller.jumpToPage(page); + }); + } + @override Widget build(BuildContext context) { if (widget.node.children.isEmpty) { @@ -116,12 +143,14 @@ class _PassivePageViewWidgetState extends State { child: DraggableScrollConfiguration( child: PageView.builder( itemCount: itemCount, - physics: widget.node.physics - .flutterScrollPhysics(widget.node.shouldAlwaysScroll), + physics: widget.node.physics.flutterScrollPhysics( + widget.node.shouldAlwaysScroll, + ), scrollDirection: widget.node.scrollDirection.flutterAxis, reverse: widget.node.reverse, clipBehavior: widget.node.clipsContent ? Clip.hardEdge : Clip.none, padEnds: widget.node.properties.padEnds, + allowImplicitScrolling: widget.node.properties.allowImplicitScrolling, pageSnapping: widget.node.properties.pageSnapping, controller: controller, onPageChanged: (index) => widget.onPageChanged?.call(context, index), diff --git a/lib/src/transformers/node_transformers/passive_progressbar_transformer.dart b/lib/src/transformers/node_transformers/passive_progressbar_transformer.dart index 7cdc3605..e0ede75e 100644 --- a/lib/src/transformers/node_transformers/passive_progressbar_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_progressbar_transformer.dart @@ -42,17 +42,26 @@ class PassiveProgressBarTransformer double? currentValue, ValueChanged? onChanged, bool animate = false, - WidgetBuildSettings settings = - const WidgetBuildSettings(debugLabel: 'buildPreview'), + WidgetBuildSettings settings = const WidgetBuildSettings( + debugLabel: 'buildPreview', + ), }) { final previewNode = ProgressBarNode( properties: properties ?? node?.properties ?? ProgressBarProperties(), id: '', name: 'ProgressBar', - basicBoxLocal: NodeBox(0, 0, width ?? kProgressBarDefaultWidth, - height ?? kProgressBarDefaultHeight), - retainedOuterBoxLocal: NodeBox(0, 0, width ?? kProgressBarDefaultWidth, - height ?? kProgressBarDefaultHeight), + basicBoxLocal: NodeBox( + 0, + 0, + width ?? kProgressBarDefaultWidth, + height ?? kProgressBarDefaultHeight, + ), + retainedOuterBoxLocal: NodeBox( + 0, + 0, + width ?? kProgressBarDefaultWidth, + height ?? kProgressBarDefaultHeight, + ), currentValue: currentValue ?? node?.currentValue ?? 0, ); return PassiveProgressBarWidget( @@ -75,11 +84,19 @@ class PassiveProgressBarTransformer } void onChanged(BuildContext context, ProgressBarNode node, double value) { - FunctionsRepository.setPropertyValue(context, - node: node, property: 'currentValue', value: value); + FunctionsRepository.setPropertyValue( + context, + node: node, + property: 'currentValue', + value: value, + ); FunctionsRepository.triggerAction( - context, node: node, TriggerType.changed, value: value); + context, + node: node, + TriggerType.changed, + value: value, + ); } } @@ -105,7 +122,8 @@ class PassiveProgressBarWidget extends StatelessWidget { context, variablesOverrides: variablesOverrides, ); - final double currentValue = PropertyValueDelegate.getPropertyValue( + final double currentValue = + PropertyValueDelegate.getPropertyValue( node, 'currentValue', scopedValues: scopedValues, @@ -114,11 +132,11 @@ class PassiveProgressBarWidget extends StatelessWidget { final Color progressColor = PropertyValueDelegate.getPropertyValue( - node, - 'progressColor', - scopedValues: scopedValues, - )?.toFlutterColor() ?? - node.properties.progressColor.toFlutterColor(); + node, + 'progressColor', + scopedValues: scopedValues, + )?.toFlutterColor() ?? + node.properties.progressColor.toFlutterColor(); return AdaptiveNodeBox( node: node, diff --git a/lib/src/transformers/node_transformers/passive_radio_transformer.dart b/lib/src/transformers/node_transformers/passive_radio_transformer.dart index 94ea69ee..6e9d0f38 100644 --- a/lib/src/transformers/node_transformers/passive_radio_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_radio_transformer.dart @@ -24,8 +24,9 @@ class PassiveRadioTransformer extends NodeWidgetTransformer { required double width, String value = '', String? groupValue, - WidgetBuildSettings settings = - const WidgetBuildSettings(debugLabel: 'buildPreview'), + WidgetBuildSettings settings = const WidgetBuildSettings( + debugLabel: 'buildPreview', + ), }) { final node = RadioNode( id: '', @@ -69,7 +70,10 @@ class PassiveRadioTransformer extends NodeWidgetTransformer { } Widget buildFromNode( - BuildContext context, RadioNode node, WidgetBuildSettings settings) { + BuildContext context, + RadioNode node, + WidgetBuildSettings settings, + ) { return PassiveRadioWidget( node: node, settings: settings, @@ -79,11 +83,19 @@ class PassiveRadioTransformer extends NodeWidgetTransformer { void onChanged(BuildContext context, RadioNode node, String? value) { NodeStateProvider.setState(context, value); - FunctionsRepository.setPropertyValue(context, - node: node, property: 'groupValue', value: value); + FunctionsRepository.setPropertyValue( + context, + node: node, + property: 'groupValue', + value: value, + ); FunctionsRepository.triggerAction( - context, node: node, TriggerType.click, value: value); + context, + node: node, + TriggerType.click, + value: value, + ); } void onAction(Reaction reaction) { @@ -116,13 +128,15 @@ class PassiveRadioWidget extends StatelessWidget { context, variablesOverrides: variablesOverrides, ); - final value = PropertyValueDelegate.getPropertyValue( + final value = + PropertyValueDelegate.getPropertyValue( node, 'value', scopedValues: scopedValues, ) ?? node.value; - final groupValue = PropertyValueDelegate.getPropertyValue( + final groupValue = + PropertyValueDelegate.getPropertyValue( node, 'groupValue', scopedValues: scopedValues, @@ -136,22 +150,34 @@ class PassiveRadioWidget extends StatelessWidget { height: node.basicBoxLocal.height, child: Transform.scale( scale: scale, - child: Radio( - value: value, + child: RadioGroup( groupValue: groupValue, - autofocus: node.properties.autofocus, - activeColor: node.properties.activeColor.toFlutterColor(), - hoverColor: node.properties.hoverColor.toFlutterColor(), - focusColor: node.properties.focusColor.toFlutterColor(), onChanged: (value) => onChanged?.call(context, value), - toggleable: node.properties.toggleable, - splashRadius: node.properties.splashRadius, - fillColor: WidgetStateProperty.resolveWith((states) { - if (states.contains(WidgetState.selected)) { - return node.properties.activeColor.toFlutterColor(); - } - return node.properties.inactiveColor.toFlutterColor(); - }), + child: Radio( + value: value, + enabled: onChanged != null, + autofocus: node.properties.autofocus, + activeColor: node.properties.activeColor.toFlutterColor(), + hoverColor: node.properties.hoverColor.toFlutterColor(), + focusColor: node.properties.focusColor.toFlutterColor(), + toggleable: node.properties.toggleable, + splashRadius: node.properties.splashRadius, + side: node.properties.side?.toFlutter(), + innerRadius: node.properties.innerRadius == null + ? null + : WidgetStatePropertyAll(node.properties.innerRadius), + backgroundColor: node.properties.backgroundColor == null + ? null + : WidgetStatePropertyAll( + node.properties.backgroundColor!.toFlutterColor(), + ), + fillColor: WidgetStateProperty.resolveWith((states) { + if (states.contains(WidgetState.selected)) { + return node.properties.activeColor.toFlutterColor(); + } + return node.properties.inactiveColor.toFlutterColor(); + }), + ), ), ), ); diff --git a/lib/src/transformers/node_transformers/passive_rectangle_transformer.dart b/lib/src/transformers/node_transformers/passive_rectangle_transformer.dart index e0cf6ec9..cc778d34 100644 --- a/lib/src/transformers/node_transformers/passive_rectangle_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_rectangle_transformer.dart @@ -19,16 +19,15 @@ class PassiveRectangleTransformer extends NodeWidgetTransformer { final List children = []; for (String childID in node.childrenOrEmpty) { - final Widget builtWidget = - manager.buildWidgetByID(childID, context, settings: settings); + final Widget builtWidget = manager.buildWidgetByID( + childID, + context, + settings: settings, + ); children.add(builtWidget); } - return buildRectangle( - node, - settings: settings, - children: children, - ); + return buildRectangle(node, settings: settings, children: children); } Widget buildRectangle( @@ -81,27 +80,25 @@ class PassiveRectangleWidget extends StatelessWidget { /// TODO Birju / Saad. Why does the commented out parent code below break published layouts only? /// Saad's note: The below code looks like AdaptiveNodeBox - final double? width = (node.horizontalFit == SizeFit.shrinkWrap || - isPlaceholder) + final double? width = + (node.horizontalFit == SizeFit.shrinkWrap || isPlaceholder) ? null : (node.horizontalFit == - SizeFit - .expanded /* && - (parent is! CanvasNode || parent.properties.bodyId != node.id)*/ - ) - ? double.infinity - : node.basicBoxLocal.width; - - final double? height = (node.verticalFit == SizeFit.shrinkWrap || - isPlaceholder) + SizeFit + .expanded /* && + (parent is! CanvasNode || parent.properties.bodyId != node.id)*/ ) + ? double.infinity + : node.basicBoxLocal.width; + + final double? height = + (node.verticalFit == SizeFit.shrinkWrap || isPlaceholder) ? null : (node.verticalFit == - SizeFit - .expanded /*&& - (parent is! CanvasNode || parent.properties.bodyId != node.id)*/ - ) - ? double.infinity - : node.basicBoxLocal.height; + SizeFit + .expanded /*&& + (parent is! CanvasNode || parent.properties.bodyId != node.id)*/ ) + ? double.infinity + : node.basicBoxLocal.height; Widget? portalWidget; if (node case PortalMixin portal) { @@ -143,10 +140,7 @@ class PassiveRectangleWidget extends StatelessWidget { node, wrapWithPaddingAndScroll( node, - [ - ...children, - if (portalWidget != null) portalWidget, - ], + [...children, ?portalWidget], stackAlignment: stackAlignment, applyPadding: applyPadding, ), @@ -154,10 +148,11 @@ class PassiveRectangleWidget extends StatelessWidget { ], ); - bool hasComplexRendering = node is GeometryMixin && + bool hasComplexRendering = + node is GeometryMixin && (node as GeometryMixin).fills.any( - (PaintModel fill) => fill.blendMode != BlendModeC.srcOver, - ); + (PaintModel fill) => fill.blendMode != BlendModeC.srcOver, + ); if (hasComplexRendering) { child = BlendMask(child: child); @@ -190,13 +185,11 @@ List wrapWithInkWell( if (inkWell == null) return children; - final List onClickReactions = (node as ReactionMixin) - .reactions + final List onClickReactions = (node as ReactionMixin).reactions .where((reaction) => reaction.trigger.type == TriggerType.click) .toList(); - final List onLongPressReactions = (node as ReactionMixin) - .reactions + final List onLongPressReactions = (node as ReactionMixin).reactions .where((reaction) => reaction.trigger.type == TriggerType.longPress) .toList(); @@ -206,17 +199,17 @@ List wrapWithInkWell( onTap: onClickReactions.isEmpty ? null : () => FunctionsRepository.triggerAction( - context, - TriggerType.click, - reactions: onClickReactions, - ), + context, + TriggerType.click, + reactions: onClickReactions, + ), onLongPress: onLongPressReactions.isEmpty ? null : () => FunctionsRepository.triggerAction( - context, - TriggerType.longPress, - reactions: onLongPressReactions, - ), + context, + TriggerType.longPress, + reactions: onLongPressReactions, + ), borderRadius: getBorderRadius(node), overlayColor: inkWell.overlayColor != null ? WidgetStatePropertyAll( @@ -227,9 +220,7 @@ List wrapWithInkWell( highlightColor: inkWell.highlightColor?.toFlutterColor(), hoverColor: inkWell.hoverColor?.toFlutterColor(), focusColor: inkWell.focusColor?.toFlutterColor(), - child: Stack( - children: children, - ), + child: Stack(children: children), ), ); @@ -239,10 +230,7 @@ List wrapWithInkWell( class PortalPreviewWidget extends StatelessWidget { final BaseNode node; - const PortalPreviewWidget({ - super.key, - required this.node, - }); + const PortalPreviewWidget({super.key, required this.node}); @override Widget build(BuildContext context) { @@ -330,32 +318,34 @@ List retrieveBoxShadow(BaseNode node, ScopedValues scopedValues) { if (node is! DefaultShapeNode) return []; return node.effects .where((effect) => effect.type == EffectType.dropShadow && effect.visible) - .map( - (effect) { - final ColorRGBA? color = - PropertyValueDelegate.getPropertyValue( - node, - 'shadow-color-${effect.id}', - scopedValues: scopedValues, - ) ?? - effect.color; - return BoxShadow( - spreadRadius: effect.spread!, - offset: - Offset(effect.offset!.x.toDouble(), effect.offset!.y.toDouble()), - blurRadius: effect.radius, - color: color!.toFlutterColor(), - ); - }, - ).toList(); + .map((effect) { + final ColorRGBA? color = + PropertyValueDelegate.getPropertyValue( + node, + 'shadow-color-${effect.id}', + scopedValues: scopedValues, + ) ?? + effect.color; + return BoxShadow( + spreadRadius: effect.spread!, + offset: Offset( + effect.offset!.x.toDouble(), + effect.offset!.y.toDouble(), + ), + blurRadius: effect.radius, + color: color!.toFlutterColor(), + ); + }) + .toList(); } BorderRadius? getBorderRadius(BaseNode node) { if (node is CornerMixin) { if (node.cornerRadius.linked && node.cornerRadius.type == RadiusType.elliptical) { - return BorderRadius.all(Radius.elliptical( - node.basicBoxLocal.width, node.basicBoxLocal.height)); + return BorderRadius.all( + Radius.elliptical(node.basicBoxLocal.width, node.basicBoxLocal.height), + ); } return node.cornerRadius.borderRadius; } else { @@ -397,12 +387,13 @@ void getLinearGradientAlignment(double rotationDeg) {} Gradient? retrieveGradient(PaintModel paint) { try { - final double rotation = - decomposeRelativeTransform(paint.gradientTransform).rotation; + final double rotation = decomposeRelativeTransform(paint.gradientTransform) + .rotation; // We sort the colors to mimic the way Figma changes the order. - final gradientStops = paint.gradientStops! - .sorted((g1, g2) => g1.position.compareTo(g2.position)); + final gradientStops = paint.gradientStops!.sorted( + (g1, g2) => g1.position.compareTo(g2.position), + ); final radians = rotation * pi / 180; if (paint.type == PaintType.gradientLinear) { @@ -433,8 +424,10 @@ Gradient? retrieveGradient(PaintModel paint) { return null; } catch (error, stacktrace) { - print(error); - print(stacktrace); + DebugLogger.instance.printInfo( + 'Failed to build gradient: $error\n$stacktrace', + name: 'PassiveRectangleTransformer', + ); return null; } } @@ -494,8 +487,17 @@ List applyGradientRotation(List? t1, double angle) { final cosA = cos(rads); final sinA = sin(rads); - final vec_math.Matrix3 matrix = - vec_math.Matrix3(cosA, sinA, tx, -sinA, cosA, ty, 0, 0, 1); + final vec_math.Matrix3 matrix = vec_math.Matrix3( + cosA, + sinA, + tx, + -sinA, + cosA, + ty, + 0, + 0, + 1, + ); final List list = List.filled(9, 0); matrix.copyIntoArray(list); @@ -636,11 +638,7 @@ List buildFills( } lastBlendTree.clear(); - raster.add(wrapFillWithPositioned( - node: node, - fill: fill, - paint: model, - )); + raster.add(wrapFillWithPositioned(node: node, fill: fill, paint: model)); } return raster.reversed.toList(); @@ -650,19 +648,18 @@ Widget wrapFillWithPositioned({ required GeometryMixin node, required Widget fill, required PaintModel paint, -}) => - switch (paint.type) { - PaintType.solid => Positioned.fill(child: fill), - PaintType.gradientLinear || - PaintType.gradientRadial || - PaintType.gradientAngular || - PaintType.gradientDiamond => - Positioned.fill(child: fill), - PaintType.image => node.isHorizontalWrap || node.isVerticalWrap - ? Positioned(child: fill) - : Positioned.fill(child: fill), - PaintType.emoji => fill, - }; +}) => switch (paint.type) { + PaintType.solid => Positioned.fill(child: fill), + PaintType.gradientLinear || + PaintType.gradientRadial || + PaintType.gradientAngular || + PaintType.gradientDiamond => Positioned.fill(child: fill), + PaintType.image => + node.isHorizontalWrap || node.isVerticalWrap + ? Positioned(child: fill) + : Positioned.fill(child: fill), + PaintType.emoji => fill, +}; Widget buildFill( GeometryMixin node, { @@ -711,7 +708,8 @@ Widget buildFill( // Substitute URL value from [CodelesslyContext]'s [data] map if // [imageURL] represents a JSON path. String? imageURL = paint.croppedImageURL ?? paint.downloadUrl!; - final imageURLValue = PropertyValueDelegate.getPropertyValue( + final imageURLValue = + PropertyValueDelegate.getPropertyValue( node, 'fill-image-${paint.id}', scopedValues: scopedValues, @@ -747,13 +745,13 @@ Widget buildFill( width: (node.horizontalFit == SizeFit.shrinkWrap) ? null : (node.horizontalFit == SizeFit.expanded) - ? double.infinity - : node.basicBoxLocal.width, + ? double.infinity + : node.basicBoxLocal.width, height: (node.verticalFit == SizeFit.shrinkWrap) ? null : (node.verticalFit == SizeFit.expanded) - ? double.infinity - : node.basicBoxLocal.height, + ? double.infinity + : node.basicBoxLocal.height, paint: paint, node: node, bytes: bytes, @@ -763,16 +761,10 @@ Widget buildFill( } if (modifiedOpacity != 1) { - child = Opacity( - opacity: modifiedOpacity, - child: child, - ); + child = Opacity(opacity: modifiedOpacity, child: child); } if (imageRotation != null) { - child = Transform.rotate( - angle: imageRotation, - child: child, - ); + child = Transform.rotate(angle: imageRotation, child: child); } return child; case PaintType.emoji: @@ -794,21 +786,20 @@ class RelativeTransform { }); const RelativeTransform.empty() - : translation = const [0, 0], - rotation = 0, - scale = const [0, 0], - skew = const [0, 0]; + : translation = const [0, 0], + rotation = 0, + scale = const [0, 0], + skew = const [0, 0]; RelativeTransform copyWith({ List? translation, double? rotation, List? scale, List? skew, - }) => - RelativeTransform( - translation: translation ?? this.translation, - rotation: rotation ?? this.rotation, - scale: scale ?? this.scale, - skew: skew ?? this.skew, - ); + }) => RelativeTransform( + translation: translation ?? this.translation, + rotation: rotation ?? this.rotation, + scale: scale ?? this.scale, + skew: skew ?? this.skew, + ); } diff --git a/lib/src/transformers/node_transformers/passive_row_column_transformer.dart b/lib/src/transformers/node_transformers/passive_row_column_transformer.dart index 0ec42cc5..affaeccd 100644 --- a/lib/src/transformers/node_transformers/passive_row_column_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_row_column_transformer.dart @@ -85,11 +85,7 @@ class PassiveRowColumnTransformer extends NodeWidgetTransformer { if (fixWidth == null && fixHeight == null) return res; - return SizedBox( - width: fixWidth, - height: fixHeight, - child: res, - ); + return SizedBox(width: fixWidth, height: fixHeight, child: res); } else { Widget res = Column( mainAxisAlignment: rowColumnNode.mainAxisAlignment.flutterAxis, @@ -122,11 +118,7 @@ class PassiveRowColumnTransformer extends NodeWidgetTransformer { if (fixWidth == null && fixHeight == null) return res; - return SizedBox( - width: fixWidth, - height: fixHeight, - child: res, - ); + return SizedBox(width: fixWidth, height: fixHeight, child: res); } } @@ -148,10 +140,11 @@ class PassiveRowColumnTransformer extends NodeWidgetTransformer { final AxisC mainAxis = ((parentRowColumn as RowColumnMixin).rowColumnType == RowColumnType.row - ? AxisC.horizontal - : AxisC.vertical); - final SizeFit mainAxisFit = - (mainAxis == AxisC.horizontal ? node.horizontalFit : node.verticalFit); + ? AxisC.horizontal + : AxisC.vertical); + final SizeFit mainAxisFit = (mainAxis == AxisC.horizontal + ? node.horizontalFit + : node.verticalFit); // final SizeFit crossAxisFit = // (mainAxis == AxisC.horizontal ? node.verticalFit : node.horizontalFit); @@ -232,10 +225,7 @@ class PassiveRowColumnTransformer extends NodeWidgetTransformer { // We can do this because children on cross axis get restricted bounds. if (mainAxisFit.isFlex) { if (mainAxisFit == SizeFit.flexible) { - childWidget = Flexible( - flex: flex, - child: childWidget, - ); + childWidget = Flexible(flex: flex, child: childWidget); } else { if (mainAxisMaxConstraint != null) { // When there's constraints on the main axis, we need to use @@ -269,13 +259,12 @@ class PassiveRowColumnTransformer extends NodeWidgetTransformer { BaseNode node, BuildContext context, WidgetBuildSettings settings, - ) => - PassiveRowColumnWidget( - node: node, - children: node.childrenOrEmpty.map(getNode).toList(), - manager: manager, - settings: settings, - ); + ) => PassiveRowColumnWidget( + node: node, + children: node.childrenOrEmpty.map(getNode).toList(), + manager: manager, + settings: settings, + ); } class PassiveRowColumnWidget extends StatelessWidget { @@ -295,8 +284,11 @@ class PassiveRowColumnWidget extends StatelessWidget { @override Widget build(BuildContext context) { final List widgetChildren = children.map((child) { - final Widget builtWidget = - manager.buildWidgetByID(child.id, context, settings: settings); + final Widget builtWidget = manager.buildWidgetByID( + child.id, + context, + settings: settings, + ); return PassiveRowColumnTransformer.wrapChildWithSizeFits( child, @@ -308,20 +300,21 @@ class PassiveRowColumnWidget extends StatelessWidget { final Widget child = (node is DefaultShapeNode) ? manager.getTransformer().buildRectangle( - node as DefaultShapeNode, - applyPadding: node is! ScrollableMixin || - !(node as ScrollableMixin).isScrollable, - children: [ - PassiveRowColumnTransformer.buildRowColumnWidget( - node, - childrenWidgets: widgetChildren, - childrenNodes: children, - withScroll: true, - getNode: manager.getNode, - ) - ], - settings: settings, - ) + node as DefaultShapeNode, + applyPadding: + node is! ScrollableMixin || + !(node as ScrollableMixin).isScrollable, + children: [ + PassiveRowColumnTransformer.buildRowColumnWidget( + node, + childrenWidgets: widgetChildren, + childrenNodes: children, + withScroll: true, + getNode: manager.getNode, + ), + ], + settings: settings, + ) : PassiveRowColumnTransformer.buildRowColumnWidget( node, childrenWidgets: widgetChildren, diff --git a/lib/src/transformers/node_transformers/passive_slider_transformer.dart b/lib/src/transformers/node_transformers/passive_slider_transformer.dart index efe03abc..a7ecc97f 100644 --- a/lib/src/transformers/node_transformers/passive_slider_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_slider_transformer.dart @@ -14,15 +14,9 @@ class PassiveSliderTransformer extends NodeWidgetTransformer { WidgetBuildSettings settings, ) { if (settings.isPreview) { - return PreviewSliderWidget( - node: node, - settings: settings, - ); + return PreviewSliderWidget(node: node, settings: settings); } else { - return PassiveSliderWidget( - node: node, - settings: settings, - ); + return PassiveSliderWidget(node: node, settings: settings); } } } @@ -93,7 +87,8 @@ class PassiveSliderWidget extends StatelessWidget { context, variablesOverrides: variablesOverrides, ); - final double value = PropertyValueDelegate.getPropertyValue( + final double value = + PropertyValueDelegate.getPropertyValue( node, 'value', scopedValues: scopedValues, @@ -128,17 +123,40 @@ class TransformerSlider extends StatelessWidget { @override Widget build(BuildContext context) { - final SliderTrackShape trackShape; - if (node.properties.trackShape == SliderTrackShapeEnum.rectangle) { - trackShape = const RectangularSliderTrackShape(); - } else { - trackShape = const RoundedRectSliderTrackShape(); - } - - final valueIndicatorShape = node.properties.valueIndicatorShape == - SliderValueIndicatorShape.rectangle - ? const RectangularSliderValueIndicatorShape() - : const PaddleSliderValueIndicatorShape(); + final SliderTrackShape trackShape = switch (node.properties.trackShape) { + SliderTrackShapeEnum.rectangle => const RectangularSliderTrackShape(), + SliderTrackShapeEnum.roundedRectangle => + const RoundedRectSliderTrackShape(), + SliderTrackShapeEnum.gapped => const GappedSliderTrackShape(), + }; + + final SliderComponentShape valueIndicatorShape = switch (node + .properties + .valueIndicatorShape) { + SliderValueIndicatorShape.rectangle => + const RectangularSliderValueIndicatorShape(), + SliderValueIndicatorShape.paddle => + const PaddleSliderValueIndicatorShape(), + SliderValueIndicatorShape.drop => const DropSliderValueIndicatorShape(), + SliderValueIndicatorShape.roundedRectangle => + const RoundedRectSliderValueIndicatorShape(), + }; + + final ShowValueIndicator showValueIndicator = switch (node + .properties + .showValueIndicator) { + null => + node.properties.showLabel + ? ShowValueIndicator.onDrag + : ShowValueIndicator.never, + ShowValueIndicatorC.onlyForDiscrete => ShowValueIndicator.onlyForDiscrete, + ShowValueIndicatorC.onlyForContinuous => + ShowValueIndicator.onlyForContinuous, + ShowValueIndicatorC.onDrag => ShowValueIndicator.onDrag, + ShowValueIndicatorC.alwaysVisible => ShowValueIndicator.alwaysVisible, + ShowValueIndicatorC.never => ShowValueIndicator.never, + }; + final bool showsLabel = showValueIndicator != ShowValueIndicator.never; return SizedBox( width: node.basicBoxLocal.width, @@ -150,36 +168,43 @@ class TransformerSlider extends StatelessWidget { ? node.properties.overlayColor.toFlutterColor() : Colors.transparent, activeTrackColor: node.properties.activeTrackColor.toFlutterColor(), - inactiveTrackColor: - node.properties.inactiveTrackColor.toFlutterColor(), + inactiveTrackColor: node.properties.inactiveTrackColor + .toFlutterColor(), thumbColor: node.properties.thumbColor.toFlutterColor(), thumbShape: RoundSliderThumbShape( - enabledThumbRadius: - node.properties.showThumb ? node.properties.thumbRadius : 0, + enabledThumbRadius: node.properties.showThumb + ? node.properties.thumbRadius + : 0, ), trackShape: trackShape, + // GappedSliderTrackShape requires a gap; 6 is the Material 3 + // (2024) default. + trackGap: node.properties.trackShape == SliderTrackShapeEnum.gapped + ? 6 + : null, tickMarkShape: node.properties.isDiscrete ? RoundSliderTickMarkShape( tickMarkRadius: node.properties.tickMarkRadius, ) : null, - showValueIndicator: node.properties.showLabel - ? ShowValueIndicator.always - : ShowValueIndicator.never, - activeTickMarkColor: - node.properties.activeTickMarkColor.toFlutterColor(), - inactiveTickMarkColor: - node.properties.inactiveTickMarkColor.toFlutterColor(), - valueIndicatorColor: - node.properties.valueIndicatorColor.toFlutterColor(), + showValueIndicator: showValueIndicator, + secondaryActiveTrackColor: node.properties.secondaryActiveTrackColor + ?.toFlutterColor(), + activeTickMarkColor: node.properties.activeTickMarkColor + .toFlutterColor(), + inactiveTickMarkColor: node.properties.inactiveTickMarkColor + .toFlutterColor(), + valueIndicatorColor: node.properties.valueIndicatorColor + .toFlutterColor(), valueIndicatorTextStyle: TextStyle( color: node.properties.valueIndicatorTextColor.toFlutterColor(), fontSize: node.properties.valueIndicatorFontSize, ), valueIndicatorShape: valueIndicatorShape, overlayShape: RoundSliderOverlayShape( - overlayRadius: - node.properties.showThumb ? node.properties.overlayRadius : 0, + overlayRadius: node.properties.showThumb + ? node.properties.overlayRadius + : 0, ), ), child: Slider( @@ -188,9 +213,15 @@ class TransformerSlider extends StatelessWidget { autofocus: node.properties.autofocus, min: node.properties.min, max: node.properties.max, - divisions: - node.properties.isDiscrete ? node.properties.divisions : null, - label: node.properties.showLabel ? getSliderLabel(node, value) : null, + divisions: node.properties.isDiscrete + ? node.properties.divisions + : null, + label: showsLabel ? getSliderLabel(node, value) : null, + secondaryTrackValue: node.properties.secondaryTrackValue?.clamp( + node.properties.min, + node.properties.max, + ), + padding: node.properties.padding?.flutterEdgeInsets, ), ), ); diff --git a/lib/src/transformers/node_transformers/passive_spacer_transformer.dart b/lib/src/transformers/node_transformers/passive_spacer_transformer.dart index 800d39be..ed514c98 100644 --- a/lib/src/transformers/node_transformers/passive_spacer_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_spacer_transformer.dart @@ -10,16 +10,15 @@ class PassiveSpacerTransformer extends NodeWidgetTransformer { final BaseNode parentNode = getNode(node.parentID); if (parentNode is! RowColumnMixin) { - return AdaptiveNodeBox( - node: node, - ); + return AdaptiveNodeBox(node: node); } final AxisC mainAxis = parentNode.mainAxis; final SizeFit horizontalFit = node.horizontalFit; final SizeFit verticalFit = node.verticalFit; - final SizeFit mainFit = - (mainAxis == AxisC.horizontal ? horizontalFit : verticalFit); + final SizeFit mainFit = (mainAxis == AxisC.horizontal + ? horizontalFit + : verticalFit); final Widget spacerWidget; if (mainFit.isFlex) { diff --git a/lib/src/transformers/node_transformers/passive_stack_transformer.dart b/lib/src/transformers/node_transformers/passive_stack_transformer.dart index 545fe979..9122c460 100644 --- a/lib/src/transformers/node_transformers/passive_stack_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_stack_transformer.dart @@ -17,15 +17,18 @@ class PassiveStackTransformer extends NodeWidgetTransformer { required AlignmentModel commonAlignment, required WidgetBuildSettings settings, }) { - assert(childWidget != null || manager != null, - 'Either childWidget or manager must be provided.'); + assert( + childWidget != null || manager != null, + 'Either childWidget or manager must be provided.', + ); - Widget child = childWidget ?? + Widget child = + childWidget ?? manager!.buildWidgetFromNode(node, context, settings: settings); final bool potentiallyPositionable = node.alignment == AlignmentModel.none || - (parent.isOneOrBothWrap && node.alignment != commonAlignment); + (parent.isOneOrBothWrap && node.alignment != commonAlignment); if (potentiallyPositionable) { // You cannot use the Align widget when inside a shrink-wrapping Stack. @@ -46,14 +49,14 @@ class PassiveStackTransformer extends NodeWidgetTransformer { : null, right: align.x >= 0 ? (parent.innerBoxLocal.width - node.outerBoxLocal.right) + - parent.innerBoxLocal.edgeRight + parent.innerBoxLocal.edgeRight : null, top: align.y <= 0 ? node.outerBoxLocal.top - parent.innerBoxLocal.edgeTop : null, bottom: align.y >= 0 ? (parent.innerBoxLocal.height - node.outerBoxLocal.bottom) + - parent.innerBoxLocal.edgeBottom + parent.innerBoxLocal.edgeBottom : null, child: child, ); @@ -100,29 +103,31 @@ class PassiveStackTransformer extends NodeWidgetTransformer { final double? left = horizontallyExpands ? 0 : node.edgePins.left != null - ? node.outerBoxLocal.x - parent.innerBoxGlobal.edgeLeft - : null; + ? node.outerBoxLocal.x - parent.innerBoxGlobal.edgeLeft + : null; final double? right = horizontallyExpands ? 0 : node.edgePins.right != null - ? node.edgePins.right! - parent.innerBoxGlobal.edgeRight - : null; + ? node.edgePins.right! - parent.innerBoxGlobal.edgeRight + : null; final double? top = verticallyExpands ? 0 : node.edgePins.top != null - ? node.outerBoxLocal.y - parent.innerBoxGlobal.edgeTop - : null; + ? node.outerBoxLocal.y - parent.innerBoxGlobal.edgeTop + : null; final double? bottom = verticallyExpands ? 0 : node.edgePins.bottom != null - ? node.edgePins.bottom! - parent.innerBoxGlobal.edgeBottom - : null; - final double? width = horizontallyExpands || + ? node.edgePins.bottom! - parent.innerBoxGlobal.edgeBottom + : null; + final double? width = + horizontallyExpands || node.isHorizontalWrap || node.edgePins.isHorizontallyExpanded ? null : node.outerBoxLocal.width; - final double? height = verticallyExpands || + final double? height = + verticallyExpands || node.isVerticalWrap || node.edgePins.isVerticallyExpanded ? null @@ -182,8 +187,7 @@ class PassiveStackTransformer extends NodeWidgetTransformer { BaseNode node, BuildContext context, WidgetBuildSettings settings, - ) => - buildWidgetForChildren(node, context, settings: settings); + ) => buildWidgetForChildren(node, context, settings: settings); Widget buildWidgetForChildren( BaseNode node, @@ -205,10 +209,13 @@ class PassiveStackTransformer extends NodeWidgetTransformer { for (final String childId in node.children) getNode(childId), ]; - final AlignmentModel commonAlignment = - retrieveCommonStackAlignment(node, children); - final Set potentiallyPositionableChildren = - children.where((child) { + final AlignmentModel commonAlignment = retrieveCommonStackAlignment( + node, + children, + ); + final Set potentiallyPositionableChildren = children.where(( + child, + ) { return child.alignment == AlignmentModel.none || (node.isOneOrBothWrap && child.alignment != commonAlignment); }).toSet(); @@ -236,13 +243,14 @@ class PassiveStackTransformer extends NodeWidgetTransformer { childrenWidgets.add(child); } - Widget stack = - manager.getTransformer().buildRectangle( - node, - stackAlignment: commonAlignment, - children: childrenWidgets, - settings: settings, - ); + Widget stack = manager + .getTransformer() + .buildRectangle( + node, + stackAlignment: commonAlignment, + children: childrenWidgets, + settings: settings, + ); // // This makes sure that children stay positioned correctly even if the stack // // is shrink wrapping. It makes it so the stack sizes itself to its tallest diff --git a/lib/src/transformers/node_transformers/passive_switch_transformer.dart b/lib/src/transformers/node_transformers/passive_switch_transformer.dart index 592f6381..25d2c4c8 100644 --- a/lib/src/transformers/node_transformers/passive_switch_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_switch_transformer.dart @@ -14,15 +14,9 @@ class PassiveSwitchTransformer extends NodeWidgetTransformer { WidgetBuildSettings settings, ) { if (settings.isPreview) { - return PreviewSwitchWidget( - node: node, - settings: settings, - ); + return PreviewSwitchWidget(node: node, settings: settings); } else { - return PassiveSwitchWidget( - node: node, - settings: settings, - ); + return PassiveSwitchWidget(node: node, settings: settings); } } } @@ -72,10 +66,18 @@ class PassiveSwitchWidget extends StatelessWidget { void onChanged(BuildContext context, bool internalValue) { NodeStateProvider.setState(context, internalValue); - FunctionsRepository.setPropertyValue(context, - node: node, property: 'value', value: internalValue); + FunctionsRepository.setPropertyValue( + context, + node: node, + property: 'value', + value: internalValue, + ); FunctionsRepository.triggerAction( - context, node: node, TriggerType.changed, value: internalValue); + context, + node: node, + TriggerType.changed, + value: internalValue, + ); } @override @@ -84,7 +86,8 @@ class PassiveSwitchWidget extends StatelessWidget { context, variablesOverrides: variablesOverrides, ); - final bool value = PropertyValueDelegate.getPropertyValue( + final bool value = + PropertyValueDelegate.getPropertyValue( node, 'value', scopedValues: scopedValues, @@ -132,11 +135,11 @@ class TransformerSwitch extends StatelessWidget { onChanged: (value) => onChanged?.call(context, value), autofocus: node.properties.autofocus, activeTrackColor: node.properties.activeTrackColor.toFlutterColor(), - inactiveTrackColor: - node.properties.inactiveTrackColor.toFlutterColor(), - activeColor: node.properties.activeThumbColor.toFlutterColor(), - inactiveThumbColor: - node.properties.inactiveThumbColor.toFlutterColor(), + inactiveTrackColor: node.properties.inactiveTrackColor + .toFlutterColor(), + activeThumbColor: node.properties.activeThumbColor.toFlutterColor(), + inactiveThumbColor: node.properties.inactiveThumbColor + .toFlutterColor(), hoverColor: node.properties.hoverColor.toFlutterColor(), focusColor: node.properties.focusColor.toFlutterColor(), splashRadius: node.properties.splashRadius, @@ -146,8 +149,9 @@ class TransformerSwitch extends StatelessWidget { } return node.properties.inactiveTrackBorderColor?.toFlutterColor(); }), - trackOutlineWidth: - WidgetStateProperty.all(node.properties.trackOutlineWidth), + trackOutlineWidth: WidgetStateProperty.all( + node.properties.trackOutlineWidth, + ), ), ), ), diff --git a/lib/src/transformers/node_transformers/passive_tab_bar_transformer.dart b/lib/src/transformers/node_transformers/passive_tab_bar_transformer.dart index 33c11890..0c971251 100644 --- a/lib/src/transformers/node_transformers/passive_tab_bar_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_tab_bar_transformer.dart @@ -50,17 +50,22 @@ class PassiveTabBarTransformer extends NodeWidgetTransformer { id: '', name: 'tabBar', basicBoxLocal: NodeBox( - 0, 0, width ?? 32, height ?? node?.basicBoxLocal.height ?? 56), + 0, + 0, + width ?? 32, + height ?? node?.basicBoxLocal.height ?? 56, + ), retainedOuterBoxLocal: NodeBox( - 0, 0, width ?? 32, height ?? node?.basicBoxLocal.height ?? 56), + 0, + 0, + width ?? 32, + height ?? node?.basicBoxLocal.height ?? 56, + ), initialIndex: node?.initialIndex ?? 0, isScrollable: node?.isScrollable ?? false, physics: node?.physics ?? ScrollPhysicsC.platformDependent, ); - return PassiveTabBarWidget( - node: previewNode, - settings: settings, - ); + return PassiveTabBarWidget(node: previewNode, settings: settings); } Widget buildFromNode( @@ -78,8 +83,12 @@ class PassiveTabBarTransformer extends NodeWidgetTransformer { void onChanged(BuildContext context, TabBarNode node, int index) { NodeStateProvider.setState(context, index); final tab = node.properties.tabs[index]; - FunctionsRepository.triggerAction(context, TriggerType.changed, - reactions: tab.reactions, value: index); + FunctionsRepository.triggerAction( + context, + TriggerType.changed, + reactions: tab.reactions, + value: index, + ); } } @@ -106,6 +115,10 @@ class _PassiveTabBarWidgetState extends State late int initialIndex; late int length; + /// TabController asserts on an index outside the tabs, which happens when + /// data-bound tabs shrink below the configured initial index. + int get _clampedIndex => length == 0 ? 0 : initialIndex.clamp(0, length - 1); + @override void initState() { super.initState(); @@ -114,7 +127,7 @@ class _PassiveTabBarWidgetState extends State controller = TabController( length: widget.node.properties.tabs.length, vsync: this, - initialIndex: initialIndex, + initialIndex: _clampedIndex, ); } @@ -139,17 +152,19 @@ class _PassiveTabBarWidgetState extends State final ScopedValues scopedValues = ScopedValues.of(context); final width = widget.node.isHorizontalExpanded || widget.node.isHorizontalWrap - ? null - : widget.node.basicBoxLocal.width; + ? null + : widget.node.basicBoxLocal.width; final height = widget.node.isVerticalExpanded || widget.node.isVerticalWrap ? null : widget.node.basicBoxLocal.height; - final TextStyle labelStyle = - TextUtils.retrieveTextStyleFromProp(widget.node.properties.labelStyle); + final TextStyle labelStyle = TextUtils.retrieveTextStyleFromProp( + widget.node.properties.labelStyle, + ); final TextStyle unselectedLabelStyle = TextUtils.retrieveTextStyleFromProp( - widget.node.properties.unselectedLabelStyle); + widget.node.properties.unselectedLabelStyle, + ); final Decoration? indicator = getIndicator(); @@ -162,16 +177,26 @@ class _PassiveTabBarWidgetState extends State controller: controller, padding: widget.node.padding.flutterEdgeInsets, overlayColor: WidgetStateProperty.all( - widget.node.properties.overlayColor?.toFlutterColor()), - dividerColor: widget.node.properties.dividerColor?.toFlutterColor(), - indicatorColor: - widget.node.properties.indicatorColor.toFlutterColor(), + widget.node.properties.overlayColor?.toFlutterColor(), + ), + dividerColor: widget.node.properties.showDivider + ? widget.node.properties.dividerColor?.toFlutterColor() + : Colors.transparent, + dividerHeight: widget.node.properties.showDivider + ? widget.node.properties.dividerHeight + : 0, + tabAlignment: widget.node.properties.tabAlignment?.toFlutter( + isScrollable: widget.node.isScrollable, + ), + indicatorColor: widget.node.properties.indicatorColor + .toFlutterColor(), indicatorPadding: widget.node.properties.indicatorPadding.flutterEdgeInsets, indicatorSize: widget.node.properties.indicatorSize.toFlutter(), isScrollable: widget.node.isScrollable, - indicatorWeight: - indicator != null ? 0 : widget.node.properties.indicatorWeight, + indicatorWeight: indicator != null + ? 0 + : widget.node.properties.indicatorWeight, labelStyle: labelStyle, indicator: indicator, splashBorderRadius: @@ -179,11 +204,12 @@ class _PassiveTabBarWidgetState extends State splashFactory: NoSplash.splashFactory, labelColor: widget.node.properties.labelColor?.toFlutterColor(), labelPadding: widget.node.properties.labelPadding.flutterEdgeInsets, - unselectedLabelColor: - widget.node.properties.unselectedLabelColor?.toFlutterColor(), + unselectedLabelColor: widget.node.properties.unselectedLabelColor + ?.toFlutterColor(), unselectedLabelStyle: unselectedLabelStyle, - physics: widget.node.physics - .flutterScrollPhysics(widget.node.shouldAlwaysScroll), + physics: widget.node.physics.flutterScrollPhysics( + widget.node.shouldAlwaysScroll, + ), onTap: widget.onChanged, tabs: [ for (final (index, tab) in widget.node.properties.tabs.indexed) @@ -198,9 +224,12 @@ class _PassiveTabBarWidgetState extends State } Widget getTab(TabItem tab, ScopedValues scopedValues) { - final String rawLabel = PropertyValueDelegate.getPropertyValue( - widget.node, 'tab-label-${tab.id}', - scopedValues: scopedValues) ?? + final String rawLabel = + PropertyValueDelegate.getPropertyValue( + widget.node, + 'tab-label-${tab.id}', + scopedValues: scopedValues, + ) ?? tab.label; String label = PropertyValueDelegate.substituteVariables( @@ -208,15 +237,18 @@ class _PassiveTabBarWidgetState extends State nullSubstitutionMode: widget.settings.nullSubstitutionMode, scopedValues: scopedValues, ); - final double effectiveIconSize = - min(tab.icon.size ?? 24, widget.node.basicBoxLocal.height); + final double effectiveIconSize = min( + tab.icon.size ?? 24, + widget.node.basicBoxLocal.height, + ); Widget? iconWidget = widget.node.properties.contentType.showIcon ? retrieveIconWidget( tab.icon.color != null || !tab.icon.isSvgImage ? tab.icon : tab.icon.copyWith(color: ColorRGBA.transparent), - effectiveIconSize) + effectiveIconSize, + ) : null; if (iconWidget == null) return Tab(text: label); @@ -257,8 +289,10 @@ class _PassiveTabBarWidgetState extends State color: widget.node.properties.indicatorColor.toFlutterColor(), width: widget.node.properties.indicatorWeight, ), - ).orNullIf(widget.node.properties.indicatorCornerRadius.isUniform && - widget.node.properties.indicatorCornerRadius == CornerRadius.zero); + ).orNullIf( + widget.node.properties.indicatorCornerRadius.isUniform && + widget.node.properties.indicatorCornerRadius == CornerRadius.zero, + ); case TabIndicatorStyle.filled: return ShapeDecoration( color: widget.node.properties.indicatorColor.toFlutterColor(), @@ -266,7 +300,8 @@ class _PassiveTabBarWidgetState extends State shape: widget.node.properties.indicatorShape, radius: widget.node.properties.indicatorCornerRadius, borderColor: widget.node.properties.indicatorColor, - borderWidth: widget.node.properties.indicatorStyle == + borderWidth: + widget.node.properties.indicatorStyle == TabIndicatorStyle.border ? widget.node.properties.indicatorWeight : 0, @@ -278,7 +313,8 @@ class _PassiveTabBarWidgetState extends State shape: widget.node.properties.indicatorShape, radius: widget.node.properties.indicatorCornerRadius, borderColor: widget.node.properties.indicatorColor, - borderWidth: widget.node.properties.indicatorStyle == + borderWidth: + widget.node.properties.indicatorStyle == TabIndicatorStyle.border ? widget.node.properties.indicatorWeight : 0, diff --git a/lib/src/transformers/node_transformers/passive_text_field_transformer.dart b/lib/src/transformers/node_transformers/passive_text_field_transformer.dart index 96acd7b3..e82c8ed5 100644 --- a/lib/src/transformers/node_transformers/passive_text_field_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_text_field_transformer.dart @@ -40,10 +40,7 @@ class PassiveTextFieldTransformer extends NodeWidgetTransformer { properties: props, edgePins: EdgePinsModel.standard, ); - return PassiveTextFieldWidget( - node: node, - settings: settings, - ); + return PassiveTextFieldWidget(node: node, settings: settings); } void onTap(BuildContext context, TextFieldNode node, String inputValue) { @@ -53,14 +50,26 @@ class PassiveTextFieldTransformer extends NodeWidgetTransformer { void onChanged(BuildContext context, TextFieldNode node, String inputValue) { NodeStateProvider.setState(context, inputValue); - FunctionsRepository.setNodeValue(context, - node: node, property: 'inputValue', value: inputValue); + FunctionsRepository.setNodeValue( + context, + node: node, + property: 'inputValue', + value: inputValue, + ); - FunctionsRepository.setPropertyVariable(context, - node: node, property: 'inputValue', value: inputValue); + FunctionsRepository.setPropertyVariable( + context, + node: node, + property: 'inputValue', + value: inputValue, + ); FunctionsRepository.triggerAction( - context, node: node, TriggerType.changed, value: inputValue); + context, + node: node, + TriggerType.changed, + value: inputValue, + ); } void onSubmitted( @@ -70,11 +79,19 @@ class PassiveTextFieldTransformer extends NodeWidgetTransformer { ) { NodeStateProvider.setState(context, inputValue); FunctionsRepository.triggerAction( - context, node: node, TriggerType.submitted, value: inputValue); + context, + node: node, + TriggerType.submitted, + value: inputValue, + ); } - void onIconTap(BuildContext context, TextFieldNode node, - List reactions, String inputValue) { + void onIconTap( + BuildContext context, + TextFieldNode node, + List reactions, + String inputValue, + ) { NodeStateProvider.setState(context, inputValue); FunctionsRepository.triggerAction( context, @@ -94,7 +111,7 @@ class PassiveTextFieldWidget extends StatefulWidget { final Function(BuildContext context, String value)? onChanged; final Function(BuildContext context, String value)? onSubmitted; final Function(BuildContext context, List reactions, String value)? - onIconTap; + onIconTap; final bool withAutofill; final AutovalidateMode? autovalidateMode; final FormFieldValidator? validator; @@ -120,8 +137,9 @@ class PassiveTextFieldWidget extends StatefulWidget { } class _PassiveTextFieldWidgetState extends State { - late final TextEditingController controller = - TextEditingController(text: getInitialText(widget.node.initialText)); + late final TextEditingController controller = TextEditingController( + text: getInitialText(widget.node.initialText), + ); final FocusNode focusNode = FocusNode(); @@ -164,11 +182,11 @@ class _PassiveTextFieldWidgetState extends State { scopedValues: scopedValues, ) : findIndexedValueIfApplied(scopedValues) ?? - PropertyValueDelegate.getPropertyValueFromNodeValues( - widget.node, - 'inputValue', - scopedValues: scopedValues, - ); + PropertyValueDelegate.getPropertyValueFromNodeValues( + widget.node, + 'inputValue', + scopedValues: scopedValues, + ); if (currentPropertyValue != null) { currentPropertyValue = PropertyValueDelegate.substituteVariables( currentPropertyValue, @@ -230,16 +248,18 @@ class _PassiveTextFieldWidgetState extends State { final TextInputType keyboardType = properties.keyboardType == TextInputTypeEnum.number - ? TextInputType.numberWithOptions( - decimal: properties.showDecimalKey, - signed: properties.showSignKey) - : properties.keyboardType.toFlutter(); + ? TextInputType.numberWithOptions( + decimal: properties.showDecimalKey, + signed: properties.showSignKey, + ) + : properties.keyboardType.toFlutter(); final TextInputValidatorModel validatorModel = properties.validator; final FormFieldValidator validator = widget.validator ?? validatorModel.validate; - final bool autofocus = !widget.settings.isPreview && + final bool autofocus = + !widget.settings.isPreview && (widget.autofocus ?? properties.autoFocus); Widget field = TextFormField( @@ -249,7 +269,8 @@ class _PassiveTextFieldWidgetState extends State { enableInteractiveSelection: properties.enableInteractiveSelection, enabled: enabled, controller: controller, - obscureText: widget.node.properties.maxLines == 1 && + obscureText: + widget.node.properties.maxLines == 1 && widget.node.isVerticalWrap && obscureText, readOnly: readOnly, @@ -267,25 +288,28 @@ class _PassiveTextFieldWidgetState extends State { cursorRadius: Radius.circular(properties.cursorRadius), maxLength: maxLength, validator: validator, - autovalidateMode: widget.autovalidateMode ?? + autovalidateMode: + widget.autovalidateMode ?? widget.node.properties.autovalidateMode.flutterAutovalidateMode, autofillHints: AutofillGroup.maybeOf(context) != null && !widget.settings.isPreview - ? widget.node.properties.autofillHints.map((hint) => hint.code) - : null, + ? widget.node.properties.autofillHints.map((hint) => hint.code) + : null, inputFormatters: [ if (maxLength != null) LengthLimitingTextInputFormatter(maxLength), - if (properties.formatter.toFlutterFormatter() case var formatter?) - formatter, + ?properties.formatter.toFlutterFormatter(), ], - maxLines: - widget.node.properties.maxLines.orNullIf(!widget.node.isVerticalWrap), - minLines: - widget.node.properties.minLines.orNullIf(!widget.node.isVerticalWrap), + maxLines: widget.node.properties.maxLines.orNullIf( + !widget.node.isVerticalWrap, + ), + minLines: widget.node.properties.minLines.orNullIf( + !widget.node.isVerticalWrap, + ), expands: !widget.node.isVerticalWrap, obscuringCharacter: widget.node.properties.obscuringCharacter, style: TextUtils.retrieveTextStyleFromProp( - widget.node.properties.inputStyle), + widget.node.properties.inputStyle, + ), onTap: () => widget.onTap?.call(context, controller.text), onChanged: (value) => widget.onChanged?.call(context, value), onEditingComplete: () {}, @@ -308,10 +332,7 @@ class _PassiveTextFieldWidgetState extends State { field = IntrinsicHeight(child: field); } - field = AdaptiveNodeBox( - node: node, - child: field, - ); + field = AdaptiveNodeBox(node: node, child: field); return field; } @@ -424,8 +445,9 @@ class _PassiveTextFieldWidgetState extends State { ? null : retrieveIconWidget(decoration.icon, null), label: labelText, - floatingLabelStyle: - TextUtils.retrieveTextStyleFromProp(decoration.floatingLabelStyle), + floatingLabelStyle: TextUtils.retrieveTextStyleFromProp( + decoration.floatingLabelStyle, + ), helperText: helperText, helperStyle: TextUtils.retrieveTextStyleFromProp(decoration.helperStyle), helperMaxLines: decoration.helperMaxLines, @@ -439,24 +461,30 @@ class _PassiveTextFieldWidgetState extends State { isCollapsed: isCollapsed, isDense: isDense, contentPadding: node.padding.flutterEdgeInsets, - prefixIcon: !decoration.prefixIcon.icon.show || - decoration.prefixIcon.icon.isEmpty + prefixIcon: + !decoration.prefixIcon.icon.show || decoration.prefixIcon.icon.isEmpty ? null : _ReactiveIcon( onTap: () => widget.onIconTap?.call( - context, decoration.prefixIcon.reactions, controller.text), + context, + decoration.prefixIcon.reactions, + controller.text, + ), iconModel: decoration.prefixIcon, ), // prefixIconConstraints: // decoration.prefixIconConstraints.flutterConstraints, prefix: prefixText, prefixStyle: TextUtils.retrieveTextStyleFromProp(decoration.prefixStyle), - suffixIcon: !decoration.suffixIcon.icon.show || - decoration.suffixIcon.icon.isEmpty + suffixIcon: + !decoration.suffixIcon.icon.show || decoration.suffixIcon.icon.isEmpty ? null : _ReactiveIcon( onTap: () => widget.onIconTap?.call( - context, decoration.suffixIcon.reactions, controller.text), + context, + decoration.suffixIcon.reactions, + controller.text, + ), iconModel: decoration.suffixIcon, ), suffix: suffixText, @@ -465,8 +493,8 @@ class _PassiveTextFieldWidgetState extends State { // decoration.suffixIconConstraints.flutterConstraints, counter: decoration.showCounter ? decoration.counterText?.isNotEmpty == true - ? counterText - : null + ? counterText + : null : null, counterText: decoration.showCounter ? null : '', // counterText: decoration.showCounter @@ -474,8 +502,9 @@ class _PassiveTextFieldWidgetState extends State { // ? decoration.counterText // : null // : '', - counterStyle: - TextUtils.retrieveTextStyleFromProp(decoration.counterStyle), + counterStyle: TextUtils.retrieveTextStyleFromProp( + decoration.counterStyle, + ), filled: decoration.filled, fillColor: decoration.fillColor.toFlutterColor(), focusColor: decoration.focusColor.toFlutterColor(), @@ -577,10 +606,7 @@ class _ReactiveIcon extends StatelessWidget { final VoidCallback onTap; final ReactiveIconModel iconModel; - const _ReactiveIcon({ - required this.onTap, - required this.iconModel, - }); + const _ReactiveIcon({required this.onTap, required this.iconModel}); @override Widget build(BuildContext context) { @@ -594,10 +620,7 @@ class _ReactiveIcon extends StatelessWidget { return GestureDetector( onTap: onTap, - child: MouseRegion( - cursor: SystemMouseCursors.click, - child: icon, - ), + child: MouseRegion(cursor: SystemMouseCursors.click, child: icon), ); } } diff --git a/lib/src/transformers/node_transformers/passive_text_transformer.dart b/lib/src/transformers/node_transformers/passive_text_transformer.dart index e9682571..732f5bb3 100644 --- a/lib/src/transformers/node_transformers/passive_text_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_text_transformer.dart @@ -1,7 +1,6 @@ import 'package:codelessly_api/codelessly_api.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; import '../../../codelessly_sdk.dart'; @@ -76,9 +75,9 @@ class _PassiveTextWidgetState extends State { widget.node.textMixedProps.forEach((property) { String link = property.link; if (link.isNotEmpty) { - TapGestureRecognizer tapGestureRecognizer = - TapGestureRecognizer(debugOwner: link) - ..onTap = () => launchUrl(Uri.parse(link)); + TapGestureRecognizer tapGestureRecognizer = TapGestureRecognizer( + debugOwner: link, + )..onTap = () => FunctionsRepository.openLink(link); tapGestureRecognizerRegistry[link] = tapGestureRecognizer; } }); @@ -88,7 +87,8 @@ class _PassiveTextWidgetState extends State { void disposeTapGestureRecognizers() { if (!widget.clickable) return; tapGestureRecognizerRegistry.forEach( - (link, tapGestureRecognizer) => tapGestureRecognizer.dispose()); + (link, tapGestureRecognizer) => tapGestureRecognizer.dispose(), + ); tapGestureRecognizerRegistry = {}; } diff --git a/lib/src/transformers/node_transformers/passive_variance_transformer.dart b/lib/src/transformers/node_transformers/passive_variance_transformer.dart index ca83a019..e95da501 100644 --- a/lib/src/transformers/node_transformers/passive_variance_transformer.dart +++ b/lib/src/transformers/node_transformers/passive_variance_transformer.dart @@ -39,13 +39,14 @@ class PassiveVarianceWidget extends StatelessWidget { if (settings.isPreview) return node.currentVariant.children; final String variantIdOrName = PropertyValueDelegate.getPropertyValue( - node, - 'currentVariantId', - scopedValues: scopedValues, - ) ?? - node.currentVariantId; + node, + 'currentVariantId', + scopedValues: scopedValues, + ) ?? + node.currentVariantId; - final Variant variant = node.variants.findById(variantIdOrName) ?? + final Variant variant = + node.variants.findById(variantIdOrName) ?? node.variants.findByName(variantIdOrName) ?? node.currentVariant; return variant.children; diff --git a/lib/src/transformers/node_transformers/passive_web_view_transformer_io.dart b/lib/src/transformers/node_transformers/passive_web_view_transformer_io.dart index 7f50c525..2c1f2211 100644 --- a/lib/src/transformers/node_transformers/passive_web_view_transformer_io.dart +++ b/lib/src/transformers/node_transformers/passive_web_view_transformer_io.dart @@ -5,7 +5,6 @@ import 'package:codelessly_api/codelessly_api.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; import 'package:webview_flutter/webview_flutter.dart'; import 'package:webview_flutter_android/webview_flutter_android.dart'; import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; @@ -41,10 +40,7 @@ class PassiveWebViewTransformer extends NodeWidgetTransformer { } Widget buildFromNode(WebViewNode node, WidgetBuildSettings settings) { - return PassiveWebViewWidget( - node: node, - settings: settings, - ); + return PassiveWebViewWidget(node: node, settings: settings); } void onChanged(List reactions) => reactions @@ -54,7 +50,7 @@ class PassiveWebViewTransformer extends NodeWidgetTransformer { void onAction(Reaction reaction) { switch (reaction.action.type) { case ActionType.link: - launchUrl(Uri.parse((reaction.action as LinkAction).url)); + FunctionsRepository.openLink((reaction.action as LinkAction).url); default: break; } @@ -118,15 +114,17 @@ class WebViewPreviewWidget extends StatelessWidget { child: FractionallySizedBox( widthFactor: aspectRatio > 1 ? 0.2 : null, heightFactor: aspectRatio < 1 ? 0.2 : null, - child: LayoutBuilder(builder: (context, constraints) { - return IconTheme( - data: IconThemeData( - color: Colors.blue, - size: min(constraints.maxWidth, constraints.maxHeight), - ), - child: icon, - ); - }), + child: LayoutBuilder( + builder: (context, constraints) { + return IconTheme( + data: IconThemeData( + color: Colors.blue, + size: min(constraints.maxWidth, constraints.maxHeight), + ), + child: icon, + ); + }, + ), ), ), ); @@ -161,7 +159,10 @@ class _RawWebViewWidgetState extends State { void initState() { super.initState(); if (!isPlatformSupportedForWebView || widget.settings.isPreview) { - print('Unsupported platform: $defaultTargetPlatform for WebView.'); + DebugLogger.instance.printInfo( + 'Unsupported platform: $defaultTargetPlatform for WebView.', + name: 'RawWebViewWidget', + ); return; } @@ -189,8 +190,10 @@ class _RawWebViewWidgetState extends State { if (_controller.platform is AndroidWebViewController) { (_controller.platform as AndroidWebViewController) - .setMediaPlaybackRequiresUserGesture(props.mediaAutoPlaybackPolicy != - WebViewMediaAutoPlaybackPolicy.alwaysPlayAllMedia); + .setMediaPlaybackRequiresUserGesture( + props.mediaAutoPlaybackPolicy != + WebViewMediaAutoPlaybackPolicy.alwaysPlayAllMedia, + ); } // Not yet supported on MacOS. Throws an exception. @@ -205,7 +208,10 @@ class _RawWebViewWidgetState extends State { void didChangeDependencies() { super.didChangeDependencies(); if (!isPlatformSupportedForWebView || widget.settings.isPreview) { - print('Unsupported platform: $defaultTargetPlatform for WebView.'); + DebugLogger.instance.printInfo( + 'Unsupported platform: $defaultTargetPlatform for WebView.', + name: 'RawWebViewWidget', + ); return; } if (!_isDataLoaded) { @@ -214,37 +220,71 @@ class _RawWebViewWidgetState extends State { } } - Future _loadData() async { + /// The source most recently loaded into [_controller]. + String? _loadedSource; + + @override + void didUpdateWidget(covariant RawWebViewWidget oldWidget) { + super.didUpdateWidget(oldWidget); + // Reload when the properties or the variables they read changed. + if (_isDataLoaded) _loadData(); + } + + /// The URL (or Flutter asset path) the properties currently point to. + ({String source, bool isAsset}) _resolveSource() { final ScopedValues scopedValues = ScopedValues.of(context); final WebViewProperties props = widget.properties; switch (props.webviewType) { case WebViewType.webpage: final WebPageWebViewProperties properties = props as WebPageWebViewProperties; - url = PropertyValueDelegate.getVariableValueFromPath( - properties.input, - scopedValues: scopedValues) ?? - properties.input; switch (properties.pageSourceType) { case WebViewWebpageSourceType.url: - return _controller.loadRequest(Uri.parse(url)); + return ( + source: + PropertyValueDelegate.getVariableValueFromPath( + properties.input, + scopedValues: scopedValues, + ) ?? + properties.input, + isAsset: false, + ); case WebViewWebpageSourceType.html: - final String content = _buildHtmlContent(properties.input); - return _controller.loadRequest(Uri.parse(content)); + return ( + source: _buildHtmlContent(properties.input), + isAsset: false, + ); case WebViewWebpageSourceType.asset: - // provided from onWebViewCreated callback. - return _controller.loadFlutterAsset(properties.input); + return (source: properties.input, isAsset: true); } case WebViewType.googleMaps: - url = buildGoogleMapsURL( - props as GoogleMapsWebViewProperties, scopedValues); - return _controller.loadRequest(Uri.parse(url)); + return ( + source: buildGoogleMapsURL( + props as GoogleMapsWebViewProperties, + scopedValues, + ), + isAsset: false, + ); case WebViewType.twitter: - url = buildTwitterURL(props as TwitterWebViewProperties, scopedValues); - return _controller.loadRequest(Uri.parse(url)); + return ( + source: buildTwitterURL( + props as TwitterWebViewProperties, + scopedValues, + ), + isAsset: false, + ); } } + Future _loadData() async { + final (:source, :isAsset) = _resolveSource(); + if (source == _loadedSource) return; + _loadedSource = source; + if (isAsset) return _controller.loadFlutterAsset(source); + url = source; + return _controller.loadRequest(Uri.parse(source)); + } + @override Widget build(BuildContext context) { final WebViewProperties props = widget.properties; @@ -254,9 +294,7 @@ class _RawWebViewWidgetState extends State { child = buildWebpageWebView(context, props as WebPageWebViewProperties); case WebViewType.googleMaps: if (!isPlatformSupportedForWebView || widget.settings.isPreview) { - return const WebViewPreviewWidget( - icon: Icon(Icons.map_outlined), - ); + return const WebViewPreviewWidget(icon: Icon(Icons.map_outlined)); } child = buildWebView(props as GoogleMapsWebViewProperties); @@ -264,9 +302,7 @@ class _RawWebViewWidgetState extends State { if (!isPlatformSupportedForWebView || widget.settings.isPreview) { return const WebViewPreviewWidget( icon: ImageIcon( - NetworkImage( - 'https://img.icons8.com/color/344/twitter--v2.png', - ), + NetworkImage('https://img.icons8.com/color/344/twitter--v2.png'), ), ); } @@ -278,43 +314,52 @@ class _RawWebViewWidgetState extends State { } Widget buildWebpageWebView( - BuildContext context, WebPageWebViewProperties properties) { + BuildContext context, + WebPageWebViewProperties properties, + ) { if (!isPlatformSupportedForWebView || widget.settings.isPreview) { - return const WebViewPreviewWidget( - icon: Icon(Icons.language_rounded), - ); + return const WebViewPreviewWidget(icon: Icon(Icons.language_rounded)); } return buildWebView(properties); } String buildGoogleMapsURL( - GoogleMapsWebViewProperties properties, ScopedValues scopedValues) { + GoogleMapsWebViewProperties properties, + ScopedValues scopedValues, + ) { final String? originalSrc = properties.src; final String? updatedSrc = originalSrc != null - ? PropertyValueDelegate.getVariableValueFromPath(originalSrc, - scopedValues: scopedValues) ?? - originalSrc + ? PropertyValueDelegate.getVariableValueFromPath( + originalSrc, + scopedValues: scopedValues, + ) ?? + originalSrc : null; return _buildHtmlContent(updatedSrc); } String _buildHtmlContent(String? src) { final String html = src?.replaceAll('\n', '') ?? 'about:blank'; - final String contentBase64 = - base64Encode(const Utf8Encoder().convert(html)); + final String contentBase64 = base64Encode( + const Utf8Encoder().convert(html), + ); final String content = 'data:text/html;base64,$contentBase64'; return content; } String buildTwitterURL( - TwitterWebViewProperties properties, ScopedValues scopedValues) { + TwitterWebViewProperties properties, + ScopedValues scopedValues, + ) { final String? originalSrc = properties.src; final String? updatedSrc = originalSrc != null - ? PropertyValueDelegate.getVariableValueFromPath(originalSrc, - scopedValues: scopedValues) ?? - originalSrc + ? PropertyValueDelegate.getVariableValueFromPath( + originalSrc, + scopedValues: scopedValues, + ) ?? + originalSrc : null; return _buildHtmlContent(updatedSrc); } @@ -326,20 +371,24 @@ class _RawWebViewWidgetState extends State { gestureRecognizers: { if (properties.controlVerticalScrollGesture == true) const Factory( - VerticalDragGestureRecognizer.new), + VerticalDragGestureRecognizer.new, + ), if (properties.controlHorizontalScrollGesture == true) const Factory( - HorizontalDragGestureRecognizer.new), + HorizontalDragGestureRecognizer.new, + ), if (properties.controlScaleGesture == true) const Factory(ScaleGestureRecognizer.new), if (properties.controlTapGesture == true) const Factory(TapGestureRecognizer.new), if (properties.controlLongPressGesture == true) const Factory( - LongPressGestureRecognizer.new), + LongPressGestureRecognizer.new, + ), if (properties.controlForcePressGesture == true) const Factory( - ForcePressGestureRecognizer.new), + ForcePressGestureRecognizer.new, + ), // const Factory( // TapAndPanGestureRecognizer.new), // const Factory(PanGestureRecognizer.new), diff --git a/lib/src/transformers/node_transformers/passive_web_view_transformer_web.dart b/lib/src/transformers/node_transformers/passive_web_view_transformer_web.dart index 51025903..ce749a0d 100644 --- a/lib/src/transformers/node_transformers/passive_web_view_transformer_web.dart +++ b/lib/src/transformers/node_transformers/passive_web_view_transformer_web.dart @@ -4,7 +4,6 @@ import 'dart:ui_web'; import 'package:codelessly_api/codelessly_api.dart'; import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; import 'package:web/web.dart' as web; import 'package:webview_flutter/webview_flutter.dart'; @@ -39,10 +38,7 @@ class PassiveWebViewTransformer extends NodeWidgetTransformer { } Widget buildFromNode(WebViewNode node, WidgetBuildSettings settings) { - return PassiveWebViewWidget( - node: node, - settings: settings, - ); + return PassiveWebViewWidget(node: node, settings: settings); } void onChanged(List reactions) => reactions @@ -52,7 +48,7 @@ class PassiveWebViewTransformer extends NodeWidgetTransformer { void onAction(Reaction reaction) { switch (reaction.action.type) { case ActionType.link: - launchUrl(Uri.parse((reaction.action as LinkAction).url)); + FunctionsRepository.openLink((reaction.action as LinkAction).url); default: break; } @@ -116,15 +112,17 @@ class WebViewPreviewWidget extends StatelessWidget { child: FractionallySizedBox( widthFactor: aspectRatio > 1 ? 0.2 : null, heightFactor: aspectRatio < 1 ? 0.2 : null, - child: LayoutBuilder(builder: (context, constraints) { - return IconTheme( - data: IconThemeData( - color: Colors.blue, - size: min(constraints.maxWidth, constraints.maxHeight), - ), - child: icon, - ); - }), + child: LayoutBuilder( + builder: (context, constraints) { + return IconTheme( + data: IconThemeData( + color: Colors.blue, + size: min(constraints.maxWidth, constraints.maxHeight), + ), + child: icon, + ); + }, + ), ), ), ); @@ -156,19 +154,13 @@ class _RawWebViewWidgetState extends State { @override void initState() { super.initState(); - if (widget.settings.isPreview) { - print('Preview mode for WebView.'); - return; - } + if (widget.settings.isPreview) return; } @override void didChangeDependencies() { super.didChangeDependencies(); - if (widget.settings.isPreview) { - print('Preview mode for WebView.'); - return; - } + if (widget.settings.isPreview) return; if (!_isDataLoaded) { _isDataLoaded = true; _loadData(); @@ -182,13 +174,17 @@ class _RawWebViewWidgetState extends State { case WebViewType.webpage: final WebPageWebViewProperties properties = props as WebPageWebViewProperties; - url = PropertyValueDelegate.getVariableValueFromPath( - properties.input, - scopedValues: scopedValues) ?? + url = + PropertyValueDelegate.getVariableValueFromPath( + properties.input, + scopedValues: scopedValues, + ) ?? properties.input; case WebViewType.googleMaps: url = buildGoogleMapsURL( - props as GoogleMapsWebViewProperties, scopedValues); + props as GoogleMapsWebViewProperties, + scopedValues, + ); case WebViewType.twitter: url = buildTwitterURL(props as TwitterWebViewProperties, scopedValues); } @@ -206,31 +202,40 @@ class _RawWebViewWidgetState extends State { } String buildGoogleMapsURL( - GoogleMapsWebViewProperties properties, ScopedValues scopedValues) { + GoogleMapsWebViewProperties properties, + ScopedValues scopedValues, + ) { final String? originalSrc = properties.src; final String? updatedSrc = originalSrc != null - ? PropertyValueDelegate.getVariableValueFromPath(originalSrc, - scopedValues: scopedValues) ?? - originalSrc + ? PropertyValueDelegate.getVariableValueFromPath( + originalSrc, + scopedValues: scopedValues, + ) ?? + originalSrc : null; return _buildHtmlContent(updatedSrc); } String buildTwitterURL( - TwitterWebViewProperties properties, ScopedValues scopedValues) { + TwitterWebViewProperties properties, + ScopedValues scopedValues, + ) { final String? originalSrc = properties.src; final String? updatedSrc = originalSrc != null - ? PropertyValueDelegate.getVariableValueFromPath(originalSrc, - scopedValues: scopedValues) ?? - originalSrc + ? PropertyValueDelegate.getVariableValueFromPath( + originalSrc, + scopedValues: scopedValues, + ) ?? + originalSrc : null; return _buildHtmlContent(updatedSrc); } String _buildHtmlContent(String? src) { final String html = src?.replaceAll('\n', '') ?? 'about:blank'; - final String contentBase64 = - base64Encode(const Utf8Encoder().convert(html)); + final String contentBase64 = base64Encode( + const Utf8Encoder().convert(html), + ); final String content = 'data:text/html;base64,$contentBase64'; return content; @@ -242,17 +247,14 @@ class _RawWebViewWidgetState extends State { if (widget.settings.isPreview) { switch (props.webviewType) { case WebViewType.webpage: - return const WebViewPreviewWidget( - icon: Icon(Icons.language_rounded), - ); + return const WebViewPreviewWidget(icon: Icon(Icons.language_rounded)); case WebViewType.googleMaps: - return const WebViewPreviewWidget( - icon: Icon(Icons.map_outlined), - ); + return const WebViewPreviewWidget(icon: Icon(Icons.map_outlined)); case WebViewType.twitter: return const WebViewPreviewWidget( - icon: ImageIcon(NetworkImage( - 'https://img.icons8.com/color/344/twitter--v2.png')), + icon: ImageIcon( + NetworkImage('https://img.icons8.com/color/344/twitter--v2.png'), + ), ); } } diff --git a/lib/src/transformers/passive_transformer_manager.dart b/lib/src/transformers/passive_transformer_manager.dart index bcdecd8f..5c2453d9 100644 --- a/lib/src/transformers/passive_transformer_manager.dart +++ b/lib/src/transformers/passive_transformer_manager.dart @@ -39,8 +39,10 @@ class PassiveNodeTransformerManager extends WidgetNodeTransformerManager { 'radio': PassiveRadioTransformer(getNode, this), 'icon': PassiveIconTransformer(getNode, this), 'spacer': PassiveSpacerTransformer(getNode, this), - 'floatingActionButton': - PassiveFloatingActionButtonTransformer(getNode, this), + 'floatingActionButton': PassiveFloatingActionButtonTransformer( + getNode, + this, + ), 'expansionTile': PassiveExpansionTileTransformer(getNode, this), 'accordion': PassiveAccordionTransformer(getNode, this), 'listTile': PassiveListTileTransformer(getNode, this), @@ -73,8 +75,8 @@ class PassiveNodeTransformerManager extends WidgetNodeTransformerManager { context, node: node, builder: (context) { - Widget widget = - getTransformerByNode(node).buildWidget(node, context, settings); + Widget widget = getTransformerByNode(node) + .buildWidget(node, context, settings); if (settings.withOpacity) { widget = applyWidgetOpacity(node, widget); @@ -122,7 +124,8 @@ class PassiveNodeTransformerManager extends WidgetNodeTransformerManager { required BaseNode node, required Widget Function(BuildContext context) builder, }) { - final bool enabled = PropertyValueDelegate.getPropertyValue( + final bool enabled = + PropertyValueDelegate.getPropertyValue( node, 'node_enabled', scopedValues: ScopedValues.of(context), @@ -131,8 +134,8 @@ class PassiveNodeTransformerManager extends WidgetNodeTransformerManager { if (!enabled) return const SizedBox.shrink(); - final CodelesslyContext codelesslyContext = - context.read(); + final CodelesslyContext codelesslyContext = context + .read(); // List of value notifiers that the widget listens to. This includes node's // local values and the variables attached to node properties. @@ -164,8 +167,8 @@ class PassiveNodeTransformerManager extends WidgetNodeTransformerManager { } // Get corresponding variable data from codelessly context. - final ValueNotifier? listenableVariable = - codelesslyContext.findVariableByName(match.name); + final ValueNotifier? listenableVariable = codelesslyContext + .findVariableByName(match.name); // Add variable to [listenables]. if (listenableVariable != null) listenables.add(listenableVariable); } @@ -191,8 +194,8 @@ class PassiveNodeTransformerManager extends WidgetNodeTransformerManager { } // Get corresponding variable data from codelessly context. - final ValueNotifier? listenableVariable = - codelesslyContext.findVariableByName(match.name); + final ValueNotifier? listenableVariable = codelesslyContext + .findVariableByName(match.name); // Add variable to [listenables]. if (listenableVariable != null) listenables.add(listenableVariable); } @@ -201,23 +204,27 @@ class PassiveNodeTransformerManager extends WidgetNodeTransformerManager { for (final BaseCondition condition in codelesslyContext.conditions.values) { if (!condition.hasNode(node.id)) continue; - final Set variableNames = - condition.getReactiveVariables().toSet(); + final Set variableNames = condition + .getReactiveVariables() + .toSet(); for (final name in variableNames) { if (predefinedListenableVariableNames.contains(name)) { switch (name) { case 'storage': final notifier = getStorageListenerFor( - VariableMatch.parse(name.wrapWithVariableSyntax())!, context); + VariableMatch.parse(name.wrapWithVariableSyntax())!, + context, + ); if (notifier != null) listenables.add(notifier); } continue; } // Get corresponding variable data from codelessly context. final ValueNotifier? listenableVariable = - codelesslyContext.variables.values - .firstWhereOrNull((notifier) => notifier.value.name == name); + codelesslyContext.variables.values.firstWhereOrNull( + (notifier) => notifier.value.name == name, + ); // Add variable to [listenables]. if (listenableVariable != null) listenables.add(listenableVariable); } @@ -227,17 +234,19 @@ class PassiveNodeTransformerManager extends WidgetNodeTransformerManager { key: ValueKey(node.id), node: node, codelesslyContext: codelesslyContext, - child: Builder(builder: (context) { - if (listenables.isNotEmpty) { - return ManagedListenableBuilder( - key: ValueKey(node.id), - listenables: listenables, - builder: (context) => builder(context), - ); - } else { - return builder(context); - } - }), + child: Builder( + builder: (context) { + if (listenables.isNotEmpty) { + return ManagedListenableBuilder( + key: ValueKey(node.id), + listenables: listenables, + builder: (context) => builder(context), + ); + } else { + return builder(context); + } + }, + ), ); } @@ -245,8 +254,9 @@ class PassiveNodeTransformerManager extends WidgetNodeTransformerManager { final localStorage = context.read()?.localDatabase; if (localStorage == null) return null; if (match.hasPath) { - final pathMatch = - VariableMatch.parse(match.path!.wrapWithVariableSyntax()); + final pathMatch = VariableMatch.parse( + match.path!.wrapWithVariableSyntax(), + ); if (pathMatch == null) return null; final key = pathMatch.name; return localStorage.getNotifier(key); @@ -287,8 +297,10 @@ class _ManagedListenableBuilderState extends State { @override void didUpdateWidget(covariant ManagedListenableBuilder oldWidget) { super.didUpdateWidget(oldWidget); - if (!const ListEquality() - .equals(oldWidget.listenables, widget.listenables)) { + if (!const ListEquality().equals( + oldWidget.listenables, + widget.listenables, + )) { // ValueNotifier objects are equatable by object identity, so we check // if those objects are the same or not. listenable?.removeListener(onChanged); @@ -337,14 +349,16 @@ class _NodeStateProviderWidgetState extends State @override void initState() { super.initState(); - widget.codelesslyContext.nodeRebuildNotifier - .addListener(onNodeNotification); + widget.codelesslyContext.nodeRebuildNotifier.addListener( + onNodeNotification, + ); } @override void dispose() { - widget.codelesslyContext.nodeRebuildNotifier - .removeListener(onNodeNotification); + widget.codelesslyContext.nodeRebuildNotifier.removeListener( + onNodeNotification, + ); super.dispose(); } diff --git a/lib/src/transformers/transformer_functions.dart b/lib/src/transformers/transformer_functions.dart index a0e6b1d2..3270ca0d 100644 --- a/lib/src/transformers/transformer_functions.dart +++ b/lib/src/transformers/transformer_functions.dart @@ -11,16 +11,20 @@ void convertNodeToButtonType(ButtonTypeEnum type, ButtonNode node) { switch (type) { case ButtonTypeEnum.elevated: final brightness = ThemeData.estimateBrightnessForColor( - node.properties.buttonColor.toFlutterColor()); + node.properties.buttonColor.toFlutterColor(), + ); final color = (brightness == Brightness.light ? Colors.black : Colors.white) .toColorRGBA(); node.properties - ..labelStyle = node.properties.labelStyle.copyWith(fills: [ - color.toPaint(node.properties.labelStyle.fills.firstOrNull?.id) - ]) - ..icon = node.properties.icon - .copyWith(color: color.toFlutterColor().toColorRGBA()) + ..labelStyle = node.properties.labelStyle.copyWith( + fills: [ + color.toPaint(node.properties.labelStyle.fills.firstOrNull?.id), + ], + ) + ..icon = node.properties.icon.copyWith( + color: color.toFlutterColor().toColorRGBA(), + ) ..buttonColor = node.properties.buttonColor.copyWith(a: 1) ..elevation = 8 ..gap = node.properties.icon.show ? 8 : 0 @@ -30,13 +34,16 @@ void convertNodeToButtonType(ButtonTypeEnum type, ButtonNode node) { case ButtonTypeEnum.text: node.properties ..buttonColor = node.properties.buttonColor.copyWith(a: 0.1) - ..labelStyle = node.properties.labelStyle.copyWith(fills: [ - node.properties.buttonColor - .copyWith(a: 1) - .toPaint(node.properties.labelStyle.fills.firstOrNull?.id) - ]) - ..icon = node.properties.icon - .copyWith(color: node.properties.buttonColor.copyWith(a: 1)) + ..labelStyle = node.properties.labelStyle.copyWith( + fills: [ + node.properties.buttonColor + .copyWith(a: 1) + .toPaint(node.properties.labelStyle.fills.firstOrNull?.id), + ], + ) + ..icon = node.properties.icon.copyWith( + color: node.properties.buttonColor.copyWith(a: 1), + ) ..elevation = 0 ..shape = CShapeBorder.roundedRectangle ..borderWidth = null @@ -44,27 +51,71 @@ void convertNodeToButtonType(ButtonTypeEnum type, ButtonNode node) { case ButtonTypeEnum.outlined: node.properties ..buttonColor = node.properties.buttonColor.copyWith(a: 1) - ..labelStyle = node.properties.labelStyle.copyWith(fills: [ - node.properties.buttonColor - .copyWith(a: 1) - .toPaint(node.properties.labelStyle.fills.firstOrNull?.id) - ]) - ..icon = node.properties.icon - .copyWith(color: node.properties.buttonColor.copyWith(a: 1)) + ..labelStyle = node.properties.labelStyle.copyWith( + fills: [ + node.properties.buttonColor + .copyWith(a: 1) + .toPaint(node.properties.labelStyle.fills.firstOrNull?.id), + ], + ) + ..icon = node.properties.icon.copyWith( + color: node.properties.buttonColor.copyWith(a: 1), + ) ..elevation = 0 ..shape = CShapeBorder.roundedRectangle ..borderWidth = 1 ..borderColor = null; + case ButtonTypeEnum.filled: + final brightness = ThemeData.estimateBrightnessForColor( + node.properties.buttonColor.toFlutterColor(), + ); + final color = + (brightness == Brightness.light ? Colors.black : Colors.white) + .toColorRGBA(); + node.properties + ..labelStyle = node.properties.labelStyle.copyWith( + fills: [ + color.toPaint(node.properties.labelStyle.fills.firstOrNull?.id), + ], + ) + ..icon = node.properties.icon.copyWith(color: color) + ..buttonColor = node.properties.buttonColor.copyWith(a: 1) + ..elevation = 0 + ..gap = node.properties.icon.show ? 8 : 0 + ..shape = CShapeBorder.stadium + ..borderColor = null + ..borderWidth = null; + case ButtonTypeEnum.filledTonal: + node.properties + ..buttonColor = node.properties.buttonColor.copyWith(a: 0.2) + ..labelStyle = node.properties.labelStyle.copyWith( + fills: [ + node.properties.buttonColor + .copyWith(a: 1) + .toPaint(node.properties.labelStyle.fills.firstOrNull?.id), + ], + ) + ..icon = node.properties.icon.copyWith( + color: node.properties.buttonColor.copyWith(a: 1), + ) + ..elevation = 0 + ..gap = node.properties.icon.show ? 8 : 0 + ..shape = CShapeBorder.stadium + ..borderColor = null + ..borderWidth = null; case ButtonTypeEnum.icon: final brightness = ThemeData.estimateBrightnessForColor( - node.properties.buttonColor.toFlutterColor()); + node.properties.buttonColor.toFlutterColor(), + ); final color = (brightness == Brightness.light ? Colors.black : Colors.white) .toColorRGBA(); node.properties - ..labelStyle = node.properties.labelStyle.copyWith(fills: [ - color.toPaint(node.properties.labelStyle.fills.firstOrNull?.id) - ]) + ..labelStyle = node.properties.labelStyle.copyWith( + fills: [ + color.toPaint(node.properties.labelStyle.fills.firstOrNull?.id), + ], + ) ..icon = node.properties.icon.copyWith(color: color) ..buttonColor = node.properties.buttonColor.copyWith(a: 1) ..icon = node.properties.icon.copyWith(show: true) @@ -103,10 +154,7 @@ void transformNodeFromIconButton(ButtonNode node) { } double height = painter.height + (10 * 2); node.update( - middleBoxLocal: node.basicBoxLocal.copyWith( - width: width, - height: height, - ), + middleBoxLocal: node.basicBoxLocal.copyWith(width: width, height: height), padding: defaultButtonPadding, ); node.properties.icon = node.properties.icon.copyWith(size: painter.height); @@ -115,10 +163,7 @@ void transformNodeFromIconButton(ButtonNode node) { void transformNodeToIconButton(ButtonNode node) { node.update( - middleBoxLocal: node.basicBoxLocal.copyWith( - width: 48, - height: 48, - ), + middleBoxLocal: node.basicBoxLocal.copyWith(width: 48, height: 48), padding: EdgeInsetsModel.zero, ); node.properties.icon = node.properties.icon.copyWith(size: 20); @@ -128,25 +173,32 @@ void transformNodeToIconButton(ButtonNode node) { /// Flutter's ButtonStyle parameters do different things depending on the button /// type. So we have to manually change them. -ButtonStyle createMasterButtonStyle(ButtonNode node, - {double? elevation, required ScopedValues scopedValues}) { - final TextStyle textStyle = - TextUtils.retrieveTextStyleFromProp(node.properties.labelStyle); - final Color? labelColor = PropertyValueDelegate.getPropertyValue( +ButtonStyle createMasterButtonStyle( + ButtonNode node, { + double? elevation, + required ScopedValues scopedValues, +}) { + final TextStyle textStyle = TextUtils.retrieveTextStyleFromProp( + node.properties.labelStyle, + ); + final Color? labelColor = + PropertyValueDelegate.getPropertyValue( node, 'labelColor', scopedValues: scopedValues, )?.toFlutterColor() ?? TextUtils.retrievePropColor(node.properties.labelStyle); - final buttonColor = PropertyValueDelegate.getPropertyValue( + final buttonColor = + PropertyValueDelegate.getPropertyValue( node, 'buttonColor', scopedValues: scopedValues, ) ?? node.properties.buttonColor; - final shadowColor = PropertyValueDelegate.getPropertyValue( + final shadowColor = + PropertyValueDelegate.getPropertyValue( node, 'shadowColor', scopedValues: scopedValues, @@ -179,6 +231,19 @@ ButtonStyle createMasterButtonStyle(ButtonNode node, shadowColor: shadowColor.toFlutterColor(), shape: getButtonShape(node, scopedValues), ); + case ButtonTypeEnum.filled: + case ButtonTypeEnum.filledTonal: + buttonStyle = FilledButton.styleFrom( + foregroundColor: labelColor, + elevation: elevation ?? node.properties.elevation, + shadowColor: shadowColor.toFlutterColor(), + backgroundColor: buttonColor.toFlutterColor(), + visualDensity: VisualDensity.standard, + padding: node.padding.flutterEdgeInsets, + textStyle: textStyle, + minimumSize: Size.zero, + shape: getButtonShape(node, scopedValues), + ); case ButtonTypeEnum.outlined: buttonStyle = OutlinedButton.styleFrom( foregroundColor: labelColor, @@ -194,11 +259,12 @@ ButtonStyle createMasterButtonStyle(ButtonNode node, ); case ButtonTypeEnum.icon: Color? primary; - if (buttonColor.toFlutterColor().opacity > 0 && - buttonColor.toFlutterColor().opacity < 0.7) { + if (buttonColor.toFlutterColor().a > 0 && + buttonColor.toFlutterColor().a < 0.7) { primary = buttonColor.toFlutterColor().withValues(alpha: 0.1); - } else if (buttonColor.toFlutterColor().opacity == 0) { - final iconColor = PropertyValueDelegate.getPropertyValue( + } else if (buttonColor.toFlutterColor().a == 0) { + final iconColor = + PropertyValueDelegate.getPropertyValue( node, 'iconColor', scopedValues: scopedValues, @@ -218,18 +284,20 @@ ButtonStyle createMasterButtonStyle(ButtonNode node, ); } return buttonStyle.copyWith( - elevation: - WidgetStateProperty.all(elevation ?? node.properties.elevation)); + elevation: WidgetStateProperty.all(elevation ?? node.properties.elevation), + ); } OutlinedBorder? getButtonShape(ButtonNode node, ScopedValues scopedValues) { - final buttonColor = PropertyValueDelegate.getPropertyValue( + final buttonColor = + PropertyValueDelegate.getPropertyValue( node, 'buttonColor', scopedValues: scopedValues, ) ?? node.properties.buttonColor; - final borderColor = PropertyValueDelegate.getPropertyValue( + final borderColor = + PropertyValueDelegate.getPropertyValue( node, 'borderColor', scopedValues: scopedValues, @@ -247,7 +315,9 @@ OutlinedBorder? getButtonShape(ButtonNode node, ScopedValues scopedValues) { } String? getSliderLabel(SliderNode node, double value) { - if (!node.properties.showLabel) return null; + final showValueIndicator = node.properties.showValueIndicator; + if (showValueIndicator == ShowValueIndicatorC.never) return null; + if (showValueIndicator == null && !node.properties.showLabel) return null; if (node.properties.label.trim().isEmpty) return kSliderDefaultLabel; final String valueString = node.properties.allowFractionalPoints ? value.toStringAsFixed(1) @@ -271,10 +341,14 @@ Widget? retrieveIconWidget( icon: icon.icon!, size: size ?? icon.size ?? kDefaultIconSize, color: color, + fill: icon.fill, + weight: icon.weight, + grade: icon.grade, + opticalSize: icon.opticalSize, ); case IconTypeEnum.image: if (icon.iconImage == null) return null; - if (color != null && color.opacity == 0) { + if (color != null && color.a == 0) { // when opacity is zero, we need to display original colors of // the image icon which `ImageIcon` widget cannot do. So we // use the raw Image widget. @@ -283,6 +357,9 @@ Widget? retrieveIconWidget( icon.iconImage!, width: size ?? icon.size ?? kDefaultIconSize, height: size ?? icon.size ?? kDefaultIconSize, + errorBuilder: (context, error, stackTrace) => SizedBox.square( + dimension: size ?? icon.size ?? kDefaultIconSize, + ), ); } return SizedBox.square( @@ -314,8 +391,7 @@ Widget retrieveNavBarItemIconWidget( MultiSourceIconModel icon, [ double? size, Color? themeColor, -]) => - retrieveIconWidget(icon, size, themeColor) ?? const SizedBox.shrink(); +]) => retrieveIconWidget(icon, size, themeColor) ?? const SizedBox.shrink(); String buildYoutubeEmbedUrl({ required EmbeddedYoutubeVideoProperties properties, @@ -403,14 +479,15 @@ String getVideoUrl(EmbeddedVideoSource source, String baseUrl) { } } -OutlinedBorder? getShapeFromMixin(ShapeBorderMixin mixin, - {bool onlyShape = false}) => - getShape( - shape: mixin.shape, - radius: mixin.cornerRadius, - borderColor: onlyShape ? null : mixin.borderColor, - borderWidth: onlyShape ? null : mixin.borderWidth, - ); +OutlinedBorder? getShapeFromMixin( + ShapeBorderMixin mixin, { + bool onlyShape = false, +}) => getShape( + shape: mixin.shape, + radius: mixin.cornerRadius, + borderColor: onlyShape ? null : mixin.borderColor, + borderWidth: onlyShape ? null : mixin.borderWidth, +); OutlinedBorder getShape({ required CShapeBorder shape, @@ -420,11 +497,8 @@ OutlinedBorder getShape({ }) { final BorderSide side = borderColor != null && borderWidth != null && borderWidth > 0 - ? BorderSide( - color: borderColor.toFlutterColor(), - width: borderWidth, - ) - : BorderSide.none; + ? BorderSide(color: borderColor.toFlutterColor(), width: borderWidth) + : BorderSide.none; switch (shape) { case CShapeBorder.rectangle: @@ -448,6 +522,11 @@ OutlinedBorder getShape({ borderRadius: radius.borderRadius, side: side, ); + case CShapeBorder.roundedSuperellipse: + return RoundedSuperellipseBorder( + borderRadius: radius.borderRadius, + side: side, + ); } } @@ -468,8 +547,12 @@ OutlinedBorder getShape({ /// This method must be called before calling [convertAlignmentToPosition] /// and [convertPositionToAlignment] to get the correct childWidth and /// childHeight for these methods. -SizeC getEffectiveChildSizeForImage(BaseNode node, PaintModel paint, - {double? scaleX, double? scaleY}) { +SizeC getEffectiveChildSizeForImage( + BaseNode node, + PaintModel paint, { + double? scaleX, + double? scaleY, +}) { final double modifiedScaleX = (scaleX ?? paint.scaleX).abs(); final double modifiedScaleY = (scaleY ?? paint.scaleY).abs(); @@ -538,7 +621,8 @@ bool isCustomPositionRequiredForImage(PaintModel paint, SizeC nodeSize) { if (paint.sourceWidth == null || paint.sourceHeight == null) return false; if (paint.isNonUniformScale) return true; if (paint.fit == Fit.fitWidth || paint.fit == Fit.fitHeight) { - final diff = (paint.sourceWidth! / paint.sourceHeight!) - + final diff = + (paint.sourceWidth! / paint.sourceHeight!) - (nodeSize.width / nodeSize.height); if (diff.closeTo(0)) return true; } @@ -560,13 +644,13 @@ Widget wrapWithPaddedScrollable({ width: (node.horizontalFit == SizeFit.shrinkWrap) ? null : (node.horizontalFit == SizeFit.expanded) - ? double.infinity - : node.basicBoxLocal.width, + ? double.infinity + : node.basicBoxLocal.width, height: (node.verticalFit == SizeFit.shrinkWrap) ? null : (node.verticalFit == SizeFit.expanded) - ? double.infinity - : node.basicBoxLocal.height, + ? double.infinity + : node.basicBoxLocal.height, child: SingleChildScrollView( scrollDirection: node.scrollDirection.flutterAxis, reverse: node.reverse, @@ -583,5 +667,6 @@ Widget wrapWithPaddedScrollable({ bool get isPlatformSupportedForWebView => kIsWeb || - PassiveEmbeddedVideoWidget.supportedPlatforms - .contains(defaultTargetPlatform); + PassiveEmbeddedVideoWidget.supportedPlatforms.contains( + defaultTargetPlatform, + ); diff --git a/lib/src/transformers/utils/blend_mask.dart b/lib/src/transformers/utils/blend_mask.dart index 268ba9f2..cdfd1d51 100644 --- a/lib/src/transformers/utils/blend_mask.dart +++ b/lib/src/transformers/utils/blend_mask.dart @@ -31,11 +31,7 @@ class BlendMask extends SingleChildRenderObjectWidget { /// Creates a widget that blends its child using the given [blendMode]. /// /// The [blendMode] argument must not be null. - const BlendMask({ - this.blendMode = BlendMode.srcOver, - super.key, - super.child, - }); + const BlendMask({this.blendMode = BlendMode.srcOver, super.key, super.child}); @override RenderObject createRenderObject(BuildContext context) { @@ -79,10 +75,10 @@ class RenderBlendMask extends RenderProxyBox { /// /// The [blendMode] must not be null. RenderBlendMask(BlendMode blendMode) - : _blendMode = blendMode, - blender = Paint() - ..blendMode = blendMode - ..color = Colors.white; + : _blendMode = blendMode, + blender = Paint() + ..blendMode = blendMode + ..color = Colors.white; @override void paint(PaintingContext context, Offset offset) { diff --git a/lib/src/transformers/utils/condition_evaluator.dart b/lib/src/transformers/utils/condition_evaluator.dart index 5c9e71f3..f5bbba45 100644 --- a/lib/src/transformers/utils/condition_evaluator.dart +++ b/lib/src/transformers/utils/condition_evaluator.dart @@ -59,15 +59,17 @@ class ConditionEvaluator @override Object? visitVariablePart(VariablePart part) { - final value = - part.valueString.splitMapJoinRegex(variablePathRegex, onMatch: (match) { - final Object? value = PropertyValueDelegate.getVariableValueFromPath( - match[0]!, - scopedValues: scopedValues, - ); - - return value?.typedValue() ?? 'null'; - }); + final value = part.valueString.splitMapJoinRegex( + variablePathRegex, + onMatch: (match) { + final Object? value = PropertyValueDelegate.getVariableValueFromPath( + match[0]!, + scopedValues: scopedValues, + ); + + return value?.typedValue() ?? 'null'; + }, + ); return _visitRawValue(value); } @@ -108,10 +110,9 @@ class ConditionEvaluator if (left == null || right == null) return false; // This is required since we have loose type checking. - return left - .toString() - .toLowerCase() - .compareTo(right.toString().toLowerCase()) > + return left.toString().toLowerCase().compareTo( + right.toString().toLowerCase(), + ) > 0; } @@ -122,10 +123,9 @@ class ConditionEvaluator if (left == null || right == null) return false; // This is required since we have loose type checking. - return left - .toString() - .toLowerCase() - .compareTo(right.toString().toLowerCase()) < + return left.toString().toLowerCase().compareTo( + right.toString().toLowerCase(), + ) < 0; } @@ -136,10 +136,9 @@ class ConditionEvaluator if (left == null || right == null) return false; // This is required since we have loose type checking. - return left - .toString() - .toLowerCase() - .compareTo(right.toString().toLowerCase()) >= + return left.toString().toLowerCase().compareTo( + right.toString().toLowerCase(), + ) >= 0; } @@ -150,10 +149,9 @@ class ConditionEvaluator if (left == null || right == null) return false; // This is required since we have loose type checking. - return left - .toString() - .toLowerCase() - .compareTo(right.toString().toLowerCase()) <= + return left.toString().toLowerCase().compareTo( + right.toString().toLowerCase(), + ) <= 0; } @@ -273,13 +271,15 @@ class ConditionPrinter final StringBuffer _buffer = StringBuffer(); void printCondition(BaseCondition condition) { - _buffer.clear(); - condition.accept(this); - print('\n${_buffer.toString()}'); + DebugLogger.instance.printInfo( + '\n${prettify(condition)}', + name: 'ConditionPrinter', + ); } String prettify(BaseCondition condition) { - printCondition(condition); + _buffer.clear(); + condition.accept(this); return _buffer.toString(); } @@ -320,8 +320,9 @@ class ConditionPrinter void visitExpression(Expression expression) { _buffer.write('( '); expression.leftPart.accept(this); - _buffer - .write(' ${expression.operator.sign ?? expression.operator.sentence} '); + _buffer.write( + ' ${expression.operator.sign ?? expression.operator.sentence} ', + ); expression.rightPart.accept(this); _buffer.write(' )'); } @@ -347,7 +348,8 @@ class ConditionPrinter @override void visitSetValueAction(SetValueAction action) { _buffer.writeln( - '\tSet Value of ${action.values.first.name} to ${valueModelToString(action.values.first)}'); + '\tSet Value of ${action.values.first.name} to ${valueModelToString(action.values.first)}', + ); } String valueModelToString(ValueModel model) { @@ -364,7 +366,8 @@ class ConditionPrinter @override void visitSetVariableAction(SetVariableAction action) { _buffer.writeln( - '\tSet Variable ${action.variable.name} to ${action.variable.value}'); + '\tSet Variable ${action.variable.name} to ${action.variable.value}', + ); } @override diff --git a/lib/src/transformers/utils/condition_visitors.dart b/lib/src/transformers/utils/condition_visitors.dart index fcefe139..88cb9db0 100644 --- a/lib/src/transformers/utils/condition_visitors.dart +++ b/lib/src/transformers/utils/condition_visitors.dart @@ -7,9 +7,7 @@ class ConditionVariablesVisitor ExpressionVisitor>, ExpressionPartVisitor>, ActionVisitor> { - const ConditionVariablesVisitor({ - this.excludedVariableNames = const {}, - }); + const ConditionVariablesVisitor({this.excludedVariableNames = const {}}); final Set excludedVariableNames; @@ -17,8 +15,9 @@ class ConditionVariablesVisitor Set visitCondition(Condition condition) { return { ...condition.expression.accept>(this) ?? {}, - ...condition.actions - .expand((action) => action.accept>(this) ?? {}), + ...condition.actions.expand( + (action) => action.accept>(this) ?? {}, + ), }; } @@ -117,8 +116,8 @@ class ConditionVariablesVisitor @override Set? visitLoadFromCloudStorageAction( - LoadFromCloudStorageAction action) => - {}; + LoadFromCloudStorageAction action, + ) => {}; } /// A visitor that returns the list of node IDs used in a condition's actions. @@ -134,18 +133,18 @@ class ConditionNodesVisitor @override Set? visitCondition(Condition condition) => { - ...condition.actions - .map((action) => action.accept(this)) - .nonNulls - .toSet(), - }; + ...condition.actions + .map((action) => action.accept(this)) + .nonNulls + .toSet(), + }; @override Set? visitConditionGroup(ConditionGroup condition) => { - ...condition.ifCondition.actions, - ...condition.elseIfConditions.expand((condition) => condition.actions), - ...condition.elseCondition?.actions ?? [], - }.map((action) => action.accept(this)).nonNulls.toSet(); + ...condition.ifCondition.actions, + ...condition.elseIfConditions.expand((condition) => condition.actions), + ...condition.elseCondition?.actions ?? [], + }.map((action) => action.accept(this)).nonNulls.toSet(); @override Set? visitElseCondition(ElseCondition condition) { diff --git a/lib/src/transformers/utils/dash_path.dart b/lib/src/transformers/utils/dash_path.dart index 68b0e62e..4b496a53 100644 --- a/lib/src/transformers/utils/dash_path.dart +++ b/lib/src/transformers/utils/dash_path.dart @@ -2,8 +2,7 @@ import 'dart:ui'; /// Converts a [path] to a dashed [Path] with a given [length] and [gap]. /// The [distance] parameter can be used to offset the dash pattern. -Path dashPath(final Path path, double length, - [double? gap, double? distance = 0]) { +Path dashPath(Path path, double length, [double? gap, double? distance = 0]) { gap ??= length; PathMetrics pathMetrics = path.computeMetrics(); Path dest = Path(); diff --git a/lib/src/transformers/utils/indexed_item_provider.dart b/lib/src/transformers/utils/indexed_item_provider.dart index dc5270bf..c12ccba3 100644 --- a/lib/src/transformers/utils/indexed_item_provider.dart +++ b/lib/src/transformers/utils/indexed_item_provider.dart @@ -18,7 +18,7 @@ class IndexedItemProvider extends InheritedWidget { oldWidget.item != item; } -class IndexedItem with EquatableMixin { +class IndexedItem with Equatable { final int index; final Object? item; diff --git a/lib/src/transformers/utils/node_state_provider.dart b/lib/src/transformers/utils/node_state_provider.dart index 72aa7354..9cf6b80c 100644 --- a/lib/src/transformers/utils/node_state_provider.dart +++ b/lib/src/transformers/utils/node_state_provider.dart @@ -43,8 +43,8 @@ class NodeStateProvider extends InheritedWidget { /// /// If there is no [NodeStateProvider] ancestor, returns null. static NodeStateProvider? maybeOf(BuildContext context) { - final NodeStateProvider? provider = - context.dependOnInheritedWidgetOfExactType(); + final NodeStateProvider? provider = context + .dependOnInheritedWidgetOfExactType(); if (provider == null) return null; return provider; } diff --git a/lib/src/transformers/utils/placeholder_painter.dart b/lib/src/transformers/utils/placeholder_painter.dart index 24a1f678..61285e30 100644 --- a/lib/src/transformers/utils/placeholder_painter.dart +++ b/lib/src/transformers/utils/placeholder_painter.dart @@ -40,14 +40,15 @@ class PlaceholderPainter extends CustomPainter { outerLeft = 0; } - final Rect outerRect = - Rect.fromLTWH(outerLeft, outerTop, outerWidth, outerHeight); - - canvas.drawRect( - outerRect, - bgPaint, + final Rect outerRect = Rect.fromLTWH( + outerLeft, + outerTop, + outerWidth, + outerHeight, ); + canvas.drawRect(outerRect, bgPaint); + gap = 4; double innerWidth = size.width - gap * 4; double innerHeight = size.height - gap * 4; @@ -62,8 +63,12 @@ class PlaceholderPainter extends CustomPainter { // innerTop = outerTop; // } - final Rect innerRect = - Rect.fromLTWH(innerLeft, innerTop, innerWidth, innerHeight); + final Rect innerRect = Rect.fromLTWH( + innerLeft, + innerTop, + innerWidth, + innerHeight, + ); canvas.drawRect(innerRect, bgPaint); @@ -101,27 +106,15 @@ class PlaceholderPainter extends CustomPainter { // Scale the text to always fit inside the box. final double textScale = min( 1.0, - min( - maxWidth / textWidth, - maxHeight / textHeight, - ), + min(maxWidth / textWidth, maxHeight / textHeight), ); canvas.scale(textScale); - canvas.translate( - -textPainter.width / 2, - -textPainter.height / 2, - ); + canvas.translate(-textPainter.width / 2, -textPainter.height / 2); - textPainter.paint( - canvas, - Offset.zero, - ); + textPainter.paint(canvas, Offset.zero); - canvas.translate( - textPainter.width / 2, - textPainter.height / 2, - ); + canvas.translate(textPainter.width / 2, textPainter.height / 2); canvas.scale(1 / textScale); canvas.translate(-size.width / 2, -size.height / 2); diff --git a/lib/src/transformers/utils/property_value_delegate.dart b/lib/src/transformers/utils/property_value_delegate.dart index 612ef571..31463cc2 100644 --- a/lib/src/transformers/utils/property_value_delegate.dart +++ b/lib/src/transformers/utils/property_value_delegate.dart @@ -101,8 +101,8 @@ class PropertyValueDelegate { if (codelesslyContext == null) return null; - final BaseCondition? condition = - codelesslyContext.conditions.findByNodeProperty(node.id, property); + final BaseCondition? condition = codelesslyContext.conditions + .findByNodeProperty(node.id, property); // if (condition != null) { // print('----------------------------------------------------------------'); @@ -214,8 +214,9 @@ class PropertyValueDelegate { if (variable == null) return null; - VariableType accessorType = - match.hasAccessor ? VariableType.fromObjectType(value) : variable.type; + VariableType accessorType = match.hasAccessor + ? VariableType.fromObjectType(value) + : variable.type; List accessors = variable.name.startsWith('api') ? getApiResponseAccessorsForType(VariableType.map) : getAccessorsForType(accessorType); @@ -224,8 +225,9 @@ class PropertyValueDelegate { if (value == null) return null; String accessor = segments.removeAt(0); - final Accessor? matchedAccessor = - accessors.firstWhereOrNull((element) => element.name == accessor); + final Accessor? matchedAccessor = accessors.firstWhereOrNull( + (element) => element.name == accessor, + ); if (matchedAccessor == null) { // If the accessor is not found, it means that the path is invalid. But @@ -258,8 +260,10 @@ class PropertyValueDelegate { if (match == null) return null; if (match.isPredefinedVariable) { - final Object? value = - retrievePredefinedVariableValue(match, scopedValues); + final Object? value = retrievePredefinedVariableValue( + match, + scopedValues, + ); return value; } @@ -268,29 +272,34 @@ class PropertyValueDelegate { if (match.hasAccessor) { if (variable.type.isText) { final characters = variable.getValue().toString().characters.toList(); - final (Object? value, _) = - substituteJsonPath(match.fullPath, {match.name: characters}); + final (Object? value, _) = substituteJsonPath(match.fullPath, { + match.name: characters, + }); return value; } if (!variable.type.isList) return null; - final (Object? value, _) = - substituteJsonPath(match.fullPath, {match.name: variable.getValue()}); + final (Object? value, _) = substituteJsonPath(match.fullPath, { + match.name: variable.getValue(), + }); return value; } if (!match.hasPath) return variable.getValue(); - final variableProps = - getVariableProperties(variable.getValue(), variable.type); + final variableProps = getVariableProperties( + variable.getValue(), + variable.type, + ); final Object? variableValue = variable.getValue(); final Map values = { ...variableProps, if (variable.type.isMap && variableValue is Map) ...variableValue, }; - final (Object? value, _) = - substituteJsonPath(match.fullPath, {match.name: values}); + final (Object? value, _) = substituteJsonPath(match.fullPath, { + match.name: values, + }); return value; } @@ -312,19 +321,22 @@ class PropertyValueDelegate { // substitute path with screen. if (variableValue == null) return null; - final VariableType variableType = - VariableType.fromObjectType(variableValue); + final VariableType variableType = VariableType.fromObjectType( + variableValue, + ); if (match.hasAccessor) { if (variableType.isText) { final characters = variableValue.toString().characters.toList(); - final (Object? value, _) = - substituteJsonPath(match.fullPath, {match.name: characters}); + final (Object? value, _) = substituteJsonPath(match.fullPath, { + match.name: characters, + }); return value; } if (!variableType.isList) return null; - final (Object? value, _) = - substituteJsonPath(match.text, {match.name: variableValue}); + final (Object? value, _) = substituteJsonPath(match.text, { + match.name: variableValue, + }); return value; } @@ -336,8 +348,9 @@ class PropertyValueDelegate { if (variableType.isMap && variableValue is Map) ...Map.from(variableValue), }; - final (Object? value, _) = - substituteJsonPath(match.text, {match.name: values}); + final (Object? value, _) = substituteJsonPath(match.text, { + match.name: values, + }); return value; } @@ -377,8 +390,9 @@ class PropertyValueDelegate { if (nodeValues == null) return null; - final model = - nodeValues.firstWhereOrNull((element) => element.name == property); + final model = nodeValues.firstWhereOrNull( + (element) => element.name == property, + ); if (model == null) return null; @@ -391,7 +405,9 @@ class PropertyValueDelegate { /// - data.name: will be replaced with data['name']. /// static (Object?, JsonPointer?) substituteJsonPath( - String text, Map data) { + String text, + Map data, + ) { // If the text represents a JSON path, get the relevant value from [data] map. if (data.isEmpty) return (null, null); @@ -429,8 +445,9 @@ class PropertyValueDelegate { final match = VariableMatch.parse(token.wrapWithVariableSyntax()); if (match != null && match.hasAccessor) { if (traversedValue is List) { - (traversedValue, _) = - substituteJsonPath(token, {match.name: traversedValue}); + (traversedValue, _) = substituteJsonPath(token, { + match.name: traversedValue, + }); } else { traversedValue = []; } @@ -448,11 +465,14 @@ class PropertyValueDelegate { if (match != null && match.hasAccessor) { if (traversedValue is List) { - final index = - match.accessor!.replaceAll('[', '').replaceAll(']', '').toInt(); + final index = match.accessor! + .replaceAll('[', '') + .replaceAll(']', '') + .toInt(); if (index != null) { - traversedValue[index] = - substituteJsonPath(leafKey, {match.name: value}).$1; + traversedValue[index] = substituteJsonPath(leafKey, { + match.name: value, + }).$1; } } } else if (traversedValue is Map) { @@ -466,69 +486,69 @@ class PropertyValueDelegate { @internal @experimental static Map getVariableProperties( - Object? value, VariableType type) { + Object? value, + VariableType type, + ) { return switch (value) { String() => { - 'length': value.length, - 'isEmpty': value.isEmpty, - 'isNotEmpty': value.isNotEmpty, - 'isBlank': value.trim().isEmpty, - 'upcase': value.toUpperCase(), - 'downcase': value.toLowerCase(), - 'capitalize': value.capitalized, - }, + 'length': value.length, + 'isEmpty': value.isEmpty, + 'isNotEmpty': value.isNotEmpty, + 'isBlank': value.trim().isEmpty, + 'upcase': value.toUpperCase(), + 'downcase': value.toLowerCase(), + 'capitalize': value.capitalized, + }, int() => { - 'isEven': value.isEven, - 'isOdd': value.isOdd, - 'isFinite': value.isFinite, - 'isInfinite': value.isInfinite, - 'isNegative': value.isNegative, - 'isPositive': !value.isNegative, - }, + 'isEven': value.isEven, + 'isOdd': value.isOdd, + 'isFinite': value.isFinite, + 'isInfinite': value.isInfinite, + 'isNegative': value.isNegative, + 'isPositive': !value.isNegative, + }, double() => { - 'isFinite': value.isFinite, - 'isInfinite': value.isInfinite, - 'isNegative': value.isNegative, - 'isPositive': !value.isNegative, - 'isNaN': value.isNaN, - 'whole': value.toInt(), - 'floor': value.floor(), - 'ceil': value.ceil(), - }, - bool() => { - 'toggled': !value, - }, + 'isFinite': value.isFinite, + 'isInfinite': value.isInfinite, + 'isNegative': value.isNegative, + 'isPositive': !value.isNegative, + 'isNaN': value.isNaN, + 'whole': value.toInt(), + 'floor': value.floor(), + 'ceil': value.ceil(), + }, + bool() => {'toggled': !value}, Map() => { - 'length': value.length, - 'isEmpty': value.isEmpty, - 'isNotEmpty': value.isNotEmpty, - 'keys': value.keys.toList(), - 'values': value.values.toList(), - }, + 'length': value.length, + 'isEmpty': value.isEmpty, + 'isNotEmpty': value.isNotEmpty, + 'keys': value.keys.toList(), + 'values': value.values.toList(), + }, List() => { - 'length': value.length, - 'isEmpty': value.isEmpty, - 'isNotEmpty': value.isNotEmpty - }, + 'length': value.length, + 'isEmpty': value.isEmpty, + 'isNotEmpty': value.isNotEmpty, + }, ColorRGBA() => { - 'red': value.r, - 'green': value.g, - 'blue': value.b, - 'alpha': value.a, - 'opacity': value.a / 255, - }, + 'red': value.r, + 'green': value.g, + 'blue': value.b, + 'alpha': value.a, + 'opacity': value.a / 255, + }, ColorRGB() => { - 'red': value.r, - 'green': value.g, - 'blue': value.b, - 'alpha': 255, - 'opacity': 1, - }, + 'red': value.r, + 'green': value.g, + 'blue': value.b, + 'alpha': 255, + 'opacity': 1, + }, _ when type.isText => { - 'isBlank': true, - 'isEmpty': true, - 'isNotEmpty': false, - }, + 'isBlank': true, + 'isEmpty': true, + 'isNotEmpty': false, + }, _ => {}, }; } @@ -783,108 +803,108 @@ List getAccessorsForType(VariableType type) { /// Defines accessors for an api variable based on the type of date it holds. List getApiResponseAccessorsForType(VariableType dataType) => [ - Accessor( - name: 'url', - type: VariableType.text, - getValue: (value) => value.toMap()?['url'], - ), - Accessor( - name: 'status', - type: VariableType.text, - getValue: (value) => value.toMap()?['status']?.toString(), - ), - Accessor( - name: 'statusCode', - type: VariableType.integer, - getValue: (value) => value.toMap()?['statusCode']?.toInt(), - ), - Accessor( - name: 'isIdle', - type: VariableType.boolean, - getValue: (value) => value.toMap()?['isIdle']?.toBool(), - ), - Accessor( - name: 'isLoading', - type: VariableType.boolean, - getValue: (value) => (value.toMap()?['isLoading']).toBool(), - ), - Accessor( - name: 'isError', - type: VariableType.boolean, - getValue: (value) => (value.toMap()?['isError']).toBool(), - ), - Accessor( - name: 'isSuccess', - type: VariableType.boolean, - getValue: (value) => (value.toMap()?['isSuccess']).toBool(), - ), - Accessor( - name: 'hasData', - type: VariableType.boolean, - getValue: (value) => (value.toMap()?['hasData']).toBool(), - ), - Accessor( - name: 'data', - type: dataType, - getValue: (value) => value.toMap()?['data'], - ), - Accessor( - name: 'error', - type: dataType, - getValue: (value) => value.toMap()?['error'], - ), - Accessor( - name: 'headers', - type: VariableType.map, - getValue: (value) => value.toMap()?['headers']?.toMap(), - ), - ]; + Accessor( + name: 'url', + type: VariableType.text, + getValue: (value) => value.toMap()?['url'], + ), + Accessor( + name: 'status', + type: VariableType.text, + getValue: (value) => value.toMap()?['status']?.toString(), + ), + Accessor( + name: 'statusCode', + type: VariableType.integer, + getValue: (value) => value.toMap()?['statusCode']?.toInt(), + ), + Accessor( + name: 'isIdle', + type: VariableType.boolean, + getValue: (value) => value.toMap()?['isIdle']?.toBool(), + ), + Accessor( + name: 'isLoading', + type: VariableType.boolean, + getValue: (value) => (value.toMap()?['isLoading']).toBool(), + ), + Accessor( + name: 'isError', + type: VariableType.boolean, + getValue: (value) => (value.toMap()?['isError']).toBool(), + ), + Accessor( + name: 'isSuccess', + type: VariableType.boolean, + getValue: (value) => (value.toMap()?['isSuccess']).toBool(), + ), + Accessor( + name: 'hasData', + type: VariableType.boolean, + getValue: (value) => (value.toMap()?['hasData']).toBool(), + ), + Accessor( + name: 'data', + type: dataType, + getValue: (value) => value.toMap()?['data'], + ), + Accessor( + name: 'error', + type: dataType, + getValue: (value) => value.toMap()?['error'], + ), + Accessor( + name: 'headers', + type: VariableType.map, + getValue: (value) => value.toMap()?['headers']?.toMap(), + ), +]; /// Defines accessors for a cloud data variable based on the type of date it holds. List getCloudDataVariableAccessorsForType(VariableType dataType) => [ - Accessor( - name: 'url', - type: VariableType.text, - getValue: (value) => value.toMap()?['url'], - ), - Accessor( - name: 'status', - type: VariableType.text, - getValue: (value) => value.toMap()?['status']?.toString(), - ), - Accessor( - name: 'isIdle', - type: VariableType.boolean, - getValue: (value) => value.toMap()?['isIdle']?.toBool(), - ), - Accessor( - name: 'isLoading', - type: VariableType.boolean, - getValue: (value) => value.toMap()?['isLoading']?.toBool(), - ), - Accessor( - name: 'isError', - type: VariableType.boolean, - getValue: (value) => value.toMap()?['isError']?.toBool(), - ), - Accessor( - name: 'isSuccess', - type: VariableType.boolean, - getValue: (value) => value.toMap()?['isSuccess']?.toBool(), - ), - Accessor( - name: 'hasData', - type: VariableType.boolean, - getValue: (value) => value.toMap()?['hasData']?.toBool(), - ), - Accessor( - name: 'data', - type: dataType, - getValue: (value) => value.toMap()?['data'], - ), - Accessor( - name: 'error', - type: dataType, - getValue: (value) => value.toMap()?['error'], - ), - ]; + Accessor( + name: 'url', + type: VariableType.text, + getValue: (value) => value.toMap()?['url'], + ), + Accessor( + name: 'status', + type: VariableType.text, + getValue: (value) => value.toMap()?['status']?.toString(), + ), + Accessor( + name: 'isIdle', + type: VariableType.boolean, + getValue: (value) => value.toMap()?['isIdle']?.toBool(), + ), + Accessor( + name: 'isLoading', + type: VariableType.boolean, + getValue: (value) => value.toMap()?['isLoading']?.toBool(), + ), + Accessor( + name: 'isError', + type: VariableType.boolean, + getValue: (value) => value.toMap()?['isError']?.toBool(), + ), + Accessor( + name: 'isSuccess', + type: VariableType.boolean, + getValue: (value) => value.toMap()?['isSuccess']?.toBool(), + ), + Accessor( + name: 'hasData', + type: VariableType.boolean, + getValue: (value) => value.toMap()?['hasData']?.toBool(), + ), + Accessor( + name: 'data', + type: dataType, + getValue: (value) => value.toMap()?['data'], + ), + Accessor( + name: 'error', + type: dataType, + getValue: (value) => value.toMap()?['error'], + ), +]; diff --git a/lib/src/transformers/utils/scoped_values.dart b/lib/src/transformers/utils/scoped_values.dart index 43d4cc73..86cb822f 100644 --- a/lib/src/transformers/utils/scoped_values.dart +++ b/lib/src/transformers/utils/scoped_values.dart @@ -38,31 +38,31 @@ class ScopedValues { /// Variable name -> Variable. Map get variables => { - ...codelesslyContext?.variableNamesMap() ?? const {}, - for (final variable in _variablesOverrides ?? const []) - variable.id: variable, - }; + ...codelesslyContext?.variableNamesMap() ?? const {}, + for (final variable in _variablesOverrides ?? const []) + variable.id: variable, + }; /// A map of data that is passed to loaded layouts for nodes to replace their /// values with. Map get data => { - ...codelesslyContext?.data ?? const {}, - ..._dataOverrides ?? const {}, - }; + ...codelesslyContext?.data ?? const {}, + ..._dataOverrides ?? const {}, + }; Map>> get nodeValues => codelesslyContext?.nodeValues ?? {}; /// Creates a new scoped values instance with empty values. ScopedValues.empty() - : _codelesslyContextRef = null, - _localStorageRef = null, - _cloudDatabaseRef = null, - _variablesOverrides = null, - _dataOverrides = null, - indexedItem = null, - nodeState = null, - routeParams = {}; + : _codelesslyContextRef = null, + _localStorageRef = null, + _cloudDatabaseRef = null, + _variablesOverrides = null, + _dataOverrides = null, + indexedItem = null, + nodeState = null, + routeParams = {}; /// Creates a new scoped values instance from a given context. ScopedValues.of( @@ -73,14 +73,14 @@ class ScopedValues { _variablesOverrides = variablesOverrides; _dataOverrides = dataOverrides; final codelesslyContext = context.read(); - if (codelesslyContext != null) { - _codelesslyContextRef = WeakReference(codelesslyContext); - } + _codelesslyContextRef = codelesslyContext == null + ? null + : WeakReference(codelesslyContext); indexedItem = IndexedItemProvider.maybeOf(context); nodeState = NodeStateProvider.maybeOf(context)?.state; routeParams = ModalRoute.of(context)?.settings.arguments as Map? ?? - {}; + {}; final instance = context.read(); if (instance != null) { _localStorageRef = WeakReference(instance.localDatabase); diff --git a/lib/src/transformers/utils/stroke_painter.dart b/lib/src/transformers/utils/stroke_painter.dart index 1234d4ac..c2f5c460 100644 --- a/lib/src/transformers/utils/stroke_painter.dart +++ b/lib/src/transformers/utils/stroke_painter.dart @@ -71,21 +71,21 @@ class StrokePainter extends CustomPainter { this.strokeAlign = StrokeAlignC.inside, this.strokeCap = StrokeCapEnum.square, this.borderRadius = BorderRadius.zero, - }) : dashArray = LoopingList(dashPattern), - border = Border( - left: strokeSide == StrokeSide.left - ? BorderSide(color: color, width: strokeWidth) - : BorderSide.none, - right: strokeSide == StrokeSide.right - ? BorderSide(color: color, width: strokeWidth) - : BorderSide.none, - bottom: strokeSide == StrokeSide.bottom - ? BorderSide(color: color, width: strokeWidth) - : BorderSide.none, - top: strokeSide == StrokeSide.top - ? BorderSide(color: color, width: strokeWidth) - : BorderSide.none, - ); + }) : dashArray = LoopingList(dashPattern), + border = Border( + left: strokeSide == StrokeSide.left + ? BorderSide(color: color, width: strokeWidth) + : BorderSide.none, + right: strokeSide == StrokeSide.right + ? BorderSide(color: color, width: strokeWidth) + : BorderSide.none, + bottom: strokeSide == StrokeSide.bottom + ? BorderSide(color: color, width: strokeWidth) + : BorderSide.none, + top: strokeSide == StrokeSide.top + ? BorderSide(color: color, width: strokeWidth) + : BorderSide.none, + ); @override void paint(Canvas canvas, Size size) { @@ -127,9 +127,10 @@ class StrokePainter extends CustomPainter { if (boxShape == BoxShape.circle) { final double circleDiameter = min(size.width, size.height); final Rect basicRect = Rect.fromCenter( - center: Offset(size.width / 2, size.height / 2), - width: circleDiameter, - height: circleDiameter); + center: Offset(size.width / 2, size.height / 2), + width: circleDiameter, + height: circleDiameter, + ); path.addOval(basicRect); } else { final Rect basicRect = Rect.fromLTWH(0, 0, size.width, size.height); @@ -174,14 +175,14 @@ class StrokePainter extends CustomPainter { // the vector's direction between (-y, x) or (y, -x), flipping // the direction for our convenience. final Offset parallelTangentVector = tangent.vector; - final Offset normal = Offset( + final Offset normal = + Offset( parallelTangentVector.dy * isInside * isCenter, parallelTangentVector.dx * isOutside, ) * // Multiply by the stroke thickness to figure out the distance // needed from the start position to the stroke. strokeWidth * - // If it's a center stroke, we need to divide the normal by 2. // It will be rendered twice, once inside, once outside, // achieving the center stroke effect. @@ -227,8 +228,10 @@ class StrokePainter extends CustomPainter { realSize = Size(size.width + strokeWidth, size.height + strokeWidth); case StrokeAlignC.outside: topLeft = Offset(-strokeWidth, -strokeWidth); - realSize = - Size(size.width + strokeWidth * 2, size.height + strokeWidth * 2); + realSize = Size( + size.width + strokeWidth * 2, + size.height + strokeWidth * 2, + ); case StrokeAlignC.inside: break; } @@ -245,9 +248,10 @@ class StrokePainter extends CustomPainter { if (boxShape == BoxShape.circle) { final double circleDiameter = min(size.width, size.height); final Rect basicRect = Rect.fromCenter( - center: Offset(size.width / 2, size.height / 2), - width: circleDiameter, - height: circleDiameter); + center: Offset(size.width / 2, size.height / 2), + width: circleDiameter, + height: circleDiameter, + ); innerPath.addOval(basicRect); outerPath.addOval(basicRect.inflate(strokeWidth)); } else { @@ -273,26 +277,31 @@ class StrokePainter extends CustomPainter { // // We get a really nice and seamless outside border transition this // way. - outerPath.addRRect((borderRadius.add( - BorderRadius.only( - topRight: Radius.circular( - strokeWidth * (min(borderRadius.topRight.x, 10) / 10)), - topLeft: Radius.circular( - strokeWidth * (min(borderRadius.topLeft.x, 10) / 10)), - bottomLeft: Radius.circular( - strokeWidth * (min(borderRadius.bottomLeft.x, 10) / 10)), - bottomRight: Radius.circular( - strokeWidth * (min(borderRadius.bottomRight.x, 10) / 10)), + outerPath.addRRect( + (borderRadius.add( + BorderRadius.only( + topRight: Radius.circular( + strokeWidth * (min(borderRadius.topRight.x, 10) / 10), + ), + topLeft: Radius.circular( + strokeWidth * (min(borderRadius.topLeft.x, 10) / 10), + ), + bottomLeft: Radius.circular( + strokeWidth * (min(borderRadius.bottomLeft.x, 10) / 10), + ), + bottomRight: Radius.circular( + strokeWidth * (min(borderRadius.bottomRight.x, 10) / 10), + ), + ), + ) as BorderRadius).toRRect( + Rect.fromLTWH( + topLeft.dx, + topLeft.dy, + realSize.width, + realSize.height, + ), ), - ) as BorderRadius) - .toRRect( - Rect.fromLTWH( - topLeft.dx, - topLeft.dy, - realSize.width, - realSize.height, - ), - )); + ); } // This is where the magic happens. We want to fill the area @@ -303,13 +312,14 @@ class StrokePainter extends CustomPainter { dest.addPath(innerPath, Offset.zero); dest.addPath(outerPath, Offset.zero); - canvas.drawPath( - dest..close(), - paint..style = PaintingStyle.fill, - ); + canvas.drawPath(dest..close(), paint..style = PaintingStyle.fill); } else { final Rect rect = Rect.fromLTWH( - topLeft.dx, topLeft.dy, realSize.width, realSize.height); + topLeft.dx, + topLeft.dy, + realSize.width, + realSize.height, + ); // For any other border, we can just use Flutter's built in painter. // No special handling required here. diff --git a/lib/src/transformers/utils/text_utils.dart b/lib/src/transformers/utils/text_utils.dart index 873d795e..6a287ac3 100644 --- a/lib/src/transformers/utils/text_utils.dart +++ b/lib/src/transformers/utils/text_utils.dart @@ -78,21 +78,21 @@ class TextUtils { final TextStyle style = switch (hasMissingFont) { false => retrieveTextStyle( - fontSize: fontSize, - fontName: fontName, - textDecoration: textDecoration, - lineHeight: lineHeight, - letterSpacing: letterSpacing, - color: color, - effects: effects ?? (node is BlendMixin ? node.effects : const []), - ), + fontSize: fontSize, + fontName: fontName, + textDecoration: textDecoration, + lineHeight: lineHeight, + letterSpacing: letterSpacing, + color: color, + effects: effects ?? (node is BlendMixin ? node.effects : const []), + ), true => TextStyle( - color: Colors.red, - fontSize: fontSize, - fontWeight: FontWeight.bold, - decorationColor: Colors.yellow, - decoration: TextDecoration.underline, - ), + color: Colors.red, + fontSize: fontSize, + fontWeight: FontWeight.bold, + decorationColor: Colors.yellow, + decoration: TextDecoration.underline, + ), }; // Replace with fx symbol if required. @@ -215,6 +215,10 @@ class TextUtils { textAlign: textNode.textAlignHorizontal.toFlutter(), maxLines: textNode.maxLines, overflow: textNode.overflow.flutterOverflow, + softWrap: textNode.softWrap, + textScaler: textNode.textScaleFactor == null + ? null + : TextScaler.linear(textNode.textScaleFactor!), ); } @@ -287,38 +291,33 @@ class TextUtils { static double retrieveLetterSpacing( LetterSpacing letterSpacing, double fontSize, - ) => - switch (letterSpacing.unit) { - LetterSpacingUnitEnum.pixels => letterSpacing.value, - LetterSpacingUnitEnum.percent => (letterSpacing.value * fontSize) / 100, - }; + ) => switch (letterSpacing.unit) { + LetterSpacingUnitEnum.pixels => letterSpacing.value, + LetterSpacingUnitEnum.percent => (letterSpacing.value * fontSize) / 100, + }; /// Convert LineHeight from Figma values into Flutter. - static double? retrieveLineHeight( - LineHeight lineHeight, - double fontSize, - ) => + static double? retrieveLineHeight(LineHeight lineHeight, double fontSize) => lineHeight.value == null || lineHeight.value! <= 0 - ? null - : switch (lineHeight.unit) { - LineHeightUnitEnum.pixels => lineHeight.value! / fontSize, - LineHeightUnitEnum.percent => lineHeight.value! / 100, - LineHeightUnitEnum.auto => null, - }; + ? null + : switch (lineHeight.unit) { + LineHeightUnitEnum.pixels => lineHeight.value! / fontSize, + LineHeightUnitEnum.percent => lineHeight.value! / 100, + LineHeightUnitEnum.auto => null, + }; static TextStyle retrieveTextStyleFromProp( TextProp prop, { List effects = const [], - }) => - retrieveTextStyle( - fontSize: prop.fontSize, - fontName: prop.fontName, - textDecoration: prop.textDecoration, - lineHeight: prop.lineHeight, - letterSpacing: prop.letterSpacing, - color: retrievePropColor(prop), - effects: effects, - ); + }) => retrieveTextStyle( + fontSize: prop.fontSize, + fontName: prop.fontName, + textDecoration: prop.textDecoration, + lineHeight: prop.lineHeight, + letterSpacing: prop.letterSpacing, + color: retrievePropColor(prop), + effects: effects, + ); /// Sets default values. static TextStyle retrieveTextStyle({ @@ -341,8 +340,8 @@ class TextUtils { final FontWeight? fontWeightProp = fontName?.flutterFontWeight; final FontStyle? fontStyle = fontName != null ? (fontName.style.toLowerCase().contains('italic')) - ? FontStyle.italic - : FontStyle.normal + ? FontStyle.italic + : FontStyle.normal : null; // Not supported yet but good to have. @@ -373,8 +372,9 @@ class TextUtils { shadows: shadows, ); } else { - final String? fontFamily = - fontName == null ? null : getFontFamilyNameAndVariant(fontName); + final String? fontFamily = fontName == null + ? null + : getFontFamilyNameAndVariant(fontName); // Enable this for fonts debugging. // print('Transformer is using fontFamily: $fontFamily'); diff --git a/lib/src/transformers/utils/variable_span.dart b/lib/src/transformers/utils/variable_span.dart index 1d92458b..859846cc 100644 --- a/lib/src/transformers/utils/variable_span.dart +++ b/lib/src/transformers/utils/variable_span.dart @@ -15,7 +15,7 @@ class VariableSpan extends TextSpan { super.onEnter, super.onExit, }) : super( - text: 'fx', - style: style?.merge(GoogleFonts.unna(fontStyle: FontStyle.italic)), - ); + text: 'fx', + style: style?.merge(GoogleFonts.unna(fontStyle: FontStyle.italic)), + ); } diff --git a/lib/src/transformers/web/web_webview_platform.dart b/lib/src/transformers/web/web_webview_platform.dart index d3dfd29b..6f562cbc 100644 --- a/lib/src/transformers/web/web_webview_platform.dart +++ b/lib/src/transformers/web/web_webview_platform.dart @@ -1,4 +1,4 @@ library; export 'web_webview_platform_stub.dart' - if (dart.library.html) 'web_webview_platform_web.dart'; + if (dart.library.js_interop) 'web_webview_platform_web.dart'; diff --git a/lib/src/transformers/widget_node_transformer_manager.dart b/lib/src/transformers/widget_node_transformer_manager.dart index 3b293784..268434f8 100644 --- a/lib/src/transformers/widget_node_transformer_manager.dart +++ b/lib/src/transformers/widget_node_transformer_manager.dart @@ -22,8 +22,14 @@ typedef LayoutRetrieverBuilder = Widget Function( /// [ActiveNodeTransformerManager]. /// /// Helps separate the functions that are common between them into one place. -abstract class WidgetNodeTransformerManager extends NodeTransformerManager< - Widget, BuildContext, WidgetBuildSettings, NodeWidgetTransformer> { +abstract class WidgetNodeTransformerManager + extends + NodeTransformerManager< + Widget, + BuildContext, + WidgetBuildSettings, + NodeWidgetTransformer + > { /// Returns a widget that decides how to load nested layouts of a rendered /// node. final LayoutRetrieverBuilder layoutRetrievalBuilder; @@ -47,7 +53,8 @@ abstract class WidgetNodeTransformerManager extends NodeTransformerManager< Widget widget, { required bool maintainState, }) { - final bool visible = PropertyValueDelegate.getPropertyValue( + final bool visible = + PropertyValueDelegate.getPropertyValue( node, 'visible', scopedValues: ScopedValues.of(context), @@ -72,13 +79,17 @@ abstract class WidgetNodeTransformerManager extends NodeTransformerManager< /// Takes into account margin and padding to figure out the origin of /// the rotation. Widget applyWidgetRotation( - BuildContext context, BaseNode node, Widget widget) { + BuildContext context, + BaseNode node, + Widget widget, + ) { final CodelesslyContext payload = context.read(); // Get node's values. final List nodeValues = payload.nodeValues[node.id]?.value ?? []; // Get local rotation value if it exists, else use node's rotation value. - final int rotationDegrees = nodeValues + final int rotationDegrees = + nodeValues .firstWhereOrNull((value) => value.name == 'rotationDegrees') ?.value .typedValue() ?? @@ -99,10 +110,7 @@ abstract class WidgetNodeTransformerManager extends NodeTransformerManager< /// Convenience method to handle widget margins. Widget applyWidgetMargins(BaseNode node, Widget widget) { if (node.margin == EdgeInsetsModel.zero) return widget; - return Padding( - padding: node.margin.flutterEdgeInsets, - child: widget, - ); + return Padding(padding: node.margin.flutterEdgeInsets, child: widget); } /// Convenience method to handle widget constraints. @@ -132,18 +140,21 @@ abstract class WidgetNodeTransformerManager extends NodeTransformerManager< // because then it has fills that obscure the inkwell effect. if (node is DefaultShapeNode && inkWell != null) return widget; - final List onClickReactions = (node as ReactionMixin) - .reactions - .where((reaction) => - reaction.trigger.type == TriggerType.click && - reaction.action.enabled) + final List onClickReactions = (node as ReactionMixin).reactions + .where( + (reaction) => + reaction.trigger.type == TriggerType.click && + reaction.action.enabled, + ) .toList(); final List onLongPressReactions = (node as ReactionMixin) .reactions - .where((reaction) => - reaction.trigger.type == TriggerType.longPress && - reaction.action.enabled) + .where( + (reaction) => + reaction.trigger.type == TriggerType.longPress && + reaction.action.enabled, + ) .toList(); if (inkWell != null) { @@ -163,7 +174,8 @@ abstract class WidgetNodeTransformerManager extends NodeTransformerManager< borderRadius: getBorderRadius(node), overlayColor: inkWell.overlayColor != null ? WidgetStatePropertyAll( - inkWell.overlayColor!.toFlutterColor()) + inkWell.overlayColor!.toFlutterColor(), + ) : null, splashColor: inkWell.splashColor?.toFlutterColor(), highlightColor: inkWell.highlightColor?.toFlutterColor(), @@ -202,8 +214,12 @@ abstract class WidgetNodeTransformerManager extends NodeTransformerManager< if (node is CornerMixin) { if (node.cornerRadius.linked && node.cornerRadius.type == RadiusType.elliptical) { - return BorderRadius.all(Radius.elliptical( - node.basicBoxLocal.width, node.basicBoxLocal.height)); + return BorderRadius.all( + Radius.elliptical( + node.basicBoxLocal.width, + node.basicBoxLocal.height, + ), + ); } return node.cornerRadius.borderRadius; } else { @@ -324,30 +340,42 @@ class AdaptiveSizeFitBox extends StatelessWidget { width: (horizontalFit == SizeFit.shrinkWrap) ? null : (horizontalFit == SizeFit.expanded) - ? double.infinity - : size.width, + ? double.infinity + : size.width, height: (verticalFit == SizeFit.shrinkWrap) ? null : (verticalFit == SizeFit.expanded) - ? double.infinity - : size.height, + ? double.infinity + : size.height, child: child, ); } } +/// Renders an [IconModel]: Material icons and symbols from Google's prebuilt +/// SVGs, Cupertino icons from the cupertino_icons font. class SvgIcon extends StatelessWidget { final IconModel icon; final Color? color; final double? size; final BoxFit fit; + /// Material Symbols axes; see [IconModelExt.toSvgUrl]. + final double? fill; + final double? weight; + final double? grade; + final double? opticalSize; + const SvgIcon({ super.key, required this.icon, this.color, this.size, this.fit = BoxFit.contain, + this.fill, + this.weight, + this.grade, + this.opticalSize, }); @override @@ -364,27 +392,43 @@ class SvgIcon extends StatelessWidget { final Color iconColor = switch (icon) { _ when color != null => color!, _ when iconTheme.color != null => iconTheme.color!, - _ => Theme.of(context).brightness == Brightness.light - ? Colors.black - : Colors.white, + _ => + Theme.of(context).brightness == Brightness.light + ? Colors.black + : Colors.white, }; - final url = icon.toSvgUrl(); + final double dimension = size ?? Theme.of(context).iconTheme.size ?? 24; - if (url != null) { - // log('SVG ICON URL: $url'); - } else { - log('SVG ICON URL: 404 | name: ${icon.name} | type: ${icon.type} | ' - 'codepoint: ${icon.codepoint} | fontFamily: ${icon.fontFamily} | ' - 'fontPackage: ${icon.fontPackage}${icon is MaterialIcon ? ' | ' - 'style: ${(icon as MaterialIcon).style}' : ''}'); + final IconData? cupertinoIcon = icon is CupertinoIcon + ? cupertinoIconsByCodepoint[icon.codepoint] + : null; + if (cupertinoIcon != null) { + return Icon(cupertinoIcon, size: dimension, color: iconColor); + } + + final url = icon.toSvgUrl( + fill: fill, + weight: weight, + grade: grade, + opticalSize: opticalSize, + ); + if (url == null) { + log( + 'No icon source | name: ${icon.name} | type: ${icon.type} | ' + 'codepoint: ${icon.codepoint} | fontFamily: ${icon.fontFamily} | ' + 'fontPackage: ${icon.fontPackage}', + ); + return SizedBox.square(dimension: dimension); } return SizedBox.square( - dimension: size ?? Theme.of(context).iconTheme.size ?? 24, + dimension: dimension, child: SvgPicture.network( - url ?? '', + url, fit: fit, colorFilter: ColorFilter.mode(iconColor, BlendMode.srcIn), + // Offline or unavailable icons leave their space empty. + errorBuilder: (context, error, stackTrace) => const SizedBox.shrink(), ), ); } @@ -404,8 +448,10 @@ class SvgIconImage extends StatelessWidget { this.color, this.size, this.fit = BoxFit.contain, - }) : assert(url != null || bytes != null, - 'Either url or bytes must be provided'); + }) : assert( + url != null || bytes != null, + 'Either url or bytes must be provided', + ); @override Widget build(BuildContext context) { @@ -421,9 +467,10 @@ class SvgIconImage extends StatelessWidget { final Color iconColor = switch (url) { _ when color != null => color!, _ when iconTheme.color != null => iconTheme.color!, - _ => Theme.of(context).brightness == Brightness.light - ? Colors.black - : Colors.white, + _ => + Theme.of(context).brightness == Brightness.light + ? Colors.black + : Colors.white, }; return SizedBox.square( @@ -438,6 +485,8 @@ class SvgIconImage extends StatelessWidget { url!, fit: fit, colorFilter: ColorFilter.mode(iconColor, BlendMode.srcIn), + errorBuilder: (context, error, stackTrace) => + const SizedBox.shrink(), ), ); } diff --git a/lib/src/ui/codelessly_context.dart b/lib/src/ui/codelessly_context.dart index 40ac3482..48c8c976 100644 --- a/lib/src/ui/codelessly_context.dart +++ b/lib/src/ui/codelessly_context.dart @@ -9,7 +9,7 @@ import '../../codelessly_sdk.dart'; /// Holds data passed from the Codelessly instance down the widget tree where /// all of the [WidgetNodeTransformer]s have access to it. -class CodelesslyContext with ChangeNotifier, EquatableMixin { +class CodelesslyContext with ChangeNotifier, Equatable { /// A map of data that is passed to loaded layouts for nodes to replace their /// values with. Map _data; @@ -62,27 +62,26 @@ class CodelesslyContext with ChangeNotifier, EquatableMixin { /// A notifier that is used to rebuild individual nodes. final ValueNotifier nodeRebuildNotifier = ValueNotifier(''); - /// Creates a [CodelesslyContext] with the given [data], [functions], and + /// Creates a [CodelesslyContext] with the given [_data], [functions], and /// [nodeValues]. CodelesslyContext({ - required Map data, + required this._data, required this.functions, required Map? externalComponentBuilders, required this.nodeValues, required this.variables, required this.conditions, required this.layoutID, - }) : _data = data, - _externalComponentBuilders = externalComponentBuilders ?? {}; + }) : _externalComponentBuilders = externalComponentBuilders ?? {}; /// Creates a [CodelesslyContext] with empty an empty map of each property. CodelesslyContext.empty({String? layoutID}) - : _data = {}, - functions = {}, - _externalComponentBuilders = {}, - nodeValues = {}, - variables = {}, - conditions = {}; + : _data = {}, + functions = {}, + _externalComponentBuilders = {}, + nodeValues = {}, + variables = {}, + conditions = {}; @override void dispose() { @@ -151,11 +150,14 @@ class CodelesslyContext with ChangeNotifier, EquatableMixin { // Populate node values with node's values, not action's values. if (connectedNode != null) { addToNodeValues( - connectedNode, - connectedNode.propertyVariables - .where((property) => - action.values.any((value) => property.name == value.name)) - .toList()); + connectedNode, + connectedNode.propertyVariables + .where( + (property) => + action.values.any((value) => property.name == value.name), + ) + .toList(), + ); } case ActionType.setVariant: final action = actionModel as SetVariantAction; @@ -168,7 +170,7 @@ class CodelesslyContext with ChangeNotifier, EquatableMixin { StringValue( name: 'currentVariantId', value: connectedNode.currentVariantId, - ) + ), ]); } default: @@ -185,8 +187,9 @@ class CodelesslyContext with ChangeNotifier, EquatableMixin { final List newValues = []; // Filter out and populate new values. for (final ValueModel value in values) { - if (!currentValues - .any((currentValue) => currentValue.name == value.name)) { + if (!currentValues.any( + (currentValue) => currentValue.name == value.name, + )) { newValues.add(value); } } @@ -217,7 +220,9 @@ class CodelesslyContext with ChangeNotifier, EquatableMixin { // If the value is the same, then the underlying value notifier will not // notify listeners, so we need to return false. if (variable.value.value == newValue) { - log('[CodelesslyContext] Variable with name $name already has the value $newValue.'); + log( + '[CodelesslyContext] Variable with name $name already has the value $newValue.', + ); return false; } variable.value = variable.value.copyWith(value: newValue); diff --git a/lib/src/ui/codelessly_dialog_widget.dart b/lib/src/ui/codelessly_dialog_widget.dart index 002b13d4..ee771c26 100644 --- a/lib/src/ui/codelessly_dialog_widget.dart +++ b/lib/src/ui/codelessly_dialog_widget.dart @@ -38,8 +38,6 @@ class CodelesslyDialogWidget extends StatelessWidget { ); } - return Center( - child: builder(context), - ); + return Center(child: builder(context)); } } diff --git a/lib/src/ui/codelessly_error_screen.dart b/lib/src/ui/codelessly_error_screen.dart index 09e5128b..f4361c57 100644 --- a/lib/src/ui/codelessly_error_screen.dart +++ b/lib/src/ui/codelessly_error_screen.dart @@ -45,10 +45,7 @@ class _CodelesslyErrorScreenState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text( - 'Uh oh!', - style: Theme.of(context).textTheme.titleLarge, - ), + Text('Uh oh!', style: Theme.of(context).textTheme.titleLarge), const SizedBox(height: 16), if (widget.errors.length > 1) ...[ Row( @@ -81,8 +78,10 @@ class _CodelesslyErrorScreenState extends State { const SizedBox(height: 16), Container( padding: const EdgeInsets.symmetric(horizontal: 8), - constraints: - const BoxConstraints(maxWidth: 500, maxHeight: 500), + constraints: const BoxConstraints( + maxWidth: 500, + maxHeight: 500, + ), child: SingleChildScrollView( primary: false, padding: EdgeInsets.zero, @@ -145,7 +144,7 @@ class _CodelesslyErrorScreenState extends State { detailsVisible = !detailsVisible; }); }, - ) + ), ], ), const SizedBox(height: 8), @@ -175,7 +174,7 @@ class _CodelesslyErrorScreenState extends State { ], ), ), - ] + ], ], ), ), diff --git a/lib/src/ui/codelessly_widget.dart b/lib/src/ui/codelessly_widget.dart index fe70d2b8..a842ce19 100644 --- a/lib/src/ui/codelessly_widget.dart +++ b/lib/src/ui/codelessly_widget.dart @@ -1,12 +1,11 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:provider/provider.dart'; import '../../codelessly_sdk.dart'; -import '../logging/debug_logger.dart'; -import '../logging/error_logger.dart'; import '../logging/stat_tracker.dart'; import 'codelessly_error_screen.dart'; import 'codelessly_loading_screen.dart'; @@ -26,7 +25,7 @@ typedef CodelesslyWidgetErrorBuilder = Widget Function( ErrorLog error, ); -Widget _defaultLayoutBuilder(context, layout) => layout; +Widget _defaultLayoutBuilder(BuildContext context, Widget layout) => layout; /// SDK widget that requires the SDK to be initialized beforehand. /// @@ -186,38 +185,38 @@ class CodelesslyWidget extends StatefulWidget { // Additional parameters. this.codelesslyContext, - }) : data = {...data}, - functions = {...functions}, - externalComponentBuilders = {...externalComponentBuilders}, - assert( - ((config ?? - (codelessly ?? Codelessly.instance).config ?? - (controller?.config)) != - null), - controller != null - ? 'You must provide a [config] inside of your [controller] or configure the provided codelessly instance with one.' - : 'You must provide a [config] inside of your CodelesslyWidget or through the Codelessly instance.', - ), - assert( - ((layoutID != null) != (controller != null)) || - ((config ?? - (codelessly ?? Codelessly.instance).config ?? - (controller?.config)))! - .slug != - null, - 'You must provide either a [layoutID] or a [controller]. One must be specified, and both cannot be specified at the same time.' - '\nIf you provided neither, then a slug must be configured in the config.', - ), - assert( - (publishSource == null && controller == null) || - (publishSource != null) != (controller != null), - 'You must provide either an [publishSource] or a [controller]. One must be specified, and both cannot be specified at the same time.', - ), - assert( - (codelessly == null && controller == null) || - (codelessly != null) != (controller != null), - 'You must provide either a [codelessly] or a [controller]. One may be specified, and both cannot be specified at the same time.', - ); + }) : data = {...data}, + functions = {...functions}, + externalComponentBuilders = {...externalComponentBuilders}, + assert( + ((config ?? + (codelessly ?? Codelessly.instance).config ?? + (controller?.config)) != + null), + controller != null + ? 'You must provide a [config] inside of your [controller] or configure the provided codelessly instance with one.' + : 'You must provide a [config] inside of your CodelesslyWidget or through the Codelessly instance.', + ), + assert( + ((layoutID != null) != (controller != null)) || + ((config ?? + (codelessly ?? Codelessly.instance).config ?? + (controller?.config)))! + .slug != + null, + 'You must provide either a [layoutID] or a [controller]. One must be specified, and both cannot be specified at the same time.' + '\nIf you provided neither, then a slug must be configured in the config.', + ), + assert( + (publishSource == null && controller == null) || + (publishSource != null) != (controller != null), + 'You must provide either an [publishSource] or a [controller]. One must be specified, and both cannot be specified at the same time.', + ), + assert( + (codelessly == null && controller == null) || + (codelessly != null) != (controller != null), + 'You must provide either a [codelessly] or a [controller]. One may be specified, and both cannot be specified at the same time.', + ); @override State createState() => _CodelesslyWidgetState(); @@ -287,15 +286,15 @@ class _CodelesslyWidgetState extends State { _effectiveController.initialize(); } - _errorSubscription = ErrorLogger.instance.errorStream.listen( - (ErrorLog error) { - if (error.layoutID == _effectiveController.layoutID) { - setState(() { - _lastError = error; - }); - } - }, - ); + _errorSubscription = ErrorLogger.instance.errorStream.listen(( + ErrorLog error, + ) { + if (error.layoutID == _effectiveController.layoutID) { + setState(() { + _lastError = error; + }); + } + }); if (widget.codelesslyContext != null) { DebugLogger.instance.printInfo( @@ -304,7 +303,8 @@ class _CodelesslyWidgetState extends State { ); } - codelesslyContext = widget.codelesslyContext ?? + codelesslyContext = + widget.codelesslyContext ?? CodelesslyContext( data: widget.data, functions: widget.functions, @@ -339,18 +339,27 @@ class _CodelesslyWidgetState extends State { effectiveLayoutID != null) { // Get the canvas ID from layout group for the current screen size. final newCanvasID = _getCanvasIDForLayoutGroup( - effectiveLayoutID, _effectiveController.publishModel!, screenSize); + effectiveLayoutID, + _effectiveController.publishModel!, + screenSize, + ); if (newCanvasID != canvasID) { // Breakpoint changed. Everything needs to be reloaded. canvasID = newCanvasID; final breakpoint = _effectiveController - .publishModel!.layouts[effectiveLayoutID!]!.breakpoints - .firstWhere((breakpoint) => breakpoint.nodeId == canvasID); + .publishModel! + .layouts[effectiveLayoutID!] + ?.breakpoints + .firstWhereOrNull((breakpoint) => breakpoint.nodeId == canvasID); // Notify breakpoints listeners. - _effectiveController.effectiveCodelessly - .notifyBreakpointsListeners(context, breakpoint); + if (breakpoint != null) { + _effectiveController.effectiveCodelessly.notifyBreakpointsListeners( + context, + breakpoint, + ); + } } } } @@ -364,13 +373,10 @@ class _CodelesslyWidgetState extends State { widget.codelesslyContext == null) { // explicitly provided context is removed codelesslyContext = codelesslyContext.copyWith(); - } else if (oldWidget.codelesslyContext == null && - widget.codelesslyContext != null) { - // explicitly provided context is added + } else { + // explicitly provided context is added or replaced codelesslyContext = widget.codelesslyContext!; } - // explicitly provided context is updated - codelesslyContext = widget.codelesslyContext!; } if (widget.data != codelesslyContext.data) { @@ -379,9 +385,11 @@ class _CodelesslyWidgetState extends State { // the build method is called since setting the data will trigger a // rebuild and it will crash if this happens when a rebuild is ongoing. SchedulerBinding.instance.addPostFrameCallback((timeStamp) { + if (!mounted) return; for (final notifier in codelesslyContext.variables.values) { - notifier.value = - notifier.value.copyWith(value: widget.data[notifier.value.name]); + notifier.value = notifier.value.copyWith( + value: widget.data[notifier.value.name], + ); } }); } @@ -397,15 +405,17 @@ class _CodelesslyWidgetState extends State { } if (widget.controller == null && oldWidget.controller != null) { - _controller = oldWidget.controller ?? createDefaultController(); + // The external controller was removed. It belongs to the caller, so + // create and own a new internal one instead of adopting it. + _controller = createDefaultController(); + _effectiveController.initialize(); + codelesslyContext.layoutID = _effectiveController.layoutID; } else if (widget.controller != null && oldWidget.controller == null) { _controller!.dispose(); _controller = null; - } - - // If the controller is intrinsic, then we need to dispose of the old one - // and create a new one if any of the values change. - if (widget.controller == null) { + } else if (widget.controller == null) { + // If the controller is intrinsic, then we need to dispose of the old one + // and create a new one if any of the values change. if (widget.publishSource != oldWidget.publishSource || widget.layoutID != oldWidget.layoutID || widget.codelessly != oldWidget.codelessly) { @@ -451,7 +461,10 @@ class _CodelesslyWidgetState extends State { /// Returns null if the layout group does not have a canvas for the current /// screen size or if the layout group does not exist. String? _getCanvasIDForLayoutGroup( - String? layoutID, SDKPublishModel model, Size screenSize) { + String? layoutID, + SDKPublishModel model, + Size screenSize, + ) { if (layoutID != null && model.layouts.containsKey(layoutID)) { final SDKPublishLayout? layout = model.layouts[layoutID]; if (layout != null) { @@ -517,15 +530,11 @@ class _CodelesslyWidgetState extends State { if (effectiveLayoutID == null) { final errorLog = ErrorLog( timestamp: DateTime.now(), - message: - 'A layoutID was not specified and the model does not have an entry layoutID specified.', + message: 'A layoutID was not specified and the model does not have an entry layoutID specified.', type: 'layout_not_initialized', ); - return _effectiveErrorBuilder?.call( - context, - errorLog, - ) ?? + return _effectiveErrorBuilder?.call(context, errorLog) ?? CodelesslyErrorScreen( errors: [errorLog], publishSource: _effectiveController.publishSource, @@ -635,9 +644,11 @@ class _CodelesslyWidgetState extends State { return MultiProvider( providers: [ ChangeNotifierProvider.value( - value: codelesslyContext), + value: codelesslyContext, + ), ChangeNotifierProvider.value( - value: _effectiveController), + value: _effectiveController, + ), Provider.value(value: codelessly), ], child: StreamBuilder( @@ -709,10 +720,10 @@ class _NavigationBuilderState extends State<_NavigationBuilder> { SchedulerBinding.instance.addPostFrameCallback((timeStamp) { if (!context.mounted) return; context.read().notifyNavigationListeners( - context, - layoutId: widget.layoutId, - canvasId: widget.canvasId, - ); + context, + layoutId: widget.layoutId, + canvasId: widget.canvasId, + ); }); } diff --git a/lib/src/ui/codelessly_widget_controller.dart b/lib/src/ui/codelessly_widget_controller.dart index 64e6d231..72a8bb70 100644 --- a/lib/src/ui/codelessly_widget_controller.dart +++ b/lib/src/ui/codelessly_widget_controller.dart @@ -4,10 +4,8 @@ import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; import '../../codelessly_sdk.dart'; -import '../logging/debug_logger.dart'; -import '../logging/error_logger.dart'; -Widget _defaultLayoutBuilder(context, layout) => layout; +Widget _defaultLayoutBuilder(BuildContext context, Widget layout) => layout; /// The provides advanced explicit control over /// the state of the instantiated [CodelesslyWidget] that it is attached to. @@ -155,11 +153,11 @@ class CodelesslyWidgetController extends ChangeNotifier { this.previewDataManager, this.templateDataManager, this.cacheManager, - }) : assert( - (config ?? (codelessly ?? Codelessly.instance).config) != null, - 'A [config] must be provided. Please provide one either in the constructor of this controller, or in the passed Codelessly instance.', - ), - config = config ?? (codelessly ?? Codelessly.instance).config! { + }) : assert( + (config ?? (codelessly ?? Codelessly.instance).config) != null, + 'A [config] must be provided. Please provide one either in the constructor of this controller, or in the passed Codelessly instance.', + ), + config = config ?? (codelessly ?? Codelessly.instance).config! { // If this controller is configured with its own Codelessly config but // wants to use the global instance, and the global instance is already // configured with a different config, then we need to create a custom @@ -229,8 +227,10 @@ class CodelesslyWidgetController extends ChangeNotifier { /// Listens to the SDK's status. If the SDK is done, then we can start /// listening to the data manager's status for layout updates. void initialize({String? layoutID}) { - DebugLogger.instance - .printFunction('initialize(layoutID: $layoutID)', name: name); + DebugLogger.instance.printFunction( + 'initialize(layoutID: $layoutID)', + name: name, + ); if (layoutID != null) { this.layoutID = layoutID; } @@ -257,8 +257,10 @@ class CodelesslyWidgetController extends ChangeNotifier { // here. if (isGlobalInstance || createdIntrinsicCodelessly) { if (status is CEmpty) { - DebugLogger.instance - .printInfo('Codelessly SDK is idle, configuring...', name: name); + DebugLogger.instance.printInfo( + 'Codelessly SDK is idle, configuring...', + name: name, + ); effectiveCodelessly.configure( config: config, authManager: authManager, @@ -271,8 +273,9 @@ class CodelesslyWidgetController extends ChangeNotifier { status = effectiveCodelessly.status; if (status is CConfigured) { DebugLogger.instance.printInfo( - 'Codelessly SDK is configured, initializing...', - name: name); + 'Codelessly SDK is configured, initializing...', + name: name, + ); effectiveCodelessly.initialize(); } } @@ -285,79 +288,93 @@ class CodelesslyWidgetController extends ChangeNotifier { stackTrace: str, ); - effectiveCodelessly.status = - CStatus.error('Failed to initialize widget controller'); + effectiveCodelessly.status = CStatus.error( + 'Failed to initialize widget controller', + ); } // First event. if (effectiveCodelessly.status case CLoaded() || CLoading()) { - if (effectiveCodelessly.status - case CLoading(state: CLoadingState state)) { + if (effectiveCodelessly.status case CLoading( + state: CLoadingState state, + )) { // Listen to data manager after it has been created. If it hasn't been // created yet, Firebase may still be initializing. DebugLogger.instance.printInfo( - '[${this.layoutID}]: Codelessly SDK is loading with step $state.', - name: name); + '[${this.layoutID}]: Codelessly SDK is loading with step $state.', + name: name, + ); if (state.hasPassed(CLoadingState.createdManagers)) { DebugLogger.instance.printInfo( - '[${this.layoutID}]: Checking layout because it passed the created managers step.', - name: name); + '[${this.layoutID}]: Checking layout because it passed the created managers step.', + name: name, + ); _checkLayout(); } else { DebugLogger.instance.printInfo( - '[${this.layoutID}]: Waiting for data manager to be created. Skipping for now.', - name: name); + '[${this.layoutID}]: Waiting for data manager to be created. Skipping for now.', + name: name, + ); } } else { DebugLogger.instance.printInfo( - '[${this.layoutID}]: Codelessly SDK is already loaded, checking layout.', - name: name); + '[${this.layoutID}]: Codelessly SDK is already loaded, checking layout.', + name: name, + ); _checkLayout(); } } DebugLogger.instance.printInfo( - '[${this.layoutID}]: Listening to sdk status stream.', - name: name); + '[${this.layoutID}]: Listening to sdk status stream.', + name: name, + ); DebugLogger.instance.printInfo( - '[${this.layoutID}]: Initial sdk status is: ${effectiveCodelessly.status}', - name: name); + '[${this.layoutID}]: Initial sdk status is: ${effectiveCodelessly.status}', + name: name, + ); _sdkStatusListener?.cancel(); _sdkStatusListener = effectiveCodelessly.statusStream.listen((status) { DebugLogger.instance.printInfo( - '[${this.layoutID}]: (Listener) SDK status changed to $status.', - name: name); + '[${this.layoutID}]: (Listener) SDK status changed to $status.', + name: name, + ); if (status case CLoaded() || CLoading()) { if (status case CLoading(state: CLoadingState state)) { DebugLogger.instance.printInfo( - '[${this.layoutID}]: (Listener) Codelessly SDK is loading with step $state.', - name: name); + '[${this.layoutID}]: (Listener) Codelessly SDK is loading with step $state.', + name: name, + ); // Listen to data manager after it has been created. If it hasn't been // created yet, Firebase may still be initializing. if (state.hasPassed(CLoadingState.createdManagers)) { DebugLogger.instance.printInfo( - '[${this.layoutID}]: (Listener) Checking layout because it passed the created managers step.', - name: name); + '[${this.layoutID}]: (Listener) Checking layout because it passed the created managers step.', + name: name, + ); _checkLayout(); } else { DebugLogger.instance.printInfo( - '[${this.layoutID}]: (Listener) Waiting for data manager to be created. Skipping for now.', - name: name); + '[${this.layoutID}]: (Listener) Waiting for data manager to be created. Skipping for now.', + name: name, + ); } } else { DebugLogger.instance.printInfo( - '[${this.layoutID}]: (Listener) Codelessly SDK is already loaded, checking layout.', - name: name); + '[${this.layoutID}]: (Listener) Codelessly SDK is already loaded, checking layout.', + name: name, + ); _checkLayout(); } } else { DebugLogger.instance.printInfo( - '[${this.layoutID}]: (Listener) Codelessly SDK is not loaded, skipping layout check.', - name: name); + '[${this.layoutID}]: (Listener) Codelessly SDK is not loaded, skipping layout check.', + name: name, + ); } }); } @@ -385,39 +402,42 @@ class CodelesslyWidgetController extends ChangeNotifier { stackTrace: str, ); - effectiveCodelessly.status = - CStatus.error('Failed to initialize data manager'); + effectiveCodelessly.status = CStatus.error( + 'Failed to initialize data manager', + ); }); } - // If the config has a slug specified and the data manager doesn't have // a publish model yet, then we need to fetch the publish model from the // data manager. else if (config.slug != null && dataManager.publishModel == null) { DebugLogger.instance.printInfo( - '[$layoutID]: (Check) A slug is specified and publish model is null.', - name: name); + '[$layoutID]: (Check) A slug is specified and publish model is null.', + name: name, + ); DebugLogger.instance.printInfo( - '[$layoutID]: (Check) Fetching complete publish bundle from data manager.', - name: name); + '[$layoutID]: (Check) Fetching complete publish bundle from data manager.', + name: name, + ); dataManager .fetchCompletePublishBundle( - slug: config.slug!, - source: publishSource, - ) + slug: config.slug!, + source: publishSource, + ) .catchError((error, str) { - ErrorLogger.instance.captureException( - error, - message: 'Failed to fetch publish bundle', - type: 'publish_bundle_fetch_failed', - layoutID: layoutID, - stackTrace: str, - ); - - effectiveCodelessly.status = - CStatus.error('Failed to fetch publish bundle'); - return false; - }); + ErrorLogger.instance.captureException( + error, + message: 'Failed to fetch publish bundle', + type: 'publish_bundle_fetch_failed', + layoutID: layoutID, + stackTrace: str, + ); + + effectiveCodelessly.status = CStatus.error( + 'Failed to fetch publish bundle', + ); + return false; + }); } // DataManager is initialized or downloading a publish bundle. If the // layoutID is not null, then we need to signal to the data manager that we @@ -427,39 +447,44 @@ class CodelesslyWidgetController extends ChangeNotifier { // taking care of this layout and we just need to tell it to prioritize it. else if (layoutID != null) { DebugLogger.instance.printInfo( - '[$layoutID]: (Check) Queuing layout [$layoutID] in data manager.', - name: name); + '[$layoutID]: (Check) Queuing layout [$layoutID] in data manager.', + name: name, + ); DebugLogger.instance.printInfo( - '[$layoutID]: (Check) Using publish source $publishSource.', - name: name); + '[$layoutID]: (Check) Using publish source $publishSource.', + name: name, + ); dataManager .queueLayout(layoutID: layoutID!, prioritize: true) .catchError((error, str) { - ErrorLogger.instance.captureException( - error, - message: 'Failed to queue layout', - type: 'layout_queue_failed', - layoutID: layoutID, - stackTrace: str, - ); - - // Set error status - effectiveCodelessly.status = CStatus.error('Failed to queue layout'); - }); + ErrorLogger.instance.captureException( + error, + message: 'Failed to queue layout', + type: 'layout_queue_failed', + layoutID: layoutID, + stackTrace: str, + ); + + // Set error status + effectiveCodelessly.status = CStatus.error( + 'Failed to queue layout', + ); + }); } - // At this point in the execution, layoutID is null, slug is not specified. // Preloading must be true, so this controller can only wait... else { if (layoutID != null) { DebugLogger.instance.printInfo( - '[$layoutID]: (Check) LayoutID specified, but preload is set to ${config.preload}, skipping to let data manager to download everything', - name: name); + '[$layoutID]: (Check) LayoutID specified, but preload is set to ${config.preload}, skipping to let data manager to download everything', + name: name, + ); } else { DebugLogger.instance.printInfo( - '[$layoutID]: (Check) LayoutID is null, skipping to let data manager to download everything.', - name: name); + '[$layoutID]: (Check) LayoutID is null, skipping to let data manager to download everything.', + name: name, + ); } } } catch (e, str) { diff --git a/lib/src/ui/draggable_scroll_configuration.dart b/lib/src/ui/draggable_scroll_configuration.dart index 89cb0c4a..5a272639 100644 --- a/lib/src/ui/draggable_scroll_configuration.dart +++ b/lib/src/ui/draggable_scroll_configuration.dart @@ -4,10 +4,7 @@ import 'package:flutter/material.dart'; class DraggableScrollConfiguration extends StatelessWidget { final Widget child; - const DraggableScrollConfiguration({ - super.key, - required this.child, - }); + const DraggableScrollConfiguration({super.key, required this.child}); @override Widget build(BuildContext context) { diff --git a/lib/src/ui/layout_builder.dart b/lib/src/ui/layout_builder.dart index 04e0f412..fe4da449 100644 --- a/lib/src/ui/layout_builder.dart +++ b/lib/src/ui/layout_builder.dart @@ -1,10 +1,11 @@ +import 'dart:async'; + import 'package:codelessly_api/codelessly_api.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../../codelessly_sdk.dart'; -import '../logging/debug_logger.dart'; /// A widget that retrieves a layout from a [CodelesslyWidgetController]. class CodelesslyLayoutRetriever extends StatefulWidget { @@ -56,9 +57,7 @@ class _CodelesslyLayoutRetrieverState extends State { return SizedBox( width: widget.bounds.width, height: widget.bounds.height, - child: CodelesslyWidget( - controller: controller, - ), + child: CodelesslyWidget(controller: controller), ); } } @@ -114,17 +113,22 @@ class _CodelesslyLayoutBuilderState extends State { /// delegates the node lookup to it. late final PassiveNodeTransformerManager transformerManager = PassiveNodeTransformerManager( - nodeRegistry.getNodeByID, - (context, bounds, pageID, layoutID, canvasID) => - widget.layoutRetrievalBuilder - ?.call(context, bounds, pageID, layoutID, canvasID) ?? - CodelesslyLayoutRetriever( - controller: widget.controller, - pageID: pageID, - layoutID: layoutID, - bounds: bounds, - ), - ); + nodeRegistry.getNodeByID, + (context, bounds, pageID, layoutID, canvasID) => + widget.layoutRetrievalBuilder?.call( + context, + bounds, + pageID, + layoutID, + canvasID, + ) ?? + CodelesslyLayoutRetriever( + controller: widget.controller, + pageID: pageID, + layoutID: layoutID, + bounds: bounds, + ), + ); CanvasNode get canvasNode => widget.layout.canvases[widget.canvasId]![widget.canvasId] as CanvasNode; @@ -157,8 +161,8 @@ class _CodelesslyLayoutBuilderState extends State { /// Loads the layout into the [nodeRegistry] and updates the /// [CodelesslyContext] with the node values. void loadLayout(BuildContext context) { - final CodelesslyContext codelesslyContext = - context.read(); + final CodelesslyContext codelesslyContext = context + .read(); nodeRegistry.clear(); nodeRegistry.setNodes(widget.layout.canvases[widget.canvasId]!); final List allNodes = nodeRegistry.getNodes().values.toList(); @@ -172,8 +176,8 @@ class _CodelesslyLayoutBuilderState extends State { .toList(); for (final node in nodesWithInternalValue) { final ValueModel? value = node.propertyVariables.firstWhereOrNull( - (property) => - property.name == 'value' || property.name == 'inputValue'); + (property) => property.name == 'value' || property.name == 'inputValue', + ); if (value == null) continue; codelesslyContext.addToNodeValues(node, [value]); } @@ -186,7 +190,9 @@ class _CodelesslyLayoutBuilderState extends State { for (final Reaction reaction in (node as ReactionMixin).reactions) { final ActionModel actionModel = reaction.action; codelesslyContext.handleActionConnections( - actionModel, nodeRegistry.getNodes()); + actionModel, + nodeRegistry.getNodes(), + ); } } @@ -197,7 +203,9 @@ class _CodelesslyLayoutBuilderState extends State { for (final Reaction reaction in reactiveChild.reactions) { final ActionModel actionModel = reaction.action; codelesslyContext.handleActionConnections( - actionModel, nodeRegistry.getNodes()); + actionModel, + nodeRegistry.getNodes(), + ); } } } @@ -206,23 +214,25 @@ class _CodelesslyLayoutBuilderState extends State { final Map variablesMap = publishModel.variables[widget.layout.id]?.variables[widget.canvasId] ?? - {}; + {}; // Clear all the variables that are not api variables. We persist api // variables across canvases from the same layout group so that the // api request is not made again. - codelesslyContext.variables - .removeWhere((key, value) => !value.value.isApiVariable); + codelesslyContext.variables.removeWhere( + (key, value) => !value.value.isApiVariable, + ); for (final variable in variablesMap.values) { // Override default values of variables with values provided in data. - final notifier = Observable(variable.copyWith( - value: codelesslyContext.data[variable.name], - )); + final notifier = Observable( + variable.copyWith(value: codelesslyContext.data[variable.name]), + ); codelesslyContext.variables[variable.id] = notifier; } - final conditions = publishModel - .conditions[widget.layout.id]?.conditions[widget.canvasId] ?? + final conditions = + publishModel.conditions[widget.layout.id]?.conditions[widget + .canvasId] ?? {}; codelesslyContext.conditions.clear(); codelesslyContext.conditions.addAll(conditions); @@ -247,14 +257,15 @@ class _CodelesslyLayoutBuilderState extends State { .where((action) => action.enabled && action.apiId != null) .toList(); - final CodelesslyContext codelesslyContext = - context.read(); + final CodelesslyContext codelesslyContext = context + .read(); final apisMap = context.read().dataManager.publishModel!.apis; for (final api in apisMap.values) { - final ApiCallAction? canvasAction = - apiCallActions.firstWhereOrNull((action) => action.apiId == api.id); + final ApiCallAction? canvasAction = apiCallActions.firstWhereOrNull( + (action) => action.apiId == api.id, + ); final variableName = apiNameToVariableName(api.name); @@ -262,17 +273,17 @@ class _CodelesslyLayoutBuilderState extends State { ? ApiResponseVariableUtils.loading(api.url) : ApiResponseVariableUtils.idle(api.url); - final existingVariable = - codelesslyContext.variableNamesMap()[variableName]; + final existingVariable = codelesslyContext + .variableNamesMap()[variableName]; final RuntimeVariableData variable = existingVariable is RuntimeVariableData - ? existingVariable - : RuntimeVariableData( - name: variableName, - type: VariableType.map, - value: data, - ); + ? existingVariable + : RuntimeVariableData( + name: variableName, + type: VariableType.map, + value: data, + ); // Override default values of variables with values provided in data. final notifier = Observable(variable); @@ -284,8 +295,24 @@ class _CodelesslyLayoutBuilderState extends State { 'Making api request for onLoad canvas action', name: CodelesslyLayoutRetriever.name, ); - FunctionsRepository.makeApiRequestFromAction( - canvasAction, context, notifier); + // The variable carries the error state; report the failure instead + // of leaving it as an unhandled async error. + unawaited( + FunctionsRepository.makeApiRequestFromAction( + canvasAction, + context, + notifier, + ).then( + (_) {}, + onError: (Object e, StackTrace stackTrace) => + ErrorLogger.instance.captureException( + e, + message: 'onLoad API call failed', + type: 'api_call_failed', + stackTrace: stackTrace, + ), + ), + ); } } } @@ -299,14 +326,15 @@ class _CodelesslyLayoutBuilderState extends State { .where((action) => action.enabled && action.variable != null) .toList(); - final CodelesslyContext codelesslyContext = - context.read(); + final CodelesslyContext codelesslyContext = context + .read(); for (final action in loadFromStorageActions) { final data = CloudDatabaseVariableUtils.idle(); - final variable = - codelesslyContext.findVariableByName(action.variable!.name); + final variable = codelesslyContext.findVariableByName( + action.variable!.name, + ); if (variable == null) { DebugLogger.instance.printInfo( @@ -322,10 +350,18 @@ class _CodelesslyLayoutBuilderState extends State { @override Widget build(BuildContext context) { + final CodelesslyConfig config = widget.controller.config; + final MediaQueryData mediaQuery = MediaQuery.of(context); return MediaQuery( - data: MediaQuery.of(context).copyWith(textScaler: TextScaler.noScaling), + data: mediaQuery.copyWith( + textScaler: config.maxTextScaleFactor <= 1 + ? TextScaler.noScaling + : mediaQuery.textScaler.clamp( + maxScaleFactor: config.maxTextScaleFactor, + ), + ), child: Theme( - data: ThemeData(), + data: config.useHostTheme ? Theme.of(context) : ThemeData(), child: transformerManager.buildWidgetFromNode( canvasNode, context, diff --git a/lib/src/utils/cupertino_icons_by_codepoint.dart b/lib/src/utils/cupertino_icons_by_codepoint.dart new file mode 100644 index 00000000..450f0b75 --- /dev/null +++ b/lib/src/utils/cupertino_icons_by_codepoint.dart @@ -0,0 +1,1243 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND. +// Generated by tool/generate_cupertino_icons.py from Flutter's +// CupertinoIcons. + +import 'package:flutter/cupertino.dart'; + +/// Flutter's [CupertinoIcons] keyed by codepoint. +const Map cupertinoIconsByCodepoint = { + 0xf111: CupertinoIcons.battery_charging, + 0xf112: CupertinoIcons.battery_empty, + 0xf113: CupertinoIcons.battery_full, + 0xf114: CupertinoIcons.battery_75_percent, + 0xf115: CupertinoIcons.battery_25_percent, + 0xf116: CupertinoIcons.bluetooth, + 0xf21c: CupertinoIcons.restart, + 0xf21d: CupertinoIcons.reply_all, + 0xf21e: CupertinoIcons.reply_thick_solid, + 0xf220: CupertinoIcons.share_up, + 0xf221: CupertinoIcons.shuffle_thick, + 0xf2bf: CupertinoIcons.pen, + 0xf2c1: CupertinoIcons.car_detailed, + 0xf2d7: CupertinoIcons.clear_thick, + 0xf2f7: CupertinoIcons.gear_big, + 0xf35d: CupertinoIcons.down_arrow, + 0xf366: CupertinoIcons.up_arrow, + 0xf36d: CupertinoIcons.bus, + 0xf36e: CupertinoIcons.clear_thick_circled, + 0xf36f: CupertinoIcons.car, + 0xf37d: CupertinoIcons.fullscreen_exit, + 0xf37e: CupertinoIcons.pencil, + 0xf37f: CupertinoIcons.delete_simple, + 0xf386: CupertinoIcons.fullscreen, + 0xf38a: CupertinoIcons.folder_open, + 0xf3a8: CupertinoIcons.refresh_thick, + 0xf3af: CupertinoIcons.train_style_one, + 0xf3b4: CupertinoIcons.train_style_two, + 0xf3b7: CupertinoIcons.volume_down, + 0xf3b8: CupertinoIcons.volume_mute, + 0xf3b9: CupertinoIcons.volume_off, + 0xf3ba: CupertinoIcons.volume_up, + 0xf3c9: CupertinoIcons.collections, + 0xf3ca: CupertinoIcons.collections_solid, + 0xf3cf: CupertinoIcons.back, + 0xf3d1: CupertinoIcons.forward, + 0xf3d2: CupertinoIcons.left_chevron, + 0xf3d3: CupertinoIcons.right_chevron, + 0xf3e1: CupertinoIcons.bell, + 0xf3e2: CupertinoIcons.bell_solid, + 0xf3e7: CupertinoIcons.book, + 0xf3e8: CupertinoIcons.book_solid, + 0xf3e9: CupertinoIcons.bookmark, + 0xf3ea: CupertinoIcons.bookmark_solid, + 0xf3f5: CupertinoIcons.photo_camera, + 0xf3f6: CupertinoIcons.photo_camera_solid, + 0xf3f7: CupertinoIcons.shopping_cart, + 0xf3fb: CupertinoIcons.conversation_bubble, + 0xf3fd: CupertinoIcons.check_mark, + 0xf3fe: CupertinoIcons.check_mark_circled, + 0xf3ff: CupertinoIcons.check_mark_circled_solid, + 0xf400: CupertinoIcons.circle_filled, + 0xf401: CupertinoIcons.circle, + 0xf402: CupertinoIcons.time, + 0xf403: CupertinoIcons.time_solid, + 0xf404: CupertinoIcons.clear, + 0xf405: CupertinoIcons.clear_circled, + 0xf406: CupertinoIcons.clear_circled_solid, + 0xf411: CupertinoIcons.settings, + 0xf412: CupertinoIcons.settings_solid, + 0xf417: CupertinoIcons.create, + 0xf419: CupertinoIcons.profile_circled, + 0xf422: CupertinoIcons.mail, + 0xf423: CupertinoIcons.mail_solid, + 0xf424: CupertinoIcons.eye, + 0xf425: CupertinoIcons.eye_solid, + 0xf42c: CupertinoIcons.flag, + 0xf430: CupertinoIcons.lab_flask, + 0xf431: CupertinoIcons.lab_flask_solid, + 0xf434: CupertinoIcons.folder, + 0xf435: CupertinoIcons.folder_solid, + 0xf43a: CupertinoIcons.game_controller, + 0xf43b: CupertinoIcons.game_controller_solid, + 0xf43c: CupertinoIcons.gear, + 0xf43d: CupertinoIcons.gear_solid, + 0xf442: CupertinoIcons.heart, + 0xf443: CupertinoIcons.heart_solid, + 0xf447: CupertinoIcons.home, + 0xf449: CupertinoIcons.loop, + 0xf44a: CupertinoIcons.loop_thick, + 0xf44c: CupertinoIcons.info, + 0xf455: CupertinoIcons.placemark, + 0xf456: CupertinoIcons.location_solid, + 0xf45f: CupertinoIcons.mic_off, + 0xf460: CupertinoIcons.mic, + 0xf461: CupertinoIcons.mic_solid, + 0xf463: CupertinoIcons.minus_circled, + 0xf46a: CupertinoIcons.ellipsis, + 0xf46b: CupertinoIcons.music_note, + 0xf46c: CupertinoIcons.double_music_note, + 0xf471: CupertinoIcons.news, + 0xf472: CupertinoIcons.news_solid, + 0xf477: CupertinoIcons.pause, + 0xf478: CupertinoIcons.pause_solid, + 0xf479: CupertinoIcons.paw, + 0xf47a: CupertinoIcons.paw_solid, + 0xf47b: CupertinoIcons.group, + 0xf47c: CupertinoIcons.group_solid, + 0xf47d: CupertinoIcons.person, + 0xf47e: CupertinoIcons.person_solid, + 0xf47f: CupertinoIcons.person_add, + 0xf480: CupertinoIcons.person_add_solid, + 0xf487: CupertinoIcons.play_arrow, + 0xf488: CupertinoIcons.play_arrow_solid, + 0xf489: CupertinoIcons.add, + 0xf48a: CupertinoIcons.plus_circled, + 0xf48b: CupertinoIcons.add_circled_solid, + 0xf48c: CupertinoIcons.tag, + 0xf48d: CupertinoIcons.tag_solid, + 0xf48e: CupertinoIcons.tags, + 0xf48f: CupertinoIcons.tags_solid, + 0xf49a: CupertinoIcons.refresh, + 0xf49b: CupertinoIcons.refresh_circled, + 0xf49c: CupertinoIcons.refresh_circled_solid, + 0xf49d: CupertinoIcons.refresh_thin, + 0xf49e: CupertinoIcons.switch_camera, + 0xf49f: CupertinoIcons.switch_camera_solid, + 0xf4a5: CupertinoIcons.search, + 0xf4a8: CupertinoIcons.shuffle_medium, + 0xf4a9: CupertinoIcons.shuffle, + 0xf4b6: CupertinoIcons.brightness, + 0xf4b7: CupertinoIcons.brightness_solid, + 0xf4b8: CupertinoIcons.phone, + 0xf4b9: CupertinoIcons.phone_solid, + 0xf4be: CupertinoIcons.clock, + 0xf4bf: CupertinoIcons.clock_solid, + 0xf4c4: CupertinoIcons.delete, + 0xf4c5: CupertinoIcons.delete_solid, + 0xf4c6: CupertinoIcons.reply, + 0xf4c8: CupertinoIcons.padlock, + 0xf4c9: CupertinoIcons.padlock_solid, + 0xf4ca: CupertinoIcons.share, + 0xf4cb: CupertinoIcons.share_solid, + 0xf4cc: CupertinoIcons.video_camera, + 0xf4cd: CupertinoIcons.video_camera_solid, + 0xf4d4: CupertinoIcons.airplane, + 0xf4d5: CupertinoIcons.alarm, + 0xf4d6: CupertinoIcons.alarm_fill, + 0xf4d7: CupertinoIcons.alt, + 0xf4d8: CupertinoIcons.ant, + 0xf4d9: CupertinoIcons.ant_circle, + 0xf4da: CupertinoIcons.ant_circle_fill, + 0xf4db: CupertinoIcons.ant_fill, + 0xf4dc: CupertinoIcons.antenna_radiowaves_left_right, + 0xf4dd: CupertinoIcons.app, + 0xf4de: CupertinoIcons.app_badge, + 0xf4df: CupertinoIcons.app_badge_fill, + 0xf4e0: CupertinoIcons.app_fill, + 0xf4e1: CupertinoIcons.archivebox, + 0xf4e2: CupertinoIcons.archivebox_fill, + 0xf4e3: CupertinoIcons.arrow_2_circlepath, + 0xf4e4: CupertinoIcons.arrow_2_circlepath_circle, + 0xf4e5: CupertinoIcons.arrow_2_circlepath_circle_fill, + 0xf4e6: CupertinoIcons.arrow_2_squarepath, + 0xf4e7: CupertinoIcons.arrow_3_trianglepath, + 0xf4e8: CupertinoIcons.arrow_branch, + 0xf4e9: CupertinoIcons.arrow_counterclockwise_circle, + 0xf4ea: CupertinoIcons.arrow_counterclockwise_circle_fill, + 0xf4eb: CupertinoIcons.arrow_down_circle, + 0xf4ec: CupertinoIcons.arrow_down_circle_fill, + 0xf4ed: CupertinoIcons.arrow_down_doc, + 0xf4ee: CupertinoIcons.arrow_down_doc_fill, + 0xf4ef: CupertinoIcons.arrow_down_left, + 0xf4f0: CupertinoIcons.arrow_down_left_circle, + 0xf4f1: CupertinoIcons.arrow_down_left_circle_fill, + 0xf4f2: CupertinoIcons.arrow_down_left_square, + 0xf4f3: CupertinoIcons.arrow_down_left_square_fill, + 0xf4f4: CupertinoIcons.arrow_down_right, + 0xf4f5: CupertinoIcons.arrow_down_right_circle, + 0xf4f6: CupertinoIcons.arrow_down_right_circle_fill, + 0xf4f7: CupertinoIcons.arrow_down_right_square, + 0xf4f8: CupertinoIcons.arrow_down_right_square_fill, + 0xf4f9: CupertinoIcons.arrow_down_square, + 0xf4fa: CupertinoIcons.arrow_down_square_fill, + 0xf4fb: CupertinoIcons.arrow_down_to_line, + 0xf4fc: CupertinoIcons.arrow_down_to_line_alt, + 0xf4fd: CupertinoIcons.arrow_left, + 0xf4fe: CupertinoIcons.arrow_left_circle, + 0xf4ff: CupertinoIcons.arrow_left_circle_fill, + 0xf500: CupertinoIcons.arrow_left_right, + 0xf501: CupertinoIcons.arrow_left_right_circle, + 0xf502: CupertinoIcons.arrow_left_right_circle_fill, + 0xf503: CupertinoIcons.arrow_left_right_square, + 0xf504: CupertinoIcons.arrow_left_right_square_fill, + 0xf505: CupertinoIcons.arrow_left_square, + 0xf506: CupertinoIcons.arrow_left_square_fill, + 0xf507: CupertinoIcons.arrow_left_to_line, + 0xf508: CupertinoIcons.arrow_left_to_line_alt, + 0xf509: CupertinoIcons.arrow_merge, + 0xf50a: CupertinoIcons.arrow_right, + 0xf50b: CupertinoIcons.arrow_right_arrow_left, + 0xf50c: CupertinoIcons.arrow_right_arrow_left_circle, + 0xf50d: CupertinoIcons.arrow_right_arrow_left_circle_fill, + 0xf50e: CupertinoIcons.arrow_right_arrow_left_square, + 0xf50f: CupertinoIcons.arrow_right_arrow_left_square_fill, + 0xf510: CupertinoIcons.arrow_right_circle, + 0xf511: CupertinoIcons.arrow_right_circle_fill, + 0xf512: CupertinoIcons.arrow_right_square, + 0xf513: CupertinoIcons.arrow_right_square_fill, + 0xf514: CupertinoIcons.arrow_right_to_line, + 0xf515: CupertinoIcons.arrow_right_to_line_alt, + 0xf516: CupertinoIcons.arrow_swap, + 0xf517: CupertinoIcons.arrow_turn_down_left, + 0xf518: CupertinoIcons.arrow_turn_down_right, + 0xf519: CupertinoIcons.arrow_turn_left_down, + 0xf51a: CupertinoIcons.arrow_turn_left_up, + 0xf51b: CupertinoIcons.arrow_turn_right_down, + 0xf51c: CupertinoIcons.arrow_turn_right_up, + 0xf51d: CupertinoIcons.arrow_turn_up_left, + 0xf51e: CupertinoIcons.arrow_turn_up_right, + 0xf51f: CupertinoIcons.arrow_up_arrow_down, + 0xf520: CupertinoIcons.arrow_up_arrow_down_circle, + 0xf521: CupertinoIcons.arrow_up_arrow_down_circle_fill, + 0xf522: CupertinoIcons.arrow_up_arrow_down_square, + 0xf523: CupertinoIcons.arrow_up_arrow_down_square_fill, + 0xf524: CupertinoIcons.arrow_up_bin, + 0xf525: CupertinoIcons.arrow_up_bin_fill, + 0xf526: CupertinoIcons.arrow_up_circle, + 0xf527: CupertinoIcons.arrow_up_circle_fill, + 0xf528: CupertinoIcons.arrow_up_doc, + 0xf529: CupertinoIcons.arrow_up_doc_fill, + 0xf52a: CupertinoIcons.arrow_up_down, + 0xf52b: CupertinoIcons.arrow_up_down_circle, + 0xf52c: CupertinoIcons.arrow_up_down_circle_fill, + 0xf52d: CupertinoIcons.arrow_up_down_square, + 0xf52e: CupertinoIcons.arrow_up_down_square_fill, + 0xf52f: CupertinoIcons.arrow_up_left, + 0xf530: CupertinoIcons.arrow_up_left_circle, + 0xf531: CupertinoIcons.arrow_up_left_circle_fill, + 0xf532: CupertinoIcons.arrow_up_left_square, + 0xf533: CupertinoIcons.arrow_up_left_square_fill, + 0xf534: CupertinoIcons.arrow_up_right, + 0xf535: CupertinoIcons.arrow_up_right_circle, + 0xf536: CupertinoIcons.arrow_up_right_circle_fill, + 0xf537: CupertinoIcons.arrow_up_right_diamond, + 0xf538: CupertinoIcons.arrow_up_right_diamond_fill, + 0xf539: CupertinoIcons.arrow_up_right_square, + 0xf53a: CupertinoIcons.arrow_up_right_square_fill, + 0xf53b: CupertinoIcons.arrow_up_square, + 0xf53c: CupertinoIcons.arrow_up_square_fill, + 0xf53d: CupertinoIcons.arrow_up_to_line, + 0xf53e: CupertinoIcons.arrow_up_to_line_alt, + 0xf53f: CupertinoIcons.arrow_uturn_down, + 0xf540: CupertinoIcons.arrow_uturn_down_circle, + 0xf541: CupertinoIcons.arrow_uturn_down_circle_fill, + 0xf542: CupertinoIcons.arrow_uturn_down_square, + 0xf543: CupertinoIcons.arrow_uturn_down_square_fill, + 0xf544: CupertinoIcons.arrow_uturn_left, + 0xf545: CupertinoIcons.arrow_uturn_left_circle, + 0xf546: CupertinoIcons.arrow_uturn_left_circle_fill, + 0xf547: CupertinoIcons.arrow_uturn_left_square, + 0xf548: CupertinoIcons.arrow_uturn_left_square_fill, + 0xf549: CupertinoIcons.arrow_uturn_right, + 0xf54a: CupertinoIcons.arrow_uturn_right_circle, + 0xf54b: CupertinoIcons.arrow_uturn_right_circle_fill, + 0xf54c: CupertinoIcons.arrow_uturn_right_square, + 0xf54d: CupertinoIcons.arrow_uturn_right_square_fill, + 0xf54e: CupertinoIcons.arrow_uturn_up, + 0xf54f: CupertinoIcons.arrow_uturn_up_circle, + 0xf550: CupertinoIcons.arrow_uturn_up_circle_fill, + 0xf551: CupertinoIcons.arrow_uturn_up_square, + 0xf552: CupertinoIcons.arrow_uturn_up_square_fill, + 0xf553: CupertinoIcons.arrowshape_turn_up_left_circle, + 0xf554: CupertinoIcons.arrowshape_turn_up_left_circle_fill, + 0xf555: CupertinoIcons.arrowshape_turn_up_left_fill, + 0xf556: CupertinoIcons.arrowshape_turn_up_right, + 0xf557: CupertinoIcons.arrowshape_turn_up_right_circle, + 0xf558: CupertinoIcons.arrowshape_turn_up_right_circle_fill, + 0xf559: CupertinoIcons.arrowshape_turn_up_right_fill, + 0xf55a: CupertinoIcons.arrowtriangle_down, + 0xf55b: CupertinoIcons.arrowtriangle_down_circle, + 0xf55c: CupertinoIcons.arrowtriangle_down_circle_fill, + 0xf55d: CupertinoIcons.arrowtriangle_down_fill, + 0xf55e: CupertinoIcons.arrowtriangle_down_square, + 0xf55f: CupertinoIcons.arrowtriangle_down_square_fill, + 0xf560: CupertinoIcons.arrowtriangle_left, + 0xf561: CupertinoIcons.arrowtriangle_left_circle, + 0xf562: CupertinoIcons.arrowtriangle_left_circle_fill, + 0xf563: CupertinoIcons.arrowtriangle_left_fill, + 0xf564: CupertinoIcons.arrowtriangle_left_square, + 0xf565: CupertinoIcons.arrowtriangle_left_square_fill, + 0xf566: CupertinoIcons.arrowtriangle_right, + 0xf567: CupertinoIcons.arrowtriangle_right_circle, + 0xf568: CupertinoIcons.arrowtriangle_right_circle_fill, + 0xf569: CupertinoIcons.arrowtriangle_right_fill, + 0xf56a: CupertinoIcons.arrowtriangle_right_square, + 0xf56b: CupertinoIcons.arrowtriangle_right_square_fill, + 0xf56c: CupertinoIcons.arrowtriangle_up, + 0xf56d: CupertinoIcons.arrowtriangle_up_circle, + 0xf56e: CupertinoIcons.arrowtriangle_up_circle_fill, + 0xf56f: CupertinoIcons.arrowtriangle_up_fill, + 0xf570: CupertinoIcons.arrowtriangle_up_square, + 0xf571: CupertinoIcons.arrowtriangle_up_square_fill, + 0xf572: CupertinoIcons.asterisk_circle, + 0xf573: CupertinoIcons.asterisk_circle_fill, + 0xf574: CupertinoIcons.at, + 0xf575: CupertinoIcons.at_badge_minus, + 0xf576: CupertinoIcons.at_badge_plus, + 0xf577: CupertinoIcons.backward, + 0xf578: CupertinoIcons.backward_end, + 0xf579: CupertinoIcons.backward_end_alt, + 0xf57a: CupertinoIcons.backward_end_alt_fill, + 0xf57b: CupertinoIcons.backward_end_fill, + 0xf57c: CupertinoIcons.backward_fill, + 0xf57d: CupertinoIcons.badge_plus_radiowaves_right, + 0xf57e: CupertinoIcons.bag, + 0xf57f: CupertinoIcons.bag_badge_minus, + 0xf580: CupertinoIcons.bag_badge_plus, + 0xf581: CupertinoIcons.bag_fill, + 0xf582: CupertinoIcons.bag_fill_badge_minus, + 0xf583: CupertinoIcons.bag_fill_badge_plus, + 0xf584: CupertinoIcons.bandage, + 0xf585: CupertinoIcons.bandage_fill, + 0xf586: CupertinoIcons.barcode, + 0xf587: CupertinoIcons.barcode_viewfinder, + 0xf588: CupertinoIcons.bed_double, + 0xf589: CupertinoIcons.bed_double_fill, + 0xf58a: CupertinoIcons.bell_circle, + 0xf58b: CupertinoIcons.bell_circle_fill, + 0xf58c: CupertinoIcons.bell_slash, + 0xf58d: CupertinoIcons.bell_slash_fill, + 0xf58e: CupertinoIcons.bin_xmark, + 0xf58f: CupertinoIcons.bin_xmark_fill, + 0xf590: CupertinoIcons.bold, + 0xf591: CupertinoIcons.bold_italic_underline, + 0xf592: CupertinoIcons.bold_underline, + 0xf593: CupertinoIcons.bolt, + 0xf594: CupertinoIcons.bolt_badge_a, + 0xf595: CupertinoIcons.bolt_badge_a_fill, + 0xf596: CupertinoIcons.bolt_circle, + 0xf597: CupertinoIcons.bolt_circle_fill, + 0xf598: CupertinoIcons.bolt_fill, + 0xf599: CupertinoIcons.bolt_horizontal, + 0xf59a: CupertinoIcons.bolt_horizontal_circle, + 0xf59b: CupertinoIcons.bolt_horizontal_circle_fill, + 0xf59c: CupertinoIcons.bolt_horizontal_fill, + 0xf59d: CupertinoIcons.bolt_slash, + 0xf59e: CupertinoIcons.bolt_slash_fill, + 0xf59f: CupertinoIcons.book_circle, + 0xf5a0: CupertinoIcons.book_circle_fill, + 0xf5a1: CupertinoIcons.briefcase, + 0xf5a2: CupertinoIcons.briefcase_fill, + 0xf5a3: CupertinoIcons.bubble_left, + 0xf5a4: CupertinoIcons.bubble_left_bubble_right, + 0xf5a5: CupertinoIcons.bubble_left_bubble_right_fill, + 0xf5a6: CupertinoIcons.bubble_left_fill, + 0xf5a7: CupertinoIcons.bubble_middle_bottom, + 0xf5a8: CupertinoIcons.bubble_middle_bottom_fill, + 0xf5a9: CupertinoIcons.bubble_middle_top, + 0xf5aa: CupertinoIcons.bubble_middle_top_fill, + 0xf5ab: CupertinoIcons.bubble_right, + 0xf5ac: CupertinoIcons.bubble_right_fill, + 0xf5ad: CupertinoIcons.burn, + 0xf5ae: CupertinoIcons.burst, + 0xf5af: CupertinoIcons.burst_fill, + 0xf5b0: CupertinoIcons.calendar, + 0xf5b1: CupertinoIcons.calendar_badge_minus, + 0xf5b2: CupertinoIcons.calendar_badge_plus, + 0xf5b3: CupertinoIcons.calendar_circle, + 0xf5b4: CupertinoIcons.calendar_circle_fill, + 0xf5b5: CupertinoIcons.camera_circle, + 0xf5b6: CupertinoIcons.camera_circle_fill, + 0xf5b7: CupertinoIcons.camera_on_rectangle, + 0xf5b8: CupertinoIcons.camera_on_rectangle_fill, + 0xf5b9: CupertinoIcons.camera_viewfinder, + 0xf5ba: CupertinoIcons.capslock, + 0xf5bb: CupertinoIcons.capslock_fill, + 0xf5bc: CupertinoIcons.capsule, + 0xf5bd: CupertinoIcons.capsule_fill, + 0xf5be: CupertinoIcons.captions_bubble, + 0xf5bf: CupertinoIcons.captions_bubble_fill, + 0xf5c0: CupertinoIcons.cart_badge_minus, + 0xf5c1: CupertinoIcons.cart_badge_plus, + 0xf5c2: CupertinoIcons.cart_fill, + 0xf5c3: CupertinoIcons.cart_fill_badge_minus, + 0xf5c4: CupertinoIcons.cart_fill_badge_plus, + 0xf5c5: CupertinoIcons.chart_bar, + 0xf5c6: CupertinoIcons.chart_bar_fill, + 0xf5c7: CupertinoIcons.chart_pie, + 0xf5c8: CupertinoIcons.chart_pie_fill, + 0xf5c9: CupertinoIcons.checkmark_rectangle, + 0xf5ca: CupertinoIcons.checkmark_rectangle_fill, + 0xf5cb: CupertinoIcons.checkmark_seal, + 0xf5cc: CupertinoIcons.checkmark_seal_fill, + 0xf5cd: CupertinoIcons.checkmark_shield, + 0xf5ce: CupertinoIcons.checkmark_shield_fill, + 0xf5cf: CupertinoIcons.checkmark_square, + 0xf5d0: CupertinoIcons.checkmark_square_fill, + 0xf5d1: CupertinoIcons.chevron_compact_down, + 0xf5d2: CupertinoIcons.chevron_compact_left, + 0xf5d3: CupertinoIcons.chevron_compact_right, + 0xf5d4: CupertinoIcons.chevron_compact_up, + 0xf5d5: CupertinoIcons.chevron_down, + 0xf5d6: CupertinoIcons.chevron_down_circle, + 0xf5d7: CupertinoIcons.chevron_down_circle_fill, + 0xf5d8: CupertinoIcons.chevron_down_square, + 0xf5d9: CupertinoIcons.chevron_down_square_fill, + 0xf5da: CupertinoIcons.chevron_left_2, + 0xf5db: CupertinoIcons.chevron_left_circle, + 0xf5dc: CupertinoIcons.chevron_left_circle_fill, + 0xf5dd: CupertinoIcons.chevron_left_slash_chevron_right, + 0xf5de: CupertinoIcons.chevron_left_square, + 0xf5df: CupertinoIcons.chevron_left_square_fill, + 0xf5e0: CupertinoIcons.chevron_right_2, + 0xf5e1: CupertinoIcons.chevron_right_circle, + 0xf5e2: CupertinoIcons.chevron_right_circle_fill, + 0xf5e3: CupertinoIcons.chevron_right_square, + 0xf5e4: CupertinoIcons.chevron_right_square_fill, + 0xf5e5: CupertinoIcons.chevron_up, + 0xf5e6: CupertinoIcons.chevron_up_chevron_down, + 0xf5e7: CupertinoIcons.chevron_up_circle, + 0xf5e8: CupertinoIcons.chevron_up_circle_fill, + 0xf5e9: CupertinoIcons.chevron_up_square, + 0xf5ea: CupertinoIcons.chevron_up_square_fill, + 0xf5eb: CupertinoIcons.circle_bottomthird_split, + 0xf5ec: CupertinoIcons.circle_grid_3x3, + 0xf5ed: CupertinoIcons.circle_grid_3x3_fill, + 0xf5ee: CupertinoIcons.circle_grid_hex, + 0xf5ef: CupertinoIcons.circle_grid_hex_fill, + 0xf5f0: CupertinoIcons.circle_lefthalf_fill, + 0xf5f1: CupertinoIcons.circle_righthalf_fill, + 0xf5f3: CupertinoIcons.clear_fill, + 0xf5f4: CupertinoIcons.cloud, + 0xf5f5: CupertinoIcons.cloud_bolt, + 0xf5f6: CupertinoIcons.cloud_bolt_fill, + 0xf5f7: CupertinoIcons.cloud_bolt_rain, + 0xf5f8: CupertinoIcons.cloud_bolt_rain_fill, + 0xf5f9: CupertinoIcons.cloud_drizzle, + 0xf5fa: CupertinoIcons.cloud_drizzle_fill, + 0xf5fb: CupertinoIcons.cloud_fill, + 0xf5fc: CupertinoIcons.cloud_fog, + 0xf5fd: CupertinoIcons.cloud_fog_fill, + 0xf5fe: CupertinoIcons.cloud_hail, + 0xf5ff: CupertinoIcons.cloud_hail_fill, + 0xf600: CupertinoIcons.cloud_heavyrain, + 0xf601: CupertinoIcons.cloud_heavyrain_fill, + 0xf602: CupertinoIcons.cloud_moon, + 0xf603: CupertinoIcons.cloud_moon_bolt, + 0xf604: CupertinoIcons.cloud_moon_bolt_fill, + 0xf605: CupertinoIcons.cloud_moon_fill, + 0xf606: CupertinoIcons.cloud_moon_rain, + 0xf607: CupertinoIcons.cloud_moon_rain_fill, + 0xf608: CupertinoIcons.cloud_rain, + 0xf609: CupertinoIcons.cloud_rain_fill, + 0xf60a: CupertinoIcons.cloud_sleet, + 0xf60b: CupertinoIcons.cloud_sleet_fill, + 0xf60c: CupertinoIcons.cloud_snow, + 0xf60d: CupertinoIcons.cloud_snow_fill, + 0xf60e: CupertinoIcons.cloud_sun, + 0xf60f: CupertinoIcons.cloud_sun_bolt, + 0xf610: CupertinoIcons.cloud_sun_bolt_fill, + 0xf611: CupertinoIcons.cloud_sun_fill, + 0xf612: CupertinoIcons.cloud_sun_rain, + 0xf613: CupertinoIcons.cloud_sun_rain_fill, + 0xf614: CupertinoIcons.command, + 0xf615: CupertinoIcons.control, + 0xf616: CupertinoIcons.creditcard, + 0xf617: CupertinoIcons.creditcard_fill, + 0xf618: CupertinoIcons.crop, + 0xf619: CupertinoIcons.crop_rotate, + 0xf61a: CupertinoIcons.cube, + 0xf61b: CupertinoIcons.cube_box, + 0xf61c: CupertinoIcons.cube_box_fill, + 0xf61d: CupertinoIcons.cube_fill, + 0xf61e: CupertinoIcons.cursor_rays, + 0xf61f: CupertinoIcons.decrease_indent, + 0xf620: CupertinoIcons.decrease_quotelevel, + 0xf621: CupertinoIcons.delete_left, + 0xf622: CupertinoIcons.delete_left_fill, + 0xf623: CupertinoIcons.delete_right, + 0xf624: CupertinoIcons.delete_right_fill, + 0xf625: CupertinoIcons.desktopcomputer, + 0xf626: CupertinoIcons.dial, + 0xf627: CupertinoIcons.dial_fill, + 0xf628: CupertinoIcons.divide, + 0xf629: CupertinoIcons.divide_circle, + 0xf62a: CupertinoIcons.divide_circle_fill, + 0xf62b: CupertinoIcons.divide_square, + 0xf62c: CupertinoIcons.divide_square_fill, + 0xf62d: CupertinoIcons.doc, + 0xf62e: CupertinoIcons.doc_append, + 0xf62f: CupertinoIcons.doc_circle, + 0xf630: CupertinoIcons.doc_circle_fill, + 0xf631: CupertinoIcons.doc_fill, + 0xf632: CupertinoIcons.doc_on_clipboard, + 0xf633: CupertinoIcons.doc_on_clipboard_fill, + 0xf634: CupertinoIcons.doc_on_doc, + 0xf635: CupertinoIcons.doc_on_doc_fill, + 0xf636: CupertinoIcons.doc_plaintext, + 0xf637: CupertinoIcons.doc_richtext, + 0xf638: CupertinoIcons.doc_text, + 0xf639: CupertinoIcons.doc_text_fill, + 0xf63a: CupertinoIcons.doc_text_search, + 0xf63b: CupertinoIcons.doc_text_viewfinder, + 0xf63c: CupertinoIcons.dot_radiowaves_left_right, + 0xf63d: CupertinoIcons.dot_radiowaves_right, + 0xf63e: CupertinoIcons.dot_square, + 0xf63f: CupertinoIcons.dot_square_fill, + 0xf640: CupertinoIcons.drop_triangle, + 0xf641: CupertinoIcons.drop_triangle_fill, + 0xf642: CupertinoIcons.ear, + 0xf643: CupertinoIcons.eject, + 0xf644: CupertinoIcons.eject_fill, + 0xf645: CupertinoIcons.ellipses_bubble, + 0xf646: CupertinoIcons.ellipses_bubble_fill, + 0xf647: CupertinoIcons.ellipsis_circle, + 0xf648: CupertinoIcons.ellipsis_circle_fill, + 0xf649: CupertinoIcons.envelope_badge, + 0xf64a: CupertinoIcons.envelope_badge_fill, + 0xf64b: CupertinoIcons.envelope_circle, + 0xf64c: CupertinoIcons.envelope_circle_fill, + 0xf64d: CupertinoIcons.envelope_open, + 0xf64e: CupertinoIcons.envelope_open_fill, + 0xf64f: CupertinoIcons.equal, + 0xf650: CupertinoIcons.equal_circle, + 0xf651: CupertinoIcons.equal_circle_fill, + 0xf652: CupertinoIcons.equal_square, + 0xf653: CupertinoIcons.equal_square_fill, + 0xf654: CupertinoIcons.escape, + 0xf655: CupertinoIcons.exclamationmark, + 0xf656: CupertinoIcons.exclamationmark_bubble, + 0xf657: CupertinoIcons.exclamationmark_bubble_fill, + 0xf658: CupertinoIcons.exclamationmark_circle, + 0xf659: CupertinoIcons.exclamationmark_circle_fill, + 0xf65a: CupertinoIcons.exclamationmark_octagon, + 0xf65b: CupertinoIcons.exclamationmark_octagon_fill, + 0xf65c: CupertinoIcons.exclamationmark_shield, + 0xf65d: CupertinoIcons.exclamationmark_shield_fill, + 0xf65e: CupertinoIcons.exclamationmark_square, + 0xf65f: CupertinoIcons.exclamationmark_square_fill, + 0xf660: CupertinoIcons.exclamationmark_triangle, + 0xf661: CupertinoIcons.exclamationmark_triangle_fill, + 0xf662: CupertinoIcons.eye_slash, + 0xf663: CupertinoIcons.eye_slash_fill, + 0xf664: CupertinoIcons.eyedropper, + 0xf665: CupertinoIcons.eyedropper_full, + 0xf666: CupertinoIcons.eyedropper_halffull, + 0xf667: CupertinoIcons.eyeglasses, + 0xf668: CupertinoIcons.f_cursive, + 0xf669: CupertinoIcons.f_cursive_circle, + 0xf66a: CupertinoIcons.f_cursive_circle_fill, + 0xf66b: CupertinoIcons.film, + 0xf66c: CupertinoIcons.film_fill, + 0xf66d: CupertinoIcons.flag_circle, + 0xf66e: CupertinoIcons.flag_circle_fill, + 0xf66f: CupertinoIcons.flag_fill, + 0xf670: CupertinoIcons.flag_slash, + 0xf671: CupertinoIcons.flag_slash_fill, + 0xf672: CupertinoIcons.flame, + 0xf673: CupertinoIcons.flame_fill, + 0xf674: CupertinoIcons.flowchart, + 0xf675: CupertinoIcons.flowchart_fill, + 0xf676: CupertinoIcons.folder_badge_minus, + 0xf677: CupertinoIcons.folder_badge_person_crop, + 0xf678: CupertinoIcons.folder_badge_plus, + 0xf679: CupertinoIcons.folder_circle, + 0xf67a: CupertinoIcons.folder_circle_fill, + 0xf67b: CupertinoIcons.folder_fill_badge_minus, + 0xf67c: CupertinoIcons.folder_fill_badge_person_crop, + 0xf67d: CupertinoIcons.folder_fill_badge_plus, + 0xf67f: CupertinoIcons.forward_end, + 0xf680: CupertinoIcons.forward_end_alt, + 0xf681: CupertinoIcons.forward_end_alt_fill, + 0xf682: CupertinoIcons.forward_end_fill, + 0xf683: CupertinoIcons.forward_fill, + 0xf684: CupertinoIcons.function, + 0xf685: CupertinoIcons.fx, + 0xf686: CupertinoIcons.gauge, + 0xf687: CupertinoIcons.gauge_badge_minus, + 0xf688: CupertinoIcons.gauge_badge_plus, + 0xf689: CupertinoIcons.gift, + 0xf68a: CupertinoIcons.gift_alt, + 0xf68b: CupertinoIcons.gift_alt_fill, + 0xf68c: CupertinoIcons.gift_fill, + 0xf68d: CupertinoIcons.globe, + 0xf68e: CupertinoIcons.gobackward, + 0xf68f: CupertinoIcons.gobackward_10, + 0xf690: CupertinoIcons.gobackward_15, + 0xf691: CupertinoIcons.gobackward_30, + 0xf692: CupertinoIcons.gobackward_45, + 0xf693: CupertinoIcons.gobackward_60, + 0xf694: CupertinoIcons.gobackward_75, + 0xf695: CupertinoIcons.gobackward_90, + 0xf696: CupertinoIcons.gobackward_minus, + 0xf697: CupertinoIcons.goforward, + 0xf698: CupertinoIcons.goforward_10, + 0xf699: CupertinoIcons.goforward_15, + 0xf69a: CupertinoIcons.goforward_30, + 0xf69b: CupertinoIcons.goforward_45, + 0xf69c: CupertinoIcons.goforward_60, + 0xf69d: CupertinoIcons.goforward_75, + 0xf69e: CupertinoIcons.goforward_90, + 0xf69f: CupertinoIcons.goforward_plus, + 0xf6a0: CupertinoIcons.greaterthan, + 0xf6a1: CupertinoIcons.greaterthan_circle, + 0xf6a2: CupertinoIcons.greaterthan_circle_fill, + 0xf6a3: CupertinoIcons.greaterthan_square, + 0xf6a4: CupertinoIcons.greaterthan_square_fill, + 0xf6a5: CupertinoIcons.grid, + 0xf6a6: CupertinoIcons.grid_circle, + 0xf6a7: CupertinoIcons.grid_circle_fill, + 0xf6a8: CupertinoIcons.guitars, + 0xf6a9: CupertinoIcons.hammer, + 0xf6aa: CupertinoIcons.hammer_fill, + 0xf6ab: CupertinoIcons.hand_draw, + 0xf6ac: CupertinoIcons.hand_draw_fill, + 0xf6ad: CupertinoIcons.hand_point_left, + 0xf6ae: CupertinoIcons.hand_point_left_fill, + 0xf6af: CupertinoIcons.hand_point_right, + 0xf6b0: CupertinoIcons.hand_point_right_fill, + 0xf6b1: CupertinoIcons.hand_raised, + 0xf6b2: CupertinoIcons.hand_raised_fill, + 0xf6b3: CupertinoIcons.hand_raised_slash, + 0xf6b4: CupertinoIcons.hand_raised_slash_fill, + 0xf6b5: CupertinoIcons.hand_thumbsdown, + 0xf6b6: CupertinoIcons.hand_thumbsdown_fill, + 0xf6b7: CupertinoIcons.hand_thumbsup, + 0xf6b8: CupertinoIcons.hand_thumbsup_fill, + 0xf6b9: CupertinoIcons.hare, + 0xf6ba: CupertinoIcons.hare_fill, + 0xf6bb: CupertinoIcons.headphones, + 0xf6bc: CupertinoIcons.heart_circle, + 0xf6bd: CupertinoIcons.heart_circle_fill, + 0xf6be: CupertinoIcons.heart_slash, + 0xf6bf: CupertinoIcons.heart_slash_circle, + 0xf6c0: CupertinoIcons.heart_slash_circle_fill, + 0xf6c1: CupertinoIcons.heart_slash_fill, + 0xf6c2: CupertinoIcons.helm, + 0xf6c3: CupertinoIcons.hexagon, + 0xf6c4: CupertinoIcons.hexagon_fill, + 0xf6c5: CupertinoIcons.hifispeaker, + 0xf6c6: CupertinoIcons.hifispeaker_fill, + 0xf6c7: CupertinoIcons.hourglass, + 0xf6c8: CupertinoIcons.hourglass_bottomhalf_fill, + 0xf6c9: CupertinoIcons.hourglass_tophalf_fill, + 0xf6ca: CupertinoIcons.house_fill, + 0xf6cb: CupertinoIcons.hurricane, + 0xf6cc: CupertinoIcons.increase_indent, + 0xf6cd: CupertinoIcons.increase_quotelevel, + 0xf6cf: CupertinoIcons.info_circle_fill, + 0xf6d0: CupertinoIcons.italic, + 0xf6d1: CupertinoIcons.keyboard, + 0xf6d2: CupertinoIcons.keyboard_chevron_compact_down, + 0xf6d3: CupertinoIcons.largecircle_fill_circle, + 0xf6d4: CupertinoIcons.lasso, + 0xf6d5: CupertinoIcons.leaf_arrow_circlepath, + 0xf6d6: CupertinoIcons.lessthan, + 0xf6d7: CupertinoIcons.lessthan_circle, + 0xf6d8: CupertinoIcons.lessthan_circle_fill, + 0xf6d9: CupertinoIcons.lessthan_square, + 0xf6da: CupertinoIcons.lessthan_square_fill, + 0xf6db: CupertinoIcons.light_max, + 0xf6dc: CupertinoIcons.light_min, + 0xf6dd: CupertinoIcons.lightbulb, + 0xf6de: CupertinoIcons.lightbulb_fill, + 0xf6df: CupertinoIcons.lightbulb_slash, + 0xf6e0: CupertinoIcons.lightbulb_slash_fill, + 0xf6e1: CupertinoIcons.line_horizontal_3, + 0xf6e2: CupertinoIcons.line_horizontal_3_decrease, + 0xf6e3: CupertinoIcons.line_horizontal_3_decrease_circle, + 0xf6e4: CupertinoIcons.line_horizontal_3_decrease_circle_fill, + 0xf6e5: CupertinoIcons.link, + 0xf6e6: CupertinoIcons.link_circle, + 0xf6e7: CupertinoIcons.link_circle_fill, + 0xf6e8: CupertinoIcons.list_bullet, + 0xf6e9: CupertinoIcons.list_bullet_below_rectangle, + 0xf6ea: CupertinoIcons.list_bullet_indent, + 0xf6eb: CupertinoIcons.list_dash, + 0xf6ec: CupertinoIcons.list_number, + 0xf6ed: CupertinoIcons.list_number_rtl, + 0xf6ee: CupertinoIcons.location, + 0xf6ef: CupertinoIcons.location_circle, + 0xf6f0: CupertinoIcons.location_circle_fill, + 0xf6f1: CupertinoIcons.location_fill, + 0xf6f2: CupertinoIcons.location_north, + 0xf6f3: CupertinoIcons.location_north_fill, + 0xf6f4: CupertinoIcons.location_north_line, + 0xf6f5: CupertinoIcons.location_north_line_fill, + 0xf6f6: CupertinoIcons.location_slash, + 0xf6f7: CupertinoIcons.location_slash_fill, + 0xf6f8: CupertinoIcons.lock_circle, + 0xf6f9: CupertinoIcons.lock_circle_fill, + 0xf6fa: CupertinoIcons.lock_open, + 0xf6fb: CupertinoIcons.lock_open_fill, + 0xf6fc: CupertinoIcons.lock_rotation, + 0xf6fd: CupertinoIcons.lock_rotation_open, + 0xf6fe: CupertinoIcons.lock_shield, + 0xf6ff: CupertinoIcons.lock_shield_fill, + 0xf700: CupertinoIcons.lock_slash, + 0xf701: CupertinoIcons.lock_slash_fill, + 0xf702: CupertinoIcons.macwindow, + 0xf703: CupertinoIcons.map, + 0xf704: CupertinoIcons.map_fill, + 0xf705: CupertinoIcons.map_pin, + 0xf706: CupertinoIcons.map_pin_ellipse, + 0xf707: CupertinoIcons.map_pin_slash, + 0xf708: CupertinoIcons.memories, + 0xf709: CupertinoIcons.memories_badge_minus, + 0xf70a: CupertinoIcons.memories_badge_plus, + 0xf70b: CupertinoIcons.metronome, + 0xf70c: CupertinoIcons.mic_circle, + 0xf70d: CupertinoIcons.mic_circle_fill, + 0xf70e: CupertinoIcons.mic_slash_fill, + 0xf70f: CupertinoIcons.minus, + 0xf710: CupertinoIcons.minus_circle_fill, + 0xf711: CupertinoIcons.minus_rectangle, + 0xf712: CupertinoIcons.minus_rectangle_fill, + 0xf713: CupertinoIcons.minus_slash_plus, + 0xf714: CupertinoIcons.minus_square, + 0xf715: CupertinoIcons.minus_square_fill, + 0xf716: CupertinoIcons.moon, + 0xf717: CupertinoIcons.moon_circle, + 0xf718: CupertinoIcons.moon_circle_fill, + 0xf719: CupertinoIcons.moon_fill, + 0xf71a: CupertinoIcons.moon_stars, + 0xf71b: CupertinoIcons.moon_stars_fill, + 0xf71c: CupertinoIcons.moon_zzz, + 0xf71d: CupertinoIcons.moon_zzz_fill, + 0xf71e: CupertinoIcons.multiply, + 0xf71f: CupertinoIcons.multiply_circle, + 0xf720: CupertinoIcons.multiply_circle_fill, + 0xf721: CupertinoIcons.multiply_square, + 0xf722: CupertinoIcons.multiply_square_fill, + 0xf723: CupertinoIcons.music_house, + 0xf724: CupertinoIcons.music_house_fill, + 0xf725: CupertinoIcons.music_mic, + 0xf726: CupertinoIcons.music_note_list, + 0xf727: CupertinoIcons.nosign, + 0xf728: CupertinoIcons.number, + 0xf729: CupertinoIcons.number_circle, + 0xf72a: CupertinoIcons.number_circle_fill, + 0xf72b: CupertinoIcons.number_square, + 0xf72c: CupertinoIcons.number_square_fill, + 0xf72d: CupertinoIcons.option, + 0xf72e: CupertinoIcons.paintbrush, + 0xf72f: CupertinoIcons.paintbrush_fill, + 0xf730: CupertinoIcons.pano, + 0xf731: CupertinoIcons.pano_fill, + 0xf732: CupertinoIcons.paperclip, + 0xf733: CupertinoIcons.paperplane, + 0xf734: CupertinoIcons.paperplane_fill, + 0xf735: CupertinoIcons.paragraph, + 0xf736: CupertinoIcons.pause_circle, + 0xf737: CupertinoIcons.pause_circle_fill, + 0xf738: CupertinoIcons.pause_rectangle, + 0xf739: CupertinoIcons.pause_rectangle_fill, + 0xf73a: CupertinoIcons.pencil_circle, + 0xf73b: CupertinoIcons.pencil_circle_fill, + 0xf73c: CupertinoIcons.pencil_ellipsis_rectangle, + 0xf73d: CupertinoIcons.pencil_outline, + 0xf73e: CupertinoIcons.pencil_slash, + 0xf73f: CupertinoIcons.percent, + 0xf740: CupertinoIcons.person_2, + 0xf741: CupertinoIcons.person_2_fill, + 0xf742: CupertinoIcons.person_2_square_stack, + 0xf743: CupertinoIcons.person_2_square_stack_fill, + 0xf744: CupertinoIcons.person_badge_minus, + 0xf745: CupertinoIcons.person_badge_minus_fill, + 0xf746: CupertinoIcons.person_circle, + 0xf747: CupertinoIcons.person_circle_fill, + 0xf748: CupertinoIcons.person_crop_circle_badge_checkmark, + 0xf749: CupertinoIcons.person_crop_circle_badge_exclam, + 0xf74a: CupertinoIcons.person_crop_circle_badge_minus, + 0xf74b: CupertinoIcons.person_crop_circle_badge_plus, + 0xf74c: CupertinoIcons.person_crop_circle_badge_xmark, + 0xf74d: CupertinoIcons.person_crop_circle_fill, + 0xf74e: CupertinoIcons.person_crop_circle_fill_badge_checkmark, + 0xf74f: CupertinoIcons.person_crop_circle_fill_badge_exclam, + 0xf750: CupertinoIcons.person_crop_circle_fill_badge_minus, + 0xf751: CupertinoIcons.person_crop_circle_fill_badge_plus, + 0xf752: CupertinoIcons.person_crop_circle_fill_badge_xmark, + 0xf753: CupertinoIcons.person_crop_rectangle, + 0xf754: CupertinoIcons.person_crop_rectangle_fill, + 0xf755: CupertinoIcons.person_crop_square, + 0xf756: CupertinoIcons.person_crop_square_fill, + 0xf757: CupertinoIcons.personalhotspot, + 0xf758: CupertinoIcons.perspective, + 0xf759: CupertinoIcons.phone_arrow_down_left, + 0xf75a: CupertinoIcons.phone_arrow_right, + 0xf75b: CupertinoIcons.phone_arrow_up_right, + 0xf75c: CupertinoIcons.phone_badge_plus, + 0xf75d: CupertinoIcons.phone_circle, + 0xf75e: CupertinoIcons.phone_circle_fill, + 0xf75f: CupertinoIcons.phone_down, + 0xf760: CupertinoIcons.phone_down_circle, + 0xf761: CupertinoIcons.phone_down_circle_fill, + 0xf762: CupertinoIcons.phone_down_fill, + 0xf763: CupertinoIcons.phone_fill_arrow_down_left, + 0xf764: CupertinoIcons.phone_fill_arrow_right, + 0xf765: CupertinoIcons.phone_fill_arrow_up_right, + 0xf766: CupertinoIcons.phone_fill_badge_plus, + 0xf767: CupertinoIcons.photo, + 0xf768: CupertinoIcons.photo_fill, + 0xf769: CupertinoIcons.photo_fill_on_rectangle_fill, + 0xf76a: CupertinoIcons.photo_on_rectangle, + 0xf76b: CupertinoIcons.pin, + 0xf76c: CupertinoIcons.pin_fill, + 0xf76d: CupertinoIcons.pin_slash, + 0xf76e: CupertinoIcons.pin_slash_fill, + 0xf76f: CupertinoIcons.play_circle, + 0xf770: CupertinoIcons.play_circle_fill, + 0xf771: CupertinoIcons.play_rectangle, + 0xf772: CupertinoIcons.play_rectangle_fill, + 0xf773: CupertinoIcons.playpause, + 0xf774: CupertinoIcons.playpause_fill, + 0xf775: CupertinoIcons.plus_app, + 0xf776: CupertinoIcons.plus_app_fill, + 0xf777: CupertinoIcons.plus_bubble, + 0xf778: CupertinoIcons.plus_bubble_fill, + 0xf779: CupertinoIcons.plus_rectangle, + 0xf77a: CupertinoIcons.plus_rectangle_fill, + 0xf77b: CupertinoIcons.plus_rectangle_fill_on_rectangle_fill, + 0xf77c: CupertinoIcons.plus_rectangle_on_rectangle, + 0xf77d: CupertinoIcons.plus_slash_minus, + 0xf77e: CupertinoIcons.plus_square, + 0xf77f: CupertinoIcons.plus_square_fill, + 0xf780: CupertinoIcons.plus_square_fill_on_square_fill, + 0xf781: CupertinoIcons.plus_square_on_square, + 0xf782: CupertinoIcons.plusminus, + 0xf783: CupertinoIcons.plusminus_circle, + 0xf784: CupertinoIcons.plusminus_circle_fill, + 0xf785: CupertinoIcons.power, + 0xf786: CupertinoIcons.printer, + 0xf787: CupertinoIcons.printer_fill, + 0xf788: CupertinoIcons.projective, + 0xf789: CupertinoIcons.purchased, + 0xf78a: CupertinoIcons.purchased_circle, + 0xf78b: CupertinoIcons.purchased_circle_fill, + 0xf78c: CupertinoIcons.qrcode, + 0xf78d: CupertinoIcons.qrcode_viewfinder, + 0xf78e: CupertinoIcons.question, + 0xf78f: CupertinoIcons.question_circle, + 0xf790: CupertinoIcons.question_circle_fill, + 0xf791: CupertinoIcons.question_diamond, + 0xf792: CupertinoIcons.question_diamond_fill, + 0xf793: CupertinoIcons.question_square, + 0xf794: CupertinoIcons.question_square_fill, + 0xf795: CupertinoIcons.quote_bubble, + 0xf796: CupertinoIcons.quote_bubble_fill, + 0xf797: CupertinoIcons.radiowaves_left, + 0xf798: CupertinoIcons.radiowaves_right, + 0xf799: CupertinoIcons.rays, + 0xf79a: CupertinoIcons.recordingtape, + 0xf79b: CupertinoIcons.rectangle, + 0xf79c: CupertinoIcons.rectangle_3_offgrid, + 0xf79d: CupertinoIcons.rectangle_3_offgrid_fill, + 0xf79e: CupertinoIcons.rectangle_arrow_up_right_arrow_down_left, + 0xf79f: CupertinoIcons.rectangle_arrow_up_right_arrow_down_left_slash, + 0xf7a0: CupertinoIcons.rectangle_badge_checkmark, + 0xf7a1: CupertinoIcons.rectangle_badge_xmark, + 0xf7a2: CupertinoIcons.rectangle_compress_vertical, + 0xf7a3: CupertinoIcons.rectangle_dock, + 0xf7a4: CupertinoIcons.rectangle_expand_vertical, + 0xf7a5: CupertinoIcons.rectangle_fill, + 0xf7a6: CupertinoIcons.rectangle_fill_badge_checkmark, + 0xf7a7: CupertinoIcons.rectangle_fill_badge_xmark, + 0xf7a8: CupertinoIcons.rectangle_fill_on_rectangle_angled_fill, + 0xf7a9: CupertinoIcons.rectangle_fill_on_rectangle_fill, + 0xf7aa: CupertinoIcons.rectangle_grid_1x2, + 0xf7ab: CupertinoIcons.rectangle_grid_1x2_fill, + 0xf7ac: CupertinoIcons.rectangle_grid_2x2, + 0xf7ad: CupertinoIcons.rectangle_grid_2x2_fill, + 0xf7ae: CupertinoIcons.rectangle_grid_3x2, + 0xf7af: CupertinoIcons.rectangle_grid_3x2_fill, + 0xf7b0: CupertinoIcons.rectangle_on_rectangle, + 0xf7b1: CupertinoIcons.rectangle_on_rectangle_angled, + 0xf7b2: CupertinoIcons.rectangle_paperclip, + 0xf7b3: CupertinoIcons.rectangle_split_3x1, + 0xf7b4: CupertinoIcons.rectangle_split_3x1_fill, + 0xf7b5: CupertinoIcons.rectangle_split_3x3, + 0xf7b6: CupertinoIcons.rectangle_split_3x3_fill, + 0xf7b7: CupertinoIcons.rectangle_stack_badge_minus, + 0xf7b8: CupertinoIcons.rectangle_stack_badge_person_crop, + 0xf7b9: CupertinoIcons.rectangle_stack_badge_plus, + 0xf7ba: CupertinoIcons.rectangle_stack_fill_badge_minus, + 0xf7bb: CupertinoIcons.rectangle_stack_fill_badge_person_crop, + 0xf7bc: CupertinoIcons.rectangle_stack_fill_badge_plus, + 0xf7bd: CupertinoIcons.rectangle_stack_person_crop, + 0xf7be: CupertinoIcons.rectangle_stack_person_crop_fill, + 0xf7bf: CupertinoIcons.repeat, + 0xf7c0: CupertinoIcons.repeat_1, + 0xf7c1: CupertinoIcons.return_icon, + 0xf7c2: CupertinoIcons.rhombus, + 0xf7c3: CupertinoIcons.rhombus_fill, + 0xf7c4: CupertinoIcons.rosette, + 0xf7c5: CupertinoIcons.rotate_left, + 0xf7c6: CupertinoIcons.rotate_left_fill, + 0xf7c7: CupertinoIcons.rotate_right, + 0xf7c8: CupertinoIcons.rotate_right_fill, + 0xf7c9: CupertinoIcons.scissors, + 0xf7ca: CupertinoIcons.scope, + 0xf7cb: CupertinoIcons.scribble, + 0xf7cc: CupertinoIcons.search_circle, + 0xf7cd: CupertinoIcons.search_circle_fill, + 0xf7ce: CupertinoIcons.selection_pin_in_out, + 0xf7cf: CupertinoIcons.shield, + 0xf7d0: CupertinoIcons.shield_fill, + 0xf7d1: CupertinoIcons.shield_lefthalf_fill, + 0xf7d2: CupertinoIcons.shield_slash, + 0xf7d3: CupertinoIcons.shield_slash_fill, + 0xf7d4: CupertinoIcons.shift, + 0xf7d5: CupertinoIcons.shift_fill, + 0xf7d6: CupertinoIcons.sidebar_left, + 0xf7d7: CupertinoIcons.sidebar_right, + 0xf7d8: CupertinoIcons.signature, + 0xf7d9: CupertinoIcons.skew, + 0xf7da: CupertinoIcons.slash_circle, + 0xf7db: CupertinoIcons.slash_circle_fill, + 0xf7dc: CupertinoIcons.slider_horizontal_3, + 0xf7dd: CupertinoIcons.slider_horizontal_below_rectangle, + 0xf7de: CupertinoIcons.slowmo, + 0xf7df: CupertinoIcons.smallcircle_circle, + 0xf7e0: CupertinoIcons.smallcircle_circle_fill, + 0xf7e1: CupertinoIcons.smallcircle_fill_circle, + 0xf7e2: CupertinoIcons.smallcircle_fill_circle_fill, + 0xf7e3: CupertinoIcons.smiley, + 0xf7e4: CupertinoIcons.smiley_fill, + 0xf7e5: CupertinoIcons.smoke, + 0xf7e6: CupertinoIcons.smoke_fill, + 0xf7e7: CupertinoIcons.snow, + 0xf7e8: CupertinoIcons.sparkles, + 0xf7e9: CupertinoIcons.speaker, + 0xf7ea: CupertinoIcons.speaker_1, + 0xf7eb: CupertinoIcons.speaker_2, + 0xf7ec: CupertinoIcons.speaker_2_fill, + 0xf7ed: CupertinoIcons.speaker_3, + 0xf7ee: CupertinoIcons.speaker_slash, + 0xf7ef: CupertinoIcons.speaker_slash_fill_rtl, + 0xf7f0: CupertinoIcons.speaker_slash_rtl, + 0xf7f1: CupertinoIcons.speaker_zzz, + 0xf7f2: CupertinoIcons.speaker_zzz_fill, + 0xf7f3: CupertinoIcons.speaker_zzz_fill_rtl, + 0xf7f4: CupertinoIcons.speaker_zzz_rtl, + 0xf7f5: CupertinoIcons.speedometer, + 0xf7f6: CupertinoIcons.sportscourt, + 0xf7f7: CupertinoIcons.sportscourt_fill, + 0xf7f8: CupertinoIcons.square, + 0xf7f9: CupertinoIcons.square_arrow_down, + 0xf7fa: CupertinoIcons.square_arrow_down_fill, + 0xf7fb: CupertinoIcons.square_arrow_down_on_square, + 0xf7fc: CupertinoIcons.square_arrow_down_on_square_fill, + 0xf7fd: CupertinoIcons.square_arrow_up_on_square, + 0xf7fe: CupertinoIcons.square_arrow_up_on_square_fill, + 0xf7ff: CupertinoIcons.square_fill, + 0xf800: CupertinoIcons.square_fill_line_vertical_square, + 0xf801: CupertinoIcons.square_fill_line_vertical_square_fill, + 0xf802: CupertinoIcons.square_fill_on_circle_fill, + 0xf803: CupertinoIcons.square_fill_on_square_fill, + 0xf804: CupertinoIcons.square_grid_2x2, + 0xf805: CupertinoIcons.square_grid_2x2_fill, + 0xf806: CupertinoIcons.square_grid_3x2, + 0xf807: CupertinoIcons.square_grid_3x2_fill, + 0xf808: CupertinoIcons.square_grid_4x3_fill, + 0xf809: CupertinoIcons.square_lefthalf_fill, + 0xf80a: CupertinoIcons.square_line_vertical_square, + 0xf80b: CupertinoIcons.square_line_vertical_square_fill, + 0xf80c: CupertinoIcons.square_on_circle, + 0xf80d: CupertinoIcons.square_on_square, + 0xf80e: CupertinoIcons.square_righthalf_fill, + 0xf80f: CupertinoIcons.square_split_1x2, + 0xf810: CupertinoIcons.square_split_1x2_fill, + 0xf811: CupertinoIcons.square_split_2x1, + 0xf812: CupertinoIcons.square_split_2x1_fill, + 0xf813: CupertinoIcons.square_split_2x2, + 0xf814: CupertinoIcons.square_split_2x2_fill, + 0xf815: CupertinoIcons.square_stack, + 0xf816: CupertinoIcons.square_stack_3d_down_dottedline, + 0xf817: CupertinoIcons.square_stack_3d_down_right, + 0xf818: CupertinoIcons.square_stack_3d_down_right_fill, + 0xf819: CupertinoIcons.square_stack_3d_up, + 0xf81a: CupertinoIcons.square_stack_3d_up_fill, + 0xf81b: CupertinoIcons.square_stack_3d_up_slash, + 0xf81c: CupertinoIcons.square_stack_3d_up_slash_fill, + 0xf81d: CupertinoIcons.square_stack_fill, + 0xf81e: CupertinoIcons.squares_below_rectangle, + 0xf81f: CupertinoIcons.star, + 0xf820: CupertinoIcons.star_circle, + 0xf821: CupertinoIcons.star_circle_fill, + 0xf822: CupertinoIcons.star_fill, + 0xf823: CupertinoIcons.star_lefthalf_fill, + 0xf824: CupertinoIcons.star_slash, + 0xf825: CupertinoIcons.star_slash_fill, + 0xf826: CupertinoIcons.staroflife, + 0xf827: CupertinoIcons.staroflife_fill, + 0xf828: CupertinoIcons.stop, + 0xf829: CupertinoIcons.stop_circle, + 0xf82a: CupertinoIcons.stop_circle_fill, + 0xf82b: CupertinoIcons.stop_fill, + 0xf82c: CupertinoIcons.stopwatch, + 0xf82d: CupertinoIcons.stopwatch_fill, + 0xf82e: CupertinoIcons.strikethrough, + 0xf82f: CupertinoIcons.suit_club, + 0xf830: CupertinoIcons.suit_club_fill, + 0xf831: CupertinoIcons.suit_diamond, + 0xf832: CupertinoIcons.suit_diamond_fill, + 0xf833: CupertinoIcons.suit_heart, + 0xf834: CupertinoIcons.suit_heart_fill, + 0xf835: CupertinoIcons.suit_spade, + 0xf836: CupertinoIcons.suit_spade_fill, + 0xf837: CupertinoIcons.sum, + 0xf838: CupertinoIcons.sun_dust, + 0xf839: CupertinoIcons.sun_dust_fill, + 0xf83a: CupertinoIcons.sun_haze, + 0xf83b: CupertinoIcons.sun_haze_fill, + 0xf83c: CupertinoIcons.sun_min, + 0xf83d: CupertinoIcons.sun_min_fill, + 0xf83e: CupertinoIcons.sunrise, + 0xf83f: CupertinoIcons.sunrise_fill, + 0xf840: CupertinoIcons.sunset, + 0xf841: CupertinoIcons.sunset_fill, + 0xf842: CupertinoIcons.t_bubble, + 0xf843: CupertinoIcons.t_bubble_fill, + 0xf844: CupertinoIcons.table, + 0xf845: CupertinoIcons.table_badge_more, + 0xf846: CupertinoIcons.table_badge_more_fill, + 0xf847: CupertinoIcons.table_fill, + 0xf848: CupertinoIcons.tag_circle, + 0xf849: CupertinoIcons.tag_circle_fill, + 0xf84a: CupertinoIcons.text_aligncenter, + 0xf84b: CupertinoIcons.text_alignleft, + 0xf84c: CupertinoIcons.text_alignright, + 0xf84d: CupertinoIcons.text_append, + 0xf84e: CupertinoIcons.text_badge_checkmark, + 0xf84f: CupertinoIcons.text_badge_minus, + 0xf850: CupertinoIcons.text_badge_plus, + 0xf851: CupertinoIcons.text_badge_star, + 0xf852: CupertinoIcons.text_badge_xmark, + 0xf853: CupertinoIcons.text_bubble, + 0xf854: CupertinoIcons.text_bubble_fill, + 0xf855: CupertinoIcons.text_cursor, + 0xf856: CupertinoIcons.text_insert, + 0xf857: CupertinoIcons.text_justify, + 0xf858: CupertinoIcons.text_justifyleft, + 0xf859: CupertinoIcons.text_justifyright, + 0xf85a: CupertinoIcons.text_quote, + 0xf85b: CupertinoIcons.textbox, + 0xf85c: CupertinoIcons.textformat, + 0xf85d: CupertinoIcons.textformat_123, + 0xf85e: CupertinoIcons.textformat_abc, + 0xf85f: CupertinoIcons.textformat_abc_dottedunderline, + 0xf860: CupertinoIcons.textformat_alt, + 0xf861: CupertinoIcons.textformat_size, + 0xf862: CupertinoIcons.textformat_subscript, + 0xf863: CupertinoIcons.textformat_superscript, + 0xf864: CupertinoIcons.thermometer, + 0xf865: CupertinoIcons.thermometer_snowflake, + 0xf866: CupertinoIcons.thermometer_sun, + 0xf867: CupertinoIcons.timelapse, + 0xf868: CupertinoIcons.timer, + 0xf869: CupertinoIcons.tornado, + 0xf86a: CupertinoIcons.tortoise, + 0xf86b: CupertinoIcons.tortoise_fill, + 0xf86c: CupertinoIcons.tram_fill, + 0xf86d: CupertinoIcons.trash_circle, + 0xf86e: CupertinoIcons.trash_circle_fill, + 0xf86f: CupertinoIcons.trash_slash, + 0xf870: CupertinoIcons.trash_slash_fill, + 0xf871: CupertinoIcons.tray, + 0xf872: CupertinoIcons.tray_2, + 0xf873: CupertinoIcons.tray_2_fill, + 0xf874: CupertinoIcons.tray_arrow_down, + 0xf875: CupertinoIcons.tray_arrow_down_fill, + 0xf876: CupertinoIcons.tray_arrow_up, + 0xf877: CupertinoIcons.tray_arrow_up_fill, + 0xf878: CupertinoIcons.tray_fill, + 0xf879: CupertinoIcons.tray_full, + 0xf87a: CupertinoIcons.tray_full_fill, + 0xf87b: CupertinoIcons.triangle, + 0xf87c: CupertinoIcons.triangle_fill, + 0xf87d: CupertinoIcons.triangle_lefthalf_fill, + 0xf87e: CupertinoIcons.triangle_righthalf_fill, + 0xf87f: CupertinoIcons.tropicalstorm, + 0xf880: CupertinoIcons.tuningfork, + 0xf881: CupertinoIcons.tv, + 0xf882: CupertinoIcons.tv_circle, + 0xf883: CupertinoIcons.tv_circle_fill, + 0xf884: CupertinoIcons.tv_fill, + 0xf885: CupertinoIcons.tv_music_note, + 0xf886: CupertinoIcons.tv_music_note_fill, + 0xf887: CupertinoIcons.uiwindow_split_2x1, + 0xf888: CupertinoIcons.umbrella, + 0xf889: CupertinoIcons.umbrella_fill, + 0xf88a: CupertinoIcons.underline, + 0xf88b: CupertinoIcons.view_2d, + 0xf88c: CupertinoIcons.view_3d, + 0xf88d: CupertinoIcons.viewfinder, + 0xf88e: CupertinoIcons.viewfinder_circle, + 0xf88f: CupertinoIcons.viewfinder_circle_fill, + 0xf890: CupertinoIcons.wand_rays, + 0xf891: CupertinoIcons.wand_rays_inverse, + 0xf892: CupertinoIcons.wand_stars, + 0xf893: CupertinoIcons.wand_stars_inverse, + 0xf894: CupertinoIcons.waveform, + 0xf895: CupertinoIcons.waveform_circle, + 0xf896: CupertinoIcons.waveform_circle_fill, + 0xf897: CupertinoIcons.waveform_path, + 0xf898: CupertinoIcons.waveform_path_badge_minus, + 0xf899: CupertinoIcons.waveform_path_badge_plus, + 0xf89a: CupertinoIcons.waveform_path_ecg, + 0xf89b: CupertinoIcons.wifi, + 0xf89c: CupertinoIcons.wifi_exclamationmark, + 0xf89d: CupertinoIcons.wifi_slash, + 0xf89e: CupertinoIcons.wind, + 0xf89f: CupertinoIcons.wind_snow, + 0xf8a0: CupertinoIcons.wrench, + 0xf8a1: CupertinoIcons.wrench_fill, + 0xf8a2: CupertinoIcons.xmark_octagon, + 0xf8a3: CupertinoIcons.xmark_octagon_fill, + 0xf8a4: CupertinoIcons.xmark_rectangle, + 0xf8a5: CupertinoIcons.xmark_rectangle_fill, + 0xf8a6: CupertinoIcons.xmark_seal, + 0xf8a7: CupertinoIcons.xmark_seal_fill, + 0xf8a8: CupertinoIcons.xmark_shield, + 0xf8a9: CupertinoIcons.xmark_shield_fill, + 0xf8aa: CupertinoIcons.xmark_square, + 0xf8ab: CupertinoIcons.xmark_square_fill, + 0xf8ac: CupertinoIcons.zoom_in, + 0xf8ad: CupertinoIcons.zoom_out, + 0xf8ae: CupertinoIcons.zzz, + 0xf8af: CupertinoIcons.at_circle, + 0xf8b0: CupertinoIcons.at_circle_fill, + 0xf8b1: CupertinoIcons.bars, + 0xf8b2: CupertinoIcons.bitcoin, + 0xf8b3: CupertinoIcons.bitcoin_circle, + 0xf8b4: CupertinoIcons.bitcoin_circle_fill, + 0xf8b5: CupertinoIcons.building_2_fill, + 0xf8b6: CupertinoIcons.calendar_today, + 0xf8b7: CupertinoIcons.chart_bar_alt_fill, + 0xf8b8: CupertinoIcons.chart_bar_circle, + 0xf8b9: CupertinoIcons.chart_bar_circle_fill, + 0xf8ba: CupertinoIcons.chart_bar_square, + 0xf8bb: CupertinoIcons.chart_bar_square_fill, + 0xf8bc: CupertinoIcons.chat_bubble_2, + 0xf8bd: CupertinoIcons.chat_bubble_2_fill, + 0xf8be: CupertinoIcons.chat_bubble_fill, + 0xf8bf: CupertinoIcons.chat_bubble_text, + 0xf8c0: CupertinoIcons.chat_bubble_text_fill, + 0xf8c1: CupertinoIcons.checkmark_alt, + 0xf8c2: CupertinoIcons.checkmark_alt_circle, + 0xf8c3: CupertinoIcons.checkmark_alt_circle_fill, + 0xf8c4: CupertinoIcons.cloud_download, + 0xf8c5: CupertinoIcons.cloud_download_fill, + 0xf8c6: CupertinoIcons.cloud_upload, + 0xf8c7: CupertinoIcons.cloud_upload_fill, + 0xf8c8: CupertinoIcons.color_filter, + 0xf8c9: CupertinoIcons.color_filter_fill, + 0xf8ca: CupertinoIcons.compass, + 0xf8cb: CupertinoIcons.compass_fill, + 0xf8cc: CupertinoIcons.device_desktop, + 0xf8cd: CupertinoIcons.device_laptop, + 0xf8ce: CupertinoIcons.device_phone_landscape, + 0xf8cf: CupertinoIcons.device_phone_portrait, + 0xf8d0: CupertinoIcons.doc_chart, + 0xf8d1: CupertinoIcons.doc_chart_fill, + 0xf8d2: CupertinoIcons.doc_checkmark, + 0xf8d3: CupertinoIcons.doc_checkmark_fill, + 0xf8d4: CupertinoIcons.doc_person, + 0xf8d5: CupertinoIcons.doc_person_fill, + 0xf8d6: CupertinoIcons.download_circle, + 0xf8d7: CupertinoIcons.download_circle_fill, + 0xf8d8: CupertinoIcons.drop, + 0xf8d9: CupertinoIcons.drop_fill, + 0xf8da: CupertinoIcons.ellipsis_vertical, + 0xf8db: CupertinoIcons.ellipsis_vertical_circle, + 0xf8dc: CupertinoIcons.ellipsis_vertical_circle_fill, + 0xf8dd: CupertinoIcons.floppy_disk, + 0xf8de: CupertinoIcons.gamecontroller_alt_fill, + 0xf8df: CupertinoIcons.graph_circle, + 0xf8e0: CupertinoIcons.graph_circle_fill, + 0xf8e1: CupertinoIcons.graph_square, + 0xf8e2: CupertinoIcons.graph_square_fill, + 0xf8e3: CupertinoIcons.house_alt, + 0xf8e4: CupertinoIcons.house_alt_fill, + 0xf8e5: CupertinoIcons.layers, + 0xf8e6: CupertinoIcons.layers_alt, + 0xf8e7: CupertinoIcons.layers_alt_fill, + 0xf8e8: CupertinoIcons.layers_fill, + 0xf8e9: CupertinoIcons.money_dollar, + 0xf8ea: CupertinoIcons.money_dollar_circle, + 0xf8eb: CupertinoIcons.money_dollar_circle_fill, + 0xf8ec: CupertinoIcons.money_euro, + 0xf8ed: CupertinoIcons.money_euro_circle, + 0xf8ee: CupertinoIcons.money_euro_circle_fill, + 0xf8ef: CupertinoIcons.money_pound, + 0xf8f0: CupertinoIcons.money_pound_circle, + 0xf8f1: CupertinoIcons.money_pound_circle_fill, + 0xf8f2: CupertinoIcons.money_rubl, + 0xf8f3: CupertinoIcons.money_rubl_circle, + 0xf8f4: CupertinoIcons.money_rubl_circle_fill, + 0xf8f5: CupertinoIcons.money_yen, + 0xf8f6: CupertinoIcons.money_yen_circle, + 0xf8f7: CupertinoIcons.money_yen_circle_fill, + 0xf8f8: CupertinoIcons.move, + 0xf8f9: CupertinoIcons.music_albums, + 0xf8fa: CupertinoIcons.music_albums_fill, + 0xf8fb: CupertinoIcons.person_2_alt, + 0xf8fc: CupertinoIcons.person_alt, + 0xf8fd: CupertinoIcons.person_alt_circle, + 0xf8fe: CupertinoIcons.person_alt_circle_fill, + 0xf8ff: CupertinoIcons.piano, + 0xf900: CupertinoIcons.resize, + 0xf901: CupertinoIcons.resize_h, + 0xf902: CupertinoIcons.resize_v, + 0xf903: CupertinoIcons.rocket, + 0xf904: CupertinoIcons.rocket_fill, + 0xf905: CupertinoIcons.scissors_alt, + 0xf906: CupertinoIcons.sort_down, + 0xf907: CupertinoIcons.sort_down_circle, + 0xf908: CupertinoIcons.sort_down_circle_fill, + 0xf909: CupertinoIcons.sort_up, + 0xf90a: CupertinoIcons.sort_up_circle, + 0xf90b: CupertinoIcons.sort_up_circle_fill, + 0xf90c: CupertinoIcons.square_arrow_left, + 0xf90d: CupertinoIcons.square_arrow_left_fill, + 0xf90e: CupertinoIcons.square_arrow_right, + 0xf90f: CupertinoIcons.square_arrow_right_fill, + 0xf910: CupertinoIcons.square_favorites, + 0xf911: CupertinoIcons.square_favorites_alt, + 0xf912: CupertinoIcons.square_favorites_alt_fill, + 0xf913: CupertinoIcons.square_favorites_fill, + 0xf914: CupertinoIcons.square_list, + 0xf915: CupertinoIcons.square_list_fill, + 0xf916: CupertinoIcons.ticket, + 0xf917: CupertinoIcons.ticket_fill, + 0xf918: CupertinoIcons.tickets, + 0xf919: CupertinoIcons.tickets_fill, + 0xf91a: CupertinoIcons.timer_fill, + 0xf91b: CupertinoIcons.today, + 0xf91c: CupertinoIcons.today_fill, + 0xf91d: CupertinoIcons.tree, + 0xf91e: CupertinoIcons.upload_circle, + 0xf91f: CupertinoIcons.upload_circle_fill, + 0xf920: CupertinoIcons.videocam_circle, + 0xf921: CupertinoIcons.videocam_circle_fill, +}; diff --git a/lib/src/utils/debouncer.dart b/lib/src/utils/debouncer.dart index 6e9f7148..78f059b6 100644 --- a/lib/src/utils/debouncer.dart +++ b/lib/src/utils/debouncer.dart @@ -11,14 +11,14 @@ final class DeBouncer { /// Allows to create an instance with optional [Duration] with /// immediateFirstRun set to false. See [immediateFirstRun] for more details. DeBouncer([Duration? duration]) - : duration = duration ?? const Duration(milliseconds: 300), - immediateFirstRun = false; + : duration = duration ?? const Duration(milliseconds: 300), + immediateFirstRun = false; /// Allows to create an instance with optional [Duration] with /// immediateFirstRun set to true. See [immediateFirstRun] for more details. DeBouncer.immediate([Duration? duration]) - : duration = duration ?? const Duration(milliseconds: 300), - immediateFirstRun = true; + : duration = duration ?? const Duration(milliseconds: 300), + immediateFirstRun = true; /// de-bounce period. Default is 300 milliseconds. /// It will ignore any calls to [run] until [duration] has passed since the @@ -191,6 +191,7 @@ final DeBouncer debouncer = DeBouncer(); /// /// This will run the action immediately for the first call and then it will /// wait for 300 milliseconds to run the next call if there's any. -Future debounce(DeBounceAction action, - {bool immediateFirstRun = false}) => - debouncer.run(action, immediateFirstRun: immediateFirstRun); +Future debounce( + DeBounceAction action, { + bool immediateFirstRun = false, +}) => debouncer.run(action, immediateFirstRun: immediateFirstRun); diff --git a/lib/src/utils/extensions.dart b/lib/src/utils/extensions.dart index 24e57635..b072c4d1 100644 --- a/lib/src/utils/extensions.dart +++ b/lib/src/utils/extensions.dart @@ -51,6 +51,8 @@ extension FABLocationHelper on FABLocation { return FloatingActionButtonLocation.endDocked; case FABLocation.miniEndDocked: return FloatingActionButtonLocation.miniEndDocked; + case FABLocation.endContained: + return FloatingActionButtonLocation.endContained; } } } @@ -92,7 +94,8 @@ extension M3NavigationDestinationLabelBehaviorHelper return NavigationDestinationLabelBehavior.onlyShowSelected; case MaterialNavigationBarLabelBehavior.onlyShowUnselected: throw UnsupportedError( - 'MaterialNavigationBarLabelBehavior.onlyShowUnselected is not supported for material 2.'); + 'MaterialNavigationBarLabelBehavior.onlyShowUnselected is not supported for material 2.', + ); } } } @@ -191,8 +194,10 @@ extension BaseNodeHelper on BaseNode { } } - RectC rectFromBoundary(NodeBoundaryType type, - {required PositioningSpace space}) { + RectC rectFromBoundary( + NodeBoundaryType type, { + required PositioningSpace space, + }) { switch (space) { case PositioningSpace.local: return localRectFromBoundary(type); @@ -201,8 +206,10 @@ extension BaseNodeHelper on BaseNode { } } - Vec posFromBoundary(NodeBoundaryType type, - {required PositioningSpace space}) { + Vec posFromBoundary( + NodeBoundaryType type, { + required PositioningSpace space, + }) { switch (space) { case PositioningSpace.global: return globalPosFromBoundary(type); @@ -238,7 +245,7 @@ extension ScrollPhysicsHelper on ScrollPhysicsC { ScrollPhysicsC.rangeMaintainingScrollPhysics => const RangeMaintainingScrollPhysics(), ScrollPhysicsC.neverScrollableScrollPhysics => - const NeverScrollableScrollPhysics() + const NeverScrollableScrollPhysics(), }; if (shouldAlwaysScroll) { return AlwaysScrollableScrollPhysics(parent: parent); @@ -258,6 +265,10 @@ extension TextModelEnumHelper on TextAlignHorizontalEnum { return TextAlign.right; case TextAlignHorizontalEnum.justified: return TextAlign.justify; + case TextAlignHorizontalEnum.start: + return TextAlign.start; + case TextAlignHorizontalEnum.end: + return TextAlign.end; } } } @@ -288,59 +299,57 @@ extension RowColumnTypeHelper on RowColumnType { extension CornerRadiusHelper on CornerRadius { flutter.BorderRadius get borderRadius => flutter.BorderRadius.only( - topLeft: Radius.elliptical(tl.x, tl.y), - topRight: Radius.elliptical(tr.x, tr.y), - bottomLeft: Radius.elliptical(bl.x, bl.y), - bottomRight: Radius.elliptical(br.x, br.y), - ); + topLeft: Radius.elliptical(tl.x, tl.y), + topRight: Radius.elliptical(tr.x, tr.y), + bottomLeft: Radius.elliptical(bl.x, bl.y), + bottomRight: Radius.elliptical(br.x, br.y), + ); } extension ColorHelper on flutter.Color { - ColorRGB get colorRGB => - ColorRGB(r: red / 255.0, g: green / 255.0, b: blue / 255.0); + ColorRGB get colorRGB => ColorRGB(r: r, g: g, b: b); - ColorRGBA get colorRGBA => ColorRGBA( - r: red / 255.0, g: green / 255.0, b: blue / 255.0, a: alpha / 255.0); + ColorRGBA get colorRGBA => ColorRGBA(r: r, g: g, b: b, a: a); Color darken([double percent = 10]) { assert(1 <= percent && percent <= 100); var f = 1 - percent / 100; - return Color.fromARGB( - alpha, (red * f).round(), (green * f).round(), (blue * f).round()); + return Color.from(alpha: a, red: r * f, green: g * f, blue: b * f); } // lighten Color lighten([double percent = 10]) { assert(1 <= percent && percent <= 100); var p = percent / 100; - return Color.fromARGB( - alpha, - red + ((255 - red) * p).round(), - green + ((255 - green) * p).round(), - blue + ((255 - blue) * p).round()); + return Color.from( + alpha: a, + red: r + (1 - r) * p, + green: g + (1 - g) * p, + blue: b + (1 - b) * p, + ); } ColorRGB toColorRGB() => colorRGB; ColorRGBA toColorRGBA() => colorRGBA; - String get hex => value.toRadixString(16).padLeft(8, '0').toUpperCase(); + String get hex => toARGB32().toRadixString(16).padLeft(8, '0').toUpperCase(); PaintModel toPaint([String? id]) => PaintModel.solid( - id: id ?? generateId(), - visible: true, - opacity: opacity, - color: toColorRGB(), - ); + id: id ?? generateId(), + visible: true, + opacity: a, + color: toColorRGB(), + ); Effect toShadow() => Effect( - id: generateId(), - type: EffectType.dropShadow, - color: colorToRGBA(this), - offset: const Vec(0, 4), - radius: 8, - spread: 0, - ); + id: generateId(), + type: EffectType.dropShadow, + color: colorToRGBA(this), + offset: const Vec(0, 4), + radius: 8, + spread: 0, + ); } extension BrightnessModelHelper on BrightnessModel { @@ -368,10 +377,11 @@ extension ColorRGBAHelper on ColorRGBA { ColorRGB toRGB() => toFlutterColor().colorRGB; PaintModel toPaint([String? id]) => PaintModel.solid( - id: id ?? generateId(), - visible: true, - opacity: toFlutterColor().opacity, - color: toRGB()); + id: id ?? generateId(), + visible: true, + opacity: toFlutterColor().a, + color: toRGB(), + ); } extension PaintModelExt on PaintModel { @@ -392,7 +402,11 @@ extension PaintModelExt on PaintModel { extension ColorRGBHelper on ColorRGB { flutter.Color get flutterColor => flutter.Color.fromRGBO( - (r * 255).toInt(), (g * 255).toInt(), (b * 255).toInt(), 1); + (r * 255).toInt(), + (g * 255).toInt(), + (b * 255).toInt(), + 1, + ); Color toFlutterColor({required double opacity}) { var ir = (r * 255).toInt(); @@ -408,11 +422,11 @@ extension ColorRGBHelper on ColorRGB { extension BoxConstraintsModelHelper on BoxConstraintsModel { flutter.BoxConstraints get flutterConstraints => flutter.BoxConstraints( - minWidth: minWidth ?? 0.0, - maxWidth: maxWidth ?? double.infinity, - minHeight: minHeight ?? 0.0, - maxHeight: maxHeight ?? double.infinity, - ); + minWidth: minWidth ?? 0.0, + maxWidth: maxWidth ?? double.infinity, + minHeight: minHeight ?? 0.0, + maxHeight: maxHeight ?? double.infinity, + ); } extension AlignmentDataHelper on AlignmentData { @@ -737,14 +751,14 @@ extension FitsExtension on BaseNode { edgePins.inner(parent).isBothExpanded || isBothExpanded; bool isWrapOnAxis(AxisC axis) => switch (axis) { - AxisC.horizontal => isHorizontalWrap, - AxisC.vertical => isVerticalWrap, - }; + AxisC.horizontal => isHorizontalWrap, + AxisC.vertical => isVerticalWrap, + }; bool doesFlexOnAxis(AxisC axis) => switch (axis) { - AxisC.horizontal => isHorizontalExpanded || isHorizontalFlexible, - AxisC.vertical => isVerticalExpanded || isVerticalFlexible, - }; + AxisC.horizontal => isHorizontalExpanded || isHorizontalFlexible, + AxisC.vertical => isVerticalExpanded || isVerticalFlexible, + }; } extension RowColumnMixinExtensions on RowColumnMixin { @@ -777,11 +791,11 @@ extension RowColumnMixinExtensions on RowColumnMixin { extension EdgePinsExtensions on EdgePinsModel { EdgePinsModel inner(BaseNode? parent) => EdgePinsModel( - left: effectiveLeft(parent), - top: effectiveTop(parent), - right: effectiveRight(parent), - bottom: effectiveBottom(parent), - ); + left: effectiveLeft(parent), + top: effectiveTop(parent), + right: effectiveRight(parent), + bottom: effectiveBottom(parent), + ); double? effectiveLeft(BaseNode? parent) { if (parent == null || left == null) return left; @@ -849,11 +863,11 @@ extension FitExtension on Fit { extension BoxFitExt on BoxFit { bool get supportsAlignment => { - BoxFit.fitHeight, - BoxFit.fitWidth, - BoxFit.scaleDown, - BoxFit.none, - }.contains(this); + BoxFit.fitHeight, + BoxFit.fitWidth, + BoxFit.scaleDown, + BoxFit.none, + }.contains(this); } extension AlignmentModelExtensions on AlignmentModel { @@ -936,6 +950,10 @@ extension TextAlignHorizontalEnumExtensions on TextAlignHorizontalEnum { return TextAlign.right; case TextAlignHorizontalEnum.justified: return TextAlign.justify; + case TextAlignHorizontalEnum.start: + return TextAlign.start; + case TextAlignHorizontalEnum.end: + return TextAlign.end; } } } @@ -976,8 +994,10 @@ extension FontNameExtensions on FontName { // Deprecated // Convert 'extra-bold' or 'extra bold' into 'extrabold'. - final String style = - this.style.toLowerCase().replaceAll('-', '').replaceAll(' ', ''); + final String style = this.style + .toLowerCase() + .replaceAll('-', '') + .replaceAll(' ', ''); // String might contain 'italic', which is why it uses [contains] for the // comparison. @@ -1069,6 +1089,10 @@ extension TextInputTypeEnumExtensions on TextInputTypeEnum { return TextInputType.url; case TextInputTypeEnum.visiblePassword: return TextInputType.visiblePassword; + case TextInputTypeEnum.webSearch: + return TextInputType.webSearch; + case TextInputTypeEnum.twitter: + return TextInputType.twitter; } } } @@ -1139,26 +1163,58 @@ extension IconModelExt on IconModel { return null; } - String? toSvgUrl() { - if (this is MaterialIcon) { - final icon = this as MaterialIcon; - // e.g https://fonts.gstatic.com/s/i/materialiconsoutlined/home/v16/24px.svg - /// The template url for the svg icons. - /// {{style}}: The style of the icon. e.g. materialiconsoutlined - /// {{name}}: The name of the icon. e.g. home - /// {{version}}: The version of the icon. e.g. 16 - /// {{size}}: The size of the icon. e.g. 24 - return 'https://fonts.gstatic.com/s/i/{{style}}/{{name}}/v{{version}}/{{size}}px.svg' - .replaceAll( - '{{style}}', 'materialicons${icon.style.styleName.toLowerCase()}') - .replaceAll('{{name}}', icon.name.toLowerCase()) - .replaceAll('{{version}}', '${icon.version ?? 1}') - .replaceAll('{{size}}', '24'); + /// URL of Google's prebuilt SVG for this icon, or null when there is + /// none (Cupertino and custom font icons). + /// + /// Material Symbols are variable fonts; their SVGs exist only for some + /// axis values, so [fill], [weight], [grade] and [opticalSize] are snapped + /// to the nearest available variant. + String? toSvgUrl({ + double? fill, + double? weight, + double? grade, + double? opticalSize, + }) { + final IconModel icon = this; + if (icon is MaterialIcon) { + // e.g. https://fonts.gstatic.com/s/i/materialiconsoutlined/home/v16/24px.svg + final String style = icon.style.styleName.toLowerCase().replaceAll( + ' ', + '', + ); + return 'https://fonts.gstatic.com/s/i/materialicons$style/' + '${icon.name.toLowerCase()}/v${icon.version ?? 1}/24px.svg'; + } + if (icon is MaterialSymbol) { + // e.g. https://fonts.gstatic.com/s/i/short-term/release/materialsymbolsrounded/home/wght300gradN25fill1/40px.svg + return 'https://fonts.gstatic.com/s/i/short-term/release/' + 'materialsymbols${icon.style.name}/${icon.name.toLowerCase()}/' + '${materialSymbolVariant(fill: fill, weight: weight, grade: grade)}/' + '${_nearest(opticalSize ?? 24, const [20, 24, 40, 48])}px.svg'; } return null; } } +/// The path segment naming a Material Symbols SVG variant, e.g. +/// `wght700grad200fill1`, or `default` when every axis is at its default. +String materialSymbolVariant({double? fill, double? weight, double? grade}) { + final int w = weight == null + ? 400 + : ((weight / 100).round() * 100).clamp(100, 700); + final int g = grade == null ? 0 : _nearest(grade, const [-25, 0, 200]); + final bool filled = (fill ?? 0) >= 0.5; + final String variant = [ + if (w != 400) 'wght$w', + if (g != 0) 'grad${g < 0 ? 'N${-g}' : g}', + if (filled) 'fill1', + ].join(); + return variant.isEmpty ? 'default' : variant; +} + +int _nearest(double value, List options) => + options.reduce((a, b) => (a - value).abs() <= (b - value).abs() ? a : b); + extension MaterialIconExt on MaterialIcon { String? get flutterID => flutterIconNames[styledName]; @@ -1246,10 +1302,7 @@ extension VisualDensityModelExt on VisualDensityModel { vertical: VisualDensity.maximumDensity, ); case VisualDensityType.custom: - return VisualDensity( - horizontal: horizontal, - vertical: vertical, - ); + return VisualDensity(horizontal: horizontal, vertical: vertical); } } } @@ -1293,7 +1346,7 @@ extension StringExt on String { String get capitalized => characters.isEmpty ? this : characters.first.toUpperCase() + - characters.skip(1).string.toLowerCase(); + characters.skip(1).string.toLowerCase(); /// Whether the string contains all the valid variable paths inside it. /// This will also evaluate the path inside the ${} to be valid. @@ -1301,9 +1354,9 @@ extension StringExt on String { String unwrapVariablePath() => isValidVariablePath ? variablePathRegex - .allMatches(this) - .map((match) => match.namedGroup('value')) - .join(' ') + .allMatches(this) + .map((match) => match.namedGroup('value')) + .join(' ') : this; /// Whether the string contains a variable path that is not checked for @@ -1439,11 +1492,13 @@ extension StringExt on String { /// 2. [https://www.youtube.com/embed/] /// 3. [https://youtube.com/embed/] RegExp youtubeShareUrlRegex = RegExp( - 'https://(www.)?(youtu.be|((youtube.com)(?:/embed)))/(?[a-zA-Z_0-9]+)'); + 'https://(www.)?(youtu.be|((youtube.com)(?:/embed)))/(?[a-zA-Z_0-9]+)', +); /// Can match: [https://www.youtube.com/watch?v=] RegExp youtubeWatchUrlRegex = RegExp( - '^https://(www.)?(youtube.com/watch)?.*v=(?[a-zA-Z_0-9]+)'); + '^https://(www.)?(youtube.com/watch)?.*v=(?[a-zA-Z_0-9]+)', +); RegExp vimeoUrlRegex = RegExp('^https://vimeo.com/(?[a-zA-Z0-9]+)'); @@ -1511,8 +1566,9 @@ extension BuildContextExt on BuildContext { T? getNodeValue(String nodeId, String key) { return nodeValues(nodeId) - .firstWhereOrNull((value) => value.name == key) - ?.value as T?; + .firstWhereOrNull((value) => value.name == key) + ?.value + as T?; } } @@ -1578,15 +1634,19 @@ extension ActionModelExt on ActionModel { final NavigationAction action = this as NavigationAction; strings.addAll([action.destinationId]); for (final params in action.params.entries) { - strings - .addAll([params.key, if (params.value is String) params.value]); + strings.addAll([ + params.key, + if (params.value is String) params.value, + ]); } case ActionType.showDialog: final ShowDialogAction action = this as ShowDialogAction; strings.addAll([action.destinationId]); for (final params in action.params.entries) { - strings - .addAll([params.key, if (params.value is String) params.value]); + strings.addAll([ + params.key, + if (params.value is String) params.value, + ]); } case ActionType.link: final LinkAction action = this as LinkAction; @@ -1606,10 +1666,13 @@ extension ActionModelExt on ActionModel { } case ActionType.setValue: final SetValueAction action = this as SetValueAction; - strings.addAll(action.values - .map( - (value) => value.value is String ? value.value as String : null) - .nonNulls); + strings.addAll( + action.values + .map( + (value) => value.value is String ? value.value as String : null, + ) + .nonNulls, + ); case ActionType.setVariable: final SetVariableAction action = this as SetVariableAction; strings.addAll([action.index, action.mapKey, action.newValue]); @@ -1624,8 +1687,12 @@ extension ActionModelExt on ActionModel { strings.addAll(action.parameters.values); case ActionType.setStorage: final SetStorageAction action = this as SetStorageAction; - strings - .addAll([action.key, action.index, action.mapKey, action.newValue]); + strings.addAll([ + action.key, + action.index, + action.mapKey, + action.newValue, + ]); case ActionType.setCloudStorage: final SetCloudStorageAction action = this as SetCloudStorageAction; switch (action.subAction) { @@ -1637,10 +1704,7 @@ extension ActionModelExt on ActionModel { if (!action.useRawValue) action.newValue, ]); case RemoveDocumentSubAction action: - strings.addAll([ - action.path, - action.documentId, - ]); + strings.addAll([action.path, action.documentId]); case UpdateDocumentSubAction action: strings.addAll([ action.path, @@ -1656,20 +1720,26 @@ extension ActionModelExt on ActionModel { final LoadFromCloudStorageAction action = this as LoadFromCloudStorageAction; strings.addAll([action.path, action.documentId]); - strings.addAll(action.whereFilters - .map((filter) => filter.getUsedVariableNames()) - .expand((e) => e)); - strings.addAll(action.orderByFilters - .map((filter) => filter.getUsedVariableNames()) - .expand((e) => e)); + strings.addAll( + action.whereFilters + .map((filter) => filter.getUsedVariableNames()) + .expand((e) => e), + ); + strings.addAll( + action.orderByFilters + .map((filter) => filter.getUsedVariableNames()) + .expand((e) => e), + ); } return strings .map((text) => VariableMatch.parseAll(text)) .expand((matches) => matches) .map((match) => match.name) - .where((name) => - includePredefined || !predefinedVariableNames.contains(name)) + .where( + (name) => + includePredefined || !predefinedVariableNames.contains(name), + ) .toSet(); } } @@ -1692,18 +1762,21 @@ extension BaseConditionExt on BaseCondition { final condition = this; if (condition is Condition) { final condition = this as Condition; - return condition.actions - .any((action) => action.hasProperty(nodeId, name)); + return condition.actions.any( + (action) => action.hasProperty(nodeId, name), + ); } if (condition is ConditionGroup) { return condition.ifCondition.hasProperty(nodeId, name) || condition.elseCondition?.hasProperty(nodeId, name) == true || - condition.elseIfConditions - .any((condition) => condition.hasProperty(nodeId, name)); + condition.elseIfConditions.any( + (condition) => condition.hasProperty(nodeId, name), + ); } if (condition is ElseCondition) { - return condition.actions - .any((action) => action.hasProperty(nodeId, name)); + return condition.actions.any( + (action) => action.hasProperty(nodeId, name), + ); } return false; } @@ -1717,8 +1790,9 @@ extension BaseConditionExt on BaseCondition { if (condition is ConditionGroup) { return condition.ifCondition.hasNode(nodeId) || condition.elseCondition?.hasNode(nodeId) == true || - condition.elseIfConditions - .any((condition) => condition.hasNode(nodeId)); + condition.elseIfConditions.any( + (condition) => condition.hasNode(nodeId), + ); } if (condition is ElseCondition) { return condition.actions.any((action) => action.hasNode(nodeId)); @@ -1734,15 +1808,17 @@ extension BaseConditionExt on BaseCondition { /// Returns all the variables used in this condition except the predefined /// variables. Returns names of the variables. Set getReactiveVariables() => - accept>(ConditionVariablesVisitor( + accept>( + ConditionVariablesVisitor( excludedVariableNames: {...predefinedVariableNames} - ..removeAll(predefinedListenableVariableNames))) ?? + ..removeAll(predefinedListenableVariableNames), + ), + ) ?? {}; /// [variablesOverrides] is a map of variable name to variable value. - R? evaluate(ScopedValues scopedValues) => accept( - ConditionEvaluator(scopedValues: scopedValues), - ); + R? evaluate(ScopedValues scopedValues) => + accept(ConditionEvaluator(scopedValues: scopedValues)); void prettyPrint() => ConditionPrinter().printCondition(this); @@ -1753,29 +1829,32 @@ extension BaseConditionExt on BaseCondition { final condition = this; return switch (condition) { ElseCondition() => condition.copyWith( - actions: condition.actions - .map((action) => action.withUpdatedNodeId(oldNodeId, newNodeId)) - .toList()), + actions: condition.actions + .map((action) => action.withUpdatedNodeId(oldNodeId, newNodeId)) + .toList(), + ), Condition() => condition.copyWith( - actions: condition.actions - .map((action) => action.withUpdatedNodeId(oldNodeId, newNodeId)) - .toList()), + actions: condition.actions + .map((action) => action.withUpdatedNodeId(oldNodeId, newNodeId)) + .toList(), + ), ConditionGroup() => condition.copyWith( - elseCondition: condition.elseCondition?.withUpdatedNodeId( - oldNodeId, - newNodeId, - ) as ElseCondition?, - elseIfConditions: condition.elseIfConditions - .map((condition) => condition.withUpdatedNodeId( - oldNodeId, - newNodeId, - ) as Condition) - .toList(), - ifCondition: condition.ifCondition.withUpdatedNodeId( - oldNodeId, - newNodeId, - ) as Condition, - ), + elseCondition: condition.elseCondition?.withUpdatedNodeId( + oldNodeId, + newNodeId, + ) as ElseCondition?, + elseIfConditions: condition.elseIfConditions + .map( + (condition) => + condition.withUpdatedNodeId(oldNodeId, newNodeId) + as Condition, + ) + .toList(), + ifCondition: condition.ifCondition.withUpdatedNodeId( + oldNodeId, + newNodeId, + ) as Condition, + ), }; } } @@ -1824,8 +1903,9 @@ extension ConditionsMapExt on Map { extension CanvasConditionsExt on CanvasConditions { bool hasCondition(String nodeId, String name) { - return conditions.values - .any((condition) => condition.hasProperty(nodeId, name)); + return conditions.values.any( + (condition) => condition.hasProperty(nodeId, name), + ); } } @@ -1851,8 +1931,8 @@ extension VariantIterable on Iterable { extension IterableExt on Iterable { Map mapBy(K Function(V) key) => { - for (final value in this) key(value): value, - }; + for (final value in this) key(value): value, + }; int count(V item) => where((e) => e == item).length; } @@ -1866,6 +1946,27 @@ extension TabBarIndicatorSizeExtensions on TabBarIndicatorSizeEnum { } } +extension ListTileTitleAlignmentCExtensions on ListTileTitleAlignmentC { + ListTileTitleAlignment toFlutter() => switch (this) { + ListTileTitleAlignmentC.threeLine => ListTileTitleAlignment.threeLine, + ListTileTitleAlignmentC.titleHeight => ListTileTitleAlignment.titleHeight, + ListTileTitleAlignmentC.top => ListTileTitleAlignment.top, + ListTileTitleAlignmentC.center => ListTileTitleAlignment.center, + ListTileTitleAlignmentC.bottom => ListTileTitleAlignment.bottom, + }; +} + +extension TabAlignmentCExtensions on TabAlignmentC { + /// Returns null for alignments that Flutter's [TabBar] rejects for the + /// given scrollability, so it falls back to its default. + TabAlignment? toFlutter({required bool isScrollable}) => switch (this) { + TabAlignmentC.start => isScrollable ? TabAlignment.start : null, + TabAlignmentC.startOffset => isScrollable ? TabAlignment.startOffset : null, + TabAlignmentC.fill => isScrollable ? null : TabAlignment.fill, + TabAlignmentC.center => TabAlignment.center, + }; +} + extension TextAlignVerticalEnumExt on TextAlignVerticalEnum { TextAlignVertical get flutterTextAlignVertical { return switch (this) { @@ -1924,44 +2025,44 @@ extension BaseNodeExt on BaseNode { extension StrokeCapEnumExt on StrokeCapEnum { StrokeCap get flutterStrokeCap => switch (this) { - StrokeCapEnum.round => StrokeCap.round, - StrokeCapEnum.none => StrokeCap.butt, - StrokeCapEnum.square => StrokeCap.square, - }; + StrokeCapEnum.round => StrokeCap.round, + StrokeCapEnum.none => StrokeCap.butt, + StrokeCapEnum.square => StrokeCap.square, + }; } extension BlendModeCExt on BlendModeC { BlendMode get flutterBlendMode => switch (this) { - BlendModeC.src => BlendMode.src, - BlendModeC.srcOver => BlendMode.srcOver, - BlendModeC.srcATop => BlendMode.srcATop, - BlendModeC.srcIn => BlendMode.srcIn, - BlendModeC.srcOut => BlendMode.srcOut, - BlendModeC.dst => BlendMode.dst, - BlendModeC.dstOver => BlendMode.dstOver, - BlendModeC.dstIn => BlendMode.dstIn, - BlendModeC.dstOut => BlendMode.dstOut, - BlendModeC.dstATop => BlendMode.dstATop, - BlendModeC.clear => BlendMode.clear, - BlendModeC.multiply => BlendMode.multiply, - BlendModeC.screen => BlendMode.screen, - BlendModeC.overlay => BlendMode.overlay, - BlendModeC.darken => BlendMode.darken, - BlendModeC.lighten => BlendMode.lighten, - BlendModeC.colorDodge => BlendMode.colorDodge, - BlendModeC.colorBurn => BlendMode.colorBurn, - BlendModeC.hardLight => BlendMode.hardLight, - BlendModeC.softLight => BlendMode.softLight, - BlendModeC.difference => BlendMode.difference, - BlendModeC.exclusion => BlendMode.exclusion, - BlendModeC.hue => BlendMode.hue, - BlendModeC.saturation => BlendMode.saturation, - BlendModeC.color => BlendMode.color, - BlendModeC.luminosity => BlendMode.luminosity, - BlendModeC.xor => BlendMode.xor, - BlendModeC.plus => BlendMode.plus, - BlendModeC.modulate => BlendMode.modulate, - }; + BlendModeC.src => BlendMode.src, + BlendModeC.srcOver => BlendMode.srcOver, + BlendModeC.srcATop => BlendMode.srcATop, + BlendModeC.srcIn => BlendMode.srcIn, + BlendModeC.srcOut => BlendMode.srcOut, + BlendModeC.dst => BlendMode.dst, + BlendModeC.dstOver => BlendMode.dstOver, + BlendModeC.dstIn => BlendMode.dstIn, + BlendModeC.dstOut => BlendMode.dstOut, + BlendModeC.dstATop => BlendMode.dstATop, + BlendModeC.clear => BlendMode.clear, + BlendModeC.multiply => BlendMode.multiply, + BlendModeC.screen => BlendMode.screen, + BlendModeC.overlay => BlendMode.overlay, + BlendModeC.darken => BlendMode.darken, + BlendModeC.lighten => BlendMode.lighten, + BlendModeC.colorDodge => BlendMode.colorDodge, + BlendModeC.colorBurn => BlendMode.colorBurn, + BlendModeC.hardLight => BlendMode.hardLight, + BlendModeC.softLight => BlendMode.softLight, + BlendModeC.difference => BlendMode.difference, + BlendModeC.exclusion => BlendMode.exclusion, + BlendModeC.hue => BlendMode.hue, + BlendModeC.saturation => BlendMode.saturation, + BlendModeC.color => BlendMode.color, + BlendModeC.luminosity => BlendMode.luminosity, + BlendModeC.xor => BlendMode.xor, + BlendModeC.plus => BlendMode.plus, + BlendModeC.modulate => BlendMode.modulate, + }; } extension GenericExt on T? { @@ -1975,17 +2076,21 @@ extension GenericExt on T? { extension BreakpointExt on Iterable { /// Retrieves the breakpoint applicable for the given width. - Breakpoint? findForWidth(double width) => firstWhereOrNull((breakpoint) => - width >= breakpoint.lowerBound && width < breakpoint.upperBound); + Breakpoint? findForWidth(double width) => firstWhereOrNull( + (breakpoint) => + width >= breakpoint.lowerBound && width < breakpoint.upperBound, + ); } extension LayoutGroupsExt on Map> { /// Finds the layout group and breakpoint for the given canvas id. ({String? layoutGroupId, Breakpoint? breakpoint}) findByCanvasId( - String canvasId) { + String canvasId, + ) { for (final MapEntry(key: groupId, value: breakpoints) in entries) { - final breakpoint = breakpoints - .firstWhereOrNull((breakpoint) => breakpoint.nodeId == canvasId); + final breakpoint = breakpoints.firstWhereOrNull( + (breakpoint) => breakpoint.nodeId == canvasId, + ); if (breakpoint != null) { return (layoutGroupId: groupId, breakpoint: breakpoint); } @@ -1994,8 +2099,12 @@ extension LayoutGroupsExt on Map> { } Map toJson() { - return map((key, value) => - MapEntry(key, value.map((breakpoint) => breakpoint.toJson()).toList())); + return map( + (key, value) => MapEntry( + key, + value.map((breakpoint) => breakpoint.toJson()).toList(), + ), + ); } } @@ -2029,27 +2138,29 @@ extension TextFieldFormatterExt on TextInputFormatterModel { extension AutovalidateModeCExt on AutovalidateModeC { AutovalidateMode get flutterAutovalidateMode => switch (this) { - AutovalidateModeC.disabled => AutovalidateMode.disabled, - AutovalidateModeC.always => AutovalidateMode.always, - AutovalidateModeC.onUserInteraction => - AutovalidateMode.onUserInteraction, - }; + AutovalidateModeC.disabled => AutovalidateMode.disabled, + AutovalidateModeC.always => AutovalidateMode.always, + AutovalidateModeC.onUserInteraction => AutovalidateMode.onUserInteraction, + AutovalidateModeC.onUnfocus => AutovalidateMode.onUnfocus, + AutovalidateModeC.onUserInteractionIfError => + AutovalidateMode.onUserInteractionIfError, + }; } extension TextInputActionCExt on TextInputActionC { TextInputAction get flutterTextInputAction => switch (this) { - TextInputActionC.none => TextInputAction.none, - TextInputActionC.unspecified => TextInputAction.unspecified, - TextInputActionC.done => TextInputAction.done, - TextInputActionC.go => TextInputAction.go, - TextInputActionC.search => TextInputAction.search, - TextInputActionC.send => TextInputAction.send, - TextInputActionC.next => TextInputAction.next, - TextInputActionC.previous => TextInputAction.previous, - TextInputActionC.continueAction => TextInputAction.continueAction, - TextInputActionC.join => TextInputAction.join, - TextInputActionC.route => TextInputAction.route, - TextInputActionC.emergencyCall => TextInputAction.emergencyCall, - TextInputActionC.newline => TextInputAction.newline, - }; + TextInputActionC.none => TextInputAction.none, + TextInputActionC.unspecified => TextInputAction.unspecified, + TextInputActionC.done => TextInputAction.done, + TextInputActionC.go => TextInputAction.go, + TextInputActionC.search => TextInputAction.search, + TextInputActionC.send => TextInputAction.send, + TextInputActionC.next => TextInputAction.next, + TextInputActionC.previous => TextInputAction.previous, + TextInputActionC.continueAction => TextInputAction.continueAction, + TextInputActionC.join => TextInputAction.join, + TextInputActionC.route => TextInputAction.route, + TextInputActionC.emergencyCall => TextInputAction.emergencyCall, + TextInputActionC.newline => TextInputAction.newline, + }; } diff --git a/lib/src/utils/flutter_icons_data_map.dart b/lib/src/utils/flutter_icons_data_map.dart index 5bf396d7..15e679e5 100644 --- a/lib/src/utils/flutter_icons_data_map.dart +++ b/lib/src/utils/flutter_icons_data_map.dart @@ -8823,5 +8823,5 @@ const Map flutterIconsDataMap = { 'zoom_out_map': 0xe6ff, 'zoom_out_map_sharp': 0xedf0, 'zoom_out_map_rounded': 0xf02cf, - 'zoom_out_map_outlined': 0xf4dd + 'zoom_out_map_outlined': 0xf4dd, }; diff --git a/lib/src/utils/flutter_icons_names_map.dart b/lib/src/utils/flutter_icons_names_map.dart index 008e72a6..36572f4b 100644 --- a/lib/src/utils/flutter_icons_names_map.dart +++ b/lib/src/utils/flutter_icons_names_map.dart @@ -8866,5 +8866,5 @@ const Map flutterIconNames = { "zoom_out_rounded": "zoom_out_rounded", "zoom_in_rounded": "zoom_in_rounded", "zoom_out_sharp": "zoom_out_sharp", - "zoom_in_sharp": "zoom_in_sharp" + "zoom_in_sharp": "zoom_in_sharp", }; diff --git a/lib/src/utils/font_parser.dart b/lib/src/utils/font_parser.dart index abe60285..ebb3f534 100644 --- a/lib/src/utils/font_parser.dart +++ b/lib/src/utils/font_parser.dart @@ -1,17 +1,9 @@ -import 'dart:convert'; -import 'dart:io'; import 'dart:typed_data'; import 'package:codelessly_api/codelessly_api.dart'; import '../model/font_family.dart'; -void main(List args) { - final file = File(args[0]); - final result = FontParser.parse(file.readAsBytesSync()); - print('Result: ${const JsonEncoder.withIndent(' ').convert(result)}'); -} - /// Compares the given [a] [FontVariantModel] with the [b] [FontVariantModel] /// and returns the score. /// @@ -37,8 +29,10 @@ int _computeMatch(FontVariantModel a, FontVariantModel b) { /// /// The [FontVariantModel] with the lowest score is returned. // ignore: unused_element -FontVariantModel _closestMatch(FontVariantModel sourceVariant, - Iterable variantsToCompare) { +FontVariantModel _closestMatch( + FontVariantModel sourceVariant, + Iterable variantsToCompare, +) { int? bestScore; late FontVariantModel bestMatch; for (final variantToCompare in variantsToCompare) { @@ -58,8 +52,9 @@ String getFontFamilyNameAndVariant( }) { FontWeightNumeric weight = fontName.weight ?? FontWeightNumeric.w400; String family = fontName.family; - String style = - fontName.style.toLowerCase().contains('italic') ? 'Italic' : 'Normal'; + String style = fontName.style.toLowerCase().contains('italic') + ? 'Italic' + : 'Normal'; if (familyModel != null) { /// If [familyModel] is not null, then this is running from the editor. In @@ -81,11 +76,7 @@ String getFontFamilyNameAndVariant( } // If [familyModel] is null, then this is running from the SDK. In this case, // we don't need to derive the font family name since it is already processed. - return deriveFontFamily( - family: family, - style: style, - weight: weight, - ); + return deriveFontFamily(family: family, style: style, weight: weight); } String deriveFontFamily({ @@ -244,7 +235,7 @@ class FontParser { 'wwsFamilyName', 'wwsSubfamilyName', 'lightPalette', - 'darkPalette' + 'darkPalette', ]; var offset0 = offset; @@ -280,10 +271,10 @@ class FontParser { str = bin.readUnicode(data, soff, slen ~/ 2); } else if (platformID == 1) { str = bin.readASCII(data, soff, slen); - print('reading unknown MAC encoding $encodingID as ASCII'); } else { throw Exception( - 'unknown encoding $encodingID, platformID: $platformID'); + 'unknown encoding $encodingID, platformID: $platformID', + ); } var tid = 'p$platformID,${languageID.toRadixString(16)}'; @@ -306,7 +297,6 @@ class FontParser { tname = p; break; } - print(obj); return obj[tname]; } } diff --git a/lib/src/utils/image_builder.dart b/lib/src/utils/image_builder.dart index a3591dca..da306588 100644 --- a/lib/src/utils/image_builder.dart +++ b/lib/src/utils/image_builder.dart @@ -42,8 +42,10 @@ class UltimateImageBuilder extends StatefulWidget { this.node, this.blendMode, this.useInk = false, - }) : assert(url != null || bytes != null || paint != null, - 'url or bytes or paint must be provided') + }) : assert( + url != null || bytes != null || paint != null, + 'url or bytes or paint must be provided', + ) /*assert( paint == null || (width != null && height != null && node != null), 'width, height and node must be provided when paint is provided')*/ @@ -64,10 +66,13 @@ class UltimateImageBuilder extends StatefulWidget { properties.add(EnumProperty('repeat', repeat)); properties.add(ColorProperty('color', color)); properties.add( - ObjectFlagProperty.has('errorBuilder', errorBuilder)); - properties.add(ObjectFlagProperty< - Widget Function(BuildContext context, Widget? child)>.has( - 'loadingBuilder', loadingBuilder)); + ObjectFlagProperty.has('errorBuilder', errorBuilder), + ); + properties.add( + ObjectFlagProperty< + Widget Function(BuildContext context, Widget? child) + >.has('loadingBuilder', loadingBuilder), + ); properties.add(DiagnosticsProperty('paint', paint)); properties.add(DiagnosticsProperty('node', node)); properties.add(EnumProperty('blendMode', blendMode)); @@ -103,7 +108,8 @@ class _UltimateImageBuilderState extends State { widget.color!, widget.blendMode ?? widget.paint?.blendMode.flutterBlendMode ?? - BlendMode.srcIn) + BlendMode.srcIn, + ) : null; double? get width { @@ -156,9 +162,13 @@ class _UltimateImageBuilderState extends State { return; } if (isCustomPositionRequiredForImage( - widget.paint!, SizeC(widget.width!, widget.height!))) { - effectiveChildSize = - getEffectiveChildSizeForImage(widget.node!, widget.paint!); + widget.paint!, + SizeC(widget.width!, widget.height!), + )) { + effectiveChildSize = getEffectiveChildSizeForImage( + widget.node!, + widget.paint!, + ); position = convertAlignmentToPosition( parentWidth: widget.node!.basicBoxGlobal.width, @@ -185,7 +195,7 @@ class _UltimateImageBuilderState extends State { width: effectiveChildSize!.width, height: effectiveChildSize!.height, child: buildImage(withAlignment: false), - ) + ), ], ); } @@ -202,7 +212,8 @@ class _UltimateImageBuilderState extends State { /// TODO: Decorate the rest of the image widgets, not just CachedNetworkImage. Widget buildFromUrl({required bool withAlignment}) { - final String url = widget.url ?? + final String url = + widget.url ?? widget.paint!.croppedImageURL ?? widget.paint!.downloadUrl!; @@ -221,6 +232,8 @@ class _UltimateImageBuilderState extends State { placeholderBuilder: widget.loadingBuilder != null ? (context) => widget.loadingBuilder!(context, null) : null, + errorBuilder: (context, error, stackTrace) => + (widget.errorBuilder ?? _defaultErrorBuilder)(context), ); } else if (url.isBase64Blob) { child = Image.memory( @@ -233,7 +246,7 @@ class _UltimateImageBuilderState extends State { repeat: repeat, color: widget.color, colorBlendMode: colorBlendMode, - errorBuilder: (context, _, __) => + errorBuilder: (context, _, _) => (widget.errorBuilder ?? _defaultErrorBuilder)(context), ); } else { @@ -251,10 +264,13 @@ class _UltimateImageBuilderState extends State { colorBlendMode: colorBlendMode, loadingBuilder: widget.loadingBuilder != null ? (context, child, loadingProgress) => - widget.loadingBuilder!(context, child) + widget.loadingBuilder!(context, child) : null, errorBuilder: (context, error, stackTrace) { - print('Image Loading Error: $error'); + DebugLogger.instance.printInfo( + 'Image Loading Error: $error', + name: 'ImageBuilder', + ); return (widget.errorBuilder ?? _defaultErrorBuilder)(context); }, ); @@ -307,7 +323,7 @@ class _UltimateImageBuilderState extends State { repeat: repeat, color: widget.color, colorBlendMode: colorBlendMode, - errorBuilder: (context, _, __) => + errorBuilder: (context, _, _) => (widget.errorBuilder ?? _defaultErrorBuilder)(context), ); } @@ -317,9 +333,7 @@ class _UltimateImageBuilderState extends State { builder: (context, constraints) { if (constraints.maxHeight < 64 || constraints.maxWidth < 64) { return const FittedBox( - child: Center( - child: Icon(Icons.image_outlined), - ), + child: Center(child: Icon(Icons.image_outlined)), ); } return FractionallySizedBox( @@ -336,29 +350,26 @@ class _UltimateImageBuilderState extends State { mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - const Icon( - Icons.image_outlined, - size: 100, - ), + const Icon(Icons.image_outlined, size: 100), Text( 'Variable Image', textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium?.merge( - GoogleFonts.sourceCodePro( - fontSize: 24, - fontWeight: FontWeight.w600, - ), - ), + GoogleFonts.sourceCodePro( + fontSize: 24, + fontWeight: FontWeight.w600, + ), + ), ), Text( path, textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium?.merge( - GoogleFonts.sourceCodePro( - color: Colors.greenAccent.shade700, - fontSize: 24, - ), - ), + GoogleFonts.sourceCodePro( + color: Colors.greenAccent.shade700, + fontSize: 24, + ), + ), ), ], ), diff --git a/lib/src/utils/rgb_to_color.dart b/lib/src/utils/rgb_to_color.dart index 4143dafe..979e52e7 100644 --- a/lib/src/utils/rgb_to_color.dart +++ b/lib/src/utils/rgb_to_color.dart @@ -32,8 +32,11 @@ Color retrieveFillColor(GeometryMixin node) { } /// Converts the strokes of a given [node] to a single [Color] object. -Color retrieveStrokeColor(GeometryMixin node, - [int index = 0, bool all = false]) { +Color retrieveStrokeColor( + GeometryMixin node, [ + int index = 0, + bool all = false, +]) { final strokes = node.strokes.visible(); if (strokes.isEmpty) { // Some methods expect a non-null color. @@ -59,9 +62,11 @@ Color retrieveStrokeColor(GeometryMixin node, if (stroke == null) { final List gradients = strokes - .where((element) => - element.gradientStops != null && - element.gradientStops!.isNotEmpty) + .where( + (element) => + element.gradientStops != null && + element.gradientStops!.isNotEmpty, + ) .toList(); if (gradients.isNotEmpty) { @@ -94,7 +99,9 @@ Color retrieveStrokeColor(GeometryMixin node, /// Converts a [PaintModel] to a [Color] object. Color retrieveColorFromPaint(PaintModel paint) { - assert(paint.type == PaintType.solid, - 'retrieveColorFromPaint() only accepts Paints that are PaintType.SOLID'); + assert( + paint.type == PaintType.solid, + 'retrieveColorFromPaint() only accepts Paints that are PaintType.SOLID', + ); return paint.toFlutterColor()!; } diff --git a/lib/src/utils/typed_image_bytes.dart b/lib/src/utils/typed_image_bytes.dart index 7c96cd40..a8cd75ee 100644 --- a/lib/src/utils/typed_image_bytes.dart +++ b/lib/src/utils/typed_image_bytes.dart @@ -25,13 +25,13 @@ enum ResourceType { bool get isStandardImage => isPng || isJpg || isWebp; factory ResourceType.fromExtension(String type) => switch (type) { - 'svg' || '.svg' => ResourceType.svg, - 'png' || '.png' => ResourceType.png, - 'jpg' || '.jpg' || 'jpeg' || '.jpeg' => ResourceType.jpg, - 'gif' || '.gif' => ResourceType.gif, - 'webp' || '.webp' => ResourceType.webp, - _ => ResourceType.unknown, - }; + 'svg' || '.svg' => ResourceType.svg, + 'png' || '.png' => ResourceType.png, + 'jpg' || '.jpg' || 'jpeg' || '.jpeg' => ResourceType.jpg, + 'gif' || '.gif' => ResourceType.gif, + 'webp' || '.webp' => ResourceType.webp, + _ => ResourceType.unknown, + }; factory ResourceType.fromUrl(String url) => ResourceType.fromExtension(path.extension(Uri.parse(url).path)); diff --git a/lib/src/utils/utils.dart b/lib/src/utils/utils.dart index 77914750..5d913ec1 100644 --- a/lib/src/utils/utils.dart +++ b/lib/src/utils/utils.dart @@ -89,7 +89,7 @@ enum AlignmentEnum { } /// Parsed data from a variable path using [variablePathRegex]. -class VariableMatch with EquatableMixin { +class VariableMatch with Equatable { final String text; final String name; final String? path; @@ -150,11 +150,15 @@ class VariableMatch with EquatableMixin { } /// Supported image file types that can be dropped on the canvas. -final RegExp supportedAssetTypesRegex = - RegExp('jpg|jpeg|png|webp|gif|svg', caseSensitive: false); +final RegExp supportedAssetTypesRegex = RegExp( + 'jpg|jpeg|png|webp|gif|svg', + caseSensitive: false, +); -final RegExp staticImageTypesRegex = - RegExp('jpg|jpeg|png|webp', caseSensitive: false); +final RegExp staticImageTypesRegex = RegExp( + 'jpg|jpeg|png|webp', + caseSensitive: false, +); /// Returns the most common value in a list. /// Returns null for ties. @@ -228,27 +232,33 @@ BaseNode? getTallestNode(Iterable siblings) { /// largest child by multiplying its width and height as a sort of best /// approximation. BaseNode? getLargestNodeForWrappingStack( - BaseNode parent, List sibling) { + BaseNode parent, + List sibling, +) { if (sibling.isEmpty) return null; // Only horizontal wrap. if (parent.isHorizontalWrap && !parent.isVerticalWrap) { return sibling.reduce( - (a, b) => a.outerBoxLocal.width > b.outerBoxLocal.width ? a : b); + (a, b) => a.outerBoxLocal.width > b.outerBoxLocal.width ? a : b, + ); } // Only vertical wrap, compare heights. if (parent.isVerticalWrap && !parent.isHorizontalWrap) { return sibling.reduce( - (a, b) => a.outerBoxLocal.height > b.outerBoxLocal.height ? a : b); + (a, b) => a.outerBoxLocal.height > b.outerBoxLocal.height ? a : b, + ); } // Both wrap. - return sibling.reduce((a, b) => - a.outerBoxLocal.height * a.outerBoxLocal.width > - b.outerBoxLocal.height * b.outerBoxLocal.width - ? a - : b); + return sibling.reduce( + (a, b) => + a.outerBoxLocal.height * a.outerBoxLocal.width > + b.outerBoxLocal.height * b.outerBoxLocal.width + ? a + : b, + ); } AlignmentModel retrieveCommonStackAlignment( @@ -313,12 +323,14 @@ String apiNameToVariableName(String name) { if (name.startsWith(RegExp(r'\d+'))) { name = name.replaceFirst(RegExp(r'\d+'), '').trim(); } - name = - name.replaceAll(RegExp(r'\s+'), '_').replaceAll(RegExp(r'\W'), '').trim(); + name = name + .replaceAll(RegExp(r'\s+'), '_') + .replaceAll(RegExp(r'\W'), '') + .trim(); final tokens = name.split(RegExp(r'_+')); final sanitizedName = [ SDKConstants.apiVariablePrefix, - ...tokens.map((e) => e.capitalized) + ...tokens.map((e) => e.capitalized), ].join(''); return sanitizedName; } @@ -340,62 +352,60 @@ class ApiResponseVariableUtils { ApiResponseVariableUtils._(); static Map idle(String url) => { - 'url': url, - 'isIdle': true, - 'isLoading': false, - 'isError': false, - 'isSuccess': false, - 'status': 'idle', - }; + 'url': url, + 'isIdle': true, + 'isLoading': false, + 'isError': false, + 'isSuccess': false, + 'status': 'idle', + }; static Map loading(String url, {Object? data}) => { - 'url': url, - 'isIdle': false, - 'isLoading': true, - 'isError': false, - 'isSuccess': false, - 'status': 'loading', - 'data': data, - 'hasData': data != null, - }; + 'url': url, + 'isIdle': false, + 'isLoading': true, + 'isError': false, + 'isSuccess': false, + 'status': 'loading', + 'data': data, + 'hasData': data != null, + }; static Map error( String url, Object? error, { Object? data, Map? headers, - }) => - { - 'url': url, - 'isIdle': false, - 'isLoading': false, - 'isError': true, - 'isSuccess': false, - 'error': error is Map || error is List ? error : error.toString(), - 'status': 'error', - 'headers': headers, - 'hasData': data != null, - 'data': data is Map || data is List ? data : data.toString(), - }; + }) => { + 'url': url, + 'isIdle': false, + 'isLoading': false, + 'isError': true, + 'isSuccess': false, + 'error': error is Map || error is List ? error : error.toString(), + 'status': 'error', + 'headers': headers, + 'hasData': data != null, + 'data': data is Map || data is List ? data : data.toString(), + }; static Map success( String url, Object? data, { int statusCode = 200, Map? headers, - }) => - { - 'url': url, - 'isIdle': false, - 'isLoading': false, - 'isError': false, - 'isSuccess': true, - 'data': data, - 'status': 'error', - 'headers': headers, - 'statusCode': statusCode, - 'hasData': data != null, - }; + }) => { + 'url': url, + 'isIdle': false, + 'isLoading': false, + 'isError': false, + 'isSuccess': true, + 'data': data, + 'status': 'error', + 'headers': headers, + 'statusCode': statusCode, + 'hasData': data != null, + }; static Map fromResponse(http.Response response, {Object? existingData}) { final bool isSuccess = @@ -421,15 +431,15 @@ class ApiResponseVariableUtils { }; } -// static Object? sanitizeValueForJson(Object? value) { -// return switch (value) { -// String() || int() || double() || bool() => value, -// List() => value.map(sanitizeValueForJson).toList(), -// Map() => value.map((key, value) => -// MapEntry(key.toString(), sanitizeValueForJson(value))), -// _ => value?.toString(), -// }; -// } + // static Object? sanitizeValueForJson(Object? value) { + // return switch (value) { + // String() || int() || double() || bool() => value, + // List() => value.map(sanitizeValueForJson).toList(), + // Map() => value.map((key, value) => + // MapEntry(key.toString(), sanitizeValueForJson(value))), + // _ => value?.toString(), + // }; + // } } /// Helper utility class to easily update different statuses for cloud storage variable @@ -437,52 +447,44 @@ class CloudDatabaseVariableUtils { CloudDatabaseVariableUtils._(); static Map idle() => { - 'isIdle': true, - 'isLoading': false, - 'isError': false, - 'isSuccess': false, - 'status': 'idle', - }; + 'isIdle': true, + 'isLoading': false, + 'isError': false, + 'isSuccess': false, + 'status': 'idle', + }; static Map loading({Object? data}) => { - 'isIdle': false, - 'isLoading': true, - 'isError': false, - 'isSuccess': false, - 'status': 'loading', - 'data': data, - 'hasData': data != null, - }; - - static Map error( - Object? error, { - Object? data, - }) => - { - 'isIdle': false, - 'isLoading': false, - 'isError': true, - 'isSuccess': false, - 'error': error is Map || error is List ? error : error.toString(), - 'status': 'error', - 'hasData': data != null, - 'data': data is Map || data is List ? data : data.toString(), - }; - - static Map success( - Object? data, { - String? docId, - }) => - { - 'id': docId, - 'isIdle': false, - 'isLoading': false, - 'isError': false, - 'isSuccess': true, - 'data': data, - 'status': 'error', - 'hasData': data != null, - }; + 'isIdle': false, + 'isLoading': true, + 'isError': false, + 'isSuccess': false, + 'status': 'loading', + 'data': data, + 'hasData': data != null, + }; + + static Map error(Object? error, {Object? data}) => { + 'isIdle': false, + 'isLoading': false, + 'isError': true, + 'isSuccess': false, + 'error': error is Map || error is List ? error : error.toString(), + 'status': 'error', + 'hasData': data != null, + 'data': data is Map || data is List ? data : data.toString(), + }; + + static Map success(Object? data, {String? docId}) => { + 'id': docId, + 'isIdle': false, + 'isLoading': false, + 'isError': false, + 'isSuccess': true, + 'data': data, + 'status': 'error', + 'hasData': data != null, + }; } /// Constructs a [Query] on top of given [Query] reference using given [whereFilters] @@ -515,7 +517,8 @@ Query> constructQueryFromRef( scopedValues: scopedValues, nullSubstitutionMode: nullSubstitutionMode, ); - final Object value = PropertyValueDelegate.substituteVariables( + final Object value = + PropertyValueDelegate.substituteVariables( whereFilter.value, scopedValues: scopedValues, nullSubstitutionMode: nullSubstitutionMode, @@ -523,46 +526,37 @@ Query> constructQueryFromRef( ''; query = switch (whereFilter.operator) { - WhereQueryOperator.equal => query.where( - field, - isEqualTo: value, - ), - WhereQueryOperator.notEqual => query.where( - field, - isNotEqualTo: value, - ), + WhereQueryOperator.equal => query.where(field, isEqualTo: value), + WhereQueryOperator.notEqual => query.where(field, isNotEqualTo: value), WhereQueryOperator.arrayContains => query.where( - field, - arrayContains: value, - ), + field, + arrayContains: value, + ), WhereQueryOperator.greaterThan => query.where( - field, - isGreaterThan: value, - ), + field, + isGreaterThan: value, + ), WhereQueryOperator.greaterThanOrEqual => query.where( - field, - isGreaterThanOrEqualTo: value, - ), - WhereQueryOperator.lessThan => query.where( - field, - isLessThan: value, - ), + field, + isGreaterThanOrEqualTo: value, + ), + WhereQueryOperator.lessThan => query.where(field, isLessThan: value), WhereQueryOperator.lessThanOrEqual => query.where( - field, - isLessThanOrEqualTo: value, - ), + field, + isLessThanOrEqualTo: value, + ), WhereQueryOperator.arrayContainsAny => query.where( - field, - arrayContainsAny: value.typedValue(), - ), + field, + arrayContainsAny: value.typedValue(), + ), WhereQueryOperator.inArray => query.where( - field, - whereIn: value.typedValue(), - ), + field, + whereIn: value.typedValue(), + ), WhereQueryOperator.notInArray => query.where( - field, - whereNotIn: value.typedValue(), - ), + field, + whereNotIn: value.typedValue(), + ), }; } diff --git a/pubspec.yaml b/pubspec.yaml index ffb52650..7156fd09 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,12 +1,20 @@ name: codelessly_sdk description: Codelessly CloudUI™ - Server-driven UI for Flutter. Supercharge your Flutter apps with dynamic UI and real-time updates. Build and publish UI without code! -version: 6.2.0 +version: 7.0.0 homepage: https://codelessly.com/ repository: https://github.com/Codelessly/CodelesslySDK +issue_tracker: https://github.com/Codelessly/CodelesslySDK/issues +documentation: https://docs.codelessly.com/ + +topics: + - server-driven-ui + - ui + - no-code + - widget environment: - sdk: ">=3.5.0 <4.0.0" - flutter: ">=3.24.0" + sdk: ^3.13.0 + flutter: ">=3.47.0" platforms: android: @@ -20,20 +28,19 @@ dependencies: flutter: sdk: flutter # Codelessly - codelessly_api: 4.4.0 - codelessly_json_annotation: 4.9.0+3 + codelessly_api: ^5.0.0 + json_annotation: ^4.12.0 # Firebase - firebase_core: ^3.10.0 - firebase_auth: ^5.4.0 - cloud_firestore: ^5.6.1 - firebase_ui_firestore: ^1.7.1 - google_fonts: ^6.2.1 + firebase_core: ^4.15.0 + firebase_auth: ^6.7.0 + cloud_firestore: ^6.10.0 + firebase_ui_firestore: ^2.1.0 + google_fonts: ^9.0.0 # Core Flutter collection: '>=1.17.0 <2.0.0' http: '>=1.1.2 <2.0.0' - intl: '>=0.17.0 <1.0.0' logging: ^1.3.0 meta: ">=1.9.0 <2.0.0" shared_preferences: ^2.3.5 @@ -45,22 +52,21 @@ dependencies: web: ^1.1.0 # 3rd Party Core Framework - equatable: '>=2.0.0 <3.0.0' + equatable: ^3.0.0 vector_math: ^2.1.4 provider: ^6.1.2 - uuid: ^4.5.1 + cupertino_icons: ^1.0.9 flutter_svg: ^2.0.17 # File and Image path: ^1.9.0 - path_provider: ^2.1.5 # Storage and Caching hive_ce: ^2.9.0 hive_ce_flutter: ^2.2.0 # JSON - json_path: ^0.7.4 + json_path: ^0.9.0 rfc_6901: ^0.2.0 # JSON pointer spec jwt_decoder: ^2.0.1 @@ -70,9 +76,9 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter - codelessly_json_serializable: 6.8.0+1 + json_serializable: ^6.14.1 build_runner: ^2.4.14 - flutter_lints: ^5.0.0 + flutter_lints: ^6.0.0 flutter: uses-material-design: true diff --git a/test/actions_test.dart b/test/actions_test.dart new file mode 100644 index 00000000..0b1ea830 --- /dev/null +++ b/test/actions_test.dart @@ -0,0 +1,106 @@ +import 'dart:async'; + +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:codelessly_sdk/codelessly_sdk.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; + +Reaction callFunction(String name, {bool nonBlocking = false}) => Reaction( + action: CallFunctionAction(name: name, nonBlocking: nonBlocking), + trigger: TriggerModel(type: TriggerType.click), +); + +/// Pumps a tree with [functions] and returns a context inside it. +Future pumpContext( + WidgetTester tester, + Map functions, +) async { + late BuildContext context; + await tester.pumpWidget( + ChangeNotifierProvider.value( + value: CodelesslyContext.empty()..functions = functions, + child: Builder( + builder: (c) { + context = c; + return const SizedBox(); + }, + ), + ), + ); + return context; +} + +void main() { + testWidgets('blocking functions run in order and are awaited', ( + tester, + ) async { + final calls = []; + final first = Completer(); + final context = await pumpContext(tester, { + 'first': (context, reference, params) { + calls.add('first started'); + return first.future.then((_) => calls.add('first done')); + }, + 'second': (context, reference, params) => calls.add('second'), + }); + + final done = FunctionsRepository.triggerAction( + context, + TriggerType.click, + reactions: [callFunction('first'), callFunction('second')], + ); + await tester.pump(); + expect(calls, ['first started']); + + first.complete(); + await tester.runAsync(() => done); + expect(calls, ['first started', 'first done', 'second']); + }); + + testWidgets('a failing function stops the chain without escaping', ( + tester, + ) async { + final calls = []; + final context = await pumpContext(tester, { + 'fails': (context, reference, params) async => throw StateError('x'), + 'throwsSync': (context, reference, params) => throw StateError('y'), + 'next': (context, reference, params) => calls.add('next'), + }); + + for (final failing in ['fails', 'throwsSync']) { + expect( + await FunctionsRepository.triggerAction( + context, + TriggerType.click, + reactions: [callFunction(failing), callFunction('next')], + ), + isTrue, + ); + } + expect(calls, isEmpty); + expect(tester.takeException(), isNull); + }); + + testWidgets('a failing non-blocking function does not stop the chain', ( + tester, + ) async { + final calls = []; + final context = await pumpContext(tester, { + 'fails': (context, reference, params) async => throw StateError('x'), + 'next': (context, reference, params) => calls.add('next'), + }); + + await FunctionsRepository.triggerAction( + context, + TriggerType.click, + reactions: [ + callFunction('fails', nonBlocking: true), + callFunction('next'), + ], + ); + await tester.pump(); + expect(calls, ['next']); + expect(tester.takeException(), isNull); + }); +} diff --git a/test/auth_manager_test.dart b/test/auth_manager_test.dart new file mode 100644 index 00000000..757dfc55 --- /dev/null +++ b/test/auth_manager_test.dart @@ -0,0 +1,130 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:codelessly_sdk/codelessly_sdk.dart'; +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:http/http.dart' as http; + +class _FakeIdTokenResult extends Fake implements IdTokenResult { + _FakeIdTokenResult(this.claims) : token = _jwt(claims); + + @override + final Map? claims; + + @override + final String? token; + + static String _jwt(Map? claims) { + String part(Object json) => + base64Url.encode(utf8.encode(jsonEncode(json))).replaceAll('=', ''); + return '${part({'alg': 'none'})}.${part(claims ?? {})}.signature'; + } +} + +class _FakeUser extends Fake implements User { + _FakeUser(this.result); + + final Future Function() result; + + @override + Future getIdTokenResult([bool forceRefresh = false]) => + result(); +} + +class _FakeAuth extends Fake implements FirebaseAuth { + _FakeAuth(this.currentUser, this.changes); + + @override + final User? currentUser; + + final Stream changes; + + @override + Stream userChanges() => changes; +} + +class _NoCache extends Fake implements CacheManager {} + +AuthManager authManager(FirebaseAuth auth) => AuthManager( + config: CodelesslyConfig(authToken: 'token'), + cacheManager: _NoCache(), + firebaseAuth: auth, + client: http.Client(), + claimsPropagationTimeout: const Duration(milliseconds: 100), +); + +AuthData authData() => AuthData( + authToken: 'token', + projectId: 'project', + timestamp: DateTime(2026), + owner: 'owner', + teams: const {}, + users: const {}, + roles: const {}, +); + +void main() { + group('checkClaimsForProject', () { + final result = _FakeIdTokenResult({ + 'project_ids': ['project'], + }); + + test('reads the claims', () { + final manager = authManager(_FakeAuth(null, const Stream.empty())); + expect(manager.checkClaimsForProject(result, 'project'), isTrue); + expect(manager.checkClaimsForProject(result, 'other'), isFalse); + }); + + test('decodes the token on Windows', () { + debugDefaultTargetPlatformOverride = TargetPlatform.windows; + addTearDown(() => debugDefaultTargetPlatformOverride = null); + final manager = authManager(_FakeAuth(null, const Stream.empty())); + expect(manager.checkClaimsForProject(result, 'project'), isTrue); + }); + }); + + group('postAuthSuccess', () { + test('completes once the project claim arrives', () async { + final changes = StreamController(); + addTearDown(changes.close); + var claims = {}; + final user = _FakeUser(() async => _FakeIdTokenResult(claims)); + final manager = authManager(_FakeAuth(user, changes.stream)); + + final done = manager.postAuthSuccess(authData()); + await Future.delayed(Duration.zero); + claims = { + 'project_ids': ['project'], + }; + changes.add(user); + await expectLater(done, completes); + }); + + test('times out when the claim never arrives', () async { + final user = _FakeUser(() async => _FakeIdTokenResult({})); + final manager = authManager(_FakeAuth(user, const Stream.empty())); + await expectLater( + manager.postAuthSuccess(authData()), + throwsA(isA()), + ); + }); + + test('fails instead of hanging when refreshing the token fails', () async { + final changes = StreamController(); + addTearDown(changes.close); + var calls = 0; + final user = _FakeUser(() async { + if (calls++ == 0) return _FakeIdTokenResult({}); + throw FirebaseAuthException(code: 'network-request-failed'); + }); + final manager = authManager(_FakeAuth(user, changes.stream)); + + final done = manager.postAuthSuccess(authData()); + await Future.delayed(Duration.zero); + changes.add(user); + await expectLater(done, throwsA(isA())); + }); + }); +} diff --git a/test/cloud_database_test.dart b/test/cloud_database_test.dart new file mode 100644 index 00000000..60f5d3b7 --- /dev/null +++ b/test/cloud_database_test.dart @@ -0,0 +1,92 @@ +// Firestore marks its references @sealed; these test fakes stand in for them. +// ignore_for_file: subtype_of_sealed_class + +import 'dart:async'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:codelessly_sdk/codelessly_sdk.dart'; +import 'package:flutter_test/flutter_test.dart'; + +/// One snapshot stream per collection path, so tests can see who listens. +final Map>>> +snapshotStreams = {}; + +class _FakeCollection extends Fake + implements CollectionReference> { + _FakeCollection(this.path); + + @override + final String path; + + @override + DocumentReference> doc([String? id]) => + _FakeDoc('$path/$id'); + + @override + Stream>> snapshots({ + bool includeMetadataChanges = false, + ListenSource source = ListenSource.defaultSource, + }) => snapshotStreams.putIfAbsent(path, StreamController.new).stream; +} + +class _FakeDoc extends Fake implements DocumentReference> { + _FakeDoc(this.path); + + @override + final String path; + + @override + CollectionReference> collection(String id) => + _FakeCollection('$path/$id'); +} + +class _FakeFirestore extends Fake implements FirebaseFirestore { + @override + CollectionReference> collection(String path) => + _FakeCollection(path); +} + +void main() { + late FirestoreCloudDatabase database; + late Observable variable; + + void stream(String path) => database.streamCollectionToVariable( + path, + variable, + whereFilters: const [], + orderByOperations: const [], + scopedValues: ScopedValues.empty(), + nullSubstitutionMode: NullSubstitutionMode.emptyString, + ); + + bool listening(String path) => snapshotStreams.entries + .firstWhere((entry) => entry.key.endsWith('/$path')) + .value + .hasListener; + + setUp(() { + snapshotStreams.clear(); + database = FirestoreCloudDatabase( + 'project', + PublishSource.publish, + firestore: _FakeFirestore(), + ); + variable = Observable(VariableData(id: 'items', name: 'items')); + }); + + test('a new query for a variable replaces its previous listener', () { + stream('first'); + expect(listening('first'), isTrue); + + stream('second'); + expect(listening('first'), isFalse); + expect(listening('second'), isTrue); + }); + + test('reset cancels every listener', () { + stream('first'); + database.reset(); + expect(listening('first'), isFalse); + }); +} diff --git a/test/data_manager_test.dart b/test/data_manager_test.dart new file mode 100644 index 00000000..00e37ec5 --- /dev/null +++ b/test/data_manager_test.dart @@ -0,0 +1,66 @@ +import 'dart:async'; + +import 'package:cloud_firestore/cloud_firestore.dart'; +import 'package:codelessly_sdk/codelessly_sdk.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class _StreamingRepository extends Fake implements NetworkDataRepository { + _StreamingRepository(this.stream); + + final Stream stream; + + @override + Stream streamPublishModel({ + required String projectID, + required PublishSource source, + }) => stream; +} + +DataManager dataManager(Stream stream) => DataManager( + 'test', + config: CodelesslyConfig(authToken: 'token'), + cacheManager: _FakeCache(), + authManager: _FakeAuthManager(), + networkDataRepository: _StreamingRepository(stream), + localDataRepository: _FakeLocalRepository(), + firebaseFirestore: _FakeFirestore(), + errorLogger: ErrorLogger.instance, +); + +class _FakeCache extends Fake implements CacheManager {} + +class _FakeAuthManager extends Fake implements AuthManager {} + +class _FakeLocalRepository extends Fake implements LocalDataRepository {} + +class _FakeFirestore extends Fake implements FirebaseFirestore {} + +void main() { + test('the first publish model completes listenToPublishModel', () async { + final model = SDKPublishModel( + projectId: 'project', + owner: 'owner', + teams: const {}, + users: const {}, + roles: const {}, + public: false, + ); + final manager = dataManager(Stream.value(model)); + expect(await manager.listenToPublishModel('project'), model); + }); + + test( + 'a stream error fails listenToPublishModel instead of hanging', + () async { + final manager = dataManager( + Stream.error(FirebaseException(plugin: 'firestore', code: 'denied')), + ); + await expectLater( + manager + .listenToPublishModel('project') + .timeout(const Duration(seconds: 5)), + throwsA(isA()), + ); + }, + ); +} diff --git a/test/icon_test.dart b/test/icon_test.dart new file mode 100644 index 00000000..494fdb56 --- /dev/null +++ b/test/icon_test.dart @@ -0,0 +1,106 @@ +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:codelessly_sdk/codelessly_sdk.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('Material icon URLs', () { + MaterialIcon icon(MaterialIconStyle style) => MaterialIcon( + codepoint: 0xe88a, + fontFamily: 'MaterialIcons', + name: 'home', + style: style, + version: 16, + ); + + test('use the style in the path', () { + expect( + icon(MaterialIconStyle.outlined).toSvgUrl(), + 'https://fonts.gstatic.com/s/i/materialiconsoutlined/home/v16/24px.svg', + ); + expect( + icon(MaterialIconStyle.filled).toSvgUrl(), + 'https://fonts.gstatic.com/s/i/materialicons/home/v16/24px.svg', + ); + }); + + test('two-tone has no space in the path', () { + expect( + icon(MaterialIconStyle.twoTone).toSvgUrl(), + 'https://fonts.gstatic.com/s/i/materialiconstwotone/home/v16/24px.svg', + ); + }); + }); + + group('Material Symbol URLs', () { + final symbol = MaterialSymbol( + codepoint: 0xe88a, + fontFamily: 'MaterialSymbolsRounded', + name: 'home', + style: MaterialSymbolStyle.rounded, + ); + + test('default axes', () { + expect( + symbol.toSvgUrl(), + 'https://fonts.gstatic.com/s/i/short-term/release/' + 'materialsymbolsrounded/home/default/24px.svg', + ); + }); + + test('axes are snapped to the prebuilt variants', () { + expect( + symbol.toSvgUrl(fill: 1, weight: 680, grade: -20, opticalSize: 46), + 'https://fonts.gstatic.com/s/i/short-term/release/' + 'materialsymbolsrounded/home/wght700gradN25fill1/48px.svg', + ); + }); + + test('variant names', () { + expect(materialSymbolVariant(), 'default'); + expect(materialSymbolVariant(weight: 400, grade: 0, fill: 0), 'default'); + expect(materialSymbolVariant(weight: 100), 'wght100'); + expect(materialSymbolVariant(grade: 150), 'grad200'); + expect(materialSymbolVariant(fill: 0.6), 'fill1'); + expect( + materialSymbolVariant(weight: 300, grade: -25, fill: 1), + 'wght300gradN25fill1', + ); + }); + }); + + testWidgets('Cupertino icons render from the cupertino_icons font', ( + tester, + ) async { + await tester.pumpWidget( + MaterialApp( + home: SvgIcon( + icon: CupertinoIcon( + codepoint: CupertinoIcons.heart.codePoint, + name: 'heart', + ), + size: 30, + ), + ), + ); + final Icon icon = tester.widget(find.byType(Icon)); + expect(icon.icon, CupertinoIcons.heart); + expect(icon.size, 30); + }); + + testWidgets('Icons without a source render an empty box', (tester) async { + await tester.pumpWidget( + MaterialApp( + home: Center( + child: SvgIcon( + icon: CupertinoIcon(codepoint: 0x1, name: 'unknown'), + size: 20, + ), + ), + ), + ); + expect(find.byType(Icon), findsNothing); + expect(tester.getSize(find.byType(SizedBox).last), const Size(20, 20)); + }); +} diff --git a/test/smoke_test.dart b/test/smoke_test.dart new file mode 100644 index 00000000..57a2ba97 --- /dev/null +++ b/test/smoke_test.dart @@ -0,0 +1,72 @@ +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:codelessly_sdk/codelessly_sdk.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Widget host(Widget child) => MaterialApp( + home: Scaffold(body: Center(child: child)), +); + +void main() { + const settings = WidgetBuildSettings(debugLabel: 'test'); + + testWidgets('radio renders inside a RadioGroup', (tester) async { + final node = RadioNode( + id: 'r', + name: 'r', + basicBoxLocal: NodeBox(0, 0, 40, 40), + value: 'a', + groupValue: 'b', + properties: RadioProperties( + innerRadius: 5, + backgroundColor: ColorRGBA.white, + side: const BorderSideModel(width: 2), + ), + ); + String? changed; + await tester.pumpWidget( + host( + PassiveRadioWidget( + node: node, + onChanged: (_, v) => changed = v, + settings: settings, + ), + ), + ); + expect(find.byType(RadioGroup), findsOneWidget); + expect(tester.takeException(), isNull); + await tester.tap(find.byType(Radio)); + expect(changed, 'a'); + }); + + testWidgets('slider renders every shape and indicator mode', (tester) async { + for (final track in SliderTrackShapeEnum.values) { + for (final indicator in SliderValueIndicatorShape.values) { + final node = SliderNode( + id: 's', + name: 's', + basicBoxLocal: NodeBox(0, 0, 300, 48), + value: 20, + properties: SliderProperties( + trackShape: track, + valueIndicatorShape: indicator, + showValueIndicator: ShowValueIndicatorC.alwaysVisible, + secondaryTrackValue: 50, + padding: const EdgeInsetsModel.all(4), + ), + ); + await tester.pumpWidget( + host( + TransformerSlider( + node: node, + value: 20, + onChanged: (_) {}, + settings: settings, + ), + ), + ); + expect(tester.takeException(), isNull, reason: '$track $indicator'); + } + } + }); +} diff --git a/test/transformers_test.dart b/test/transformers_test.dart new file mode 100644 index 00000000..4bc27c1c --- /dev/null +++ b/test/transformers_test.dart @@ -0,0 +1,77 @@ +import 'package:codelessly_api/codelessly_api.dart'; +import 'package:codelessly_sdk/codelessly_sdk.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; + +/// Nodes that build without children or remote data. +List standaloneNodes() => [ + ButtonNode.empty(), + CheckboxNode.empty(), + DividerNode.empty(), + FloatingActionButtonNode.empty(), + // A Cupertino icon renders from the bundled font; Material icons load + // SVGs over the network, which widget tests cannot reach. + IconNode( + id: 'icon', + name: 'Icon', + basicBoxLocal: NodeBox(0, 0, 24, 24), + icon: CupertinoIcon( + codepoint: CupertinoIcons.heart.codePoint, + name: 'heart', + ), + ), + ProgressBarNode.empty(), + RadioNode.empty(), + SliderNode.empty(), + SwitchNode.empty(), + ListTileNode.empty(), + ExpansionTileNode.empty(), + TextNode( + id: 'text', + name: 'Text', + basicBoxLocal: NodeBox(0, 0, 100, 20), + characters: 'Hello', + ), + AccordionNode( + id: 'accordion', + name: 'Accordion', + basicBoxLocal: NodeBox(0, 0, 200, 100), + children: const [], + properties: AccordionProperties(isExpanded: true), + ), +]; + +void main() { + for (final node in standaloneNodes()) { + testWidgets('${node.type} builds without errors', (tester) async { + final registry = NodeRegistry()..setNodes({node.id: node}); + final manager = PassiveNodeTransformerManager( + registry.getNodeByID, + (context, bounds, pageID, layoutID, canvasID) => const SizedBox(), + ); + + await tester.pumpWidget( + ChangeNotifierProvider.value( + value: CodelesslyContext.empty(), + child: MaterialApp( + home: Scaffold( + body: Center( + child: Builder( + builder: (context) => manager.buildWidgetFromNode( + node, + context, + settings: const WidgetBuildSettings(debugLabel: 'test'), + ), + ), + ), + ), + ), + ), + ); + await tester.pump(); + expect(tester.takeException(), isNull); + }); + } +} diff --git a/tool/generate_cupertino_icons.py b/tool/generate_cupertino_icons.py new file mode 100644 index 00000000..2b7bfab5 --- /dev/null +++ b/tool/generate_cupertino_icons.py @@ -0,0 +1,39 @@ +"""Generates lib/src/utils/cupertino_icons_by_codepoint.dart. + +Maps each CupertinoIcons codepoint to its const IconData so Cupertino icon +models can render with the cupertino_icons font while icon tree shaking still +works. Run with the Flutter SDK whose CupertinoIcons should be mirrored: + + python3 tool/generate_cupertino_icons.py /path/to/flutter +""" +import re +import sys +from pathlib import Path + +flutter = Path(sys.argv[1] if len(sys.argv) > 1 else '/root/flutter') +source = (flutter / 'packages/flutter/lib/src/cupertino/icons.dart').read_text() + +entries = {} +for match in re.finditer( + r'static const IconData (\w+) = IconData\(\s*(0x[0-9a-fA-F]+)', source +): + name, codepoint = match.group(1), int(match.group(2), 16) + # Several names alias one glyph; the first declared name wins. + entries.setdefault(codepoint, name) + +lines = [ + '// GENERATED CODE - DO NOT MODIFY BY HAND.', + '// Generated by tool/generate_cupertino_icons.py from Flutter\'s', + '// CupertinoIcons.', + '', + "import 'package:flutter/cupertino.dart';", + '', + '/// Flutter\'s [CupertinoIcons] keyed by codepoint.', + 'const Map cupertinoIconsByCodepoint = {', +] +for codepoint in sorted(entries): + lines.append(f' 0x{codepoint:x}: CupertinoIcons.{entries[codepoint]},') +lines.append('};') +out = Path(__file__).resolve().parent.parent / 'lib/src/utils/cupertino_icons_by_codepoint.dart' +out.write_text('\n'.join(lines) + '\n') +print(f'{len(entries)} icons -> {out}')