You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

886 lines
34 KiB

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
P2P Chat Client GUI 启动脚本
用法:
python run_client_gui.py [--server HOST] [--port PORT]
示例:
python run_client_gui.py
python run_client_gui.py --server 192.168.1.100 --port 8888
"""
import asyncio
import argparse
import logging
import sys
import os
from typing import Optional, List
# 确保能找到模块
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from PyQt6.QtWidgets import QApplication, QMessageBox, QSplashScreen
from PyQt6.QtCore import Qt, QTimer, QThread, pyqtSignal, QObject
from PyQt6.QtGui import QPixmap, QFont
from client.app import P2PClientApp
from client.connection_manager import ConnectionState
from client.ui import (
MainWindow, LoginDialog, ChatWidget, ContactListWidget,
FileTransferWidget, MediaPlayerWidget, VoiceCallWidget, SystemTrayManager
)
from shared.models import UserInfo, UserStatus, Message, MessageType
from config import load_client_config, ClientConfig
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(sys.stdout),
logging.FileHandler('client_gui.log', encoding='utf-8')
]
)
logger = logging.getLogger(__name__)
class AsyncWorker(QThread):
"""异步工作线程,用于运行 asyncio 事件循环"""
connected = pyqtSignal(bool, str) # (success, message)
disconnected = pyqtSignal(str) # reason
message_received = pyqtSignal(object) # Message
user_list_updated = pyqtSignal(list) # List[UserInfo]
state_changed = pyqtSignal(str, str) # (state, reason)
def __init__(self, client: P2PClientApp, user_info: UserInfo):
super().__init__()
self.client = client
self.user_info = user_info
self._running = False
self._loop: Optional[asyncio.AbstractEventLoop] = None
def run(self):
"""运行异步事件循环"""
self._loop = asyncio.new_event_loop()
asyncio.set_event_loop(self._loop)
self._running = True
try:
# 连接到服务器
success = self._loop.run_until_complete(
self.client.start(self.user_info)
)
if success:
self.connected.emit(True, "连接成功")
# 设置回调
self.client.add_message_callback(self._on_message)
self.client.add_state_callback(self._on_state_change)
self.client.add_user_list_callback(self._on_user_list)
# 保持运行
while self._running:
self._loop.run_until_complete(asyncio.sleep(0.1))
else:
self.connected.emit(False, "连接失败")
except Exception as e:
logger.error(f"Worker error: {e}")
self.connected.emit(False, str(e))
finally:
self._loop.close()
def stop(self):
"""停止工作线程"""
self._running = False
if self._loop and self._loop.is_running():
self._loop.call_soon_threadsafe(self._loop.stop)
def _on_message(self, message: Message):
"""消息回调"""
self.message_received.emit(message)
def _on_state_change(self, state: ConnectionState, reason: Optional[str]):
"""状态变化回调"""
self.state_changed.emit(state.value, reason or "")
if state == ConnectionState.DISCONNECTED:
self.disconnected.emit(reason or "连接断开")
def _on_user_list(self, users: List[UserInfo]):
"""用户列表更新回调"""
self.user_list_updated.emit(users)
def send_message(self, peer_id: str, content: str) -> bool:
"""发送消息(线程安全)"""
if self._loop and self._running:
future = asyncio.run_coroutine_threadsafe(
self.client.send_text_message(peer_id, content),
self._loop
)
try:
return future.result(timeout=5.0)
except Exception as e:
logger.error(f"Send message error: {e}")
return False
return False
def send_file(self, peer_id: str, file_path: str) -> bool:
"""发送文件(线程安全)"""
if self._loop and self._running:
future = asyncio.run_coroutine_threadsafe(
self.client.send_file(peer_id, file_path),
self._loop
)
try:
return future.result(timeout=300.0) # 5分钟超时
except Exception as e:
logger.error(f"Send file error: {e}")
return False
return False
def send_image(self, peer_id: str, image_path: str) -> bool:
"""发送图片(线程安全)"""
if self._loop and self._running:
future = asyncio.run_coroutine_threadsafe(
self.client.send_image(peer_id, image_path),
self._loop
)
try:
return future.result(timeout=300.0)
except Exception as e:
logger.error(f"Send image error: {e}")
return False
return False
def start_voice_call(self, peer_id: str) -> bool:
"""发起语音通话(线程安全)"""
if self._loop and self._running:
future = asyncio.run_coroutine_threadsafe(
self.client.start_voice_call(peer_id),
self._loop
)
try:
return future.result(timeout=30.0)
except Exception as e:
logger.error(f"Start voice call error: {e}")
return False
return False
def end_voice_call(self):
"""结束语音通话(线程安全)"""
if self._loop and self._running:
self.client.end_voice_call()
def accept_voice_call(self, peer_id: str) -> bool:
"""接听语音通话(线程安全)"""
if self._loop and self._running:
future = asyncio.run_coroutine_threadsafe(
self.client.accept_voice_call(peer_id),
self._loop
)
try:
return future.result(timeout=10.0)
except Exception as e:
logger.error(f"Accept voice call error: {e}")
return False
return False
def reject_voice_call(self, peer_id: str):
"""拒绝语音通话(线程安全)"""
if self._loop and self._running:
self.client.reject_voice_call(peer_id)
def refresh_users(self):
"""刷新用户列表(线程安全)"""
if self._loop and self._running:
asyncio.run_coroutine_threadsafe(
self.client.refresh_online_users(),
self._loop
)
class P2PChatGUI(QObject):
"""P2P 聊天 GUI 应用程序"""
# 信号定义 - 用于跨线程通信
file_received_signal = pyqtSignal(str, str, str) # sender_id, file_name, file_path
def __init__(self, config: ClientConfig):
super().__init__()
self.config = config
self.app: Optional[QApplication] = None
self.main_window: Optional[MainWindow] = None
self.client: Optional[P2PClientApp] = None
self.worker: Optional[AsyncWorker] = None
self._current_user: Optional[UserInfo] = None
self._current_chat_peer: Optional[str] = None
# 连接信号到槽
self.file_received_signal.connect(self._handle_file_received_ui)
def run(self) -> int:
"""运行 GUI 应用程序"""
# 创建 Qt 应用
self.app = QApplication(sys.argv)
self.app.setApplicationName("P2P Chat")
self.app.setApplicationVersion("0.1.0")
# 设置默认字体
font = QFont("Microsoft YaHei", 10)
self.app.setFont(font)
# 设置样式
self.app.setStyle("Fusion")
# 应用退出时清理资源
self.app.aboutToQuit.connect(self.cleanup)
# 显示登录对话框
if not self._show_login():
return 0
# 创建主窗口
self._create_main_window()
# 连接到服务器
self._connect_to_server()
# 运行应用
return self.app.exec()
def _show_login(self) -> bool:
"""显示登录对话框"""
dialog = LoginDialog()
# 设置服务器地址
dialog._login_server_input.setText(
f"{self.config.server_host}:{self.config.server_port}"
)
dialog._reg_server_input.setText(
f"{self.config.server_host}:{self.config.server_port}"
)
if dialog.exec():
self._current_user = dialog.get_user_info()
host, port = dialog.get_server_address()
self.config.server_host = host
self.config.server_port = port
return True
return False
def _create_main_window(self):
"""创建主窗口"""
self.main_window = MainWindow(self.config)
if self._current_user:
self.main_window.set_current_user(self._current_user)
# 创建并设置联系人列表组件
self._contact_list_widget = ContactListWidget()
self.main_window.set_contact_list_widget(self._contact_list_widget)
# 创建并设置聊天组件
self._chat_widget = ChatWidget()
self.main_window.set_chat_widget(self._chat_widget)
# 连接聊天组件信号
self._chat_widget.message_sent.connect(self._on_send_message)
self._chat_widget.file_send_requested.connect(lambda peer_id: self._on_send_file())
self._chat_widget.image_send_requested.connect(lambda peer_id: self._on_send_image())
self._chat_widget.voice_call_requested.connect(lambda peer_id: self._on_voice_call())
self._chat_widget.voice_call_end_requested.connect(self._on_end_voice_call)
# 连接联系人列表信号
self._contact_list_widget.contact_selected.connect(self._on_contact_selected)
self._contact_list_widget.contact_double_clicked.connect(self._on_contact_double_clicked)
self._contact_list_widget.refresh_requested.connect(self._refresh_users)
self._contact_list_widget.lan_discovery_requested.connect(self._on_lan_discovery)
# 设置系统托盘
try:
tray = SystemTrayManager(self.main_window)
self.main_window.set_system_tray(tray)
except Exception as e:
logger.warning(f"Failed to create system tray: {e}")
# 连接菜单动作
self._connect_menu_actions()
self.main_window.show()
def _connect_menu_actions(self):
"""连接菜单动作"""
# 发送文件
self.main_window._send_file_action.triggered.connect(self._on_send_file)
# 发送图片
self.main_window._send_image_action.triggered.connect(self._on_send_image)
# 语音通话
self.main_window._voice_call_action.triggered.connect(self._on_voice_call)
def _connect_to_server(self):
"""连接到服务器"""
if not self._current_user:
return
# 更新状态
self.main_window.connection_state_changed.emit("连接中...")
# 创建客户端
self.client = P2PClientApp(self.config)
# 添加文件接收回调
self.client.add_file_received_callback(self._on_file_received)
# 创建工作线程
self.worker = AsyncWorker(self.client, self._current_user)
self.worker.connected.connect(self._on_connected)
self.worker.disconnected.connect(self._on_disconnected)
self.worker.message_received.connect(self._on_message_received)
self.worker.user_list_updated.connect(self._on_user_list_updated)
self.worker.state_changed.connect(self._on_state_changed)
# 启动工作线程
self.worker.start()
def _on_connected(self, success: bool, message: str):
"""连接结果回调"""
if success:
self.main_window.connection_state_changed.emit("已连接")
self.main_window._statusbar.showMessage(f"已连接到服务器")
logger.info("Connected to server")
# 刷新用户列表
QTimer.singleShot(500, self._refresh_users)
else:
self.main_window.connection_state_changed.emit("连接失败")
QMessageBox.critical(
self.main_window,
"连接失败",
f"无法连接到服务器: {message}"
)
def _on_disconnected(self, reason: str):
"""断开连接回调"""
self.main_window.connection_state_changed.emit("已断开")
self.main_window._statusbar.showMessage(f"连接断开: {reason}")
# 询问是否重连
reply = QMessageBox.question(
self.main_window,
"连接断开",
f"与服务器的连接已断开: {reason}\n\n是否尝试重新连接?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply == QMessageBox.StandardButton.Yes:
self._connect_to_server()
def _on_message_received(self, message: Message):
"""消息接收回调"""
if message.msg_type == MessageType.TEXT:
content = message.payload.decode('utf-8')
sender = message.sender_id
# 显示通知
if self.main_window.isMinimized() or not self.main_window.isActiveWindow():
self.main_window.show_notification(
f"来自 {sender} 的消息",
content[:50] + ("..." if len(content) > 50 else "")
)
# 更新状态栏
self.main_window._statusbar.showMessage(
f"收到来自 {sender} 的消息", 3000
)
# 将消息添加到聊天窗口显示
from datetime import datetime
from shared.models import ChatMessage
msg = ChatMessage(
message_id=message.message_id,
sender_id=sender,
receiver_id=message.receiver_id,
content_type=MessageType.TEXT,
content=content,
timestamp=datetime.fromtimestamp(message.timestamp),
is_sent=True,
is_read=False
)
# 如果当前正在和发送者聊天,直接显示消息
if hasattr(self, '_chat_widget') and self._chat_widget:
if self._current_chat_peer == sender:
self._chat_widget.add_message(msg, is_self=False)
logger.info(f"Message from {sender}: {content[:50]}...")
elif message.msg_type == MessageType.VOICE_CALL_REQUEST:
sender = message.sender_id
logger.info(f"Voice call request from {sender}")
self.main_window.show_notification(
"来电",
f"{sender} 正在呼叫你"
)
# 显示来电对话框
self._show_incoming_call_dialog(sender)
elif message.msg_type == MessageType.VOICE_CALL_ACCEPT:
sender = message.sender_id
logger.info(f"Voice call accepted by {sender}")
self.main_window._statusbar.showMessage(f"通话已接通 - {sender}", 5000)
self.main_window.show_notification("通话已接通", f"{sender} 的通话已建立")
# 更新UI状态为通话中
if hasattr(self, '_chat_widget') and self._chat_widget:
self._chat_widget.set_call_state(True, "通话中...")
elif message.msg_type == MessageType.VOICE_CALL_REJECT:
sender = message.sender_id
reason = message.payload.decode('utf-8') if message.payload else "对方拒绝"
logger.info(f"Voice call rejected by {sender}: {reason}")
self.main_window._statusbar.showMessage(f"呼叫被拒绝: {reason}", 5000)
self.main_window.show_notification("呼叫被拒绝", f"{sender} 拒绝了你的通话请求")
# 恢复UI状态
if hasattr(self, '_chat_widget') and self._chat_widget:
self._chat_widget.set_call_state(False)
elif message.msg_type == MessageType.VOICE_CALL_END:
sender = message.sender_id
logger.info(f"Voice call ended by {sender}")
self.main_window._statusbar.showMessage("通话已结束", 3000)
self.main_window.show_notification("通话结束", f"{sender} 的通话已结束")
# 恢复UI状态
if hasattr(self, '_chat_widget') and self._chat_widget:
self._chat_widget.set_call_state(False)
def _on_user_list_updated(self, users: List[UserInfo]):
"""用户列表更新回调"""
logger.info(f"Online users: {len(users)}")
self.main_window._statusbar.showMessage(
f"在线用户: {len(users)}", 3000
)
# 更新联系人列表(排除自己)
if hasattr(self, '_contact_list_widget') and self._contact_list_widget:
# 过滤掉当前用户自己
other_users = [
user for user in users
if self._current_user is None or user.user_id != self._current_user.user_id
]
self._contact_list_widget.set_contacts(other_users)
logger.info(f"Contact list updated: {len(other_users)} contacts")
# 更新连接模式显示
self._update_connection_modes()
def _on_state_changed(self, state: str, reason: str):
"""状态变化回调"""
state_text = {
"disconnected": "已断开",
"connecting": "连接中...",
"connected": "已连接",
"reconnecting": "重连中..."
}.get(state, state)
self.main_window.connection_state_changed.emit(state_text)
def _refresh_users(self):
"""刷新用户列表"""
if self.worker:
self.worker.refresh_users()
def _on_lan_discovery(self):
"""发现局域网用户"""
self.main_window._statusbar.showMessage("正在发现局域网用户...", 5000)
if self.worker and self.worker._loop and self.worker._running:
future = asyncio.run_coroutine_threadsafe(
self._discover_lan_peers_async(),
self.worker._loop
)
try:
peers = future.result(timeout=5.0)
if peers:
self.main_window._statusbar.showMessage(f"发现 {len(peers)} 个局域网用户", 3000)
self.main_window.show_notification("局域网发现", f"发现 {len(peers)} 个局域网用户")
# 更新联系人列表中的连接模式
self._update_connection_modes()
else:
self.main_window._statusbar.showMessage("未发现局域网用户", 3000)
except Exception as e:
logger.error(f"LAN discovery error: {e}")
self.main_window._statusbar.showMessage("局域网发现失败", 3000)
async def _discover_lan_peers_async(self):
"""异步发现局域网用户"""
if self.client:
return await self.client.discover_lan_peers()
return []
def _update_connection_modes(self):
"""更新联系人列表中的连接模式显示"""
if not self.client or not hasattr(self, '_contact_list_widget'):
return
# 获取所有连接模式
connection_manager = self.client.connection_manager
# 更新每个联系人的连接模式
for user_id in list(self._contact_list_widget._contacts.keys()):
mode = connection_manager.get_connection_mode(user_id)
mode_str = "p2p" if mode.value == "p2p" else "relay"
self._contact_list_widget.update_connection_mode(user_id, mode_str)
def _on_send_file(self):
"""发送文件"""
from PyQt6.QtWidgets import QFileDialog, QMessageBox
import os
if not self._current_chat_peer:
QMessageBox.warning(self.main_window, "提示", "请先选择一个联系人")
return
file_path, _ = QFileDialog.getOpenFileName(
self.main_window,
"选择文件",
"",
"所有文件 (*.*)"
)
if file_path and self._current_chat_peer:
self.main_window._statusbar.showMessage(f"正在发送文件...")
if self.worker:
success = self.worker.send_file(self._current_chat_peer, file_path)
if success:
self.main_window._statusbar.showMessage("文件发送成功", 3000)
# 在聊天窗口显示发送的文件
self._add_sent_file_message(file_path, is_image=False)
else:
self.main_window._statusbar.showMessage("文件发送失败", 3000)
def _on_send_image(self):
"""发送图片"""
from PyQt6.QtWidgets import QFileDialog, QMessageBox
import os
if not self._current_chat_peer:
QMessageBox.warning(self.main_window, "提示", "请先选择一个联系人")
return
file_path, _ = QFileDialog.getOpenFileName(
self.main_window,
"选择图片",
"",
"图片文件 (*.png *.jpg *.jpeg *.gif *.bmp)"
)
if file_path and self._current_chat_peer:
self.main_window._statusbar.showMessage(f"正在发送图片...")
if self.worker:
success = self.worker.send_image(self._current_chat_peer, file_path)
if success:
self.main_window._statusbar.showMessage("图片发送成功", 3000)
# 在聊天窗口显示发送的图片
self._add_sent_file_message(file_path, is_image=True)
else:
self.main_window._statusbar.showMessage("图片发送失败", 3000)
def _add_sent_file_message(self, file_path: str, is_image: bool):
"""在聊天窗口添加发送的文件/图片消息"""
from datetime import datetime
from shared.models import ChatMessage
content_type = MessageType.IMAGE if is_image else MessageType.FILE_REQUEST
msg = ChatMessage(
message_id=f"sent_file_{datetime.now().timestamp()}",
sender_id=self._current_user.user_id if self._current_user else "",
receiver_id=self._current_chat_peer,
content_type=content_type,
content=file_path,
timestamp=datetime.now(),
is_sent=True,
is_read=False
)
if hasattr(self, '_chat_widget') and self._chat_widget:
self._chat_widget.add_message(msg, is_self=True)
def _on_voice_call(self):
"""发起语音通话"""
from PyQt6.QtWidgets import QMessageBox
if not self._current_chat_peer:
QMessageBox.warning(self.main_window, "提示", "请先选择一个联系人")
return
self.main_window._statusbar.showMessage(f"正在呼叫 {self._current_chat_peer}...")
# 更新UI状态为呼叫中
if hasattr(self, '_chat_widget') and self._chat_widget:
self._chat_widget.set_call_state(True, "正在呼叫...")
if self.worker:
success = self.worker.start_voice_call(self._current_chat_peer)
if success:
self.main_window._statusbar.showMessage("正在等待对方接听...", 30000)
else:
self.main_window._statusbar.showMessage("呼叫失败", 3000)
QMessageBox.warning(self.main_window, "呼叫失败", "无法发起语音通话,请检查网络连接")
# 恢复UI状态
if hasattr(self, '_chat_widget') and self._chat_widget:
self._chat_widget.set_call_state(False)
def _on_end_voice_call(self):
"""结束语音通话"""
logger.info("Ending voice call from GUI")
if self.worker:
self.worker.end_voice_call()
self.main_window._statusbar.showMessage("通话已结束", 3000)
# 更新UI状态
if hasattr(self, '_chat_widget') and self._chat_widget:
self._chat_widget.set_call_state(False)
def _show_incoming_call_dialog(self, caller_id: str):
"""显示来电对话框"""
from PyQt6.QtWidgets import QMessageBox
logger.info(f"Showing incoming call dialog for {caller_id}")
reply = QMessageBox.question(
self.main_window,
"来电",
f"{caller_id} 正在呼叫你,是否接听?",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.Yes # 默认选择接听
)
if reply == QMessageBox.StandardButton.Yes:
# 接听
logger.info(f"Accepting call from {caller_id}")
if self.worker and self.worker._loop and self.worker._running:
future = asyncio.run_coroutine_threadsafe(
self.client.accept_voice_call(caller_id),
self.worker._loop
)
try:
success = future.result(timeout=10.0)
if success:
self.main_window._statusbar.showMessage("通话已接通", 3000)
logger.info(f"Call accepted successfully with {caller_id}")
# 更新UI状态为通话中
if hasattr(self, '_chat_widget') and self._chat_widget:
self._chat_widget.set_call_state(True, "通话中...")
else:
self.main_window._statusbar.showMessage("接听失败", 3000)
logger.error(f"Failed to accept call from {caller_id}")
except Exception as e:
logger.error(f"Accept call error: {e}")
self.main_window._statusbar.showMessage(f"接听失败: {e}", 3000)
else:
# 拒绝
logger.info(f"Rejecting call from {caller_id}")
if self.client:
self.client.reject_voice_call(caller_id)
self.main_window._statusbar.showMessage("已拒绝来电", 3000)
def _on_file_received(self, sender_id: str, file_name: str, file_path: str):
"""文件接收完成回调 - 使用信号在主线程中更新GUI"""
logger.info(f"GUI: File received callback triggered - {sender_id}, {file_name}, {file_path}")
# 使用信号在主线程中执行 GUI 更新
self.file_received_signal.emit(sender_id, file_name, file_path)
def _handle_file_received_ui(self, sender_id: str, file_name: str, file_path: str):
"""在主线程中处理文件接收的UI更新"""
import os
from datetime import datetime
from shared.models import ChatMessage
logger.info(f"GUI: _handle_file_received_ui called - sender: {sender_id}, current_peer: {self._current_chat_peer}")
# 判断是否是图片
image_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'}
ext = os.path.splitext(file_name)[1].lower()
is_image = ext in image_extensions
# 显示通知
if is_image:
self.main_window.show_notification("收到图片", f"来自 {sender_id}: {file_name}")
else:
self.main_window.show_notification("收到文件", f"来自 {sender_id}: {file_name}")
self.main_window._statusbar.showMessage(f"收到文件: {file_name},已保存到 {file_path}", 5000)
# 创建消息对象
content_type = MessageType.IMAGE if is_image else MessageType.FILE_REQUEST
msg = ChatMessage(
message_id=f"file_{datetime.now().timestamp()}",
sender_id=sender_id,
receiver_id=self._current_user.user_id if self._current_user else "",
content_type=content_type,
content=file_path,
timestamp=datetime.now(),
is_sent=True,
is_read=False
)
# 如果当前没有打开聊天窗口或不是和发送者聊天,自动切换到发送者
if self._current_chat_peer != sender_id:
logger.info(f"GUI: Switching to chat with {sender_id}")
self._current_chat_peer = sender_id
if hasattr(self, '_chat_widget') and self._chat_widget:
# 获取发送者信息
peer_info = None
if hasattr(self, '_contact_list_widget') and self._contact_list_widget:
peer_info = self._contact_list_widget.get_contact(sender_id)
self._chat_widget.set_peer(sender_id, peer_info)
self.main_window.set_current_chat_peer(sender_id)
# 在聊天窗口显示
if hasattr(self, '_chat_widget') and self._chat_widget:
logger.info(f"GUI: Adding file message to chat widget")
self._chat_widget.add_message(msg, is_self=False)
def _on_contact_selected(self, user_id: str):
"""联系人选中回调 - 打开聊天窗口"""
self._current_chat_peer = user_id
logger.debug(f"Contact selected: {user_id}")
# 获取联系人信息并打开聊天窗口
if hasattr(self, '_contact_list_widget') and self._contact_list_widget:
peer_info = self._contact_list_widget.get_contact(user_id)
if hasattr(self, '_chat_widget') and self._chat_widget:
self._chat_widget.set_peer(user_id, peer_info)
self.main_window.set_current_chat_peer(user_id)
def _on_contact_double_clicked(self, user_id: str):
"""联系人双击回调 - 打开聊天"""
self._current_chat_peer = user_id
logger.info(f"Opening chat with: {user_id}")
# 获取联系人信息并打开聊天窗口
if hasattr(self, '_contact_list_widget') and self._contact_list_widget:
peer_info = self._contact_list_widget.get_contact(user_id)
if hasattr(self, '_chat_widget') and self._chat_widget:
self._chat_widget.set_peer(user_id, peer_info)
self.main_window.set_current_chat_peer(user_id)
self.main_window._statusbar.showMessage(f"{user_id} 聊天", 3000)
def _on_send_message(self, peer_id: str, content: str):
"""发送消息回调"""
if self.worker:
success = self.worker.send_message(peer_id, content)
if success:
# 添加消息到聊天窗口显示
from datetime import datetime
from shared.models import ChatMessage
msg = ChatMessage(
message_id=f"msg_{datetime.now().timestamp()}",
sender_id=self._current_user.user_id if self._current_user else "",
receiver_id=peer_id,
content_type=MessageType.TEXT,
content=content,
timestamp=datetime.now(),
is_sent=True
)
if hasattr(self, '_chat_widget') and self._chat_widget:
self._chat_widget.add_message(msg, is_self=True)
logger.info(f"Message sent to {peer_id}")
else:
self.main_window._statusbar.showMessage("消息发送失败", 3000)
def cleanup(self):
"""清理资源"""
logger.info("Cleaning up resources...")
if self.worker:
self.worker.stop()
self.worker.wait(3000)
self.worker = None
if self.client:
# 在新的事件循环中停止客户端
loop = asyncio.new_event_loop()
try:
loop.run_until_complete(self.client.stop())
except Exception as e:
logger.error(f"Error stopping client: {e}")
finally:
loop.close()
self.client = None
logger.info("Cleanup completed")
def parse_args() -> argparse.Namespace:
"""解析命令行参数"""
parser = argparse.ArgumentParser(
description='P2P Chat Client (GUI)',
formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument(
'--server', '-s',
type=str,
default=None,
help='服务器地址 (默认: 127.0.0.1)'
)
parser.add_argument(
'--port', '-p',
type=int,
default=None,
help='服务器端口 (默认: 8888)'
)
parser.add_argument(
'--debug',
action='store_true',
help='启用调试模式'
)
return parser.parse_args()
def main() -> int:
"""主函数"""
args = parse_args()
# 设置日志级别
if args.debug:
logging.getLogger().setLevel(logging.DEBUG)
# 加载配置
config = load_client_config()
# 命令行参数覆盖配置
if args.server:
config.server_host = args.server
if args.port:
config.server_port = args.port
# 创建并运行 GUI
gui = P2PChatGUI(config)
try:
exit_code = gui.run()
return exit_code
except KeyboardInterrupt:
logger.info("Application interrupted")
gui.cleanup()
return 0
except Exception as e:
logger.error(f"Application error: {e}")
gui.cleanup()
return 1
if __name__ == "__main__":
sys.exit(main())