图片、文件发送修改

main
杨博文 2 weeks ago
parent c9a0d59355
commit 0b7ad39d94

6
.gitignore vendored

@ -3,4 +3,8 @@ __pycache__/
.kiro/*
client_gui.log
client_gui.log
data/
downloads/

@ -8,6 +8,7 @@
import asyncio
import logging
import os
import sys
import time
from typing import Optional, Callable, Dict, Any, List
@ -68,6 +69,10 @@ class P2PClientApp:
# 设置文件传输的消息发送函数
self._file_transfer.set_send_message_func(self._send_message_async)
# 文件接收回调
self._file_received_callbacks: List[Callable[[str, str, str], None]] = []
self._file_transfer.add_file_received_callback(self._on_file_received)
# 回调函数
self._message_callbacks: List[Callable[[Message], None]] = []
self._state_callbacks: List[Callable[[ConnectionState, Optional[str]], None]] = []
@ -536,6 +541,51 @@ class P2PClientApp:
error = message.payload.decode('utf-8')
logger.error(f"Error from server: {error}")
def _on_file_received(self, sender_id: str, file_name: str, file_path: str) -> None:
"""
处理文件接收完成
Args:
sender_id: 发送者ID
file_name: 文件名
file_path: 保存路径
"""
logger.info(f"File received from {sender_id}: {file_name} -> {file_path}")
# 判断是否是图片
image_extensions = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'}
ext = os.path.splitext(file_name)[1].lower()
content_type = MessageType.IMAGE if ext in image_extensions else MessageType.FILE_REQUEST
# 保存到聊天历史
chat_msg = ChatMessage(
message_id=f"file_{time.time()}",
sender_id=sender_id,
receiver_id=self._user_info.user_id if self._user_info else "",
content_type=content_type,
content=file_path, # 保存文件路径
timestamp=datetime.now(),
is_read=False,
is_sent=True
)
self._add_to_history(sender_id, chat_msg)
# 通知回调
for callback in self._file_received_callbacks:
try:
callback(sender_id, file_name, file_path)
except Exception as e:
logger.error(f"File received callback error: {e}")
def add_file_received_callback(self, callback: Callable[[str, str, str], None]) -> None:
"""添加文件接收回调"""
self._file_received_callbacks.append(callback)
def remove_file_received_callback(self, callback: Callable[[str, str, str], None]) -> None:
"""移除文件接收回调"""
if callback in self._file_received_callbacks:
self._file_received_callbacks.remove(callback)
def _on_connection_state_changed(self, state: ConnectionState, reason: Optional[str]) -> None:
"""
处理连接状态变化

@ -174,6 +174,9 @@ class FileTransferModule:
# 进度回调
self._progress_callbacks: Dict[str, ProgressCallback] = {}
# 文件接收完成回调
self._file_received_callbacks: List[Callable[[str, str, str], None]] = [] # (sender_id, file_name, file_path)
# 消息处理器
self._message_handler = MessageHandler()
@ -181,6 +184,10 @@ class FileTransferModule:
self._state_dir = Path(self.config.data_dir) / self.STATE_DIR
self._state_dir.mkdir(parents=True, exist_ok=True)
# 确保下载目录存在
self._downloads_dir = Path(self.config.downloads_dir)
self._downloads_dir.mkdir(parents=True, exist_ok=True)
# 加载未完成的传输状态
self._load_transfer_states()
@ -195,6 +202,22 @@ class FileTransferModule:
"""
self._send_message = func
def add_file_received_callback(self, callback: Callable[[str, str, str], None]) -> None:
"""
添加文件接收完成回调
Args:
callback: 回调函数 (sender_id, file_name, file_path)
"""
self._file_received_callbacks.append(callback)
def remove_file_received_callback(self, callback: Callable[[str, str, str], None]) -> None:
"""
移除文件接收完成回调
"""
if callback in self._file_received_callbacks:
self._file_received_callbacks.remove(callback)
# ==================== 文件哈希计算 (需求 4.4) ====================
def calculate_file_hash(self, file_path: str, algorithm: str = "sha256") -> str:
@ -731,25 +754,39 @@ class FileTransferModule:
state.status = TransferStatus.FAILED
return False
# 如果没有设置保存路径,自动保存到 downloads 目录
if not state.save_path:
state.save_path = str(self._downloads_dir / state.file_name)
# 如果文件已存在,添加序号
base_path = state.save_path
counter = 1
while os.path.exists(state.save_path):
name, ext = os.path.splitext(base_path)
state.save_path = f"{name}_{counter}{ext}"
counter += 1
# 组装文件
if state.save_path:
success = self._assemble_file(file_id, state.save_path)
if success:
# 验证文件完整性
if self.verify_file_integrity(state.save_path, file_hash):
state.status = TransferStatus.COMPLETED
logger.info(f"File received and verified: {state.file_name}")
else:
state.status = TransferStatus.FAILED
logger.error(f"File integrity check failed: {state.file_name}")
return False
success = self._assemble_file(file_id, state.save_path)
if success:
# 验证文件完整性
if self.verify_file_integrity(state.save_path, file_hash):
state.status = TransferStatus.COMPLETED
logger.info(f"File received and verified: {state.file_name} -> {state.save_path}")
# 通知回调
for callback in self._file_received_callbacks:
try:
callback(state.sender_id, state.file_name, state.save_path)
except Exception as e:
logger.error(f"File received callback error: {e}")
else:
state.status = TransferStatus.FAILED
logger.error(f"File integrity check failed: {state.file_name}")
return False
else:
# 保存路径未设置,保持数据在缓冲区
state.status = TransferStatus.COMPLETED
state.status = TransferStatus.FAILED
return False
# 清理缓冲区
if file_id in self._receive_buffers:

@ -37,10 +37,18 @@ class MessageBubble(QFrame):
layout.setContentsMargins(10, 5, 10, 5)
layout.setSpacing(2)
# 消息内容
content_label = QLabel(self.message.content)
content_label.setWordWrap(True)
content_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
# 根据消息类型显示不同内容
if self.message.content_type == MessageType.IMAGE:
# 图片消息 - 显示缩略图
content_widget = self._create_image_widget()
elif self.message.content_type == MessageType.FILE_REQUEST:
# 文件消息 - 显示文件信息
content_widget = self._create_file_widget()
else:
# 文本消息
content_widget = QLabel(self.message.content)
content_widget.setWordWrap(True)
content_widget.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse)
# 时间戳
time_str = self.message.timestamp.strftime("%H:%M")
@ -68,7 +76,7 @@ class MessageBubble(QFrame):
bottom_layout.addWidget(time_label)
bottom_layout.addWidget(status_label)
layout.addWidget(content_label)
layout.addWidget(content_widget)
layout.addLayout(bottom_layout)
else:
# 对方发送的消息,左对齐
@ -82,8 +90,110 @@ class MessageBubble(QFrame):
""")
layout.setAlignment(Qt.AlignmentFlag.AlignLeft)
layout.addWidget(content_label)
layout.addWidget(content_widget)
layout.addWidget(time_label)
def _create_image_widget(self) -> QWidget:
"""创建图片显示组件"""
import os
from PyQt6.QtGui import QPixmap
container = QWidget()
layout = QVBoxLayout(container)
layout.setContentsMargins(0, 0, 0, 0)
file_path = self.message.content
file_name = os.path.basename(file_path)
# 尝试加载图片
if os.path.exists(file_path):
pixmap = QPixmap(file_path)
if not pixmap.isNull():
# 缩放图片
scaled = pixmap.scaled(200, 200, Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation)
image_label = QLabel()
image_label.setPixmap(scaled)
image_label.setCursor(Qt.CursorShape.PointingHandCursor)
image_label.setToolTip(f"点击打开: {file_path}")
image_label.mousePressEvent = lambda e: self._open_file(file_path)
layout.addWidget(image_label)
else:
layout.addWidget(QLabel(f"[图片] {file_name}"))
else:
layout.addWidget(QLabel(f"[图片] {file_name}\n(文件不存在)"))
return container
def _create_file_widget(self) -> QWidget:
"""创建文件显示组件"""
import os
container = QWidget()
layout = QHBoxLayout(container)
layout.setContentsMargins(0, 0, 0, 0)
file_path = self.message.content
file_name = os.path.basename(file_path)
# 文件图标
icon_label = QLabel("📄")
icon_label.setStyleSheet("font-size: 24px;")
layout.addWidget(icon_label)
# 文件信息
info_layout = QVBoxLayout()
name_label = QLabel(file_name)
name_label.setStyleSheet("font-weight: bold;")
info_layout.addWidget(name_label)
if os.path.exists(file_path):
size = os.path.getsize(file_path)
size_str = self._format_size(size)
size_label = QLabel(size_str)
size_label.setStyleSheet("color: #666; font-size: 11px;")
info_layout.addWidget(size_label)
else:
info_layout.addWidget(QLabel("(文件不存在)"))
layout.addLayout(info_layout)
# 点击打开
container.setCursor(Qt.CursorShape.PointingHandCursor)
container.setToolTip(f"点击打开: {file_path}")
container.mousePressEvent = lambda e: self._open_file(file_path)
return container
def _format_size(self, size: int) -> str:
"""格式化文件大小"""
if size < 1024:
return f"{size} B"
elif size < 1024 * 1024:
return f"{size / 1024:.1f} KB"
elif size < 1024 * 1024 * 1024:
return f"{size / (1024 * 1024):.1f} MB"
else:
return f"{size / (1024 * 1024 * 1024):.1f} GB"
def _open_file(self, file_path: str):
"""打开文件"""
import os
import subprocess
import sys
if not os.path.exists(file_path):
return
try:
if sys.platform == 'win32':
os.startfile(file_path)
elif sys.platform == 'darwin':
subprocess.run(['open', file_path])
else:
subprocess.run(['xdg-open', file_path])
except Exception as e:
logger.error(f"Failed to open file: {e}")
class ChatWidget(QWidget):

@ -60,6 +60,7 @@ class ClientConfig:
# Local storage
data_dir: str = "data"
cache_dir: str = "cache"
downloads_dir: str = "downloads" # 接收文件保存目录
@dataclass

@ -306,6 +306,9 @@ class P2PChatGUI(QObject):
# 创建客户端
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)
@ -528,6 +531,49 @@ class P2PChatGUI(QObject):
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"""
# 使用 QTimer 在主线程中执行 GUI 更新
QTimer.singleShot(0, lambda: self._handle_file_received_ui(sender_id, file_name, file_path))
def _handle_file_received_ui(self, sender_id: str, file_name: str, file_path: str):
"""在主线程中处理文件接收的UI更新"""
import os
# 判断是否是图片
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)
# 如果当前正在和发送者聊天,在聊天窗口显示
if self._current_chat_peer == sender_id:
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"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 hasattr(self, '_chat_widget') and self._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

Loading…
Cancel
Save