frontend/src/component/Uploader/Uploader.tsx

278 lines
8.6 KiB
TypeScript

import dayjs from "dayjs";
import { useSnackbar } from "notistack";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { ContextMenuTypes } from "../../redux/fileManagerSlice.ts";
import {
closeUploadTaskList,
openUploadTaskList,
setUploadProgress,
setUploadRawFiles,
} from "../../redux/globalStateSlice.ts";
import { useAppDispatch, useAppSelector } from "../../redux/hooks.ts";
import { refreshFileList, updateUserCapacity } from "../../redux/thunks/filemanager.ts";
import SessionManager, { UserSettings } from "../../session";
import useActionDisplayOpt from "../FileManager/ContextMenu/useActionDisplayOpt.ts";
import { FileManagerIndex } from "../FileManager/FileManager.tsx";
import UploadManager, { SelectType } from "./core";
import { UploaderError } from "./core/errors";
import Base, { Status } from "./core/uploader/base.ts";
import { DropFileBackground } from "./DropFile.tsx";
import PasteUploadDialog from "./PasteUploadDialog.tsx";
import TaskList from "./Popup/TaskList.tsx";
let totalProgressCollector: NodeJS.Timeout | null = null;
let lastProgressStart = -1;
let dragCounter = 0;
const defaultClipboardImageName = "image.png";
const Uploader = () => {
const { t } = useTranslation();
const dispatch = useAppDispatch();
const { enqueueSnackbar } = useSnackbar();
const [uploaders, setUploaders] = useState<Base[]>([]);
const [dropBgOpen, setDropBgOpen] = useState(false);
const uploadEnabled = useRef<boolean>(false);
const totalProgress = useAppSelector((state) => state.globalState.uploadProgress);
const taskListOpen = useAppSelector((state) => state.globalState.uploadTaskListOpen);
const parent = useAppSelector((state) => state.fileManager[FileManagerIndex.main].list?.parent);
const path = useAppSelector((state) => state.fileManager[FileManagerIndex.main].pure_path);
const policy = useAppSelector((state) => state.fileManager[FileManagerIndex.main].list?.storage_policy);
const selectFileSignal = useAppSelector((state) => state.globalState.uploadFileSignal);
const selectFolderSignal = useAppSelector((state) => state.globalState.uploadFolderSignal);
const uploadRawPromiseId = useAppSelector((state) => state.globalState.uploadRawPromiseId);
const uploadRawFiles = useAppSelector((state) => state.globalState.uploadRawFiles);
const displayOpt = useActionDisplayOpt([], ContextMenuTypes.empty, parent, FileManagerIndex.main);
useEffect(() => {
if (!parent) {
uploadEnabled.current = false;
return;
}
uploadEnabled.current = displayOpt.showUpload ?? false;
}, [parent, displayOpt.showUpload]);
const taskAdded = useCallback(
(original?: Base) => (tasks: Base[]) => {
if (original !== undefined) {
if (tasks.length !== 1 || tasks[0].key() !== original.key()) {
enqueueSnackbar(t("uploader.fileNotMatchError"), {
variant: "warning",
});
return;
}
}
tasks.forEach((t) => t.start());
dispatch(openUploadTaskList());
setUploaders((uploaders) => {
if (original !== undefined) {
uploaders = uploaders.filter((u) => u.key() !== original.key());
}
return [...uploaders, ...tasks];
});
},
[enqueueSnackbar, dispatch, setUploaders],
);
const uploadManager = useMemo(() => {
return new UploadManager({
logLevel: "INFO",
concurrentLimit: parseInt(SessionManager.getWithFallback(UserSettings.ConcurrentLimit)),
overwrite: SessionManager.getWithFallback(UserSettings.UploadOverwrite),
dropZone: document.querySelector("body"),
onToast: (type, msg) => {
enqueueSnackbar(msg, { variant: type });
},
onDropOver: (_e) => {
if (!uploadEnabled.current) {
return;
}
dragCounter++;
setDropBgOpen((value) => !value);
},
onDropLeave: (_e) => {
if (!uploadEnabled.current) {
return;
}
dragCounter--;
setDropBgOpen((value) => !value);
},
onProactiveFileAdded: taskAdded(),
onPoolEmpty: () => {
setTimeout(() => {
dispatch(refreshFileList(0));
dispatch(updateUserCapacity(0));
}, 1000);
},
});
}, [enqueueSnackbar, taskAdded, dispatch]);
useEffect(() => {
uploadManager.setPolicy(policy, path);
}, [policy, path]);
const handleUploaderError = useCallback(
(e: any) => {
if (e instanceof UploaderError) {
enqueueSnackbar(e.Message(), { variant: "warning" });
} else {
enqueueSnackbar(t("uploader:unknownError", { msg: e.message }), {
variant: "error",
});
}
},
[enqueueSnackbar, t],
);
const selectFile = useCallback(
(path: string, type = SelectType.File, original?: Base) => {
dispatch(openUploadTaskList());
// eslint-disable-next-line no-unreachable
uploadManager
.select(path, type)
.then(taskAdded(original))
.catch((e) => {
handleUploaderError(e);
});
},
[uploadManager, taskAdded, handleUploaderError, dispatch],
);
const getClipboardFileName = useCallback(
(f: File) => {
if (f.type.startsWith("image") && f.name == defaultClipboardImageName) {
return t("uploader.clipboardDefaultFileName", {
date: dayjs().valueOf(),
});
}
return f.name;
},
[t],
);
const addRawFiles = useCallback(
(files: File[]) => {
uploadManager.addRawFiles(files, getClipboardFileName).catch((e) => {
handleUploaderError(e);
});
},
[uploadManager, taskAdded, handleUploaderError, dispatch, getClipboardFileName],
);
useEffect(() => {
if (uploadRawFiles && uploadRawFiles.length > 0) {
uploadManager.addRawFiles(uploadRawFiles, getClipboardFileName, uploadRawPromiseId).catch((e) => {
handleUploaderError(e);
});
dispatch(setUploadRawFiles({ files: [], promiseId: [] }));
}
}, [uploadRawFiles, uploadRawPromiseId, handleUploaderError, uploadManager]);
useEffect(() => {
const unfinished = uploadManager.resumeTasks();
setUploaders((uploaders) => [
...uploaders,
...unfinished.filter((u) => uploaders.find((v) => v.key() === u.key()) === undefined),
]);
if (!totalProgressCollector) {
totalProgressCollector = setInterval(() => {
let totalSize = 0;
let processedSize = 0;
let total = 0;
let processed = 0;
setUploaders((uploaders) => {
uploaders.forEach((u) => {
if (u.id <= lastProgressStart) {
return;
}
totalSize += u.task.size;
total += 1;
if (u.status === Status.finished || u.status === Status.canceled || u.status === Status.error) {
processedSize += u.task.size;
processed += 1;
}
if (
u.status === Status.added ||
u.status === Status.initialized ||
u.status === Status.queued ||
u.status === Status.preparing ||
u.status === Status.processing ||
u.status === Status.finishing
) {
processedSize += u.progress ? u.progress.total.loaded : 0;
}
});
if (total > 0 && processed === total) {
lastProgressStart = uploaders[uploaders.length - 1].id;
}
return uploaders;
});
dispatch(
setUploadProgress({
progress: {
totalSize,
processedSize,
},
count: total,
}),
);
}, 2000);
}
return () => {
if (uploadManager) {
uploadManager.destroy();
}
};
}, []);
useEffect(() => {
if (selectFileSignal && selectFileSignal > 0 && path) {
selectFile(path);
}
}, [selectFileSignal]);
useEffect(() => {
if (selectFolderSignal && selectFolderSignal > 0 && path) {
selectFile(path, SelectType.Directory);
}
}, [selectFolderSignal]);
const deleteTask = useCallback((filter: (b: Base) => boolean) => {
setUploaders((uploaders) => uploaders.filter(filter));
}, []);
return (
<>
<PasteUploadDialog onFilePasted={addRawFiles} />
<DropFileBackground open={dropBgOpen} />
<TaskList
progress={totalProgress}
uploadManager={uploadManager}
taskList={uploaders}
open={!!taskListOpen}
onCancel={deleteTask}
selectFile={selectFile}
onClose={() => dispatch(closeUploadTaskList())}
setUploaders={setUploaders}
/>
</>
);
};
export default Uploader;