mirror of
https://github.com/StarCitizenToolBox/app.git
synced 2026-01-13 19:50:28 +00:00
feat: update UI
This commit is contained in:
parent
0983ebe21f
commit
a6a5a117bc
@ -33,8 +33,8 @@ sealed class PartyRoomState with _$PartyRoomState {
|
|||||||
const factory PartyRoomState({
|
const factory PartyRoomState({
|
||||||
partroom.RoomInfo? currentRoom,
|
partroom.RoomInfo? currentRoom,
|
||||||
@Default([]) List<partroom.RoomMember> members,
|
@Default([]) List<partroom.RoomMember> members,
|
||||||
@Default([]) List<common.Tag> tags,
|
@Default({}) Map<String, common.Tag> tags,
|
||||||
@Default([]) List<common.SignalType> signalTypes,
|
@Default({}) Map<String, common.SignalType> signalTypes,
|
||||||
@Default(false) bool isInRoom,
|
@Default(false) bool isInRoom,
|
||||||
@Default(false) bool isOwner,
|
@Default(false) bool isOwner,
|
||||||
String? roomUuid,
|
String? roomUuid,
|
||||||
@ -249,10 +249,9 @@ class PartyRoom extends _$PartyRoom {
|
|||||||
// 清除本地认证信息
|
// 清除本地认证信息
|
||||||
await _confBox?.delete(_secretKeyKey);
|
await _confBox?.delete(_secretKeyKey);
|
||||||
|
|
||||||
state = state.copyWith(
|
_dismissRoom();
|
||||||
auth: state.auth.copyWith(secretKey: '', isLoggedIn: false, userInfo: null),
|
|
||||||
room: const PartyRoomState(),
|
state = state.copyWith(auth: state.auth.copyWith(secretKey: '', isLoggedIn: false, userInfo: null));
|
||||||
);
|
|
||||||
|
|
||||||
dPrint('[PartyRoom] Unregistered successfully');
|
dPrint('[PartyRoom] Unregistered successfully');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -273,13 +272,15 @@ class PartyRoom extends _$PartyRoom {
|
|||||||
final response = await commonClient.getTags(common.GetTagsRequest());
|
final response = await commonClient.getTags(common.GetTagsRequest());
|
||||||
final signalTypesResponse = await commonClient.getSignalTypes(common.GetSignalTypesRequest());
|
final signalTypesResponse = await commonClient.getSignalTypes(common.GetSignalTypesRequest());
|
||||||
|
|
||||||
|
// 转换为 Map
|
||||||
|
final tagsMap = {for (var tag in response.tags) tag.id: tag};
|
||||||
|
final signalTypesMap = {for (var signal in signalTypesResponse.signals) signal.id: signal};
|
||||||
|
|
||||||
state = state.copyWith(
|
state = state.copyWith(
|
||||||
room: state.room.copyWith(tags: response.tags, signalTypes: signalTypesResponse.signals),
|
room: state.room.copyWith(tags: tagsMap, signalTypes: signalTypesMap),
|
||||||
);
|
);
|
||||||
|
|
||||||
dPrint(
|
dPrint('[PartyRoom] Tags and SignalTypes loaded: ${tagsMap.length} tags, ${signalTypesMap.length} signal types');
|
||||||
'[PartyRoom] Tags and SignalTypes loaded: ${response.tags.length} tags, ${signalTypesResponse.signals.length} signal types',
|
|
||||||
);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dPrint('[PartyRoom] LoadTags error: $e');
|
dPrint('[PartyRoom] LoadTags error: $e');
|
||||||
rethrow;
|
rethrow;
|
||||||
@ -397,7 +398,7 @@ class PartyRoom extends _$PartyRoom {
|
|||||||
await _stopHeartbeat();
|
await _stopHeartbeat();
|
||||||
await _stopEventStream();
|
await _stopEventStream();
|
||||||
|
|
||||||
state = state.copyWith(room: const PartyRoomState());
|
_dismissRoom();
|
||||||
|
|
||||||
dPrint('[PartyRoom] Left room: $roomUuid');
|
dPrint('[PartyRoom] Left room: $roomUuid');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -420,7 +421,7 @@ class PartyRoom extends _$PartyRoom {
|
|||||||
await _stopHeartbeat();
|
await _stopHeartbeat();
|
||||||
await _stopEventStream();
|
await _stopEventStream();
|
||||||
|
|
||||||
state = state.copyWith(room: const PartyRoomState());
|
_dismissRoom();
|
||||||
|
|
||||||
dPrint('[PartyRoom] Dismissed room: $roomUuid');
|
dPrint('[PartyRoom] Dismissed room: $roomUuid');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -616,9 +617,8 @@ class PartyRoom extends _$PartyRoom {
|
|||||||
if (roomUuid == null) return;
|
if (roomUuid == null) return;
|
||||||
|
|
||||||
// 验证信号类型是否有效
|
// 验证信号类型是否有效
|
||||||
final validSignalIds = state.room.signalTypes.map((s) => s.id).toList();
|
if (state.room.signalTypes.isNotEmpty && !state.room.signalTypes.containsKey(signalId)) {
|
||||||
if (validSignalIds.isNotEmpty && !validSignalIds.contains(signalId)) {
|
throw Exception('Invalid signal ID: $signalId. Valid IDs: ${state.room.signalTypes.keys.join(", ")}');
|
||||||
throw Exception('Invalid signal ID: $signalId. Valid IDs: ${validSignalIds.join(", ")}');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final request = partroom.SendSignalRequest(roomUuid: roomUuid, signalId: signalId);
|
final request = partroom.SendSignalRequest(roomUuid: roomUuid, signalId: signalId);
|
||||||
@ -822,7 +822,7 @@ class PartyRoom extends _$PartyRoom {
|
|||||||
// 房间被解散
|
// 房间被解散
|
||||||
_stopHeartbeat();
|
_stopHeartbeat();
|
||||||
_stopEventStream();
|
_stopEventStream();
|
||||||
state = state.copyWith(room: const PartyRoomState());
|
_dismissRoom();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case partroom.RoomEventType.SIGNAL_BROADCAST:
|
case partroom.RoomEventType.SIGNAL_BROADCAST:
|
||||||
@ -877,6 +877,13 @@ class PartyRoom extends _$PartyRoom {
|
|||||||
|
|
||||||
// ========== 清理 ==========
|
// ========== 清理 ==========
|
||||||
|
|
||||||
|
/// 重置房间状态(保留 tags 和 signalTypes)
|
||||||
|
void _dismissRoom() {
|
||||||
|
state = state.copyWith(
|
||||||
|
room: PartyRoomState(tags: state.room.tags, signalTypes: state.room.signalTypes),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _cleanup() {
|
void _cleanup() {
|
||||||
_stopHeartbeat();
|
_stopHeartbeat();
|
||||||
_stopEventStream();
|
_stopEventStream();
|
||||||
|
|||||||
@ -277,7 +277,7 @@ as DateTime?,
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$PartyRoomState {
|
mixin _$PartyRoomState {
|
||||||
|
|
||||||
partroom.RoomInfo? get currentRoom; List<partroom.RoomMember> get members; List<common.Tag> get tags; List<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;
|
||||||
/// 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)
|
||||||
@ -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, List<common.Tag> tags, List<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
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -330,8 +330,8 @@ class _$PartyRoomStateCopyWithImpl<$Res>
|
|||||||
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
|
||||||
as List<partroom.RoomMember>,tags: null == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable
|
as List<partroom.RoomMember>,tags: null == tags ? _self.tags : tags // ignore: cast_nullable_to_non_nullable
|
||||||
as List<common.Tag>,signalTypes: null == signalTypes ? _self.signalTypes : signalTypes // ignore: cast_nullable_to_non_nullable
|
as Map<String, common.Tag>,signalTypes: null == signalTypes ? _self.signalTypes : signalTypes // ignore: cast_nullable_to_non_nullable
|
||||||
as List<common.SignalType>,isInRoom: null == isInRoom ? _self.isInRoom : isInRoom // ignore: cast_nullable_to_non_nullable
|
as Map<String, common.SignalType>,isInRoom: null == isInRoom ? _self.isInRoom : isInRoom // ignore: cast_nullable_to_non_nullable
|
||||||
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
|
||||||
@ -417,7 +417,7 @@ return $default(_that);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( partroom.RoomInfo? currentRoom, List<partroom.RoomMember> members, List<common.Tag> tags, List<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)? $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);case _:
|
||||||
@ -438,7 +438,7 @@ 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, List<common.Tag> tags, List<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) $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);}
|
||||||
@ -455,7 +455,7 @@ 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, List<common.Tag> tags, List<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)? $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);case _:
|
||||||
@ -470,7 +470,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 List<common.Tag> tags = const [], final List<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 []}): _members = members,_tags = tags,_signalTypes = signalTypes,_recentEvents = recentEvents;
|
||||||
|
|
||||||
|
|
||||||
@override final partroom.RoomInfo? currentRoom;
|
@override final partroom.RoomInfo? currentRoom;
|
||||||
@ -481,18 +481,18 @@ class _PartyRoomState implements PartyRoomState {
|
|||||||
return EqualUnmodifiableListView(_members);
|
return EqualUnmodifiableListView(_members);
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<common.Tag> _tags;
|
final Map<String, common.Tag> _tags;
|
||||||
@override@JsonKey() List<common.Tag> get tags {
|
@override@JsonKey() Map<String, common.Tag> get tags {
|
||||||
if (_tags is EqualUnmodifiableListView) return _tags;
|
if (_tags is EqualUnmodifiableMapView) return _tags;
|
||||||
// ignore: implicit_dynamic_type
|
// ignore: implicit_dynamic_type
|
||||||
return EqualUnmodifiableListView(_tags);
|
return EqualUnmodifiableMapView(_tags);
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<common.SignalType> _signalTypes;
|
final Map<String, common.SignalType> _signalTypes;
|
||||||
@override@JsonKey() List<common.SignalType> get signalTypes {
|
@override@JsonKey() Map<String, common.SignalType> get signalTypes {
|
||||||
if (_signalTypes is EqualUnmodifiableListView) return _signalTypes;
|
if (_signalTypes is EqualUnmodifiableMapView) return _signalTypes;
|
||||||
// ignore: implicit_dynamic_type
|
// ignore: implicit_dynamic_type
|
||||||
return EqualUnmodifiableListView(_signalTypes);
|
return EqualUnmodifiableMapView(_signalTypes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override@JsonKey() final bool isInRoom;
|
@override@JsonKey() final bool isInRoom;
|
||||||
@ -536,7 +536,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, List<common.Tag> tags, List<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
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -558,8 +558,8 @@ class __$PartyRoomStateCopyWithImpl<$Res>
|
|||||||
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
|
||||||
as List<partroom.RoomMember>,tags: null == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable
|
as List<partroom.RoomMember>,tags: null == tags ? _self._tags : tags // ignore: cast_nullable_to_non_nullable
|
||||||
as List<common.Tag>,signalTypes: null == signalTypes ? _self._signalTypes : signalTypes // ignore: cast_nullable_to_non_nullable
|
as Map<String, common.Tag>,signalTypes: null == signalTypes ? _self._signalTypes : signalTypes // ignore: cast_nullable_to_non_nullable
|
||||||
as List<common.SignalType>,isInRoom: null == isInRoom ? _self.isInRoom : isInRoom // ignore: cast_nullable_to_non_nullable
|
as Map<String, common.SignalType>,isInRoom: null == isInRoom ? _self.isInRoom : isInRoom // ignore: cast_nullable_to_non_nullable
|
||||||
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
|
||||||
|
|||||||
@ -44,7 +44,7 @@ final class PartyRoomProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$partyRoomHash() => r'2c521709721292458d5459359cac376f123ec226';
|
String _$partyRoomHash() => r'f427838c330942d59faf614f420236dc5a699381';
|
||||||
|
|
||||||
/// PartyRoom Provider
|
/// PartyRoom Provider
|
||||||
|
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
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:local_hero/local_hero.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/party_room_connect_page.dart';
|
import 'package:starcitizen_doctor/ui/party_room/widgets/party_room_connect_page.dart';
|
||||||
import 'package:starcitizen_doctor/ui/party_room/widgets/party_room_list_page.dart';
|
import 'package:starcitizen_doctor/ui/party_room/widgets/party_room_list_page.dart';
|
||||||
import 'package:starcitizen_doctor/ui/party_room/widgets/party_room_detail_page.dart';
|
import 'package:starcitizen_doctor/ui/party_room/widgets/detail/party_room_detail_page.dart';
|
||||||
import 'package:starcitizen_doctor/ui/party_room/widgets/party_room_register_page.dart';
|
import 'package:starcitizen_doctor/ui/party_room/widgets/party_room_register_page.dart';
|
||||||
|
|
||||||
class PartyRoomUI extends HookConsumerWidget {
|
class PartyRoomUI extends HookConsumerWidget {
|
||||||
@ -13,20 +14,27 @@ class PartyRoomUI extends HookConsumerWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final partyRoomState = ref.watch(partyRoomProvider);
|
final partyRoomState = ref.watch(partyRoomProvider);
|
||||||
ref.watch(partyRoomUIModelProvider.select((_) => null));
|
final uiState = ref.watch(partyRoomUIModelProvider);
|
||||||
|
|
||||||
|
Widget widget = const PartyRoomListPage();
|
||||||
|
|
||||||
// 根据状态显示不同页面
|
// 根据状态显示不同页面
|
||||||
if (!partyRoomState.client.isConnected) {
|
if (!partyRoomState.client.isConnected) {
|
||||||
return const PartyRoomConnectPage();
|
widget = PartyRoomConnectPage();
|
||||||
|
} else if (!partyRoomState.auth.isLoggedIn) {
|
||||||
|
widget = PartyRoomRegisterPage();
|
||||||
|
} else if (partyRoomState.room.isInRoom && !uiState.isMinimized) {
|
||||||
|
widget = PartyRoomDetailPage();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!partyRoomState.auth.isLoggedIn) {
|
return LocalHeroScope(
|
||||||
return const PartyRoomRegisterPage();
|
duration: Duration(milliseconds: 180),
|
||||||
}
|
child: AnimatedSwitcher(
|
||||||
|
duration: const Duration(milliseconds: 230),
|
||||||
if (partyRoomState.room.isInRoom) {
|
switchInCurve: Curves.easeInOut,
|
||||||
return const PartyRoomDetailPage();
|
switchOutCurve: Curves.easeInOut,
|
||||||
}
|
child: widget,
|
||||||
|
),
|
||||||
return const PartyRoomListPage();
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,7 @@ sealed class PartyRoomUIState with _$PartyRoomUIState {
|
|||||||
@Default('') String registerGameUserId,
|
@Default('') String registerGameUserId,
|
||||||
@Default(false) bool isReconnecting,
|
@Default(false) bool isReconnecting,
|
||||||
@Default(0) int reconnectAttempts,
|
@Default(0) int reconnectAttempts,
|
||||||
|
@Default(false) bool isMinimized,
|
||||||
}) = _PartyRoomUIState;
|
}) = _PartyRoomUIState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,6 +41,11 @@ class PartyRoomUIModel extends _$PartyRoomUIModel {
|
|||||||
state = const PartyRoomUIState();
|
state = const PartyRoomUIState();
|
||||||
ref.listen(partyRoomProvider, (previous, next) {
|
ref.listen(partyRoomProvider, (previous, next) {
|
||||||
_handleConnectionStateChange(previous, next);
|
_handleConnectionStateChange(previous, next);
|
||||||
|
|
||||||
|
// 如果房间被解散或离开房间,重置最小化状态
|
||||||
|
if (previous?.room.isInRoom == true && !next.room.isInRoom) {
|
||||||
|
state = state.copyWith(isMinimized: false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
connectToServer();
|
connectToServer();
|
||||||
@ -259,4 +265,8 @@ class PartyRoomUIModel extends _$PartyRoomUIModel {
|
|||||||
ref.read(partyRoomProvider.notifier).dismissRoom();
|
ref.read(partyRoomProvider.notifier).dismissRoom();
|
||||||
ref.read(partyRoomProvider.notifier).loadTags();
|
ref.read(partyRoomProvider.notifier).loadTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setMinimized(bool minimized) {
|
||||||
|
state = state.copyWith(isMinimized: minimized);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ T _$identity<T>(T value) => value;
|
|||||||
/// @nodoc
|
/// @nodoc
|
||||||
mixin _$PartyRoomUIState {
|
mixin _$PartyRoomUIState {
|
||||||
|
|
||||||
bool get isConnecting; bool get showRoomList; List<RoomListItem> get roomListItems; int get currentPage; int get pageSize; int get totalRooms; String? get selectedMainTagId; String? get selectedSubTagId; String get searchOwnerName; bool get isLoading; String? get errorMessage; String get preRegisterCode; String get registerGameUserId; bool get isReconnecting; int get reconnectAttempts;
|
bool get isConnecting; bool get showRoomList; List<RoomListItem> get roomListItems; int get currentPage; int get pageSize; int get totalRooms; String? get selectedMainTagId; String? get selectedSubTagId; String get searchOwnerName; bool get isLoading; String? get errorMessage; String get preRegisterCode; String get registerGameUserId; bool get isReconnecting; int get reconnectAttempts; bool get isMinimized;
|
||||||
/// Create a copy of PartyRoomUIState
|
/// Create a copy of PartyRoomUIState
|
||||||
/// 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)
|
||||||
@ -25,16 +25,16 @@ $PartyRoomUIStateCopyWith<PartyRoomUIState> get copyWith => _$PartyRoomUIStateCo
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is PartyRoomUIState&&(identical(other.isConnecting, isConnecting) || other.isConnecting == isConnecting)&&(identical(other.showRoomList, showRoomList) || other.showRoomList == showRoomList)&&const DeepCollectionEquality().equals(other.roomListItems, roomListItems)&&(identical(other.currentPage, currentPage) || other.currentPage == currentPage)&&(identical(other.pageSize, pageSize) || other.pageSize == pageSize)&&(identical(other.totalRooms, totalRooms) || other.totalRooms == totalRooms)&&(identical(other.selectedMainTagId, selectedMainTagId) || other.selectedMainTagId == selectedMainTagId)&&(identical(other.selectedSubTagId, selectedSubTagId) || other.selectedSubTagId == selectedSubTagId)&&(identical(other.searchOwnerName, searchOwnerName) || other.searchOwnerName == searchOwnerName)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.preRegisterCode, preRegisterCode) || other.preRegisterCode == preRegisterCode)&&(identical(other.registerGameUserId, registerGameUserId) || other.registerGameUserId == registerGameUserId)&&(identical(other.isReconnecting, isReconnecting) || other.isReconnecting == isReconnecting)&&(identical(other.reconnectAttempts, reconnectAttempts) || other.reconnectAttempts == reconnectAttempts));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is PartyRoomUIState&&(identical(other.isConnecting, isConnecting) || other.isConnecting == isConnecting)&&(identical(other.showRoomList, showRoomList) || other.showRoomList == showRoomList)&&const DeepCollectionEquality().equals(other.roomListItems, roomListItems)&&(identical(other.currentPage, currentPage) || other.currentPage == currentPage)&&(identical(other.pageSize, pageSize) || other.pageSize == pageSize)&&(identical(other.totalRooms, totalRooms) || other.totalRooms == totalRooms)&&(identical(other.selectedMainTagId, selectedMainTagId) || other.selectedMainTagId == selectedMainTagId)&&(identical(other.selectedSubTagId, selectedSubTagId) || other.selectedSubTagId == selectedSubTagId)&&(identical(other.searchOwnerName, searchOwnerName) || other.searchOwnerName == searchOwnerName)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.preRegisterCode, preRegisterCode) || other.preRegisterCode == preRegisterCode)&&(identical(other.registerGameUserId, registerGameUserId) || other.registerGameUserId == registerGameUserId)&&(identical(other.isReconnecting, isReconnecting) || other.isReconnecting == isReconnecting)&&(identical(other.reconnectAttempts, reconnectAttempts) || other.reconnectAttempts == reconnectAttempts)&&(identical(other.isMinimized, isMinimized) || other.isMinimized == isMinimized));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,isConnecting,showRoomList,const DeepCollectionEquality().hash(roomListItems),currentPage,pageSize,totalRooms,selectedMainTagId,selectedSubTagId,searchOwnerName,isLoading,errorMessage,preRegisterCode,registerGameUserId,isReconnecting,reconnectAttempts);
|
int get hashCode => Object.hash(runtimeType,isConnecting,showRoomList,const DeepCollectionEquality().hash(roomListItems),currentPage,pageSize,totalRooms,selectedMainTagId,selectedSubTagId,searchOwnerName,isLoading,errorMessage,preRegisterCode,registerGameUserId,isReconnecting,reconnectAttempts,isMinimized);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'PartyRoomUIState(isConnecting: $isConnecting, showRoomList: $showRoomList, roomListItems: $roomListItems, currentPage: $currentPage, pageSize: $pageSize, totalRooms: $totalRooms, selectedMainTagId: $selectedMainTagId, selectedSubTagId: $selectedSubTagId, searchOwnerName: $searchOwnerName, isLoading: $isLoading, errorMessage: $errorMessage, preRegisterCode: $preRegisterCode, registerGameUserId: $registerGameUserId, isReconnecting: $isReconnecting, reconnectAttempts: $reconnectAttempts)';
|
return 'PartyRoomUIState(isConnecting: $isConnecting, showRoomList: $showRoomList, roomListItems: $roomListItems, currentPage: $currentPage, pageSize: $pageSize, totalRooms: $totalRooms, selectedMainTagId: $selectedMainTagId, selectedSubTagId: $selectedSubTagId, searchOwnerName: $searchOwnerName, isLoading: $isLoading, errorMessage: $errorMessage, preRegisterCode: $preRegisterCode, registerGameUserId: $registerGameUserId, isReconnecting: $isReconnecting, reconnectAttempts: $reconnectAttempts, isMinimized: $isMinimized)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -45,7 +45,7 @@ abstract mixin class $PartyRoomUIStateCopyWith<$Res> {
|
|||||||
factory $PartyRoomUIStateCopyWith(PartyRoomUIState value, $Res Function(PartyRoomUIState) _then) = _$PartyRoomUIStateCopyWithImpl;
|
factory $PartyRoomUIStateCopyWith(PartyRoomUIState value, $Res Function(PartyRoomUIState) _then) = _$PartyRoomUIStateCopyWithImpl;
|
||||||
@useResult
|
@useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
bool isConnecting, bool showRoomList, List<RoomListItem> roomListItems, int currentPage, int pageSize, int totalRooms, String? selectedMainTagId, String? selectedSubTagId, String searchOwnerName, bool isLoading, String? errorMessage, String preRegisterCode, String registerGameUserId, bool isReconnecting, int reconnectAttempts
|
bool isConnecting, bool showRoomList, List<RoomListItem> roomListItems, int currentPage, int pageSize, int totalRooms, String? selectedMainTagId, String? selectedSubTagId, String searchOwnerName, bool isLoading, String? errorMessage, String preRegisterCode, String registerGameUserId, bool isReconnecting, int reconnectAttempts, bool isMinimized
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -62,7 +62,7 @@ class _$PartyRoomUIStateCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of PartyRoomUIState
|
/// Create a copy of PartyRoomUIState
|
||||||
/// 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? isConnecting = null,Object? showRoomList = null,Object? roomListItems = null,Object? currentPage = null,Object? pageSize = null,Object? totalRooms = null,Object? selectedMainTagId = freezed,Object? selectedSubTagId = freezed,Object? searchOwnerName = null,Object? isLoading = null,Object? errorMessage = freezed,Object? preRegisterCode = null,Object? registerGameUserId = null,Object? isReconnecting = null,Object? reconnectAttempts = null,}) {
|
@pragma('vm:prefer-inline') @override $Res call({Object? isConnecting = null,Object? showRoomList = null,Object? roomListItems = null,Object? currentPage = null,Object? pageSize = null,Object? totalRooms = null,Object? selectedMainTagId = freezed,Object? selectedSubTagId = freezed,Object? searchOwnerName = null,Object? isLoading = null,Object? errorMessage = freezed,Object? preRegisterCode = null,Object? registerGameUserId = null,Object? isReconnecting = null,Object? reconnectAttempts = null,Object? isMinimized = null,}) {
|
||||||
return _then(_self.copyWith(
|
return _then(_self.copyWith(
|
||||||
isConnecting: null == isConnecting ? _self.isConnecting : isConnecting // ignore: cast_nullable_to_non_nullable
|
isConnecting: null == isConnecting ? _self.isConnecting : isConnecting // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,showRoomList: null == showRoomList ? _self.showRoomList : showRoomList // ignore: cast_nullable_to_non_nullable
|
as bool,showRoomList: null == showRoomList ? _self.showRoomList : showRoomList // ignore: cast_nullable_to_non_nullable
|
||||||
@ -79,7 +79,8 @@ as String?,preRegisterCode: null == preRegisterCode ? _self.preRegisterCode : pr
|
|||||||
as String,registerGameUserId: null == registerGameUserId ? _self.registerGameUserId : registerGameUserId // ignore: cast_nullable_to_non_nullable
|
as String,registerGameUserId: null == registerGameUserId ? _self.registerGameUserId : registerGameUserId // ignore: cast_nullable_to_non_nullable
|
||||||
as String,isReconnecting: null == isReconnecting ? _self.isReconnecting : isReconnecting // ignore: cast_nullable_to_non_nullable
|
as String,isReconnecting: null == isReconnecting ? _self.isReconnecting : isReconnecting // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,reconnectAttempts: null == reconnectAttempts ? _self.reconnectAttempts : reconnectAttempts // ignore: cast_nullable_to_non_nullable
|
as bool,reconnectAttempts: null == reconnectAttempts ? _self.reconnectAttempts : reconnectAttempts // ignore: cast_nullable_to_non_nullable
|
||||||
as int,
|
as int,isMinimized: null == isMinimized ? _self.isMinimized : isMinimized // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,10 +162,10 @@ return $default(_that);case _:
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isConnecting, bool showRoomList, List<RoomListItem> roomListItems, int currentPage, int pageSize, int totalRooms, String? selectedMainTagId, String? selectedSubTagId, String searchOwnerName, bool isLoading, String? errorMessage, String preRegisterCode, String registerGameUserId, bool isReconnecting, int reconnectAttempts)? $default,{required TResult orElse(),}) {final _that = this;
|
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isConnecting, bool showRoomList, List<RoomListItem> roomListItems, int currentPage, int pageSize, int totalRooms, String? selectedMainTagId, String? selectedSubTagId, String searchOwnerName, bool isLoading, String? errorMessage, String preRegisterCode, String registerGameUserId, bool isReconnecting, int reconnectAttempts, bool isMinimized)? $default,{required TResult orElse(),}) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _PartyRoomUIState() when $default != null:
|
case _PartyRoomUIState() when $default != null:
|
||||||
return $default(_that.isConnecting,_that.showRoomList,_that.roomListItems,_that.currentPage,_that.pageSize,_that.totalRooms,_that.selectedMainTagId,_that.selectedSubTagId,_that.searchOwnerName,_that.isLoading,_that.errorMessage,_that.preRegisterCode,_that.registerGameUserId,_that.isReconnecting,_that.reconnectAttempts);case _:
|
return $default(_that.isConnecting,_that.showRoomList,_that.roomListItems,_that.currentPage,_that.pageSize,_that.totalRooms,_that.selectedMainTagId,_that.selectedSubTagId,_that.searchOwnerName,_that.isLoading,_that.errorMessage,_that.preRegisterCode,_that.registerGameUserId,_that.isReconnecting,_that.reconnectAttempts,_that.isMinimized);case _:
|
||||||
return orElse();
|
return orElse();
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -182,10 +183,10 @@ return $default(_that.isConnecting,_that.showRoomList,_that.roomListItems,_that.
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isConnecting, bool showRoomList, List<RoomListItem> roomListItems, int currentPage, int pageSize, int totalRooms, String? selectedMainTagId, String? selectedSubTagId, String searchOwnerName, bool isLoading, String? errorMessage, String preRegisterCode, String registerGameUserId, bool isReconnecting, int reconnectAttempts) $default,) {final _that = this;
|
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isConnecting, bool showRoomList, List<RoomListItem> roomListItems, int currentPage, int pageSize, int totalRooms, String? selectedMainTagId, String? selectedSubTagId, String searchOwnerName, bool isLoading, String? errorMessage, String preRegisterCode, String registerGameUserId, bool isReconnecting, int reconnectAttempts, bool isMinimized) $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _PartyRoomUIState():
|
case _PartyRoomUIState():
|
||||||
return $default(_that.isConnecting,_that.showRoomList,_that.roomListItems,_that.currentPage,_that.pageSize,_that.totalRooms,_that.selectedMainTagId,_that.selectedSubTagId,_that.searchOwnerName,_that.isLoading,_that.errorMessage,_that.preRegisterCode,_that.registerGameUserId,_that.isReconnecting,_that.reconnectAttempts);}
|
return $default(_that.isConnecting,_that.showRoomList,_that.roomListItems,_that.currentPage,_that.pageSize,_that.totalRooms,_that.selectedMainTagId,_that.selectedSubTagId,_that.searchOwnerName,_that.isLoading,_that.errorMessage,_that.preRegisterCode,_that.registerGameUserId,_that.isReconnecting,_that.reconnectAttempts,_that.isMinimized);}
|
||||||
}
|
}
|
||||||
/// A variant of `when` that fallback to returning `null`
|
/// A variant of `when` that fallback to returning `null`
|
||||||
///
|
///
|
||||||
@ -199,10 +200,10 @@ return $default(_that.isConnecting,_that.showRoomList,_that.roomListItems,_that.
|
|||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
|
|
||||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isConnecting, bool showRoomList, List<RoomListItem> roomListItems, int currentPage, int pageSize, int totalRooms, String? selectedMainTagId, String? selectedSubTagId, String searchOwnerName, bool isLoading, String? errorMessage, String preRegisterCode, String registerGameUserId, bool isReconnecting, int reconnectAttempts)? $default,) {final _that = this;
|
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isConnecting, bool showRoomList, List<RoomListItem> roomListItems, int currentPage, int pageSize, int totalRooms, String? selectedMainTagId, String? selectedSubTagId, String searchOwnerName, bool isLoading, String? errorMessage, String preRegisterCode, String registerGameUserId, bool isReconnecting, int reconnectAttempts, bool isMinimized)? $default,) {final _that = this;
|
||||||
switch (_that) {
|
switch (_that) {
|
||||||
case _PartyRoomUIState() when $default != null:
|
case _PartyRoomUIState() when $default != null:
|
||||||
return $default(_that.isConnecting,_that.showRoomList,_that.roomListItems,_that.currentPage,_that.pageSize,_that.totalRooms,_that.selectedMainTagId,_that.selectedSubTagId,_that.searchOwnerName,_that.isLoading,_that.errorMessage,_that.preRegisterCode,_that.registerGameUserId,_that.isReconnecting,_that.reconnectAttempts);case _:
|
return $default(_that.isConnecting,_that.showRoomList,_that.roomListItems,_that.currentPage,_that.pageSize,_that.totalRooms,_that.selectedMainTagId,_that.selectedSubTagId,_that.searchOwnerName,_that.isLoading,_that.errorMessage,_that.preRegisterCode,_that.registerGameUserId,_that.isReconnecting,_that.reconnectAttempts,_that.isMinimized);case _:
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -214,7 +215,7 @@ return $default(_that.isConnecting,_that.showRoomList,_that.roomListItems,_that.
|
|||||||
|
|
||||||
|
|
||||||
class _PartyRoomUIState implements PartyRoomUIState {
|
class _PartyRoomUIState implements PartyRoomUIState {
|
||||||
const _PartyRoomUIState({this.isConnecting = false, this.showRoomList = false, final List<RoomListItem> roomListItems = const [], this.currentPage = 1, this.pageSize = 20, this.totalRooms = 0, this.selectedMainTagId, this.selectedSubTagId, this.searchOwnerName = '', this.isLoading = false, this.errorMessage, this.preRegisterCode = '', this.registerGameUserId = '', this.isReconnecting = false, this.reconnectAttempts = 0}): _roomListItems = roomListItems;
|
const _PartyRoomUIState({this.isConnecting = false, this.showRoomList = false, final List<RoomListItem> roomListItems = const [], this.currentPage = 1, this.pageSize = 20, this.totalRooms = 0, this.selectedMainTagId, this.selectedSubTagId, this.searchOwnerName = '', this.isLoading = false, this.errorMessage, this.preRegisterCode = '', this.registerGameUserId = '', this.isReconnecting = false, this.reconnectAttempts = 0, this.isMinimized = false}): _roomListItems = roomListItems;
|
||||||
|
|
||||||
|
|
||||||
@override@JsonKey() final bool isConnecting;
|
@override@JsonKey() final bool isConnecting;
|
||||||
@ -238,6 +239,7 @@ class _PartyRoomUIState implements PartyRoomUIState {
|
|||||||
@override@JsonKey() final String registerGameUserId;
|
@override@JsonKey() final String registerGameUserId;
|
||||||
@override@JsonKey() final bool isReconnecting;
|
@override@JsonKey() final bool isReconnecting;
|
||||||
@override@JsonKey() final int reconnectAttempts;
|
@override@JsonKey() final int reconnectAttempts;
|
||||||
|
@override@JsonKey() final bool isMinimized;
|
||||||
|
|
||||||
/// Create a copy of PartyRoomUIState
|
/// Create a copy of PartyRoomUIState
|
||||||
/// with the given fields replaced by the non-null parameter values.
|
/// with the given fields replaced by the non-null parameter values.
|
||||||
@ -249,16 +251,16 @@ _$PartyRoomUIStateCopyWith<_PartyRoomUIState> get copyWith => __$PartyRoomUIStat
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) {
|
bool operator ==(Object other) {
|
||||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PartyRoomUIState&&(identical(other.isConnecting, isConnecting) || other.isConnecting == isConnecting)&&(identical(other.showRoomList, showRoomList) || other.showRoomList == showRoomList)&&const DeepCollectionEquality().equals(other._roomListItems, _roomListItems)&&(identical(other.currentPage, currentPage) || other.currentPage == currentPage)&&(identical(other.pageSize, pageSize) || other.pageSize == pageSize)&&(identical(other.totalRooms, totalRooms) || other.totalRooms == totalRooms)&&(identical(other.selectedMainTagId, selectedMainTagId) || other.selectedMainTagId == selectedMainTagId)&&(identical(other.selectedSubTagId, selectedSubTagId) || other.selectedSubTagId == selectedSubTagId)&&(identical(other.searchOwnerName, searchOwnerName) || other.searchOwnerName == searchOwnerName)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.preRegisterCode, preRegisterCode) || other.preRegisterCode == preRegisterCode)&&(identical(other.registerGameUserId, registerGameUserId) || other.registerGameUserId == registerGameUserId)&&(identical(other.isReconnecting, isReconnecting) || other.isReconnecting == isReconnecting)&&(identical(other.reconnectAttempts, reconnectAttempts) || other.reconnectAttempts == reconnectAttempts));
|
return identical(this, other) || (other.runtimeType == runtimeType&&other is _PartyRoomUIState&&(identical(other.isConnecting, isConnecting) || other.isConnecting == isConnecting)&&(identical(other.showRoomList, showRoomList) || other.showRoomList == showRoomList)&&const DeepCollectionEquality().equals(other._roomListItems, _roomListItems)&&(identical(other.currentPage, currentPage) || other.currentPage == currentPage)&&(identical(other.pageSize, pageSize) || other.pageSize == pageSize)&&(identical(other.totalRooms, totalRooms) || other.totalRooms == totalRooms)&&(identical(other.selectedMainTagId, selectedMainTagId) || other.selectedMainTagId == selectedMainTagId)&&(identical(other.selectedSubTagId, selectedSubTagId) || other.selectedSubTagId == selectedSubTagId)&&(identical(other.searchOwnerName, searchOwnerName) || other.searchOwnerName == searchOwnerName)&&(identical(other.isLoading, isLoading) || other.isLoading == isLoading)&&(identical(other.errorMessage, errorMessage) || other.errorMessage == errorMessage)&&(identical(other.preRegisterCode, preRegisterCode) || other.preRegisterCode == preRegisterCode)&&(identical(other.registerGameUserId, registerGameUserId) || other.registerGameUserId == registerGameUserId)&&(identical(other.isReconnecting, isReconnecting) || other.isReconnecting == isReconnecting)&&(identical(other.reconnectAttempts, reconnectAttempts) || other.reconnectAttempts == reconnectAttempts)&&(identical(other.isMinimized, isMinimized) || other.isMinimized == isMinimized));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode => Object.hash(runtimeType,isConnecting,showRoomList,const DeepCollectionEquality().hash(_roomListItems),currentPage,pageSize,totalRooms,selectedMainTagId,selectedSubTagId,searchOwnerName,isLoading,errorMessage,preRegisterCode,registerGameUserId,isReconnecting,reconnectAttempts);
|
int get hashCode => Object.hash(runtimeType,isConnecting,showRoomList,const DeepCollectionEquality().hash(_roomListItems),currentPage,pageSize,totalRooms,selectedMainTagId,selectedSubTagId,searchOwnerName,isLoading,errorMessage,preRegisterCode,registerGameUserId,isReconnecting,reconnectAttempts,isMinimized);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'PartyRoomUIState(isConnecting: $isConnecting, showRoomList: $showRoomList, roomListItems: $roomListItems, currentPage: $currentPage, pageSize: $pageSize, totalRooms: $totalRooms, selectedMainTagId: $selectedMainTagId, selectedSubTagId: $selectedSubTagId, searchOwnerName: $searchOwnerName, isLoading: $isLoading, errorMessage: $errorMessage, preRegisterCode: $preRegisterCode, registerGameUserId: $registerGameUserId, isReconnecting: $isReconnecting, reconnectAttempts: $reconnectAttempts)';
|
return 'PartyRoomUIState(isConnecting: $isConnecting, showRoomList: $showRoomList, roomListItems: $roomListItems, currentPage: $currentPage, pageSize: $pageSize, totalRooms: $totalRooms, selectedMainTagId: $selectedMainTagId, selectedSubTagId: $selectedSubTagId, searchOwnerName: $searchOwnerName, isLoading: $isLoading, errorMessage: $errorMessage, preRegisterCode: $preRegisterCode, registerGameUserId: $registerGameUserId, isReconnecting: $isReconnecting, reconnectAttempts: $reconnectAttempts, isMinimized: $isMinimized)';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -269,7 +271,7 @@ abstract mixin class _$PartyRoomUIStateCopyWith<$Res> implements $PartyRoomUISta
|
|||||||
factory _$PartyRoomUIStateCopyWith(_PartyRoomUIState value, $Res Function(_PartyRoomUIState) _then) = __$PartyRoomUIStateCopyWithImpl;
|
factory _$PartyRoomUIStateCopyWith(_PartyRoomUIState value, $Res Function(_PartyRoomUIState) _then) = __$PartyRoomUIStateCopyWithImpl;
|
||||||
@override @useResult
|
@override @useResult
|
||||||
$Res call({
|
$Res call({
|
||||||
bool isConnecting, bool showRoomList, List<RoomListItem> roomListItems, int currentPage, int pageSize, int totalRooms, String? selectedMainTagId, String? selectedSubTagId, String searchOwnerName, bool isLoading, String? errorMessage, String preRegisterCode, String registerGameUserId, bool isReconnecting, int reconnectAttempts
|
bool isConnecting, bool showRoomList, List<RoomListItem> roomListItems, int currentPage, int pageSize, int totalRooms, String? selectedMainTagId, String? selectedSubTagId, String searchOwnerName, bool isLoading, String? errorMessage, String preRegisterCode, String registerGameUserId, bool isReconnecting, int reconnectAttempts, bool isMinimized
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
@ -286,7 +288,7 @@ class __$PartyRoomUIStateCopyWithImpl<$Res>
|
|||||||
|
|
||||||
/// Create a copy of PartyRoomUIState
|
/// Create a copy of PartyRoomUIState
|
||||||
/// 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? isConnecting = null,Object? showRoomList = null,Object? roomListItems = null,Object? currentPage = null,Object? pageSize = null,Object? totalRooms = null,Object? selectedMainTagId = freezed,Object? selectedSubTagId = freezed,Object? searchOwnerName = null,Object? isLoading = null,Object? errorMessage = freezed,Object? preRegisterCode = null,Object? registerGameUserId = null,Object? isReconnecting = null,Object? reconnectAttempts = null,}) {
|
@override @pragma('vm:prefer-inline') $Res call({Object? isConnecting = null,Object? showRoomList = null,Object? roomListItems = null,Object? currentPage = null,Object? pageSize = null,Object? totalRooms = null,Object? selectedMainTagId = freezed,Object? selectedSubTagId = freezed,Object? searchOwnerName = null,Object? isLoading = null,Object? errorMessage = freezed,Object? preRegisterCode = null,Object? registerGameUserId = null,Object? isReconnecting = null,Object? reconnectAttempts = null,Object? isMinimized = null,}) {
|
||||||
return _then(_PartyRoomUIState(
|
return _then(_PartyRoomUIState(
|
||||||
isConnecting: null == isConnecting ? _self.isConnecting : isConnecting // ignore: cast_nullable_to_non_nullable
|
isConnecting: null == isConnecting ? _self.isConnecting : isConnecting // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,showRoomList: null == showRoomList ? _self.showRoomList : showRoomList // ignore: cast_nullable_to_non_nullable
|
as bool,showRoomList: null == showRoomList ? _self.showRoomList : showRoomList // ignore: cast_nullable_to_non_nullable
|
||||||
@ -303,7 +305,8 @@ as String?,preRegisterCode: null == preRegisterCode ? _self.preRegisterCode : pr
|
|||||||
as String,registerGameUserId: null == registerGameUserId ? _self.registerGameUserId : registerGameUserId // ignore: cast_nullable_to_non_nullable
|
as String,registerGameUserId: null == registerGameUserId ? _self.registerGameUserId : registerGameUserId // ignore: cast_nullable_to_non_nullable
|
||||||
as String,isReconnecting: null == isReconnecting ? _self.isReconnecting : isReconnecting // ignore: cast_nullable_to_non_nullable
|
as String,isReconnecting: null == isReconnecting ? _self.isReconnecting : isReconnecting // ignore: cast_nullable_to_non_nullable
|
||||||
as bool,reconnectAttempts: null == reconnectAttempts ? _self.reconnectAttempts : reconnectAttempts // ignore: cast_nullable_to_non_nullable
|
as bool,reconnectAttempts: null == reconnectAttempts ? _self.reconnectAttempts : reconnectAttempts // ignore: cast_nullable_to_non_nullable
|
||||||
as int,
|
as int,isMinimized: null == isMinimized ? _self.isMinimized : isMinimized // ignore: cast_nullable_to_non_nullable
|
||||||
|
as bool,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -41,7 +41,7 @@ final class PartyRoomUIModelProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$partyRoomUIModelHash() => r'262069d02bbc7d76fe6797c6c744bdf848122492';
|
String _$partyRoomUIModelHash() => r'0e86aeb2bf3524907836e9951b04c062c84327a6';
|
||||||
|
|
||||||
abstract class _$PartyRoomUIModel extends $Notifier<PartyRoomUIState> {
|
abstract class _$PartyRoomUIModel extends $Notifier<PartyRoomUIState> {
|
||||||
PartyRoomUIState build();
|
PartyRoomUIState build();
|
||||||
|
|||||||
@ -20,87 +20,149 @@ class CreateRoomDialog extends HookConsumerWidget {
|
|||||||
final socialLinksController = useTextEditingController();
|
final socialLinksController = useTextEditingController();
|
||||||
final isCreating = useState(false);
|
final isCreating = useState(false);
|
||||||
|
|
||||||
|
// 获取选中的主标签
|
||||||
|
final selectedMainTagData = selectedMainTag.value != null ? partyRoomState.room.tags[selectedMainTag.value] : null;
|
||||||
|
|
||||||
return ContentDialog(
|
return ContentDialog(
|
||||||
constraints: const BoxConstraints(maxWidth: 500),
|
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.4),
|
||||||
title: const Text('创建房间'),
|
title: const Text('创建房间'),
|
||||||
content: SingleChildScrollView(
|
content: SizedBox(
|
||||||
child: Column(
|
child: SingleChildScrollView(
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
InfoLabel(
|
children: [
|
||||||
label: '房间类型',
|
Column(
|
||||||
child: ComboBox<String>(
|
children: [
|
||||||
placeholder: const Text('选择主标签'),
|
InfoLabel(
|
||||||
value: selectedMainTag.value,
|
label: '房间类型',
|
||||||
items: partyRoomState.room.tags.map((tag) {
|
child: ComboBox<String>(
|
||||||
return ComboBoxItem(value: tag.id, child: Text(tag.name));
|
placeholder: const Text('选择主标签'),
|
||||||
}).toList(),
|
value: selectedMainTag.value,
|
||||||
onChanged: (value) {
|
isExpanded: true,
|
||||||
selectedMainTag.value = value;
|
items: partyRoomState.room.tags.values.map((tag) {
|
||||||
selectedSubTag.value = null;
|
return ComboBoxItem(
|
||||||
},
|
value: tag.id,
|
||||||
),
|
child: Row(
|
||||||
),
|
children: [
|
||||||
const SizedBox(height: 12),
|
if (tag.color.isNotEmpty)
|
||||||
|
Container(
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
margin: const EdgeInsets.only(right: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _parseColor(tag.color),
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(tag.name),
|
||||||
|
if (tag.info.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8),
|
||||||
|
child: Text(tag.info, style: TextStyle(fontSize: 11, color: Colors.grey[100])),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (value) {
|
||||||
|
selectedMainTag.value = value;
|
||||||
|
selectedSubTag.value = null;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
if (selectedMainTag.value != null) ...[
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// 子标签 - 始终显示,避免布局跳动
|
||||||
|
InfoLabel(
|
||||||
|
label: '子标签 (可选)',
|
||||||
|
child: ComboBox<String>(
|
||||||
|
placeholder: const Text('选择子标签'),
|
||||||
|
value: selectedSubTag.value,
|
||||||
|
isExpanded: true,
|
||||||
|
items: [
|
||||||
|
const ComboBoxItem(value: null, child: Text('无')),
|
||||||
|
if (selectedMainTagData != null)
|
||||||
|
...selectedMainTagData.subTags.map((subTag) {
|
||||||
|
return ComboBoxItem(
|
||||||
|
value: subTag.id,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
if (subTag.color.isNotEmpty)
|
||||||
|
Container(
|
||||||
|
width: 12,
|
||||||
|
height: 12,
|
||||||
|
margin: const EdgeInsets.only(right: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: _parseColor(subTag.color),
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(subTag.name),
|
||||||
|
if (subTag.info.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8),
|
||||||
|
child: Text(subTag.info, style: TextStyle(fontSize: 11, color: Colors.grey[100])),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
onChanged: selectedMainTagData != null
|
||||||
|
? (value) {
|
||||||
|
selectedSubTag.value = value;
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
InfoLabel(
|
InfoLabel(
|
||||||
label: '子标签 (可选)',
|
label: '目标人数 (2-600)',
|
||||||
child: ComboBox<String>(
|
child: TextBox(
|
||||||
placeholder: const Text('选择子标签'),
|
controller: targetMembersController,
|
||||||
value: selectedSubTag.value,
|
placeholder: '输入目标人数',
|
||||||
items: [
|
keyboardType: TextInputType.number,
|
||||||
const ComboBoxItem(value: null, child: Text('无')),
|
|
||||||
...partyRoomState.room.tags.firstWhere((tag) => tag.id == selectedMainTag.value).subTags.map((
|
|
||||||
subTag,
|
|
||||||
) {
|
|
||||||
return ComboBoxItem(value: subTag.id, child: Text(subTag.name));
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
onChanged: (value) {
|
|
||||||
selectedSubTag.value = value;
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 16),
|
||||||
],
|
|
||||||
|
|
||||||
InfoLabel(
|
Row(
|
||||||
label: '目标人数 (2-600)',
|
children: [
|
||||||
child: TextBox(
|
Checkbox(
|
||||||
controller: targetMembersController,
|
checked: hasPassword.value,
|
||||||
placeholder: '输入目标人数',
|
onChanged: (value) {
|
||||||
keyboardType: TextInputType.number,
|
hasPassword.value = value ?? false;
|
||||||
|
},
|
||||||
|
content: const Text('设置密码'),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Checkbox(
|
|
||||||
checked: hasPassword.value,
|
|
||||||
onChanged: (value) {
|
|
||||||
hasPassword.value = value ?? false;
|
|
||||||
},
|
|
||||||
content: const Text('设置密码'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (hasPassword.value) ...[
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// 密码输入框 - 始终显示,避免布局跳动
|
||||||
InfoLabel(
|
InfoLabel(
|
||||||
label: '房间密码',
|
label: '房间密码',
|
||||||
child: TextBox(controller: passwordController, placeholder: '输入密码', obscureText: true),
|
child: TextBox(
|
||||||
|
controller: passwordController,
|
||||||
|
placeholder: hasPassword.value ? '输入密码' : '未启用密码',
|
||||||
|
obscureText: hasPassword.value,
|
||||||
|
maxLines: 1,
|
||||||
|
maxLength: 12,
|
||||||
|
enabled: hasPassword.value,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
InfoLabel(
|
||||||
|
label: '社交链接 (可选)',
|
||||||
|
child: TextBox(controller: socialLinksController, placeholder: 'https://discord.gg/xxxxx', maxLines: 1),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: 12),
|
),
|
||||||
|
|
||||||
InfoLabel(
|
|
||||||
label: '社交链接 (可选)',
|
|
||||||
child: TextBox(controller: socialLinksController, placeholder: 'https://discord.gg/xxxxx', maxLines: 1),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
@ -187,4 +249,26 @@ class CreateRoomDialog extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 解析颜色字符串
|
||||||
|
Color _parseColor(String colorStr) {
|
||||||
|
if (colorStr.isEmpty) return Colors.grey;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 移除 # 前缀
|
||||||
|
String hexColor = colorStr.replaceAll('#', '');
|
||||||
|
|
||||||
|
// 如果是3位或6位,添加 alpha 通道
|
||||||
|
if (hexColor.length == 3) {
|
||||||
|
hexColor = hexColor.split('').map((c) => '$c$c').join();
|
||||||
|
}
|
||||||
|
if (hexColor.length == 6) {
|
||||||
|
hexColor = 'FF$hexColor';
|
||||||
|
}
|
||||||
|
|
||||||
|
return Color(int.parse(hexColor, radix: 16));
|
||||||
|
} catch (e) {
|
||||||
|
return Colors.grey;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
101
lib/ui/party_room/widgets/detail/party_room_detail_page.dart
Normal file
101
lib/ui/party_room/widgets/detail/party_room_detail_page.dart
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:starcitizen_doctor/provider/party_room.dart';
|
||||||
|
import 'package:starcitizen_doctor/ui/party_room/widgets/detail/party_room_message_list.dart';
|
||||||
|
|
||||||
|
import 'party_room_header.dart';
|
||||||
|
import 'party_room_member_list.dart';
|
||||||
|
import 'party_room_signal_sender.dart';
|
||||||
|
|
||||||
|
/// 房间详情页面 (Discord 样式)
|
||||||
|
class PartyRoomDetailPage extends ConsumerStatefulWidget {
|
||||||
|
const PartyRoomDetailPage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<PartyRoomDetailPage> createState() => _PartyRoomDetailPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PartyRoomDetailPageState extends ConsumerState<PartyRoomDetailPage> {
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
int _lastEventCount = 0;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _scrollToBottom() {
|
||||||
|
if (_scrollController.hasClients) {
|
||||||
|
Future.delayed(const Duration(milliseconds: 100), () {
|
||||||
|
if (_scrollController.hasClients) {
|
||||||
|
_scrollController.animateTo(
|
||||||
|
_scrollController.position.maxScrollExtent,
|
||||||
|
duration: const Duration(milliseconds: 300),
|
||||||
|
curve: Curves.easeOut,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final partyRoomState = ref.watch(partyRoomProvider);
|
||||||
|
final partyRoom = ref.read(partyRoomProvider.notifier);
|
||||||
|
final room = partyRoomState.room.currentRoom;
|
||||||
|
final members = partyRoomState.room.members;
|
||||||
|
final isOwner = partyRoomState.room.isOwner;
|
||||||
|
final events = partyRoomState.room.recentEvents;
|
||||||
|
|
||||||
|
// 检测消息数量变化,触发滚动
|
||||||
|
if (events.length != _lastEventCount) {
|
||||||
|
_lastEventCount = events.length;
|
||||||
|
if (events.isNotEmpty) {
|
||||||
|
_scrollToBottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ScaffoldPage(
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
content: Row(
|
||||||
|
children: [
|
||||||
|
// 左侧成员列表 (类似 Discord 侧边栏)
|
||||||
|
Container(
|
||||||
|
width: 240,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Color(0xFF232428).withValues(alpha: .3),
|
||||||
|
border: Border(right: BorderSide(color: Colors.black.withValues(alpha: 0.3), width: 1)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// 房间信息头部
|
||||||
|
PartyRoomHeader(room: room, members: members, isOwner: isOwner, partyRoom: partyRoom),
|
||||||
|
const Divider(
|
||||||
|
style: DividerThemeData(thickness: 1, decoration: BoxDecoration(color: Color(0xFF1E1F22))),
|
||||||
|
),
|
||||||
|
// 成员列表
|
||||||
|
Expanded(
|
||||||
|
child: PartyRoomMemberList(members: members, isOwner: isOwner, partyRoom: partyRoom),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 右侧消息区域
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// 消息列表
|
||||||
|
Expanded(
|
||||||
|
child: PartyRoomMessageList(events: events, scrollController: _scrollController),
|
||||||
|
),
|
||||||
|
// 信号发送按钮
|
||||||
|
PartyRoomSignalSender(partyRoom: partyRoom, room: room),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
112
lib/ui/party_room/widgets/detail/party_room_header.dart
Normal file
112
lib/ui/party_room/widgets/detail/party_room_header.dart
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:starcitizen_doctor/provider/party_room.dart';
|
||||||
|
import 'package:starcitizen_doctor/ui/party_room/party_room_ui_model.dart';
|
||||||
|
|
||||||
|
/// 房间信息头部组件
|
||||||
|
class PartyRoomHeader extends ConsumerWidget {
|
||||||
|
final dynamic room;
|
||||||
|
final List members;
|
||||||
|
final bool isOwner;
|
||||||
|
final PartyRoom partyRoom;
|
||||||
|
|
||||||
|
const PartyRoomHeader({
|
||||||
|
super.key,
|
||||||
|
required this.room,
|
||||||
|
required this.members,
|
||||||
|
required this.isOwner,
|
||||||
|
required this.partyRoom,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(FluentIcons.back, size: 16, color: Color(0xFFB5BAC1)),
|
||||||
|
onPressed: () {
|
||||||
|
ref.read(partyRoomUIModelProvider.notifier).setMinimized(true);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
const Icon(FluentIcons.room, size: 16, color: Color(0xFFB5BAC1)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
room?.ownerGameId ?? '房间',
|
||||||
|
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.white),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(FluentIcons.group, size: 12, color: Color(0xFF80848E)),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'${members.length}/${room?.targetMembers ?? 0} 成员',
|
||||||
|
style: const TextStyle(fontSize: 11, color: Color(0xFF80848E)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (isOwner) ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
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 ...[
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Button(
|
||||||
|
onPressed: () async {
|
||||||
|
await partyRoom.leaveRoom();
|
||||||
|
},
|
||||||
|
style: ButtonStyle(backgroundColor: WidgetStateProperty.all(const Color(0xFF404249))),
|
||||||
|
child: const Text('离开房间', style: TextStyle(color: Color(0xFFB5BAC1))),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
252
lib/ui/party_room/widgets/detail/party_room_member_list.dart
Normal file
252
lib/ui/party_room/widgets/detail/party_room_member_list.dart
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:local_hero/local_hero.dart';
|
||||||
|
import 'package:starcitizen_doctor/common/conf/url_conf.dart';
|
||||||
|
import 'package:starcitizen_doctor/generated/proto/partroom/partroom.pb.dart';
|
||||||
|
import 'package:starcitizen_doctor/provider/party_room.dart';
|
||||||
|
import 'package:starcitizen_doctor/widgets/src/cache_image.dart';
|
||||||
|
|
||||||
|
/// 成员列表侧边栏
|
||||||
|
class PartyRoomMemberList extends ConsumerWidget {
|
||||||
|
final List<RoomMember> members;
|
||||||
|
final bool isOwner;
|
||||||
|
final PartyRoom partyRoom;
|
||||||
|
|
||||||
|
const PartyRoomMemberList({super.key, required this.members, required this.isOwner, required this.partyRoom});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
if (members.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Text('暂无成员', style: TextStyle(color: Colors.white.withValues(alpha: 0.5), fontSize: 12)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
itemCount: members.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final member = members[index];
|
||||||
|
return PartyRoomMemberItem(member: member, isOwner: isOwner, partyRoom: partyRoom);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 成员列表项
|
||||||
|
class PartyRoomMemberItem extends ConsumerWidget {
|
||||||
|
final RoomMember member;
|
||||||
|
final bool isOwner;
|
||||||
|
final PartyRoom partyRoom;
|
||||||
|
|
||||||
|
const PartyRoomMemberItem({super.key, required this.member, required this.isOwner, required this.partyRoom});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final avatarUrl = member.avatarUrl.isNotEmpty ? '${URLConf.rsiAvatarBaseUrl}${member.avatarUrl}' : null;
|
||||||
|
final partyRoomState = ref.watch(partyRoomProvider);
|
||||||
|
final currentUserId = partyRoomState.auth.userInfo?.gameUserId ?? '';
|
||||||
|
final isSelf = member.gameUserId == currentUserId;
|
||||||
|
final flyoutController = FlyoutController();
|
||||||
|
|
||||||
|
return FlyoutTarget(
|
||||||
|
controller: flyoutController,
|
||||||
|
child: Container(
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 1),
|
||||||
|
child: GestureDetector(
|
||||||
|
onSecondaryTapUp: (details) =>
|
||||||
|
_showMemberContextMenu(context, member, partyRoom, isOwner, isSelf, flyoutController),
|
||||||
|
child: HoverButton(
|
||||||
|
onPressed: null,
|
||||||
|
builder: (context, states) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: states.isHovered ? const Color(0xFF404249) : Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// 头像
|
||||||
|
_buildUserAvatar(member.handleName, avatarUrl: avatarUrl, size: 32, isOwner: isOwner),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
// 名称和状态
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
member.handleName.isNotEmpty ? member.handleName : member.gameUserId,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: member.isOwner ? const Color(0xFFFAA81A) : const Color(0xFFDBDEE1),
|
||||||
|
fontWeight: member.isOwner ? FontWeight.bold : FontWeight.normal,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (member.isOwner) ...[
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
const Icon(FluentIcons.crown, size: 10, color: Color(0xFFFAA81A)),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (member.status.currentLocation.isNotEmpty)
|
||||||
|
Text(
|
||||||
|
member.status.currentLocation,
|
||||||
|
style: const TextStyle(fontSize: 10, color: Color(0xFF80848E)),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// 状态指示器
|
||||||
|
Container(
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: Color(0xFF23A559), // 在线绿色
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showMemberContextMenu(
|
||||||
|
BuildContext context,
|
||||||
|
RoomMember member,
|
||||||
|
PartyRoom partyRoom,
|
||||||
|
bool isOwner,
|
||||||
|
bool isSelf,
|
||||||
|
FlyoutController controller,
|
||||||
|
) {
|
||||||
|
final menuItems = <MenuFlyoutItemBase>[
|
||||||
|
// 复制ID - 所有用户可用
|
||||||
|
MenuFlyoutItem(
|
||||||
|
leading: const Icon(FluentIcons.copy, size: 16),
|
||||||
|
text: const Text('复制用户ID'),
|
||||||
|
onPressed: () async {
|
||||||
|
await Clipboard.setData(ClipboardData(text: member.gameUserId));
|
||||||
|
},
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
// 房主专属功能 - 不能对自己和其他房主使用
|
||||||
|
if (isOwner && !member.isOwner && !isSelf) {
|
||||||
|
menuItems.addAll([
|
||||||
|
const MenuFlyoutSeparator(),
|
||||||
|
MenuFlyoutItem(
|
||||||
|
leading: const Icon(FluentIcons.people, size: 16),
|
||||||
|
text: const Text('转移房主'),
|
||||||
|
onPressed: () async {
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => ContentDialog(
|
||||||
|
title: const Text('转移房主'),
|
||||||
|
content: Text('确定要将房主转移给 ${member.handleName.isNotEmpty ? member.handleName : member.gameUserId} 吗?'),
|
||||||
|
actions: [
|
||||||
|
Button(child: const Text('取消'), onPressed: () => Navigator.pop(context, false)),
|
||||||
|
FilledButton(child: const Text('转移'), onPressed: () => Navigator.pop(context, true)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (confirmed == true && context.mounted) {
|
||||||
|
try {
|
||||||
|
await partyRoom.transferOwnership(member.gameUserId);
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => ContentDialog(
|
||||||
|
title: const Text('操作失败'),
|
||||||
|
content: Text('转移房主失败:$e'),
|
||||||
|
actions: [FilledButton(child: const Text('确定'), onPressed: () => Navigator.pop(context))],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
MenuFlyoutItem(
|
||||||
|
leading: const Icon(FluentIcons.remove_from_shopping_list, size: 16),
|
||||||
|
text: const Text('踢出成员'),
|
||||||
|
onPressed: () async {
|
||||||
|
final confirmed = await showDialog<bool>(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => ContentDialog(
|
||||||
|
title: const Text('踢出成员'),
|
||||||
|
content: Text('确定要踢出 ${member.handleName.isNotEmpty ? member.handleName : member.gameUserId} 吗?'),
|
||||||
|
actions: [
|
||||||
|
Button(child: const Text('取消'), onPressed: () => Navigator.pop(context, false)),
|
||||||
|
FilledButton(
|
||||||
|
style: ButtonStyle(backgroundColor: WidgetStateProperty.all(const Color(0xFFDA373C))),
|
||||||
|
child: const Text('踢出'),
|
||||||
|
onPressed: () => Navigator.pop(context, true),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (confirmed == true && context.mounted) {
|
||||||
|
try {
|
||||||
|
await partyRoom.kickMember(member.gameUserId);
|
||||||
|
} catch (e) {
|
||||||
|
if (context.mounted) {
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => ContentDialog(
|
||||||
|
title: const Text('操作失败'),
|
||||||
|
content: Text('踢出成员失败:$e'),
|
||||||
|
actions: [FilledButton(child: const Text('确定'), onPressed: () => Navigator.pop(context))],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.showFlyout(
|
||||||
|
autoModeConfiguration: FlyoutAutoConfiguration(preferredMode: FlyoutPlacementMode.bottomCenter),
|
||||||
|
barrierColor: Colors.transparent,
|
||||||
|
builder: (context) {
|
||||||
|
return MenuFlyout(items: menuItems);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildUserAvatar(String memberName, {String? avatarUrl, bool isOwner = false, double size = 32}) {
|
||||||
|
final avatarWidget = SizedBox(
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
child: avatarUrl == null
|
||||||
|
? CircleAvatar(
|
||||||
|
radius: 16,
|
||||||
|
backgroundColor: const Color(0xFF5865F2),
|
||||||
|
child: Text(
|
||||||
|
memberName.toUpperCase(),
|
||||||
|
style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(100),
|
||||||
|
child: CacheNetImage(url: avatarUrl),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (isOwner) return LocalHero(tag: 'party_room_detail_hero', child: avatarWidget);
|
||||||
|
return avatarWidget;
|
||||||
|
}
|
||||||
|
}
|
||||||
305
lib/ui/party_room/widgets/detail/party_room_message_list.dart
Normal file
305
lib/ui/party_room/widgets/detail/party_room_message_list.dart
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:starcitizen_doctor/common/conf/url_conf.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/widgets/src/cache_image.dart';
|
||||||
|
import 'package:url_launcher/url_launcher_string.dart';
|
||||||
|
|
||||||
|
/// 消息列表组件
|
||||||
|
class PartyRoomMessageList extends ConsumerWidget {
|
||||||
|
final List events;
|
||||||
|
final ScrollController scrollController;
|
||||||
|
|
||||||
|
const PartyRoomMessageList({super.key, required this.events, required this.scrollController});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final partyRoomState = ref.watch(partyRoomProvider);
|
||||||
|
final room = partyRoomState.room.currentRoom;
|
||||||
|
final hasSocialLinks = room != null && room.socialLinks.isNotEmpty;
|
||||||
|
|
||||||
|
// 计算总项数:社交链接消息(如果有)+ 事件消息
|
||||||
|
final totalItems = (hasSocialLinks ? 1 : 0) + events.length;
|
||||||
|
|
||||||
|
if (totalItems == 0) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Icon(FluentIcons.chat, size: 64, color: Color(0xFF404249)),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text('暂无消息', style: TextStyle(color: Colors.white.withValues(alpha: 0.5), fontSize: 14)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text('发送一条信号开始对话吧!', style: TextStyle(color: Colors.white.withValues(alpha: 0.3), fontSize: 12)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.builder(
|
||||||
|
controller: scrollController,
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
itemCount: totalItems,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
// 第一条消息显示社交链接(如果有)
|
||||||
|
if (hasSocialLinks && index == 0) {
|
||||||
|
return _buildSocialLinksMessage(room);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其他消息显示事件
|
||||||
|
final eventIndex = hasSocialLinks ? index - 1 : index;
|
||||||
|
final event = events[eventIndex];
|
||||||
|
return _MessageItem(event: event);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSocialLinksMessage(dynamic room) {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF2B2D31),
|
||||||
|
border: Border.all(color: const Color(0xFF5865F2).withValues(alpha: 0.3)),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(6),
|
||||||
|
decoration: const BoxDecoration(color: Color(0xFF5865F2), shape: BoxShape.circle),
|
||||||
|
child: const Icon(FluentIcons.info, size: 14, color: Colors.white),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
const Expanded(
|
||||||
|
child: Text(
|
||||||
|
'该房间包含第三方社交连接,点击加入一起开黑吧~',
|
||||||
|
style: TextStyle(fontSize: 14, color: Color(0xFFDBDEE1), fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: room.socialLinks.map<Widget>((link) {
|
||||||
|
return HyperlinkButton(
|
||||||
|
onPressed: () => launchUrlString(link),
|
||||||
|
style: ButtonStyle(
|
||||||
|
padding: WidgetStateProperty.all(const EdgeInsets.symmetric(horizontal: 12, vertical: 8)),
|
||||||
|
backgroundColor: WidgetStateProperty.resolveWith((states) {
|
||||||
|
if (states.isHovered) return const Color(0xFF4752C4);
|
||||||
|
return const Color(0xFF5865F2);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(_getSocialIcon(link), size: 16, color: Colors.white),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
_getSocialName(link),
|
||||||
|
style: const TextStyle(fontSize: 13, color: Colors.white, fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData _getSocialIcon(String link) {
|
||||||
|
if (link.contains('qq.com')) return FontAwesomeIcons.qq;
|
||||||
|
if (link.contains('discord')) return FontAwesomeIcons.discord;
|
||||||
|
if (link.contains('kook')) return FluentIcons.chat;
|
||||||
|
return FluentIcons.link;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getSocialName(String link) {
|
||||||
|
if (link.contains('discord')) return 'Discord';
|
||||||
|
if (link.contains('kook')) return 'KOOK';
|
||||||
|
if (link.contains('qq')) return 'QQ';
|
||||||
|
return '链接';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 消息项
|
||||||
|
class _MessageItem extends ConsumerWidget {
|
||||||
|
final dynamic event;
|
||||||
|
|
||||||
|
const _MessageItem({required this.event});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final roomEvent = event as partroom.RoomEvent;
|
||||||
|
final isSignal = roomEvent.type == partroom.RoomEventType.SIGNAL_BROADCAST;
|
||||||
|
final userName = _getEventUserName(roomEvent);
|
||||||
|
final avatarUrl = _getEventAvatarUrl(roomEvent);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
_buildUserAvatar(userName, avatarUrl: avatarUrl, size: 28),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
// 消息内容
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
userName,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: isSignal ? Colors.white : const Color(0xFF80848E),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
_formatTime(roomEvent.timestamp),
|
||||||
|
style: const TextStyle(fontSize: 11, color: Color(0xFF80848E)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
_getEventText(roomEvent, ref),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: isSignal ? const Color(0xFFDBDEE1) : const Color(0xFF949BA4),
|
||||||
|
fontStyle: isSignal ? FontStyle.normal : FontStyle.italic,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getEventUserName(partroom.RoomEvent event) {
|
||||||
|
switch (event.type) {
|
||||||
|
case partroom.RoomEventType.SIGNAL_BROADCAST:
|
||||||
|
return event.signalSender.isNotEmpty ? event.signalSender : '未知用户';
|
||||||
|
case partroom.RoomEventType.MEMBER_JOINED:
|
||||||
|
case partroom.RoomEventType.MEMBER_LEFT:
|
||||||
|
case partroom.RoomEventType.MEMBER_KICKED:
|
||||||
|
return event.hasMember() && event.member.handleName.isNotEmpty
|
||||||
|
? event.member.handleName
|
||||||
|
: event.hasMember()
|
||||||
|
? event.member.gameUserId
|
||||||
|
: '未知用户';
|
||||||
|
case partroom.RoomEventType.OWNER_CHANGED:
|
||||||
|
return event.hasMember() && event.member.handleName.isNotEmpty ? event.member.handleName : '新房主';
|
||||||
|
default:
|
||||||
|
return '系统';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _getEventAvatarUrl(partroom.RoomEvent event) {
|
||||||
|
if (event.type == partroom.RoomEventType.SIGNAL_BROADCAST ||
|
||||||
|
event.type == partroom.RoomEventType.MEMBER_JOINED ||
|
||||||
|
event.type == partroom.RoomEventType.MEMBER_LEFT ||
|
||||||
|
event.type == partroom.RoomEventType.MEMBER_KICKED ||
|
||||||
|
event.type == partroom.RoomEventType.OWNER_CHANGED) {
|
||||||
|
if (event.hasMember() && event.member.avatarUrl.isNotEmpty) {
|
||||||
|
return '${URLConf.rsiAvatarBaseUrl}${event.member.avatarUrl}';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getEventText(partroom.RoomEvent event, WidgetRef ref) {
|
||||||
|
final partyRoomState = ref.read(partyRoomProvider);
|
||||||
|
final signalTypes = partyRoomState.room.signalTypes;
|
||||||
|
switch (event.type) {
|
||||||
|
case partroom.RoomEventType.SIGNAL_BROADCAST:
|
||||||
|
final signalType = signalTypes[event.signalId];
|
||||||
|
if (event.signalId.isNotEmpty) {
|
||||||
|
if (event.signalParams.isNotEmpty) {
|
||||||
|
final params = event.signalParams;
|
||||||
|
return "signalId: ${event.signalId},params:$params";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return signalType?.name ?? event.signalId;
|
||||||
|
case partroom.RoomEventType.MEMBER_JOINED:
|
||||||
|
return '加入了房间';
|
||||||
|
case partroom.RoomEventType.MEMBER_LEFT:
|
||||||
|
return '离开了房间';
|
||||||
|
case partroom.RoomEventType.OWNER_CHANGED:
|
||||||
|
return '成为了新房主';
|
||||||
|
case partroom.RoomEventType.ROOM_UPDATED:
|
||||||
|
return '房间信息已更新';
|
||||||
|
case partroom.RoomEventType.MEMBER_STATUS_UPDATED:
|
||||||
|
if (event.hasMember()) {
|
||||||
|
final member = event.member;
|
||||||
|
final name = member.handleName.isNotEmpty ? member.handleName : member.gameUserId;
|
||||||
|
if (member.hasStatus() && member.status.currentLocation.isNotEmpty) {
|
||||||
|
return '$name 更新了状态: ${member.status.currentLocation}';
|
||||||
|
}
|
||||||
|
return '$name 更新了状态';
|
||||||
|
}
|
||||||
|
return '成员状态已更新';
|
||||||
|
case partroom.RoomEventType.ROOM_DISMISSED:
|
||||||
|
return '房间已解散';
|
||||||
|
case partroom.RoomEventType.MEMBER_KICKED:
|
||||||
|
return '被踢出房间';
|
||||||
|
default:
|
||||||
|
return '未知事件';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _formatTime(dynamic timestamp) {
|
||||||
|
try {
|
||||||
|
final date = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt() * 1000);
|
||||||
|
final now = DateTime.now();
|
||||||
|
final diff = now.difference(date);
|
||||||
|
|
||||||
|
if (diff.inMinutes < 1) {
|
||||||
|
return '刚刚';
|
||||||
|
} else if (diff.inMinutes < 60) {
|
||||||
|
return '${diff.inMinutes} 分钟前';
|
||||||
|
} else if (diff.inHours < 24) {
|
||||||
|
return '${diff.inHours} 小时前';
|
||||||
|
} else {
|
||||||
|
return '${diff.inDays} 天前';
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildUserAvatar(String memberName, {String? avatarUrl, double size = 32}) {
|
||||||
|
return SizedBox(
|
||||||
|
width: size,
|
||||||
|
height: size,
|
||||||
|
child: avatarUrl == null
|
||||||
|
? CircleAvatar(
|
||||||
|
radius: 16,
|
||||||
|
backgroundColor: const Color(0xFF5865F2),
|
||||||
|
child: Text(
|
||||||
|
memberName.toUpperCase(),
|
||||||
|
style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(100),
|
||||||
|
child: CacheNetImage(url: avatarUrl),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
import 'package:fluent_ui/fluent_ui.dart';
|
||||||
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:starcitizen_doctor/provider/party_room.dart';
|
||||||
|
|
||||||
|
/// 信号发送器组件
|
||||||
|
class PartyRoomSignalSender extends ConsumerWidget {
|
||||||
|
final PartyRoom partyRoom;
|
||||||
|
final dynamic room;
|
||||||
|
|
||||||
|
const PartyRoomSignalSender({super.key, required this.partyRoom, required this.room});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final partyRoomState = ref.watch(partyRoomProvider);
|
||||||
|
final signalTypes = partyRoomState.room.signalTypes.values.where((s) => !s.isSpecial).toList();
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF2B2D31).withValues(alpha: .4),
|
||||||
|
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.3))),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Spacer(),
|
||||||
|
DropDownButton(
|
||||||
|
leading: const Icon(FluentIcons.send, size: 16),
|
||||||
|
title: Text(signalTypes.isEmpty ? '加载中...' : '发送信号'),
|
||||||
|
disabled: signalTypes.isEmpty || room == null,
|
||||||
|
items: signalTypes.map((signal) {
|
||||||
|
return MenuFlyoutItem(
|
||||||
|
leading: const Icon(FluentIcons.radio_bullet, size: 16),
|
||||||
|
text: Text(signal.name.isNotEmpty ? signal.name : signal.id),
|
||||||
|
onPressed: () => _sendSignal(context, signal),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _sendSignal(BuildContext context, dynamic signal) async {
|
||||||
|
if (room == null) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await partyRoom.sendSignal(signal.id);
|
||||||
|
// 信号已发送,会通过事件流更新到消息列表
|
||||||
|
} catch (e) {
|
||||||
|
// 显示错误提示
|
||||||
|
if (context.mounted) {
|
||||||
|
await showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => ContentDialog(
|
||||||
|
title: const Text('发送失败'),
|
||||||
|
content: Text(e.toString()),
|
||||||
|
actions: [FilledButton(child: const Text('确定'), onPressed: () => Navigator.pop(context))],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,689 +0,0 @@
|
|||||||
import 'package:fluent_ui/fluent_ui.dart';
|
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|
||||||
import 'package:starcitizen_doctor/common/conf/url_conf.dart';
|
|
||||||
import 'package:starcitizen_doctor/generated/proto/partroom/partroom.pb.dart' as partroom;
|
|
||||||
import 'package:starcitizen_doctor/generated/proto/partroom/partroom.pb.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/widgets/src/cache_image.dart';
|
|
||||||
import 'package:url_launcher/url_launcher_string.dart';
|
|
||||||
|
|
||||||
/// 房间详情页面 (Discord 样式)
|
|
||||||
class PartyRoomDetailPage extends ConsumerStatefulWidget {
|
|
||||||
const PartyRoomDetailPage({super.key});
|
|
||||||
|
|
||||||
@override
|
|
||||||
ConsumerState<PartyRoomDetailPage> createState() => _PartyRoomDetailPageState();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _PartyRoomDetailPageState extends ConsumerState<PartyRoomDetailPage> {
|
|
||||||
final ScrollController _scrollController = ScrollController();
|
|
||||||
int _lastEventCount = 0;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void dispose() {
|
|
||||||
_scrollController.dispose();
|
|
||||||
super.dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
void _scrollToBottom() {
|
|
||||||
if (_scrollController.hasClients) {
|
|
||||||
Future.delayed(const Duration(milliseconds: 100), () {
|
|
||||||
if (_scrollController.hasClients) {
|
|
||||||
_scrollController.animateTo(
|
|
||||||
_scrollController.position.maxScrollExtent,
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
curve: Curves.easeOut,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final partyRoomState = ref.watch(partyRoomProvider);
|
|
||||||
final partyRoom = ref.read(partyRoomProvider.notifier);
|
|
||||||
final room = partyRoomState.room.currentRoom;
|
|
||||||
final members = partyRoomState.room.members;
|
|
||||||
final isOwner = partyRoomState.room.isOwner;
|
|
||||||
final events = partyRoomState.room.recentEvents;
|
|
||||||
|
|
||||||
// 检测消息数量变化,触发滚动
|
|
||||||
if (events.length != _lastEventCount) {
|
|
||||||
_lastEventCount = events.length;
|
|
||||||
if (events.isNotEmpty) {
|
|
||||||
_scrollToBottom();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ScaffoldPage(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
content: Row(
|
|
||||||
children: [
|
|
||||||
// 左侧成员列表 (类似 Discord 侧边栏)
|
|
||||||
Container(
|
|
||||||
width: 240,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Color(0xFF232428).withValues(alpha: .3),
|
|
||||||
border: Border(right: BorderSide(color: Colors.black.withValues(alpha: 0.3), width: 1)),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
// 房间信息头部
|
|
||||||
_buildRoomHeader(context, room, members, isOwner, partyRoom),
|
|
||||||
const Divider(
|
|
||||||
style: DividerThemeData(thickness: 1, decoration: BoxDecoration(color: Color(0xFF1E1F22))),
|
|
||||||
),
|
|
||||||
// 成员列表
|
|
||||||
Expanded(child: _buildMembersSidebar(context, ref, members, isOwner, partyRoom)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 右侧消息区域
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
// 消息列表
|
|
||||||
Expanded(child: _buildMessageList(context, events, _scrollController, ref)),
|
|
||||||
// 信号发送按钮
|
|
||||||
_buildSignalSender(context, ref, partyRoom, room),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 房间信息头部
|
|
||||||
Widget _buildRoomHeader(BuildContext context, dynamic room, List members, bool isOwner, PartyRoom partyRoom) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Icon(FluentIcons.room, size: 16, color: Color(0xFFB5BAC1)),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
room?.ownerGameId ?? '房间',
|
|
||||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.white),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Icon(FluentIcons.group, size: 12, color: Color(0xFF80848E)),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
'${members.length}/${room?.targetMembers ?? 0} 成员',
|
|
||||||
style: const TextStyle(fontSize: 11, color: Color(0xFF80848E)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (isOwner) ...[
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
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 ...[
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Button(
|
|
||||||
onPressed: () async {
|
|
||||||
await partyRoom.leaveRoom();
|
|
||||||
},
|
|
||||||
style: ButtonStyle(backgroundColor: WidgetStateProperty.all(const Color(0xFF404249))),
|
|
||||||
child: const Text('离开房间', style: TextStyle(color: Color(0xFFB5BAC1))),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
IconData _getSocialIcon(String link) {
|
|
||||||
if (link.contains('qq.com')) return FontAwesomeIcons.qq;
|
|
||||||
if (link.contains('discord')) return FontAwesomeIcons.discord;
|
|
||||||
if (link.contains('kook')) return FluentIcons.chat;
|
|
||||||
return FluentIcons.link;
|
|
||||||
}
|
|
||||||
|
|
||||||
String _getSocialName(String link) {
|
|
||||||
if (link.contains('discord')) return 'Discord';
|
|
||||||
if (link.contains('kook')) return 'KOOK';
|
|
||||||
if (link.contains('qq')) return 'QQ';
|
|
||||||
return '链接';
|
|
||||||
}
|
|
||||||
|
|
||||||
// 成员侧边栏
|
|
||||||
Widget _buildMembersSidebar(BuildContext context, WidgetRef ref, List members, bool isOwner, PartyRoom partyRoom) {
|
|
||||||
if (members.isEmpty) {
|
|
||||||
return Center(
|
|
||||||
child: Text('暂无成员', style: TextStyle(color: Colors.white.withValues(alpha: 0.5), fontSize: 12)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListView.builder(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
itemCount: members.length,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final member = members[index];
|
|
||||||
return _buildMemberItem(context, ref, member, isOwner, partyRoom);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildMemberItem(BuildContext context, WidgetRef ref, RoomMember member, bool isOwner, PartyRoom partyRoom) {
|
|
||||||
final avatarUrl = member.avatarUrl.isNotEmpty ? '${URLConf.rsiAvatarBaseUrl}${member.avatarUrl}' : null;
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 8, vertical: 1),
|
|
||||||
child: HoverButton(
|
|
||||||
onPressed: isOwner && !member.isOwner ? () => _showMemberContextMenu(context, member, partyRoom) : null,
|
|
||||||
builder: (context, states) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: states.isHovered ? const Color(0xFF404249) : Colors.transparent,
|
|
||||||
borderRadius: BorderRadius.circular(4),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
// 头像
|
|
||||||
makeUserAvatar(member.handleName, avatarUrl: avatarUrl, size: 32),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
// 名称和状态
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Flexible(
|
|
||||||
child: Text(
|
|
||||||
member.handleName.isNotEmpty ? member.handleName : member.gameUserId,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
color: member.isOwner ? const Color(0xFFFAA81A) : const Color(0xFFDBDEE1),
|
|
||||||
fontWeight: member.isOwner ? FontWeight.bold : FontWeight.normal,
|
|
||||||
),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (member.isOwner) ...[
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
const Icon(FluentIcons.crown, size: 10, color: Color(0xFFFAA81A)),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
if (member.status.currentLocation.isNotEmpty)
|
|
||||||
Text(
|
|
||||||
member.status.currentLocation,
|
|
||||||
style: const TextStyle(fontSize: 10, color: Color(0xFF80848E)),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 状态指示器
|
|
||||||
Container(
|
|
||||||
width: 8,
|
|
||||||
height: 8,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Color(0xFF23A559), // 在线绿色
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget makeUserAvatar(String memberName, {String? avatarUrl, double size = 32}) {
|
|
||||||
return SizedBox(
|
|
||||||
width: size,
|
|
||||||
height: size,
|
|
||||||
child: avatarUrl == null
|
|
||||||
? CircleAvatar(
|
|
||||||
radius: 16,
|
|
||||||
backgroundColor: const Color(0xFF5865F2),
|
|
||||||
child: Text(
|
|
||||||
memberName.toUpperCase(),
|
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(100),
|
|
||||||
child: CacheNetImage(url: avatarUrl),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showMemberContextMenu(BuildContext context, dynamic member, PartyRoom partyRoom) async {
|
|
||||||
await showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => ContentDialog(
|
|
||||||
title: Text(member.handleName.isNotEmpty ? member.handleName : member.gameUserId),
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
FilledButton(
|
|
||||||
onPressed: () async {
|
|
||||||
Navigator.pop(context);
|
|
||||||
final confirmed = await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => ContentDialog(
|
|
||||||
title: const Text('转移房主'),
|
|
||||||
content: Text(
|
|
||||||
'确定要将房主转移给 ${member.handleName.isNotEmpty ? member.handleName : member.gameUserId} 吗?',
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
Button(child: const Text('取消'), onPressed: () => Navigator.pop(context, false)),
|
|
||||||
FilledButton(child: const Text('转移'), onPressed: () => Navigator.pop(context, true)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (confirmed == true) {
|
|
||||||
await partyRoom.transferOwnership(member.gameUserId);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
child: const Text('转移房主'),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Button(
|
|
||||||
onPressed: () async {
|
|
||||||
Navigator.pop(context);
|
|
||||||
final confirmed = await showDialog<bool>(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => ContentDialog(
|
|
||||||
title: const Text('踢出成员'),
|
|
||||||
content: Text('确定要踢出 ${member.handleName.isNotEmpty ? member.handleName : member.gameUserId} 吗?'),
|
|
||||||
actions: [
|
|
||||||
Button(child: const Text('取消'), onPressed: () => Navigator.pop(context, false)),
|
|
||||||
FilledButton(
|
|
||||||
style: ButtonStyle(backgroundColor: WidgetStateProperty.all(const Color(0xFFDA373C))),
|
|
||||||
child: const Text('踢出'),
|
|
||||||
onPressed: () => Navigator.pop(context, true),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
if (confirmed == true) {
|
|
||||||
await partyRoom.kickMember(member.gameUserId);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
style: ButtonStyle(backgroundColor: WidgetStateProperty.all(const Color(0xFFDA373C))),
|
|
||||||
child: const Text('踢出成员', style: TextStyle(color: Colors.white)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
actions: [Button(child: const Text('关闭'), onPressed: () => Navigator.pop(context))],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 消息列表
|
|
||||||
Widget _buildMessageList(BuildContext context, List events, ScrollController scrollController, WidgetRef ref) {
|
|
||||||
final partyRoomState = ref.watch(partyRoomProvider);
|
|
||||||
final room = partyRoomState.room.currentRoom;
|
|
||||||
final hasSocialLinks = room != null && room.socialLinks.isNotEmpty;
|
|
||||||
|
|
||||||
// 计算总项数:社交链接消息(如果有)+ 事件消息
|
|
||||||
final totalItems = (hasSocialLinks ? 1 : 0) + events.length;
|
|
||||||
|
|
||||||
if (totalItems == 0) {
|
|
||||||
return Center(
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
const Icon(FluentIcons.chat, size: 64, color: Color(0xFF404249)),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text('暂无消息', style: TextStyle(color: Colors.white.withValues(alpha: 0.5), fontSize: 14)),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text('发送一条信号开始对话吧!', style: TextStyle(color: Colors.white.withValues(alpha: 0.3), fontSize: 12)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return ListView.builder(
|
|
||||||
controller: scrollController,
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
itemCount: totalItems,
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
// 第一条消息显示社交链接(如果有)
|
|
||||||
if (hasSocialLinks && index == 0) {
|
|
||||||
return _buildSocialLinksMessage(room);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 其他消息显示事件
|
|
||||||
final eventIndex = hasSocialLinks ? index - 1 : index;
|
|
||||||
final event = events[eventIndex];
|
|
||||||
return _buildMessageItem(event, ref);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 社交链接系统消息
|
|
||||||
Widget _buildSocialLinksMessage(dynamic room) {
|
|
||||||
return Container(
|
|
||||||
margin: const EdgeInsets.only(bottom: 16),
|
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0xFF2B2D31),
|
|
||||||
border: Border.all(color: const Color(0xFF5865F2).withValues(alpha: 0.3)),
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(6),
|
|
||||||
decoration: const BoxDecoration(color: Color(0xFF5865F2), shape: BoxShape.circle),
|
|
||||||
child: const Icon(FluentIcons.info, size: 14, color: Colors.white),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
const Expanded(
|
|
||||||
child: Text(
|
|
||||||
'该房间包含第三方社交连接,点击加入一起开黑吧~',
|
|
||||||
style: TextStyle(fontSize: 14, color: Color(0xFFDBDEE1), fontWeight: FontWeight.w500),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Wrap(
|
|
||||||
spacing: 8,
|
|
||||||
runSpacing: 8,
|
|
||||||
children: room.socialLinks.map<Widget>((link) {
|
|
||||||
return HyperlinkButton(
|
|
||||||
onPressed: () => launchUrlString(link),
|
|
||||||
style: ButtonStyle(
|
|
||||||
padding: WidgetStateProperty.all(const EdgeInsets.symmetric(horizontal: 12, vertical: 8)),
|
|
||||||
backgroundColor: WidgetStateProperty.resolveWith((states) {
|
|
||||||
if (states.isHovered) return const Color(0xFF4752C4);
|
|
||||||
return const Color(0xFF5865F2);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(_getSocialIcon(link), size: 16, color: Colors.white),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
_getSocialName(link),
|
|
||||||
style: const TextStyle(fontSize: 13, color: Colors.white, fontWeight: FontWeight.w500),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildMessageItem(dynamic event, WidgetRef ref) {
|
|
||||||
final roomEvent = event as partroom.RoomEvent;
|
|
||||||
final isSignal = roomEvent.type == partroom.RoomEventType.SIGNAL_BROADCAST;
|
|
||||||
final userName = _getEventUserName(roomEvent);
|
|
||||||
final avatarUrl = _getEventAvatarUrl(roomEvent);
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
margin: const EdgeInsets.only(bottom: 16),
|
|
||||||
child: Row(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
makeUserAvatar(userName, avatarUrl: avatarUrl, size: 28),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
// 消息内容
|
|
||||||
Expanded(
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
userName,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: isSignal ? Colors.white : const Color(0xFF80848E),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
_formatTime(roomEvent.timestamp),
|
|
||||||
style: const TextStyle(fontSize: 11, color: Color(0xFF80848E)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
_getEventText(roomEvent, ref),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: isSignal ? const Color(0xFFDBDEE1) : const Color(0xFF949BA4),
|
|
||||||
fontStyle: isSignal ? FontStyle.normal : FontStyle.italic,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
String _getEventUserName(partroom.RoomEvent event) {
|
|
||||||
switch (event.type) {
|
|
||||||
case partroom.RoomEventType.SIGNAL_BROADCAST:
|
|
||||||
return event.signalSender.isNotEmpty ? event.signalSender : '未知用户';
|
|
||||||
case partroom.RoomEventType.MEMBER_JOINED:
|
|
||||||
case partroom.RoomEventType.MEMBER_LEFT:
|
|
||||||
case partroom.RoomEventType.MEMBER_KICKED:
|
|
||||||
return event.hasMember() && event.member.handleName.isNotEmpty
|
|
||||||
? event.member.handleName
|
|
||||||
: event.hasMember()
|
|
||||||
? event.member.gameUserId
|
|
||||||
: '未知用户';
|
|
||||||
case partroom.RoomEventType.OWNER_CHANGED:
|
|
||||||
return event.hasMember() && event.member.handleName.isNotEmpty ? event.member.handleName : '新房主';
|
|
||||||
default:
|
|
||||||
return '系统';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String? _getEventAvatarUrl(partroom.RoomEvent event) {
|
|
||||||
if (event.type == partroom.RoomEventType.SIGNAL_BROADCAST ||
|
|
||||||
event.type == partroom.RoomEventType.MEMBER_JOINED ||
|
|
||||||
event.type == partroom.RoomEventType.MEMBER_LEFT ||
|
|
||||||
event.type == partroom.RoomEventType.MEMBER_KICKED ||
|
|
||||||
event.type == partroom.RoomEventType.OWNER_CHANGED) {
|
|
||||||
if (event.hasMember() && event.member.avatarUrl.isNotEmpty) {
|
|
||||||
return '${URLConf.rsiAvatarBaseUrl}${event.member.avatarUrl}';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 信号发送器
|
|
||||||
Widget _buildSignalSender(BuildContext context, WidgetRef ref, PartyRoom partyRoom, dynamic room) {
|
|
||||||
final partyRoomState = ref.watch(partyRoomProvider);
|
|
||||||
final signalTypes = partyRoomState.room.signalTypes.where((s) => !s.isSpecial).toList();
|
|
||||||
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0xFF2B2D31).withValues(alpha: .4),
|
|
||||||
border: Border(top: BorderSide(color: Colors.black.withValues(alpha: 0.3))),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
const Spacer(),
|
|
||||||
DropDownButton(
|
|
||||||
leading: const Icon(FluentIcons.send, size: 16),
|
|
||||||
title: Text(signalTypes.isEmpty ? '加载中...' : '发送信号'),
|
|
||||||
disabled: signalTypes.isEmpty || room == null,
|
|
||||||
items: signalTypes.map((signal) {
|
|
||||||
return MenuFlyoutItem(
|
|
||||||
leading: const Icon(FluentIcons.radio_bullet, size: 16),
|
|
||||||
text: Text(signal.name.isNotEmpty ? signal.name : signal.id),
|
|
||||||
onPressed: () => _sendSignal(context, ref, partyRoom, room, signal),
|
|
||||||
);
|
|
||||||
}).toList(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> _sendSignal(
|
|
||||||
BuildContext context,
|
|
||||||
WidgetRef ref,
|
|
||||||
PartyRoom partyRoom,
|
|
||||||
dynamic room,
|
|
||||||
dynamic signal,
|
|
||||||
) async {
|
|
||||||
if (room == null) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
await partyRoom.sendSignal(signal.id);
|
|
||||||
|
|
||||||
// 发送成功后,显示在消息列表中
|
|
||||||
if (context.mounted) {
|
|
||||||
// 信号已发送,会通过事件流更新到消息列表
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// 显示错误提示
|
|
||||||
if (context.mounted) {
|
|
||||||
await showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => ContentDialog(
|
|
||||||
title: const Text('发送失败'),
|
|
||||||
content: Text(e.toString()),
|
|
||||||
actions: [FilledButton(child: const Text('确定'), onPressed: () => Navigator.pop(context))],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _getEventText(partroom.RoomEvent event, WidgetRef ref) {
|
|
||||||
final partyRoomState = ref.read(partyRoomProvider);
|
|
||||||
final signalTypes = partyRoomState.room.signalTypes;
|
|
||||||
switch (event.type) {
|
|
||||||
case partroom.RoomEventType.SIGNAL_BROADCAST:
|
|
||||||
// 从 signalTypes 提取信号名称
|
|
||||||
final signalType = signalTypes.where((s) => s.id == event.signalId).firstOrNull;
|
|
||||||
// 显示信号ID和参数
|
|
||||||
if (event.signalId.isNotEmpty) {
|
|
||||||
if (event.signalParams.isNotEmpty) {
|
|
||||||
final params = event.signalParams;
|
|
||||||
return "signalId: ${event.signalId},params:$params";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return signalType?.name ?? event.signalId;
|
|
||||||
|
|
||||||
case partroom.RoomEventType.MEMBER_JOINED:
|
|
||||||
return '加入了房间';
|
|
||||||
|
|
||||||
case partroom.RoomEventType.MEMBER_LEFT:
|
|
||||||
return '离开了房间';
|
|
||||||
|
|
||||||
case partroom.RoomEventType.OWNER_CHANGED:
|
|
||||||
return '成为了新房主';
|
|
||||||
|
|
||||||
case partroom.RoomEventType.ROOM_UPDATED:
|
|
||||||
return '房间信息已更新';
|
|
||||||
|
|
||||||
case partroom.RoomEventType.MEMBER_STATUS_UPDATED:
|
|
||||||
if (event.hasMember()) {
|
|
||||||
final member = event.member;
|
|
||||||
final name = member.handleName.isNotEmpty ? member.handleName : member.gameUserId;
|
|
||||||
if (member.hasStatus() && member.status.currentLocation.isNotEmpty) {
|
|
||||||
return '$name 更新了状态: ${member.status.currentLocation}';
|
|
||||||
}
|
|
||||||
return '$name 更新了状态';
|
|
||||||
}
|
|
||||||
return '成员状态已更新';
|
|
||||||
|
|
||||||
case partroom.RoomEventType.ROOM_DISMISSED:
|
|
||||||
return '房间已解散';
|
|
||||||
|
|
||||||
case partroom.RoomEventType.MEMBER_KICKED:
|
|
||||||
return '被踢出房间';
|
|
||||||
|
|
||||||
default:
|
|
||||||
return '未知事件';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String _formatTime(dynamic timestamp) {
|
|
||||||
try {
|
|
||||||
final date = DateTime.fromMillisecondsSinceEpoch(timestamp.toInt() * 1000);
|
|
||||||
final now = DateTime.now();
|
|
||||||
final diff = now.difference(date);
|
|
||||||
|
|
||||||
if (diff.inMinutes < 1) {
|
|
||||||
return '刚刚';
|
|
||||||
} else if (diff.inMinutes < 60) {
|
|
||||||
return '${diff.inMinutes} 分钟前';
|
|
||||||
} else if (diff.inHours < 24) {
|
|
||||||
return '${diff.inHours} 小时前';
|
|
||||||
} else {
|
|
||||||
return '${diff.inDays} 天前';
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,11 +1,14 @@
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:extended_image/extended_image.dart';
|
||||||
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:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||||
import 'package:flutter_tilt/flutter_tilt.dart';
|
import 'package:flutter_tilt/flutter_tilt.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:local_hero/local_hero.dart';
|
||||||
import 'package:starcitizen_doctor/common/conf/url_conf.dart';
|
import 'package:starcitizen_doctor/common/conf/url_conf.dart';
|
||||||
|
import 'package:starcitizen_doctor/generated/proto/partroom/partroom.pb.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';
|
import 'package:starcitizen_doctor/ui/party_room/widgets/create_room_dialog.dart';
|
||||||
@ -86,6 +89,53 @@ class PartyRoomListPage extends HookConsumerWidget {
|
|||||||
Expanded(child: _buildRoomList(context, ref, uiState, partyRoom, scrollController)),
|
Expanded(child: _buildRoomList(context, ref, uiState, partyRoom, scrollController)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
bottomBar: _buildFloatingRoomButton(context, ref, partyRoomState),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget? _buildFloatingRoomButton(BuildContext context, WidgetRef ref, PartyRoomFullState partyRoomState) {
|
||||||
|
if (!partyRoomState.room.isInRoom || partyRoomState.room.currentRoom == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final currentRoom = partyRoomState.room.currentRoom!;
|
||||||
|
final owner = partyRoomState.room.members.firstWhere(
|
||||||
|
(m) => m.gameUserId == currentRoom.ownerGameId,
|
||||||
|
orElse: () => RoomMember(),
|
||||||
|
);
|
||||||
|
final avatarUrl = owner.avatarUrl.isNotEmpty ? '${URLConf.rsiAvatarBaseUrl}${owner.avatarUrl}' : '';
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
alignment: Alignment.bottomRight,
|
||||||
|
child: Tooltip(
|
||||||
|
message: '返回当前房间',
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
ref.read(partyRoomUIModelProvider.notifier).setMinimized(false);
|
||||||
|
},
|
||||||
|
child: LocalHero(
|
||||||
|
tag: 'party_room_detail_hero',
|
||||||
|
child: Container(
|
||||||
|
width: 56,
|
||||||
|
height: 56,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFF2D2D2D),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(color: Colors.black.withValues(alpha: 0.3), blurRadius: 8, offset: const Offset(0, 4)),
|
||||||
|
],
|
||||||
|
border: Border.all(color: const Color(0xFF4A9EFF), width: 2),
|
||||||
|
),
|
||||||
|
child: ClipOval(
|
||||||
|
child: avatarUrl.isNotEmpty
|
||||||
|
? CacheNetImage(url: avatarUrl, fit: BoxFit.cover)
|
||||||
|
: const Icon(FluentIcons.group, color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,7 +152,7 @@ class PartyRoomListPage extends HookConsumerWidget {
|
|||||||
value: uiState.selectedMainTagId,
|
value: uiState.selectedMainTagId,
|
||||||
items: [
|
items: [
|
||||||
const ComboBoxItem(value: null, child: Text('全部标签')),
|
const ComboBoxItem(value: null, child: Text('全部标签')),
|
||||||
...tags.map((tag) => ComboBoxItem(value: tag.id, child: Text(tag.name))),
|
...tags.values.map((tag) => ComboBoxItem(value: tag.id, child: Text(tag.name))),
|
||||||
],
|
],
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
ref.read(partyRoomUIModelProvider.notifier).setSelectedMainTagId(value);
|
ref.read(partyRoomUIModelProvider.notifier).setSelectedMainTagId(value);
|
||||||
@ -189,6 +239,8 @@ class PartyRoomListPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
Widget _buildRoomCard(BuildContext context, WidgetRef ref, PartyRoom partyRoom, dynamic room, int index) {
|
Widget _buildRoomCard(BuildContext context, WidgetRef ref, PartyRoom partyRoom, dynamic room, int index) {
|
||||||
final avatarUrl = room.ownerAvatar.isNotEmpty ? '${URLConf.rsiAvatarBaseUrl}${room.ownerAvatar}' : '';
|
final avatarUrl = room.ownerAvatar.isNotEmpty ? '${URLConf.rsiAvatarBaseUrl}${room.ownerAvatar}' : '';
|
||||||
|
final partyRoomState = ref.watch(partyRoomProvider);
|
||||||
|
final isCurrentRoom = partyRoomState.room.isInRoom && partyRoomState.room.roomUuid == room.roomUuid;
|
||||||
|
|
||||||
return GridItemAnimator(
|
return GridItemAnimator(
|
||||||
index: index,
|
index: index,
|
||||||
@ -197,6 +249,7 @@ class PartyRoomListPage extends HookConsumerWidget {
|
|||||||
child: Tilt(
|
child: Tilt(
|
||||||
shadowConfig: const ShadowConfig(maxIntensity: .3),
|
shadowConfig: const ShadowConfig(maxIntensity: .3),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: isCurrentRoom ? Border.all(color: Colors.green, width: 2) : null,
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12)),
|
decoration: BoxDecoration(borderRadius: BorderRadius.circular(12)),
|
||||||
@ -234,7 +287,9 @@ class PartyRoomListPage extends HookConsumerWidget {
|
|||||||
CircleAvatar(
|
CircleAvatar(
|
||||||
radius: 24,
|
radius: 24,
|
||||||
backgroundColor: const Color(0xFF4A9EFF).withValues(alpha: 0.5),
|
backgroundColor: const Color(0xFF4A9EFF).withValues(alpha: 0.5),
|
||||||
backgroundImage: avatarUrl.isNotEmpty ? NetworkImage(avatarUrl) : null,
|
backgroundImage: avatarUrl.isNotEmpty
|
||||||
|
? ExtendedNetworkImageProvider(avatarUrl, cache: true)
|
||||||
|
: null,
|
||||||
child: avatarUrl.isEmpty ? const Icon(FluentIcons.contact, color: Colors.white) : null,
|
child: avatarUrl.isEmpty ? const Icon(FluentIcons.contact, color: Colors.white) : null,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
@ -332,29 +387,68 @@ class PartyRoomListPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _joinRoom(BuildContext context, WidgetRef ref, PartyRoom partyRoom, dynamic room) async {
|
Future<void> _joinRoom(BuildContext context, WidgetRef ref, PartyRoom partyRoom, dynamic room) async {
|
||||||
|
final partyRoomState = ref.read(partyRoomProvider);
|
||||||
|
|
||||||
|
// 如果已经在房间中
|
||||||
|
if (partyRoomState.room.isInRoom) {
|
||||||
|
// 如果点击的是当前房间,直接返回
|
||||||
|
if (partyRoomState.room.roomUuid == room.roomUuid) {
|
||||||
|
ref.read(partyRoomUIModelProvider.notifier).setMinimized(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果点击的是其他房间,提示用户
|
||||||
|
if (context.mounted) {
|
||||||
|
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(child: const Text('继续'), onPressed: () => Navigator.pop(context, true)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (confirmed != true) return;
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 退出当前房间
|
||||||
|
await partyRoom.leaveRoom();
|
||||||
|
}
|
||||||
|
|
||||||
String? password;
|
String? password;
|
||||||
|
|
||||||
if (room.hasPassword) {
|
if (room.hasPassword) {
|
||||||
password = await showDialog<String>(
|
if (context.mounted) {
|
||||||
context: context,
|
password = await showDialog<String>(
|
||||||
builder: (context) {
|
context: context,
|
||||||
final passwordController = TextEditingController();
|
builder: (context) {
|
||||||
return ContentDialog(
|
final passwordController = TextEditingController();
|
||||||
title: const Text('输入房间密码'),
|
return ContentDialog(
|
||||||
content: TextBox(controller: passwordController, placeholder: '请输入密码', obscureText: true),
|
title: const Text('输入房间密码'),
|
||||||
actions: [
|
content: TextBox(controller: passwordController, placeholder: '请输入密码', obscureText: true),
|
||||||
Button(child: const Text('取消'), onPressed: () => Navigator.pop(context)),
|
actions: [
|
||||||
FilledButton(child: const Text('加入'), onPressed: () => Navigator.pop(context, passwordController.text)),
|
Button(child: const Text('取消'), onPressed: () => Navigator.pop(context)),
|
||||||
],
|
FilledButton(child: const Text('加入'), onPressed: () => Navigator.pop(context, passwordController.text)),
|
||||||
);
|
],
|
||||||
},
|
);
|
||||||
);
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (password == null) return;
|
if (password == null) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await partyRoom.joinRoom(room.roomUuid, password: password);
|
await partyRoom.joinRoom(room.roomUuid, password: password);
|
||||||
|
// 加入成功后,确保不处于最小化状态
|
||||||
|
ref.read(partyRoomUIModelProvider.notifier).setMinimized(false);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
await showDialog(
|
await showDialog(
|
||||||
|
|||||||
@ -830,6 +830,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.0.0"
|
version: "6.0.0"
|
||||||
|
local_hero:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: local_hero
|
||||||
|
sha256: "5c85451dd51ecd0e8d3656775fac9a6db82f296f200d9931217186d34fed6089"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.3.0"
|
||||||
logging:
|
logging:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -36,6 +36,7 @@ dependencies:
|
|||||||
markdown: ^7.3.0
|
markdown: ^7.3.0
|
||||||
markdown_widget: ^2.3.2+8
|
markdown_widget: ^2.3.2+8
|
||||||
extended_image: ^10.0.1
|
extended_image: ^10.0.1
|
||||||
|
local_hero: ^0.3.0
|
||||||
device_info_plus: ^12.2.0
|
device_info_plus: ^12.2.0
|
||||||
file_picker: ^10.3.6
|
file_picker: ^10.3.6
|
||||||
file_sizes: ^1.0.6
|
file_sizes: ^1.0.6
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user