import { useContext, useMemo, useState } from "react"; import { TorrentDetails, TorrentStats } from "../api-types"; import { FormCheckbox } from "./forms/FormCheckbox"; import { CiSquarePlus, CiSquareMinus } from "react-icons/ci"; import { IconButton } from "./buttons/IconButton"; import { formatBytes } from "../helper/formatBytes"; import { ProgressBar } from "./ProgressBar"; import sortBy from "lodash.sortby"; import { APIContext } from "../context"; type TorrentFileForCheckbox = { id: number; filename: string; pathComponents: string[]; length: number; have_bytes: number; }; type FileTree = { id: string; name: string; dirs: FileTree[]; files: TorrentFileForCheckbox[]; }; const newFileTree = ( torrentDetails: TorrentDetails, stats: TorrentStats | null ): FileTree => { const newFileTreeInner = ( name: string, id: string, files: TorrentFileForCheckbox[], depth: number ): FileTree => { let directFiles: TorrentFileForCheckbox[] = []; let groups: FileTree[] = []; let groupsByName: { [key: string]: TorrentFileForCheckbox[] } = {}; const getGroup = (prefix: string): TorrentFileForCheckbox[] => { groupsByName[prefix] = groupsByName[prefix] || []; return groupsByName[prefix]; }; files.forEach((file: TorrentFileForCheckbox) => { if (depth == file.pathComponents.length - 1) { directFiles.push(file); return; } getGroup(file.pathComponents[depth]).push(file); }); directFiles = sortBy(directFiles, (f) => f.filename); let sortedGroupsByName = sortBy( Object.entries(groupsByName), ([k, _]) => k ); let childId = 0; for (const [key, value] of sortedGroupsByName) { groups.push(newFileTreeInner(key, id + "." + childId, value, depth + 1)); childId += 1; } return { name, id, dirs: groups, files: directFiles, }; }; return newFileTreeInner( "", "filetree-root", torrentDetails.files .map((file, id) => { if (file.attributes.padding) { return null; } return { id, filename: file.components[file.components.length - 1], pathComponents: file.components, length: file.length, have_bytes: stats ? (stats.file_progress[id] ?? 0) : 0, }; }) .filter((f) => f !== null), 0 ); }; const FileTreeComponent: React.FC<{ torrentId?: number; tree: FileTree; torrentDetails: TorrentDetails; torrentStats: TorrentStats | null; selectedFiles: Set; setSelectedFiles: (_: Set) => void; initialExpanded: boolean; showProgressBar?: boolean; disabled?: boolean; allowStream?: boolean; }> = ({ torrentId, tree, selectedFiles, setSelectedFiles, initialExpanded, torrentDetails, torrentStats, showProgressBar, disabled, allowStream, }) => { const API = useContext(APIContext); let [expanded, setExpanded] = useState(initialExpanded); let children = useMemo(() => { let getAllChildren = (tree: FileTree): number[] => { let children = tree.dirs.flatMap(getAllChildren); children.push(...tree.files.map((file) => file.id)); return children; }; return getAllChildren(tree); }, [tree]); const handleToggleTree: React.ChangeEventHandler = (e) => { if (e.target.checked) { let copy = new Set(selectedFiles); children.forEach((c) => copy.add(c)); setSelectedFiles(copy); } else { let copy = new Set(selectedFiles); children.forEach((c) => copy.delete(c)); setSelectedFiles(copy); } }; const handleToggleFile = (toggledId: number) => { if (selectedFiles.has(toggledId)) { let copy = new Set(selectedFiles); copy.delete(toggledId); setSelectedFiles(copy); } else { let copy = new Set(selectedFiles); copy.add(toggledId); setSelectedFiles(copy); } }; const getTotalSelectedFiles = () => { return children.filter((c) => selectedFiles.has(c)).length; }; const getTotalSelectedBytes = () => { return children .filter((c) => selectedFiles.has(c)) .map((c) => torrentDetails.files[c].length) .reduce((a, b) => a + b, 0); }; const fileLink = (file: TorrentFileForCheckbox) => { if (allowStream && torrentId != null) { return API.getTorrentStreamUrl(torrentId, file.id, file.filename); } }; return ( <>
setExpanded(!expanded)}> {expanded ? : } selectedFiles.has(c))} label={`${ tree.name ? tree.name + ", " : "" } ${getTotalSelectedFiles()} files, ${formatBytes( getTotalSelectedBytes() )}`} name={tree.id} onChange={handleToggleTree} >
); }; export const FileListInput: React.FC<{ torrentId?: number; torrentDetails: TorrentDetails; torrentStats: TorrentStats | null; selectedFiles: Set; setSelectedFiles: (_: Set) => void; showProgressBar?: boolean; disabled?: boolean; allowStream?: boolean; }> = ({ torrentId, torrentDetails, selectedFiles, setSelectedFiles, torrentStats, showProgressBar, disabled, allowStream, }) => { let fileTree = useMemo( () => newFileTree(torrentDetails, torrentStats), [torrentDetails, torrentStats] ); return ( ); };