mirror of
https://github.com/StarCitizenToolBox/app.git
synced 2026-02-12 02:00:22 +00:00
feat: update messages
This commit is contained in:
@@ -1,23 +1,28 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:starcitizen_doctor/common/utils/base_utils.dart';
|
||||
import 'package:starcitizen_doctor/generated/proto/partroom/partroom.pb.dart' as partroom;
|
||||
import 'package:starcitizen_doctor/provider/party_room.dart';
|
||||
|
||||
/// 创建房间对话框
|
||||
/// 创建/编辑房间对话框
|
||||
class CreateRoomDialog extends HookConsumerWidget {
|
||||
const CreateRoomDialog({super.key});
|
||||
final partroom.RoomInfo? roomInfo;
|
||||
|
||||
const CreateRoomDialog({super.key, this.roomInfo});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final partyRoomState = ref.watch(partyRoomProvider);
|
||||
final partyRoom = ref.read(partyRoomProvider.notifier);
|
||||
final isEdit = roomInfo != null;
|
||||
|
||||
final selectedMainTag = useState<String?>(null);
|
||||
final selectedSubTag = useState<String?>(null);
|
||||
final targetMembersController = useTextEditingController(text: '6');
|
||||
final hasPassword = useState(false);
|
||||
final selectedMainTag = useState<String?>(roomInfo?.mainTagId);
|
||||
final selectedSubTag = useState<String?>(roomInfo?.subTagId);
|
||||
final targetMembersController = useTextEditingController(text: roomInfo?.targetMembers.toString() ?? '6');
|
||||
final hasPassword = useState(roomInfo?.hasPassword ?? false);
|
||||
final passwordController = useTextEditingController();
|
||||
final socialLinksController = useTextEditingController();
|
||||
final socialLinksController = useTextEditingController(text: roomInfo?.socialLinks.join('\n'));
|
||||
final isCreating = useState(false);
|
||||
|
||||
// 获取选中的主标签
|
||||
@@ -25,7 +30,7 @@ class CreateRoomDialog extends HookConsumerWidget {
|
||||
|
||||
return ContentDialog(
|
||||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.5),
|
||||
title: const Text('创建房间'),
|
||||
title: Text(isEdit ? '编辑房间' : '创建房间'),
|
||||
content: SizedBox(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
@@ -135,34 +140,37 @@ class CreateRoomDialog extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
checked: hasPassword.value,
|
||||
onChanged: (value) {
|
||||
hasPassword.value = value ?? false;
|
||||
},
|
||||
content: const Text('设置密码'),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// 密码输入框 - 始终显示,避免布局跳动
|
||||
InfoLabel(
|
||||
label: '房间密码',
|
||||
child: TextBox(
|
||||
controller: passwordController,
|
||||
placeholder: hasPassword.value ? '输入密码' : '未启用密码',
|
||||
obscureText: hasPassword.value,
|
||||
maxLines: 1,
|
||||
maxLength: 12,
|
||||
enabled: hasPassword.value,
|
||||
if (!isEdit) ...[
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
checked: hasPassword.value,
|
||||
onChanged: (value) {
|
||||
hasPassword.value = value ?? false;
|
||||
},
|
||||
content: const Text('设置密码'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
// 密码输入框 - 始终显示,避免布局跳动
|
||||
InfoLabel(
|
||||
label: '房间密码',
|
||||
child: TextBox(
|
||||
controller: passwordController,
|
||||
placeholder: isEdit
|
||||
? "为空则不更新密码,取消勾选则取消密码"
|
||||
: hasPassword.value
|
||||
? '输入密码'
|
||||
: '未启用密码',
|
||||
obscureText: hasPassword.value,
|
||||
maxLines: 1,
|
||||
maxLength: 12,
|
||||
enabled: hasPassword.value,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
InfoLabel(
|
||||
label: '社交链接 (可选)',
|
||||
child: TextBox(
|
||||
@@ -207,32 +215,50 @@ class CreateRoomDialog extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
if (hasPassword.value && passwordController.text.trim().isEmpty) {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => ContentDialog(
|
||||
title: const Text('提示'),
|
||||
content: const Text('请输入密码'),
|
||||
actions: [FilledButton(child: const Text('确定'), onPressed: () => Navigator.pop(context))],
|
||||
),
|
||||
);
|
||||
if (!isEdit) {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => ContentDialog(
|
||||
title: const Text('提示'),
|
||||
content: const Text('请输入密码'),
|
||||
actions: [FilledButton(child: const Text('确定'), onPressed: () => Navigator.pop(context))],
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
final socialLinks = socialLinksController.text.split('\n');
|
||||
|
||||
// 检查是否为 https 开头的链接
|
||||
final invalidLinks = socialLinks.where((link) => !link.startsWith('https://')).toList();
|
||||
if (invalidLinks.isNotEmpty) {
|
||||
showToast(context, "链接格式错误!");
|
||||
return;
|
||||
}
|
||||
|
||||
final socialLinks = socialLinksController.text
|
||||
.split('\n')
|
||||
.where((link) => link.trim().isNotEmpty && link.trim().startsWith('http'))
|
||||
.toList();
|
||||
|
||||
isCreating.value = true;
|
||||
try {
|
||||
await partyRoom.createRoom(
|
||||
mainTagId: mainTagId,
|
||||
subTagId: selectedSubTag.value,
|
||||
targetMembers: targetMembers,
|
||||
hasPassword: hasPassword.value,
|
||||
password: hasPassword.value ? passwordController.text : null,
|
||||
socialLinks: socialLinks.isEmpty ? null : socialLinks,
|
||||
);
|
||||
if (isEdit) {
|
||||
await partyRoom.updateRoom(
|
||||
mainTagId: mainTagId,
|
||||
subTagId: selectedSubTag.value,
|
||||
targetMembers: targetMembers,
|
||||
password: !hasPassword.value
|
||||
? ''
|
||||
: (passwordController.text.isNotEmpty ? passwordController.text : null),
|
||||
socialLinks: socialLinks,
|
||||
);
|
||||
} else {
|
||||
await partyRoom.createRoom(
|
||||
mainTagId: mainTagId,
|
||||
subTagId: selectedSubTag.value,
|
||||
targetMembers: targetMembers,
|
||||
hasPassword: hasPassword.value,
|
||||
password: hasPassword.value ? passwordController.text : null,
|
||||
socialLinks: socialLinks.isEmpty ? null : socialLinks,
|
||||
);
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
Navigator.pop(context);
|
||||
@@ -243,7 +269,7 @@ class CreateRoomDialog extends HookConsumerWidget {
|
||||
await showDialog(
|
||||
context: context,
|
||||
builder: (context) => ContentDialog(
|
||||
title: const Text('创建失败'),
|
||||
title: Text(isEdit ? '更新失败' : '创建失败'),
|
||||
content: Text(e.toString()),
|
||||
actions: [FilledButton(child: const Text('确定'), onPressed: () => Navigator.pop(context))],
|
||||
),
|
||||
@@ -253,7 +279,7 @@ class CreateRoomDialog extends HookConsumerWidget {
|
||||
},
|
||||
child: isCreating.value
|
||||
? const SizedBox(width: 16, height: 16, child: ProgressRing(strokeWidth: 2))
|
||||
: const Text('创建'),
|
||||
: Text(isEdit ? '保存' : '创建'),
|
||||
),
|
||||
Button(onPressed: isCreating.value ? null : () => Navigator.pop(context), child: const Text('取消')),
|
||||
],
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:fluent_ui/fluent_ui.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:starcitizen_doctor/common/utils/base_utils.dart';
|
||||
import 'package:starcitizen_doctor/common/utils/log.dart';
|
||||
import 'package:starcitizen_doctor/provider/party_room.dart';
|
||||
import 'package:starcitizen_doctor/ui/party_room/widgets/detail/party_room_message_list.dart';
|
||||
|
||||
@@ -18,6 +20,7 @@ class PartyRoomDetailPage extends ConsumerStatefulWidget {
|
||||
class _PartyRoomDetailPageState extends ConsumerState<PartyRoomDetailPage> {
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
int _lastEventCount = 0;
|
||||
bool _isShowingDialog = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -25,6 +28,59 @@ class _PartyRoomDetailPageState extends ConsumerState<PartyRoomDetailPage> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _showReconnectDialog() async {
|
||||
if (_isShowingDialog || !mounted) return;
|
||||
_isShowingDialog = true;
|
||||
|
||||
final partyRoom = ref.read(partyRoomProvider.notifier);
|
||||
|
||||
final result = await showBaseDialog(
|
||||
context,
|
||||
title: '连接已断开',
|
||||
content: const Text('与房间服务器的连接已断开,是否重新连接?'),
|
||||
actions: [
|
||||
Button(
|
||||
onPressed: () => Navigator.of(context).pop('leave'),
|
||||
child: const Padding(padding: EdgeInsets.only(top: 2, bottom: 2, left: 8, right: 8), child: Text('退出房间')),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop('reconnect'),
|
||||
child: const Padding(padding: EdgeInsets.only(top: 2, bottom: 2, left: 8, right: 8), child: Text('重新连接')),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
_isShowingDialog = false;
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (result == 'reconnect') {
|
||||
try {
|
||||
await partyRoom.refreshRoomAndReconnect();
|
||||
dPrint('[PartyRoomDetailPage] Reconnect success');
|
||||
} catch (e) {
|
||||
dPrint('[PartyRoomDetailPage] Reconnect failed: $e');
|
||||
// 重连失败,重新显示对话框
|
||||
if (mounted) {
|
||||
_showReconnectDialog();
|
||||
}
|
||||
}
|
||||
} else if (result == 'leave') {
|
||||
try {
|
||||
partyRoom.acknowledgeDisconnection();
|
||||
await partyRoom.leaveRoom();
|
||||
} catch (e) {
|
||||
dPrint('[PartyRoomDetailPage] Leave room failed: $e');
|
||||
if (mounted) {
|
||||
await showToast(context, '退出房间失败: $e');
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 用户关闭对话框
|
||||
partyRoom.acknowledgeDisconnection();
|
||||
}
|
||||
}
|
||||
|
||||
void _scrollToBottom() {
|
||||
if (_scrollController.hasClients) {
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
@@ -41,6 +97,17 @@ class _PartyRoomDetailPageState extends ConsumerState<PartyRoomDetailPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ref.listen<PartyRoomFullState>(partyRoomProvider, (previous, next) {
|
||||
// 监听事件流断开状态
|
||||
if (next.room.isInRoom && next.room.eventStreamDisconnected && !_isShowingDialog) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (mounted) {
|
||||
_showReconnectDialog();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
final partyRoomState = ref.watch(partyRoomProvider);
|
||||
final partyRoom = ref.read(partyRoomProvider.notifier);
|
||||
final room = partyRoomState.room.currentRoom;
|
||||
|
||||
@@ -2,6 +2,7 @@ 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';
|
||||
import 'package:starcitizen_doctor/ui/party_room/widgets/create_room_dialog.dart';
|
||||
|
||||
/// 房间信息头部组件
|
||||
class PartyRoomHeader extends ConsumerWidget {
|
||||
@@ -58,39 +59,54 @@ class PartyRoomHeader extends ConsumerWidget {
|
||||
),
|
||||
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);
|
||||
}),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Button(
|
||||
onPressed: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => CreateRoomDialog(roomInfo: room),
|
||||
);
|
||||
},
|
||||
child: const Text('编辑房间'),
|
||||
),
|
||||
),
|
||||
child: const Text('解散房间', style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Button(
|
||||
onPressed: () async {
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => ContentDialog(
|
||||
title: const Text('确认解散'),
|
||||
content: const Text('确定要解散房间吗?所有成员将被移出。'),
|
||||
actions: [
|
||||
Button(child: const Text('取消'), onPressed: () => Navigator.pop(context, false)),
|
||||
FilledButton(
|
||||
style: ButtonStyle(backgroundColor: WidgetStateProperty.all(const Color(0xFFDA373C))),
|
||||
child: const Text('解散', style: TextStyle(color: Colors.white)),
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (confirmed == true) {
|
||||
ref.read(partyRoomUIModelProvider.notifier).dismissRoom();
|
||||
}
|
||||
},
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.resolveWith((state) {
|
||||
if (state.isHovered || state.isPressed) {
|
||||
return const Color(0xFFB3261E);
|
||||
}
|
||||
return const Color(0xFFDA373C);
|
||||
}),
|
||||
),
|
||||
child: const Text('解散房间', style: TextStyle(color: Colors.white)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
] else ...[
|
||||
const SizedBox(height: 8),
|
||||
@@ -100,8 +116,21 @@ class PartyRoomHeader extends ConsumerWidget {
|
||||
onPressed: () async {
|
||||
await partyRoom.leaveRoom();
|
||||
},
|
||||
style: ButtonStyle(backgroundColor: WidgetStateProperty.all(const Color(0xFF404249))),
|
||||
child: const Text('离开房间', style: TextStyle(color: Color(0xFFB5BAC1))),
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.resolveWith((state) {
|
||||
if (state.isHovered || state.isPressed) {
|
||||
return const Color(0xFFDA373C);
|
||||
}
|
||||
return const Color(0xFF404249);
|
||||
}),
|
||||
foregroundColor: WidgetStateProperty.resolveWith((state) {
|
||||
if (state.isHovered || state.isPressed) {
|
||||
return Colors.white;
|
||||
}
|
||||
return const Color(0xFFDBDEE1);
|
||||
}),
|
||||
),
|
||||
child: const Text('离开房间'),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -430,6 +430,7 @@ class PartyRoomListPage extends HookConsumerWidget {
|
||||
|
||||
Future<void> _showCreateRoomDialog(BuildContext context, WidgetRef ref) async {
|
||||
final uiState = ref.read(partyRoomUIModelProvider);
|
||||
final partyRoomState = ref.read(partyRoomProvider);
|
||||
|
||||
// 检查是否为游客模式
|
||||
if (uiState.isGuestMode) {
|
||||
@@ -451,6 +452,26 @@ class PartyRoomListPage extends HookConsumerWidget {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否已经在房间内
|
||||
if (partyRoomState.room.isInRoom) {
|
||||
final shouldLeave = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => ContentDialog(
|
||||
title: const Text('创建新房间'),
|
||||
content: const Text('你已经在其他房间中,创建新房间将自动退出当前房间。是否继续?'),
|
||||
actions: [
|
||||
Button(child: const Text('取消'), onPressed: () => Navigator.pop(context, false)),
|
||||
FilledButton(child: const Text('继续'), onPressed: () => Navigator.pop(context, true)),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (shouldLeave != true) return;
|
||||
|
||||
// 退出当前房间
|
||||
await ref.read(partyRoomProvider.notifier).leaveRoom();
|
||||
}
|
||||
if (!context.mounted) return;
|
||||
await showDialog(context: context, builder: (context) => const CreateRoomDialog());
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ final class SettingsUIModelProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$settingsUIModelHash() => r'5c08c56bf5464ef44bee8edb8c18c08d4217f135';
|
||||
String _$settingsUIModelHash() => r'd19104d924f018a9230548d0372692fc344adacd';
|
||||
|
||||
abstract class _$SettingsUIModel extends $Notifier<SettingsUIState> {
|
||||
SettingsUIState build();
|
||||
|
||||
Reference in New Issue
Block a user