2347f382c6
- Исправлены ошибки Flutter анализа (withOpacity, pbkdf2, импорты) - Упрощён pubspec.yaml (только нужные зависимости) - Android APK собран: build/app/outputs/flutter-apk/app-release.apk (22MB) - Linux Desktop собран: build/linux/x64/release/bundle/jam_client Для Windows: кросс-компиляция невозможна с Linux. Нужен Windows хост для flutter build windows.
198 lines
4.7 KiB
Dart
198 lines
4.7 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:web_socket_channel/web_socket_channel.dart';
|
|
|
|
enum ConnectionState { disconnected, connecting, connected }
|
|
|
|
class ConnectionService extends ChangeNotifier {
|
|
WebSocketChannel? _channel;
|
|
ConnectionState _state = ConnectionState.disconnected;
|
|
String _serverUrl = 'ws://localhost:8443/ws';
|
|
String? _userId;
|
|
String? _username;
|
|
String? _token;
|
|
Timer? _heartbeatTimer;
|
|
int _seq = 0;
|
|
|
|
final StreamController<Map<String, dynamic>> _messageController =
|
|
StreamController<Map<String, dynamic>>.broadcast();
|
|
|
|
Stream<Map<String, dynamic>> get messageStream => _messageController.stream;
|
|
|
|
ConnectionState get state => _state;
|
|
String? get userId => _userId;
|
|
String? get username => _username;
|
|
String? get token => _token;
|
|
bool get isConnected => _state == ConnectionState.connected;
|
|
|
|
String get serverUrl => _serverUrl;
|
|
set serverUrl(String url) {
|
|
_serverUrl = url;
|
|
notifyListeners();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
disconnect();
|
|
_messageController.close();
|
|
super.dispose();
|
|
}
|
|
|
|
void init() {
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> connect() async {
|
|
_state = ConnectionState.connecting;
|
|
notifyListeners();
|
|
|
|
try {
|
|
_channel = WebSocketChannel.connect(Uri.parse(_serverUrl));
|
|
await _channel!.ready;
|
|
|
|
_channel!.stream.listen(
|
|
(data) {
|
|
_handleMessage(data);
|
|
},
|
|
onError: (error) {
|
|
_state = ConnectionState.disconnected;
|
|
notifyListeners();
|
|
},
|
|
onDone: () {
|
|
_state = ConnectionState.disconnected;
|
|
_heartbeatTimer?.cancel();
|
|
notifyListeners();
|
|
},
|
|
);
|
|
|
|
_state = ConnectionState.connected;
|
|
_startHeartbeat();
|
|
notifyListeners();
|
|
} catch (e) {
|
|
_state = ConnectionState.disconnected;
|
|
notifyListeners();
|
|
rethrow;
|
|
}
|
|
}
|
|
|
|
void disconnect() {
|
|
_heartbeatTimer?.cancel();
|
|
_channel?.sink.close();
|
|
_channel = null;
|
|
_state = ConnectionState.disconnected;
|
|
notifyListeners();
|
|
}
|
|
|
|
void _startHeartbeat() {
|
|
_heartbeatTimer = Timer.periodic(const Duration(seconds: 30), (_) {
|
|
send({'op': 0});
|
|
});
|
|
}
|
|
|
|
void _handleMessage(dynamic data) {
|
|
try {
|
|
final Map<String, dynamic> packet = jsonDecode(data as String);
|
|
final int op = packet['op'] as int;
|
|
|
|
switch (op) {
|
|
case 1: // Hello
|
|
break;
|
|
case 3: // Authenticated
|
|
final d = packet['d'] as Map<String, dynamic>;
|
|
_userId = d['user_id'] as String?;
|
|
_username = d['username'] as String?;
|
|
_token = d['token'] as String?;
|
|
notifyListeners();
|
|
break;
|
|
case 10: // MessageCreate
|
|
_messageController.add(packet['d'] as Map<String, dynamic>);
|
|
break;
|
|
case 13: // Reaction
|
|
_messageController.add(packet['d'] as Map<String, dynamic>);
|
|
break;
|
|
case 40: // VoiceState
|
|
_messageController.add(packet['d'] as Map<String, dynamic>);
|
|
break;
|
|
case 60: // Typing
|
|
_messageController.add(packet['d'] as Map<String, dynamic>);
|
|
break;
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Failed to handle message: $e');
|
|
}
|
|
}
|
|
|
|
void send(Map<String, dynamic> packet) {
|
|
if (_channel == null) return;
|
|
packet['s'] = ++_seq;
|
|
_channel!.sink.add(jsonEncode(packet));
|
|
}
|
|
|
|
void authenticate({String? token, String? username}) {
|
|
send({
|
|
'op': 2,
|
|
'd': {
|
|
if (token != null) 'token': token,
|
|
if (username != null) 'username': username,
|
|
},
|
|
});
|
|
}
|
|
|
|
void sendMessage({
|
|
required String channelId,
|
|
required Uint8List content,
|
|
bool encrypted = false,
|
|
Uint8List? nonce,
|
|
String messageType = 'text',
|
|
String? replyTo,
|
|
}) {
|
|
send({
|
|
'op': 10,
|
|
'd': {
|
|
'channel_id': channelId,
|
|
'content': content.toList(),
|
|
'encrypted': encrypted,
|
|
if (nonce != null) 'nonce': nonce.toList(),
|
|
'message_type': messageType,
|
|
if (replyTo != null) 'reply_to': replyTo,
|
|
},
|
|
});
|
|
}
|
|
|
|
void sendReaction(String messageId, String emoji, {bool add = true}) {
|
|
send({
|
|
'op': 13,
|
|
'd': {
|
|
'message_id': messageId,
|
|
'emoji': emoji,
|
|
'add': add,
|
|
},
|
|
});
|
|
}
|
|
|
|
void updateVoiceState({
|
|
required String guildId,
|
|
String? channelId,
|
|
bool muted = false,
|
|
bool deafened = false,
|
|
}) {
|
|
send({
|
|
'op': 40,
|
|
'd': {
|
|
'guild_id': guildId,
|
|
'channel_id': channelId ?? '',
|
|
'muted': muted,
|
|
'deafened': deafened,
|
|
},
|
|
});
|
|
}
|
|
|
|
void sendTyping(String channelId) {
|
|
send({
|
|
'op': 60,
|
|
'd': {'channel_id': channelId},
|
|
});
|
|
}
|
|
}
|