更新,未测试
This commit is contained in:
@@ -8,10 +8,6 @@ import React, {
|
||||
} from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import WebRTC_Initiator from "../lib/webrtc_Initiator";
|
||||
import WebRTC_Recipient from "../lib/webrtc_Recipient";
|
||||
import FileReceiver from "../lib/fileReceiver";
|
||||
import FileSender from "../lib/fileSender";
|
||||
import { debounce } from "lodash";
|
||||
import FileListDisplay from "./self_define/FileListDisplay";
|
||||
import { FileMeta, CustomFile, fileMetadata } from "@/lib/types/file";
|
||||
@@ -30,18 +26,16 @@ import JSZip from "jszip";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Tooltip } from "./Tooltip";
|
||||
import RichTextEditor from "@/components/Editor/RichTextEditor";
|
||||
import { config } from "@/app/config/environment";
|
||||
import { fetchRoom, createRoom, checkRoom } from "@/app/config/api";
|
||||
import { trackReferrer } from "@/components/utils/tracking";
|
||||
import { postLogInDebug } from "@/app/config/api";
|
||||
import AnimatedButton from "./self_define/AnimatedButton";
|
||||
import { format_peopleMsg } from "@/utils/formatMessage";
|
||||
import { getDictionary } from "@/lib/dictionary";
|
||||
import { useLocale } from "@/hooks/useLocale";
|
||||
import type { Messages } from "@/types/messages";
|
||||
import { useClipboardAppMessages } from "@/hooks/useClipboardAppMessages";
|
||||
import { usePageSetup } from "@/hooks/usePageSetup";
|
||||
import { useRoomManager } from "@/hooks/useRoomManager";
|
||||
import {
|
||||
useWebRTCConnection,
|
||||
ProgressState,
|
||||
} from "@/hooks/useWebRTCConnection";
|
||||
|
||||
const developmentEnv = process.env.NEXT_PUBLIC_development!; //开发环境
|
||||
// 处理 beforeunload 事件的函数
|
||||
@@ -54,72 +48,81 @@ const handleBeforeUnload = (event: any) => {
|
||||
function allowUnload() {
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
}
|
||||
|
||||
const AdvancedClipboardApp = () => {
|
||||
const { shareMessage, retrieveMessage, putMessageInMs } =
|
||||
useClipboardAppMessages();
|
||||
|
||||
const [retrieveRoomId, setRetrieveRoomId] = useState(""); //接收端--房间ID
|
||||
//发送端:编辑器文本、文件
|
||||
const [shareContent, setShareContent] = useState("");
|
||||
const [sendFiles, setSendFiles] = useState<CustomFile[]>([]); //FILE对象只会先引用文件,并不会将文件内容读取进内存。只有当分片读取时,才加载一小片到内存。理论上支持大文件。
|
||||
|
||||
const [sendProgress, setSendProgress] = useState<{
|
||||
//文件的进度--发送端--{fileId:0-1}--支持区分多个接收端
|
||||
[fileId: string]: {
|
||||
[peerId: string]: { progress: number; speed: number };
|
||||
};
|
||||
}>({});
|
||||
|
||||
const [receiveProgress, setReceiveProgress] = useState<{
|
||||
//文件的进度--接收端--{fileId:0-1}--目前只有一个发送端(为了和发送进度保持一致)
|
||||
[fileId: string]: {
|
||||
[peerId: string]: { progress: number; speed: number };
|
||||
};
|
||||
}>({});
|
||||
|
||||
// 取回端:编辑器文本、文件
|
||||
const [retrievedContent, setRetrievedContent] = useState("");
|
||||
const [retrievedFiles, setRetrievedFiles] = useState<CustomFile[]>([]);
|
||||
const [retrievedFileMetas, setRetrievedFileMetas] = useState<FileMeta[]>([]); //接收到的meta信息
|
||||
|
||||
//初始化 p2p通信/文件传输 对象
|
||||
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);
|
||||
|
||||
const retrieveJoinRoomBtnRef = useRef<HTMLButtonElement>(null); //接收方--加入房间按钮ref
|
||||
const [activeTab, setActiveTab] = useState<"send" | "retrieve">("send");
|
||||
const richTextToPlainText = useRichTextToPlainText();
|
||||
// 1. 添加一个状态来追踪连接数量
|
||||
const [sharePeerCount, setSharePeerCount] = useState(0);
|
||||
const [retrievePeerCount, setRetrievePeerCount] = useState(0);
|
||||
const locale = useLocale();
|
||||
const retrieveJoinRoomBtnRef = useRef<HTMLButtonElement>(null); //接收方--加入房间按钮ref
|
||||
const { messages, isLoadingMessages } = usePageSetup({
|
||||
setRetrieveRoomId,
|
||||
setActiveTab,
|
||||
retrieveJoinRoomBtnRef,
|
||||
});
|
||||
// This function will be properly defined in useWebRTCConnection or useFileTransferHandler
|
||||
const dummyBroadcastDataToPeers = useCallback(async () => {
|
||||
if (!senderFileTransfer || !sender) return;
|
||||
console.log("Attempting to broadcast data (dummy)...");
|
||||
const peerIds = Array.from(sender.peerConnections.keys());
|
||||
await Promise.all(
|
||||
peerIds.map(async (peerId) => {
|
||||
if (shareContent)
|
||||
await senderFileTransfer.sendString(shareContent, peerId);
|
||||
if (sendFiles.length)
|
||||
senderFileTransfer.sendFileMeta(sendFiles, peerId);
|
||||
})
|
||||
);
|
||||
}, [sender, senderFileTransfer, shareContent, sendFiles]);
|
||||
//发送端:编辑器文本、文件
|
||||
const [shareContent, setShareContent] = useState("");
|
||||
const [sendFiles, setSendFiles] = useState<CustomFile[]>([]); //FILE对象只会先引用文件,并不会将文件内容读取进内存。只有当分片读取时,才加载一小片到内存。理论上支持大文件。
|
||||
// 取回端:编辑器文本、文件
|
||||
const [retrievedContent, setRetrievedContent] = useState("");
|
||||
const [retrievedFiles, setRetrievedFiles] = useState<CustomFile[]>([]);
|
||||
const [retrievedFileMetas, setRetrievedFileMetas] = useState<FileMeta[]>([]); //接收到的meta信息
|
||||
|
||||
const richTextToPlainText = useRichTextToPlainText();
|
||||
|
||||
// Initialize WebRTC Connection Hook
|
||||
const {
|
||||
sender,
|
||||
receiver,
|
||||
sharePeerCount,
|
||||
retrievePeerCount,
|
||||
sendProgress,
|
||||
receiveProgress,
|
||||
broadcastDataToAllPeers,
|
||||
requestFile,
|
||||
requestFolder,
|
||||
setReceiverDirectoryHandle,
|
||||
getReceiverSaveType,
|
||||
} = useWebRTCConnection({
|
||||
messages, // Pass messages for logging or potential internal messages from the hook
|
||||
putMessageInMs, // Pass for user feedback on connection events
|
||||
onStringReceived: useCallback((data, peerId) => {
|
||||
if (developmentEnv)
|
||||
console.log(
|
||||
`App received string from ${peerId}: ${data.substring(0, 30)}`
|
||||
);
|
||||
setRetrievedContent(data);
|
||||
}, []), // Empty dependency array if setRetrievedContent is stable
|
||||
onFileMetaReceived: useCallback((meta, peerId) => {
|
||||
if (developmentEnv)
|
||||
console.log(`App received file meta from ${peerId}: ${meta.name}`);
|
||||
const { type, ...metaWithoutType } = meta; // Assuming type is part of fileMetadata but not FileMeta
|
||||
setRetrievedFileMetas((prev) => {
|
||||
const DPrev = prev.filter(
|
||||
(existingFile) => existingFile.fileId !== metaWithoutType.fileId
|
||||
); // Prevent duplicates by fileId
|
||||
return [...DPrev, metaWithoutType];
|
||||
});
|
||||
}, []),
|
||||
onFileReceived: useCallback((file, peerId) => {
|
||||
if (developmentEnv)
|
||||
console.log(`App received file from ${peerId}: ${file.name}`);
|
||||
setRetrievedFiles((prev) => {
|
||||
const isDuplicate = prev.some(
|
||||
(existingFile) =>
|
||||
existingFile.fullName === file.fullName &&
|
||||
existingFile.size === file.size
|
||||
); // More robust duplicate check
|
||||
if (isDuplicate) return prev;
|
||||
return [...prev, file];
|
||||
});
|
||||
}, []),
|
||||
});
|
||||
// Initialize Room Manager Hook
|
||||
const {
|
||||
shareRoomId,
|
||||
// initShareRoomId, // Not directly used in JSX, managed internally by hook
|
||||
setShareRoomId, // Keep this if shareRoomId can be set from input/clipboard
|
||||
setShareRoomId, // If needed for pasting room ID directly into sender's input
|
||||
shareLink,
|
||||
shareRoomStatusText,
|
||||
retrieveRoomStatusText,
|
||||
@@ -129,12 +132,14 @@ const AdvancedClipboardApp = () => {
|
||||
} = useRoomManager({
|
||||
messages,
|
||||
putMessageInMs,
|
||||
sender, // Placeholder
|
||||
receiver, // Placeholder
|
||||
sender,
|
||||
receiver,
|
||||
activeTab,
|
||||
sharePeerCount, // Placeholder
|
||||
retrievePeerCount, // Placeholder
|
||||
broadcastDataToPeers: dummyBroadcastDataToPeers, // Placeholder for actual broadcast function
|
||||
sharePeerCount,
|
||||
retrievePeerCount,
|
||||
// Pass the actual broadcast function from useWebRTCConnection
|
||||
broadcastDataToPeers: () =>
|
||||
broadcastDataToAllPeers(shareContent, sendFiles),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -153,184 +158,71 @@ const AdvancedClipboardApp = () => {
|
||||
};
|
||||
}, [sendFiles, shareContent, retrievedFiles, retrievedContent]);
|
||||
|
||||
//useCallback 钩子来定义 函数。这确保了函数只在其依赖项(content, files, senderFileTransfer)发生变化时才会重新创建
|
||||
const sendStringAndMetas = useCallback(
|
||||
async (peerId: string) => {
|
||||
if (!senderFileTransfer) {
|
||||
console.error(
|
||||
"senderFileTransfer is not initialized, delaying send operation..."
|
||||
);
|
||||
// 重试逻辑改为异步
|
||||
setTimeout(async () => {
|
||||
if (senderFileTransfer) {
|
||||
console.log("Retrying send operation...");
|
||||
if (shareContent)
|
||||
await (senderFileTransfer as FileSender).sendString(
|
||||
shareContent,
|
||||
peerId
|
||||
);
|
||||
if (sendFiles.length)
|
||||
(senderFileTransfer as FileSender).sendFileMeta(
|
||||
sendFiles,
|
||||
peerId
|
||||
);
|
||||
}
|
||||
}, 1000);
|
||||
return;
|
||||
}
|
||||
if (shareContent) {
|
||||
if (developmentEnv === "true")
|
||||
postLogInDebug(`Sending string content:${shareContent}`);
|
||||
// console.log('Sending string content:', shareContent);
|
||||
await senderFileTransfer.sendString(shareContent, peerId);
|
||||
}
|
||||
if (sendFiles.length) {
|
||||
// console.log('Sending file metadata:', sendFiles);
|
||||
senderFileTransfer.sendFileMeta(sendFiles, peerId);
|
||||
}
|
||||
},
|
||||
[shareContent, sendFiles, senderFileTransfer]
|
||||
);
|
||||
|
||||
// 使用useEffect钩子来 在组件加载时 初始化,并在组件卸载时清理连接
|
||||
useEffect(() => {
|
||||
const senderConnection = new WebRTC_Initiator(config.API_URL);
|
||||
const receiverConnection = new WebRTC_Recipient(config.API_URL);
|
||||
|
||||
setSender(senderConnection);
|
||||
setReceiver(receiverConnection);
|
||||
|
||||
const senderFT = new FileSender(senderConnection);
|
||||
const receiverFT = new FileReceiver(receiverConnection);
|
||||
setSenderFileTransfer(senderFT);
|
||||
setReceiverFileTransfer(receiverFT);
|
||||
|
||||
return () => {
|
||||
senderConnection.cleanUpBeforeExit();
|
||||
receiverConnection.cleanUpBeforeExit();
|
||||
};
|
||||
}, []);
|
||||
//定义一些文件接收处理函数
|
||||
useEffect(() => {
|
||||
if (sender && senderFileTransfer) {
|
||||
sender.onConnectionStateChange = (
|
||||
state: RTCPeerConnectionState,
|
||||
peerId: string
|
||||
) => {
|
||||
console.log(`connection status: ${state} with peerId: ${peerId}`);
|
||||
setSharePeerCount(sender.peerConnections.size);
|
||||
if (state === "connected") {
|
||||
//当建立连接后,设置进度回调函数
|
||||
senderFileTransfer?.setProgressCallback(
|
||||
(fileId: string, progress: number, speed: number) => {
|
||||
setSendProgress((prev) => ({
|
||||
...prev,
|
||||
[fileId]: {
|
||||
...prev[fileId],
|
||||
[peerId]: { progress, speed },
|
||||
},
|
||||
}));
|
||||
},
|
||||
peerId
|
||||
);
|
||||
}
|
||||
};
|
||||
sender.onDataChannelOpen = sendStringAndMetas;
|
||||
}
|
||||
|
||||
if (receiver && receiverFileTransfer) {
|
||||
receiver.onConnectionStateChange = (state: string, peerId: string) => {
|
||||
console.log(`connection status: ${state} with peerId: ${peerId}`);
|
||||
setRetrievePeerCount(receiver.peerConnections.size);
|
||||
if (state === "connected") {
|
||||
receiverFileTransfer?.setProgressCallback(
|
||||
(fileId: string, progress: number, speed: number) => {
|
||||
setReceiveProgress((prev) => ({
|
||||
...prev,
|
||||
[fileId]: {
|
||||
...prev[fileId],
|
||||
[peerId]: { progress, speed },
|
||||
},
|
||||
}));
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// receiver.onDataChannelOpen = () => {
|
||||
// putMessageInMs(messages!.text.ClipboardApp.channelOpen_msg,false);
|
||||
// };
|
||||
}
|
||||
|
||||
if (receiverFileTransfer) {
|
||||
receiverFileTransfer.onStringReceived = (value: string) => {
|
||||
setRetrievedContent(value);
|
||||
};
|
||||
|
||||
receiverFileTransfer.onFileMetaReceived = (fileMeta: fileMetadata) => {
|
||||
const { type, ...metaWithoutType } = fileMeta; // 剔除 type 属性
|
||||
setRetrievedFileMetas((prev) => [...prev, metaWithoutType]);
|
||||
};
|
||||
|
||||
receiverFileTransfer.onFileReceived = async (file: CustomFile) => {
|
||||
setRetrievedFiles((prev) => {
|
||||
// 检查 fullName 是否已经存在
|
||||
const isDuplicate = prev.some(
|
||||
(existingFile) => existingFile.fullName === file.fullName
|
||||
);
|
||||
if (isDuplicate) {
|
||||
return prev; // 如果存在,返回原数组
|
||||
}
|
||||
return [...prev, file]; // 否则添加到数组中
|
||||
});
|
||||
};
|
||||
}
|
||||
}, [
|
||||
sender,
|
||||
receiver,
|
||||
senderFileTransfer,
|
||||
receiverFileTransfer,
|
||||
sendStringAndMetas,
|
||||
messages,
|
||||
]);
|
||||
//只有接收端支持下载
|
||||
const handleDownload = useCallback(
|
||||
async (meta: FileMeta) => {
|
||||
if (meta.folderName !== "") {
|
||||
const downloadFiles = retrievedFiles.filter(
|
||||
if (meta.folderName && meta.folderName !== "") {
|
||||
// Check for non-empty folderName
|
||||
const filesToZip = retrievedFiles.filter(
|
||||
(file) => file.folderName === meta.folderName
|
||||
);
|
||||
if (filesToZip.length === 0) {
|
||||
putMessageInMs(
|
||||
`No files found for folder ${meta.folderName} to download.`,
|
||||
false
|
||||
);
|
||||
return;
|
||||
}
|
||||
const zip = new JSZip();
|
||||
for (let file of downloadFiles) zip.file(file.fullName, file); // Add files to the zip
|
||||
for (let file of filesToZip) {
|
||||
zip.file(file.fullName, file);
|
||||
}
|
||||
try {
|
||||
// Generate the zip file
|
||||
const content = await zip.generateAsync({ type: "blob" });
|
||||
DownloadAs(content, `downloaded_folder_${meta.folderName}.zip`);
|
||||
DownloadAs(content, `${meta.folderName}.zip`);
|
||||
} catch (error) {
|
||||
console.error("Error creating zip file:", error);
|
||||
// alert('An error occurred while creating the zip file.');
|
||||
putMessageInMs(
|
||||
// messages?.text.ClipboardApp.zipError ||
|
||||
"Error creating ZIP.",
|
||||
false
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const downloadFiles = retrievedFiles.filter(
|
||||
const filesToDownload = retrievedFiles.filter(
|
||||
(file) => file.name === meta.name
|
||||
);
|
||||
for (let file of downloadFiles) DownloadAs(file, file.name);
|
||||
if (filesToDownload) {
|
||||
for (let file of filesToDownload) DownloadAs(file, file.name);
|
||||
} else {
|
||||
putMessageInMs(`File ${meta.name} not found for download.`, false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[retrievedFiles]
|
||||
[retrievedFiles, messages]
|
||||
);
|
||||
|
||||
const onFilePicked = useCallback((files: CustomFile[]) => {
|
||||
setSendFiles((prevFiles) => [...prevFiles, ...files]);
|
||||
}, []);
|
||||
//点击删除按钮之后,将对应文件删掉
|
||||
const removeSenderFile = useCallback((meta: FileMeta) => {
|
||||
// Uses sendFiles
|
||||
const onFilePicked = useCallback((pickedFiles: CustomFile[]) => {
|
||||
setSendFiles((prevFiles) => {
|
||||
if (meta.folderName !== "") {
|
||||
return prevFiles.filter((file) => file.folderName !== meta.folderName);
|
||||
// Basic duplicate check by name and size for picked files
|
||||
const newFiles = pickedFiles.filter(
|
||||
(pf) =>
|
||||
!prevFiles.some((ef) => ef.name === pf.name && ef.size === pf.size)
|
||||
);
|
||||
return [...prevFiles, ...newFiles];
|
||||
});
|
||||
}, []);
|
||||
|
||||
//点击删除按钮之后,将对应文件删掉
|
||||
const removeSenderFile = useCallback((metaToRemove: FileMeta) => {
|
||||
setSendFiles((prevFiles) => {
|
||||
if (metaToRemove.folderName && metaToRemove.folderName !== "") {
|
||||
return prevFiles.filter(
|
||||
(file) => file.folderName !== metaToRemove.folderName
|
||||
);
|
||||
} else {
|
||||
return prevFiles.filter((file) => file.name !== meta.name);
|
||||
return prevFiles.filter((file) => file.name !== metaToRemove.name);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
@@ -339,48 +231,57 @@ const AdvancedClipboardApp = () => {
|
||||
const onLocationPick = useCallback(async (): Promise<boolean> => {
|
||||
if (!messages) return false; // Added messages dependency
|
||||
if (!window.showDirectoryPicker) {
|
||||
console.error("showDirectoryPicker is not supported in this browser.");
|
||||
putMessageInMs(
|
||||
//messages.text.ClipboardApp.pickSaveUnsupported ||
|
||||
"Directory picker not supported.",
|
||||
false
|
||||
);
|
||||
console.error("showDirectoryPicker is not supported.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 确认操作
|
||||
const userConfirmed = window.confirm(
|
||||
messages.text.ClipboardApp.pickSaveMsg
|
||||
);
|
||||
if (!userConfirmed) return false;
|
||||
|
||||
if (!window.confirm(messages.text.ClipboardApp.pickSaveMsg)) return false;
|
||||
try {
|
||||
// 选择保存目录
|
||||
const directory = await window.showDirectoryPicker();
|
||||
|
||||
if (receiverFileTransfer && directory) {
|
||||
console.log("onLocationPick", directory);
|
||||
await receiverFileTransfer.setSaveDirectory(directory);
|
||||
return true;
|
||||
const directoryHandle = await window.showDirectoryPicker();
|
||||
await setReceiverDirectoryHandle(directoryHandle); // From useWebRTCConnection
|
||||
putMessageInMs(
|
||||
// messages.text.ClipboardApp.pickSaveSuccess ||
|
||||
"Save location set.",
|
||||
false
|
||||
);
|
||||
return true;
|
||||
} catch (err: any) {
|
||||
if (err.name !== "AbortError") {
|
||||
// Don't show error if user cancelled
|
||||
console.error("Failed to set up folder receive:", err);
|
||||
putMessageInMs(
|
||||
// messages.text.ClipboardApp.pickSaveError ||
|
||||
"Could not set save location.",
|
||||
false
|
||||
);
|
||||
}
|
||||
return false;
|
||||
} catch (err) {
|
||||
console.error("Failed to set up folder receive:", err);
|
||||
return false;
|
||||
}
|
||||
}, [receiverFileTransfer, messages]); // Added messages
|
||||
}, [messages, putMessageInMs, setReceiverDirectoryHandle]);
|
||||
|
||||
const handleRequest = useCallback(
|
||||
async (meta: FileMeta) => {
|
||||
if (!receiverFileTransfer) return;
|
||||
const handleFileRequest = useCallback(
|
||||
(meta: FileMeta) => {
|
||||
if (meta.folderName) {
|
||||
receiverFileTransfer.requestFolder(meta.folderName);
|
||||
requestFolder(meta.folderName);
|
||||
} else if (meta.fileId) {
|
||||
// Ensure fileId exists for individual file requests
|
||||
requestFile(meta.fileId);
|
||||
} else {
|
||||
receiverFileTransfer.requestFile(meta.fileId);
|
||||
console.warn("Cannot request file: missing fileId", meta);
|
||||
}
|
||||
},
|
||||
[receiverFileTransfer]
|
||||
[requestFile, requestFolder]
|
||||
);
|
||||
|
||||
if (isLoadingMessages || !messages) {
|
||||
// Updated loading condition
|
||||
return <div>Loading...</div>;
|
||||
return <div className="p-4 text-center">Loading messages...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8 w-full md:max-w-4xl">
|
||||
<div className="flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-2 mb-4">
|
||||
@@ -388,6 +289,7 @@ const AdvancedClipboardApp = () => {
|
||||
variant={activeTab === "send" ? "default" : "outline"}
|
||||
onClick={() => setActiveTab("send")}
|
||||
className="flex-1"
|
||||
aria-selected={activeTab === "send"}
|
||||
>
|
||||
{messages.text.ClipboardApp.html.senderTab}
|
||||
</Button>
|
||||
@@ -395,6 +297,7 @@ const AdvancedClipboardApp = () => {
|
||||
variant={activeTab === "retrieve" ? "default" : "outline"}
|
||||
onClick={() => setActiveTab("retrieve")}
|
||||
className="flex-1"
|
||||
aria-selected={activeTab === "retrieve"}
|
||||
>
|
||||
{messages.text.ClipboardApp.html.retrieveTab}
|
||||
</Button>
|
||||
@@ -415,34 +318,40 @@ const AdvancedClipboardApp = () => {
|
||||
<span>{`${messages.text.ClipboardApp.html.RoomStatus_dis} ${shareRoomStatusText}`}</span>
|
||||
)}
|
||||
</div>
|
||||
<RichTextEditor
|
||||
value={shareContent}
|
||||
onChange={(value) => setShareContent(value)}
|
||||
/>
|
||||
<RichTextEditor value={shareContent} onChange={setShareContent} />
|
||||
<div className="flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-2 mb-4">
|
||||
<ReadClipboardButton
|
||||
title={messages.text.ClipboardApp.html.Paste_dis}
|
||||
onRead={(text: string) => setShareContent(text)}
|
||||
onRead={setShareContent}
|
||||
/>
|
||||
<WriteClipboardButton
|
||||
title={messages.text.ClipboardApp.html.Copy_dis}
|
||||
textToCopy={richTextToPlainText(shareContent)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-2">
|
||||
<FileUploadHandler onFilePicked={onFilePicked} />
|
||||
<div className="mb-3">
|
||||
<FileUploadHandler
|
||||
onFilePicked={onFilePicked}
|
||||
// messages={messages}
|
||||
/>
|
||||
<FileListDisplay
|
||||
mode="sender"
|
||||
files={sendFiles}
|
||||
fileProgresses={sendProgress}
|
||||
onDelete={removeSenderFile}
|
||||
// messages={messages}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col md:flex-row items-center space-y-2 md:space-y-0 md:space-x-2 mb-2">
|
||||
<span>{messages.text.ClipboardApp.html.inputRoomId_tips}</span>
|
||||
<Input
|
||||
value={shareRoomId} //展示值
|
||||
aria-label="Share Room ID"
|
||||
value={shareRoomId}
|
||||
onChange={(e) => checkAndSetShareRoomId(e.target.value)}
|
||||
onPaste={(e) => {
|
||||
const pastedText = e.clipboardData.getData("text");
|
||||
checkAndSetShareRoomId(pastedText); // also check pasted text
|
||||
}}
|
||||
className="w-full md:w-36 border-2 border-gray-300 rounded-md px-3 py-2 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 transition-all duration-200"
|
||||
/>
|
||||
<Button
|
||||
@@ -476,7 +385,7 @@ const AdvancedClipboardApp = () => {
|
||||
<div className="mb-4">
|
||||
<ReadClipboardButton
|
||||
title={messages.text.ClipboardApp.html.readClipboard_dis}
|
||||
onRead={(text: string) => setRetrieveRoomId(text)}
|
||||
onRead={setRetrieveRoomId}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
@@ -503,7 +412,7 @@ const AdvancedClipboardApp = () => {
|
||||
<>
|
||||
<RichTextEditor
|
||||
value={retrievedContent}
|
||||
onChange={(value) => setRetrievedContent(value)}
|
||||
onChange={setRetrievedContent}
|
||||
/>
|
||||
<div className="flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-2 mb-2">
|
||||
<WriteClipboardButton
|
||||
@@ -518,16 +427,17 @@ const AdvancedClipboardApp = () => {
|
||||
files={retrievedFileMetas}
|
||||
fileProgresses={receiveProgress}
|
||||
onDownload={handleDownload}
|
||||
onRequest={handleRequest}
|
||||
onRequest={handleFileRequest}
|
||||
onLocationPick={onLocationPick}
|
||||
saveType={receiverFileTransfer?.saveType}
|
||||
saveType={getReceiverSaveType()}
|
||||
// messages={messages}
|
||||
/>
|
||||
{retrieveMessage && <p className="mb-4">{retrieveMessage}</p>}
|
||||
</>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
{activeTab === "send" && shareLink !== "" && messages && (
|
||||
{activeTab === "send" && shareLink && messages && (
|
||||
<Card className="border-2 shadow-md mt-4">
|
||||
<CardHeader>
|
||||
<CardTitle>
|
||||
|
||||
@@ -24,7 +24,7 @@ interface UseRoomManagerProps {
|
||||
// `sendStringAndMetas` itself will be part of useWebRTCConnection or useFileTransfer.
|
||||
// For now, we might need to pass a simplified broadcast function or rethink.
|
||||
// Let's assume for now `broadcastData` is a function passed from the hook that will own `sendStringAndMetas`.
|
||||
broadcastDataToPeers: () => Promise<void>;
|
||||
broadcastDataToPeers: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export function useRoomManager({
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
import { useState, useEffect, useCallback } 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 } from "@/app/config/environment"; // For API_URL
|
||||
import { postLogInDebug } from "@/app/config/api"; // For debug logging
|
||||
import type { CustomFile, fileMetadata, FileMeta } from "@/lib/types/file"; // Assuming FileMeta might be used by caller
|
||||
import type { Messages } from "@/types/messages";
|
||||
|
||||
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 {
|
||||
// Callbacks for data received from peers
|
||||
onStringReceived: (data: string, peerId: string) => void;
|
||||
onFileMetaReceived: (meta: fileMetadata, peerId: string) => void;
|
||||
onFileReceived: (file: CustomFile, peerId: string) => void;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
export function useWebRTCConnection({
|
||||
onStringReceived,
|
||||
onFileMetaReceived,
|
||||
onFileReceived,
|
||||
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);
|
||||
|
||||
const [sharePeerCount, setSharePeerCount] = useState(0);
|
||||
const [retrievePeerCount, setRetrievePeerCount] = useState(0);
|
||||
|
||||
const [sendProgress, setSendProgress] = useState<ProgressState>({});
|
||||
const [receiveProgress, setReceiveProgress] = useState<ProgressState>({});
|
||||
|
||||
// Initialize WebRTC objects and their cleanup
|
||||
useEffect(() => {
|
||||
const senderConn = new WebRTC_Initiator(config.API_URL);
|
||||
const receiverConn = new WebRTC_Recipient(config.API_URL);
|
||||
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");
|
||||
|
||||
return () => {
|
||||
if (developmentEnv) console.log("Cleaning up WebRTC instances");
|
||||
senderConn.cleanUpBeforeExit();
|
||||
receiverConn.cleanUpBeforeExit();
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 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"
|
||||
);
|
||||
// TODO: Use putMessageInMs for critical errors visible to user?
|
||||
return;
|
||||
}
|
||||
if (textContent) {
|
||||
if (developmentEnv)
|
||||
postLogInDebug(
|
||||
`Sending string content to ${peerId}: ${textContent.substring(
|
||||
0,
|
||||
100
|
||||
)}...`
|
||||
);
|
||||
await senderFileTransfer.sendString(textContent, peerId);
|
||||
}
|
||||
if (filesToSend.length > 0) {
|
||||
if (developmentEnv)
|
||||
postLogInDebug(
|
||||
`Sending file metadata to ${peerId} for ${filesToSend.length} files.`
|
||||
);
|
||||
senderFileTransfer.sendFileMeta(filesToSend, peerId);
|
||||
}
|
||||
},
|
||||
[senderFileTransfer]
|
||||
);
|
||||
|
||||
// Setup sender and receiver event handlers
|
||||
useEffect(() => {
|
||||
if (sender && senderFileTransfer) {
|
||||
sender.onConnectionStateChange = (state, peerId) => {
|
||||
if (developmentEnv)
|
||||
console.log(`Sender connection state with ${peerId}: ${state}`);
|
||||
setSharePeerCount(sender.peerConnections.size);
|
||||
if (state === "connected") {
|
||||
senderFileTransfer.setProgressCallback((fileId, progress, speed) => {
|
||||
setSendProgress((prev) => ({
|
||||
...prev,
|
||||
[fileId]: { ...prev[fileId], [peerId]: { progress, speed } },
|
||||
}));
|
||||
}, peerId);
|
||||
// putMessageInMs(`Connected to a new peer (sender side). Total: ${sender.peerConnections.size}`, true);
|
||||
}
|
||||
// Add more detailed user messages based on state if needed via putMessageInMs
|
||||
};
|
||||
// The original `sender.onDataChannelOpen = sendStringAndMetas` is removed.
|
||||
// Sending is now explicitly triggered by `broadcastDataToAllPeers`.
|
||||
// `FileSender` should internally handle queueing if data channel is not open yet.
|
||||
}
|
||||
|
||||
if (receiver && receiverFileTransfer) {
|
||||
receiver.onConnectionStateChange = (state, peerId) => {
|
||||
if (developmentEnv)
|
||||
console.log(`Receiver connection state with ${peerId}: ${state}`);
|
||||
setRetrievePeerCount(receiver.peerConnections.size);
|
||||
if (state === "connected") {
|
||||
receiverFileTransfer.setProgressCallback(
|
||||
(fileId, progress, speed) => {
|
||||
setReceiveProgress((prev) => ({
|
||||
...prev,
|
||||
[fileId]: { ...prev[fileId], [peerId]: { progress, speed } },
|
||||
}));
|
||||
}
|
||||
);
|
||||
// Example: putMessageInMs(`Connected to a new peer (receiver side). Total: ${receiver.peerConnections.size}`, false);
|
||||
}
|
||||
};
|
||||
|
||||
receiverFileTransfer.onStringReceived = (data) => {
|
||||
const peerId = "testId";
|
||||
if (developmentEnv) console.log(`String received from peer ${peerId}`);
|
||||
onStringReceived(data, peerId || "unknown_peer");
|
||||
};
|
||||
receiverFileTransfer.onFileMetaReceived = (meta) => {
|
||||
const peerId = "testId";
|
||||
if (developmentEnv)
|
||||
console.log(
|
||||
`File meta received from peer ${peerId} for: ${meta.name}`
|
||||
);
|
||||
onFileMetaReceived(meta, peerId || "unknown_peer");
|
||||
};
|
||||
receiverFileTransfer.onFileReceived = (file) => {
|
||||
const peerId = "testId";
|
||||
if (developmentEnv)
|
||||
console.log(`File received from peer ${peerId}: ${file.name}`);
|
||||
onFileReceived(file, peerId || "unknown_peer");
|
||||
};
|
||||
}
|
||||
}, [
|
||||
sender,
|
||||
senderFileTransfer,
|
||||
receiver,
|
||||
receiverFileTransfer,
|
||||
onStringReceived,
|
||||
onFileMetaReceived,
|
||||
onFileReceived,
|
||||
// messages, putMessageInMs // Removed messages/putMessageInMs if only for console logs for now
|
||||
]);
|
||||
|
||||
// Exposed function to broadcast data to all connected sender peers
|
||||
const broadcastDataToAllPeers = useCallback(
|
||||
async (textContent: string, filesToSend: CustomFile[]) => {
|
||||
if (!sender || sender.peerConnections.size === 0) {
|
||||
// The caller (useRoomManager) will handle user message like "waiting for peers"
|
||||
if (developmentEnv)
|
||||
console.warn(
|
||||
"No sender peers to broadcast to, or sender not initialized."
|
||||
);
|
||||
return false; // Indicate failure or no action
|
||||
}
|
||||
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, textContent, filesToSend)
|
||||
)
|
||||
);
|
||||
return true; // Indicate success
|
||||
} catch (error) {
|
||||
console.error("Error broadcasting data to peers:", error);
|
||||
// Optionally use putMessageInMs here for a generic broadcast error
|
||||
return false; // Indicate failure
|
||||
}
|
||||
},
|
||||
[sender, senderFileTransfer, sendStringAndMetasToPeer]
|
||||
);
|
||||
|
||||
const requestFile = useCallback(
|
||||
(fileId: string, peerId?: string) => {
|
||||
// Assuming FileReceiver methods can take optional peerId
|
||||
if (!receiverFileTransfer) return;
|
||||
if (developmentEnv)
|
||||
console.log(
|
||||
`Requesting file ${fileId} from peer ${peerId || "default"}`
|
||||
);
|
||||
receiverFileTransfer.requestFile(fileId, peerId);
|
||||
},
|
||||
[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, peerId);
|
||||
},
|
||||
[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]);
|
||||
|
||||
return {
|
||||
sender, // Exposed for useRoomManager (e.g., sender.isInRoom, sender.joinRoom)
|
||||
receiver, // Exposed for useRoomManager
|
||||
// Not exposing senderFileTransfer/receiverFileTransfer directly to encourage using specific methods
|
||||
sharePeerCount,
|
||||
retrievePeerCount,
|
||||
sendProgress,
|
||||
receiveProgress,
|
||||
broadcastDataToAllPeers,
|
||||
requestFile,
|
||||
requestFolder,
|
||||
setReceiverDirectoryHandle,
|
||||
getReceiverSaveType,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user