feat: 支持预览图片,发送图片到文生图,图生图等。支持下载其他文件,支持缩略图,改用虚拟列表

pull/3/head
zanllp 2023-03-26 04:07:07 +08:00
parent d758a2978a
commit 892e952b62
20 changed files with 799 additions and 423 deletions

View File

@ -6,8 +6,8 @@
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<script type="module" crossorigin src="/baidu_netdisk/fe-static/assets/index-a8fb8db6.js"></script>
<link rel="stylesheet" href="/baidu_netdisk/fe-static/assets/index-56cef25f.css">
<script type="module" crossorigin src="/baidu_netdisk/fe-static/assets/index-390bfde0.js"></script>
<link rel="stylesheet" href="/baidu_netdisk/fe-static/assets/index-f245e7cd.css">
</head>
<body>
<div id="zanllp_dev_gradio_fe"></div>

View File

@ -7,9 +7,14 @@ import re
import subprocess
import asyncio
import subprocess
from typing import Any, List, Literal, Union
from typing import Any, List, Literal, Optional, Union
from scripts.baiduyun_task import BaiduyunTask
from pydantic import BaseModel
from fastapi.responses import FileResponse
from PIL import Image
from io import BytesIO
import hashlib
from scripts.bin import (
check_bin_exists,
cwd,
@ -67,7 +72,7 @@ def list_file(cwd="/"):
"date": match.group(3),
"name": name.strip("/"),
"type": f_type,
"bytes": convert_to_bytes(size) if size != "-" else size
"bytes": convert_to_bytes(size) if size != "-" else size,
}
files.append(file_info)
return files
@ -92,7 +97,6 @@ def logout():
return bool(match)
def singleton_async(fn):
@functools.wraps(fn)
async def wrapper(*args, **kwargs):
@ -108,8 +112,9 @@ def singleton_async(fn):
wrapper.busy = []
return wrapper
send_img_path = { "value": "" }
def baidu_netdisk_api(_: Any, app: FastAPI):
def baidu_netdisk_api(_: Any = None, app: FastAPI):
pre = "/baidu_netdisk"
app.mount(
f"{pre}/fe-static",
@ -144,6 +149,7 @@ def baidu_netdisk_api(_: Any, app: FastAPI):
conf = {}
try:
from modules.shared import opts
conf = opts.data
except:
pass
@ -260,3 +266,59 @@ def baidu_netdisk_api(_: Any, app: FastAPI):
raise HTTPException(status_code=400, detail=str(e))
return {"files": files}
@app.get(pre + "/image-thumbnail")
async def thumbnail(path: str, size: str = '256,256'):
# 生成缓存文件的路径
hash = hashlib.md5((path + size).encode('utf-8')).hexdigest()
cache_path = f'/tmp/{hash}.webp'
# 如果缓存文件存在,则直接返回该文件
if os.path.exists(cache_path):
return FileResponse(
cache_path,
media_type="image/webp",
headers={"Cache-Control": "max-age=31536000", "ETag": hash},
)
# 如果缓存文件不存在,则生成缩略图并保存
with open(path, "rb") as f:
img = Image.open(BytesIO(f.read()))
w,h = size.split(',')
img.thumbnail((int(w),int(h)))
buffer = BytesIO()
img.save(buffer, 'webp')
# 将二进制数据写入缓存文件中
with open(cache_path, 'wb') as f:
f.write(buffer.getvalue())
# 返回缓存文件
return FileResponse(
cache_path,
media_type="image/webp",
headers={"Cache-Control": "max-age=31536000", "ETag": hash},
)
@app.get(pre+"/file")
async def get_file(filename: str, disposition: Optional[str] = None):
import mimetypes
if not os.path.exists(filename):
raise HTTPException(status_code=404)
# 根据文件后缀名获取媒体类型
media_type, _ = mimetypes.guess_type(filename)
headers = {}
if disposition:
headers["Content-Disposition"] = f'attachment; filename="{disposition}"'
return FileResponse(
filename,
media_type=media_type,
headers=headers,
)
@app.post(pre+"/send_img_path")
async def api_set_send_img_path(path: str):
global send_img_path
send_img_path["value"] = path

View File

@ -1,5 +1,5 @@
from scripts.api import baidu_netdisk_api
from modules import script_callbacks
from scripts.api import baidu_netdisk_api, send_img_path
from modules import script_callbacks, generation_parameters_copypaste as send
from scripts.bin import (
bin_file_name,
get_matched_summary,
@ -7,9 +7,10 @@ from scripts.bin import (
download_bin_file,
)
from scripts.tool import cwd
'''
"""
api函数声明和启动分离方便另外一边被外部调用
'''
"""
not_exists_msg = (
f"找不到{bin_file_name},尝试手动从 {get_matched_summary()[1]} 下载,下载后放到 {cwd} 文件夹下,重启界面"
@ -31,6 +32,29 @@ def on_ui_tabs():
if not exists:
print(f"\033[31m{not_exists_msg}\033[0m")
with gr.Blocks(analytics_enabled=False) as baidu_netdisk:
img = gr.Image(
type="pil",
elem_id="bd_hidden_img",
)
img_update_trigger = gr.Button("button", elem_id="bd_hidden_img_update_trigger")
def img_update_func():
return send_img_path.get("value")
img_update_trigger.click(img_update_func, outputs=img)
img_file_info = gr.Textbox(elem_id="bd_hidden_img_file_info")
for tab in ["txt2img", "img2img", "inpaint", "extras"]:
btn = gr.Button(f"Send to {tab}", elem_id=f"bd_hidden_tab_{tab}")
send.register_paste_params_button(
send.ParamBinding(
paste_button=btn,
tabname=tab,
source_image_component=img,
source_text_component=img_file_info,
)
)
gr.Textbox(not_exists_msg, visible=not exists)
with gr.Row(visible=bool(exists)):
with gr.Column():

3
style.css Normal file
View File

@ -0,0 +1,3 @@
[id^="bd_hidden_"] {
display: none;
}

4
vue/components.d.ts vendored
View File

@ -9,21 +9,23 @@ export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
AAlert: typeof import('ant-design-vue/es')['Alert']
ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']
ABreadcrumbItem: typeof import('ant-design-vue/es')['BreadcrumbItem']
AButton: typeof import('ant-design-vue/es')['Button']
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem']
AImage: typeof import('ant-design-vue/es')['Image']
AInput: typeof import('ant-design-vue/es')['Input']
AInputNumber: typeof import('ant-design-vue/es')['InputNumber']
AInputSearch: typeof import('ant-design-vue/es')['InputSearch']
AMenu: typeof import('ant-design-vue/es')['Menu']
AMenuItem: typeof import('ant-design-vue/es')['MenuItem']
AModal: typeof import('ant-design-vue/es')['Modal']
AProgress: typeof import('ant-design-vue/es')['Progress']
ASelect: typeof import('ant-design-vue/es')['Select']
ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
ASwitch: typeof import('ant-design-vue/es')['Switch']
ATabPane: typeof import('ant-design-vue/es')['TabPane']
ATabs: typeof import('ant-design-vue/es')['Tabs']
ATag: typeof import('ant-design-vue/es')['Tag']

199
vue/dist/assets/index-390bfde0.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
vue/dist/assets/index-f245e7cd.css vendored Normal file

File diff suppressed because one or more lines are too long

4
vue/dist/index.html vendored
View File

@ -5,8 +5,8 @@
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vite App</title>
<script type="module" crossorigin src="/baidu_netdisk/fe-static/assets/index-a8fb8db6.js"></script>
<link rel="stylesheet" href="/baidu_netdisk/fe-static/assets/index-56cef25f.css">
<script type="module" crossorigin src="/baidu_netdisk/fe-static/assets/index-390bfde0.js"></script>
<link rel="stylesheet" href="/baidu_netdisk/fe-static/assets/index-f245e7cd.css">
</head>
<body>
<div id="zanllp_dev_gradio_fe"></div>

View File

@ -24,6 +24,7 @@
"pinia": "^2.0.33",
"pinia-plugin-persistedstate": "^3.1.0",
"vue": "^3.2.47",
"vue-virtual-scroller": "^2.0.0-beta.8",
"vue3-ts-util": "^0.8.2",
"vuedraggable": "^4.1.0"
},

View File

@ -12,6 +12,7 @@ import { useGlobalStore } from './store/useGlobalStore'
import { getAutoCompletedTagList } from './taskRecord/autoComplete'
import { useTaskListStore } from './store/useTaskListStore'
import TaskOperation from './taskRecord/taskOperation.vue'
import { useIntervalFn } from '@vueuse/core'
const user = ref<UserInfo>()
const bduss = ref('')
@ -21,7 +22,6 @@ const taskStore = useTaskListStore()
onMounted(async () => {
getGlobalSetting().then((resp) => {
globalStore.conf = resp
globalStore.autoCompletedDirList = getAutoCompletedTagList(resp).filter(v => v?.dir?.trim?.())
})
user.value = await queue.pushAction(getUserInfo).res
@ -40,6 +40,19 @@ const onLoginBtnClick = async () => {
message.error(isAxiosError(error) ? error.response?.data?.detail ?? '未知错误' : '未知错误')
}
}
const tips = ref('')
const msgs = [
'使用“快速移动”, 可以直接到到达图生图,文生图等文件夹',
"你可以通过拖拽调整两边区域的大小",
"使用ctrlshift可以很方便的进行多选",
"从百度云向本地拖拽是下载,本地向百度云拖拽是上传",
"可以多尝试更多里面的功能",
"鼠标在文件上右键可以打开上下文菜单",
"提醒任务完成后,你需要手动刷新下才能看到新文件"]
useIntervalFn(() => {
tips.value = msgs[~~(Math.random() * msgs.length)]
}, 3000)
</script>
<template>
@ -50,12 +63,22 @@ const onLoginBtnClick = async () => {
<div>
已登录用户{{ user.username }}
</div>
<div class="flex-placeholder" /><a-alert :message="tips" type="info" show-icon />
<a-form layout="inline">
<a-form-item label="使用缩略图预览">
<a-switch v-model:checked="globalStore.enableThumbnail" />
</a-form-item>
<a-form-item>
<a-button @click="onLogoutBtnClick">
<template #icon>
<logout-outlined />
</template>
登出
</a-button>
</a-form-item>
</a-form>
</template>
<a-form layout="inline" v-else>
@ -89,7 +112,6 @@ const onLoginBtnClick = async () => {
</a-skeleton>
</template>
<style scoped lang="scss">
:deep() .ant-tabs-nav {
margin: 0 16px;
}
@ -102,7 +124,7 @@ const onLoginBtnClick = async () => {
display: flex;
justify-content: space-between;
.actions-bar>* {
&> :not(:first-child) {
margin-left: 16px;
}
}

View File

@ -156,3 +156,7 @@ export const cancelTask = async (id: string) => {
export const removeTask = async (id: string) => {
return axiosInst.delete(`/task/${id}`)
}
export const setImgPath = async (path: string) => {
return axiosInst.post(`/send_img_path?path=${encodeURIComponent(path)}`)
}

View File

@ -1,10 +1,11 @@
<script setup lang="ts">
import { SplitView } from 'vue3-ts-util'
import StackView from './stackView.vue'
import { useGlobalStore } from '@/store/useGlobalStore'
const store = useGlobalStore()
</script>
<template>
<split-view>
<split-view v-model:percent="store.stackViewSplit">
<template #left>
<stack-view target="local" />
</template>

View File

@ -13,7 +13,7 @@ const onOK = () => {
</script>
<template>
<a-modal v-model:visible="visible" title="输入地址回车" @ok="onOK">
<a-input @press-enter="onOK" v-model:value="loc" style="width: 300px;" allow-clear></a-input>
<a-input @press-enter="onOK" v-model:value="loc" style="width: 100%;" allow-clear></a-input>
</a-modal>
<a style="margin-left: 4px;" @click="visible = true">前往</a>
<a style="margin-left: 8px;" @click="visible = true">前往</a>
</template>

View File

@ -1,20 +1,26 @@
<script setup lang="ts">
import { getTargetFolderFiles, type FileNodeInfo } from '@/api/files'
import { setImgPath } from '@/api'
import { cloneDeep, last, range, uniq } from 'lodash'
import { ref, computed, onMounted, watch, h } from 'vue'
import { FileOutlined, FolderOpenOutlined, DownOutlined } from '@/icon'
import { sortMethodMap, sortFiles, SortMethod } from './fileSort'
import path from 'path-browserify'
import { useGlobalStore } from '@/store/useGlobalStore'
import { copy2clipboard, ok, type SearchSelectConv, SearchSelect, useWatchDocument } from 'vue3-ts-util'
import { copy2clipboard, ok, type SearchSelectConv, SearchSelect, useWatchDocument, fallbackImage, delay } from 'vue3-ts-util'
// @ts-ignore
import NProgress from 'multi-nprogress'
import 'multi-nprogress/nprogress.css'
import type Progress from 'nprogress'
import { message, Modal } from 'ant-design-vue'
import FolderNavigator from './folderNavigator.vue'
import { gradioApp, isImageFile } from '@/util'
import { useElementSize } from '@vueuse/core'
// @ts-ignore
import { RecycleScroller } from 'vue-virtual-scroller'
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css'
import type { MenuInfo } from 'ant-design-vue/lib/menu/src/interface'
const np = ref<Progress.NProgress>()
const el = ref<HTMLDivElement>()
const props = defineProps<{
target: 'local' | 'netdisk'
@ -23,24 +29,61 @@ interface Page {
files: FileNodeInfo[]
curr: string
}
const stack = ref<Page[]>([])
interface FileNodeInfoR extends FileNodeInfo {
fullpath: string
}
type ViewMode = 'line' | 'grid' | 'large-size-grid'
const global = useGlobalStore()
const currPage = computed(() => last(stack.value))
const multiSelectedIdxs = ref([] as number[])
const currLocation = computed(() => path.join(...getBasePath()))
const { currLocation, currPage, refresh, copyLocation, back, openNext, stack, to } = useLocation()
const { gridItems, sortMethodConv, moreActionsDropdownShow, sortedFiles, sortMethod, viewMode, gridSize, viewModeMap, largeGridSize } = useFilesDisplay()
const { onDrop, onFileDragStart, multiSelectedIdxs } = useFileTransfer()
const { onFileItemClick, onContextMenuClick } = useFileItemActions()
useWatchDocument('click', () => multiSelectedIdxs.value = [])
useWatchDocument('blur', () => multiSelectedIdxs.value = [])
watch(currPage, () => multiSelectedIdxs.value = [])
const toRawFileUrl = (file: FileNodeInfoR, download = false) => `/baidu_netdisk/file?filename=${encodeURIComponent(file.fullpath)}${download ? `&disposition=${encodeURIComponent(file.name)}` : ''}`
const toImageThumbnailUrl = (file: FileNodeInfoR, size = '256,256') => `/baidu_netdisk/image-thumbnail?path=${encodeURIComponent(file.fullpath)}&size=${size}`
function useFilesDisplay () {
const moreActionsDropdownShow = ref(false)
const viewMode = ref<ViewMode>('line')
const viewModeMap: Record<ViewMode, string> = { line: '详情列表', 'grid': '预览网格', 'large-size-grid': '大尺寸预览网格' }
const sortMethodConv: SearchSelectConv<SortMethod> = {
value: (v) => v,
text: (v) => '按' + sortMethodMap[v]
}
const sortMethod = ref(SortMethod.DATE_DESC)
const sortedFiles = computed(() => sortFiles(currPage.value?.files ?? [], sortMethod.value))
const sortedFiles = computed(() => sortFiles(currPage.value?.files ?? [], sortMethod.value).map(v => ({ ...v, fullpath: path.join(currLocation.value, v.name) })))
const gridSize = 288
const largeGridSize = gridSize * 2
const { width } = useElementSize(el)
const gridItems = computed(() => {
const w = width.value
if (viewMode.value === 'line' || !w) {
return
}
return ~~(w / (viewMode.value === 'grid' ? gridSize : largeGridSize))
})
return {
gridItems,
sortedFiles,
sortMethodConv,
viewModeMap,
moreActionsDropdownShow,
viewMode,
gridSize,
sortMethod,
largeGridSize
}
}
function useLocation () {
const np = ref<Progress.NProgress>()
const currPage = computed(() => last(stack.value))
const stack = ref<Page[]>([])
const currLocation = computed(() => path.join(...getBasePath()))
onMounted(async () => {
const resp = await getTargetFolderFiles(props.target, '/')
stack.value.push({
@ -54,7 +97,6 @@ onMounted(async () => {
}
})
const getBasePath = () =>
stack.value.map((v) => v.curr).slice(global.conf?.is_win && props.target === 'local' ? 1 : 0)
@ -114,36 +156,6 @@ const to = async (dir: string) => {
}
}
const onDrop = async (e: DragEvent) => {
type Data = {
from: typeof props.target
path: string[],
includeDir: boolean
}
const data = JSON.parse(e.dataTransfer?.getData('text') || '{}') as Data
console.log(data)
if (data.from && data.path && typeof data.includeDir !== 'undefined') {
if (data.from === props.target) {
return
}
const type = data.from === 'local' ? 'upload' : 'download'
const typeZH = type === 'upload' ? '上传' : '下载'
const toPath = path.join(...getBasePath())
const content = h('div', [
h('div', `${props.target !== 'local' ? '本地' : '云盘'} `),
h('ol', data.path.map(v => v.split(/[/\\]/).pop()).map(v => h('li', v))),
h('div', `${typeZH} ${props.target === 'local' ? '本地' : '云盘'} ${toPath}`)
])
Modal.confirm({
title: `确定创建${typeZH}任务${data.includeDir ? ', 这是文件夹或者包含文件夹!' : ''}`,
content,
maskClosable: true,
async onOk () {
global.eventEmitter.emit('createNewTask', { send_dirs: data.path, recv_dir: toPath, type })
}
})
}
}
const refresh = async () => {
if (stack.value.length === 1) {
@ -160,24 +172,27 @@ const refresh = async () => {
await openNext(currPage.value?.files.find((v) => v.name === last?.curr)!)
}
}
const onFileItemClick = async (e: MouseEvent, file: FileNodeInfo) => {
e.stopPropagation()
const files = sortedFiles.value
const idx = files.indexOf(file)
if (e.shiftKey) {
multiSelectedIdxs.value.push(idx)
multiSelectedIdxs.value.sort((a, b) => a - b)
const first = multiSelectedIdxs.value[0]
const last = multiSelectedIdxs.value[multiSelectedIdxs.value.length - 1]
multiSelectedIdxs.value = range(first, last + 1)
} else if (e.ctrlKey) {
multiSelectedIdxs.value.push(idx)
} else {
await openNext(file)
return {
refresh,
copyLocation,
back,
openNext,
currPage,
currLocation,
to,
stack
}
}
function useFileTransfer () {
const multiSelectedIdxs = ref([] as number[])
const recover = () => {
multiSelectedIdxs.value = []
}
useWatchDocument('click', recover)
useWatchDocument('blur', recover)
watch(currPage, recover)
const onFileDragStart = (e: DragEvent, idx: number) => {
const file = cloneDeep(sortedFiles.value[idx])
const names = [file.name]
@ -188,21 +203,104 @@ const onFileDragStart = (e: DragEvent, idx: number) => {
includeDir = selectedFiles.some(v => v.type === 'dir')
}
const basePath = getBasePath()
e.dataTransfer!.setData(
'text/plain',
JSON.stringify({
from: props.target,
includeDir,
path: uniq(names).map(name => path.join(...basePath, name))
path: uniq(names).map(name => path.join(currLocation.value, name))
})
)
}
const onDrop = async (e: DragEvent) => {
type Data = {
from: typeof props.target
path: string[],
includeDir: boolean
}
const data = JSON.parse(e.dataTransfer?.getData('text') || '{}') as Data
console.log(data)
if (data.from && data.path && typeof data.includeDir !== 'undefined') {
if (data.from === props.target) {
return
}
const type = data.from === 'local' ? 'upload' : 'download'
const typeZH = type === 'upload' ? '上传' : '下载'
const toPath = currLocation.value
const content = h('div', [
h('div', `${props.target !== 'local' ? '本地' : '云盘'} `),
h('ol', data.path.map(v => v.split(/[/\\]/).pop()).map(v => h('li', v))),
h('div', `${typeZH} ${props.target === 'local' ? '本地' : '云盘'} ${toPath}`)
])
Modal.confirm({
title: `确定创建${typeZH}任务${data.includeDir ? ', 这是文件夹或者包含文件夹!' : ''}`,
content,
maskClosable: true,
async onOk () {
global.eventEmitter.emit('createNewTask', { send_dirs: data.path, recv_dir: toPath, type })
}
})
}
}
return {
onFileDragStart,
onDrop,
multiSelectedIdxs
}
}
function useFileItemActions () {
const onFileItemClick = async (e: MouseEvent, file: FileNodeInfo) => {
const files = sortedFiles.value
const idx = files.findIndex(v => v.name === file.name)
if (e.shiftKey) {
multiSelectedIdxs.value.push(idx)
multiSelectedIdxs.value.sort((a, b) => a - b)
const first = multiSelectedIdxs.value[0]
const last = multiSelectedIdxs.value[multiSelectedIdxs.value.length - 1]
multiSelectedIdxs.value = range(first, last + 1)
e.stopPropagation()
} else if (e.ctrlKey) {
multiSelectedIdxs.value.push(idx)
e.stopPropagation()
} else {
await openNext(file)
}
}
const onContextMenuClick = async (e: MenuInfo, file: FileNodeInfoR) => {
const url = toRawFileUrl(file)
const copyImgTo = async (tab: ["txt2img", "img2img", "inpaint", "extras"][number]) => {
await setImgPath(file.fullpath) //
const btn = gradioApp().querySelector('#bd_hidden_img_update_trigger')! as HTMLButtonElement
btn.click() //
await delay(500) // workaround
const tabBtn = gradioApp().querySelector(`#bd_hidden_tab_${tab}`) as HTMLButtonElement
tabBtn.click() //
}
switch (e.key) {
case 'openInNewWindow': return window.open(url)
case 'download': return window.open(toRawFileUrl(file, true))
case 'copyPreviewUrl': return copy2clipboard(location.host + url)
case 'send2txt2img': return copyImgTo('txt2img')
case 'send2img2img': return copyImgTo('img2img')
case 'send2inpaint': return copyImgTo('inpaint')
case 'send2extras': return copyImgTo('extras')
}
}
return {
onFileItemClick,
onContextMenuClick
}
}
</script>
<template>
<div ref="el" @dragover.prevent @drop.prevent="onDrop($event)" class="container">
<div class="location-bar">
<div class="breadcrumb">
<a-breadcrumb style="flex: 1">
@ -213,12 +311,11 @@ const onFileDragStart = (e: DragEvent, idx: number) => {
</div>
<div class="actions">
<SearchSelect v-model:value="sortMethod" :conv="sortMethodConv" :options="Object.keys(sortMethodMap)" />
<a class="opt" @click.prevent="refresh"> 刷新 </a>
<a-dropdown v-if="props.target === 'local'">
<a class="ant-dropdown-link opt" @click.prevent>
<a class="opt" @click.prevent>
快速移动
<DownOutlined />
<down-outlined />
</a>
<template #overlay>
<a-menu>
@ -228,17 +325,55 @@ const onFileDragStart = (e: DragEvent, idx: number) => {
</a-menu>
</template>
</a-dropdown>
<a class="opt" @click.prevent="copyLocation">复制路径</a>
<a-dropdown :trigger="['click']" v-model:visible="moreActionsDropdownShow" placement="bottomLeft">
<a class="opt" @click.prevent>
更多
</a>
<template #overlay>
<div @click.stop
style=" width: 384px; background: white; padding: 16px; border-radius: 4px; box-shadow: 0 0 4px #aaa; border: 1px solid #aaa;">
<a-form v-bind="{
labelCol: { span: 6 },
wrapperCol: { span: 18 }
}">
<a-form-item label="查看模式">
<search-select v-model:value="viewMode" :conv="{ value: v => v, text: v => viewModeMap[v as ViewMode] }"
:options="Object.keys(viewModeMap)" />
</a-form-item>
<a-form-item label="排序方法">
<search-select v-model:value="sortMethod" :conv="sortMethodConv"
:options="Object.keys(sortMethodMap)" />
</a-form-item>
<a-form-item>
<a @click.prevent="copyLocation">复制路径</a>
<folder-navigator :loc="currLocation" @to="to" />
</a-form-item>
</a-form>
</div>
</template>
</a-dropdown>
</div>
</div>
<div v-if="currPage" class="view">
<ul class="file-list">
<li class="file" v-for="file, idx in sortedFiles"
:class="{ clickable: file.type === 'dir', selected: multiSelectedIdxs.includes(idx) }" :key="file.name"
draggable="true" @dragstart="onFileDragStart($event, idx)" @click.capture="onFileItemClick($event, file)">
<file-outlined v-if="file.type === 'file'" />
<folder-open-outlined v-else />
<RecycleScroller class="file-list" :items="sortedFiles" :prerender="10"
:item-size="viewMode === 'line' ? 80 : (viewMode === 'grid' ? gridSize : largeGridSize)" key-field="fullpath"
:gridItems="gridItems">
<template v-slot="{ item: file, index: idx }">
<a-dropdown :trigger="['contextmenu']">
<li class="file"
:class="{ clickable: file.type === 'dir', selected: multiSelectedIdxs.includes(idx), grid: viewMode === 'grid', 'large-grid': viewMode === 'large-size-grid' }"
:key="file.name" draggable="true" @dragstart="onFileDragStart($event, idx)"
@click.capture="onFileItemClick($event, file)">
<a-image :key="file.fullpath"
v-if="props.target === 'local' && viewMode !== 'line' && isImageFile(file.name)"
:src="global.enableThumbnail ? toImageThumbnailUrl(file, viewMode === 'grid' ? void 0 : '512,512') : toRawFileUrl(file)"
:fallback="fallbackImage" :preview="{ src: toRawFileUrl(file) }">
</a-image>
<template v-else>
<file-outlined class="icon" v-if="file.type === 'file'" />
<folder-open-outlined class="icon" v-else />
<div class="name">
{{ file.name }}
</div>
@ -250,8 +385,22 @@ const onFileDragStart = (e: DragEvent, idx: number) => {
{{ file.date }}
</div>
</div>
</template>
</li>
</ul>
<template #overlay>
<a-menu v-if="props.target === 'local' && file.type === 'file'" @click="onContextMenuClick($event, file)">
<a-menu-item key="openInNewWindow">在新窗口预览如果浏览器处理不了会下载大文件的话谨慎</a-menu-item>
<a-menu-item key="download">直接下载大文件的话谨慎</a-menu-item>
<a-menu-item key="copyPreviewUrl">复制源文件预览链接</a-menu-item>
<a-menu-item key="send2txt2img">发送到文生图</a-menu-item>
<a-menu-item key="send2img2img">发送到图生图</a-menu-item>
<a-menu-item key="send2inpaint">发送到局部重绘</a-menu-item>
<a-menu-item key="send2extras">发送到附加功能</a-menu-item>
</a-menu>
</template>
</a-dropdown>
</template>
</RecycleScroller>
</div>
</div>
</template>
@ -271,6 +420,7 @@ const onFileDragStart = (e: DragEvent, idx: number) => {
align-items: center;
flex-shrink: 0;
}
a.opt {
@ -295,6 +445,74 @@ const onFileDragStart = (e: DragEvent, idx: number) => {
background: white;
border-radius: 8px;
box-shadow: 0 0 4px #ccc;
position: relative;
&.grid {
padding: 8px;
height: 256px;
width: 256px;
margin: 16px;
display: inline-block;
box-sizing: content-box;
:deep() {
.icon {
font-size: 6em;
margin-top: 16px;
}
.name {
margin: 16px 0;
}
.basic-info {
position: absolute;
bottom: 16px;
right: 16px;
}
img {
height: 256px;
width: 256px;
object-fit: contain;
}
}
}
&.large-grid {
padding: 8px;
height: 512px;
width: 512px;
margin: 16px;
display: inline-block;
box-sizing: content-box;
:deep() {
.icon {
font-size: 6em;
margin-top: 16px;
}
.name {
margin: 16px 0;
}
.basic-info {
position: absolute;
bottom: 16px;
right: 16px;
}
img {
height: 512px;
width: 512px;
object-fit: contain;
}
}
}
&.clickable {
cursor: pointer;
@ -307,6 +525,7 @@ const onFileDragStart = (e: DragEvent, idx: number) => {
.name {
flex: 1;
padding: 8px;
word-break: break-all
}
.basic-info {

View File

@ -7,10 +7,17 @@ import { typedEventEmitter } from 'vue3-ts-util'
export const useGlobalStore = defineStore('useGlobalStore', () => {
const conf = ref<GlobalConf>()
const autoCompletedDirList = ref([] as ReturnType<typeof getAutoCompletedTagList>)
const enableThumbnail = ref(true)
const stackViewSplit = ref(50)
return {
conf,
autoCompletedDirList,
enableThumbnail,
stackViewSplit,
...typedEventEmitter<{ createNewTask: Partial<UploadTaskSummary> }>()
}
}, {
persist: {
paths: ['enableThumbnail', 'stackViewSplit']
}
})

View File

@ -24,19 +24,19 @@ export const getAutoCompletedTagList = ({ global_setting, sd_cwd, home }: Return
}
type Keys = keyof (typeof allTag)
const cnMap: Record<Keys, string> = {
outdir_txt2img_samples: '文生图的输出目录',
outdir_img2img_samples: '图生图的输出目录',
outdir_save: '使用“保存”按钮保存图像的目录',
outdir_extras_samples: '附加功能选项卡的输出目录',
additional_networks_extra_lora_path: '扫描 LoRA 模型的附加目录',
outdir_grids: '宫格图的输出目录',
outdir_extras_samples: '附加功能选项卡的输出目录',
outdir_img2img_grids: '图生图网格文件夹',
outdir_img2img_samples: '图生图的输出目录',
outdir_samples: '图像的输出目录',
outdir_txt2img_samples: '文生图的输出目录',
outdir_txt2img_grids: '文生图宫格的输出目录',
hypernetworks: '超网络模型的路径',
outdir_save: '使用“保存”按钮保存图像的目录',
embeddings: 'Embedding的文件夹',
cwd: '工作文件夹',
home: 'home'
home: 'home',
}
return Object.keys(cnMap).map((k) => {
const key = k as Keys

View File

@ -1,7 +1,7 @@
import { idKey, type UniqueId } from 'vue3-ts-util'
export function gradioApp() {
const elems = document.getElementsByTagName('gradio-app')
const elems = parent.document.getElementsByTagName('gradio-app')
const gradioShadowRoot = elems.length == 0 ? null : elems[0].shadowRoot
return gradioShadowRoot ? gradioShadowRoot : document;
}
@ -41,3 +41,9 @@ export const pick = <T extends Dict, keys extends Array<keyof T>> (v: T, ...keys
* ReturnTypeAsync\<typeof fn\>
*/
export type ReturnTypeAsync<T extends (...arg: any) => Promise<any>> = Awaited<ReturnType<T>>
export function isImageFile(filename: string): boolean {
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'];
const extension = filename.split('.').pop()?.toLowerCase();
return extension !== undefined && imageExtensions.includes(`.${extension}`);
}

View File

@ -2008,6 +2008,11 @@ minimatch@^7.4.2:
dependencies:
brace-expansion "^2.0.1"
mitt@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/mitt/-/mitt-2.1.0.tgz#f740577c23176c6205b121b2973514eade1b2230"
integrity sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==
ms@2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
@ -2755,6 +2760,16 @@ vue-eslint-parser@^9.0.0, vue-eslint-parser@^9.0.1:
lodash "^4.17.21"
semver "^7.3.6"
vue-observe-visibility@^2.0.0-alpha.1:
version "2.0.0-alpha.1"
resolved "https://registry.yarnpkg.com/vue-observe-visibility/-/vue-observe-visibility-2.0.0-alpha.1.tgz#1e4eda7b12562161d58984b7e0dea676d83bdb13"
integrity sha512-flFbp/gs9pZniXR6fans8smv1kDScJ8RS7rEpMjhVabiKeq7Qz3D9+eGsypncjfIyyU84saU88XZ0zjbD6Gq/g==
vue-resize@^2.0.0-alpha.1:
version "2.0.0-alpha.1"
resolved "https://registry.yarnpkg.com/vue-resize/-/vue-resize-2.0.0-alpha.1.tgz#43eeb79e74febe932b9b20c5c57e0ebc14e2df3a"
integrity sha512-7+iqOueLU7uc9NrMfrzbG8hwMqchfVfSzpVlCMeJQe4pyibqyoifDNbKTZvwxZKDvGkB+PdFeKvnGZMoEb8esg==
vue-router@^4.1.6:
version "4.1.6"
resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.1.6.tgz#b70303737e12b4814578d21d68d21618469375a1"
@ -2785,6 +2800,15 @@ vue-types@^3.0.0:
dependencies:
is-plain-object "3.0.1"
vue-virtual-scroller@^2.0.0-beta.8:
version "2.0.0-beta.8"
resolved "https://registry.yarnpkg.com/vue-virtual-scroller/-/vue-virtual-scroller-2.0.0-beta.8.tgz#eeceda57e4faa5ba1763994c873923e2a956898b"
integrity sha512-b8/f5NQ5nIEBRTNi6GcPItE4s7kxNHw2AIHLtDp+2QvqdTjVN0FgONwX9cr53jWRgnu+HRLPaWDOR2JPI5MTfQ==
dependencies:
mitt "^2.1.0"
vue-observe-visibility "^2.0.0-alpha.1"
vue-resize "^2.0.0-alpha.1"
vue3-ts-util@^0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/vue3-ts-util/-/vue3-ts-util-0.8.2.tgz#d0d9dfa6077c65bd124ba5fcf0e4ed32906be3ab"