feat: 支持预览图片,发送图片到文生图,图生图等。支持下载其他文件,支持缩略图,改用虚拟列表
parent
d758a2978a
commit
892e952b62
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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():
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
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
File diff suppressed because one or more lines are too long
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
'使用“快速移动”, 可以直接到到达图生图,文生图等文件夹',
|
||||
"你可以通过拖拽调整两边区域的大小",
|
||||
"使用ctrl,shift可以很方便的进行多选",
|
||||
"从百度云向本地拖拽是下载,本地向百度云拖拽是上传",
|
||||
"可以多尝试更多里面的功能",
|
||||
"鼠标在文件上右键可以打开上下文菜单",
|
||||
"提醒任务完成后,你需要手动刷新下才能看到新文件"]
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)}`)
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
}
|
||||
})
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}`);
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in New Issue