Improved custom tag display for better user experience.
parent
fd9552c83c
commit
1c4479a394
|
|
@ -13,7 +13,7 @@ Promise.resolve().then(async () => {
|
|||
<link rel="icon" href="/favicon.ico" />
|
||||
<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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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']
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
import{d as t,o as a,m as r,cE as n}from"./index-d9e8fbed.js";const p=t({__name:"ImgSliPagePane",props:{paneIdx:{},tabIdx:{},left:{},right:{}},setup(o){return(e,s)=>(a(),r(n,{left:e.left,right:e.right},null,8,["left","right"]))}});export{p as default};
|
||||
import{d as t,o as a,m as r,cI as n}from"./index-bd9cfb84.js";const p=t({__name:"ImgSliPagePane",props:{paneIdx:{},tabIdx:{},left:{},right:{}},setup(o){return(e,s)=>(a(),r(n,{left:e.left,right:e.right},null,8,["left","right"]))}});export{p as default};
|
||||
|
|
@ -1 +0,0 @@
|
|||
.preview-switch[data-v-d4722c8d]{position:fixed;top:0;bottom:0;left:0;right:0;display:flex;align-items:center;justify-content:space-between;z-index:11111;pointer-events:none}.preview-switch>*[data-v-d4722c8d]{color:#fff;margin:16px;font-size:4em;pointer-events:all;cursor:pointer}.preview-switch>*.disable[data-v-d4722c8d]{opacity:0;pointer-events:none;cursor:none}.container[data-v-d4722c8d]{background:var(--zp-secondary-background)}.container .file-list[data-v-d4722c8d]{list-style:none;padding:8px;height:100%;overflow:auto;height:var(--pane-max-height);width:100%}
|
||||
|
|
@ -0,0 +1 @@
|
|||
.preview-switch[data-v-3c251729]{position:fixed;top:0;bottom:0;left:0;right:0;display:flex;align-items:center;justify-content:space-between;z-index:11111;pointer-events:none}.preview-switch>*[data-v-3c251729]{color:#fff;margin:16px;font-size:4em;pointer-events:all;cursor:pointer}.preview-switch>*.disable[data-v-3c251729]{opacity:0;pointer-events:none;cursor:none}.container[data-v-3c251729]{background:var(--zp-secondary-background)}.container .file-list[data-v-3c251729]{list-style:none;padding:8px;height:100%;overflow:auto;height:var(--pane-max-height);width:100%}
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{d as q,l as Q,ax as j,o as r,y as _,c as s,n as a,r as e,s as h,p as y,t as W,v as b,x as X,m as M,L as H,E as u,N as S,Q as J,R as K,X as Y}from"./index-bd9cfb84.js";import{h as Z,i as ee,L as te,R as ie,j as le,S as se}from"./fullScreenContextMenu-c82c54b8.js";import{g as ne}from"./db-a47df277.js";import{u as ae}from"./hook-1cb05846.js";import"./shortcut-6308494d.js";const oe={class:"hint"},re={key:1,class:"preview-switch"},de=q({__name:"MatchedImageGrid",props:{tabIdx:{},paneIdx:{},selectedTagIds:{},id:{}},setup(T){const m=T,{queue:p,images:i,onContextMenuClickU:g,stackViewEl:V,previewIdx:n,previewing:v,onPreviewVisibleChange:D,previewImgMove:f,canPreview:w,itemSize:I,gridItems:z,showGenInfo:o,imageGenInfo:k,q:F,multiSelectedIdxs:$,onFileItemClick:B,scroller:x,showMenuIdx:d,onFileDragStart:E,onFileDragEnd:G,cellWidth:N,onScroll:R,updateImageTag:A}=ae();return Q(()=>m.selectedTagIds,async()=>{const{res:c}=p.pushAction(()=>ne(m.selectedTagIds));i.value=await c,await j(),A(),x.value.scrollToItem(0)},{immediate:!0}),(c,t)=>{const P=J,U=K,L=se;return r(),_("div",{class:"container",ref_key:"stackViewEl",ref:V},[s(L,{size:"large",spinning:!e(p).isIdle},{default:a(()=>[s(U,{visible:e(o),"onUpdate:visible":t[1]||(t[1]=l=>h(o)?o.value=l:null),width:"70vw","mask-closable":"",onOk:t[2]||(t[2]=l=>o.value=!1)},{cancelText:a(()=>[]),default:a(()=>[s(P,{active:"",loading:!e(F).isIdle},{default:a(()=>[y("div",{style:{width:"100%","word-break":"break-all","white-space":"pre-line","max-height":"70vh",overflow:"auto"},onDblclick:t[0]||(t[0]=l=>e(W)(e(k)))},[y("div",oe,b(c.$t("doubleClickToCopy")),1),X(" "+b(e(k)),1)],32)]),_:1},8,["loading"])]),_:1},8,["visible"]),e(i)?(r(),M(e(Z),{key:0,ref_key:"scroller",ref:x,class:"file-list",items:e(i),"item-size":e(I).first,"key-field":"fullpath","item-secondary-size":e(I).second,gridItems:e(z),onScroll:e(R)},{default:a(({item:l,index:C})=>[s(ee,{idx:C,file:l,"cell-width":e(N),"show-menu-idx":e(d),"onUpdate:showMenuIdx":t[3]||(t[3]=O=>h(d)?d.value=O:null),onDragstart:e(E),onDragend:e(G),onFileItemClick:e(B),"full-screen-preview-image-url":e(i)[e(n)]?e(H)(e(i)[e(n)]):"",selected:e($).includes(C),onContextMenuClick:e(g),onPreviewVisibleChange:e(D)},null,8,["idx","file","cell-width","show-menu-idx","onDragstart","onDragend","onFileItemClick","full-screen-preview-image-url","selected","onContextMenuClick","onPreviewVisibleChange"])]),_:1},8,["items","item-size","item-secondary-size","gridItems","onScroll"])):u("",!0),e(v)?(r(),_("div",re,[s(e(te),{onClick:t[4]||(t[4]=l=>e(f)("prev")),class:S({disable:!e(w)("prev")})},null,8,["class"]),s(e(ie),{onClick:t[5]||(t[5]=l=>e(f)("next")),class:S({disable:!e(w)("next")})},null,8,["class"])])):u("",!0)]),_:1},8,["spinning"]),e(v)&&e(i)&&e(i)[e(n)]?(r(),M(le,{key:0,file:e(i)[e(n)],idx:e(n),onContextMenuClick:e(g)},null,8,["file","idx","onContextMenuClick"])):u("",!0)],512)}}});const ve=Y(de,[["__scopeId","data-v-3c251729"]]);export{ve as default};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{d as L,l as O,o as r,y as _,c as l,n as a,r as e,s as h,p as y,t as q,v as b,x as Q,m as M,L as j,E as u,N as S,Q as W,R as X,X as H}from"./index-d9e8fbed.js";import{h as J,i as K,L as Y,R as Z,j as ee,S as te}from"./fullScreenContextMenu-caca4231.js";import{g as ie}from"./db-ea72b770.js";import{u as se}from"./hook-900c55c9.js";import"./shortcut-9b4bff3d.js";const le={class:"hint"},ne={key:1,class:"preview-switch"},ae=L({__name:"MatchedImageGrid",props:{tabIdx:{},paneIdx:{},selectedTagIds:{},id:{}},setup(V){const m=V,{queue:p,images:i,onContextMenuClickU:g,stackViewEl:D,previewIdx:n,previewing:v,onPreviewVisibleChange:T,previewImgMove:f,canPreview:w,itemSize:I,gridItems:z,showGenInfo:o,imageGenInfo:k,q:F,multiSelectedIdxs:$,onFileItemClick:B,scroller:C,showMenuIdx:d,onFileDragStart:E,onFileDragEnd:G,cellWidth:N}=se();return O(()=>m.selectedTagIds,async()=>{var t;const{res:c}=p.pushAction(()=>ie(m.selectedTagIds));i.value=await c,(t=C.value)==null||t.scrollToItem(0)},{immediate:!0}),(c,t)=>{const R=W,A=X,P=te;return r(),_("div",{class:"container",ref_key:"stackViewEl",ref:D},[l(P,{size:"large",spinning:!e(p).isIdle},{default:a(()=>[l(A,{visible:e(o),"onUpdate:visible":t[1]||(t[1]=s=>h(o)?o.value=s:null),width:"70vw","mask-closable":"",onOk:t[2]||(t[2]=s=>o.value=!1)},{cancelText:a(()=>[]),default:a(()=>[l(R,{active:"",loading:!e(F).isIdle},{default:a(()=>[y("div",{style:{width:"100%","word-break":"break-all","white-space":"pre-line","max-height":"70vh",overflow:"auto"},onDblclick:t[0]||(t[0]=s=>e(q)(e(k)))},[y("div",le,b(c.$t("doubleClickToCopy")),1),Q(" "+b(e(k)),1)],32)]),_:1},8,["loading"])]),_:1},8,["visible"]),e(i)?(r(),M(e(J),{key:0,ref_key:"scroller",ref:C,class:"file-list",items:e(i),"item-size":e(I).first,"key-field":"fullpath","item-secondary-size":e(I).second,gridItems:e(z)},{default:a(({item:s,index:x})=>[l(K,{idx:x,file:s,"cell-width":e(N),"show-menu-idx":e(d),"onUpdate:showMenuIdx":t[3]||(t[3]=U=>h(d)?d.value=U:null),onDragstart:e(E),onDragend:e(G),onFileItemClick:e(B),"full-screen-preview-image-url":e(i)[e(n)]?e(j)(e(i)[e(n)]):"",selected:e($).includes(x),onContextMenuClick:e(g),onPreviewVisibleChange:e(T)},null,8,["idx","file","cell-width","show-menu-idx","onDragstart","onDragend","onFileItemClick","full-screen-preview-image-url","selected","onContextMenuClick","onPreviewVisibleChange"])]),_:1},8,["items","item-size","item-secondary-size","gridItems"])):u("",!0),e(v)?(r(),_("div",ne,[l(e(Y),{onClick:t[4]||(t[4]=s=>e(f)("prev")),class:S({disable:!e(w)("prev")})},null,8,["class"]),l(e(Z),{onClick:t[5]||(t[5]=s=>e(f)("next")),class:S({disable:!e(w)("next")})},null,8,["class"])])):u("",!0)]),_:1},8,["spinning"]),e(v)&&e(i)&&e(i)[e(n)]?(r(),M(ee,{key:0,file:e(i)[e(n)],idx:e(n),onContextMenuClick:e(g)},null,8,["file","idx","onContextMenuClick"])):u("",!0)],512)}}});const me=H(ae,[["__scopeId","data-v-d4722c8d"]]);export{me as default};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{d as X,$,aw as J,bQ as Y,bP as B,o,y as k,c as r,r as e,bT as Z,m,n as d,x as w,v,E as f,s as V,p as A,t as ee,L as ne,N as E,ar as te,ai as se,U as ae,V as ie,Q as le,R as oe,X as re}from"./index-d9e8fbed.js";import{h as de,i as ue,L as ce,R as pe,j as me,S as ve}from"./fullScreenContextMenu-caca4231.js";/* empty css */import{b as U,c as fe,e as ge,u as ke}from"./db-ea72b770.js";import{u as we}from"./hook-900c55c9.js";import"./shortcut-9b4bff3d.js";const ye={key:0,class:"search-bar"},Ie={class:"hint"},Ce={key:1,class:"preview-switch"},xe=X({__name:"SubstrSearch",setup(be){const{queue:l,images:a,onContextMenuClickU:y,stackViewEl:F,previewIdx:u,previewing:I,onPreviewVisibleChange:R,previewImgMove:C,canPreview:x,itemSize:b,gridItems:T,showGenInfo:c,imageGenInfo:h,q:N,multiSelectedIdxs:P,onFileItemClick:L,scroller:_,showMenuIdx:g,onFileDragStart:q,onFileDragEnd:G,cellWidth:K}=we(),p=$(""),t=$();J(async()=>{t.value=await U(),t.value.img_count&&t.value.expired&&S()});const S=Y(()=>l.pushAction(async()=>(await ke(),t.value=await U(),t.value)).res),M=async()=>{var s;a.value=await l.pushAction(()=>ge(p.value)).res,(s=_.value)==null||s.scrollToItem(0),a.value.length||te.info(se("fuzzy-search-noResults"))};return B("returnToIIB",async()=>{const s=await l.pushAction(fe).res;t.value.expired=s.expired}),B("searchIndexExpired",()=>t.value&&(t.value.expired=!0)),(s,n)=>{const O=ae,z=ie,Q=le,j=oe,H=ve;return o(),k("div",{class:"container",ref_key:"stackViewEl",ref:F},[t.value?(o(),k("div",ye,[r(O,{value:p.value,"onUpdate:value":n[0]||(n[0]=i=>p.value=i),placeholder:s.$t("fuzzy-search-placeholder"),disabled:!e(l).isIdle,onKeydown:Z(M,["enter"])},null,8,["value","placeholder","disabled","onKeydown"]),t.value.expired||!t.value.img_count?(o(),m(z,{key:0,onClick:e(S),loading:!e(l).isIdle,type:"primary"},{default:d(()=>[w(v(t.value.img_count===0?s.$t("generateIndexHint"):s.$t("UpdateIndex")),1)]),_:1},8,["onClick","loading"])):(o(),m(z,{key:1,type:"primary",onClick:M,loading:!e(l).isIdle,disabled:!p.value},{default:d(()=>[w(v(s.$t("search")),1)]),_:1},8,["loading","disabled"]))])):f("",!0),r(H,{size:"large",spinning:!e(l).isIdle},{default:d(()=>[r(j,{visible:e(c),"onUpdate:visible":n[2]||(n[2]=i=>V(c)?c.value=i:null),width:"70vw","mask-closable":"",onOk:n[3]||(n[3]=i=>c.value=!1)},{cancelText:d(()=>[]),default:d(()=>[r(Q,{active:"",loading:!e(N).isIdle},{default:d(()=>[A("div",{style:{width:"100%","word-break":"break-all","white-space":"pre-line","max-height":"70vh",overflow:"auto"},onDblclick:n[1]||(n[1]=i=>e(ee)(e(h)))},[A("div",Ie,v(s.$t("doubleClickToCopy")),1),w(" "+v(e(h)),1)],32)]),_:1},8,["loading"])]),_:1},8,["visible"]),e(a)?(o(),m(e(de),{key:0,ref_key:"scroller",ref:_,class:"file-list",items:e(a),"item-size":e(b).first,"key-field":"fullpath","item-secondary-size":e(b).second,gridItems:e(T)},{default:d(({item:i,index:D})=>[r(ue,{idx:D,file:i,"show-menu-idx":e(g),"onUpdate:showMenuIdx":n[4]||(n[4]=W=>V(g)?g.value=W:null),onFileItemClick:e(L),"full-screen-preview-image-url":e(a)[e(u)]?e(ne)(e(a)[e(u)]):"","cell-width":e(K),selected:e(P).includes(D),onContextMenuClick:e(y),onDragstart:e(q),onDragend:e(G),onPreviewVisibleChange:e(R)},null,8,["idx","file","show-menu-idx","onFileItemClick","full-screen-preview-image-url","cell-width","selected","onContextMenuClick","onDragstart","onDragend","onPreviewVisibleChange"])]),_:1},8,["items","item-size","item-secondary-size","gridItems"])):f("",!0),e(I)?(o(),k("div",Ce,[r(e(ce),{onClick:n[5]||(n[5]=i=>e(C)("prev")),class:E({disable:!e(x)("prev")})},null,8,["class"]),r(e(pe),{onClick:n[6]||(n[6]=i=>e(C)("next")),class:E({disable:!e(x)("next")})},null,8,["class"])])):f("",!0)]),_:1},8,["spinning"]),e(I)&&e(a)&&e(a)[e(u)]?(o(),m(me,{key:1,file:e(a)[e(u)],idx:e(u),onContextMenuClick:e(y)},null,8,["file","idx","onContextMenuClick"])):f("",!0)],512)}}});const $e=re(xe,[["__scopeId","data-v-01615fdf"]]);export{$e as default};
|
||||
|
|
@ -1 +0,0 @@
|
|||
.search-bar[data-v-01615fdf]{padding:8px;display:flex}.preview-switch[data-v-01615fdf]{position:fixed;top:0;bottom:0;left:0;right:0;display:flex;align-items:center;justify-content:space-between;z-index:11111;pointer-events:none}.preview-switch>*[data-v-01615fdf]{color:#fff;margin:16px;font-size:4em;pointer-events:all;cursor:pointer}.preview-switch>*.disable[data-v-01615fdf]{opacity:0;pointer-events:none;cursor:none}.container[data-v-01615fdf]{background:var(--zp-secondary-background)}.container .file-list[data-v-01615fdf]{list-style:none;padding:8px;height:100%;overflow:auto;height:var(--pane-max-height);width:100%}
|
||||
|
|
@ -0,0 +1 @@
|
|||
import{d as Y,$,aw as Z,bQ as ee,bP as B,o,y as k,c as r,r as e,bT as ae,m,n as d,x as w,v,E as f,s as V,p as A,t as ne,L as te,N as E,ax as le,ar as se,ai as ie,U as oe,V as re,Q as de,R as ue,X as ce}from"./index-bd9cfb84.js";import{h as pe,i as me,L as ve,R as fe,j as ge,S as ke}from"./fullScreenContextMenu-c82c54b8.js";/* empty css */import{b as T,c as we,e as ye,u as Ie}from"./db-a47df277.js";import{u as xe}from"./hook-1cb05846.js";import"./shortcut-6308494d.js";const be={key:0,class:"search-bar"},Ce={class:"hint"},he={key:1,class:"preview-switch"},_e=Y({__name:"SubstrSearch",setup(Se){const{queue:s,images:t,onContextMenuClickU:y,stackViewEl:U,previewIdx:u,previewing:I,onPreviewVisibleChange:F,previewImgMove:x,canPreview:b,itemSize:C,gridItems:R,showGenInfo:c,imageGenInfo:h,q:N,multiSelectedIdxs:P,onFileItemClick:L,scroller:_,showMenuIdx:g,onFileDragStart:q,onFileDragEnd:G,cellWidth:K,onScroll:O,updateImageTag:Q}=xe(),p=$(""),n=$();Z(async()=>{n.value=await T(),n.value.img_count&&n.value.expired&&S()});const S=ee(()=>s.pushAction(async()=>(await Ie(),n.value=await T(),n.value)).res),M=async()=>{t.value=await s.pushAction(()=>ye(p.value)).res,await le(),Q(),_.value.scrollToItem(0),t.value.length||se.info(ie("fuzzy-search-noResults"))};return B("returnToIIB",async()=>{const i=await s.pushAction(we).res;n.value.expired=i.expired}),B("searchIndexExpired",()=>n.value&&(n.value.expired=!0)),(i,a)=>{const j=oe,z=re,H=de,W=ue,X=ke;return o(),k("div",{class:"container",ref_key:"stackViewEl",ref:U},[n.value?(o(),k("div",be,[r(j,{value:p.value,"onUpdate:value":a[0]||(a[0]=l=>p.value=l),placeholder:i.$t("fuzzy-search-placeholder"),disabled:!e(s).isIdle,onKeydown:ae(M,["enter"])},null,8,["value","placeholder","disabled","onKeydown"]),n.value.expired||!n.value.img_count?(o(),m(z,{key:0,onClick:e(S),loading:!e(s).isIdle,type:"primary"},{default:d(()=>[w(v(n.value.img_count===0?i.$t("generateIndexHint"):i.$t("UpdateIndex")),1)]),_:1},8,["onClick","loading"])):(o(),m(z,{key:1,type:"primary",onClick:M,loading:!e(s).isIdle,disabled:!p.value},{default:d(()=>[w(v(i.$t("search")),1)]),_:1},8,["loading","disabled"]))])):f("",!0),r(X,{size:"large",spinning:!e(s).isIdle},{default:d(()=>[r(W,{visible:e(c),"onUpdate:visible":a[2]||(a[2]=l=>V(c)?c.value=l:null),width:"70vw","mask-closable":"",onOk:a[3]||(a[3]=l=>c.value=!1)},{cancelText:d(()=>[]),default:d(()=>[r(H,{active:"",loading:!e(N).isIdle},{default:d(()=>[A("div",{style:{width:"100%","word-break":"break-all","white-space":"pre-line","max-height":"70vh",overflow:"auto"},onDblclick:a[1]||(a[1]=l=>e(ne)(e(h)))},[A("div",Ce,v(i.$t("doubleClickToCopy")),1),w(" "+v(e(h)),1)],32)]),_:1},8,["loading"])]),_:1},8,["visible"]),e(t)?(o(),m(e(pe),{key:0,ref_key:"scroller",ref:_,class:"file-list",items:e(t),"item-size":e(C).first,"key-field":"fullpath","item-secondary-size":e(C).second,gridItems:e(R),onScroll:e(O)},{default:d(({item:l,index:D})=>[r(me,{idx:D,file:l,"show-menu-idx":e(g),"onUpdate:showMenuIdx":a[4]||(a[4]=J=>V(g)?g.value=J:null),onFileItemClick:e(L),"full-screen-preview-image-url":e(t)[e(u)]?e(te)(e(t)[e(u)]):"","cell-width":e(K),selected:e(P).includes(D),onContextMenuClick:e(y),onDragstart:e(q),onDragend:e(G),onPreviewVisibleChange:e(F)},null,8,["idx","file","show-menu-idx","onFileItemClick","full-screen-preview-image-url","cell-width","selected","onContextMenuClick","onDragstart","onDragend","onPreviewVisibleChange"])]),_:1},8,["items","item-size","item-secondary-size","gridItems","onScroll"])):f("",!0),e(I)?(o(),k("div",he,[r(e(ve),{onClick:a[5]||(a[5]=l=>e(x)("prev")),class:E({disable:!e(b)("prev")})},null,8,["class"]),r(e(fe),{onClick:a[6]||(a[6]=l=>e(x)("next")),class:E({disable:!e(b)("next")})},null,8,["class"])])):f("",!0)]),_:1},8,["spinning"]),e(I)&&e(t)&&e(t)[e(u)]?(o(),m(ge,{key:1,file:e(t)[e(u)],idx:e(u),onContextMenuClick:e(y)},null,8,["file","idx","onContextMenuClick"])):f("",!0)],512)}}});const Ae=ce(_e,[["__scopeId","data-v-905bf6da"]]);export{Ae as default};
|
||||
|
|
@ -0,0 +1 @@
|
|||
.search-bar[data-v-905bf6da]{padding:8px;display:flex}.preview-switch[data-v-905bf6da]{position:fixed;top:0;bottom:0;left:0;right:0;display:flex;align-items:center;justify-content:space-between;z-index:11111;pointer-events:none}.preview-switch>*[data-v-905bf6da]{color:#fff;margin:16px;font-size:4em;pointer-events:all;cursor:pointer}.preview-switch>*.disable[data-v-905bf6da]{opacity:0;pointer-events:none;cursor:none}.container[data-v-905bf6da]{background:var(--zp-secondary-background)}.container .file-list[data-v-905bf6da]{list-style:none;padding:8px;height:100%;overflow:auto;height:var(--pane-max-height);width:100%}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
import{c4 as t}from"./index-d9e8fbed.js";const o=async()=>(await t.value.get("/db/basic_info")).data,c=async()=>(await t.value.get("/db/expired_dirs")).data,r=async()=>{await t.value.post("/db/update_image_data",{},{timeout:1/0})},d=async a=>(await t.value.post("/db/match_images_by_tags",a)).data,g=async a=>(await t.value.post("/db/add_custom_tag",a)).data,u=async a=>(await t.value.post("/db/toggle_custom_tag_to_img",a)).data,p=async a=>{await t.value.post("/db/remove_custom_tag",a)},i=async a=>(await t.value.get("/db/img_selected_custom_tag",{params:{path:a}})).data,m=async a=>(await t.value.get("/db/search_by_substr",{params:{substr:a}})).data,e="/db/scanned_paths",_=async a=>{await t.value.post(e,{path:a})},b=async a=>{await t.value.delete(e,{data:{path:a}})};export{_ as a,o as b,c,g as d,m as e,b as f,d as g,i as h,p as r,u as t,r as u};
|
||||
import{c6 as t}from"./index-bd9cfb84.js";const o=async()=>(await t.value.get("/db/basic_info")).data,c=async()=>(await t.value.get("/db/expired_dirs")).data,r=async()=>{await t.value.post("/db/update_image_data",{},{timeout:1/0})},d=async a=>(await t.value.post("/db/match_images_by_tags",a)).data,g=async a=>(await t.value.post("/db/add_custom_tag",a)).data,u=async a=>(await t.value.post("/db/toggle_custom_tag_to_img",a)).data,p=async a=>{await t.value.post("/db/remove_custom_tag",a)},i=async a=>(await t.value.get("/db/search_by_substr",{params:{substr:a}})).data,e="/db/scanned_paths",m=async a=>{await t.value.post(e,{path:a})},_=async a=>{await t.value.delete(e,{data:{path:a}})},b=async a=>(await t.value.post("/db/get_image_tags",{paths:a})).data;export{m as a,o as b,c,g as d,i as e,b as f,d as g,_ as h,p as r,u as t,r as u};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1 @@
|
|||
import{$ as D,bO as E,bd as P,aC as $}from"./index-bd9cfb84.js";import{k as z,u as G,b as L,f as O,a as Q,c as R,d as V,e as _,l as A}from"./fullScreenContextMenu-c82c54b8.js";const U=()=>{const e=D(),c=E(),u=z(),n={tabIdx:-1,target:"local",paneIdx:-1,walkMode:!1},{stackViewEl:r,multiSelectedIdxs:d,stack:m,scroller:o}=G({images:e}).toRefs(),{itemSize:g,gridItems:p,cellWidth:v}=L(n),{showMenuIdx:I}=O();Q(n);const{onFileDragStart:f,onFileDragEnd:x}=R(),{showGenInfo:h,imageGenInfo:w,q:k,onContextMenuClick:i,onFileItemClick:S}=V(n,{openNext:P}),{previewIdx:M,previewing:b,onPreviewVisibleChange:C,previewImgMove:F,canPreview:y}=_(n),T=async(s,a,t)=>{m.value=[{curr:"",files:e.value}],await i(s,a,t)};A("removeFiles",async({paths:s})=>{var a;e.value=(a=e.value)==null?void 0:a.filter(t=>!s.includes(t.fullpath))});const l=()=>{const s=o.value;if(s&&e.value){const a=e.value.slice(Math.max(s.$_startIndex-10,0),s.$_endIndex+10).map(t=>t.fullpath);u.fetchImageTags(a)}},q=$(l,300);return{scroller:o,queue:c,images:e,onContextMenuClickU:T,stackViewEl:r,previewIdx:M,previewing:b,onPreviewVisibleChange:C,previewImgMove:F,canPreview:y,itemSize:g,gridItems:p,showGenInfo:h,imageGenInfo:w,q:k,onContextMenuClick:i,onFileItemClick:S,showMenuIdx:I,multiSelectedIdxs:d,onFileDragStart:f,onFileDragEnd:x,cellWidth:v,onScroll:q,updateImageTag:l}};export{U as u};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{$ as c,bO as q,bd as D}from"./index-d9e8fbed.js";import{u as E,b as P,f as z,a as G,c as L,d as O,e as Q,k as R}from"./fullScreenContextMenu-caca4231.js";const H=()=>{const e=c(),l=q(),o=c(),s={tabIdx:-1,target:"local",paneIdx:-1,walkMode:!1},{stackViewEl:r,multiSelectedIdxs:u,stack:m}=E({images:e}).toRefs(),{itemSize:d,gridItems:v,cellWidth:f}=P(s),{showMenuIdx:p}=z();G(s);const{onFileDragStart:I,onFileDragEnd:g}=L(),{showGenInfo:w,imageGenInfo:k,q:x,onContextMenuClick:i,onFileItemClick:h}=O(s,{openNext:D}),{previewIdx:F,previewing:M,onPreviewVisibleChange:b,previewImgMove:C,canPreview:S}=Q(s,{scroller:o}),y=async(a,t,n)=>{m.value=[{curr:"",files:e.value}],await i(a,t,n)};return R("removeFiles",async({paths:a})=>{var t;e.value=(t=e.value)==null?void 0:t.filter(n=>!a.includes(n.fullpath))}),{scroller:o,queue:l,images:e,onContextMenuClickU:y,stackViewEl:r,previewIdx:F,previewing:M,onPreviewVisibleChange:b,previewImgMove:C,canPreview:S,itemSize:d,gridItems:v,showGenInfo:w,imageGenInfo:k,q:x,onContextMenuClick:i,onFileItemClick:h,showMenuIdx:p,multiSelectedIdxs:u,onFileDragStart:I,onFileDragEnd:g,cellWidth:f}};export{H as u};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
import{cD as s}from"./index-d9e8fbed.js";var r=1/0,i=17976931348623157e292;function e(t){if(!t)return t===0?t:0;if(t=s(t),t===r||t===-r){var n=t<0?-1:1;return n*i}return t===t?t:0}function f(t){var n=t==null?0:t.length;return n?t[n-1]:void 0}const h=t=>{const n=[];return t.shiftKey&&n.push("Shift"),t.ctrlKey&&n.push("Ctrl"),t.metaKey&&n.push("Cmd"),(t.code.startsWith("Key")||t.code.startsWith("Digit"))&&n.push(t.code),n.join(" + ")};export{h as g,f as l,e as t};
|
||||
import{cH as s}from"./index-bd9cfb84.js";var r=1/0,i=17976931348623157e292;function e(t){if(!t)return t===0?t:0;if(t=s(t),t===r||t===-r){var n=t<0?-1:1;return n*i}return t===t?t:0}function f(t){var n=t==null?0:t.length;return n?t[n-1]:void 0}const h=t=>{const n=[];return t.shiftKey&&n.push("Shift"),t.ctrlKey&&n.push("Ctrl"),t.metaKey&&n.push("Cmd"),(t.code.startsWith("Key")||t.code.startsWith("Digit"))&&n.push(t.code),n.join(" + ")};export{h as g,f as l,e as t};
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -7,7 +7,7 @@
|
|||
<link rel="icon" href="/favicon.ico" />
|
||||
<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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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[]>
|
||||
}
|
||||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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! }] // hack,for 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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)">
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
import { Tag, batchGetTagsByPath } from '@/api/db'
|
||||
import { createReactiveQueue } from '@/util'
|
||||
import { defineStore } from 'pinia'
|
||||
import sjcl from 'sjcl'
|
||||
import { reactive } from 'vue'
|
||||
|
||||
export const useTagStore = defineStore('useTagStore', () => {
|
||||
const q = createReactiveQueue()
|
||||
const fetchPendingImagePaths = new Set<string>()
|
||||
const tagMap = reactive(new Map<string, Tag[]>())
|
||||
const fetchImageTags = async (paths: string[]) => {
|
||||
paths = paths.filter(v => !fetchPendingImagePaths.has(v) && !tagMap.has(v))
|
||||
if (!paths.length) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
paths.forEach(v => tagMap.set(v, []))
|
||||
const res = await batchGetTagsByPath(paths)
|
||||
for (const path in res) {
|
||||
tagMap.set(path, res[path])
|
||||
}
|
||||
} finally {
|
||||
paths.forEach(v => fetchPendingImagePaths.delete(v))
|
||||
}
|
||||
}
|
||||
const colors = ['pink', 'red', 'orange', 'green', 'cyan', 'blue', 'purple']
|
||||
const colorCache = new Map<string, string>()
|
||||
const getColor = (tag: string) => {
|
||||
let color = colorCache.get(tag)
|
||||
if (!color) {
|
||||
const hash = sjcl.hash.sha256.hash(tag)
|
||||
const num = parseInt(sjcl.codec.hex.fromBits(hash), 16) % colors.length
|
||||
color = colors[num]
|
||||
colorCache.set(tag, color)
|
||||
}
|
||||
return color
|
||||
}
|
||||
const refreshTags = async (paths: string[]) => {
|
||||
paths.forEach(v => tagMap.delete(v))
|
||||
await fetchImageTags(paths)
|
||||
}
|
||||
return {
|
||||
tagMap,
|
||||
q,
|
||||
getColor,
|
||||
fetchImageTags,
|
||||
refreshTags
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue