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