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.

394 lines
12 KiB

# P2P Network Communication - Voice Call Widget
"""
语音通话界面组件
实现来电提示、通话中界面和通话状态显示
需求: 7.2, 7.8
"""
import logging
from typing import Optional
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QFrame, QDialog
)
from PyQt6.QtCore import Qt, pyqtSignal, QTimer
from client.voice_chat import CallState, NetworkQuality
logger = logging.getLogger(__name__)
class IncomingCallDialog(QDialog):
"""
来电提示对话框
实现来电提示界面 (需求 7.2)
WHEN 收到语音通话邀请 THEN P2P_Client SHALL 显示来电提示并提供接听或拒绝选项
"""
accepted = pyqtSignal()
rejected = pyqtSignal()
def __init__(self, caller_name: str, parent=None):
super().__init__(parent)
self.caller_name = caller_name
self._setup_ui()
# 响铃动画定时器
self._ring_timer = QTimer()
self._ring_timer.setInterval(500)
self._ring_timer.timeout.connect(self._animate_ring)
self._ring_state = False
self._ring_timer.start()
def _setup_ui(self) -> None:
"""设置UI"""
self.setWindowTitle("来电")
self.setFixedSize(300, 200)
self.setModal(True)
self.setWindowFlags(self.windowFlags() | Qt.WindowType.WindowStaysOnTopHint)
layout = QVBoxLayout(self)
layout.setSpacing(20)
layout.setContentsMargins(30, 30, 30, 30)
# 来电图标
self._icon_label = QLabel("📞")
self._icon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._icon_label.setStyleSheet("font-size: 48px;")
layout.addWidget(self._icon_label)
# 来电信息
info_label = QLabel(f"{self.caller_name}\n正在呼叫您...")
info_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
info_label.setStyleSheet("font-size: 16px;")
layout.addWidget(info_label)
# 按钮
button_layout = QHBoxLayout()
button_layout.setSpacing(20)
self._reject_btn = QPushButton("拒绝")
self._reject_btn.setFixedSize(80, 40)
self._reject_btn.setStyleSheet("""
QPushButton {
background-color: #f44336;
color: white;
border: none;
border-radius: 20px;
font-size: 14px;
}
QPushButton:hover {
background-color: #d32f2f;
}
""")
self._reject_btn.clicked.connect(self._on_reject)
button_layout.addWidget(self._reject_btn)
self._accept_btn = QPushButton("接听")
self._accept_btn.setFixedSize(80, 40)
self._accept_btn.setStyleSheet("""
QPushButton {
background-color: #4caf50;
color: white;
border: none;
border-radius: 20px;
font-size: 14px;
}
QPushButton:hover {
background-color: #388e3c;
}
""")
self._accept_btn.clicked.connect(self._on_accept)
button_layout.addWidget(self._accept_btn)
layout.addLayout(button_layout)
def _animate_ring(self) -> None:
"""响铃动画"""
self._ring_state = not self._ring_state
self._icon_label.setText("📞" if self._ring_state else "📱")
def _on_accept(self) -> None:
"""接听"""
self._ring_timer.stop()
self.accepted.emit()
self.accept()
def _on_reject(self) -> None:
"""拒绝"""
self._ring_timer.stop()
self.rejected.emit()
self.reject()
def closeEvent(self, event) -> None:
"""关闭事件"""
self._ring_timer.stop()
self.rejected.emit()
super().closeEvent(event)
class VoiceCallWidget(QWidget):
"""
语音通话界面组件
实现通话中界面 (需求 7.8)
实现通话状态显示 (需求 7.8)
WHEN 语音通话进行中 THEN P2P_Client SHALL 显示通话时长和网络质量指示
"""
# 信号定义
mute_toggled = pyqtSignal(bool)
end_call_requested = pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self._peer_name: str = ""
self._is_muted: bool = False
self._call_duration: int = 0
self._setup_ui()
# 通话计时器
self._duration_timer = QTimer()
self._duration_timer.setInterval(1000)
self._duration_timer.timeout.connect(self._update_duration)
logger.info("VoiceCallWidget initialized")
def _setup_ui(self) -> None:
"""设置UI"""
self.setStyleSheet("background-color: #2d2d2d;")
layout = QVBoxLayout(self)
layout.setSpacing(20)
layout.setContentsMargins(30, 30, 30, 30)
layout.addStretch()
# 通话对象头像
avatar_label = QLabel("👤")
avatar_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
avatar_label.setStyleSheet("""
font-size: 64px;
background-color: #4a90d9;
border-radius: 50px;
padding: 20px;
""")
avatar_label.setFixedSize(100, 100)
avatar_container = QHBoxLayout()
avatar_container.addStretch()
avatar_container.addWidget(avatar_label)
avatar_container.addStretch()
layout.addLayout(avatar_container)
# 通话对象名称
self._peer_name_label = QLabel("未知用户")
self._peer_name_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._peer_name_label.setStyleSheet("color: white; font-size: 24px; font-weight: bold;")
layout.addWidget(self._peer_name_label)
# 通话状态
self._status_label = QLabel("正在连接...")
self._status_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._status_label.setStyleSheet("color: #aaa; font-size: 14px;")
layout.addWidget(self._status_label)
# 通话时长
self._duration_label = QLabel("00:00")
self._duration_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._duration_label.setStyleSheet("color: white; font-size: 32px;")
layout.addWidget(self._duration_label)
# 网络质量
self._quality_label = QLabel("网络质量: --")
self._quality_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
self._quality_label.setStyleSheet("color: #aaa; font-size: 12px;")
layout.addWidget(self._quality_label)
layout.addStretch()
# 控制按钮
button_layout = QHBoxLayout()
button_layout.setSpacing(30)
button_layout.addStretch()
# 静音按钮
self._mute_btn = QPushButton("🎤")
self._mute_btn.setFixedSize(60, 60)
self._mute_btn.setStyleSheet("""
QPushButton {
background-color: #555;
color: white;
border: none;
border-radius: 30px;
font-size: 24px;
}
QPushButton:hover {
background-color: #666;
}
QPushButton:checked {
background-color: #f44336;
}
""")
self._mute_btn.setCheckable(True)
self._mute_btn.clicked.connect(self._on_mute_clicked)
button_layout.addWidget(self._mute_btn)
# 挂断按钮
self._end_btn = QPushButton("📞")
self._end_btn.setFixedSize(70, 70)
self._end_btn.setStyleSheet("""
QPushButton {
background-color: #f44336;
color: white;
border: none;
border-radius: 35px;
font-size: 28px;
}
QPushButton:hover {
background-color: #d32f2f;
}
""")
self._end_btn.clicked.connect(self._on_end_clicked)
button_layout.addWidget(self._end_btn)
# 扬声器按钮
self._speaker_btn = QPushButton("🔊")
self._speaker_btn.setFixedSize(60, 60)
self._speaker_btn.setStyleSheet("""
QPushButton {
background-color: #555;
color: white;
border: none;
border-radius: 30px;
font-size: 24px;
}
QPushButton:hover {
background-color: #666;
}
""")
button_layout.addWidget(self._speaker_btn)
button_layout.addStretch()
layout.addLayout(button_layout)
layout.addSpacing(30)
def _format_duration(self, seconds: int) -> str:
"""格式化时长"""
minutes = seconds // 60
secs = seconds % 60
return f"{minutes:02d}:{secs:02d}"
def _update_duration(self) -> None:
"""更新通话时长"""
self._call_duration += 1
self._duration_label.setText(self._format_duration(self._call_duration))
def _on_mute_clicked(self) -> None:
"""处理静音按钮点击"""
self._is_muted = self._mute_btn.isChecked()
self._mute_btn.setText("🔇" if self._is_muted else "🎤")
self.mute_toggled.emit(self._is_muted)
def _on_end_clicked(self) -> None:
"""处理挂断按钮点击"""
self.end_call_requested.emit()
# ==================== 公共方法 ====================
def start_call(self, peer_name: str) -> None:
"""
开始通话
Args:
peer_name: 通话对象名称
"""
self._peer_name = peer_name
self._peer_name_label.setText(peer_name)
self._status_label.setText("通话中")
self._call_duration = 0
self._duration_label.setText("00:00")
self._duration_timer.start()
logger.info(f"Call started with {peer_name}")
def end_call(self) -> None:
"""结束通话"""
self._duration_timer.stop()
self._status_label.setText("通话已结束")
logger.info("Call ended")
def update_state(self, state: CallState) -> None:
"""
更新通话状态
Args:
state: 通话状态
"""
state_texts = {
CallState.IDLE: "空闲",
CallState.CALLING: "正在呼叫...",
CallState.RINGING: "对方响铃中...",
CallState.CONNECTED: "通话中",
CallState.ENDING: "正在结束...",
}
self._status_label.setText(state_texts.get(state, "未知"))
if state == CallState.CONNECTED:
self._duration_timer.start()
elif state in (CallState.IDLE, CallState.ENDING):
self._duration_timer.stop()
def update_network_quality(self, quality: NetworkQuality) -> None:
"""
更新网络质量显示
实现网络质量指示 (需求 7.8)
WHEN 语音通话进行中 THEN P2P_Client SHALL 显示通话时长和网络质量指示
Args:
quality: 网络质量
"""
quality_texts = {
NetworkQuality.EXCELLENT: ("优秀", "#4caf50"),
NetworkQuality.GOOD: ("良好", "#8bc34a"),
NetworkQuality.FAIR: ("一般", "#ffeb3b"),
NetworkQuality.POOR: ("较差", "#ff9800"),
NetworkQuality.BAD: ("很差", "#f44336"),
}
text, color = quality_texts.get(quality, ("未知", "#999"))
self._quality_label.setText(f"网络质量: {text}")
self._quality_label.setStyleSheet(f"color: {color}; font-size: 12px;")
def get_call_duration(self) -> int:
"""
获取通话时长
Returns:
通话时长(秒)
"""
return self._call_duration
def reset(self) -> None:
"""重置界面"""
self._duration_timer.stop()
self._peer_name = ""
self._is_muted = False
self._call_duration = 0
self._peer_name_label.setText("未知用户")
self._status_label.setText("正在连接...")
self._duration_label.setText("00:00")
self._quality_label.setText("网络质量: --")
self._mute_btn.setChecked(False)
self._mute_btn.setText("🎤")