refactor:Optimize chaotic state management
This commit is contained in:
@@ -49,10 +49,8 @@ const ClipboardApp = () => {
|
||||
handleDownloadFile,
|
||||
} = useFileTransferHandler({ messages, putMessageInMs });
|
||||
|
||||
// Initialize WebRTC Connection Hook
|
||||
// 简化的 WebRTC 连接初始化
|
||||
const {
|
||||
sender,
|
||||
receiver,
|
||||
sharePeerCount,
|
||||
retrievePeerCount,
|
||||
broadcastDataToAllPeers,
|
||||
@@ -87,7 +85,7 @@ const ClipboardApp = () => {
|
||||
}
|
||||
}, [resetReceiverConnection, setRetrieveRoomIdInput]);
|
||||
|
||||
// Initialize Room Manager Hook
|
||||
// 大大简化的房间管理 - 不再需要传递任何 WebRTC 依赖
|
||||
const {
|
||||
processRoomIdInput,
|
||||
joinRoom,
|
||||
@@ -97,11 +95,6 @@ const ClipboardApp = () => {
|
||||
} = useRoomManager({
|
||||
messages,
|
||||
putMessageInMs,
|
||||
sender,
|
||||
receiver,
|
||||
broadcastDataToPeers: broadcastDataToAllPeers,
|
||||
resetSenderConnection,
|
||||
resetReceiverConnection,
|
||||
});
|
||||
|
||||
const handleFileDrop = useCallback(
|
||||
@@ -231,7 +224,7 @@ const ClipboardApp = () => {
|
||||
processRoomIdInput={processRoomIdInput}
|
||||
joinRoom={joinRoom}
|
||||
generateShareLinkAndBroadcast={generateShareLinkAndBroadcast}
|
||||
sender={sender}
|
||||
|
||||
shareMessage={shareMessage}
|
||||
currentValidatedShareRoomId={shareRoomId}
|
||||
handleLeaveSenderRoom={handleLeaveSenderRoom}
|
||||
@@ -243,7 +236,7 @@ const ClipboardApp = () => {
|
||||
setRetrieveRoomIdInput={setRetrieveRoomIdInput}
|
||||
joinRoom={joinRoom}
|
||||
retrieveJoinRoomBtnRef={retrieveJoinRoomBtnRef}
|
||||
receiver={receiver}
|
||||
|
||||
richTextToPlainText={richTextToPlainText}
|
||||
handleDownloadFile={handleDownloadFile}
|
||||
requestFile={requestFile}
|
||||
|
||||
@@ -10,7 +10,7 @@ import FileListDisplay from "@/components/ClipboardApp/FileListDisplay";
|
||||
import type { Messages } from "@/types/messages";
|
||||
import type { FileMeta } from "@/types/webrtc";
|
||||
import type { ProgressState } from "@/hooks/useWebRTCConnection"; // Assuming this type is exported
|
||||
import type WebRTC_Recipient from "@/lib/webrtc_Recipient";
|
||||
|
||||
import { useFileTransferStore } from "@/stores/fileTransferStore";
|
||||
|
||||
interface RetrieveTabPanelProps {
|
||||
@@ -23,7 +23,6 @@ interface RetrieveTabPanelProps {
|
||||
setRetrieveRoomIdInput: (value: string) => void;
|
||||
joinRoom: (isSender: boolean, roomId: string) => void;
|
||||
retrieveJoinRoomBtnRef: React.RefObject<HTMLButtonElement>;
|
||||
receiver: WebRTC_Recipient | null;
|
||||
richTextToPlainText: (html: string) => string;
|
||||
handleDownloadFile: (meta: FileMeta) => void;
|
||||
// Functions for WebRTC interaction, passed from parent via useWebRTCConnection
|
||||
@@ -44,7 +43,6 @@ export function RetrieveTabPanel({
|
||||
setRetrieveRoomIdInput,
|
||||
joinRoom,
|
||||
retrieveJoinRoomBtnRef,
|
||||
receiver,
|
||||
richTextToPlainText,
|
||||
handleDownloadFile,
|
||||
requestFile,
|
||||
@@ -117,7 +115,7 @@ export function RetrieveTabPanel({
|
||||
<div id="retrieve-panel" role="tabpanel" aria-labelledby="retrieve-tab">
|
||||
<div className="mb-3 text-sm text-gray-600">
|
||||
{retrieveRoomStatusText ||
|
||||
(receiver && receiver.isInRoom
|
||||
(isReceiverInRoom
|
||||
? messages.text.ClipboardApp.roomStatus.connected_dis
|
||||
: messages.text.ClipboardApp.roomStatus.receiverEmptyMsg)}
|
||||
</div>
|
||||
@@ -141,16 +139,14 @@ export function RetrieveTabPanel({
|
||||
className="flex-1"
|
||||
onClick={() => joinRoom(false, retrieveRoomIdInput)}
|
||||
ref={retrieveJoinRoomBtnRef}
|
||||
disabled={
|
||||
!receiver || isReceiverInRoom || !retrieveRoomIdInput.trim()
|
||||
}
|
||||
disabled={isReceiverInRoom || !retrieveRoomIdInput.trim()}
|
||||
>
|
||||
{messages.text.ClipboardApp.html.joinRoom_dis}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleLeaveRoom}
|
||||
disabled={!receiver || !isReceiverInRoom || isAnyFileTransferring}
|
||||
disabled={!isReceiverInRoom || isAnyFileTransferring}
|
||||
>
|
||||
{messages.text.ClipboardApp.roomStatus.leaveRoomBtn}
|
||||
</Button>
|
||||
|
||||
@@ -12,7 +12,7 @@ import AnimatedButton from "@/components/ui/AnimatedButton";
|
||||
import type { Messages } from "@/types/messages";
|
||||
import type { CustomFile, FileMeta } from "@/types/webrtc";
|
||||
import type { ProgressState } from "@/hooks/useWebRTCConnection";
|
||||
import type WebRTC_Initiator from "@/lib/webrtc_Initiator";
|
||||
|
||||
import { useFileTransferStore } from "@/stores/fileTransferStore";
|
||||
|
||||
// Dynamically import the RichTextEditor
|
||||
@@ -37,7 +37,6 @@ interface SendTabPanelProps {
|
||||
processRoomIdInput: (roomId: string) => void; // Passed from useRoomManager
|
||||
joinRoom: (isSender: boolean, roomId: string) => void;
|
||||
generateShareLinkAndBroadcast: () => void;
|
||||
sender: WebRTC_Initiator | null;
|
||||
shareMessage: string;
|
||||
currentValidatedShareRoomId: string;
|
||||
handleLeaveSenderRoom: () => void; // New prop for leaving room
|
||||
@@ -52,7 +51,6 @@ export function SendTabPanel({
|
||||
processRoomIdInput,
|
||||
joinRoom,
|
||||
generateShareLinkAndBroadcast,
|
||||
sender,
|
||||
shareMessage,
|
||||
currentValidatedShareRoomId,
|
||||
handleLeaveSenderRoom,
|
||||
@@ -137,7 +135,7 @@ export function SendTabPanel({
|
||||
<Button
|
||||
className="w-full sm:w-auto"
|
||||
onClick={() => joinRoom(true, inputFieldValue.trim())} // Attempt to join using the current input field value
|
||||
disabled={!sender || isSenderInRoom || !inputFieldValue.trim()}
|
||||
disabled={isSenderInRoom || !inputFieldValue.trim()}
|
||||
>
|
||||
{messages.text.ClipboardApp.html.joinRoom_dis}
|
||||
</Button>
|
||||
@@ -148,7 +146,6 @@ export function SendTabPanel({
|
||||
onClick={generateShareLinkAndBroadcast}
|
||||
loadingText={messages.text.ClipboardApp.html.SyncSending_loadingText}
|
||||
disabled={
|
||||
!sender ||
|
||||
!isSenderInRoom ||
|
||||
(sendFiles.length === 0 && shareContent.trim() === "") ||
|
||||
!currentValidatedShareRoomId.trim() ||
|
||||
@@ -160,7 +157,7 @@ export function SendTabPanel({
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleLeaveSenderRoom}
|
||||
disabled={!sender || !isSenderInRoom || isAnyFileTransferring}
|
||||
disabled={!isSenderInRoom || isAnyFileTransferring}
|
||||
>
|
||||
{messages.text.ClipboardApp.roomStatus.leaveRoomBtn}
|
||||
</Button>
|
||||
|
||||
+195
-295
@@ -1,39 +1,22 @@
|
||||
import { useEffect, useCallback, useMemo } from "react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { webrtcService } from "@/lib/webrtcService";
|
||||
import { useFileTransferStore } from "@/stores/fileTransferStore";
|
||||
import { fetchRoom, createRoom, checkRoom, leaveRoom } from "@/app/config/api";
|
||||
import { debounce } from "lodash";
|
||||
import type { Messages } from "@/types/messages";
|
||||
import type WebRTC_Initiator from "@/lib/webrtc_Initiator";
|
||||
import type WebRTC_Recipient from "@/lib/webrtc_Recipient";
|
||||
import { useFileTransferStore } from "@/stores/fileTransferStore";
|
||||
|
||||
function format_peopleMsg(template: string, peerCount: number) {
|
||||
return template.replace("{peerCount}", peerCount.toString());
|
||||
}
|
||||
|
||||
// 移除所有 WebRTC 相关的 props 依赖
|
||||
interface UseRoomManagerProps {
|
||||
messages: Messages | null;
|
||||
putMessageInMs: (
|
||||
message: string,
|
||||
isShareEnd?: boolean,
|
||||
displayTimeMs?: number
|
||||
) => void;
|
||||
sender: WebRTC_Initiator | null;
|
||||
receiver: WebRTC_Recipient | null;
|
||||
broadcastDataToPeers: () => Promise<boolean>;
|
||||
resetSenderConnection: () => Promise<void>;
|
||||
resetReceiverConnection: () => Promise<void>;
|
||||
putMessageInMs: (message: string, isShareEnd?: boolean, displayTimeMs?: number) => void;
|
||||
}
|
||||
|
||||
export function useRoomManager({
|
||||
messages,
|
||||
putMessageInMs,
|
||||
sender,
|
||||
receiver,
|
||||
broadcastDataToPeers,
|
||||
resetSenderConnection,
|
||||
resetReceiverConnection,
|
||||
}: UseRoomManagerProps) {
|
||||
// 从 store 中获取状态
|
||||
export function useRoomManager({ messages, putMessageInMs }: UseRoomManagerProps) {
|
||||
// 从 store 获取状态
|
||||
const {
|
||||
shareRoomId,
|
||||
initShareRoomId,
|
||||
@@ -51,295 +34,207 @@ export function useRoomManager({
|
||||
setShareLink,
|
||||
setShareRoomStatusText,
|
||||
setRetrieveRoomStatusText,
|
||||
setSharePeerCount,
|
||||
setRetrievePeerCount,
|
||||
resetReceiverState,
|
||||
resetSenderApp,
|
||||
} = useFileTransferStore();
|
||||
|
||||
// Receiver leave room function
|
||||
const handleLeaveReceiverRoom = useCallback(async () => {
|
||||
console.log(`[RoomManager Debug] Receiver leaving room manually`);
|
||||
if (!receiver || !receiver.roomId || !receiver.peerId || !messages) return;
|
||||
try {
|
||||
await leaveRoom(receiver.roomId, receiver.peerId);
|
||||
putMessageInMs(messages.text.ClipboardApp.roomStatus.leftRoomMsg, false);
|
||||
} catch (error) {
|
||||
console.error("Error leaving room:", error);
|
||||
putMessageInMs("Failed to leave the room.", false);
|
||||
} finally {
|
||||
// Reset application state (不清空房间ID)
|
||||
resetReceiverState();
|
||||
// 清理WebRTC连接状态
|
||||
await resetReceiverConnection();
|
||||
console.log(
|
||||
`[RoomManager Debug] Receiver left room and WebRTC connection reset`
|
||||
);
|
||||
}
|
||||
}, [
|
||||
receiver,
|
||||
putMessageInMs,
|
||||
messages,
|
||||
resetReceiverState,
|
||||
resetReceiverConnection,
|
||||
]);
|
||||
// 加入房间方法 - 直接使用 webrtcService
|
||||
const joinRoom = useCallback(async (isSenderSide: boolean, roomId: string) => {
|
||||
if (!messages) return;
|
||||
|
||||
// Reset sender app state
|
||||
try {
|
||||
console.log(`[RoomManager] 正在加入房间: ${roomId} (${isSenderSide ? '发送方' : '接收方'})`);
|
||||
|
||||
// 如果是发送方且房间ID不是初始ID,需要先创建房间
|
||||
if (isSenderSide && activeTab === "send" && roomId !== initShareRoomId) {
|
||||
try {
|
||||
const success = await createRoom(roomId);
|
||||
if (!success) {
|
||||
putMessageInMs(messages.text.ClipboardApp.joinRoom.DuplicateMsg, isSenderSide);
|
||||
return;
|
||||
}
|
||||
setShareRoomId(roomId);
|
||||
} catch (error) {
|
||||
putMessageInMs(
|
||||
messages.text.ClipboardApp.joinRoom.failMsg + ` (Create room error)`,
|
||||
isSenderSide
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 确定实际要加入的房间ID
|
||||
const actualRoomId = isSenderSide && roomId !== initShareRoomId
|
||||
? roomId
|
||||
: isSenderSide
|
||||
? shareRoomId
|
||||
: roomId;
|
||||
|
||||
// 直接调用 service 方法,无需依赖注入
|
||||
await webrtcService.joinRoom(actualRoomId, isSenderSide);
|
||||
|
||||
putMessageInMs(messages.text.ClipboardApp.joinRoom.successMsg, isSenderSide, 6000);
|
||||
|
||||
// 更新分享链接
|
||||
if (isSenderSide) {
|
||||
const link = `${window.location.origin}${window.location.pathname}?roomId=${actualRoomId}`;
|
||||
setShareLink(link);
|
||||
if (actualRoomId !== shareRoomId) {
|
||||
setShareRoomId(actualRoomId);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[RoomManager] 成功加入房间: ${actualRoomId}`);
|
||||
|
||||
// 注意:广播逻辑已经在 webrtcService 的 onDataChannelOpen 事件中自动处理
|
||||
} catch (error) {
|
||||
console.error("[RoomManager] 加入房间失败:", error);
|
||||
let errorMsg = messages.text.ClipboardApp.joinRoom.failMsg;
|
||||
if (error instanceof Error) {
|
||||
errorMsg = error.message === "Room does not exist"
|
||||
? messages.text.ClipboardApp.joinRoom.notExist
|
||||
: `${messages.text.ClipboardApp.joinRoom.failMsg} ${error.message}`;
|
||||
}
|
||||
putMessageInMs(errorMsg, isSenderSide);
|
||||
}
|
||||
}, [messages, putMessageInMs, activeTab, initShareRoomId, shareRoomId, setShareRoomId, setShareLink]);
|
||||
|
||||
// 生成分享链接并广播
|
||||
const generateShareLinkAndBroadcast = useCallback(async () => {
|
||||
if (!messages || !shareRoomId) return;
|
||||
|
||||
try {
|
||||
if (sharePeerCount === 0) {
|
||||
putMessageInMs(messages.text.ClipboardApp.waitting_tips, true);
|
||||
} else {
|
||||
// 直接调用 service 的广播方法
|
||||
await webrtcService.broadcastDataToAllPeers();
|
||||
}
|
||||
|
||||
// 更新分享链接
|
||||
const link = `${window.location.origin}${window.location.pathname}?roomId=${shareRoomId}`;
|
||||
setShareLink(link);
|
||||
|
||||
console.log("[RoomManager] 分享链接已生成并广播");
|
||||
} catch (error) {
|
||||
console.error("[RoomManager] 生成分享链接失败:", error);
|
||||
putMessageInMs("生成分享链接失败", true);
|
||||
}
|
||||
}, [messages, putMessageInMs, shareRoomId, sharePeerCount, setShareLink]);
|
||||
|
||||
// 接收方离开房间
|
||||
const handleLeaveReceiverRoom = useCallback(async () => {
|
||||
if (!messages) return;
|
||||
|
||||
try {
|
||||
console.log("[RoomManager] 接收方离开房间");
|
||||
|
||||
// 调用后端 API 离开房间
|
||||
if (webrtcService.receiver.roomId && webrtcService.receiver.peerId) {
|
||||
await leaveRoom(webrtcService.receiver.roomId, webrtcService.receiver.peerId);
|
||||
}
|
||||
|
||||
putMessageInMs(messages.text.ClipboardApp.roomStatus.leftRoomMsg, false);
|
||||
|
||||
// 重置接收方状态
|
||||
resetReceiverState();
|
||||
|
||||
// 清理 WebRTC 连接
|
||||
await webrtcService.leaveRoom(false);
|
||||
|
||||
console.log("[RoomManager] 接收方已离开房间并重置状态");
|
||||
} catch (error) {
|
||||
console.error("[RoomManager] 接收方离开房间失败:", error);
|
||||
putMessageInMs("离开房间失败", true);
|
||||
}
|
||||
}, [messages, putMessageInMs, resetReceiverState]);
|
||||
|
||||
// 发送方重置应用状态
|
||||
const resetSenderAppState = useCallback(async () => {
|
||||
try {
|
||||
// 1. Clean up WebRTC connections and reset peer count
|
||||
await resetSenderConnection();
|
||||
console.log("[RoomManager] 重置发送方应用状态");
|
||||
|
||||
// 1. 清理 WebRTC 连接
|
||||
await webrtcService.leaveRoom(true);
|
||||
|
||||
// 2. Clear share link and progress
|
||||
// 2. 清除分享链接和进度
|
||||
resetSenderApp();
|
||||
|
||||
// 3. Get new room ID from backend
|
||||
// 3. 从后端获取新的房间ID
|
||||
const newRoomId = await fetchRoom();
|
||||
setShareRoomId(newRoomId || "");
|
||||
setInitShareRoomId(newRoomId || "");
|
||||
|
||||
console.log(
|
||||
"Sender application state reset successfully, new room ID:",
|
||||
newRoomId
|
||||
);
|
||||
console.log("[RoomManager] 发送方状态重置完成,新房间ID:", newRoomId);
|
||||
} catch (error) {
|
||||
console.error("Error during sender state reset:", error);
|
||||
putMessageInMs("Error resetting sender state.", true);
|
||||
console.error("[RoomManager] 重置发送方状态失败:", error);
|
||||
putMessageInMs("重置发送方状态失败", true);
|
||||
}
|
||||
}, [
|
||||
resetSenderConnection,
|
||||
putMessageInMs,
|
||||
resetSenderApp,
|
||||
setShareRoomId,
|
||||
setInitShareRoomId,
|
||||
]);
|
||||
}, [putMessageInMs, resetSenderApp, setShareRoomId, setInitShareRoomId]);
|
||||
|
||||
// Sender leave room function
|
||||
// 发送方离开房间
|
||||
const handleLeaveSenderRoom = useCallback(async () => {
|
||||
if (!sender || !sender.roomId || !sender.peerId || !messages) return;
|
||||
try {
|
||||
await leaveRoom(sender.roomId, sender.peerId);
|
||||
putMessageInMs(messages.text.ClipboardApp.roomStatus.leftRoomMsg, true);
|
||||
} catch (error) {
|
||||
console.error("Error leaving room:", error);
|
||||
putMessageInMs("Failed to leave the room.", true);
|
||||
} finally {
|
||||
// Reset sender state and get new room ID
|
||||
await resetSenderAppState();
|
||||
}
|
||||
}, [sender, putMessageInMs, resetSenderAppState, messages]);
|
||||
if (!messages) return;
|
||||
|
||||
// Initialize shareRoomId on mount
|
||||
try {
|
||||
console.log("[RoomManager] 发送方离开房间");
|
||||
|
||||
// 调用后端 API 离开房间
|
||||
if (webrtcService.sender.roomId && webrtcService.sender.peerId) {
|
||||
await leaveRoom(webrtcService.sender.roomId, webrtcService.sender.peerId);
|
||||
}
|
||||
|
||||
putMessageInMs(messages.text.ClipboardApp.roomStatus.leftRoomMsg, true);
|
||||
|
||||
// 重置发送方状态并获取新房间ID
|
||||
await resetSenderAppState();
|
||||
} catch (error) {
|
||||
console.error("[RoomManager] 发送方离开房间失败:", error);
|
||||
putMessageInMs("离开房间失败", true);
|
||||
}
|
||||
}, [messages, putMessageInMs, resetSenderAppState]);
|
||||
|
||||
// 房间ID输入处理
|
||||
const processRoomIdInput = useCallback(
|
||||
debounce(async (input: string) => {
|
||||
if (!input.trim() || !messages) return;
|
||||
|
||||
try {
|
||||
const isValid = await checkRoom(input);
|
||||
if (isValid) {
|
||||
console.log(`[RoomManager] 房间 ${input} 验证成功`);
|
||||
setShareRoomId(input);
|
||||
putMessageInMs(messages.text.ClipboardApp.roomCheck.available_msg, true);
|
||||
} else {
|
||||
putMessageInMs(messages.text.ClipboardApp.roomCheck.notAvailable_msg, true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[RoomManager] 验证房间失败:", error);
|
||||
putMessageInMs("验证房间失败", true);
|
||||
}
|
||||
}, 750),
|
||||
[messages, putMessageInMs, setShareRoomId]
|
||||
);
|
||||
|
||||
// 初始化发送方房间ID
|
||||
useEffect(() => {
|
||||
if (
|
||||
messages &&
|
||||
putMessageInMs &&
|
||||
!initShareRoomId &&
|
||||
activeTab === "send"
|
||||
) {
|
||||
// Ensure this only runs on the sender's side on initial load
|
||||
if (messages && putMessageInMs && !initShareRoomId && activeTab === "send") {
|
||||
const initNewRoom = async () => {
|
||||
try {
|
||||
const newRoomId = await fetchRoom();
|
||||
setShareRoomId(newRoomId || "");
|
||||
setInitShareRoomId(newRoomId || "");
|
||||
} catch (err) {
|
||||
console.error("Error fetching initial room:", err);
|
||||
const errorMsg =
|
||||
messages.text?.ClipboardApp?.fetchRoom_err ||
|
||||
"Error fetching room ID.";
|
||||
console.error("[RoomManager] 获取初始房间失败:", err);
|
||||
const errorMsg = messages.text?.ClipboardApp?.fetchRoom_err || "获取房间ID失败";
|
||||
putMessageInMs(errorMsg, true);
|
||||
}
|
||||
};
|
||||
initNewRoom();
|
||||
}
|
||||
}, [
|
||||
messages,
|
||||
initShareRoomId,
|
||||
activeTab,
|
||||
setShareRoomId,
|
||||
setInitShareRoomId,
|
||||
putMessageInMs,
|
||||
]);
|
||||
}, [messages, putMessageInMs, initShareRoomId, activeTab, setShareRoomId, setInitShareRoomId]);
|
||||
|
||||
// Debounced function to actually check the room ID and update the state
|
||||
const performDebouncedRoomCheck = useMemo(
|
||||
() =>
|
||||
debounce(async (roomIdToCheck: string) => {
|
||||
if (!messages || !putMessageInMs) return;
|
||||
|
||||
if (!roomIdToCheck.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const available = await checkRoom(roomIdToCheck);
|
||||
if (available) {
|
||||
setShareRoomId(roomIdToCheck);
|
||||
putMessageInMs(
|
||||
messages.text.ClipboardApp.roomCheck.available_msg,
|
||||
true
|
||||
);
|
||||
} else {
|
||||
putMessageInMs(
|
||||
messages.text.ClipboardApp.roomCheck.notAvailable_msg,
|
||||
true
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error checking room availability:", error);
|
||||
putMessageInMs("Error checking room.", true);
|
||||
}
|
||||
}, 750),
|
||||
[messages, putMessageInMs, setShareRoomId]
|
||||
);
|
||||
|
||||
// UI calls this function to handle changes in the room ID input
|
||||
const processRoomIdInput = useCallback(
|
||||
(inputRoomId: string) => {
|
||||
if (!inputRoomId.trim() && messages && putMessageInMs) {
|
||||
putMessageInMs(messages.text.ClipboardApp.roomCheck.empty_msg, true);
|
||||
performDebouncedRoomCheck.cancel();
|
||||
return;
|
||||
}
|
||||
performDebouncedRoomCheck(inputRoomId);
|
||||
},
|
||||
[performDebouncedRoomCheck, messages, putMessageInMs]
|
||||
);
|
||||
|
||||
const joinRoom = useCallback(
|
||||
async (isSenderSide: boolean, currentRoomIdToJoin: string) => {
|
||||
if (
|
||||
!messages ||
|
||||
!putMessageInMs ||
|
||||
(isSenderSide && !sender) ||
|
||||
(!isSenderSide && !receiver)
|
||||
) {
|
||||
console.warn("joinRoom prerequisites not met");
|
||||
return;
|
||||
}
|
||||
|
||||
const peer = isSenderSide ? sender : receiver;
|
||||
if (!peer) return;
|
||||
|
||||
if (!currentRoomIdToJoin.trim()) {
|
||||
putMessageInMs(
|
||||
messages.text.ClipboardApp.joinRoom.EmptyMsg,
|
||||
isSenderSide
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create room if sender and not the initial room ID
|
||||
if (isSenderSide && activeTab === "send" && !peer.isInRoom) {
|
||||
if (currentRoomIdToJoin !== initShareRoomId) {
|
||||
try {
|
||||
const success = await createRoom(currentRoomIdToJoin);
|
||||
if (!success) {
|
||||
putMessageInMs(
|
||||
messages.text.ClipboardApp.joinRoom.DuplicateMsg,
|
||||
isSenderSide
|
||||
);
|
||||
return;
|
||||
}
|
||||
setShareRoomId(currentRoomIdToJoin);
|
||||
} catch (error) {
|
||||
putMessageInMs(
|
||||
messages.text.ClipboardApp.joinRoom.failMsg +
|
||||
` (Create room error)`,
|
||||
isSenderSide
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const actualRoomIdForSenderJoin =
|
||||
isSenderSide && currentRoomIdToJoin !== initShareRoomId
|
||||
? currentRoomIdToJoin
|
||||
: isSenderSide
|
||||
? shareRoomId
|
||||
: currentRoomIdToJoin;
|
||||
|
||||
console.log(
|
||||
`[RoomManager Debug] ${
|
||||
isSenderSide ? "Sender" : "Receiver"
|
||||
} joining room: ${actualRoomIdForSenderJoin}`
|
||||
);
|
||||
console.log(
|
||||
`[RoomManager Debug] Peer current state - isInRoom: ${peer.isInRoom}, roomId: ${peer.roomId}`
|
||||
);
|
||||
|
||||
await peer.joinRoom(actualRoomIdForSenderJoin, isSenderSide);
|
||||
putMessageInMs(
|
||||
messages.text.ClipboardApp.joinRoom.successMsg,
|
||||
isSenderSide,
|
||||
6000
|
||||
);
|
||||
|
||||
// 更新 Store 中的房间状态
|
||||
if (isSenderSide) {
|
||||
useFileTransferStore.getState().setIsSenderInRoom(true);
|
||||
console.log(
|
||||
`[RoomManager Debug] Sender joined room, setIsSenderInRoom(true)`
|
||||
);
|
||||
const link = `${window.location.origin}${window.location.pathname}?roomId=${actualRoomIdForSenderJoin}`;
|
||||
setShareLink(link);
|
||||
if (actualRoomIdForSenderJoin !== shareRoomId) {
|
||||
setShareRoomId(actualRoomIdForSenderJoin);
|
||||
}
|
||||
} else {
|
||||
useFileTransferStore.getState().setIsReceiverInRoom(true);
|
||||
console.log(
|
||||
`[RoomManager Debug] Receiver joined room, setIsReceiverInRoom(true)`
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
let errorMsgToShow = messages.text.ClipboardApp.joinRoom.failMsg;
|
||||
if (error instanceof Error) {
|
||||
errorMsgToShow =
|
||||
error.message === "Room does not exist"
|
||||
? messages.text.ClipboardApp.joinRoom.notExist
|
||||
: `${messages.text.ClipboardApp.joinRoom.failMsg} ${error.message}`;
|
||||
}
|
||||
putMessageInMs(errorMsgToShow, isSenderSide);
|
||||
}
|
||||
},
|
||||
[
|
||||
messages,
|
||||
putMessageInMs,
|
||||
sender,
|
||||
receiver,
|
||||
activeTab,
|
||||
initShareRoomId,
|
||||
shareRoomId,
|
||||
setShareLink,
|
||||
setShareRoomId,
|
||||
]
|
||||
);
|
||||
|
||||
const generateShareLinkAndBroadcast = useCallback(async () => {
|
||||
if (!sender || !messages || !putMessageInMs || !shareRoomId) return;
|
||||
|
||||
if (sharePeerCount === 0) {
|
||||
putMessageInMs(messages.text.ClipboardApp.waitting_tips, true);
|
||||
} else {
|
||||
await broadcastDataToPeers();
|
||||
}
|
||||
const link = `${window.location.origin}${window.location.pathname}?roomId=${shareRoomId}`;
|
||||
setShareLink(link);
|
||||
}, [
|
||||
sender,
|
||||
messages,
|
||||
putMessageInMs,
|
||||
shareRoomId,
|
||||
sharePeerCount,
|
||||
setShareLink,
|
||||
broadcastDataToPeers,
|
||||
]);
|
||||
|
||||
// useEffect for room status text
|
||||
// 房间状态文本更新
|
||||
useEffect(() => {
|
||||
if (!messages) {
|
||||
if (activeTab === "send") setShareRoomStatusText("");
|
||||
@@ -348,25 +243,22 @@ export function useRoomManager({
|
||||
}
|
||||
|
||||
const isInRoom = activeTab === "send" ? isSenderInRoom : isReceiverInRoom;
|
||||
const currentPeerCount =
|
||||
activeTab === "send" ? sharePeerCount : retrievePeerCount;
|
||||
const currentPeerCount = activeTab === "send" ? sharePeerCount : retrievePeerCount;
|
||||
let statusText = "";
|
||||
|
||||
if (!isInRoom) {
|
||||
statusText =
|
||||
activeTab === "retrieve"
|
||||
? messages.text.ClipboardApp.roomStatus.receiverEmptyMsg
|
||||
: messages.text.ClipboardApp.roomStatus.senderEmptyMsg;
|
||||
statusText = activeTab === "retrieve"
|
||||
? messages.text.ClipboardApp.roomStatus.receiverEmptyMsg
|
||||
: messages.text.ClipboardApp.roomStatus.senderEmptyMsg;
|
||||
} else if (currentPeerCount === 0) {
|
||||
statusText = messages.text.ClipboardApp.roomStatus.onlyOneMsg;
|
||||
} else {
|
||||
statusText =
|
||||
activeTab === "send"
|
||||
? format_peopleMsg(
|
||||
messages.text.ClipboardApp.roomStatus.peopleMsg_template,
|
||||
currentPeerCount + 1
|
||||
)
|
||||
: messages.text.ClipboardApp.roomStatus.connected_dis;
|
||||
statusText = activeTab === "send"
|
||||
? format_peopleMsg(
|
||||
messages.text.ClipboardApp.roomStatus.peopleMsg_template,
|
||||
currentPeerCount + 1
|
||||
)
|
||||
: messages.text.ClipboardApp.roomStatus.connected_dis;
|
||||
}
|
||||
|
||||
if (activeTab === "send") setShareRoomStatusText(statusText);
|
||||
@@ -384,15 +276,23 @@ export function useRoomManager({
|
||||
]);
|
||||
|
||||
return {
|
||||
// 状态
|
||||
shareRoomId,
|
||||
initShareRoomId,
|
||||
shareLink,
|
||||
shareRoomStatusText,
|
||||
retrieveRoomStatusText,
|
||||
sharePeerCount,
|
||||
retrievePeerCount,
|
||||
senderDisconnected,
|
||||
isSenderInRoom,
|
||||
isReceiverInRoom,
|
||||
|
||||
// 方法
|
||||
processRoomIdInput,
|
||||
joinRoom,
|
||||
generateShareLinkAndBroadcast,
|
||||
handleLeaveReceiverRoom,
|
||||
handleLeaveSenderRoom,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,410 +1,77 @@
|
||||
import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import WebRTC_Initiator from "@/lib/webrtc_Initiator";
|
||||
import WebRTC_Recipient from "@/lib/webrtc_Recipient";
|
||||
import FileSender from "@/lib/fileSender";
|
||||
import FileReceiver from "@/lib/fileReceiver";
|
||||
import {
|
||||
config,
|
||||
getIceServers,
|
||||
getSocketOptions,
|
||||
} from "@/app/config/environment";
|
||||
import type { CustomFile, fileMetadata, FileMeta } from "@/types/webrtc";
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { webrtcService } from '@/lib/webrtcService';
|
||||
import { useFileTransferStore } from '@/stores/fileTransferStore';
|
||||
import type { Messages } from "@/types/messages";
|
||||
import { useFileTransferStore } from "@/stores/fileTransferStore";
|
||||
|
||||
const developmentEnv = process.env.NEXT_PUBLIC_development === "true";
|
||||
|
||||
// Types for progress states
|
||||
// 保留类型定义以保持兼容性
|
||||
export type PeerProgressDetails = { progress: number; speed: number };
|
||||
export type FileProgressPeers = { [peerId: string]: PeerProgressDetails };
|
||||
export type ProgressState = { [fileId: string]: FileProgressPeers };
|
||||
|
||||
interface UseWebRTCConnectionProps {
|
||||
// For user feedback and messages from the hook, if any (mostly for console for now)
|
||||
messages: Messages | null;
|
||||
putMessageInMs: (
|
||||
message: string,
|
||||
isShareEnd?: boolean,
|
||||
displayTimeMs?: number
|
||||
) => void;
|
||||
putMessageInMs: (message: string, isShareEnd?: boolean, displayTimeMs?: number) => void;
|
||||
}
|
||||
|
||||
export function useWebRTCConnection({
|
||||
messages,
|
||||
putMessageInMs,
|
||||
}: UseWebRTCConnectionProps) {
|
||||
const [sender, setSender] = useState<WebRTC_Initiator | null>(null);
|
||||
const [receiver, setReceiver] = useState<WebRTC_Recipient | null>(null);
|
||||
const [senderFileTransfer, setSenderFileTransfer] =
|
||||
useState<FileSender | null>(null);
|
||||
const [receiverFileTransfer, setReceiverFileTransfer] =
|
||||
useState<FileReceiver | null>(null);
|
||||
|
||||
// 从 store 中获取状态和数据
|
||||
export function useWebRTCConnection({ messages, putMessageInMs }: UseWebRTCConnectionProps) {
|
||||
// 从 store 获取状态
|
||||
const {
|
||||
shareContent,
|
||||
sendFiles,
|
||||
sharePeerCount,
|
||||
retrievePeerCount,
|
||||
senderDisconnected,
|
||||
sendProgress,
|
||||
receiveProgress,
|
||||
senderDisconnected,
|
||||
setSharePeerCount,
|
||||
setRetrievePeerCount,
|
||||
setSendProgress,
|
||||
setReceiveProgress,
|
||||
setSenderDisconnected,
|
||||
setIsAnyFileTransferring,
|
||||
} = useFileTransferStore();
|
||||
|
||||
// Calculate isAnyFileTransferring internally based on progress states
|
||||
// 计算是否有文件正在传输
|
||||
const isAnyFileTransferring = useMemo(() => {
|
||||
const allProgress = [
|
||||
...Object.values(sendProgress),
|
||||
...Object.values(receiveProgress),
|
||||
];
|
||||
return allProgress.some((fileProgress: unknown) => {
|
||||
const typedFileProgress = fileProgress as FileProgressPeers;
|
||||
return Object.values(typedFileProgress).some((progress: unknown) => {
|
||||
const typedProgress = progress as PeerProgressDetails;
|
||||
return typedProgress.progress > 0 && typedProgress.progress < 1;
|
||||
return allProgress.some((fileProgress: any) => {
|
||||
return Object.values(fileProgress).some((progress: any) => {
|
||||
return progress.progress > 0 && progress.progress < 1;
|
||||
});
|
||||
});
|
||||
}, [sendProgress, receiveProgress]);
|
||||
|
||||
// 更新 store 中的 isAnyFileTransferring 状态
|
||||
useEffect(() => {
|
||||
setIsAnyFileTransferring(isAnyFileTransferring);
|
||||
}, [isAnyFileTransferring, setIsAnyFileTransferring]);
|
||||
|
||||
// Initialize WebRTC objects and their cleanup
|
||||
// 确保服务在 React 生命周期内被初始化
|
||||
useEffect(() => {
|
||||
const webRTCConfig = {
|
||||
iceServers: getIceServers(),
|
||||
socketOptions: getSocketOptions() || {},
|
||||
signalingServer: config.API_URL,
|
||||
};
|
||||
const senderConn = new WebRTC_Initiator(webRTCConfig);
|
||||
const receiverConn = new WebRTC_Recipient(webRTCConfig);
|
||||
setSender(senderConn);
|
||||
setReceiver(receiverConn);
|
||||
|
||||
const senderFT = new FileSender(senderConn);
|
||||
const receiverFT = new FileReceiver(receiverConn);
|
||||
setSenderFileTransfer(senderFT);
|
||||
setReceiverFileTransfer(receiverFT);
|
||||
if (developmentEnv)
|
||||
console.log("WebRTC connection and file transfer instances created");
|
||||
|
||||
console.log('[useWebRTCConnection] WebRTC 服务已初始化');
|
||||
|
||||
return () => {
|
||||
if (developmentEnv) console.log("Cleaning up WebRTC instances");
|
||||
senderConn.cleanUpBeforeExit();
|
||||
receiverConn.cleanUpBeforeExit();
|
||||
console.log('[useWebRTCConnection] Hook 清理');
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Internal function to send text and file metadata to a specific peer
|
||||
const sendStringAndMetasToPeer = useCallback(
|
||||
async (peerId: string, textContent: string, filesToSend: CustomFile[]) => {
|
||||
if (!senderFileTransfer) {
|
||||
console.error(
|
||||
"SenderFileTransfer not initialized for sendStringAndMetasToPeer"
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (textContent) {
|
||||
await senderFileTransfer.sendString(textContent, peerId);
|
||||
}
|
||||
if (filesToSend.length > 0) {
|
||||
senderFileTransfer.sendFileMeta(filesToSend, peerId);
|
||||
}
|
||||
},
|
||||
[senderFileTransfer]
|
||||
);
|
||||
|
||||
// Exposed function to broadcast data to all connected sender peers
|
||||
const broadcastDataToAllPeers = useCallback(async () => {
|
||||
if (!sender || sender.peerConnections.size === 0) {
|
||||
if (developmentEnv)
|
||||
console.warn(
|
||||
"No sender peers to broadcast to, or sender not initialized."
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!senderFileTransfer) {
|
||||
console.error("senderFileTransfer is not initialized for broadcast.");
|
||||
return false;
|
||||
}
|
||||
const peerIds = Array.from(sender.peerConnections.keys());
|
||||
if (developmentEnv)
|
||||
console.log(`Broadcasting to peers: ${peerIds.join(", ")}`);
|
||||
try {
|
||||
await Promise.all(
|
||||
peerIds.map((peerId) =>
|
||||
sendStringAndMetasToPeer(peerId, shareContent, sendFiles)
|
||||
)
|
||||
);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("Error broadcasting data to peers:", error);
|
||||
return false;
|
||||
}
|
||||
}, [
|
||||
sender,
|
||||
senderFileTransfer,
|
||||
sendStringAndMetasToPeer,
|
||||
shareContent,
|
||||
sendFiles,
|
||||
]);
|
||||
|
||||
// Setup sender and receiver event handlers
|
||||
useEffect(() => {
|
||||
if (sender && senderFileTransfer) {
|
||||
sender.onConnectionStateChange = (state, peerId) => {
|
||||
if (developmentEnv)
|
||||
console.log(`Sender connection state with ${peerId}: ${state}`);
|
||||
// 更新连接状态
|
||||
useFileTransferStore.getState().setShareConnectionState(state as any);
|
||||
setSharePeerCount(sender.peerConnections.size);
|
||||
if (state === "connected") {
|
||||
senderFileTransfer.setProgressCallback(
|
||||
(fileId, progress: number, speed: number) => {
|
||||
useFileTransferStore
|
||||
.getState()
|
||||
.updateSendProgress(fileId, peerId, { progress, speed });
|
||||
},
|
||||
peerId
|
||||
);
|
||||
}
|
||||
};
|
||||
sender.onDataChannelOpen = () => {
|
||||
// 当数据通道打开时,标记发送方已加入房间
|
||||
useFileTransferStore.getState().setIsSenderInRoom(true);
|
||||
broadcastDataToAllPeers();
|
||||
};
|
||||
|
||||
sender.onPeerDisconnected = (peerId) => {
|
||||
console.log(`[WebRTC Debug] Sender peer ${peerId} disconnected`);
|
||||
setTimeout(() => {
|
||||
const newPeerCount = sender.peerConnections.size;
|
||||
console.log(
|
||||
`[WebRTC Debug] Sender peer count after disconnect: ${newPeerCount}`
|
||||
);
|
||||
setSharePeerCount(newPeerCount);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
sender.onError = (error) => {
|
||||
console.error("Sender Error:", error.message, error.context);
|
||||
putMessageInMs(`Connection error: ${error.message}`, true);
|
||||
};
|
||||
}
|
||||
|
||||
if (receiver && receiverFileTransfer) {
|
||||
receiver.onConnectionStateChange = (state, peerId) => {
|
||||
if (developmentEnv)
|
||||
console.log(`Receiver connection state with ${peerId}: ${state}`);
|
||||
// 更新连接状态
|
||||
useFileTransferStore
|
||||
.getState()
|
||||
.setRetrieveConnectionState(state as any);
|
||||
setRetrievePeerCount(receiver.peerConnections.size);
|
||||
if (state === "connected") {
|
||||
receiverFileTransfer.setProgressCallback(
|
||||
(fileId, progress: number, speed: number) => {
|
||||
useFileTransferStore
|
||||
.getState()
|
||||
.updateReceiveProgress(fileId, peerId, { progress, speed });
|
||||
}
|
||||
);
|
||||
} else if (state === "failed" || state === "disconnected") {
|
||||
if (isAnyFileTransferring) {
|
||||
receiverFileTransfer.gracefulShutdown();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
receiverFileTransfer.onStringReceived = (data) => {
|
||||
const peerId = "testId";
|
||||
if (developmentEnv) console.log(`String received from peer ${peerId}`);
|
||||
useFileTransferStore.getState().setRetrievedContent(data);
|
||||
};
|
||||
|
||||
receiverFileTransfer.onFileMetaReceived = (meta) => {
|
||||
const peerId = "testId";
|
||||
if (developmentEnv)
|
||||
console.log(
|
||||
`File meta received from peer ${peerId} for: ${meta.name}`
|
||||
);
|
||||
const { type, ...metaWithoutType } = meta;
|
||||
const store = useFileTransferStore.getState();
|
||||
// Filter out existing file with same ID and add the new one
|
||||
const DPrev = store.retrievedFileMetas.filter(
|
||||
(existingFile) => existingFile.fileId !== metaWithoutType.fileId
|
||||
);
|
||||
store.setRetrievedFileMetas([...DPrev, metaWithoutType]);
|
||||
};
|
||||
|
||||
receiverFileTransfer.onFileReceived = async (file) => {
|
||||
const peerId = "testId"; // This should be dynamic in a multi-peer scenario
|
||||
if (developmentEnv)
|
||||
console.log(`File received from peer ${peerId}: ${file.name}`);
|
||||
// Directly call the store action
|
||||
useFileTransferStore.getState().addRetrievedFile(file);
|
||||
};
|
||||
|
||||
receiver.onPeerDisconnected = (peerId) => {
|
||||
console.log(`[WebRTC Debug] Receiver peer ${peerId} disconnected`);
|
||||
setSenderDisconnected(true);
|
||||
setRetrievePeerCount(0);
|
||||
// 注意:接收端断开连接时应该保持在房间状态,除非主动离开
|
||||
console.log(
|
||||
`[WebRTC Debug] Receiver peer disconnected, but staying in room`
|
||||
);
|
||||
};
|
||||
|
||||
receiver.onConnectionEstablished = (peerId) => {
|
||||
console.log(
|
||||
`[WebRTC Debug] Receiver connection established with ${peerId}`
|
||||
);
|
||||
setSenderDisconnected(false);
|
||||
useFileTransferStore.getState().setIsReceiverInRoom(true);
|
||||
console.log(
|
||||
`[WebRTC Debug] Receiver setIsReceiverInRoom(true) after connection established`
|
||||
);
|
||||
};
|
||||
|
||||
receiver.onError = (error) => {
|
||||
console.error("Receiver Error:", error.message, error.context);
|
||||
putMessageInMs(`Connection error: ${error.message}`, false);
|
||||
};
|
||||
}
|
||||
}, [
|
||||
sender,
|
||||
senderFileTransfer,
|
||||
receiver,
|
||||
receiverFileTransfer,
|
||||
putMessageInMs,
|
||||
broadcastDataToAllPeers,
|
||||
isAnyFileTransferring,
|
||||
setSharePeerCount,
|
||||
setRetrievePeerCount,
|
||||
setSenderDisconnected,
|
||||
]);
|
||||
|
||||
// Calculate isContentPresent from store data
|
||||
const isContentPresent = useMemo(() => {
|
||||
return shareContent !== "" || sendFiles.length > 0;
|
||||
}, [shareContent, sendFiles]);
|
||||
|
||||
// Effect to handle graceful shutdown on page unload
|
||||
useEffect(() => {
|
||||
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
||||
if (isContentPresent || isAnyFileTransferring) {
|
||||
if (isAnyFileTransferring) {
|
||||
receiverFileTransfer?.gracefulShutdown();
|
||||
}
|
||||
e.preventDefault();
|
||||
e.returnValue = "";
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
};
|
||||
}, [isContentPresent, isAnyFileTransferring, receiverFileTransfer]);
|
||||
|
||||
const requestFile = useCallback(
|
||||
(fileId: string, peerId?: string) => {
|
||||
if (!receiverFileTransfer) return;
|
||||
if (developmentEnv)
|
||||
console.log(
|
||||
`Requesting file ${fileId} from peer ${peerId || "default"}`
|
||||
);
|
||||
receiverFileTransfer.requestFile(fileId);
|
||||
},
|
||||
[receiverFileTransfer]
|
||||
);
|
||||
|
||||
const requestFolder = useCallback(
|
||||
(folderName: string, peerId?: string) => {
|
||||
if (!receiverFileTransfer) return;
|
||||
if (developmentEnv)
|
||||
console.log(
|
||||
`Requesting folder ${folderName} from peer ${peerId || "default"}`
|
||||
);
|
||||
receiverFileTransfer.requestFolder(folderName);
|
||||
},
|
||||
[receiverFileTransfer]
|
||||
);
|
||||
|
||||
const setReceiverDirectoryHandle = useCallback(
|
||||
async (directoryHandle: FileSystemDirectoryHandle): Promise<void> => {
|
||||
if (!receiverFileTransfer) return;
|
||||
if (developmentEnv)
|
||||
console.log("Setting receiver save directory handle.");
|
||||
return receiverFileTransfer.setSaveDirectory(directoryHandle);
|
||||
},
|
||||
[receiverFileTransfer]
|
||||
);
|
||||
|
||||
const getReceiverSaveType = useCallback(():
|
||||
| { [fileId: string]: boolean }
|
||||
| undefined => {
|
||||
return receiverFileTransfer?.saveType;
|
||||
}, [receiverFileTransfer]);
|
||||
|
||||
// Reset function for receiver connection (for leave room functionality)
|
||||
const resetReceiverConnection = useCallback(async () => {
|
||||
if (receiver) {
|
||||
setSenderDisconnected(false);
|
||||
setRetrievePeerCount(0);
|
||||
useFileTransferStore.getState().setIsReceiverInRoom(false);
|
||||
await receiver.leaveRoomAndCleanup();
|
||||
}
|
||||
}, [receiver, setSenderDisconnected, setRetrievePeerCount]);
|
||||
|
||||
// Reset function for sender connection (for leave room functionality)
|
||||
const resetSenderConnection = useCallback(async () => {
|
||||
if (sender) {
|
||||
await sender.leaveRoomAndCleanup();
|
||||
setSharePeerCount(0);
|
||||
useFileTransferStore.getState().setIsSenderInRoom(false);
|
||||
}
|
||||
}, [sender, setSharePeerCount]);
|
||||
|
||||
// Manual safe save function
|
||||
const manualSafeSave = useCallback(() => {
|
||||
if (receiverFileTransfer) {
|
||||
receiverFileTransfer.gracefulShutdown();
|
||||
if (putMessageInMs && messages) {
|
||||
putMessageInMs(
|
||||
messages.text.FileListDisplay.safeSaveSuccessMsg,
|
||||
false,
|
||||
3000
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [receiverFileTransfer, putMessageInMs, messages]);
|
||||
|
||||
return {
|
||||
sender,
|
||||
receiver,
|
||||
// 状态从 store 获取
|
||||
sharePeerCount,
|
||||
retrievePeerCount,
|
||||
senderDisconnected,
|
||||
sendProgress,
|
||||
receiveProgress,
|
||||
isAnyFileTransferring,
|
||||
broadcastDataToAllPeers,
|
||||
requestFile,
|
||||
requestFolder,
|
||||
setReceiverDirectoryHandle,
|
||||
getReceiverSaveType,
|
||||
senderDisconnected,
|
||||
resetReceiverConnection,
|
||||
resetSenderConnection,
|
||||
manualSafeSave,
|
||||
|
||||
// 方法直接从 service 暴露
|
||||
broadcastDataToAllPeers: webrtcService.broadcastDataToAllPeers.bind(webrtcService),
|
||||
requestFile: webrtcService.requestFile.bind(webrtcService),
|
||||
requestFolder: webrtcService.requestFolder.bind(webrtcService),
|
||||
setReceiverDirectoryHandle: webrtcService.setReceiverDirectoryHandle.bind(webrtcService),
|
||||
getReceiverSaveType: webrtcService.getReceiverSaveType.bind(webrtcService),
|
||||
manualSafeSave: webrtcService.manualSafeSave.bind(webrtcService),
|
||||
|
||||
// 重置连接方法
|
||||
resetSenderConnection: () => webrtcService.leaveRoom(true),
|
||||
resetReceiverConnection: () => webrtcService.leaveRoom(false),
|
||||
|
||||
// 为了兼容性,保留这些属性(但实际上不再需要)
|
||||
sender: webrtcService.sender,
|
||||
receiver: webrtcService.receiver,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
import WebRTC_Initiator from "@/lib/webrtc_Initiator";
|
||||
import WebRTC_Recipient from "@/lib/webrtc_Recipient";
|
||||
import FileSender from "@/lib/fileSender";
|
||||
import FileReceiver from "@/lib/fileReceiver";
|
||||
import { getIceServers, getSocketOptions, config } from "@/app/config/environment";
|
||||
import { useFileTransferStore } from "@/stores/fileTransferStore";
|
||||
import type { CustomFile } from "@/types/webrtc";
|
||||
|
||||
class WebRTCService {
|
||||
public sender: WebRTC_Initiator;
|
||||
public receiver: WebRTC_Recipient;
|
||||
public fileSender: FileSender;
|
||||
public fileReceiver: FileReceiver;
|
||||
|
||||
private static instance: WebRTCService;
|
||||
|
||||
private constructor() {
|
||||
const webRTCConfig = {
|
||||
iceServers: getIceServers(),
|
||||
socketOptions: getSocketOptions() || {},
|
||||
signalingServer: config.API_URL,
|
||||
};
|
||||
|
||||
this.sender = new WebRTC_Initiator(webRTCConfig);
|
||||
this.receiver = new WebRTC_Recipient(webRTCConfig);
|
||||
this.fileSender = new FileSender(this.sender);
|
||||
this.fileReceiver = new FileReceiver(this.receiver);
|
||||
|
||||
this.initializeEventHandlers();
|
||||
console.log("WebRTC Service 初始化完成 (单例模式)");
|
||||
}
|
||||
|
||||
public static getInstance(): WebRTCService {
|
||||
if (!WebRTCService.instance) {
|
||||
WebRTCService.instance = new WebRTCService();
|
||||
}
|
||||
return WebRTCService.instance;
|
||||
}
|
||||
|
||||
private initializeEventHandlers(): void {
|
||||
// 发送方事件处理
|
||||
this.sender.onConnectionStateChange = (state, peerId) => {
|
||||
console.log(`[WebRTC Service] 发送方连接状态: ${state} (对等端: ${peerId})`);
|
||||
useFileTransferStore.getState().setShareConnectionState(state as any);
|
||||
useFileTransferStore.getState().setSharePeerCount(this.sender.peerConnections.size);
|
||||
|
||||
if (state === 'connected') {
|
||||
this.fileSender.setProgressCallback((fileId, progress, speed) => {
|
||||
useFileTransferStore.getState().updateSendProgress(fileId, peerId, { progress, speed });
|
||||
}, peerId);
|
||||
}
|
||||
};
|
||||
|
||||
this.sender.onDataChannelOpen = (peerId) => {
|
||||
console.log(`[WebRTC Service] 发送方数据通道打开: ${peerId}`);
|
||||
useFileTransferStore.getState().setIsSenderInRoom(true);
|
||||
// 自动广播当前内容
|
||||
this.broadcastDataToAllPeers();
|
||||
};
|
||||
|
||||
this.sender.onPeerDisconnected = (peerId) => {
|
||||
console.log(`[WebRTC Service] 发送方对等端断开: ${peerId}`);
|
||||
setTimeout(() => {
|
||||
useFileTransferStore.getState().setSharePeerCount(this.sender.peerConnections.size);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
this.sender.onError = (error) => {
|
||||
console.error("[WebRTC Service] 发送方错误:", error.message);
|
||||
};
|
||||
|
||||
// 接收方事件处理
|
||||
this.receiver.onConnectionStateChange = (state, peerId) => {
|
||||
console.log(`[WebRTC Service] 接收方连接状态: ${state} (对等端: ${peerId})`);
|
||||
useFileTransferStore.getState().setRetrieveConnectionState(state as any);
|
||||
useFileTransferStore.getState().setRetrievePeerCount(this.receiver.peerConnections.size);
|
||||
|
||||
if (state === 'connected') {
|
||||
this.fileReceiver.setProgressCallback((fileId, progress, speed) => {
|
||||
useFileTransferStore.getState().updateReceiveProgress(fileId, peerId, { progress, speed });
|
||||
});
|
||||
} else if (state === 'failed' || state === 'disconnected') {
|
||||
const { isAnyFileTransferring } = useFileTransferStore.getState();
|
||||
if (isAnyFileTransferring) {
|
||||
this.fileReceiver.gracefulShutdown();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.receiver.onConnectionEstablished = (peerId) => {
|
||||
console.log(`[WebRTC Service] 接收方连接建立: ${peerId}`);
|
||||
useFileTransferStore.getState().setSenderDisconnected(false);
|
||||
useFileTransferStore.getState().setIsReceiverInRoom(true);
|
||||
};
|
||||
|
||||
this.receiver.onPeerDisconnected = (peerId) => {
|
||||
console.log(`[WebRTC Service] 接收方对等端断开: ${peerId}`);
|
||||
useFileTransferStore.getState().setSenderDisconnected(true);
|
||||
useFileTransferStore.getState().setRetrievePeerCount(0);
|
||||
};
|
||||
|
||||
this.fileReceiver.onStringReceived = (data) => {
|
||||
useFileTransferStore.getState().setRetrievedContent(data);
|
||||
};
|
||||
|
||||
this.fileReceiver.onFileMetaReceived = (meta) => {
|
||||
const { type, ...metaWithoutType } = meta;
|
||||
const store = useFileTransferStore.getState();
|
||||
const filteredMetas = store.retrievedFileMetas.filter(
|
||||
(existingFile) => existingFile.fileId !== metaWithoutType.fileId
|
||||
);
|
||||
store.setRetrievedFileMetas([...filteredMetas, metaWithoutType]);
|
||||
};
|
||||
|
||||
this.fileReceiver.onFileReceived = async (file) => {
|
||||
useFileTransferStore.getState().addRetrievedFile(file);
|
||||
};
|
||||
}
|
||||
|
||||
// 业务方法
|
||||
public async joinRoom(roomId: string, isSender: boolean): Promise<void> {
|
||||
console.log(`[WebRTC Service] 加入房间: ${roomId} (${isSender ? '发送方' : '接收方'})`);
|
||||
|
||||
const peer = isSender ? this.sender : this.receiver;
|
||||
await peer.joinRoom(roomId, isSender);
|
||||
|
||||
const setInRoom = isSender
|
||||
? useFileTransferStore.getState().setIsSenderInRoom
|
||||
: useFileTransferStore.getState().setIsReceiverInRoom;
|
||||
setInRoom(true);
|
||||
}
|
||||
|
||||
public async leaveRoom(isSender: boolean): Promise<void> {
|
||||
console.log(`[WebRTC Service] 离开房间 (${isSender ? '发送方' : '接收方'})`);
|
||||
|
||||
if (isSender) {
|
||||
await this.sender.leaveRoomAndCleanup();
|
||||
useFileTransferStore.getState().setIsSenderInRoom(false);
|
||||
useFileTransferStore.getState().setSharePeerCount(0);
|
||||
} else {
|
||||
await this.receiver.leaveRoomAndCleanup();
|
||||
useFileTransferStore.getState().setIsReceiverInRoom(false);
|
||||
useFileTransferStore.getState().setRetrievePeerCount(0);
|
||||
}
|
||||
}
|
||||
|
||||
public async broadcastDataToAllPeers(): Promise<boolean> {
|
||||
const { shareContent, sendFiles } = useFileTransferStore.getState();
|
||||
const peerIds = Array.from(this.sender.peerConnections.keys());
|
||||
|
||||
if (peerIds.length === 0) {
|
||||
console.warn("[WebRTC Service] 没有连接的对等端进行广播");
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await Promise.all(
|
||||
peerIds.map(async (peerId) => {
|
||||
if (shareContent) {
|
||||
await this.fileSender.sendString(shareContent, peerId);
|
||||
}
|
||||
if (sendFiles.length > 0) {
|
||||
this.fileSender.sendFileMeta(sendFiles, peerId);
|
||||
}
|
||||
})
|
||||
);
|
||||
console.log(`[WebRTC Service] 成功广播到 ${peerIds.length} 个对等端`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error("[WebRTC Service] 广播失败:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public requestFile(fileId: string): void {
|
||||
this.fileReceiver.requestFile(fileId);
|
||||
}
|
||||
|
||||
public requestFolder(folderName: string): void {
|
||||
this.fileReceiver.requestFolder(folderName);
|
||||
}
|
||||
|
||||
public async setReceiverDirectoryHandle(directoryHandle: FileSystemDirectoryHandle): Promise<void> {
|
||||
return this.fileReceiver.setSaveDirectory(directoryHandle);
|
||||
}
|
||||
|
||||
public getReceiverSaveType(): { [fileId: string]: boolean } | undefined {
|
||||
return this.fileReceiver.saveType;
|
||||
}
|
||||
|
||||
public manualSafeSave(): void {
|
||||
this.fileReceiver.gracefulShutdown();
|
||||
}
|
||||
|
||||
public async cleanup(): Promise<void> {
|
||||
console.log("[WebRTC Service] 开始清理...");
|
||||
try {
|
||||
await Promise.all([
|
||||
this.sender.cleanUpBeforeExit(),
|
||||
this.receiver.cleanUpBeforeExit()
|
||||
]);
|
||||
console.log("[WebRTC Service] 清理完成");
|
||||
} catch (error) {
|
||||
console.error("[WebRTC Service] 清理过程中出错:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const webrtcService = WebRTCService.getInstance();
|
||||
Reference in New Issue
Block a user