Add more isolation mechanisms and export functions for easier use as a library

pull/357/head
zanllp 2023-07-31 09:01:20 +08:00
parent 6efef97594
commit 6d181ef8ba
39 changed files with 604 additions and 70 deletions

39
app.py
View File

@ -1,11 +1,16 @@
import codecs
from typing import List
from fastapi import FastAPI
from fastapi import FastAPI, Response
from fastapi.responses import FileResponse
import uvicorn
import os
from scripts.iib.api import infinite_image_browsing_api, index_html_path
from scripts.iib.tool import get_sd_webui_conf, get_valid_img_dirs, sd_img_dirs, normalize_paths
from scripts.iib.api import infinite_image_browsing_api, index_html_path, DEFAULT_BASE
from scripts.iib.tool import (
get_sd_webui_conf,
get_valid_img_dirs,
sd_img_dirs,
normalize_paths,
)
from scripts.iib.db.datamodel import DataBase, Image
from scripts.iib.db.update_image_data import update_image_data
import argparse
@ -70,6 +75,8 @@ class AppUtils:
allow_cors=False,
enable_shutdown=False,
sd_webui_dir: Optional[str] = None,
base="",
export_fe_fn=False,
):
"""
Parameter definitions can be found by running the `python app.py -h `command or by examining the setup_parser() function.
@ -81,6 +88,8 @@ class AppUtils:
self.allow_cors = allow_cors
self.enable_shutdown = enable_shutdown
self.sd_webui_dir = sd_webui_dir
self.base = base
self.export_fe_fn = export_fe_fn
if sd_webui_dir:
DataBase.path = os.path.join(
sd_webui_dir, "extensions/sd-webui-infinite-image-browsing/iib.db"
@ -93,7 +102,9 @@ class AppUtils:
self.__init__(*args, **kwargs)
@staticmethod
def async_run(app: FastAPI, port: int = default_port, host = default_host) -> Coroutine:
def async_run(
app: FastAPI, port: int = default_port, host=default_host
) -> Coroutine:
"""
用于从异步运行的 FastAPI Jupyter Notebook 环境中非常有用
"""
@ -127,6 +138,8 @@ class AppUtils:
allow_cors=self.allow_cors,
enable_shutdown=self.enable_shutdown,
launch_mode="server",
base=self.base,
export_fe_fn=self.export_fe_fn,
)
def get_root_browser_app(self) -> FastAPI:
@ -138,6 +151,10 @@ class AppUtils:
# 用于在首页显示
@app.get("/")
def index():
if self.base and self.base != DEFAULT_BASE:
with open(index_html_path, "r", encoding="utf-8") as file:
content = file.read().replace(DEFAULT_BASE, self.base)
return Response(content=content, media_type="text/html")
return FileResponse(index_html_path)
self.wrap_app(app)
@ -187,10 +204,18 @@ def setup_parser() -> argparse.ArgumentParser:
default=None,
help="The path to the sd_webui folder. When specified, the sd_webui's configuration will be used and the extension must be installed within the sd_webui. Data will be shared between the two.",
)
parser.add_argument(
"--export_fe_fn",
action="store_true",
help="Export front-end functions to enable external access through iframe.",
)
parser.add_argument("--base", type=str, help="The base URL for the IIB Api.")
return parser
def launch_app(port: int = default_port, host: str = default_host, *args, **kwargs: dict) -> None:
def launch_app(
port: int = default_port, host: str = default_host, *args, **kwargs: dict
) -> None:
"""
Launches the application on the specified port.
@ -203,7 +228,9 @@ def launch_app(port: int = default_port, host: str = default_host, *args, **kwar
uvicorn.run(app, host=host, port=port)
async def async_launch_app(port: int = default_port, host: str = default_host, *args, **kwargs: dict) -> None:
async def async_launch_app(
port: int = default_port, host: str = default_host, *args, **kwargs: dict
) -> None:
"""
Asynchronously launches the application on the specified port.

View File

@ -14,6 +14,8 @@ Promise.resolve().then(async () => {
<title>Infinite Image Browsing</title>
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-594b377e.js"></script>
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-4cc43e92.css">
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-8897613b.js"></script>
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-6369b2e2.css">
</head>
<body>

View File

@ -27,7 +27,7 @@ from scripts.iib.tool import (
to_abs_path,
is_secret_key_required
)
from fastapi import FastAPI, HTTPException
from fastapi import FastAPI, HTTPException, Response
from fastapi.staticfiles import StaticFiles
import asyncio
from typing import List, Optional
@ -94,7 +94,7 @@ async def verify_secret(request: Request):
if mem["secret_key_hash"] != token:
raise HTTPException(status_code=401, detail="Unauthorized")
DEFAULT_BASE = "/infinite_image_browsing"
def infinite_image_browsing_api(app: FastAPI, **kwargs):
pre = "/infinite_image_browsing"
@ -133,6 +133,7 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
f"An exception occurred while processing {request.method} {request.url}: {exc}"
)
pre = kwargs.get("base") or DEFAULT_BASE
if kwargs.get("allow_cors"):
app.add_middleware(
CORSMiddleware,
@ -145,7 +146,7 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
try:
return get_valid_img_dirs(get_sd_webui_conf(**kwargs))
except Exception as e:
print(e)
print(e)
return []
def update_all_scanned_paths():
@ -174,7 +175,9 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
)
else:
paths = (
get_img_search_dirs() + mem["extra_paths"] + kwargs.get("extra_paths_cli", [])
get_img_search_dirs()
+ mem["extra_paths"]
+ kwargs.get("extra_paths_cli", [])
)
mem["all_scanned_paths"] = unique_by(paths)
@ -235,12 +238,15 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
return [x for x in files if is_path_trusted(x["fullpath"])]
static_dir = f"{cwd}/vue/dist"
if os.path.exists(static_dir):
app.mount(
f"{pre}/fe-static",
StaticFiles(directory=static_dir),
name="infinite_image_browsing-fe-static",
)
@app.get(pre + "/fe-static/{file_path:path}")
async def serve_static_file(file_path: str):
file_full_path = f"{static_dir}/{file_path}"
if file_path.endswith(".js"):
with open(file_full_path, "r", encoding="utf-8") as file:
content = file.read().replace(DEFAULT_BASE, pre)
return Response(content=content, media_type="text/javascript")
else:
return FileResponse(file_full_path)
@app.get(f"{pre}/hello")
async def greeting():
@ -271,6 +277,7 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
"extra_paths": extra_paths,
"enable_access_control": enable_access_control,
"launch_mode": kwargs.get("launch_mode", "sd"),
"export_fe_fn": bool(kwargs.get("export_fe_fn")),
}
class DeleteFilesReq(BaseModel):
@ -565,6 +572,10 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
@app.get(pre)
def index_bd():
if pre != DEFAULT_BASE:
with open(index_html_path, "r", encoding="utf-8") as file:
content = file.read().replace(DEFAULT_BASE, pre)
return Response(content=content, media_type="text/html")
return FileResponse(index_html_path)
class PathsReq(BaseModel):
@ -638,7 +649,9 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
img_count = DbImg.count(conn)
update_extra_paths(conn)
dirs = (
get_img_search_dirs() if img_count == 0 else Folder.get_expired_dirs(conn)
get_img_search_dirs()
if img_count == 0
else Floder.get_expired_dirs(conn)
) + mem["extra_paths"]
update_image_data(dirs)
@ -746,10 +759,10 @@ def infinite_image_browsing_api(app: FastAPI, **kwargs):
)
img = DbImg.get(conn, path)
if not img:
if DbImg.count(conn):
if DbImg.count(conn):
update_image_data([os.path.dirname(path)])
img = DbImg.get(conn, path)
else:
else:
raise HTTPException(
400,
"你需要先通过图像搜索页生成索引"

4
vue/dist/assets/FileItem-3695dcaf.js vendored Normal file

File diff suppressed because one or more lines are too long

5
vue/dist/assets/FileItem-982b78d3.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
import{d as t,o as a,m as r,b$ as n}from"./index-8897613b.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

@ -0,0 +1 @@
import{d as q,l as Q,ax as W,o as r,y as _,c as s,n as a,r as e,s as h,p as y,t as X,v as b,x as j,m as M,L as H,C as m,N as S,Q as J,R as K,X as Y}from"./index-8897613b.js";import{L as Z,R as ee,f as te,S as ie}from"./fullScreenContextMenu-2b4c635b.js";import{g as le,F as se}from"./FileItem-3695dcaf.js";import{g as ne}from"./db-afd72581.js";import{u as ae}from"./hook-40f49198.js";import"./shortcut-902028a8.js";const oe={class:"hint"},re={key:1,class:"preview-switch"},de=q({__name:"MatchedImageGrid",props:{tabIdx:{},paneIdx:{},selectedTagIds:{},id:{}},setup(T){const u=T,{queue:p,images:i,onContextMenuClickU:g,stackViewEl:V,previewIdx:n,previewing:v,onPreviewVisibleChange:D,previewImgMove:f,canPreview:w,itemSize:I,gridItems:F,showGenInfo:o,imageGenInfo:k,q:z,multiSelectedIdxs:$,onFileItemClick:B,scroller:x,showMenuIdx:d,onFileDragStart:G,onFileDragEnd:N,cellWidth:R,onScroll:A,updateImageTag:E}=ae();return Q(()=>u.selectedTagIds,async()=>{const{res:c}=p.pushAction(()=>ne(u.selectedTagIds));i.value=await c,await W(),E(),x.value.scrollToItem(0)},{immediate:!0}),(c,t)=>{const P=J,U=K,L=ie;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(z).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(X)(e(k)))},[y("div",oe,b(c.$t("doubleClickToCopy")),1),j(" "+b(e(k)),1)],32)]),_:1},8,["loading"])]),_:1},8,["visible"]),e(i)?(r(),M(e(le),{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(F),onScroll:e(A)},{default:a(({item:l,index:C})=>[s(se,{idx:C,file:l,"cell-width":e(R),"show-menu-idx":e(d),"onUpdate:showMenuIdx":t[3]||(t[3]=O=>h(d)?d.value=O:null),onDragstart:e(G),onDragend:e(N),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"])):m("",!0),e(v)?(r(),_("div",re,[s(e(Z),{onClick:t[4]||(t[4]=l=>e(f)("prev")),class:S({disable:!e(w)("prev")})},null,8,["class"]),s(e(ee),{onClick:t[5]||(t[5]=l=>e(f)("next")),class:S({disable:!e(w)("next")})},null,8,["class"])])):m("",!0)]),_:1},8,["spinning"]),e(v)&&e(i)&&e(i)[e(n)]?(r(),M(te,{key:0,file:e(i)[e(n)],idx:e(n),onContextMenuClick:e(g)},null,8,["file","idx","onContextMenuClick"])):m("",!0)],512)}}});const fe=Y(de,[["__scopeId","data-v-d698e678"]]);export{fe as default};

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 ne,m,n as u,x as w,v,C as g,s as V,p as F,t as te,L as ae,N as A,ax as se,ar as le,ai as ie,U as oe,V as re,Q as ue,R as de,X as ce}from"./index-8897613b.js";import{L as pe,R as me,f as ve,S as ge}from"./fullScreenContextMenu-2b4c635b.js";/* empty css */import{g as fe,F as ke}from"./FileItem-3695dcaf.js";import{b as T,c as we,f as be,u as ye}from"./db-afd72581.js";import{u as Ie}from"./hook-40f49198.js";import"./shortcut-902028a8.js";const xe={key:0,class:"search-bar"},Ce={class:"hint"},_e={key:1,class:"preview-switch"},he=Y({__name:"SubstrSearch",setup(Se){const{queue:l,images:a,onContextMenuClickU:b,stackViewEl:U,previewIdx:d,previewing:y,onPreviewVisibleChange:E,previewImgMove:I,canPreview:x,itemSize:C,gridItems:R,showGenInfo:c,imageGenInfo:_,q:N,multiSelectedIdxs:P,onFileItemClick:L,scroller:h,showMenuIdx:f,onFileDragStart:q,onFileDragEnd:G,cellWidth:K,onScroll:O,updateImageTag:Q}=Ie(),p=$(""),t=$();Z(async()=>{t.value=await T(),t.value.img_count&&t.value.expired&&S()});const S=ee(()=>l.pushAction(async()=>(await ye(),t.value=await T(),t.value)).res),M=async()=>{a.value=await l.pushAction(()=>be(p.value)).res,await se(),Q(),h.value.scrollToItem(0),a.value.length||le.info(ie("fuzzy-search-noResults"))};return B("returnToIIB",async()=>{const i=await l.pushAction(we).res;t.value.expired=i.expired}),B("searchIndexExpired",()=>t.value&&(t.value.expired=!0)),(i,n)=>{const H=oe,z=re,W=ue,X=de,j=ge;return o(),k("div",{class:"container",ref_key:"stackViewEl",ref:U},[t.value?(o(),k("div",xe,[r(H,{value:p.value,"onUpdate:value":n[0]||(n[0]=s=>p.value=s),placeholder:i.$t("fuzzy-search-placeholder"),disabled:!e(l).isIdle,onKeydown:ne(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:u(()=>[w(v(t.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(l).isIdle,disabled:!p.value},{default:u(()=>[w(v(i.$t("search")),1)]),_:1},8,["loading","disabled"]))])):g("",!0),r(j,{size:"large",spinning:!e(l).isIdle},{default:u(()=>[r(X,{visible:e(c),"onUpdate:visible":n[2]||(n[2]=s=>V(c)?c.value=s:null),width:"70vw","mask-closable":"",onOk:n[3]||(n[3]=s=>c.value=!1)},{cancelText:u(()=>[]),default:u(()=>[r(W,{active:"",loading:!e(N).isIdle},{default:u(()=>[F("div",{style:{width:"100%","word-break":"break-all","white-space":"pre-line","max-height":"70vh",overflow:"auto"},onDblclick:n[1]||(n[1]=s=>e(te)(e(_)))},[F("div",Ce,v(i.$t("doubleClickToCopy")),1),w(" "+v(e(_)),1)],32)]),_:1},8,["loading"])]),_:1},8,["visible"]),e(a)?(o(),m(e(fe),{key:0,ref_key:"scroller",ref:h,class:"file-list",items:e(a),"item-size":e(C).first,"key-field":"fullpath","item-secondary-size":e(C).second,gridItems:e(R),onScroll:e(O)},{default:u(({item:s,index:D})=>[r(ke,{idx:D,file:s,"show-menu-idx":e(f),"onUpdate:showMenuIdx":n[4]||(n[4]=J=>V(f)?f.value=J:null),onFileItemClick:e(L),"full-screen-preview-image-url":e(a)[e(d)]?e(ae)(e(a)[e(d)]):"","cell-width":e(K),selected:e(P).includes(D),onContextMenuClick:e(b),onDragstart:e(q),onDragend:e(G),onPreviewVisibleChange:e(E)},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"])):g("",!0),e(y)?(o(),k("div",_e,[r(e(pe),{onClick:n[5]||(n[5]=s=>e(I)("prev")),class:A({disable:!e(x)("prev")})},null,8,["class"]),r(e(me),{onClick:n[6]||(n[6]=s=>e(I)("next")),class:A({disable:!e(x)("next")})},null,8,["class"])])):g("",!0)]),_:1},8,["spinning"]),e(y)&&e(a)&&e(a)[e(d)]?(o(),m(ve,{key:1,file:e(a)[e(d)],idx:e(d),onContextMenuClick:e(b)},null,8,["file","idx","onContextMenuClick"])):g("",!0)],512)}}});const Ae=ce(he,[["__scopeId","data-v-bb005cb9"]]);export{Ae as default};

1
vue/dist/assets/TagSearch-7cbe22ad.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
import{d as v,c0 as C,bO as I,o as i,y as _,p as f,c,n as r,x as h,v as d,r as e,m as F,L as x,c1 as z,c2 as B,V as $,X as R}from"./index-8897613b.js";import{u as S,b as V,k as E,F as A,g as L}from"./FileItem-3695dcaf.js";import"./db-afd72581.js";import"./shortcut-902028a8.js";const T={class:"actions-panel actions"},N={key:0,class:"file-list"},U={class:"hint"},H=v({__name:"batchDownload",props:{tabIdx:{},paneIdx:{},id:{}},setup(O){const{stackViewEl:w}=S().toRefs(),{itemSize:p,gridItems:k,cellWidth:b}=V(),l=E(),{selectdFiles:n}=C(l),u=I(),y=async t=>{const s=z(t);s&&l.addFiles(s.nodes)},D=async()=>{u.pushAction(async()=>{const t=await B.value.post("/zip",{paths:n.value.map(o=>o.fullpath)},{responseType:"blob"}),s=window.URL.createObjectURL(new Blob([t.data])),a=document.createElement("a");a.href=s,a.setAttribute("download",`iib_${new Date().toLocaleString()}.zip`),document.body.appendChild(a),a.click()})},g=t=>{n.value.splice(t,1)};return(t,s)=>{const a=$;return i(),_("div",{class:"container",ref_key:"stackViewEl",ref:w,onDrop:y},[f("div",T,[c(a,{onClick:s[0]||(s[0]=o=>e(l).selectdFiles=[])},{default:r(()=>[h(d(t.$t("clear")),1)]),_:1}),c(a,{onClick:D,type:"primary",loading:!e(u).isIdle},{default:r(()=>[h(d(t.$t("zipDownload")),1)]),_:1},8,["loading"])]),e(n).length?(i(),F(e(L),{key:1,ref:"scroller",class:"file-list",items:e(n).slice(),"item-size":e(p).first,"key-field":"fullpath","item-secondary-size":e(p).second,gridItems:e(k)},{default:r(({item:o,index:m})=>[c(A,{idx:m,file:o,"cell-width":e(b),"enable-close-icon":"",onCloseIconClick:j=>g(m),"full-screen-preview-image-url":e(x)(o),"enable-right-click-menu":!1},null,8,["idx","file","cell-width","onCloseIconClick","full-screen-preview-image-url"])]),_:1},8,["items","item-size","item-secondary-size","gridItems"])):(i(),_("div",N,[f("p",U,d(t.$t("batchDownloaDDragAndDropHint")),1)]))],544)}}});const G=R(H,[["__scopeId","data-v-aab31da2"]]);export{G as default};

1
vue/dist/assets/db-afd72581.js vendored Normal file
View File

@ -0,0 +1 @@
import{c2 as t}from"./index-8897613b.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,p as e,i as f,d as g,b as h,_ as r,u as t,r as u};

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1 @@
.ant-alert{box-sizing:border-box;margin:0;color:#000000d9;font-size:14px;font-variant:tabular-nums;line-height:1.5715;list-style:none;font-feature-settings:"tnum";position:relative;display:flex;align-items:center;padding:8px 15px;word-wrap:break-word;border-radius:2px}.ant-alert-content{flex:1;min-width:0}.ant-alert-icon{margin-right:8px}.ant-alert-description{display:none;font-size:14px;line-height:22px}.ant-alert-success{background-color:#f6ffed;border:1px solid #b7eb8f}.ant-alert-success .ant-alert-icon{color:#52c41a}.ant-alert-info{background-color:#fff1e6;border:1px solid #f7ae83}.ant-alert-info .ant-alert-icon{color:#d03f0a}.ant-alert-warning{background-color:#fffbe6;border:1px solid #ffe58f}.ant-alert-warning .ant-alert-icon{color:#faad14}.ant-alert-error{background-color:#fff2f0;border:1px solid #ffccc7}.ant-alert-error .ant-alert-icon{color:#ff4d4f}.ant-alert-error .ant-alert-description>pre{margin:0;padding:0}.ant-alert-action{margin-left:8px}.ant-alert-close-icon{margin-left:8px;padding:0;overflow:hidden;font-size:12px;line-height:12px;background-color:transparent;border:none;outline:none;cursor:pointer}.ant-alert-close-icon .anticon-close{color:#00000073;transition:color .3s}.ant-alert-close-icon .anticon-close:hover{color:#000000bf}.ant-alert-close-text{color:#00000073;transition:color .3s}.ant-alert-close-text:hover{color:#000000bf}.ant-alert-with-description{align-items:flex-start;padding:15px 15px 15px 24px}.ant-alert-with-description.ant-alert-no-icon{padding:15px}.ant-alert-with-description .ant-alert-icon{margin-right:15px;font-size:24px}.ant-alert-with-description .ant-alert-message{display:block;margin-bottom:4px;color:#000000d9;font-size:16px}.ant-alert-message{color:#000000d9}.ant-alert-with-description .ant-alert-description{display:block}.ant-alert.ant-alert-motion-leave{overflow:hidden;opacity:1;transition:max-height .3s cubic-bezier(.78,.14,.15,.86),opacity .3s cubic-bezier(.78,.14,.15,.86),padding-top .3s cubic-bezier(.78,.14,.15,.86),padding-bottom .3s cubic-bezier(.78,.14,.15,.86),margin-bottom .3s cubic-bezier(.78,.14,.15,.86)}.ant-alert.ant-alert-motion-leave-active{max-height:0;margin-bottom:0!important;padding-top:0;padding-bottom:0;opacity:0}.ant-alert-banner{margin-bottom:0;border:0;border-radius:0}.ant-alert.ant-alert-rtl{direction:rtl}.ant-alert-rtl .ant-alert-icon{margin-right:auto;margin-left:8px}.ant-alert-rtl .ant-alert-action,.ant-alert-rtl .ant-alert-close-icon{margin-right:8px;margin-left:auto}.ant-alert-rtl.ant-alert-with-description{padding-right:24px;padding-left:15px}.ant-alert-rtl.ant-alert-with-description .ant-alert-icon{margin-right:auto;margin-left:15px}.access-mode-message[data-v-f96eb6be]{display:flex;flex-direction:row;align-items:center}.access-mode-message a[data-v-f96eb6be]{margin-left:16px}.container[data-v-f96eb6be]{padding:20px;background-color:var(--zp-secondary-background);height:100%;overflow:auto}.header[data-v-f96eb6be]{display:flex;justify-content:space-between;align-items:center}.header h1[data-v-f96eb6be]{font-size:28px;font-weight:700;color:var(--zp-primary);margin:0}.last-record[data-v-f96eb6be]{margin-left:16px;font-size:14px;color:var(--zp-secondary)}.last-record a[data-v-f96eb6be]{text-decoration:none;color:var(--zp-secondary)}.last-record a[data-v-f96eb6be]:hover{color:var(--zp-primary)}.content[data-v-f96eb6be]{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));grid-gap:20px;margin-top:16px}.feature-item[data-v-f96eb6be]{background-color:var(--zp-primary-background);border-radius:8px;box-shadow:0 1px 2px #0000001a;padding:20px}.feature-item ul[data-v-f96eb6be]{list-style:none;padding:4px;max-height:70vh;overflow-y:auto}.feature-item.recent .title[data-v-f96eb6be]{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px}.feature-item.recent .title h2[data-v-f96eb6be]{margin:0}.feature-item .item[data-v-f96eb6be]{margin-bottom:10px;padding:4px 8px;display:flex;align-items:center}.feature-item .item.rem[data-v-f96eb6be]{display:flex;align-items:center;justify-content:space-between}.feature-item .item[data-v-f96eb6be]:hover{background:var(--zp-secondary-background);border-radius:4px;color:var(--primary-color);cursor:pointer}.feature-item .icon[data-v-f96eb6be]{margin-right:8px}.feature-item h2[data-v-f96eb6be]{margin-top:0;margin-bottom:20px;font-size:20px;font-weight:700;color:var(--zp-primary)}.text[data-v-f96eb6be]{flex:1;font-size:16px}

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/gridView-5f202d15.js vendored Normal file
View File

@ -0,0 +1 @@
import{u as v,b as w,F as y,g as b}from"./FileItem-3695dcaf.js";import{d as k,k as h,$ as x,b0 as D,b4 as F,o as I,y as C,c,n as E,r as s,L as V,c1 as z,X as B}from"./index-8897613b.js";import"./db-afd72581.js";import"./shortcut-902028a8.js";const S=k({__name:"gridView",props:{tabIdx:{},paneIdx:{},id:{},removable:{type:Boolean},allowDragAndDrop:{type:Boolean},files:{},paneKey:{}},setup(r){const l=r,p=h(),{stackViewEl:d}=v().toRefs(),{itemSize:i,gridItems:m,cellWidth:u}=w(),t=x(l.files??[]),f=async e=>{const o=z(e);l.allowDragAndDrop&&o&&t.value.push(...o.nodes)},_=e=>{t.value.splice(e,1)},g=e=>({id:e.name,count:0,display_name:null,type:"temp",...e});return D(()=>{p.pageFuncExportMap.set(l.paneKey,{getFiles:()=>F(t.value),setFiles:e=>t.value=e})}),(e,o)=>(I(),C("div",{class:"container",ref_key:"stackViewEl",ref:d,onDrop:f},[c(s(b),{ref:"scroller",class:"file-list",items:t.value.slice(),"item-size":s(i).first,"key-field":"fullpath","item-secondary-size":s(i).second,gridItems:s(m)},{default:E(({item:a,index:n})=>[c(y,{idx:n,file:a,"cell-width":s(u),"enable-close-icon":l.removable,onCloseIconClick:R=>_(n),"full-screen-preview-image-url":s(V)(a),tags:a==null?void 0:a.tags.map(g),"enable-right-click-menu":!1},null,8,["idx","file","cell-width","enable-close-icon","onCloseIconClick","full-screen-preview-image-url","tags"])]),_:1},8,["items","item-size","item-secondary-size","gridItems"])],544))}});const H=B(S,[["__scopeId","data-v-f022c26b"]]);export{H as default};

1
vue/dist/assets/gridView-fffa1abe.css vendored Normal file
View File

@ -0,0 +1 @@
.container[data-v-f022c26b]{background:var(--zp-secondary-background);height:100%;overflow:auto;display:flex;flex-direction:column}.container .actions-panel[data-v-f022c26b]{padding:8px;background-color:var(--zp-primary-background)}.container .file-list[data-v-f022c26b]{flex:1;list-style:none;padding:8px;height:var(--pane-max-height);width:100%}.container .file-list .hint[data-v-f022c26b]{text-align:center;font-size:2em;padding:30vh 128px 0}

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

@ -0,0 +1 @@
import{$ as q,bO as D,bd as E,aC as P}from"./index-8897613b.js";import{h as $,u as z,b as G,f as O,c as Q,d as R,e as V,i as _}from"./FileItem-3695dcaf.js";const L=()=>{const e=q(),c=D(),l=$(),{stackViewEl:u,multiSelectedIdxs:r,stack:m,scroller:n}=z({images:e}).toRefs(),{itemSize:v,gridItems:d,cellWidth:g}=G(),{showMenuIdx:f}=O(),{onFileDragStart:I,onFileDragEnd:p}=Q(),{showGenInfo:h,imageGenInfo:w,q:x,onContextMenuClick:o,onFileItemClick:S}=R({openNext:E}),{previewIdx:C,previewing:F,onPreviewVisibleChange:b,previewImgMove:k,canPreview:M}=V(),y=async(s,t,a)=>{m.value=[{curr:"",files:e.value}],await o(s,t,a)};_("removeFiles",async({paths:s})=>{var t;e.value=(t=e.value)==null?void 0:t.filter(a=>!s.includes(a.fullpath))});const i=()=>{const s=n.value;if(s&&e.value){const t=e.value.slice(Math.max(s.$_startIndex-10,0),s.$_endIndex+10).map(a=>a.fullpath);l.fetchImageTags(t)}},T=P(i,300);return{scroller:n,queue:c,images:e,onContextMenuClickU:y,stackViewEl:u,previewIdx:C,previewing:F,onPreviewVisibleChange:b,previewImgMove:k,canPreview:M,itemSize:v,gridItems:d,showGenInfo:h,imageGenInfo:w,q:x,onContextMenuClick:o,onFileItemClick:S,showMenuIdx:f,multiSelectedIdxs:r,onFileDragStart:I,onFileDragEnd:p,cellWidth:g,onScroll:T,updateImageTag:i}};export{L as u};

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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

190
vue/dist/assets/index-8897613b.js vendored Normal file

File diff suppressed because one or more lines are too long

12
vue/dist/assets/numInput-7aa4d2a1.js vendored Normal file

File diff suppressed because one or more lines are too long

1
vue/dist/assets/shortcut-902028a8.js vendored Normal file
View File

@ -0,0 +1 @@
import{cL as s}from"./index-8897613b.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};

1
vue/dist/assets/stackView-3a13f62d.js vendored Normal file

File diff suppressed because one or more lines are too long

2
vue/dist/index.html vendored
View File

@ -9,6 +9,8 @@
<title>Infinite Image Browsing</title>
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-594b377e.js"></script>
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-4cc43e92.css">
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-8897613b.js"></script>
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-6369b2e2.css">
</head>
<body>

View File

@ -10,6 +10,7 @@ import { refreshTauriConf, tauriConf } from './util/tauriAppConf'
import { openModal } from './taurilaunchModal'
import { isTauri } from './util/env'
import { delay } from 'vue3-ts-util'
import { exportFn } from './defineExportFunc'
const globalStore = useGlobalStore()
const queue = createReactiveQueue()
@ -21,6 +22,7 @@ useGlobalEventListen('updateGlobalSetting', async () => {
globalStore.conf = resp
const r = await getQuickMovePaths(resp)
globalStore.quickMovePaths = r.filter((v) => v?.dir?.trim?.())
exportFn(globalStore)
resolveQueryActions(globalStore)
})

View File

@ -5,7 +5,7 @@ import { PageCursor } from 'vue3-ts-util'
export interface Tag {
name: string
id: number
id: number | string
display_name: string | null
type: string
count: number
@ -31,12 +31,12 @@ export const getExpiredDirs = async () => {
export const updateImageData = async () => {
await axiosInst.value.post('/db/update_image_data', {}, { timeout: Infinity })
}
type TagId = number | string
export interface MatchImageByTagsReq {
and_tags: number[]
or_tags: number[]
not_tags: number[]
folder_paths_str?: string
and_tags: TagId[]
or_tags: TagId[]
not_tags: TagId[]
}
export const getImagesByTags = async (req: MatchImageByTagsReq, cursor: string) => {
@ -56,16 +56,16 @@ export const addCustomTag = async (req: { tag_name: string }) => {
return resp.data as Tag
}
export const toggleCustomTagToImg = async (req: { tag_id: number; img_path: string }) => {
export const toggleCustomTagToImg = async (req: { tag_id: TagId; img_path: string }) => {
const resp = await axiosInst.value.post('/db/toggle_custom_tag_to_img', req)
return resp.data as { is_remove: boolean }
}
export const removeCustomTag = async (req: { tag_id: number }) => {
export const removeCustomTag = async (req: { tag_id: TagId }) => {
await axiosInst.value.post('/db/remove_custom_tag', req)
}
export const removeCustomTagToImg = async (req: { tag_id: number; img_id: number }) => {
export const removeCustomTagToImg = async (req: { tag_id: TagId; img_id: TagId }) => {
await axiosInst.value.post('/db/add_custom_tag_from_img', req)
}

View File

@ -90,6 +90,7 @@ export interface GlobalConf {
extra_paths: ExtraPathModel[]
enable_access_control: boolean
launch_mode: 'server' | 'sd'
export_fe_fn: boolean
}
export const getGlobalSetting = async () => {
@ -111,7 +112,8 @@ export const genInfoCompleted = async () => {
}
export const getImageGenerationInfo = async (path: string) => {
return (await axiosInst.value.get(`/image_geninfo?path=${encodeURIComponent(path)}`)).data as string
return (await axiosInst.value.get(`/image_geninfo?path=${encodeURIComponent(path)}`))
.data as string
}
export const getImageGenerationInfoBatch = async (paths: string[]) => {

View File

@ -26,13 +26,14 @@ const props = withDefaults(
showMenuIdx?: number
cellWidth: number
fullScreenPreviewImageUrl?: string
enableRightClickMenu?: boolean,
enableCloseIcon?: boolean
isSelectedMutilFiles?: boolean
genDiffToPrevious?: GenDiffInfo
genDiffToNext?: GenDiffInfo
genInfo?: string
enableChangeIndicator?: boolean
enableRightClickMenu: boolean,
enableCloseIcon: boolean,
tags?: Tag[]
}>(),
{ selected: false, enableRightClickMenu: true, enableCloseIcon: false, genDiffToNext: () => ({
empty: true,
@ -131,7 +132,7 @@ const taggleLikeTag = () => {
onVisibleChange: (v: boolean, lv: boolean) => emit('previewVisibleChange', v, lv)
}" />
<div class="tags-container" v-if="customTags && cellWidth > 128">
<a-tag v-for="tag in customTags" :key="tag.id" :color="tagStore.getColor(tag.name)">
<a-tag v-for="tag in tags ?? customTags" :key="tag.id" :color="tagStore.getColor(tag.name)">
{{ tag.name }}
</a-tag>
</div>

View File

@ -0,0 +1,73 @@
import { GridViewFile, TabPane, useGlobalStore } from './store/useGlobalStore'
import { Dict, globalEvents } from './util'
import { uniqueId } from 'lodash-es'
export const exportFn = async (g: ReturnType<typeof useGlobalStore>) => {
if (!g.conf?.export_fe_fn) {
return
}
const insertTabPane = ({
tabIdx = 0,
paneIdx = 0,
pane
}: {
tabIdx?: number
paneIdx?: number
pane: TabPane
}) => {
const tab = g.tabList[tabIdx]
if (!pane.key) {
;(pane as any).key = uniqueId()
}
tab.panes.splice(paneIdx, 0, pane)
tab.key = pane.key
return {
key: pane.key,
ref: getPageRef(pane.key)
}
}
define({
insertTabPane,
getTabList: () => g.tabList,
getPageRef,
createGridViewFile(path: string, tags:string[] = []): GridViewFile {
return {
name: path.split(/[/\\]/).pop() ?? '',
size: '-',
bytes: 0,
type: 'file',
created_time: '',
date: '',
fullpath: path,
tags: tags.map(v => ({ name: v })),
is_under_scanned_path: true
}
}
})
function getPageRef (key: string) {
return new Proxy({}, {
get(_target, p, _receiver) {
if (p === 'close') {
const tabIdx = g.tabList.findIndex(v => v.panes.some(v => v.key === key))
return () => globalEvents.emit('closeTabPane', tabIdx , key)
}
return g.pageFuncExportMap.get(key)?.[p as string]
},
})
}
function define(funcs: Dict<(...args: any[]) => any>) {
const w = window as any
for (const key in funcs) {
w[key] = (...args: any) => {
return funcs[key](...args)
}
}
}
}

View File

@ -4,7 +4,7 @@ import { Splitpanes, Pane } from 'splitpanes'
import 'splitpanes/dist/splitpanes.css'
import { useGlobalStore, type TabPane } from '@/store/useGlobalStore'
import { defineAsyncComponent, watch, ref, nextTick } from 'vue'
import { globalEvents, asyncCheck } from '@/util'
import { globalEvents, asyncCheck, useGlobalEventListen } from '@/util'
import { debounce, uniqueId } from 'lodash-es'
import edgeTrigger from './edgeTrigger.vue'
import { t } from '@/i18n'
@ -24,7 +24,8 @@ const compMap: Record<TabPane['type'], ReturnType<typeof defineAsyncComponent>>
'tag-search': defineAsyncComponent(() => import('@/page/TagSearch/TagSearch.vue')),
'fuzzy-search': defineAsyncComponent(() => import('@/page/TagSearch/SubstrSearch.vue')),
'img-sli': defineAsyncComponent(() => import('@/page/ImgSli/ImgSliPagePane.vue')),
'batch-download': defineAsyncComponent(() => import('@/page/batchDownload/batchDownload.vue'))
'batch-download': defineAsyncComponent(() => import('@/page/batchDownload/batchDownload.vue')),
'grid-view': defineAsyncComponent(() => import('@/page/gridView/gridView.vue'))
}
const onEdit = (idx: number, targetKey: any, action: string) => {
const tab = global.tabList[idx]
@ -48,6 +49,8 @@ const onEdit = (idx: number, targetKey: any, action: string) => {
}
}
}
useGlobalEventListen('closeTabPane', (tabIdx, key) => onEdit(tabIdx, key, 'del'))
const container = ref<HTMLDivElement>()
watch(
() => global.tabList,
@ -102,7 +105,7 @@ watch(useDocumentVisibility(), v => v && emitReturnToIIB())
<edge-trigger :tabIdx="tabIdx">
<a-tabs type="editable-card" v-model:activeKey="tab.key" @edit="(key:any, act:any) => onEdit(tabIdx, key, act)">
<a-tab-pane v-for="(pane, paneIdx) in tab.panes" :key="pane.key" :tab="pane.name" class="pane">
<component :is="compMap[pane.type]" :tabIdx="tabIdx" :paneIdx="paneIdx" v-bind="pane" />
<component :is="compMap[pane.type]" :tabIdx="tabIdx" :paneKey="pane.key" :paneIdx="paneIdx" v-bind="pane" />
</a-tab-pane>
</a-tabs>
</edge-trigger>

View File

@ -24,6 +24,7 @@ const compCnMap: Partial<Record<TabPane['type'], string>> = {
const createPane = (type: TabPane['type'], path?: string, walkMode = false) => {
let pane: TabPane
switch (type) {
case 'grid-view':
case 'tag-search-matched-image-grid':
case 'img-sli':
return

View File

@ -105,7 +105,7 @@ const onAddTagBtnSubmit = async () => {
addTagName.value = ''
addInputing.value = false
}
const onTagRemoveClick = (tagId: number) => {
const onTagRemoveClick = (tagId: number|string) => {
Modal.confirm({
title: t('confirmDelete'),
async onOk () {

View File

@ -0,0 +1,89 @@
<script lang="ts" setup>
// @ts-ignore
import { RecycleScroller } from '@zanllp/vue-virtual-scroller'
import '@zanllp/vue-virtual-scroller/dist/vue-virtual-scroller.css'
import FileItem from '@/components/FileItem.vue'
import { useFilesDisplay, useHookShareState } from '@/page/fileTransfer/hook'
import { getFileTransferDataFromDragEvent, toRawFileUrl } from '@/util/file'
import { ref, watchEffect, toRaw } from 'vue'
import { GridViewFile, GridViewFileTag, useGlobalStore } from '@/store/useGlobalStore'
import { Tag } from '@/api/db'
const g = useGlobalStore()
const { stackViewEl } = useHookShareState().toRefs()
const { itemSize, gridItems, cellWidth } = useFilesDisplay()
const props = defineProps<{
tabIdx: number
paneIdx: number
id: string,
removable?: boolean
allowDragAndDrop?: boolean,
files: GridViewFile[]
paneKey: string
}>()
const files = ref(props.files ?? [])
const onDrop = async (e: DragEvent) => {
const data = getFileTransferDataFromDragEvent(e)
if (props.allowDragAndDrop && data) {
files.value.push(...data.nodes)
}
}
const onDeleteClick = (idx: number) => {
files.value.splice(idx, 1)
}
const tagConvert = (tag: GridViewFileTag): Tag => ({ id: tag.name, count: 0, display_name: null, type: 'temp', ...tag })
watchEffect(() => {
g.pageFuncExportMap.set(props.paneKey, {
getFiles: () => toRaw(files.value),
setFiles: (_files: GridViewFile[]) => files.value = _files
})
})
</script>
<template>
<div class="container" ref="stackViewEl" @drop="onDrop">
<RecycleScroller ref="scroller" class="file-list" :items="files.slice()" :item-size="itemSize.first"
key-field="fullpath" :item-secondary-size="itemSize.second" :gridItems="gridItems">
<template v-slot="{ item: file, index: idx }">
<file-item :idx="idx" :file="file" :cell-width="cellWidth" :enable-close-icon="props.removable"
@close-icon-click="onDeleteClick(idx)" :full-screen-preview-image-url="toRawFileUrl(file)"
:tags="file?.tags.map(tagConvert)" :enable-right-click-menu="false" />
</template>
</RecycleScroller>
</div>
</template>
<style scoped lang="scss">
.container {
background: var(--zp-secondary-background);
height: 100%;
overflow: auto;
display: flex;
flex-direction: column;
.actions-panel {
padding: 8px;
background-color: var(--zp-primary-background);
}
.file-list {
flex: 1;
list-style: none;
padding: 8px;
height: var(--pane-max-height);
width: 100%;
.hint {
text-align: center;
font-size: 2em;
padding: 30vh 128px 0;
}
}
}
</style>

View File

@ -1,16 +1,16 @@
import type { GlobalConf } from '@/api'
import type { MatchImageByTagsReq } from '@/api/db'
import type { MatchImageByTagsReq, Tag } from '@/api/db'
import { FileNodeInfo } from '@/api/files'
import { i18n, t } from '@/i18n'
import { getPreferredLang } from '@/i18n'
import { SortMethod } from '@/page/fileTransfer/fileSort'
import type { getQuickMovePaths } from '@/page/taskRecord/autoComplete'
import { type Dict, type ReturnTypeAsync } from '@/util'
import { usePreferredDark } from '@vueuse/core'
import { cloneDeep, uniqueId } from 'lodash-es'
import { defineStore } from 'pinia'
import { VNode, computed, onMounted, reactive, toRaw, watch } from 'vue'
import { ref } from 'vue'
import { WithRequired } from 'vue3-ts-util'
interface TabPaneBase {
name: string | VNode
@ -21,7 +21,32 @@ interface TabPaneBase {
interface OtherTabPane extends TabPaneBase {
type: 'empty' | 'global-setting' | 'tag-search' | 'batch-download'
}
// logDetailId
export type GridViewFileTag = WithRequired<Partial<Tag>, 'name'>;
export interface GridViewFile extends FileNodeInfo {
/**
* Tags for displaying the file. The 'name' property is required,
* while the other properties are optional.
*/
tags?: GridViewFileTag[];
}
/**
* A tab pane that displays files in a grid view.
*/
interface GridViewTabPane extends TabPaneBase {
type: 'grid-view'
/**
* Indicates whether the files in the grid view can be deleted.
*/
removable?: boolean
/**
* Indicates whether files can be dragged and dropped from other pages into the grid view.
*/
allowDragAndDrop?: boolean,
files: GridViewFile[]
}
interface TagSearchMatchedImageGridTabPane extends TabPaneBase {
type: 'tag-search-matched-image-grid'
@ -41,17 +66,7 @@ export interface FileTransferTabPane extends TabPaneBase {
stackKey?: string
}
export interface TagSearchTabPane extends TabPaneBase {
type: 'tag-search'
searchScope?: string
}
export interface FuzzySearchTabPane extends TabPaneBase {
type: 'fuzzy-search'
searchScope?: string
}
export type TabPane = FileTransferTabPane | OtherTabPane | TagSearchMatchedImageGridTabPane | ImgSliTabPane | TagSearchTabPane | FuzzySearchTabPane
export type TabPane = FileTransferTabPane | OtherTabPane | TagSearchMatchedImageGridTabPane | ImgSliTabPane
/**
* This interface represents a tab, which contains an array of panes, an ID, and a key
@ -94,7 +109,7 @@ export const useGlobalStore = defineStore(
() => {
const conf = ref<GlobalConf>()
const quickMovePaths = ref([] as ReturnTypeAsync<typeof getQuickMovePaths>)
const enableThumbnail = ref(true)
const gridThumbnailResolution = ref(512)
const defaultSortingMethod = ref(SortMethod.CREATED_TIME_DESC)
@ -158,7 +173,6 @@ export const useGlobalStore = defineStore(
}
}
const lang = ref(getPreferredLang())
watch(lang, (v) => (i18n.global.locale.value = v as any))
@ -170,27 +184,17 @@ export const useGlobalStore = defineStore(
})
const pathAliasMap = computed((): Dict<string> => {
const keys = ['outdir_extras_samples','outdir_save','outdir_txt2img_samples',
'outdir_img2img_samples','outdir_img2img_grids','outdir_txt2img_grids']
const res = quickMovePaths.value.filter((v) => keys.includes(v.key)).map(v => [v.zh, v.dir])
const keys = [
'outdir_extras_samples',
'outdir_save',
'outdir_txt2img_samples',
'outdir_img2img_samples',
'outdir_img2img_grids',
'outdir_txt2img_grids'
]
const res = quickMovePaths.value.filter((v) => keys.includes(v.key)).map((v) => [v.zh, v.dir])
return Object.fromEntries(res)
})
const ignoredConfirmActions = reactive<Record<ActionConfirmRequired, boolean>>({ deleteOneOnly: false })
const dark = usePreferredDark()
const computedTheme = computed(() => {
const getParDark = () => {
try {
return parent.location.search.includes('theme=dark') // sd-webui的
} catch (error) {
return false
}
}
const isDark = darkModeControl.value === 'auto' ? (dark.value || getParDark()) : (darkModeControl.value === 'dark')
return isDark ? 'dark' : 'light'
})
return {
computedTheme,
darkModeControl,
@ -213,6 +217,7 @@ export const useGlobalStore = defineStore(
onlyFoldersAndImages: ref(true),
fullscreenPreviewInitialUrl: ref(''),
shortcut,
pageFuncExportMap,
dontShowAgain: ref(false),
dontShowAgainNewImgOpts: ref(false),
ignoredConfirmActions

View File

@ -100,6 +100,7 @@ export const { useEventListen: useGlobalEventListen, eventEmitter: globalEvents
returnToIIB(): void
updateGlobalSetting(): void
searchIndexExpired(): void
closeTabPane(tabIdx: number, key: string): void
}>()
type AsyncFunction<T> = (...args: any[]) => Promise<T>

72
vue/usage.md Normal file
View File

@ -0,0 +1,72 @@
```python
AppUtils(base = "/foo", export_fn_fe=True).wrap_app(app)
```
Add a container for IIB
```diff
+ gr.HTML("error", elem_id="bar_iib_container")
- gr.HTML("error", elem_id="infinite_image_browsing_container_wrapper")
```
Load index.js on the browser side.
```js
const jscodeResp = await fetch("/file?path=/path/to/your/submodue-iib/index.js") // fake api
const jsText = await jscodeResp.text()
const js = jsText
.replace("__iib_root_container__", "'#bar_iib_container'")
.replace("__iib_should_maximize__", "false")
.replace(/\/infinite_image_browsing/g, "/foo")
eval(js)
```
```js
const iib = gradioApp().querySelector('#bar_iib_container iframe').contentWindow
const { insertTabPane, getTabList, getPageRef, createGridViewFile: f } = iib
// The createGridViewFile function is a helper function that simplifies the creation of a FileNodeInfo object.
const files = [
// Create an array of files with their corresponding tags.
f('/path/to/img/1', ['tag1', 'tag2']),
f('/path/to/img/2', ['tag3', 'tag4', 'tag6']),
f('/path/to/img/3', ['tag2', 'tag5']),
f('/path/to/img/4', ['tag1', 'tag2'])
]
// Insert a new tab pane of grid view type and assign it to the gridView variable.
const gridView = insertTabPane({
// Optional parameters for tab index and pane index.
tabIdx: 0,
paneIdx: 0,
pane: {
type: 'grid-view', // Other types are also available, see https://github.com/zanllp/sd-webui-infinite-image-browsing/tree/main/vue/src/store/useGlobalStore.ts#L15
name: 'Grid View 1',
removable: true, // Optional parameter to allow the files to be removed, default is false.
allowDragAndDrop: true, // Optional parameter to allow drag and drop, default is false.
files // Use the files array created earlier for this pane.
}
})
// Retrieve the files from the gridView pane and set them back to the same pane.
const files = gridView.ref.getFiles()
gridView.ref.setFiles(files)
// Get the tab list
const tabList = getTabList()
tabList[0].panes.key
// Get the file list from the first pane of the first tab.
const pane = tabList[0].panes[0]
getPageRef(pane.key).getFiles()
// Insert a new tab pane of local type with the specified directory path.
const localDirPane = insertTabPane({
pane: {
type: 'local',
path: 'E:/_归档/green'
}
})
localDirPane.ref.close() // Closes the newly created tab pane
```
To learn more information, you can refer to the type definition in the following file: https://github.com/zanllp/sd-webui-infinite-image-browsing/tree/main/vue/src/store/useGlobalStore.ts#L15 and this file: [define](./src//defineExportFunc.ts).