Files
myDrive/src/components/PhotoViewerPopup/PhotoViewerPopup.tsx
T
2025-01-10 09:55:34 -05:00

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;