# 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("🎤")