Improved custom tag display for better user experience.

pull/324/head
zanllp 2023-07-19 19:07:43 +08:00
parent fd9552c83c
commit 1c4479a394
36 changed files with 274 additions and 147 deletions

View File

@ -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>

View File

@ -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

View File

@ -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:
@ -272,6 +280,8 @@ class Tag:
) )
class ImageTag: class ImageTag:
def __init__(self, image_id: int, tag_id: int): def __init__(self, image_id: int, tag_id: int):
assert tag_id and image_id assert tag_id and image_id
@ -400,6 +410,30 @@ class ImageTag:
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,

1
vue/components.d.ts vendored
View File

@ -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']

View File

@ -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};

View File

@ -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%}

View File

@ -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%}

View File

@ -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};

View File

@ -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};

View File

@ -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};

View File

@ -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%}

View File

@ -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};

View File

@ -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

View File

@ -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

1
vue/dist/assets/hook-1cb05846.js vendored Normal file
View File

@ -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};

View File

@ -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

View File

@ -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

2
vue/dist/index.html vendored
View File

@ -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>

View File

@ -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'
@ -85,3 +86,8 @@ 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[]>
}

View File

@ -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) => {

View File

@ -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

View File

@ -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"

View File

@ -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! }] // hackfor delete multi files stack.value = [{ curr: '', files: images.value! }] // hackfor 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
} }
} }

View File

@ -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;

View File

@ -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)">

View File

@ -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
} }

View File

@ -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
}
})