124 lines
3.7 KiB
Dart
124 lines
3.7 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:shelf/shelf_io.dart' as shelf_io;
|
|
import 'package:shelf_web_socket/shelf_web_socket.dart';
|
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
|
import 'package:mirror_protocol/mirror_protocol.dart';
|
|
import '../providers/display_state_provider.dart';
|
|
import '../providers/layout_provider.dart';
|
|
import 'layout_persistence.dart';
|
|
|
|
final wsServerProvider = Provider<WsServer>((ref) => WsServer(ref));
|
|
|
|
class WsServer {
|
|
final Ref _ref;
|
|
final Set<WebSocketChannel> _clients = {};
|
|
HttpServer? _server;
|
|
final int port;
|
|
final LayoutPersistence _layoutPersistence = LayoutPersistence();
|
|
DashboardLayoutConfig? _currentLayout;
|
|
|
|
WsServer(this._ref, {this.port = 9090});
|
|
|
|
Future<void> start() async {
|
|
_currentLayout = await _layoutPersistence.load();
|
|
|
|
final handler = webSocketHandler((WebSocketChannel channel) {
|
|
_clients.add(channel);
|
|
|
|
// Send current state on connect
|
|
final state = _ref.read(displayStateProvider);
|
|
final sync = StateSync(state: state);
|
|
channel.sink.add(sync.encode());
|
|
|
|
channel.stream.listen(
|
|
(data) => _handleMessage(channel, data as String),
|
|
onDone: () => _clients.remove(channel),
|
|
onError: (_) => _clients.remove(channel),
|
|
);
|
|
});
|
|
|
|
_server = await shelf_io.serve(handler, InternetAddress.anyIPv4, port);
|
|
print('WebSocket server running on :$port');
|
|
}
|
|
|
|
void _handleMessage(WebSocketChannel sender, String raw) {
|
|
try {
|
|
final message = MirrorMessage.decode(raw);
|
|
_routeMessage(sender, message);
|
|
} catch (e) {
|
|
print('Invalid message: $e');
|
|
}
|
|
}
|
|
|
|
void _routeMessage(WebSocketChannel sender, MirrorMessage message) {
|
|
final notifier = _ref.read(displayStateProvider.notifier);
|
|
|
|
switch (message) {
|
|
case NavigateCommand(:final target):
|
|
notifier.navigate(target);
|
|
case PlaybackCommand(:final action, :final value):
|
|
notifier.handlePlayback(action, value);
|
|
case WorkoutCommand(:final action, :final payload):
|
|
notifier.handleWorkout(action, payload);
|
|
case ProgramCommand(:final action, :final payload):
|
|
notifier.handleProgram(action, payload);
|
|
case MediaCommand(:final action, :final payload):
|
|
notifier.handleMedia(action, payload);
|
|
case RegisterMessage():
|
|
// Already handled on connect
|
|
break;
|
|
case DashboardConfigRequest():
|
|
_handleConfigRequest(sender);
|
|
case DashboardConfigUpdate(:final layout):
|
|
_handleConfigUpdate(layout);
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
void _handleConfigRequest(WebSocketChannel sender) {
|
|
final layout = _currentLayout ?? DashboardLayoutConfig.defaultLayout();
|
|
final response = DashboardConfigResponse(layout: layout);
|
|
try {
|
|
sender.sink.add(response.encode());
|
|
} catch (_) {
|
|
_clients.remove(sender);
|
|
}
|
|
}
|
|
|
|
void _handleConfigUpdate(DashboardLayoutConfig layout) {
|
|
_currentLayout = layout;
|
|
_layoutPersistence.save(layout);
|
|
// Update the layout provider so dashboard screen rebuilds
|
|
_ref.read(layoutProvider.notifier).updateLayout(layout);
|
|
}
|
|
|
|
void broadcast(MirrorMessage message) {
|
|
final encoded = message.encode();
|
|
for (final client in _clients.toList()) {
|
|
try {
|
|
client.sink.add(encoded);
|
|
} catch (_) {
|
|
_clients.remove(client);
|
|
}
|
|
}
|
|
}
|
|
|
|
void broadcastState(DisplayState state) {
|
|
broadcast(StateSync(state: state));
|
|
}
|
|
|
|
DashboardLayoutConfig get currentLayout =>
|
|
_currentLayout ?? DashboardLayoutConfig.defaultLayout();
|
|
|
|
Future<void> stop() async {
|
|
for (final client in _clients) {
|
|
await client.sink.close();
|
|
}
|
|
_clients.clear();
|
|
await _server?.close();
|
|
}
|
|
}
|