A widget to select files better when there are many

This commit is contained in:
Igor Katson 2023-12-17 11:18:17 +00:00
parent ee307c11c5
commit ccc19f9e1a
No known key found for this signature in database
GPG key ID: B4EC22B66D61A3F5
6 changed files with 229 additions and 54 deletions

View file

@ -0,0 +1,203 @@
import { useMemo, useState } from "react";
import { AddTorrentResponse } 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";
type TorrentFileForCheckbox = {
id: number;
name: string;
length: number;
};
type FileTree = {
id: string;
name: string;
dirs: FileTree[];
files: TorrentFileForCheckbox[];
};
const splitOnce = (s: string, sep: string): [string, string | undefined] => {
if (s.indexOf(sep) === -1) {
return [s, undefined];
}
return [s.slice(0, s.indexOf(sep)), s.slice(s.indexOf(sep) + 1)];
};
const newFileTree = (listTorrentResponse: AddTorrentResponse): FileTree => {
const separator = "/";
const newFileTreeInner = (
name: string,
id: string,
files: TorrentFileForCheckbox[]
): 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) => {
let [prefix, name] = splitOnce(file.name, separator);
if (name === undefined) {
directFiles.push(file);
return;
}
getGroup(prefix).push({
id: file.id,
name: name,
length: file.length,
});
});
let childId = 0;
for (const [key, value] of Object.entries(groupsByName)) {
groups.push(newFileTreeInner(key, id + "." + childId, value));
childId += 1;
}
return {
name,
id,
dirs: groups,
files: directFiles,
};
};
return newFileTreeInner(
"",
"filetree-root",
listTorrentResponse.details.files.map((data, id) => {
return { id, name: data.name, length: data.length };
})
);
};
const FileTreeComponent: React.FC<{
tree: FileTree;
listTorrentResponse: AddTorrentResponse;
selectedFiles: Set<number>;
setSelectedFiles: React.Dispatch<React.SetStateAction<Set<number>>>;
initialExpanded: boolean;
}> = ({
tree,
selectedFiles,
setSelectedFiles,
initialExpanded,
listTorrentResponse,
}) => {
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<HTMLInputElement> = (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) => listTorrentResponse.details.files[c].length)
.reduce((a, b) => a + b, 0);
};
return (
<>
<div className="flex items-center">
<IconButton onClick={() => setExpanded(!expanded)}>
{expanded ? <CiSquareMinus /> : <CiSquarePlus />}
</IconButton>
<FormCheckbox
checked={children.every((c) => selectedFiles.has(c))}
label={`${
tree.name ? tree.name + ", " : ""
} ${getTotalSelectedFiles()} files, ${formatBytes(
getTotalSelectedBytes()
)}`}
name={tree.id}
onChange={handleToggleTree}
></FormCheckbox>
</div>
<div className="pl-5" hidden={!expanded}>
{tree.dirs.map((dir) => (
<FileTreeComponent
listTorrentResponse={listTorrentResponse}
key={dir.name}
tree={dir}
selectedFiles={selectedFiles}
setSelectedFiles={setSelectedFiles}
initialExpanded={false}
/>
))}
<div className="pl-1">
{tree.files.map((file) => (
<FormCheckbox
checked={selectedFiles.has(file.id)}
key={file.id}
label={`${file.name} (${formatBytes(file.length)})`}
name={`file-${file.id}`}
onChange={() => handleToggleFile(file.id)}
></FormCheckbox>
))}
</div>
</div>
</>
);
};
export const FileListInput: React.FC<{
listTorrentResponse: AddTorrentResponse;
selectedFiles: Set<number>;
setSelectedFiles: React.Dispatch<React.SetStateAction<Set<number>>>;
}> = ({ listTorrentResponse, selectedFiles, setSelectedFiles }) => {
let fileTree = useMemo(
() => newFileTree(listTorrentResponse),
[listTorrentResponse]
);
return (
<>
<FileTreeComponent
listTorrentResponse={listTorrentResponse}
tree={fileTree}
selectedFiles={selectedFiles}
setSelectedFiles={setSelectedFiles}
initialExpanded={true}
/>
</>
);
};

View file

@ -19,7 +19,7 @@ export const IconButton: React.FC<{
const colorClassName = color ? `text-${color}` : "";
return (
<a
className={`block p-1 text-blue-500 ${colorClassName} ${className}`}
className={`block p-1 text-blue-500 flex items-center justify-center ${colorClassName} ${className}`}
onClick={onClickStopPropagation}
href="#"
{...otherProps}

View file

@ -13,6 +13,7 @@ import { FormCheckbox } from "../forms/FormCheckbox";
import { Fieldset } from "../forms/Fieldset";
import { FormInput } from "../forms/FormInput";
import { Form } from "../forms/Form";
import { FileListInput } from "../FileListInput";
export const FileSelectionModal = (props: {
onHide: () => void;
@ -29,7 +30,7 @@ export const FileSelectionModal = (props: {
data,
} = props;
const [selectedFiles, setSelectedFiles] = useState<number[]>([]);
const [selectedFiles, setSelectedFiles] = useState<Set<number>>(new Set());
const [uploading, setUploading] = useState(false);
const [uploadError, setUploadError] = useState<ErrorWithLabel | null>(null);
const [unpopularTorrent, setUnpopularTorrent] = useState(false);
@ -37,35 +38,21 @@ export const FileSelectionModal = (props: {
const ctx = useContext(AppContext);
const API = useContext(APIContext);
const selectAll = () => {
setSelectedFiles(
listTorrentResponse
? listTorrentResponse.details.files.map((_, id) => id)
: []
);
};
useEffect(() => {
console.log(listTorrentResponse);
selectAll();
setSelectedFiles(
new Set(listTorrentResponse?.details.files.map((_, i) => i))
);
setOutputFolder(listTorrentResponse?.output_folder || "");
}, [listTorrentResponse]);
const clear = () => {
onHide();
setSelectedFiles([]);
setSelectedFiles(new Set());
setUploadError(null);
setUploading(false);
};
const handleToggleFile = (toggledId: number) => {
if (selectedFiles.includes(toggledId)) {
setSelectedFiles(selectedFiles.filter((i) => i !== toggledId));
} else {
setSelectedFiles([...selectedFiles, toggledId]);
}
};
const handleUpload = async () => {
if (!listTorrentResponse) {
return;
@ -76,7 +63,7 @@ export const FileSelectionModal = (props: {
: null;
let opts: AddTorrentOptions = {
overwrite: true,
only_files: selectedFiles,
only_files: Array.from(selectedFiles),
initial_peers: initialPeers,
output_folder: outputFolder,
};
@ -116,24 +103,11 @@ export const FileSelectionModal = (props: {
/>
<Fieldset>
<label className="text-sm mb-2 block">Select files</label>
<div className="mb-3 flex gap-2">
<Button onClick={selectAll} className="text-sm">
Select all
</Button>
<Button onClick={() => setSelectedFiles([])} className="text-sm">
Deselect all
</Button>
</div>
{listTorrentResponse.details.files.map((file, index) => (
<FormCheckbox
key={index}
label={`${file.name} (${formatBytes(file.length)})`}
checked={selectedFiles.includes(index)}
onChange={() => handleToggleFile(index)}
name={`check-${index}`}
/>
))}
<FileListInput
selectedFiles={selectedFiles}
setSelectedFiles={setSelectedFiles}
listTorrentResponse={listTorrentResponse}
/>
</Fieldset>
{/* <Fieldset label="Options">
@ -163,9 +137,7 @@ export const FileSelectionModal = (props: {
<Button
onClick={handleUpload}
variant="primary"
disabled={
listTorrentLoading || uploading || selectedFiles.length == 0
}
disabled={listTorrentLoading || uploading || selectedFiles.size == 0}
>
OK
</Button>