init: rough companion app stub
This commit is contained in:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user