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" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<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">
</head>

View File

@ -17,6 +17,7 @@ from scripts.iib.tool import (
get_valid_img_dirs,
open_folder,
get_img_geninfo_txt_path,
unique_by,
)
from fastapi import FastAPI, HTTPException
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": ""}
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")
if secret_key:
print("Secret key loaded successfully. ")
@ -56,11 +57,11 @@ async def get_token(request: Request):
token = request.cookies.get("IIB_S")
if not token:
raise HTTPException(status_code=401, detail="Unauthorized")
if not mem["IIB_SECRET_KEY_HASH"]:
mem["IIB_SECRET_KEY_HASH"] = hashlib.sha256(
if not mem["secret_key_hash"]:
mem["secret_key_hash"] = hashlib.sha256(
(secret_key + "_ciallo").encode("utf-8")
).hexdigest()
if mem["IIB_SECRET_KEY_HASH"] != token:
if mem["secret_key_hash"] != token:
raise HTTPException(status_code=401, detail="Unauthorized")
@ -81,9 +82,16 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
except:
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):
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):
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.
: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.
"""
try:
if not parent_paths:
parent_paths = (
img_search_dirs
+ mem["EXTRA_PATHS"]
+ kwargs.get("extra_paths_cli", [])
)
parent_paths = mem["all_scanned_paths"]
path = os.path.normpath(path)
for parent_path in parent_paths:
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:
return True
try:
parent_paths: List[str] = (
img_search_dirs + mem["EXTRA_PATHS"] + kwargs.get("extra_paths_cli", [])
)
parent_paths = mem["all_scanned_paths"]
path = os.path.normpath(path)
for parent_path in parent_paths:
if len(path) <= len(parent_path):
@ -214,7 +216,6 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
)
raise HTTPException(400, detail=error_msg)
class CreateFoldersReq(BaseModel):
dest_folder: str
@ -225,13 +226,11 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
raise HTTPException(status_code=403)
os.makedirs(req.dest_folder, exist_ok=True)
class MoveFilesReq(BaseModel):
file_paths: List[str]
dest: str
create_dest_folder: Optional[bool] = False
@app.post(pre + "/copy_files", dependencies=[Depends(get_token)])
async def copy_files(req: MoveFilesReq):
for path in req.file_paths:
@ -265,13 +264,14 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
for path in req.file_paths:
check_path_trust(path)
try:
shutil.move(path, req.dest)
ret_path = shutil.move(path, req.dest)
txt_path = get_img_geninfo_txt_path(path)
if txt_path:
shutil.move(txt_path, req.dest)
img = DbImg.get(conn, os.path.normpath(path))
if img:
DbImg.safe_batch_remove(conn, [img.id])
img.update_path(conn, ret_path)
conn.commit()
except OSError as e:
error_msg = (
f"Error moving file {path} to {req.dest}: {e}"
@ -303,6 +303,7 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
return {"files": []}
check_path_trust(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:
if not os.path.exists(item.path):
continue
@ -322,6 +323,7 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
"bytes": bytes,
"created_time": created_time,
"fullpath": fullpath,
"is_under_scanned_path": is_under_scanned_path,
}
)
elif item.is_dir():
@ -332,6 +334,7 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
"created_time": created_time,
"size": "-",
"name": name,
"is_under_scanned_path": is_under_scanned_path,
"fullpath": fullpath,
}
)
@ -489,7 +492,7 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
update_extra_paths(conn)
dirs = (
img_search_dirs if img_count == 0 else Floder.get_expired_dirs(conn)
) + mem["EXTRA_PATHS"]
) + mem["extra_paths"]
update_image_data(dirs)
finally:
@ -529,6 +532,14 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
# tags = Tag.get_all_custom_tag()
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):
img_path: str
tag_id: int

View File

@ -1,5 +1,5 @@
from sqlite3 import Connection, connect
from typing import Dict, List, Optional, Union
from typing import Dict, List, Optional
from scripts.iib.tool import (
cwd,
get_modified_date,
@ -79,6 +79,14 @@ class Image:
)
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
def get(cls, conn: Connection, id_or_path):
with closing(conn.cursor()) as cur:
@ -174,7 +182,7 @@ class Image:
deleted_ids.append(img.id)
cls.safe_batch_remove(conn, deleted_ids)
return images
class Tag:
def __init__(self, name: str, score: int, type: str, count=0):
@ -270,6 +278,8 @@ class Tag:
VALUES ("like", 0, "custom", 0);
"""
)
class ImageTag:
@ -399,7 +409,31 @@ class ImageTag:
deleted_ids.append(img.id)
Image.safe_batch_remove(conn, deleted_ids)
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
def remove(
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']
ATabPane: typeof import('ant-design-vue/es')['TabPane']
ATabs: typeof import('ant-design-vue/es')['Tabs']
ATag: typeof import('ant-design-vue/es')['Tag']
ATooltip: typeof import('ant-design-vue/es')['Tooltip']
NumInput: typeof import('./src/components/numInput.vue')['default']
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" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<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">
</head>

View File

@ -1,3 +1,4 @@
import { Dict } from '@/util'
import type { FileNodeInfo } from './files'
import { axiosInst } from './index'
@ -84,4 +85,9 @@ export const addScannedPath = async (path: string) => {
}
export const removeScannedPath = async (path: string) => {
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
bytes: number
fullpath: string
is_under_scanned_path: boolean
}
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 { toRawFileUrl } from '@/page/fileTransfer/hook'
import { getImagesByTags, type MatchImageByTagsReq } from '@/api/db'
import { watch } from 'vue'
import { nextTick, watch } from 'vue'
import { copy2clipboardI18n } from '@/util'
import fullScreenContextMenu from '@/page/fileTransfer/fullScreenContextMenu.vue'
import { LeftCircleOutlined, RightCircleOutlined } from '@/icon'
@ -31,7 +31,9 @@ const {
showMenuIdx,
onFileDragStart,
onFileDragEnd,
cellWidth
cellWidth,
onScroll,
updateImageTag
} = useImageSearch()
const props = defineProps<{
@ -46,7 +48,9 @@ watch(
async () => {
const { res } = queue.pushAction(() => getImagesByTags(props.selectedTagIds))
images.value = await res
scroller.value?.scrollToItem(0)
await nextTick()
updateImageTag()
scroller.value!.scrollToItem(0)
},
{ immediate: true }
)
@ -81,6 +85,7 @@ watch(
key-field="fullpath"
:item-secondary-size="itemSize.second"
:gridItems="gridItems"
@scroll="onScroll"
>
<template v-slot="{ item: file, index: idx }">
<file-item-cell

View File

@ -1,5 +1,5 @@
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { nextTick, onMounted, ref } from 'vue'
import fileItemCell from '@/page/fileTransfer/FileItem.vue'
import '@zanllp/vue-virtual-scroller/dist/vue-virtual-scroller.css'
// @ts-ignore
@ -33,7 +33,9 @@ const {
showMenuIdx,
onFileDragStart,
onFileDragEnd,
cellWidth
cellWidth,
onScroll,
updateImageTag
} = useImageSearch()
const substr = ref('')
@ -56,7 +58,9 @@ const onUpdateBtnClick = makeAsyncFunctionSingle(
)
const query = async () => {
images.value = await queue.pushAction(() => getImagesBySubstr(substr.value)).res
scroller.value?.scrollToItem(0)
await nextTick()
updateImageTag()
scroller.value!.scrollToItem(0)
if (!images.value.length) {
message.info(t('fuzzy-search-noResults'))
}
@ -98,7 +102,7 @@ useGlobalEventListen('searchIndexExpired', () => info.value && (info.value.expir
</ASkeleton>
</AModal>
<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 }">
<!-- idx 和file有可能丢失 -->
<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,
useFileItemActions,
usePreview,
type Scroller,
useEventListen,
useLocation
} from '../fileTransfer/hook'
import { useTagStore } from '@/store/useTagStore'
import { debounce } from 'lodash-es'
export const useImageSearch = () => {
const images = ref<FileNodeInfo[]>()
const queue = createReactiveQueue()
const scroller = ref<Scroller>()
const tagStore = useTagStore()
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 { showMenuIdx } = useMobileOptimization()
useLocation(propsMock)
const { onFileDragStart, onFileDragEnd } = useFileTransfer()
const { onFileDragStart, onFileDragEnd } = useFileTransfer()
const {
showGenInfo,
imageGenInfo,
@ -31,10 +32,7 @@ export const useImageSearch = () => {
onContextMenuClick,
onFileItemClick
} = useFileItemActions(propsMock, { openNext: identity })
const { previewIdx, previewing, onPreviewVisibleChange, previewImgMove, canPreview } = usePreview(
propsMock,
{ scroller }
)
const { previewIdx, previewing, onPreviewVisibleChange, previewImgMove, canPreview } = usePreview(propsMock)
const onContextMenuClickU: typeof onContextMenuClick = async (e, file, idx) => {
stack.value = [{ curr: '', files: images.value! }] // hackfor delete multi files
@ -42,9 +40,20 @@ export const useImageSearch = () => {
}
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 {
scroller,
@ -68,6 +77,8 @@ export const useImageSearch = () => {
multiSelectedIdxs,
onFileDragStart,
onFileDragEnd,
cellWidth
cellWidth,
onScroll,
updateImageTag
}
}

View File

@ -3,14 +3,15 @@ import { FileOutlined, FolderOpenOutlined, EllipsisOutlined } from '@/icon'
import { useGlobalStore } from '@/store/useGlobalStore'
import { fallbackImage } from 'vue3-ts-util'
import type { FileNodeInfo } from '@/api/files'
import { createReactiveQueue, isImageFile } from '@/util'
import { isImageFile } from '@/util'
import { toImageThumbnailUrl, toRawFileUrl } from './hook'
import type { MenuInfo } from 'ant-design-vue/lib/menu/src/interface'
import { computed, ref } from 'vue'
import { getImageSelectedCustomTag, type Tag } from '@/api/db'
import { computed } from 'vue'
import ContextMenu from './ContextMenu.vue'
import { useTagStore } from '@/store/useTagStore'
const global = useGlobalStore()
const tagStore = useTagStore()
const props = withDefaults(
defineProps<{
file: FileNodeInfo
@ -32,72 +33,48 @@ const emit = defineEmits<{
(type: 'contextMenuClick', e: MenuInfo, file: FileNodeInfo, idx: number): void
}>()
const selectedTag = ref([] as Tag[])
const onRightClick = () => {
if (props?.file?.type !== 'file') {
return
}
q.pushAction(() => getImageSelectedCustomTag(props.file.fullpath)).res.then((res) => {
selectedTag.value = res
})
}
const customTags = computed(() => {
return tagStore.tagMap.get(props.file.fullpath) ?? []
})
const q = createReactiveQueue()
const imageSrc = computed(() => {
const r = global.gridThumbnailResolution
return global.enableThumbnail ? toImageThumbnailUrl(props.file, [r,r].join('x')) : toRawFileUrl(props.file)
const r = global.gridThumbnailResolution
return global.enableThumbnail ? toImageThumbnailUrl(props.file, [r, r].join('x')) : toRawFileUrl(props.file)
})
</script>
<template>
<a-dropdown
:trigger="['contextmenu']"
:visible="
!global.longPressOpenContextMenu ? undefined : typeof idx === 'number' && showMenuIdx === idx
"
@update:visible="(v: boolean) => typeof idx === 'number' && emit('update:showMenuIdx', v ? idx : -1)"
>
<li
class="file file-item-trigger grid"
: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 :trigger="['contextmenu']" :visible="!global.longPressOpenContextMenu ? undefined : typeof idx === 'number' && showMenuIdx === idx
" @update:visible="(v: boolean) => typeof idx === 'number' && emit('update:showMenuIdx', v ? idx : -1)">
<li class="file file-item-trigger grid" :class="{
clickable: file.type === 'dir',
selected
}" :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)">
<div>
<a-dropdown>
<div class="more">
<ellipsis-outlined />
</div>
<template #overlay>
<context-menu
:file="file"
:idx="idx"
:selected-tag="selectedTag"
@context-menu-click="(e, f, i) => emit('contextMenuClick', e, f, i)"
/>
<context-menu :file="file" :idx="idx" :selected-tag="customTags"
@context-menu-click="(e, f, i) => emit('contextMenuClick', e, f, i)" />
</template>
</a-dropdown>
<!-- :key="fullScreenPreviewImageUrl ? undefined : file.fullpath"
这么复杂是因为再全屏预览时可能因为直接删除导致fullpath变化然后整个预览直接退出-->
<a-image
:key="file.fullpath"
:class="`idx-${idx}`"
v-if="isImageFile(file.name) "
:src="imageSrc"
:fallback="fallbackImage"
:preview="{
<div style="position: relative;" :key="file.fullpath" :class="`idx-${idx}`" v-if="isImageFile(file.name)">
<a-image :src="imageSrc" :fallback="fallbackImage" :preview="{
src: fullScreenPreviewImageUrl,
onVisibleChange: (v: boolean, lv: boolean) => emit('previewVisibleChange', v, lv)
}"
>
</a-image>
}" />
<div class="tags-container" v-if="customTags && cellWidth > 128">
<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">
<file-outlined class="icon center" v-if="file.type === 'file'" />
<folder-open-outlined class="icon center" v-else />
@ -118,12 +95,8 @@ const imageSrc = computed(() => {
</div>
</li>
<template #overlay>
<context-menu
:file="file"
:idx="idx"
:selected-tag="selectedTag"
@context-menu-click="(e, f, i) => emit('contextMenuClick', e, f, i)"
/>
<context-menu :file="file" :idx="idx" :selected-tag="customTags"
@context-menu-click="(e, f, i) => emit('contextMenuClick', e, f, i)" />
</template>
</a-dropdown>
</template>
@ -134,6 +107,20 @@ const imageSrc = computed(() => {
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 {
padding: 8px 16px;
margin: 8px;
@ -207,7 +194,7 @@ const imageSrc = computed(() => {
}
img,
.preview-icon-wrap > [role='img'] {
.preview-icon-wrap>[role='img'] {
height: v-bind('$props.cellWidth + "px"');
width: v-bind('$props.cellWidth + "px"');
object-fit: contain;

View File

@ -19,19 +19,21 @@ import {
EllipsisOutlined
} from '@/icon'
import { t } from '@/i18n'
import { getImageSelectedCustomTag, type Tag } from '@/api/db'
import { type Tag } from '@/api/db'
import { createReactiveQueue } from '@/util'
import { toRawFileUrl } from './hook'
import ContextMenu from './ContextMenu.vue'
import { useWatchDocument } from 'vue3-ts-util'
import { useTagStore } from '@/store/useTagStore'
const global = useGlobalStore()
const tagStore = useTagStore()
const el = ref<HTMLElement>()
const props = defineProps<{
file: FileNodeInfo
idx: number
}>()
const selectedTag = ref([] as Tag[])
const selectedTag = computed(() => tagStore.tagMap.get(props.file.fullpath) ?? [])
const tags = computed(() => {
return (global.conf?.all_custom_tags ?? []).reduce((p, c) => {
return [...p, { ...c, selected: !!selectedTag.value.find((v) => v.id === c.id) }]
@ -57,14 +59,6 @@ watch(
},
{ 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 dragHandle = ref<HTMLElement>()
@ -124,7 +118,7 @@ const baseInfoTags = computed(() => {
<FullscreenExitOutlined v-if="state.expanded" />
<FullscreenOutlined v-else />
</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">
<ellipsis-outlined />
</div>
@ -136,7 +130,7 @@ const baseInfoTags = computed(() => {
</a-dropdown>
<div flex-placeholder v-if="state.expanded" />
<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>
<template #overlay>
<a-menu @click="emit('contextMenuClick', $event, file, idx)">

View File

@ -1,7 +1,7 @@
import { useGlobalStore, type FileTransferTabPane, type Shortcut } from '@/store/useGlobalStore'
import { useImgSliStore } from '@/store/useImgSli'
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 {
useWatchDocument,
@ -33,11 +33,14 @@ import { addScannedPath, removeScannedPath, toggleCustomTagToImg } from '@/api/d
import { FileTransferData, getFileTransferDataFromDragEvent, toRawFileUrl } from './util'
import { getShortcutStrFromEvent } from '@/util/shortcut'
import { openCreateFlodersModal, MultiSelectTips } from './functionalCallableComp'
import { useTagStore } from '@/store/useTagStore'
export * from './util'
export const stackCache = new Map<string, Page[]>()
const global = useGlobalStore()
const tagStore = useTagStore()
const sli = useImgSliStore()
const imgTransferBus = new BroadcastChannel('iib-image-transfer-bus')
export const { eventEmitter: events, useEventListen } = typedEventEmitter<{
@ -95,7 +98,7 @@ export const { useHookShareState } = createTypedShareStateHook(
const previewing = ref(false)
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 {
previewing,
@ -140,16 +143,16 @@ export interface Page {
* @param props
* @returns
*/
export function usePreview (props: Props, custom?: { scroller: Ref<Scroller | undefined> }) {
export function usePreview (props: Props) {
const {
previewIdx,
eventEmitter,
canLoadNext,
previewing,
sortedFiles: files
sortedFiles: files,
scroller
} = useHookShareState().toRefs()
const { state } = useHookShareState()
const scroller = computed(() => custom?.scroller.value ?? state.scroller)
let waitScrollTo = null as number | null
const onPreviewVisibleChange = (v: boolean, lv: boolean) => {
previewing.value = v
@ -317,6 +320,9 @@ export function useLocation (props: Props) {
currLocation,
debounce((loc) => {
const pane = getPane.value()
if (!pane) {
return
}
pane.path = loc
const filename = pane.path!.split('/').pop()
const getTitle = () => {
@ -634,7 +640,23 @@ export function useFilesDisplay (props: Props) {
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 {
gridItems,
@ -648,7 +670,8 @@ export function useFilesDisplay (props: Props) {
loadNextDirLoading,
canLoadNext,
itemSize,
cellWidth
cellWidth,
onViewedImagesChange
}
}
@ -715,7 +738,7 @@ export function useFileTransfer () {
width: '60vw',
content: () => <div>
<div>
{`${t('moveSelectedFilesTo')}${toPath}`}
{`${t('moveSelectedFilesTo')} ${toPath}`}
<ol style={{ maxHeight: '50vh', overflow: 'auto' }}>
{data.path.map((v) => <li>{v.split(/[/\\]/).pop()}</li>)}
</ol>
@ -860,6 +883,7 @@ export function useFileItemActions (
const tagId = +`${e.key}`.split('toggle-tag-')[1]
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!
tagStore.refreshTags([file.fullpath])
message.success(t(is_remove ? 'removedTagFromImage' : 'addedTagToImage', { tag }))
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
}
})