add image output tab in file manager
This commit is contained in:
parent
a7382807be
commit
f1c7f6dc99
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) => {
|
||||
if (!filteredFilenames) {
|
||||
return []
|
||||
}
|
||||
return filteredFilenames.map((filename: Filename) => {
|
||||
const width = photoWidth
|
||||
const height = filename.height * (width / filename.width)
|
||||
const src = `/media_thumbnail/${filename.name}?width=${width}&height=${height}`
|
||||
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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}, [filteredFilenames, photoWidth, sortBy, sortOrder])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@ -162,7 +174,22 @@ export default function FileManager(props: Props) {
|
||||
className="file-manager-modal"
|
||||
show={show}
|
||||
>
|
||||
<Flex style={{ justifyContent: 'end', gap: 8 }}>
|
||||
<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',
|
||||
@ -220,6 +247,7 @@ export default function FileManager(props: Props) {
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Flex>
|
||||
<ScrollArea.Root className="ScrollAreaRoot">
|
||||
<ScrollArea.Viewport
|
||||
className="ScrollAreaViewport"
|
||||
|
@ -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;
|
||||
|
@ -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">
|
||||
|
@ -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)
|
||||
}}
|
||||
|
@ -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
|
||||
|
@ -102,19 +102,23 @@ def save_image():
|
||||
return 'ok', 200
|
||||
|
||||
|
||||
@app.route("/medias")
|
||||
def medias():
|
||||
@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:
|
||||
|
Loading…
Reference in New Issue
Block a user