refactor:Refactored the useFileTransferHandler hook

Using zustand for state management, created frontend/stores/fileTransferStore.ts
This commit is contained in:
david_bai
2025-08-16 23:47:48 +08:00
parent 8f79a1ad16
commit e15783aeff
12 changed files with 302 additions and 91 deletions
+8 -2
View File
@@ -255,8 +255,10 @@ export const de: Messages = {
"Speichern Sie große Dateien oder Ordner direkt in einem ausgewählten Verzeichnis. 👉",
chooseSavePath_dis: "Speicherort auswählen",
safeSave_dis: "Sicheres Speichern",
safeSave_tooltip: "Keine Angst vor Verbindungsunterbrechung, klicken Sie hier, um Dateien sicher zu speichern für die nächste Fortsetzung",
safeSaveSuccessMsg: "Dateien wurden sicher auf der Festplatte gespeichert, sicher die Seite zu schließen, unterstützt Wiederaufnahme der Übertragung!",
safeSave_tooltip:
"Keine Angst vor Verbindungsunterbrechung, klicken Sie hier, um Dateien sicher zu speichern für die nächste Fortsetzung",
safeSaveSuccessMsg:
"Dateien wurden sicher auf der Festplatte gespeichert, sicher die Seite zu schließen, unterstützt Wiederaufnahme der Übertragung!",
},
RetrieveMethod: {
P: "Glückwunsch 🎉 Freigegebene Inhalte warten darauf, abgerufen zu werden:",
@@ -305,6 +307,10 @@ export const de: Messages = {
leftRoomMsg: "Sie haben den Raum verlassen.",
leaveRoomBtn: "Raum Verlassen",
},
fileExistMsg: "Einige Dateien wurden bereits hinzugefügt.",
noFilesForFolderMsg: "Keine Dateien im Ordner '{folderName}' gefunden.",
zipError: "Fehler beim Erstellen der ZIP-Datei.",
fileNotFoundMsg: "Datei '{fileName}' zum Herunterladen nicht gefunden.",
html: {
senderTab: "Senden",
retrieveTab: "Abrufen",
+8 -2
View File
@@ -252,8 +252,10 @@ export const en: Messages = {
"Save large files or folders directly to a selected directory. 👉",
chooseSavePath_dis: "Choose save location",
safeSave_dis: "Safe Save",
safeSave_tooltip: "Don't worry about connection interruption, click here to safely save files for next resume",
safeSaveSuccessMsg: "Files have been safely saved to disk, safe to close page, supports resume transfer!",
safeSave_tooltip:
"Don't worry about connection interruption, click here to safely save files for next resume",
safeSaveSuccessMsg:
"Files have been safely saved to disk, safe to close page, supports resume transfer!",
},
RetrieveMethod: {
P: "Congrats 🎉 Share content is waiting to be retrieved:",
@@ -297,6 +299,10 @@ export const en: Messages = {
leftRoomMsg: "You have left the room.",
leaveRoomBtn: "Leave Room",
},
fileExistMsg: "Some files were already added.",
noFilesForFolderMsg: "No files found for folder '{folderName}'.",
zipError: "Error creating ZIP.",
fileNotFoundMsg: "File '{fileName}' not found for download.",
html: {
senderTab: "Send",
retrieveTab: "Retrieve",
+9 -2
View File
@@ -253,8 +253,10 @@ export const es: Messages = {
"Guarda archivos grandes o carpetas directamente en un directorio seleccionado. 👉",
chooseSavePath_dis: "Elegir ubicación de guardado",
safeSave_dis: "Guardar Seguro",
safeSave_tooltip: "No te preocupes por la interrupción de la conexión, haz clic aquí para guardar archivos de forma segura para la próxima reanudación",
safeSaveSuccessMsg: "Los archivos se han guardado de forma segura en el disco, es seguro cerrar la página, ¡admite transferencia de reanudación!",
safeSave_tooltip:
"No te preocupes por la interrupción de la conexión, haz clic aquí para guardar archivos de forma segura para la próxima reanudación",
safeSaveSuccessMsg:
"Los archivos se han guardado de forma segura en el disco, es seguro cerrar la página, ¡admite transferencia de reanudación!",
},
RetrieveMethod: {
P: "¡Felicitaciones 🎉 El contenido compartido está esperando ser recuperado:",
@@ -298,6 +300,11 @@ export const es: Messages = {
leftRoomMsg: "Has salido de la sala.",
leaveRoomBtn: "Salir de la Sala",
},
fileExistMsg: "Algunos archivos ya han sido añadidos.",
noFilesForFolderMsg:
"No se encontraron archivos en la carpeta '{folderName}'.",
zipError: "Error al crear el archivo ZIP.",
fileNotFoundMsg: "Archivo '{fileName}' no encontrado para descargar.",
html: {
senderTab: "Enviar",
retrieveTab: "Recuperar",
+10 -2
View File
@@ -256,8 +256,10 @@ export const fr: Messages = {
"Enregistrez des fichiers volumineux ou des dossiers directement dans un répertoire sélectionné. 👉",
chooseSavePath_dis: "Choisir l'emplacement de sauvegarde",
safeSave_dis: "Sauvegarde Sécurisée",
safeSave_tooltip: "N'ayez pas peur de l'interruption de connexion, cliquez ici pour sauvegarder les fichiers en toute sécurité pour la prochaine reprise",
safeSaveSuccessMsg: "Les fichiers ont été sauvegardés en toute sécurité sur le disque, sûr de fermer la page, prend en charge la reprise du transfert !",
safeSave_tooltip:
"N'ayez pas peur de l'interruption de connexion, cliquez ici pour sauvegarder les fichiers en toute sécurité pour la prochaine reprise",
safeSaveSuccessMsg:
"Les fichiers ont été sauvegardés en toute sécurité sur le disque, sûr de fermer la page, prend en charge la reprise du transfert !",
},
RetrieveMethod: {
P: "Félicitations 🎉 Le contenu partagé attend d'être récupéré :",
@@ -305,6 +307,12 @@ export const fr: Messages = {
leftRoomMsg: "Vous avez quitté la salle.",
leaveRoomBtn: "Quitter la Salle",
},
fileExistMsg: "Certains fichiers ont déjà été ajoutés.",
noFilesForFolderMsg:
"Aucun fichier trouvé dans le dossier '{folderName}'.",
zipError: "Erreur lors de la création du fichier ZIP.",
fileNotFoundMsg:
"Fichier '{fileName}' introuvable pour le téléchargement.",
html: {
senderTab: "Envoyer",
retrieveTab: "Récupérer",
+4
View File
@@ -293,6 +293,10 @@ export const ja: Messages = {
leftRoomMsg: "ルームを離れました。",
leaveRoomBtn: "ルームを離れる",
},
fileExistMsg: "一部のファイルは既に追加されています。",
noFilesForFolderMsg: "フォルダ '{folderName}' にファイルが見つかりません。",
zipError: "ZIP の作成中にエラーが発生しました。",
fileNotFoundMsg: "ダウンロードするファイル '{fileName}' が見つかりません。",
html: {
senderTab: "送信",
retrieveTab: "取得",
+4
View File
@@ -291,6 +291,10 @@ export const ko: Messages = {
leftRoomMsg: "방을 나갔습니다.",
leaveRoomBtn: "방 나가기",
},
fileExistMsg: "일부 파일이 이미 추가되었습니다.",
noFilesForFolderMsg: "폴더 '{folderName}'에서 파일을 찾을 수 없습니다.",
zipError: "ZIP 파일 생성 중 오류가 발생했습니다.",
fileNotFoundMsg: "다운로드할 파일 '{fileName}'을(를) 찾을 수 없습니다.",
html: {
senderTab: "보내기",
retrieveTab: "검색",
+4
View File
@@ -278,6 +278,10 @@ export const zh: Messages = {
leftRoomMsg: "您已离开房间。",
leaveRoomBtn: "离开房间",
},
fileExistMsg: "某些文件已添加。",
noFilesForFolderMsg: "在文件夹 '{folderName}' 中未找到文件。",
zipError: "创建 ZIP 文件时出错。",
fileNotFoundMsg: "未找到要下载的文件 '{fileName}'。",
html: {
senderTab: "发送",
retrieveTab: "接收",
+63 -81
View File
@@ -1,8 +1,9 @@
import { useState, useCallback } from "react";
import { useCallback } from "react";
import { CustomFile, FileMeta, fileMetadata } from "@/types/webrtc";
import { Messages } from "@/types/messages";
import JSZip from "jszip";
import { downloadAs } from "@/lib/fileUtils";
import { useFileTransferStore } from "@/stores/fileTransferStore";
interface UseFileTransferHandlerProps {
messages: Messages | null;
@@ -17,98 +18,76 @@ export function useFileTransferHandler({
messages,
putMessageInMs,
}: UseFileTransferHandlerProps) {
const [shareContent, setShareContent] = useState("");
const [sendFiles, setSendFiles] = useState<CustomFile[]>([]);
const [retrievedContent, setRetrievedContent] = useState("");
const [retrievedFiles, setRetrievedFiles] = useState<CustomFile[]>([]);
const [retrievedFileMetas, setRetrievedFileMetas] = useState<FileMeta[]>([]);
// 从 store 中获取状态
const {
shareContent,
sendFiles,
retrievedContent,
retrievedFiles,
retrievedFileMetas,
setShareContent,
setSendFiles,
addSendFiles,
removeSendFile,
setRetrievedContent,
setRetrievedFiles,
setRetrievedFileMetas,
} = useFileTransferStore();
const updateShareContent = useCallback((content: string) => {
setShareContent(content);
}, []);
}, [setShareContent]);
const addFilesToSend = useCallback(
(pickedFiles: CustomFile[]) => {
setSendFiles((prevFiles) => {
const newFiles = pickedFiles.filter(
(pf) =>
!prevFiles.some((ef) => ef.name === pf.name && ef.size === pf.size)
const newFiles = pickedFiles.filter(
(pf) =>
!sendFiles.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
);
if (newFiles.length < pickedFiles.length && messages) {
putMessageInMs(
// messages.text.ClipboardApp.fileExistMsg ||
"Some files were already added.",
true
);
}
return [...prevFiles, ...newFiles];
});
}
addSendFiles(newFiles);
},
[messages, putMessageInMs]
[sendFiles, messages, putMessageInMs, addSendFiles]
);
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([]);
}, []);
// Reset function specifically for receiver state (for leave room functionality)
const resetReceiverState = useCallback(() => {
setRetrievedContent("");
setRetrievedFiles([]);
setRetrievedFileMetas([]);
}, []);
removeSendFile(metaToRemove);
}, [removeSendFile]);
// Callbacks for useWebRTCConnection
const onStringDataReceived = useCallback((data: string, peerId: string) => {
// console.log(`FileTransferHandler received string from ${peerId}`);
setRetrievedContent(data);
}, []);
}, [setRetrievedContent]);
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 { type, ...metaWithoutType } = meta;
// Filter out existing file with same ID and add the new one
const DPrev = retrievedFileMetas.filter(
(existingFile: FileMeta) => existingFile.fileId !== metaWithoutType.fileId
);
setRetrievedFileMetas([...DPrev, metaWithoutType]);
},
[]
[retrievedFileMetas, setRetrievedFileMetas]
);
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 onFileFullyReceived = useCallback((file: CustomFile, peerId: string) => {
// Check if file already exists
const isDuplicate = retrievedFiles.some(
(existingFile: CustomFile) =>
existingFile.fullName === file.fullName &&
existingFile.size === file.size
);
if (!isDuplicate) {
setRetrievedFiles([...retrievedFiles, file]);
}
}, [retrievedFiles, setRetrievedFiles]);
const handleDownloadFile = useCallback(
async (meta: FileMeta) => {
@@ -120,7 +99,7 @@ export function useFileTransferHandler({
);
if (filesToZip.length === 0) {
putMessageInMs(
// messages.text.ClipboardApp.noFilesForFolderMsg ||
messages.text.ClipboardApp.noFilesForFolderMsg ||
"No files found for folder '{folderName}'.".replace(
"{folderName}",
meta.folderName
@@ -139,7 +118,7 @@ export function useFileTransferHandler({
} catch (error) {
console.error("Error creating zip file:", error);
putMessageInMs(
// messages.text.ClipboardApp.zipError ||
messages.text.ClipboardApp.zipError ||
"Error creating ZIP.",
false
);
@@ -150,7 +129,7 @@ export function useFileTransferHandler({
downloadAs(fileToDownload, fileToDownload.name);
} else {
putMessageInMs(
// messages.text.ClipboardApp.fileNotFoundMsg ||
messages.text.ClipboardApp.fileNotFoundMsg ||
"File '{fileName}' not found for download.".replace(
"{fileName}",
meta.name
@@ -163,6 +142,13 @@ export function useFileTransferHandler({
[retrievedFiles, messages, putMessageInMs]
);
// Reset function specifically for receiver state (for leave room functionality)
const resetReceiverState = useCallback(() => {
setRetrievedContent("");
setRetrievedFiles([]);
setRetrievedFileMetas([]);
}, [setRetrievedContent, setRetrievedFiles, setRetrievedFileMetas]);
return {
shareContent,
sendFiles,
@@ -172,14 +158,10 @@ export function useFileTransferHandler({
updateShareContent,
addFilesToSend,
removeFileToSend,
clearSentItems,
clearRetrievedItems,
resetReceiverState, // Export the new reset function
// Callbacks to provide to useWebRTCConnection
resetReceiverState, // Export the reset function
onStringDataReceived,
onFileMetadataReceived,
onFileFullyReceived,
// Download function
handleDownloadFile,
};
}
}
+2 -1
View File
@@ -48,7 +48,8 @@
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
"unified": "^11.0.5",
"unist-util-visit": "^5.0.0"
"unist-util-visit": "^5.0.0",
"zustand": "^5.0.7"
},
"devDependencies": {
"@types/react": "^18.3.18",
+26
View File
@@ -128,6 +128,9 @@ importers:
unist-util-visit:
specifier: ^5.0.0
version: 5.0.0
zustand:
specifier: ^5.0.7
version: 5.0.7(@types/react@18.3.22)(react@18.3.1)
devDependencies:
'@types/react':
specifier: ^18.3.18
@@ -3465,6 +3468,24 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
zustand@5.0.7:
resolution: {integrity: sha512-Ot6uqHDW/O2VdYsKLLU8GQu8sCOM1LcoE8RwvLv9uuRT9s6SOHCKs0ZEOhxg+I1Ld+A1Q5lwx+UlKXXUoCZITg==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@types/react': '>=18.0.0'
immer: '>=9.0.6'
react: '>=18.0.0'
use-sync-external-store: '>=1.2.0'
peerDependenciesMeta:
'@types/react':
optional: true
immer:
optional: true
react:
optional: true
use-sync-external-store:
optional: true
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
@@ -7504,4 +7525,9 @@ snapshots:
yocto-queue@0.1.0: {}
zustand@5.0.7(@types/react@18.3.22)(react@18.3.1):
optionalDependencies:
'@types/react': 18.3.22
react: 18.3.1
zwitch@2.0.4: {}
+159
View File
@@ -0,0 +1,159 @@
import { create } from 'zustand';
import { CustomFile, FileMeta } from '@/types/webrtc';
interface FileTransferState {
// 房间相关状态
shareRoomId: string;
initShareRoomId: string;
shareLink: string;
shareRoomStatusText: string;
retrieveRoomStatusText: string;
// WebRTC 连接状态
sharePeerCount: number;
retrievePeerCount: number;
senderDisconnected: boolean;
// 文件传输状态
shareContent: string;
sendFiles: CustomFile[];
retrievedContent: string;
retrievedFiles: CustomFile[];
retrievedFileMetas: FileMeta[];
// 传输进度状态
sendProgress: Record<string, any>;
receiveProgress: Record<string, any>;
isAnyFileTransferring: boolean;
// UI 状态
activeTab: 'send' | 'receive';
retrieveRoomIdInput: string;
isDragging: boolean;
// 消息状态
shareMessage: string;
retrieveMessage: string;
// Actions
// 房间相关 actions
setShareRoomId: (id: string) => void;
setInitShareRoomId: (id: string) => void;
setShareLink: (link: string) => void;
setShareRoomStatusText: (text: string) => void;
setRetrieveRoomStatusText: (text: string) => void;
// WebRTC 连接相关 actions
setSharePeerCount: (count: number) => void;
setRetrievePeerCount: (count: number) => void;
setSenderDisconnected: (disconnected: boolean) => void;
// 文件传输相关 actions
setShareContent: (content: string) => void;
setSendFiles: (files: CustomFile[]) => void;
addSendFiles: (files: CustomFile[]) => void;
removeSendFile: (meta: FileMeta) => void;
setRetrievedContent: (content: string) => void;
setRetrievedFiles: (files: CustomFile[]) => void;
setRetrievedFileMetas: (metas: FileMeta[]) => void;
// 传输进度相关 actions
setSendProgress: (progress: Record<string, any>) => void;
setReceiveProgress: (progress: Record<string, any>) => void;
setIsAnyFileTransferring: (transferring: boolean) => void;
// UI 状态相关 actions
setActiveTab: (tab: 'send' | 'receive') => void;
setRetrieveRoomIdInput: (input: string) => void;
setIsDragging: (dragging: boolean) => void;
// 消息相关 actions
setShareMessage: (message: string) => void;
setRetrieveMessage: (message: string) => void;
setRetrieveRoomId: (input: string) => void;
// 重置相关 actions
resetReceiverState: () => void;
resetSenderApp: () => void;
}
export const useFileTransferStore = create<FileTransferState>()((set, get) => ({
// 初始状态
shareRoomId: '',
initShareRoomId: '',
shareLink: '',
shareRoomStatusText: '',
retrieveRoomStatusText: '',
sharePeerCount: 0,
retrievePeerCount: 0,
senderDisconnected: false,
shareContent: '',
sendFiles: [],
retrievedContent: '',
retrievedFiles: [],
retrievedFileMetas: [],
sendProgress: {},
receiveProgress: {},
isAnyFileTransferring: false,
activeTab: 'send',
retrieveRoomIdInput: '',
isDragging: false,
shareMessage: '',
retrieveMessage: '',
// Actions 实现
setShareRoomId: (id) => set({ shareRoomId: id }),
setInitShareRoomId: (id) => set({ initShareRoomId: id }),
setShareLink: (link) => set({ shareLink: link }),
setShareRoomStatusText: (text) => set({ shareRoomStatusText: text }),
setRetrieveRoomStatusText: (text) => set({ retrieveRoomStatusText: text }),
setSharePeerCount: (count) => set({ sharePeerCount: count }),
setRetrievePeerCount: (count) => set({ retrievePeerCount: count }),
setSenderDisconnected: (disconnected) => set({ senderDisconnected: disconnected }),
setShareContent: (content) => set({ shareContent: content }),
setSendFiles: (files) => set({ sendFiles: files }),
addSendFiles: (files) => set((state) => ({ sendFiles: [...state.sendFiles, ...files] })),
removeSendFile: (meta) => set((state) => {
if (meta.folderName && meta.folderName !== '') {
return {
sendFiles: state.sendFiles.filter(
(file) => file.folderName !== meta.folderName
)
};
} else {
return {
sendFiles: state.sendFiles.filter((file) => file.name !== meta.name)
};
}
}),
setRetrievedContent: (content) => set({ retrievedContent: content }),
setRetrievedFiles: (files) => set({ retrievedFiles: files }),
setRetrievedFileMetas: (metas) => set({ retrievedFileMetas: metas }),
setSendProgress: (progress) => set({ sendProgress: progress }),
setReceiveProgress: (progress) => set({ receiveProgress: progress }),
setIsAnyFileTransferring: (transferring) => set({ isAnyFileTransferring: transferring }),
setActiveTab: (tab) => set({ activeTab: tab }),
setRetrieveRoomIdInput: (input) => set({ retrieveRoomIdInput: input }),
setRetrieveRoomId: (input) => set({ retrieveRoomIdInput: input }),
setIsDragging: (dragging) => set({ isDragging: dragging }),
setShareMessage: (message) => set({ shareMessage: message }),
setRetrieveMessage: (message) => set({ retrieveMessage: message }),
resetReceiverState: () => set({
retrievedContent: '',
retrievedFiles: [],
retrievedFileMetas: []
}),
resetSenderApp: () => set({
shareLink: '',
sendProgress: {},
receiveProgress: {},
isAnyFileTransferring: false
})
}));
+5 -1
View File
@@ -264,6 +264,10 @@ export type ClipboardApp = {
pickSaveMsg: string;
roomStatus: RoomStatus;
html: ClipboardAppHtml;
fileExistMsg?: string;
noFilesForFolderMsg?: string;
zipError?: string;
fileNotFoundMsg?: string;
};
export type Home = {
@@ -300,4 +304,4 @@ export type Text = {
export type Messages = {
meta: Meta;
text: Text;
};
};