From 4e670e1abab0199ba5e4fb608551fc19cd89a696 Mon Sep 17 00:00:00 2001 From: dvdrw Date: Wed, 13 May 2026 12:49:30 +0200 Subject: [PATCH] refactor: move floor plan editor into own widget --- assets/konva/app.js | 10 +- lib/data/sources/localiser/sensor_client.dart | 3 +- lib/features/floorplan/floor_plan_screen.dart | 79 +++-- .../floorplan/widgets/floor_plan_editor.dart | 269 ++++++++++++++++++ .../floorplan/widgets/konva_web_view.dart | 169 ----------- .../floorplan/widgets/room_edit_sheet.dart | 119 ++++++++ .../onboarding/steps/step_floor_plan.dart | 8 +- lib/features/sensors/sensor_list_screen.dart | 1 + lib/router.dart | 8 + 9 files changed, 448 insertions(+), 218 deletions(-) create mode 100644 lib/features/floorplan/widgets/floor_plan_editor.dart delete mode 100644 lib/features/floorplan/widgets/konva_web_view.dart create mode 100644 lib/features/floorplan/widgets/room_edit_sheet.dart diff --git a/assets/konva/app.js b/assets/konva/app.js index ef1e192..c3e3813 100644 --- a/assets/konva/app.js +++ b/assets/konva/app.js @@ -133,6 +133,7 @@ function _buildRoomGroup(room) { _dimRoom = room; _dimGroup = group; _showDimensions(room, group.x(), group.y()); + notifyFlutter({ type: 'roomTapped', id: room.id }); } }); @@ -255,18 +256,22 @@ function _showDimensions(room, lx, ly) { dimensionsLayer.add(new Konva.Line({ points: [rx + rw, ry, hx + EXT, ry], stroke: c, strokeWidth: 0.8, listening: false })); dimensionsLayer.add(new Konva.Line({ points: [rx + rw, ry + rh, hx + EXT, ry + rh], stroke: c, strokeWidth: 0.8, listening: false })); dimensionsLayer.add(new Konva.Text({ - x: hx + 4, y: ry + rh / 2 - 4, + x: hx - 10, y: ry + rh / 2, + width: rh, align: 'center', text: `${room.height.toFixed(1)} m`, fontSize: 9, fill: c, listening: false, + rotation: -90, offsetX: rh / 2, offsetY: 4.5, })); } else { // Inside the room near the right edge const hx = rx + rw - 18; dimensionsLayer.add(new Konva.Arrow({ ...arrowCfg, points: [hx, ry + 4, hx, ry + rh - 4] })); dimensionsLayer.add(new Konva.Text({ - x: hx - 22, y: ry + rh / 2 - 4, + x: hx - 10, y: ry + rh / 2, + width: rh, align: 'center', text: `${room.height.toFixed(1)} m`, fontSize: 9, fill: c, listening: false, + rotation: -90, offsetX: rh / 2, offsetY: 4.5, })); } @@ -508,6 +513,7 @@ stage.on('click tap', (e) => { if (e.target === stage) { _setTooltip(null); _clearDimensions(); + notifyFlutter({ type: 'selectionCleared' }); } }); diff --git a/lib/data/sources/localiser/sensor_client.dart b/lib/data/sources/localiser/sensor_client.dart index f426a7a..82de3e8 100644 --- a/lib/data/sources/localiser/sensor_client.dart +++ b/lib/data/sources/localiser/sensor_client.dart @@ -2,8 +2,7 @@ import '../../../domain/models/server_config.dart'; import 'localiser_client.dart'; class SensorClient extends LocaliserdClient { - SensorClient({required ServerConfig config, required String token}) - : super(config: config, token: token); + SensorClient({required super.config, required String super.token}); Future> getSensors() async => await get('/api/sensors') as List; diff --git a/lib/features/floorplan/floor_plan_screen.dart b/lib/features/floorplan/floor_plan_screen.dart index eb81720..fb8a97e 100644 --- a/lib/features/floorplan/floor_plan_screen.dart +++ b/lib/features/floorplan/floor_plan_screen.dart @@ -6,7 +6,8 @@ import '../../domain/models/floor_plan_mode.dart'; import '../../domain/models/sensor.dart'; import '../../providers.dart'; import '../ble_provision/ble_provision_sheet.dart'; -import 'widgets/konva_web_view.dart'; +import 'widgets/floor_plan_editor.dart'; +import 'widgets/room_edit_sheet.dart'; class FloorPlanScreen extends ConsumerStatefulWidget { const FloorPlanScreen({super.key}); @@ -16,22 +17,21 @@ class FloorPlanScreen extends ConsumerStatefulWidget { } class _FloorPlanScreenState extends ConsumerState { - final _konvaKey = GlobalKey(); + final _editorKey = GlobalKey(); @override void initState() { super.initState(); - // Push any already-loaded data once the WebView is ready. WidgetsBinding.instance.addPostFrameCallback((_) => _syncAll()); } void _syncAll() { ref .read(roomsProvider.future) - .then((rooms) => _konvaKey.currentState?.loadFloorPlan(rooms)); + .then((rooms) => _editorKey.currentState?.loadFloorPlan(rooms)); ref .read(sensorsProvider.future) - .then((sensors) => _konvaKey.currentState?.loadSensors(sensors)); + .then((sensors) => _editorKey.currentState?.loadSensors(sensors)); } @override @@ -40,21 +40,20 @@ class _FloorPlanScreenState extends ConsumerState { final roomsAsync = ref.watch(roomsProvider); final sensorsAsync = ref.watch(sensorsProvider); - // Push data updates to the WebView whenever providers refresh. ref.listen(roomsProvider, (_, next) { - next.whenData((r) => _konvaKey.currentState?.loadFloorPlan(r)); + next.whenData((r) => _editorKey.currentState?.loadFloorPlan(r)); }); ref.listen(sensorsProvider, (_, next) { - next.whenData((s) => _konvaKey.currentState?.loadSensors(s)); + next.whenData((s) => _editorKey.currentState?.loadSensors(s)); }); ref.listen(tagPositionsProvider, (_, next) { - next.whenData((t) => _konvaKey.currentState?.updateTags(t)); + next.whenData((t) => _editorKey.currentState?.updateTags(t)); }); ref.listen(particleCloudProvider, (_, next) { - next.whenData((p) => _konvaKey.currentState?.updateParticleCloud(p)); + next.whenData((p) => _editorKey.currentState?.updateParticleCloud(p)); }); ref.listen(selectedSensorIdProvider, (_, id) { - _konvaKey.currentState?.highlightSensor(id); + _editorKey.currentState?.highlightSensor(id); }); final unplaced = sensorsAsync.valueOrNull @@ -76,7 +75,7 @@ class _FloorPlanScreenState extends ConsumerState { ? FloorPlanMode.view : FloorPlanMode.edit; ref.read(floorPlanModeProvider.notifier).state = next; - _konvaKey.currentState?.setMode(next); + _editorKey.currentState?.setMode(next); }, ), ], @@ -86,8 +85,8 @@ class _FloorPlanScreenState extends ConsumerState { Expanded( child: Stack( children: [ - KonvaWebView( - key: _konvaKey, + FloorPlanEditor( + key: _editorKey, mode: mode, onSensorTapped: (id) { ref.read(selectedSensorIdProvider.notifier).state = id; @@ -115,6 +114,17 @@ class _FloorPlanScreenState extends ConsumerState { // drag-closure references and causing snap-back. }, onRoomAdded: (x, y) => _showAddRoomDialog(x, y), + onEditRoomTapped: _showRoomEditSheet, + onEditSensorTapped: (sensorId) => + context.push('/floorplan/sensors/$sensorId'), + onAddSensorTapped: () => showModalBottomSheet( + context: context, + isScrollControlled: true, + useRootNavigator: true, + builder: (_) => const BleProvisionSheet(), + ).then((_) { + ref.invalidate(sensorsProvider); + }), ), if (roomsAsync.valueOrNull?.isEmpty ?? false) Center( @@ -150,36 +160,23 @@ class _FloorPlanScreenState extends ConsumerState { _UnplacedPanel(sensors: unplaced), ], ), - floatingActionButton: mode == FloorPlanMode.edit - ? Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - FloatingActionButton.small( - heroTag: 'add-room', - tooltip: 'Add room', - onPressed: () => _konvaKey.currentState?.addRoom(), - child: const Icon(Icons.add_home_outlined), - ), - const SizedBox(height: 8), - FloatingActionButton.extended( - heroTag: 'add-sensor', - icon: const Icon(Icons.bluetooth), - label: const Text('Add sensor'), - onPressed: () => showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (_) => const BleProvisionSheet(), - ).then((_) { - ref.invalidate(sensorsProvider); - }), - ), - ], - ) - : null, ); } + void _showRoomEditSheet(int roomId) { + final rooms = ref.read(roomsProvider).valueOrNull ?? []; + final room = rooms.where((r) => r.id == roomId).firstOrNull; + if (room == null) return; + final floor = ref.read(floorProvider).valueOrNull; + if (floor == null) return; + showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + builder: (_) => RoomEditSheet(room: room, floorId: floor.id), + ).then((_) => ref.invalidate(roomsProvider)); + } + Future _showAddRoomDialog(double x, double y) async { final nameCtrl = TextEditingController(); final widthCtrl = TextEditingController(text: '5.0'); diff --git a/lib/features/floorplan/widgets/floor_plan_editor.dart b/lib/features/floorplan/widgets/floor_plan_editor.dart new file mode 100644 index 0000000..b61fac0 --- /dev/null +++ b/lib/features/floorplan/widgets/floor_plan_editor.dart @@ -0,0 +1,269 @@ +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 FloorPlanEditor extends StatefulWidget { + const FloorPlanEditor({ + super.key, + required this.mode, + required this.onSensorTapped, + required this.onSensorMoved, + this.onRoomsUpdated, + this.onRoomAdded, + this.onEditRoomTapped, + this.onEditSensorTapped, + this.onAddSensorTapped, + }); + + 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; + final void Function(int roomId)? onEditRoomTapped; + final void Function(String sensorId)? onEditSensorTapped; + final void Function()? onAddSensorTapped; + + @override + State createState() => FloorPlanEditorState(); +} + +class FloorPlanEditorState extends State { + late final WebViewController _controller; + bool _ready = false; + final _pending = Function()>[]; + + int? _selectedRoomId; + String? _selectedSensorId; + + @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': + final id = data['id'] as String; + if (widget.mode == FloorPlanMode.edit) { + setState(() { + _selectedSensorId = id; + _selectedRoomId = null; + }); + } else { + widget.onSensorTapped(id); + } + 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(), + ); + case 'roomTapped': + setState(() { + _selectedRoomId = (data['id'] as num).toInt(); + _selectedSensorId = null; + }); + case 'selectionCleared': + setState(() { + _selectedRoomId = null; + _selectedSensorId = null; + }); + } + } + + 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) { + setState(() { + _selectedRoomId = null; + _selectedSensorId = null; + }); + 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) { + final inEditMode = widget.mode == FloorPlanMode.edit; + return Stack( + children: [ + WebViewWidget(controller: _controller), + + Positioned( + bottom: 16, + right: 16, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (inEditMode) ...[ + if (_selectedSensorId != null) ...[ + FloatingActionButton.small( + heroTag: 'editor-info-sensor', + tooltip: 'Sensor details', + onPressed: () => + widget.onEditSensorTapped?.call(_selectedSensorId!), + child: const Icon(Icons.info_outline), + ), + const SizedBox(height: 8), + ], + if (_selectedRoomId != null) ...[ + FloatingActionButton.small( + heroTag: 'editor-edit-room', + tooltip: 'Edit room', + onPressed: () => + widget.onEditRoomTapped?.call(_selectedRoomId!), + child: const Icon(Icons.edit_outlined), + ), + const SizedBox(height: 8), + ], + + FloatingActionButton.extended( + heroTag: 'editor-add-room', + tooltip: 'Add room', + onPressed: addRoom, + icon: const Icon(Icons.add_home_outlined), + label: const Text('Add room'), + ), + const SizedBox(height: 8), + ], + + FloatingActionButton.extended( + heroTag: 'editor-add-sensor', + icon: const Icon(Icons.bluetooth), + label: const Text('Add sensor'), + onPressed: () => widget.onAddSensorTapped?.call(), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/features/floorplan/widgets/konva_web_view.dart b/lib/features/floorplan/widgets/konva_web_view.dart deleted file mode 100644 index a2892f2..0000000 --- a/lib/features/floorplan/widgets/konva_web_view.dart +++ /dev/null @@ -1,169 +0,0 @@ -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); -} diff --git a/lib/features/floorplan/widgets/room_edit_sheet.dart b/lib/features/floorplan/widgets/room_edit_sheet.dart new file mode 100644 index 0000000..98236ae --- /dev/null +++ b/lib/features/floorplan/widgets/room_edit_sheet.dart @@ -0,0 +1,119 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../domain/models/floor.dart'; +import '../../../providers.dart'; + +class RoomEditSheet extends ConsumerStatefulWidget { + const RoomEditSheet({super.key, required this.room, required this.floorId}); + + final Room room; + final int floorId; + + @override + ConsumerState createState() => _RoomEditSheetState(); +} + +class _RoomEditSheetState extends ConsumerState { + late final TextEditingController _nameCtrl; + late final TextEditingController _widthCtrl; + late final TextEditingController _heightCtrl; + bool _saving = false; + + @override + void initState() { + super.initState(); + _nameCtrl = TextEditingController(text: widget.room.name); + _widthCtrl = TextEditingController(text: widget.room.width.toString()); + _heightCtrl = TextEditingController(text: widget.room.height.toString()); + } + + @override + void dispose() { + _nameCtrl.dispose(); + _widthCtrl.dispose(); + _heightCtrl.dispose(); + super.dispose(); + } + + Future _save() async { + final name = _nameCtrl.text.trim(); + final width = double.tryParse(_widthCtrl.text); + final height = double.tryParse(_heightCtrl.text); + if (name.isEmpty || width == null || height == null) return; + + setState(() => _saving = true); + try { + await ref.read(floorRepositoryProvider).updateRoom( + widget.floorId, + widget.room.id, + name: name, + width: width, + height: height, + ); + if (mounted) Navigator.of(context).pop(); + } finally { + if (mounted) setState(() => _saving = false); + } + } + + @override + Widget build(BuildContext context) { + final insets = MediaQuery.viewInsetsOf(context); + return Padding( + padding: EdgeInsets.fromLTRB(24, 24, 24, 24 + insets.bottom), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text('Edit room', style: Theme.of(context).textTheme.titleMedium), + const SizedBox(height: 16), + TextField( + controller: _nameCtrl, + decoration: const InputDecoration(labelText: 'Name'), + autofocus: true, + textInputAction: TextInputAction.next, + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: TextField( + controller: _widthCtrl, + decoration: const InputDecoration( + labelText: 'Width', suffixText: 'm'), + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + textInputAction: TextInputAction.next, + ), + ), + const SizedBox(width: 12), + Expanded( + child: TextField( + controller: _heightCtrl, + decoration: const InputDecoration( + labelText: 'Height', suffixText: 'm'), + keyboardType: + const TextInputType.numberWithOptions(decimal: true), + textInputAction: TextInputAction.done, + onSubmitted: (_) => _save(), + ), + ), + ], + ), + const SizedBox(height: 24), + FilledButton( + onPressed: _saving ? null : _save, + child: _saving + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator(strokeWidth: 2), + ) + : const Text('Save'), + ), + ], + ), + ); + } +} diff --git a/lib/features/onboarding/steps/step_floor_plan.dart b/lib/features/onboarding/steps/step_floor_plan.dart index cb59bd3..bc1c729 100644 --- a/lib/features/onboarding/steps/step_floor_plan.dart +++ b/lib/features/onboarding/steps/step_floor_plan.dart @@ -3,7 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../domain/models/floor_plan_mode.dart'; import '../../../providers.dart'; -import '../../floorplan/widgets/konva_web_view.dart'; +import '../../floorplan/widgets/floor_plan_editor.dart'; class StepFloorPlan extends ConsumerStatefulWidget { const StepFloorPlan({super.key, required this.onComplete}); @@ -15,7 +15,7 @@ class StepFloorPlan extends ConsumerStatefulWidget { } class _StepFloorPlanState extends ConsumerState { - final _konvaKey = GlobalKey(); + final _konvaKey = GlobalKey(); @override void initState() { @@ -42,11 +42,11 @@ class _StepFloorPlanState extends ConsumerState { style: Theme.of(context).textTheme.titleLarge), ), Expanded( - child: KonvaWebView( + child: FloorPlanEditor( key: _konvaKey, mode: FloorPlanMode.edit, onSensorTapped: (_) {}, - onSensorMoved: (_, __, ___, ____) {}, + onSensorMoved: (_, _, _, _) {}, onRoomsUpdated: (roomUpdates) async { final floor = ref.read(floorProvider).valueOrNull; if (floor == null) return; diff --git a/lib/features/sensors/sensor_list_screen.dart b/lib/features/sensors/sensor_list_screen.dart index 8bbf8be..d14e3a4 100644 --- a/lib/features/sensors/sensor_list_screen.dart +++ b/lib/features/sensors/sensor_list_screen.dart @@ -68,6 +68,7 @@ class SensorListScreen extends ConsumerWidget { onPressed: () => showModalBottomSheet( context: context, isScrollControlled: true, + useRootNavigator: true, builder: (_) => const BleProvisionSheet(), ).then((_) => ref.invalidate(sensorsProvider)), child: const Icon(Icons.add), diff --git a/lib/router.dart b/lib/router.dart index a30cfb4..68d7c51 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -58,6 +58,14 @@ final routerProvider = Provider((ref) { GoRoute( path: '/floorplan', builder: (context, state) => const FloorPlanScreen(), + routes: [ + GoRoute( + path: 'sensors/:id', + builder: (context, state) => SensorDetailScreen( + sensorId: state.pathParameters['id']!, + ), + ), + ], ), ]), StatefulShellBranch(routes: [