百度云独立出去

pull/43/head
zanllp 2023-04-23 22:13:31 +08:00
parent f9867e5a64
commit 21668a1dfe
67 changed files with 303 additions and 1712 deletions

View File

@ -2,10 +2,10 @@
# Stable-Diffusion-WebUI无边图像浏览
高性能的图片(文件)浏览器😋。它适合在所有地方使用,针对云端还做了优化,你可以使用缩略图进行更快的预览和使用自带百度云进行文件传输
高性能的图片(文件)浏览器😋。它适合在所有地方使用,针对云端还做了优化,你可以使用缩略图进行更快的预览。
如果您对该项目有任何疑问或建议请在GitHub上提交issue或者看下最下面的FAQ部分。
> 百度云部分已独立,如果你有需要请[点此单独安装](https://github.com/zanllp/sd-webui-baidu-netdisk)
## 主要特性
- 类chrome,vscode的多标签页多窗格。自由拖拽创建同时预览多个文件夹在多窗格之间移动文件

View File

@ -8,8 +8,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="/infinite_image_browsing/fe-static/assets/index-6eb64f08.js"></script>
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-9ee28aec.css">
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-8bb7713e.js"></script>
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-92aac6a6.css">
</head>
<body>

View File

@ -1,22 +1,19 @@
from datetime import datetime, timedelta
import os
import shutil
import time
from scripts.tool import (
human_readable_size,
is_valid_image_path,
temp_path,
read_info_from_image,
get_modified_date
get_modified_date,
is_win,
cwd
)
from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
import re
import subprocess
import asyncio
import subprocess
from typing import Any, List, Literal, Optional, Union
from scripts.baiduyun_task import BaiduyunTask
from typing import Any, List, Literal, Optional
from pydantic import BaseModel
from fastapi.responses import FileResponse, RedirectResponse
from PIL import Image
@ -25,118 +22,10 @@ import hashlib
from urllib.parse import urlencode
from scripts.db.datamodel import DataBase, Image as DbImg, Tag, Floder, ImageTag
from scripts.db.update_image_data import update_image_data
from scripts.bin import (
bin_file_name,
get_matched_summary,
check_bin_exists,
download_bin_file,
)
from scripts.bin import (
check_bin_exists,
cwd,
bin_file_path,
is_win,
)
from scripts.tool import get_windows_drives, convert_to_bytes
import functools
from scripts.tool import get_windows_drives
from scripts.logger import logger
class AutoUpload:
# 已成等待发送图像的队列
files = []
task_id: Union[None, str] = None
def exec_ops(args: Union[List[str], str]):
args = [args] if isinstance(args, str) else args
res = ""
if check_bin_exists():
result = subprocess.run([bin_file_path, *args], capture_output=True)
try:
res = result.stdout.decode().strip()
except UnicodeDecodeError:
res = result.stdout.decode("gbk", errors="ignore").strip()
if args[0] != "ls":
logger.info(res)
return res
def login_by_bduss(bduss: str):
output = exec_ops(["login", f"-bduss={bduss}"])
match = re.search("百度帐号登录成功: (.+)$", output)
if match:
return {"status": "ok", "msg": match.group(1).strip()}
else:
return {"status": "error", "msg": output}
def get_curr_working_dir():
return exec_ops("pwd")
def list_file(cwd="/"):
output = exec_ops(["ls", cwd])
pattern = re.compile(
r"\s+(\d+)\s+([\w\-.]+)\s+(\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2})\s+(.*)"
)
if output.find("获取目录下的文件列表: 网络错误") != -1:
raise Exception("获取目录下的文件列表: 网络错误")
files = []
for line in output.split("\n"):
match = re.match(pattern, line)
if match:
name = match.group(4).strip()
f_type = "dir" if name.endswith("/") else "file"
size = match.group(2)
name = name.strip("/")
file_info = {
"size": size,
"date": match.group(3),
"name": name,
"type": f_type,
"bytes": convert_to_bytes(size) if size != "-" else size,
"fullpath": f"{cwd}/{name}",
}
files.append(file_info)
return files
def get_curr_user():
match = re.search(
r"uid:\s*(\d+), 用户名:\s*(\w+),",
exec_ops("who"),
)
if not match:
return
uid = match.group(1)
if int(uid) == 0:
return
username = match.group(2)
return {"uid": uid, "username": username}
def logout():
match = re.search("退出用户成功", exec_ops(["logout", "-y"]))
return bool(match)
def singleton_async(fn):
@functools.wraps(fn)
async def wrapper(*args, **kwargs):
key = args[0] if len(args) > 0 else None
if key in wrapper.busy:
raise Exception("Function is busy, please try again later.")
wrapper.busy.append(key)
try:
return await fn(*args, **kwargs)
finally:
wrapper.busy.remove(key)
wrapper.busy = []
return wrapper
send_img_path = {"value": ""}
@ -150,24 +39,6 @@ def infinite_image_browsing_api(_: Any, app: FastAPI):
name="infinite_image_browsing-fe-static",
)
@app.get(f"{pre}/user")
async def user():
return get_curr_user()
@app.post(f"{pre}/user/logout")
async def user_logout():
return logout()
class BaiduyunUserLoginReq(BaseModel):
bduss: str
@app.post(f"{pre}/user/login")
async def user_login(req: BaiduyunUserLoginReq):
res = login_by_bduss(req.bduss)
if res["status"] != "ok":
raise HTTPException(status_code=401, detail=res["msg"])
return get_curr_user()
@app.get(f"{pre}/hello")
async def greeting():
return "hello"
@ -193,63 +64,6 @@ def infinite_image_browsing_api(_: Any, app: FastAPI):
send_dirs: List[str]
recv_dir: str
@app.post(f"{pre}/task")
async def upload(req: BaiduyunUploadDownloadReq):
task = await BaiduyunTask.create(**req.dict())
return {"id": task.id}
@app.get(f"{pre}/tasks")
async def upload_tasks():
tasks = []
for key in BaiduyunTask.get_cache():
task = BaiduyunTask.get_by_id(key)
task.update_state()
tasks.append(task.get_summary())
return {"tasks": list(reversed(tasks))}
@app.delete(pre + "/task/{id}")
async def remove_task_cache(id: str):
c = BaiduyunTask.get_cache()
if id in c:
c.pop(id)
@app.get(pre + "/task/{id}/files_state")
async def task_files_state(id):
p = BaiduyunTask.get_by_id(id)
if not p:
raise HTTPException(status_code=404, detail="找不到该上传任务")
return {"files_state": p.files_state}
@app.post(pre + "/task/{id}/cancel")
async def cancel_task(id):
p = BaiduyunTask.get_by_id(id)
if not p:
raise HTTPException(status_code=404, detail="找不到该上传任务")
last_tick = await p.cancel()
return {"last_tick": last_tick}
upload_poll_promise_dict = {}
@app.get(pre + "/task/{id}/tick")
async def upload_poll(id):
async def get_tick_sync_wait_wrapper():
task = BaiduyunTask.get_by_id(id)
if not task:
raise HTTPException(status_code=404, detail="找不到该上传任务")
return await task.get_tick()
res = upload_poll_promise_dict.get(id)
if res:
res = await res
else:
upload_poll_promise_dict[id] = asyncio.create_task(
get_tick_sync_wait_wrapper()
)
res = await upload_poll_promise_dict[id]
upload_poll_promise_dict.pop(id)
return res
class DeleteFilesReq(BaseModel):
file_paths: List[str]
@ -266,7 +80,7 @@ def infinite_image_browsing_api(_: Any, app: FastAPI):
# 处理删除失败的情况
raise HTTPException(400, detail=f"删除文件{path}时出错:{e}")
else:
exec_ops(["rm", *req.file_paths]) # 没检查是否失败,暂时先这样
pass
class MoveFilesReq(BaseModel):
file_paths: List[str]
@ -281,7 +95,7 @@ def infinite_image_browsing_api(_: Any, app: FastAPI):
except OSError as e:
raise HTTPException(400, detail=f"移动文件{path}{req.dest}时出错:{e}")
else:
exec_ops(["mv", *req.file_paths, req.dest]) # 没检查是否失败,暂时先这样
pass
@app.get(pre + "/files/{target}")
async def get_target_floder_files(
@ -329,7 +143,7 @@ def infinite_image_browsing_api(_: Any, app: FastAPI):
}
)
else:
files = list_file(folder_path)
pass
except Exception as e:
logger.error(e)
@ -457,6 +271,8 @@ def infinite_image_browsing_api(_: Any, app: FastAPI):
for _ in range(600): # 等待60s
if send_img_path["value"] == "": # 等待setup里面生成完成
return True
v = send_img_path["value"]
logger.info("gen_info_completed %s %s",_,v )
await asyncio.sleep(0.1)
return send_img_path["value"] == ""
@ -465,26 +281,6 @@ def infinite_image_browsing_api(_: Any, app: FastAPI):
with Image.open(path) as img:
return read_info_from_image(img)
class AutoUploadParams(BaseModel):
recv_dir: str
@app.post(pre + "/auto_upload")
async def auto_upload(req: AutoUploadParams):
tick_info = None
if AutoUpload.task_id:
task = BaiduyunTask.get_by_id(AutoUpload.task_id)
tick_info = await task.get_tick()
if not task.running:
AutoUpload.task_id = None
else:
recived_file = AutoUpload.files
AutoUpload.files = []
if len(recived_file):
logger.info(f"创建上传任务 {recived_file} ----> {req.recv_dir}")
task = await BaiduyunTask.create("upload", recived_file, req.recv_dir)
AutoUpload.task_id = task.id
return {"tick_info": tick_info, "pending_files": AutoUpload.files}
class CheckPathExistsReq(BaseModel):
paths: List[str]
@ -495,24 +291,10 @@ def infinite_image_browsing_api(_: Any, app: FastAPI):
res[path] = os.path.exists(path)
return res
@app.get(pre + "/baiduyun_exists")
async def baiduyun_exists():
return check_bin_exists()
@app.get(pre)
def index_bd():
return FileResponse(os.path.join(cwd, "vue/dist/index.html"))
@app.post(pre + "/download_baiduyun")
async def download_baiduyun():
if not check_bin_exists():
try:
download_bin_file()
except:
raise HTTPException(
500,
detail=f"安装失败,找不到{bin_file_name},尝试手动从 {get_matched_summary()[1]} 或者 {get_matched_summary()[2]} 下载,下载后放到 {cwd} 文件夹下,重启界面",
)
db_pre = pre + "/db"

View File

@ -1,170 +0,0 @@
import asyncio
import datetime
import os
from typing import Dict, List, Literal
import uuid
import re
import subprocess
from scripts.log_parser import parse_log_line
from scripts.bin import bin_file_path
from scripts.logger import logger
from scripts.tool import is_dev
class BaiduyunTask:
def __init__(
self,
subprocess: asyncio.subprocess.Process,
type: Literal["upload", "download"],
send_dirs: List[str],
recv_dir: str,
):
self.subprocess = subprocess
self.id = str(uuid.uuid4())
self.start_time = datetime.datetime.now()
self.running = True
self.logs = []
self.raw_logs = []
self.files_state = {}
self.type = type
self.send_dirs = send_dirs
self.recv_dir = recv_dir
self.n_files = 0
self.n_success_files = 0
self.n_failed_files = 0
self.canceled = False
def start_time_human_readable(self):
return self.start_time.strftime("%Y-%m-%d %H:%M:%S")
def update_state(self):
self.n_files = 0
self.n_success_files = 0
self.n_failed_files = 0
for key in self.files_state:
status = self.files_state[key]["status"]
self.n_files += 1
if status == "upload-success" or status == "file-skipped" or status == "download-success":
self.n_success_files += 1
elif status == "upload-failed":
self.n_failed_files += 1
self.running = False if self.canceled else not isinstance(self.subprocess.returncode, int)
def append_log(self, parsed_log, raw_log):
self.raw_logs.append(raw_log)
self.logs.append(parsed_log)
if isinstance(parsed_log, dict) and "id" in parsed_log:
self.files_state[parsed_log["id"]] = parsed_log
def get_summary(task):
return {
"type": task.type,
"id": task.id,
"running": task.running,
"start_time": task.start_time_human_readable(),
"recv_dir": task.recv_dir,
"send_dirs": task.send_dirs,
"n_files": task.n_files,
"n_failed_files": task.n_failed_files,
"n_success_files": task.n_success_files,
"canceled": task.canceled
}
#
async def get_tick(self):
tasks = []
while True:
try:
line = await asyncio.wait_for(
self.subprocess.stdout.readline(), timeout=0.1
)
line = line.decode()
logger.info(line)
if not line:
break
if line.isspace():
continue
info = parse_log_line(line, self.type == "upload")
if is_dev and line.find("md5") != -1:
line = ''
tasks.append({"info": info, "log": line})
self.append_log(info, line)
except asyncio.TimeoutError:
break
self.update_state()
return {"tasks": tasks, "task_summary": self.get_summary()}
async def cancel(self):
self.subprocess.terminate()
self.canceled = True
last_tick = await self.get_tick()
return last_tick
@staticmethod
async def create(
type: Literal["upload", "download"], send_dirs: List[str], recv_dir: str
):
if type == "upload" :
process = await asyncio.create_subprocess_exec(
bin_file_path,
type,
*process_path_arr(send_dirs),
parse_and_replace_time(recv_dir),
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
elif type == "download":
process = await asyncio.create_subprocess_exec(
bin_file_path,
type,
*process_path_arr(send_dirs),
"--saveto",
parse_and_replace_time(recv_dir),
"-p",
"4",
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
else:
raise Exception("unknown task type")
task = BaiduyunTask(process, type, send_dirs, recv_dir)
task.update_state()
baiduyun_task_cache[task.id] = task
return task
@staticmethod
def get_by_id(id: str):
return baiduyun_task_cache.get(id)
@staticmethod
def get_cache():
return baiduyun_task_cache
baiduyun_task_cache: Dict[str, BaiduyunTask] = {}
def process_path_arr(path_arr):
"""
处理路径顺便替换模板
如果是绝对路径直接返回
如果是相对路径则与当前工作目录拼接返回
"""
cwd = os.getcwd()
result = []
for path in path_arr:
if os.path.isabs(path):
result.append(path)
else:
result.append(os.path.join(cwd, path))
return list(map(parse_and_replace_time, result))
def parse_and_replace_time(s):
pattern = r'<#(.+?)#>'
matches = re.findall(pattern, s)
for match in matches:
formatted_time = datetime.datetime.now().strftime(match)
s = s.replace(f'<#{match}#>', formatted_time)
return s

View File

@ -1,64 +0,0 @@
import urllib.request
import zipfile
import os
import platform
from scripts.tool import cwd,is_win
bin_file_name = "BaiduPCS-Go.exe" if is_win else "BaiduPCS-Go"
bin_file_path = os.path.join(cwd, bin_file_name)
def check_bin_exists():
return os.path.exists(bin_file_path)
def get_matched_summary():
system = platform.system()
machine = platform.machine()
if system == "Darwin":
if machine == "x86_64":
file_name = "BaiduPCS-Go-v3.9.0-darwin-osx-amd64"
elif machine == "arm64":
file_name = "BaiduPCS-Go-v3.9.0-darwin-osx-arm64"
elif system == "Linux":
if machine == "i386":
file_name = "BaiduPCS-Go-v3.9.0-linux-386"
elif machine == "x86_64":
file_name = "BaiduPCS-Go-v3.9.0-linux-amd64"
elif system == "Windows":
if machine == "AMD64":
file_name = "BaiduPCS-Go-v3.9.0-windows-x64"
elif machine == "x86":
file_name = "BaiduPCS-Go-v3.9.0-windows-x86"
if not file_name:
raise Exception(f"找不到对应的文件,请携带此信息找开发者 machine:{machine} system:{system}")
return file_name, f"https://github.com/qjfoidnh/BaiduPCS-Go/releases/download/v3.9.0/{file_name}.zip", f"http://static.zanllp.cn/{file_name}.zip"
def download_bin_file():
summary, url, fallback_url = get_matched_summary()
# 下载文件保存路径
download_path = "BaiduPCS-Go.zip"
try:
# 下载文件并保存
urllib.request.urlretrieve(url, download_path)
except:
urllib.request.urlretrieve(fallback_url, download_path)
# 解压缩
with zipfile.ZipFile(download_path, "r") as zip_ref:
zip_ref.extractall()
# 移动文件夹到当前目录下
os.rename(os.path.join(summary, bin_file_name), bin_file_path)
try:
os.chmod(bin_file_path, 0o755) # unix only
except Exception:
pass
# 删除下载的压缩包和空的文件夹
os.remove(download_path)
os.remove(os.path.join(summary, "README.md"))
os.rmdir(summary)

View File

@ -1,50 +0,0 @@
import re
upload_regex = re.compile(
r"\[(\d+)\] (上传文件失败|秒传失败|上传文件成功|准备上传|目标文件|加入上传队列)(?:\:|,)?(.*)"
)
upload_success_extra_re = re.compile(r"保存到网盘路径: (.*)$")
upload_concurrent_re = re.compile(r"上传单个文件最大并发量为: (\d+), 最大同时上传文件数为: (\d+)")
download_regex = re.compile(r"\[(\d+)\] (加入下载队列|准备下载|下载完成, 保存位置|文件已经存在)(?:\:|,)?(.*)")
def parse_log_line(line_str: str, is_upload=True):
match = upload_regex.match(line_str) if is_upload else download_regex.match(line_str)
if not match:
if line_str.startswith("上传结束") or line_str.startswith("下载结束"):
return {"status": "done", "extra_info": line_str}
concurrent_match = upload_concurrent_re.search(line_str)
if concurrent_match:
return {"status": "start", "concurrent": int(concurrent_match.group(1))}
return
line = {
"id": match.group(1),
"status": {
"秒传失败": "fast-upload-failed",
"上传文件成功": "upload-success",
"准备上传": "upload-preparing",
"目标文件": "file-skipped",
"加入上传队列": "queued",
"上传文件失败": "upload-failed",
"准备下载": "download-preparing",
"文件已经存在": "file-skipped",
"下载完成, 保存位置": "download-success",
"加入下载队列": "queued",
}.get(match.group(2)),
}
extra_info = match.group(3).strip()
if is_upload:
if line["status"] == "upload-success":
line["remote_path"] = upload_success_extra_re.match(
extra_info
).group(1)
elif line["status"] == "upload-preparing":
line["local_path"] = extra_info
elif line["status"] == "upload-failed":
line["extra_info"] = extra_info
else:
if line["status"] == "download-success":
line["local_path"] = extra_info
elif line["status"] == "download-preparing":
line["remote_path"] = extra_info
return line

View File

@ -1,7 +1,9 @@
from scripts.api import infinite_image_browsing_api, send_img_path, AutoUpload
from modules import script_callbacks, generation_parameters_copypaste as send, extras
from scripts.api import infinite_image_browsing_api, send_img_path
from modules import script_callbacks, generation_parameters_copypaste as send
from scripts.tool import locale
from scripts.tool import read_info_from_image
from PIL import Image
from scripts.logger import logger
"""
@ -9,56 +11,62 @@ api函数声明和启动分离方便另外一边被外部调用
"""
def on_ui_tabs():
import gradio as gr
with gr.Blocks(analytics_enabled=False) as view:
with gr.Row():
with gr.Column():
gr.HTML(
"如果你看到这个那说明此项那说明出现了问题", elem_id="infinite_image_browsing_container_wrapper"
)
gr.HTML("error", elem_id="infinite_image_browsing_container_wrapper")
# 以下是使用2个组件模拟粘贴过程
img = gr.Image(
type="pil",
elem_id="bd_hidden_img",
elem_id="iib_hidden_img",
)
def on_img_change():
send_img_path["value"] = '' # 真正收到图片改变才允许放行
send_img_path["value"] = "" # 真正收到图片改变才允许放行
img.change(on_img_change)
img_update_trigger = gr.Button("button", elem_id="bd_hidden_img_update_trigger")
img_update_trigger = gr.Button(
"button", elem_id="iib_hidden_img_update_trigger"
)
# 修改文本和图像,等待修改完成后前端触发粘贴按钮
# 有时在触发后收不到回调可能是在解析params。txt时除了问题删除掉就行了
def img_update_func():
try:
path = send_img_path.get("value")
geninfo,_ = extras.images.read_info_from_image(Image.open(path))
return path, geninfo
logger.info("img_update_func %s", path)
img = Image.open(path)
info = read_info_from_image(img)
return img, info
except Exception as e:
logger.error("img_update_func % c",e)
img_file_info = gr.Textbox(elem_id="bd_hidden_img_file_info")
img_file_info = gr.Textbox(elem_id="iib_hidden_img_file_info")
img_update_trigger.click(img_update_func, outputs=[img, img_file_info])
for tab in ["txt2img", "img2img", "inpaint", "extras"]:
btn = gr.Button(f"Send to {tab}", elem_id=f"bd_hidden_tab_{tab}")
btn = gr.Button(f"Send to {tab}", elem_id=f"iib_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
source_text_component=img_file_info,
)
)
return ((view, "无边图像浏览" if locale == "zh" else "Infinite image browsing", "infinite-image-browsing"),)
def on_img_saved(params: script_callbacks.ImageSaveParams):
AutoUpload.files.append(params.filename)
return (
(
view,
"无边图像浏览" if locale == "zh" else "Infinite image browsing",
"infinite-image-browsing",
),
)
script_callbacks.on_ui_tabs(on_ui_tabs)
script_callbacks.on_app_started(infinite_image_browsing_api)
script_callbacks.on_image_saved(on_img_saved)

View File

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

10
vue/components.d.ts vendored
View File

@ -9,13 +9,9 @@ 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']
ACol: typeof import('ant-design-vue/es')['Col']
ACollapse: typeof import('ant-design-vue/es')['Collapse']
ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel']
ADropdown: typeof import('ant-design-vue/es')['Dropdown']
AForm: typeof import('ant-design-vue/es')['Form']
AFormItem: typeof import('ant-design-vue/es')['FormItem']
@ -25,19 +21,13 @@ declare module '@vue/runtime-core' {
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']
ARow: typeof import('ant-design-vue/es')['Row']
ASelect: typeof import('ant-design-vue/es')['Select']
ASelectOption: typeof import('ant-design-vue/es')['SelectOption']
ASkeleton: typeof import('ant-design-vue/es')['Skeleton']
ASpin: typeof import('ant-design-vue/es')['Spin']
AStatistic: typeof import('ant-design-vue/es')['Statistic']
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']
ATextarea: typeof import('ant-design-vue/es')['Textarea']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
}

View File

@ -1 +0,0 @@
import{B as s}from"./button-3e5cfde3.js";import{d as g,u as O,G as C,_ as u,a as m,a3 as y,D as h}from"./index-6eb64f08.js";import{_ as E,a as _}from"./styleChecker-572b0d40.js";var z=E(function e(t){_(this,e),this.error=new Error("unreachable case: ".concat(JSON.stringify(t)))}),P=function(){return{prefixCls:String,size:{type:String}}};const l=g({compatConfig:{MODE:3},name:"AButtonGroup",props:P(),setup:function(t,n){var a=n.slots,o=O("btn-group",t),i=o.prefixCls,v=o.direction,b=C(function(){var r,p=t.size,c="";switch(p){case"large":c="lg";break;case"small":c="sm";break;case"middle":case void 0:break;default:console.warn(new z(p).error)}return r={},u(r,"".concat(i.value),!0),u(r,"".concat(i.value,"-").concat(c),c),u(r,"".concat(i.value,"-rtl"),v.value==="rtl"),r});return function(){var r;return m("div",{class:b.value},[y((r=a.default)===null||r===void 0?void 0:r.call(a))])}}});s.Group=l;s.install=function(e){return e.component(s.name,s),e.component(l.name,l),e};var w={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z"}}]},name:"eye",theme:"outlined"};const x=w;function d(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]!=null?Object(arguments[t]):{},a=Object.keys(n);typeof Object.getOwnPropertySymbols=="function"&&(a=a.concat(Object.getOwnPropertySymbols(n).filter(function(o){return Object.getOwnPropertyDescriptor(n,o).enumerable}))),a.forEach(function(o){S(e,o,n[o])})}return e}function S(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}var f=function(t,n){var a=d({},t,n.attrs);return m(h,d({},a,{icon:x}),null)};f.displayName="EyeOutlined";f.inheritAttrs=!1;const $=f;export{$ as E};

File diff suppressed because one or more lines are too long

3
vue/dist/assets/FileItem-c8b1e15c.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
import{d as z,r as v,J as V,b0 as A,ad as D,K as G,L as N,a,N as o,M as e,ac as _,V as x,U as k,R as B,br as F,S as R,a0 as U}from"./index-6eb64f08.js";import{u as $,b as E,f as q,d as O,i as Q,j,t as H,M as J,S as K}from"./FileItem-ba9b9206.js";import{g as L}from"./db-2f654f91.js";import{c as P}from"./copy2clipboard-b3ef2142.js";import"./index-8b6f769c.js";import"./styleChecker-572b0d40.js";import"./useTaskListStore-db67ad53.js";import"./EyeOutlined-652d774a.js";import"./button-3e5cfde3.js";const W={class:"hint"},X=z({__name:"MatchedImageGrid",props:{tabIdx:null,paneIdx:null,selectedTagIds:null,id:null},setup(w){const c=w,p=v(),u=V(new A);D(()=>c.selectedTagIds,async()=>{var t;const{res:l}=u.pushAction(()=>L(c.selectedTagIds));p.value=(await l).sort((r,d)=>Date.parse(d.date)-Date.parse(r.date)),(t=m.value)==null||t.scrollToItem(0)},{immediate:!0});const m=v(),f={tabIdx:-1,target:"local",paneIdx:-1},{stackViewEl:b}=$().toRefs(),{itemSize:g,gridItems:h}=E(f),{showMenuIdx:n}=q(),{showGenInfo:i,imageGenInfo:I,q:y,onContextMenuClick:M}=O(f,{openNext:F});return(l,t)=>{const r=R,d=J,C=K;return G(),N("div",{class:"container",ref_key:"stackViewEl",ref:b},[a(C,{size:"large",spinning:!u.isIdle},{default:o(()=>[a(d,{visible:e(i),"onUpdate:visible":t[1]||(t[1]=s=>_(i)?i.value=s:null),width:"70vw","mask-closable":"",onOk:t[2]||(t[2]=s=>i.value=!1)},{cancelText:o(()=>[]),default:o(()=>[a(r,{active:"",loading:!e(y).isIdle},{default:o(()=>[x("div",{style:{width:"100%","word-break":"break-all","white-space":"pre-line","max-height":"70vh",overflow:"auto"},onDblclick:t[0]||(t[0]=s=>e(P)(e(I),"copied"))},[x("div",W,k(l.$t("doubleClickToCopy")),1),B(" "+k(e(I)),1)],32)]),_:1},8,["loading"])]),_:1},8,["visible"]),a(e(Q),{ref_key:"scroller",ref:m,class:"file-list",items:p.value||[],"item-size":e(g).first,"key-field":"fullpath","item-secondary-size":e(g).second,gridItems:e(h)},{default:o(({item:s,index:S})=>[a(j,{idx:S,file:s,"show-menu-idx":e(n),"onUpdate:showMenuIdx":t[3]||(t[3]=T=>_(n)?n.value=T:null),"full-screen-preview-image-url":e(H)(s),onContextMenuClick:e(M)},null,8,["idx","file","show-menu-idx","full-screen-preview-image-url","onContextMenuClick"])]),_:1},8,["items","item-size","item-secondary-size","gridItems"])]),_:1},8,["spinning"])],512)}}});const le=U(X,[["__scopeId","data-v-b2de5e08"]]);export{le as default};

View File

@ -0,0 +1 @@
import{d as z,r as _,aV as V,a_ as A,n as D,K as G,L as N,c as a,N as o,O as e,Q as v,T as k,U as x,Y as B,aQ as F,a6 as Q,a8 as U}from"./index-8bb7713e.js";import{u as $,b as E,f as O,d as R,h as q,j,k as H,t as K,M as L,S as Y}from"./FileItem-c8b1e15c.js";import{g as J}from"./db-94066177.js";import"./index-f11b4f57.js";import"./button-37307691.js";const P={class:"hint"},W=z({__name:"MatchedImageGrid",props:{tabIdx:null,paneIdx:null,selectedTagIds:null,id:null},setup(w){const c=w,u=_(),p=V(new A);D(()=>c.selectedTagIds,async()=>{var s;const{res:l}=p.pushAction(()=>J(c.selectedTagIds));u.value=(await l).sort((d,r)=>Date.parse(r.date)-Date.parse(d.date)),(s=m.value)==null||s.scrollToItem(0)},{immediate:!0});const m=_(),f={tabIdx:-1,target:"local",paneIdx:-1},{stackViewEl:h}=$().toRefs(),{itemSize:g,gridItems:b}=E(f),{showMenuIdx:n}=O(),{showGenInfo:i,imageGenInfo:I,q:y,onContextMenuClick:M}=R(f,{openNext:F});return(l,s)=>{const d=Q,r=L,C=Y;return G(),N("div",{class:"container",ref_key:"stackViewEl",ref:h},[a(C,{size:"large",spinning:!p.isIdle},{default:o(()=>[a(r,{visible:e(i),"onUpdate:visible":s[1]||(s[1]=t=>v(i)?i.value=t:null),width:"70vw","mask-closable":"",onOk:s[2]||(s[2]=t=>i.value=!1)},{cancelText:o(()=>[]),default:o(()=>[a(d,{active:"",loading:!e(y).isIdle},{default:o(()=>[k("div",{style:{width:"100%","word-break":"break-all","white-space":"pre-line","max-height":"70vh",overflow:"auto"},onDblclick:s[0]||(s[0]=t=>e(q)(e(I),"copied"))},[k("div",P,x(l.$t("doubleClickToCopy")),1),B(" "+x(e(I)),1)],32)]),_:1},8,["loading"])]),_:1},8,["visible"]),a(e(j),{ref_key:"scroller",ref:m,class:"file-list",items:u.value||[],"item-size":e(g).first,"key-field":"fullpath","item-secondary-size":e(g).second,gridItems:e(b)},{default:o(({item:t,index:S})=>[a(H,{idx:S,file:t,"show-menu-idx":e(n),"onUpdate:showMenuIdx":s[3]||(s[3]=T=>v(n)?n.value=T:null),"full-screen-preview-image-url":e(K)(t),onContextMenuClick:e(M)},null,8,["idx","file","show-menu-idx","full-screen-preview-image-url","onContextMenuClick"])]),_:1},8,["items","item-size","item-secondary-size","gridItems"])]),_:1},8,["spinning"])],512)}}});const ae=U(W,[["__scopeId","data-v-b2de5e08"]]);export{ae as default};

View File

@ -1 +0,0 @@
import{d as b,F as N,J as T,b0 as V,r as h,G as q,aV as A,y as D,K as o,L as l,Q as r,W as y,V as d,a as G,M as c,ah as U,O as u,N as f,R as p,U as g,X as F,Y as M,aq as $,a0 as L}from"./index-6eb64f08.js";/* empty css */import{a as k,u as O}from"./db-2f654f91.js";import{B as Q}from"./button-3e5cfde3.js";const R={class:"container"},z={class:"search-bar"},E={class:"tag-list"},J=["onClick"],K=b({__name:"TagSearch",props:{tabIdx:null,paneIdx:null},setup(x){const I=x,S=N(),i=T(new V),a=h(),t=h(new Set),m=q(()=>a.value?a.value.tags.slice().sort((s,n)=>n.count-s.count):[]),C=A();D(async()=>{a.value=await k()});const B=async()=>{i.pushAction(async()=>{await O(),a.value=await k()})},w=()=>{S.openTagSearchMatchedImageGridInRight(I.tabIdx,C,Array.from(t.value))},_=s=>s.display_name?`${s.display_name} : ${s.name}`:s.name;return(s,n)=>{const v=Q;return o(),l("div",R,[r("",!0),a.value?(o(),l(y,{key:1},[d("div",null,[d("div",z,[G(c(U),{conv:{value:e=>e.id,text:_},mode:"multiple",style:{width:"100%"},options:c(m),value:Array.from(t.value),placeholder:"Select tags to match images","onUpdate:value":n[0]||(n[0]=e=>t.value=new Set(e))},null,8,["conv","options","value"]),a.value.expired||!a.value.img_count?(o(),u(v,{key:0,onClick:B,loading:!i.isIdle,type:"primary"},{default:f(()=>[p(g(a.value.img_count===0?"Generate index for search image":"Update index"),1)]),_:1},8,["loading"])):(o(),u(v,{key:1,type:"primary",onClick:w,loading:!i.isIdle},{default:f(()=>[p("Search")]),_:1},8,["loading"]))])]),d("ul",E,[(o(!0),l(y,null,F(c(m),e=>(o(),l("li",{key:e.id,class:M(["tag",{selected:t.value.has(e.id)}]),onClick:W=>t.value.has(e.id)?t.value.delete(e.id):t.value.add(e.id)},[t.value.has(e.id)?(o(),u(c($),{key:0})):r("",!0),p(" "+g(_(e)),1)],10,J))),128))])],64)):r("",!0)])}}});const P=L(K,[["__scopeId","data-v-ffc944e7"]]);export{P as default};

1
vue/dist/assets/TagSearch-ab0ce2af.js vendored Normal file
View File

@ -0,0 +1 @@
import{d as b,V as T,aV as N,a_ as V,r as h,A,ab as D,p as U,K as o,L as l,$ as r,x as y,T as d,c as $,O as c,a2 as q,W as u,N as f,Y as p,U as g,Z as G,a4 as F,cj as L,a8 as M}from"./index-8bb7713e.js";/* empty css */import{a as k,u as O}from"./db-94066177.js";import{B as j}from"./button-37307691.js";const z={class:"container"},E={class:"search-bar"},K={class:"tag-list"},Q=["onClick"],R=b({__name:"TagSearch",props:{tabIdx:null,paneIdx:null},setup(x){const I=x,S=T(),i=N(new V),a=h(),t=h(new Set),m=A(()=>a.value?a.value.tags.slice().sort((s,n)=>n.count-s.count):[]),C=D();U(async()=>{a.value=await k()});const B=async()=>{i.pushAction(async()=>{await O(),a.value=await k()})},w=()=>{S.openTagSearchMatchedImageGridInRight(I.tabIdx,C,Array.from(t.value))},_=s=>s.display_name?`${s.display_name} : ${s.name}`:s.name;return(s,n)=>{const v=j;return o(),l("div",z,[r("",!0),a.value?(o(),l(y,{key:1},[d("div",null,[d("div",E,[$(c(q),{conv:{value:e=>e.id,text:_},mode:"multiple",style:{width:"100%"},options:c(m),value:Array.from(t.value),placeholder:"Select tags to match images","onUpdate:value":n[0]||(n[0]=e=>t.value=new Set(e))},null,8,["conv","options","value"]),a.value.expired||!a.value.img_count?(o(),u(v,{key:0,onClick:B,loading:!i.isIdle,type:"primary"},{default:f(()=>[p(g(a.value.img_count===0?"Generate index for search image":"Update index"),1)]),_:1},8,["loading"])):(o(),u(v,{key:1,type:"primary",onClick:w,loading:!i.isIdle},{default:f(()=>[p("Search")]),_:1},8,["loading"]))])]),d("ul",K,[(o(!0),l(y,null,G(c(m),e=>(o(),l("li",{key:e.id,class:F(["tag",{selected:t.value.has(e.id)}]),onClick:W=>t.value.has(e.id)?t.value.delete(e.id):t.value.add(e.id)},[t.value.has(e.id)?(o(),u(c(L),{key:0})):r("",!0),p(" "+g(_(e)),1)],10,Q))),128))])],64)):r("",!0)])}}});const P=M(R,[["__scopeId","data-v-ffc944e7"]]);export{P as default};

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
.ant-alert{box-sizing:border-box;margin:0;color:#000000d9;font-size:14px;font-variant:tabular-nums;line-height:1.5715;list-style:none;font-feature-settings:"tnum";position:relative;display:flex;align-items:center;padding:8px 15px;word-wrap:break-word;border-radius:2px}.ant-alert-content{flex:1;min-width:0}.ant-alert-icon{margin-right:8px}.ant-alert-description{display:none;font-size:14px;line-height:22px}.ant-alert-success{background-color:#f6ffed;border:1px solid #b7eb8f}.ant-alert-success .ant-alert-icon{color:#52c41a}.ant-alert-info{background-color:#fff1e6;border:1px solid #f7ae83}.ant-alert-info .ant-alert-icon{color:#d03f0a}.ant-alert-warning{background-color:#fffbe6;border:1px solid #ffe58f}.ant-alert-warning .ant-alert-icon{color:#faad14}.ant-alert-error{background-color:#fff2f0;border:1px solid #ffccc7}.ant-alert-error .ant-alert-icon{color:#ff4d4f}.ant-alert-error .ant-alert-description>pre{margin:0;padding:0}.ant-alert-action{margin-left:8px}.ant-alert-close-icon{margin-left:8px;padding:0;overflow:hidden;font-size:12px;line-height:12px;background-color:transparent;border:none;outline:none;cursor:pointer}.ant-alert-close-icon .anticon-close{color:#00000073;transition:color .3s}.ant-alert-close-icon .anticon-close:hover{color:#000000bf}.ant-alert-close-text{color:#00000073;transition:color .3s}.ant-alert-close-text:hover{color:#000000bf}.ant-alert-with-description{align-items:flex-start;padding:15px 15px 15px 24px}.ant-alert-with-description.ant-alert-no-icon{padding:15px}.ant-alert-with-description .ant-alert-icon{margin-right:15px;font-size:24px}.ant-alert-with-description .ant-alert-message{display:block;margin-bottom:4px;color:#000000d9;font-size:16px}.ant-alert-message{color:#000000d9}.ant-alert-with-description .ant-alert-description{display:block}.ant-alert.ant-alert-motion-leave{overflow:hidden;opacity:1;transition:max-height .3s cubic-bezier(.78,.14,.15,.86),opacity .3s cubic-bezier(.78,.14,.15,.86),padding-top .3s cubic-bezier(.78,.14,.15,.86),padding-bottom .3s cubic-bezier(.78,.14,.15,.86),margin-bottom .3s cubic-bezier(.78,.14,.15,.86)}.ant-alert.ant-alert-motion-leave-active{max-height:0;margin-bottom:0!important;padding-top:0;padding-bottom:0;opacity:0}.ant-alert-banner{margin-bottom:0;border:0;border-radius:0}.ant-alert.ant-alert-rtl{direction:rtl}.ant-alert-rtl .ant-alert-icon{margin-right:auto;margin-left:8px}.ant-alert-rtl .ant-alert-action,.ant-alert-rtl .ant-alert-close-icon{margin-right:8px;margin-left:auto}.ant-alert-rtl.ant-alert-with-description{padding-right:24px;padding-left:15px}.ant-alert-rtl.ant-alert-with-description .ant-alert-icon{margin-right:auto;margin-left:15px}.ant-statistic{box-sizing:border-box;margin:0;padding:0;color:#000000d9;font-size:14px;font-variant:tabular-nums;line-height:1.5715;list-style:none;font-feature-settings:"tnum"}.ant-statistic-title{margin-bottom:4px;color:#00000073;font-size:14px}.ant-statistic-content{color:#000000d9;font-size:24px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}.ant-statistic-content-value{display:inline-block;direction:ltr}.ant-statistic-content-prefix,.ant-statistic-content-suffix{display:inline-block}.ant-statistic-content-prefix{margin-right:4px}.ant-statistic-content-suffix{margin-left:4px}.ant-statistic-rtl{direction:rtl}.ant-statistic-rtl .ant-statistic-content-prefix{margin-right:0;margin-left:4px}.ant-statistic-rtl .ant-statistic-content-suffix{margin-right:4px;margin-left:0}.container[data-v-e8df9a1a]{margin:16px}.container>*[data-v-e8df9a1a]{margin:8px}.log-container[data-v-e8df9a1a]{margin-top:24px}.log-container .scroll-container[data-v-e8df9a1a]{max-height:512px;overflow:auto}.log-container ul[data-v-e8df9a1a]{list-style:none;padding:0}.log-container ul li[data-v-e8df9a1a]{display:inline-block;padding:8px;margin:8px;background:var(--zp-secondary-background);border-radius:4px}.log-container ul li.err[data-v-e8df9a1a]{color:red}

4
vue/dist/assets/button-37307691.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

View File

@ -1 +0,0 @@
import{b as s}from"./index-8b6f769c.js";import{cq as t,at as a}from"./index-6eb64f08.js";function r(e,o){return e&&e.length?t(e,s(o)):[]}const i=(e,o)=>(a.success({content:o??`已复制内容 "${e}" 到粘贴板`}),navigator.clipboard.writeText(e));export{i as c,r as u};

View File

@ -1 +1 @@
import{c1 as a}from"./index-6eb64f08.js";const n=async()=>(await a.get("/db/basic_info")).data,i=async()=>{await a.post("/db/update_image_data",{},{timeout:1/0})},o=async t=>(await a.get("/db/match_images_by_tags",{params:{tag_ids:t.join()}})).data;export{n as a,o as g,i as u};
import{bC as a}from"./index-8bb7713e.js";const n=async()=>(await a.get("/db/basic_info")).data,i=async()=>{await a.post("/db/update_image_data",{},{timeout:1/0})},o=async t=>(await a.get("/db/match_images_by_tags",{params:{tag_ids:t.join()}})).data;export{n as a,o as g,i as u};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
.ant-collapse{box-sizing:border-box;margin:0;padding:0;color:#000000d9;font-size:14px;font-variant:tabular-nums;line-height:1.5715;list-style:none;font-feature-settings:"tnum";background-color:#fafafa;border:1px solid #d9d9d9;border-bottom:0;border-radius:2px}.ant-collapse>.ant-collapse-item{border-bottom:1px solid #d9d9d9}.ant-collapse>.ant-collapse-item:last-child,.ant-collapse>.ant-collapse-item:last-child>.ant-collapse-header{border-radius:0 0 2px 2px}.ant-collapse>.ant-collapse-item>.ant-collapse-header{position:relative;display:flex;flex-wrap:nowrap;align-items:flex-start;padding:12px 16px;color:#000000d9;line-height:1.5715;cursor:pointer;transition:all .3s,visibility 0s}.ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow{display:inline-block;margin-right:12px;font-size:12px;vertical-align:-1px}.ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow svg{transition:transform .24s}.ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-extra{margin-left:auto}.ant-collapse>.ant-collapse-item>.ant-collapse-header:focus{outline:none}.ant-collapse>.ant-collapse-item .ant-collapse-header-collapsible-only{cursor:default}.ant-collapse>.ant-collapse-item .ant-collapse-header-collapsible-only .ant-collapse-header-text{cursor:pointer}.ant-collapse>.ant-collapse-item.ant-collapse-no-arrow>.ant-collapse-header{padding-left:12px}.ant-collapse-icon-position-right>.ant-collapse-item>.ant-collapse-header{position:relative;padding:12px 40px 12px 16px}.ant-collapse-icon-position-right>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow{position:absolute;top:50%;right:16px;left:auto;margin:0;transform:translateY(-50%)}.ant-collapse-content{color:#000000d9;background-color:#fff;border-top:1px solid #d9d9d9}.ant-collapse-content>.ant-collapse-content-box{padding:16px}.ant-collapse-content-hidden{display:none}.ant-collapse-item:last-child>.ant-collapse-content{border-radius:0 0 2px 2px}.ant-collapse-borderless{background-color:#fafafa;border:0}.ant-collapse-borderless>.ant-collapse-item{border-bottom:1px solid #d9d9d9}.ant-collapse-borderless>.ant-collapse-item:last-child,.ant-collapse-borderless>.ant-collapse-item:last-child .ant-collapse-header{border-radius:0}.ant-collapse-borderless>.ant-collapse-item>.ant-collapse-content{background-color:transparent;border-top:0}.ant-collapse-borderless>.ant-collapse-item>.ant-collapse-content>.ant-collapse-content-box{padding-top:4px}.ant-collapse-ghost{background-color:transparent;border:0}.ant-collapse-ghost>.ant-collapse-item{border-bottom:0}.ant-collapse-ghost>.ant-collapse-item>.ant-collapse-content{background-color:transparent;border-top:0}.ant-collapse-ghost>.ant-collapse-item>.ant-collapse-content>.ant-collapse-content-box{padding-top:12px;padding-bottom:12px}.ant-collapse .ant-collapse-item-disabled>.ant-collapse-header,.ant-collapse .ant-collapse-item-disabled>.ant-collapse-header>.arrow{color:#00000040;cursor:not-allowed}.ant-collapse-rtl{direction:rtl}.ant-collapse-rtl .ant-collapse>.ant-collapse-item>.ant-collapse-header{padding:12px 40px 12px 16px}.ant-collapse-rtl.ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow{margin-right:0;margin-left:12px}.ant-collapse-rtl.ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-arrow svg{transform:rotate(180deg)}.ant-collapse-rtl.ant-collapse>.ant-collapse-item>.ant-collapse-header .ant-collapse-extra{margin-right:auto;margin-left:0}.ant-collapse-rtl.ant-collapse>.ant-collapse-item.ant-collapse-no-arrow>.ant-collapse-header{padding-right:12px;padding-left:0}.container[data-v-e0979915]{padding:20px;background-color:var(--zp-secondary-background);height:100%;overflow:auto}.header[data-v-e0979915]{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.header h1[data-v-e0979915]{font-size:28px;font-weight:700;color:var(--zp-primary)}.last-record[data-v-e0979915]{margin-left:8px;font-size:14px;color:var(--zp-tertiary)}.last-record a[data-v-e0979915]{text-decoration:none;color:var(--zp-tertiary)}.last-record a[data-v-e0979915]:hover{color:var(--zp-primary)}.content[data-v-e0979915]{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));grid-gap:20px}.quick-start[data-v-e0979915]{background-color:var(--zp-primary-background);border-radius:8px;box-shadow:0 1px 2px #0000001a;padding:20px}.quick-start ul[data-v-e0979915]{list-style:none;padding:4px}.quick-start h2[data-v-e0979915]{margin-top:0;margin-bottom:20px;font-size:20px;font-weight:700;color:var(--zp-primary)}.quick-start__item[data-v-e0979915]{margin-bottom:10px;padding:4px 8px;display:flex;align-items:center}.quick-start__item[data-v-e0979915]:hover{background:var(--zp-secondary-background);border-radius:4px;color:var(--primary-color);cursor:pointer}.quick-start__text[data-v-e0979915]{flex:1;font-size:16px}.quick-start__icon[data-v-e0979915]{margin-right:8px}

View File

@ -0,0 +1 @@
.container[data-v-8c6e0bce]{padding:20px;background-color:var(--zp-secondary-background);height:100%;overflow:auto}.header[data-v-8c6e0bce]{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px}.header h1[data-v-8c6e0bce]{font-size:28px;font-weight:700;color:var(--zp-primary)}.last-record[data-v-8c6e0bce]{margin-left:8px;font-size:14px;color:var(--zp-tertiary)}.last-record a[data-v-8c6e0bce]{text-decoration:none;color:var(--zp-tertiary)}.last-record a[data-v-8c6e0bce]:hover{color:var(--zp-primary)}.content[data-v-8c6e0bce]{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));grid-gap:20px}.quick-start[data-v-8c6e0bce]{background-color:var(--zp-primary-background);border-radius:8px;box-shadow:0 1px 2px #0000001a;padding:20px}.quick-start ul[data-v-8c6e0bce]{list-style:none;padding:4px}.quick-start h2[data-v-8c6e0bce]{margin-top:0;margin-bottom:20px;font-size:20px;font-weight:700;color:var(--zp-primary)}.quick-start__item[data-v-8c6e0bce]{margin-bottom:10px;padding:4px 8px;display:flex;align-items:center}.quick-start__item[data-v-8c6e0bce]:hover{background:var(--zp-secondary-background);border-radius:4px;color:var(--primary-color);cursor:pointer}.quick-start__text[data-v-8c6e0bce]{flex:1;font-size:16px}.quick-start__icon[data-v-8c6e0bce]{margin-right:8px}

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

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

File diff suppressed because one or more lines are too long

167
vue/dist/assets/index-8bb7713e.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

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

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
import{bs as D,bA as K,r as G,y as F,b4 as V,G as v,bb as W,t as I,d as B,u as T,bB as P,a5 as j,A as k,bC as R,c as $,_ as s,a as E,h as _}from"./index-6eb64f08.js";import{g as U,t as q,d as H}from"./styleChecker-572b0d40.js";var J="[object Object]",Q=Function.prototype,X=Object.prototype,L=Q.toString,Y=X.hasOwnProperty,Z=L.call(Object);function ie(n){if(!D(n)||K(n)!=J)return!1;var e=U(n);if(e===null)return!0;var a=Y.call(e,"constructor")&&e.constructor;return typeof a=="function"&&a instanceof a&&L.call(a)==Z}function se(n,e,a){var p=-1,i=n.length;e<0&&(e=-e>i?0:i+e),a=a>i?i:a,a<0&&(a+=i),i=e>a?0:a-e>>>0,e>>>=0;for(var f=Array(i);++p<i;)f[p]=n[p+e];return f}function le(n){var e=q(n),a=e%1;return e===e?a?e-a:e:0}const z=function(){var n=G(!1);return F(function(){n.value=H()}),n};var M=Symbol("rowContextKey"),ee=function(e){W(M,e)},te=function(){return V(M,{gutter:v(function(){}),wrap:v(function(){}),supportFlexGap:v(function(){})})};I("top","middle","bottom","stretch");I("start","end","center","space-around","space-between");var re=function(){return{align:String,justify:String,prefixCls:String,gutter:{type:[Number,Array,Object],default:0},wrap:{type:Boolean,default:void 0}}},ne=B({compatConfig:{MODE:3},name:"ARow",props:re(),setup:function(e,a){var p=a.slots,i=T("row",e),f=i.prefixCls,O=i.direction,S,x=G({xs:!0,sm:!0,md:!0,lg:!0,xl:!0,xxl:!0,xxxl:!0}),w=z();F(function(){S=P.subscribe(function(t){var r=e.gutter||0;(!Array.isArray(r)&&j(r)==="object"||Array.isArray(r)&&(j(r[0])==="object"||j(r[1])==="object"))&&(x.value=t)})}),k(function(){P.unsubscribe(S)});var h=v(function(){var t=[0,0],r=e.gutter,o=r===void 0?0:r,l=Array.isArray(o)?o:[o,0];return l.forEach(function(d,y){if(j(d)==="object")for(var u=0;u<R.length;u++){var g=R[u];if(x.value[g]&&d[g]!==void 0){t[y]=d[g];break}}else t[y]=d||0}),t});ee({gutter:h,supportFlexGap:w,wrap:v(function(){return e.wrap})});var N=v(function(){var t;return $(f.value,(t={},s(t,"".concat(f.value,"-no-wrap"),e.wrap===!1),s(t,"".concat(f.value,"-").concat(e.justify),e.justify),s(t,"".concat(f.value,"-").concat(e.align),e.align),s(t,"".concat(f.value,"-rtl"),O.value==="rtl"),t))}),A=v(function(){var t=h.value,r={},o=t[0]>0?"".concat(t[0]/-2,"px"):void 0,l=t[1]>0?"".concat(t[1]/-2,"px"):void 0;return o&&(r.marginLeft=o,r.marginRight=o),w.value?r.rowGap="".concat(t[1],"px"):l&&(r.marginTop=l,r.marginBottom=l),r});return function(){var t;return E("div",{class:N.value,style:A.value},[(t=p.default)===null||t===void 0?void 0:t.call(p)])}}});const fe=ne;function ae(n){return typeof n=="number"?"".concat(n," ").concat(n," auto"):/^\d+(\.\d+)?(px|em|rem|%)$/.test(n)?"0 0 ".concat(n):n}var oe=function(){return{span:[String,Number],order:[String,Number],offset:[String,Number],push:[String,Number],pull:[String,Number],xs:{type:[String,Number,Object],default:void 0},sm:{type:[String,Number,Object],default:void 0},md:{type:[String,Number,Object],default:void 0},lg:{type:[String,Number,Object],default:void 0},xl:{type:[String,Number,Object],default:void 0},xxl:{type:[String,Number,Object],default:void 0},xxxl:{type:[String,Number,Object],default:void 0},prefixCls:String,flex:[String,Number]}};const de=B({compatConfig:{MODE:3},name:"ACol",props:oe(),setup:function(e,a){var p=a.slots,i=te(),f=i.gutter,O=i.supportFlexGap,S=i.wrap,x=T("col",e),w=x.prefixCls,h=x.direction,N=v(function(){var t,r=e.span,o=e.order,l=e.offset,d=e.push,y=e.pull,u=w.value,g={};return["xs","sm","md","lg","xl","xxl","xxxl"].forEach(function(m){var b,c={},C=e[m];typeof C=="number"?c.span=C:j(C)==="object"&&(c=C||{}),g=_(_({},g),{},(b={},s(b,"".concat(u,"-").concat(m,"-").concat(c.span),c.span!==void 0),s(b,"".concat(u,"-").concat(m,"-order-").concat(c.order),c.order||c.order===0),s(b,"".concat(u,"-").concat(m,"-offset-").concat(c.offset),c.offset||c.offset===0),s(b,"".concat(u,"-").concat(m,"-push-").concat(c.push),c.push||c.push===0),s(b,"".concat(u,"-").concat(m,"-pull-").concat(c.pull),c.pull||c.pull===0),s(b,"".concat(u,"-rtl"),h.value==="rtl"),b))}),$(u,(t={},s(t,"".concat(u,"-").concat(r),r!==void 0),s(t,"".concat(u,"-order-").concat(o),o),s(t,"".concat(u,"-offset-").concat(l),l),s(t,"".concat(u,"-push-").concat(d),d),s(t,"".concat(u,"-pull-").concat(y),y),t),g)}),A=v(function(){var t=e.flex,r=f.value,o={};if(r&&r[0]>0){var l="".concat(r[0]/2,"px");o.paddingLeft=l,o.paddingRight=l}if(r&&r[1]>0&&!O.value){var d="".concat(r[1]/2,"px");o.paddingTop=d,o.paddingBottom=d}return t&&(o.flex=ae(t),S.value===!1&&!o.minWidth&&(o.minWidth=0)),o});return function(){var t;return E("div",{class:N.value,style:A.value},[(t=p.default)===null||t===void 0?void 0:t.call(p)])}}});export{de as C,fe as R,se as b,ie as i,le as t};

12
vue/dist/assets/index-efe6e3a7.js vendored Normal file

File diff suppressed because one or more lines are too long

1
vue/dist/assets/index-f11b4f57.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

View File

@ -1 +0,0 @@
.container[data-v-59148842]{height:100%;position:relative}.container .close-btn[data-v-59148842]{position:absolute;right:20px;top:20px}.list[data-v-59148842]{font-family:Consolas,Menlo,monospace;height:90vh;overflow:auto;padding:16px;border-radius:16px;background-color:var(--zp-secondary-background);color:var(--zp-primary);margin:16px;list-style:none;font-size:12px}.list pre[data-v-59148842]{margin:2px;white-space:normal}.list[data-v-59148842]::-webkit-scrollbar{display:none}

View File

@ -1 +0,0 @@
import{u as p}from"./useTaskListStore-db67ad53.js";import{d as u,r as d,G as g,ad as f,aH as m,K as t,L as s,V as l,W as k,X as L,U as y,M as D,a0 as v}from"./index-6eb64f08.js";const x={class:"container"},h=u({__name:"logDetail",props:{logDetailId:null},setup(r){const n=r,c=p(),a=d(),o=g(()=>c.taskLogMap.get(n.logDetailId));return f(o,async()=>{await m();const e=a.value;e&&(e.scrollTop=e.scrollHeight)},{deep:!0}),(e,B)=>(t(),s("div",x,[l("ul",{class:"list",ref_key:"logListEl",ref:a},[(t(!0),s(k,null,L(D(o),(i,_)=>(t(),s("li",{key:_},[l("pre",null,y(i.log),1)]))),128))],512)]))}});const T=v(h,[["__scopeId","data-v-59148842"]]);export{T as default};

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
.ant-breadcrumb{box-sizing:border-box;margin:0;padding:0;color:#000000d9;font-variant:tabular-nums;line-height:1.5715;list-style:none;font-feature-settings:"tnum";color:#00000073;font-size:14px}.ant-breadcrumb .anticon{font-size:14px}.ant-breadcrumb a{color:#00000073;transition:color .3s}.ant-breadcrumb a:hover{color:#de632f}.ant-breadcrumb>span:last-child{color:#000000d9}.ant-breadcrumb>span:last-child a{color:#000000d9}.ant-breadcrumb>span:last-child .ant-breadcrumb-separator{display:none}.ant-breadcrumb-separator{margin:0 8px;color:#00000073}.ant-breadcrumb-link>.anticon+span,.ant-breadcrumb-link>.anticon+a{margin-left:4px}.ant-breadcrumb-overlay-link>.anticon{margin-left:4px}.ant-breadcrumb-rtl{direction:rtl}.ant-breadcrumb-rtl:before{display:table;content:""}.ant-breadcrumb-rtl:after{display:table;clear:both;content:""}.ant-breadcrumb-rtl>span{float:right}.ant-breadcrumb-rtl .ant-breadcrumb-link>.anticon+span,.ant-breadcrumb-rtl .ant-breadcrumb-link>.anticon+a{margin-right:4px;margin-left:0}.ant-breadcrumb-rtl .ant-breadcrumb-overlay-link>.anticon{margin-right:4px;margin-left:0}.nprogress{pointer-events:none}.nprogress .bar{background:#29d;position:fixed;z-index:1031;top:0;left:0;width:100%;height:2px}.nprogress .peg{display:block;position:absolute;right:0px;width:100px;height:100%;box-shadow:0 0 10px #29d,0 0 5px #29d;opacity:1;-webkit-transform:rotate(3deg) translate(0px,-4px);-ms-transform:rotate(3deg) translate(0px,-4px);transform:rotate(3deg) translateY(-4px)}.nprogress .spinner{display:block;position:fixed;z-index:1031;top:15px;right:15px}.nprogress .spinner-icon{width:18px;height:18px;box-sizing:border-box;border:solid 2px transparent;border-top-color:#29d;border-left-color:#29d;border-radius:50%;-webkit-animation:nprogress-spinner .4s linear infinite;animation:nprogress-spinner .4s linear infinite}.nprogress-custom-parent{overflow:hidden;position:relative}.nprogress-custom-parent .nprogress .spinner,.nprogress-custom-parent .nprogress .bar{position:absolute}@-webkit-keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(360deg)}}@keyframes nprogress-spinner{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.uninstalled-hint[data-v-70134b8c]{margin:256px auto;display:flex;flex-flow:column;justify-content:center;align-items:center}.uninstalled-hint>*[data-v-70134b8c]{margin:16px;text-align:center}.preview-switch[data-v-70134b8c]{position:fixed;top:0;bottom:0;left:0;right:0;display:flex;align-items:center;justify-content:space-between;z-index:11111;pointer-events:none}.preview-switch>*[data-v-70134b8c]{margin:16px;font-size:4em;pointer-events:all;cursor:pointer}.preview-switch>*.disable[data-v-70134b8c]{opacity:0;pointer-events:none;cursor:none}.container[data-v-70134b8c]{height:100%;background:var(--zp-secondary-background)}.location-bar[data-v-70134b8c]{padding:4px 16px;background:var(--zp-primary-background);border-bottom:1px solid var(--zp-border);display:flex;align-items:center;justify-content:space-between}.location-bar .actions[data-v-70134b8c]{display:flex;align-items:center;flex-shrink:0}.location-bar a.opt[data-v-70134b8c]{margin-left:8px}.view[data-v-70134b8c]{padding:8px;height:calc(100vh - 96px)}.view .file-list[data-v-70134b8c]{list-style:none;padding:8px;height:100%;overflow:auto}.hint[data-v-70134b8c]{padding:4px;border:4px;background:var(--zp-secondary-background);border:1px solid var(--zp-border)}

11
vue/dist/assets/stackView-85fea162.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

View File

@ -1 +0,0 @@
import{cL as f,cM as i,cN as l,bH as d}from"./index-6eb64f08.js";var u=f(Object.getPrototypeOf,Object);const y=u;function o(e,t){for(var r=0;r<t.length;r++){var n=t[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(e,i(n.key),n)}}function b(e,t,r){return t&&o(e.prototype,t),r&&o(e,r),Object.defineProperty(e,"prototype",{writable:!1}),e}function E(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}function g(e){return function(t){return t==null?void 0:t[e]}}var c=1/0,p=17976931348623157e292;function h(e){if(!e)return e===0?e:0;if(e=l(e),e===c||e===-c){var t=e<0?-1:1;return t*p}return e===e?e:0}var m=function(){return d()&&window.document.documentElement},a,x=function(){if(!m())return!1;if(a!==void 0)return a;var t=document.createElement("div");return t.style.display="flex",t.style.flexDirection="column",t.style.rowGap="1px",t.appendChild(document.createElement("div")),t.appendChild(document.createElement("div")),document.body.appendChild(t),a=t.scrollHeight===1,document.body.removeChild(t),a};export{b as _,E as a,g as b,m as c,x as d,y as g,h as t};

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

View File

@ -1 +0,0 @@
import{cO as r,r as e,J as t,b0 as i,cP as d}from"./index-6eb64f08.js";const v=r("useTaskListStore",()=>{const a=e(new Map),n=t(new i),u=e(3),o=e([]),c=t([]),l=e(-1),s=e(null);return{checkBaiduyunInstalled:async()=>(s.value===null&&(s.value=d()),s.value),baiduyunInstalled:s,pollInterval:u,taskLogMap:a,queue:n,tasks:o,showDirAutoCompletedIdx:l,pendingBaiduyunTaskQueue:c}},{persist:{paths:["pollInterval","tasks"],key:"useTaskListStore-v0.0.1"}});export{v as u};

4
vue/dist/index.html vendored
View File

@ -7,8 +7,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="/infinite_image_browsing/fe-static/assets/index-6eb64f08.js"></script>
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-9ee28aec.css">
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-8bb7713e.js"></script>
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-92aac6a6.css">
</head>
<body>

View File

@ -2,7 +2,6 @@
<script setup lang="ts">
import { onMounted, reactive } from 'vue'
import { FetchQueue } from 'vue3-ts-util'
import { getUserInfo } from './api/user'
import { getGlobalSetting } from './api'
import { useGlobalStore } from './store/useGlobalStore'
import { getAutoCompletedTagList } from '@/page/taskRecord/autoComplete'
@ -16,7 +15,6 @@ onMounted(async () => {
const r = await getAutoCompletedTagList(resp)
globalStore.autoCompletedDirList = r.filter(v => v?.dir?.trim?.())
})
globalStore.user = await queue.pushAction(getUserInfo).res
})
</script>

View File

@ -17,118 +17,6 @@ export const greeting = async () => {
const resp = await axiosInst.get('hello')
return resp.data as string
}
interface BaiduYunTaskCreateReq {
type: 'upload' | 'download'
send_dirs: string[]
recv_dir: string
}
export const createBaiduYunTask = async (req: BaiduYunTaskCreateReq) => {
const resp = await axiosInst.post('task', req)
return resp.data as {
id: string
}
}
interface UploadTaskStart {
status: 'start'
concurrent: number
}
interface UploadTaskFileSkipped {
status: 'file-skipped'
id: string
}
interface UploadTaskPreparing {
id: string
status: 'upload-preparing'
local_path: string
}
interface UploadTaskSuccess {
id: string
status: 'upload-success'
remote_path: string
}
interface UploadTaskFastuploadFailed {
id: string
status: 'fast-upload-failed'
}
interface UploadTaskFailed {
id: string
status: 'upload-failed'
extra_info: string
}
interface UploadTaskDone {
status: 'done'
}
interface UploadTaskQueued {
id: string
status: 'queued'
local_file_path: string
}
export type UploadTaskFileStatus =
| UploadTaskFastuploadFailed
| UploadTaskFileSkipped
| UploadTaskPreparing
| UploadTaskQueued
| UploadTaskSuccess
| UploadTaskFailed
export interface UploadTaskTickStatus {
log: string
info: UploadTaskDone | UploadTaskStart | UploadTaskFileStatus
}
/**
*
*/
export const getUploadTaskTickStatus = async (id: string) => {
const resp = await axiosInst.get(`/task/${id}/tick`)
return resp.data as {
tasks: UploadTaskTickStatus[],
task_summary: UploadTaskSummary
}
}
export interface UploadTaskSummary {
id: string
running: boolean
start_time: string
send_dirs: string[]
recv_dir: string
type: 'upload' | 'download'
n_files: number
n_failed_files: number
canceled: boolean
n_success_files: number
}
/**
*
* @param id
*/
export const getUploadTaskFilesState = async (id: string) => {
const resp = await axiosInst.get(`/task/${id}/files_state`)
return resp.data as {
files_state: { [x: string]: UploadTaskFileStatus }
}
}
/**
*
*/
export const getUploadTasks = async () => {
const resp = await axiosInst.get('/tasks')
return resp.data as {
tasks: UploadTaskSummary[]
}
}
export interface GlobalConf {
global_setting: GlobalSettingPart,
@ -149,19 +37,6 @@ export const checkPathExists = async (paths: string[]) => {
}
export const cancelTask = async (id: string) => {
const resp = await axiosInst.post(`/task/${id}/cancel`)
return resp.data as {
last_tick: {
tasks: UploadTaskTickStatus[],
task_summary: UploadTaskSummary
}
}
}
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)}`)
@ -174,17 +49,3 @@ export const genInfoCompleted = async () => {
export const getImageGenerationInfo = async (path: string) => {
return (await axiosInst.get(`/image_geninfo?path=${encodeURIComponent(path)}`)).data as string
}
export const autoUploadOutput = async (recv_dir: string) => {
const resp = await axiosInst.post(`/auto_upload`, { recv_dir })
return resp.data as {
pending_files: string[]
tick_info?: {
tasks: UploadTaskTickStatus[],
task_summary: UploadTaskSummary
}
}
}
export const checkBaiduyunExists = () => axiosInst.get('/baiduyun_exists').then(v => v.data as boolean)
export const downloadBaiduyun = () => axiosInst.post('/download_baiduyun')

View File

@ -11,12 +11,8 @@ import { message } from 'ant-design-vue'
import { t } from '@/i18n'
const global = useGlobalStore()
const compMap: Record<TabPane['type'], ReturnType<typeof defineAsyncComponent>> = {
'auto-upload': defineAsyncComponent(() => import('@/page/autoUpload/autoUpload.vue')),
local: defineAsyncComponent(() => import('@/page/fileTransfer/stackView.vue')),
netdisk: defineAsyncComponent(() => import('@/page/fileTransfer/stackView.vue')),
"task-record": defineAsyncComponent(() => import('@/page/taskRecord/taskRecord.vue')),
empty: defineAsyncComponent(() => import('./emptyStartup.vue')),
"log-detail": defineAsyncComponent(() => import('@/page/taskRecord/logDetail.vue')),
"global-setting": defineAsyncComponent(() => import('@/page/globalSetting.vue')),
"tag-search-matched-image-grid": defineAsyncComponent(() => import('@/page/TagSearch/MatchedImageGrid.vue')),
"tag-search": defineAsyncComponent(() => import('@/page/TagSearch/TagSearch.vue'))
@ -74,8 +70,7 @@ watch(() => global.tabList, async () => {
<pane v-for="tab, tabIdx in global.tabList" :key="key(tab)">
<edge-trigger :tabIdx="tabIdx">
<a-tabs type="editable-card" v-model:activeKey="tab.key" @edit="(key, act) => onEdit(tabIdx, key, act)">
<a-tab-pane v-for="pane, paneIdx in tab.panes" :key="pane.key" :tab="pane.name" class="pane"
:force-render="pane.type === 'task-record'">
<a-tab-pane v-for="pane, paneIdx in tab.panes" :key="pane.key" :tab="pane.name" class="pane">
<component :is="compMap[pane.type]" :tabIdx="tabIdx" :paneIdx="paneIdx" v-bind="pane" />
</a-tab-pane>
</a-tabs>

View File

@ -13,28 +13,18 @@ const compCnMap: Partial<Record<TabPane['type'], string>> = {
local: t('local'),
"tag-search": t('imgSearch'),
'global-setting': t('globalSettings'),
netdisk: t('baiduCloud'),
"task-record": t('taskRecord'),
"auto-upload": t('autoUpload'),
}
const openInCurrentTab = (type: TabPane['type'], path?: string, walkMode = false) => {
let pane: TabPane
if (type === 'task-record' && global.tabList.map(v => v.panes).flat().find(v => v.type === 'task-record')) {
return message.error(t('onlyOneTaskRecordAllowed')) //
}
switch (type) {
case 'tag-search-matched-image-grid':
return
case 'auto-upload':
case 'task-record':
case 'log-detail':
case 'global-setting':
case 'tag-search':
case 'empty':
pane = { type, name: compCnMap[type]!, key: Date.now() + uniqueId() }
break
case 'local':
case 'netdisk':
pane = { type, name: compCnMap[type]!, key: Date.now() + uniqueId(), target: type, path, walkMode }
}
const tab = global.tabList[props.tabIdx]
@ -87,19 +77,10 @@ const previewInNewWindow = () => window.parent.open('/infinite_image_browsing')
<div class="quick-start">
<h2>{{ $t('launch') }}</h2>
<ul>
<li v-for="comp in Object.keys(compCnMap).slice(0, 3) as TabPane['type'][]" :key="comp"
<li v-for="comp in Object.keys(compCnMap) as TabPane['type'][]" :key="comp"
class="quick-start__item" @click.prevent="openInCurrentTab(comp)">
<span class="quick-start__text line-clamp-1">{{ compCnMap[comp] }}</span>
</li>
<a-collapse style="margin-top: 32px; " v-model:activeKey="global.baiduNetdiskPageOpened" :bordered="false">
<a-collapse-panel key="true" :header="$t('baiduNetdiskCollapseTitle')" >
<li v-for="comp in Object.keys(compCnMap).slice(3) as TabPane['type'][]" :key="comp"
class="quick-start__item" @click.prevent="openInCurrentTab(comp)">
<span class="quick-start__text line-clamp-1">{{ compCnMap[comp] }}</span>
</li>
</a-collapse-panel>
</a-collapse>
</ul>
</div>
<div class="quick-start" v-if="global.recent.length">

View File

@ -1,136 +0,0 @@
<script setup lang="ts">
import { computed, reactive, ref, watchEffect } from 'vue'
import { autoUploadOutput, type UploadTaskSummary } from '@/api/index'
import { delay, Task } from 'vue3-ts-util'
import { useGlobalStore } from '@/store/useGlobalStore'
import { onBeforeUnmount } from 'vue'
import { Loading3QuartersOutlined, CloseCircleOutlined } from '@/icon'
import { useTaskListStore } from '@/store/useTaskListStore'
import { onMounted } from 'vue'
const props = defineProps<{ tabIdx: number, paneIdx: number }>()
const emit = defineEmits<{ (e: 'runningChange', v: boolean): void }>()
const global = useGlobalStore()
const taskStore = useTaskListStore()
const pendingFiles = ref<string[]>([])
const task = ref<ReturnType<typeof runPollTask>>()
const running = computed(() => !!(task.value || pendingFiles.value.length))
watchEffect(() => emit('runningChange', running.value))
const taskLog = reactive(new Map<string, UploadTaskSummary>())
const taskLogList = computed(() => Array.from(taskLog.values()))
const completedFiles = computed(() => taskLogList.value.reduce((p, c) => p + c.n_success_files, 0))
const failededFiles = computed(() => taskLogList.value.reduce((p, c) => p + c.n_failed_files, 0))
// const allFiles = computed(() => taskLogList.value.reduce((p, c) => p + c.n_files, 0) + pendingFiles.value.length)
onMounted(() => global.openBaiduYunIfNotLogged(props.tabIdx, props.paneIdx))
onBeforeUnmount(() => {
task.value?.clearTask()
})
const runPollTask = () => {
return Task.run({
action: async () => {
const res = await autoUploadOutput(global.autoUploadRecvDir)
if (res.tick_info) {
const info = res.tick_info!
taskLog.set(info.task_summary.id, info.task_summary)
taskStore.taskLogMap.set(info.task_summary.id, info.tasks)
}
pendingFiles.value = res.pending_files
await delay(10000 * Math.random())
return res
},
pollInterval: 30_000
})
}
const onStart = async () => {
if (task.value) {
task.value.clearTask()
task.value = undefined
pendingFiles.value = []
} else {
task.value = runPollTask()
}
}
const openLogDetail = (id: string) => {
global.openLogDetailInRight(props.tabIdx, id)
}
</script>
<template>
<div class="container">
<AInput v-model:value="global.autoUploadRecvDir"></AInput>
<AButton @click="onStart">
<template v-if="task">
<Loading3QuartersOutlined spin />
</template>
{{ task ? $t('start') : $t('pause') }}
</AButton>
<a-row>
<a-col :span="12">
<a-statistic :title="$t('waitingUploadCount')" :value="pendingFiles.length" style="margin-right: 50px" />
</a-col>
<a-col :span="12">
<a-statistic :title="$t('uploadFailureCount')" :value="failededFiles" />
</a-col>
</a-row>
<a-row>
<a-col :span="12">
<a-statistic :title="$t('completedCount')" :value="completedFiles" style="margin-right: 50px" />
</a-col>
</a-row>
<div class="log-container">
<h2>
{{ $t('realTimeLog') }}
</h2> <a-alert :message="$t('tip')" :description="$t('clickToViewLogs')" type="info" show-icon />
<ul class="scroll-container">
<li v-for="item in taskLog.values()" :key="item.id" :class="{ err: item.n_failed_files }"
@click="openLogDetail(item.id)">
<CloseCircleOutlined v-if="item.n_failed_files" /> {{ $t('startedAt') + item.start_time }}
</li>
</ul>
</div>
</div>
</template>
<style lang="scss" scoped>
.container {
margin: 16px;
&>* {
margin: 8px;
}
}
.log-container {
margin-top: 24px;
.scroll-container {
max-height: 512px;
overflow: auto;
}
ul {
list-style: none;
padding: 0px;
li {
display: inline-block;
padding: 8px;
margin: 8px;
background: var(--zp-secondary-background);
border-radius: 4px;
&.err {
color: red;
}
}
}
}
</style>

View File

@ -1,10 +1,8 @@
import { useGlobalStore, type FileTransferTabPane } from '@/store/useGlobalStore'
import { useTaskListStore } from '@/store/useTaskListStore'
import { computedAsync, onLongPress, useElementSize } from '@vueuse/core'
import { onLongPress, useElementSize } from '@vueuse/core'
import { ref, computed, watch, onMounted, h, reactive } from 'vue'
import { downloadBaiduyun, genInfoCompleted, getImageGenerationInfo, setImgPath } from '@/api'
import { isAxiosError } from 'axios'
import { genInfoCompleted, getImageGenerationInfo, setImgPath } from '@/api'
import { useWatchDocument, type SearchSelectConv, ok, createTypedShareStateHook, copy2clipboard, delay, FetchQueue, typedEventEmitter, ID } from 'vue3-ts-util'
import { gradioApp, isImageFile } from '@/util'
import { getTargetFolderFiles, type FileNodeInfo, deleteFiles, moveFiles } from '@/api/files'
@ -17,9 +15,7 @@ import NProgress from 'multi-nprogress'
import { Modal, message } from 'ant-design-vue'
import type { MenuInfo } from 'ant-design-vue/lib/menu/src/interface'
import { nextTick } from 'vue'
import { loginByBduss } from '@/api/user'
import { t } from '@/i18n'
import { locale } from '@/i18n'
import { CloudServerOutlined, DatabaseOutlined } from '@/icon'
export const stackCache = new Map<string, Page[]>()
@ -85,13 +81,12 @@ export const { useHookShareState } = createTypedShareStateHook(() => {
stackViewEl: ref<HTMLDivElement>(),
walkModePath,
props,
...useBaiduyun(),
...typedEventEmitter<{ loadNextDir: undefined, refresh: void }>()
}
})
export interface Props {
target: 'local' | 'netdisk',
target: 'local',
tabIdx: number,
paneIdx: number,
path?: string,
@ -99,54 +94,6 @@ export interface Props {
}
export type ViewMode = 'line' | 'grid' | 'large-size-grid'
const taskListStore = useTaskListStore()
export const useBaiduyun = () => {
const bduss = ref('')
const installedBaiduyun = computedAsync(taskListStore.checkBaiduyunInstalled, false)
const baiduyunLoading = ref(false)
const failedHint = ref('')
const installBaiduyunBin = async () => {
try {
failedHint.value = ''
baiduyunLoading.value = true
await downloadBaiduyun()
taskListStore.baiduyunInstalled = null
await taskListStore.checkBaiduyunInstalled()
} catch (e) {
if (isAxiosError(e)) {
failedHint.value = e.response?.data.detail ?? 'error'
}
} finally {
baiduyunLoading.value = false
}
}
const onLoginBtnClick = async () => {
if (baiduyunLoading.value) {
return
}
try {
baiduyunLoading.value = true
global.user = await loginByBduss(bduss.value)
} catch (error) {
console.error(error)
message.error(isAxiosError(error) ? error.response?.data?.detail ?? t('unknownError') : t('unknownError'))
} finally {
baiduyunLoading.value = false
}
}
return {
installBaiduyunBin,
installedBaiduyun,
failedHint,
baiduyunLoading,
bduss,
onLoginBtnClick
}
}
export interface Page {
files: FileNodeInfo[]
@ -254,7 +201,7 @@ export function usePreview (props: Props) {
export function useLocation (props: Props) {
const np = ref<Progress.NProgress>()
const { installedBaiduyun, scroller, stackViewEl, stack, currPage, currLocation, basePath
const { scroller, stackViewEl, stack, currPage, currLocation, basePath
, sortMethod, useEventListen, walkModePath
} = useHookShareState().toRefs()
@ -265,9 +212,6 @@ export function useLocation (props: Props) {
}, 300))
onMounted(async () => {
if (props.target === 'netdisk' && installedBaiduyun.value) {
return
}
if (!stack.value.length) { // 有传入stack时直接使用传入的
const resp = await getTargetFolderFiles(props.target, '/')
stack.value.push({
@ -292,20 +236,6 @@ export function useLocation (props: Props) {
}
})
/**
*
*/
watch(() => props.target === 'netdisk' && installedBaiduyun.value && global.user, async (v, last) => {
if (v && !last) {
const resp = await getTargetFolderFiles(props.target, '/')
stack.value = [{
files: resp.files,
curr: '/'
}]
}
})
watch(currLocation, debounce((loc) => {
const pane = global.tabList[props.tabIdx].panes[props.paneIdx] as FileTransferTabPane
pane.path = loc
@ -600,23 +530,7 @@ export function useFileTransfer (props: Props) {
}
})
} else {
const type = data.from === 'local' ? 'upload' : 'download'
const typeT = type === 'upload' ? t('upload') : t('download')
const content = h('div', [
h('div', `${locale.value === 'en' ? 'from' : '从'} ${props.target !== 'local' ? t('local') : t('cloud')} `),
h('ol', data.path.map(v => v.split(/[/\\]/).pop()).map(v => h('li', v))),
h('div', `${typeT} ${props.target === 'local' ? t('local') : t('cloud')} ${toPath}`)
])
Modal.confirm({
title: t('confirmCreateTask', { type: typeT, more: locale.value === 'zh' ? ', 这是文件夹或者包含文件夹!' : ',which contains folders!' }),
content,
maskClosable: true,
async onOk () {
await global.createTaskRecordPaneIfNotExist(props.tabIdx)
console.log('request createNewTask', { send_dirs: data.path, recv_dir: toPath, type })
taskListStore.pendingBaiduyunTaskQueue.push({ send_dirs: data.path, recv_dir: toPath, type })
}
})
// 原有的百度云
}
}
@ -680,10 +594,10 @@ export function useFileItemActions (props: Props, { openNext }: { openNext: (fil
try {
spinning.value = true
await setImgPath(file.fullpath) // 设置图像路径
const btn = gradioApp().querySelector('#bd_hidden_img_update_trigger')! as HTMLButtonElement
const btn = gradioApp().querySelector('#iib_hidden_img_update_trigger')! as HTMLButtonElement
btn.click() // 触发图像组件更新
ok(await genInfoCompleted(), 'genInfoCompleted timeout') // 等待消息生成完成
const tabBtn = gradioApp().querySelector(`#bd_hidden_tab_${tab}`) as HTMLButtonElement
const tabBtn = gradioApp().querySelector(`#iib_hidden_tab_${tab}`) as HTMLButtonElement
tabBtn.click() // 触发粘贴
} catch (error) {
console.error(error)

View File

@ -16,7 +16,7 @@ import FileItem from './FileItem.vue'
const global = useGlobalStore()
const props = defineProps<{
target: 'local' | 'netdisk',
target: 'local',
tabIdx: number,
paneIdx: number,
/**
@ -29,8 +29,8 @@ const props = defineProps<{
*/
stackKey?: string
}>()
const { installBaiduyunBin, installedBaiduyun, failedHint, baiduyunLoading,
scroller, walkModePath, stackViewEl, props: _props, bduss, onLoginBtnClick, multiSelectedIdxs,
const {
scroller, walkModePath, stackViewEl, props: _props, multiSelectedIdxs,
spinning
} = useHookShareState().toRefs()
const { currLocation, currPage, refresh, copyLocation, back, openNext, stack, to } = useLocation(props)
@ -59,30 +59,8 @@ watch(() => props, () => {
<template>
<ASpin :spinning="spinning" size="large">
<ASelect style="display: none;"></ASelect>
<div v-if="props.target === 'netdisk' && (!installedBaiduyun || !global.user)" class="uninstalled-hint">
<template v-if="!installedBaiduyun">
<div>{{ $t('dependenciesNotInstalled') }}</div>
<AButton type="primary" :loading="baiduyunLoading" @click="installBaiduyunBin">{{ $t('clickHere2install') }}
</AButton>
<p v-if="failedHint">{{ failedHint }}</p>
</template>
<template v-else>
<a-form layout="inline">
<a-form-item label="bduss">
<a-input v-model:value="bduss" style="width:300px"></a-input>
</a-form-item>
<a-form-item>
<a-button @click="onLoginBtnClick" type="primary" :loading="baiduyunLoading">
<template #icon>
<login-outlined />
</template>
{{ $t('login') }}
</a-button>
</a-form-item>
</a-form>
</template>
</div>
<div ref="stackViewEl" @dragover.prevent @drop.prevent="onDrop($event)" class="container" v-else>
<div ref="stackViewEl" @dragover.prevent @drop.prevent="onDrop($event)" class="container" >
<AModal v-model:visible="showGenInfo" width="70vw" mask-closable @ok="showGenInfo = false">
<template #cancelText />
<ASkeleton active :loading="!q.isIdle">

View File

@ -1,24 +1,10 @@
<script setup lang="ts">
import { logout } from '@/api/user'
import { t } from '@/i18n'
import { useGlobalStore } from '@/store/useGlobalStore'
import { useTaskListStore } from '@/store/useTaskListStore'
import { message } from 'ant-design-vue'
import { storeToRefs } from 'pinia'
import { ref } from 'vue'
import { reactive } from 'vue'
import { FetchQueue } from 'vue3-ts-util'
const queue = reactive(new FetchQueue(-1, 0, 0, 'throw'))
const globalStore = useGlobalStore()
const taskStore = useTaskListStore()
const { user } = storeToRefs(globalStore)
const onLogoutBtnClick = async () => {
await queue.pushAction(logout).res
user.value = undefined
message.info(t('logoutSuccess'))
}
const langChanged = ref(false)
const w = window
</script>
@ -28,10 +14,7 @@ const w = window
<a-form-item :label="$t('useThumbnailPreview')">
<a-switch v-model:checked="globalStore.enableThumbnail" />
</a-form-item>
<a-form-item :label="$t('pollingInterval')">
<a-input-number v-model:value="taskStore.pollInterval" :min="0.5" :disabled="!taskStore.queue.isIdle" /> (s)
<sub>{{ $t('smallerIntervalMeansMoreNetworkTraffic') }}</sub>
</a-form-item>
<a-form-item :label="$t('gridThumbnailWidth')">
<a-input-number v-model:value="globalStore.gridThumbnailSize" :min="256" :max="1024" /> (px)
</a-form-item>
@ -54,17 +37,6 @@ const w = window
</div>
<a-button type="primary" @click="w.location.reload()" v-if="langChanged" ghost>{{ t('langChangeReload') }}</a-button>
</a-form-item>
<template v-if="user">
<a-form-item label="百度云已登录用户">
{{ user.username }}
<a-button @click="onLogoutBtnClick" :loading="!queue.isIdle">
<template #icon>
<logout-outlined />
</template>
登出
</a-button>
</a-form-item>
</template>
</a-form>
</div>
</template>

View File

@ -1,35 +0,0 @@
<script setup lang="ts">
import type { UploadTaskSummary } from '@/api'
import { useGlobalStore } from '@/store/useGlobalStore'
import { useTaskListStore } from '@/store/useTaskListStore'
import { storeToRefs } from 'pinia'
import { deepComputedEffect, type WithId } from 'vue3-ts-util'
const props = defineProps<{ task: WithId<UploadTaskSummary>, idx: number }>()
const emit = defineEmits<{ (t: 'update:task', v: WithId<UploadTaskSummary>): void }>()
const task = deepComputedEffect({ get: () => props.task, set: v => emit('update:task', v) })
const store = useTaskListStore()
const globalStore = useGlobalStore()
const { showDirAutoCompletedIdx } = storeToRefs(store)
const addDir2task = (dir: string) => {
if (task.value.type === 'download') {
task.value.recv_dir = dir
return
}
task.value.send_dirs.push(dir)
}
const colors = ['#f5222d', '#1890ff', '#ff3125', '#d46b08', '#007bff', '#52c41a', '#13c2c2', '#fa541c', '#eb2f96', '#2f54eb']
</script>
<template>
<div v-if="showDirAutoCompletedIdx === idx && globalStore.autoCompletedDirList.length" class="auto-completed-dirs">
<a-tooltip v-for="item, tagIdx in globalStore.autoCompletedDirList" :key="item.dir" :title="item.dir + ' 点击添加'">
<a-tag :visible="!task.send_dirs.includes(item.dir)" :color="colors[tagIdx % colors.length]"
@click="addDir2task(item.dir)">{{ item.zh }}</a-tag>
</a-tooltip>
</div>
</template>
<style lang="scss" scoped>
.auto-completed-dirs {
margin-top: 16px;
}
</style>

View File

@ -1,64 +0,0 @@
<script lang="ts" setup>
import { useTaskListStore } from '@/store/useTaskListStore'
import { computed, watch, nextTick, ref } from 'vue'
const props = defineProps<{
logDetailId: string
}>()
const store = useTaskListStore()
const logListEl = ref<HTMLDivElement>()
const currList = computed(() => store.taskLogMap.get(props.logDetailId))
watch(currList, async () => {
await nextTick()
const el = logListEl.value
if (el) {
el.scrollTop = el.scrollHeight
}
}, { deep: true })
</script>
<template>
<div class="container" >
<ul class="list" ref="logListEl" >
<li v-for="log, idx in currList" :key="idx" >
<pre>{{ log.log }}</pre>
</li>
</ul>
</div>
</template>
<style lang="scss" scoped>
.container {
height: 100%;
position: relative;
.close-btn {
position: absolute;
right: 20px;
top: 20px;
}
}
.list {
font-family: Consolas, Menlo, monospace;
height: 90vh;
overflow: auto;
padding: 16px;
margin: 16px;
border-radius: 16px;
background-color: var(--zp-secondary-background);
color: var(--zp-primary);
margin-left: 16px;
list-style: none;
font-size: 12px;
pre {
margin: 2px;
white-space: normal;
}
&::-webkit-scrollbar {
display: none;
}
}
</style>

View File

@ -1,270 +0,0 @@
<script setup lang="ts">
import { key, pick } from '@/util'
import { onMounted, ref } from 'vue'
import { typedID, Task, SearchSelect, copy2clipboard } from 'vue3-ts-util'
import { PlusOutlined, SyncOutlined, MinusCircleOutlined } from '@/icon'
import { cancelTask, createBaiduYunTask, getUploadTasks, getUploadTaskTickStatus, removeTask, type UploadTaskSummary } from '@/api'
import { message } from 'ant-design-vue'
import { useTaskListStore } from '@/store/useTaskListStore'
import { storeToRefs } from 'pinia'
import { uniqBy } from 'lodash-es'
import localPathShortcut from './localPathShortcut.vue'
import { useGlobalStore } from '@/store/useGlobalStore'
import { onBeforeUnmount } from 'vue'
import { watch } from 'vue'
const props = defineProps<{ tabIdx: number, paneIdx: number }>()
const ID = typedID<UploadTaskSummary>(true)
const store = useTaskListStore()
const globalStore = useGlobalStore()
const { tasks } = storeToRefs(store)
const { showDirAutoCompletedIdx } = storeToRefs(store)
const pollTaskMap = new Map<string, ReturnType<typeof createPollTask>>()
const loadNum = ref(10)
onMounted(() => globalStore.openBaiduYunIfNotLogged(props.tabIdx, props.paneIdx))
onBeforeUnmount(() => {
pollTaskMap.forEach(v => v.clearTask())
})
const canProcessQueue = ref(false)
watch([() => store.pendingBaiduyunTaskQueue, canProcessQueue], async ([q, can]) => {
if (!q.length || !can) {
return
}
console.log('processQueue', q)
for (const task of q) {
tasks.value.unshift(ID({ ...getEmptyTask(), ...task }))
createNewTask(0).then(() => message.success('创建完成,在任务列表查看进度'))
}
store.pendingBaiduyunTaskQueue = []
}, { deep: true, immediate: true })
onMounted(async () => {
const resp = await getUploadTasks()
tasks.value = uniqBy([...resp.tasks, ...tasks.value].map(ID), v => v.id) //
.sort((a, b) => Date.parse(b.start_time) - Date.parse(a.start_time))
.slice(0, 100)
let runningTasks = tasks.value.filter(v => v.running)
runningTasks.filter(task => !resp.tasks.find(beTask => beTask.id === task.id)).forEach(task => { //
task.running = false
})
runningTasks = tasks.value.filter(v => v.running)
if (runningTasks.length) {
runningTasks.forEach(v => {
createPollTask(v.id).completedTask.then(() => message.success(`${v.type === 'download' ? '下载' : '上传'}完成`))
})
}
if (!tasks.value.length) {
addEmptyTask()
}
canProcessQueue.value = true
console.log('task record load')
})
const getEmptyTask = () => ID({
type: 'upload',
send_dirs: [],
recv_dir: '',
id: '',
running: false,
start_time: '',
n_failed_files: 0,
n_files: 0,
n_success_files: 0,
canceled: false
})
const addEmptyTask = () => {
tasks.value.unshift(getEmptyTask())
}
const createNewTask = async (idx: number) => {
const task = tasks.value[idx]
task.send_dirs = task.send_dirs.map(v => v.trim()).filter(v => v)
task.recv_dir = task.recv_dir.trim()
if (!(task.type === 'upload' ? task.recv_dir.startsWith('/') : task.send_dirs.every(v => v.startsWith('/')))) {
return message.error('百度云的位置必须以 “/” 开头')
}
task.running = true
task.n_files = 100
const resp = await createBaiduYunTask(task)
task.id = resp.id
createPollTask(resp.id).completedTask.then(() => message.success(task.type === 'upload' ? '上传完成' : '下载完成'))
}
const createPollTask = (id: string) => {
store.taskLogMap.set(id, [])
const task = Task.run({
action: () => getUploadTaskTickStatus(id),
pollInterval: store.pollInterval * 1000,
validator (r) {
store.taskLogMap.get(id)!.push(...r.tasks)
const idx = tasks.value.findIndex(v => v.id === id)
tasks.value[idx] = ID(r.task_summary)
return !r.task_summary.running
}
})
pollTaskMap.set(id, task)
store.queue.pushAction(() => task.completedTask) // 使queue
return task
}
const getIntPercent = (task: UploadTaskSummary) => parseInt((((task.n_failed_files + task.n_success_files) / task.n_files) * 100).toString())
const isDone = (task: UploadTaskSummary) => !!task.id && !task.running && !task.canceled
const isDisable = (task: UploadTaskSummary) => task.running || isDone(task)
const copyFrom = (idx: number) => {
const prevTask = tasks.value[idx]
tasks.value.unshift({
...getEmptyTask(),
...pick(prevTask, 'send_dirs', 'type', 'recv_dir')
})
message.success('复制完成,已添加到最前端')
}
const openLogDetail = (idx: number) => {
globalStore.openLogDetailInRight(props.tabIdx, tasks.value[idx].id)
}
const cancel = async (idx: number) => {
const task = tasks.value[idx]
const { last_tick } = await cancelTask(task.id)
store.taskLogMap.get(task.id)!.push(...last_tick.tasks)
tasks.value[idx] = ID(last_tick.task_summary)
pollTaskMap.get(task.id)?.clearTask()
}
const remove = async (idx: number) => {
const task = tasks.value[idx]
tasks.value.splice(idx, 1)
task.id && removeTask(task.id)
message.success('删除完成')
}
const copy = (text: string) => {
copy2clipboard(text, `复制 "${text}" 成功,粘贴使用"`)
}
</script>
<template>
<div class="panel">
<div class="actions-bar">
<a-button @click="copy('<#%Y-%m-%d#>')"></a-button>
<a-button @click="copy('<#%H-%M-%S#>')"></a-button>
<a-button @click="copy('<#%Y-%m-%d %H-%M-%S#>')">复制日期+时间占位符</a-button>
</div>
</div>
<div class="wrapper" @click="showDirAutoCompletedIdx = -1">
<a-select style="display: none" />
<a-button @click="addEmptyTask" block style="border-radius: 8px;">
<template #icon>
<plus-outlined />
</template>
添加一个任务
</a-button>
<div v-for="task, idx in tasks.slice(0, loadNum)" :key="key(task)" class="task-form">
<div class="top-bar">
<a-tag color="success" v-if="isDone(task)"></a-tag>
<a-tag color="processing" v-if="task.running">{{ task.type === 'download' ? '' : '' }} <template #icon>
<sync-outlined :spin="true" />
</template></a-tag>
<a-tag color="default" v-if="task.canceled">
<template #icon>
<minus-circle-outlined />
</template>
已取消
</a-tag>
<div class="flex-placeholder"></div>
<div v-if="task.start_time">
开始时间 {{ task.start_time }}
</div>
</div>
<a-form layout="vertical" label-align="left">
<a-form-item label="任务类型">
<search-select v-model:value="task.type" :disabled="isDisable(task)" :options="['upload', 'download']"
:conv="{ value: (v) => v, text: (v) => (v === 'upload' ? '上传' : '下载') }"></search-select>
</a-form-item>
<a-form-item :label="`发送的文件夹 (${task.type === 'upload' ? '本地' : '百度云'})`"
@click.stop="task.type === 'upload' && (showDirAutoCompletedIdx = idx)">
<a-textarea auto-size :disabled="isDisable(task)" :value="task.send_dirs.join()"
@update:value="v => task.send_dirs = v.split(',')" allow-clear
placeholder="发送文件的文件夹,多个文件夹使用逗号或者换行分隔。支持使用占位符例如stable-diffusion-webui最常用表示日期的<#%Y-%m-%d#>"></a-textarea>
<local-path-shortcut v-if="task.type === 'upload'" :task="task" @update:task="v => tasks[idx] = v" :idx="idx" />
</a-form-item>
<a-form-item :label="`接收的文件夹 (${task.type !== 'upload' ? '本地' : '百度云'})`">
<a-input v-model:value="task.recv_dir" :disabled="isDisable(task)" allow-clear
@click.stop="task.type === 'download' && (showDirAutoCompletedIdx = idx)"
placeholder="用于接收的文件夹支持使用占位符例如stable-diffusion-webui最常用表示日期的<#%Y-%m-%d#>"></a-input>
<local-path-shortcut v-if="task.type === 'download'" :task="task" @update:task="v => tasks[idx] = v"
:idx="idx" />
</a-form-item>
</a-form>
<div class="action-bar">
<a-button @click="openLogDetail(idx)" v-if="store.taskLogMap.get(task.id)"></a-button>
<a-button @click="copyFrom(idx)"></a-button>
<a-button @click="cancel(idx)" v-if="task.running" danger>取消任务</a-button>
<a-button @click="remove(idx)" :disabled="task.running" danger>移除</a-button>
<a-button type="primary" v-if="!isDone(task)" :loading="task.running" :disabled="task.running"
@click="createNewTask(idx)">开始</a-button>
</div>
<a-progress v-if="task.running" :stroke-color="{
from: '#108ee9',
to: '#87d068'
}" :percent="getIntPercent(task)" status="active" />
</div>
<a-button @click="loadNum += 5" v-if="loadNum < tasks.length" block style="border-radius: 8px;">
继续加载
</a-button>
</div>
</template>
<style scoped lang="scss">
.panel {
display: flex;
padding: 4px;
justify-content: space-between;
.actions-bar>* {
margin-left: 16px;
}
}
.wrapper {
height: calc(100vh - 128px);
overflow: auto;
padding: 8px;
.action-bar {
display: flex;
justify-content: end;
align-items: center;
&>* {
margin-left: 8px;
}
}
.task-form {
border-radius: 16px;
background: var(--zp-secondary-background);
padding: 16px;
margin: 16px 8px;
.top-bar {
display: flex;
flex-direction: row;
margin-bottom: 16px;
}
}
}
</style>

View File

@ -1,20 +1,18 @@
import type { GlobalConf, UploadTaskSummary } from '@/api'
import type { GlobalConf} from '@/api'
import type { UserInfo } from '@/api/user'
import { i18n, t } from '@/i18n'
import { getPreferredLang } from '@/i18n'
import type { getAutoCompletedTagList } from '@/page/taskRecord/autoComplete'
import type { ReturnTypeAsync } from '@/util'
import { message } from 'ant-design-vue'
import { uniqueId } from 'lodash-es'
import { defineStore } from 'pinia'
import { watch } from 'vue'
import { nextTick } from 'vue'
import { ref } from 'vue'
import { typedEventEmitter, type UniqueId, ID } from 'vue3-ts-util'
interface OtherTabPane {
type: 'auto-upload' | 'task-record' | 'empty' | 'log-detail' | 'global-setting' | 'tag-search'
type: 'empty' | 'global-setting' | 'tag-search'
name: string
readonly key: string
}
@ -29,16 +27,9 @@ interface TagSearchMatchedImageGridTabPane {
id: string
}
interface LogDetailTabPane {
type: 'log-detail'
logDetailId: string
name: string
readonly key: string
}
export interface FileTransferTabPane {
type: 'local' | 'netdisk'
target: 'local' | 'netdisk' // type 和target一致
type: 'local' // | 'netdisk'
target: 'local' // | 'netdisk' // type 和target一致
name: string
readonly key: string
path?: string
@ -46,7 +37,7 @@ export interface FileTransferTabPane {
stackKey?: string
}
export type TabPane = FileTransferTabPane | OtherTabPane | LogDetailTabPane | TagSearchMatchedImageGridTabPane
export type TabPane = FileTransferTabPane | OtherTabPane | TagSearchMatchedImageGridTabPane
export interface Tab extends UniqueId {
panes: TabPane[]
@ -56,7 +47,6 @@ export interface Tab extends UniqueId {
export const useGlobalStore = defineStore('useGlobalStore', () => {
const conf = ref<GlobalConf>()
const user = ref<UserInfo>()
const autoCompletedDirList = ref([] as ReturnTypeAsync<typeof getAutoCompletedTagList>)
const enableThumbnail = ref(true)
const stackViewSplit = ref(50)
@ -80,24 +70,6 @@ export const useGlobalStore = defineStore('useGlobalStore', () => {
}
const createTaskRecordPaneIfNotExist = async (tabIdx = 0) => {
if (!tabList.value.map(v => v.panes).flat().find(v => v.type === 'task-record')) {
tabList.value[tabIdx].panes.push({ type: 'task-record', key: uniqueId(), name: '任务记录' })
}
await nextTick()
}
const openLogDetailInRight = async (tabIdx: number, id: string) => {
const tab = tabList.value[tabIdx + 1]
const log: LogDetailTabPane = { type: 'log-detail', logDetailId: id, key: uniqueId(), name: `日志详情:${id.split('-')[0]}...` }
if (!tab) {
tabList.value.push(ID({ panes: [log], key: log.key }))
} else {
tab.key = log.key
tab.panes.push(log)
}
}
const openTagSearchMatchedImageGridInRight = async (tabIdx: number, id: string, tagIds: number[]) => {
let pane = tabList.value.map(v => v.panes).flat()
.find(v => v.type === 'tag-search-matched-image-grid' && v.id === id) as TagSearchMatchedImageGridTabPane
@ -130,20 +102,10 @@ export const useGlobalStore = defineStore('useGlobalStore', () => {
const lang = ref(getPreferredLang())
watch(lang, v => i18n.global.locale.value = v as any)
const openBaiduYunIfNotLogged = (tabIdx: number, paneIdx: number) => {
if (!user.value) {
message.info(t('loginPrompt'))
const pane: FileTransferTabPane = { key: uniqueId(), type: 'netdisk', target: 'netdisk', name: t('baiduCloud') + ' ' + t('login') }
tabList.value[tabIdx].panes[paneIdx] = pane
tabList.value[tabIdx].key = pane.key
}
}
const longPressOpenContextMenu = ref(false)
const baiduNetdiskPageOpened = ref('')
return {
lang,
user,
tabList,
conf,
autoCompletedDirList,
@ -153,15 +115,11 @@ export const useGlobalStore = defineStore('useGlobalStore', () => {
dragingTab,
saveRecord,
recent, lastTabListRecord,
openLogDetailInRight,
gridThumbnailSize,
largeGridThumbnailSize,
createTaskRecordPaneIfNotExist,
openBaiduYunIfNotLogged,
longPressOpenContextMenu,
baiduNetdiskPageOpened,
openTagSearchMatchedImageGridInRight,
...typedEventEmitter<{ createNewTask: Partial<UploadTaskSummary> }>()
baiduNetdiskPageOpened : ref('')
}
}, {
persist: {

View File

@ -1,40 +0,0 @@
import { checkBaiduyunExists, type UploadTaskSummary, type UploadTaskTickStatus } from '@/api'
import { defineStore } from 'pinia'
import { reactive, ref } from 'vue'
import { FetchQueue } from 'vue3-ts-util'
import type { WithId } from 'vue3-ts-util'
export const useTaskListStore = defineStore('useTaskListStore', () => {
const taskLogMap = ref(new Map<string, UploadTaskTickStatus[]>())
const queue = reactive(new FetchQueue())
const pollInterval = ref(3)
const tasks = ref<WithId<UploadTaskSummary>[]>([])
/**
* taskRecord
*/
const pendingBaiduyunTaskQueue = reactive([] as Partial<UploadTaskSummary>[])
const showDirAutoCompletedIdx = ref(-1)
const baiduyunInstalled = ref(null as null | Promise<boolean>)
const checkBaiduyunInstalled = async () => {
if (baiduyunInstalled.value === null) {
baiduyunInstalled.value = checkBaiduyunExists()
}
return baiduyunInstalled.value
}
return {
checkBaiduyunInstalled,
baiduyunInstalled,
pollInterval,
taskLogMap,
queue,
tasks,
showDirAutoCompletedIdx,
pendingBaiduyunTaskQueue
}
}, {
persist: {
paths: ['pollInterval', 'tasks'],
key: 'useTaskListStore-v0.0.1'
}
})