init: rough companion app stub

This commit is contained in:
2026-05-07 18:35:58 +02:00
commit 5f017ac05d
73 changed files with 3520 additions and 0 deletions
@@ -0,0 +1,82 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../../domain/models/floor_plan_mode.dart';
import '../../providers.dart';
import '../ble_provision/ble_provision_sheet.dart';
import 'widgets/konva_web_view.dart';
class FloorPlanScreen extends ConsumerStatefulWidget {
const FloorPlanScreen({super.key});
@override
ConsumerState<FloorPlanScreen> createState() => _FloorPlanScreenState();
}
class _FloorPlanScreenState extends ConsumerState<FloorPlanScreen> {
final _konvaKey = GlobalKey<KonvaWebViewState>();
@override
Widget build(BuildContext context) {
final mode = ref.watch(floorPlanModeProvider);
// TODO: forward live tag positions into the WebView.
// ref.listen(tagPositionsProvider, (_, next) {
// next.whenData((positions) => _konvaKey.currentState?.updateTags(positions));
// });
// TODO: forward particle cloud updates into the WebView.
// ref.listen(particleCloudProvider, (_, next) {
// next.whenData((particles) => _konvaKey.currentState?.updateParticleCloud(particles));
// });
// TODO: react to selectedSensorIdProvider and highlight sensor in WebView.
// ref.listen(selectedSensorIdProvider, (_, id) {
// _konvaKey.currentState?.highlightSensor(id);
// });
return Scaffold(
appBar: AppBar(
title: const Text('Floor Plan'),
actions: [
IconButton(
tooltip: mode == FloorPlanMode.edit ? 'View mode' : 'Edit mode',
icon: Icon(
mode == FloorPlanMode.edit ? Icons.visibility : Icons.edit,
),
onPressed: () {
final next = mode == FloorPlanMode.edit
? FloorPlanMode.view
: FloorPlanMode.edit;
ref.read(floorPlanModeProvider.notifier).state = next;
_konvaKey.currentState?.setMode(next);
},
),
],
),
body: KonvaWebView(
key: _konvaKey,
mode: mode,
onSensorTapped: (id) {
ref.read(selectedSensorIdProvider.notifier).state = id;
// TODO: optionally navigate to sensor detail or show tooltip.
},
onSensorMoved: (id, position) {
// TODO: persist new position via sensorRepositoryProvider.
},
),
floatingActionButton: mode == FloorPlanMode.edit
? FloatingActionButton.extended(
icon: const Icon(Icons.add),
label: const Text('Add sensor'),
onPressed: () => showModalBottomSheet<void>(
context: context,
isScrollControlled: true,
builder: (_) => const BleProvisionSheet(),
),
)
: null,
);
}
}
@@ -0,0 +1,82 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:webview_flutter/webview_flutter.dart';
import '../../../domain/models/floor_plan_mode.dart';
import '../../../domain/models/tag.dart';
import '../../../domain/models/particle.dart';
import '../../../domain/models/position.dart';
class KonvaWebView extends StatefulWidget {
const KonvaWebView({
super.key,
required this.mode,
required this.onSensorTapped,
required this.onSensorMoved,
});
final FloorPlanMode mode;
final void Function(String sensorId) onSensorTapped;
final void Function(String sensorId, Position newPosition) onSensorMoved;
@override
State<KonvaWebView> createState() => KonvaWebViewState();
}
class KonvaWebViewState extends State<KonvaWebView> {
late final WebViewController _controller;
@override
void initState() {
super.initState();
_controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..addJavaScriptChannel('FlutterBridge', onMessageReceived: _onMessage)
..loadFlutterAsset('assets/konva/index.html');
}
void _onMessage(JavaScriptMessage message) {
final data = jsonDecode(message.message) as Map<String, dynamic>;
switch (data['type'] as String?) {
case 'sensorTapped':
widget.onSensorTapped(data['id'] as String);
case 'sensorMoved':
widget.onSensorMoved(
data['id'] as String,
Position(
x: (data['x'] as num).toDouble(),
y: (data['y'] as num).toDouble(),
),
);
}
}
Future<void> updateTags(List<TagPosition> positions) async {
final payload = jsonEncode(positions
.map((p) => {'tagId': p.tagId, 'x': p.position.x, 'y': p.position.y})
.toList());
await _controller.runJavaScript('window.companion.updateTags($payload)');
}
Future<void> updateParticleCloud(List<Particle> particles) 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<void> highlightSensor(String? sensorId) async {
final id = sensorId == null ? 'null' : '"$sensorId"';
await _controller.runJavaScript('window.companion.highlightSensor($id)');
}
Future<void> setMode(FloorPlanMode mode) async {
await _controller.runJavaScript(
'window.companion.setMode("${mode.name}")',
);
}
@override
Widget build(BuildContext context) => WebViewWidget(controller: _controller);
}