mirror of
https://github.com/cloudreve/frontend.git
synced 2025-12-25 19:52:48 +00:00
278 lines
8.6 KiB
TypeScript
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;
|