244 lines
7.7 KiB
TypeScript
244 lines
7.7 KiB
TypeScript
|
import FileEntry from "components/system/Files/FileEntry";
|
||
|
import StyledSelection from "components/system/Files/FileManager/Selection/StyledSelection";
|
||
|
import useSelection from "components/system/Files/FileManager/Selection/useSelection";
|
||
|
import useDraggableEntries from "components/system/Files/FileManager/useDraggableEntries";
|
||
|
import useFileDrop from "components/system/Files/FileManager/useFileDrop";
|
||
|
import useFileKeyboardShortcuts from "components/system/Files/FileManager/useFileKeyboardShortcuts";
|
||
|
import useFocusableEntries from "components/system/Files/FileManager/useFocusableEntries";
|
||
|
import useFolder from "components/system/Files/FileManager/useFolder";
|
||
|
import useFolderContextMenu from "components/system/Files/FileManager/useFolderContextMenu";
|
||
|
import type { FileManagerViewNames } from "components/system/Files/Views";
|
||
|
import { FileManagerViews } from "components/system/Files/Views";
|
||
|
import { useFileSystem } from "contexts/fileSystem";
|
||
|
import { requestPermission } from "contexts/fileSystem/functions";
|
||
|
import dynamic from "next/dynamic";
|
||
|
import { basename, extname, join } from "path";
|
||
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||
|
import {
|
||
|
FOCUSABLE_ELEMENT,
|
||
|
MOUNTABLE_EXTENSIONS,
|
||
|
PREVENT_SCROLL,
|
||
|
SHORTCUT_EXTENSION,
|
||
|
} from "utils/constants";
|
||
|
|
||
|
const StatusBar = dynamic(
|
||
|
() => import("components/system/Files/FileManager/StatusBar")
|
||
|
);
|
||
|
|
||
|
const StyledLoading = dynamic(
|
||
|
() => import("components/system/Files/FileManager/StyledLoading")
|
||
|
);
|
||
|
|
||
|
type FileManagerProps = {
|
||
|
allowMovingDraggableEntries?: boolean;
|
||
|
hideFolders?: boolean;
|
||
|
hideLoading?: boolean;
|
||
|
hideScrolling?: boolean;
|
||
|
hideShortcutIcons?: boolean;
|
||
|
id?: string;
|
||
|
isDesktop?: boolean;
|
||
|
loadIconsImmediately?: boolean;
|
||
|
preloadShortcuts?: boolean;
|
||
|
readOnly?: boolean;
|
||
|
showStatusBar?: boolean;
|
||
|
skipFsWatcher?: boolean;
|
||
|
skipSorting?: boolean;
|
||
|
url: string;
|
||
|
useNewFolderIcon?: boolean;
|
||
|
view: FileManagerViewNames;
|
||
|
};
|
||
|
|
||
|
const FileManager: FC<FileManagerProps> = ({
|
||
|
allowMovingDraggableEntries,
|
||
|
hideFolders,
|
||
|
hideLoading,
|
||
|
hideScrolling,
|
||
|
hideShortcutIcons,
|
||
|
id,
|
||
|
isDesktop,
|
||
|
loadIconsImmediately,
|
||
|
preloadShortcuts,
|
||
|
readOnly,
|
||
|
showStatusBar,
|
||
|
skipFsWatcher,
|
||
|
skipSorting,
|
||
|
url,
|
||
|
useNewFolderIcon,
|
||
|
view,
|
||
|
}) => {
|
||
|
const [currentUrl, setCurrentUrl] = useState(url);
|
||
|
const [renaming, setRenaming] = useState("");
|
||
|
const [mounted, setMounted] = useState<boolean>(false);
|
||
|
const fileManagerRef = useRef<HTMLOListElement | null>(null);
|
||
|
const { focusedEntries, focusableEntry, ...focusFunctions } =
|
||
|
useFocusableEntries(fileManagerRef);
|
||
|
const { fileActions, files, folderActions, isLoading, updateFiles } =
|
||
|
useFolder(url, setRenaming, focusFunctions, {
|
||
|
hideFolders,
|
||
|
hideLoading,
|
||
|
preloadShortcuts,
|
||
|
skipFsWatcher,
|
||
|
skipSorting,
|
||
|
});
|
||
|
const { mountFs, rootFs, stat } = useFileSystem();
|
||
|
const { StyledFileEntry, StyledFileManager } = FileManagerViews[view];
|
||
|
const { isSelecting, selectionRect, selectionStyling, selectionEvents } =
|
||
|
useSelection(fileManagerRef);
|
||
|
const draggableEntry = useDraggableEntries(
|
||
|
focusedEntries,
|
||
|
focusFunctions,
|
||
|
fileManagerRef,
|
||
|
isSelecting,
|
||
|
allowMovingDraggableEntries
|
||
|
);
|
||
|
const fileDrop = useFileDrop({
|
||
|
callback: folderActions.newPath,
|
||
|
directory: url,
|
||
|
updatePositions: allowMovingDraggableEntries,
|
||
|
});
|
||
|
const folderContextMenu = useFolderContextMenu(url, folderActions, isDesktop);
|
||
|
const loading = (!hideLoading && isLoading) || url !== currentUrl;
|
||
|
const keyShortcuts = useFileKeyboardShortcuts(
|
||
|
files,
|
||
|
url,
|
||
|
focusedEntries,
|
||
|
setRenaming,
|
||
|
focusFunctions,
|
||
|
folderActions,
|
||
|
updateFiles,
|
||
|
fileManagerRef,
|
||
|
id,
|
||
|
view
|
||
|
);
|
||
|
const [permission, setPermission] = useState<PermissionState>("prompt");
|
||
|
const requestingPermissions = useRef(false);
|
||
|
const onKeyDown = useMemo(
|
||
|
() => (renaming === "" ? keyShortcuts() : undefined),
|
||
|
[keyShortcuts, renaming]
|
||
|
);
|
||
|
|
||
|
useEffect(() => {
|
||
|
if (
|
||
|
!requestingPermissions.current &&
|
||
|
permission !== "granted" &&
|
||
|
rootFs?.mntMap[currentUrl]?.getName() === "FileSystemAccess"
|
||
|
) {
|
||
|
requestingPermissions.current = true;
|
||
|
requestPermission(currentUrl)
|
||
|
.then((permissions) => {
|
||
|
const isGranted = permissions === "granted";
|
||
|
|
||
|
if (!permissions || isGranted) {
|
||
|
setPermission("granted");
|
||
|
|
||
|
if (isGranted) updateFiles();
|
||
|
}
|
||
|
})
|
||
|
.catch((error: Error) => {
|
||
|
if (error.message === "Permission already granted") {
|
||
|
setPermission("granted");
|
||
|
}
|
||
|
})
|
||
|
.finally(() => {
|
||
|
requestingPermissions.current = false;
|
||
|
});
|
||
|
}
|
||
|
}, [currentUrl, permission, rootFs?.mntMap, updateFiles]);
|
||
|
|
||
|
useEffect(() => {
|
||
|
if (!mounted && MOUNTABLE_EXTENSIONS.has(extname(url).toLowerCase())) {
|
||
|
const mountUrl = async (): Promise<void> => {
|
||
|
if (!(await stat(url)).isDirectory()) {
|
||
|
setMounted((currentlyMounted) => {
|
||
|
if (!currentlyMounted) {
|
||
|
mountFs(url)
|
||
|
.then(() => setTimeout(updateFiles, 100))
|
||
|
.catch(() => {
|
||
|
// Ignore race-condtion failures
|
||
|
});
|
||
|
}
|
||
|
return true;
|
||
|
});
|
||
|
}
|
||
|
};
|
||
|
|
||
|
mountUrl();
|
||
|
}
|
||
|
}, [mountFs, mounted, stat, updateFiles, url]);
|
||
|
|
||
|
useEffect(() => {
|
||
|
if (url !== currentUrl) {
|
||
|
folderActions.resetFiles();
|
||
|
setCurrentUrl(url);
|
||
|
setPermission("denied");
|
||
|
}
|
||
|
}, [currentUrl, folderActions, url]);
|
||
|
|
||
|
useEffect(() => {
|
||
|
if (!loading) fileManagerRef.current?.focus(PREVENT_SCROLL);
|
||
|
}, [loading]);
|
||
|
|
||
|
return (
|
||
|
<>
|
||
|
{loading ? (
|
||
|
<StyledLoading />
|
||
|
) : (
|
||
|
<StyledFileManager
|
||
|
ref={fileManagerRef}
|
||
|
$scrollable={!hideScrolling}
|
||
|
onKeyDown={onKeyDown}
|
||
|
{...(!readOnly && {
|
||
|
$selecting: isSelecting,
|
||
|
...fileDrop,
|
||
|
...folderContextMenu,
|
||
|
...selectionEvents,
|
||
|
})}
|
||
|
{...FOCUSABLE_ELEMENT}
|
||
|
>
|
||
|
{isSelecting && <StyledSelection style={selectionStyling} />}
|
||
|
{Object.keys(files).map((file) => (
|
||
|
<StyledFileEntry
|
||
|
key={file}
|
||
|
$selecting={isSelecting}
|
||
|
$visible={!isLoading}
|
||
|
{...(!readOnly && draggableEntry(url, file, renaming === file))}
|
||
|
{...(renaming === "" && { onKeyDown: keyShortcuts(file) })}
|
||
|
{...focusableEntry(file)}
|
||
|
>
|
||
|
<FileEntry
|
||
|
fileActions={fileActions}
|
||
|
fileManagerId={id}
|
||
|
fileManagerRef={fileManagerRef}
|
||
|
focusFunctions={focusFunctions}
|
||
|
focusedEntries={focusedEntries}
|
||
|
hideShortcutIcon={hideShortcutIcons}
|
||
|
isLoadingFileManager={isLoading}
|
||
|
loadIconImmediately={loadIconsImmediately}
|
||
|
name={basename(file, SHORTCUT_EXTENSION)}
|
||
|
path={join(url, file)}
|
||
|
readOnly={readOnly}
|
||
|
renaming={renaming === file}
|
||
|
selectionRect={selectionRect}
|
||
|
setRenaming={setRenaming}
|
||
|
stats={files[file]}
|
||
|
useNewFolderIcon={useNewFolderIcon}
|
||
|
view={view}
|
||
|
/>
|
||
|
</StyledFileEntry>
|
||
|
))}
|
||
|
</StyledFileManager>
|
||
|
)}
|
||
|
{showStatusBar && (
|
||
|
<StatusBar
|
||
|
count={loading ? 0 : Object.keys(files).length}
|
||
|
directory={url}
|
||
|
fileDrop={fileDrop}
|
||
|
selected={focusedEntries}
|
||
|
/>
|
||
|
)}
|
||
|
</>
|
||
|
);
|
||
|
};
|
||
|
|
||
|
export default FileManager;
|