Files
IOPaint/lama_cleaner/app/src/components/FileManager/FileManager.tsx
2023-03-21 12:31:31 +08:00

287 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, {
SyntheticEvent,
useEffect,
useState,
useCallback,
useRef,
FormEvent,
} from 'react'
import _ from 'lodash'
import * as Tabs from '@radix-ui/react-tabs'
import { useRecoilState, useSetRecoilState } from 'recoil'
import PhotoAlbum from 'react-photo-album'
import { BarsArrowDownIcon, BarsArrowUpIcon } from '@heroicons/react/24/outline'
import { MagnifyingGlassIcon } from '@radix-ui/react-icons'
import { useDebounce } from 'react-use'
import { Id, Index, IndexSearchResult } from 'flexsearch'
import * as ScrollArea from '@radix-ui/react-scroll-area'
import Modal from '../shared/Modal'
import Flex from '../shared/Layout'
import {
fileManagerLayout,
fileManagerSortBy,
fileManagerSortOrder,
SortBy,
SortOrder,
toastState,
} from '../../store/Atoms'
import { getMedias } from '../../adapters/inpainting'
import Selector from '../shared/Selector'
import Button from '../shared/Button'
import TextInput from '../shared/Input'
interface Photo {
src: string
height: number
width: number
name: string
}
interface Filename {
name: string
height: number
width: number
ctime: number
mtime: number
}
const SORT_BY_NAME = 'Name'
const SORT_BY_CREATED_TIME = 'Created time'
const SORT_BY_MODIFIED_TIME = 'Modified time'
const IMAGE_TAB = 'image'
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 {
show: boolean
onClose: () => void
onPhotoClick(tab: string, filename: string): void
photoWidth: number
}
export default function FileManager(props: Props) {
const { show, onClose, onPhotoClick, photoWidth } = props
const [scrollTop, setScrollTop] = useState(0)
const [closeScrollTop, setCloseScrollTop] = useState(0)
const setToastState = useSetRecoilState(toastState)
const [sortBy, setSortBy] = useRecoilState<SortBy>(fileManagerSortBy)
const [sortOrder, setSortOrder] = useRecoilState(fileManagerSortOrder)
const [layout, setLayout] = useRecoilState(fileManagerLayout)
const ref = useRef(null)
const [searchText, setSearchText] = useState('')
const [debouncedSearchText, setDebouncedSearchText] = useState('')
const [tab, setTab] = useState(IMAGE_TAB)
const [photos, setPhotos] = useState<Photo[]>([])
const [, cancel] = useDebounce(
() => {
setDebouncedSearchText(searchText)
},
500,
[searchText]
)
useEffect(() => {
if (!show) {
setCloseScrollTop(scrollTop)
}
}, [show, scrollTop])
const onRefChange = useCallback(
(node: HTMLDivElement) => {
if (node !== null) {
if (show) {
setTimeout(() => {
// TODO: without timeout, scrollTo not work, why?
node.scrollTo({ top: closeScrollTop, left: 0 })
}, 100)
}
}
},
[show, closeScrollTop]
)
useEffect(() => {
if (!show) {
return
}
const fetchData = async () => {
try {
const filenames = await getMedias(tab)
let filteredFilenames = filenames
if (debouncedSearchText) {
const index = new Index()
filenames.forEach((filename: Filename, id: number) =>
index.add(id, filename.name)
)
const results: IndexSearchResult = index.search(debouncedSearchText)
filteredFilenames = results.map((id: Id) => filenames[id as number])
}
filteredFilenames = _.orderBy(filteredFilenames, sortBy, sortOrder)
const newPhotos = filteredFilenames.map((filename: Filename) => {
const width = photoWidth
const height = filename.height * (width / filename.width)
const src = `/media_thumbnail/${tab}/${filename.name}?width=${width}&height=${height}`
return { src, height, width, name: filename.name }
})
setPhotos(newPhotos)
} catch (e: any) {
setToastState({
open: true,
desc: e.message ? e.message : e.toString(),
state: 'error',
duration: 2000,
})
}
}
fetchData()
}, [
setToastState,
tab,
debouncedSearchText,
sortBy,
sortOrder,
photoWidth,
show,
])
const onScroll = (event: SyntheticEvent) => {
setScrollTop(event.currentTarget.scrollTop)
}
const onClick = ({ index }: { index: number }) => {
onPhotoClick(tab, photos[index].name)
}
return (
<Modal
onClose={onClose}
// TODOlayout switch 放到标题中
title={`Images (${photos.length})`}
className="file-manager-modal"
show={show}
>
<Flex style={{ justifyContent: 'space-between', gap: 8 }}>
<Tabs.Root
className="TabsRoot"
defaultValue={tab}
onValueChange={val => setTab(val)}
>
<Tabs.List className="TabsList" aria-label="Manage your account">
<Tabs.Trigger className="TabsTrigger" value={IMAGE_TAB}>
Image Directory
</Tabs.Trigger>
<Tabs.Trigger className="TabsTrigger" value={OUTPUT_TAB}>
Output Directory
</Tabs.Trigger>
</Tabs.List>
</Tabs.Root>
<Flex style={{ gap: 8 }}>
<Flex
style={{
position: 'relative',
justifyContent: 'start',
}}
>
<MagnifyingGlassIcon style={{ position: 'absolute', left: 8 }} />
<TextInput
ref={ref}
value={searchText}
className="file-search-input"
tabIndex={-1}
onInput={(evt: FormEvent<HTMLInputElement>) => {
evt.preventDefault()
evt.stopPropagation()
const target = evt.target as HTMLInputElement
setSearchText(target.value)
}}
placeholder="Search by file name"
/>
</Flex>
<Flex style={{ gap: 8 }}>
<Selector
width={140}
value={SortByMap[sortBy]}
options={Object.values(SortByMap)}
onChange={val => {
switch (val) {
case SORT_BY_NAME:
setSortBy(SortBy.NAME)
break
case SORT_BY_CREATED_TIME:
setSortBy(SortBy.CTIME)
break
case SORT_BY_MODIFIED_TIME:
setSortBy(SortBy.MTIME)
break
default:
break
}
}}
chevronDirection="down"
/>
<Button
icon={<BarsArrowDownIcon />}
toolTip="Descending order"
onClick={() => {
setSortOrder(SortOrder.DESCENDING)
}}
className={
sortOrder !== SortOrder.DESCENDING ? 'sort-btn-inactive' : ''
}
/>
<Button
icon={<BarsArrowUpIcon />}
toolTip="Ascending order"
onClick={() => {
setSortOrder(SortOrder.ASCENDING)
}}
className={
sortOrder !== SortOrder.ASCENDING ? 'sort-btn-inactive' : ''
}
/>
</Flex>
</Flex>
</Flex>
<ScrollArea.Root className="ScrollAreaRoot">
<ScrollArea.Viewport
className="ScrollAreaViewport"
onScroll={onScroll}
ref={onRefChange}
>
<PhotoAlbum
layout={layout}
photos={photos}
spacing={8}
padding={0}
onClick={onClick}
/>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar
className="ScrollAreaScrollbar"
orientation="vertical"
>
<ScrollArea.Thumb className="ScrollAreaThumb" />
</ScrollArea.Scrollbar>
{/* <ScrollArea.Scrollbar
className="ScrollAreaScrollbar"
orientation="horizontal"
>
<ScrollArea.Thumb className="ScrollAreaThumb" />
</ScrollArea.Scrollbar> */}
<ScrollArea.Corner className="ScrollAreaCorner" />
</ScrollArea.Root>
</Modal>
)
}