ente/web/packages/gallery/components/FullScreenDropZone.tsx
Manav Rathi 3147800486
Fin
2025-02-20 14:11:40 +05:30

147 lines
4.7 KiB
TypeScript

import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator";
import CloseIcon from "@mui/icons-material/Close";
import { IconButton, Stack, styled, Typography } from "@mui/material";
import { t } from "i18next";
import React, { useCallback, useEffect, useState } from "react";
import { type FileWithPath, useDropzone } from "react-dropzone";
interface FullScreenDropZoneProps {
/**
* Optional override to the message show to the user when a drag is in
* progress.
*
* Default: t("upload_dropzone_hint")
*/
message?: string;
/**
* If `true`, then drag and drop functionality is disabled.
*/
disabled?: boolean;
/**
* Callback invoked when the user drags and drops files.
*
* It will only be called if there is at least one file in {@link files}.
*/
onDrop: (files: FileWithPath[]) => void;
}
/**
* A full screen container that accepts drag and drop of files, and also shows a
* visual overlay to the user while a drag is in progress.
*
* It can serves as the root component of the gallery pages as the container
* itself is a stack with flex 1, and so will fill all the height (and width)
* available to it. The other contents of the screen can then be placed as its
* children.
*
* It is meant to be used in tandem with "react-dropzone"; specifically, it
* requires the `getRootProps` function returned by a call to
* {@link useDropzone}.
*/
export const FullScreenDropZone: React.FC<
React.PropsWithChildren<FullScreenDropZoneProps>
> = ({ message, disabled, onDrop, children }) => {
const {
// A function to call to get the props we should apply to the container,
getRootProps,
// ... the props we should apply to the <input> element,
getInputProps,
} = useDropzone({
noClick: true,
noKeyboard: true,
disabled,
onDrop(acceptedFiles) {
setIsDropPending(false);
// Invoked the `onDrop` callback only if there is at least 1 file.
if (acceptedFiles.length) {
// Create a regular array from the readonly array returned by
// dropzone.
onDrop([...acceptedFiles]);
}
},
});
const [isDragActive, setIsDragActive] = useState(false);
const [isDropPending, setIsDropPending] = useState(false);
const handleDragEnter = useCallback(() => {
setIsDragActive(true);
}, []);
const handleOverlayDrop = useCallback(() => {
setIsDropPending(true);
setIsDragActive(false);
}, []);
const handleDragLeave = useCallback(() => {
setIsDragActive(false);
}, []);
useEffect(() => {
const handleKeydown = (event: KeyboardEvent) => {
if (event.code == "Escape" && !isDropPending) handleDragLeave();
};
window.addEventListener("keydown", handleKeydown);
return () => window.removeEventListener("keydown", handleKeydown);
}, [isDropPending, handleDragLeave]);
return (
<>
<input {...getInputProps()} />
<Stack
sx={{ flex: 1 }}
{...getRootProps({ onDragEnter: handleDragEnter })}
>
{(isDragActive || isDropPending) && (
<DropZoneOverlay
onDrop={handleOverlayDrop}
onDragLeave={handleDragLeave}
>
<CloseButton
disabled={isDropPending}
onClick={handleDragLeave}
>
<CloseIcon />
</CloseButton>
<Typography variant="h3">
{isDropPending ? (
<ActivityIndicator />
) : (
(message ?? t("upload_dropzone_hint"))
)}
</Typography>
</DropZoneOverlay>
)}
{children}
</Stack>
</>
);
};
const DropZoneOverlay = styled(Stack)(
({ theme }) => `
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
outline: none;
justify-content: center;
align-items: center;
transition: border 0.24s ease-in-out;
border-width: 5px;
border-style: solid;
border-color: ${theme.vars.palette.accent.light};
background-color: ${theme.vars.palette.backdrop.base};
backdrop-filter: blur(10px);
z-index: 2000; /* aboveFileViewerContentZ + delta */
`,
);
const CloseButton = styled(IconButton)`
position: absolute;
top: 10px;
right: 10px;
`;