127 lines
3.5 KiB
Dart
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;
|
|
}
|
|
}
|