// 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 (
);
};
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")}
);
}
};