feat:Receiver adds a secure save button

Receiver adds a secure save button, used to save cache files during connection interruptions, making it convenient to resume transfers next time.
Button tooltip information added in multiple languages.
This commit is contained in:
david_bai
2025-08-15 23:49:24 +08:00
parent bf9aa9f7ed
commit 7ee9360b17
13 changed files with 95 additions and 10 deletions
+2
View File
@@ -76,6 +76,7 @@ const ClipboardApp = () => {
senderDisconnected,
resetReceiverConnection,
resetSenderConnection,
manualSafeSave,
} = useWebRTCConnection({
shareContent,
sendFiles,
@@ -307,6 +308,7 @@ const ClipboardApp = () => {
retrieveMessage={retrieveMessage}
senderDisconnected={senderDisconnected}
handleLeaveRoom={handleLeaveReceiverRoom}
manualSafeSave={manualSafeSave}
/>
)}
</CardContent>
@@ -40,6 +40,7 @@ interface FileListDisplayProps {
onRequest?: (item: FileMeta) => void; // Request file
onDelete?: (item: FileMeta) => void;
onLocationPick?: () => Promise<boolean>;
onSafeSave?: () => void; // New prop for safe save functionality
saveType?: { [fileId: string]: boolean }; // File stored on disk or in memory
largeFileThreshold?: number;
}
@@ -61,6 +62,7 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
onRequest,
onDelete,
onLocationPick,
onSafeSave,
saveType,
largeFileThreshold = 500 * 1024 * 1024, // 500MB default
}) => {
@@ -380,6 +382,28 @@ const FileListDisplay: React.FC<FileListDisplayProps> = ({
{messages.text.FileListDisplay.chooseSavePath_dis}
</Button>
)}
{/* Safe Save Button - only show when location is picked and files are saved to disk */}
{onSafeSave &&
pickedLocation &&
(isAnyFileTransferring ||
(saveType && Object.values(saveType).some(
(isSavedToDisk) => isSavedToDisk
))) && (
<Tooltip
content={messages.text.FileListDisplay.safeSave_tooltip}
>
<Button
onClick={() => {
onSafeSave();
}}
variant="outline"
size="sm"
className="mr-2 text-green-600 border-green-600 hover:bg-green-50"
>
{messages.text.FileListDisplay.safeSave_dis}
</Button>
</Tooltip>
)}
</div>
</div>
)}
@@ -39,6 +39,7 @@ interface RetrieveTabPanelProps {
directoryHandle: FileSystemDirectoryHandle
) => Promise<void>;
getReceiverSaveType: () => { [fileId: string]: boolean } | undefined;
manualSafeSave: () => void; // Add manual safe save function
retrieveMessage: string;
senderDisconnected: boolean;
handleLeaveRoom: () => void;
@@ -63,6 +64,7 @@ export function RetrieveTabPanel({
requestFolder,
setReceiverDirectoryHandle,
getReceiverSaveType,
manualSafeSave,
retrieveMessage,
senderDisconnected,
handleLeaveRoom,
@@ -180,6 +182,7 @@ export function RetrieveTabPanel({
onDownload={handleDownloadFile}
onRequest={handleFileRequestFromPanel} // Use the panel's own handler
onLocationPick={onLocationPick} // Use the panel's own handler
onSafeSave={manualSafeSave} // Add safe save handler
saveType={getReceiverSaveType()}
/>
{retrieveMessage && (
+5
View File
@@ -254,6 +254,9 @@ export const de: Messages = {
chooseSavePath_tips:
"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!",
},
RetrieveMethod: {
P: "Glückwunsch 🎉 Freigegebene Inhalte warten darauf, abgerufen zu werden:",
@@ -298,6 +301,8 @@ export const de: Messages = {
onlyOneMsg: "Sie sind der Einzige hier",
peopleMsg_template: "{peerCount} Personen im Raum",
connected_dis: "Verbunden",
senderDisconnectedMsg: "Sender getrennt",
leftRoomMsg: "Sie haben den Raum verlassen.",
},
html: {
senderTab: "Senden",
+6 -1
View File
@@ -251,6 +251,9 @@ export const en: Messages = {
chooseSavePath_tips:
"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!",
},
RetrieveMethod: {
P: "Congrats 🎉 Share content is waiting to be retrieved:",
@@ -287,9 +290,11 @@ export const en: Messages = {
roomStatus: {
senderEmptyMsg: "Room is empty",
receiverEmptyMsg: "You can accept an invitation to join the room",
onlyOneMsg: "Youre the only one here",
onlyOneMsg: "You're the only one here",
peopleMsg_template: "{peerCount} People in the room",
connected_dis: "Connected",
senderDisconnectedMsg: "Sender disconnected",
leftRoomMsg: "You have left the room.",
},
html: {
senderTab: "Send",
+5
View File
@@ -252,6 +252,9 @@ export const es: Messages = {
chooseSavePath_tips:
"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!",
},
RetrieveMethod: {
P: "¡Felicitaciones 🎉 El contenido compartido está esperando ser recuperado:",
@@ -291,6 +294,8 @@ export const es: Messages = {
onlyOneMsg: "Eres el único aquí",
peopleMsg_template: "{peerCount} Personas en la sala",
connected_dis: "Conectado",
senderDisconnectedMsg: "Remitente desconectado",
leftRoomMsg: "Has salido de la sala.",
},
html: {
senderTab: "Enviar",
+7 -2
View File
@@ -253,8 +253,11 @@ export const fr: Messages = {
PopupDialog_description:
"Nous recommandons de sélectionner un répertoire de sauvegarde pour enregistrer directement les fichiers sur votre disque. Cela facilite le transfert de fichiers volumineux et la synchronisation efficace des dossiers.",
chooseSavePath_tips:
"Enregistrez des fichiers ou dossiers volumineux directement dans un répertoire sélectionné. 👉",
chooseSavePath_dis: "Choisir un emplacement de sauvegarde",
"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 !",
},
RetrieveMethod: {
P: "Félicitations 🎉 Le contenu partagé attend d'être récupéré :",
@@ -298,6 +301,8 @@ export const fr: Messages = {
onlyOneMsg: "Vous êtes le seul ici",
peopleMsg_template: "{peerCount} personnes dans la salle",
connected_dis: "Connecté",
senderDisconnectedMsg: "Expéditeur déconnecté",
leftRoomMsg: "Vous avez quitté la salle.",
},
html: {
senderTab: "Envoyer",
+5
View File
@@ -247,6 +247,9 @@ export const ja: Messages = {
chooseSavePath_tips:
"大きなファイルやフォルダを選択したディレクトリに直接保存します。👉",
chooseSavePath_dis: "保存場所を選択",
safeSave_dis: "安全保存",
safeSave_tooltip: "接続の中断を恐れる必要はありません。ここをクリックして、次回の再開のためにファイルを安全に保存してください",
safeSaveSuccessMsg: "ファイルが安全にディスクに保存されました。ページを安全に閉じることができ、転送の再開をサポートします!",
},
RetrieveMethod: {
P: "おめでとう 🎉 共有コンテンツが取得待ちです:",
@@ -286,6 +289,8 @@ export const ja: Messages = {
onlyOneMsg: "あなただけがここにいます",
peopleMsg_template: "{peerCount} 人がルームにいます",
connected_dis: "接続済み",
senderDisconnectedMsg: "送信者が切断されました",
leftRoomMsg: "ルームを離れました。",
},
html: {
senderTab: "送信",
+6 -1
View File
@@ -243,8 +243,11 @@ export const ko: Messages = {
PopupDialog_description:
"대용량 파일이나 폴더를 직접 디스크에 저장하는 것을 권장합니다. 이를 통해 대용량 파일 전송 및 폴더 동기화가 더 효율적으로 이루어집니다.",
chooseSavePath_tips:
"대용량 파일이나 폴더를 선택한 디렉리에 직접 저장하세요. 👉",
" 파일이나 폴더를 선택한 디렉리에 직접 저장합니다. 👉",
chooseSavePath_dis: "저장 위치 선택",
safeSave_dis: "안전 저장",
safeSave_tooltip: "연결 중단을 두려워하지 마세요. 다음 재개를 위해 파일을 안전하게 저장하려면 여기를 클릭하세요",
safeSaveSuccessMsg: "파일이 디스크에 안전하게 저장되었습니다. 페이지를 안전하게 닫을 수 있으며 전송 재개를 지원합니다!",
},
RetrieveMethod: {
P: "축하합니다 🎉 공유된 콘텐츠가 검색을 기다리고 있습니다:",
@@ -284,6 +287,8 @@ export const ko: Messages = {
onlyOneMsg: "현재 방에 혼자 있습니다",
peopleMsg_template: "방에 {peerCount}명이 있습니다",
connected_dis: "연결됨",
senderDisconnectedMsg: "발신자가 연결 해제됨",
leftRoomMsg: "방을 나갔습니다.",
},
html: {
senderTab: "보내기",
+5
View File
@@ -233,6 +233,9 @@ export const zh: Messages = {
"我们建议选择一个保存目录来直接将文件保存到磁盘。这样可以更方便地传输大文件和同步文件夹。",
chooseSavePath_tips: "大文件或文件夹可直接保存到指定目录 👉",
chooseSavePath_dis: "选择保存位置",
safeSave_dis: "安全保存",
safeSave_tooltip: "连接中断不要怕,点击这里安全保存文件,方便下次续传",
safeSaveSuccessMsg: "文件已安全保存到磁盘,可以安全关闭页面,支持断点续传!",
},
RetrieveMethod: {
P: "恭喜 🎉 共享内容等待接收:",
@@ -270,6 +273,8 @@ export const zh: Messages = {
onlyOneMsg: "只有您一人在房间内",
peopleMsg_template: `房间内共{peerCount}人`,
connected_dis: "已连接",
senderDisconnectedMsg: "发送端已断开连接",
leftRoomMsg: "您已离开房间。",
},
html: {
senderTab: "发送",
+6 -6
View File
@@ -48,10 +48,10 @@ export function useRoomManager({
// Receiver leave room function (renamed and simplified)
const handleLeaveReceiverRoom = useCallback(async () => {
if (!receiver || !receiver.roomId || !receiver.peerId) return;
if (!receiver || !receiver.roomId || !receiver.peerId || !messages) return;
try {
await leaveRoom(receiver.roomId, receiver.peerId);
putMessageInMs("You have left the room.", false);
putMessageInMs(messages.text.ClipboardApp.roomStatus.leftRoomMsg, false);
} catch (error) {
console.error("Error leaving room:", error);
putMessageInMs("Failed to leave the room.", false);
@@ -59,7 +59,7 @@ export function useRoomManager({
// Reset application state
resetApp();
}
}, [receiver, putMessageInMs, resetApp]);
}, [receiver, putMessageInMs, resetApp, messages]);
// Reset sender app state (preserve send content, get new room ID)
const resetSenderApp = useCallback(async () => {
@@ -87,10 +87,10 @@ export function useRoomManager({
// Sender leave room function (new)
const handleLeaveSenderRoom = useCallback(async () => {
if (!sender || !sender.roomId || !sender.peerId) return;
if (!sender || !sender.roomId || !sender.peerId || !messages) return;
try {
await leaveRoom(sender.roomId, sender.peerId);
putMessageInMs("You have left the room.", true);
putMessageInMs(messages.text.ClipboardApp.roomStatus.leftRoomMsg, true);
} catch (error) {
console.error("Error leaving room:", error);
putMessageInMs("Failed to leave the room.", true);
@@ -98,7 +98,7 @@ export function useRoomManager({
// Reset sender state and get new room ID
await resetSenderApp();
}
}, [sender, putMessageInMs, resetSenderApp]);
}, [sender, putMessageInMs, resetSenderApp, messages]);
// Initialize shareRoomId on mount
useEffect(() => {
+16
View File
@@ -351,6 +351,21 @@ export function useWebRTCConnection({
}
}, [sender]);
// Manual safe save function (replaces the beforeunload graceful shutdown)
const manualSafeSave = useCallback(() => {
if (receiverFileTransfer) {
receiverFileTransfer.gracefulShutdown();
// Provide user feedback
if (putMessageInMs && messages) {
putMessageInMs(
messages.text.FileListDisplay.safeSaveSuccessMsg,
false,
3000
);
}
}
}, [receiverFileTransfer, putMessageInMs, messages]);
return {
sender, // Exposed for useRoomManager (e.g., sender.isInRoom, sender.joinRoom)
receiver, // Exposed for useRoomManager
@@ -368,5 +383,6 @@ export function useWebRTCConnection({
senderDisconnected,
resetReceiverConnection, // Export the new reset function
resetSenderConnection, // Export the new sender reset function
manualSafeSave, // Export the manual safe save function
};
}
+5
View File
@@ -191,6 +191,9 @@ export type FileListDisplay = {
PopupDialog_description: string;
chooseSavePath_tips: string;
chooseSavePath_dis: string;
safeSave_dis: string;
safeSave_tooltip: string;
safeSaveSuccessMsg: string;
};
export type RetrieveMethod = {
@@ -225,6 +228,8 @@ export type RoomStatus = {
onlyOneMsg: string;
peopleMsg_template: string;
connected_dis: string;
senderDisconnectedMsg: string;
leftRoomMsg: string;
};
export type ClipboardAppHtml = {