Improved custom tag display for better user experience.
parent
fd9552c83c
commit
1c4479a394
|
|
@ -13,7 +13,7 @@ Promise.resolve().then(async () => {
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Infinite Image Browsing</title>
|
<title>Infinite Image Browsing</title>
|
||||||
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-d9e8fbed.js"></script>
|
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-bd9cfb84.js"></script>
|
||||||
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-618900f2.css">
|
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-618900f2.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ from scripts.iib.tool import (
|
||||||
get_valid_img_dirs,
|
get_valid_img_dirs,
|
||||||
open_folder,
|
open_folder,
|
||||||
get_img_geninfo_txt_path,
|
get_img_geninfo_txt_path,
|
||||||
|
unique_by,
|
||||||
)
|
)
|
||||||
from fastapi import FastAPI, HTTPException
|
from fastapi import FastAPI, HTTPException
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
@ -44,7 +45,7 @@ index_html_path = os.path.join(cwd, "vue/dist/index.html") # 在app.py也被使
|
||||||
|
|
||||||
|
|
||||||
send_img_path = {"value": ""}
|
send_img_path = {"value": ""}
|
||||||
mem = {"IIB_SECRET_KEY_HASH": None, "EXTRA_PATHS": []}
|
mem = {"secret_key_hash": None, "extra_paths": [], "all_scanned_paths": []}
|
||||||
secret_key = os.getenv("IIB_SECRET_KEY")
|
secret_key = os.getenv("IIB_SECRET_KEY")
|
||||||
if secret_key:
|
if secret_key:
|
||||||
print("Secret key loaded successfully. ")
|
print("Secret key loaded successfully. ")
|
||||||
|
|
@ -56,11 +57,11 @@ async def get_token(request: Request):
|
||||||
token = request.cookies.get("IIB_S")
|
token = request.cookies.get("IIB_S")
|
||||||
if not token:
|
if not token:
|
||||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||||
if not mem["IIB_SECRET_KEY_HASH"]:
|
if not mem["secret_key_hash"]:
|
||||||
mem["IIB_SECRET_KEY_HASH"] = hashlib.sha256(
|
mem["secret_key_hash"] = hashlib.sha256(
|
||||||
(secret_key + "_ciallo").encode("utf-8")
|
(secret_key + "_ciallo").encode("utf-8")
|
||||||
).hexdigest()
|
).hexdigest()
|
||||||
if mem["IIB_SECRET_KEY_HASH"] != token:
|
if mem["secret_key_hash"] != token:
|
||||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -81,9 +82,16 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def update_all_scanned_paths():
|
||||||
|
paths = img_search_dirs + mem["extra_paths"] + kwargs.get("extra_paths_cli", [])
|
||||||
|
mem["all_scanned_paths"] = unique_by(paths)
|
||||||
|
|
||||||
|
update_all_scanned_paths()
|
||||||
|
|
||||||
def update_extra_paths(conn: sqlite3.Connection):
|
def update_extra_paths(conn: sqlite3.Connection):
|
||||||
r = ExtraPath.get_extra_paths(conn, "scanned")
|
r = ExtraPath.get_extra_paths(conn, "scanned")
|
||||||
mem["EXTRA_PATHS"] = [x.path for x in r]
|
mem["extra_paths"] = [x.path for x in r]
|
||||||
|
update_all_scanned_paths()
|
||||||
|
|
||||||
def safe_commonpath(seq):
|
def safe_commonpath(seq):
|
||||||
try:
|
try:
|
||||||
|
|
@ -96,16 +104,12 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
|
||||||
"""
|
"""
|
||||||
Check if the given path is under one of the specified parent paths.
|
Check if the given path is under one of the specified parent paths.
|
||||||
:param path: The path to check.
|
:param path: The path to check.
|
||||||
:param parent_paths: A list of parent paths.
|
:param parent_paths: By default, all scanned paths are included in the list of parent paths
|
||||||
:return: True if the path is under one of the parent paths, False otherwise.
|
:return: True if the path is under one of the parent paths, False otherwise.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
if not parent_paths:
|
if not parent_paths:
|
||||||
parent_paths = (
|
parent_paths = mem["all_scanned_paths"]
|
||||||
img_search_dirs
|
|
||||||
+ mem["EXTRA_PATHS"]
|
|
||||||
+ kwargs.get("extra_paths_cli", [])
|
|
||||||
)
|
|
||||||
path = os.path.normpath(path)
|
path = os.path.normpath(path)
|
||||||
for parent_path in parent_paths:
|
for parent_path in parent_paths:
|
||||||
if safe_commonpath([path, parent_path]) == parent_path:
|
if safe_commonpath([path, parent_path]) == parent_path:
|
||||||
|
|
@ -118,9 +122,7 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
|
||||||
if not enable_access_control:
|
if not enable_access_control:
|
||||||
return True
|
return True
|
||||||
try:
|
try:
|
||||||
parent_paths: List[str] = (
|
parent_paths = mem["all_scanned_paths"]
|
||||||
img_search_dirs + mem["EXTRA_PATHS"] + kwargs.get("extra_paths_cli", [])
|
|
||||||
)
|
|
||||||
path = os.path.normpath(path)
|
path = os.path.normpath(path)
|
||||||
for parent_path in parent_paths:
|
for parent_path in parent_paths:
|
||||||
if len(path) <= len(parent_path):
|
if len(path) <= len(parent_path):
|
||||||
|
|
@ -214,7 +216,6 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
|
||||||
)
|
)
|
||||||
raise HTTPException(400, detail=error_msg)
|
raise HTTPException(400, detail=error_msg)
|
||||||
|
|
||||||
|
|
||||||
class CreateFoldersReq(BaseModel):
|
class CreateFoldersReq(BaseModel):
|
||||||
dest_folder: str
|
dest_folder: str
|
||||||
|
|
||||||
|
|
@ -225,13 +226,11 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
|
||||||
raise HTTPException(status_code=403)
|
raise HTTPException(status_code=403)
|
||||||
os.makedirs(req.dest_folder, exist_ok=True)
|
os.makedirs(req.dest_folder, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
class MoveFilesReq(BaseModel):
|
class MoveFilesReq(BaseModel):
|
||||||
file_paths: List[str]
|
file_paths: List[str]
|
||||||
dest: str
|
dest: str
|
||||||
create_dest_folder: Optional[bool] = False
|
create_dest_folder: Optional[bool] = False
|
||||||
|
|
||||||
|
|
||||||
@app.post(pre + "/copy_files", dependencies=[Depends(get_token)])
|
@app.post(pre + "/copy_files", dependencies=[Depends(get_token)])
|
||||||
async def copy_files(req: MoveFilesReq):
|
async def copy_files(req: MoveFilesReq):
|
||||||
for path in req.file_paths:
|
for path in req.file_paths:
|
||||||
|
|
@ -265,13 +264,14 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
|
||||||
for path in req.file_paths:
|
for path in req.file_paths:
|
||||||
check_path_trust(path)
|
check_path_trust(path)
|
||||||
try:
|
try:
|
||||||
shutil.move(path, req.dest)
|
ret_path = shutil.move(path, req.dest)
|
||||||
txt_path = get_img_geninfo_txt_path(path)
|
txt_path = get_img_geninfo_txt_path(path)
|
||||||
if txt_path:
|
if txt_path:
|
||||||
shutil.move(txt_path, req.dest)
|
shutil.move(txt_path, req.dest)
|
||||||
img = DbImg.get(conn, os.path.normpath(path))
|
img = DbImg.get(conn, os.path.normpath(path))
|
||||||
if img:
|
if img:
|
||||||
DbImg.safe_batch_remove(conn, [img.id])
|
img.update_path(conn, ret_path)
|
||||||
|
conn.commit()
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
error_msg = (
|
error_msg = (
|
||||||
f"Error moving file {path} to {req.dest}: {e}"
|
f"Error moving file {path} to {req.dest}: {e}"
|
||||||
|
|
@ -303,6 +303,7 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
|
||||||
return {"files": []}
|
return {"files": []}
|
||||||
check_path_trust(folder_path)
|
check_path_trust(folder_path)
|
||||||
folder_listing: List[os.DirEntry] = os.scandir(folder_path)
|
folder_listing: List[os.DirEntry] = os.scandir(folder_path)
|
||||||
|
is_under_scanned_path = is_path_under_parents(folder_path)
|
||||||
for item in folder_listing:
|
for item in folder_listing:
|
||||||
if not os.path.exists(item.path):
|
if not os.path.exists(item.path):
|
||||||
continue
|
continue
|
||||||
|
|
@ -322,6 +323,7 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
|
||||||
"bytes": bytes,
|
"bytes": bytes,
|
||||||
"created_time": created_time,
|
"created_time": created_time,
|
||||||
"fullpath": fullpath,
|
"fullpath": fullpath,
|
||||||
|
"is_under_scanned_path": is_under_scanned_path,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
elif item.is_dir():
|
elif item.is_dir():
|
||||||
|
|
@ -332,6 +334,7 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
|
||||||
"created_time": created_time,
|
"created_time": created_time,
|
||||||
"size": "-",
|
"size": "-",
|
||||||
"name": name,
|
"name": name,
|
||||||
|
"is_under_scanned_path": is_under_scanned_path,
|
||||||
"fullpath": fullpath,
|
"fullpath": fullpath,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -489,7 +492,7 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
|
||||||
update_extra_paths(conn)
|
update_extra_paths(conn)
|
||||||
dirs = (
|
dirs = (
|
||||||
img_search_dirs if img_count == 0 else Floder.get_expired_dirs(conn)
|
img_search_dirs if img_count == 0 else Floder.get_expired_dirs(conn)
|
||||||
) + mem["EXTRA_PATHS"]
|
) + mem["extra_paths"]
|
||||||
|
|
||||||
update_image_data(dirs)
|
update_image_data(dirs)
|
||||||
finally:
|
finally:
|
||||||
|
|
@ -529,6 +532,14 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
|
||||||
# tags = Tag.get_all_custom_tag()
|
# tags = Tag.get_all_custom_tag()
|
||||||
return ImageTag.get_tags_for_image(conn, img.id, type="custom")
|
return ImageTag.get_tags_for_image(conn, img.id, type="custom")
|
||||||
|
|
||||||
|
class PathsReq(BaseModel):
|
||||||
|
paths: List[str]
|
||||||
|
|
||||||
|
@app.post(db_pre + "/get_image_tags", dependencies=[Depends(get_token)])
|
||||||
|
async def get_img_tags(req: PathsReq):
|
||||||
|
conn = DataBase.get_conn()
|
||||||
|
return ImageTag.batch_get_tags_by_path(conn, req.paths)
|
||||||
|
|
||||||
class ToggleCustomTagToImgReq(BaseModel):
|
class ToggleCustomTagToImgReq(BaseModel):
|
||||||
img_path: str
|
img_path: str
|
||||||
tag_id: int
|
tag_id: int
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
from sqlite3 import Connection, connect
|
from sqlite3 import Connection, connect
|
||||||
from typing import Dict, List, Optional, Union
|
from typing import Dict, List, Optional
|
||||||
from scripts.iib.tool import (
|
from scripts.iib.tool import (
|
||||||
cwd,
|
cwd,
|
||||||
get_modified_date,
|
get_modified_date,
|
||||||
|
|
@ -79,6 +79,14 @@ class Image:
|
||||||
)
|
)
|
||||||
self.id = cur.lastrowid
|
self.id = cur.lastrowid
|
||||||
|
|
||||||
|
def update_path(self, conn: Connection, new_path: str):
|
||||||
|
self.path = os.path.normpath(new_path)
|
||||||
|
with closing(conn.cursor()) as cur:
|
||||||
|
cur.execute(
|
||||||
|
"UPDATE image SET path = ? WHERE id = ?",
|
||||||
|
(self.path, self.id)
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get(cls, conn: Connection, id_or_path):
|
def get(cls, conn: Connection, id_or_path):
|
||||||
with closing(conn.cursor()) as cur:
|
with closing(conn.cursor()) as cur:
|
||||||
|
|
@ -174,7 +182,7 @@ class Image:
|
||||||
deleted_ids.append(img.id)
|
deleted_ids.append(img.id)
|
||||||
cls.safe_batch_remove(conn, deleted_ids)
|
cls.safe_batch_remove(conn, deleted_ids)
|
||||||
return images
|
return images
|
||||||
|
|
||||||
|
|
||||||
class Tag:
|
class Tag:
|
||||||
def __init__(self, name: str, score: int, type: str, count=0):
|
def __init__(self, name: str, score: int, type: str, count=0):
|
||||||
|
|
@ -270,6 +278,8 @@ class Tag:
|
||||||
VALUES ("like", 0, "custom", 0);
|
VALUES ("like", 0, "custom", 0);
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class ImageTag:
|
class ImageTag:
|
||||||
|
|
@ -399,7 +409,31 @@ class ImageTag:
|
||||||
deleted_ids.append(img.id)
|
deleted_ids.append(img.id)
|
||||||
Image.safe_batch_remove(conn, deleted_ids)
|
Image.safe_batch_remove(conn, deleted_ids)
|
||||||
return images
|
return images
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def batch_get_tags_by_path(cls, conn: Connection, paths: List[str], type = "custom") -> Dict[str, List[Tag]]:
|
||||||
|
if not paths:
|
||||||
|
return {}
|
||||||
|
tag_dict = {}
|
||||||
|
with closing(conn.cursor()) as cur:
|
||||||
|
placeholders = ",".join("?" * len(paths))
|
||||||
|
query = f"""
|
||||||
|
SELECT image.path, tag.* FROM image_tag
|
||||||
|
INNER JOIN image ON image_tag.image_id = image.id
|
||||||
|
INNER JOIN tag ON image_tag.tag_id = tag.id
|
||||||
|
WHERE tag.type = '{type}' AND image.path IN ({placeholders})
|
||||||
|
"""
|
||||||
|
cur.execute(query, paths)
|
||||||
|
rows = cur.fetchall()
|
||||||
|
for row in rows:
|
||||||
|
path = row[0]
|
||||||
|
tag = Tag.from_row(row[1:])
|
||||||
|
if path in tag_dict:
|
||||||
|
tag_dict[path].append(tag)
|
||||||
|
else:
|
||||||
|
tag_dict[path] = [tag]
|
||||||
|
return tag_dict
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def remove(
|
def remove(
|
||||||
cls,
|
cls,
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,7 @@ declare module '@vue/runtime-core' {
|
||||||
ASwitch: typeof import('ant-design-vue/es')['Switch']
|
ASwitch: typeof import('ant-design-vue/es')['Switch']
|
||||||
ATabPane: typeof import('ant-design-vue/es')['TabPane']
|
ATabPane: typeof import('ant-design-vue/es')['TabPane']
|
||||||
ATabs: typeof import('ant-design-vue/es')['Tabs']
|
ATabs: typeof import('ant-design-vue/es')['Tabs']
|
||||||
|
ATag: typeof import('ant-design-vue/es')['Tag']
|
||||||
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
|
||||||
NumInput: typeof import('./src/components/numInput.vue')['default']
|
NumInput: typeof import('./src/components/numInput.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
import{d as t,o as a,m as r,cE as n}from"./index-d9e8fbed.js";const p=t({__name:"ImgSliPagePane",props:{paneIdx:{},tabIdx:{},left:{},right:{}},setup(o){return(e,s)=>(a(),r(n,{left:e.left,right:e.right},null,8,["left","right"]))}});export{p as default};
|
import{d as t,o as a,m as r,cI as n}from"./index-bd9cfb84.js";const p=t({__name:"ImgSliPagePane",props:{paneIdx:{},tabIdx:{},left:{},right:{}},setup(o){return(e,s)=>(a(),r(n,{left:e.left,right:e.right},null,8,["left","right"]))}});export{p as default};
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
.preview-switch[data-v-d4722c8d]{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-d4722c8d]{color:#fff;margin:16px;font-size:4em;pointer-events:all;cursor:pointer}.preview-switch>*.disable[data-v-d4722c8d]{opacity:0;pointer-events:none;cursor:none}.container[data-v-d4722c8d]{background:var(--zp-secondary-background)}.container .file-list[data-v-d4722c8d]{list-style:none;padding:8px;height:100%;overflow:auto;height:var(--pane-max-height);width:100%}
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
.preview-switch[data-v-3c251729]{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-3c251729]{color:#fff;margin:16px;font-size:4em;pointer-events:all;cursor:pointer}.preview-switch>*.disable[data-v-3c251729]{opacity:0;pointer-events:none;cursor:none}.container[data-v-3c251729]{background:var(--zp-secondary-background)}.container .file-list[data-v-3c251729]{list-style:none;padding:8px;height:100%;overflow:auto;height:var(--pane-max-height);width:100%}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
import{d as q,l as Q,ax as j,o as r,y as _,c as s,n as a,r as e,s as h,p as y,t as W,v as b,x as X,m as M,L as H,E as u,N as S,Q as J,R as K,X as Y}from"./index-bd9cfb84.js";import{h as Z,i as ee,L as te,R as ie,j as le,S as se}from"./fullScreenContextMenu-c82c54b8.js";import{g as ne}from"./db-a47df277.js";import{u as ae}from"./hook-1cb05846.js";import"./shortcut-6308494d.js";const oe={class:"hint"},re={key:1,class:"preview-switch"},de=q({__name:"MatchedImageGrid",props:{tabIdx:{},paneIdx:{},selectedTagIds:{},id:{}},setup(T){const m=T,{queue:p,images:i,onContextMenuClickU:g,stackViewEl:V,previewIdx:n,previewing:v,onPreviewVisibleChange:D,previewImgMove:f,canPreview:w,itemSize:I,gridItems:z,showGenInfo:o,imageGenInfo:k,q:F,multiSelectedIdxs:$,onFileItemClick:B,scroller:x,showMenuIdx:d,onFileDragStart:E,onFileDragEnd:G,cellWidth:N,onScroll:R,updateImageTag:A}=ae();return Q(()=>m.selectedTagIds,async()=>{const{res:c}=p.pushAction(()=>ne(m.selectedTagIds));i.value=await c,await j(),A(),x.value.scrollToItem(0)},{immediate:!0}),(c,t)=>{const P=J,U=K,L=se;return r(),_("div",{class:"container",ref_key:"stackViewEl",ref:V},[s(L,{size:"large",spinning:!e(p).isIdle},{default:a(()=>[s(U,{visible:e(o),"onUpdate:visible":t[1]||(t[1]=l=>h(o)?o.value=l:null),width:"70vw","mask-closable":"",onOk:t[2]||(t[2]=l=>o.value=!1)},{cancelText:a(()=>[]),default:a(()=>[s(P,{active:"",loading:!e(F).isIdle},{default:a(()=>[y("div",{style:{width:"100%","word-break":"break-all","white-space":"pre-line","max-height":"70vh",overflow:"auto"},onDblclick:t[0]||(t[0]=l=>e(W)(e(k)))},[y("div",oe,b(c.$t("doubleClickToCopy")),1),X(" "+b(e(k)),1)],32)]),_:1},8,["loading"])]),_:1},8,["visible"]),e(i)?(r(),M(e(Z),{key:0,ref_key:"scroller",ref:x,class:"file-list",items:e(i),"item-size":e(I).first,"key-field":"fullpath","item-secondary-size":e(I).second,gridItems:e(z),onScroll:e(R)},{default:a(({item:l,index:C})=>[s(ee,{idx:C,file:l,"cell-width":e(N),"show-menu-idx":e(d),"onUpdate:showMenuIdx":t[3]||(t[3]=O=>h(d)?d.value=O:null),onDragstart:e(E),onDragend:e(G),onFileItemClick:e(B),"full-screen-preview-image-url":e(i)[e(n)]?e(H)(e(i)[e(n)]):"",selected:e($).includes(C),onContextMenuClick:e(g),onPreviewVisibleChange:e(D)},null,8,["idx","file","cell-width","show-menu-idx","onDragstart","onDragend","onFileItemClick","full-screen-preview-image-url","selected","onContextMenuClick","onPreviewVisibleChange"])]),_:1},8,["items","item-size","item-secondary-size","gridItems","onScroll"])):u("",!0),e(v)?(r(),_("div",re,[s(e(te),{onClick:t[4]||(t[4]=l=>e(f)("prev")),class:S({disable:!e(w)("prev")})},null,8,["class"]),s(e(ie),{onClick:t[5]||(t[5]=l=>e(f)("next")),class:S({disable:!e(w)("next")})},null,8,["class"])])):u("",!0)]),_:1},8,["spinning"]),e(v)&&e(i)&&e(i)[e(n)]?(r(),M(le,{key:0,file:e(i)[e(n)],idx:e(n),onContextMenuClick:e(g)},null,8,["file","idx","onContextMenuClick"])):u("",!0)],512)}}});const ve=Y(de,[["__scopeId","data-v-3c251729"]]);export{ve as default};
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
import{d as L,l as O,o as r,y as _,c as l,n as a,r as e,s as h,p as y,t as q,v as b,x as Q,m as M,L as j,E as u,N as S,Q as W,R as X,X as H}from"./index-d9e8fbed.js";import{h as J,i as K,L as Y,R as Z,j as ee,S as te}from"./fullScreenContextMenu-caca4231.js";import{g as ie}from"./db-ea72b770.js";import{u as se}from"./hook-900c55c9.js";import"./shortcut-9b4bff3d.js";const le={class:"hint"},ne={key:1,class:"preview-switch"},ae=L({__name:"MatchedImageGrid",props:{tabIdx:{},paneIdx:{},selectedTagIds:{},id:{}},setup(V){const m=V,{queue:p,images:i,onContextMenuClickU:g,stackViewEl:D,previewIdx:n,previewing:v,onPreviewVisibleChange:T,previewImgMove:f,canPreview:w,itemSize:I,gridItems:z,showGenInfo:o,imageGenInfo:k,q:F,multiSelectedIdxs:$,onFileItemClick:B,scroller:C,showMenuIdx:d,onFileDragStart:E,onFileDragEnd:G,cellWidth:N}=se();return O(()=>m.selectedTagIds,async()=>{var t;const{res:c}=p.pushAction(()=>ie(m.selectedTagIds));i.value=await c,(t=C.value)==null||t.scrollToItem(0)},{immediate:!0}),(c,t)=>{const R=W,A=X,P=te;return r(),_("div",{class:"container",ref_key:"stackViewEl",ref:D},[l(P,{size:"large",spinning:!e(p).isIdle},{default:a(()=>[l(A,{visible:e(o),"onUpdate:visible":t[1]||(t[1]=s=>h(o)?o.value=s:null),width:"70vw","mask-closable":"",onOk:t[2]||(t[2]=s=>o.value=!1)},{cancelText:a(()=>[]),default:a(()=>[l(R,{active:"",loading:!e(F).isIdle},{default:a(()=>[y("div",{style:{width:"100%","word-break":"break-all","white-space":"pre-line","max-height":"70vh",overflow:"auto"},onDblclick:t[0]||(t[0]=s=>e(q)(e(k)))},[y("div",le,b(c.$t("doubleClickToCopy")),1),Q(" "+b(e(k)),1)],32)]),_:1},8,["loading"])]),_:1},8,["visible"]),e(i)?(r(),M(e(J),{key:0,ref_key:"scroller",ref:C,class:"file-list",items:e(i),"item-size":e(I).first,"key-field":"fullpath","item-secondary-size":e(I).second,gridItems:e(z)},{default:a(({item:s,index:x})=>[l(K,{idx:x,file:s,"cell-width":e(N),"show-menu-idx":e(d),"onUpdate:showMenuIdx":t[3]||(t[3]=U=>h(d)?d.value=U:null),onDragstart:e(E),onDragend:e(G),onFileItemClick:e(B),"full-screen-preview-image-url":e(i)[e(n)]?e(j)(e(i)[e(n)]):"",selected:e($).includes(x),onContextMenuClick:e(g),onPreviewVisibleChange:e(T)},null,8,["idx","file","cell-width","show-menu-idx","onDragstart","onDragend","onFileItemClick","full-screen-preview-image-url","selected","onContextMenuClick","onPreviewVisibleChange"])]),_:1},8,["items","item-size","item-secondary-size","gridItems"])):u("",!0),e(v)?(r(),_("div",ne,[l(e(Y),{onClick:t[4]||(t[4]=s=>e(f)("prev")),class:S({disable:!e(w)("prev")})},null,8,["class"]),l(e(Z),{onClick:t[5]||(t[5]=s=>e(f)("next")),class:S({disable:!e(w)("next")})},null,8,["class"])])):u("",!0)]),_:1},8,["spinning"]),e(v)&&e(i)&&e(i)[e(n)]?(r(),M(ee,{key:0,file:e(i)[e(n)],idx:e(n),onContextMenuClick:e(g)},null,8,["file","idx","onContextMenuClick"])):u("",!0)],512)}}});const me=H(ae,[["__scopeId","data-v-d4722c8d"]]);export{me as default};
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
import{d as X,$,aw as J,bQ as Y,bP as B,o,y as k,c as r,r as e,bT as Z,m,n as d,x as w,v,E as f,s as V,p as A,t as ee,L as ne,N as E,ar as te,ai as se,U as ae,V as ie,Q as le,R as oe,X as re}from"./index-d9e8fbed.js";import{h as de,i as ue,L as ce,R as pe,j as me,S as ve}from"./fullScreenContextMenu-caca4231.js";/* empty css */import{b as U,c as fe,e as ge,u as ke}from"./db-ea72b770.js";import{u as we}from"./hook-900c55c9.js";import"./shortcut-9b4bff3d.js";const ye={key:0,class:"search-bar"},Ie={class:"hint"},Ce={key:1,class:"preview-switch"},xe=X({__name:"SubstrSearch",setup(be){const{queue:l,images:a,onContextMenuClickU:y,stackViewEl:F,previewIdx:u,previewing:I,onPreviewVisibleChange:R,previewImgMove:C,canPreview:x,itemSize:b,gridItems:T,showGenInfo:c,imageGenInfo:h,q:N,multiSelectedIdxs:P,onFileItemClick:L,scroller:_,showMenuIdx:g,onFileDragStart:q,onFileDragEnd:G,cellWidth:K}=we(),p=$(""),t=$();J(async()=>{t.value=await U(),t.value.img_count&&t.value.expired&&S()});const S=Y(()=>l.pushAction(async()=>(await ke(),t.value=await U(),t.value)).res),M=async()=>{var s;a.value=await l.pushAction(()=>ge(p.value)).res,(s=_.value)==null||s.scrollToItem(0),a.value.length||te.info(se("fuzzy-search-noResults"))};return B("returnToIIB",async()=>{const s=await l.pushAction(fe).res;t.value.expired=s.expired}),B("searchIndexExpired",()=>t.value&&(t.value.expired=!0)),(s,n)=>{const O=ae,z=ie,Q=le,j=oe,H=ve;return o(),k("div",{class:"container",ref_key:"stackViewEl",ref:F},[t.value?(o(),k("div",ye,[r(O,{value:p.value,"onUpdate:value":n[0]||(n[0]=i=>p.value=i),placeholder:s.$t("fuzzy-search-placeholder"),disabled:!e(l).isIdle,onKeydown:Z(M,["enter"])},null,8,["value","placeholder","disabled","onKeydown"]),t.value.expired||!t.value.img_count?(o(),m(z,{key:0,onClick:e(S),loading:!e(l).isIdle,type:"primary"},{default:d(()=>[w(v(t.value.img_count===0?s.$t("generateIndexHint"):s.$t("UpdateIndex")),1)]),_:1},8,["onClick","loading"])):(o(),m(z,{key:1,type:"primary",onClick:M,loading:!e(l).isIdle,disabled:!p.value},{default:d(()=>[w(v(s.$t("search")),1)]),_:1},8,["loading","disabled"]))])):f("",!0),r(H,{size:"large",spinning:!e(l).isIdle},{default:d(()=>[r(j,{visible:e(c),"onUpdate:visible":n[2]||(n[2]=i=>V(c)?c.value=i:null),width:"70vw","mask-closable":"",onOk:n[3]||(n[3]=i=>c.value=!1)},{cancelText:d(()=>[]),default:d(()=>[r(Q,{active:"",loading:!e(N).isIdle},{default:d(()=>[A("div",{style:{width:"100%","word-break":"break-all","white-space":"pre-line","max-height":"70vh",overflow:"auto"},onDblclick:n[1]||(n[1]=i=>e(ee)(e(h)))},[A("div",Ie,v(s.$t("doubleClickToCopy")),1),w(" "+v(e(h)),1)],32)]),_:1},8,["loading"])]),_:1},8,["visible"]),e(a)?(o(),m(e(de),{key:0,ref_key:"scroller",ref:_,class:"file-list",items:e(a),"item-size":e(b).first,"key-field":"fullpath","item-secondary-size":e(b).second,gridItems:e(T)},{default:d(({item:i,index:D})=>[r(ue,{idx:D,file:i,"show-menu-idx":e(g),"onUpdate:showMenuIdx":n[4]||(n[4]=W=>V(g)?g.value=W:null),onFileItemClick:e(L),"full-screen-preview-image-url":e(a)[e(u)]?e(ne)(e(a)[e(u)]):"","cell-width":e(K),selected:e(P).includes(D),onContextMenuClick:e(y),onDragstart:e(q),onDragend:e(G),onPreviewVisibleChange:e(R)},null,8,["idx","file","show-menu-idx","onFileItemClick","full-screen-preview-image-url","cell-width","selected","onContextMenuClick","onDragstart","onDragend","onPreviewVisibleChange"])]),_:1},8,["items","item-size","item-secondary-size","gridItems"])):f("",!0),e(I)?(o(),k("div",Ce,[r(e(ce),{onClick:n[5]||(n[5]=i=>e(C)("prev")),class:E({disable:!e(x)("prev")})},null,8,["class"]),r(e(pe),{onClick:n[6]||(n[6]=i=>e(C)("next")),class:E({disable:!e(x)("next")})},null,8,["class"])])):f("",!0)]),_:1},8,["spinning"]),e(I)&&e(a)&&e(a)[e(u)]?(o(),m(me,{key:1,file:e(a)[e(u)],idx:e(u),onContextMenuClick:e(y)},null,8,["file","idx","onContextMenuClick"])):f("",!0)],512)}}});const $e=re(xe,[["__scopeId","data-v-01615fdf"]]);export{$e as default};
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
.search-bar[data-v-01615fdf]{padding:8px;display:flex}.preview-switch[data-v-01615fdf]{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-01615fdf]{color:#fff;margin:16px;font-size:4em;pointer-events:all;cursor:pointer}.preview-switch>*.disable[data-v-01615fdf]{opacity:0;pointer-events:none;cursor:none}.container[data-v-01615fdf]{background:var(--zp-secondary-background)}.container .file-list[data-v-01615fdf]{list-style:none;padding:8px;height:100%;overflow:auto;height:var(--pane-max-height);width:100%}
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
import{d as Y,$,aw as Z,bQ as ee,bP as B,o,y as k,c as r,r as e,bT as ae,m,n as d,x as w,v,E as f,s as V,p as A,t as ne,L as te,N as E,ax as le,ar as se,ai as ie,U as oe,V as re,Q as de,R as ue,X as ce}from"./index-bd9cfb84.js";import{h as pe,i as me,L as ve,R as fe,j as ge,S as ke}from"./fullScreenContextMenu-c82c54b8.js";/* empty css */import{b as T,c as we,e as ye,u as Ie}from"./db-a47df277.js";import{u as xe}from"./hook-1cb05846.js";import"./shortcut-6308494d.js";const be={key:0,class:"search-bar"},Ce={class:"hint"},he={key:1,class:"preview-switch"},_e=Y({__name:"SubstrSearch",setup(Se){const{queue:s,images:t,onContextMenuClickU:y,stackViewEl:U,previewIdx:u,previewing:I,onPreviewVisibleChange:F,previewImgMove:x,canPreview:b,itemSize:C,gridItems:R,showGenInfo:c,imageGenInfo:h,q:N,multiSelectedIdxs:P,onFileItemClick:L,scroller:_,showMenuIdx:g,onFileDragStart:q,onFileDragEnd:G,cellWidth:K,onScroll:O,updateImageTag:Q}=xe(),p=$(""),n=$();Z(async()=>{n.value=await T(),n.value.img_count&&n.value.expired&&S()});const S=ee(()=>s.pushAction(async()=>(await Ie(),n.value=await T(),n.value)).res),M=async()=>{t.value=await s.pushAction(()=>ye(p.value)).res,await le(),Q(),_.value.scrollToItem(0),t.value.length||se.info(ie("fuzzy-search-noResults"))};return B("returnToIIB",async()=>{const i=await s.pushAction(we).res;n.value.expired=i.expired}),B("searchIndexExpired",()=>n.value&&(n.value.expired=!0)),(i,a)=>{const j=oe,z=re,H=de,W=ue,X=ke;return o(),k("div",{class:"container",ref_key:"stackViewEl",ref:U},[n.value?(o(),k("div",be,[r(j,{value:p.value,"onUpdate:value":a[0]||(a[0]=l=>p.value=l),placeholder:i.$t("fuzzy-search-placeholder"),disabled:!e(s).isIdle,onKeydown:ae(M,["enter"])},null,8,["value","placeholder","disabled","onKeydown"]),n.value.expired||!n.value.img_count?(o(),m(z,{key:0,onClick:e(S),loading:!e(s).isIdle,type:"primary"},{default:d(()=>[w(v(n.value.img_count===0?i.$t("generateIndexHint"):i.$t("UpdateIndex")),1)]),_:1},8,["onClick","loading"])):(o(),m(z,{key:1,type:"primary",onClick:M,loading:!e(s).isIdle,disabled:!p.value},{default:d(()=>[w(v(i.$t("search")),1)]),_:1},8,["loading","disabled"]))])):f("",!0),r(X,{size:"large",spinning:!e(s).isIdle},{default:d(()=>[r(W,{visible:e(c),"onUpdate:visible":a[2]||(a[2]=l=>V(c)?c.value=l:null),width:"70vw","mask-closable":"",onOk:a[3]||(a[3]=l=>c.value=!1)},{cancelText:d(()=>[]),default:d(()=>[r(H,{active:"",loading:!e(N).isIdle},{default:d(()=>[A("div",{style:{width:"100%","word-break":"break-all","white-space":"pre-line","max-height":"70vh",overflow:"auto"},onDblclick:a[1]||(a[1]=l=>e(ne)(e(h)))},[A("div",Ce,v(i.$t("doubleClickToCopy")),1),w(" "+v(e(h)),1)],32)]),_:1},8,["loading"])]),_:1},8,["visible"]),e(t)?(o(),m(e(pe),{key:0,ref_key:"scroller",ref:_,class:"file-list",items:e(t),"item-size":e(C).first,"key-field":"fullpath","item-secondary-size":e(C).second,gridItems:e(R),onScroll:e(O)},{default:d(({item:l,index:D})=>[r(me,{idx:D,file:l,"show-menu-idx":e(g),"onUpdate:showMenuIdx":a[4]||(a[4]=J=>V(g)?g.value=J:null),onFileItemClick:e(L),"full-screen-preview-image-url":e(t)[e(u)]?e(te)(e(t)[e(u)]):"","cell-width":e(K),selected:e(P).includes(D),onContextMenuClick:e(y),onDragstart:e(q),onDragend:e(G),onPreviewVisibleChange:e(F)},null,8,["idx","file","show-menu-idx","onFileItemClick","full-screen-preview-image-url","cell-width","selected","onContextMenuClick","onDragstart","onDragend","onPreviewVisibleChange"])]),_:1},8,["items","item-size","item-secondary-size","gridItems","onScroll"])):f("",!0),e(I)?(o(),k("div",he,[r(e(ve),{onClick:a[5]||(a[5]=l=>e(x)("prev")),class:E({disable:!e(b)("prev")})},null,8,["class"]),r(e(fe),{onClick:a[6]||(a[6]=l=>e(x)("next")),class:E({disable:!e(b)("next")})},null,8,["class"])])):f("",!0)]),_:1},8,["spinning"]),e(I)&&e(t)&&e(t)[e(u)]?(o(),m(ge,{key:1,file:e(t)[e(u)],idx:e(u),onContextMenuClick:e(y)},null,8,["file","idx","onContextMenuClick"])):f("",!0)],512)}}});const Ae=ce(_e,[["__scopeId","data-v-905bf6da"]]);export{Ae as default};
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
.search-bar[data-v-905bf6da]{padding:8px;display:flex}.preview-switch[data-v-905bf6da]{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-905bf6da]{color:#fff;margin:16px;font-size:4em;pointer-events:all;cursor:pointer}.preview-switch>*.disable[data-v-905bf6da]{opacity:0;pointer-events:none;cursor:none}.container[data-v-905bf6da]{background:var(--zp-secondary-background)}.container .file-list[data-v-905bf6da]{list-style:none;padding:8px;height:100%;overflow:auto;height:var(--pane-max-height);width:100%}
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
||||||
import{c4 as t}from"./index-d9e8fbed.js";const o=async()=>(await t.value.get("/db/basic_info")).data,c=async()=>(await t.value.get("/db/expired_dirs")).data,r=async()=>{await t.value.post("/db/update_image_data",{},{timeout:1/0})},d=async a=>(await t.value.post("/db/match_images_by_tags",a)).data,g=async a=>(await t.value.post("/db/add_custom_tag",a)).data,u=async a=>(await t.value.post("/db/toggle_custom_tag_to_img",a)).data,p=async a=>{await t.value.post("/db/remove_custom_tag",a)},i=async a=>(await t.value.get("/db/img_selected_custom_tag",{params:{path:a}})).data,m=async a=>(await t.value.get("/db/search_by_substr",{params:{substr:a}})).data,e="/db/scanned_paths",_=async a=>{await t.value.post(e,{path:a})},b=async a=>{await t.value.delete(e,{data:{path:a}})};export{_ as a,o as b,c,g as d,m as e,b as f,d as g,i as h,p as r,u as t,r as u};
|
import{c6 as t}from"./index-bd9cfb84.js";const o=async()=>(await t.value.get("/db/basic_info")).data,c=async()=>(await t.value.get("/db/expired_dirs")).data,r=async()=>{await t.value.post("/db/update_image_data",{},{timeout:1/0})},d=async a=>(await t.value.post("/db/match_images_by_tags",a)).data,g=async a=>(await t.value.post("/db/add_custom_tag",a)).data,u=async a=>(await t.value.post("/db/toggle_custom_tag_to_img",a)).data,p=async a=>{await t.value.post("/db/remove_custom_tag",a)},i=async a=>(await t.value.get("/db/search_by_substr",{params:{substr:a}})).data,e="/db/scanned_paths",m=async a=>{await t.value.post(e,{path:a})},_=async a=>{await t.value.delete(e,{data:{path:a}})},b=async a=>(await t.value.post("/db/get_image_tags",{paths:a})).data;export{m as a,o as b,c,g as d,i as e,b as f,d as g,_ as h,p as r,u as t,r as u};
|
||||||
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
|
|
@ -0,0 +1 @@
|
||||||
|
import{$ as D,bO as E,bd as P,aC as $}from"./index-bd9cfb84.js";import{k as z,u as G,b as L,f as O,a as Q,c as R,d as V,e as _,l as A}from"./fullScreenContextMenu-c82c54b8.js";const U=()=>{const e=D(),c=E(),u=z(),n={tabIdx:-1,target:"local",paneIdx:-1,walkMode:!1},{stackViewEl:r,multiSelectedIdxs:d,stack:m,scroller:o}=G({images:e}).toRefs(),{itemSize:g,gridItems:p,cellWidth:v}=L(n),{showMenuIdx:I}=O();Q(n);const{onFileDragStart:f,onFileDragEnd:x}=R(),{showGenInfo:h,imageGenInfo:w,q:k,onContextMenuClick:i,onFileItemClick:S}=V(n,{openNext:P}),{previewIdx:M,previewing:b,onPreviewVisibleChange:C,previewImgMove:F,canPreview:y}=_(n),T=async(s,a,t)=>{m.value=[{curr:"",files:e.value}],await i(s,a,t)};A("removeFiles",async({paths:s})=>{var a;e.value=(a=e.value)==null?void 0:a.filter(t=>!s.includes(t.fullpath))});const l=()=>{const s=o.value;if(s&&e.value){const a=e.value.slice(Math.max(s.$_startIndex-10,0),s.$_endIndex+10).map(t=>t.fullpath);u.fetchImageTags(a)}},q=$(l,300);return{scroller:o,queue:c,images:e,onContextMenuClickU:T,stackViewEl:r,previewIdx:M,previewing:b,onPreviewVisibleChange:C,previewImgMove:F,canPreview:y,itemSize:g,gridItems:p,showGenInfo:h,imageGenInfo:w,q:k,onContextMenuClick:i,onFileItemClick:S,showMenuIdx:I,multiSelectedIdxs:d,onFileDragStart:f,onFileDragEnd:x,cellWidth:v,onScroll:q,updateImageTag:l}};export{U as u};
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
import{$ as c,bO as q,bd as D}from"./index-d9e8fbed.js";import{u as E,b as P,f as z,a as G,c as L,d as O,e as Q,k as R}from"./fullScreenContextMenu-caca4231.js";const H=()=>{const e=c(),l=q(),o=c(),s={tabIdx:-1,target:"local",paneIdx:-1,walkMode:!1},{stackViewEl:r,multiSelectedIdxs:u,stack:m}=E({images:e}).toRefs(),{itemSize:d,gridItems:v,cellWidth:f}=P(s),{showMenuIdx:p}=z();G(s);const{onFileDragStart:I,onFileDragEnd:g}=L(),{showGenInfo:w,imageGenInfo:k,q:x,onContextMenuClick:i,onFileItemClick:h}=O(s,{openNext:D}),{previewIdx:F,previewing:M,onPreviewVisibleChange:b,previewImgMove:C,canPreview:S}=Q(s,{scroller:o}),y=async(a,t,n)=>{m.value=[{curr:"",files:e.value}],await i(a,t,n)};return R("removeFiles",async({paths:a})=>{var t;e.value=(t=e.value)==null?void 0:t.filter(n=>!a.includes(n.fullpath))}),{scroller:o,queue:l,images:e,onContextMenuClickU:y,stackViewEl:r,previewIdx:F,previewing:M,onPreviewVisibleChange:b,previewImgMove:C,canPreview:S,itemSize:d,gridItems:v,showGenInfo:w,imageGenInfo:k,q:x,onContextMenuClick:i,onFileItemClick:h,showMenuIdx:p,multiSelectedIdxs:u,onFileDragStart:I,onFileDragEnd:g,cellWidth:f}};export{H as u};
|
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
||||||
import{cD as s}from"./index-d9e8fbed.js";var r=1/0,i=17976931348623157e292;function e(t){if(!t)return t===0?t:0;if(t=s(t),t===r||t===-r){var n=t<0?-1:1;return n*i}return t===t?t:0}function f(t){var n=t==null?0:t.length;return n?t[n-1]:void 0}const h=t=>{const n=[];return t.shiftKey&&n.push("Shift"),t.ctrlKey&&n.push("Ctrl"),t.metaKey&&n.push("Cmd"),(t.code.startsWith("Key")||t.code.startsWith("Digit"))&&n.push(t.code),n.join(" + ")};export{h as g,f as l,e as t};
|
import{cH as s}from"./index-bd9cfb84.js";var r=1/0,i=17976931348623157e292;function e(t){if(!t)return t===0?t:0;if(t=s(t),t===r||t===-r){var n=t<0?-1:1;return n*i}return t===t?t:0}function f(t){var n=t==null?0:t.length;return n?t[n-1]:void 0}const h=t=>{const n=[];return t.shiftKey&&n.push("Shift"),t.ctrlKey&&n.push("Ctrl"),t.metaKey&&n.push("Cmd"),(t.code.startsWith("Key")||t.code.startsWith("Digit"))&&n.push(t.code),n.join(" + ")};export{h as g,f as l,e as t};
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -7,7 +7,7 @@
|
||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Infinite Image Browsing</title>
|
<title>Infinite Image Browsing</title>
|
||||||
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-d9e8fbed.js"></script>
|
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-bd9cfb84.js"></script>
|
||||||
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-618900f2.css">
|
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-618900f2.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { Dict } from '@/util'
|
||||||
import type { FileNodeInfo } from './files'
|
import type { FileNodeInfo } from './files'
|
||||||
import { axiosInst } from './index'
|
import { axiosInst } from './index'
|
||||||
|
|
||||||
|
|
@ -84,4 +85,9 @@ export const addScannedPath = async (path: string) => {
|
||||||
}
|
}
|
||||||
export const removeScannedPath = async (path: string) => {
|
export const removeScannedPath = async (path: string) => {
|
||||||
await axiosInst.value.delete(scannedPaths, { data: { path } })
|
await axiosInst.value.delete(scannedPaths, { data: { path } })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const batchGetTagsByPath = async (paths: string[]) => {
|
||||||
|
const resp = await axiosInst.value.post('/db/get_image_tags', { paths })
|
||||||
|
return resp.data as Dict<Tag[]>
|
||||||
}
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ export interface FileNodeInfo {
|
||||||
date: string
|
date: string
|
||||||
bytes: number
|
bytes: number
|
||||||
fullpath: string
|
fullpath: string
|
||||||
|
is_under_scanned_path: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getTargetFolderFiles = async (folder_path: string) => {
|
export const getTargetFolderFiles = async (folder_path: string) => {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import '@zanllp/vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||||
import { RecycleScroller } from '@zanllp/vue-virtual-scroller'
|
import { RecycleScroller } from '@zanllp/vue-virtual-scroller'
|
||||||
import { toRawFileUrl } from '@/page/fileTransfer/hook'
|
import { toRawFileUrl } from '@/page/fileTransfer/hook'
|
||||||
import { getImagesByTags, type MatchImageByTagsReq } from '@/api/db'
|
import { getImagesByTags, type MatchImageByTagsReq } from '@/api/db'
|
||||||
import { watch } from 'vue'
|
import { nextTick, watch } from 'vue'
|
||||||
import { copy2clipboardI18n } from '@/util'
|
import { copy2clipboardI18n } from '@/util'
|
||||||
import fullScreenContextMenu from '@/page/fileTransfer/fullScreenContextMenu.vue'
|
import fullScreenContextMenu from '@/page/fileTransfer/fullScreenContextMenu.vue'
|
||||||
import { LeftCircleOutlined, RightCircleOutlined } from '@/icon'
|
import { LeftCircleOutlined, RightCircleOutlined } from '@/icon'
|
||||||
|
|
@ -31,7 +31,9 @@ const {
|
||||||
showMenuIdx,
|
showMenuIdx,
|
||||||
onFileDragStart,
|
onFileDragStart,
|
||||||
onFileDragEnd,
|
onFileDragEnd,
|
||||||
cellWidth
|
cellWidth,
|
||||||
|
onScroll,
|
||||||
|
updateImageTag
|
||||||
} = useImageSearch()
|
} = useImageSearch()
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
|
|
@ -46,7 +48,9 @@ watch(
|
||||||
async () => {
|
async () => {
|
||||||
const { res } = queue.pushAction(() => getImagesByTags(props.selectedTagIds))
|
const { res } = queue.pushAction(() => getImagesByTags(props.selectedTagIds))
|
||||||
images.value = await res
|
images.value = await res
|
||||||
scroller.value?.scrollToItem(0)
|
await nextTick()
|
||||||
|
updateImageTag()
|
||||||
|
scroller.value!.scrollToItem(0)
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
@ -81,6 +85,7 @@ watch(
|
||||||
key-field="fullpath"
|
key-field="fullpath"
|
||||||
:item-secondary-size="itemSize.second"
|
:item-secondary-size="itemSize.second"
|
||||||
:gridItems="gridItems"
|
:gridItems="gridItems"
|
||||||
|
@scroll="onScroll"
|
||||||
>
|
>
|
||||||
<template v-slot="{ item: file, index: idx }">
|
<template v-slot="{ item: file, index: idx }">
|
||||||
<file-item-cell
|
<file-item-cell
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, ref } from 'vue'
|
import { nextTick, onMounted, ref } from 'vue'
|
||||||
import fileItemCell from '@/page/fileTransfer/FileItem.vue'
|
import fileItemCell from '@/page/fileTransfer/FileItem.vue'
|
||||||
import '@zanllp/vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
import '@zanllp/vue-virtual-scroller/dist/vue-virtual-scroller.css'
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
@ -33,7 +33,9 @@ const {
|
||||||
showMenuIdx,
|
showMenuIdx,
|
||||||
onFileDragStart,
|
onFileDragStart,
|
||||||
onFileDragEnd,
|
onFileDragEnd,
|
||||||
cellWidth
|
cellWidth,
|
||||||
|
onScroll,
|
||||||
|
updateImageTag
|
||||||
} = useImageSearch()
|
} = useImageSearch()
|
||||||
const substr = ref('')
|
const substr = ref('')
|
||||||
|
|
||||||
|
|
@ -56,7 +58,9 @@ const onUpdateBtnClick = makeAsyncFunctionSingle(
|
||||||
)
|
)
|
||||||
const query = async () => {
|
const query = async () => {
|
||||||
images.value = await queue.pushAction(() => getImagesBySubstr(substr.value)).res
|
images.value = await queue.pushAction(() => getImagesBySubstr(substr.value)).res
|
||||||
scroller.value?.scrollToItem(0)
|
await nextTick()
|
||||||
|
updateImageTag()
|
||||||
|
scroller.value!.scrollToItem(0)
|
||||||
if (!images.value.length) {
|
if (!images.value.length) {
|
||||||
message.info(t('fuzzy-search-noResults'))
|
message.info(t('fuzzy-search-noResults'))
|
||||||
}
|
}
|
||||||
|
|
@ -98,7 +102,7 @@ useGlobalEventListen('searchIndexExpired', () => info.value && (info.value.expir
|
||||||
</ASkeleton>
|
</ASkeleton>
|
||||||
</AModal>
|
</AModal>
|
||||||
<RecycleScroller ref="scroller" class="file-list" v-if="images" :items="images" :item-size="itemSize.first"
|
<RecycleScroller ref="scroller" class="file-list" v-if="images" :items="images" :item-size="itemSize.first"
|
||||||
key-field="fullpath" :item-secondary-size="itemSize.second" :gridItems="gridItems">
|
key-field="fullpath" :item-secondary-size="itemSize.second" :gridItems="gridItems" @scroll="onScroll">
|
||||||
<template v-slot="{ item: file, index: idx }">
|
<template v-slot="{ item: file, index: idx }">
|
||||||
<!-- idx 和file有可能丢失 -->
|
<!-- idx 和file有可能丢失 -->
|
||||||
<file-item-cell :idx="idx" :file="file" v-model:show-menu-idx="showMenuIdx" @file-item-click="onFileItemClick"
|
<file-item-cell :idx="idx" :file="file" v-model:show-menu-idx="showMenuIdx" @file-item-click="onFileItemClick"
|
||||||
|
|
|
||||||
|
|
@ -9,21 +9,22 @@ import {
|
||||||
useFileTransfer,
|
useFileTransfer,
|
||||||
useFileItemActions,
|
useFileItemActions,
|
||||||
usePreview,
|
usePreview,
|
||||||
type Scroller,
|
|
||||||
useEventListen,
|
useEventListen,
|
||||||
useLocation
|
useLocation
|
||||||
} from '../fileTransfer/hook'
|
} from '../fileTransfer/hook'
|
||||||
|
import { useTagStore } from '@/store/useTagStore'
|
||||||
|
import { debounce } from 'lodash-es'
|
||||||
|
|
||||||
export const useImageSearch = () => {
|
export const useImageSearch = () => {
|
||||||
const images = ref<FileNodeInfo[]>()
|
const images = ref<FileNodeInfo[]>()
|
||||||
const queue = createReactiveQueue()
|
const queue = createReactiveQueue()
|
||||||
const scroller = ref<Scroller>()
|
const tagStore = useTagStore()
|
||||||
const propsMock = { tabIdx: -1, target: 'local', paneIdx: -1, walkMode: false } as const
|
const propsMock = { tabIdx: -1, target: 'local', paneIdx: -1, walkMode: false } as const
|
||||||
const { stackViewEl, multiSelectedIdxs, stack } = useHookShareState({ images }).toRefs()
|
const { stackViewEl, multiSelectedIdxs, stack, scroller } = useHookShareState({ images }).toRefs()
|
||||||
const { itemSize, gridItems, cellWidth } = useFilesDisplay(propsMock)
|
const { itemSize, gridItems, cellWidth } = useFilesDisplay(propsMock)
|
||||||
const { showMenuIdx } = useMobileOptimization()
|
const { showMenuIdx } = useMobileOptimization()
|
||||||
useLocation(propsMock)
|
useLocation(propsMock)
|
||||||
const { onFileDragStart, onFileDragEnd } = useFileTransfer()
|
const { onFileDragStart, onFileDragEnd } = useFileTransfer()
|
||||||
const {
|
const {
|
||||||
showGenInfo,
|
showGenInfo,
|
||||||
imageGenInfo,
|
imageGenInfo,
|
||||||
|
|
@ -31,10 +32,7 @@ export const useImageSearch = () => {
|
||||||
onContextMenuClick,
|
onContextMenuClick,
|
||||||
onFileItemClick
|
onFileItemClick
|
||||||
} = useFileItemActions(propsMock, { openNext: identity })
|
} = useFileItemActions(propsMock, { openNext: identity })
|
||||||
const { previewIdx, previewing, onPreviewVisibleChange, previewImgMove, canPreview } = usePreview(
|
const { previewIdx, previewing, onPreviewVisibleChange, previewImgMove, canPreview } = usePreview(propsMock)
|
||||||
propsMock,
|
|
||||||
{ scroller }
|
|
||||||
)
|
|
||||||
|
|
||||||
const onContextMenuClickU: typeof onContextMenuClick = async (e, file, idx) => {
|
const onContextMenuClickU: typeof onContextMenuClick = async (e, file, idx) => {
|
||||||
stack.value = [{ curr: '', files: images.value! }] // hack,for delete multi files
|
stack.value = [{ curr: '', files: images.value! }] // hack,for delete multi files
|
||||||
|
|
@ -42,9 +40,20 @@ export const useImageSearch = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
useEventListen('removeFiles', async ({ paths }) => {
|
useEventListen('removeFiles', async ({ paths }) => {
|
||||||
images.value = images.value?.filter(v => !paths.includes(v.fullpath))
|
images.value = images.value?.filter((v) => !paths.includes(v.fullpath))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const updateImageTag = () => {
|
||||||
|
const s = scroller.value
|
||||||
|
if (s && images.value) {
|
||||||
|
const paths = images.value
|
||||||
|
.slice(Math.max(s.$_startIndex - 10, 0), s.$_endIndex + 10)
|
||||||
|
.map((v) => v.fullpath)
|
||||||
|
tagStore.fetchImageTags(paths)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onScroll = debounce(updateImageTag, 300)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
scroller,
|
scroller,
|
||||||
|
|
@ -68,6 +77,8 @@ export const useImageSearch = () => {
|
||||||
multiSelectedIdxs,
|
multiSelectedIdxs,
|
||||||
onFileDragStart,
|
onFileDragStart,
|
||||||
onFileDragEnd,
|
onFileDragEnd,
|
||||||
cellWidth
|
cellWidth,
|
||||||
|
onScroll,
|
||||||
|
updateImageTag
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,15 @@ import { FileOutlined, FolderOpenOutlined, EllipsisOutlined } from '@/icon'
|
||||||
import { useGlobalStore } from '@/store/useGlobalStore'
|
import { useGlobalStore } from '@/store/useGlobalStore'
|
||||||
import { fallbackImage } from 'vue3-ts-util'
|
import { fallbackImage } from 'vue3-ts-util'
|
||||||
import type { FileNodeInfo } from '@/api/files'
|
import type { FileNodeInfo } from '@/api/files'
|
||||||
import { createReactiveQueue, isImageFile } from '@/util'
|
import { isImageFile } from '@/util'
|
||||||
import { toImageThumbnailUrl, toRawFileUrl } from './hook'
|
import { toImageThumbnailUrl, toRawFileUrl } from './hook'
|
||||||
import type { MenuInfo } from 'ant-design-vue/lib/menu/src/interface'
|
import type { MenuInfo } from 'ant-design-vue/lib/menu/src/interface'
|
||||||
import { computed, ref } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { getImageSelectedCustomTag, type Tag } from '@/api/db'
|
|
||||||
import ContextMenu from './ContextMenu.vue'
|
import ContextMenu from './ContextMenu.vue'
|
||||||
|
import { useTagStore } from '@/store/useTagStore'
|
||||||
|
|
||||||
const global = useGlobalStore()
|
const global = useGlobalStore()
|
||||||
|
const tagStore = useTagStore()
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
file: FileNodeInfo
|
file: FileNodeInfo
|
||||||
|
|
@ -32,72 +33,48 @@ const emit = defineEmits<{
|
||||||
(type: 'contextMenuClick', e: MenuInfo, file: FileNodeInfo, idx: number): void
|
(type: 'contextMenuClick', e: MenuInfo, file: FileNodeInfo, idx: number): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const selectedTag = ref([] as Tag[])
|
const customTags = computed(() => {
|
||||||
const onRightClick = () => {
|
return tagStore.tagMap.get(props.file.fullpath) ?? []
|
||||||
if (props?.file?.type !== 'file') {
|
})
|
||||||
return
|
|
||||||
}
|
|
||||||
q.pushAction(() => getImageSelectedCustomTag(props.file.fullpath)).res.then((res) => {
|
|
||||||
selectedTag.value = res
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const q = createReactiveQueue()
|
|
||||||
const imageSrc = computed(() => {
|
const imageSrc = computed(() => {
|
||||||
const r = global.gridThumbnailResolution
|
const r = global.gridThumbnailResolution
|
||||||
return global.enableThumbnail ? toImageThumbnailUrl(props.file, [r,r].join('x')) : toRawFileUrl(props.file)
|
return global.enableThumbnail ? toImageThumbnailUrl(props.file, [r, r].join('x')) : toRawFileUrl(props.file)
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
<template>
|
<template>
|
||||||
<a-dropdown
|
<a-dropdown :trigger="['contextmenu']" :visible="!global.longPressOpenContextMenu ? undefined : typeof idx === 'number' && showMenuIdx === idx
|
||||||
:trigger="['contextmenu']"
|
" @update:visible="(v: boolean) => typeof idx === 'number' && emit('update:showMenuIdx', v ? idx : -1)">
|
||||||
:visible="
|
<li class="file file-item-trigger grid" :class="{
|
||||||
!global.longPressOpenContextMenu ? undefined : typeof idx === 'number' && showMenuIdx === idx
|
clickable: file.type === 'dir',
|
||||||
"
|
selected
|
||||||
@update:visible="(v: boolean) => typeof idx === 'number' && emit('update:showMenuIdx', v ? idx : -1)"
|
}" :data-idx="idx" :key="file.name" draggable="true" @dragstart="emit('dragstart', $event, idx)"
|
||||||
>
|
@dragend="emit('dragend', $event, idx)" @click.capture="emit('fileItemClick', $event, file, idx)">
|
||||||
<li
|
|
||||||
class="file file-item-trigger grid"
|
<div>
|
||||||
:class="{
|
|
||||||
clickable: file.type === 'dir',
|
|
||||||
selected
|
|
||||||
}"
|
|
||||||
:data-idx="idx"
|
|
||||||
:key="file.name"
|
|
||||||
draggable="true"
|
|
||||||
@dragstart="emit('dragstart', $event, idx)"
|
|
||||||
@dragend="emit('dragend', $event, idx)"
|
|
||||||
@contextmenu="onRightClick"
|
|
||||||
@click.capture="emit('fileItemClick', $event, file, idx)"
|
|
||||||
>
|
|
||||||
<div >
|
|
||||||
<a-dropdown>
|
<a-dropdown>
|
||||||
<div class="more">
|
<div class="more">
|
||||||
<ellipsis-outlined />
|
<ellipsis-outlined />
|
||||||
</div>
|
</div>
|
||||||
<template #overlay>
|
<template #overlay>
|
||||||
<context-menu
|
<context-menu :file="file" :idx="idx" :selected-tag="customTags"
|
||||||
:file="file"
|
@context-menu-click="(e, f, i) => emit('contextMenuClick', e, f, i)" />
|
||||||
:idx="idx"
|
|
||||||
:selected-tag="selectedTag"
|
|
||||||
@context-menu-click="(e, f, i) => emit('contextMenuClick', e, f, i)"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
<!-- :key="fullScreenPreviewImageUrl ? undefined : file.fullpath"
|
<!-- :key="fullScreenPreviewImageUrl ? undefined : file.fullpath"
|
||||||
这么复杂是因为再全屏预览时可能因为直接删除导致fullpath变化,然后整个预览直接退出-->
|
这么复杂是因为再全屏预览时可能因为直接删除导致fullpath变化,然后整个预览直接退出-->
|
||||||
<a-image
|
<div style="position: relative;" :key="file.fullpath" :class="`idx-${idx}`" v-if="isImageFile(file.name)">
|
||||||
:key="file.fullpath"
|
|
||||||
:class="`idx-${idx}`"
|
<a-image :src="imageSrc" :fallback="fallbackImage" :preview="{
|
||||||
v-if="isImageFile(file.name) "
|
|
||||||
:src="imageSrc"
|
|
||||||
:fallback="fallbackImage"
|
|
||||||
:preview="{
|
|
||||||
src: fullScreenPreviewImageUrl,
|
src: fullScreenPreviewImageUrl,
|
||||||
onVisibleChange: (v: boolean, lv: boolean) => emit('previewVisibleChange', v, lv)
|
onVisibleChange: (v: boolean, lv: boolean) => emit('previewVisibleChange', v, lv)
|
||||||
}"
|
}" />
|
||||||
>
|
<div class="tags-container" v-if="customTags && cellWidth > 128">
|
||||||
</a-image>
|
<a-tag v-for="tag in customTags" :key="tag.id" :color="tagStore.getColor(tag.name)">
|
||||||
|
{{ tag.name }}
|
||||||
|
</a-tag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div v-else class="preview-icon-wrap">
|
<div v-else class="preview-icon-wrap">
|
||||||
<file-outlined class="icon center" v-if="file.type === 'file'" />
|
<file-outlined class="icon center" v-if="file.type === 'file'" />
|
||||||
<folder-open-outlined class="icon center" v-else />
|
<folder-open-outlined class="icon center" v-else />
|
||||||
|
|
@ -118,12 +95,8 @@ const imageSrc = computed(() => {
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
<template #overlay>
|
<template #overlay>
|
||||||
<context-menu
|
<context-menu :file="file" :idx="idx" :selected-tag="customTags"
|
||||||
:file="file"
|
@context-menu-click="(e, f, i) => emit('contextMenuClick', e, f, i)" />
|
||||||
:idx="idx"
|
|
||||||
:selected-tag="selectedTag"
|
|
||||||
@context-menu-click="(e, f, i) => emit('contextMenuClick', e, f, i)"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -134,6 +107,20 @@ const imageSrc = computed(() => {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tags-container {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
bottom: 8px;
|
||||||
|
display: flex;
|
||||||
|
width: calc(100% - 16px);
|
||||||
|
flex-wrap: wrap-reverse;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
|
||||||
|
&>* {
|
||||||
|
margin: 0 0 4px 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.file {
|
.file {
|
||||||
padding: 8px 16px;
|
padding: 8px 16px;
|
||||||
margin: 8px;
|
margin: 8px;
|
||||||
|
|
@ -207,7 +194,7 @@ const imageSrc = computed(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
img,
|
img,
|
||||||
.preview-icon-wrap > [role='img'] {
|
.preview-icon-wrap>[role='img'] {
|
||||||
height: v-bind('$props.cellWidth + "px"');
|
height: v-bind('$props.cellWidth + "px"');
|
||||||
width: v-bind('$props.cellWidth + "px"');
|
width: v-bind('$props.cellWidth + "px"');
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
|
|
|
||||||
|
|
@ -19,19 +19,21 @@ import {
|
||||||
EllipsisOutlined
|
EllipsisOutlined
|
||||||
} from '@/icon'
|
} from '@/icon'
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
import { getImageSelectedCustomTag, type Tag } from '@/api/db'
|
import { type Tag } from '@/api/db'
|
||||||
import { createReactiveQueue } from '@/util'
|
import { createReactiveQueue } from '@/util'
|
||||||
import { toRawFileUrl } from './hook'
|
import { toRawFileUrl } from './hook'
|
||||||
import ContextMenu from './ContextMenu.vue'
|
import ContextMenu from './ContextMenu.vue'
|
||||||
import { useWatchDocument } from 'vue3-ts-util'
|
import { useWatchDocument } from 'vue3-ts-util'
|
||||||
|
import { useTagStore } from '@/store/useTagStore'
|
||||||
|
|
||||||
const global = useGlobalStore()
|
const global = useGlobalStore()
|
||||||
|
const tagStore = useTagStore()
|
||||||
const el = ref<HTMLElement>()
|
const el = ref<HTMLElement>()
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
file: FileNodeInfo
|
file: FileNodeInfo
|
||||||
idx: number
|
idx: number
|
||||||
}>()
|
}>()
|
||||||
const selectedTag = ref([] as Tag[])
|
const selectedTag = computed(() => tagStore.tagMap.get(props.file.fullpath) ?? [])
|
||||||
const tags = computed(() => {
|
const tags = computed(() => {
|
||||||
return (global.conf?.all_custom_tags ?? []).reduce((p, c) => {
|
return (global.conf?.all_custom_tags ?? []).reduce((p, c) => {
|
||||||
return [...p, { ...c, selected: !!selectedTag.value.find((v) => v.id === c.id) }]
|
return [...p, { ...c, selected: !!selectedTag.value.find((v) => v.id === c.id) }]
|
||||||
|
|
@ -57,14 +59,6 @@ watch(
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
const onMouseHoverContext = (show: boolean) => {
|
|
||||||
if (!show) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
q.pushAction(() => getImageSelectedCustomTag(props.file.fullpath)).res.then((res) => {
|
|
||||||
selectedTag.value = res
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const resizeHandle = ref<HTMLElement>()
|
const resizeHandle = ref<HTMLElement>()
|
||||||
const dragHandle = ref<HTMLElement>()
|
const dragHandle = ref<HTMLElement>()
|
||||||
|
|
@ -124,7 +118,7 @@ const baseInfoTags = computed(() => {
|
||||||
<FullscreenExitOutlined v-if="state.expanded" />
|
<FullscreenExitOutlined v-if="state.expanded" />
|
||||||
<FullscreenOutlined v-else />
|
<FullscreenOutlined v-else />
|
||||||
</div>
|
</div>
|
||||||
<a-dropdown @visible-change="onMouseHoverContext" :get-popup-container="getParNode">
|
<a-dropdown :get-popup-container="getParNode">
|
||||||
<div class="icon" style="cursor: pointer" v-if="!state.expanded">
|
<div class="icon" style="cursor: pointer" v-if="!state.expanded">
|
||||||
<ellipsis-outlined />
|
<ellipsis-outlined />
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -136,7 +130,7 @@ const baseInfoTags = computed(() => {
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
<div flex-placeholder v-if="state.expanded" />
|
<div flex-placeholder v-if="state.expanded" />
|
||||||
<div v-if="state.expanded" class="action-bar">
|
<div v-if="state.expanded" class="action-bar">
|
||||||
<a-dropdown :trigger="['hover']" :get-popup-container="getParNode" @visible-change="onMouseHoverContext">
|
<a-dropdown :trigger="['hover']" :get-popup-container="getParNode" >
|
||||||
<a-button>{{ $t('toggleTag') }}</a-button>
|
<a-button>{{ $t('toggleTag') }}</a-button>
|
||||||
<template #overlay>
|
<template #overlay>
|
||||||
<a-menu @click="emit('contextMenuClick', $event, file, idx)">
|
<a-menu @click="emit('contextMenuClick', $event, file, idx)">
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useGlobalStore, type FileTransferTabPane, type Shortcut } from '@/store/useGlobalStore'
|
import { useGlobalStore, type FileTransferTabPane, type Shortcut } from '@/store/useGlobalStore'
|
||||||
import { useImgSliStore } from '@/store/useImgSli'
|
import { useImgSliStore } from '@/store/useImgSli'
|
||||||
import { onLongPress, useElementSize, useMouseInElement } from '@vueuse/core'
|
import { onLongPress, useElementSize, useMouseInElement } from '@vueuse/core'
|
||||||
import { ref, computed, watch, onMounted, h, type Ref } from 'vue'
|
import { ref, computed, watch, onMounted, h } from 'vue'
|
||||||
import { genInfoCompleted, getImageGenerationInfo, openFolder, setImgPath } from '@/api'
|
import { genInfoCompleted, getImageGenerationInfo, openFolder, setImgPath } from '@/api'
|
||||||
import {
|
import {
|
||||||
useWatchDocument,
|
useWatchDocument,
|
||||||
|
|
@ -33,11 +33,14 @@ import { addScannedPath, removeScannedPath, toggleCustomTagToImg } from '@/api/d
|
||||||
import { FileTransferData, getFileTransferDataFromDragEvent, toRawFileUrl } from './util'
|
import { FileTransferData, getFileTransferDataFromDragEvent, toRawFileUrl } from './util'
|
||||||
import { getShortcutStrFromEvent } from '@/util/shortcut'
|
import { getShortcutStrFromEvent } from '@/util/shortcut'
|
||||||
import { openCreateFlodersModal, MultiSelectTips } from './functionalCallableComp'
|
import { openCreateFlodersModal, MultiSelectTips } from './functionalCallableComp'
|
||||||
|
import { useTagStore } from '@/store/useTagStore'
|
||||||
export * from './util'
|
export * from './util'
|
||||||
|
|
||||||
export const stackCache = new Map<string, Page[]>()
|
export const stackCache = new Map<string, Page[]>()
|
||||||
|
|
||||||
const global = useGlobalStore()
|
const global = useGlobalStore()
|
||||||
|
|
||||||
|
const tagStore = useTagStore()
|
||||||
const sli = useImgSliStore()
|
const sli = useImgSliStore()
|
||||||
const imgTransferBus = new BroadcastChannel('iib-image-transfer-bus')
|
const imgTransferBus = new BroadcastChannel('iib-image-transfer-bus')
|
||||||
export const { eventEmitter: events, useEventListen } = typedEventEmitter<{
|
export const { eventEmitter: events, useEventListen } = typedEventEmitter<{
|
||||||
|
|
@ -95,7 +98,7 @@ export const { useHookShareState } = createTypedShareStateHook(
|
||||||
const previewing = ref(false)
|
const previewing = ref(false)
|
||||||
|
|
||||||
const getPane = () => {
|
const getPane = () => {
|
||||||
return global.tabList[props.value.tabIdx].panes[props.value.paneIdx] as FileTransferTabPane
|
return global.tabList?.[props.value.tabIdx]?.panes?.[props.value.paneIdx] as FileTransferTabPane
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
previewing,
|
previewing,
|
||||||
|
|
@ -140,16 +143,16 @@ export interface Page {
|
||||||
* @param props
|
* @param props
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export function usePreview (props: Props, custom?: { scroller: Ref<Scroller | undefined> }) {
|
export function usePreview (props: Props) {
|
||||||
const {
|
const {
|
||||||
previewIdx,
|
previewIdx,
|
||||||
eventEmitter,
|
eventEmitter,
|
||||||
canLoadNext,
|
canLoadNext,
|
||||||
previewing,
|
previewing,
|
||||||
sortedFiles: files
|
sortedFiles: files,
|
||||||
|
scroller
|
||||||
} = useHookShareState().toRefs()
|
} = useHookShareState().toRefs()
|
||||||
const { state } = useHookShareState()
|
const { state } = useHookShareState()
|
||||||
const scroller = computed(() => custom?.scroller.value ?? state.scroller)
|
|
||||||
let waitScrollTo = null as number | null
|
let waitScrollTo = null as number | null
|
||||||
const onPreviewVisibleChange = (v: boolean, lv: boolean) => {
|
const onPreviewVisibleChange = (v: boolean, lv: boolean) => {
|
||||||
previewing.value = v
|
previewing.value = v
|
||||||
|
|
@ -317,6 +320,9 @@ export function useLocation (props: Props) {
|
||||||
currLocation,
|
currLocation,
|
||||||
debounce((loc) => {
|
debounce((loc) => {
|
||||||
const pane = getPane.value()
|
const pane = getPane.value()
|
||||||
|
if (!pane) {
|
||||||
|
return
|
||||||
|
}
|
||||||
pane.path = loc
|
pane.path = loc
|
||||||
const filename = pane.path!.split('/').pop()
|
const filename = pane.path!.split('/').pop()
|
||||||
const getTitle = () => {
|
const getTitle = () => {
|
||||||
|
|
@ -634,7 +640,23 @@ export function useFilesDisplay (props: Props) {
|
||||||
|
|
||||||
state.useEventListen('loadNextDir', fill)
|
state.useEventListen('loadNextDir', fill)
|
||||||
|
|
||||||
const onScroll = debounce(() => fill(), 300)
|
|
||||||
|
const onViewedImagesChange = () => {
|
||||||
|
const s = scroller.value
|
||||||
|
if (s) {
|
||||||
|
const paths = sortedFiles.value.slice(Math.max(s.$_startIndex - 10, 0), s.$_endIndex + 10)
|
||||||
|
.filter(v => v.is_under_scanned_path && isImageFile(v.name))
|
||||||
|
.map(v => v.fullpath)
|
||||||
|
tagStore.fetchImageTags(paths)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(currLocation, debounce(onViewedImagesChange, 150))
|
||||||
|
|
||||||
|
const onScroll = debounce(() => {
|
||||||
|
fill()
|
||||||
|
onViewedImagesChange()
|
||||||
|
}, 300)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
gridItems,
|
gridItems,
|
||||||
|
|
@ -648,7 +670,8 @@ export function useFilesDisplay (props: Props) {
|
||||||
loadNextDirLoading,
|
loadNextDirLoading,
|
||||||
canLoadNext,
|
canLoadNext,
|
||||||
itemSize,
|
itemSize,
|
||||||
cellWidth
|
cellWidth,
|
||||||
|
onViewedImagesChange
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -715,7 +738,7 @@ export function useFileTransfer () {
|
||||||
width: '60vw',
|
width: '60vw',
|
||||||
content: () => <div>
|
content: () => <div>
|
||||||
<div>
|
<div>
|
||||||
{`${t('moveSelectedFilesTo')}${toPath}`}
|
{`${t('moveSelectedFilesTo')} ${toPath}`}
|
||||||
<ol style={{ maxHeight: '50vh', overflow: 'auto' }}>
|
<ol style={{ maxHeight: '50vh', overflow: 'auto' }}>
|
||||||
{data.path.map((v) => <li>{v.split(/[/\\]/).pop()}</li>)}
|
{data.path.map((v) => <li>{v.split(/[/\\]/).pop()}</li>)}
|
||||||
</ol>
|
</ol>
|
||||||
|
|
@ -860,6 +883,7 @@ export function useFileItemActions (
|
||||||
const tagId = +`${e.key}`.split('toggle-tag-')[1]
|
const tagId = +`${e.key}`.split('toggle-tag-')[1]
|
||||||
const { is_remove } = await toggleCustomTagToImg({ tag_id: tagId, img_path: file.fullpath })
|
const { is_remove } = await toggleCustomTagToImg({ tag_id: tagId, img_path: file.fullpath })
|
||||||
const tag = global.conf?.all_custom_tags.find((v) => v.id === tagId)?.name!
|
const tag = global.conf?.all_custom_tags.find((v) => v.id === tagId)?.name!
|
||||||
|
tagStore.refreshTags([file.fullpath])
|
||||||
message.success(t(is_remove ? 'removedTagFromImage' : 'addedTagToImage', { tag }))
|
message.success(t(is_remove ? 'removedTagFromImage' : 'addedTagToImage', { tag }))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { Tag, batchGetTagsByPath } from '@/api/db'
|
||||||
|
import { createReactiveQueue } from '@/util'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import sjcl from 'sjcl'
|
||||||
|
import { reactive } from 'vue'
|
||||||
|
|
||||||
|
export const useTagStore = defineStore('useTagStore', () => {
|
||||||
|
const q = createReactiveQueue()
|
||||||
|
const fetchPendingImagePaths = new Set<string>()
|
||||||
|
const tagMap = reactive(new Map<string, Tag[]>())
|
||||||
|
const fetchImageTags = async (paths: string[]) => {
|
||||||
|
paths = paths.filter(v => !fetchPendingImagePaths.has(v) && !tagMap.has(v))
|
||||||
|
if (!paths.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
paths.forEach(v => tagMap.set(v, []))
|
||||||
|
const res = await batchGetTagsByPath(paths)
|
||||||
|
for (const path in res) {
|
||||||
|
tagMap.set(path, res[path])
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
paths.forEach(v => fetchPendingImagePaths.delete(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const colors = ['pink', 'red', 'orange', 'green', 'cyan', 'blue', 'purple']
|
||||||
|
const colorCache = new Map<string, string>()
|
||||||
|
const getColor = (tag: string) => {
|
||||||
|
let color = colorCache.get(tag)
|
||||||
|
if (!color) {
|
||||||
|
const hash = sjcl.hash.sha256.hash(tag)
|
||||||
|
const num = parseInt(sjcl.codec.hex.fromBits(hash), 16) % colors.length
|
||||||
|
color = colors[num]
|
||||||
|
colorCache.set(tag, color)
|
||||||
|
}
|
||||||
|
return color
|
||||||
|
}
|
||||||
|
const refreshTags = async (paths: string[]) => {
|
||||||
|
paths.forEach(v => tagMap.delete(v))
|
||||||
|
await fetchImageTags(paths)
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
tagMap,
|
||||||
|
q,
|
||||||
|
getColor,
|
||||||
|
fetchImageTags,
|
||||||
|
refreshTags
|
||||||
|
}
|
||||||
|
})
|
||||||
Loading…
Reference in New Issue