Files
mirror-os-flutter/apps/remote/lib/providers/connection_provider.dart
T
2026-07-11 21:51:34 -05:00

127 lines
3.5 KiB
Dart

import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:mirror_protocol/mirror_protocol.dart';
final connectionProvider =
NotifierProvider<ConnectionNotifier, ConnectionState>(ConnectionNotifier.new);
final mirrorStateProvider = StateProvider<DisplayState?>((ref) => null);
final dashboardConfigProvider = StateProvider<DashboardLayoutConfig?>((ref) => null);
class ConnectionState {
final bool connected;
final bool connecting;
final String? host;
final String? error;
final bool manualDisconnect;
const ConnectionState({
this.connected = false,
this.connecting = false,
this.host,
this.error,
this.manualDisconnect = false,
});
ConnectionState copyWith({
bool? connected,
bool? connecting,
String? host,
String? error,
bool clearError = false,
bool? manualDisconnect,
}) =>
ConnectionState(
connected: connected ?? this.connected,
connecting: connecting ?? this.connecting,
host: host ?? this.host,
error: clearError ? null : (error ?? this.error),
manualDisconnect: manualDisconnect ?? this.manualDisconnect,
);
}
class ConnectionNotifier extends Notifier<ConnectionState> {
WebSocketChannel? _channel;
Timer? _reconnectTimer;
@override
ConnectionState build() => const ConnectionState();
Future<void> connect(String host, {int port = 9090}) async {
state = state.copyWith(connecting: true, clearError: true, manualDisconnect: false);
try {
final uri = Uri.parse('ws://$host:$port');
_channel = WebSocketChannel.connect(uri);
await _channel!.ready;
state = state.copyWith(connected: true, connecting: false, host: host);
// Send registration
_channel!.sink.add(RegisterMessage(role: 'remote').encode());
// Listen for messages
_channel!.stream.listen(
_handleMessage,
onDone: () => _onDisconnected(),
onError: (e) => _onDisconnected(error: e.toString()),
);
} catch (e) {
state = state.copyWith(
connected: false,
connecting: false,
error: 'Failed to connect: $e',
);
}
}
void _handleMessage(dynamic data) {
try {
final message = MirrorMessage.decode(data as String);
switch (message) {
case StateSync(state: final mirrorState):
ref.read(mirrorStateProvider.notifier).state = mirrorState;
case DashboardConfigResponse(layout: final layout):
ref.read(dashboardConfigProvider.notifier).state = layout;
case StatePatch():
break;
case EventMessage():
break;
default:
break;
}
} catch (e) {
print('Failed to parse message: $e');
}
}
void _onDisconnected({String? error}) {
state = state.copyWith(connected: false, error: error);
_channel = null;
// Only auto-reconnect if not intentionally disconnected
if (state.host != null && error != null) {
_reconnectTimer?.cancel();
_reconnectTimer = Timer(const Duration(seconds: 3), () {
if (state.host != null && !state.connected) {
connect(state.host!);
}
});
}
}
void send(MirrorMessage message) {
_channel?.sink.add(message.encode());
}
void disconnect() {
_reconnectTimer?.cancel();
_channel?.sink.close();
_channel = null;
state = const ConnectionState(manualDisconnect: true);
ref.read(mirrorStateProvider.notifier).state = null;
}
}