fixed loc

pull/652/head
zanllp 2024-06-06 04:25:23 +08:00
parent b9f70a2a06
commit f1f59dee74
63 changed files with 1630 additions and 1449 deletions

View File

@ -13,8 +13,8 @@ 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-f08bcee4.js"></script>
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-b5fcdaa7.css">
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-db6e6f1f.js"></script>
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-daea5cd8.css">
</head>
<body>

View File

@ -625,6 +625,7 @@ class Folder:
class ExtraPathType(Enum):
scanned = "scanned"
scanned_fixed = "scanned-fixed"
walk = "walk"
cli_only = "cli_access_only"
@ -638,7 +639,7 @@ class ExtraPath:
def save(self, conn):
type_str = '+'.join(self.types)
for type in self.types:
assert type in [ExtraPathType.walk.value, ExtraPathType.scanned.value]
assert type in [ExtraPathType.walk.value, ExtraPathType.scanned.value, ExtraPathType.scanned_fixed.value]
with closing(conn.cursor()) as cur:
cur.execute(
"INSERT INTO extra_path (path, type, alias) VALUES (?, ?, ?) "

View File

@ -8,6 +8,7 @@ from scripts.iib.tool import (
get_video_type,
is_dev,
get_modified_date,
is_image_file
)
from scripts.iib.parsers.model import ImageGenerationInfo, ImageGenerationParams
from scripts.iib.logger import logger
@ -146,6 +147,8 @@ def build_single_img_idx(conn, file_path, is_rebuild, safe_save_img_tag):
type="size",
)
safe_save_img_tag(ImageTag(img.id, size_tag.id))
media_type_tag = Tag.get_or_create(conn, "Image" if is_image_file(file_path) else "Video", 'Media Type')
safe_save_img_tag(ImageTag(img.id, media_type_tag.id))
for k in [
"Model",
"Sampler",

2
vue/components.d.ts vendored
View File

@ -10,6 +10,8 @@ export {}
declare module '@vue/runtime-core' {
export interface GlobalComponents {
AAlert: typeof import('ant-design-vue/es')['Alert']
ABadge: typeof import('ant-design-vue/es')['Badge']
ABadgeRibbon: typeof import('ant-design-vue/es')['BadgeRibbon']
ABreadcrumb: typeof import('ant-design-vue/es')['Breadcrumb']
ABreadcrumbItem: typeof import('ant-design-vue/es')['BreadcrumbItem']
AButton: typeof import('ant-design-vue/es')['Button']

View File

@ -1 +1 @@
import{d as E,bg as $,v as f,s as M,_ as T,a as c,a0 as W,h as g,c as v,P as z}from"./index-f08bcee4.js";var G=["prefixCls","name","id","type","disabled","readonly","tabindex","autofocus","value","required"],H={prefixCls:String,name:String,id:String,type:String,defaultChecked:{type:[Boolean,Number],default:void 0},checked:{type:[Boolean,Number],default:void 0},disabled:Boolean,tabindex:{type:[Number,String]},readonly:Boolean,autofocus:Boolean,value:z.any,required:Boolean};const L=E({compatConfig:{MODE:3},name:"Checkbox",inheritAttrs:!1,props:$(H,{prefixCls:"rc-checkbox",type:"checkbox",defaultChecked:!1}),emits:["click","change"],setup:function(a,d){var t=d.attrs,h=d.emit,x=d.expose,o=f(a.checked===void 0?a.defaultChecked:a.checked),i=f();M(function(){return a.checked},function(){o.value=a.checked}),x({focus:function(){var e;(e=i.value)===null||e===void 0||e.focus()},blur:function(){var e;(e=i.value)===null||e===void 0||e.blur()}});var l=f(),m=function(e){if(!a.disabled){a.checked===void 0&&(o.value=e.target.checked),e.shiftKey=l.value;var r={target:c(c({},a),{},{checked:e.target.checked}),stopPropagation:function(){e.stopPropagation()},preventDefault:function(){e.preventDefault()},nativeEvent:e};a.checked!==void 0&&(i.value.checked=!!a.checked),h("change",r),l.value=!1}},C=function(e){h("click",e),l.value=e.shiftKey};return function(){var n,e=a.prefixCls,r=a.name,s=a.id,p=a.type,b=a.disabled,K=a.readonly,P=a.tabindex,B=a.autofocus,S=a.value,N=a.required,_=T(a,G),q=t.class,D=t.onFocus,j=t.onBlur,w=t.onKeydown,A=t.onKeypress,F=t.onKeyup,y=c(c({},_),t),O=Object.keys(y).reduce(function(k,u){return(u.substr(0,5)==="aria-"||u.substr(0,5)==="data-"||u==="role")&&(k[u]=y[u]),k},{}),R=W(e,q,(n={},g(n,"".concat(e,"-checked"),o.value),g(n,"".concat(e,"-disabled"),b),n)),V=c(c({name:r,id:s,type:p,readonly:K,disabled:b,tabindex:P,class:"".concat(e,"-input"),checked:!!o.value,autofocus:B,value:S},O),{},{onChange:m,onClick:C,onFocus:D,onBlur:j,onKeydown:w,onKeypress:A,onKeyup:F,required:N});return v("span",{class:R},[v("input",c({ref:i},V),null),v("span",{class:"".concat(e,"-inner")},null)])}}});export{L as V};
import{d as E,bo as $,r as f,k as M,_ as T,a as c,ai as W,h as g,c as v,P as z}from"./index-db6e6f1f.js";var G=["prefixCls","name","id","type","disabled","readonly","tabindex","autofocus","value","required"],H={prefixCls:String,name:String,id:String,type:String,defaultChecked:{type:[Boolean,Number],default:void 0},checked:{type:[Boolean,Number],default:void 0},disabled:Boolean,tabindex:{type:[Number,String]},readonly:Boolean,autofocus:Boolean,value:z.any,required:Boolean};const L=E({compatConfig:{MODE:3},name:"Checkbox",inheritAttrs:!1,props:$(H,{prefixCls:"rc-checkbox",type:"checkbox",defaultChecked:!1}),emits:["click","change"],setup:function(a,d){var t=d.attrs,h=d.emit,m=d.expose,o=f(a.checked===void 0?a.defaultChecked:a.checked),i=f();M(function(){return a.checked},function(){o.value=a.checked}),m({focus:function(){var e;(e=i.value)===null||e===void 0||e.focus()},blur:function(){var e;(e=i.value)===null||e===void 0||e.blur()}});var l=f(),x=function(e){if(!a.disabled){a.checked===void 0&&(o.value=e.target.checked),e.shiftKey=l.value;var r={target:c(c({},a),{},{checked:e.target.checked}),stopPropagation:function(){e.stopPropagation()},preventDefault:function(){e.preventDefault()},nativeEvent:e};a.checked!==void 0&&(i.value.checked=!!a.checked),h("change",r),l.value=!1}},C=function(e){h("click",e),l.value=e.shiftKey};return function(){var n,e=a.prefixCls,r=a.name,s=a.id,p=a.type,b=a.disabled,K=a.readonly,P=a.tabindex,B=a.autofocus,S=a.value,N=a.required,_=T(a,G),q=t.class,D=t.onFocus,j=t.onBlur,w=t.onKeydown,A=t.onKeypress,F=t.onKeyup,k=c(c({},_),t),O=Object.keys(k).reduce(function(y,u){return(u.substr(0,5)==="aria-"||u.substr(0,5)==="data-"||u==="role")&&(y[u]=k[u]),y},{}),R=W(e,q,(n={},g(n,"".concat(e,"-checked"),o.value),g(n,"".concat(e,"-disabled"),b),n)),V=c(c({name:r,id:s,type:p,readonly:K,disabled:b,tabindex:P,class:"".concat(e,"-input"),checked:!!o.value,autofocus:B,value:S},O),{},{onChange:x,onClick:C,onFocus:D,onBlur:j,onKeydown:w,onKeypress:A,onKeyup:F,required:N});return v("span",{class:R},[v("input",c({ref:i},V),null),v("span",{class:"".concat(e,"-inner")},null)])}}});export{L as V};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

3
vue/dist/assets/FileItem-e0fb56db.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
import{d as a,o as t,k as s,c as n,ce as _,q as o}from"./index-f08bcee4.js";const c={class:"img-sli-container"},i=a({__name:"ImgSliPagePane",props:{paneIdx:{},tabIdx:{},left:{},right:{}},setup(l){return(e,r)=>(t(),s("div",c,[n(_,{left:e.left,right:e.right},null,8,["left","right"])]))}});const d=o(i,[["__scopeId","data-v-ae3fb9a8"]]);export{d as default};
import{d as a,L as t,N as s,c as n,cv as _,U as o}from"./index-db6e6f1f.js";const c={class:"img-sli-container"},i=a({__name:"ImgSliPagePane",props:{paneIdx:{},tabIdx:{},left:{},right:{}},setup(l){return(e,r)=>(t(),s("div",c,[n(_,{left:e.left,right:e.right},null,8,["left","right"])]))}});const d=o(i,[["__scopeId","data-v-ae3fb9a8"]]);export{d as default};

View File

@ -1 +0,0 @@
import{d as se,s as ne,a3 as oe,r as ie,o as u,k as I,c as s,B as e,A as n,E as R,l as d,G as ae,t as a,m as p,z as V,Q as de,p as k,R as D,U as re,V as ce,X as z,am as ue,an as me,bD as pe,q as ge}from"./index-f08bcee4.js";import{S as ve}from"./index-9cd8e050.js";import{L as fe,R as Ie,f as ke,M as _e}from"./MultiSelectKeep-f1e091a9.js";import{g as he,h as Ce,F as we}from"./FileItem-966f0b1f.js";import{c as Se,u as xe}from"./hook-8919e6ff.js";import{o as be}from"./functionalCallableComp-05bdb498.js";import"./index-846c776c.js";import"./index-23966e66.js";const Me=r=>(ue("data-v-479efe51"),r=r(),me(),r),ye={class:"hint"},Ae={class:"action-bar"},Te=Me(()=>d("div",{style:{padding:"16px 0 512px"}},null,-1)),$e={key:1},Fe={class:"no-res-hint"},Re={class:"hint"},Ve={key:2,class:"preview-switch"},De=se({__name:"MatchedImageGrid",props:{tabIdx:{},paneIdx:{},selectedTagIds:{},id:{}},setup(r){const _=r,g=Se(l=>pe(_.selectedTagIds,l)),{queue:B,images:i,onContextMenuClickU:h,stackViewEl:G,previewIdx:c,previewing:C,onPreviewVisibleChange:E,previewImgMove:w,canPreview:S,itemSize:x,gridItems:N,showGenInfo:m,imageGenInfo:b,q:U,multiSelectedIdxs:v,onFileItemClick:J,scroller:M,showMenuIdx:f,onFileDragStart:L,onFileDragEnd:P,cellWidth:q,onScroll:y,saveAllFileAsJson:K,saveLoadedFileAsJson:O}=xe(g);ne(()=>_.selectedTagIds,async()=>{var l;await g.reset(),await oe(),(l=M.value)==null||l.scrollToItem(0),y()},{immediate:!0});const Q=ie(),{onClearAllSelected:W,onSelectAll:X,onReverseSelect:j}=he();return(l,t)=>{const H=_e,Y=re,Z=ce,A=z,ee=z,te=ve;return u(),I("div",{class:"container",ref_key:"stackViewEl",ref:G},[s(H,{show:!!e(v).length||e(Q).keepMultiSelect,onClearAllSelected:e(W),onSelectAll:e(X),onReverseSelect:e(j)},null,8,["show","onClearAllSelected","onSelectAll","onReverseSelect"]),s(te,{size:"large",spinning:!e(B).isIdle},{default:n(()=>{var T,$;return[s(Z,{visible:e(m),"onUpdate:visible":t[1]||(t[1]=o=>R(m)?m.value=o:null),width:"70vw","mask-closable":"",onOk:t[2]||(t[2]=o=>m.value=!1)},{cancelText:n(()=>[]),default:n(()=>[s(Y,{active:"",loading:!e(U).isIdle},{default:n(()=>[d("div",{style:{width:"100%","word-break":"break-all","white-space":"pre-line","max-height":"70vh",overflow:"auto"},onDblclick:t[0]||(t[0]=o=>e(ae)(e(b)))},[d("div",ye,a(l.$t("doubleClickToCopy")),1),p(" "+a(e(b)),1)],32)]),_:1},8,["loading"])]),_:1},8,["visible"]),d("div",Ae,[s(A,{onClick:e(O)},{default:n(()=>[p(a(l.$t("saveLoadedImageAsJson")),1)]),_:1},8,["onClick"]),s(A,{onClick:e(K)},{default:n(()=>[p(a(l.$t("saveAllAsJson")),1)]),_:1},8,["onClick"])]),(T=e(i))!=null&&T.length?(u(),V(e(Ce),{key:0,ref_key:"scroller",ref:M,class:"file-list",items:e(i),"item-size":e(x).first,"key-field":"fullpath","item-secondary-size":e(x).second,gridItems:e(N),onScroll:e(y)},{after:n(()=>[Te]),default:n(({item:o,index:F})=>[s(we,{idx:F,file:o,"cell-width":e(q),"show-menu-idx":e(f),"onUpdate:showMenuIdx":t[3]||(t[3]=le=>R(f)?f.value=le:null),onDragstart:e(L),onDragend:e(P),onFileItemClick:e(J),"full-screen-preview-image-url":e(i)[e(c)]?e(de)(e(i)[e(c)]):"",selected:e(v).includes(F),onContextMenuClick:e(h),onPreviewVisibleChange:e(E),"is-selected-mutil-files":e(v).length>1},null,8,["idx","file","cell-width","show-menu-idx","onDragstart","onDragend","onFileItemClick","full-screen-preview-image-url","selected","onContextMenuClick","onPreviewVisibleChange","is-selected-mutil-files"])]),_:1},8,["items","item-size","item-secondary-size","gridItems","onScroll"])):e(g).load&&l.selectedTagIds.and_tags.length===1&&!(($=l.selectedTagIds.folder_paths_str)!=null&&$.trim())?(u(),I("div",$e,[d("div",Fe,[d("p",Re,a(l.$t("tagSearchNoResultsMessage")),1),s(ee,{onClick:t[4]||(t[4]=o=>e(be)()),type:"primary"},{default:n(()=>[p(a(l.$t("rebuildImageIndex")),1)]),_:1})])])):k("",!0),e(C)?(u(),I("div",Ve,[s(e(fe),{onClick:t[5]||(t[5]=o=>e(w)("prev")),class:D({disable:!e(S)("prev")})},null,8,["class"]),s(e(Ie),{onClick:t[6]||(t[6]=o=>e(w)("next")),class:D({disable:!e(S)("next")})},null,8,["class"])])):k("",!0)]}),_:1},8,["spinning"]),e(C)&&e(i)&&e(i)[e(c)]?(u(),V(ke,{key:0,file:e(i)[e(c)],idx:e(c),onContextMenuClick:e(h)},null,8,["file","idx","onContextMenuClick"])):k("",!0)],512)}}});const Pe=ge(De,[["__scopeId","data-v-479efe51"]]);export{Pe as default};

View File

@ -0,0 +1 @@
import{d as se,k as ne,al as oe,V as ie,L as u,N as I,c as s,Y as e,X as n,$ as R,O as d,E as ae,R as a,Q as p,W as V,a8 as de,T as k,a9 as D,ab as re,ac as ce,ae as z,aw as ue,ax as me,bL as pe,U as ge}from"./index-db6e6f1f.js";import{S as ve}from"./index-78e8fb0a.js";import{L as fe,R as Ie,f as ke,M as _e}from"./MultiSelectKeep-c82145ae.js";import{c as we,d as Ce,F as he}from"./FileItem-e0fb56db.js";import{c as Se,u as xe}from"./hook-40c4a7de.js";import{a as be}from"./functionalCallableComp-398e1966.js";import"./shortcut-4f133b16.js";import"./Checkbox-b330ff1b.js";import"./index-fab27d40.js";/* empty css */const Me=r=>(ue("data-v-479efe51"),r=r(),me(),r),ye={class:"hint"},Ae={class:"action-bar"},Te=Me(()=>d("div",{style:{padding:"16px 0 512px"}},null,-1)),$e={key:1},Fe={class:"no-res-hint"},Re={class:"hint"},Ve={key:2,class:"preview-switch"},De=se({__name:"MatchedImageGrid",props:{tabIdx:{},paneIdx:{},selectedTagIds:{},id:{}},setup(r){const _=r,g=Se(l=>pe(_.selectedTagIds,l)),{queue:B,images:i,onContextMenuClickU:w,stackViewEl:G,previewIdx:c,previewing:C,onPreviewVisibleChange:L,previewImgMove:h,canPreview:S,itemSize:x,gridItems:N,showGenInfo:m,imageGenInfo:b,q:E,multiSelectedIdxs:v,onFileItemClick:U,scroller:M,showMenuIdx:f,onFileDragStart:J,onFileDragEnd:O,cellWidth:P,onScroll:y,saveAllFileAsJson:K,saveLoadedFileAsJson:q}=xe(g);ne(()=>_.selectedTagIds,async()=>{var l;await g.reset(),await oe(),(l=M.value)==null||l.scrollToItem(0),y()},{immediate:!0});const Q=ie(),{onClearAllSelected:W,onSelectAll:X,onReverseSelect:Y}=we();return(l,t)=>{const j=_e,H=re,Z=ce,A=z,ee=z,te=ve;return u(),I("div",{class:"container",ref_key:"stackViewEl",ref:G},[s(j,{show:!!e(v).length||e(Q).keepMultiSelect,onClearAllSelected:e(W),onSelectAll:e(X),onReverseSelect:e(Y)},null,8,["show","onClearAllSelected","onSelectAll","onReverseSelect"]),s(te,{size:"large",spinning:!e(B).isIdle},{default:n(()=>{var T,$;return[s(Z,{visible:e(m),"onUpdate:visible":t[1]||(t[1]=o=>R(m)?m.value=o:null),width:"70vw","mask-closable":"",onOk:t[2]||(t[2]=o=>m.value=!1)},{cancelText:n(()=>[]),default:n(()=>[s(H,{active:"",loading:!e(E).isIdle},{default:n(()=>[d("div",{style:{width:"100%","word-break":"break-all","white-space":"pre-line","max-height":"70vh",overflow:"auto"},onDblclick:t[0]||(t[0]=o=>e(ae)(e(b)))},[d("div",ye,a(l.$t("doubleClickToCopy")),1),p(" "+a(e(b)),1)],32)]),_:1},8,["loading"])]),_:1},8,["visible"]),d("div",Ae,[s(A,{onClick:e(q)},{default:n(()=>[p(a(l.$t("saveLoadedImageAsJson")),1)]),_:1},8,["onClick"]),s(A,{onClick:e(K)},{default:n(()=>[p(a(l.$t("saveAllAsJson")),1)]),_:1},8,["onClick"])]),(T=e(i))!=null&&T.length?(u(),V(e(Ce),{key:0,ref_key:"scroller",ref:M,class:"file-list",items:e(i),"item-size":e(x).first,"key-field":"fullpath","item-secondary-size":e(x).second,gridItems:e(N),onScroll:e(y)},{after:n(()=>[Te]),default:n(({item:o,index:F})=>[s(he,{idx:F,file:o,"cell-width":e(P),"show-menu-idx":e(f),"onUpdate:showMenuIdx":t[3]||(t[3]=le=>R(f)?f.value=le:null),onDragstart:e(J),onDragend:e(O),onFileItemClick:e(U),"full-screen-preview-image-url":e(i)[e(c)]?e(de)(e(i)[e(c)]):"",selected:e(v).includes(F),onContextMenuClick:e(w),onPreviewVisibleChange:e(L),"is-selected-mutil-files":e(v).length>1},null,8,["idx","file","cell-width","show-menu-idx","onDragstart","onDragend","onFileItemClick","full-screen-preview-image-url","selected","onContextMenuClick","onPreviewVisibleChange","is-selected-mutil-files"])]),_:1},8,["items","item-size","item-secondary-size","gridItems","onScroll"])):e(g).load&&l.selectedTagIds.and_tags.length===1&&!(($=l.selectedTagIds.folder_paths_str)!=null&&$.trim())?(u(),I("div",$e,[d("div",Fe,[d("p",Re,a(l.$t("tagSearchNoResultsMessage")),1),s(ee,{onClick:t[4]||(t[4]=o=>e(be)()),type:"primary"},{default:n(()=>[p(a(l.$t("rebuildImageIndex")),1)]),_:1})])])):k("",!0),e(C)?(u(),I("div",Ve,[s(e(fe),{onClick:t[5]||(t[5]=o=>e(h)("prev")),class:D({disable:!e(S)("prev")})},null,8,["class"]),s(e(Ie),{onClick:t[6]||(t[6]=o=>e(h)("next")),class:D({disable:!e(S)("next")})},null,8,["class"])])):k("",!0)]}),_:1},8,["spinning"]),e(C)&&e(i)&&e(i)[e(c)]?(u(),V(ke,{key:0,file:e(i)[e(c)],idx:e(c),onContextMenuClick:e(w)},null,8,["file","idx","onContextMenuClick"])):k("",!0)],512)}}});const Ke=ge(De,[["__scopeId","data-v-479efe51"]]);export{Ke as default};

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

1
vue/dist/assets/TagSearch-1d48f23d.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

View File

@ -1 +0,0 @@
import{d as v,cf as C,bQ as I,o as i,k as _,l as f,c,A as r,m as h,t as d,B as e,z,Q as B,cg as F,ch as x,X as $,q as R}from"./index-f08bcee4.js";import{u as S,b as A,k as E,F as V,h as T}from"./FileItem-966f0b1f.js";import"./functionalCallableComp-05bdb498.js";import"./index-23966e66.js";import"./index-846c776c.js";const L={class:"actions-panel actions"},N={key:0,class:"file-list"},Q={class:"hint"},U=v({__name:"batchDownload",props:{tabIdx:{},paneIdx:{},id:{}},setup(q){const{stackViewEl:k}=S().toRefs(),{itemSize:p,gridItems:w,cellWidth:b}=A(),n=E(),{selectdFiles:l}=C(n),m=I(),y=async t=>{const s=F(t);s&&n.addFiles(s.nodes)},D=async()=>{m.pushAction(async()=>{const t=await x.value.post("/zip",{paths:l.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=>{l.value.splice(t,1)};return(t,s)=>{const a=$;return i(),_("div",{class:"container",ref_key:"stackViewEl",ref:k,onDrop:y},[f("div",L,[c(a,{onClick:s[0]||(s[0]=o=>e(n).selectdFiles=[])},{default:r(()=>[h(d(t.$t("clear")),1)]),_:1}),c(a,{onClick:D,type:"primary",loading:!e(m).isIdle},{default:r(()=>[h(d(t.$t("zipDownload")),1)]),_:1},8,["loading"])]),e(l).length?(i(),z(e(T),{key:1,ref:"scroller",class:"file-list",items:e(l).slice(),"item-size":e(p).first,"key-field":"fullpath","item-secondary-size":e(p).second,gridItems:e(w)},{default:r(({item:o,index:u})=>[c(V,{idx:u,file:o,"cell-width":e(b),"enable-close-icon":"",onCloseIconClick:H=>g(u),"full-screen-preview-image-url":e(B)(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",Q,d(t.$t("batchDownloaDDragAndDropHint")),1)]))],544)}}});const J=R(U,[["__scopeId","data-v-aab31da2"]]);export{J as default};

View File

@ -0,0 +1 @@
import{d as v,cw as C,bY as I,L as l,N as _,O as f,c,X as r,Q as w,R as d,Y as e,W as F,a8 as x,cb as z,cx as B,ae as R,U as $}from"./index-db6e6f1f.js";import{u as S,a as E,k as V,F as A,d as L}from"./FileItem-e0fb56db.js";import"./functionalCallableComp-398e1966.js";import"./index-fab27d40.js";/* empty css */const N={class:"actions-panel actions"},T={key:0,class:"file-list"},U={class:"hint"},H=v({__name:"batchDownload",props:{tabIdx:{},paneIdx:{},id:{}},setup(O){const{stackViewEl:h}=S().toRefs(),{itemSize:p,gridItems:k,cellWidth:b}=E(),i=V(),{selectdFiles:n}=C(i),u=I(),y=async t=>{const s=z(t);s&&i.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=R;return l(),_("div",{class:"container",ref_key:"stackViewEl",ref:h,onDrop:y},[f("div",N,[c(a,{onClick:s[0]||(s[0]=o=>e(i).selectdFiles=[])},{default:r(()=>[w(d(t.$t("clear")),1)]),_:1}),c(a,{onClick:D,type:"primary",loading:!e(u).isIdle},{default:r(()=>[w(d(t.$t("zipDownload")),1)]),_:1},8,["loading"])]),e(n).length?(l(),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:Q=>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"])):(l(),_("div",T,[f("p",U,d(t.$t("batchDownloaDDragAndDropHint")),1)]))],544)}}});const G=$(H,[["__scopeId","data-v-aab31da2"]]);export{G as default};

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

View File

@ -0,0 +1 @@
import{d4 as b,c as o,A as w,cx as i,cm as y,V as h,ac as c,d5 as x,d6 as O,ae as I,ci as F,a8 as S,v as l,d7 as M,ap as N,t as _,r as f,ad as v,p as k,Q as D,d8 as V}from"./index-db6e6f1f.js";var p=1/0,P=17976931348623157e292;function A(e){if(!e)return e===0?e:0;if(e=b(e),e===p||e===-p){var t=e<0?-1:1;return t*P}return e===e?e:0}function U(e){var t=e==null?0:e.length;return t?e[t-1]:void 0}var j={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M505.7 661a8 8 0 0012.6 0l112-141.7c4.1-5.2.4-12.9-6.3-12.9h-74.1V168c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v338.3H400c-6.7 0-10.4 7.7-6.3 12.9l112 141.8zM878 626h-60c-4.4 0-8 3.6-8 8v154H214V634c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8v198c0 17.7 14.3 32 32 32h684c17.7 0 32-14.3 32-32V634c0-4.4-3.6-8-8-8z"}}]},name:"download",theme:"outlined"};const C=j;function m(e){for(var t=1;t<arguments.length;t++){var n=arguments[t]!=null?Object(arguments[t]):{},a=Object.keys(n);typeof Object.getOwnPropertySymbols=="function"&&(a=a.concat(Object.getOwnPropertySymbols(n).filter(function(r){return Object.getOwnPropertyDescriptor(n,r).enumerable}))),a.forEach(function(r){T(e,r,n[r])})}return e}function T(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}var d=function(t,n){var a=m({},t,n.attrs);return o(w,m({},a,{icon:C}),null)};d.displayName="DownloadOutlined";d.inheritAttrs=!1;const E=d,H=async e=>(await i.value.get("/files",{params:{folder_path:e}})).data,$=async e=>(await i.value.post("/delete_files",{file_paths:e})).data,B=async(e,t,n)=>(await i.value.post("/move_files",{file_paths:e,dest:t,create_dest_folder:n})).data,G=async(e,t,n)=>(await i.value.post("/copy_files",{file_paths:e,dest:t,create_dest_folder:n})).data,R=async e=>{await i.value.post("/mkdirs",{dest_folder:e})},W=e=>{const t=f("");return new Promise(n=>{c.confirm({title:l("inputFolderName"),content:()=>o(v,{value:t.value,"onUpdate:value":a=>t.value=a},null),async onOk(){if(!t.value)return;const a=k(e,t.value);await R(a),n()}})})},L=()=>o("p",{style:{background:"var(--zp-secondary-background)",padding:"8px",borderLeft:"4px solid var(--primary-color)"}},[D("Tips: "),l("multiSelectTips")]),Q=(e,t)=>{const n=y(),a=h(),r=s=>{var u;return!!((u=n.tagMap.get(e.fullpath))!=null&&u.some(g=>g.id===s))};c.confirm({width:"80vw",title:e.name,icon:null,content:()=>o("div",{style:{display:"flex",alignItems:"center",justifyContent:"center",flexDirection:"column"}},[o("video",{style:{maxHeight:x?"80vh":"60vh",maxWidth:"100%",minWidth:"70%"},src:O(e),controls:!0,autoplay:!0},null),o("div",{style:{marginTop:"4px"}},[a.conf.all_custom_tags.map(s=>o("div",{key:s.id,onClick:()=>t==null?void 0:t(s.id),style:{background:r(s.id)?n.getColor(s.name):"var(--zp-primary-background)",color:r(s.id)?"white":n.getColor(s.name),margin:"2px",padding:"2px 16px","border-radius":"4px",display:"inline-block",cursor:"pointer","font-weight":"bold",transition:".5s all ease",border:`2px solid ${n.getColor(s.name)}`,"user-select":"none"}},[s.name]))]),o("div",{class:"actions",style:{marginTop:"16px"}},[o(I,{onClick:()=>F([S(e,!0)])},{icon:o(E,null,null),default:l("download")})])]),maskClosable:!0,wrapClassName:"hidden-antd-btns-modal"})},X=()=>{c.confirm({title:l("confirmRebuildImageIndex"),onOk:async()=>{await M(),N.emit("searchIndexExpired"),_.success(l("rebuildComplete"))}})},Y=e=>{const t=f(e.split(/[\\/]/).pop()??"");return new Promise(n=>{c.confirm({title:l("rename"),content:()=>o(v,{value:t.value,"onUpdate:value":a=>t.value=a},null),async onOk(){if(!t.value)return;const a=await V({path:e,name:t.value});n(a.new_path)}})})};export{L as M,X as a,Y as b,G as c,$ as d,Q as e,H as g,U as l,B as m,W as o,A as t};

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-ae680024.js vendored Normal file
View File

@ -0,0 +1 @@
import{u as w,a as y,F as k,d as x}from"./FileItem-e0fb56db.js";import{d as F,V as b,cm as h,r as D,a_ as I,b2 as C,L as V,N as E,c,X as z,Y as e,a8 as S,cb as B,cy as R,U as A}from"./index-db6e6f1f.js";import"./functionalCallableComp-398e1966.js";import"./index-fab27d40.js";/* empty css */const K=F({__name:"gridView",props:{tabIdx:{},paneIdx:{},id:{},removable:{type:Boolean},allowDragAndDrop:{type:Boolean},files:{},paneKey:{}},setup(p){const o=p,d=b(),{stackViewEl:m}=w().toRefs(),{itemSize:i,gridItems:u,cellWidth:f}=y(),g=h(),s=D(o.files??[]),_=async a=>{const l=B(a);o.allowDragAndDrop&&l&&(s.value=R([...s.value,...l.nodes]))},v=a=>{s.value.splice(a,1)};return I(()=>{d.pageFuncExportMap.set(o.paneKey,{getFiles:()=>C(s.value),setFiles:a=>s.value=a})}),(a,l)=>(V(),E("div",{class:"container",ref_key:"stackViewEl",ref:m,onDrop:_},[c(e(x),{ref:"scroller",class:"file-list",items:s.value.slice(),"item-size":e(i).first,"key-field":"fullpath","item-secondary-size":e(i).second,gridItems:e(u)},{default:z(({item:t,index:r})=>{var n;return[c(k,{idx:r,file:t,"cell-width":e(f),"enable-close-icon":o.removable,onCloseIconClick:N=>v(r),"full-screen-preview-image-url":e(S)(t),"extra-tags":(n=t==null?void 0:t.tags)==null?void 0:n.map(e(g).tagConvert),"enable-right-click-menu":!1},null,8,["idx","file","cell-width","enable-close-icon","onCloseIconClick","full-screen-preview-image-url","extra-tags"])]}),_:1},8,["items","item-size","item-secondary-size","gridItems"])],544))}});const H=A(K,[["__scopeId","data-v-f35f4802"]]);export{H as default};

View File

@ -1 +0,0 @@
import{u as w,b as k,F as y,h as x}from"./FileItem-966f0b1f.js";import{d as h,r as F,c4 as D,v as I,aS as b,aW as C,o as E,k as S,c,A as V,B as e,Q as z,cg as B,ci as A,q as R}from"./index-f08bcee4.js";import"./functionalCallableComp-05bdb498.js";import"./index-23966e66.js";import"./index-846c776c.js";const q=h({__name:"gridView",props:{tabIdx:{},paneIdx:{},id:{},removable:{type:Boolean},allowDragAndDrop:{type:Boolean},files:{},paneKey:{}},setup(p){const o=p,d=F(),{stackViewEl:m}=w().toRefs(),{itemSize:i,gridItems:u,cellWidth:f}=k(),g=D(),s=I(o.files??[]),_=async a=>{const l=B(a);o.allowDragAndDrop&&l&&(s.value=A([...s.value,...l.nodes]))},v=a=>{s.value.splice(a,1)};return b(()=>{d.pageFuncExportMap.set(o.paneKey,{getFiles:()=>C(s.value),setFiles:a=>s.value=a})}),(a,l)=>(E(),S("div",{class:"container",ref_key:"stackViewEl",ref:m,onDrop:_},[c(e(x),{ref:"scroller",class:"file-list",items:s.value.slice(),"item-size":e(i).first,"key-field":"fullpath","item-secondary-size":e(i).second,gridItems:e(u)},{default:V(({item:t,index:r})=>{var n;return[c(y,{idx:r,file:t,"cell-width":e(f),"enable-close-icon":o.removable,onCloseIconClick:K=>v(r),"full-screen-preview-image-url":e(z)(t),"extra-tags":(n=t==null?void 0:t.tags)==null?void 0:n.map(e(g).tagConvert),"enable-right-click-menu":!1},null,8,["idx","file","cell-width","enable-close-icon","onCloseIconClick","full-screen-preview-image-url","extra-tags"])]}),_:1},8,["items","item-size","item-secondary-size","gridItems"])],544))}});const M=R(q,[["__scopeId","data-v-f35f4802"]]);export{M as default};

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

@ -0,0 +1 @@
import{bl as F,r as g,c6 as q,c7 as x,G as A,z,bY as D,bb as G,c8 as N}from"./index-db6e6f1f.js";import{u as L,a as O,b as Q,e as j}from"./FileItem-e0fb56db.js";import{a as H,b as T,c as U}from"./MultiSelectKeep-c82145ae.js";let W=0;const Y=()=>++W,B=(o,c,{dataUpdateStrategy:l="replace"}={})=>{const n=F([""]),u=g(!1),t=g(),a=g(!1);let f=g(-1);const v=new Set,w=e=>{var s;l==="replace"?t.value=e:l==="merge"&&(A((Array.isArray(t.value)||typeof t.value>"u")&&Array.isArray(e),"数据更新策略为合并时仅可用于值为数组的情况"),t.value=[...(s=t==null?void 0:t.value)!==null&&s!==void 0?s:[],...e])},d=e=>x(void 0,void 0,void 0,function*(){if(a.value||u.value&&typeof e>"u")return!1;a.value=!0;const s=Y();f.value=s;try{let r;if(typeof e=="number"){if(r=n[e],typeof r!="string")return!1}else r=n[n.length-1];const p=yield o(r);if(v.has(s))return v.delete(s),!1;w(c(p));const i=p.cursor;if((e===n.length-1||typeof e!="number")&&(u.value=!i.has_next,i.has_next)){const I=i.next_cursor||i.next;A(typeof I=="string"),n.push(I)}}finally{f.value===s&&(a.value=!1)}return!0}),m=()=>{v.add(f.value),a.value=!1},b=(e=!1)=>x(void 0,void 0,void 0,function*(){const{refetch:s,force:r}=typeof e=="object"?e:{refetch:e};r&&m(),A(!a.value),n.splice(0,n.length,""),a.value=!1,t.value=void 0,u.value=!1,s&&(yield d())}),h=()=>({next:()=>x(void 0,void 0,void 0,function*(){if(a.value)throw new Error("不允许同时迭代");return{done:!(yield d()),value:t.value}})});return q({abort:m,load:u,next:d,res:t,loading:a,cursorStack:n,reset:b,[Symbol.asyncIterator]:h,iter:{[Symbol.asyncIterator]:h}})},$=o=>F(B(o,c=>c.files,{dataUpdateStrategy:"merge"})),ee=o=>{const c=F(new Set),l=z(()=>(o.res??[]).filter(y=>!c.has(y.fullpath))),n=D(),{stackViewEl:u,multiSelectedIdxs:t,stack:a,scroller:f}=L({images:l}).toRefs(),{itemSize:v,gridItems:w,cellWidth:d,onScroll:m}=O({fetchNext:()=>o.next()}),{showMenuIdx:b}=Q(),{onFileDragStart:h,onFileDragEnd:e}=H(),{showGenInfo:s,imageGenInfo:r,q:p,onContextMenuClick:i,onFileItemClick:I}=T({openNext:G}),{previewIdx:C,previewing:_,onPreviewVisibleChange:E,previewImgMove:M,canPreview:J}=U(),P=async(y,S,R)=>{a.value=[{curr:"",files:l.value}],await i(y,S,R)};j("removeFiles",async({paths:y})=>{y.forEach(S=>c.add(S))});const k=()=>{N(l.value)};return{images:l,scroller:f,queue:n,iter:o,onContextMenuClickU:P,stackViewEl:u,previewIdx:C,previewing:_,onPreviewVisibleChange:E,previewImgMove:M,canPreview:J,itemSize:v,gridItems:w,showGenInfo:s,imageGenInfo:r,q:p,onContextMenuClick:i,onFileItemClick:I,showMenuIdx:b,multiSelectedIdxs:t,onFileDragStart:h,onFileDragEnd:e,cellWidth:d,onScroll:m,saveLoadedFileAsJson:k,saveAllFileAsJson:async()=>{for(;!o.load;)await o.next();k()}}};export{$ as c,ee as u};

View File

@ -1 +0,0 @@
import{bd as F,v as g,c1 as q,c2 as A,aj as b,ag as D,bQ as N,b3 as Q,c3 as j}from"./index-f08bcee4.js";import{u as z,b as G,f as L,c as O,d as H,e as T,i as U}from"./FileItem-966f0b1f.js";let W=0;const B=()=>++W,K=(o,c,{dataUpdateStrategy:l="replace"}={})=>{const n=F([""]),u=g(!1),t=g(),a=g(!1);let f=g(-1);const v=new Set,w=e=>{var s;l==="replace"?t.value=e:l==="merge"&&(b((Array.isArray(t.value)||typeof t.value>"u")&&Array.isArray(e),"数据更新策略为合并时仅可用于值为数组的情况"),t.value=[...(s=t==null?void 0:t.value)!==null&&s!==void 0?s:[],...e])},d=e=>A(void 0,void 0,void 0,function*(){if(a.value||u.value&&typeof e>"u")return!1;a.value=!0;const s=B();f.value=s;try{let r;if(typeof e=="number"){if(r=n[e],typeof r!="string")return!1}else r=n[n.length-1];const m=yield o(r);if(v.has(s))return v.delete(s),!1;w(c(m));const i=m.cursor;if((e===n.length-1||typeof e!="number")&&(u.value=!i.has_next,i.has_next)){const p=i.next_cursor||i.next;b(typeof p=="string"),n.push(p)}}finally{f.value===s&&(a.value=!1)}return!0}),h=()=>{v.add(f.value),a.value=!1},S=(e=!1)=>A(void 0,void 0,void 0,function*(){const{refetch:s,force:r}=typeof e=="object"?e:{refetch:e};r&&h(),b(!a.value),n.splice(0,n.length,""),a.value=!1,t.value=void 0,u.value=!1,s&&(yield d())}),I=()=>({next:()=>A(void 0,void 0,void 0,function*(){if(a.value)throw new Error("不允许同时迭代");return{done:!(yield d()),value:t.value}})});return q({abort:h,load:u,next:d,res:t,loading:a,cursorStack:n,reset:S,[Symbol.asyncIterator]:I,iter:{[Symbol.asyncIterator]:I}})},Z=o=>F(K(o,c=>c.files,{dataUpdateStrategy:"merge"})),$=o=>{const c=F(new Set),l=D(()=>(o.res??[]).filter(y=>!c.has(y.fullpath))),n=N(),{stackViewEl:u,multiSelectedIdxs:t,stack:a,scroller:f}=z({images:l}).toRefs(),{itemSize:v,gridItems:w,cellWidth:d,onScroll:h}=G({fetchNext:()=>o.next()}),{showMenuIdx:S}=L(),{onFileDragStart:I,onFileDragEnd:e}=O(),{showGenInfo:s,imageGenInfo:r,q:m,onContextMenuClick:i,onFileItemClick:p}=H({openNext:Q}),{previewIdx:C,previewing:_,onPreviewVisibleChange:E,previewImgMove:M,canPreview:J}=T(),P=async(y,x,R)=>{a.value=[{curr:"",files:l.value}],await i(y,x,R)};U("removeFiles",async({paths:y})=>{y.forEach(x=>c.add(x))});const k=()=>{j(l.value)};return{images:l,scroller:f,queue:n,iter:o,onContextMenuClickU:P,stackViewEl:u,previewIdx:C,previewing:_,onPreviewVisibleChange:E,previewImgMove:M,canPreview:J,itemSize:v,gridItems:w,showGenInfo:s,imageGenInfo:r,q:m,onContextMenuClick:i,onFileItemClick:p,showMenuIdx:S,multiSelectedIdxs:t,onFileDragStart:I,onFileDragEnd:e,cellWidth:d,onScroll:h,saveLoadedFileAsJson:k,saveAllFileAsJson:async()=>{for(;!o.load;)await o.next();k()}}};export{Z as c,$ as u};

View File

@ -1 +1 @@
import{d as z,bg as D,Z as A,cb as j,a3 as k,ao as V,cc as B,cd as y,e as $,c as a,_ as T,h as r,a as P,br as M,P as b}from"./index-f08bcee4.js";var O=["class","style"],W=function(){return{prefixCls:String,spinning:{type:Boolean,default:void 0},size:String,wrapperClassName:String,tip:b.any,delay:Number,indicator:b.any}},p=null;function Z(t,n){return!!t&&!!n&&!isNaN(Number(n))}function F(t){var n=t.indicator;p=typeof n=="function"?n:function(){return a(n,null,null)}}const G=z({compatConfig:{MODE:3},name:"ASpin",inheritAttrs:!1,props:D(W(),{size:"default",spinning:!0,wrapperClassName:""}),setup:function(){return{originalUpdateSpinning:null,configProvider:A("configProvider",j)}},data:function(){var n=this.spinning,e=this.delay,i=Z(n,e);return{sSpinning:n&&!i}},created:function(){this.originalUpdateSpinning=this.updateSpinning,this.debouncifyUpdateSpinning(this.$props)},mounted:function(){this.updateSpinning()},updated:function(){var n=this;k(function(){n.debouncifyUpdateSpinning(),n.updateSpinning()})},beforeUnmount:function(){this.cancelExistingSpin()},methods:{debouncifyUpdateSpinning:function(n){var e=n||this.$props,i=e.delay;i&&(this.cancelExistingSpin(),this.updateSpinning=V(this.originalUpdateSpinning,i))},updateSpinning:function(){var n=this.spinning,e=this.sSpinning;e!==n&&(this.sSpinning=n)},cancelExistingSpin:function(){var n=this.updateSpinning;n&&n.cancel&&n.cancel()},renderIndicator:function(n){var e="".concat(n,"-dot"),i=B(this,"indicator");return i===null?null:(Array.isArray(i)&&(i=i.length===1?i[0]:i),y(i)?$(i,{class:e}):p&&y(p())?$(p(),{class:e}):a("span",{class:"".concat(e," ").concat(n,"-dot-spin")},[a("i",{class:"".concat(n,"-dot-item")},null),a("i",{class:"".concat(n,"-dot-item")},null),a("i",{class:"".concat(n,"-dot-item")},null),a("i",{class:"".concat(n,"-dot-item")},null)]))}},render:function(){var n,e,i,o=this.$props,f=o.size,x=o.prefixCls,h=o.tip,d=h===void 0?(n=(e=this.$slots).tip)===null||n===void 0?void 0:n.call(e):h,N=o.wrapperClassName,l=this.$attrs,v=l.class,_=l.style,C=T(l,O),S=this.configProvider,U=S.getPrefixCls,E=S.direction,s=U("spin",x),u=this.sSpinning,I=(i={},r(i,s,!0),r(i,"".concat(s,"-sm"),f==="small"),r(i,"".concat(s,"-lg"),f==="large"),r(i,"".concat(s,"-spinning"),u),r(i,"".concat(s,"-show-text"),!!d),r(i,"".concat(s,"-rtl"),E==="rtl"),r(i,v,!!v),i),m=a("div",P(P({},C),{},{style:_,class:I}),[this.renderIndicator(s),d?a("div",{class:"".concat(s,"-text")},[d]):null]),g=M(this);if(g&&g.length){var c,w=(c={},r(c,"".concat(s,"-container"),!0),r(c,"".concat(s,"-blur"),u),c);return a("div",{class:["".concat(s,"-nested-loading"),N]},[u&&a("div",{key:"loading"},[m]),a("div",{class:w,key:"container"},[g])])}return m}});export{G as S,F as s};
import{d as w,bo as D,ag as A,ct as j,al as k,l as V,cu as B,cc as y,e as $,c as a,_ as T,h as r,a as P,bz as M,P as b}from"./index-db6e6f1f.js";var O=["class","style"],W=function(){return{prefixCls:String,spinning:{type:Boolean,default:void 0},size:String,wrapperClassName:String,tip:b.any,delay:Number,indicator:b.any}},p=null;function q(t,n){return!!t&&!!n&&!isNaN(Number(n))}function G(t){var n=t.indicator;p=typeof n=="function"?n:function(){return a(n,null,null)}}const H=w({compatConfig:{MODE:3},name:"ASpin",inheritAttrs:!1,props:D(W(),{size:"default",spinning:!0,wrapperClassName:""}),setup:function(){return{originalUpdateSpinning:null,configProvider:A("configProvider",j)}},data:function(){var n=this.spinning,e=this.delay,i=q(n,e);return{sSpinning:n&&!i}},created:function(){this.originalUpdateSpinning=this.updateSpinning,this.debouncifyUpdateSpinning(this.$props)},mounted:function(){this.updateSpinning()},updated:function(){var n=this;k(function(){n.debouncifyUpdateSpinning(),n.updateSpinning()})},beforeUnmount:function(){this.cancelExistingSpin()},methods:{debouncifyUpdateSpinning:function(n){var e=n||this.$props,i=e.delay;i&&(this.cancelExistingSpin(),this.updateSpinning=V(this.originalUpdateSpinning,i))},updateSpinning:function(){var n=this.spinning,e=this.sSpinning;e!==n&&(this.sSpinning=n)},cancelExistingSpin:function(){var n=this.updateSpinning;n&&n.cancel&&n.cancel()},renderIndicator:function(n){var e="".concat(n,"-dot"),i=B(this,"indicator");return i===null?null:(Array.isArray(i)&&(i=i.length===1?i[0]:i),y(i)?$(i,{class:e}):p&&y(p())?$(p(),{class:e}):a("span",{class:"".concat(e," ").concat(n,"-dot-spin")},[a("i",{class:"".concat(n,"-dot-item")},null),a("i",{class:"".concat(n,"-dot-item")},null),a("i",{class:"".concat(n,"-dot-item")},null),a("i",{class:"".concat(n,"-dot-item")},null)]))}},render:function(){var n,e,i,o=this.$props,f=o.size,x=o.prefixCls,h=o.tip,d=h===void 0?(n=(e=this.$slots).tip)===null||n===void 0?void 0:n.call(e):h,N=o.wrapperClassName,l=this.$attrs,v=l.class,_=l.style,C=T(l,O),S=this.configProvider,U=S.getPrefixCls,z=S.direction,s=U("spin",x),u=this.sSpinning,E=(i={},r(i,s,!0),r(i,"".concat(s,"-sm"),f==="small"),r(i,"".concat(s,"-lg"),f==="large"),r(i,"".concat(s,"-spinning"),u),r(i,"".concat(s,"-show-text"),!!d),r(i,"".concat(s,"-rtl"),z==="rtl"),r(i,v,!!v),i),m=a("div",P(P({},C),{},{style:_,class:E}),[this.renderIndicator(s),d?a("div",{class:"".concat(s,"-text")},[d]):null]),g=M(this);if(g&&g.length){var c,I=(c={},r(c,"".concat(s,"-container"),!0),r(c,"".concat(s,"-blur"),u),c);return a("div",{class:["".concat(s,"-nested-loading"),N]},[u&&a("div",{key:"loading"},[m]),a("div",{class:I,key:"container"},[g])])}return m}});export{H as S,G as s};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
vue/dist/assets/index-fab27d40.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

2
vue/dist/assets/shortcut-4f133b16.js vendored Normal file

File diff suppressed because one or more lines are too long

1
vue/dist/assets/stackView-0e3d0459.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
.ant-breadcrumb{box-sizing:border-box;margin:0;padding:0;color:#000000d9;font-variant:tabular-nums;line-height:1.5715;list-style:none;font-feature-settings:"tnum";color:#00000073;font-size:14px}.ant-breadcrumb .anticon{font-size:14px}.ant-breadcrumb a{color:#00000073;transition:color .3s}.ant-breadcrumb a:hover{color:#de632f}.ant-breadcrumb>span:last-child{color:#000000d9}.ant-breadcrumb>span:last-child a{color:#000000d9}.ant-breadcrumb>span:last-child .ant-breadcrumb-separator{display:none}.ant-breadcrumb-separator{margin:0 8px;color:#00000073}.ant-breadcrumb-link>.anticon+span,.ant-breadcrumb-link>.anticon+a{margin-left:4px}.ant-breadcrumb-overlay-link>.anticon{margin-left:4px}.ant-breadcrumb-rtl{direction:rtl}.ant-breadcrumb-rtl:before{display:table;content:""}.ant-breadcrumb-rtl:after{display:table;clear:both;content:""}.ant-breadcrumb-rtl>span{float:right}.ant-breadcrumb-rtl .ant-breadcrumb-link>.anticon+span,.ant-breadcrumb-rtl .ant-breadcrumb-link>.anticon+a{margin-right:4px;margin-left:0}.ant-breadcrumb-rtl .ant-breadcrumb-overlay-link>.anticon{margin-right:4px;margin-left:0}.nprogress{pointer-events:none}.nprogress .bar{background:#29d;position:fixed;z-index:1031;top:0;left:0;width:100%;height:2px}.nprogress .peg{display:block;position:absolute;right:0px;width:100px;height:100%;box-shadow:0 0 10px #29d,0 0 5px #29d;opacity:1;-webkit-transform:rotate(3deg) translate(0px,-4px);-ms-transform:rotate(3deg) translate(0px,-4px);transform:rotate(3deg) translateY(-4px)}.nprogress .spinner{display:block;position:fixed;z-index:1031;top:15px;right:15px}.nprogress .spinner-icon{width:18px;height:18px;box-sizing:border-box;border:solid 2px transparent;border-top-color:#29d;border-left-color:#29d;border-radius:50%;-webkit-animation:nprogress-spinner .4s linear infinite;animation:nprogress-spinner .4s linear infinite}.nprogress-custom-parent{overflow:hidden;position:relative}.nprogress-custom-parent .nprogress .spinner,.nprogress-custom-parent .nprogress .bar{position:absolute}@-webkit-keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(360deg)}}@keyframes nprogress-spinner{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.base-info[data-v-afd25667]{position:absolute;padding:4px;font-size:.8em;background:var(--zp-primary-background);color:var(--zp-primary);left:0;bottom:0;border-top-right-radius:4px}.preview-switch[data-v-b8160253]{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-b8160253]{color:#fff;margin:16px;font-size:4em;pointer-events:all;cursor:pointer}.preview-switch>*.disable[data-v-b8160253]{opacity:0;pointer-events:none;cursor:none}.location-act[data-v-b8160253]{margin-left:8px}.location-act .copy[data-v-b8160253]{margin-right:4px}@media (max-width: 768px){.location-act[data-v-b8160253]{display:flex;flex-direction:column}.location-act>*[data-v-b8160253],.location-act .copy[data-v-b8160253]{margin:2px}}.breadcrumb[data-v-b8160253]{display:flex;align-items:center}.breadcrumb>*[data-v-b8160253]{margin-right:4px}@media (max-width: 768px){.breadcrumb[data-v-b8160253]{width:100%}.breadcrumb .ant-breadcrumb>*[data-v-b8160253]{display:inline-block}}.container[data-v-b8160253]{background:var(--zp-secondary-background);height:var(--pane-max-height)}.location-bar[data-v-b8160253]{padding:4px 16px;background:var(--zp-primary-background);border-bottom:1px solid var(--zp-border);display:flex;align-items:center;justify-content:space-between}@media (max-width: 768px){.location-bar[data-v-b8160253]{flex-direction:column}.location-bar[data-v-b8160253] ::-webkit-scrollbar{height:2px;background-color:var(--zp-secondary-variant-background)}.location-bar .actions[data-v-b8160253]{padding:4px 0;width:100%;overflow:auto;display:flex;align-items:center}.location-bar .actions>*[data-v-b8160253]{flex-shrink:0}}.location-bar .actions[data-v-b8160253]{display:flex;align-items:center;flex-shrink:0}.location-bar a.opt[data-v-b8160253]{margin-left:8px}.view[data-v-b8160253]{padding:8px;height:calc(100vh - 48px)}.view .file-list[data-v-b8160253]{list-style:none;padding:8px;height:100%;overflow:auto}.hint[data-v-b8160253]{padding:4px;border:4px;background:var(--zp-secondary-background);border:1px solid var(--zp-border)}
.ant-breadcrumb{box-sizing:border-box;margin:0;padding:0;color:#000000d9;font-variant:tabular-nums;line-height:1.5715;list-style:none;font-feature-settings:"tnum";color:#00000073;font-size:14px}.ant-breadcrumb .anticon{font-size:14px}.ant-breadcrumb a{color:#00000073;transition:color .3s}.ant-breadcrumb a:hover{color:#de632f}.ant-breadcrumb>span:last-child{color:#000000d9}.ant-breadcrumb>span:last-child a{color:#000000d9}.ant-breadcrumb>span:last-child .ant-breadcrumb-separator{display:none}.ant-breadcrumb-separator{margin:0 8px;color:#00000073}.ant-breadcrumb-link>.anticon+span,.ant-breadcrumb-link>.anticon+a{margin-left:4px}.ant-breadcrumb-overlay-link>.anticon{margin-left:4px}.ant-breadcrumb-rtl{direction:rtl}.ant-breadcrumb-rtl:before{display:table;content:""}.ant-breadcrumb-rtl:after{display:table;clear:both;content:""}.ant-breadcrumb-rtl>span{float:right}.ant-breadcrumb-rtl .ant-breadcrumb-link>.anticon+span,.ant-breadcrumb-rtl .ant-breadcrumb-link>.anticon+a{margin-right:4px;margin-left:0}.ant-breadcrumb-rtl .ant-breadcrumb-overlay-link>.anticon{margin-right:4px;margin-left:0}.nprogress{pointer-events:none}.nprogress .bar{background:#29d;position:fixed;z-index:1031;top:0;left:0;width:100%;height:2px}.nprogress .peg{display:block;position:absolute;right:0px;width:100px;height:100%;box-shadow:0 0 10px #29d,0 0 5px #29d;opacity:1;-webkit-transform:rotate(3deg) translate(0px,-4px);-ms-transform:rotate(3deg) translate(0px,-4px);transform:rotate(3deg) translateY(-4px)}.nprogress .spinner{display:block;position:fixed;z-index:1031;top:15px;right:15px}.nprogress .spinner-icon{width:18px;height:18px;box-sizing:border-box;border:solid 2px transparent;border-top-color:#29d;border-left-color:#29d;border-radius:50%;-webkit-animation:nprogress-spinner .4s linear infinite;animation:nprogress-spinner .4s linear infinite}.nprogress-custom-parent{overflow:hidden;position:relative}.nprogress-custom-parent .nprogress .spinner,.nprogress-custom-parent .nprogress .bar{position:absolute}@-webkit-keyframes nprogress-spinner{0%{-webkit-transform:rotate(0deg)}to{-webkit-transform:rotate(360deg)}}@keyframes nprogress-spinner{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.base-info[data-v-afd25667]{position:absolute;padding:4px;font-size:.8em;background:var(--zp-primary-background);color:var(--zp-primary);left:0;bottom:0;border-top-right-radius:4px}.preview-switch[data-v-817f474b]{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-817f474b]{color:#fff;margin:16px;font-size:4em;pointer-events:all;cursor:pointer}.preview-switch>*.disable[data-v-817f474b]{opacity:0;pointer-events:none;cursor:none}.location-act[data-v-817f474b]{margin-left:8px}.location-act .copy[data-v-817f474b]{margin-right:4px}@media (max-width: 768px){.location-act[data-v-817f474b]{display:flex;flex-direction:column}.location-act>*[data-v-817f474b],.location-act .copy[data-v-817f474b]{margin:2px}}.breadcrumb[data-v-817f474b]{display:flex;align-items:center}.breadcrumb>*[data-v-817f474b]{margin-right:4px}@media (max-width: 768px){.breadcrumb[data-v-817f474b]{width:100%}.breadcrumb .ant-breadcrumb>*[data-v-817f474b]{display:inline-block}}.container[data-v-817f474b]{background:var(--zp-secondary-background);height:var(--pane-max-height)}.location-bar[data-v-817f474b]{padding:4px 16px;background:var(--zp-primary-background);border-bottom:1px solid var(--zp-border);display:flex;align-items:center;justify-content:space-between}@media (max-width: 768px){.location-bar[data-v-817f474b]{flex-direction:column}.location-bar[data-v-817f474b] ::-webkit-scrollbar{height:2px;background-color:var(--zp-secondary-variant-background)}.location-bar .actions[data-v-817f474b]{padding:4px 0;width:100%;overflow:auto;display:flex;align-items:center}.location-bar .actions>*[data-v-817f474b]{flex-shrink:0}}.location-bar .actions[data-v-817f474b]{display:flex;align-items:center;flex-shrink:0}.location-bar a.opt[data-v-817f474b]{margin-left:8px}.view[data-v-817f474b]{padding:8px;height:calc(100vh - 48px)}.view .file-list[data-v-817f474b]{list-style:none;padding:8px;height:100%;overflow:auto}.hint[data-v-817f474b]{padding:4px;border:4px;background:var(--zp-secondary-background);border:1px solid var(--zp-border)}

File diff suppressed because one or more lines are too long

4
vue/dist/index.html vendored
View File

@ -7,8 +7,8 @@
<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-f08bcee4.js"></script>
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-b5fcdaa7.css">
<script type="module" crossorigin src="/infinite_image_browsing/fe-static/assets/index-db6e6f1f.js"></script>
<link rel="stylesheet" href="/infinite_image_browsing/fe-static/assets/index-daea5cd8.css">
</head>
<body>

View File

@ -92,7 +92,7 @@ export const getImagesBySubstr = async (req: SearchBySubstrReq) => {
}
const extraPaths = '/db/extra_paths'
export type ExtraPathType = 'scanned' | 'walk' | 'cli_access_only' | ''
export type ExtraPathType = 'scanned' | 'walk' | 'cli_access_only' | '' | 'scanned-fixed'
export interface ExtraPathModel {
path: string

View File

@ -6,7 +6,7 @@ import type { FileNodeInfo } from '@/api/files'
import { isImageFile, isVideoFile } from '@/util'
import { toImageThumbnailUrl, toVideoCoverUrl, toRawFileUrl } from '@/util/file'
import type { MenuInfo } from 'ant-design-vue/lib/menu/src/interface'
import { computed } from 'vue'
import { computed, ref } from 'vue'
import ContextMenu from './ContextMenu.vue'
import ChangeIndicator from './ChangeIndicator.vue'
import { useTagStore } from '@/store/useTagStore'
@ -16,13 +16,15 @@ import { openVideoModal } from './functionalCallableComp'
import type { GenDiffInfo } from '@/api/files'
import { play } from '@/icon'
import { Top4MediaInfo } from '@/api'
import { watch } from 'vue'
import { debounce } from 'lodash-es'
const global = useGlobalStore()
const tagStore = useTagStore()
const props = withDefaults(
defineProps<{
file: FileNodeInfo
file: FileNodeInfo,
idx: number
selected?: boolean
showMenuIdx?: number
@ -31,29 +33,34 @@ const props = withDefaults(
enableRightClickMenu?: boolean,
enableCloseIcon?: boolean,
isSelectedMutilFiles?: boolean
genDiffToPrevious?: GenDiffInfo
genDiffToNext?: GenDiffInfo
genInfo?: string
enableChangeIndicator?: boolean
extraTags?: Tag[]
coverFiles?: Top4MediaInfo[]
getGenDiff?: (ownGenInfo: any, idx: any, increment: any, ownFile: FileNodeInfo) => GenDiffInfo,
getGenDiffWatchDep?: (idx: number) => any
}>(),
{
selected: false, enableRightClickMenu: true, enableCloseIcon: false, genDiffToNext: () => ({
empty: true,
ownFile: '',
otherFile: '',
diff: '',
}), genDiffToPrevious: () => ({
empty: true,
ownFile: '',
otherFile: '',
diff: '',
})
selected: false, enableRightClickMenu: true, enableCloseIcon: false
}
)
const genDiffToPrevious = ref<GenDiffInfo>()
const genDiffToNext = ref<GenDiffInfo>()
const calcGenInfoDiff = debounce(() => {
const { getGenDiff, file, idx } = props
if (!getGenDiff) return
genDiffToNext.value = getGenDiff(file.gen_info_obj, idx, 1, file)
genDiffToPrevious.value = getGenDiff(file.gen_info_obj, idx, -1, file)
}, 200 + 100 * Math.random())
watch(() => props.getGenDiffWatchDep?.(props.idx), () => {
genDiffToNext.value = undefined
genDiffToPrevious.value = undefined
calcGenInfoDiff()
}, { immediate: true, deep: true })
const emit = defineEmits<{
'update:showMenuIdx': [v: number],
'fileItemClick': [event: MouseEvent, file: FileNodeInfo, idx: number],
@ -130,8 +137,8 @@ const taggleLikeTag = () => {
<div :key="file.fullpath" :class="`idx-${idx} item-content`" v-if="isImageFile(file.name)">
<!-- change indicators -->
<ChangeIndicator v-if="enableChangeIndicator" :gen-diff-to-next="genDiffToNext"
:gen-diff-to-previous="genDiffToPrevious" />
<ChangeIndicator v-if="enableChangeIndicator && genDiffToNext && genDiffToPrevious"
:gen-diff-to-next="genDiffToNext" :gen-diff-to-previous="genDiffToPrevious" />
<!-- change indicators END -->
<a-image :src="imageSrc" :fallback="fallbackImage" :preview="{
@ -164,7 +171,7 @@ const taggleLikeTag = () => {
:src="item.media_type === 'image' ? toImageThumbnailUrl(item) : toVideoCoverUrl(item)"
v-for="item in coverFiles" :key="item.fullpath">
</div>
<folder-open-outlined class="icon center" v-else />
</div>
<div class="profile" v-if="cellWidth > 128">

View File

@ -105,7 +105,6 @@ export const de: Partial<IIBI18nMap> = {
restoreLastRecord: 'Letztes Verzeichnis wiederherstellen',
launch: 'Ausführen',
walkMode: 'Verwende den Walk-Modus, um Bilder zu durchsuchen',
launchFromQuickMove: 'Ausführen aus Schnellzugriff',
recent: 'Kürzlich',
emptyStartPage: 'Leere Startseite',
t2i: 'Text-zu-Bild',

View File

@ -1,6 +1,7 @@
import type { IIBI18nMap } from '.'
export const en: IIBI18nMap = {
type: 'Type',
filterByKeyword: 'Filter tags by keyword',
loadmore: 'Load more',
rename: 'Rename',
@ -116,7 +117,7 @@ export const en: IIBI18nMap = {
serverKeyRequired:
'The server has configured a key. You must provide the same key to continue using it.',
removeFromSearchScanPathAndQuickMove: 'Remove from Search Scan Path and Quick Move',
addToSearchScanPathAndQuickMove: 'Add to Search Scan Path and Quick Move',
addToSearchScanPathAndQuickMove: 'Add to Search Scan Path',
openWithLocalFileBrowser: 'Open with Local File Browser',
'fuzzy-search-noResults': 'Nothing was found',
'fuzzy-search-placeholder': 'Enter a part of the image information or filename to search',
@ -217,7 +218,7 @@ export const en: IIBI18nMap = {
restoreLastRecord: 'Restore last record',
launch: 'Launch',
walkMode: 'Use Walk mode to browse images',
launchFromQuickMove: 'Launch from Quick Move',
launchFromNormalAndFixed: 'Use Normal / Fixed mode to browse images',
recent: 'Recent',
emptyStartPage: 'Empty start page',
t2i: 'txt2img',

View File

@ -1,4 +1,5 @@
export const zhHans = {
type: '类型',
filterByKeyword: '输入标签关键词过滤',
loadmore: '加载更多',
rename: '重命名',
@ -79,7 +80,7 @@ export const zhHans = {
restoreLastRecord: '还原上次记录',
launch: '启动',
walkMode: '使用 Walk 模式浏览图片',
launchFromQuickMove: '从快速移动启动',
launchFromNormalAndFixed: '使用 Normal / Fixed 模式浏览图片',
recent: '最近',
emptyStartPage: '空启动页',
t2i: '文生图',
@ -143,7 +144,7 @@ export const zhHans = {
'fuzzy-search-placeholder': '输入图像信息或者文件名的一部分来进行搜索',
'fuzzy-search-noResults': '什么都没找到',
openWithLocalFileBrowser: '使用本地文件浏览器打开',
addToSearchScanPathAndQuickMove: '添加到搜索扫描路径和快速移动',
addToSearchScanPathAndQuickMove: '添加到搜索扫描路径',
removeFromSearchScanPathAndQuickMove: '从搜索扫描路径和快速移动中移除',
serverKeyRequired: '服务器配置了密匙,你必须提供相同的密匙才能继续使用',
shortcutKey: '快捷键(仅允许在全屏查看下使用)',

View File

@ -83,7 +83,8 @@ export const zhHant: Partial<IIBI18nMap> = {
restoreLastRecord: '還原上次記錄',
launch: '啟動',
walkMode: '使用 Walk 模式瀏覽圖片',
launchFromQuickMove: '從快速移動啟動',
launchFromNormalAndFixed: '使用 Normal / Fixed 模式瀏覽圖片',
recent: '最近',
emptyStartPage: '空啟動頁',
t2i: '文生圖',
@ -147,7 +148,7 @@ export const zhHant: Partial<IIBI18nMap> = {
'fuzzy-search-placeholder': '輸入圖片信息或者文件名的一部分來進行搜尋',
'fuzzy-search-noResults': '什麼都沒找到',
openWithLocalFileBrowser: '使用本地檔案瀏覽器打開',
addToSearchScanPathAndQuickMove: '加入搜尋掃描路徑和快速移動',
addToSearchScanPathAndQuickMove: '加入搜尋掃描路徑',
removeFromSearchScanPathAndQuickMove: '從搜尋掃描路徑和快速移動中移除',
serverKeyRequired: '伺服器配置了密鑰,你必须提供相同的密鑰才能繼續使用',
shortcutKey: '快速鍵(僅允許在全螢幕檢視下使用)',

View File

@ -97,6 +97,7 @@ tryOnMounted(async () => {
})
})
watch(useDocumentVisibility(), v => v && emitReturnToIIB())
</script>
<template>
<div ref="container">

View File

@ -9,10 +9,24 @@ import { cloneDeep } from 'lodash-es'
import { useImgSliStore } from '@/store/useImgSli'
import { addToExtraPath, onAliasExtraPathClick, onRemoveExtraPathClick } from './extraPathControlFunc'
import actionContextMenu from './actionContextMenu.vue'
import { ExtraPathType } from '@/api/db'
import { onMounted } from 'vue'
const global = useGlobalStore()
const imgsli = useImgSliStore()
const props = defineProps<{ tabIdx: number; paneIdx: number }>()
const props = defineProps<{
tabIdx: number; paneIdx: number, popAddPathModal?: {
path: string
type: ExtraPathType
}
}>()
onMounted(() => {
if (props.popAddPathModal) {
addToExtraPath(props.popAddPathModal.type, props.popAddPathModal.path)
}
})
const compCnMap: Partial<Record<TabPane['type'], string>> = {
local: t('local'),
'tag-search': t('imgSearch'),
@ -20,8 +34,8 @@ const compCnMap: Partial<Record<TabPane['type'], string>> = {
'global-setting': t('globalSettings'),
'batch-download': t('batchDownload') + ' / ' + t('archive')
}
const createPane = (type: TabPane['type'], path?: string, walkMode = false) => {
type FileTransModeIn = 'preset' | ExtraPathType
const createPane = (type: TabPane['type'], path?: string, mode?: FileTransModeIn) => {
let pane: TabPane
switch (type) {
case 'grid-view':
@ -41,28 +55,28 @@ const createPane = (type: TabPane['type'], path?: string, walkMode = false) => {
name: compCnMap[type]!,
key: Date.now() + uniqueId(),
path,
walkModePath: walkMode ? path : undefined
mode: mode === 'scanned-fixed' || mode === 'walk' ? mode : 'scanned'
}
}
return pane
}
const openInCurrentTab = (type: TabPane['type'], path?: string, walkMode = false) => {
const pane = createPane(type, path, walkMode)
const openInCurrentTab = (type: TabPane['type'], path?: string, mode?: FileTransModeIn) => {
const pane = createPane(type, path, mode)
if (!pane) return
const tab = global.tabList[props.tabIdx]
tab.panes.splice(props.paneIdx, 1, pane)
tab.key = pane.key
}
const openInNewTab = (type: TabPane['type'], path?: string, walkMode = false) => {
const pane = createPane(type, path, walkMode)
const openInNewTab = (type: TabPane['type'], path?: string, mode?: FileTransModeIn) => {
const pane = createPane(type, path, mode)
if (!pane) return
const tab = global.tabList[props.tabIdx]
tab.panes.push(pane)
}
const openOnTheRight = (type: TabPane['type'], path?: string, walkMode = false) => {
const pane = createPane(type, path, walkMode)
const openOnTheRight = (type: TabPane['type'], path?: string, mode?: FileTransModeIn) => {
const pane = createPane(type, path, mode)
if (!pane) return
let tab = global.tabList[props.tabIdx + 1]
if (!tab) {
@ -115,9 +129,9 @@ const restoreRecord = () => {
<a href="https://github.com/zanllp/sd-webui-infinite-image-browsing/issues/90" target="_blank"
class="last-record">{{ $t('faq') }}</a>
<a-radio-group v-model:value="global.darkModeControl" button-style="solid">
<a-radio-button value="light">light</a-radio-button>
<a-radio-button value="auto">auto</a-radio-button>
<a-radio-button value="dark">dark</a-radio-button>
<a-radio-button value="light">Light</a-radio-button>
<a-radio-button value="auto">Auto</a-radio-button>
<a-radio-button value="dark">Dark</a-radio-button>
</a-radio-group>
</div>
<a-alert show-icon v-if="global.conf?.enable_access_control && !global.dontShowAgain">
@ -155,9 +169,9 @@ const restoreRecord = () => {
</span>
</li>
<actionContextMenu v-for="dir in walkModeSupportedDir" :key="dir.key"
@open-in-new-tab="openInNewTab('local', dir.dir, true)"
@open-on-the-right="openOnTheRight('local', dir.dir, true)">
<li class="item rem" @click.prevent="openInCurrentTab('local', dir.dir, true)">
@open-in-new-tab="openInNewTab('local', dir.dir, 'walk')"
@open-on-the-right="openOnTheRight('local', dir.dir, 'walk')">
<li class="item rem" @click.prevent="openInCurrentTab('local', dir.dir, 'walk')">
<span class="text line-clamp-2">{{ dir.zh }}</span>
<template v-if="dir.can_delete">
<AButton type="link" @click.stop="onAliasExtraPathClick(dir.dir)">{{ $t('alias') }}
@ -171,27 +185,31 @@ const restoreRecord = () => {
</ul>
</div>
<div class="feature-item" v-if="global.quickMovePaths.length">
<h2>{{ $t('launchFromQuickMove') }}</h2>
<h2>{{ $t('launchFromNormalAndFixed') }}</h2>
<ul>
<li @click="addToExtraPath('scanned')" class="item">
<span class="text line-clamp-1">
<PlusOutlined /> {{ $t('add') }}
</span>
</li>
<actionContextMenu
v-for="dir in global.quickMovePaths.filter(({ types: ts }) => ts.includes('cli_access_only') || ts.includes('preset') || ts.includes('scanned'))"
:key="dir.key" @open-in-new-tab="openInNewTab('local', dir.dir)"
@open-on-the-right="openOnTheRight('local', dir.dir)">
<li class="item rem" @click.prevent="openInCurrentTab('local', dir.dir)">
<span class="text line-clamp-2">{{ dir.zh }}</span>
<template v-if="dir.can_delete && dir.types.includes('scanned')">
<AButton type="link" @click.stop="onAliasExtraPathClick(dir.dir)">{{ $t('alias') }}
</AButton>
<AButton type="link" @click.stop="onRemoveExtraPathClick(dir.dir, 'scanned')">{{ $t('remove') }}
</AButton>
</template>
</li>
</actionContextMenu>
<template
v-for="dir in global.quickMovePaths.filter(({ types: ts }) => ts.includes('cli_access_only') || ts.includes('preset') || ts.includes('scanned') || ts.includes('scanned-fixed')) "
:key="dir.key">
<actionContextMenu v-for="t in dir.types.filter(v => v !== 'walk')" :key="t" @open-in-new-tab="openInNewTab('local', dir.dir, t)"
@open-on-the-right="openOnTheRight('local', dir.dir,t)">
<li class="item rem" @click.prevent="openInCurrentTab('local', dir.dir, t)">
<span class="text line-clamp-2"><span v-if="t == 'scanned-fixed'" class="fixed">Fixed</span>{{ dir.zh }}</span>
<template v-if="dir.can_delete && (t === 'scanned-fixed' || t === 'scanned')">
<AButton type="link" @click.stop="onAliasExtraPathClick(dir.dir)">{{ $t('alias') }}
</AButton>
<AButton type="link" @click.stop="onRemoveExtraPathClick(dir.dir, t)">{{ $t('remove') }}
</AButton>
</template>
</li>
</actionContextMenu>
</template>
</ul>
</div>
<div class="feature-item">
@ -315,6 +333,7 @@ const restoreRecord = () => {
padding: 4px 8px;
display: flex;
align-items: center;
position: relative;
&.rem {
display: flex;
@ -328,6 +347,15 @@ const restoreRecord = () => {
color: var(--primary-color);
cursor: pointer;
}
.fixed {
background: var(--primary-color);
color: white;
font-size: .8em;
padding: 2px 4px;
border-radius: 8px;
margin-right: 4px;
}
}
.icon {

View File

@ -1,7 +1,7 @@
import { ExtraPathType, addExtraPath, aliasExtraPath, removeExtraPath } from '@/api/db'
import { globalEvents } from '@/util'
import { Input, Modal, message } from 'ant-design-vue'
import { Input, Modal, RadioButton, RadioGroup, message } from 'ant-design-vue'
import { open } from '@tauri-apps/api/dialog'
import { checkPathExists } from '@/api'
import { h, ref } from 'vue'
@ -10,11 +10,13 @@ import { useGlobalStore } from '@/store/useGlobalStore'
export const addToExtraPath = async (type: ExtraPathType) => {
export const addToExtraPath = async (initType: ExtraPathType, initPath?: string) => {
const g = useGlobalStore()
let path: string
let path: string = initPath ?? ''
const type = ref(initType)
if (import.meta.env.TAURI_ARCH) {
const ret = await open({ directory: true })
const ret = await open({ directory: true, defaultPath: initPath })
if (typeof ret === 'string') {
path = ret
} else {
@ -22,14 +24,15 @@ export const addToExtraPath = async (type: ExtraPathType) => {
}
} else {
path = await new Promise<string>((resolve) => {
const key = ref('')
const key = ref(path)
console.log('dfd', key.value)
Modal.confirm({
title: t('inputTargetFolderPath'),
width: '800px',
content: () => {
return h('div', [
g.conf?.enable_access_control ? h('a', {
style: {
g.conf?.enable_access_control ? h('a', {
style: {
'word-break': 'break-all',
'margin-bottom': '4px',
display: 'block'
@ -40,7 +43,23 @@ export const addToExtraPath = async (type: ExtraPathType) => {
h(Input, {
value: key.value,
'onUpdate:value': (v: string) => (key.value = v)
})
}),
h('div', [
h('span', t('type')+': '),
h(RadioGroup, {
value: type.value,
'onUpdate:value': (v: ExtraPathType) => (type.value = v),
buttonStyle: 'solid',
style: { margin: '16px 0 32px' }
}, [
h(RadioButton, { value: 'walk' }, 'Walk'),
h(RadioButton, { value: 'scanned' }, 'Normal'),
h(RadioButton, { value: 'scanned-fixed' }, 'Fixed')
])
]),
h('p', 'Walk: 无需翻页即可浏览指定文件夹下的所有文件使用无限滚动的方式呈现。将会使用DFS的方式遍历所有文件. 注意:该模式下排序仅在同层之间进行'),
h('p', 'Normal: 类似于windows的文件浏览器拥有较高的灵活性. 但在访问云存储或者类似SMB这样的地方时可能会出现异常'),
h('p', 'Fixed: 类似Normal模式但页面初始速度更快兼容性更好灵活性稍差。在Normal模式下出现了异常的话都可以改用这个试试')
])
},
async onOk () {
@ -58,7 +77,7 @@ export const addToExtraPath = async (type: ExtraPathType) => {
Modal.confirm({
content: t('confirmToAddToExtraPath'),
async onOk () {
await addExtraPath({ types: [type], path })
await addExtraPath({ types: [type.value], path })
message.success(t('addCompleted'))
globalEvents.emit('searchIndexExpired')
globalEvents.emit('updateGlobalSetting')
@ -85,11 +104,11 @@ export const onAliasExtraPathClick = (path: string) => {
title: t('inputAlias'),
content: () => {
return h('div', [
h('div', {
style: {
h('div', {
style: {
'word-break': 'break-all',
'margin-bottom': '4px'
}
}
}, 'Path: ' + path),
h(Input, {
value: alias.value,

View File

@ -37,13 +37,14 @@ const classSort = [
'custom',
'Source Identifier',
'Model',
'Media Type',
'lora',
'lyco',
'pos',
'size',
'Sampler',
'Postprocess upscaler',
'Postprocess upscale by',
'Sampler'
].reduce((p, c, i) => {
p[c] = i
return p
@ -51,7 +52,11 @@ const classSort = [
const classifyTags = computed(() => {
return Object.entries(groupBy(tags.value, (v) => v.type)).sort(
(a, b) => classSort[a[0]] - classSort[b[0]]
(a, b) => {
const aClass = classSort[a[0]] !== undefined ? classSort[a[0]] : Number.MAX_SAFE_INTEGER;
const bClass = classSort[b[0]] !== undefined ? classSort[b[0]] : Number.MAX_SAFE_INTEGER;
return aClass - bClass;
}
)
})
const tagMaxNum = reactive(new Map<string, number>())
@ -218,7 +223,8 @@ const tagListFilter = (list: Tag[], name: string) => {
{{ $t('needGenerateIdx') }}
</p>
<div class="list-container">
<ul class="tag-list" :key="name" v-for="[name, list] in classifyTags">
<template :key="name" v-for="[name, list] in classifyTags">
<ul class="tag-list" v-if="name !== 'Media Type' || list.length > 1">
<h3 class="cat-name"
@click="!openedKeys.includes(name) ? openedKeys.push(name) : openedKeys.splice(openedKeys.indexOf(name), 1)">
<ArrowRightOutlined class="arrow" :class="{ down: openedKeys.includes(name) }" />
@ -254,6 +260,7 @@ const tagListFilter = (list: Tag[], name: string) => {
</a-collapse-panel>
</a-collapse>
</ul>
</template>
</div>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,190 @@
import { useGlobalStore, type FileTransferTabPane } from '@/store/useGlobalStore'
import { useImgSliStore } from '@/store/useImgSli'
import { onLongPress } from '@vueuse/core'
import { ref, computed, watch, reactive } from 'vue'
import {
createTypedShareStateHook,
typedEventEmitter
} from 'vue3-ts-util'
import { type FileNodeInfo } from '@/api/files'
import { sortFiles } from '../fileSort'
import { last, range } from 'lodash-es'
import * as Path from '@/util/path'
import { isMediaFile } from '@/util/file'
import { useTagStore } from '@/store/useTagStore'
import { useBatchDownloadStore } from '@/store/useBatchDownloadStore'
import { Walker } from '../walker'
export const stackCache = new Map<string, Page[]>()
export const global = useGlobalStore()
export const batchDownload = useBatchDownloadStore()
export const tagStore = useTagStore()
export const sli = useImgSliStore()
export const imgTransferBus = new BroadcastChannel('iib-image-transfer-bus')
export const { eventEmitter: events, useEventListen } = typedEventEmitter<{
removeFiles (_: { paths: string[]; loc: string }): void
addFiles (_: { files: FileNodeInfo[]; loc: string }): void
}>()
export * from './useLocation'
export * from './usePreview'
export * from './useFilesDisplay'
export * from './useFileTransfer'
export * from './useFileItemActions'
export * from './useGenInfoDiff'
export interface Scroller {
$_startIndex: number
$_endIndex: number
scrollToItem (idx: number): void
}
export const { useHookShareState } = createTypedShareStateHook(
(_inst, { images }) => {
const props = ref<Props>({ tabIdx: -1, paneIdx: -1 })
const currPage = computed(() => last(stack.value))
const stack = ref<Page[]>([])
const basePath = computed(() =>
stack.value.map((v) => v.curr).slice(global.conf?.is_win && props.value.mode !== 'scanned-fixed' ? 1 : 0)
)
const currLocation = computed(() => Path.join(...basePath.value))
const sortMethod = ref(global.defaultSortingMethod)
const walker = ref(props.value.mode == 'walk' ? new Walker(props.value.path!, sortMethod.value) : undefined)
watch([() => props.value.mode, sortMethod], () => {
walker.value = props.value.mode === 'walk' ? new Walker(props.value.path!, sortMethod.value) : undefined
})
const deletedFiles = reactive(new Set<string>())
watch(currPage, () => deletedFiles.clear())
const sortedFiles = computed(() => {
if (images.value) {
return images.value
}
if (walker.value) {
return walker.value.images.filter(v => !deletedFiles.has(v.fullpath))
}
if (!currPage.value) {
return []
}
const files = currPage.value?.files ?? []
const method = sortMethod.value
const filter = (files: FileNodeInfo[]) =>
global.onlyFoldersAndImages
? files.filter((file) => file.type === 'dir' || isMediaFile(file.name))
: files
return sortFiles(filter(files), method).filter(v => !deletedFiles.has(v.fullpath))
})
const multiSelectedIdxs = ref([] as number[])
const previewIdx = ref(-1)
const canLoadNext = computed(() => walker.value ? !walker.value.isCompleted : false)
const spinning = ref(false)
const previewing = ref(false)
const getPane = () => {
return global.tabList?.[props.value.tabIdx]?.panes?.[props.value.paneIdx] as FileTransferTabPane
}
const events = typedEventEmitter<{
loadNextDir (isFullscreenPreview?: boolean): Promise<void>
refresh (): Promise<void>
selectAll (): void
viewableAreaFilesChange(_: { files: FileNodeInfo[], startIdx: number }): void
}>()
events.useEventListen('selectAll', () => {
console.log(`select all 0 -> ${sortedFiles.value.length}`)
multiSelectedIdxs.value = range(0, sortedFiles.value.length)
})
return {
previewing,
spinning,
canLoadNext,
multiSelectedIdxs,
previewIdx,
basePath,
currLocation,
currPage,
stack,
sortMethod,
sortedFiles,
scroller: ref<Scroller>(),
stackViewEl: ref<HTMLDivElement>(),
props,
getPane,
walker,
deletedFiles,
...events
}
},
() => ({ images: ref<FileNodeInfo[]>() })
)
export interface Props {
tabIdx: number
paneIdx: number
path?: string
mode?: 'walk' | 'scanned' | 'scanned-fixed'
fixed?: boolean
}
export interface Page {
files: FileNodeInfo[]
curr: string
}
export function useKeepMultiSelect () {
const { eventEmitter, multiSelectedIdxs, sortedFiles } = useHookShareState().toRefs()
const onSelectAll = () => eventEmitter.value.emit('selectAll')
const onReverseSelect = () => {
multiSelectedIdxs.value = sortedFiles.value.map((_, idx) => idx)
.filter(v => !multiSelectedIdxs.value.includes(v))
}
const onClearAllSelected = () => {
multiSelectedIdxs.value = []
}
return {
onSelectAll,
onReverseSelect,
onClearAllSelected
}
}
/**
* 使
*/
export const useMobileOptimization = () => {
const { stackViewEl } = useHookShareState().toRefs()
const showMenuIdx = ref(-1)
onLongPress(stackViewEl, (e) => {
let fileEl = e.target as HTMLDivElement
while (fileEl.parentElement) {
fileEl = fileEl.parentElement as any
if (fileEl.tagName.toLowerCase() === 'li' && fileEl.classList.contains('file-item-trigger')) {
const idx = fileEl.dataset?.idx
if (idx && Number.isSafeInteger(+idx)) {
showMenuIdx.value = +idx
}
return
}
}
})
return {
showMenuIdx
}
}

View File

@ -0,0 +1,368 @@
import { type FileTransferTabPane, type Shortcut } from '@/store/useGlobalStore'
import { useMouseInElement } from '@vueuse/core'
import { ref } from 'vue'
import { genInfoCompleted, getImageGenerationInfo, openFolder, openWithDefaultApp, setImgPath } from '@/api'
import {
useWatchDocument} from 'vue3-ts-util'
import {
createReactiveQueue,
isImageFile,
copy2clipboardI18n} from '@/util'
import { type FileNodeInfo, deleteFiles, moveFiles } from '@/api/files'
import { last, range, uniqueId } from 'lodash-es'
import * as Path from '@/util/path'
import { Checkbox, Modal, message } from 'ant-design-vue'
import type { MenuInfo } from 'ant-design-vue/lib/menu/src/interface'
import { t } from '@/i18n'
import { batchUpdateImageTag, toggleCustomTagToImg } from '@/api/db'
import { downloadFileInfoJSON, downloadFiles, toRawFileUrl } from '@/util/file'
import { getShortcutStrFromEvent } from '@/util/shortcut'
import { MultiSelectTips, openRenameFileModal } from '@/components/functionalCallableComp'
import { batchDownload, events, imgTransferBus, stackCache, tagStore, useEventListen, useHookShareState, global } from '.'
export function useFileItemActions (
{ openNext }: { openNext: (file: FileNodeInfo) => Promise<void> }
) {
const showGenInfo = ref(false)
const imageGenInfo = ref('')
const {
sortedFiles,
previewIdx,
multiSelectedIdxs,
stack,
currLocation,
spinning,
previewing,
stackViewEl,
eventEmitter,
props,
deletedFiles
} = useHookShareState().toRefs()
const nor = Path.normalize
useEventListen('removeFiles', ({ paths, loc }) => {
if (nor(loc) !== nor(currLocation.value)) {
return
}
const top = last(stack.value)
if (!top) {
return
}
paths.forEach(path => deletedFiles.value.add(path))
paths.filter(isImageFile).forEach(path => deletedFiles.value.add(path.replace(/\.\w+$/, '.txt')))
})
useEventListen('addFiles', ({ files, loc }) => {
if (nor(loc) !== nor(currLocation.value)) {
return
}
const top = last(stack.value)
if (!top) {
return
}
top.files.unshift(...files)
})
const q = createReactiveQueue()
const onFileItemClick = async (e: MouseEvent, file: FileNodeInfo, idx: number) => {
previewIdx.value = idx
global.fullscreenPreviewInitialUrl = toRawFileUrl(file)
const idxInSelected = multiSelectedIdxs.value.indexOf(idx)
if (e.shiftKey) {
if (idxInSelected !== -1) {
multiSelectedIdxs.value.splice(idxInSelected, 1)
} else {
multiSelectedIdxs.value.push(idx)
multiSelectedIdxs.value.sort((a, b) => a - b)
const first = multiSelectedIdxs.value[0]
const last = multiSelectedIdxs.value[multiSelectedIdxs.value.length - 1]
multiSelectedIdxs.value = range(first, last + 1)
}
e.stopPropagation()
} else if (e.ctrlKey || e.metaKey) {
if (idxInSelected !== -1) {
multiSelectedIdxs.value.splice(idxInSelected, 1)
} else {
multiSelectedIdxs.value.push(idx)
}
e.stopPropagation()
} else {
await openNext(file)
}
}
const onContextMenuClick = async (e: MenuInfo, file: FileNodeInfo, idx: number) => {
const url = toRawFileUrl(file)
const path = currLocation.value
const preset = { IIB_container_id: parent.IIB_container_id }
/**
*
*
*/
const getSelectedImg = () => {
let selectedFiles: FileNodeInfo[] = []
if (multiSelectedIdxs.value.includes(idx)) {
// 如果索引已被选中,则获取所有已选中的图片信息
selectedFiles = multiSelectedIdxs.value.map((idx) => sortedFiles.value[idx])
} else {
// 否则,只获取当前图片信息
selectedFiles.push(file)
}
return selectedFiles
}
const copyImgTo = async (tab: ['txt2img', 'img2img', 'inpaint', 'extras'][number]) => {
if (spinning.value) {
return
}
try {
spinning.value = true
await setImgPath(file.fullpath) // 设置图像路径
imgTransferBus.postMessage({ ...preset, event: 'click_hidden_button', btnEleId: 'iib_hidden_img_update_trigger' }) // 触发图像组件更新
// ok(await genInfoCompleted(), 'genInfoCompleted timeout') // 等待消息生成完成
await genInfoCompleted() // 等待消息生成完成
imgTransferBus.postMessage({ ...preset, event: 'click_hidden_button', btnEleId: `iib_hidden_tab_${tab}` }) // 触发粘贴
} catch (error) {
console.error(error)
message.error('发送图像失败请携带console的错误消息找开发者')
} finally {
spinning.value = false
}
}
const key = `${e.key}`
if (key.startsWith('toggle-tag-')) {
const tagId = +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!
await tagStore.refreshTags([file.fullpath])
message.success(t(is_remove ? 'removedTagFromImage' : 'addedTagToImage', { tag }))
return
} else if (key.startsWith('batch-add-tag-') || key.startsWith('batch-remove-tag-')) {
const tagId = +key.split('-tag-')[1]
const action = key.includes('add') ? 'add' : 'remove'
const paths = getSelectedImg().map(v => v.fullpath)
await batchUpdateImageTag({
tag_id: tagId,
img_paths: paths,
action
})
await tagStore.refreshTags(paths)
message.success(t(action === 'add' ? 'addCompleted' : 'removeCompleted'))
return
}
switch (e.key) {
case 'previewInNewWindow':
return window.open(url)
case 'saveSelectedAsJson':
return downloadFileInfoJSON(getSelectedImg())
case 'openWithDefaultApp':
return openWithDefaultApp(file.fullpath)
case 'download':{
const selectedFiles = getSelectedImg()
downloadFiles(selectedFiles.map(file => toRawFileUrl(file, true)))
break
}
case 'copyPreviewUrl': {
return copy2clipboardI18n(parent.document.location.origin + url)
}
case 'rename': {
let newPath = await openRenameFileModal(file.fullpath)
newPath = Path.normalize(newPath)
const map = tagStore.tagMap
map.set(newPath, map.get(file.fullpath) ?? [])
map.delete(file.fullpath)
file.fullpath = newPath
file.name = newPath.split(/[\\/]/).pop() ?? ''
return
}
case 'send2txt2img':
return copyImgTo('txt2img')
case 'send2img2img':
return copyImgTo('img2img')
case 'send2inpaint':
return copyImgTo('inpaint')
case 'send2extras':
return copyImgTo('extras')
case 'send2savedDir': {
const dir = global.quickMovePaths.find((v) => v.key === 'outdir_save')
if (!dir) {
return message.error(t('unknownSavedDir'))
}
const absolutePath = Path.normalizeRelativePathToAbsolute(dir.dir, global.conf?.sd_cwd!)
const selectedImg = getSelectedImg()
await moveFiles(
selectedImg.map((v) => v.fullpath),
absolutePath,
true
)
events.emit('removeFiles', {
paths: selectedImg.map((v) => v.fullpath),
loc: currLocation.value
})
events.emit('addFiles', { files: selectedImg, loc: absolutePath })
break
}
case 'send2controlnet-img2img':
case 'send2controlnet-txt2img': {
const type = e.key.split('-')[1] as 'img2img' | 'txt2img'
imgTransferBus.postMessage({ ...preset, event: 'send_to_control_net', type, url: toRawFileUrl(file) })
break
}
case 'send2outpaint': {
imageGenInfo.value = await q.pushAction(() => getImageGenerationInfo(file.fullpath)).res
const [prompt, negPrompt] = (imageGenInfo.value || '').split('\n')
imgTransferBus.postMessage({
...preset,
event: 'send_to_outpaint',
url: toRawFileUrl(file),
prompt,
negPrompt: negPrompt.slice('Negative prompt: '.length)
})
break
}
case 'openWithWalkMode': {
stackCache.set(path, stack.value)
const tab = global.tabList[props.value.tabIdx]
const pane: FileTransferTabPane = {
type: 'local',
key: uniqueId(),
path: file.fullpath,
name: t('local'),
stackKey: path,
mode: 'walk'
}
tab.panes.push(pane)
tab.key = pane.key
break
}
case 'openInNewTab': {
stackCache.set(path, stack.value)
const tab = global.tabList[props.value.tabIdx]
const pane: FileTransferTabPane = {
type: 'local',
key: uniqueId(),
path: file.fullpath,
name: t('local'),
stackKey: path
}
tab.panes.push(pane)
tab.key = pane.key
break
}
case 'openOnTheRight': {
stackCache.set(path, stack.value)
let tab = global.tabList[props.value.tabIdx + 1]
if (!tab) {
tab = { panes: [], key: '', id: uniqueId() }
global.tabList[props.value.tabIdx + 1] = tab
}
const pane: FileTransferTabPane = {
type: 'local',
key: uniqueId(),
path: file.fullpath,
name: t('local'),
stackKey: path
}
tab.panes.push(pane)
tab.key = pane.key
break
}
case 'send2BatchDownload': {
batchDownload.addFiles(getSelectedImg())
break
}
case 'viewGenInfo': {
showGenInfo.value = true
imageGenInfo.value = await q.pushAction(() => getImageGenerationInfo(file.fullpath)).res
break
}
case 'openWithLocalFileBrowser': {
await openFolder(file.fullpath)
break
}
case 'deleteFiles': {
const selectedFiles = getSelectedImg()
const removeFile = async () => {
const paths = selectedFiles.map((v) => v.fullpath)
await deleteFiles(paths)
message.success(t('deleteSuccess'))
events.emit('removeFiles', { paths: paths, loc: currLocation.value })
}
if (selectedFiles.length === 1 && global.ignoredConfirmActions.deleteOneOnly) {
return removeFile()
}
await new Promise<void>((resolve) => {
Modal.confirm({
title: t('confirmDelete'),
maskClosable: true,
width: '60vw',
content:() =>
<div>
<ol style={{ maxHeight: '50vh', overflow: 'auto' }}>
{selectedFiles.map((v) => <li>{v.fullpath.split(/[/\\]/).pop()}</li>)}
</ol>
<MultiSelectTips />
<Checkbox v-model:checked={global.ignoredConfirmActions.deleteOneOnly}>{t('deleteOneOnlySkipConfirm')} ({t('resetOnGlobalSettingsPage')})</Checkbox>
</div>,
async onOk () {
await removeFile()
resolve()
}
})
})
break
}
}
return {}
}
const { isOutside } = useMouseInElement(stackViewEl)
useWatchDocument('keydown', (e) => {
const keysStr = getShortcutStrFromEvent(e)
if (previewing.value) {
const action = Object.entries(global.shortcut).find(
(v) => v[1] === keysStr && v[1]
)?.[0] as keyof Shortcut
if (action) {
e.stopPropagation()
e.preventDefault()
const idx = previewIdx.value
const file = sortedFiles.value[idx]
switch (action) {
case 'delete': {
if (toRawFileUrl(file) === global.fullscreenPreviewInitialUrl) {
return message.warn(t('fullscreenRestriction'))
}
return onContextMenuClick({ key: 'deleteFiles' } as MenuInfo, file, idx)
}
case 'download': {
return onContextMenuClick({ key: 'download' } as MenuInfo, file, idx)
}
default: {
const name = /^toggle_tag_(.*)$/.exec(action)?.[1]
const tag = global.conf?.all_custom_tags.find((v) => v.name === name)
if (!tag) return
return onContextMenuClick({ key: `toggle-tag-${tag.id}` } as MenuInfo, file, idx)
}
}
}
} else if (!isOutside.value && ['Ctrl + KeyA', 'Cmd + KeyA'].includes(keysStr)) {
e.preventDefault()
e.stopPropagation()
eventEmitter.value.emit('selectAll')
}
})
return {
onFileItemClick,
onContextMenuClick,
showGenInfo,
imageGenInfo,
q
}
}

View File

@ -0,0 +1,110 @@
import { watch } from 'vue'
import {
useWatchDocument
} from 'vue3-ts-util'
import { FileTransferData, getFileTransferDataFromDragEvent } from '@/util/file'
import { useHookShareState, global, events, sli } from '.'
import { copyFiles, moveFiles } from '@/api/files'
import { MultiSelectTips } from '@/components/functionalCallableComp'
import { t } from '@/i18n'
import { createReactiveQueue } from '@/util'
import { Modal, Button } from 'ant-design-vue'
import { cloneDeep, uniqBy } from 'lodash-es'
export function useFileTransfer () {
const { currLocation, sortedFiles, currPage, multiSelectedIdxs, eventEmitter, walker } =
useHookShareState().toRefs()
const recover = () => {
multiSelectedIdxs.value = []
}
useWatchDocument('click', () => {
if (!global.keepMultiSelect) {
recover()
}
})
useWatchDocument('blur', () => {
if (!global.keepMultiSelect) {
recover()
}
})
watch(currPage, recover)
const onFileDragStart = (e: DragEvent, idx: number) => {
const file = cloneDeep(sortedFiles.value[idx])
sli.fileDragging = true
console.log('onFileDragStart set drag file ', e, idx, file)
const files = [file]
let includeDir = file.type === 'dir'
if (multiSelectedIdxs.value.includes(idx)) {
const selectedFiles = multiSelectedIdxs.value.map((idx) => sortedFiles.value[idx])
files.push(...selectedFiles)
includeDir = selectedFiles.some((v) => v.type === 'dir')
}
const data: FileTransferData = {
includeDir,
loc: currLocation.value || 'search-result',
path: uniqBy(files, 'fullpath').map((f) => f.fullpath),
nodes: uniqBy(files, 'fullpath'),
__id: 'FileTransferData'
}
e.dataTransfer!.setData('text/plain', JSON.stringify(data))
}
const onFileDragEnd = () => {
sli.fileDragging = false
}
const onDrop = async (e: DragEvent) => {
if (walker.value) {
return
}
const data = getFileTransferDataFromDragEvent(e)
if (!data) {
return
}
const toPath = currLocation.value
if (data.loc === toPath) {
return
}
const q = createReactiveQueue()
const onCopyBtnClick = async () => q.pushAction(async () => {
await copyFiles(data.path, toPath)
eventEmitter.value.emit('refresh')
Modal.destroyAll()
})
const onMoveBtnClick = () => q.pushAction(async () => {
await moveFiles(data.path, toPath)
events.emit('removeFiles', { paths: data.path, loc: data.loc })
eventEmitter.value.emit('refresh')
Modal.destroyAll()
})
Modal.confirm({
title: t('confirm') + '?',
width: '60vw',
content: () => <div>
<div>
{`${t('moveSelectedFilesTo')} ${toPath}`}
<ol style={{ maxHeight: '50vh', overflow: 'auto' }}>
{data.path.map((v) => <li>{v.split(/[/\\]/).pop()}</li>)}
</ol>
</div>
<MultiSelectTips />
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-end' }} class="actions">
<Button onClick={Modal.destroyAll}>{t('cancel')}</Button>
<Button type="primary" loading={!q.isIdle} onClick={onCopyBtnClick}>{t('copy')}</Button>
<Button type="primary" loading={!q.isIdle} onClick={onMoveBtnClick}>{t('move')}</Button>
</div>
</div>,
maskClosable: true,
wrapClassName: 'hidden-antd-btns-modal'
})
}
return {
onFileDragStart,
onDrop,
multiSelectedIdxs,
onFileDragEnd
}
}

View File

@ -0,0 +1,131 @@
import { useElementSize } from '@vueuse/core'
import { ref, computed, watch, reactive } from 'vue'
import { Top4MediaInfo, batchGetDirTop4MediaInfo } from '@/api'
import {
delay} from 'vue3-ts-util'
import { sortMethodConv } from '../fileSort'
import { debounce } from 'lodash-es'
import { isMediaFile } from '@/util/file'
import { useHookShareState, global, tagStore } from '.'
export function useFilesDisplay ({ fetchNext }: {fetchNext?: () => Promise<any>} = { }) {
const {
scroller,
sortedFiles,
sortMethod,
currLocation,
stackViewEl,
canLoadNext,
previewIdx,
props,
walker
} = useHookShareState().toRefs()
const { state } = useHookShareState()
const moreActionsDropdownShow = ref(false)
const cellWidth = ref(global.defaultGridCellWidth)
const gridSize = computed(() => cellWidth.value + 16) // margin 8
const profileHeight = 44
const { width } = useElementSize(stackViewEl)
const gridItems = computed(() => ~~(width.value / gridSize.value))
const dirCoverCache = reactive(new Map<string, Top4MediaInfo[]>())
const itemSize = computed(() => {
const second = gridSize.value
const first = second + (cellWidth.value <= 160 ? 0 : profileHeight)
return {
first,
second
}
})
const loadNextDirLoading = ref(false)
const loadNextDir = async () => {
if (loadNextDirLoading.value || props.value.mode !== 'walk' || !canLoadNext.value) {
return
}
try {
loadNextDirLoading.value = true
await walker.value?.next()
} finally {
loadNextDirLoading.value = false
}
}
// 填充够一页,直到不行为止
const fetchDataUntilViewFilled = async (isFullScreenPreview = false) => {
const s = scroller.value
const currIdx = () => (isFullScreenPreview ? previewIdx.value : s?.$_endIndex ?? 0)
const needLoad = () => {
const len = sortedFiles.value.length
const preload = 50
if (!len) {
return true
}
if (fetchNext) {
return currIdx() > len - preload
}
return currIdx() > len - preload && canLoadNext.value // canLoadNext 是walker的表示加载完成
}
while (needLoad()) {
await delay(30)
const ret = await (fetchNext ?? loadNextDir)()
if (typeof ret === 'boolean' && !ret) {
return // 返回false同样表示加载完成
}
}
}
state.useEventListen('loadNextDir', fetchDataUntilViewFilled)
const onViewableAreaChange = () => {
const s = scroller.value
if (s) {
const startIdx = Math.max(s.$_startIndex - 10, 0)
const viewableAreaFiles = sortedFiles.value.slice(startIdx, s.$_endIndex + 10)
state.eventEmitter.emit('viewableAreaFilesChange', { files: viewableAreaFiles, startIdx })
const fetchTagPaths = viewableAreaFiles
.filter(v => v.is_under_scanned_path && isMediaFile(v.name))
.map(v => v.fullpath)
tagStore.fetchImageTags(fetchTagPaths)
const fetchDirTop4MediaPaths = viewableAreaFiles
.filter(v => v.is_under_scanned_path && v.type === 'dir' && !dirCoverCache.has(v.fullpath))
.map(v => v.fullpath)
if (fetchDirTop4MediaPaths.length) {
batchGetDirTop4MediaInfo(fetchDirTop4MediaPaths).then(v => {
for (const key in v) {
if (Object.prototype.hasOwnProperty.call(v, key)) {
const element = v[key];
dirCoverCache.set(key, element)
}
}
})
}
}
}
const onViewableAreaChangeDebounced = debounce(onViewableAreaChange, 300)
watch(currLocation, onViewableAreaChangeDebounced)
const onScroll = debounce(async () => {
await fetchDataUntilViewFilled()
onViewableAreaChangeDebounced()
}, 150)
return {
gridItems,
sortedFiles,
sortMethodConv,
moreActionsDropdownShow,
gridSize,
sortMethod,
onScroll,
loadNextDir,
loadNextDirLoading,
canLoadNext,
itemSize,
cellWidth,
dirCoverCache
}
}

View File

@ -0,0 +1,121 @@
import { ref } from 'vue'
import { global, useHookShareState } from '.'
import { getImageGenerationInfoBatch } from '@/api'
import { FileNodeInfo, GenDiffInfo } from '@/api/files'
import { parse } from '@/util/stable-diffusion-image-metadata'
import { delay } from 'vue3-ts-util'
import { isMediaFile } from '@/util'
const geninfocache = new Map<string, string>()
export const useGenInfoDiff = () => {
const { useEventListen, sortedFiles } = useHookShareState().toRefs()
const changeIndchecked = ref<boolean>(global.defaultChangeIndchecked)
const seedChangeChecked = ref<boolean>(global.defaultSeedChangeChecked)
const setGenInfo = async ({ files }: { files: FileNodeInfo[], startIdx: number }) => {
await delay(100)
if (!changeIndchecked.value) return
files = files.filter(v => isMediaFile(v.fullpath) && !v.gen_info_obj)
if (!files.length) return
const geninfos = await getImageGenerationInfoBatch(files.map(v => v.fullpath).filter(v => !geninfocache.has(v)))
files.forEach(item => {
const info = geninfos[item.fullpath] || geninfocache.get(item.fullpath) || ''
geninfocache.set(item.fullpath, info)
item.gen_info_obj = parse(info)
item.gen_info_raw = info
})
}
useEventListen.value('viewableAreaFilesChange',setGenInfo)
const getGenDiffWatchDep = (idx: number) => {
const fs = sortedFiles.value
return [idx, seedChangeChecked.value , fs[idx-1], fs[idx], fs[idx+1] ]
}
function getGenDiff (ownGenInfo: any, idx: any, increment: any, ownFile: FileNodeInfo) {
//init result obj
const result: GenDiffInfo = {
diff: {},
empty: true,
ownFile: '',
otherFile: ''
}
//check for out of bounds
if (idx + increment < 0
|| idx + increment >= sortedFiles.value.length
|| sortedFiles.value[idx] == undefined) {
return result
}
//check for gen_info_obj existence
if (!('gen_info_obj' in sortedFiles.value[idx])
|| !('gen_info_obj' in sortedFiles.value[idx + increment])) {
return result
}
//diff vars init
const gen_a = ownGenInfo
const gen_b: any = sortedFiles.value[idx + increment].gen_info_obj
if (gen_b == undefined) {
return result
}
//further vars
const skip = ['hashes', 'resources']
result.diff = {}
result.ownFile = ownFile.name,
result.otherFile = sortedFiles.value[idx + increment].name,
result.empty = false
if (!seedChangeChecked.value) {
skip.push('seed')
}
//actual per property diff
for (const k in gen_a) {
//skip unwanted values
if (skip.includes(k)) {
continue
}
//for all non-identical values, compare type based
//existence test
if (!(k in gen_b)) {
result.diff[k] = '+'
continue
}
//content test
if (gen_a[k] != gen_b[k]) {
if (k.includes('rompt') && gen_a[k] != '' && gen_b[k] != '') {
//prompt values are comma separated, handle them differently
const tokenize_a = gen_a[k].split(',')
const tokenize_b = gen_b[k].split(',')
//count how many tokens are different or at a different place
let diff_count = 0
for (const i in tokenize_a) {
if (tokenize_a[i] != tokenize_b[i]) {
diff_count++
}
}
result.diff[k] = diff_count
} else {
//all others
result.diff[k] = [gen_a[k], gen_b[k]]
}
}
}
//result
return result
}
return {
getGenDiff,
changeIndchecked,
seedChangeChecked,
getRawGenParams: () => setGenInfo({ files: sortedFiles.value, startIdx: 0 }),
getGenDiffWatchDep
}
}

View File

@ -0,0 +1,370 @@
import { getTargetFolderFiles, FileNodeInfo } from '@/api/files'
import { openCreateFlodersModal } from '@/components/functionalCallableComp'
import { t } from '@/i18n'
import { DatabaseOutlined } from '@/icon'
import { TagSearchTabPane, FuzzySearchTabPane, FileTransferTabPane, EmptyStartTabPane } from '@/store/useGlobalStore'
import { copy2clipboardI18n, makeAsyncFunctionSingle, useGlobalEventListen } from '@/util'
import { message } from 'ant-design-vue'
import { ref, watch, onMounted, h, computed } from 'vue'
import { delay, ok, useWatchDocument } from 'vue3-ts-util'
import { useHookShareState, stackCache, global } from '.'
import * as Path from '@/util/path'
import type Progress from 'nprogress'
// @ts-ignore
import NProgress from 'multi-nprogress'
import { cloneDeep, debounce, last, uniqueId } from 'lodash-es'
/**
*
*/
export function useLocation () {
const np = ref<Progress.NProgress>()
const {
scroller,
stackViewEl,
stack,
currPage,
currLocation,
useEventListen,
eventEmitter,
getPane,
props,
deletedFiles,
walker,
sortedFiles
} = useHookShareState().toRefs()
watch(
() => stack.value.length,
debounce((v, lv) => {
if (v !== lv) {
scroller.value?.scrollToItem(0)
}
}, 300)
)
const handleWalkModeTo = async (path: string) => {
await to(path)
if (props.value.mode === 'walk') {
await delay()
await walker.value?.reset()
eventEmitter.value.emit('loadNextDir')
}
}
onMounted(async () => {
if (!stack.value.length) {
// 有传入stack时直接使用传入的
const resp = await getTargetFolderFiles('/')
stack.value.push({
files: resp.files,
curr: '/'
})
}
np.value = new NProgress()
np.value!.configure({ parent: stackViewEl.value as any })
if (props.value.path && props.value.path !== '/') {
await handleWalkModeTo(props.value.path)
} else {
global.conf?.home && to(global.conf.home)
}
})
watch(
currLocation,
debounce((loc) => {
const pane = getPane.value()
if (!pane) {
return
}
pane.path = loc
const filename = pane.path!.split('/').pop() ?? ''
const getTitle = () => {
const prefix = {
walk: 'Walk',
'scanned-fixed': 'Fixed',
scanned: null
}[props.value.mode ?? 'scanned']
const wrap = (v: string) => prefix ? `${prefix}: ${v}` : v
const np = Path.normalize(loc)
for (const [k, v] of Object.entries(global.pathAliasMap)) {
if (np.startsWith(v)) {
return wrap(np.replace(v, k))
}
}
return wrap(filename)
}
const title = getTitle()
pane.name = h('div', { style: 'display:flex;align-items:center' }, [
h(DatabaseOutlined),
h('span', { class: 'line-clamp-1', style: 'max-width: 256px' }, title)
])
pane.nameFallbackStr = title
global.recent = global.recent.filter((v) => v.key !== pane.key)
global.recent.unshift({ path: loc, key: pane.key })
if (global.recent.length > 20) {
global.recent = global.recent.slice(0, 20)
}
}, 300)
)
const copyLocation = () => copy2clipboardI18n(currLocation.value)
const openNext = async (file: FileNodeInfo) => {
if (file.type !== 'dir') {
return
}
try {
np.value?.start()
const { files } = await getTargetFolderFiles(file.fullpath)
if (props.value.mode == 'scanned-fixed') {
stack.value = [{
files,
curr: file.fullpath
}]
} else {
stack.value.push({
files,
curr: file.name
})
}
} finally {
np.value?.done()
}
}
const back = (idx: number) => {
while (idx < stack.value.length - 1) {
stack.value.pop()
}
}
const backToLastUseTo = () => {
const lastLevelPath = Path.join(...Path.splitPath(currLocation.value).slice(0, -1))
to(lastLevelPath)
}
const isDirNameEqual = (a: string, b: string) => {
ok(global.conf, 'global.conf load failed')
if (global.conf.is_win) {
// window下忽略
return a.toLowerCase() == b.toLowerCase()
}
return a == b
}
const to = async (dir: string) => {
if (props.value.mode === 'scanned-fixed') {
return openNext({ fullpath: dir, name: dir, type: 'dir' } as FileNodeInfo)
}
const backup = stack.value.slice()
try {
if (!Path.isAbsolute(dir)) {
// 相对路径
dir = Path.join(global.conf?.sd_cwd ?? '/', dir)
}
const frags = Path.splitPath(dir)
const currPaths = stack.value.map((v) => v.curr)
currPaths.shift() // 是 /
while (currPaths[0] && frags[0]) {
if (!isDirNameEqual(currPaths[0], frags[0])) {
break
} else {
currPaths.shift()
frags.shift()
}
}
for (let index = 0; index < currPaths.length; index++) {
stack.value.pop()
}
if (!frags.length) {
return refresh()
}
for (const frag of frags) {
const target = currPage.value?.files.find((v) => isDirNameEqual(v.name, frag))
if (!target) {
console.error({ frags, frag, stack: cloneDeep(stack.value) })
throw new Error(`${frag} not found`)
}
await openNext(target)
}
} catch (error) {
message.error(t('moveFailedCheckPath') + (error instanceof Error ? error.message : ''))
console.error(dir, Path.splitPath(dir), currPage.value)
stack.value = backup
throw error
}
}
const refresh = makeAsyncFunctionSingle(async () => {
try {
np.value?.start()
if (walker.value) {
await walker.value.reset()
eventEmitter.value.emit('loadNextDir')
} else {
const { files } = await getTargetFolderFiles(
stack.value.length === 1 && props.value.mode !== 'scanned-fixed' ? '/' : currLocation.value
)
last(stack.value)!.files = files
}
deletedFiles.value.clear()
scroller.value?.scrollToItem(0)
message.success(t('refreshCompleted'))
} finally {
np.value?.done()
}
})
useGlobalEventListen(
'returnToIIB',
makeAsyncFunctionSingle(async () => {
if (props.value.mode === 'walk') return
try {
np.value?.start()
const { files } = await getTargetFolderFiles(
stack.value.length === 1 && props.value.mode !== 'scanned-fixed' ? '/' : currLocation.value
)
const currFiles = last(stack.value)!.files
if (currFiles.map((v) => v.date).join() !== files.map((v) => v.date).join()) {
last(stack.value)!.files = files
message.success(t('autoUpdate'))
}
} finally {
np.value?.done()
}
})
)
useEventListen.value('refresh', refresh)
const quickMoveTo = (path: string) => {
// todo
handleWalkModeTo(path)
}
const normalizedScandPath = computed(() => {
return global.quickMovePaths.map((v) => ({ ...v, path: Path.normalize(v.dir) }))
})
const searchPathInfo = computed(() => {
const c = Path.normalize(currLocation.value)
const path = normalizedScandPath.value.find((v) => v.path === c)
return path
})
const addToSearchScanPathAndQuickMove = async () => {
const tab = global.tabList[props.value.tabIdx]
const pane: EmptyStartTabPane = {
type: 'empty',
name: t('emptyStartPage'),
key: Date.now() + uniqueId(),
popAddPathModal: {
path: currLocation.value,
type: 'scanned'
}
}
tab.panes.push(pane)
tab.key = pane.key
}
const isLocationEditing = ref(false)
const locInputValue = ref(currLocation.value)
const onEditBtnClick = () => {
isLocationEditing.value = true
locInputValue.value = currLocation.value
}
const onLocEditEnter = async () => {
await to(locInputValue.value)
isLocationEditing.value = false
}
useWatchDocument('click', (e) => {
if (!(e.target as HTMLElement)?.className?.includes?.('ant-input')) {
isLocationEditing.value = false
}
})
const share = () => {
const loc = parent.location
const baseUrl = loc.href.substring(0, loc.href.length - loc.search.length)
const params = new URLSearchParams(loc.search)
params.set('action', 'open')
if (walker.value) {
params.set('walk', '1')
}
params.set('path', currLocation.value)
params.set('mode', props.value.mode ?? 'scanned')
const url = `${baseUrl}?${params.toString()}`
copy2clipboardI18n(url, t('copyLocationUrlSuccessMsg'))
}
const searchInCurrentDir = (type: (TagSearchTabPane | FuzzySearchTabPane)['type'] = 'tag-search') => {
const tab = global.tabList[props.value.tabIdx]
const pane = {
type,
key: uniqueId(),
searchScope: currLocation.value,
name: t(type === 'tag-search' ? 'imgSearch' : 'fuzzy-search'),
}
tab.panes.push(pane)
tab.key = pane.key
}
const selectAll = () => eventEmitter.value.emit('selectAll')
const onCreateFloderBtnClick = async () => {
await openCreateFlodersModal(currLocation.value)
await refresh()
}
const onWalkBtnClick = () => {
const path = currLocation.value
stackCache.set(path, stack.value)
const tab = global.tabList[props.value.tabIdx]
const pane: FileTransferTabPane = {
type: 'local',
key: uniqueId(),
path: path,
name: t('local'),
stackKey: path,
mode: 'walk'
}
tab.panes.push(pane)
tab.key = pane.key
}
const showWalkButton = computed(() => !walker.value && sortedFiles.value.some(v => v.type === 'dir'))
return {
locInputValue,
isLocationEditing,
onLocEditEnter,
onEditBtnClick,
addToSearchScanPathAndQuickMove,
searchPathInfo,
refresh,
copyLocation,
back,
openNext,
currPage,
currLocation,
to,
stack,
scroller,
share,
selectAll,
quickMoveTo,
onCreateFloderBtnClick,
onWalkBtnClick,
showWalkButton,
searchInCurrentDir,
backToLastUseTo
}
}

View File

@ -0,0 +1,124 @@
import { t } from '@/i18n'
import { isImageFile } from '@/util'
import { message } from 'ant-design-vue'
import { useWatchDocument, delay } from 'vue3-ts-util'
import { useHookShareState, useEventListen } from '.'
/**
*
* @param props
* @returns
*/
export function usePreview () {
const {
previewIdx,
eventEmitter,
canLoadNext,
previewing,
sortedFiles: files,
scroller,
props
} = useHookShareState().toRefs()
const { state } = useHookShareState()
let waitScrollTo = null as number | null
const onPreviewVisibleChange = (v: boolean, lv: boolean) => {
previewing.value = v
if (waitScrollTo != null && !v && lv) {
// 关闭预览时滚动过去
scroller.value?.scrollToItem(waitScrollTo)
waitScrollTo = null
}
}
const loadNextIfNeeded = () => {
if (props.value.mode === 'walk') {
if (!canPreview('next') && canLoadNext) {
message.info(t('loadingNextFolder'))
eventEmitter.value.emit('loadNextDir', true) // 如果在全屏查看时外面scroller可能还停留在很久之前使用全屏查看的索引
}
}
}
useWatchDocument('keydown', (e) => {
if (previewing.value) {
let next = previewIdx.value
if (['ArrowDown', 'ArrowRight'].includes(e.key)) {
next++
while (files.value[next] && !isImageFile(files.value[next].name)) {
next++
}
} else if (['ArrowUp', 'ArrowLeft'].includes(e.key)) {
next--
while (files.value[next] && !isImageFile(files.value[next].name)) {
next--
}
}
if (isImageFile(files.value[next]?.name) ?? '') {
previewIdx.value = next
const s = scroller.value
if (s && !(next >= s.$_startIndex && next <= s.$_endIndex)) {
waitScrollTo = next // 关闭预览时滚动过去
}
}
loadNextIfNeeded()
}
})
const previewImgMove = (type: 'next' | 'prev') => {
let next = previewIdx.value
if (type === 'next') {
next++
while (files.value[next] && !isImageFile(files.value[next].name)) {
next++
}
} else if (type === 'prev') {
next--
while (files.value[next] && !isImageFile(files.value[next].name)) {
next--
}
}
if (isImageFile(files.value[next]?.name) ?? '') {
previewIdx.value = next
const s = scroller.value
if (s && !(next >= s.$_startIndex && next <= s.$_endIndex)) {
waitScrollTo = next // 关闭预览时滚动过去
}
}
loadNextIfNeeded()
}
const canPreview = (type: 'next' | 'prev') => {
let next = previewIdx.value
if (type === 'next') {
next++
while (files.value[next] && !isImageFile(files.value[next].name)) {
next++
}
} else if (type === 'prev') {
next--
while (files.value[next] && !isImageFile(files.value[next].name)) {
next--
}
}
return isImageFile(files.value[next]?.name) ?? ''
}
useEventListen('removeFiles', async () => {
if (previewing.value && !state.sortedFiles[previewIdx.value]) {
message.info(t('manualExitFullScreen'), 5)
await delay(500)
; (
document.querySelector(
'.ant-image-preview-operations-operation .anticon-close'
) as HTMLDivElement
)?.click()
previewIdx.value = -1
}
})
return {
previewIdx,
onPreviewVisibleChange,
previewing,
previewImgMove,
canPreview
}
}

View File

@ -1,5 +1,5 @@
<script setup lang="ts">
import { DownOutlined, LeftCircleOutlined, RightCircleOutlined } from '@/icon'
import { DownOutlined, LeftCircleOutlined, RightCircleOutlined, ArrowLeftOutlined } from '@/icon'
import { useGlobalStore } from '@/store/useGlobalStore'
import {
useFileTransfer,
@ -10,7 +10,9 @@ import {
useFileItemActions,
useMobileOptimization,
stackCache,
useKeepMultiSelect
useKeepMultiSelect,
Props,
useGenInfoDiff
} from './hook'
import { SearchSelect } from 'vue3-ts-util'
import { toRawFileUrl } from '@/util/file'
@ -24,12 +26,9 @@ import FileItem from '@/components/FileItem.vue'
import fullScreenContextMenu from './fullScreenContextMenu.vue'
import BaseFileListInfo from '@/components/BaseFileListInfo.vue'
import { copy2clipboardI18n } from '@/util'
import { openFolder, getImageGenerationInfoBatch } from '@/api'
import { openFolder } from '@/api'
import { sortMethods } from './fileSort'
import { isTauri } from '@/util/env'
import { parse } from '@/util/stable-diffusion-image-metadata'
import { ref } from 'vue'
import type { FileNodeInfo, GenDiffInfo } from '@/api/files'
import MultiSelectKeep from '@/components/MultiSelectKeep.vue'
const global = useGlobalStore()
@ -40,7 +39,7 @@ const props = defineProps<{
* 初始打开路径
*/
path?: string
walkModePath?: string
mode?: Props['mode']
/**
* 页面栈,跳过不必要的api请求
*/
@ -54,9 +53,9 @@ const {
spinning
} = useHookShareState().toRefs()
const { currLocation, currPage, refresh, copyLocation, back, openNext, stack, quickMoveTo,
addToSearchScanPathAndQuickMove, searchPathInfo, locInputValue, isLocationEditing,
addToSearchScanPathAndQuickMove, locInputValue, isLocationEditing,
onLocEditEnter, onEditBtnClick, share, selectAll, onCreateFloderBtnClick, onWalkBtnClick,
showWalkButton, searchInCurrentDir
showWalkButton, searchInCurrentDir, backToLastUseTo
} = useLocation()
const {
gridItems,
@ -77,6 +76,7 @@ const { onFileItemClick, onContextMenuClick, showGenInfo, imageGenInfo, q } = us
const { previewIdx, onPreviewVisibleChange, previewing, previewImgMove, canPreview } = usePreview()
const { showMenuIdx } = useMobileOptimization()
const { onClearAllSelected, onReverseSelect, onSelectAll } = useKeepMultiSelect()
const { getGenDiff, changeIndchecked, seedChangeChecked, getRawGenParams, getGenDiffWatchDep } = useGenInfoDiff()
watch(
() => props,
@ -90,109 +90,6 @@ watch(
{ immediate: true }
)
watch(sortedFiles, async (newList, oldList) => {
//check files in newList if it is an image-only list
if (newList.length > 0 && newList.length !== oldList.length) {
getRawGenParams()
}
})
const changeIndchecked = ref<boolean>(global.defaultChangeIndchecked)
const seedChangeChecked = ref<boolean>(global.defaultSeedChangeChecked)
function getRawGenParams () {
//extract fullpaths of all files from sortedfiles to array, but only if it's an actual file (not a folder or something else)
let paths: string[] = []
const allowedExtensions = ['.png', '.jpg', '.jpeg']
for (let f in sortedFiles.value) {
if (sortedFiles.value[f].type == 'file' && allowedExtensions.includes(sortedFiles.value[f].fullpath.slice(-4).toLowerCase())) {
paths.push(sortedFiles.value[f].fullpath)
}
}
q.pushAction(() => getImageGenerationInfoBatch(paths)).res.then((v) => {
//result is a json object with fullpath as key and gen_info_raw as value
for (let f in sortedFiles.value) {
sortedFiles.value[f].gen_info_raw = v[sortedFiles.value[f].fullpath]
sortedFiles.value[f].gen_info_obj = parse(v[sortedFiles.value[f].fullpath])
}
})
}
function getGenDiff (ownGenInfo: any, idx: any, increment: any, ownFile: FileNodeInfo) {
//init result obj
let result: GenDiffInfo = {
diff: {},
empty: true,
ownFile: '',
otherFile: ''
}
//check for out of bounds
if (idx + increment < 0
|| idx + increment >= sortedFiles.value.length
|| sortedFiles.value[idx] == undefined) {
return result
}
//check for gen_info_obj existence
if (!('gen_info_obj' in sortedFiles.value[idx])
|| !('gen_info_obj' in sortedFiles.value[idx + increment])) {
return result
}
//diff vars init
let gen_a = ownGenInfo
let gen_b: any = sortedFiles.value[idx + increment].gen_info_obj
if (gen_b == undefined) {
return result
}
//further vars
let skip = ['hashes', 'resources']
result.diff = {}
result.ownFile = ownFile.name,
result.otherFile = sortedFiles.value[idx + increment].name,
result.empty = false
if (!seedChangeChecked.value) {
skip.push('seed')
}
//actual per property diff
for (let k in gen_a) {
//skip unwanted values
if (skip.includes(k)) {
continue
}
//for all non-identical values, compare type based
//existence test
if (!(k in gen_b)) {
result.diff[k] = '+'
continue
}
//content test
if (gen_a[k] != gen_b[k]) {
if (k.includes('rompt') && gen_a[k] != '' && gen_b[k] != '') {
//prompt values are comma separated, handle them differently
let tokenize_a = gen_a[k].split(',')
let tokenize_b = gen_b[k].split(',')
//count how many tokens are different or at a different place
let diff_count = 0
for (let i in tokenize_a) {
if (tokenize_a[i] != tokenize_b[i]) {
diff_count++
}
}
result.diff[k] = diff_count
} else {
//all others
result.diff[k] = [gen_a[k], gen_b[k]]
}
}
}
//result
return result
}
</script>
<template>
@ -232,6 +129,7 @@ function getGenDiff (ownGenInfo: any, idx: any, increment: any, ownFile: FileNod
<AButton size="small" v-if="isLocationEditing" @click="onLocEditEnter" type="primary">{{ $t('go') }}</AButton>
<div v-else class="location-act">
<a @click.prevent="backToLastUseTo" style="margin: 0 8px 16px 0;" v-if="mode === 'scanned-fixed'"><ArrowLeftOutlined /></a>
<a @click.prevent="copyLocation" class="copy">{{ $t('copy') }}</a>
<a @click.prevent.stop="onEditBtnClick">{{ $t('edit') }}</a>
</div>
@ -302,10 +200,8 @@ function getGenDiff (ownGenInfo: any, idx: any, increment: any, ownFile: FileNod
<a-switch v-model:checked="seedChangeChecked" :disabled="!changeIndchecked" />
</a-form-item>
<div style="padding: 4px;">
<a @click.prevent="addToSearchScanPathAndQuickMove" v-if="!searchPathInfo">{{
<a @click.prevent="addToSearchScanPathAndQuickMove" >{{
$t('addToSearchScanPathAndQuickMove') }}</a>
<a @click.prevent="addToSearchScanPathAndQuickMove" v-else-if="searchPathInfo.can_delete">{{
$t('removeFromSearchScanPathAndQuickMove') }}</a>
</div>
<div style="padding: 4px;">
<a @click.prevent="openFolder(currLocation + '/')">{{ $t('openWithLocalFileBrowser') }}</a>
@ -331,14 +227,15 @@ function getGenDiff (ownGenInfo: any, idx: any, increment: any, ownFile: FileNod
@file-item-click="onFileItemClick" @dragstart="onFileDragStart" @dragend="onFileDragEnd"
@preview-visible-change="onPreviewVisibleChange" @context-menu-click="onContextMenuClick"
:is-selected-mutil-files="multiSelectedIdxs.length > 1"
:gen-diff-to-next="getGenDiff(file.gen_info_obj, idx, 1, file)"
:gen-diff-to-previous="getGenDiff(file.gen_info_obj, idx, -1, file)"
:enable-change-indicator="changeIndchecked"
:enable-change-indicator="changeIndchecked"
:seed-change-checked="seedChangeChecked"
:get-gen-diff="getGenDiff"
:get-gen-diff-watch-dep="getGenDiffWatchDep"
:cover-files="dirCoverCache.get(file.fullpath)"/>
</template>
<template #after>
<div style="padding: 16px 0 512px;">
<AButton v-if="props.walkModePath" @click="loadNextDir" :loading="loadNextDirLoading" block type="primary"
<AButton v-if="props.mode === 'walk'" @click="loadNextDir" :loading="loadNextDirLoading" block type="primary"
:disabled="!canLoadNext" ghost>
{{ $t('loadNextPage') }}</AButton>
</div>

View File

@ -25,7 +25,7 @@ export const resolveQueryActions = async (g: ReturnType<typeof useGlobalStore>)
path,
key: uniqueId(),
name: '',
walkModePath: params.get('walk') ? path : undefined
mode: params.get('walk') ? 'walk' : 'scanned'
}
tab.panes.unshift(pane)
tab.key = pane.key

View File

@ -1,9 +1,10 @@
import type { GlobalConf } from '@/api'
import type { MatchImageByTagsReq, Tag } from '@/api/db'
import type { ExtraPathType, 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 { Props as FileTransferProps } from '@/page/fileTransfer/hooks'
import type { getQuickMovePaths } from '@/page/taskRecord/autoComplete'
import { type Dict, type ReturnTypeAsync } from '@/util'
import { AnyFn, usePreferredDark } from '@vueuse/core'
@ -20,7 +21,15 @@ interface TabPaneBase {
}
interface OtherTabPane extends TabPaneBase {
type: 'empty' | 'global-setting' | 'tag-search' | 'batch-download'
type: 'global-setting' | 'tag-search' | 'batch-download'
}
export interface EmptyStartTabPane extends TabPaneBase {
type: 'empty'
popAddPathModal?: {
path: string
type: ExtraPathType
}
}
export type GridViewFileTag = WithRequired<Partial<Tag>, 'name'>;
@ -88,7 +97,7 @@ export interface ImgSliTabPane extends TabPaneBase {
export interface FileTransferTabPane extends TabPaneBase {
type: 'local'
path?: string
walkModePath?: string
mode?: FileTransferProps['mode']
stackKey?: string
}
@ -102,7 +111,7 @@ export interface FuzzySearchTabPane extends TabPaneBase {
searchScope?: string
}
export type TabPane = FileTransferTabPane | OtherTabPane | TagSearchMatchedImageGridTabPane | ImgSliTabPane | TagSearchTabPane | FuzzySearchTabPane| GridViewTabPane
export type TabPane = EmptyStartTabPane | FileTransferTabPane | OtherTabPane | TagSearchMatchedImageGridTabPane | ImgSliTabPane | TagSearchTabPane | FuzzySearchTabPane| GridViewTabPane
/**
* This interface represents a tab, which contains an array of panes, an ID, and a key