handleSort("size")}
>
{t("sftp.columns.size")}
{sortField === "size" && (
{sortOrder === "asc" ? "^" : "v"}
)}
handleResizeStart("size", e)}
/>
handleSort("modified")}
>
{t("sftp.columns.modified")}
{sortField === "modified" && (
{sortOrder === "asc" ? "^" : "v"}
)}
handleResizeStart("modified", e)}
/>
{t("sftp.columns.actions")}
{/* File List with Virtual Scrolling */}
{dragActive && (
{t("sftp.dropFilesHere")}
)}
{loading && files.length === 0 && (
)}
{loadingTextContent && (
{t("sftp.status.loading")}
)}
{/* Reconnecting overlay */}
{reconnecting && (
{t("sftp.reconnecting.title")}
{t("sftp.reconnecting.desc")}
)}
{files.length === 0 && !loading && (
{t("sftp.emptyDirectory")}
{t("sftp.dragDropToUpload")}
)}
{/* File rows with virtual scrolling */}
{visibleRows.map(({ file, index: idx, top }) => {
// Check if this entry is navigable like a directory
const isNavigableDirectory = file.type === "directory" || (file.type === "symlink" && file.linkTarget === "directory");
const isDownloadableFile = file.type === "file" || (file.type === "symlink" && file.linkTarget === "file");
const isParentEntry = file.name === "..";
return (
handleFileClick(file, idx, e)}
onDoubleClick={() => handleFileDoubleClick(file)}
>
{getFileIcon(file.name, isNavigableDirectory, file.type === "symlink" && !isNavigableDirectory)}
{file.type === "symlink" && }
{file.name}
{file.type === "symlink" && (symbolic link)}
{isNavigableDirectory ? "--" : formatBytes(file.size)}
{formatDate(file.lastModified, resolvedLocale)}
{isDownloadableFile && (
)}
{!isParentEntry && (
)}
{isParentEntry ? (
{
// Navigate to parent directory
const segments = currentPath.split("/").filter(Boolean);
segments.pop();
const parentPath = segments.length === 0 ? "/" : `/${segments.join("/")}`;
handleNavigate(parentPath);
}}
>
{t("sftp.context.open")}
) : (
<>
{(file.type === "directory" || (file.type === "symlink" && file.linkTarget === "directory")) && (
handleNavigate(
currentPath === "/"
? `/${file.name}`
: `${currentPath}/${file.name}`,
)
}
>
{t("sftp.context.open")}
)}
{(file.type === "file" || (file.type === "symlink" && file.linkTarget === "file")) && (
<>
{/* Open - uses saved association or shows dialog */}
handleOpenFile(file)}>
{t("sftp.context.open")}
{/* Open with - always shows dialog */}
openFileOpenerDialog(file)}>
{t("sftp.context.openWith")}
{/* Edit - for text files (not known binary) */}
{!isKnownBinaryFile(file.name) && (
handleEditFile(file)}>
{t("sftp.context.edit")}
)}
handleDownload(file)}>
{t("sftp.context.download")}
>
)}
openRenameDialog(file)}>
{t("sftp.context.rename")}
{!isLocalSession && (
openPermissionsDialog(file)}>
{t("sftp.context.permissions")}
)}
handleDelete(file)}
>
{t("sftp.context.delete")}
>
)}
);
})}
{t("sftp.newFolder")}
inputRef.current?.click()}>
{t("sftp.uploadFiles")}
loadFiles(currentPath, { force: true })}>
{t("sftp.context.refresh")}
{selectedFiles.size > 0 && (
<>
{t("sftp.context.downloadSelected", { count: selectedFiles.size })}
{t("sftp.context.deleteSelected", { count: selectedFiles.size })}
>
)}
{/* Upload progress at bottom */}
{uploadTasks.length > 0 && (
{uploadTasks.map((task) => {
// Format speed
const formatSpeed = (bytesPerSec: number) => {
if (bytesPerSec <= 0) return "";
if (bytesPerSec >= 1024 * 1024)
return `${(bytesPerSec / (1024 * 1024)).toFixed(1)} MB/s`;
if (bytesPerSec >= 1024)
return `${(bytesPerSec / 1024).toFixed(1)} KB/s`;
return `${Math.round(bytesPerSec)} B/s`;
};
// Format bytes
const formatBytes = (bytes: number) => {
if (bytes === 0) return "0 B";
if (bytes >= 1024 * 1024)
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
if (bytes >= 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${bytes} B`;
};
// Remaining time
const remainingBytes = task.totalBytes - task.transferredBytes;
const remainingTime =
task.speed > 0 ? Math.ceil(remainingBytes / task.speed) : 0;
const remainingStr =
remainingTime > 60
? `~${Math.ceil(remainingTime / 60)}m left`
: remainingTime > 0
? `~${remainingTime}s left`
: "";
return (
{task.status === "uploading" && (
)}
{task.status === "pending" && (
)}
{task.status === "completed" && (
)}
{task.status === "failed" && (
)}
{task.fileName}
{task.status === "uploading" && task.speed > 0 && (
{formatSpeed(task.speed)}
)}
{task.status === "uploading" && remainingStr && (
{remainingStr}
)}
{(task.status === "uploading" ||
task.status === "pending") && (
{task.status === "uploading"
? `${task.progress}%`
: "..."}
)}
{task.status === "uploading" && task.totalBytes > 0 && (
{formatBytes(task.transferredBytes)} /{" "}
{formatBytes(task.totalBytes)}
)}
{task.status === "completed" && (
Completed �?{formatBytes(task.totalBytes)}
)}
{task.status === "failed" && task.error && (
{task.error}
)}
{task.status === "pending" && t("sftp.task.waiting")}
);
})}
)}
{/* Footer */}
{t("sftp.itemsCount", { count: files.length })}
{selectedFiles.size > 0 && (
<>
|
{t("sftp.selectedCount", { count: selectedFiles.size })}
>
)}
{loading
? t("sftp.status.loading")
: uploading
? t("sftp.status.uploading")
: t("sftp.status.ready")}
{/* Rename Dialog */}
{/* Permissions Dialog */}
{/* File Opener Dialog */}
{
setShowFileOpenerDialog(false);
setFileOpenerTarget(null);
}}
fileName={fileOpenerTarget?.name || ""}
onSelect={handleFileOpenerSelect}
onSelectSystemApp={handleSelectSystemApp}
/>
{/* Text Editor Modal */}
{
setShowTextEditor(false);
setTextEditorTarget(null);
setTextEditorContent("");
}}
fileName={textEditorTarget?.name || ""}
initialContent={textEditorContent}
onSave={handleSaveTextFile}
/>
);
};
export default SFTPModal;