diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies deleted file mode 100644 index 94e7721..0000000 --- a/.flutter-plugins-dependencies +++ /dev/null @@ -1 +0,0 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"webview_flutter_wkwebview","path":"/Users/coffeen/.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.22.0/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"webview_flutter_android","path":"/Users/coffeen/.pub-cache/hosted/pub.dev/webview_flutter_android-4.7.0/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"webview_flutter_wkwebview","path":"/Users/coffeen/.pub-cache/hosted/pub.dev/webview_flutter_wkwebview-3.22.0/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[],"windows":[],"web":[]},"dependencyGraph":[{"name":"webview_flutter","dependencies":["webview_flutter_android","webview_flutter_wkwebview"]},{"name":"webview_flutter_android","dependencies":[]},{"name":"webview_flutter_wkwebview","dependencies":[]}],"date_created":"2025-06-23 22:41:03.527735","version":"3.32.1","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/.gitignore b/.gitignore index ac5aa98..8622390 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,7 @@ migrate_working_dir/ **/doc/api/ .dart_tool/ build/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ diff --git a/CHANGELOG.md b/CHANGELOG.md index acd5595..1148484 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## 1.2.0 + +* 31 refactor colorutil by @warioddly in https://github.com/warioddly/graphify/pull/33 +* refactor GraphifyController and update dependencies by @warioddly in https://github.com/warioddly/graphify/pull/36 +* refactor: update plugin paths and improve Graphify interface structure by @warioddly in https://github.com/warioddly/graphify/pull/37 +* bump version to 1.2.0 in pubspec.yaml by @warioddly in https://github.com/warioddly/graphify/pull/38 +**Full Changelog**: https://github.com/warioddly/graphify/compare/v1.1.1...v1.2.0 + ## 1.1.1 * Fix bug in old versions Flutter "Try changing the name to the name of an existing improvement, or identify an improvement, or a field named "r"." diff --git a/README.md b/README.md index b3f8df7..4e340ee 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ Pub Package GitHub Repo stars -GitHub closed issues -GitHub open issues +GitHub closed issues +GitHub open issues GitHub contributors Contributing diff --git a/android/build.gradle b/android/build.gradle index 62827b9..9cc64c5 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -2,14 +2,14 @@ group 'com.warioddly.graphify' version '1.0-SNAPSHOT' buildscript { - ext.kotlin_version = '1.7.10' + ext.kotlin_version = '2.1.0' repositories { google() mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.3.0' + classpath 'com.android.tools.build:gradle:8.7.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/example/android/build.gradle b/example/android/build.gradle index 0ae2992..a0c36a3 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.8.10' + ext.kotlin_version = '2.1.0' repositories { google() mavenCentral() diff --git a/example/android/settings.gradle b/example/android/settings.gradle index 7ef64d7..dfd3282 100644 --- a/example/android/settings.gradle +++ b/example/android/settings.gradle @@ -23,7 +23,7 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "8.1.0" apply false + id "com.android.application" version "8.7.0" apply false } include ":app" diff --git a/example/lib/charts/basic_line_chart.dart b/example/lib/charts/basic_line_chart.dart index 4738033..d104a46 100644 --- a/example/lib/charts/basic_line_chart.dart +++ b/example/lib/charts/basic_line_chart.dart @@ -12,7 +12,6 @@ class BasicLineChart extends StatefulWidget { } class _BasicLineChartState extends State { - final controller = GraphifyController(); Timer? timer; @@ -28,7 +27,6 @@ class _BasicLineChartState extends State { ] }); }); - } @override @@ -44,9 +42,7 @@ class _BasicLineChartState extends State { "type": "category", "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] }, - "yAxis": { - "type": "value" - }, + "yAxis": {"type": "value"}, "series": [ { "data": [150, 230, 224, 218, 135, 147, 260], @@ -63,5 +59,4 @@ class _BasicLineChartState extends State { controller.dispose(); super.dispose(); } - } diff --git a/example/lib/charts/chart_click.dart b/example/lib/charts/chart_click.dart new file mode 100644 index 0000000..7ffa9f1 --- /dev/null +++ b/example/lib/charts/chart_click.dart @@ -0,0 +1,96 @@ +import 'dart:developer'; + +import 'package:flutter/material.dart'; +import 'package:graphify/graphify.dart'; + +class ChartClick extends StatefulWidget { + const ChartClick({super.key}); + + @override + State createState() => _ChartClickState(); +} + +class _ChartClickState extends State { + late final GraphifyController _controller; + String _status = 'Ready to test clicks'; + + @override + void initState() { + super.initState(); + _controller = GraphifyController(); + _controller.chartClickedEvent.listen((data) { + debugPrint('listener received: $data'); + setState(() { + _status = 'Click received: ${data['name'] ?? 'Unknown'}'; + }); + + log('TEST: Chart click received: $data'); + }); + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + _status, + style: const TextStyle(fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + ), + ), + Container( + height: 400, + margin: const EdgeInsets.all(8), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey), + borderRadius: BorderRadius.circular(8), + ), + child: GraphifyView( + controller: _controller, + onConsoleMessage: (msg) { + debugPrint('WebView console: ${(msg as dynamic).message}'); + }, + initialOptions: const { + "tooltip": { + "trigger": "axis", + "axisPointer": {"type": "shadow"} + }, + "legend": { + "data": ["Sales", "Marketing"] + }, + "xAxis": { + "type": "category", + "data": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] + }, + "yAxis": {"type": "value"}, + "series": [ + { + "name": "Sales", + "type": "bar", + "data": [120, 200, 150, 80, 70, 110, 130], + "itemStyle": {"color": "#5470c6"} + }, + { + "name": "Marketing", + "type": "bar", + "data": [60, 100, 75, 40, 35, 55, 65], + "itemStyle": {"color": "#91cc75"} + } + ] + }, + ), + ), + ], + ); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } +} diff --git a/example/lib/main.dart b/example/lib/main.dart index 69eff2e..397a359 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -5,6 +5,7 @@ import 'package:graphify_example/charts/basic_area_chart.dart'; import 'package:graphify_example/charts/basic_bar_chart.dart'; import 'package:graphify_example/charts/basic_line_chart.dart'; import 'package:graphify_example/charts/candle_stick_brush.dart'; +import 'package:graphify_example/charts/chart_click.dart'; import 'package:graphify_example/charts/customized_radar_chart.dart'; import 'package:graphify_example/charts/graph_webkit_dep.dart'; import 'package:graphify_example/charts/heatmap_discrete_mapping_of_color.dart'; @@ -28,6 +29,7 @@ class MyApp extends StatelessWidget { const MyApp({super.key}); static const charts = { + "Chart Click Test": ChartClick(), "Basic Line Chart": BasicLineChart(), 'Basic Area Chart': BasicAreaChart(), 'Stacked Area Chart': StackedAreaChart(), @@ -90,13 +92,15 @@ class MyApp extends StatelessWidget { ListTile( title: const Text("Open Source Code"), onTap: () { - launchUrlString("https://github.com/warioddly/graphify"); + launchUrlString( + "https://github.com/warioddly/graphify"); }, ), ListTile( title: const Text("Pub.dev"), onTap: () { - launchUrlString("https://pub.dev/packages/graphify"); + launchUrlString( + "https://pub.dev/packages/graphify"); }, ), ], diff --git a/example/pubspec.lock b/example/pubspec.lock index 9de9b25..09f21c2 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -104,7 +104,7 @@ packages: path: ".." relative: true source: path - version: "1.1.2" + version: "1.2.1" integration_test: dependency: "direct dev" description: flutter @@ -114,26 +114,26 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" + sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0" url: "https://pub.dev" source: hosted - version: "10.0.9" + version: "11.0.1" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.10" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" lints: dependency: transitive description: @@ -255,10 +255,10 @@ packages: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.6" url_launcher: dependency: "direct main" description: @@ -327,10 +327,10 @@ packages: dependency: transitive description: name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" vm_service: dependency: transitive description: @@ -388,5 +388,5 @@ packages: source: hosted version: "3.18.4" sdks: - dart: ">=3.7.0-0 <4.0.0" + dart: ">=3.8.0-0 <4.0.0" flutter: ">=3.27.0" diff --git a/lib/graphify.dart b/lib/graphify.dart index 3600e10..a97fefb 100644 --- a/lib/graphify.dart +++ b/lib/graphify.dart @@ -2,11 +2,10 @@ library graphify; export 'src/controller/implements/facade.dart' if (dart.library.io) 'src/controller/implements/mobile.dart' - if (dart.library.html) 'src/controller/implements/web.dart'; + if (dart.library.js_interop) 'src/controller/implements/web.dart'; + export 'src/view/implements/facade.dart' if (dart.library.io) 'src/view/implements/mobile.dart' - if (dart.library.html) 'src/view/implements/web.dart'; + if (dart.library.js_interop) 'src/view/implements/web.dart'; -export 'src/utils/gradient/graphify_gradient.dart'; -export 'src/utils/gradient/graphify_linear_gradient.dart'; -export 'src/utils/gradient/graphify_radial_gradient.dart'; \ No newline at end of file +export 'src/utils/gradient/_gradient.dart'; diff --git a/lib/src/controller/implements/facade.dart b/lib/src/controller/implements/facade.dart index 86d20f9..aa2db1b 100644 --- a/lib/src/controller/implements/facade.dart +++ b/lib/src/controller/implements/facade.dart @@ -1,15 +1,17 @@ import 'package:graphify/src/controller/interface.dart' as controller_interface; class GraphifyController extends controller_interface.GraphifyController { - @override void update(Map? options) { throw UnimplementedError("update() is not implemented"); } + @override + Stream> get chartClickedEvent => + throw UnimplementedError("chartClickedEvent is not implemented"); + @override void dispose() { throw UnimplementedError("dispose() is not implemented"); } - -} \ No newline at end of file +} diff --git a/lib/src/controller/implements/mobile.dart b/lib/src/controller/implements/mobile.dart index 88111b3..a5008da 100644 --- a/lib/src/controller/implements/mobile.dart +++ b/lib/src/controller/implements/mobile.dart @@ -2,26 +2,52 @@ import 'dart:async'; import 'dart:convert'; import 'package:graphify/src/controller/interface.dart' as controller_interface; -import 'package:graphify/src/utils/js_methods.dart'; +import 'package:graphify/src/controller/js_methods.dart'; import 'package:webview_flutter/webview_flutter.dart'; class GraphifyController extends controller_interface.GraphifyController { + final StreamController> _clicks = + StreamController>.broadcast(); late final WebViewController _connector; + set connector(WebViewController connector) => _connector = connector; + + String get _quotedUid => '"$uid"'; + @override Future update(Map? options) async { - await runJavaScript('window.${JsMethods.updateChart}("$uid", ${jsonEncode(options ?? {})})'); + return _eval( + JsMethods.updateChart, + [_quotedUid, jsonEncode(options ?? {})], + ); } - set connector(WebViewController connector) => _connector = connector; - - Future runJavaScript(String javaScript) async { - await _connector.runJavaScript(javaScript); + @override + void dispose() async { + await _clicks.close(); + try { + await _eval(JsMethods.disposeChart, [_quotedUid]); + } catch (_) {} } @override - void dispose() async { - await runJavaScript('window.${JsMethods.disposeChart}("$uid")'); + Stream> get chartClickedEvent => _clicks.stream; + + void onChartClick(String message) { + try { + final dynamic decoded = jsonDecode(message); + if (decoded is Map) { + _clicks.sink.add(decoded); + } + } catch (_) {} + } + + Future _eval(String method, List args) async { + return _connector.runJavaScript(_buildJsMethod(method, args)); + } + + String _buildJsMethod(String method, List args) { + return 'window.$method(${args.map((String arg) => arg).join(', ')})'; } } diff --git a/lib/src/controller/implements/web.dart b/lib/src/controller/implements/web.dart index e344503..55191e7 100644 --- a/lib/src/controller/implements/web.dart +++ b/lib/src/controller/implements/web.dart @@ -1,12 +1,52 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:js_interop'; import 'dart:js_interop_unsafe'; import 'package:graphify/src/controller/interface.dart' as controller_interface; -import 'package:graphify/src/utils/js_methods.dart'; +import 'package:graphify/src/controller/js_methods.dart'; import 'package:web/web.dart'; class GraphifyController extends controller_interface.GraphifyController { + GraphifyController() { + _messageListener = ((Event event) { + try { + final msgEvent = event as MessageEvent; + final dataAny = msgEvent.data; + if (dataAny is! String) { + return; + } + final decoded = jsonDecode(dataAny as String); + if (decoded is! Map) { + return; + } + if (decoded['source'] != 'graphify') { + return; + } + if (decoded['chartId'] != uid) { + return; + } + + final payloadString = decoded['payload']; + if (payloadString is! String) { + return; + } + final payloadDecoded = jsonDecode(payloadString); + if (payloadDecoded is Map) { + _clicks.add(payloadDecoded); + } + } catch (_) {} + }).toJS as EventListener; + window.addEventListener('message', _messageListener); + } + + final StreamController> _clicks = + StreamController>.broadcast(); + + late final EventListener _messageListener; + + @override + Stream> get chartClickedEvent => _clicks.stream; @override void update(Map? options) { @@ -19,6 +59,12 @@ class GraphifyController extends controller_interface.GraphifyController { @override void dispose() { - window.callMethod(JsMethods.disposeChart.toJS, uid.toJS); + try { + window.removeEventListener('message', _messageListener); + } catch (_) {} + try { + window.callMethod(JsMethods.disposeChart.toJS, uid.toJS); + } catch (_) {} + _clicks.close(); } } diff --git a/lib/src/controller/interface.dart b/lib/src/controller/interface.dart index 4f0ada1..60d3931 100644 --- a/lib/src/controller/interface.dart +++ b/lib/src/controller/interface.dart @@ -1,14 +1,10 @@ +import 'dart:async'; -import 'package:graphify/src/utils/utils.dart'; - -mixin Disposable { - void dispose(); -} - -abstract class GraphifyController with Disposable { +import 'package:graphify/src/utils/uid.dart'; +abstract class GraphifyController { /// Creates a new instance of [GraphifyController] with a unique identifier. - GraphifyController() : uid = Utils.uid(); + GraphifyController() : uid = UID.generate(); /// Unique identifier for the chart instance. final String uid; @@ -16,4 +12,9 @@ abstract class GraphifyController with Disposable { /// Updates the chart with the provided options. void update(Map? options); -} \ No newline at end of file + /// Streams a value when the chart is clicked. + Stream> get chartClickedEvent; + + /// Disposes of the chart instance, cleaning up resources. + void dispose(); +} diff --git a/lib/src/utils/js_methods.dart b/lib/src/controller/js_methods.dart similarity index 78% rename from lib/src/utils/js_methods.dart rename to lib/src/controller/js_methods.dart index 12973a1..27cef73 100644 --- a/lib/src/utils/js_methods.dart +++ b/lib/src/controller/js_methods.dart @@ -6,4 +6,6 @@ final class JsMethods { static const disposeChart = 'disposeChart'; static const normalizeJson = 'normalizeJson'; + + static const initClickListener = 'initClickListener'; } diff --git a/lib/src/resources/dependencies.js.dart b/lib/src/resources/dependencies.js.dart index f9bd5e8..6700313 100644 --- a/lib/src/resources/dependencies.js.dart +++ b/lib/src/resources/dependencies.js.dart @@ -1,7 +1,7 @@ import 'package:graphify/src/resources/lib/echarts.gl.min.dart'; import 'package:graphify/src/resources/lib/echarts.min.dart'; import 'package:graphify/src/resources/lib/jquery.min.dart'; -import 'package:graphify/src/utils/js_methods.dart'; +import 'package:graphify/src/controller/js_methods.dart'; const dependencies = "$jQuery " "$echartsMin " @@ -25,6 +25,67 @@ const String chartScripts = """ chart.setOption(option); graphify_charts[chart_id].option = option; } + + function ${JsMethods.initClickListener}(chart, chart_id) { + try { + chart.off && chart.off('click'); + } catch (e) {} + + chart.on('click', function (data) { + try { + if (!data) return; + + function safeValue(value, depth) { + if (value == null) return null; + if (depth > 3) return undefined; + const t = typeof value; + if (t === 'string' || t === 'number' || t === 'boolean') return value; + if (Array.isArray(value)) return value.map(v => safeValue(v, depth + 1)).filter(v => v !== undefined); + if (t === 'object') { + const out = {}; + const keys = ['type', 'componentType', 'seriesType', 'seriesIndex', 'seriesName', 'name', 'dataIndex', 'data', 'value', 'color', 'marker']; + keys.forEach(k => { + if (k in value) { + const v = safeValue(value[k], depth + 1); + if (v !== undefined) out[k] = v; + } + }); + // fallback: pick plain props if 'data' is missing + if (!('data' in out) && value && typeof value === 'object') { + Object.keys(value).forEach(k => { + if (out[k] !== undefined) return; + const v = value[k]; + const vt = typeof v; + if (vt === 'string' || vt === 'number' || vt === 'boolean') out[k] = v; + }); + } + return out; + } + return undefined; + } + + const sanitized = safeValue(data, 0) || {}; + const payload = JSON.stringify(sanitized); + // Mobile (WebView) channel + if (window && window.ClickEventChannel && typeof window.ClickEventChannel.postMessage === 'function') { + window.ClickEventChannel.postMessage(payload); + } + // Web (Flutter web) channel - post directly to the hosting window + try { + const envelope = JSON.stringify({ + source: 'graphify', + chartId: chart_id, + payload: String(payload) + }); + window.postMessage(envelope, '*'); + } catch (e) { + console.error('Error posting window message:', e); + } + } catch (e) { + console.error('Error posting click event:', e); + } + }); + } function ${JsMethods.disposeChart} (chart_id) { const chart = graphify_charts[chart_id]?.chart; @@ -40,4 +101,3 @@ const String chartScripts = """ } """; - diff --git a/lib/src/resources/index.html.dart b/lib/src/resources/index.html.dart index 3361ffc..1d1438e 100644 --- a/lib/src/resources/index.html.dart +++ b/lib/src/resources/index.html.dart @@ -1,8 +1,8 @@ // ignore_for_file: leading_newlines_in_multiline_strings -import 'package:graphify/src/utils/js_methods.dart'; +import 'package:graphify/src/controller/js_methods.dart'; -String indexHtml({ required String id, String? dependencies}) { +String indexHtml({required String id, String? dependencies}) { return ''' @@ -30,6 +30,7 @@ String indexHtml({ required String id, String? dependencies}) { const chart = context.echarts.init(dom, 'dark', { renderer: 'canvas', useDirtyRect: false }); context.${JsMethods.initChart}('$id', chart, {}); context.${JsMethods.updateChart}('$id', {}); + context.${JsMethods.initClickListener}(chart, '$id'); window.addEventListener('resize', chart.resize); diff --git a/lib/src/utils/color_extension.dart b/lib/src/utils/color_extension.dart index 0762dd8..3d783eb 100644 --- a/lib/src/utils/color_extension.dart +++ b/lib/src/utils/color_extension.dart @@ -1,9 +1,11 @@ import 'dart:ui' show Color; extension ColorExtension on Color { - String get toRGBA { - return 'rgba(${(red * 255).toInt()}, ${(green * 255).toInt()}, ${(blue * 255).toInt()}, ${opacity})'; + final r255 = (r * 255.0).round() & 0xff; + final g255 = (g * 255.0).round() & 0xff; + final b255 = (b * 255.0).round() & 0xff; + final alpha = a; // 0..1 + return 'rgba($r255, $g255, $b255, $alpha)'; } - -} \ No newline at end of file +} diff --git a/lib/src/utils/gradient/_gradient.dart b/lib/src/utils/gradient/_gradient.dart new file mode 100644 index 0000000..bec2b7f --- /dev/null +++ b/lib/src/utils/gradient/_gradient.dart @@ -0,0 +1,3 @@ +export 'graphify_gradient.dart'; +export 'graphify_linear_gradient.dart'; +export 'graphify_radial_gradient.dart'; \ No newline at end of file diff --git a/lib/src/utils/uid.dart b/lib/src/utils/uid.dart new file mode 100644 index 0000000..0161394 --- /dev/null +++ b/lib/src/utils/uid.dart @@ -0,0 +1,17 @@ +import 'dart:math'; + +class UID { + + static const _chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; + static final _secureRandom = Random.secure(); + + static String generate([int length = 10]) { + final buffer = StringBuffer(); + + for (var i = 0; i < length; i++) { + buffer.write(_chars[_secureRandom.nextInt(_chars.length)]); + } + + return buffer.toString(); + } +} diff --git a/lib/src/utils/utils.dart b/lib/src/utils/utils.dart deleted file mode 100644 index 13a8f87..0000000 --- a/lib/src/utils/utils.dart +++ /dev/null @@ -1,16 +0,0 @@ -import 'dart:math'; - -class Utils { - static String uid() { - var random = Random(); - - var chars = 'abcdefghijklmnopqrstuvwxyz'; - var uid = ''; - - for (var i = 0; i < 10; i++) { - uid += chars[random.nextInt(chars.length)]; - } - - return uid; - } -} diff --git a/lib/src/view/interface.dart b/lib/src/view/_interface.dart similarity index 65% rename from lib/src/view/interface.dart rename to lib/src/view/_interface.dart index f068507..12aea0c 100644 --- a/lib/src/view/interface.dart +++ b/lib/src/view/_interface.dart @@ -1,10 +1,12 @@ import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:graphify/src/controller/interface.dart'; +import 'package:graphify/src/view/console_message.dart'; -typedef OnConsoleMessage = void Function(String message); -abstract class GraphifyView { +abstract class GraphifyView extends StatefulWidget { const GraphifyView({ + super.key, this.controller, this.initialOptions, this.onConsoleMessage, @@ -19,11 +21,15 @@ abstract class GraphifyView { final VoidCallback? onCreated; + @override + State createState(); + } -abstract class GraphifyViewState extends State { +abstract class GraphifyViewState extends State { late Widget view; + @nonVirtual @override void initState() { super.initState(); @@ -31,6 +37,7 @@ abstract class GraphifyViewState extends State { buildView(); } + @nonVirtual @override Widget build(BuildContext context) => view; diff --git a/lib/src/view/console_message.dart b/lib/src/view/console_message.dart new file mode 100644 index 0000000..a16b8bb --- /dev/null +++ b/lib/src/view/console_message.dart @@ -0,0 +1,2 @@ + +typedef OnConsoleMessage = void Function(Object message); \ No newline at end of file diff --git a/lib/src/view/implements/facade.dart b/lib/src/view/implements/facade.dart index bd41723..17a98e8 100644 --- a/lib/src/view/implements/facade.dart +++ b/lib/src/view/implements/facade.dart @@ -1,30 +1,29 @@ import 'package:flutter/cupertino.dart'; -import 'package:graphify/src/controller/interface.dart'; -import 'package:graphify/src/view/interface.dart' as g_view; +import 'package:graphify/src/view/_interface.dart' as g_view; -class GraphifyView extends StatelessWidget implements g_view.GraphifyView { +class GraphifyView extends g_view.GraphifyView { const GraphifyView({ super.key, - this.controller, - this.initialOptions, - this.onConsoleMessage, - this.onCreated, + super.controller, + super.initialOptions, + super.onConsoleMessage, + super.onCreated, }); @override - final GraphifyController? controller; + State createState() => _GraphifyViewState(); - @override - final Map? initialOptions; +} - @override - final g_view.OnConsoleMessage? onConsoleMessage; +class _GraphifyViewState extends g_view.GraphifyViewState { @override - final VoidCallback? onCreated; + void initView() { + throw UnimplementedError("initView() is not implemented"); + } @override - Widget build(BuildContext context) { - throw UnimplementedError(); + Widget buildView() { + throw UnimplementedError("buildView() is not implemented"); } -} +} \ No newline at end of file diff --git a/lib/src/view/implements/mobile.dart b/lib/src/view/implements/mobile.dart index ddb30c0..c67321c 100644 --- a/lib/src/view/implements/mobile.dart +++ b/lib/src/view/implements/mobile.dart @@ -1,59 +1,56 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:graphify/src/controller/implements/mobile.dart'; import 'package:graphify/src/resources/dependencies.js.dart'; import 'package:graphify/src/resources/index.html.dart'; -import 'package:graphify/src/view/interface.dart' as g_view; +import 'package:graphify/src/view/_interface.dart' as g_view; import 'package:webview_flutter/webview_flutter.dart'; -class GraphifyView extends StatefulWidget implements g_view.GraphifyView { +class GraphifyView extends g_view.GraphifyView { const GraphifyView({ super.key, - this.controller, - this.initialOptions, - this.onConsoleMessage, - this.onCreated, + super.controller, + super.initialOptions, + super.onConsoleMessage, + super.onCreated, }); @override - final GraphifyController? controller; - - @override - final Map? initialOptions; - - @override - final g_view.OnConsoleMessage? onConsoleMessage; - - @override - final VoidCallback? onCreated; - - @override - State createState() => _GraphifyViewMobile(); + State createState() => _GraphifyViewState(); } -class _GraphifyViewMobile extends g_view.GraphifyViewState { - late WebViewController webViewController; - late final _controller = widget.controller ?? GraphifyController(); +class _GraphifyViewState extends g_view.GraphifyViewState { + late final webViewController = WebViewController(); + late final controller = + (widget.controller ?? GraphifyController()) as GraphifyController; @override void initView() { - _controller.connector = webViewController = WebViewController() + controller.connector = webViewController ..setJavaScriptMode(JavaScriptMode.unrestricted) - ..setBackgroundColor(Colors.transparent) + ..setBackgroundColor(const Color(0x00000000)) + ..setOnConsoleMessage(widget.onConsoleMessage ?? (_) {}) + ..addJavaScriptChannel( + 'ClickEventChannel', + onMessageReceived: (JavaScriptMessage m) { + controller.onChartClick(m.message); + }, + ) ..loadHtmlString( indexHtml( - id: _controller.uid, + id: controller.uid, dependencies: "", ), ) - ..setOnConsoleMessage( - (message) => widget.onConsoleMessage?.call(message.message), - ) - ..setNavigationDelegate(NavigationDelegate( - onPageFinished: (_) async { - widget.onCreated?.call(); - await _controller.update(widget.initialOptions); - }, - )); + ..setNavigationDelegate( + NavigationDelegate( + onPageFinished: (_) { + widget.onCreated?.call(); + controller.update(widget.initialOptions); + }, + ), + ); } @override @@ -64,8 +61,10 @@ class _GraphifyViewMobile extends g_view.GraphifyViewState { @override void dispose() { if (widget.controller == null) { - _controller.dispose(); + controller.dispose(); } + unawaited(webViewController.clearLocalStorage()); + unawaited(webViewController.clearCache()); super.dispose(); } } diff --git a/lib/src/view/implements/web.dart b/lib/src/view/implements/web.dart index d33026e..d211ea2 100644 --- a/lib/src/view/implements/web.dart +++ b/lib/src/view/implements/web.dart @@ -5,47 +5,34 @@ import 'package:flutter/cupertino.dart'; import 'package:graphify/src/controller/implements/web.dart'; import 'package:graphify/src/resources/dependencies.js.dart'; import 'package:graphify/src/resources/index.html.dart'; -import 'package:graphify/src/view/interface.dart' as g_view; +import 'package:graphify/src/view/_interface.dart' as g_view; import 'package:web/web.dart'; const _chartDependencyId = 'graphify-chart-dependency'; -class GraphifyView extends StatefulWidget implements g_view.GraphifyView { +class GraphifyView extends g_view.GraphifyView { const GraphifyView({ super.key, - this.controller, - this.initialOptions, - this.onConsoleMessage, - this.onCreated, + super.controller, + super.initialOptions, + super.onConsoleMessage, + super.onCreated, }); @override - final GraphifyController? controller; - - @override - final Map? initialOptions; - - @override - final g_view.OnConsoleMessage? onConsoleMessage; - - @override - final VoidCallback? onCreated; - - @override - State createState() => _GraphifyViewWeb(); + State createState() => _GraphifyViewState(); } -class _GraphifyViewWeb extends g_view.GraphifyViewState { +class _GraphifyViewState extends g_view.GraphifyViewState { + late final controller = widget.controller ?? GraphifyController(); - late final _controller = widget.controller ?? GraphifyController(); - - String get _uid => _controller.uid; + String get uid => controller.uid; @override void initView() { initChartDependencies(); platformViewRegistry.registerViewFactory( - _uid, + uid, createHTMLIFrameElement, ); } @@ -53,20 +40,18 @@ class _GraphifyViewWeb extends g_view.GraphifyViewState { @override Widget buildView() { widget.onCreated?.call(); - return view = HtmlElementView(viewType: _uid); + return view = HtmlElementView(viewType: uid); } HTMLIFrameElement createHTMLIFrameElement(_) { final iframe = HTMLIFrameElement() - ..id = 'graphify_$_uid' - ..style.width = '100%' + ..id = 'graphify_$uid' + ..style.width = '100%' ..style.height = '100%' ..style.border = 'none' - ..srcdoc = indexHtml(id: _uid).toJS - ..onLoad.listen((_) => _controller.update(widget.initialOptions)) - ..onError.listen((event) { - widget.onConsoleMessage?.call(event.toString()); - }); + ..srcdoc = indexHtml(id: uid).toJS + ..onLoad.listen((_) => controller.update(widget.initialOptions)) + ..onError.listen(widget.onConsoleMessage); return iframe; } @@ -85,12 +70,42 @@ class _GraphifyViewWeb extends g_view.GraphifyViewState { body?.append(scriptElement); } + + final shim = HTMLScriptElement() + ..id = 'graphify-click-channel-shim-$uid' + ..innerHTML = ''' + (function(uid){ + // Define the channel only if not already present + if (!window.ClickEventChannel) { + window.ClickEventChannel = { + postMessage: function(payload) { + try { + var envelope = JSON.stringify({ + source: 'graphify', + chartId: uid, + payload: String(payload) + }); + // Forward as a string: the web controller expects a JSON string + window.postMessage(envelope, '*'); + } catch (e) { + console.error('ClickEventChannel shim error', e); + } + } + }; + } + })('$uid'); + ''' + .toJS; + + final dom = window.document; + final body = dom.documentElement?.children.item(1); + body?.append(shim); } @override void dispose() { if (widget.controller == null) { - _controller.dispose(); + controller.dispose(); } super.dispose(); } diff --git a/pubspec.yaml b/pubspec.yaml index 9e0db7f..f33416e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ repository: "https://github.com/warioddly/graphify" documentation: "https://echarts.apache.org/en/option.html#title" issue_tracker: "https://github.com/warioddly/graphify/issues" -version: 1.1.2 +version: 1.2.1 environment: sdk: '>=3.2.3 <4.0.0' @@ -21,10 +21,6 @@ dev_dependencies: sdk: flutter flutter_lints: ^3.0.1 -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. flutter: # This section identifies this Flutter project as a plugin project. # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) @@ -39,7 +35,6 @@ flutter: platforms: android: ios: - linux: macos: web: windows: