import { SyntheticEvent, useEffect, useState, useCallback, useRef, FormEvent, } from "react" import _ from "lodash" import PhotoAlbum from "react-photo-album" import { BarsArrowDownIcon, BarsArrowUpIcon } from "@heroicons/react/24/outline" import { MagnifyingGlassIcon, ViewHorizontalIcon, ViewGridIcon, } from "@radix-ui/react-icons" import { useToggle } from "react-use" import { useDebounce } from "@uidotdev/usehooks" import Fuse from "fuse.js" import { useToast } from "@/components/ui/use-toast" import { API_ENDPOINT, getMedias } from "@/lib/api" import { IconButton } from "./ui/button" import { Input } from "./ui/input" import { Dialog, DialogContent, DialogTitle } from "./ui/dialog" import { Tabs, TabsList, TabsTrigger } from "./ui/tabs" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "./ui/select" import { ScrollArea } from "./ui/scroll-area" import { DialogTrigger } from "@radix-ui/react-dialog" import { useStore } from "@/lib/states" import { Filename, SortBy, SortOrder } from "@/lib/types" import { FolderClosed } from "lucide-react" import useHotKey from "@/hooks/useHotkey" interface Photo { src: string height: number width: number name: string } const SORT_BY_NAME = "Name" const SORT_BY_CREATED_TIME = "Created time" const SORT_BY_MODIFIED_TIME = "Modified time" const IMAGE_TAB = "input" const OUTPUT_TAB = "output" const SortByMap = { [SortBy.NAME]: SORT_BY_NAME, [SortBy.CTIME]: SORT_BY_CREATED_TIME, [SortBy.MTIME]: SORT_BY_MODIFIED_TIME, } interface Props { onPhotoClick(tab: string, filename: string): void photoWidth: number } export default function FileManager(props: Props) { const { onPhotoClick, photoWidth } = props const [open, toggleOpen] = useToggle(false) const [fileManagerState, updateFileManagerState] = useStore((state) => [ state.fileManagerState, state.updateFileManagerState, ]) const { toast } = useToast() const [scrollTop, setScrollTop] = useState(0) const [closeScrollTop, setCloseScrollTop] = useState(0) const ref = useRef(null) const debouncedSearchText = useDebounce(fileManagerState.searchText, 300) const [tab, setTab] = useState(IMAGE_TAB) const [photos, setPhotos] = useState([]) const [photoIndex, setPhotoIndex] = useState(0) useHotKey("f", () => { toggleOpen() }) useHotKey( "left", () => { let newIndex = photoIndex if (photoIndex > 0) { newIndex = photoIndex - 1 } setPhotoIndex(newIndex) onPhotoClick(tab, photos[newIndex].name) }, [photoIndex, photos] ) useHotKey( "right", () => { let newIndex = photoIndex if (photoIndex < photos.length - 1) { newIndex = photoIndex + 1 } setPhotoIndex(newIndex) onPhotoClick(tab, photos[newIndex].name) }, [photoIndex, photos] ) useEffect(() => { if (!open) { setCloseScrollTop(scrollTop) } }, [open, scrollTop]) const onRefChange = useCallback( (node: HTMLDivElement) => { if (node !== null) { if (open) { setTimeout(() => { // TODO: without timeout, scrollTo not work, why? node.scrollTo({ top: closeScrollTop, left: 0 }) }, 100) } } }, [open, closeScrollTop] ) useEffect(() => { if (!open) { return } const fetchData = async () => { try { const filenames = await getMedias(tab) let filteredFilenames = filenames if (debouncedSearchText) { const fuse = new Fuse(filteredFilenames, { keys: ["name"], }) const items = fuse.search(debouncedSearchText) filteredFilenames = items.map( (item) => filteredFilenames[item.refIndex] ) } filteredFilenames = _.orderBy( filteredFilenames, fileManagerState.sortBy, fileManagerState.sortOrder ) const newPhotos = filteredFilenames.map((filename: Filename) => { const width = photoWidth const height = filename.height * (width / filename.width) const src = `${API_ENDPOINT}/media_thumbnail_file?tab=${tab}&filename=${encodeURIComponent( filename.name )}&width=${Math.ceil(width)}&height=${Math.ceil(height)}` return { src, height, width, name: filename.name } }) setPhotos(newPhotos) } catch (e: any) { toast({ variant: "destructive", title: "Uh oh! Something went wrong.", description: e.message ? e.message : e.toString(), }) } } fetchData() }, [tab, debouncedSearchText, fileManagerState, photoWidth, open]) const onScroll = (event: SyntheticEvent) => { setScrollTop(event.currentTarget.scrollTop) } const onClick = ({ index }: { index: number }) => { toggleOpen() setPhotoIndex(index) onPhotoClick(tab, photos[index].name) } const renderTitle = () => { return (
{`Images (${photos.length})`}
{ updateFileManagerState({ layout: "rows" }) }} > { updateFileManagerState({ layout: "masonry" }) }} >
) } return ( {renderTitle()}
) => { evt.preventDefault() evt.stopPropagation() const target = evt.target as HTMLInputElement updateFileManagerState({ searchText: target.value }) }} placeholder="Search by file name" />
setTab(val)}> Image Directory Output Directory
{fileManagerState.sortOrder === SortOrder.DESCENDING ? ( { updateFileManagerState({ sortOrder: SortOrder.ASCENDING }) }} > ) : ( { updateFileManagerState({ sortOrder: SortOrder.DESCENDING }) }} > )}
) }