├── dart_test.yaml ├── example ├── linux │ ├── .gitignore │ ├── main.cc │ ├── flutter │ │ ├── generated_plugin_registrant.cc │ │ ├── generated_plugin_registrant.h │ │ └── generated_plugins.cmake │ └── my_application.h ├── README.md ├── ios │ ├── Runner │ │ ├── Runner-Bridging-Header.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ ├── AppDelegate.swift │ │ └── Base.lproj │ │ │ └── Main.storyboard │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner.xcodeproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── RunnerTests │ │ └── RunnerTests.swift │ ├── .gitignore │ ├── Podfile.lock │ └── Podfile ├── web │ ├── favicon.png │ ├── icons │ │ ├── Icon-192.png │ │ ├── Icon-512.png │ │ ├── Icon-maskable-192.png │ │ └── Icon-maskable-512.png │ └── manifest.json ├── android │ ├── gradle.properties │ ├── app │ │ └── src │ │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── liveview_flutter │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── build.gradle │ └── settings.gradle ├── macos │ ├── Runner │ │ ├── Configs │ │ │ ├── Debug.xcconfig │ │ │ ├── Release.xcconfig │ │ │ ├── Warnings.xcconfig │ │ │ └── AppInfo.xcconfig │ │ ├── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ ├── app_icon_16.png │ │ │ │ ├── app_icon_32.png │ │ │ │ ├── app_icon_64.png │ │ │ │ ├── app_icon_1024.png │ │ │ │ ├── app_icon_128.png │ │ │ │ ├── app_icon_256.png │ │ │ │ ├── app_icon_512.png │ │ │ │ └── Contents.json │ │ ├── AppDelegate.swift │ │ ├── Release.entitlements │ │ ├── DebugProfile.entitlements │ │ ├── MainFlutterWindow.swift │ │ └── Info.plist │ ├── .gitignore │ ├── Flutter │ │ ├── Flutter-Debug.xcconfig │ │ ├── Flutter-Release.xcconfig │ │ └── GeneratedPluginRegistrant.swift │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── Runner.xcodeproj │ │ └── project.xcworkspace │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── RunnerTests │ │ └── RunnerTests.swift │ └── Podfile ├── windows │ ├── runner │ │ ├── resources │ │ │ └── app_icon.ico │ │ ├── resource.h │ │ ├── utils.h │ │ ├── runner.exe.manifest │ │ ├── flutter_window.h │ │ └── main.cpp │ ├── flutter │ │ ├── generated_plugin_registrant.cc │ │ ├── generated_plugin_registrant.h │ │ └── generated_plugins.cmake │ └── .gitignore ├── devtools_options.yaml ├── .gitignore ├── lib │ └── main.dart └── analysis_options.yaml ├── lib ├── live_view │ ├── ui │ │ ├── theme │ │ │ └── theme_loader.dart │ │ ├── root_view │ │ │ ├── internal_view.dart │ │ │ ├── root_material_app.dart │ │ │ ├── root_view.dart │ │ │ ├── root_app_bar.dart │ │ │ └── root_bottom_navigation_bar.dart │ │ ├── components │ │ │ ├── live_bottom_sheet.dart │ │ │ ├── live_stack.dart │ │ │ ├── live_view_body.dart │ │ │ ├── live_expanded.dart │ │ │ ├── live_link.dart │ │ │ ├── live_title_attribute.dart │ │ │ ├── live_avatar_attribute.dart │ │ │ ├── live_hint_attribute.dart │ │ │ ├── live_icon_attribute.dart │ │ │ ├── live_label_attribute.dart │ │ │ ├── live_leading_attribute.dart │ │ │ ├── live_subtitle_attribute.dart │ │ │ ├── live_trailing_attribute.dart │ │ │ ├── live_content_attribute.dart │ │ │ ├── live_underline_attribute.dart │ │ │ ├── live_disabled_hint_attribute.dart │ │ │ ├── live_icon_selected_attribute.dart │ │ │ ├── live_persistent_footer_button.dart │ │ │ ├── live_sized_box.dart │ │ │ ├── live_filled_button.dart │ │ │ ├── live_center.dart │ │ │ ├── live_positioned.dart │ │ │ ├── live_cached_networked_image.dart │ │ │ ├── live_safe_area.dart │ │ │ ├── live_drawer.dart │ │ │ ├── live_text.dart │ │ │ ├── live_end_drawer.dart │ │ │ ├── live_container.dart │ │ │ ├── live_drawer_header.dart │ │ │ ├── live_scaffold.dart │ │ │ ├── live_bottom_app_bar.dart │ │ │ ├── live_card.dart │ │ │ ├── live_icon.dart │ │ │ ├── live_checkbox.dart │ │ │ ├── live_text_button.dart │ │ │ ├── live_elevated_button.dart │ │ │ ├── live_row.dart │ │ │ ├── live_column.dart │ │ │ └── live_single_child_scroll_view.dart │ │ ├── live_view_ui_registry.dart │ │ ├── page_transition.dart │ │ ├── errors │ │ │ ├── error_404.dart │ │ │ ├── flutter_error_view.dart │ │ │ ├── no_server_error_view.dart │ │ │ └── missing_page_component.dart │ │ ├── loading │ │ │ └── reload_widget.dart │ │ └── node_state.dart │ ├── reactive │ │ ├── live_go_back_notifier.dart │ │ ├── live_connection_notifier.dart │ │ └── state_notifier.dart │ ├── mapping │ │ ├── boolean.dart │ │ ├── duration.dart │ │ ├── axis_direction.dart │ │ ├── text_direction.dart │ │ ├── mouse_cursor.dart │ │ ├── notched_shape.dart │ │ ├── text_baseline.dart │ │ ├── drag_start_behavior.dart │ │ ├── options_view_open_direction.dart │ │ ├── material_tap_target_size.dart │ │ ├── number.dart │ │ ├── shape_border.dart │ │ ├── clip_behavior.dart │ │ ├── tooltip_trigger_mode.dart │ │ ├── overflow_bar_alignment.dart │ │ ├── scroll_view_keyboard_dismiss_behavior.dart │ │ ├── navigation_rail_label_type.dart │ │ ├── visual_density.dart │ │ ├── border_radius.dart │ │ ├── text_align.dart │ │ ├── list_title_alignment.dart │ │ ├── box_height_style.dart │ │ ├── button_style.dart │ │ ├── material_state.dart │ │ ├── bottom_navigation_bar_type.dart │ │ ├── input_decoration.dart │ │ ├── alignment_directional.dart │ │ ├── keyboard_type.dart │ │ ├── edge_insets.dart │ │ ├── decoration.dart │ │ ├── border.dart │ │ ├── axis_alignment.dart │ │ └── css.dart │ ├── plugin.dart │ ├── state │ │ └── element_key.dart │ ├── routes │ │ ├── no_transition_page.dart │ │ └── live_custom_page.dart │ └── webdocs.dart ├── liveview_flutter.dart ├── exec │ ├── exec_go_back.dart │ ├── exec_save_current_theme.dart │ ├── exec_live_patch.dart │ ├── exec_show_bottom_sheet.dart │ ├── exec_switch_theme.dart │ ├── exec_live_event.dart │ ├── exec_phx_href.dart │ ├── exec.dart │ ├── live_view_exec_registry.dart │ └── exec_confirmable.dart └── platform_name.dart ├── CHANGELOG.md ├── test ├── components │ ├── card_test.png │ ├── modal_test.png │ ├── action_chip_test.png │ ├── icon_button_test.png │ ├── list_title_test.png │ ├── text_button_test.png │ ├── bottom_sheet_test.png │ ├── modal_closed_test.png │ ├── modal_opened_test.png │ ├── show_scaffold_test.png │ ├── bottom_app_bar_test.png │ ├── material_banner_test.png │ ├── foating_action_button_test.png │ ├── show_scaffold_removed_test.png │ ├── persistent_footer_button_test.png │ ├── foating_action_button_extended_test.png │ ├── text_button_test.dart │ ├── card_test.dart │ ├── icon_button_test.dart │ ├── list_tile_test.dart │ ├── persistent_footer_button_test.dart │ ├── material_banner_test.dart │ ├── bottom_app_bar_test.dart │ ├── bottom_sheet_test.dart │ ├── floating_action_button_test.dart │ └── action_chip_test.dart ├── theme │ └── switch_theme_test.png ├── forms │ ├── elevated_button_test.png │ ├── live_segmented_button.png │ ├── value_attribute_test.dart │ └── text_field_test.dart ├── render │ ├── implicit_column_test.png │ ├── button_text_test.dart │ ├── data_phx_main_test.dart │ ├── implicit_column_test.dart │ ├── render_with_variables_test.dart │ ├── single_child_test.dart │ ├── render_component_slots_test.dart │ ├── whole_component_update_test.dart │ ├── multiple_components_test.dart │ ├── margin_test.dart │ ├── padding_test.dart │ └── dynamic_diff_test.dart ├── navigation │ ├── navigation_rail_test.png │ ├── transitions_test_loading.png │ └── transitions_test_second_page.png ├── screenshots │ ├── bottom_navigation_bar.png │ └── bottom_navigation_bar_test.dart ├── errors │ └── missing_view_body_test.dart ├── plugins │ ├── basic_plugin_test.dart │ └── basic_plugin.dart ├── events │ └── phx_onload_test.dart ├── mapping │ ├── colors_test.dart │ ├── edge_insets_test.dart │ ├── css_test.dart │ ├── edge_colors_test.dart │ └── text_style_test.dart ├── network │ ├── reconnect_test.dart │ └── cookies_test.dart └── when │ └── when_test.dart ├── documentation ├── execute-actions.md ├── lifecycle.md └── widgets.md ├── .metadata ├── .gitignore └── analysis_options.yaml /dart_test.yaml: -------------------------------------------------------------------------------- 1 | tags: 2 | golden: 3 | -------------------------------------------------------------------------------- /example/linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /lib/live_view/ui/theme/theme_loader.dart: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.1 2 | 3 | * TODO: Describe initial release. 4 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | A basic demo project for the live view client 4 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/web/favicon.png -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /lib/liveview_flutter.dart: -------------------------------------------------------------------------------- 1 | library liveview_flutter; 2 | 3 | export 'package:liveview_flutter/live_view/live_view.dart'; 4 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /example/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/web/icons/Icon-512.png -------------------------------------------------------------------------------- /test/components/card_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/test/components/card_test.png -------------------------------------------------------------------------------- /test/components/modal_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/test/components/modal_test.png -------------------------------------------------------------------------------- /example/macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /test/theme/switch_theme_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/test/theme/switch_theme_test.png -------------------------------------------------------------------------------- /test/components/action_chip_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/test/components/action_chip_test.png -------------------------------------------------------------------------------- /test/components/icon_button_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/test/components/icon_button_test.png -------------------------------------------------------------------------------- /test/components/list_title_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/test/components/list_title_test.png -------------------------------------------------------------------------------- /test/components/text_button_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/test/components/text_button_test.png -------------------------------------------------------------------------------- /test/forms/elevated_button_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/test/forms/elevated_button_test.png -------------------------------------------------------------------------------- /test/forms/live_segmented_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/test/forms/live_segmented_button.png -------------------------------------------------------------------------------- /test/render/implicit_column_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/test/render/implicit_column_test.png -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /test/components/bottom_sheet_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/test/components/bottom_sheet_test.png -------------------------------------------------------------------------------- /test/components/modal_closed_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/test/components/modal_closed_test.png -------------------------------------------------------------------------------- /test/components/modal_opened_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/test/components/modal_opened_test.png -------------------------------------------------------------------------------- /test/components/show_scaffold_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/test/components/show_scaffold_test.png -------------------------------------------------------------------------------- /example/web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /test/components/bottom_app_bar_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/test/components/bottom_app_bar_test.png -------------------------------------------------------------------------------- /test/components/material_banner_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/test/components/material_banner_test.png -------------------------------------------------------------------------------- /test/navigation/navigation_rail_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/test/navigation/navigation_rail_test.png -------------------------------------------------------------------------------- /test/screenshots/bottom_navigation_bar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/test/screenshots/bottom_navigation_bar.png -------------------------------------------------------------------------------- /example/windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /test/components/foating_action_button_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/test/components/foating_action_button_test.png -------------------------------------------------------------------------------- /test/components/show_scaffold_removed_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/test/components/show_scaffold_removed_test.png -------------------------------------------------------------------------------- /test/navigation/transitions_test_loading.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/test/navigation/transitions_test_loading.png -------------------------------------------------------------------------------- /test/navigation/transitions_test_second_page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/test/navigation/transitions_test_second_page.png -------------------------------------------------------------------------------- /example/macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /test/components/persistent_footer_button_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/test/components/persistent_footer_button_test.png -------------------------------------------------------------------------------- /example/macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /test/components/foating_action_button_extended_test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/test/components/foating_action_button_extended_test.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /lib/live_view/reactive/live_go_back_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class LiveGoBackNotifier extends ChangeNotifier { 4 | void notify() => notifyListeners(); 5 | } 6 | -------------------------------------------------------------------------------- /lib/live_view/reactive/live_connection_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class LiveConnectionNotifier extends ChangeNotifier { 4 | void wipeState() => notifyListeners(); 5 | } 6 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /example/devtools_options.yaml: -------------------------------------------------------------------------------- 1 | description: This file stores settings for Dart & Flutter DevTools. 2 | documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states 3 | extensions: 4 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/linux/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/example/liveview_flutter/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.liveview_flutter 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alex-min/live_view_native_flutter_client/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /documentation/execute-actions.md: -------------------------------------------------------------------------------- 1 | # How to execute actions on the client 2 | 3 | Properties on the server-side which execute actions like phx-click, live-patch or switchTheme are defined in the client as "Execs". 4 | 5 | 6 | [....Add more here...] -------------------------------------------------------------------------------- /lib/live_view/mapping/boolean.dart: -------------------------------------------------------------------------------- 1 | bool? getBoolean(String? prop) { 2 | switch (prop) { 3 | case 'true': 4 | return true; 5 | case 'false': 6 | return false; 7 | default: 8 | return null; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/linux/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | 10 | void fl_register_plugins(FlPluginRegistry* registry) { 11 | } 12 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 6 | -------------------------------------------------------------------------------- /example/windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | 10 | void RegisterPlugins(flutter::PluginRegistry* registry) { 11 | } 12 | -------------------------------------------------------------------------------- /lib/live_view/mapping/duration.dart: -------------------------------------------------------------------------------- 1 | Duration? getDuration(String? prop) { 2 | if (prop == null) { 3 | return null; 4 | } 5 | var parse = int.tryParse(prop); 6 | if (parse == null) { 7 | return null; 8 | } 9 | return Duration(milliseconds: parse); 10 | } 11 | -------------------------------------------------------------------------------- /example/macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @NSApplicationMain 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/live_view/mapping/axis_direction.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/rendering.dart'; 2 | 3 | Axis? getAxis(String? prop) { 4 | switch (prop) { 5 | case 'vertical': 6 | return Axis.vertical; 7 | case 'horizontal': 8 | return Axis.horizontal; 9 | default: 10 | return null; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/live_view/ui/root_view/internal_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class InternalView extends StatelessWidget { 4 | final Widget child; 5 | const InternalView({super.key, required this.child}); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return child; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/live_view/mapping/text_direction.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | TextDirection? getTextDirection(String? property) { 4 | switch (property) { 5 | case 'ltr': 6 | return TextDirection.ltr; 7 | case 'rtl': 8 | return TextDirection.rtl; 9 | default: 10 | return null; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/live_view/mapping/mouse_cursor.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/rendering.dart'; 2 | 3 | MouseCursor? getMouseCursor(String? attribute) { 4 | switch (attribute) { 5 | case 'defer': 6 | return MouseCursor.defer; 7 | case 'uncontrolled': 8 | return MouseCursor.uncontrolled; 9 | default: 10 | return null; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/live_view/mapping/notched_shape.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | NotchedShape? getNotchedShape(String? prop) { 4 | // TODO: handle AutomaticNotchedReactable 5 | switch (prop) { 6 | case 'CircularNotchedRectangle': 7 | return const CircularNotchedRectangle(); 8 | default: 9 | return null; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/live_view/mapping/text_baseline.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | TextBaseline? getTextBaseline(String? prop) { 4 | switch (prop) { 5 | case 'alphabetic': 6 | return TextBaseline.alphabetic; 7 | case 'ideographic': 8 | return TextBaseline.ideographic; 9 | default: 10 | return null; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9" 8 | channel: "stable" 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /lib/live_view/mapping/drag_start_behavior.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/gestures.dart'; 2 | 3 | DragStartBehavior? getDragStartBehavior(String? prop) { 4 | switch (prop) { 5 | case 'down': 6 | return DragStartBehavior.down; 7 | case 'start': 8 | return DragStartBehavior.start; 9 | default: 10 | return null; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /lib/live_view/plugin.dart: -------------------------------------------------------------------------------- 1 | import 'package:liveview_flutter/exec/live_view_exec_registry.dart'; 2 | import 'package:liveview_flutter/live_view/ui/live_view_ui_registry.dart'; 3 | 4 | abstract class Plugin { 5 | String get name; 6 | 7 | void registerWidgets(LiveViewUiRegistry registry); 8 | 9 | void registerExecs(LiveViewExecRegistry registry); 10 | } 11 | -------------------------------------------------------------------------------- /lib/exec/exec_go_back.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:liveview_flutter/exec/exec.dart'; 3 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 4 | 5 | class ExecGoBack extends Exec { 6 | @override 7 | void handler(BuildContext context, StateWidget widget) { 8 | widget.liveView.goBack(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /example/macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import FlutterMacOS 2 | import Cocoa 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /example/windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /lib/live_view/mapping/options_view_open_direction.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | OptionsViewOpenDirection? getOptionsViewOpenDirection(String? prop) { 4 | switch (prop) { 5 | case 'down': 6 | return OptionsViewOpenDirection.down; 7 | case 'up': 8 | return OptionsViewOpenDirection.up; 9 | default: 10 | return null; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/live_view/mapping/material_tap_target_size.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | MaterialTapTargetSize? getMaterialTapTargetSize(String? prop) { 4 | switch (prop) { 5 | case 'padded': 6 | return MaterialTapTargetSize.padded; 7 | case 'shrinkWrap': 8 | return MaterialTapTargetSize.shrinkWrap; 9 | default: 10 | return null; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /lib/exec/exec_save_current_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:liveview_flutter/exec/exec.dart'; 3 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 4 | 5 | class ExecSaveCurrentTheme extends Exec { 6 | @override 7 | void handler(BuildContext context, StateWidget widget) { 8 | widget.liveView.saveCurrentTheme(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/live_view/mapping/number.dart: -------------------------------------------------------------------------------- 1 | double? getDouble(String? property) { 2 | if (property == null) { 3 | return null; 4 | } 5 | if (property == 'infinity') { 6 | return double.infinity; 7 | } 8 | return double.tryParse(property); 9 | } 10 | 11 | int? getInt(String? property) { 12 | if (property == null) { 13 | return null; 14 | } 15 | return int.tryParse(property); 16 | } 17 | -------------------------------------------------------------------------------- /example/linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /example/windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /lib/live_view/mapping/shape_border.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/rendering.dart'; 2 | 3 | ShapeBorder? getShapeBorder(String? prop) { 4 | // TODO: handle all the kinds of custom shape 5 | switch (prop) { 6 | case 'CircleBorder': 7 | return const CircleBorder(); 8 | case 'BeveledRectangleBorder': 9 | return const BeveledRectangleBorder(); 10 | default: 11 | return null; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/live_view/mapping/clip_behavior.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | Clip? getClip(String? prop) { 4 | switch (prop) { 5 | case 'antiAlias': 6 | return Clip.antiAlias; 7 | case 'antiAliasWithSaveLayer': 8 | return Clip.antiAliasWithSaveLayer; 9 | case 'hardEdge': 10 | return Clip.hardEdge; 11 | case 'none': 12 | return Clip.none; 13 | default: 14 | return null; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/live_view/mapping/tooltip_trigger_mode.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | TooltipTriggerMode? getTooltipTriggerMode(String? prop) { 4 | switch (prop) { 5 | case 'longPress': 6 | return TooltipTriggerMode.longPress; 7 | case 'manual': 8 | return TooltipTriggerMode.manual; 9 | case 'tap': 10 | return TooltipTriggerMode.tap; 11 | default: 12 | return null; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.server 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /lib/live_view/mapping/overflow_bar_alignment.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | OverflowBarAlignment? getOverflowBarAlignment(String? prop) { 4 | switch (prop) { 5 | case 'center': 6 | return OverflowBarAlignment.center; 7 | case 'end': 8 | return OverflowBarAlignment.end; 9 | case 'start': 10 | return OverflowBarAlignment.start; 11 | default: 12 | return null; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/live_view/mapping/scroll_view_keyboard_dismiss_behavior.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | 3 | ScrollViewKeyboardDismissBehavior? getScrollViewKeyboardDismissBehavior( 4 | String? prop) { 5 | switch (prop) { 6 | case 'manual': 7 | return ScrollViewKeyboardDismissBehavior.manual; 8 | case 'onDrag': 9 | return ScrollViewKeyboardDismissBehavior.onDrag; 10 | default: 11 | return null; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /lib/exec/exec_live_patch.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:liveview_flutter/exec/exec.dart'; 3 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 4 | 5 | class ExecLivePatch extends Exec { 6 | String url; 7 | 8 | ExecLivePatch({required this.url}); 9 | 10 | @override 11 | void handler(BuildContext context, StateWidget widget) { 12 | widget.liveView.livePatch(url); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/live_view/state/element_key.dart: -------------------------------------------------------------------------------- 1 | class ElementKey { 2 | ElementKey(this.key); 3 | 4 | final String key; 5 | 6 | @override 7 | bool operator ==(Object other) { 8 | if (other is! ElementKey) return false; 9 | if (key != other.key) return false; 10 | return true; 11 | } 12 | 13 | @override 14 | int get hashCode => key.hashCode; 15 | 16 | @override 17 | String toString() { 18 | return 'ElementKey{key: $key}'; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/live_view/mapping/navigation_rail_label_type.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | NavigationRailLabelType? getNavigationRailLabelType(String? attribute) { 4 | switch (attribute) { 5 | case 'all': 6 | return NavigationRailLabelType.all; 7 | case 'selected': 8 | return NavigationRailLabelType.selected; 9 | case 'none': 10 | return NavigationRailLabelType.none; 11 | default: 12 | return null; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/exec/exec_show_bottom_sheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:liveview_flutter/exec/exec.dart'; 3 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 4 | import 'package:liveview_flutter/live_view/ui/root_view/root_scaffold.dart'; 5 | 6 | class ExecShowBottomSheet extends Exec { 7 | @override 8 | void handler(BuildContext context, StateWidget widget) { 9 | ShowBottomSheetNotification().dispatch(context); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /example/linux/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /lib/exec/exec_switch_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:liveview_flutter/exec/exec.dart'; 3 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 4 | 5 | class ExecSwitchTheme extends Exec { 6 | String theme; 7 | String mode; 8 | 9 | ExecSwitchTheme({required this.theme, required this.mode}); 10 | 11 | @override 12 | void handler(BuildContext context, StateWidget widget) { 13 | widget.liveView.switchTheme(theme, mode); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /lib/live_view/mapping/visual_density.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | VisualDensity? getVisualDensity(String? prop) { 4 | switch (prop) { 5 | case 'adaptivePlatformDensity': 6 | return VisualDensity.adaptivePlatformDensity; 7 | case 'comfortable': 8 | return VisualDensity.comfortable; 9 | case 'standard': 10 | return VisualDensity.standard; 11 | case 'compact': 12 | return VisualDensity.compact; 13 | default: 14 | return null; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example/windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /test/components/text_button_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | 3 | import '../test_helpers.dart'; 4 | 5 | main() async { 6 | testWidgets('looks okay', (tester) => tester.checkScreenshot(""" 7 | 8 | 9 | 10 | hello 11 | hello 12 | 13 | 14 | 15 | """, 'text_button_test.png')); 16 | } 17 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/components/card_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | 3 | import '../test_helpers.dart'; 4 | 5 | main() async { 6 | testWidgets('icon button test', (tester) => tester.checkScreenshot(""" 7 | 8 | 9 | 10 | demo 11 | demo 12 | demo 13 | 14 | 15 | 16 | """, 'card_test.png')); 17 | } 18 | -------------------------------------------------------------------------------- /lib/live_view/mapping/border_radius.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/mapping/edge_insets.dart'; 3 | 4 | BorderRadius? getBorderRadius(String? borderRadio) { 5 | var edges = getEdgeInsets(borderRadio); 6 | 7 | if (edges == null) { 8 | return null; 9 | } 10 | 11 | return BorderRadius.only( 12 | topLeft: Radius.circular(edges.top), 13 | topRight: Radius.circular(edges.right), 14 | bottomRight: Radius.circular(edges.bottom), 15 | bottomLeft: Radius.circular(edges.left), 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /lib/live_view/mapping/text_align.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | TextAlign? getTextAlign(String? prop) { 4 | switch (prop) { 5 | case 'center': 6 | return TextAlign.center; 7 | case 'end': 8 | return TextAlign.end; 9 | case 'start': 10 | return TextAlign.start; 11 | case 'right': 12 | return TextAlign.right; 13 | case 'justify': 14 | return TextAlign.justify; 15 | default: 16 | if (prop != null) { 17 | debugPrint("Unknown text align property $prop"); 18 | } 19 | return null; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/live_view/mapping/list_title_alignment.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | ListTileTitleAlignment? getListTitleAligment(String? prop) { 4 | switch (prop) { 5 | case 'bottom': 6 | return ListTileTitleAlignment.bottom; 7 | case 'center': 8 | return ListTileTitleAlignment.center; 9 | case 'threeLine': 10 | return ListTileTitleAlignment.threeLine; 11 | case 'titleHeight': 12 | return ListTileTitleAlignment.titleHeight; 13 | case 'top': 14 | return ListTileTitleAlignment.top; 15 | default: 16 | return null; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/render/button_text_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:liveview_flutter/live_view/live_view.dart'; 3 | 4 | import '../test_helpers.dart'; 5 | 6 | main() async { 7 | testWidgets('Button with inner text are rendered as a Text element', 8 | (tester) async { 9 | var view = LiveView() 10 | ..handleRenderedMessage({ 11 | 's': ['My button'], 12 | }); 13 | 14 | await tester.runLiveView(view); 15 | await tester.pumpAndSettle(); 16 | 17 | expect(find.firstText(), 'My button'); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /example/macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import path_provider_foundation 9 | import shared_preferences_foundation 10 | import sqflite_darwin 11 | 12 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 13 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 14 | SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) 15 | SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) 16 | } 17 | -------------------------------------------------------------------------------- /test/render/data_phx_main_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:liveview_flutter/live_view/live_view.dart'; 3 | 4 | import '../test_helpers.dart'; 5 | 6 | main() async { 7 | testWidgets('The main data-phx-main div does not break the render', 8 | (tester) async { 9 | var view = LiveView() 10 | ..handleRenderedMessage({ 11 | 's': ['
works
'], 12 | }); 13 | 14 | await tester.runLiveView(view); 15 | await tester.pumpAndSettle(); 16 | expect(find.firstText(), 'works'); 17 | }); 18 | } 19 | -------------------------------------------------------------------------------- /lib/exec/exec_live_event.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:liveview_flutter/exec/exec_confirmable.dart'; 3 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 4 | 5 | class ExecLiveEvent extends ExecConfirmable { 6 | final String type; 7 | final String name; 8 | final dynamic value; 9 | 10 | ExecLiveEvent({ 11 | required this.type, 12 | required this.name, 13 | required this.value, 14 | super.dataConfirm, 15 | }); 16 | 17 | @override 18 | void handler(BuildContext context, StateWidget widget) { 19 | widget.liveView.sendEvent(this); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_bottom_sheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LiveBottomSheet extends LiveStateWidget { 5 | const LiveBottomSheet({super.key, required super.state}); 6 | 7 | @override 8 | State createState() => _LiveBottomSheetState(); 9 | } 10 | 11 | class _LiveBottomSheetState extends StateWidget { 12 | @override 13 | void onStateChange(Map diff) {} 14 | 15 | @override 16 | Widget render(BuildContext context) => singleChild(); 17 | } 18 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_stack.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LiveStack extends LiveStateWidget { 5 | const LiveStack({super.key, required super.state}); 6 | 7 | @override 8 | State createState() => _LiveStackState(); 9 | } 10 | 11 | class _LiveStackState extends StateWidget { 12 | @override 13 | void onStateChange(Map diff) {} 14 | 15 | @override 16 | Widget render(BuildContext context) { 17 | return Stack(children: multipleChildren()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_view_body.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LiveViewBody extends LiveStateWidget { 5 | const LiveViewBody({super.key, required super.state}); 6 | 7 | @override 8 | State createState() => _LiveMainViewState(); 9 | } 10 | 11 | class _LiveMainViewState extends StateWidget { 12 | @override 13 | void onStateChange(Map diff) {} 14 | 15 | @override 16 | Widget render(BuildContext context) { 17 | return singleChild(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /documentation/lifecycle.md: -------------------------------------------------------------------------------- 1 | # How the state lifecycle works 2 | 3 | Components are defined in ```lib/live_view/ui/components/``` and all inherit the class ```LiveStateWidget```. 4 | 5 | This class is responsible for handling all the state changes and refresh widgets if necessary. 6 | 7 | 8 | ## Changing pages & Wiping states 9 | 10 | When changing pages, the existing state is wiped. Any changes which was done a result of a user action is reset. 11 | 12 | This is done to avoid "phantom states", where the UI would carry the previous state into the next page. 13 | 14 | This client is only supporting a single page at a time at the moment so this is necessary. -------------------------------------------------------------------------------- /test/components/icon_button_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | 3 | import '../test_helpers.dart'; 4 | 5 | main() async { 6 | testWidgets('icon button test', (tester) => tester.checkScreenshot(""" 7 | 8 | 9 | 10 | 11 | 12 | 13 | demo 14 | 15 | 16 | 17 | """, 'icon_button_test.png')); 18 | } 19 | -------------------------------------------------------------------------------- /test/render/implicit_column_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | import '../test_helpers.dart'; 5 | 6 | bool? checkValue() => 7 | (find.byType(Checkbox).evaluate().first.widget as Checkbox).value; 8 | 9 | main() async { 10 | testWidgets('implicit columns behaves like html', 11 | (tester) => tester.checkScreenshot(""" 12 | 13 | multiple 14 | lines 15 | in a container supporting a single child 16 | 17 | """, 'implicit_column_test.png')); 18 | } 19 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_expanded.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LiveExpanded extends LiveStateWidget { 5 | const LiveExpanded({super.key, required super.state}); 6 | 7 | @override 8 | State createState() => _LiveExpandedState(); 9 | } 10 | 11 | class _LiveExpandedState extends StateWidget { 12 | @override 13 | void onStateChange(Map diff) {} 14 | 15 | @override 16 | Widget render(BuildContext context) { 17 | return Expanded(child: singleChild()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_link.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LiveLink extends LiveStateWidget { 5 | const LiveLink({super.key, required super.state}); 6 | 7 | @override 8 | State createState() => _LiveCenterState(); 9 | } 10 | 11 | class _LiveCenterState extends StateWidget { 12 | @override 13 | void onStateChange(Map diff) {} 14 | 15 | @override 16 | Widget render(BuildContext context) { 17 | return MouseRegion(cursor: SystemMouseCursors.click, child: singleChild()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /lib/live_view/mapping/box_height_style.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | BoxHeightStyle? getBoxHeightStyle(String? prop) { 4 | switch (prop) { 5 | case 'includeLineSpacingBottom': 6 | return BoxHeightStyle.includeLineSpacingBottom; 7 | case 'includeLineSpacingMiddle': 8 | return BoxHeightStyle.includeLineSpacingMiddle; 9 | case 'includeLineSpacingTop': 10 | return BoxHeightStyle.includeLineSpacingTop; 11 | case 'max': 12 | return BoxHeightStyle.max; 13 | case 'strut': 14 | return BoxHeightStyle.strut; 15 | case 'tight': 16 | return BoxHeightStyle.tight; 17 | default: 18 | return null; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_title_attribute.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LiveTitleAttribute extends LiveStateWidget { 5 | const LiveTitleAttribute({super.key, required super.state}); 6 | 7 | @override 8 | State createState() => _LiveTitleState(); 9 | } 10 | 11 | class _LiveTitleState extends StateWidget { 12 | @override 13 | void onStateChange(Map diff) {} 14 | 15 | @override 16 | Widget render(BuildContext context) { 17 | return singleChild(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_avatar_attribute.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LiveAvatarAttribute extends LiveStateWidget { 5 | const LiveAvatarAttribute({super.key, required super.state}); 6 | 7 | @override 8 | State createState() => _LiveAvatarAttributeState(); 9 | } 10 | 11 | class _LiveAvatarAttributeState extends StateWidget { 12 | @override 13 | void onStateChange(Map diff) {} 14 | 15 | @override 16 | Widget render(BuildContext context) => singleChild(); 17 | } 18 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_hint_attribute.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LiveHintAttribute extends LiveStateWidget { 5 | const LiveHintAttribute({super.key, required super.state}); 6 | 7 | @override 8 | State createState() => _LiveHintAttributeState(); 9 | } 10 | 11 | class _LiveHintAttributeState extends StateWidget { 12 | @override 13 | void onStateChange(Map diff) {} 14 | 15 | @override 16 | Widget render(BuildContext context) { 17 | return singleChild(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_icon_attribute.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LiveIconAttribute extends LiveStateWidget { 5 | const LiveIconAttribute({super.key, required super.state}); 6 | 7 | @override 8 | State createState() => _LiveIconAttributeState(); 9 | } 10 | 11 | class _LiveIconAttributeState extends StateWidget { 12 | @override 13 | void onStateChange(Map diff) {} 14 | 15 | @override 16 | Widget render(BuildContext context) { 17 | return singleChild(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/components/list_tile_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | 3 | import '../test_helpers.dart'; 4 | 5 | main() async { 6 | testWidgets('icon button test', (tester) => tester.checkScreenshot(""" 7 | 8 | 9 | 10 | 11 | my list tile 12 | subtitle here 13 | 14 | hello 15 | 16 | 17 | 18 | """, 'list_title_test.png')); 19 | } 20 | -------------------------------------------------------------------------------- /test/errors/missing_view_body_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:liveview_flutter/live_view/live_view.dart'; 3 | 4 | import '../test_helpers.dart'; 5 | 6 | main() async { 7 | testWidgets('Button with inner text are rendered as a Text element', 8 | (tester) async { 9 | var view = LiveView() 10 | ..handleRenderedMessage({ 11 | 's': ['helloworld'], 12 | }); 13 | 14 | await tester.runLiveView(view); 15 | await tester.pumpAndSettle(); 16 | 17 | expect( 18 | find.firstText(), 'Unable to find any component on url /'); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /lib/live_view/mapping/button_style.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/mapping/css.dart'; 3 | import 'package:liveview_flutter/live_view/mapping/text_style_map.dart'; 4 | 5 | ButtonStyle? getButtonStyle(BuildContext context, String? style) { 6 | if (style == null) { 7 | return null; 8 | } 9 | MaterialStateProperty? textStyle; 10 | 11 | for (var (styleKey, styleValue) in parseCss(style)) { 12 | switch (styleKey) { 13 | case 'textStyle': 14 | textStyle = getMaterialTextStyle(styleValue, context); 15 | break; 16 | } 17 | } 18 | return ButtonStyle(textStyle: textStyle); 19 | } 20 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_label_attribute.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LiveLabelAttribute extends LiveStateWidget { 5 | const LiveLabelAttribute({super.key, required super.state}); 6 | 7 | @override 8 | State createState() => _LiveLabelAttributeState(); 9 | } 10 | 11 | class _LiveLabelAttributeState extends StateWidget { 12 | @override 13 | void onStateChange(Map diff) {} 14 | 15 | @override 16 | Widget render(BuildContext context) { 17 | return singleChild(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_leading_attribute.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LiveLeadingAttribute extends LiveStateWidget { 5 | const LiveLeadingAttribute({super.key, required super.state}); 6 | 7 | @override 8 | State createState() => _LiveLeadingState(); 9 | } 10 | 11 | class _LiveLeadingState extends StateWidget { 12 | @override 13 | void onStateChange(Map diff) {} 14 | 15 | @override 16 | Widget render(BuildContext context) { 17 | return singleChild(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_subtitle_attribute.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LiveSubtitleAttribute extends LiveStateWidget { 5 | const LiveSubtitleAttribute({super.key, required super.state}); 6 | 7 | @override 8 | State createState() => _LiveTitleState(); 9 | } 10 | 11 | class _LiveTitleState extends StateWidget { 12 | @override 13 | void onStateChange(Map diff) {} 14 | 15 | @override 16 | Widget render(BuildContext context) { 17 | return singleChild(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_trailing_attribute.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LiveTrailingAttribute extends LiveStateWidget { 5 | const LiveTrailingAttribute({super.key, required super.state}); 6 | 7 | @override 8 | State createState() => _LiveTitleState(); 9 | } 10 | 11 | class _LiveTitleState extends StateWidget { 12 | @override 13 | void onStateChange(Map diff) {} 14 | 15 | @override 16 | Widget render(BuildContext context) { 17 | return singleChild(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_content_attribute.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LiveContentAttribute extends LiveStateWidget { 5 | const LiveContentAttribute({super.key, required super.state}); 6 | 7 | @override 8 | State createState() => _LiveContentAttributeState(); 9 | } 10 | 11 | class _LiveContentAttributeState extends StateWidget { 12 | @override 13 | void onStateChange(Map diff) {} 14 | 15 | @override 16 | Widget render(BuildContext context) { 17 | return singleChild(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = liveview_flutter 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.example.liveviewFlutter 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2023 com.example. All rights reserved. 15 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_underline_attribute.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LiveUnderlineAttribute extends LiveStateWidget { 5 | const LiveUnderlineAttribute({super.key, required super.state}); 6 | 7 | @override 8 | State createState() => _LiveUnderlineAttributeState(); 9 | } 10 | 11 | class _LiveUnderlineAttributeState extends StateWidget { 12 | @override 13 | void onStateChange(Map diff) {} 14 | 15 | @override 16 | Widget render(BuildContext context) { 17 | return singleChild(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/render/render_with_variables_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:liveview_flutter/live_view/live_view.dart'; 3 | 4 | import '../test_helpers.dart'; 5 | 6 | main() async { 7 | testWidgets('handles variable inside text', (tester) async { 8 | var view = LiveView() 9 | ..handleRenderedMessage({ 10 | 's': [ 11 | 'the first counter is (', 12 | ') and the second one is (', 13 | ')' 14 | ], 15 | '0': 10, 16 | '1': 12 17 | }); 18 | 19 | await tester.runLiveView(view); 20 | expect(find.firstText(), 21 | 'the first counter is (10) and the second one is (12)'); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /lib/platform_name.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io' show Platform; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | 5 | String getPlatformName() { 6 | var platformName = ''; 7 | if (kIsWeb) { 8 | platformName = "Web"; 9 | } else { 10 | if (Platform.isAndroid) { 11 | platformName = "Android"; 12 | } else if (Platform.isIOS) { 13 | platformName = "IOS"; 14 | } else if (Platform.isFuchsia) { 15 | platformName = "Fuchsia"; 16 | } else if (Platform.isLinux) { 17 | platformName = "Linux"; 18 | } else if (Platform.isMacOS) { 19 | platformName = "MacOS"; 20 | } else if (Platform.isWindows) { 21 | platformName = "Windows"; 22 | } 23 | } 24 | return platformName; 25 | } 26 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /lib/live_view/mapping/material_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | MaterialState? getMaterialState(String? value) { 4 | switch (value) { 5 | case 'disabled': 6 | return MaterialState.disabled; 7 | case 'hovered': 8 | return MaterialState.hovered; 9 | case 'focused': 10 | return MaterialState.focused; 11 | case 'pressed': 12 | return MaterialState.pressed; 13 | case 'dragged': 14 | return MaterialState.dragged; 15 | case 'selected': 16 | return MaterialState.selected; 17 | case 'scrolledUnder': 18 | return MaterialState.scrolledUnder; 19 | case 'error': 20 | return MaterialState.error; 21 | default: 22 | return null; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/live_view/routes/no_transition_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class NoTransitionPage extends MaterialPage { 4 | const NoTransitionPage({ 5 | required super.child, 6 | super.maintainState = true, 7 | super.fullscreenDialog = false, 8 | super.allowSnapshotting = true, 9 | super.key, 10 | super.name, 11 | super.arguments, 12 | super.restorationId, 13 | }); 14 | 15 | @override 16 | Route createRoute(BuildContext context) { 17 | return PageRouteBuilder( 18 | settings: this, 19 | pageBuilder: (BuildContext context, Animation animation, 20 | Animation secondaryAnimation) { 21 | return child; 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/render/single_child_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:liveview_flutter/live_view/live_view.dart'; 4 | 5 | import '../test_helpers.dart'; 6 | 7 | main() async { 8 | testWidgets('supports implicit Columns for single child widgets', 9 | (tester) async { 10 | var view = LiveView() 11 | ..handleRenderedMessage({ 12 | 's': [ 13 | """ 14 | hello 15 | world 16 | """ 17 | ], 18 | }); 19 | 20 | await tester.runLiveView(view); 21 | 22 | find.firstOf(); 23 | expect(find.allTexts(), ['hello', 'world']); 24 | }); 25 | } 26 | -------------------------------------------------------------------------------- /lib/exec/exec_phx_href.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/exec/exec.dart'; 3 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 4 | 5 | class ExecPhxHref extends Exec { 6 | String url; 7 | 8 | ExecPhxHref({required this.url}); 9 | 10 | @override 11 | void handler(BuildContext context, StateWidget widget) { 12 | widget.liveView.execHrefClick(url); 13 | } 14 | } 15 | 16 | class ExecPhxHrefModal extends Exec { 17 | String url; 18 | 19 | ExecPhxHrefModal({required this.url}); 20 | 21 | @override 22 | void handler(BuildContext context, StateWidget widget) { 23 | Navigator.of(context).pop(); 24 | widget.liveView.execHrefClick(url); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.3.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | tasks.register("clean", Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /test/render/render_component_slots_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:liveview_flutter/live_view/live_view.dart'; 3 | 4 | import '../test_helpers.dart'; 5 | 6 | void main() async { 7 | testWidgets('handles live components', (tester) async { 8 | var view = LiveView() 9 | ..handleRenderedMessage({ 10 | "0": { 11 | "0": { 12 | "s": ["Hello"] 13 | }, 14 | "s": ["", ""], 15 | "r": 1 16 | }, 17 | "s": ["", ""], 18 | "r": 1 19 | }); 20 | 21 | await tester.runLiveView(view); 22 | await tester.pumpAndSettle(); 23 | 24 | expect(find.allTexts(), ['Hello']); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /example/windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_disabled_hint_attribute.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LiveDisabledHintAttribute 5 | extends LiveStateWidget { 6 | const LiveDisabledHintAttribute({super.key, required super.state}); 7 | 8 | @override 9 | State createState() => 10 | _LiveDisabledHintAttributeState(); 11 | } 12 | 13 | class _LiveDisabledHintAttributeState 14 | extends StateWidget { 15 | @override 16 | void onStateChange(Map diff) {} 17 | 18 | @override 19 | Widget render(BuildContext context) { 20 | return singleChild(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_icon_selected_attribute.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LiveIconSelectedAttribute 5 | extends LiveStateWidget { 6 | const LiveIconSelectedAttribute({super.key, required super.state}); 7 | 8 | @override 9 | State createState() => 10 | _LiveIconSelectedAttributeState(); 11 | } 12 | 13 | class _LiveIconSelectedAttributeState 14 | extends StateWidget { 15 | @override 16 | void onStateChange(Map diff) {} 17 | 18 | @override 19 | Widget render(BuildContext context) { 20 | return singleChild(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_persistent_footer_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LivePersistentFooterButton 5 | extends LiveStateWidget { 6 | const LivePersistentFooterButton({super.key, required super.state}); 7 | 8 | @override 9 | State createState() => 10 | _LivePersistentFooterButtonState(); 11 | } 12 | 13 | class _LivePersistentFooterButtonState 14 | extends StateWidget { 15 | @override 16 | void onStateChange(Map diff) {} 17 | 18 | @override 19 | Widget render(BuildContext context) { 20 | return singleChild(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | } 9 | settings.ext.flutterSdkPath = flutterSdkPath() 10 | 11 | includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") 12 | 13 | plugins { 14 | id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false 15 | } 16 | } 17 | 18 | include ":app" 19 | 20 | apply from: "${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle/app_plugin_loader.gradle" 21 | -------------------------------------------------------------------------------- /lib/exec/exec.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | import 'package:liveview_flutter/live_view/ui/utils.dart'; 4 | import 'package:liveview_flutter/when/when.dart'; 5 | 6 | /// Represents an action that can be executed in the view. 7 | /// 8 | /// It can be an event or a command, like changing the current theme or switching pages. 9 | abstract class Exec { 10 | When conditions = When(); 11 | 12 | void conditionalHandler(BuildContext context, StateWidget widget) { 13 | if (conditions.execute(context) == false) return; 14 | handler(context, widget); 15 | } 16 | 17 | void handler(BuildContext context, StateWidget widget) { 18 | reportError("Unimplemented action handler: $this"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /lib/live_view/routes/live_custom_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class LiveCustomPage extends MaterialPage { 4 | const LiveCustomPage({ 5 | required super.child, 6 | super.maintainState = true, 7 | super.fullscreenDialog = false, 8 | super.allowSnapshotting = true, 9 | super.key, 10 | super.name, 11 | super.arguments, 12 | super.restorationId, 13 | }); 14 | 15 | @override 16 | Route createRoute(BuildContext context) { 17 | return PageRouteBuilder( 18 | settings: this, 19 | pageBuilder: (BuildContext context, Animation animation, 20 | Animation secondaryAnimation) { 21 | return FadeTransition( 22 | opacity: animation, 23 | child: child, 24 | ); 25 | }); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/live_view/mapping/bottom_navigation_bar_type.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | BottomNavigationBarType? getBottomNavigationBarType(String? prop) { 4 | switch (prop) { 5 | case 'fixed': 6 | return BottomNavigationBarType.fixed; 7 | case 'shifting': 8 | return BottomNavigationBarType.shifting; 9 | default: 10 | return null; 11 | } 12 | } 13 | 14 | BottomNavigationBarLandscapeLayout? getBottomNavigationBarLandscapeLayout( 15 | String? prop) { 16 | switch (prop) { 17 | case 'centered': 18 | return BottomNavigationBarLandscapeLayout.centered; 19 | case 'linear': 20 | return BottomNavigationBarLandscapeLayout.linear; 21 | case 'spread': 22 | return BottomNavigationBarLandscapeLayout.spread; 23 | default: 24 | return null; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /test/components/persistent_footer_button_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | import '../test_helpers.dart'; 5 | 6 | bool? checkValue() => 7 | (find.byType(Checkbox).evaluate().first.widget as Checkbox).value; 8 | 9 | main() async { 10 | testWidgets('looks okay', (tester) => tester.checkScreenshot(""" 11 | 12 | hello 13 | hello 14 | 15 | 16 | hello 17 | 18 | 19 | 20 | """, 'persistent_footer_button_test.png')); 21 | } 22 | -------------------------------------------------------------------------------- /test/plugins/basic_plugin_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:liveview_flutter/live_view/live_view.dart'; 3 | 4 | import '../test_helpers.dart'; 5 | import 'basic_plugin.dart'; 6 | 7 | void main() { 8 | testWidgets('basic plugin', (tester) async { 9 | var (view, _) = 10 | await connect(LiveView()..installPlugins([BasicPlugin()]), rendered: { 11 | 's': [ 12 | """ 13 | 14 | """ 15 | ], 16 | }); 17 | 18 | await tester.runLiveView(view); 19 | await tester.pumpAndSettle(); 20 | 21 | expect(find.allTexts(), ['MyComponent']); 22 | 23 | await tester.tap(find.text('MyComponent'), warnIfMissed: false); 24 | await tester.pumpAndSettle(); 25 | 26 | expect(myPluginActions, ['1']); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_sized_box.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LiveSizedBox extends LiveStateWidget { 5 | const LiveSizedBox({super.key, required super.state}); 6 | 7 | @override 8 | State createState() => _LiveSizedBoxState(); 9 | } 10 | 11 | class _LiveSizedBoxState extends StateWidget { 12 | final attributes = ['height', 'width']; 13 | 14 | @override 15 | void onStateChange(Map diff) => 16 | reloadAttributes(node, attributes); 17 | 18 | @override 19 | Widget render(BuildContext context) { 20 | return SizedBox( 21 | height: doubleAttribute('height'), 22 | width: doubleAttribute('width'), 23 | child: singleChild(), 24 | ); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_filled_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LiveFilledButton extends LiveStateWidget { 5 | const LiveFilledButton({super.key, required super.state}); 6 | 7 | @override 8 | State createState() => _LiveFilledButtonState(); 9 | } 10 | 11 | class _LiveFilledButtonState extends StateWidget { 12 | @override 13 | handleClickState() => HandleClickState.manual; 14 | 15 | @override 16 | void onStateChange(Map diff) {} 17 | 18 | @override 19 | Widget render(BuildContext context) { 20 | return FilledButton( 21 | onPressed: () { 22 | executeTapEventsManually(); 23 | }, 24 | child: singleChild()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /example/linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | ) 7 | 8 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 9 | ) 10 | 11 | set(PLUGIN_BUNDLED_LIBRARIES) 12 | 13 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 14 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 15 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 16 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 18 | endforeach(plugin) 19 | 20 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 21 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 22 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 23 | endforeach(ffi_plugin) 24 | -------------------------------------------------------------------------------- /lib/live_view/webdocs.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:liveview_flutter/live_view/live_view.dart'; 3 | import "package:universal_html/html.dart" as web_html; 4 | 5 | void bindWebDocs(LiveView view) { 6 | if (kIsWeb) { 7 | view.clientType == ClientType.webDocs; 8 | var renderFromUrl = 9 | Uri.parse('http://localhost${web_html.window.location.search}') 10 | .queryParameters['r']; 11 | if (renderFromUrl != null) { 12 | view.handleRenderedMessage({ 13 | 's': [renderFromUrl] 14 | }, viewType: ViewType.deadView); 15 | } 16 | web_html.window.onMessage.listen((event) { 17 | var data = event.data; 18 | if (data is String) { 19 | view.handleRenderedMessage({ 20 | 's': [data] 21 | }, viewType: ViewType.deadView); 22 | } 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /example/windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | ) 7 | 8 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 9 | ) 10 | 11 | set(PLUGIN_BUNDLED_LIBRARIES) 12 | 13 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 14 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 15 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 16 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 18 | endforeach(plugin) 19 | 20 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 21 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 22 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 23 | endforeach(ffi_plugin) 24 | -------------------------------------------------------------------------------- /lib/live_view/mapping/input_decoration.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/mapping/boolean.dart'; 3 | import 'package:liveview_flutter/live_view/mapping/colors.dart'; 4 | import 'package:liveview_flutter/live_view/mapping/css.dart'; 5 | 6 | InputDecoration getInputDecoration(BuildContext context, String? css, 7 | {Widget? icon}) { 8 | Color? fillColor; 9 | bool? filled; 10 | bool? isDense; 11 | for (var (prop, value) in parseCss(css ?? '')) { 12 | switch (prop) { 13 | case 'fillColor': 14 | fillColor = getColor(context, value); 15 | case 'filled': 16 | filled = getBoolean(value); 17 | case 'isDense': 18 | isDense = getBoolean(value); 19 | } 20 | } 21 | return InputDecoration( 22 | fillColor: fillColor, icon: icon, filled: filled, isDense: isDense); 23 | } 24 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_center.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LiveCenter extends LiveStateWidget { 5 | const LiveCenter({super.key, required super.state}); 6 | 7 | @override 8 | State createState() => _LiveCenterState(); 9 | } 10 | 11 | class _LiveCenterState extends StateWidget { 12 | final attributes = ['widthFactor', 'heightFactor']; 13 | 14 | @override 15 | void onStateChange(Map diff) => 16 | reloadAttributes(node, attributes); 17 | 18 | @override 19 | Widget render(BuildContext context) { 20 | return Center( 21 | widthFactor: doubleAttribute('widthFactor'), 22 | heightFactor: doubleAttribute('heightFactor'), 23 | child: singleChild()); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 26 | /pubspec.lock 27 | **/doc/api/ 28 | .dart_tool/ 29 | .flutter-plugins 30 | .flutter-plugins-dependencies 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | test/**/failures/** 42 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .pub-cache/ 31 | .pub/ 32 | /build/ 33 | 34 | # Symbolication related 35 | app.*.symbols 36 | 37 | # Obfuscation related 38 | app.*.map.json 39 | 40 | # Android Studio will place build artifacts here 41 | /android/app/debug 42 | /android/app/profile 43 | /android/app/release 44 | -------------------------------------------------------------------------------- /lib/live_view/mapping/alignment_directional.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | AlignmentDirectional? getAlignmentDirectional(String? prop) { 4 | switch (prop) { 5 | case 'bottomCenter': 6 | return AlignmentDirectional.bottomCenter; 7 | case 'bottomEnd': 8 | return AlignmentDirectional.bottomEnd; 9 | case 'bottomStart': 10 | return AlignmentDirectional.bottomStart; 11 | case 'center': 12 | return AlignmentDirectional.center; 13 | case 'centerEnd': 14 | return AlignmentDirectional.centerEnd; 15 | case 'centerStart': 16 | return AlignmentDirectional.centerStart; 17 | case 'topCenter': 18 | return AlignmentDirectional.topCenter; 19 | case 'topEnd': 20 | return AlignmentDirectional.topEnd; 21 | case 'topStart': 22 | return AlignmentDirectional.topStart; 23 | default: 24 | return null; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /test/events/phx_onload_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:liveview_flutter/live_view/live_view.dart'; 3 | 4 | import '../test_helpers.dart'; 5 | 6 | main() async { 7 | testWidgets('phx-onload', (tester) async { 8 | var (view, server) = await connect(LiveView(), rendered: { 9 | 's': ['The counter is ', ''], 10 | '0': 2 11 | }); 12 | 13 | await tester.runLiveView(view); 14 | await tester.pumpAndSettle(); 15 | 16 | expect(server.lastChannelActions, 17 | [liveEvents.join, liveEvents.event('backend_event')]); 18 | 19 | view.handleDiffMessage({'0': 5}); 20 | await tester.pumpAndSettle(); 21 | 22 | expect(server.lastChannelActions, 23 | [liveEvents.join, liveEvents.event('backend_event')], 24 | reason: 'changing the view should not retrigger the event'); 25 | }); 26 | } 27 | -------------------------------------------------------------------------------- /lib/live_view/mapping/keyboard_type.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | TextInputType? getTextInputType(String? type) { 4 | switch (type) { 5 | case 'datetime': 6 | return TextInputType.datetime; 7 | case 'emailAddress': 8 | return TextInputType.emailAddress; 9 | case 'multiline': 10 | return TextInputType.multiline; 11 | case 'name': 12 | return TextInputType.name; 13 | case 'none': 14 | return TextInputType.none; 15 | case 'number': 16 | return TextInputType.number; 17 | case 'phone': 18 | return TextInputType.phone; 19 | case 'streetAddress': 20 | return TextInputType.streetAddress; 21 | case 'text': 22 | return TextInputType.text; 23 | case 'url': 24 | return TextInputType.url; 25 | case 'visiblePassword': 26 | return TextInputType.visiblePassword; 27 | default: 28 | return null; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/mapping/colors_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:liveview_flutter/live_view/mapping/colors.dart'; 4 | 5 | void main() { 6 | testWidgets('parse colors', (tester) async { 7 | await tester.pumpWidget( 8 | Builder( 9 | builder: (BuildContext context) { 10 | expect( 11 | getColor(context, "#FAD"), 12 | const Color(0xFFFFAADD), 13 | ); 14 | expect( 15 | getColor(context, "#000000"), 16 | Colors.black, 17 | ); 18 | expect( 19 | getColor(context, "red"), 20 | Colors.red, 21 | ); 22 | expect( 23 | getColor(context, "@theme.colorScheme.primary"), 24 | Theme.of(context).colorScheme.primary, 25 | ); 26 | 27 | return const SizedBox.shrink(); 28 | }, 29 | ), 30 | ); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /example/windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /documentation/widgets.md: -------------------------------------------------------------------------------- 1 | # Widgets 2 | 3 | Widgets are defined in ```lib/live_view/ui/components/``` and all inherit the class ```LiveStateWidget```. 4 | 5 | # Widget child 6 | 7 | In Flutter, components can accept either a single child or multiple children but not both. 8 | How the client reconciles this is to add a ```Column``` widget if needed to behave more like HTML. 9 | 10 | Raw text elements in the xml payload are transformed into a basic Flutter ```Text``` widget. 11 | 12 | Those two buttons are equivalent: 13 | 14 | ```xml 15 | Click me 16 | Click me 17 | ``` 18 | 19 | And those two buttons are exactly rendered the same way as well: 20 | 21 | ```xml 22 | 23 | 24 | Click 25 | me 26 | 27 | 28 | 29 | 30 | Click 31 | me 32 | 33 | ``` -------------------------------------------------------------------------------- /lib/live_view/mapping/edge_insets.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/utils.dart'; 3 | 4 | EdgeInsets? getEdgeInsets(String? edges) { 5 | if (edges == null) { 6 | return null; 7 | } 8 | edges = edges.trim(); 9 | if (!edges.matches(r'^[\d\s]+$')) { 10 | return null; 11 | } 12 | if (edges == '0') { 13 | return EdgeInsets.zero; 14 | } 15 | 16 | var values = edges 17 | .replaceAll(RegExp(r'\s+'), ' ') 18 | .replaceAll(RegExp(r'[^\d\s]'), '') 19 | .split(' ') 20 | .map((e) => double.tryParse(e)) 21 | .toList(); 22 | 23 | if (values.any((e) => e == null)) { 24 | return null; 25 | } 26 | 27 | var top = values[0] ?? 0.0; 28 | var right = values.elementAtOrNull(1) ?? top; 29 | var bottom = values.elementAtOrNull(2) ?? top; 30 | var left = values.elementAtOrNull(3) ?? right; 31 | 32 | return EdgeInsets.only(top: top, left: left, right: right, bottom: bottom); 33 | } 34 | -------------------------------------------------------------------------------- /test/components/material_banner_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | 3 | import '../test_helpers.dart'; 4 | 5 | main() async { 6 | testWidgets('looks okay', (tester) => tester.checkScreenshot(""" 7 | 8 | 9 | 10 | action 1 11 | action 2 12 | 13 | 14 | hello 15 | icon 16 | action 1 17 | action 2 18 | 19 | hello 20 | 21 | 22 | """, 'material_banner_test.png')); 23 | } 24 | -------------------------------------------------------------------------------- /lib/live_view/mapping/decoration.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/mapping/border.dart'; 3 | import 'package:liveview_flutter/live_view/mapping/border_radius.dart'; 4 | import 'package:liveview_flutter/live_view/mapping/colors.dart'; 5 | import 'package:liveview_flutter/live_view/mapping/css.dart'; 6 | 7 | Decoration? getDecoration(BuildContext context, String? css) { 8 | if (css == null) { 9 | return null; 10 | } 11 | 12 | Color? color; 13 | BorderRadius? borderRadius; 14 | Border? border; 15 | 16 | for (var (prop, value) in parseCss(css)) { 17 | switch (prop) { 18 | case 'background': 19 | color = getColor(context, value); 20 | case 'borderRadius': 21 | borderRadius = getBorderRadius(value); 22 | case 'border': 23 | border = getBorder(context, value); 24 | } 25 | } 26 | 27 | return BoxDecoration( 28 | color: color, 29 | borderRadius: borderRadius, 30 | border: border, 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /test/forms/value_attribute_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:liveview_flutter/live_view/live_view.dart'; 4 | 5 | import '../test_helpers.dart'; 6 | 7 | main() async { 8 | testWidgets('changing value does not reset the input', (tester) async { 9 | var view = LiveView() 10 | ..handleRenderedMessage({ 11 | 's': [''], 12 | '0': 'name="myfield"', 13 | '1': 'initialValue="content"', 14 | }); 15 | 16 | await tester.runLiveView(view); 17 | await tester.pumpAndSettle(); 18 | 19 | expect(find.firstOf().initialValue, 'content'); 20 | expect((find.firstOf()).controller?.text, 'content'); 21 | 22 | view.handleDiffMessage({ 23 | '0': 'name="new name"', 24 | '1': 'initialValue="new content"', 25 | }); 26 | await tester.pumpAndSettle(); 27 | 28 | expect((find.firstOf()).controller?.text, 'content'); 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_positioned.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LivePositioned extends LiveStateWidget { 5 | const LivePositioned({super.key, required super.state}); 6 | 7 | @override 8 | State createState() => _LivePositionedState(); 9 | } 10 | 11 | class _LivePositionedState extends StateWidget { 12 | @override 13 | void onStateChange(Map diff) { 14 | reloadAttributes( 15 | node, ['left', 'top', 'right', 'bottom', 'width', 'height']); 16 | } 17 | 18 | @override 19 | Widget render(BuildContext context) { 20 | return Positioned( 21 | left: doubleAttribute('left'), 22 | top: doubleAttribute('top'), 23 | right: doubleAttribute('right'), 24 | bottom: doubleAttribute('bottom'), 25 | width: doubleAttribute('width'), 26 | height: doubleAttribute('height'), 27 | child: singleChild(), 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/network/reconnect_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter_test/flutter_test.dart'; 4 | import 'package:http/testing.dart'; 5 | import 'package:liveview_flutter/live_view/live_view.dart'; 6 | import 'package:shared_preferences/shared_preferences.dart'; 7 | import 'package:fake_async/fake_async.dart'; 8 | 9 | import '../test_helpers.dart'; 10 | 11 | void main() { 12 | var view = LiveView(); 13 | TestWidgetsFlutterBinding.ensureInitialized(); 14 | SharedPreferences.setMockInitialValues({}); 15 | var retries = 0; 16 | 17 | final MockClient failedNetwork = MockClient((request) async { 18 | retries += 1; 19 | throw const SocketException('Unable to connect'); 20 | }); 21 | 22 | view.httpClient = failedNetwork; 23 | view.liveSocket = FakeLiveSocket(); 24 | 25 | test('reconnecting when having a network failure', () async { 26 | fakeAsync((async) { 27 | view.connect('http://localhost:9999'); 28 | async.elapse(const Duration(seconds: 20)); 29 | }); 30 | expect(retries, 5); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /lib/live_view/ui/live_view_ui_registry.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:liveview_flutter/live_view/ui/node_state.dart'; 3 | import 'package:liveview_flutter/live_view/ui/utils.dart'; 4 | 5 | typedef WidgetBuilder = List Function(NodeState); 6 | 7 | class LiveViewUiRegistry { 8 | LiveViewUiRegistry._internal(); 9 | 10 | static final LiveViewUiRegistry _instance = LiveViewUiRegistry._internal(); 11 | 12 | final Map _widgets = {}; 13 | 14 | static LiveViewUiRegistry get instance => _instance; 15 | 16 | void add(List componentNames, WidgetBuilder buildWidget) { 17 | for (var componentName in componentNames) { 18 | _widgets[componentName] = buildWidget; 19 | } 20 | } 21 | 22 | List buildWidget(String componentName, NodeState state) { 23 | if (_widgets.containsKey(componentName)) { 24 | return _widgets[componentName]!.call(state); 25 | } 26 | 27 | reportError("unknown widget $componentName"); 28 | return [const SizedBox.shrink()]; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lib/live_view/ui/root_view/root_material_app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/live_view.dart'; 3 | import 'package:liveview_flutter/live_view/reactive/theme_settings.dart'; 4 | import 'package:liveview_flutter/live_view/ui/root_view/root_scaffold.dart'; 5 | import 'package:provider/provider.dart'; 6 | 7 | class LiveViewRootMaterialApp extends StatefulWidget { 8 | final LiveView view; 9 | const LiveViewRootMaterialApp({super.key, required this.view}); 10 | 11 | @override 12 | State createState() => 13 | _LiveViewRootMaterialAppState(); 14 | } 15 | 16 | class _LiveViewRootMaterialAppState extends State { 17 | @override 18 | Widget build(BuildContext context) { 19 | var theme = Provider.of(context); 20 | return MaterialApp( 21 | title: 'Flutter Demo', 22 | themeMode: theme.themeMode, 23 | theme: theme.lightTheme, 24 | darkTheme: theme.darkTheme, 25 | home: RootScaffold(view: widget.view)); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /test/render/whole_component_update_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:liveview_flutter/live_view/live_view.dart'; 3 | 4 | import '../test_helpers.dart'; 5 | 6 | main() async { 7 | // this checks that there's no reloading problem 8 | // with previous states which arent cleared in state_widget.dart#initState 9 | testWidgets('whole component update works', (tester) async { 10 | var view = LiveView() 11 | ..handleRenderedMessage({ 12 | "0": { 13 | "0": 'padding="10"', 14 | "1": { 15 | "s": ["Hello"] 16 | }, 17 | "s": ["", ""], 18 | "r": 1 19 | }, 20 | "s": ["", ""], 21 | "r": 1 22 | }); 23 | 24 | await tester.runLiveView(view); 25 | await tester.pumpAndSettle(); 26 | 27 | view.handleDiffMessage({ 28 | '0': {'0': 'padding="10"', '1': 'world'} 29 | }); 30 | await tester.pumpAndSettle(); 31 | expect(find.allTexts(), ['world']); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /example/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "liveview_flutter", 3 | "short_name": "liveview_flutter", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /example/windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /lib/live_view/mapping/border.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/mapping/edge_colors.dart'; 3 | import 'package:liveview_flutter/live_view/mapping/edge_insets.dart'; 4 | 5 | Border? getBorder(BuildContext context, String? border) { 6 | var exp = RegExp( 7 | r'^((?:\d+\s?)+)((?:(?:[@a-z\.]+\s?)|(?:#[a-f0-9]+\s?))*)$', 8 | caseSensitive: false, 9 | ); 10 | if (border == null || !exp.hasMatch(border)) { 11 | return null; 12 | } 13 | 14 | var match = exp.firstMatch(border)!; 15 | var widths = match[0]; 16 | var colors = match[1]; 17 | 18 | var edgesWidths = getEdgeInsets(widths); 19 | 20 | if (edgesWidths == null) { 21 | return null; 22 | } 23 | 24 | var edgesColors = getEdgeColors(context, colors); 25 | 26 | return Border( 27 | top: BorderSide(width: edgesWidths.top, color: edgesColors.top), 28 | right: BorderSide(width: edgesWidths.right, color: edgesColors.right), 29 | bottom: BorderSide(width: edgesWidths.bottom, color: edgesColors.bottom), 30 | left: BorderSide(width: edgesWidths.left, color: edgesColors.left), 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /test/mapping/edge_insets_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/painting.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:liveview_flutter/live_view/mapping/edge_insets.dart'; 4 | 5 | void main() { 6 | test('common cases', () { 7 | expect(getEdgeInsets(""), null); 8 | expect(getEdgeInsets("10"), const EdgeInsets.all(10)); 9 | expect( 10 | getEdgeInsets("10 0"), 11 | const EdgeInsets.symmetric( 12 | vertical: 10, 13 | horizontal: 0, 14 | ), 15 | ); 16 | expect( 17 | getEdgeInsets("10 0 5"), 18 | const EdgeInsets.only( 19 | top: 10, 20 | right: 0, 21 | bottom: 5, 22 | left: 0, 23 | ), 24 | ); 25 | expect( 26 | getEdgeInsets("10 2 5"), 27 | const EdgeInsets.only( 28 | top: 10, 29 | right: 2, 30 | bottom: 5, 31 | left: 2, 32 | ), 33 | ); 34 | expect( 35 | getEdgeInsets("10 1 5 8"), 36 | const EdgeInsets.only( 37 | top: 10, 38 | right: 1, 39 | bottom: 5, 40 | left: 8, 41 | ), 42 | ); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /lib/live_view/reactive/state_notifier.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/dynamic_component.dart'; 3 | 4 | Map nestedDiff( 5 | Map diff, List nestedState) { 6 | var fullDiff = diff; 7 | var currentDiff = fullDiff; 8 | for (var state in nestedState) { 9 | if (currentDiff.containsKey(state)) { 10 | if (currentDiff[state] == '') { 11 | currentDiff = {}; 12 | } else { 13 | currentDiff = Map.from(currentDiff[state]); 14 | } 15 | } 16 | } 17 | return currentDiff; 18 | } 19 | 20 | class StateNotifier extends ChangeNotifier { 21 | late Map _diff; 22 | StateNotifier() { 23 | _diff = {}; 24 | } 25 | 26 | void setDiff(Map diff) { 27 | _diff = expandVariables(diff); 28 | notifyListeners(); 29 | } 30 | 31 | void emptyData() { 32 | _diff = {}; 33 | } 34 | 35 | Map getDiff() => _diff; 36 | 37 | Map getNestedDiff(List nestedState) => 38 | nestedDiff(_diff, nestedState); 39 | } 40 | -------------------------------------------------------------------------------- /lib/live_view/ui/page_transition.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class CustomPageTransition extends MaterialPage { 5 | const CustomPageTransition({ 6 | required super.child, 7 | super.maintainState = true, 8 | super.fullscreenDialog = false, 9 | super.allowSnapshotting = true, 10 | super.key, 11 | super.name, 12 | super.arguments, 13 | super.restorationId, 14 | }); 15 | } 16 | 17 | class PageTransition extends MaterialPageRoute { 18 | PageTransition({required WidgetBuilder builder, RouteSettings? settings}) 19 | : super(builder: builder, settings: settings); 20 | 21 | @override 22 | get transitionDuration => const Duration(milliseconds: 250); 23 | 24 | @override 25 | Widget buildTransitions(BuildContext context, Animation animation, 26 | Animation secondaryAnimation, Widget child) { 27 | return CupertinoPageTransition( 28 | primaryRouteAnimation: animation, 29 | secondaryRouteAnimation: secondaryAnimation, 30 | linearTransition: true, 31 | child: child, 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:liveview_flutter/liveview_flutter.dart'; 6 | 7 | void main() { 8 | WidgetsFlutterBinding.ensureInitialized(); 9 | runApp(const MyApp()); 10 | } 11 | 12 | class MyApp extends StatefulWidget { 13 | const MyApp({super.key}); 14 | 15 | @override 16 | State createState() => _MyAppState(); 17 | } 18 | 19 | class _MyAppState extends State { 20 | final LiveView view = LiveView(); 21 | 22 | @override 23 | initState() { 24 | Future.microtask(boot); 25 | super.initState(); 26 | } 27 | 28 | void boot() async { 29 | if (kIsWeb) { 30 | view.connectToDocs(); 31 | return; 32 | } 33 | 34 | await view.connect( 35 | Platform.isAndroid 36 | ? 37 | // android emulator 38 | 'http://10.0.2.2:4000' 39 | // computer 40 | : 'http://localhost:4000/', 41 | ); 42 | } 43 | 44 | // This widget is the root of your application. 45 | @override 46 | Widget build(BuildContext context) { 47 | return view.rootView; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/screenshots/bottom_navigation_bar_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:golden_toolkit/golden_toolkit.dart'; 4 | import 'package:liveview_flutter/live_view/live_view.dart'; 5 | 6 | import '../test_helpers.dart'; 7 | 8 | main() async { 9 | testGoldens('appbar', (tester) async { 10 | loadAppFonts(); 11 | var (view, _) = await connect(LiveView(), rendered: { 12 | 's': [ 13 | """ 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | """ 23 | ] 24 | }); 25 | 26 | await tester.runLiveView(view); 27 | 28 | await tester.pumpAndSettle(); 29 | 30 | await expectLater(find.byType(MaterialApp), 31 | matchesGoldenFile('bottom_navigation_bar.png')); 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /example/macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_cached_networked_image.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 4 | 5 | class LiveCachedNetworkImage extends LiveStateWidget { 6 | const LiveCachedNetworkImage({super.key, required super.state}); 7 | 8 | @override 9 | State createState() => _LiveCachedNetworkImageState(); 10 | } 11 | 12 | class _LiveCachedNetworkImageState extends StateWidget { 13 | @override 14 | void onStateChange(Map diff) { 15 | reloadAttributes(node, ['imageUrl', 'width', 'height']); 16 | } 17 | 18 | @override 19 | Widget render(BuildContext context) { 20 | var url = getAttribute('imageUrl')!; 21 | if (!url.startsWith('http')) { 22 | url = '${liveView.endpointScheme}://${liveView.host}/$url'; 23 | } 24 | 25 | return CachedNetworkImage( 26 | placeholder: (context, url) => const CircularProgressIndicator(), 27 | imageUrl: url, 28 | width: doubleAttribute('width'), 29 | height: doubleAttribute('height')); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/render/multiple_components_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:liveview_flutter/live_view/live_view.dart'; 3 | 4 | import '../test_helpers.dart'; 5 | 6 | main() async { 7 | testWidgets('multiple dynamic components on the same rendering pass', 8 | (tester) async { 9 | var view = LiveView() 10 | ..handleRenderedMessage({ 11 | 's': ['', '', ''], 12 | '0': { 13 | 's': ['hello'], 14 | }, 15 | '1': { 16 | 's': ['world'], 17 | } 18 | }); 19 | 20 | await tester.runLiveView(view); 21 | await tester.pumpAndSettle(); 22 | 23 | expect(find.allTexts(), ['hello', 'world']); 24 | }); 25 | 26 | testWidgets('multiple components on the same rendering pass', (tester) async { 27 | var view = LiveView() 28 | ..handleRenderedMessage({ 29 | 's': ['', ''], 30 | '0': { 31 | 's': ['helloworld'], 32 | } 33 | }); 34 | 35 | await tester.runLiveView(view); 36 | await tester.pumpAndSettle(); 37 | 38 | expect(find.allTexts(), ['hello', 'world']); 39 | }); 40 | } 41 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_safe_area.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LiveSafeArea extends LiveStateWidget { 5 | const LiveSafeArea({super.key, required super.state}); 6 | 7 | @override 8 | State createState() => _LiveSafeAreaState(); 9 | } 10 | 11 | class _LiveSafeAreaState extends StateWidget { 12 | final attributes = [ 13 | 'left', 14 | 'top', 15 | 'right', 16 | 'bottom', 17 | 'minimum', 18 | 'maintainBottomViewPadding', 19 | ]; 20 | 21 | @override 22 | void onStateChange(Map diff) => 23 | reloadAttributes(node, attributes); 24 | 25 | @override 26 | Widget render(BuildContext context) { 27 | return SafeArea( 28 | left: booleanAttribute('left') ?? true, 29 | top: booleanAttribute('top') ?? true, 30 | right: booleanAttribute('right') ?? true, 31 | bottom: booleanAttribute('bottom') ?? true, 32 | minimum: marginOrPaddingAttribute('minimum') ?? EdgeInsets.zero, 33 | maintainBottomViewPadding: 34 | booleanAttribute('maintainBottomViewPadding') ?? false, 35 | child: singleChild(), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_drawer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LiveDrawer extends LiveStateWidget { 5 | const LiveDrawer({super.key, required super.state}); 6 | 7 | @override 8 | State createState() => _LiveDrawerState(); 9 | } 10 | 11 | class _LiveDrawerState extends StateWidget { 12 | @override 13 | void onStateChange(Map diff) => reloadAttributes(node, [ 14 | 'backgroundColor', 15 | 'elevation', 16 | 'shadowColor', 17 | 'surfaceTintColor', 18 | 'width', 19 | 'semanticLabel', 20 | 'clipBehavior' 21 | ]); 22 | 23 | @override 24 | Widget render(BuildContext context) { 25 | return Drawer( 26 | backgroundColor: colorAttribute(context, 'backgroundColor'), 27 | elevation: doubleAttribute('elevation'), 28 | shadowColor: colorAttribute(context, 'shadowColor'), 29 | surfaceTintColor: colorAttribute(context, 'surfaceTintColor'), 30 | width: doubleAttribute('width'), 31 | semanticLabel: getAttribute('semanticLabel'), 32 | clipBehavior: clipAttribute('clipBehavior'), 33 | child: singleChild()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/components/bottom_app_bar_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | 3 | import '../test_helpers.dart'; 4 | 5 | main() async { 6 | testWidgets('looks okay', (tester) => tester.checkScreenshot(""" 7 | 8 | 9 | hello 10 | 11 | 12 | 13 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | """, 'bottom_app_bar_test.png')); 26 | } 27 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_text.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/mapping/text_align.dart'; 3 | import 'package:liveview_flutter/live_view/mapping/text_replacement.dart'; 4 | import 'package:liveview_flutter/live_view/mapping/text_style_map.dart'; 5 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 6 | import 'package:xml/xml.dart'; 7 | 8 | class LiveText extends LiveStateWidget { 9 | const LiveText({required super.state, Key? key}) : super(key: key); 10 | 11 | @override 12 | State createState() => _LiveViewTextState(); 13 | } 14 | 15 | class _LiveViewTextState extends StateWidget { 16 | @override 17 | void onStateChange(Map diff) { 18 | reloadAttributes(node, ['style', 'textAlign']); 19 | listenInnerTextKeys(); 20 | } 21 | 22 | @override 23 | Widget render(BuildContext context) { 24 | var text = widget.state.node.innerText == '' 25 | ? widget.state.node.value ?? '' 26 | : widget.state.node.innerText; 27 | return Text( 28 | replaceVariables(text, currentVariables).trim(), 29 | style: getTextStyle(getAttribute('style'), context), 30 | textAlign: getTextAlign(getAttribute('textAlign')), 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - path_provider_foundation (0.0.1): 4 | - Flutter 5 | - FlutterMacOS 6 | - shared_preferences_foundation (0.0.1): 7 | - Flutter 8 | - FlutterMacOS 9 | - sqflite (0.0.3): 10 | - Flutter 11 | - FlutterMacOS 12 | 13 | DEPENDENCIES: 14 | - Flutter (from `Flutter`) 15 | - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) 16 | - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) 17 | - sqflite (from `.symlinks/plugins/sqflite/darwin`) 18 | 19 | EXTERNAL SOURCES: 20 | Flutter: 21 | :path: Flutter 22 | path_provider_foundation: 23 | :path: ".symlinks/plugins/path_provider_foundation/darwin" 24 | shared_preferences_foundation: 25 | :path: ".symlinks/plugins/shared_preferences_foundation/darwin" 26 | sqflite: 27 | :path: ".symlinks/plugins/sqflite/darwin" 28 | 29 | SPEC CHECKSUMS: 30 | Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 31 | path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c 32 | shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 33 | sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec 34 | 35 | PODFILE CHECKSUM: 70d9d25280d0dd177a5f637cdb0f0b0b12c6a189 36 | 37 | COCOAPODS: 1.15.0 38 | -------------------------------------------------------------------------------- /test/network/cookies_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:liveview_flutter/live_view/live_view.dart'; 3 | import 'package:liveview_flutter/live_view/ui/components/live_elevated_button.dart'; 4 | import 'package:shared_preferences/shared_preferences.dart'; 5 | 6 | import '../test_helpers.dart'; 7 | 8 | void main() async { 9 | testWidgets('cookies are stored and sent properly', (tester) async { 10 | var (view, server) = await connect(LiveView(), rendered: { 11 | 's': [ 12 | """
13 | 14 | Sign-in 15 | 16 | """ 17 | ], 18 | }, sharedPreferences: { 19 | 'cookie': 'storedCookie' 20 | }); 21 | 22 | await tester.runLiveView(view); 23 | await tester.pumpAndSettle(); 24 | await tester.tap(find.byType(LiveElevatedButton)); 25 | await tester.pumpAndSettle(); 26 | 27 | expect(server.httpRequestsMade[0].headers['cookie'], 'storedCookie'); 28 | expect(server.httpRequestsMade.last.headers['cookie'], 'live_view=session'); 29 | var prefs = await SharedPreferences.getInstance(); 30 | expect(prefs.getString('cookie'), 'live_view=session'); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_end_drawer.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LiveEndDrawer extends LiveStateWidget { 5 | const LiveEndDrawer({super.key, required super.state}); 6 | 7 | @override 8 | State createState() => _LiveEndDrawerState(); 9 | } 10 | 11 | class _LiveEndDrawerState extends StateWidget { 12 | @override 13 | void onStateChange(Map diff) => reloadAttributes(node, [ 14 | 'backgroundColor', 15 | 'elevation', 16 | 'shadowColor', 17 | 'surfaceTintColor', 18 | 'width', 19 | 'semanticLabel', 20 | 'clipBehavior' 21 | ]); 22 | 23 | @override 24 | Widget render(BuildContext context) { 25 | return Drawer( 26 | backgroundColor: colorAttribute(context, 'backgroundColor'), 27 | elevation: doubleAttribute('elevation'), 28 | shadowColor: colorAttribute(context, 'shadowColor'), 29 | surfaceTintColor: colorAttribute(context, 'surfaceTintColor'), 30 | width: doubleAttribute('width'), 31 | semanticLabel: getAttribute('semanticLabel'), 32 | clipBehavior: clipAttribute('clipBehavior'), 33 | child: singleChild()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/live_view/ui/root_view/root_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/live_view.dart'; 3 | import 'package:liveview_flutter/live_view/ui/root_view/root_material_app.dart'; 4 | import 'package:provider/provider.dart'; 5 | 6 | class LiveRootView extends StatefulWidget { 7 | final LiveView view; 8 | const LiveRootView({super.key, required this.view}); 9 | 10 | @override 11 | State createState() => _LiveRootViewState(); 12 | } 13 | 14 | class _LiveRootViewState extends State { 15 | LiveView get liveView => widget.view; 16 | 17 | @override 18 | Widget build(BuildContext context) { 19 | if (liveView.catchExceptions) { 20 | ErrorWidget.builder = (err) { 21 | return liveView.fallbackPages.buildFlutterError( 22 | liveView, 23 | FlutterErrorDetails( 24 | exception: err, 25 | stack: StackTrace.current, 26 | ), 27 | ); 28 | }; 29 | } 30 | return MultiProvider(providers: [ 31 | ChangeNotifierProvider.value(value: liveView.changeNotifier), 32 | ChangeNotifierProvider.value(value: liveView.connectionNotifier), 33 | ChangeNotifierProvider.value(value: liveView.themeSettings) 34 | ], child: LiveViewRootMaterialApp(view: widget.view)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_container.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/mapping/alignment_directional.dart'; 3 | import 'package:liveview_flutter/live_view/mapping/decoration.dart'; 4 | import 'package:liveview_flutter/live_view/mapping/edge_insets.dart'; 5 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 6 | 7 | class LiveContainer extends LiveStateWidget { 8 | const LiveContainer({super.key, required super.state}); 9 | 10 | @override 11 | State createState() => _LiveContainerState(); 12 | } 13 | 14 | class _LiveContainerState extends StateWidget { 15 | @override 16 | void onStateChange(Map diff) => reloadAttributes(node, 17 | ['margin', 'padding', 'decoration', 'height', 'width', 'alignment']); 18 | 19 | @override 20 | Widget render(BuildContext context) { 21 | return Container( 22 | alignment: getAlignmentDirectional('alignment'), 23 | height: doubleAttribute('height'), 24 | width: doubleAttribute('width'), 25 | margin: getEdgeInsets(getAttribute('margin')), 26 | padding: getEdgeInsets(getAttribute('padding')), 27 | decoration: getDecoration(context, getAttribute('decoration')), 28 | child: singleChild(), 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_drawer_header.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LiveDrawerHeader extends LiveStateWidget { 5 | const LiveDrawerHeader({super.key, required super.state}); 6 | 7 | @override 8 | State createState() => _LiveDrawerHeaderState(); 9 | } 10 | 11 | class _LiveDrawerHeaderState extends StateWidget { 12 | final attributes = [ 13 | 'decoration', 14 | 'margin', 15 | 'padding', 16 | 'duration', 17 | 'curve', 18 | ]; 19 | 20 | @override 21 | void onStateChange(Map diff) => 22 | reloadAttributes(node, attributes); 23 | 24 | @override 25 | Widget render(BuildContext context) { 26 | return DrawerHeader( 27 | decoration: decorationAttribute(context, 'decoration'), 28 | margin: 29 | edgeInsetsAttribute('margin') ?? const EdgeInsets.only(bottom: 8.0), 30 | padding: edgeInsetsAttribute('padding') ?? 31 | const EdgeInsets.fromLTRB(16.0, 16.0, 16.0, 8.0), 32 | duration: 33 | Duration(milliseconds: (doubleAttribute('duration') ?? 250).toInt()), 34 | curve: curveAttribute('curve') ?? Curves.fastOutSlowIn, 35 | child: singleChild(), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /test/components/bottom_sheet_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:golden_toolkit/golden_toolkit.dart'; 4 | import 'package:liveview_flutter/live_view/live_view.dart'; 5 | 6 | import '../test_helpers.dart'; 7 | 8 | main() async { 9 | testWidgets('looks okay', (tester) async { 10 | await loadAppFonts(); 11 | var (view, _) = await connect(LiveView(), rendered: { 12 | 's': [ 13 | """ 14 | 15 | bottom sheet 16 | 17 | 18 | open 19 | 20 | 21 | 22 | """ 23 | ], 24 | }); 25 | 26 | await tester.runLiveView(view); 27 | await tester.pumpAndSettle(); 28 | 29 | expect(find.text('bottom sheet'), findsNothing); 30 | 31 | await tester.tap(find.text('open'), warnIfMissed: false); 32 | await tester.pumpAndSettle(); 33 | 34 | expect(find.text('bottom sheet'), findsOneWidget); 35 | 36 | await expectLater( 37 | find.byType(MaterialApp), matchesGoldenFile('bottom_sheet_test.png')); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_scaffold.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/state/state_child.dart'; 3 | import 'package:liveview_flutter/live_view/ui/components/live_appbar.dart'; 4 | import 'package:liveview_flutter/live_view/ui/components/live_drawer.dart'; 5 | import 'package:liveview_flutter/live_view/ui/components/live_bottom_navigation_bar.dart'; 6 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 7 | 8 | class LiveScaffold extends LiveStateWidget { 9 | const LiveScaffold({super.key, required super.state}); 10 | 11 | @override 12 | State createState() => _LiveScaffoldState(); 13 | } 14 | 15 | class _LiveScaffoldState extends StateWidget { 16 | @override 17 | void onStateChange(Map diff) {} 18 | 19 | @override 20 | Widget render(BuildContext context) { 21 | var children = multipleChildren(); 22 | var appBar = StateChild.extractWidgetChild(children); 23 | var drawer = StateChild.extractWidgetChild(children); 24 | var bottomNavigationBar = 25 | StateChild.extractWidgetChild(children); 26 | 27 | return Scaffold( 28 | appBar: appBar, 29 | body: body(children), 30 | drawer: drawer, 31 | bottomNavigationBar: bottomNavigationBar, 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_bottom_app_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LiveBottomAppBar extends LiveStateWidget { 5 | const LiveBottomAppBar({super.key, required super.state}); 6 | 7 | @override 8 | State createState() => _LiveBottomAppBarState(); 9 | } 10 | 11 | class _LiveBottomAppBarState extends StateWidget { 12 | final attributes = [ 13 | 'color', 14 | 'elevation', 15 | 'clipBehavior', 16 | 'padding', 17 | 'color', 18 | 'elevation', 19 | 'shape', 20 | 'height' 21 | ]; 22 | 23 | @override 24 | void onStateChange(Map diff) => 25 | reloadAttributes(node, attributes); 26 | 27 | @override 28 | Widget render(BuildContext context) { 29 | return BottomAppBar( 30 | height: doubleAttribute('height'), 31 | clipBehavior: clipAttribute('clipBehavior') ?? Clip.none, 32 | padding: marginOrPaddingAttribute('padding'), 33 | shape: notchedShapeAttribute('shape'), 34 | color: colorAttribute(context, 'color'), 35 | elevation: doubleAttribute('elevation'), 36 | surfaceTintColor: colorAttribute(context, 'surfaceTintColor'), 37 | shadowColor: colorAttribute(context, 'shadowColor'), 38 | child: singleChild()); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /example/windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) { 10 | // Attach to console when present (e.g., 'flutter run') or create a 11 | // new console when running with a debugger. 12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 13 | CreateAndAttachConsole(); 14 | } 15 | 16 | // Initialize COM, so that it is available for use in the library and/or 17 | // plugins. 18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 19 | 20 | flutter::DartProject project(L"data"); 21 | 22 | std::vector command_line_arguments = 23 | GetCommandLineArguments(); 24 | 25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 26 | 27 | FlutterWindow window(project); 28 | Win32Window::Point origin(10, 10); 29 | Win32Window::Size size(1280, 720); 30 | if (!window.Create(L"liveview_flutter", origin, size)) { 31 | return EXIT_FAILURE; 32 | } 33 | window.SetQuitOnClose(true); 34 | 35 | ::MSG msg; 36 | while (::GetMessage(&msg, nullptr, 0, 0)) { 37 | ::TranslateMessage(&msg); 38 | ::DispatchMessage(&msg); 39 | } 40 | 41 | ::CoUninitialize(); 42 | return EXIT_SUCCESS; 43 | } 44 | -------------------------------------------------------------------------------- /test/render/margin_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:liveview_flutter/live_view/live_view.dart'; 4 | 5 | import '../test_helpers.dart'; 6 | 7 | main() async { 8 | testWidgets('global margin', (tester) async { 9 | var view = LiveView() 10 | ..handleRenderedMessage({ 11 | 's': [''], 12 | }); 13 | 14 | await tester.runLiveView(view); 15 | 16 | expect(find.firstOf().margin, const EdgeInsets.all(10.0)); 17 | }); 18 | 19 | testWidgets('symetric margin', (tester) async { 20 | var view = LiveView() 21 | ..handleRenderedMessage({ 22 | 's': [''], 23 | }); 24 | 25 | await tester.runLiveView(view); 26 | 27 | expect( 28 | find.firstOf().margin, 29 | const EdgeInsets.symmetric( 30 | vertical: 10.0, 31 | horizontal: 20.0, 32 | )); 33 | }); 34 | 35 | testWidgets('full margin', (tester) async { 36 | var view = LiveView() 37 | ..handleRenderedMessage({ 38 | 's': [''], 39 | }); 40 | 41 | await tester.runLiveView(view); 42 | 43 | expect( 44 | find.firstOf().margin, 45 | const EdgeInsets.only( 46 | top: 10.0, 47 | right: 20.0, 48 | bottom: 30.0, 49 | left: 40.0, 50 | )); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 3 | 4 | class LiveCard extends LiveStateWidget { 5 | const LiveCard({super.key, required super.state}); 6 | 7 | @override 8 | State createState() => _LiveCardState(); 9 | } 10 | 11 | class _LiveCardState extends StateWidget { 12 | final attributes = [ 13 | 'color', 14 | 'shadowColor', 15 | 'surfaceTintColor', 16 | 'elevation', 17 | 'shape', 18 | 'borderOnForeground', 19 | 'margin', 20 | 'clipBehavior', 21 | 'semanticContainer' 22 | ]; 23 | 24 | @override 25 | void onStateChange(Map diff) => 26 | reloadAttributes(node, attributes); 27 | 28 | @override 29 | Widget render(BuildContext context) { 30 | return Card( 31 | color: colorAttribute(context, 'color'), 32 | shadowColor: colorAttribute(context, 'shadowColor'), 33 | surfaceTintColor: colorAttribute(context, 'surfaceTintColor'), 34 | elevation: doubleAttribute('elevation'), 35 | shape: shapeBorderAttribute('shape'), 36 | borderOnForeground: booleanAttribute('borderOnForeground') ?? true, 37 | margin: marginOrPaddingAttribute('margin'), 38 | clipBehavior: clipAttribute('clipBehavior'), 39 | semanticContainer: booleanAttribute('semanticContainer') ?? true, 40 | child: singleChild()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/render/padding_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:liveview_flutter/live_view/live_view.dart'; 4 | 5 | import '../test_helpers.dart'; 6 | 7 | main() async { 8 | testWidgets('global padding', (tester) async { 9 | var view = LiveView() 10 | ..handleRenderedMessage({ 11 | 's': [''], 12 | }); 13 | 14 | await tester.runLiveView(view); 15 | 16 | expect(find.firstOf().padding, const EdgeInsets.all(10.0)); 17 | }); 18 | 19 | testWidgets('symetric padding', (tester) async { 20 | var view = LiveView() 21 | ..handleRenderedMessage({ 22 | 's': [''], 23 | }); 24 | 25 | await tester.runLiveView(view); 26 | 27 | expect( 28 | find.firstOf().padding, 29 | const EdgeInsets.symmetric( 30 | vertical: 10.0, 31 | horizontal: 20.0, 32 | )); 33 | }); 34 | 35 | testWidgets('full padding', (tester) async { 36 | var view = LiveView() 37 | ..handleRenderedMessage({ 38 | 's': [''], 39 | }); 40 | 41 | await tester.runLiveView(view); 42 | 43 | expect( 44 | find.firstOf().padding, 45 | const EdgeInsets.only( 46 | top: 10.0, 47 | right: 20.0, 48 | bottom: 30.0, 49 | left: 40.0, 50 | )); 51 | }); 52 | } 53 | -------------------------------------------------------------------------------- /lib/live_view/ui/errors/error_404.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class Error404 extends StatefulWidget { 4 | final String url; 5 | const Error404({super.key, required this.url}); 6 | 7 | @override 8 | State createState() => _Error404State(); 9 | } 10 | 11 | class _Error404State extends State { 12 | @override 13 | Widget build(BuildContext context) { 14 | List doc = [ 15 | Container( 16 | color: Colors.grey[200], 17 | padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 30), 18 | child: const Column( 19 | crossAxisAlignment: CrossAxisAlignment.start, 20 | children: [ 21 | Text( 22 | '404 Page not found', 23 | style: TextStyle( 24 | color: Colors.red, 25 | fontWeight: FontWeight.bold, 26 | fontSize: 20), 27 | ), 28 | ])) 29 | ]; 30 | doc.addAll([ 31 | Container( 32 | padding: const EdgeInsets.all(20), 33 | child: Text( 34 | "Flutter was unable to GET '${widget.url}', please add an HTML view at this endpoint so that the live view can get the metadada.", 35 | style: const TextStyle(color: Colors.black, fontSize: 15))), 36 | ]); 37 | return Scaffold( 38 | backgroundColor: Colors.white, 39 | body: ListView(children: doc), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at https://dart.dev/lints. 17 | # 18 | # Instead of disabling a lint rule for the entire project in the 19 | # section below, it can also be suppressed for a single line of code 20 | # or a specific dart file by using the `// ignore: name_of_lint` and 21 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 22 | # producing the lint. 23 | rules: 24 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 25 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 26 | 27 | # Additional information about this file can be found at 28 | # https://dart.dev/guides/language/analysis-options 29 | -------------------------------------------------------------------------------- /lib/exec/live_view_exec_registry.dart: -------------------------------------------------------------------------------- 1 | import 'package:liveview_flutter/exec/exec.dart'; 2 | 3 | typedef ExecBuilder = Exec Function( 4 | Map? value, Map? attributes); 5 | 6 | enum LiveViewExecTrigger { onTap } 7 | 8 | class LiveViewExecRegistry { 9 | LiveViewExecRegistry._internal(); 10 | 11 | static final LiveViewExecRegistry _instance = 12 | LiveViewExecRegistry._internal(); 13 | 14 | final Map _execs = {}; 15 | final Map> _execsByTriggers = {}; 16 | 17 | static LiveViewExecRegistry get instance => _instance; 18 | 19 | List execsByTrigger(LiveViewExecTrigger trigger) => 20 | _execsByTriggers[trigger] ?? []; 21 | 22 | void add(List execNames, ExecBuilder execBuilder, 23 | {List triggers = const []}) { 24 | for (var execName in execNames) { 25 | _execs[execName] = execBuilder; 26 | } 27 | for (var trigger in triggers) { 28 | _execsByTriggers[trigger] ??= []; 29 | for (var execName in execNames) { 30 | if (!_execsByTriggers[trigger]!.contains(execName)) { 31 | _execsByTriggers[trigger]!.add(execName); 32 | } 33 | } 34 | } 35 | } 36 | 37 | Exec? exec( 38 | String name, { 39 | Map? value, 40 | Map? attributes, 41 | }) { 42 | if (_execs.containsKey(name)) { 43 | return _execs[name]!.call(value, attributes); 44 | } 45 | 46 | return null; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/mapping/css_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:liveview_flutter/live_view/mapping/css.dart'; 3 | 4 | void main() { 5 | test('basic cases', () { 6 | expect(parseCss(''), []); 7 | 8 | expect(parseCss('hello: world'), [('hello', 'world')]); 9 | expect(parseCss(' hello : world '), [('hello', 'world')]); 10 | 11 | expect(parseCss('hello: world; a: b'), [('hello', 'world'), ('a', 'b')]); 12 | expect(parseCss(' hello : world ; a : b '), 13 | [('hello', 'world'), ('a', 'b')]); 14 | 15 | expect(parseCss('a: b; c:'), [('a', 'b')]); 16 | expect(parseCss('background: @theme.appBarTheme.backgroundColor'), 17 | [('background', '@theme.appBarTheme.backgroundColor')]); 18 | }); 19 | 20 | test('nested css', () { 21 | expect(parseCss('a: b; hello: { myprop: 1; something: 2 }; d: e'), 22 | [('a', 'b'), ('hello', 'myprop: 1; something: 2'), ('d', 'e')]); 23 | expect(parseCss('hello: { myprop: 1; world: { a: 1 }; b: 2 };'), 24 | [('hello', 'myprop: 1; world: { a: 1 }; b: 2')]); 25 | 26 | expect(parseCss('a: {b: 2}'), [('a', 'b: 2')]); 27 | expect(parseCss('a: {b: 2'), [('a', 'b: 2')]); 28 | }); 29 | 30 | test('multiline css', () { 31 | expect(parseCss(""" 32 | pressed: { 33 | fontWeight: bold 34 | } 35 | disabled: { 36 | fontWeight: w100 37 | } 38 | """), 39 | [('pressed', 'fontWeight: bold'), ('disabled', 'fontWeight: w100')]); 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /lib/live_view/ui/errors/flutter_error_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class FlutterErrorView extends StatefulWidget { 4 | final FlutterErrorDetails error; 5 | const FlutterErrorView({super.key, required this.error}); 6 | 7 | @override 8 | State createState() => _FlutterErrorViewState(); 9 | } 10 | 11 | class _FlutterErrorViewState extends State { 12 | @override 13 | Widget build(BuildContext context) { 14 | debugPrint(widget.error.toString()); 15 | List doc = [ 16 | Container( 17 | color: Colors.grey[200], 18 | padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 30), 19 | child: 20 | Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ 21 | Text( 22 | "Flutter exception: ${widget.error.summary.toString()}", 23 | style: const TextStyle( 24 | color: Colors.red, fontWeight: FontWeight.bold, fontSize: 20), 25 | ), 26 | Text('Stacktrace is shown below', 27 | style: TextStyle(fontSize: 15, color: Colors.grey[500])) 28 | ])) 29 | ]; 30 | doc.addAll([ 31 | Container( 32 | padding: const EdgeInsets.all(20), 33 | child: Text(widget.error.stack.toString(), 34 | style: const TextStyle(color: Colors.black, fontSize: 15))) 35 | ]); 36 | return Scaffold( 37 | backgroundColor: Colors.white, 38 | body: ListView(children: doc), 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /example/macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.14' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 34 | target 'RunnerTests' do 35 | inherit! :search_paths 36 | end 37 | end 38 | 39 | post_install do |installer| 40 | installer.pods_project.targets.each do |target| 41 | flutter_additional_macos_build_settings(target) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /test/when/when_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:liveview_flutter/when/when.dart'; 4 | 5 | import '../test_helpers.dart'; 6 | 7 | Future checkCondition( 8 | WidgetTester tester, String conditions, dynamic result) async { 9 | await tester.pumpWidget( 10 | MaterialApp( 11 | home: Builder( 12 | builder: (context) => Text( 13 | When(conditions: conditions).execute(context).toString()))), 14 | ); 15 | expect(find.firstText(), result.toString()); 16 | } 17 | 18 | void main() { 19 | testWidgets('when conditions', (tester) async { 20 | await checkCondition(tester, '500 > 600', false); 21 | await checkCondition(tester, '600 > 500', true); 22 | await checkCondition(tester, '700.0 > 500', true); 23 | await checkCondition(tester, '500 == 600', false); 24 | await checkCondition(tester, '500 == 500', true); 25 | await checkCondition(tester, '500 != 500', false); 26 | await checkCondition(tester, '500 != 600', true); 27 | }); 28 | 29 | testWidgets('and conditions', (tester) async { 30 | await checkCondition(tester, '600 > 500 and 300 > 400', false); 31 | await checkCondition(tester, '700 > 500 and 500 > 400', true); 32 | }); 33 | 34 | testWidgets('or conditions', (tester) async { 35 | await checkCondition(tester, '600 > 500 or 300 > 400', true); 36 | await checkCondition(tester, '700 > 500 or 500 > 400', true); 37 | await checkCondition(tester, '200 > 500 or 200 > 400', false); 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /lib/live_view/ui/errors/no_server_error_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class NoServerError extends StatefulWidget { 4 | final FlutterErrorDetails error; 5 | const NoServerError({super.key, required this.error}); 6 | 7 | @override 8 | State createState() => _NoServerErrorState(); 9 | } 10 | 11 | class _NoServerErrorState extends State { 12 | @override 13 | Widget build(BuildContext context) { 14 | debugPrint(widget.error.toString()); 15 | List doc = [ 16 | Container( 17 | color: Colors.grey[200], 18 | padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 30), 19 | child: 20 | Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ 21 | const Text( 22 | "Unable to connect to the Live View Server", 23 | style: TextStyle( 24 | color: Colors.red, fontWeight: FontWeight.bold, fontSize: 20), 25 | ), 26 | Text(widget.error.summary.toString()), 27 | Text('Stacktrace is shown below', 28 | style: TextStyle(fontSize: 15, color: Colors.grey[500])) 29 | ])) 30 | ]; 31 | doc.addAll([ 32 | Container( 33 | padding: const EdgeInsets.all(20), 34 | child: Text(widget.error.stack.toString(), 35 | style: const TextStyle(color: Colors.black, fontSize: 15))) 36 | ]); 37 | return Scaffold( 38 | backgroundColor: Colors.white, 39 | body: ListView(children: doc), 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '11.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | target 'RunnerTests' do 36 | inherit! :search_paths 37 | end 38 | end 39 | 40 | post_install do |installer| 41 | installer.pods_project.targets.each do |target| 42 | flutter_additional_ios_build_settings(target) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /test/mapping/edge_colors_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:liveview_flutter/live_view/mapping/edge_colors.dart'; 4 | 5 | void main() { 6 | testWidgets('material text style', (tester) async { 7 | await tester.pumpWidget( 8 | Builder( 9 | builder: (BuildContext context) { 10 | expect( 11 | getEdgeColors(context, ""), 12 | const EdgeColors.all(Colors.black), 13 | ); 14 | expect( 15 | getEdgeColors(context, "red"), 16 | const EdgeColors.all(Colors.red), 17 | ); 18 | expect( 19 | getEdgeColors(context, "red blue"), 20 | const EdgeColors.symmetric( 21 | vertical: Colors.red, 22 | horizontal: Colors.blue, 23 | ), 24 | ); 25 | expect( 26 | getEdgeColors(context, "red blue green"), 27 | const EdgeColors.only( 28 | top: Colors.red, 29 | right: Colors.blue, 30 | bottom: Colors.green, 31 | left: Colors.blue, 32 | ), 33 | ); 34 | expect( 35 | getEdgeColors(context, "red blue green pink"), 36 | const EdgeColors.only( 37 | top: Colors.red, 38 | right: Colors.blue, 39 | bottom: Colors.green, 40 | left: Colors.pink, 41 | ), 42 | ); 43 | 44 | return const SizedBox.shrink(); 45 | }, 46 | ), 47 | ); 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /lib/live_view/ui/root_view/root_app_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/live_view.dart'; 3 | import 'package:liveview_flutter/live_view/ui/components/live_appbar.dart'; 4 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 5 | 6 | class RootAppBar extends StatefulWidget implements PreferredSizeWidget { 7 | final LiveView view; 8 | const RootAppBar({super.key, required this.view}) 9 | : preferredSize = const Size.fromHeight(kToolbarHeight); 10 | 11 | @override 12 | final Size preferredSize; 13 | 14 | @override 15 | State createState() => _RootAppBarState(); 16 | } 17 | 18 | class _RootAppBarState extends State { 19 | LiveAppBar? bar; 20 | 21 | @override 22 | void initState() { 23 | widget.view.router.addListener(routeChange); 24 | super.initState(); 25 | } 26 | 27 | void routeChange() { 28 | if (mounted) { 29 | setState(() { 30 | bar = extractChild(widget.view.router.pages.last.widgets); 31 | }); 32 | } 33 | } 34 | 35 | T? extractChild(List children) { 36 | for (var child in children) { 37 | if (child is T) { 38 | return child; 39 | } 40 | } 41 | return null; 42 | } 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | bar ??= extractChild(widget.view.router.pages.last.widgets); 47 | return bar != null 48 | ? Container(key: const Key('main_app_bar'), child: bar) 49 | : const SizedBox.shrink(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_icon.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/mapping/colors.dart'; 3 | import 'package:liveview_flutter/live_view/mapping/number.dart'; 4 | import 'package:liveview_flutter/live_view/mapping/icons.dart'; 5 | import 'package:liveview_flutter/live_view/mapping/text_direction.dart'; 6 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 7 | 8 | class LiveIcon extends LiveStateWidget { 9 | const LiveIcon({super.key, required super.state}); 10 | 11 | @override 12 | State createState() => _LiveIconState(); 13 | } 14 | 15 | class _LiveIconState extends StateWidget { 16 | @override 17 | void onStateChange(Map diff) { 18 | reloadAttributes(node, [ 19 | 'name', 20 | 'size', 21 | 'fill', 22 | 'weight', 23 | 'grade', 24 | 'opticalSize', 25 | 'color', 26 | 'semanticLabel', 27 | 'textDirection' 28 | ]); 29 | } 30 | 31 | @override 32 | Widget render(BuildContext context) { 33 | return Icon(getIcon(getAttribute('name')), 34 | size: getDouble(getAttribute('size')), 35 | fill: getDouble(getAttribute('fill')), 36 | weight: getDouble(getAttribute('weight')), 37 | grade: getDouble(getAttribute('grade')), 38 | opticalSize: getDouble(getAttribute('opticalSize')), 39 | color: getColor(context, getAttribute('color')), 40 | semanticLabel: getAttribute('semanticLabel'), 41 | textDirection: getTextDirection(getAttribute('textDirection'))); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/live_view/mapping/axis_alignment.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | MainAxisAlignment? getMainAxisAlignment(String? prop) { 4 | switch (prop) { 5 | case 'center': 6 | return MainAxisAlignment.center; 7 | case 'start': 8 | return MainAxisAlignment.start; 9 | case 'end': 10 | return MainAxisAlignment.end; 11 | case 'spaceAround': 12 | return MainAxisAlignment.spaceAround; 13 | case 'spaceBetween': 14 | return MainAxisAlignment.spaceBetween; 15 | case 'spaceEvenly': 16 | return MainAxisAlignment.spaceEvenly; 17 | default: 18 | return null; 19 | } 20 | } 21 | 22 | MainAxisSize? getMainAxisSize(String? prop) { 23 | switch (prop) { 24 | case 'max': 25 | return MainAxisSize.max; 26 | case 'min': 27 | return MainAxisSize.min; 28 | default: 29 | return null; 30 | } 31 | } 32 | 33 | CrossAxisAlignment? getCrossAxisAlignment(String? prop) { 34 | switch (prop) { 35 | case 'center': 36 | return CrossAxisAlignment.center; 37 | case 'start': 38 | return CrossAxisAlignment.start; 39 | case 'end': 40 | return CrossAxisAlignment.end; 41 | case 'baseline': 42 | return CrossAxisAlignment.baseline; 43 | case 'stretch': 44 | return CrossAxisAlignment.stretch; 45 | default: 46 | return null; 47 | } 48 | } 49 | 50 | VerticalDirection? getVerticalDirection(String? prop) { 51 | switch (prop) { 52 | case 'down': 53 | return VerticalDirection.down; 54 | case 'up': 55 | return VerticalDirection.up; 56 | default: 57 | return null; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/forms/text_field_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:liveview_flutter/live_view/live_view.dart'; 4 | 5 | import '../test_helpers.dart'; 6 | 7 | String? fieldValue() => 8 | (find.byType(TextField).evaluate().first.widget as TextField) 9 | .controller 10 | ?.text; 11 | 12 | main() async { 13 | testWidgets('initial value cannot change from the server', (tester) async { 14 | var (view, _) = await connect(LiveView(), rendered: { 15 | 's': [''], 16 | '0': 'initialValue="initialValue"' 17 | }); 18 | await tester.runLiveView(view); 19 | await tester.pumpAndSettle(); 20 | 21 | expect(fieldValue(), 'initialValue'); 22 | 23 | view.handleDiffMessage({'0': 'initialValue="new value"'}); 24 | await tester.pumpAndSettle(); 25 | 26 | expect(fieldValue(), 'initialValue'); 27 | }); 28 | 29 | testWidgets('handles form change', (tester) async { 30 | var (view, server) = await connect(LiveView()); 31 | await tester.runLiveView(view); 32 | 33 | view.handleRenderedMessage({ 34 | 's': [ 35 | """ 36 |
37 | 38 | 39 | """ 40 | ], 41 | }); 42 | await tester.pumpAndSettle(); 43 | 44 | await tester.enterText(find.byType(TextField), 'typing'); 45 | 46 | expect( 47 | server.lastChannelAction, 48 | liveEvents.phxFormValidate( 49 | 'my_validate_event', 'myfield=typing&_target=myfield')); 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/live_view/ui/loading/reload_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ReloadWidget extends StatefulWidget { 4 | const ReloadWidget({super.key}); 5 | 6 | @override 7 | State createState() => _ReloadWidgetState(); 8 | } 9 | 10 | class _ReloadWidgetState extends State 11 | with TickerProviderStateMixin { 12 | late AnimationController controller; 13 | 14 | @override 15 | void initState() { 16 | controller = AnimationController( 17 | duration: const Duration(milliseconds: 1000), 18 | vsync: this, 19 | )..drive(CurveTween(curve: Curves.easeIn)); 20 | Tween(begin: 0, end: 1).animate(controller); 21 | controller.forward(); 22 | super.initState(); 23 | } 24 | 25 | @override 26 | void dispose() { 27 | controller.dispose(); 28 | super.dispose(); 29 | } 30 | 31 | @override 32 | Widget build(BuildContext context) { 33 | return AnimatedBuilder( 34 | animation: controller, 35 | builder: (_, __) { 36 | return FractionallySizedBox( 37 | widthFactor: controller.value, 38 | child: Container( 39 | height: 5, 40 | decoration: const BoxDecoration( 41 | gradient: LinearGradient( 42 | begin: Alignment.topLeft, 43 | end: Alignment(0.8, 1), 44 | colors: [ 45 | Colors.green, 46 | Colors.red, 47 | Colors.blue, 48 | Colors.purple 49 | ])))); 50 | }); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/mapping/text_style_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:liveview_flutter/live_view/mapping/text_style_map.dart'; 4 | 5 | MaterialStateProperty? materialTextStyle() => 6 | (find.byType(FilledButton).evaluate().first.widget as FilledButton) 7 | .style 8 | ?.textStyle; 9 | 10 | Future setStyle(WidgetTester tester, String style) async { 11 | await tester.pumpWidget(MaterialApp(home: Builder(builder: (context) { 12 | return FilledButton( 13 | onPressed: () {}, 14 | style: ButtonStyle(textStyle: getMaterialTextStyle(style, context)), 15 | child: const Text('hello'), 16 | ); 17 | }))); 18 | await tester.pumpAndSettle(); 19 | } 20 | 21 | main() { 22 | testWidgets('material text style', (tester) async { 23 | await setStyle(tester, 'hello'); 24 | expect(materialTextStyle()!.resolve({}), const TextStyle()); 25 | 26 | await setStyle(tester, 'fontWeight: bold'); 27 | expect(materialTextStyle()!.resolve({}), 28 | const TextStyle(fontWeight: FontWeight.bold)); 29 | 30 | await setStyle(tester, """' 31 | pressed: { 32 | fontWeight: bold 33 | color: #F44336 34 | } 35 | disabled: { 36 | fontWeight: w100 37 | } 38 | """); 39 | var style = materialTextStyle()!; 40 | expect(style.resolve({MaterialState.pressed}), 41 | const TextStyle(fontWeight: FontWeight.bold, color: Color(0xfff44336))); 42 | expect(style.resolve({MaterialState.disabled}), 43 | const TextStyle(fontWeight: FontWeight.w100)); 44 | }); 45 | } 46 | -------------------------------------------------------------------------------- /test/plugins/basic_plugin.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/exec/exec.dart'; 3 | import 'package:liveview_flutter/exec/live_view_exec_registry.dart'; 4 | import 'package:liveview_flutter/live_view/plugin.dart'; 5 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 6 | import 'package:liveview_flutter/live_view/ui/live_view_ui_registry.dart'; 7 | 8 | class MyWidget extends LiveStateWidget { 9 | const MyWidget({super.key, required super.state}); 10 | 11 | @override 12 | State createState() => _MyWidget(); 13 | } 14 | 15 | class _MyWidget extends StateWidget { 16 | @override 17 | void onStateChange(Map diff) {} 18 | 19 | @override 20 | Widget render(BuildContext context) => const Text('MyComponent'); 21 | } 22 | 23 | List myPluginActions = []; 24 | 25 | class MyPluginExec extends Exec { 26 | Map? attributes; 27 | MyPluginExec({required this.attributes}); 28 | 29 | @override 30 | void handler(BuildContext context, StateWidget widget) { 31 | myPluginActions.add(attributes!['phx-my-plugin']); 32 | } 33 | } 34 | 35 | class BasicPlugin extends Plugin { 36 | @override 37 | String get name => "my_plugin"; 38 | 39 | @override 40 | registerWidgets(LiveViewUiRegistry registry) { 41 | registry.add(['MyComponent'], (state) => [MyWidget(state: state)]); 42 | } 43 | 44 | @override 45 | registerExecs(LiveViewExecRegistry registry) { 46 | registry.add(['phx-my-plugin'], 47 | (value, attributes) => MyPluginExec(attributes: attributes), 48 | triggers: [LiveViewExecTrigger.onTap]); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_checkbox.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/live_form.dart'; 3 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 4 | import 'package:uuid/uuid.dart'; 5 | 6 | class LiveCheckbox extends LiveStateWidget { 7 | const LiveCheckbox({super.key, required super.state}); 8 | 9 | @override 10 | State createState() => _LiveCheckboxState(); 11 | } 12 | 13 | class _LiveCheckboxState extends StateWidget { 14 | var unamedInput = const Uuid().v4(); 15 | bool _isChecked = false; 16 | bool _initialBoot = true; 17 | 18 | @override 19 | HandleClickState handleClickState() => HandleClickState.manual; 20 | 21 | @override 22 | void onWipeState() { 23 | _initialBoot = true; 24 | super.onWipeState(); 25 | } 26 | 27 | @override 28 | void onStateChange(Map diff) { 29 | reloadAttributes(node, ['checked', 'name']); 30 | if (_initialBoot) { 31 | _isChecked = booleanAttribute('checked') ?? false; 32 | _initialBoot = false; 33 | } 34 | } 35 | 36 | @override 37 | Widget render(BuildContext context) { 38 | return Checkbox( 39 | value: _isChecked, 40 | onChanged: (val) { 41 | setState(() { 42 | _isChecked = val ?? false; 43 | }); 44 | FormFieldEvent( 45 | name: getAttribute('name') ?? "unamed-text-field-$unamedInput", 46 | data: _isChecked ? 'on' : null, 47 | type: FormFieldEventType.change, 48 | ).dispatch(context); 49 | executeTapEventsManually(); 50 | }, 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lib/exec/exec_confirmable.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/exec/exec.dart'; 3 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 4 | 5 | class DataConfirm { 6 | final String title; 7 | final String message; 8 | final String confirm; 9 | final String cancel; 10 | 11 | DataConfirm({ 12 | required this.message, 13 | String? title, 14 | String? confirm, 15 | String? cancel, 16 | }) : title = title ?? 'Confirm?', 17 | confirm = confirm ?? 'Ok', 18 | cancel = cancel ?? 'Cancel'; 19 | } 20 | 21 | abstract class ExecConfirmable extends Exec { 22 | /// This is responsible for show an alert before executing this action 23 | final DataConfirm? dataConfirm; 24 | 25 | ExecConfirmable({this.dataConfirm}); 26 | 27 | @override 28 | void conditionalHandler(BuildContext context, StateWidget widget) { 29 | if (conditions.execute(context) == false) return; 30 | 31 | if (dataConfirm == null) return handler(context, widget); 32 | 33 | showDialog( 34 | context: context, 35 | builder: (context) => AlertDialog( 36 | title: Text(dataConfirm!.title), 37 | content: Text(dataConfirm!.message), 38 | actions: [ 39 | TextButton( 40 | child: Text(dataConfirm!.cancel), 41 | onPressed: () => Navigator.of(context).pop(), 42 | ), 43 | TextButton( 44 | child: Text(dataConfirm!.confirm), 45 | onPressed: () => Navigator.of(context).pop(true), 46 | ), 47 | ], 48 | ), 49 | ).then((result) { 50 | if (result == true) return handler(context, widget); 51 | }); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_text_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/live_form.dart'; 3 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 4 | import 'package:uuid/uuid.dart'; 5 | 6 | class LiveTextButton extends LiveStateWidget { 7 | const LiveTextButton({super.key, required super.state}); 8 | 9 | @override 10 | State createState() => _LiveTextButtonState(); 11 | } 12 | 13 | class _LiveTextButtonState extends StateWidget { 14 | var unamedInput = const Uuid().v4(); 15 | 16 | @override 17 | handleClickState() => HandleClickState.manual; 18 | 19 | @override 20 | void onStateChange(Map diff) { 21 | reloadAttributes(node, [ 22 | 'type', 23 | 'name', 24 | 'style', 25 | 'autofocus', 26 | 'clipBehavior', 27 | ]); 28 | } 29 | 30 | @override 31 | Widget render(BuildContext context) { 32 | return TextButton( 33 | style: buttonStyleAttribute(context, 'style'), 34 | autofocus: booleanAttribute('autofocus') ?? false, 35 | clipBehavior: clipAttribute('clipBehavior') ?? Clip.none, 36 | onPressed: () { 37 | if (getAttribute('type') == 'submit') { 38 | FormFieldEvent( 39 | name: getAttribute('name') ?? 40 | 'unamed-text-button-$unamedInput', 41 | data: null, 42 | type: FormFieldEventType.submit) 43 | .dispatch(context); 44 | } 45 | executeTapEventsManually(); 46 | }, 47 | child: AbsorbPointer(child: singleChild())); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_elevated_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/ui/components/live_form.dart'; 3 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 4 | import 'package:uuid/uuid.dart'; 5 | 6 | class LiveElevatedButton extends LiveStateWidget { 7 | const LiveElevatedButton({super.key, required super.state}); 8 | 9 | @override 10 | State createState() => _LiveElevatedButtonState(); 11 | } 12 | 13 | class _LiveElevatedButtonState extends StateWidget { 14 | var unamedInput = const Uuid().v4(); 15 | 16 | @override 17 | void onStateChange(Map diff) { 18 | reloadAttributes(node, [ 19 | 'type', 20 | 'name', 21 | 'style', 22 | 'autofocus', 23 | 'clipBehavior', 24 | ]); 25 | } 26 | 27 | @override 28 | handleClickState() => HandleClickState.manual; 29 | 30 | @override 31 | Widget render(BuildContext context) { 32 | return ElevatedButton( 33 | style: buttonStyleAttribute(context, 'style'), 34 | autofocus: booleanAttribute('autofocus') ?? false, 35 | clipBehavior: clipAttribute('clipBehavior') ?? Clip.none, 36 | onPressed: () { 37 | if (getAttribute('type') == 'submit') { 38 | FormFieldEvent( 39 | name: getAttribute('name') ?? 40 | 'unamed-elevated-button-$unamedInput', 41 | data: null, 42 | type: FormFieldEventType.submit) 43 | .dispatch(context); 44 | } 45 | executeTapEventsManually(); 46 | }, 47 | child: AbsorbPointer(child: singleChild())); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /test/components/floating_action_button_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:liveview_flutter/live_view/live_view.dart'; 4 | 5 | import '../test_helpers.dart'; 6 | 7 | bool? checkValue() => 8 | (find.byType(Checkbox).evaluate().first.widget as Checkbox).value; 9 | 10 | main() async { 11 | testWidgets('normal looks okay', (tester) => tester.checkScreenshot(""" 12 | 13 | 14 | hello world 15 | 16 | """, 'foating_action_button_test.png')); 17 | 18 | testWidgets('extended looks okay', (tester) => tester.checkScreenshot(""" 19 | 20 | hello 21 | hello world 22 | 23 | """, 'foating_action_button_extended_test.png')); 24 | 25 | testWidgets('phx click works', (tester) async { 26 | var (view, server) = await connect(LiveView(), rendered: { 27 | 's': [ 28 | """ 29 | 30 | hello 31 | my view 32 | 33 | """ 34 | ], 35 | }); 36 | 37 | await tester.runLiveView(view); 38 | await tester.pumpAndSettle(); 39 | 40 | await tester.tap(find.text('hello')); 41 | await tester.pumpAndSettle(); 42 | 43 | expect(server.lastChannelAction, 44 | liveEvents.phxClick({}, eventName: 'server_event')); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /lib/live_view/ui/root_view/root_bottom_navigation_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/live_view.dart'; 3 | import 'package:liveview_flutter/live_view/ui/components/live_bottom_app_bar.dart'; 4 | import 'package:liveview_flutter/live_view/ui/components/live_bottom_navigation_bar.dart'; 5 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 6 | 7 | class RootBottomNavigationBar extends StatefulWidget { 8 | final LiveView view; 9 | const RootBottomNavigationBar({super.key, required this.view}); 10 | 11 | @override 12 | State createState() => 13 | _RootBottomNavigationBarState(); 14 | } 15 | 16 | class _RootBottomNavigationBarState extends State { 17 | Widget? bar; 18 | 19 | @override 20 | void initState() { 21 | widget.view.router.addListener(routeChange); 22 | 23 | super.initState(); 24 | } 25 | 26 | @override 27 | void dispose() { 28 | super.dispose(); 29 | } 30 | 31 | void routeChange() { 32 | setState(() {}); 33 | } 34 | 35 | T? extractChild(List children) { 36 | for (var child in children) { 37 | if (child is T) { 38 | return child; 39 | } 40 | } 41 | return null; 42 | } 43 | 44 | @override 45 | Widget build(BuildContext context) { 46 | if (widget.view.router.pages.last.containsGlobalNavigationWidgets) { 47 | bar = extractChild( 48 | widget.view.router.pages.last.widgets); 49 | bar ??= 50 | extractChild(widget.view.router.pages.last.widgets); 51 | } else { 52 | bar = null; 53 | } 54 | return bar ?? const SizedBox.shrink(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_row.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/mapping/axis_alignment.dart'; 3 | import 'package:liveview_flutter/live_view/mapping/text_baseline.dart'; 4 | import 'package:liveview_flutter/live_view/mapping/text_direction.dart'; 5 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 6 | 7 | class LiveRow extends LiveStateWidget { 8 | const LiveRow({super.key, required super.state}); 9 | 10 | @override 11 | State createState() => _LiveColState(); 12 | } 13 | 14 | class _LiveColState extends StateWidget { 15 | @override 16 | void onStateChange(Map diff) => reloadAttributes(node, [ 17 | 'mainAxisAlignment', 18 | 'crossAxisAlignment', 19 | 'textDirection', 20 | 'mainAxisSize', 21 | 'verticalDirection', 22 | 'textBaseline' 23 | ]); 24 | 25 | @override 26 | Widget render(BuildContext context) { 27 | return Row( 28 | mainAxisAlignment: 29 | getMainAxisAlignment(getAttribute('mainAxisAlignment')) ?? 30 | MainAxisAlignment.start, 31 | crossAxisAlignment: 32 | getCrossAxisAlignment(getAttribute('crossAxisAlignment')) ?? 33 | CrossAxisAlignment.center, 34 | mainAxisSize: 35 | getMainAxisSize(getAttribute('mainAxisSize')) ?? MainAxisSize.max, 36 | textDirection: getTextDirection(getAttribute('textDirection')), 37 | verticalDirection: 38 | getVerticalDirection(getAttribute('verticalDirection')) ?? 39 | VerticalDirection.down, 40 | textBaseline: getTextBaseline(getAttribute('textBaseline')), 41 | children: multipleChildren()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_column.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:liveview_flutter/live_view/mapping/axis_alignment.dart'; 3 | import 'package:liveview_flutter/live_view/mapping/text_baseline.dart'; 4 | import 'package:liveview_flutter/live_view/mapping/text_direction.dart'; 5 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 6 | 7 | class LiveColumn extends LiveStateWidget { 8 | const LiveColumn({super.key, required super.state}); 9 | 10 | @override 11 | State createState() => _LiveColState(); 12 | } 13 | 14 | class _LiveColState extends StateWidget { 15 | @override 16 | void onStateChange(Map diff) => reloadAttributes(node, [ 17 | 'mainAxisAlignment', 18 | 'mainAxisSize', 19 | 'crossAxisAlignment', 20 | 'textDirection', 21 | 'verticalDirection', 22 | 'textBaseline' 23 | ]); 24 | 25 | @override 26 | Widget render(BuildContext context) { 27 | return Column( 28 | mainAxisAlignment: 29 | getMainAxisAlignment(getAttribute('mainAxisAlignment')) ?? 30 | MainAxisAlignment.start, 31 | mainAxisSize: 32 | getMainAxisSize(getAttribute('mainAxisSize')) ?? MainAxisSize.max, 33 | crossAxisAlignment: 34 | getCrossAxisAlignment(getAttribute('crossAxisAlignment')) ?? 35 | CrossAxisAlignment.center, 36 | textDirection: getTextDirection(getAttribute('textDirection')), 37 | verticalDirection: 38 | getVerticalDirection(getAttribute('verticalDirection')) ?? 39 | VerticalDirection.down, 40 | textBaseline: getTextBaseline(getAttribute('textBaseline')), 41 | children: multipleChildren(), 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/components/action_chip_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:liveview_flutter/live_view/live_view.dart'; 4 | 5 | import '../test_helpers.dart'; 6 | 7 | bool? checkValue() => 8 | (find.byType(Checkbox).evaluate().first.widget as Checkbox).value; 9 | 10 | main() async { 11 | testWidgets('looks okay', (tester) => tester.checkScreenshot(""" 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | hello 23 | 24 | 25 | 26 | 27 | 28 | """, 'action_chip_test.png')); 29 | 30 | testWidgets('phx click works', (tester) async { 31 | var (view, server) = await connect(LiveView(), rendered: { 32 | 's': [ 33 | """ 34 | 35 | 36 | 37 | 38 | 39 | 40 | """ 41 | ], 42 | }); 43 | 44 | await tester.runLiveView(view); 45 | await tester.pumpAndSettle(); 46 | 47 | await tester.tap(find.text('hello'), warnIfMissed: false); 48 | await tester.pumpAndSettle(); 49 | 50 | expect(server.lastChannelAction, 51 | liveEvents.phxClick({}, eventName: 'server_event')); 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /lib/live_view/ui/components/live_single_child_scroll_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/gestures.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:liveview_flutter/live_view/ui/components/state_widget.dart'; 4 | 5 | class LiveSingleChildScrollView 6 | extends LiveStateWidget { 7 | const LiveSingleChildScrollView({super.key, required super.state}); 8 | 9 | @override 10 | State createState() => 11 | _LiveSingleChildScrollViewState(); 12 | } 13 | 14 | class _LiveSingleChildScrollViewState 15 | extends StateWidget { 16 | final attributes = [ 17 | 'scrollDirection', 18 | 'reverse', 19 | 'padding', 20 | 'primary', 21 | 'dragStartBehavior', 22 | 'keyboardDismissBehavior', 23 | 'restorationId', 24 | 'clipBehavior' 25 | ]; 26 | 27 | @override 28 | void onStateChange(Map diff) => 29 | reloadAttributes(node, attributes); 30 | 31 | @override 32 | Widget render(BuildContext context) { 33 | return SingleChildScrollView( 34 | scrollDirection: axisAttribute('scrollDirection') ?? Axis.vertical, 35 | reverse: booleanAttribute('reverse') ?? false, 36 | padding: marginOrPaddingAttribute('padding'), 37 | primary: booleanAttribute('primary'), 38 | dragStartBehavior: dragStartBehaviorAttribute('dragStartBehavior') ?? 39 | DragStartBehavior.start, 40 | keyboardDismissBehavior: scrollViewKeyboardDismissBehaviorAttribute( 41 | 'keyboardDismissBehavior') ?? 42 | ScrollViewKeyboardDismissBehavior.manual, 43 | restorationId: getAttribute('restorationId'), 44 | clipBehavior: clipAttribute('clipBehavior') ?? Clip.hardEdge, 45 | child: singleChild(), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/live_view/mapping/css.dart: -------------------------------------------------------------------------------- 1 | extension StringX on String { 2 | isNumber() { 3 | var val = codeUnitAt(0); 4 | return val >= 48 && val <= 57; 5 | } 6 | 7 | isLetterOrNumber() { 8 | var val = codeUnitAt(0); 9 | return (val >= 97 && val <= 122) || 10 | (val >= 65 && val <= 90) || 11 | (val >= 48 && val <= 57) || 12 | this == '#' || 13 | this == '@' || 14 | this == '.'; 15 | } 16 | } 17 | 18 | List<(String, String)> parseCss(String style) { 19 | style = style.replaceAll("\n", ""); 20 | var currentToken = 0; 21 | List css = []; 22 | 23 | while (currentToken < style.length) { 24 | if (style[currentToken].isLetterOrNumber()) { 25 | var from = currentToken; 26 | while (currentToken < style.length && 27 | style[currentToken].isLetterOrNumber()) { 28 | currentToken++; 29 | } 30 | css.add(style.substring(from, currentToken)); 31 | } else if (style[currentToken] == '{') { 32 | currentToken++; 33 | var from = currentToken; 34 | var count = 1; 35 | while (count != 0 && currentToken < style.length) { 36 | if (style[currentToken] == '{') { 37 | count++; 38 | } else if (style[currentToken] == '}') { 39 | count--; 40 | } 41 | currentToken++; 42 | } 43 | if (count == 0) { 44 | currentToken--; 45 | } 46 | css.add(style.substring(from, currentToken).trim()); 47 | } else { 48 | currentToken++; 49 | } 50 | } 51 | 52 | // invalid css, we remove the last property 53 | if (css.length % 2 == 1) { 54 | css.removeLast(); 55 | } 56 | 57 | List<(String, String)> ret = []; 58 | for (var i = 0; i < css.length; i += 2) { 59 | ret.add((css[i], css[i + 1])); 60 | } 61 | return ret; 62 | } 63 | -------------------------------------------------------------------------------- /lib/live_view/ui/node_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:liveview_flutter/live_view/live_view.dart'; 3 | import 'package:liveview_flutter/live_view/ui/live_view_ui_parser.dart'; 4 | import 'package:xml/xml.dart'; 5 | 6 | /// NodeState represents the state of an XML node 7 | /// 8 | /// It contains both the node itself and the local variables associated to it 9 | class NodeState { 10 | final XmlNode node; 11 | final Map variables; 12 | final LiveViewUiParser parser; 13 | final List nestedState; 14 | final LiveView liveView; 15 | final String urlPath; 16 | final ViewType viewType; 17 | List dynamicWidget; 18 | 19 | bool get isOnTheCurrentPage => urlPath == liveView.currentUrl; 20 | 21 | NodeState({ 22 | required this.node, 23 | required this.variables, 24 | required this.parser, 25 | required this.nestedState, 26 | required this.liveView, 27 | required this.urlPath, 28 | required this.viewType, 29 | this.dynamicWidget = const [], 30 | }); 31 | 32 | NodeState copyWith({ 33 | XmlNode? node, 34 | final Map? variables, 35 | LiveViewUiParser? parser, 36 | List? nestedState, 37 | LiveView? liveView, 38 | String? urlPath, 39 | List? dynamicWidget, 40 | String? componentId, 41 | ViewType? viewType, 42 | }) => 43 | NodeState( 44 | node: node ?? this.node, 45 | variables: variables ?? this.variables, 46 | parser: parser ?? this.parser, 47 | nestedState: nestedState ?? this.nestedState, 48 | liveView: liveView ?? this.liveView, 49 | urlPath: urlPath ?? this.urlPath, 50 | dynamicWidget: dynamicWidget ?? this.dynamicWidget, 51 | viewType: viewType ?? this.viewType, 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /lib/live_view/ui/errors/missing_page_component.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class MissingPageComponent extends StatefulWidget { 4 | final String url; 5 | final String html; 6 | const MissingPageComponent( 7 | {super.key, required this.url, required this.html}); 8 | 9 | @override 10 | State createState() => _MissingPageComponentState(); 11 | } 12 | 13 | class _MissingPageComponentState extends State { 14 | @override 15 | Widget build(BuildContext context) { 16 | List doc = [ 17 | Container( 18 | color: Colors.grey[200], 19 | padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 30), 20 | child: 21 | Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ 22 | Text( 23 | "Unable to find any component on url ${widget.url}", 24 | style: const TextStyle( 25 | color: Colors.red, fontWeight: FontWeight.bold, fontSize: 20), 26 | ), 27 | ])) 28 | ]; 29 | doc.addAll([ 30 | Container( 31 | padding: const EdgeInsets.all(20), 32 | child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ 33 | const Text( 34 | "Your page needs to contain a component directly inside the component representing the view", 35 | style: TextStyle(color: Colors.black, fontSize: 15)), 36 | const Text('Current invalid view returned:', 37 | style: TextStyle(color: Colors.black, fontSize: 15)), 38 | Text(widget.html) 39 | ]), 40 | ) 41 | ]); 42 | return Scaffold( 43 | backgroundColor: Colors.white, 44 | body: ListView(children: doc), 45 | ); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/render/dynamic_diff_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:liveview_flutter/live_view/live_view.dart'; 3 | 4 | import '../test_helpers.dart'; 5 | 6 | main() async { 7 | testWidgets('Dynamic diffs are working', (tester) async { 8 | var view = LiveView() 9 | ..handleRenderedMessage({ 10 | 's': ['', '', ''], 11 | '1': { 12 | 's': ['something'], 13 | } 14 | }); 15 | 16 | await tester.runLiveView(view); 17 | await tester.pumpAndSettle(); 18 | 19 | view.handleDiffMessage({ 20 | '0': { 21 | 's': ['', ''], 22 | 'd': [ 23 | ['kind="info"', "hello"], 24 | ['kind="error"', "world"], 25 | ] 26 | }, 27 | }); 28 | 29 | await tester.pumpAndSettle(); 30 | 31 | expect(find.allTexts(), ['hello', 'world', 'something']); 32 | 33 | view.handleDiffMessage({ 34 | '0': {'d': []} 35 | }); 36 | 37 | await tester.pumpAndSettle(); 38 | 39 | expect(find.allTexts(), ['something']); 40 | }); 41 | 42 | testWidgets('dynamic diffs with empty spots are working', (tester) async { 43 | var view = LiveView() 44 | ..handleRenderedMessage({ 45 | 's': ['', '', ''], 46 | '1': { 47 | 's': ['hello'], 48 | } 49 | }); 50 | 51 | await tester.runLiveView(view); 52 | await tester.pumpAndSettle(); 53 | 54 | view.handleDiffMessage({ 55 | '0': { 56 | 's': ['', ''], 57 | 'd': [ 58 | ["other text"], 59 | ] 60 | }, 61 | }); 62 | 63 | await tester.pumpAndSettle(); 64 | 65 | expect(find.allTexts(), ['other text', 'hello']); 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 16 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 34 | 35 | 36 | --------------------------------------------------------------------------------