feat: update messages

This commit is contained in:
xkeyC 2025-11-24 12:25:32 +08:00
parent 078eae8e52
commit 4718b4627b
8 changed files with 309 additions and 202 deletions

View File

@ -39,6 +39,7 @@ sealed class PartyRoomState with _$PartyRoomState {
@Default(false) bool isOwner, @Default(false) bool isOwner,
String? roomUuid, String? roomUuid,
@Default([]) List<partroom.RoomEvent> recentEvents, @Default([]) List<partroom.RoomEvent> recentEvents,
@Default(false) bool eventStreamDisconnected,
}) = _PartyRoomState; }) = _PartyRoomState;
} }
@ -75,11 +76,7 @@ class PartyRoom extends _$PartyRoom {
Box? _confBox; Box? _confBox;
StreamSubscription<partroom.RoomEvent>? _eventStreamSubscription; StreamSubscription<partroom.RoomEvent>? _eventStreamSubscription;
Timer? _heartbeatTimer; Timer? _heartbeatTimer;
Timer? _reconnectTimer;
bool _disposed = false; bool _disposed = false;
int _reconnectAttempts = 0;
static const int _maxReconnectAttempts = 5;
static const Duration _reconnectDelay = Duration(seconds: 3);
@override @override
PartyRoomFullState build() { PartyRoomFullState build() {
@ -402,7 +399,6 @@ class PartyRoom extends _$PartyRoom {
await _stopHeartbeat(); await _stopHeartbeat();
await _stopEventStream(); await _stopEventStream();
_reconnectAttempts = 0;
_dismissRoom(); _dismissRoom();
dPrint('[PartyRoom] Left room: $roomUuid'); dPrint('[PartyRoom] Left room: $roomUuid');
@ -426,7 +422,6 @@ class PartyRoom extends _$PartyRoom {
await _stopHeartbeat(); await _stopHeartbeat();
await _stopEventStream(); await _stopEventStream();
_reconnectAttempts = 0;
_dismissRoom(); _dismissRoom();
dPrint('[PartyRoom] Dismissed room: $roomUuid'); dPrint('[PartyRoom] Dismissed room: $roomUuid');
@ -760,106 +755,35 @@ class PartyRoom extends _$PartyRoom {
_eventStreamSubscription = stream.listen( _eventStreamSubscription = stream.listen(
(event) { (event) {
//
_reconnectAttempts = 0;
_handleRoomEvent(event); _handleRoomEvent(event);
}, },
onError: (error) { onError: (error) {
dPrint('[PartyRoom] Event stream error: $error'); dPrint('[PartyRoom] Event stream error: $error');
// //
_scheduleReconnect(roomUuid); state = state.copyWith(room: state.room.copyWith(eventStreamDisconnected: true));
}, },
onDone: () { onDone: () {
dPrint('[PartyRoom] Event stream closed'); dPrint('[PartyRoom] Event stream closed');
// //
_scheduleReconnect(roomUuid); state = state.copyWith(room: state.room.copyWith(eventStreamDisconnected: true));
}, },
); );
//
state = state.copyWith(room: state.room.copyWith(eventStreamDisconnected: false));
dPrint('[PartyRoom] Event stream started'); dPrint('[PartyRoom] Event stream started');
//
_reconnectAttempts = 0;
} catch (e) { } catch (e) {
dPrint('[PartyRoom] StartEventStream error: $e'); dPrint('[PartyRoom] StartEventStream error: $e');
// //
_scheduleReconnect(roomUuid); state = state.copyWith(room: state.room.copyWith(eventStreamDisconnected: true));
} rethrow;
}
///
void _scheduleReconnect(String roomUuid) {
//
if (_disposed || state.room.roomUuid == null || state.room.roomUuid != roomUuid) {
dPrint('[PartyRoom] Skip reconnect: disposed=$_disposed, roomUuid=${state.room.roomUuid}');
return;
}
//
if (_reconnectTimer?.isActive ?? false) {
return;
}
//
if (_reconnectAttempts >= _maxReconnectAttempts) {
dPrint('[PartyRoom] Max reconnect attempts reached ($_maxReconnectAttempts)');
return;
}
_reconnectAttempts++;
dPrint(
'[PartyRoom] Scheduling reconnect attempt $_reconnectAttempts/$_maxReconnectAttempts in ${_reconnectDelay.inSeconds}s',
);
_reconnectTimer = Timer(_reconnectDelay, () async {
await _attemptReconnect(roomUuid);
});
}
///
Future<void> _attemptReconnect(String roomUuid) async {
if (_disposed || state.room.roomUuid == null || state.room.roomUuid != roomUuid) {
dPrint('[PartyRoom] Abort reconnect: no longer in room');
return;
}
try {
dPrint('[PartyRoom] Attempting to reconnect event stream...');
//
final client = state.client.roomClient;
if (client == null) {
dPrint('[PartyRoom] Reconnect failed: client not available');
_scheduleReconnect(roomUuid);
return;
}
// getMyRoom
final response = await client.getMyRoom(partroom.GetMyRoomRequest(), options: _getAuthCallOptions());
if (!response.hasRoom() || response.room.roomUuid != roomUuid) {
dPrint('[PartyRoom] Reconnect failed: no longer in room');
//
await _stopHeartbeat();
await _stopEventStream();
_reconnectAttempts = 0;
_dismissRoom();
return;
}
//
dPrint('[PartyRoom] Still in room, restarting event stream');
await _startEventStream(roomUuid);
} catch (e) {
dPrint('[PartyRoom] Reconnect attempt failed: $e');
//
_scheduleReconnect(roomUuid);
} }
} }
/// ///
Future<void> _stopEventStream() async { Future<void> _stopEventStream() async {
_reconnectTimer?.cancel(); if (_eventStreamSubscription == null) return;
_reconnectTimer = null;
await _eventStreamSubscription?.cancel(); await _eventStreamSubscription?.cancel();
_eventStreamSubscription = null; _eventStreamSubscription = null;
dPrint('[PartyRoom] Event stream stopped'); dPrint('[PartyRoom] Event stream stopped');
@ -948,6 +872,43 @@ class PartyRoom extends _$PartyRoom {
} }
} }
// ========== ==========
///
/// UI
Future<void> refreshRoomAndReconnect() async {
try {
final roomUuid = state.room.roomUuid;
if (roomUuid == null) {
throw Exception('Not in a room');
}
dPrint('[PartyRoom] Refreshing room and reconnecting...');
//
await getRoomInfo(roomUuid);
//
await getRoomMembers(roomUuid);
//
await _startEventStream(roomUuid);
dPrint('[PartyRoom] Room refreshed and reconnected successfully');
} catch (e) {
dPrint('[PartyRoom] Refresh and reconnect error: $e');
rethrow;
}
}
///
void acknowledgeDisconnection() {
state = state.copyWith(room: state.room.copyWith(eventStreamDisconnected: false));
}
///
bool get isEventStreamActive => _eventStreamSubscription != null && !(_eventStreamSubscription?.isPaused ?? true);
// ========== ========== // ========== ==========
/// ///

View File

@ -277,7 +277,7 @@ as DateTime?,
/// @nodoc /// @nodoc
mixin _$PartyRoomState { mixin _$PartyRoomState {
partroom.RoomInfo? get currentRoom; List<partroom.RoomMember> get members; Map<String, common.Tag> get tags; Map<String, common.SignalType> get signalTypes; bool get isInRoom; bool get isOwner; String? get roomUuid; List<partroom.RoomEvent> get recentEvents; partroom.RoomInfo? get currentRoom; List<partroom.RoomMember> get members; Map<String, common.Tag> get tags; Map<String, common.SignalType> get signalTypes; bool get isInRoom; bool get isOwner; String? get roomUuid; List<partroom.RoomEvent> get recentEvents; bool get eventStreamDisconnected;
/// Create a copy of PartyRoomState /// Create a copy of PartyRoomState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false) @JsonKey(includeFromJson: false, includeToJson: false)
@ -288,16 +288,16 @@ $PartyRoomStateCopyWith<PartyRoomState> get copyWith => _$PartyRoomStateCopyWith
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is PartyRoomState&&(identical(other.currentRoom, currentRoom) || other.currentRoom == currentRoom)&&const DeepCollectionEquality().equals(other.members, members)&&const DeepCollectionEquality().equals(other.tags, tags)&&const DeepCollectionEquality().equals(other.signalTypes, signalTypes)&&(identical(other.isInRoom, isInRoom) || other.isInRoom == isInRoom)&&(identical(other.isOwner, isOwner) || other.isOwner == isOwner)&&(identical(other.roomUuid, roomUuid) || other.roomUuid == roomUuid)&&const DeepCollectionEquality().equals(other.recentEvents, recentEvents)); return identical(this, other) || (other.runtimeType == runtimeType&&other is PartyRoomState&&(identical(other.currentRoom, currentRoom) || other.currentRoom == currentRoom)&&const DeepCollectionEquality().equals(other.members, members)&&const DeepCollectionEquality().equals(other.tags, tags)&&const DeepCollectionEquality().equals(other.signalTypes, signalTypes)&&(identical(other.isInRoom, isInRoom) || other.isInRoom == isInRoom)&&(identical(other.isOwner, isOwner) || other.isOwner == isOwner)&&(identical(other.roomUuid, roomUuid) || other.roomUuid == roomUuid)&&const DeepCollectionEquality().equals(other.recentEvents, recentEvents)&&(identical(other.eventStreamDisconnected, eventStreamDisconnected) || other.eventStreamDisconnected == eventStreamDisconnected));
} }
@override @override
int get hashCode => Object.hash(runtimeType,currentRoom,const DeepCollectionEquality().hash(members),const DeepCollectionEquality().hash(tags),const DeepCollectionEquality().hash(signalTypes),isInRoom,isOwner,roomUuid,const DeepCollectionEquality().hash(recentEvents)); int get hashCode => Object.hash(runtimeType,currentRoom,const DeepCollectionEquality().hash(members),const DeepCollectionEquality().hash(tags),const DeepCollectionEquality().hash(signalTypes),isInRoom,isOwner,roomUuid,const DeepCollectionEquality().hash(recentEvents),eventStreamDisconnected);
@override @override
String toString() { String toString() {
return 'PartyRoomState(currentRoom: $currentRoom, members: $members, tags: $tags, signalTypes: $signalTypes, isInRoom: $isInRoom, isOwner: $isOwner, roomUuid: $roomUuid, recentEvents: $recentEvents)'; return 'PartyRoomState(currentRoom: $currentRoom, members: $members, tags: $tags, signalTypes: $signalTypes, isInRoom: $isInRoom, isOwner: $isOwner, roomUuid: $roomUuid, recentEvents: $recentEvents, eventStreamDisconnected: $eventStreamDisconnected)';
} }
@ -308,7 +308,7 @@ abstract mixin class $PartyRoomStateCopyWith<$Res> {
factory $PartyRoomStateCopyWith(PartyRoomState value, $Res Function(PartyRoomState) _then) = _$PartyRoomStateCopyWithImpl; factory $PartyRoomStateCopyWith(PartyRoomState value, $Res Function(PartyRoomState) _then) = _$PartyRoomStateCopyWithImpl;
@useResult @useResult
$Res call({ $Res call({
partroom.RoomInfo? currentRoom, List<partroom.RoomMember> members, Map<String, common.Tag> tags, Map<String, common.SignalType> signalTypes, bool isInRoom, bool isOwner, String? roomUuid, List<partroom.RoomEvent> recentEvents partroom.RoomInfo? currentRoom, List<partroom.RoomMember> members, Map<String, common.Tag> tags, Map<String, common.SignalType> signalTypes, bool isInRoom, bool isOwner, String? roomUuid, List<partroom.RoomEvent> recentEvents, bool eventStreamDisconnected
}); });
@ -325,7 +325,7 @@ class _$PartyRoomStateCopyWithImpl<$Res>
/// Create a copy of PartyRoomState /// Create a copy of PartyRoomState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? currentRoom = freezed,Object? members = null,Object? tags = null,Object? signalTypes = null,Object? isInRoom = null,Object? isOwner = null,Object? roomUuid = freezed,Object? recentEvents = null,}) { @pragma('vm:prefer-inline') @override $Res call({Object? currentRoom = freezed,Object? members = null,Object? tags = null,Object? signalTypes = null,Object? isInRoom = null,Object? isOwner = null,Object? roomUuid = freezed,Object? recentEvents = null,Object? eventStreamDisconnected = null,}) {
return _then(_self.copyWith( return _then(_self.copyWith(
currentRoom: freezed == currentRoom ? _self.currentRoom : currentRoom // ignore: cast_nullable_to_non_nullable currentRoom: freezed == currentRoom ? _self.currentRoom : currentRoom // ignore: cast_nullable_to_non_nullable
as partroom.RoomInfo?,members: null == members ? _self.members : members // ignore: cast_nullable_to_non_nullable as partroom.RoomInfo?,members: null == members ? _self.members : members // ignore: cast_nullable_to_non_nullable
@ -335,7 +335,8 @@ as Map<String, common.SignalType>,isInRoom: null == isInRoom ? _self.isInRoom :
as bool,isOwner: null == isOwner ? _self.isOwner : isOwner // ignore: cast_nullable_to_non_nullable as bool,isOwner: null == isOwner ? _self.isOwner : isOwner // ignore: cast_nullable_to_non_nullable
as bool,roomUuid: freezed == roomUuid ? _self.roomUuid : roomUuid // ignore: cast_nullable_to_non_nullable as bool,roomUuid: freezed == roomUuid ? _self.roomUuid : roomUuid // ignore: cast_nullable_to_non_nullable
as String?,recentEvents: null == recentEvents ? _self.recentEvents : recentEvents // ignore: cast_nullable_to_non_nullable as String?,recentEvents: null == recentEvents ? _self.recentEvents : recentEvents // ignore: cast_nullable_to_non_nullable
as List<partroom.RoomEvent>, as List<partroom.RoomEvent>,eventStreamDisconnected: null == eventStreamDisconnected ? _self.eventStreamDisconnected : eventStreamDisconnected // ignore: cast_nullable_to_non_nullable
as bool,
)); ));
} }
@ -417,10 +418,10 @@ return $default(_that);case _:
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( partroom.RoomInfo? currentRoom, List<partroom.RoomMember> members, Map<String, common.Tag> tags, Map<String, common.SignalType> signalTypes, bool isInRoom, bool isOwner, String? roomUuid, List<partroom.RoomEvent> recentEvents)? $default,{required TResult orElse(),}) {final _that = this; @optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( partroom.RoomInfo? currentRoom, List<partroom.RoomMember> members, Map<String, common.Tag> tags, Map<String, common.SignalType> signalTypes, bool isInRoom, bool isOwner, String? roomUuid, List<partroom.RoomEvent> recentEvents, bool eventStreamDisconnected)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) { switch (_that) {
case _PartyRoomState() when $default != null: case _PartyRoomState() when $default != null:
return $default(_that.currentRoom,_that.members,_that.tags,_that.signalTypes,_that.isInRoom,_that.isOwner,_that.roomUuid,_that.recentEvents);case _: return $default(_that.currentRoom,_that.members,_that.tags,_that.signalTypes,_that.isInRoom,_that.isOwner,_that.roomUuid,_that.recentEvents,_that.eventStreamDisconnected);case _:
return orElse(); return orElse();
} }
@ -438,10 +439,10 @@ return $default(_that.currentRoom,_that.members,_that.tags,_that.signalTypes,_th
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( partroom.RoomInfo? currentRoom, List<partroom.RoomMember> members, Map<String, common.Tag> tags, Map<String, common.SignalType> signalTypes, bool isInRoom, bool isOwner, String? roomUuid, List<partroom.RoomEvent> recentEvents) $default,) {final _that = this; @optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( partroom.RoomInfo? currentRoom, List<partroom.RoomMember> members, Map<String, common.Tag> tags, Map<String, common.SignalType> signalTypes, bool isInRoom, bool isOwner, String? roomUuid, List<partroom.RoomEvent> recentEvents, bool eventStreamDisconnected) $default,) {final _that = this;
switch (_that) { switch (_that) {
case _PartyRoomState(): case _PartyRoomState():
return $default(_that.currentRoom,_that.members,_that.tags,_that.signalTypes,_that.isInRoom,_that.isOwner,_that.roomUuid,_that.recentEvents);} return $default(_that.currentRoom,_that.members,_that.tags,_that.signalTypes,_that.isInRoom,_that.isOwner,_that.roomUuid,_that.recentEvents,_that.eventStreamDisconnected);}
} }
/// A variant of `when` that fallback to returning `null` /// A variant of `when` that fallback to returning `null`
/// ///
@ -455,10 +456,10 @@ return $default(_that.currentRoom,_that.members,_that.tags,_that.signalTypes,_th
/// } /// }
/// ``` /// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( partroom.RoomInfo? currentRoom, List<partroom.RoomMember> members, Map<String, common.Tag> tags, Map<String, common.SignalType> signalTypes, bool isInRoom, bool isOwner, String? roomUuid, List<partroom.RoomEvent> recentEvents)? $default,) {final _that = this; @optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( partroom.RoomInfo? currentRoom, List<partroom.RoomMember> members, Map<String, common.Tag> tags, Map<String, common.SignalType> signalTypes, bool isInRoom, bool isOwner, String? roomUuid, List<partroom.RoomEvent> recentEvents, bool eventStreamDisconnected)? $default,) {final _that = this;
switch (_that) { switch (_that) {
case _PartyRoomState() when $default != null: case _PartyRoomState() when $default != null:
return $default(_that.currentRoom,_that.members,_that.tags,_that.signalTypes,_that.isInRoom,_that.isOwner,_that.roomUuid,_that.recentEvents);case _: return $default(_that.currentRoom,_that.members,_that.tags,_that.signalTypes,_that.isInRoom,_that.isOwner,_that.roomUuid,_that.recentEvents,_that.eventStreamDisconnected);case _:
return null; return null;
} }
@ -470,7 +471,7 @@ return $default(_that.currentRoom,_that.members,_that.tags,_that.signalTypes,_th
class _PartyRoomState implements PartyRoomState { class _PartyRoomState implements PartyRoomState {
const _PartyRoomState({this.currentRoom, final List<partroom.RoomMember> members = const [], final Map<String, common.Tag> tags = const {}, final Map<String, common.SignalType> signalTypes = const {}, this.isInRoom = false, this.isOwner = false, this.roomUuid, final List<partroom.RoomEvent> recentEvents = const []}): _members = members,_tags = tags,_signalTypes = signalTypes,_recentEvents = recentEvents; const _PartyRoomState({this.currentRoom, final List<partroom.RoomMember> members = const [], final Map<String, common.Tag> tags = const {}, final Map<String, common.SignalType> signalTypes = const {}, this.isInRoom = false, this.isOwner = false, this.roomUuid, final List<partroom.RoomEvent> recentEvents = const [], this.eventStreamDisconnected = false}): _members = members,_tags = tags,_signalTypes = signalTypes,_recentEvents = recentEvents;
@override final partroom.RoomInfo? currentRoom; @override final partroom.RoomInfo? currentRoom;
@ -505,6 +506,7 @@ class _PartyRoomState implements PartyRoomState {
return EqualUnmodifiableListView(_recentEvents); return EqualUnmodifiableListView(_recentEvents);
} }
@override@JsonKey() final bool eventStreamDisconnected;
/// Create a copy of PartyRoomState /// Create a copy of PartyRoomState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -516,16 +518,16 @@ _$PartyRoomStateCopyWith<_PartyRoomState> get copyWith => __$PartyRoomStateCopyW
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PartyRoomState&&(identical(other.currentRoom, currentRoom) || other.currentRoom == currentRoom)&&const DeepCollectionEquality().equals(other._members, _members)&&const DeepCollectionEquality().equals(other._tags, _tags)&&const DeepCollectionEquality().equals(other._signalTypes, _signalTypes)&&(identical(other.isInRoom, isInRoom) || other.isInRoom == isInRoom)&&(identical(other.isOwner, isOwner) || other.isOwner == isOwner)&&(identical(other.roomUuid, roomUuid) || other.roomUuid == roomUuid)&&const DeepCollectionEquality().equals(other._recentEvents, _recentEvents)); return identical(this, other) || (other.runtimeType == runtimeType&&other is _PartyRoomState&&(identical(other.currentRoom, currentRoom) || other.currentRoom == currentRoom)&&const DeepCollectionEquality().equals(other._members, _members)&&const DeepCollectionEquality().equals(other._tags, _tags)&&const DeepCollectionEquality().equals(other._signalTypes, _signalTypes)&&(identical(other.isInRoom, isInRoom) || other.isInRoom == isInRoom)&&(identical(other.isOwner, isOwner) || other.isOwner == isOwner)&&(identical(other.roomUuid, roomUuid) || other.roomUuid == roomUuid)&&const DeepCollectionEquality().equals(other._recentEvents, _recentEvents)&&(identical(other.eventStreamDisconnected, eventStreamDisconnected) || other.eventStreamDisconnected == eventStreamDisconnected));
} }
@override @override
int get hashCode => Object.hash(runtimeType,currentRoom,const DeepCollectionEquality().hash(_members),const DeepCollectionEquality().hash(_tags),const DeepCollectionEquality().hash(_signalTypes),isInRoom,isOwner,roomUuid,const DeepCollectionEquality().hash(_recentEvents)); int get hashCode => Object.hash(runtimeType,currentRoom,const DeepCollectionEquality().hash(_members),const DeepCollectionEquality().hash(_tags),const DeepCollectionEquality().hash(_signalTypes),isInRoom,isOwner,roomUuid,const DeepCollectionEquality().hash(_recentEvents),eventStreamDisconnected);
@override @override
String toString() { String toString() {
return 'PartyRoomState(currentRoom: $currentRoom, members: $members, tags: $tags, signalTypes: $signalTypes, isInRoom: $isInRoom, isOwner: $isOwner, roomUuid: $roomUuid, recentEvents: $recentEvents)'; return 'PartyRoomState(currentRoom: $currentRoom, members: $members, tags: $tags, signalTypes: $signalTypes, isInRoom: $isInRoom, isOwner: $isOwner, roomUuid: $roomUuid, recentEvents: $recentEvents, eventStreamDisconnected: $eventStreamDisconnected)';
} }
@ -536,7 +538,7 @@ abstract mixin class _$PartyRoomStateCopyWith<$Res> implements $PartyRoomStateCo
factory _$PartyRoomStateCopyWith(_PartyRoomState value, $Res Function(_PartyRoomState) _then) = __$PartyRoomStateCopyWithImpl; factory _$PartyRoomStateCopyWith(_PartyRoomState value, $Res Function(_PartyRoomState) _then) = __$PartyRoomStateCopyWithImpl;
@override @useResult @override @useResult
$Res call({ $Res call({
partroom.RoomInfo? currentRoom, List<partroom.RoomMember> members, Map<String, common.Tag> tags, Map<String, common.SignalType> signalTypes, bool isInRoom, bool isOwner, String? roomUuid, List<partroom.RoomEvent> recentEvents partroom.RoomInfo? currentRoom, List<partroom.RoomMember> members, Map<String, common.Tag> tags, Map<String, common.SignalType> signalTypes, bool isInRoom, bool isOwner, String? roomUuid, List<partroom.RoomEvent> recentEvents, bool eventStreamDisconnected
}); });
@ -553,7 +555,7 @@ class __$PartyRoomStateCopyWithImpl<$Res>
/// Create a copy of PartyRoomState /// Create a copy of PartyRoomState
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? currentRoom = freezed,Object? members = null,Object? tags = null,Object? signalTypes = null,Object? isInRoom = null,Object? isOwner = null,Object? roomUuid = freezed,Object? recentEvents = null,}) { @override @pragma('vm:prefer-inline') $Res call({Object? currentRoom = freezed,Object? members = null,Object? tags = null,Object? signalTypes = null,Object? isInRoom = null,Object? isOwner = null,Object? roomUuid = freezed,Object? recentEvents = null,Object? eventStreamDisconnected = null,}) {
return _then(_PartyRoomState( return _then(_PartyRoomState(
currentRoom: freezed == currentRoom ? _self.currentRoom : currentRoom // ignore: cast_nullable_to_non_nullable currentRoom: freezed == currentRoom ? _self.currentRoom : currentRoom // ignore: cast_nullable_to_non_nullable
as partroom.RoomInfo?,members: null == members ? _self._members : members // ignore: cast_nullable_to_non_nullable as partroom.RoomInfo?,members: null == members ? _self._members : members // ignore: cast_nullable_to_non_nullable
@ -563,7 +565,8 @@ as Map<String, common.SignalType>,isInRoom: null == isInRoom ? _self.isInRoom :
as bool,isOwner: null == isOwner ? _self.isOwner : isOwner // ignore: cast_nullable_to_non_nullable as bool,isOwner: null == isOwner ? _self.isOwner : isOwner // ignore: cast_nullable_to_non_nullable
as bool,roomUuid: freezed == roomUuid ? _self.roomUuid : roomUuid // ignore: cast_nullable_to_non_nullable as bool,roomUuid: freezed == roomUuid ? _self.roomUuid : roomUuid // ignore: cast_nullable_to_non_nullable
as String?,recentEvents: null == recentEvents ? _self._recentEvents : recentEvents // ignore: cast_nullable_to_non_nullable as String?,recentEvents: null == recentEvents ? _self._recentEvents : recentEvents // ignore: cast_nullable_to_non_nullable
as List<partroom.RoomEvent>, as List<partroom.RoomEvent>,eventStreamDisconnected: null == eventStreamDisconnected ? _self.eventStreamDisconnected : eventStreamDisconnected // ignore: cast_nullable_to_non_nullable
as bool,
)); ));
} }

View File

@ -44,7 +44,7 @@ final class PartyRoomProvider
} }
} }
String _$partyRoomHash() => r'd7182854c8caf5bb362c45a4e6e2ab40c2ef1b09'; String _$partyRoomHash() => r'5640c173d0820c681f3bc68872a2ab4f2fa29285';
/// PartyRoom Provider /// PartyRoom Provider

View File

@ -1,23 +1,28 @@
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:starcitizen_doctor/common/utils/base_utils.dart';
import 'package:starcitizen_doctor/generated/proto/partroom/partroom.pb.dart' as partroom;
import 'package:starcitizen_doctor/provider/party_room.dart'; import 'package:starcitizen_doctor/provider/party_room.dart';
/// /// /
class CreateRoomDialog extends HookConsumerWidget { class CreateRoomDialog extends HookConsumerWidget {
const CreateRoomDialog({super.key}); final partroom.RoomInfo? roomInfo;
const CreateRoomDialog({super.key, this.roomInfo});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final partyRoomState = ref.watch(partyRoomProvider); final partyRoomState = ref.watch(partyRoomProvider);
final partyRoom = ref.read(partyRoomProvider.notifier); final partyRoom = ref.read(partyRoomProvider.notifier);
final isEdit = roomInfo != null;
final selectedMainTag = useState<String?>(null); final selectedMainTag = useState<String?>(roomInfo?.mainTagId);
final selectedSubTag = useState<String?>(null); final selectedSubTag = useState<String?>(roomInfo?.subTagId);
final targetMembersController = useTextEditingController(text: '6'); final targetMembersController = useTextEditingController(text: roomInfo?.targetMembers.toString() ?? '6');
final hasPassword = useState(false); final hasPassword = useState(roomInfo?.hasPassword ?? false);
final passwordController = useTextEditingController(); final passwordController = useTextEditingController();
final socialLinksController = useTextEditingController(); final socialLinksController = useTextEditingController(text: roomInfo?.socialLinks.join('\n'));
final isCreating = useState(false); final isCreating = useState(false);
// //
@ -25,7 +30,7 @@ class CreateRoomDialog extends HookConsumerWidget {
return ContentDialog( return ContentDialog(
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.5), constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.5),
title: const Text('创建房间'), title: Text(isEdit ? '编辑房间' : '创建房间'),
content: SizedBox( content: SizedBox(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
@ -135,34 +140,37 @@ class CreateRoomDialog extends HookConsumerWidget {
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
if (!isEdit) ...[
Row( Row(
children: [ children: [
Checkbox( Checkbox(
checked: hasPassword.value, checked: hasPassword.value,
onChanged: (value) { onChanged: (value) {
hasPassword.value = value ?? false; hasPassword.value = value ?? false;
}, },
content: const Text('设置密码'), content: const Text('设置密码'),
), ),
], ],
),
const SizedBox(height: 8),
// -
InfoLabel(
label: '房间密码',
child: TextBox(
controller: passwordController,
placeholder: hasPassword.value ? '输入密码' : '未启用密码',
obscureText: hasPassword.value,
maxLines: 1,
maxLength: 12,
enabled: hasPassword.value,
), ),
), const SizedBox(height: 8),
const SizedBox(height: 16), // -
InfoLabel(
label: '房间密码',
child: TextBox(
controller: passwordController,
placeholder: isEdit
? "为空则不更新密码,取消勾选则取消密码"
: hasPassword.value
? '输入密码'
: '未启用密码',
obscureText: hasPassword.value,
maxLines: 1,
maxLength: 12,
enabled: hasPassword.value,
),
),
const SizedBox(height: 16),
],
InfoLabel( InfoLabel(
label: '社交链接 (可选)', label: '社交链接 (可选)',
child: TextBox( child: TextBox(
@ -207,32 +215,50 @@ class CreateRoomDialog extends HookConsumerWidget {
} }
if (hasPassword.value && passwordController.text.trim().isEmpty) { if (hasPassword.value && passwordController.text.trim().isEmpty) {
await showDialog( if (!isEdit) {
context: context, await showDialog(
builder: (context) => ContentDialog( context: context,
title: const Text('提示'), builder: (context) => ContentDialog(
content: const Text('请输入密码'), title: const Text('提示'),
actions: [FilledButton(child: const Text('确定'), onPressed: () => Navigator.pop(context))], content: const Text('请输入密码'),
), actions: [FilledButton(child: const Text('确定'), onPressed: () => Navigator.pop(context))],
); ),
);
return;
}
}
final socialLinks = socialLinksController.text.split('\n');
// https
final invalidLinks = socialLinks.where((link) => !link.startsWith('https://')).toList();
if (invalidLinks.isNotEmpty) {
showToast(context, "链接格式错误!");
return; return;
} }
final socialLinks = socialLinksController.text
.split('\n')
.where((link) => link.trim().isNotEmpty && link.trim().startsWith('http'))
.toList();
isCreating.value = true; isCreating.value = true;
try { try {
await partyRoom.createRoom( if (isEdit) {
mainTagId: mainTagId, await partyRoom.updateRoom(
subTagId: selectedSubTag.value, mainTagId: mainTagId,
targetMembers: targetMembers, subTagId: selectedSubTag.value,
hasPassword: hasPassword.value, targetMembers: targetMembers,
password: hasPassword.value ? passwordController.text : null, password: !hasPassword.value
socialLinks: socialLinks.isEmpty ? null : socialLinks, ? ''
); : (passwordController.text.isNotEmpty ? passwordController.text : null),
socialLinks: socialLinks,
);
} else {
await partyRoom.createRoom(
mainTagId: mainTagId,
subTagId: selectedSubTag.value,
targetMembers: targetMembers,
hasPassword: hasPassword.value,
password: hasPassword.value ? passwordController.text : null,
socialLinks: socialLinks.isEmpty ? null : socialLinks,
);
}
if (context.mounted) { if (context.mounted) {
Navigator.pop(context); Navigator.pop(context);
@ -243,7 +269,7 @@ class CreateRoomDialog extends HookConsumerWidget {
await showDialog( await showDialog(
context: context, context: context,
builder: (context) => ContentDialog( builder: (context) => ContentDialog(
title: const Text('创建失败'), title: Text(isEdit ? '更新失败' : '创建失败'),
content: Text(e.toString()), content: Text(e.toString()),
actions: [FilledButton(child: const Text('确定'), onPressed: () => Navigator.pop(context))], actions: [FilledButton(child: const Text('确定'), onPressed: () => Navigator.pop(context))],
), ),
@ -253,7 +279,7 @@ class CreateRoomDialog extends HookConsumerWidget {
}, },
child: isCreating.value child: isCreating.value
? const SizedBox(width: 16, height: 16, child: ProgressRing(strokeWidth: 2)) ? const SizedBox(width: 16, height: 16, child: ProgressRing(strokeWidth: 2))
: const Text('创建'), : Text(isEdit ? '保存' : '创建'),
), ),
Button(onPressed: isCreating.value ? null : () => Navigator.pop(context), child: const Text('取消')), Button(onPressed: isCreating.value ? null : () => Navigator.pop(context), child: const Text('取消')),
], ],

View File

@ -1,5 +1,7 @@
import 'package:fluent_ui/fluent_ui.dart'; import 'package:fluent_ui/fluent_ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:starcitizen_doctor/common/utils/base_utils.dart';
import 'package:starcitizen_doctor/common/utils/log.dart';
import 'package:starcitizen_doctor/provider/party_room.dart'; import 'package:starcitizen_doctor/provider/party_room.dart';
import 'package:starcitizen_doctor/ui/party_room/widgets/detail/party_room_message_list.dart'; import 'package:starcitizen_doctor/ui/party_room/widgets/detail/party_room_message_list.dart';
@ -18,6 +20,7 @@ class PartyRoomDetailPage extends ConsumerStatefulWidget {
class _PartyRoomDetailPageState extends ConsumerState<PartyRoomDetailPage> { class _PartyRoomDetailPageState extends ConsumerState<PartyRoomDetailPage> {
final ScrollController _scrollController = ScrollController(); final ScrollController _scrollController = ScrollController();
int _lastEventCount = 0; int _lastEventCount = 0;
bool _isShowingDialog = false;
@override @override
void dispose() { void dispose() {
@ -25,6 +28,59 @@ class _PartyRoomDetailPageState extends ConsumerState<PartyRoomDetailPage> {
super.dispose(); super.dispose();
} }
Future<void> _showReconnectDialog() async {
if (_isShowingDialog || !mounted) return;
_isShowingDialog = true;
final partyRoom = ref.read(partyRoomProvider.notifier);
final result = await showBaseDialog(
context,
title: '连接已断开',
content: const Text('与房间服务器的连接已断开,是否重新连接?'),
actions: [
Button(
onPressed: () => Navigator.of(context).pop('leave'),
child: const Padding(padding: EdgeInsets.only(top: 2, bottom: 2, left: 8, right: 8), child: Text('退出房间')),
),
FilledButton(
onPressed: () => Navigator.of(context).pop('reconnect'),
child: const Padding(padding: EdgeInsets.only(top: 2, bottom: 2, left: 8, right: 8), child: Text('重新连接')),
),
],
);
_isShowingDialog = false;
if (!mounted) return;
if (result == 'reconnect') {
try {
await partyRoom.refreshRoomAndReconnect();
dPrint('[PartyRoomDetailPage] Reconnect success');
} catch (e) {
dPrint('[PartyRoomDetailPage] Reconnect failed: $e');
//
if (mounted) {
_showReconnectDialog();
}
}
} else if (result == 'leave') {
try {
partyRoom.acknowledgeDisconnection();
await partyRoom.leaveRoom();
} catch (e) {
dPrint('[PartyRoomDetailPage] Leave room failed: $e');
if (mounted) {
await showToast(context, '退出房间失败: $e');
}
}
} else {
//
partyRoom.acknowledgeDisconnection();
}
}
void _scrollToBottom() { void _scrollToBottom() {
if (_scrollController.hasClients) { if (_scrollController.hasClients) {
Future.delayed(const Duration(milliseconds: 100), () { Future.delayed(const Duration(milliseconds: 100), () {
@ -41,6 +97,17 @@ class _PartyRoomDetailPageState extends ConsumerState<PartyRoomDetailPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
ref.listen<PartyRoomFullState>(partyRoomProvider, (previous, next) {
//
if (next.room.isInRoom && next.room.eventStreamDisconnected && !_isShowingDialog) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_showReconnectDialog();
}
});
}
});
final partyRoomState = ref.watch(partyRoomProvider); final partyRoomState = ref.watch(partyRoomProvider);
final partyRoom = ref.read(partyRoomProvider.notifier); final partyRoom = ref.read(partyRoomProvider.notifier);
final room = partyRoomState.room.currentRoom; final room = partyRoomState.room.currentRoom;

View File

@ -2,6 +2,7 @@ import 'package:fluent_ui/fluent_ui.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:starcitizen_doctor/provider/party_room.dart'; import 'package:starcitizen_doctor/provider/party_room.dart';
import 'package:starcitizen_doctor/ui/party_room/party_room_ui_model.dart'; import 'package:starcitizen_doctor/ui/party_room/party_room_ui_model.dart';
import 'package:starcitizen_doctor/ui/party_room/widgets/create_room_dialog.dart';
/// ///
class PartyRoomHeader extends ConsumerWidget { class PartyRoomHeader extends ConsumerWidget {
@ -58,39 +59,54 @@ class PartyRoomHeader extends ConsumerWidget {
), ),
if (isOwner) ...[ if (isOwner) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
SizedBox( Row(
width: double.infinity, children: [
child: Button( Expanded(
onPressed: () async { child: Button(
final confirmed = await showDialog<bool>( onPressed: () {
context: context, showDialog(
builder: (context) => ContentDialog( context: context,
title: const Text('确认解散'), builder: (context) => CreateRoomDialog(roomInfo: room),
content: const Text('确定要解散房间吗?所有成员将被移出。'), );
actions: [ },
Button(child: const Text('取消'), onPressed: () => Navigator.pop(context, false)), child: const Text('编辑房间'),
FilledButton( ),
style: ButtonStyle(backgroundColor: WidgetStateProperty.all(const Color(0xFFDA373C))),
child: const Text('解散', style: TextStyle(color: Colors.white)),
onPressed: () => Navigator.pop(context, true),
),
],
),
);
if (confirmed == true) {
ref.read(partyRoomUIModelProvider.notifier).dismissRoom();
}
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.resolveWith((state) {
if (state.isHovered || state.isPressed) {
return const Color(0xFFB3261E);
}
return const Color(0xFFDA373C);
}),
), ),
child: const Text('解散房间', style: TextStyle(color: Colors.white)), const SizedBox(width: 8),
), Expanded(
child: Button(
onPressed: () async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => ContentDialog(
title: const Text('确认解散'),
content: const Text('确定要解散房间吗?所有成员将被移出。'),
actions: [
Button(child: const Text('取消'), onPressed: () => Navigator.pop(context, false)),
FilledButton(
style: ButtonStyle(backgroundColor: WidgetStateProperty.all(const Color(0xFFDA373C))),
child: const Text('解散', style: TextStyle(color: Colors.white)),
onPressed: () => Navigator.pop(context, true),
),
],
),
);
if (confirmed == true) {
ref.read(partyRoomUIModelProvider.notifier).dismissRoom();
}
},
style: ButtonStyle(
backgroundColor: WidgetStateProperty.resolveWith((state) {
if (state.isHovered || state.isPressed) {
return const Color(0xFFB3261E);
}
return const Color(0xFFDA373C);
}),
),
child: const Text('解散房间', style: TextStyle(color: Colors.white)),
),
),
],
), ),
] else ...[ ] else ...[
const SizedBox(height: 8), const SizedBox(height: 8),
@ -100,8 +116,21 @@ class PartyRoomHeader extends ConsumerWidget {
onPressed: () async { onPressed: () async {
await partyRoom.leaveRoom(); await partyRoom.leaveRoom();
}, },
style: ButtonStyle(backgroundColor: WidgetStateProperty.all(const Color(0xFF404249))), style: ButtonStyle(
child: const Text('离开房间', style: TextStyle(color: Color(0xFFB5BAC1))), backgroundColor: WidgetStateProperty.resolveWith((state) {
if (state.isHovered || state.isPressed) {
return const Color(0xFFDA373C);
}
return const Color(0xFF404249);
}),
foregroundColor: WidgetStateProperty.resolveWith((state) {
if (state.isHovered || state.isPressed) {
return Colors.white;
}
return const Color(0xFFDBDEE1);
}),
),
child: const Text('离开房间'),
), ),
), ),
], ],

View File

@ -430,6 +430,7 @@ class PartyRoomListPage extends HookConsumerWidget {
Future<void> _showCreateRoomDialog(BuildContext context, WidgetRef ref) async { Future<void> _showCreateRoomDialog(BuildContext context, WidgetRef ref) async {
final uiState = ref.read(partyRoomUIModelProvider); final uiState = ref.read(partyRoomUIModelProvider);
final partyRoomState = ref.read(partyRoomProvider);
// //
if (uiState.isGuestMode) { if (uiState.isGuestMode) {
@ -451,6 +452,26 @@ class PartyRoomListPage extends HookConsumerWidget {
return; return;
} }
//
if (partyRoomState.room.isInRoom) {
final shouldLeave = await showDialog<bool>(
context: context,
builder: (context) => ContentDialog(
title: const Text('创建新房间'),
content: const Text('你已经在其他房间中,创建新房间将自动退出当前房间。是否继续?'),
actions: [
Button(child: const Text('取消'), onPressed: () => Navigator.pop(context, false)),
FilledButton(child: const Text('继续'), onPressed: () => Navigator.pop(context, true)),
],
),
);
if (shouldLeave != true) return;
// 退
await ref.read(partyRoomProvider.notifier).leaveRoom();
}
if (!context.mounted) return;
await showDialog(context: context, builder: (context) => const CreateRoomDialog()); await showDialog(context: context, builder: (context) => const CreateRoomDialog());
} }

View File

@ -41,7 +41,7 @@ final class SettingsUIModelProvider
} }
} }
String _$settingsUIModelHash() => r'5c08c56bf5464ef44bee8edb8c18c08d4217f135'; String _$settingsUIModelHash() => r'd19104d924f018a9230548d0372692fc344adacd';
abstract class _$SettingsUIModel extends $Notifier<SettingsUIState> { abstract class _$SettingsUIModel extends $Notifier<SettingsUIState> {
SettingsUIState build(); SettingsUIState build();