From 077585bd731582cbc8c0f7e010be5aecf1a9d003 Mon Sep 17 00:00:00 2001 From: dvdrw Date: Fri, 15 May 2026 15:46:13 +0200 Subject: [PATCH] feat: add sensor move mode to floor plan --- assets/konva/app.js | 25 +++++-- lib/domain/models/floor_plan_mode.dart | 2 +- lib/features/floorplan/floor_plan_screen.dart | 75 +++++++++++++------ .../floorplan/widgets/floor_plan_editor.dart | 56 ++++++-------- .../sensors/sensor_detail_screen.dart | 5 +- lib/features/sensors/sensor_detail_sheet.dart | 5 +- lib/features/sensors/sensor_list_screen.dart | 23 +----- lib/providers.dart | 2 - 8 files changed, 99 insertions(+), 94 deletions(-) diff --git a/assets/konva/app.js b/assets/konva/app.js index 8197364..d61d948 100644 --- a/assets/konva/app.js +++ b/assets/konva/app.js @@ -364,7 +364,7 @@ function renderSensors() { const group = new Konva.Group({ id: `sensor-${sensor.id}`, x: absX, y: absY, - draggable: mode === 'edit', + draggable: mode === 'sensorMove', }); group.add(new Konva.Circle({ @@ -395,8 +395,15 @@ function renderSensors() { listening: false, })); + group.on('click tap', () => { + if (mode === 'view') { + notifyFlutter({ type: 'sensorTapped', id: String(sensor.id) }); + } + }); + let pressTimer = null; group.on('mousedown touchstart', () => { + if (mode === 'view') return; pressTimer = setTimeout(() => { notifyFlutter({ type: 'sensorTapped', id: String(sensor.id) }); pressTimer = null; @@ -544,15 +551,21 @@ window.companion = { setMode(newMode) { mode = newMode; - const edit = newMode === 'edit'; - roomsLayer.find('Group').forEach(g => g.draggable(edit)); - sensorsLayer.find('Group').forEach(g => g.draggable(edit)); - tagsLayer.visible(!edit); - particlesLayer.visible(!edit); + roomsLayer.find('Group').forEach(g => g.draggable(newMode === 'edit')); + sensorsLayer.find('Group').forEach(g => g.draggable(newMode === 'sensorMove')); + tagsLayer.visible(newMode === 'view'); + particlesLayer.visible(newMode === 'view'); _clearDimensions(); stage.batchDraw(); }, + setRepositioningSensor(id) { + sensorsLayer.find('Group').forEach(g => { + g.draggable(mode === 'sensorMove' && (id == null || g.id() === `sensor-${id}`)); + }); + stage.batchDraw(); + }, + highlightSensor(id) { sensorsLayer.find('.highlight-ring').forEach(r => r.visible(false)); if (id) { diff --git a/lib/domain/models/floor_plan_mode.dart b/lib/domain/models/floor_plan_mode.dart index ec04fd8..059bdc4 100644 --- a/lib/domain/models/floor_plan_mode.dart +++ b/lib/domain/models/floor_plan_mode.dart @@ -1 +1 @@ -enum FloorPlanMode { view, edit } +enum FloorPlanMode { view, edit, sensorMove } diff --git a/lib/features/floorplan/floor_plan_screen.dart b/lib/features/floorplan/floor_plan_screen.dart index 121ba05..7930dea 100644 --- a/lib/features/floorplan/floor_plan_screen.dart +++ b/lib/features/floorplan/floor_plan_screen.dart @@ -32,7 +32,26 @@ class _FloorPlanScreenState extends ConsumerState { .then((rooms) => _editorKey.currentState?.loadFloorPlan(rooms)); ref .read(sensorsProvider.future) - .then((sensors) => _editorKey.currentState?.loadSensors(sensors)); + .then((sensors) { + _editorKey.currentState?.loadSensors(sensors); + _applyPlacementState(); + }); + } + + void _applyPlacementState() { + final placing = ref.read(sensorPlacementProvider); + if (placing == null) { + _editorKey.currentState?.highlightSensor(null); + return; + } + final currentMode = ref.read(floorPlanModeProvider); + if (currentMode != FloorPlanMode.view) { + ref.read(floorPlanModeProvider.notifier).state = FloorPlanMode.view; + _editorKey.currentState?.setMode(FloorPlanMode.view); + } + if (placing.isPlaced) { + _editorKey.currentState?.highlightSensor(placing.id.toString()); + } } Future _confirmPlacement(Sensor sensor) async { @@ -81,9 +100,7 @@ class _FloorPlanScreenState extends ConsumerState { ref.listen(particleCloudProvider, (_, next) { next.whenData((p) => _editorKey.currentState?.updateParticleCloud(p)); }); - ref.listen(selectedSensorIdProvider, (_, id) { - _editorKey.currentState?.highlightSensor(id); - }); + ref.listen(sensorPlacementProvider, (prev, next) => _applyPlacementState()); final unplaced = sensorsAsync.valueOrNull ?.where((s) => !s.isPlaced) @@ -103,23 +120,37 @@ class _FloorPlanScreenState extends ConsumerState { appBar: AppBar( title: const Text('Floor Plan'), actions: [ - if (placingSensor == null) - IconButton( - tooltip: - mode == FloorPlanMode.edit ? 'View mode' : 'Edit mode', - icon: Icon( - mode == FloorPlanMode.edit - ? Icons.visibility - : Icons.edit, + if (placingSensor == null) ...[ + if (mode == FloorPlanMode.view) ...[ + IconButton( + tooltip: 'Edit mode', + icon: const Icon(Icons.edit), + onPressed: () { + ref.read(floorPlanModeProvider.notifier).state = + FloorPlanMode.edit; + _editorKey.currentState?.setMode(FloorPlanMode.edit); + }, ), - onPressed: () { - final next = mode == FloorPlanMode.edit - ? FloorPlanMode.view - : FloorPlanMode.edit; - ref.read(floorPlanModeProvider.notifier).state = next; - _editorKey.currentState?.setMode(next); - }, - ), + IconButton( + tooltip: 'Sensor move mode', + icon: const Icon(Icons.sensors), + onPressed: () { + ref.read(floorPlanModeProvider.notifier).state = + FloorPlanMode.sensorMove; + _editorKey.currentState?.setMode(FloorPlanMode.sensorMove); + }, + ), + ] else + IconButton( + tooltip: 'Done', + icon: const Icon(Icons.check), + onPressed: () { + ref.read(floorPlanModeProvider.notifier).state = + FloorPlanMode.view; + _editorKey.currentState?.setMode(FloorPlanMode.view); + }, + ), + ], ], ), body: Column( @@ -131,8 +162,6 @@ class _FloorPlanScreenState extends ConsumerState { key: _editorKey, mode: mode, onSensorTapped: (id) { - ref.read(selectedSensorIdProvider.notifier).state = - id; showSensorDetailSheet(context, int.parse(id)); }, onSensorMoved: (sensorId, roomId, x, y) async { @@ -163,8 +192,6 @@ class _FloorPlanScreenState extends ConsumerState { }, onRoomAdded: (x, y) => _showAddRoomDialog(x, y), onEditRoomTapped: _showRoomEditSheet, - onEditSensorTapped: (id) => - showSensorDetailSheet(context, int.parse(id)), onAddSensorTapped: () => showModalBottomSheet( context: context, isScrollControlled: true, diff --git a/lib/features/floorplan/widgets/floor_plan_editor.dart b/lib/features/floorplan/widgets/floor_plan_editor.dart index 135aa4d..993e1c5 100644 --- a/lib/features/floorplan/widgets/floor_plan_editor.dart +++ b/lib/features/floorplan/widgets/floor_plan_editor.dart @@ -20,7 +20,6 @@ class FloorPlanEditor extends StatefulWidget { this.onRoomsUpdated, this.onRoomAdded, this.onEditRoomTapped, - this.onEditSensorTapped, this.onAddSensorTapped, }); @@ -31,7 +30,6 @@ class FloorPlanEditor extends StatefulWidget { 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 @@ -44,7 +42,7 @@ class FloorPlanEditorState extends State { final _pending = Function()>[]; int? _selectedRoomId; - String? _selectedSensorId; + String? _repositioningSensorId; Completer<({int? roomId, double? x, double? y})>? _placementCompleter; @override @@ -78,19 +76,17 @@ class FloorPlanEditorState extends State { 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); - Vibration.hasVibrator() - .then((t) { if(t) Vibration.vibrate(duration: 20); }); - } + widget.onSensorTapped(id); + Vibration.hasVibrator() + .then((t) { if (t) Vibration.vibrate(duration: 20); }); case 'sensorMoved': + final id = data['id'] as String; + if (_repositioningSensorId == id) { + setState(() => _repositioningSensorId = null); + setRepositioningSensor(null); + } widget.onSensorMoved( - data['id'] as String, + id, (data['roomId'] as num).toInt(), (data['x'] as num).toDouble(), (data['y'] as num).toDouble(), @@ -106,15 +102,9 @@ class FloorPlanEditorState extends State { (data['y'] as num).toDouble(), ); case 'roomTapped': - setState(() { - _selectedRoomId = (data['id'] as num).toInt(); - _selectedSensorId = null; - }); + setState(() => _selectedRoomId = (data['id'] as num).toInt()); case 'selectionCleared': - setState(() { - _selectedRoomId = null; - _selectedSensorId = null; - }); + setState(() => _selectedRoomId = null); case 'positionAtCenter': final roomId = (data['roomId'] as num?)?.toInt(); final x = (data['x'] as num?)?.toDouble(); @@ -204,7 +194,7 @@ class FloorPlanEditorState extends State { Future setMode(FloorPlanMode mode) { setState(() { _selectedRoomId = null; - _selectedSensorId = null; + _repositioningSensorId = null; }); return _run(() async { await _controller.runJavaScript( @@ -213,6 +203,16 @@ class FloorPlanEditorState extends State { }); } + Future setRepositioningSensor(String? id) { + setState(() => _repositioningSensorId = id); + return _run(() async { + final jsId = id == null ? 'null' : jsonEncode(id); + await _controller.runJavaScript( + 'window.companion.setRepositioningSensor($jsId)', + ); + }); + } + Future addRoom() { return _run(() async { await _controller.runJavaScript('window.companion.addRoom()'); @@ -245,16 +245,6 @@ class FloorPlanEditorState extends State { 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', diff --git a/lib/features/sensors/sensor_detail_screen.dart b/lib/features/sensors/sensor_detail_screen.dart index 98cd7a4..bb87285 100644 --- a/lib/features/sensors/sensor_detail_screen.dart +++ b/lib/features/sensors/sensor_detail_screen.dart @@ -59,10 +59,7 @@ class _Body extends ConsumerWidget { OutlinedButton.icon( icon: const Icon(Icons.map_outlined), label: const Text('Locate on floor plan'), - onPressed: () { - ref.read(selectedSensorIdProvider.notifier).state = sensorId; - context.go('/floorplan'); - }, + onPressed: () => context.go('/floorplan'), ), const SizedBox(height: 12), OutlinedButton.icon( diff --git a/lib/features/sensors/sensor_detail_sheet.dart b/lib/features/sensors/sensor_detail_sheet.dart index 4859631..713a5b2 100644 --- a/lib/features/sensors/sensor_detail_sheet.dart +++ b/lib/features/sensors/sensor_detail_sheet.dart @@ -20,7 +20,8 @@ class SensorDetailSheet extends ConsumerStatefulWidget { final int sensorId; @override - ConsumerState createState() => _SensorDetailSheetState(); + ConsumerState createState() => + _SensorDetailSheetState(); } class _SensorDetailSheetState extends ConsumerState { @@ -196,7 +197,7 @@ class _SheetBody extends StatelessWidget { ), const SizedBox(height: 24), FilledButton.icon( - icon: const Icon(Icons.map_outlined), + icon: Icon(Icons.my_location), label: Text(sensor.isPlaced ? 'Reposition on floor plan' : 'Place on floor plan'), diff --git a/lib/features/sensors/sensor_list_screen.dart b/lib/features/sensors/sensor_list_screen.dart index 5e373af..4d0dfea 100644 --- a/lib/features/sensors/sensor_list_screen.dart +++ b/lib/features/sensors/sensor_list_screen.dart @@ -12,28 +12,11 @@ class SensorListScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final sensors = ref.watch(sensorsProvider); - final selectedId = ref.watch(selectedSensorIdProvider); return Scaffold( appBar: AppBar(title: const Text('Sensors')), body: Column( children: [ - if (selectedId != null) - MaterialBanner( - content: Text('Sensor $selectedId selected on floor plan'), - actions: [ - TextButton( - onPressed: () => - ref.read(selectedSensorIdProvider.notifier).state = null, - child: const Text('Dismiss'), - ), - TextButton( - onPressed: () => - showSensorDetailSheet(context, int.parse(selectedId)), - child: const Text('Open'), - ), - ], - ), Expanded( child: sensors.when( loading: () => @@ -54,8 +37,6 @@ class SensorListScreen extends ConsumerWidget { label: 'Unplaced', count: unplaced.length), ...unplaced.map((s) => _SensorTile( sensor: s, - isSelected: - selectedId == s.id.toString(), )), ], if (placed.isNotEmpty) ...[ @@ -63,8 +44,6 @@ class SensorListScreen extends ConsumerWidget { label: 'Placed', count: placed.length), ...placed.map((s) => _SensorTile( sensor: s, - isSelected: - selectedId == s.id.toString(), )), ], ], @@ -108,7 +87,7 @@ class _SectionHeader extends StatelessWidget { } class _SensorTile extends StatelessWidget { - const _SensorTile({required this.sensor, required this.isSelected}); + const _SensorTile({required this.sensor, this.isSelected = false}); final Sensor sensor; final bool isSelected; diff --git a/lib/providers.dart b/lib/providers.dart index 82c9bd6..8b2f24d 100644 --- a/lib/providers.dart +++ b/lib/providers.dart @@ -146,8 +146,6 @@ final sensorProvider = // Cross-tab UI state // --------------------------------------------------------------------------- -final selectedSensorIdProvider = StateProvider((ref) => null); - /// Non-null while the user is placing a sensor on the floor plan via the /// center-dot placement flow. Cleared on Place, Cancel, or back navigation. final sensorPlacementProvider = StateProvider((ref) => null);