feat: add sensor move mode to floor plan
This commit is contained in:
+19
-6
@@ -364,7 +364,7 @@ function renderSensors() {
|
|||||||
const group = new Konva.Group({
|
const group = new Konva.Group({
|
||||||
id: `sensor-${sensor.id}`,
|
id: `sensor-${sensor.id}`,
|
||||||
x: absX, y: absY,
|
x: absX, y: absY,
|
||||||
draggable: mode === 'edit',
|
draggable: mode === 'sensorMove',
|
||||||
});
|
});
|
||||||
|
|
||||||
group.add(new Konva.Circle({
|
group.add(new Konva.Circle({
|
||||||
@@ -395,8 +395,15 @@ function renderSensors() {
|
|||||||
listening: false,
|
listening: false,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
group.on('click tap', () => {
|
||||||
|
if (mode === 'view') {
|
||||||
|
notifyFlutter({ type: 'sensorTapped', id: String(sensor.id) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let pressTimer = null;
|
let pressTimer = null;
|
||||||
group.on('mousedown touchstart', () => {
|
group.on('mousedown touchstart', () => {
|
||||||
|
if (mode === 'view') return;
|
||||||
pressTimer = setTimeout(() => {
|
pressTimer = setTimeout(() => {
|
||||||
notifyFlutter({ type: 'sensorTapped', id: String(sensor.id) });
|
notifyFlutter({ type: 'sensorTapped', id: String(sensor.id) });
|
||||||
pressTimer = null;
|
pressTimer = null;
|
||||||
@@ -544,15 +551,21 @@ window.companion = {
|
|||||||
|
|
||||||
setMode(newMode) {
|
setMode(newMode) {
|
||||||
mode = newMode;
|
mode = newMode;
|
||||||
const edit = newMode === 'edit';
|
roomsLayer.find('Group').forEach(g => g.draggable(newMode === 'edit'));
|
||||||
roomsLayer.find('Group').forEach(g => g.draggable(edit));
|
sensorsLayer.find('Group').forEach(g => g.draggable(newMode === 'sensorMove'));
|
||||||
sensorsLayer.find('Group').forEach(g => g.draggable(edit));
|
tagsLayer.visible(newMode === 'view');
|
||||||
tagsLayer.visible(!edit);
|
particlesLayer.visible(newMode === 'view');
|
||||||
particlesLayer.visible(!edit);
|
|
||||||
_clearDimensions();
|
_clearDimensions();
|
||||||
stage.batchDraw();
|
stage.batchDraw();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setRepositioningSensor(id) {
|
||||||
|
sensorsLayer.find('Group').forEach(g => {
|
||||||
|
g.draggable(mode === 'sensorMove' && (id == null || g.id() === `sensor-${id}`));
|
||||||
|
});
|
||||||
|
stage.batchDraw();
|
||||||
|
},
|
||||||
|
|
||||||
highlightSensor(id) {
|
highlightSensor(id) {
|
||||||
sensorsLayer.find('.highlight-ring').forEach(r => r.visible(false));
|
sensorsLayer.find('.highlight-ring').forEach(r => r.visible(false));
|
||||||
if (id) {
|
if (id) {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
enum FloorPlanMode { view, edit }
|
enum FloorPlanMode { view, edit, sensorMove }
|
||||||
|
|||||||
@@ -32,7 +32,26 @@ class _FloorPlanScreenState extends ConsumerState<FloorPlanScreen> {
|
|||||||
.then((rooms) => _editorKey.currentState?.loadFloorPlan(rooms));
|
.then((rooms) => _editorKey.currentState?.loadFloorPlan(rooms));
|
||||||
ref
|
ref
|
||||||
.read(sensorsProvider.future)
|
.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<void> _confirmPlacement(Sensor sensor) async {
|
Future<void> _confirmPlacement(Sensor sensor) async {
|
||||||
@@ -81,9 +100,7 @@ class _FloorPlanScreenState extends ConsumerState<FloorPlanScreen> {
|
|||||||
ref.listen(particleCloudProvider, (_, next) {
|
ref.listen(particleCloudProvider, (_, next) {
|
||||||
next.whenData((p) => _editorKey.currentState?.updateParticleCloud(p));
|
next.whenData((p) => _editorKey.currentState?.updateParticleCloud(p));
|
||||||
});
|
});
|
||||||
ref.listen(selectedSensorIdProvider, (_, id) {
|
ref.listen(sensorPlacementProvider, (prev, next) => _applyPlacementState());
|
||||||
_editorKey.currentState?.highlightSensor(id);
|
|
||||||
});
|
|
||||||
|
|
||||||
final unplaced = sensorsAsync.valueOrNull
|
final unplaced = sensorsAsync.valueOrNull
|
||||||
?.where((s) => !s.isPlaced)
|
?.where((s) => !s.isPlaced)
|
||||||
@@ -103,23 +120,37 @@ class _FloorPlanScreenState extends ConsumerState<FloorPlanScreen> {
|
|||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Floor Plan'),
|
title: const Text('Floor Plan'),
|
||||||
actions: [
|
actions: [
|
||||||
if (placingSensor == null)
|
if (placingSensor == null) ...[
|
||||||
|
if (mode == FloorPlanMode.view) ...[
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip:
|
tooltip: 'Edit mode',
|
||||||
mode == FloorPlanMode.edit ? 'View mode' : 'Edit mode',
|
icon: const Icon(Icons.edit),
|
||||||
icon: Icon(
|
|
||||||
mode == FloorPlanMode.edit
|
|
||||||
? Icons.visibility
|
|
||||||
: Icons.edit,
|
|
||||||
),
|
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
final next = mode == FloorPlanMode.edit
|
ref.read(floorPlanModeProvider.notifier).state =
|
||||||
? FloorPlanMode.view
|
FloorPlanMode.edit;
|
||||||
: FloorPlanMode.edit;
|
_editorKey.currentState?.setMode(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(
|
body: Column(
|
||||||
@@ -131,8 +162,6 @@ class _FloorPlanScreenState extends ConsumerState<FloorPlanScreen> {
|
|||||||
key: _editorKey,
|
key: _editorKey,
|
||||||
mode: mode,
|
mode: mode,
|
||||||
onSensorTapped: (id) {
|
onSensorTapped: (id) {
|
||||||
ref.read(selectedSensorIdProvider.notifier).state =
|
|
||||||
id;
|
|
||||||
showSensorDetailSheet(context, int.parse(id));
|
showSensorDetailSheet(context, int.parse(id));
|
||||||
},
|
},
|
||||||
onSensorMoved: (sensorId, roomId, x, y) async {
|
onSensorMoved: (sensorId, roomId, x, y) async {
|
||||||
@@ -163,8 +192,6 @@ class _FloorPlanScreenState extends ConsumerState<FloorPlanScreen> {
|
|||||||
},
|
},
|
||||||
onRoomAdded: (x, y) => _showAddRoomDialog(x, y),
|
onRoomAdded: (x, y) => _showAddRoomDialog(x, y),
|
||||||
onEditRoomTapped: _showRoomEditSheet,
|
onEditRoomTapped: _showRoomEditSheet,
|
||||||
onEditSensorTapped: (id) =>
|
|
||||||
showSensorDetailSheet(context, int.parse(id)),
|
|
||||||
onAddSensorTapped: () => showModalBottomSheet<void>(
|
onAddSensorTapped: () => showModalBottomSheet<void>(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ class FloorPlanEditor extends StatefulWidget {
|
|||||||
this.onRoomsUpdated,
|
this.onRoomsUpdated,
|
||||||
this.onRoomAdded,
|
this.onRoomAdded,
|
||||||
this.onEditRoomTapped,
|
this.onEditRoomTapped,
|
||||||
this.onEditSensorTapped,
|
|
||||||
this.onAddSensorTapped,
|
this.onAddSensorTapped,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -31,7 +30,6 @@ class FloorPlanEditor extends StatefulWidget {
|
|||||||
final void Function(List<Room> rooms)? onRoomsUpdated;
|
final void Function(List<Room> rooms)? onRoomsUpdated;
|
||||||
final void Function(double x, double y)? onRoomAdded;
|
final void Function(double x, double y)? onRoomAdded;
|
||||||
final void Function(int roomId)? onEditRoomTapped;
|
final void Function(int roomId)? onEditRoomTapped;
|
||||||
final void Function(String sensorId)? onEditSensorTapped;
|
|
||||||
final void Function()? onAddSensorTapped;
|
final void Function()? onAddSensorTapped;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -44,7 +42,7 @@ class FloorPlanEditorState extends State<FloorPlanEditor> {
|
|||||||
final _pending = <Future<void> Function()>[];
|
final _pending = <Future<void> Function()>[];
|
||||||
|
|
||||||
int? _selectedRoomId;
|
int? _selectedRoomId;
|
||||||
String? _selectedSensorId;
|
String? _repositioningSensorId;
|
||||||
Completer<({int? roomId, double? x, double? y})>? _placementCompleter;
|
Completer<({int? roomId, double? x, double? y})>? _placementCompleter;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -78,19 +76,17 @@ class FloorPlanEditorState extends State<FloorPlanEditor> {
|
|||||||
switch (data['type'] as String?) {
|
switch (data['type'] as String?) {
|
||||||
case 'sensorTapped':
|
case 'sensorTapped':
|
||||||
final id = data['id'] as String;
|
final id = data['id'] as String;
|
||||||
if (widget.mode == FloorPlanMode.edit) {
|
|
||||||
setState(() {
|
|
||||||
_selectedSensorId = id;
|
|
||||||
_selectedRoomId = null;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
widget.onSensorTapped(id);
|
widget.onSensorTapped(id);
|
||||||
Vibration.hasVibrator()
|
Vibration.hasVibrator()
|
||||||
.then((t) { if (t) Vibration.vibrate(duration: 20); });
|
.then((t) { if (t) Vibration.vibrate(duration: 20); });
|
||||||
}
|
|
||||||
case 'sensorMoved':
|
case 'sensorMoved':
|
||||||
|
final id = data['id'] as String;
|
||||||
|
if (_repositioningSensorId == id) {
|
||||||
|
setState(() => _repositioningSensorId = null);
|
||||||
|
setRepositioningSensor(null);
|
||||||
|
}
|
||||||
widget.onSensorMoved(
|
widget.onSensorMoved(
|
||||||
data['id'] as String,
|
id,
|
||||||
(data['roomId'] as num).toInt(),
|
(data['roomId'] as num).toInt(),
|
||||||
(data['x'] as num).toDouble(),
|
(data['x'] as num).toDouble(),
|
||||||
(data['y'] as num).toDouble(),
|
(data['y'] as num).toDouble(),
|
||||||
@@ -106,15 +102,9 @@ class FloorPlanEditorState extends State<FloorPlanEditor> {
|
|||||||
(data['y'] as num).toDouble(),
|
(data['y'] as num).toDouble(),
|
||||||
);
|
);
|
||||||
case 'roomTapped':
|
case 'roomTapped':
|
||||||
setState(() {
|
setState(() => _selectedRoomId = (data['id'] as num).toInt());
|
||||||
_selectedRoomId = (data['id'] as num).toInt();
|
|
||||||
_selectedSensorId = null;
|
|
||||||
});
|
|
||||||
case 'selectionCleared':
|
case 'selectionCleared':
|
||||||
setState(() {
|
setState(() => _selectedRoomId = null);
|
||||||
_selectedRoomId = null;
|
|
||||||
_selectedSensorId = null;
|
|
||||||
});
|
|
||||||
case 'positionAtCenter':
|
case 'positionAtCenter':
|
||||||
final roomId = (data['roomId'] as num?)?.toInt();
|
final roomId = (data['roomId'] as num?)?.toInt();
|
||||||
final x = (data['x'] as num?)?.toDouble();
|
final x = (data['x'] as num?)?.toDouble();
|
||||||
@@ -204,7 +194,7 @@ class FloorPlanEditorState extends State<FloorPlanEditor> {
|
|||||||
Future<void> setMode(FloorPlanMode mode) {
|
Future<void> setMode(FloorPlanMode mode) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedRoomId = null;
|
_selectedRoomId = null;
|
||||||
_selectedSensorId = null;
|
_repositioningSensorId = null;
|
||||||
});
|
});
|
||||||
return _run(() async {
|
return _run(() async {
|
||||||
await _controller.runJavaScript(
|
await _controller.runJavaScript(
|
||||||
@@ -213,6 +203,16 @@ class FloorPlanEditorState extends State<FloorPlanEditor> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setRepositioningSensor(String? id) {
|
||||||
|
setState(() => _repositioningSensorId = id);
|
||||||
|
return _run(() async {
|
||||||
|
final jsId = id == null ? 'null' : jsonEncode(id);
|
||||||
|
await _controller.runJavaScript(
|
||||||
|
'window.companion.setRepositioningSensor($jsId)',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> addRoom() {
|
Future<void> addRoom() {
|
||||||
return _run(() async {
|
return _run(() async {
|
||||||
await _controller.runJavaScript('window.companion.addRoom()');
|
await _controller.runJavaScript('window.companion.addRoom()');
|
||||||
@@ -245,16 +245,6 @@ class FloorPlanEditorState extends State<FloorPlanEditor> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
if (inEditMode) ...[
|
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) ...[
|
if (_selectedRoomId != null) ...[
|
||||||
FloatingActionButton.small(
|
FloatingActionButton.small(
|
||||||
heroTag: 'editor-edit-room',
|
heroTag: 'editor-edit-room',
|
||||||
|
|||||||
@@ -59,10 +59,7 @@ class _Body extends ConsumerWidget {
|
|||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
icon: const Icon(Icons.map_outlined),
|
icon: const Icon(Icons.map_outlined),
|
||||||
label: const Text('Locate on floor plan'),
|
label: const Text('Locate on floor plan'),
|
||||||
onPressed: () {
|
onPressed: () => context.go('/floorplan'),
|
||||||
ref.read(selectedSensorIdProvider.notifier).state = sensorId;
|
|
||||||
context.go('/floorplan');
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
OutlinedButton.icon(
|
OutlinedButton.icon(
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ class SensorDetailSheet extends ConsumerStatefulWidget {
|
|||||||
final int sensorId;
|
final int sensorId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ConsumerState<SensorDetailSheet> createState() => _SensorDetailSheetState();
|
ConsumerState<SensorDetailSheet> createState() =>
|
||||||
|
_SensorDetailSheetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _SensorDetailSheetState extends ConsumerState<SensorDetailSheet> {
|
class _SensorDetailSheetState extends ConsumerState<SensorDetailSheet> {
|
||||||
@@ -196,7 +197,7 @@ class _SheetBody extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
FilledButton.icon(
|
FilledButton.icon(
|
||||||
icon: const Icon(Icons.map_outlined),
|
icon: Icon(Icons.my_location),
|
||||||
label: Text(sensor.isPlaced
|
label: Text(sensor.isPlaced
|
||||||
? 'Reposition on floor plan'
|
? 'Reposition on floor plan'
|
||||||
: 'Place on floor plan'),
|
: 'Place on floor plan'),
|
||||||
|
|||||||
@@ -12,28 +12,11 @@ class SensorListScreen extends ConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final sensors = ref.watch(sensorsProvider);
|
final sensors = ref.watch(sensorsProvider);
|
||||||
final selectedId = ref.watch(selectedSensorIdProvider);
|
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Sensors')),
|
appBar: AppBar(title: const Text('Sensors')),
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
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(
|
Expanded(
|
||||||
child: sensors.when(
|
child: sensors.when(
|
||||||
loading: () =>
|
loading: () =>
|
||||||
@@ -54,8 +37,6 @@ class SensorListScreen extends ConsumerWidget {
|
|||||||
label: 'Unplaced', count: unplaced.length),
|
label: 'Unplaced', count: unplaced.length),
|
||||||
...unplaced.map((s) => _SensorTile(
|
...unplaced.map((s) => _SensorTile(
|
||||||
sensor: s,
|
sensor: s,
|
||||||
isSelected:
|
|
||||||
selectedId == s.id.toString(),
|
|
||||||
)),
|
)),
|
||||||
],
|
],
|
||||||
if (placed.isNotEmpty) ...[
|
if (placed.isNotEmpty) ...[
|
||||||
@@ -63,8 +44,6 @@ class SensorListScreen extends ConsumerWidget {
|
|||||||
label: 'Placed', count: placed.length),
|
label: 'Placed', count: placed.length),
|
||||||
...placed.map((s) => _SensorTile(
|
...placed.map((s) => _SensorTile(
|
||||||
sensor: s,
|
sensor: s,
|
||||||
isSelected:
|
|
||||||
selectedId == s.id.toString(),
|
|
||||||
)),
|
)),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -108,7 +87,7 @@ class _SectionHeader extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _SensorTile 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 Sensor sensor;
|
||||||
final bool isSelected;
|
final bool isSelected;
|
||||||
|
|||||||
@@ -146,8 +146,6 @@ final sensorProvider =
|
|||||||
// Cross-tab UI state
|
// Cross-tab UI state
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
final selectedSensorIdProvider = StateProvider<String?>((ref) => null);
|
|
||||||
|
|
||||||
/// Non-null while the user is placing a sensor on the floor plan via the
|
/// 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.
|
/// center-dot placement flow. Cleared on Place, Cancel, or back navigation.
|
||||||
final sensorPlacementProvider = StateProvider<Sensor?>((ref) => null);
|
final sensorPlacementProvider = StateProvider<Sensor?>((ref) => null);
|
||||||
|
|||||||
Reference in New Issue
Block a user