🧱 Websocket connection

This commit is contained in:
LittleSheep 2025-05-01 01:21:52 +08:00
parent 525079a52f
commit 26093115c4
6 changed files with 319 additions and 3 deletions

View File

@ -1,3 +1,4 @@
import 'dart:developer';
import 'dart:io';
import 'package:easy_localization/easy_localization.dart' hide TextDirection;
@ -13,6 +14,7 @@ import 'package:island/pods/network.dart';
import 'package:island/pods/theme.dart';
import 'package:bitsdojo_window/bitsdojo_window.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/route.dart';
import 'package:island/services/notify.dart';
import 'package:island/widgets/app_scaffold.dart';
@ -73,12 +75,17 @@ class IslandApp extends HookConsumerWidget {
useEffect(() {
// Load userinfo
final userNotifier = ref.read(userInfoProvider.notifier);
ref.listen(websocketStateProvider, (_, state) {
log('[WebSocket] $state');
});
Future(() {
userNotifier.fetchUser().then((_) {
final user = ref.watch(userInfoProvider);
if (user.hasValue) {
final apiClient = ref.read(apiClientProvider);
subscribePushNotification(apiClient);
final wsNotifier = ref.read(websocketStateProvider.notifier);
wsNotifier.connect();
}
});
});

98
lib/pods/websocket.dart Normal file
View File

@ -0,0 +1,98 @@
import 'dart:developer';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:web_socket_channel/io.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
part 'websocket.freezed.dart';
@freezed
class WebSocketState with _$WebSocketState {
const factory WebSocketState.connected() = _Connected;
const factory WebSocketState.connecting() = _Connecting;
const factory WebSocketState.disconnected() = _Disconnected;
const factory WebSocketState.error(String message) = _Error;
}
final websocketProvider = Provider<WebSocketService>((ref) {
return WebSocketService();
});
class WebSocketService {
WebSocketChannel? _channel;
Stream<dynamic>? _broadcastStream;
Future<void> connect(String url, String atk) async {
log('[WebSocket] Trying connecting to $url');
try {
_channel = IOWebSocketChannel.connect(
Uri.parse(url),
headers: {'Authorization': 'Bearer $atk'},
);
await _channel!.ready;
_broadcastStream = _channel!.stream.asBroadcastStream();
} catch (err) {
log('[WebSocket] Failed to connect: $err');
}
}
WebSocketChannel? get ws => _channel;
Stream<dynamic> get stream => _broadcastStream!;
void sendMessage(String message) {
_channel!.sink.add(message);
}
void close() {
_channel?.sink.close();
}
}
final websocketStateProvider =
StateNotifierProvider<WebSocketStateNotifier, WebSocketState>(
(ref) => WebSocketStateNotifier(ref),
);
class WebSocketStateNotifier extends StateNotifier<WebSocketState> {
final Ref ref;
WebSocketStateNotifier(this.ref) : super(const WebSocketState.disconnected());
Future<void> connect() async {
state = const WebSocketState.connecting();
try {
final service = ref.read(websocketProvider);
final baseUrl = ref.watch(serverUrlProvider);
final atk = await getFreshAtk(
ref.watch(tokenPairProvider),
baseUrl,
onRefreshed: (atk, rtk) {
setTokenPair(ref.watch(sharedPreferencesProvider), atk, rtk);
ref.invalidate(tokenPairProvider);
},
);
if (atk == null) {
state = const WebSocketState.error('Unauthorized');
return;
}
await service.connect('$baseUrl/ws'.replaceFirst('http', 'ws'), atk);
state = const WebSocketState.connected();
} catch (err) {
state = WebSocketState.error('Failed to connect: $err');
}
}
void sendMessage(String message) {
final service = ref.read(websocketProvider);
service.sendMessage(message);
}
void close() {
final service = ref.read(websocketProvider);
service.close();
state = const WebSocketState.disconnected();
}
}

View File

@ -0,0 +1,207 @@
// dart format width=80
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'websocket.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
// dart format off
T _$identity<T>(T value) => value;
/// @nodoc
mixin _$WebSocketState {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is WebSocketState);
}
@override
int get hashCode => runtimeType.hashCode;
@override
String toString() {
return 'WebSocketState()';
}
}
/// @nodoc
class $WebSocketStateCopyWith<$Res> {
$WebSocketStateCopyWith(WebSocketState _, $Res Function(WebSocketState) __);
}
/// @nodoc
class _Connected implements WebSocketState {
const _Connected();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Connected);
}
@override
int get hashCode => runtimeType.hashCode;
@override
String toString() {
return 'WebSocketState.connected()';
}
}
/// @nodoc
class _Connecting implements WebSocketState {
const _Connecting();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Connecting);
}
@override
int get hashCode => runtimeType.hashCode;
@override
String toString() {
return 'WebSocketState.connecting()';
}
}
/// @nodoc
class _Disconnected implements WebSocketState {
const _Disconnected();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Disconnected);
}
@override
int get hashCode => runtimeType.hashCode;
@override
String toString() {
return 'WebSocketState.disconnected()';
}
}
/// @nodoc
class _Error implements WebSocketState {
const _Error(this.message);
final String message;
/// Create a copy of WebSocketState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$ErrorCopyWith<_Error> get copyWith => __$ErrorCopyWithImpl<_Error>(this, _$identity);
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _Error&&(identical(other.message, message) || other.message == message));
}
@override
int get hashCode => Object.hash(runtimeType,message);
@override
String toString() {
return 'WebSocketState.error(message: $message)';
}
}
/// @nodoc
abstract mixin class _$ErrorCopyWith<$Res> implements $WebSocketStateCopyWith<$Res> {
factory _$ErrorCopyWith(_Error value, $Res Function(_Error) _then) = __$ErrorCopyWithImpl;
@useResult
$Res call({
String message
});
}
/// @nodoc
class __$ErrorCopyWithImpl<$Res>
implements _$ErrorCopyWith<$Res> {
__$ErrorCopyWithImpl(this._self, this._then);
final _Error _self;
final $Res Function(_Error) _then;
/// Create a copy of WebSocketState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') $Res call({Object? message = null,}) {
return _then(_Error(
null == message ? _self.message : message // ignore: cast_nullable_to_non_nullable
as String,
));
}
}
// dart format on

View File

@ -11,6 +11,7 @@ import 'package:island/models/auth.dart';
import 'package:island/pods/config.dart';
import 'package:island/pods/network.dart';
import 'package:island/pods/userinfo.dart';
import 'package:island/pods/websocket.dart';
import 'package:island/services/notify.dart';
import 'package:island/widgets/alert.dart';
import 'package:island/widgets/app_scaffold.dart';
@ -136,6 +137,8 @@ class _LoginCheckScreen extends HookConsumerWidget {
userNotifier.fetchUser().then((_) {
final apiClient = ref.read(apiClientProvider);
subscribePushNotification(apiClient);
final wsNotifier = ref.read(websocketStateProvider.notifier);
wsNotifier.connect();
});
Navigator.pop(context, true);
} catch (err) {

View File

@ -1273,10 +1273,10 @@ packages:
dependency: transitive
description:
name: provider
sha256: "489024f942069c2920c844ee18bb3d467c69e48955a4f32d1677f71be103e310"
sha256: "4abbd070a04e9ddc287673bf5a030c7ca8b685ff70218720abab8b092f53dd84"
url: "https://pub.dev"
source: hosted
version: "6.1.4"
version: "6.1.5"
pub_semver:
dependency: transitive
description:
@ -1844,7 +1844,7 @@ packages:
source: hosted
version: "1.0.0"
web_socket_channel:
dependency: transitive
dependency: "direct main"
description:
name: web_socket_channel
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8

View File

@ -83,6 +83,7 @@ dependencies:
firebase_messaging: ^15.2.5
flutter_udid: ^4.0.0
firebase_core: ^3.13.0
web_socket_channel: ^3.0.3
dev_dependencies:
flutter_test: