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

View File

@ -91,7 +91,7 @@
padding-left: 30px;
height: 32px;
border: 1px solid var(--border-color);
border-radius: 8px;
border-radius: 12px;
}
.sort-btn-inactive {
@ -99,3 +99,66 @@
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,
} from 'react'
import _ from 'lodash'
import * as Tabs from '@radix-ui/react-tabs'
import { useSetRecoilState } from 'recoil'
import PhotoAlbum from 'react-photo-album'
import { BarsArrowDownIcon, BarsArrowUpIcon } from '@heroicons/react/24/outline'
@ -50,6 +51,9 @@ enum SortBy {
const SORT_BY_NAME = 'Name'
const SORT_BY_CREATED_TIME = 'Created time'
const IMAGE_TAB = 'image'
const OUTPUT_TAB = 'output'
const SortByMap = {
[SortBy.NAME]: SORT_BY_NAME,
[SortBy.CTIME]: SORT_BY_CREATED_TIME,
@ -58,7 +62,7 @@ const SortByMap = {
interface Props {
show: boolean
onClose: () => void
onPhotoClick(filename: string): void
onPhotoClick(tab: string, filename: string): void
photoWidth: number
}
@ -73,6 +77,7 @@ export default function FileManager(props: Props) {
const ref = useRef(null)
const [searchText, setSearchText] = useState('')
const [debouncedSearchText, setDebouncedSearchText] = useState('')
const [tab, setTab] = useState(IMAGE_TAB)
const [, cancel] = useDebounce(
() => {
@ -102,14 +107,10 @@ export default function FileManager(props: Props) {
[show, closeScrollTop]
)
const onClick = ({ index }: { index: number }) => {
onPhotoClick(filenames[index].name)
}
useEffect(() => {
const fetchData = async () => {
try {
const newFilenames = await getMedias()
const newFilenames = await getMedias(tab)
setFileNames(newFilenames)
} catch (e: any) {
setToastState({
@ -123,7 +124,7 @@ export default function FileManager(props: Props) {
if (show) {
fetchData()
}
}, [show, setToastState])
}, [show, setToastState, tab])
const onScroll = (event: SyntheticEvent) => {
setScrollTop(event.currentTarget.scrollTop)
@ -141,19 +142,30 @@ export default function FileManager(props: Props) {
const results: IndexSearchResult = await index.searchAsync(
debouncedSearchText
)
return results.map((id: Id) => filenames[id as number])
}, [filenames, debouncedSearchText])
return _.orderBy(
results.map((id: Id) => filenames[id as number]),
sortBy,
sortOrder
)
}, [filenames, debouncedSearchText, sortBy, sortOrder])
const photos: Photo[] = useMemo(() => {
return _.orderBy(filteredFilenames, sortBy, sortOrder).map(
(filename: Filename) => {
const width = photoWidth
const height = filename.height * (width / filename.width)
const src = `/media_thumbnail/${filename.name}?width=${width}&height=${height}`
return { src, height, width }
}
)
}, [filteredFilenames, photoWidth, sortBy, sortOrder])
if (!filteredFilenames) {
return []
}
return 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 }
})
}, [filteredFilenames, photoWidth, tab])
const onClick = ({ index }: { index: number }) => {
if (filteredFilenames) {
onPhotoClick(tab, filteredFilenames[index].name)
}
}
return (
<Modal
@ -162,62 +174,78 @@ export default function FileManager(props: Props) {
className="file-manager-modal"
show={show}
>
<Flex style={{ justifyContent: 'end', gap: 8 }}>
<Flex
style={{
position: 'relative',
justifyContent: 'start',
}}
<Flex style={{ justifyContent: 'space-between', gap: 8 }}>
<Tabs.Root
className="TabsRoot"
defaultValue={tab}
onValueChange={val => setTab(val)}
>
<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>
<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 }}>
<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)
<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={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={<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' : ''
}
/>
/>
<Button
icon={<BarsArrowUpIcon />}
toolTip="Ascending order"
onClick={() => {
setSortOrder(SortOrder.ASCENDING)
}}
className={
sortOrder !== SortOrder.ASCENDING ? 'sort-btn-inactive' : ''
}
/>
</Flex>
</Flex>
</Flex>
<ScrollArea.Root className="ScrollAreaRoot">

View File

@ -18,73 +18,6 @@
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 {
display: flex;
flex-direction: column;

View File

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

View File

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

View File

@ -82,10 +82,19 @@ class FileManager:
@property
@cached(cache=TTLCache(maxsize=1024, ttl=30))
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 = []
for name in names:
path = os.path.join(self.root_directory, name)
path = os.path.join(directory, name)
img = Image.open(path)
res.append({"name": name, "height": img.height, "width": img.width, "ctime": os.path.getctime(path)})
return res
@ -94,14 +103,14 @@ class FileManager:
def thumbnail_url(self):
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)
crop = options.get("crop", "fit")
background = options.get("background")
quality = options.get("quality", 90)
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)))
# keep ratio resize

View File

@ -102,19 +102,23 @@ def save_image():
return 'ok', 200
@app.route("/medias")
def medias():
# all images in input folder
return jsonify(thumb.media_names), 200
@app.route("/medias/<tab>")
def medias(tab):
if tab == 'image':
# all images in input folder
return jsonify(thumb.media_names), 200
return jsonify(thumb.output_media_names), 200
@app.route('/media/<filename>')
def media_file(filename):
return send_from_directory(app.config['THUMBNAIL_MEDIA_ROOT'], filename)
@app.route('/media/<tab>/<filename>')
def media_file(tab, 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>')
def media_thumbnail_file(filename):
@app.route('/media_thumbnail/<tab>/<filename>')
def media_thumbnail_file(tab, filename):
args = request.args
width = args.get('width')
height = args.get('height')
@ -125,7 +129,10 @@ def media_thumbnail_file(filename):
if 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}"
response = make_response(send_file(thumb_filepath))
@ -350,7 +357,7 @@ def main(args):
if os.path.isdir(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
thumb.output_dir = Path(args.output_dir)
else: