Preserve Flutter mirror-os codebase
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:mirror_protocol/mirror_protocol.dart';
|
||||
|
||||
class LayoutPersistence {
|
||||
final String _path;
|
||||
|
||||
LayoutPersistence({String? path})
|
||||
: _path = path ??
|
||||
'${Platform.environment['HOME']}/.config/mirror-os/dashboard_layout.json';
|
||||
|
||||
Future<DashboardLayoutConfig> load() async {
|
||||
final file = File(_path);
|
||||
if (!await file.exists()) {
|
||||
return DashboardLayoutConfig.defaultLayout();
|
||||
}
|
||||
try {
|
||||
final content = await file.readAsString();
|
||||
final json = jsonDecode(content) as Map<String, dynamic>;
|
||||
return DashboardLayoutConfig.fromJson(json);
|
||||
} catch (_) {
|
||||
return DashboardLayoutConfig.defaultLayout();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> save(DashboardLayoutConfig layout) async {
|
||||
final file = File(_path);
|
||||
final dir = file.parent;
|
||||
if (!await dir.exists()) {
|
||||
await dir.create(recursive: true);
|
||||
}
|
||||
final content = const JsonEncoder.withIndent(' ').convert(layout.toJson());
|
||||
await file.writeAsString(content);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user