mirror of
https://github.com/cloudreve/frontend.git
synced 2025-12-26 04:02:47 +00:00
215 lines
6.4 KiB
TypeScript
215 lines
6.4 KiB
TypeScript
import { CheckCircle } from "@mui/icons-material";
|
|
import { Box, Fade, IconButton, ImageListItem, ImageListItemBar, lighten, styled } from "@mui/material";
|
|
import React, { memo, useCallback, useEffect, useState } from "react";
|
|
import { TransitionGroup } from "react-transition-group";
|
|
import { FileType, Metadata } from "../../../../api/explorer.ts";
|
|
import { useAppDispatch } from "../../../../redux/hooks.ts";
|
|
import { fileIconClicked, loadFileThumb } from "../../../../redux/thunks/file.ts";
|
|
import { navigateReconcile } from "../../../../redux/thunks/filemanager.ts";
|
|
import CheckUnchecked from "../../../Icons/CheckUnchecked.tsx";
|
|
import { FileBlockProps } from "../Explorer.tsx";
|
|
import FileIcon from "../FileIcon.tsx";
|
|
import {
|
|
LargeIconContainer,
|
|
ThumbBox,
|
|
ThumbBoxContainer,
|
|
ThumbLoadingPlaceholder,
|
|
useFileBlockState,
|
|
} from "../GridView/GridFile.tsx";
|
|
|
|
const StyledImageListItem = styled(ImageListItem)<{
|
|
transparent?: boolean;
|
|
disabled?: boolean;
|
|
isDropOver?: boolean;
|
|
}>(({ transparent, isDropOver, disabled, theme }) => {
|
|
return {
|
|
opacity: transparent || disabled ? 0.5 : 1,
|
|
pointerEvents: disabled ? "none" : "auto",
|
|
cursor: "pointer",
|
|
boxShadow: isDropOver ? `0 0 0 2px ${theme.palette.primary.light}` : "none",
|
|
transition: theme.transitions.create(["height", "width", "opacity", "box-shadow"]),
|
|
};
|
|
});
|
|
|
|
const GalleryImage = memo((props: FileBlockProps) => {
|
|
const { file, columns, search, isDragging, isDropOver } = props;
|
|
const dispatch = useAppDispatch();
|
|
|
|
const {
|
|
fmIndex,
|
|
isSelected,
|
|
isLoadingIndicator,
|
|
noThumb,
|
|
uploading,
|
|
ref,
|
|
inView,
|
|
showLock,
|
|
fileTag,
|
|
onClick,
|
|
onDoubleClicked,
|
|
hoverStateOff,
|
|
hoverStateOn,
|
|
onContextMenu,
|
|
setRefFunc,
|
|
disabled,
|
|
fileDisabled,
|
|
} = useFileBlockState(props);
|
|
|
|
const [hovered, setHovered] = useState(false);
|
|
|
|
// undefined: not loaded, null: no thumb
|
|
const [thumbSrc, setThumbSrc] = useState<string | undefined | null>(noThumb ? null : undefined);
|
|
const [imageLoading, setImageLoading] = useState(true);
|
|
|
|
const tryLoadThumbSrc = useCallback(async () => {
|
|
const thumbSrc = await dispatch(loadFileThumb(0, file));
|
|
setThumbSrc(thumbSrc);
|
|
}, [dispatch, file, setThumbSrc, setImageLoading]);
|
|
|
|
const onImgLoadError = useCallback(() => {
|
|
setImageLoading(false);
|
|
setThumbSrc(null);
|
|
}, [setImageLoading, setThumbSrc]);
|
|
|
|
useEffect(() => {
|
|
if (!inView) {
|
|
return;
|
|
}
|
|
|
|
if (isLoadingIndicator) {
|
|
if (file.first) {
|
|
dispatch(navigateReconcile(fmIndex, { next_page: true }));
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (file.type == FileType.folder) {
|
|
return;
|
|
}
|
|
|
|
if ((file.metadata && file.metadata[Metadata.thumbDisabled] !== undefined) || showLock) {
|
|
// No thumb available
|
|
setThumbSrc(null);
|
|
return;
|
|
}
|
|
|
|
tryLoadThumbSrc();
|
|
}, [inView]);
|
|
|
|
const onIconClick = useCallback(
|
|
(e: React.MouseEvent<HTMLElement>) => {
|
|
e.stopPropagation();
|
|
return dispatch(fileIconClicked(fmIndex, file, e));
|
|
},
|
|
[file, dispatch],
|
|
);
|
|
|
|
return (
|
|
<StyledImageListItem
|
|
onClick={file.type == FileType.folder ? onClick : onDoubleClicked}
|
|
transparent={isDragging || fileDisabled}
|
|
isDropOver={isDropOver && !isDragging}
|
|
disabled={disabled}
|
|
onContextMenu={onContextMenu}
|
|
ref={setRefFunc}
|
|
onMouseEnter={() => setHovered(true)}
|
|
onMouseLeave={() => setHovered(false)}
|
|
>
|
|
<TransitionGroup style={{ height: "100%" }}>
|
|
{thumbSrc && (
|
|
<Fade key={"image"}>
|
|
<Box>
|
|
<ThumbBoxContainer
|
|
sx={{
|
|
p: isSelected ? "10%" : 0,
|
|
backgroundColor: (theme) => (isSelected ? lighten(theme.palette.primary.light, 0.85) : "initial"),
|
|
transition: (theme) =>
|
|
theme.transitions.create(["padding"], {
|
|
duration: theme.transitions.duration.shortest,
|
|
}),
|
|
}}
|
|
>
|
|
<ThumbBox
|
|
sx={{
|
|
borderRadius: isSelected ? 1 : 0,
|
|
}}
|
|
loaded={!imageLoading}
|
|
src={thumbSrc}
|
|
onLoad={() => setImageLoading(false)}
|
|
onError={onImgLoadError}
|
|
/>
|
|
</ThumbBoxContainer>
|
|
</Box>
|
|
</Fade>
|
|
)}
|
|
{(thumbSrc === undefined || (thumbSrc && imageLoading)) && (
|
|
<Fade key={"loading"}>
|
|
<ThumbLoadingPlaceholder
|
|
sx={{
|
|
borderRadius: 0,
|
|
}}
|
|
ref={isLoadingIndicator ? undefined : ref}
|
|
variant={"rectangular"}
|
|
height={"100%"}
|
|
/>
|
|
</Fade>
|
|
)}
|
|
{thumbSrc === null && (
|
|
<Fade key={"icon"}>
|
|
<LargeIconContainer>
|
|
<FileIcon
|
|
variant={"largeMobile"}
|
|
iconProps={{
|
|
sx: {
|
|
fontSize: "48px",
|
|
height: "64px",
|
|
width: "64px",
|
|
},
|
|
}}
|
|
file={file}
|
|
loading={isLoadingIndicator}
|
|
/>
|
|
</LargeIconContainer>
|
|
</Fade>
|
|
)}
|
|
</TransitionGroup>
|
|
<Fade in={!isLoadingIndicator && (hovered || !!isSelected)}>
|
|
<ImageListItemBar
|
|
sx={{
|
|
background: "linear-gradient(to bottom, rgba(0,0,0,0.7) 0%, " + "rgba(0,0,0,0.3) 50%, rgba(0,0,0,0) 100%)",
|
|
}}
|
|
position="top"
|
|
actionIcon={
|
|
<IconButton onClick={onIconClick} size={"small"} sx={{ color: "white", mb: 1 }}>
|
|
<TransitionGroup
|
|
style={{
|
|
width: 20,
|
|
height: 20,
|
|
}}
|
|
>
|
|
{!isSelected && (
|
|
<Fade>
|
|
<Box sx={{ position: "absolute" }}>
|
|
<CheckUnchecked fontSize={"small"} />
|
|
</Box>
|
|
</Fade>
|
|
)}
|
|
{isSelected && (
|
|
<Fade>
|
|
<Box sx={{ position: "absolute" }}>
|
|
<CheckCircle fontSize={"small"} />
|
|
</Box>
|
|
</Fade>
|
|
)}
|
|
</TransitionGroup>
|
|
</IconButton>
|
|
}
|
|
actionPosition="left"
|
|
/>
|
|
</Fade>
|
|
</StyledImageListItem>
|
|
);
|
|
});
|
|
|
|
export default GalleryImage;
|