Files
PrivyDrop/frontend/components/ClipboardApp.tsx
T
2025-08-17 10:10:40 +08:00

310 lines
9.9 KiB
TypeScript

"use client";
import React, { useRef, useCallback, useEffect, useMemo } from "react";
import { Button } from "@/components/ui/button";
import useRichTextToPlainText from "../hooks/useRichTextToPlainText";
import QRCodeComponent from "./ClipboardApp/ShareCard";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useClipboardAppMessages } from "@/hooks/useClipboardAppMessages";
import { usePageSetup } from "@/hooks/usePageSetup";
import { useRoomManager } from "@/hooks/useRoomManager";
import { useWebRTCConnection } from "@/hooks/useWebRTCConnection";
import { useFileTransferHandler } from "@/hooks/useFileTransferHandler";
import { SendTabPanel } from "./ClipboardApp/SendTabPanel";
import { RetrieveTabPanel } from "./ClipboardApp/RetrieveTabPanel";
import FullScreenDropZone from "./ClipboardApp/FullScreenDropZone";
import { traverseFileTree } from "@/lib/fileUtils";
import { useFileTransferStore } from "@/stores/fileTransferStore";
const ClipboardApp = () => {
const { shareMessage, retrieveMessage, putMessageInMs } =
useClipboardAppMessages();
const dragCounter = useRef(0);
const retrieveJoinRoomBtnRef = useRef<HTMLButtonElement>(null);
const { messages, isLoadingMessages } = usePageSetup({
setRetrieveRoomId: useFileTransferStore.getState().setRetrieveRoomIdInput,
setActiveTab: useFileTransferStore.getState().setActiveTab,
retrieveJoinRoomBtnRef,
});
// 从 store 中获取状态
const {
activeTab,
retrieveRoomIdInput,
isDragging,
shareContent,
sendFiles,
retrievedContent,
retrievedFileMetas,
sendProgress,
receiveProgress,
isAnyFileTransferring,
shareRoomId,
initShareRoomId,
shareLink,
shareRoomStatusText,
retrieveRoomStatusText,
setIsDragging,
setRetrieveRoomIdInput,
setActiveTab,
} = useFileTransferStore();
const richTextToPlainText = useRichTextToPlainText();
// Initialize File Transfer Handler Hook
const {
updateShareContent,
addFilesToSend,
removeFileToSend,
handleDownloadFile,
} = useFileTransferHandler({ messages, putMessageInMs });
// Calculate the derived states for unload protection
const isContentPresent = useMemo(() => {
return (
shareContent !== "" || retrievedContent !== "" || sendFiles.length > 0
);
}, [shareContent, retrievedContent, sendFiles]);
// Initialize WebRTC Connection Hook
const {
sender,
receiver,
sharePeerCount,
retrievePeerCount,
broadcastDataToAllPeers,
requestFile,
requestFolder,
setReceiverDirectoryHandle,
getReceiverSaveType,
senderDisconnected,
resetReceiverConnection,
resetSenderConnection,
manualSafeSave,
} = useWebRTCConnection({
shareContent,
sendFiles,
isContentPresent,
messages,
putMessageInMs,
});
const resetAppState = useCallback(async () => {
try {
// Reset file transfer state
useFileTransferStore.getState().resetReceiverState();
// Reset WebRTC connection state
await resetReceiverConnection();
// Reset room input
setRetrieveRoomIdInput("");
console.log("Application state reset successfully");
} catch (error) {
console.error("Error during state reset:", error);
window.location.reload();
}
}, [resetReceiverConnection, setRetrieveRoomIdInput]);
// Initialize Room Manager Hook
const {
processRoomIdInput,
joinRoom,
generateShareLinkAndBroadcast,
handleLeaveReceiverRoom,
handleLeaveSenderRoom,
} = useRoomManager({
messages,
putMessageInMs,
sender,
receiver,
broadcastDataToPeers: () =>
broadcastDataToAllPeers(shareContent, sendFiles),
resetSenderConnection,
});
const handleFileDrop = useCallback(
(items: DataTransferItemList) => {
if (activeTab !== "send") return;
const itemsArray = Array.from(items);
Promise.all(
itemsArray.map((item) => {
const entry = item.webkitGetAsEntry();
if (entry) {
return traverseFileTree(entry);
}
return Promise.resolve([]);
})
).then((results) => {
const allFiles = results.flat();
if (allFiles.length > 0) {
addFilesToSend(allFiles);
}
});
},
[activeTab, addFilesToSend]
);
useEffect(() => {
const handleDragEnter = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (activeTab !== "send") return;
dragCounter.current++;
if (e.dataTransfer?.items && e.dataTransfer.items.length > 0) {
setIsDragging(true);
}
};
const handleDragLeave = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (activeTab !== "send") return;
dragCounter.current--;
if (dragCounter.current === 0) {
setIsDragging(false);
}
};
const handleDragOver = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
};
const handleDrop = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (activeTab !== "send") return;
if (e.dataTransfer?.items) {
handleFileDrop(e.dataTransfer.items);
}
dragCounter.current = 0;
setIsDragging(false);
};
window.addEventListener("dragenter", handleDragEnter);
window.addEventListener("dragleave", handleDragLeave);
window.addEventListener("dragover", handleDragOver);
window.addEventListener("drop", handleDrop);
return () => {
window.removeEventListener("dragenter", handleDragEnter);
window.removeEventListener("dragleave", handleDragLeave);
window.removeEventListener("dragover", handleDragOver);
window.removeEventListener("drop", handleDrop);
};
}, [activeTab, handleFileDrop, setIsDragging]);
if (isLoadingMessages || !messages) {
return (
<div className="container mx-auto px-4 py-8 w-full md:max-w-4xl">
<div className="min-h-[1000px] w-full bg-gray-200/50 dark:bg-gray-800/50 rounded-lg animate-pulse">
{" "}
Loading Editor...{" "}
</div>
</div>
);
}
return (
<div className="container mx-auto px-4 py-8 w-full md:max-w-4xl">
<FullScreenDropZone isDragging={isDragging} messages={messages} />
<div className="flex flex-col md:flex-row space-y-2 md:space-y-0 md:space-x-2 mb-4">
<Button
variant={activeTab === "send" ? "default" : "outline"}
onClick={() => setActiveTab("send")}
className="flex-1"
aria-controls="send-panel"
id="send-tab"
aria-selected={activeTab === "send"}
>
{messages.text.ClipboardApp.html.senderTab}
</Button>
<Button
variant={activeTab === "retrieve" ? "default" : "outline"}
onClick={() => setActiveTab("retrieve")}
className="flex-1"
aria-controls="retrieve-panel"
id="retrieve-tab"
aria-selected={activeTab === "retrieve"}
>
{messages.text.ClipboardApp.html.retrieveTab}
</Button>
</div>
<Card className="border-8 shadow-md">
<CardHeader>
<CardTitle>
{activeTab === "send"
? messages.text.ClipboardApp.html.shareTitle_dis
: messages.text.ClipboardApp.html.retrieveTitle_dis}
</CardTitle>
</CardHeader>
<CardContent>
{activeTab === "send" ? (
<SendTabPanel
messages={messages}
shareRoomStatusText={shareRoomStatusText}
shareContent={shareContent}
sendFiles={sendFiles}
updateShareContent={updateShareContent}
addFilesToSend={addFilesToSend}
removeFileToSend={removeFileToSend}
richTextToPlainText={richTextToPlainText}
sendProgress={sendProgress}
isAnyFileTransferring={isAnyFileTransferring}
processRoomIdInput={processRoomIdInput}
joinRoom={joinRoom}
generateShareLinkAndBroadcast={generateShareLinkAndBroadcast}
sender={sender}
shareMessage={shareMessage}
currentValidatedShareRoomId={shareRoomId}
handleLeaveSenderRoom={handleLeaveSenderRoom}
/>
) : (
<RetrieveTabPanel
messages={messages}
putMessageInMs={putMessageInMs}
retrieveRoomStatusText={retrieveRoomStatusText}
retrieveRoomIdInput={retrieveRoomIdInput}
setRetrieveRoomIdInput={setRetrieveRoomIdInput}
joinRoom={joinRoom}
retrieveJoinRoomBtnRef={retrieveJoinRoomBtnRef}
receiver={receiver}
retrievedContent={retrievedContent}
richTextToPlainText={richTextToPlainText}
retrievedFileMetas={retrievedFileMetas}
receiveProgress={receiveProgress}
isAnyFileTransferring={isAnyFileTransferring}
handleDownloadFile={handleDownloadFile}
requestFile={requestFile}
requestFolder={requestFolder}
setReceiverDirectoryHandle={setReceiverDirectoryHandle}
getReceiverSaveType={getReceiverSaveType}
retrieveMessage={retrieveMessage}
senderDisconnected={senderDisconnected}
handleLeaveRoom={handleLeaveReceiverRoom}
manualSafeSave={manualSafeSave}
/>
)}
</CardContent>
</Card>
{activeTab === "send" && shareLink && messages && (
<Card className="border-2 shadow-md mt-4">
<CardHeader>
<CardTitle>
{messages.text.ClipboardApp.html.RetrieveMethodTitle}
</CardTitle>
</CardHeader>
<CardContent>
<QRCodeComponent RoomID={shareRoomId} shareLink={shareLink} />
</CardContent>
</Card>
)}
</div>
);
};
export default ClipboardApp;