import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; import '../../../domain/models/floor.dart'; import '../../../domain/models/floor_plan_mode.dart'; import '../../../domain/models/tag.dart'; import '../../../domain/models/particle.dart'; import '../../../domain/models/sensor.dart'; class KonvaWebView extends StatefulWidget { const KonvaWebView({ super.key, required this.mode, required this.onSensorTapped, required this.onSensorMoved, this.onRoomsUpdated, this.onRoomAdded, }); final FloorPlanMode mode; final void Function(String sensorId) onSensorTapped; final void Function(String sensorId, int roomId, double x, double y) onSensorMoved; final void Function(List rooms)? onRoomsUpdated; final void Function(double x, double y)? onRoomAdded; @override State createState() => KonvaWebViewState(); } class KonvaWebViewState extends State { late final WebViewController _controller; bool _ready = false; final _pending = Function()>[]; @override void initState() { super.initState(); _controller = WebViewController() ..setJavaScriptMode(JavaScriptMode.unrestricted) ..addJavaScriptChannel('FlutterBridge', onMessageReceived: _onMessage) ..setNavigationDelegate( NavigationDelegate(onPageFinished: (_) => _onPageReady()), ) ..loadFlutterAsset('assets/konva/index.html'); } void _onPageReady() { _ready = true; for (final call in _pending) { call(); } _pending.clear(); } Future _run(Future Function() call) { if (_ready) return call(); _pending.add(call); return Future.value(); } void _onMessage(JavaScriptMessage message) { final data = jsonDecode(message.message) as Map; switch (data['type'] as String?) { case 'sensorTapped': widget.onSensorTapped(data['id'] as String); case 'sensorMoved': widget.onSensorMoved( data['id'] as String, (data['roomId'] as num).toInt(), (data['x'] as num).toDouble(), (data['y'] as num).toDouble(), ); case 'roomsUpdated': final rooms = (data['rooms'] as List) .map((r) => Room.fromJson(r as Map)) .toList(); widget.onRoomsUpdated?.call(rooms); case 'roomAdded': widget.onRoomAdded?.call( (data['x'] as num).toDouble(), (data['y'] as num).toDouble(), ); } } Future loadFloorPlan(List rooms) { return _run(() async { final payload = jsonEncode(rooms .map((r) => { 'id': r.id, 'name': r.name, 'floor_id': r.floorId, 'x': r.x, 'y': r.y, 'width': r.width, 'height': r.height, }) .toList()); await _controller .runJavaScript('window.companion.loadFloorPlan($payload)'); }); } Future loadSensors(List sensors) { return _run(() async { final payload = jsonEncode(sensors .map((s) => { 'id': s.id, 'sensor_id': s.sensorId, 'name': s.name, 'floor_x': s.x, 'floor_y': s.y, 'room_id': s.roomId, }) .toList()); await _controller .runJavaScript('window.companion.loadSensors($payload)'); }); } Future updateTags(List positions) { return _run(() async { final payload = jsonEncode(positions .map((p) => { 'tagId': p.tagId, 'roomId': p.roomId, 'name': p.name, 'color': p.color, }) .toList()); await _controller.runJavaScript('window.companion.updateTags($payload)'); }); } Future updateParticleCloud(List particles) { return _run(() async { final payload = jsonEncode(particles .map((p) => {'x': p.x, 'y': p.y, 'weight': p.weight}) .toList()); await _controller.runJavaScript('window.companion.updateCloud($payload)'); }); } Future highlightSensor(String? sensorId) { return _run(() async { final id = sensorId == null ? 'null' : jsonEncode(sensorId); await _controller .runJavaScript('window.companion.highlightSensor($id)'); }); } Future setMode(FloorPlanMode mode) { return _run(() async { await _controller .runJavaScript('window.companion.setMode("${mode.name}")'); }); } Future addRoom() { return _run(() async { await _controller.runJavaScript('window.companion.addRoom()'); }); } @override Widget build(BuildContext context) => WebViewWidget(controller: _controller); }