// StorageFilePage.tsx import React, { useCallback, useEffect, useMemo, useState } from "react"; import { LuRefreshCw } from "react-icons/lu"; import { PlusCircleIcon } from "@heroicons/react/20/solid"; import { useNavigate } from "react-router-dom"; import { useReactAt } from 'i18n-auto-extractor/react' import { Typography , Button as AntdButton } from "antd"; import OnSDCardSvg from "@assets/second/noSD.svg?react" import RefreshSvg from "@assets/second/refresh.svg?react" import Card from "@components/Card"; import { formatters } from "@/utils"; import { DEVICE_API } from "@/ui.config"; import { useJsonRpc } from "@/hooks/useJsonRpc"; import notifications from "@/notifications"; import { FileUploader } from "@components/FileManager/FileUploader"; import { PreUploadedImageItem } from "@components/PreUploadedImageItem"; import { dark_bd_style, dark_bg_desktop, dark_font_style , text_primary_color } from "@/layout/theme_color"; const { Title } = Typography; export interface StorageFile { name: string; size: string; createdAt: string; } export interface StorageSpace { bytesUsed: number; bytesFree: number; } const LoadingOverlay: React.FC = () => { const { $at } = useReactAt(); return (
{$at("Processing...")}
); }; export interface StorageFiles { files: { filename: string; size: number; createdAt: string; }[]; } interface StorageFilePageProps { mediaType: "local" | "sd"; returnTo: string; listFilesMethod: string; getSpaceMethod: string; deleteFileMethod: string; downloadUrlPrefix: string; showSDManagement?: boolean; onResetSDStorage?: () => void; onUnmountSDStorage?: () => void; onFormatSDStorage?: () => void; onMountSDStorage?: () => void; fsType?: 'exfat' | 'fat32'; onFsTypeChange?: (value: 'exfat' | 'fat32') => void; } export const FileManager: React.FC = ({ mediaType, listFilesMethod, getSpaceMethod, deleteFileMethod, downloadUrlPrefix, showSDManagement = false, onResetSDStorage, onUnmountSDStorage, onFormatSDStorage, fsType, onFsTypeChange, }) => { const { $at } = useReactAt(); const [onStorageFiles, setOnStorageFiles] = useState([]); const [sdMountStatus, setSDMountStatus] = useState<"ok" | "none" | "fail" | null>( mediaType === "sd" ? null : "ok" ); const [currentPage, setCurrentPage] = useState(1); const [loading, setLoading] = useState(false); const filesPerPage = 5; const [send] = useJsonRpc(); const [storageSpace, setStorageSpace] = useState(null); const [uploadFile, setUploadFile] = useState(null); const percentageUsed = useMemo(() => { if (!storageSpace) return 0; return Number( ( (storageSpace.bytesUsed / (storageSpace.bytesUsed + storageSpace.bytesFree)) * 100 ).toFixed(1), ); }, [storageSpace]); const bytesUsed = useMemo(() => storageSpace?.bytesUsed || 0, [storageSpace]); const bytesFree = useMemo(() => storageSpace?.bytesFree || 0, [storageSpace]); const syncStorage = useCallback(() => { if (mediaType === "sd") { send("getSDMountStatus", {}, res => { if ("error" in res) { notifications.error(`Failed to check SD card status: ${res.error}`); setSDMountStatus(null); return; } const { status } = res.result as { status: "ok" | "none" | "fail" }; setSDMountStatus(status); if (status !== "ok") return; fetchFilesAndSpace(); }); } else { fetchFilesAndSpace(); } }, [send, mediaType]); const fetchFilesAndSpace = useCallback(() => { send(listFilesMethod, {}, res => { if ("error" in res) { notifications.error(`Error listing storage files: ${res.error}`); return; } const { files } = res.result as StorageFiles; const formattedFiles = files.map(file => ({ name: file.filename, size: formatters.bytes(file.size), createdAt: formatters.date(new Date(file?.createdAt)), })); setOnStorageFiles(formattedFiles); }); send(getSpaceMethod, {}, res => { if ("error" in res) { notifications.error(`Error getting storage space: ${res.error}`); return; } const space = res.result as StorageSpace; setStorageSpace(space); }); }, [send, listFilesMethod, getSpaceMethod]); useEffect(() => { syncStorage(); }, [syncStorage]); const handleDeleteFile = useCallback((file: StorageFile) => { send(deleteFileMethod, { filename: file.name }, res => { if ("error" in res) { notifications.error(`Error deleting file: ${res.error}`); return; } syncStorage(); }); }, [send, deleteFileMethod, syncStorage]); const handleDownloadFile = useCallback((file: StorageFile) => { const downloadUrl = `${DEVICE_API}${downloadUrlPrefix}?file=${encodeURIComponent(file.name)}`; const a = document.createElement("a"); a.href = downloadUrl; a.download = file.name; document.body.appendChild(a); a.click(); a.remove(); }, [downloadUrlPrefix]); const handleNewImageClick = useCallback((incompleteFileName?: string) => { if (incompleteFileName) { setUploadFile(incompleteFileName); } else { setUploadFile(null); } }, []); const indexOfLastFile = currentPage * filesPerPage; const indexOfFirstFile = indexOfLastFile - filesPerPage; const currentFiles = onStorageFiles.slice(indexOfFirstFile, indexOfLastFile); const totalPages = Math.ceil(onStorageFiles.length / filesPerPage); const handlePreviousPage = () => setCurrentPage(prev => Math.max(prev - 1, 1)); const handleNextPage = () => setCurrentPage(prev => Math.min(prev + 1, totalPages)); const handleUnmountWrapper = useCallback(async () => { if (onUnmountSDStorage) { setLoading(true); await onUnmountSDStorage(); setSDMountStatus(null); syncStorage(); setLoading(false); } }, [onUnmountSDStorage, syncStorage]); const handleResetWrapper = useCallback(async () => { if (onResetSDStorage) { setLoading(true); await onResetSDStorage(); setSDMountStatus(null); syncStorage(); setLoading(false); } }, [onResetSDStorage, syncStorage]); const handleFormatWrapper = useCallback(async () => { if (onFormatSDStorage) { setLoading(true); await onFormatSDStorage(); setSDMountStatus(null); syncStorage(); setLoading(false); } }, [onFormatSDStorage, syncStorage]); if (mediaType === "sd" && sdMountStatus && sdMountStatus !== "ok") { return (

{sdMountStatus === "none" ? $at("No SD Card Detected") : $at("SD Card Mount Failed")}

{sdMountStatus === "none" ? $at("Please insert an SD card and try again.") : $at("Please format the SD card and try again.")}

{sdMountStatus !== "none" && (

{$at("Choose the file system for MicroSD formatting")}

{$at("Format MicroSD Card")} ({(fsType || "fat32")})
)}
{loading && }
); } return (
{ (mediaType === "sd") ? $at("Manage Shared Folders in KVM MicroSD Card") : $at("Manage Shared Folders in KVM Storage") } filesPerPage} paginationInfo={{ indexOfFirstFile, indexOfLastFile, totalFiles: onStorageFiles.length, currentPage, totalPages }} onPreviousPage={handlePreviousPage} onNextPage={handleNextPage} />
{uploadFile ? ( { setUploadFile(null); syncStorage(); }} incompleteFileName={uploadFile} media={mediaType} /> ) : ( )}
); }; interface FileListSectionProps { files: StorageFile[]; currentFiles: StorageFile[]; loading: boolean; onDelete: (file: StorageFile) => void; onDownload: (file: StorageFile) => void; onNewImageClick: (incompleteFileName?: string) => void; showPagination: boolean; paginationInfo: { indexOfFirstFile: number; indexOfLastFile: number; totalFiles: number; currentPage: number; totalPages: number; }; onPreviousPage: () => void; onNextPage: () => void; } const FileListSection: React.FC = ({ files, currentFiles, loading, onDelete, onDownload, onNewImageClick, showPagination, paginationInfo, onPreviousPage, onNextPage }) => { const { $at } = useReactAt(); if (files.length === 0) { return (

{$at("No files found")}

{$at("Get started by uploading your first file")}

{loading && }
); } return (
{currentFiles.map((file, index) => ( { if (window.confirm($at("Are you sure you want to download ") + file.name + "?")) { onDownload(file); } }} onDelete={() => { if (window.confirm($at("Are you sure you want to delete ") + file.name + "?")) { onDelete(file); } }} onContinueUpload={() => onNewImageClick(file.name)} /> ))} {showPagination && (

{$at("Showing")} {paginationInfo.indexOfFirstFile + 1} {$at("to")}{" "} {Math.min(paginationInfo.indexOfLastFile, paginationInfo.totalFiles)} {" "} {$at("of")} {paginationInfo.totalFiles} {$at("results")}

{$at("Previous")} {$at("Next")}
)}
{loading && }
); }; interface StorageSpaceBarProps { percentageUsed: number; bytesUsed: number; bytesFree: number; } export default function StorageSpaceBar({ percentageUsed, bytesUsed, bytesFree }: StorageSpaceBarProps) { const { $at } = useReactAt(); return ( <>
{$at("Available space")} {percentageUsed}% {$at("used")}
{formatters.bytes(bytesUsed)} {$at("used")} {formatters.bytes(bytesFree)} {$at("free")}
); } interface ActionButtonsSectionProps { mediaType: "local" | "sd"; loading: boolean; showSDManagement?: boolean; onNewImageClick: (incompleteFileName?: string) => void; onUnmountSDStorage?: () => void; onFormatSDStorage?: () => void; syncStorage: () => void; fsType?: 'exfat' | 'fat32'; onFsTypeChange?: (value: 'exfat' | 'fat32') => void; } const ActionButtonsSection: React.FC = ({ mediaType, loading, showSDManagement, onUnmountSDStorage, onFormatSDStorage, fsType, onFsTypeChange, }) => { const { $at } = useReactAt(); if (mediaType === "sd" && showSDManagement) { return (

{$at("Choose the file system for MicroSD formatting")}

{$at("Format MicroSD Card")} ({(fsType || "fat32")})
{$at("Unmount MicroSD Card")}
); } };