From ac5f1dc2605cd2e446dd8119af264b44a15cc773 Mon Sep 17 00:00:00 2001 From: david_bai Date: Tue, 27 May 2025 22:19:31 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AD=A5=E6=B5=8B=E8=AF=95ok?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/components/ClipboardApp.tsx | 213 +++++------------------ frontend/hooks/useFileTransferHandler.ts | 204 ++++++++++++++++++++++ frontend/hooks/useWebRTCConnection.ts | 6 +- 3 files changed, 254 insertions(+), 169 deletions(-) create mode 100644 frontend/hooks/useFileTransferHandler.ts diff --git a/frontend/components/ClipboardApp.tsx b/frontend/components/ClipboardApp.tsx index a2557ad..f20b950 100644 --- a/frontend/components/ClipboardApp.tsx +++ b/frontend/components/ClipboardApp.tsx @@ -1,53 +1,24 @@ "use client"; -import React, { - useState, - useEffect, - useRef, - useCallback, - useMemo, -} from "react"; +import React, { useState, useRef, useCallback } from "react"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { debounce } from "lodash"; import FileListDisplay from "./self_define/FileListDisplay"; -import { FileMeta, CustomFile, fileMetadata } from "@/lib/types/file"; +import { FileMeta } from "@/lib/types/file"; import { WriteClipboardButton, ReadClipboardButton, } from "./self_define/clipboard_btn"; import useRichTextToPlainText from "./self_define/rich-text-to-plain-text"; import QRCodeComponent from "./self_define/RetrieveMethod"; -import { - FileUploadHandler, - DownloadAs, -} from "./self_define/file-upload-handler"; - -import JSZip from "jszip"; +import { FileUploadHandler } from "./self_define/file-upload-handler"; import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; -import { Tooltip } from "./Tooltip"; import RichTextEditor from "@/components/Editor/RichTextEditor"; import AnimatedButton from "./self_define/AnimatedButton"; -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 事件的函数 -const handleBeforeUnload = (event: any) => { - event.preventDefault(); - event.returnValue = ""; // This is required for older browsers -}; - -// 当用户确实想要离开页面时(例如,在保存数据后),可以调用此函数移除事件监听器 -function allowUnload() { - window.removeEventListener("beforeunload", handleBeforeUnload); -} +import { useWebRTCConnection } from "@/hooks/useWebRTCConnection"; +import { useFileTransferHandler } from "@/hooks/useFileTransferHandler"; const AdvancedClipboardApp = () => { const { shareMessage, retrieveMessage, putMessageInMs } = @@ -56,21 +27,33 @@ const AdvancedClipboardApp = () => { const [retrieveRoomId, setRetrieveRoomId] = useState(""); //接收端--房间ID const [activeTab, setActiveTab] = useState<"send" | "retrieve">("send"); const retrieveJoinRoomBtnRef = useRef(null); //接收方--加入房间按钮ref + const { messages, isLoadingMessages } = usePageSetup({ setRetrieveRoomId, setActiveTab, retrieveJoinRoomBtnRef, }); - //发送端:编辑器文本、文件 - const [shareContent, setShareContent] = useState(""); - const [sendFiles, setSendFiles] = useState([]); //FILE对象只会先引用文件,并不会将文件内容读取进内存。只有当分片读取时,才加载一小片到内存。理论上支持大文件。 - // 取回端:编辑器文本、文件 - const [retrievedContent, setRetrievedContent] = useState(""); - const [retrievedFiles, setRetrievedFiles] = useState([]); - const [retrievedFileMetas, setRetrievedFileMetas] = useState([]); //接收到的meta信息 const richTextToPlainText = useRichTextToPlainText(); + // Initialize File Transfer Handler Hook + const { + shareContent, + sendFiles, + retrievedContent, + retrievedFiles, + retrievedFileMetas, + updateShareContent, + addFilesToSend, + removeFileToSend, + // clearSentItems, // Call these when appropriate e.g. after successful send or tab switch + // clearRetrievedItems, + onStringDataReceived, // Callback for WebRTC hook + onFileMetadataReceived, // Callback for WebRTC hook + onFileFullyReceived, // Callback for WebRTC hook + handleDownloadFile, // Direct handler for UI + } = useFileTransferHandler({ messages, putMessageInMs }); + // Initialize WebRTC Connection Hook const { sender, @@ -85,44 +68,17 @@ const AdvancedClipboardApp = () => { 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]; - }); - }, []), + messages, + putMessageInMs, + onStringReceived: onStringDataReceived, + onFileMetaReceived: onFileMetadataReceived, + onFileReceived: onFileFullyReceived, }); + // Initialize Room Manager Hook const { shareRoomId, - setShareRoomId, // If needed for pasting room ID directly into sender's input + // setShareRoomId, // Keep if shareRoomId can be set from input/clipboard shareLink, shareRoomStatusText, retrieveRoomStatusText, @@ -142,91 +98,6 @@ const AdvancedClipboardApp = () => { broadcastDataToAllPeers(shareContent, sendFiles), }); - useEffect(() => { - window.addEventListener("beforeunload", handleBeforeUnload); - if ( - sendFiles.length === 0 && - shareContent === "" && - retrievedFiles.length === 0 && - retrievedContent === "" - ) { - //如果页面不存在任何内容,则不阻止刷新或离开 - allowUnload(); - } - return () => { - window.removeEventListener("beforeunload", handleBeforeUnload); - }; - }, [sendFiles, shareContent, retrievedFiles, retrievedContent]); - - //只有接收端支持下载 - const handleDownload = useCallback( - async (meta: FileMeta) => { - 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 filesToZip) { - zip.file(file.fullName, file); - } - try { - // Generate the zip file - const content = await zip.generateAsync({ type: "blob" }); - DownloadAs(content, `${meta.folderName}.zip`); - } catch (error) { - console.error("Error creating zip file:", error); - putMessageInMs( - // messages?.text.ClipboardApp.zipError || - "Error creating ZIP.", - false - ); - } - } else { - const filesToDownload = retrievedFiles.filter( - (file) => file.name === meta.name - ); - if (filesToDownload) { - for (let file of filesToDownload) DownloadAs(file, file.name); - } else { - putMessageInMs(`File ${meta.name} not found for download.`, false); - } - } - }, - [retrievedFiles, messages] - ); - - const onFilePicked = useCallback((pickedFiles: CustomFile[]) => { - setSendFiles((prevFiles) => { - // 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 !== metaToRemove.name); - } - }); - }, []); - //选择保存目录 const onLocationPick = useCallback(async (): Promise => { if (!messages) return false; // Added messages dependency @@ -318,11 +189,14 @@ const AdvancedClipboardApp = () => { {`${messages.text.ClipboardApp.html.RoomStatus_dis} ${shareRoomStatusText}`} )} - +
{
@@ -369,6 +243,11 @@ const AdvancedClipboardApp = () => { loadingText={ messages.text.ClipboardApp.html.startSending_loadingText } + disabled={ + !sender || + !sender.isInRoom || + (sendFiles.length === 0 && shareContent.trim() === "") + } > {messages.text.ClipboardApp.html.startSending_dis} @@ -412,7 +291,9 @@ const AdvancedClipboardApp = () => { <> { + /* setRetrievedContent is internal */ + }} />
{ mode="receiver" files={retrievedFileMetas} fileProgresses={receiveProgress} - onDownload={handleDownload} + onDownload={handleDownloadFile} onRequest={handleFileRequest} onLocationPick={onLocationPick} saveType={getReceiverSaveType()} diff --git a/frontend/hooks/useFileTransferHandler.ts b/frontend/hooks/useFileTransferHandler.ts new file mode 100644 index 0000000..4004a86 --- /dev/null +++ b/frontend/hooks/useFileTransferHandler.ts @@ -0,0 +1,204 @@ +import { useState, useCallback, useEffect } from "react"; +import { CustomFile, FileMeta, fileMetadata } from "@/lib/types/file"; +import { Messages } from "@/types/messages"; +import JSZip from "jszip"; +import { DownloadAs } from "@/components/self_define/file-upload-handler"; // Assuming this path is correct + +// Helper functions for beforeunload (can be kept local to this hook if not used elsewhere) +const handleWindowBeforeUnload = (event: BeforeUnloadEvent) => { + event.preventDefault(); + event.returnValue = ""; // Required for Chrome +}; + +const allowWindowUnload = () => { + window.removeEventListener("beforeunload", handleWindowBeforeUnload); +}; + +interface UseFileTransferHandlerProps { + messages: Messages | null; + putMessageInMs: ( + message: string, + isShareEnd?: boolean, + displayTimeMs?: number + ) => void; +} + +export function useFileTransferHandler({ + messages, + putMessageInMs, +}: UseFileTransferHandlerProps) { + const [shareContent, setShareContent] = useState(""); + const [sendFiles, setSendFiles] = useState([]); + const [retrievedContent, setRetrievedContent] = useState(""); + const [retrievedFiles, setRetrievedFiles] = useState([]); + const [retrievedFileMetas, setRetrievedFileMetas] = useState([]); + + // Manage beforeunload event based on content + useEffect(() => { + if ( + sendFiles.length === 0 && + shareContent === "" && + retrievedFiles.length === 0 && + retrievedContent === "" + ) { + allowWindowUnload(); + } else { + window.addEventListener("beforeunload", handleWindowBeforeUnload); + } + return () => { + allowWindowUnload(); // Clean up listener when hook unmounts or dependencies change if any + }; + }, [sendFiles, shareContent, retrievedFiles, retrievedContent]); + + const updateShareContent = useCallback((content: string) => { + setShareContent(content); + }, []); + + const addFilesToSend = useCallback( + (pickedFiles: CustomFile[]) => { + setSendFiles((prevFiles) => { + const newFiles = pickedFiles.filter( + (pf) => + !prevFiles.some((ef) => ef.name === pf.name && ef.size === pf.size) + ); + if (newFiles.length < pickedFiles.length && messages) { + putMessageInMs( + // messages.text.ClipboardApp.fileExistMsg || + "Some files were already added.", + true + ); + } + return [...prevFiles, ...newFiles]; + }); + }, + [messages, putMessageInMs] + ); + + const removeFileToSend = 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 !== metaToRemove.name); + } + }); + }, []); + + const clearSentItems = useCallback(() => { + setShareContent(""); + setSendFiles([]); + }, []); + + const clearRetrievedItems = useCallback(() => { + setRetrievedContent(""); + setRetrievedFiles([]); + setRetrievedFileMetas([]); + }, []); + + // Callbacks for useWebRTCConnection + const onStringDataReceived = useCallback((data: string, peerId: string) => { + // console.log(`FileTransferHandler received string from ${peerId}`); + setRetrievedContent(data); + }, []); + + const onFileMetadataReceived = useCallback( + (meta: fileMetadata, peerId: string) => { + // console.log(`FileTransferHandler received file meta from ${peerId}: ${meta.name}`); + const { type, ...metaWithoutType } = meta; // Assuming 'type' is not part of FileMeta + setRetrievedFileMetas((prev) => { + const DPrev = prev.filter( + (existingFile) => existingFile.fileId !== metaWithoutType.fileId + ); + return [...DPrev, metaWithoutType]; + }); + }, + [] + ); + + const onFileFullyReceived = async (file: CustomFile, peerId: string) => { + // console.log(`FileTransferHandler received file from ${peerId}: ${file.name}`); + setRetrievedFiles((prev) => { + const isDuplicate = prev.some( + (existingFile) => + existingFile.fullName === file.fullName && + existingFile.size === file.size + ); + if (isDuplicate) return prev; + return [...prev, file]; + }); + }; + + const handleDownloadFile = useCallback( + async (meta: FileMeta) => { + if (!messages) return; + + if (meta.folderName && meta.folderName !== "") { + const filesToZip = retrievedFiles.filter( + (file) => file.folderName === meta.folderName + ); + if (filesToZip.length === 0) { + putMessageInMs( + // messages.text.ClipboardApp.noFilesForFolderMsg || + "No files found for folder '{folderName}'.".replace( + "{folderName}", + meta.folderName + ), + false + ); + return; + } + const zip = new JSZip(); + for (let file of filesToZip) { + zip.file(file.fullName, file); + } + try { + const content = await zip.generateAsync({ type: "blob" }); + DownloadAs(content, `${meta.folderName}.zip`); + } catch (error) { + console.error("Error creating zip file:", error); + putMessageInMs( + // messages.text.ClipboardApp.zipError || + "Error creating ZIP.", + false + ); + } + } else { + const fileToDownload = retrievedFiles.find((f) => f.name === meta.name); + if (fileToDownload) { + DownloadAs(fileToDownload, fileToDownload.name); + } else { + putMessageInMs( + // messages.text.ClipboardApp.fileNotFoundMsg || + "File '{fileName}' not found for download.".replace( + "{fileName}", + meta.name + ), + false + ); + } + } + }, + [retrievedFiles, messages, putMessageInMs] + ); + + return { + shareContent, + sendFiles, + retrievedContent, + retrievedFiles, + retrievedFileMetas, + updateShareContent, + addFilesToSend, + removeFileToSend, + clearSentItems, + clearRetrievedItems, + // Callbacks to provide to useWebRTCConnection + onStringDataReceived, + onFileMetadataReceived, + onFileFullyReceived, + // Download function + handleDownloadFile, + }; +} diff --git a/frontend/hooks/useWebRTCConnection.ts b/frontend/hooks/useWebRTCConnection.ts index 990f83a..cf83418 100644 --- a/frontend/hooks/useWebRTCConnection.ts +++ b/frontend/hooks/useWebRTCConnection.ts @@ -156,7 +156,7 @@ export function useWebRTCConnection({ ); onFileMetaReceived(meta, peerId || "unknown_peer"); }; - receiverFileTransfer.onFileReceived = (file) => { + receiverFileTransfer.onFileReceived = async (file) => { const peerId = "testId"; if (developmentEnv) console.log(`File received from peer ${peerId}: ${file.name}`); @@ -216,7 +216,7 @@ export function useWebRTCConnection({ console.log( `Requesting file ${fileId} from peer ${peerId || "default"}` ); - receiverFileTransfer.requestFile(fileId, peerId); + receiverFileTransfer.requestFile(fileId); }, [receiverFileTransfer] ); @@ -228,7 +228,7 @@ export function useWebRTCConnection({ console.log( `Requesting folder ${folderName} from peer ${peerId || "default"}` ); - receiverFileTransfer.requestFolder(folderName, peerId); + receiverFileTransfer.requestFolder(folderName); }, [receiverFileTransfer] );