first commit

This commit is contained in:
2025-10-19 13:31:11 +00:00
commit 8bfc183b66
1248 changed files with 195992 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
import { useDrop } from "react-dnd";
import { useEffect } from "react";
const DisableDropDelay = () => {
const [_, bodyDropRef] = useDrop(() => ({
accept: "file",
drop: () => {
// do something
},
}));
useEffect(() => {
bodyDropRef(document.body);
return () => {
bodyDropRef(null);
};
}, []);
};
export default DisableDropDelay;

View File

@@ -0,0 +1,151 @@
import { memo, useCallback, useContext, useEffect } from "react";
import { useDrag, useDrop } from "react-dnd";
import { getEmptyImage } from "react-dnd-html5-backend";
import { FileResponse, FileType } from "../../../api/explorer.ts";
import { setDragging } from "../../../redux/globalStateSlice.ts";
import { useAppDispatch, useAppSelector } from "../../../redux/hooks.ts";
import { processDnd } from "../../../redux/thunks/file.ts";
import { getFileLinkedUri, mergeRefs } from "../../../util";
import { useTheme } from "@mui/material/styles";
import useMediaQuery from "@mui/material/useMediaQuery";
import CrUri, { Filesystem } from "../../../util/uri.ts";
import { FileBlockProps } from "../Explorer/Explorer.tsx";
import { FileManagerIndex } from "../FileManager.tsx";
import { FmIndexContext } from "../FmIndexContext.tsx";
export interface DragItem {
target: FileResponse;
includeSelected?: boolean;
}
export interface DropResult {
dropEffect: string;
uri?: string;
}
export const DropEffect = {
copy: "copy",
move: "move",
};
export interface UseFileDragProps {
file?: FileResponse;
includeSelected?: boolean;
dropUri?: string;
}
export const NoOpDropUri = "noop";
export const useFileDrag = ({ file, includeSelected, dropUri }: UseFileDragProps) => {
const dispatch = useAppDispatch();
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down("md"));
const fmIndex = useContext(FmIndexContext);
// const { addEventListenerForWindow, removeEventListenerForWindow } =
// useDragScrolling(["#" + MainExplorerContainerID]);
// @ts-ignore
const [{ isDragging }, drag, preview] = useDrag({
type: "file",
item: {
target: file,
includeSelected,
},
end: (item, monitor) => {
// Ignore NoOpDropUri
const target = monitor.getDropResult<DropResult>();
if (!item || !target || !target.uri || target.uri == NoOpDropUri) {
return;
}
dispatch(processDnd(0, item as DragItem, target));
},
canDrag: () => {
if (!file || fmIndex == FileManagerIndex.selector || isTablet) {
return false;
}
const crUri = new CrUri(file.path);
return file.owned && crUri.fs() != Filesystem.share;
},
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
const [{ canDrop, isOver }, drop] = useDrop({
accept: "file",
drop: () => (file ? { uri: getFileLinkedUri(file) } : { uri: dropUri }),
collect: (monitor) => ({
isOver: monitor.isOver(),
canDrop: monitor.canDrop(),
}),
canDrop: (item, _monitor) => {
const dropExist = !!dropUri || (!!file && file.type == FileType.folder);
if (!dropExist || fmIndex == FileManagerIndex.selector) {
return false;
}
if (!item) {
return false;
}
return true;
},
});
const isActive = canDrop && isOver;
useEffect(() => {
preview(getEmptyImage(), { captureDraggingState: true });
// eslint-disable-next-line
}, []);
useEffect(() => {
if (isDragging) {
// addEventListenerForWindow();
}
dispatch(
setDragging({
dragging: isDragging,
draggingWithSelected: !!includeSelected,
}),
);
}, [isDragging]);
return [drag, drop, isActive, isDragging] as const;
};
export interface DndWrappedFileProps extends FileBlockProps {
component: React.MemoExoticComponent<(props: FileBlockProps) => JSX.Element>;
}
const DndWrappedFile = memo((props: DndWrappedFileProps) => {
const fmIndex = useContext(FmIndexContext);
const globalDragging = useAppSelector((state) => state.globalState.dndState);
const isSelected = useAppSelector((state) => state.fileManager[fmIndex].selected[props.file.path]);
const [drag, drop, isOver, isDragging] = useFileDrag({
file: props.file.placeholder ? undefined : props.file,
includeSelected: true,
});
const mergedRef = useCallback(
(val: any) => {
mergeRefs(drop, drag)(val);
},
[drop, drag],
);
const Component = props.component;
return (
<Component
dragRef={mergedRef}
isDropOver={isOver}
isDragging={isDragging || (!!globalDragging.dragging && !!isSelected && globalDragging.draggingWithSelected)}
{...props}
/>
);
});
export default DndWrappedFile;

View File

@@ -0,0 +1,112 @@
import { useDragLayer, XYCoord } from "react-dnd";
import { FileResponse } from "../../../api/explorer.ts";
import { Badge, Box, Paper, PaperProps } from "@mui/material";
import { useAppSelector } from "../../../redux/hooks.ts";
import { useEffect, useMemo, useState } from "react";
import { DragItem } from "./DndWrappedFile.tsx";
import DisableDropDelay from "./DisableDropDelay.tsx";
import { FileNameText, Header } from "../Explorer/GridView/GridFile.tsx";
import FileSmallIcon from "../Explorer/FileSmallIcon.tsx";
interface DragPreviewProps extends PaperProps {
files: FileResponse[];
pointerOffset: XYCoord | null;
}
const DragPreview = ({ pointerOffset, files, ...rest }: DragPreviewProps) => {
const [size, setSize] = useState([0, 0]);
useEffect(() => {
setSize([220, 48]);
}, []);
if (!files || files.length == 0) {
return undefined;
}
return (
<Badge
badgeContent={files.length <= 1 ? undefined : files.length}
color="primary"
sx={{
"& .MuiBadge-badge": { zIndex: 1612 },
transform: `translate(${pointerOffset?.x}px, ${pointerOffset?.y}px)`,
}}
>
<Paper
elevation={3}
sx={{
width: size[0],
height: size[1],
zIndex: 1610,
transition: (theme) => theme.transitions.create(["width", "height"]),
}}
{...rest}
>
<Header>
<FileSmallIcon ignoreHovered selected={false} file={files[0]} />
<FileNameText variant="body2">{files[0]?.name}</FileNameText>
</Header>
</Paper>
{[...Array(Math.min(2, files.length - 1)).keys()].map((i) => (
<Paper
sx={{
position: "absolute",
width: size[0],
height: size[1],
zIndex: 1610 - i - 1,
top: (i + 1) * 4,
left: (i + 1) * 4,
transition: (theme) => theme.transitions.create(["width", "height"]),
}}
elevation={3}
/>
))}
</Badge>
);
};
const DragLayer = () => {
DisableDropDelay();
const { itemType, isDragging, item, pointerOffset } = useDragLayer((monitor) => ({
item: monitor.getItem(),
itemType: monitor.getItemType(),
pointerOffset: monitor.getClientOffset(),
isDragging: monitor.isDragging(),
}));
const selected = useAppSelector((state) => state.fileManager[0].selected);
const draggingFiles = useMemo(() => {
if (item && (item as DragItem) && item.target) {
const selectedList = item.includeSelected
? Object.keys(selected)
.map((key) => selected[key])
.filter((x) => x.path != item.target.path)
: [];
return [item.target, ...selectedList];
}
return [];
}, [selected, item]);
if (!isDragging) {
return null;
}
return (
<Box
sx={{
position: "fixed",
pointerEvents: "none",
zIndex: 1600,
left: 0,
top: 0,
width: "100%",
height: "100%",
}}
>
<DragPreview files={draggingFiles} pointerOffset={pointerOffset} />
</Box>
);
};
export default DragLayer;

View File

@@ -0,0 +1,72 @@
import { useRef } from "react";
import { throttle } from "lodash";
const threshold = 0.1;
const useDragScrolling = (containers: string[]) => {
const isScrolling = useRef(false);
const targets = containers.map((id) => document.querySelector(id) as HTMLElement);
const rects = useRef<DOMRect[]>([]);
const goDown = (target: HTMLElement) => {
return () => {
target.scrollTop += 5;
const { offsetHeight, scrollTop, scrollHeight } = target;
const isScrollEnd = offsetHeight + scrollTop >= scrollHeight;
if (isScrolling.current && !isScrollEnd) {
window.requestAnimationFrame(goDown(target));
}
};
};
const goUp = (target: HTMLElement) => {
return () => {
target.scrollTop -= 5;
if (isScrolling.current && target.scrollTop > 0) {
window.requestAnimationFrame(goUp(target));
}
};
};
const onDragOver = (event: MouseEvent) => {
// detect if mouse is in any rect
rects.current.forEach((rect, index) => {
if (event.clientX < rect.left || event.clientX > rect.right) {
isScrolling.current = false;
return;
}
const height = rect.bottom - rect.top;
if (event.clientY > rect.top && event.clientY < rect.top + threshold * height) {
isScrolling.current = true;
window.requestAnimationFrame(goUp(targets[index]));
} else if (event.clientY < rect.bottom && event.clientY > rect.bottom - threshold * height) {
isScrolling.current = true;
window.requestAnimationFrame(goDown(targets[index]));
} else {
isScrolling.current = false;
}
});
};
const throttleOnDragOver = throttle(onDragOver, 300);
const addEventListenerForWindow = () => {
rects.current = targets.map((t) => t.getBoundingClientRect());
window.addEventListener("dragover", throttleOnDragOver, false);
};
const removeEventListenerForWindow = () => {
window.removeEventListener("dragover", throttleOnDragOver, false);
isScrolling.current = false;
};
return {
addEventListenerForWindow,
removeEventListenerForWindow,
};
};
export default useDragScrolling;