374 lines
12 KiB
TypeScript
374 lines
12 KiB
TypeScript
import { memo, useCallback, useEffect, useRef, useState } from "react";
|
|
import { useAppDispatch, useAppSelector } from "../../hooks/store";
|
|
import { deleteVideoTokenAPI, getVideoTokenAPI } from "../../api/filesAPI";
|
|
import CloseIcon from "../../icons/CloseIcon";
|
|
import ActionsIcon from "../../icons/ActionsIcon";
|
|
import { useContextMenu } from "../../hooks/contextMenu";
|
|
import ContextMenu from "../ContextMenu/ContextMenu";
|
|
import {
|
|
resetPopupSelect,
|
|
setMainSelect,
|
|
setPopupSelect,
|
|
} from "../../reducers/selected";
|
|
import CircleLeftIcon from "../../icons/CircleLeftIcon";
|
|
import CircleRightIcon from "../../icons/CircleRightIcon";
|
|
import { useFiles, useQuickFiles } from "../../hooks/files";
|
|
import { FileInterface } from "../../types/file";
|
|
import { InfiniteData } from "react-query";
|
|
import { getFileColor, getFileExtension } from "../../utils/files";
|
|
import Spinner from "../Spinner/Spinner";
|
|
import { toast } from "react-toastify";
|
|
import getBackendURL from "../../utils/getBackendURL";
|
|
import classNames from "classnames";
|
|
|
|
interface PhotoViewerPopupProps {
|
|
file: FileInterface;
|
|
}
|
|
|
|
const PhotoViewerPopup: React.FC<PhotoViewerPopupProps> = memo((props) => {
|
|
const { file } = props;
|
|
const [video, setVideo] = useState("");
|
|
const [isVideoLoading, setIsVideoLoading] = useState(false);
|
|
const [isThumbnailLoading, setIsThumbnailLoading] = useState(
|
|
file.metadata.hasThumbnail && !file.metadata.isVideo
|
|
);
|
|
const [thumbnailError, setThumbnailError] = useState(false);
|
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
|
const type = useAppSelector((state) => state.selected.popupModal.type)!;
|
|
const thumbnailURL = `${getBackendURL()}/file-service/full-thumbnail/${
|
|
file._id
|
|
}`;
|
|
const finalLastPageLoaded = useRef(false);
|
|
const loadingNextPage = useRef(false);
|
|
const { data: quickFiles } = useQuickFiles(false);
|
|
const { data: files, fetchNextPage } = useFiles(false);
|
|
const dispatch = useAppDispatch();
|
|
const {
|
|
onContextMenu,
|
|
closeContextMenu,
|
|
onTouchStart,
|
|
onTouchMove,
|
|
onTouchEnd,
|
|
clickStopPropagation,
|
|
...contextMenuState
|
|
} = useContextMenu();
|
|
|
|
const fileExtension = getFileExtension(file.filename, 3);
|
|
|
|
const imageColor = getFileColor(file.filename);
|
|
|
|
const getVideo = useCallback(async () => {
|
|
try {
|
|
setIsVideoLoading(true);
|
|
setVideo("");
|
|
await getVideoTokenAPI();
|
|
const videoURL = `${getBackendURL()}/file-service/stream-video/${
|
|
file._id
|
|
}`;
|
|
setVideo(videoURL);
|
|
setIsVideoLoading(false);
|
|
} catch (e) {
|
|
console.log("Error getting video", e);
|
|
toast.error("Error getting video");
|
|
}
|
|
}, [file._id]);
|
|
|
|
const cleanUpVideo = useCallback(async () => {
|
|
if (!file.metadata.isVideo || !videoRef.current) return;
|
|
|
|
deleteVideoTokenAPI();
|
|
|
|
videoRef.current.pause();
|
|
videoRef.current.src = "";
|
|
setVideo("");
|
|
}, [file._id, deleteVideoTokenAPI]);
|
|
|
|
const findPrevFilesItem = (newFiles?: InfiniteData<FileInterface[]>) => {
|
|
if (newFiles) {
|
|
if (!newFiles?.pages) return 0;
|
|
const filesFiltered = newFiles.pages
|
|
.flat()
|
|
.filter(
|
|
(currentFile) =>
|
|
currentFile.metadata.hasThumbnail || currentFile.metadata.isVideo
|
|
);
|
|
const index = filesFiltered.findIndex(
|
|
(currentFile) => currentFile._id === file._id
|
|
);
|
|
const prevItem = filesFiltered[index - 1];
|
|
return prevItem;
|
|
} else {
|
|
if (!files?.pages) return 0;
|
|
const filesFiltered = files.pages
|
|
.flat()
|
|
.filter(
|
|
(currentFile) =>
|
|
currentFile.metadata.hasThumbnail || currentFile.metadata.isVideo
|
|
);
|
|
const index = filesFiltered.findIndex(
|
|
(currentFile) => currentFile._id === file._id
|
|
);
|
|
const prevItem = filesFiltered[index - 1];
|
|
return prevItem;
|
|
}
|
|
};
|
|
|
|
const goToPreviousItem = async () => {
|
|
if (type === "quick-item") {
|
|
if (!quickFiles?.length) return 0;
|
|
const filteredQuickFiles = quickFiles.filter(
|
|
(currentFile) =>
|
|
currentFile.metadata.hasThumbnail || currentFile.metadata.isVideo
|
|
);
|
|
const index = filteredQuickFiles.findIndex(
|
|
(currentFile) => currentFile._id === file._id
|
|
);
|
|
const prevItem = filteredQuickFiles[index - 1];
|
|
if (prevItem) {
|
|
dispatch(setPopupSelect({ type: "quick-item", file: prevItem }));
|
|
dispatch(
|
|
setMainSelect({
|
|
file: prevItem,
|
|
id: prevItem._id,
|
|
type: "file",
|
|
folder: null,
|
|
})
|
|
);
|
|
}
|
|
} else {
|
|
if (!files?.pages) return 0;
|
|
const prevItem = findPrevFilesItem();
|
|
if (prevItem) {
|
|
dispatch(setPopupSelect({ type: "file", file: prevItem }));
|
|
dispatch(
|
|
setMainSelect({
|
|
file: prevItem,
|
|
id: prevItem._id,
|
|
type: "file",
|
|
folder: null,
|
|
})
|
|
);
|
|
}
|
|
// TODO: Perhaps implement this if needed in the future
|
|
// else {
|
|
// console.log("fetch prev");
|
|
// const response = await fetchPreviousPage();
|
|
// if (!response.data?.pages) return;
|
|
// const fetchedPrevItem = findPrevFilesItem(response.data);
|
|
// if (fetchedPrevItem) {
|
|
// dispatch(setPopupSelect({ type: "file", file: fetchedPrevItem }));
|
|
// }
|
|
// }
|
|
}
|
|
};
|
|
|
|
const findNextFilesItem = (newFiles?: InfiniteData<FileInterface[]>) => {
|
|
if (newFiles) {
|
|
if (!newFiles?.pages) return 0;
|
|
const filesFiltered = newFiles.pages
|
|
.flat()
|
|
.filter(
|
|
(currentFile) =>
|
|
currentFile.metadata.hasThumbnail || currentFile.metadata.isVideo
|
|
);
|
|
const index = filesFiltered.findIndex(
|
|
(currentFile) => currentFile._id === file._id
|
|
);
|
|
const nextItem = filesFiltered[index + 1];
|
|
return nextItem;
|
|
} else {
|
|
if (!files?.pages) return 0;
|
|
const filesFiltered = files.pages
|
|
.flat()
|
|
.filter(
|
|
(currentFile) =>
|
|
currentFile.metadata.hasThumbnail || currentFile.metadata.isVideo
|
|
);
|
|
const index = filesFiltered.findIndex(
|
|
(currentFile) => currentFile._id === file._id
|
|
);
|
|
const nextItem = filesFiltered[index + 1];
|
|
return nextItem;
|
|
}
|
|
};
|
|
|
|
const goToNextItem = async () => {
|
|
if (type === "quick-item") {
|
|
if (!quickFiles?.length) return;
|
|
const filteredQuickFiles = quickFiles.filter(
|
|
(currentFile) =>
|
|
currentFile.metadata.hasThumbnail || currentFile.metadata.isVideo
|
|
);
|
|
const index = filteredQuickFiles.findIndex(
|
|
(currentFile) => currentFile._id === file._id
|
|
);
|
|
const nextItem = filteredQuickFiles[index + 1];
|
|
if (nextItem) {
|
|
dispatch(setPopupSelect({ type: "quick-item", file: nextItem }));
|
|
dispatch(
|
|
setMainSelect({
|
|
file: nextItem,
|
|
id: nextItem._id,
|
|
type: "file",
|
|
folder: null,
|
|
})
|
|
);
|
|
}
|
|
} else {
|
|
if (!files?.pages) return;
|
|
const nextItem = findNextFilesItem();
|
|
if (nextItem) {
|
|
dispatch(setPopupSelect({ type: "file", file: nextItem }));
|
|
dispatch(
|
|
setMainSelect({
|
|
file: nextItem,
|
|
id: nextItem._id,
|
|
type: "file",
|
|
folder: null,
|
|
})
|
|
);
|
|
} else if (!finalLastPageLoaded.current && !loadingNextPage.current) {
|
|
loadingNextPage.current = true;
|
|
console.log("fetch next page");
|
|
const newFilesResponse = await fetchNextPage();
|
|
console.log("new files response", newFilesResponse);
|
|
if (!newFilesResponse.data?.pages) return;
|
|
const fetchedNextItem = findNextFilesItem(newFilesResponse.data);
|
|
console.log("fetched next item", fetchedNextItem);
|
|
if (fetchedNextItem) {
|
|
dispatch(setPopupSelect({ type: "file", file: fetchedNextItem }));
|
|
dispatch(
|
|
setMainSelect({
|
|
file: fetchedNextItem,
|
|
id: fetchedNextItem._id,
|
|
type: "file",
|
|
folder: null,
|
|
})
|
|
);
|
|
} else {
|
|
finalLastPageLoaded.current = true;
|
|
}
|
|
loadingNextPage.current = false;
|
|
console.log("end");
|
|
}
|
|
}
|
|
};
|
|
|
|
const closePhotoViewer = () => {
|
|
dispatch(resetPopupSelect());
|
|
};
|
|
|
|
const outterWrapperClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
console.log("outter wrapper click", e.target);
|
|
if ((e.target as HTMLDivElement).id !== "outer-wrapper") {
|
|
return;
|
|
}
|
|
closePhotoViewer();
|
|
};
|
|
|
|
useEffect(() => {
|
|
console.log("useeffect");
|
|
if (file.metadata.isVideo) {
|
|
getVideo();
|
|
}
|
|
|
|
return () => {
|
|
cleanUpVideo();
|
|
};
|
|
}, [file.metadata.isVideo, getVideo, cleanUpVideo]);
|
|
|
|
return (
|
|
<div
|
|
id="outer-wrapper"
|
|
className="w-screen dynamic-height bg-black bg-opacity-80 absolute top-0 left-0 right-0 bottom-0 z-50 flex justify-center items-center flex-col"
|
|
onClick={outterWrapperClick}
|
|
>
|
|
{contextMenuState.selected && (
|
|
<div onClick={clickStopPropagation}>
|
|
<ContextMenu
|
|
quickItemMode={false}
|
|
contextSelected={contextMenuState}
|
|
closeContext={closeContextMenu}
|
|
file={file}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<div
|
|
className="absolute top-[20px] flex justify-between w-full"
|
|
id="actions-wrapper"
|
|
>
|
|
<div className="ml-4 flex items-center">
|
|
<span className="inline-flex items-center mr-[15px] max-w-[27px] min-w-[27px] min-h-[27px] max-h-[27px]">
|
|
<div
|
|
className="h-[27px] w-[27px] bg-red-500 rounded-[3px] flex flex-row justify-center items-center"
|
|
style={{ background: imageColor }}
|
|
>
|
|
<span className="font-semibold text-[9.5px] text-white select-none">
|
|
{fileExtension}
|
|
</span>
|
|
</div>
|
|
</span>
|
|
<p className="text-md text-white text-ellipsis overflow-hidden max-w-[200px] md:max-w-[600px] whitespace-nowrap select-none">
|
|
{file.filename}
|
|
</p>
|
|
</div>
|
|
<div className="flex mr-4">
|
|
<div onClick={onContextMenu} id="action-context-wrapper">
|
|
<ActionsIcon
|
|
className="pointer text-white w-[20px] h-[25px] mr-4 cursor-pointer hover:text-white-hover"
|
|
id="action-context-icon"
|
|
/>
|
|
</div>
|
|
|
|
<div onClick={closePhotoViewer} id="action-close-wrapper">
|
|
<CloseIcon
|
|
className="pointer text-white w-[25px] h-[25px] cursor-pointer hover:text-white-hover"
|
|
id="action-close-icon"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<CircleLeftIcon
|
|
onClick={goToPreviousItem}
|
|
className="fixed left-2 pointer text-white w-[45px] h-[45px] desktopMode:w-[30px] desktopMode:h-[30px] select-none cursor-pointer hover:text-white-hover"
|
|
/>
|
|
<CircleRightIcon
|
|
onClick={goToNextItem}
|
|
className="fixed right-2 pointer text-white w-[45px] h-[45px] desktopMode:w-[30px] desktopMode:h-[30px] select-none cursor-pointer hover:text-white-hover"
|
|
/>
|
|
<div className="max-w-[80vw] max-h-[80vh] flex justify-center items-center z-10">
|
|
{isThumbnailLoading && !thumbnailError && <Spinner />}
|
|
{!file.metadata.isVideo && (
|
|
<img
|
|
src={thumbnailURL}
|
|
className={classNames(
|
|
"max-w-full max-h-full object-contain select-none",
|
|
{
|
|
hidden: isThumbnailLoading,
|
|
}
|
|
)}
|
|
onLoad={() => setIsThumbnailLoading(false)}
|
|
onError={() => setThumbnailError(true)}
|
|
/>
|
|
)}
|
|
{thumbnailError && (
|
|
<div className="bg-white p-6 rounded-md">
|
|
<p className="text-center text-sm">Error loading image</p>
|
|
</div>
|
|
)}
|
|
{file.metadata.isVideo && !isVideoLoading && (
|
|
<video
|
|
src={video}
|
|
ref={videoRef}
|
|
className="max-w-full max-h-full object-contain"
|
|
controls
|
|
></video>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
});
|
|
|
|
export default PhotoViewerPopup;
|