add image output tab in file manager

This commit is contained in:
Qing 2023-01-07 20:51:05 +08:00
parent a7382807be
commit f1c7f6dc99
8 changed files with 201 additions and 161 deletions

View File

@ -172,9 +172,9 @@ export async function postInteractiveSeg(
} }
} }
export async function getMediaFile(filename: string) { export async function getMediaFile(tab: string, filename: string) {
const res = await fetch( const res = await fetch(
`${API_ENDPOINT}/media/${encodeURIComponent(filename)}`, `${API_ENDPOINT}/media/${tab}/${encodeURIComponent(filename)}`,
{ {
method: 'GET', method: 'GET',
} }
@ -188,8 +188,8 @@ export async function getMediaFile(filename: string) {
throw new Error(errMsg) throw new Error(errMsg)
} }
export async function getMedias() { export async function getMedias(tab: string) {
const res = await fetch(`${API_ENDPOINT}/medias`, { const res = await fetch(`${API_ENDPOINT}/medias/${tab}`, {
method: 'GET', method: 'GET',
}) })
if (res.ok) { if (res.ok) {

View File

@ -91,7 +91,7 @@
padding-left: 30px; padding-left: 30px;
height: 32px; height: 32px;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: 8px; border-radius: 12px;
} }
.sort-btn-inactive { .sort-btn-inactive {
@ -99,3 +99,66 @@
opacity: 0.5; opacity: 0.5;
} }
} }
/* reset */
button,
fieldset,
input {
all: unset;
}
.TabsRoot {
display: flex;
flex-direction: column;
gap: 8px;
background-color: var(--page-bg);
align-self: flex-start;
}
.TabsList {
display: flex;
flex-direction: row;
gap: 6px;
justify-content: flex-start;
border: 1px solid var(--border-color);
border-radius: 12px;
background-color: var(--page-bg);
padding: 4px;
}
.TabsTrigger {
font-family: inherit;
background-color: white;
padding: 8px;
display: flex;
align-items: center;
justify-content: flex-start;
font-size: 15px;
line-height: 1;
color: var(--modal-text-color);
user-select: none;
background-color: var(--page-bg);
border-radius: 8px;
}
.TabsTrigger:hover {
background-color: var(--tabs-active-color);
}
.TabsTrigger[data-state='active'] {
background-color: var(--tabs-active-color);
}
.TabsTrigger:focus {
position: relative;
}
.TabsContent {
background-color: white;
outline: none;
background-color: var(--page-bg);
width: 100%;
}
.TabsContent[data-state='active'] {
display: flex;
flex-direction: column;
gap: 14px;
}

View File

@ -8,6 +8,7 @@ import React, {
FormEvent, FormEvent,
} from 'react' } from 'react'
import _ from 'lodash' import _ from 'lodash'
import * as Tabs from '@radix-ui/react-tabs'
import { useSetRecoilState } from 'recoil' import { useSetRecoilState } from 'recoil'
import PhotoAlbum from 'react-photo-album' import PhotoAlbum from 'react-photo-album'
import { BarsArrowDownIcon, BarsArrowUpIcon } from '@heroicons/react/24/outline' import { BarsArrowDownIcon, BarsArrowUpIcon } from '@heroicons/react/24/outline'
@ -50,6 +51,9 @@ enum SortBy {
const SORT_BY_NAME = 'Name' const SORT_BY_NAME = 'Name'
const SORT_BY_CREATED_TIME = 'Created time' const SORT_BY_CREATED_TIME = 'Created time'
const IMAGE_TAB = 'image'
const OUTPUT_TAB = 'output'
const SortByMap = { const SortByMap = {
[SortBy.NAME]: SORT_BY_NAME, [SortBy.NAME]: SORT_BY_NAME,
[SortBy.CTIME]: SORT_BY_CREATED_TIME, [SortBy.CTIME]: SORT_BY_CREATED_TIME,
@ -58,7 +62,7 @@ const SortByMap = {
interface Props { interface Props {
show: boolean show: boolean
onClose: () => void onClose: () => void
onPhotoClick(filename: string): void onPhotoClick(tab: string, filename: string): void
photoWidth: number photoWidth: number
} }
@ -73,6 +77,7 @@ export default function FileManager(props: Props) {
const ref = useRef(null) const ref = useRef(null)
const [searchText, setSearchText] = useState('') const [searchText, setSearchText] = useState('')
const [debouncedSearchText, setDebouncedSearchText] = useState('') const [debouncedSearchText, setDebouncedSearchText] = useState('')
const [tab, setTab] = useState(IMAGE_TAB)
const [, cancel] = useDebounce( const [, cancel] = useDebounce(
() => { () => {
@ -102,14 +107,10 @@ export default function FileManager(props: Props) {
[show, closeScrollTop] [show, closeScrollTop]
) )
const onClick = ({ index }: { index: number }) => {
onPhotoClick(filenames[index].name)
}
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
try { try {
const newFilenames = await getMedias() const newFilenames = await getMedias(tab)
setFileNames(newFilenames) setFileNames(newFilenames)
} catch (e: any) { } catch (e: any) {
setToastState({ setToastState({
@ -123,7 +124,7 @@ export default function FileManager(props: Props) {
if (show) { if (show) {
fetchData() fetchData()
} }
}, [show, setToastState]) }, [show, setToastState, tab])
const onScroll = (event: SyntheticEvent) => { const onScroll = (event: SyntheticEvent) => {
setScrollTop(event.currentTarget.scrollTop) setScrollTop(event.currentTarget.scrollTop)
@ -141,19 +142,30 @@ export default function FileManager(props: Props) {
const results: IndexSearchResult = await index.searchAsync( const results: IndexSearchResult = await index.searchAsync(
debouncedSearchText debouncedSearchText
) )
return results.map((id: Id) => filenames[id as number]) return _.orderBy(
}, [filenames, debouncedSearchText]) results.map((id: Id) => filenames[id as number]),
sortBy,
sortOrder
)
}, [filenames, debouncedSearchText, sortBy, sortOrder])
const photos: Photo[] = useMemo(() => { const photos: Photo[] = useMemo(() => {
return _.orderBy(filteredFilenames, sortBy, sortOrder).map( if (!filteredFilenames) {
(filename: Filename) => { return []
const width = photoWidth }
const height = filename.height * (width / filename.width) return filteredFilenames.map((filename: Filename) => {
const src = `/media_thumbnail/${filename.name}?width=${width}&height=${height}` const width = photoWidth
return { src, height, width } const height = filename.height * (width / filename.width)
} const src = `/media_thumbnail/${tab}/${filename.name}?width=${width}&height=${height}`
) return { src, height, width }
}, [filteredFilenames, photoWidth, sortBy, sortOrder]) })
}, [filteredFilenames, photoWidth, tab])
const onClick = ({ index }: { index: number }) => {
if (filteredFilenames) {
onPhotoClick(tab, filteredFilenames[index].name)
}
}
return ( return (
<Modal <Modal
@ -162,62 +174,78 @@ export default function FileManager(props: Props) {
className="file-manager-modal" className="file-manager-modal"
show={show} show={show}
> >
<Flex style={{ justifyContent: 'end', gap: 8 }}> <Flex style={{ justifyContent: 'space-between', gap: 8 }}>
<Flex <Tabs.Root
style={{ className="TabsRoot"
position: 'relative', defaultValue={tab}
justifyContent: 'start', onValueChange={val => setTab(val)}
}}
> >
<MagnifyingGlassIcon style={{ position: 'absolute', left: 8 }} /> <Tabs.List className="TabsList" aria-label="Manage your account">
<TextInput <Tabs.Trigger className="TabsTrigger" value={IMAGE_TAB}>
ref={ref} Image Directory
value={searchText} </Tabs.Trigger>
className="file-search-input" <Tabs.Trigger className="TabsTrigger" value={OUTPUT_TAB}>
tabIndex={-1} Output Directory
onInput={(evt: FormEvent<HTMLInputElement>) => { </Tabs.Trigger>
evt.preventDefault() </Tabs.List>
evt.stopPropagation() </Tabs.Root>
const target = evt.target as HTMLInputElement
setSearchText(target.value)
}}
placeholder="Search by file name"
/>
</Flex>
<Flex style={{ gap: 8 }}> <Flex style={{ gap: 8 }}>
<Selector <Flex
width={130} style={{
value={SortByMap[sortBy]} position: 'relative',
options={Object.values(SortByMap)} justifyContent: 'start',
onChange={val => { }}
if (val === SORT_BY_CREATED_TIME) { >
setSortBy(SortBy.CTIME) <MagnifyingGlassIcon style={{ position: 'absolute', left: 8 }} />
} else { <TextInput
setSortBy(SortBy.NAME) 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={130}
value={SortByMap[sortBy]}
options={Object.values(SortByMap)}
onChange={val => {
if (val === SORT_BY_CREATED_TIME) {
setSortBy(SortBy.CTIME)
} else {
setSortBy(SortBy.NAME)
}
}}
chevronDirection="down"
/>
<Button
icon={<BarsArrowDownIcon />}
toolTip="Descending order"
onClick={() => {
setSortOrder(SortOrder.DESCENDING)
}}
className={
sortOrder !== SortOrder.DESCENDING ? 'sort-btn-inactive' : ''
} }
}} />
chevronDirection="down" <Button
/> icon={<BarsArrowUpIcon />}
<Button toolTip="Ascending order"
icon={<BarsArrowDownIcon />} onClick={() => {
toolTip="Descending order" setSortOrder(SortOrder.ASCENDING)
onClick={() => { }}
setSortOrder(SortOrder.DESCENDING) className={
}} sortOrder !== SortOrder.ASCENDING ? 'sort-btn-inactive' : ''
className={ }
sortOrder !== SortOrder.DESCENDING ? 'sort-btn-inactive' : '' />
} </Flex>
/>
<Button
icon={<BarsArrowUpIcon />}
toolTip="Ascending order"
onClick={() => {
setSortOrder(SortOrder.ASCENDING)
}}
className={
sortOrder !== SortOrder.ASCENDING ? 'sort-btn-inactive' : ''
}
/>
</Flex> </Flex>
</Flex> </Flex>
<ScrollArea.Root className="ScrollAreaRoot"> <ScrollArea.Root className="ScrollAreaRoot">

View File

@ -18,73 +18,6 @@
animation: slideDown 0.2s ease-out; animation: slideDown 0.2s ease-out;
} }
} }
/* reset */
button,
fieldset,
input {
all: unset;
}
.TabsRoot {
display: flex;
flex-direction: row;
gap: 16px;
width: 100%;
background-color: var(--page-bg);
flex-grow: 1;
}
.TabsList {
display: flex;
flex-direction: column;
gap: 6px;
justify-content: flex-start;
border-right: 1px solid var(--border-color);
background-color: var(--page-bg);
padding-right: 20px;
}
.TabsTrigger {
font-family: inherit;
background-color: white;
padding-top: 8px;
padding-bottom: 8px;
padding-right: 40px;
padding-left: 8px;
display: flex;
align-items: center;
justify-content: flex-start;
font-size: 15px;
line-height: 1;
color: var(--modal-text-color);
user-select: none;
background-color: var(--page-bg);
border-radius: 8px;
}
.TabsTrigger:hover {
background-color: var(--tabs-active-color);
}
.TabsTrigger[data-state='active'] {
background-color: var(--tabs-active-color);
}
.TabsTrigger:focus {
position: relative;
}
.TabsContent {
background-color: white;
outline: none;
background-color: var(--page-bg);
width: 100%;
}
.TabsContent[data-state='active'] {
display: flex;
flex-direction: column;
gap: 14px;
}
.folder-path-block { .folder-path-block {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@ -52,7 +52,7 @@ const SidePanel = () => {
className="btn-primary side-panel-trigger" className="btn-primary side-panel-trigger"
onClick={() => toggleOpen()} onClick={() => toggleOpen()}
> >
Configurations Config
</PopoverPrimitive.Trigger> </PopoverPrimitive.Trigger>
<PopoverPrimitive.Portal> <PopoverPrimitive.Portal>
<PopoverPrimitive.Content className="side-panel-content"> <PopoverPrimitive.Content className="side-panel-content">

View File

@ -104,8 +104,8 @@ const Workspace = () => {
onClose={() => { onClose={() => {
setShowFileManager(false) setShowFileManager(false)
}} }}
onPhotoClick={async (filename: string) => { onPhotoClick={async (tab: string, filename: string) => {
const newFile = await getMediaFile(filename) const newFile = await getMediaFile(tab, filename)
setFile(newFile) setFile(newFile)
setShowFileManager(false) setShowFileManager(false)
}} }}

View File

@ -82,10 +82,19 @@ class FileManager:
@property @property
@cached(cache=TTLCache(maxsize=1024, ttl=30)) @cached(cache=TTLCache(maxsize=1024, ttl=30))
def media_names(self): def media_names(self):
names = sorted([it.name for it in glob_img(self.root_directory)]) return self._media_names(self.root_directory)
@property
@cached(cache=TTLCache(maxsize=1024, ttl=30))
def output_media_names(self):
return self._media_names(self.output_dir)
@staticmethod
def _media_names(directory: Path):
names = sorted([it.name for it in glob_img(directory)])
res = [] res = []
for name in names: for name in names:
path = os.path.join(self.root_directory, name) path = os.path.join(directory, name)
img = Image.open(path) img = Image.open(path)
res.append({"name": name, "height": img.height, "width": img.width, "ctime": os.path.getctime(path)}) res.append({"name": name, "height": img.height, "width": img.width, "ctime": os.path.getctime(path)})
return res return res
@ -94,14 +103,14 @@ class FileManager:
def thumbnail_url(self): def thumbnail_url(self):
return self.app.config["THUMBNAIL_MEDIA_THUMBNAIL_URL"] return self.app.config["THUMBNAIL_MEDIA_THUMBNAIL_URL"]
def get_thumbnail(self, original_filename, width, height, **options): def get_thumbnail(self, directory: Path, original_filename: str, width, height, **options):
storage = FilesystemStorageBackend(self.app) storage = FilesystemStorageBackend(self.app)
crop = options.get("crop", "fit") crop = options.get("crop", "fit")
background = options.get("background") background = options.get("background")
quality = options.get("quality", 90) quality = options.get("quality", 90)
original_path, original_filename = os.path.split(original_filename) original_path, original_filename = os.path.split(original_filename)
original_filepath = os.path.join(self.root_directory, original_path, original_filename) original_filepath = os.path.join(directory, original_path, original_filename)
image = Image.open(BytesIO(storage.read(original_filepath))) image = Image.open(BytesIO(storage.read(original_filepath)))
# keep ratio resize # keep ratio resize

View File

@ -102,19 +102,23 @@ def save_image():
return 'ok', 200 return 'ok', 200
@app.route("/medias") @app.route("/medias/<tab>")
def medias(): def medias(tab):
# all images in input folder if tab == 'image':
return jsonify(thumb.media_names), 200 # all images in input folder
return jsonify(thumb.media_names), 200
return jsonify(thumb.output_media_names), 200
@app.route('/media/<filename>') @app.route('/media/<tab>/<filename>')
def media_file(filename): def media_file(tab, filename):
return send_from_directory(app.config['THUMBNAIL_MEDIA_ROOT'], filename) if tab == 'image':
return send_from_directory(thumb.root_directory, filename)
return send_from_directory(thumb.output_dir, filename)
@app.route('/media_thumbnail/<filename>') @app.route('/media_thumbnail/<tab>/<filename>')
def media_thumbnail_file(filename): def media_thumbnail_file(tab, filename):
args = request.args args = request.args
width = args.get('width') width = args.get('width')
height = args.get('height') height = args.get('height')
@ -125,7 +129,10 @@ def media_thumbnail_file(filename):
if height: if height:
height = int(float(height)) height = int(float(height))
thumb_filename, (width, height) = thumb.get_thumbnail(filename, width, height) directory = thumb.root_directory
if tab == 'output':
directory = thumb.output_dir
thumb_filename, (width, height) = thumb.get_thumbnail(directory, filename, width, height)
thumb_filepath = f"{app.config['THUMBNAIL_MEDIA_THUMBNAIL_ROOT']}{thumb_filename}" thumb_filepath = f"{app.config['THUMBNAIL_MEDIA_THUMBNAIL_ROOT']}{thumb_filename}"
response = make_response(send_file(thumb_filepath)) response = make_response(send_file(thumb_filepath))
@ -350,7 +357,7 @@ def main(args):
if os.path.isdir(args.input): if os.path.isdir(args.input):
app.config["THUMBNAIL_MEDIA_ROOT"] = args.input app.config["THUMBNAIL_MEDIA_ROOT"] = args.input
app.config["THUMBNAIL_MEDIA_THUMBNAIL_ROOT"] = os.path.join(args.output_dir, 'thumbnails') app.config["THUMBNAIL_MEDIA_THUMBNAIL_ROOT"] = os.path.join(args.output_dir, 'lama_cleaner_thumbnails')
is_enable_file_manager = True is_enable_file_manager = True
thumb.output_dir = Path(args.output_dir) thumb.output_dir = Path(args.output_dir)
else: else: