mirror of
https://github.com/vladmandic/automatic
synced 2026-09-19 09:14:35 +02:00
initial version of native gallery
This commit is contained in:
@@ -89,6 +89,8 @@
|
||||
// imageviewer.js
|
||||
"modalPrevImage": "readonly",
|
||||
"modalNextImage": "readonly",
|
||||
"galleryClickEventHandler": "readonly",
|
||||
"getExif": "readonly",
|
||||
// logMonitor.js
|
||||
"jobStatusEl": "readonly",
|
||||
// loader.js
|
||||
|
||||
+25
-1
@@ -5,10 +5,34 @@
|
||||
- Include reference styles
|
||||
- Quick apply style
|
||||
- Add refine workflow in img2img
|
||||
- New image browser
|
||||
|
||||
## Update for 2024-03-20
|
||||
|
||||
Gallery:
|
||||
- All operations are async, non-blocking and auto-cancelled as needed
|
||||
This gives up a little bit of raw performance, but there is no "wait for current folder which has 10k images"
|
||||
Right now enumerate is ~100-300 images/sec which is sufficient to show first screen of images near-instant
|
||||
- Images are loaded using lazy-loading, meaning on-demand when they are scrolled into view
|
||||
But...If you want to have 10k images in a folder and then scroll-to-bottom, well...
|
||||
- Majority of work is done client-side in browser without needing for typical Gradio round-trip
|
||||
- Nothing cached by choice, images are always up-to-date (I may add thumbnail caching later)
|
||||
- Search is matching any part of folder/filename and image properties, but not image metadata (yet)
|
||||
- Search allows for syntax like `size > 1000000` or `width > 1000`
|
||||
Operators are `<>=` and keys are `size`, `width`, `height`, `mtime`
|
||||
- Sort and search can easily handle 20k images-per-sec
|
||||
- Folders are scanned recursively
|
||||
- Optional user-defined folders: settings -> image options -> image browser
|
||||
- Thumbnails can be fixed or varible width, set in settings -> image options -> image browser
|
||||
Gallery ToDo:
|
||||
- Send to buttons row
|
||||
- Client-side caching? thumbnails, metadata
|
||||
- Search by metadata
|
||||
|
||||
Changes:
|
||||
- Removed built-in extensions: *ControlNet* and *Image-Browser*
|
||||
as both *image-browser* and *controlnet* have native equivalents
|
||||
both can still be installed by user if desired
|
||||
Improvements:
|
||||
- Styles apply wildcards to params
|
||||
- Make metadata in full screen viewer optional
|
||||
|
||||
|
||||
@@ -228,9 +228,10 @@ List of available parameters, run `webui --help` for the full & up-to-date list:
|
||||
|
||||
SD.Next comes with several extensions pre-installed:
|
||||
|
||||
- [ControlNet](https://github.com/Mikubill/sd-webui-controlnet) (*active in backend: original only*)
|
||||
- [System Info](https://github.com/vladmandic/sd-extension-system-info)
|
||||
- [chaiNNer](https://github.com/vladmandic/sd-extension-chainner)
|
||||
- [RemBg](https://github.com/vladmandic/sd-extension-rembg)
|
||||
- [Agent Scheduler](https://github.com/ArtVentureX/sd-webui-agent-scheduler)
|
||||
- [Image Browser](https://github.com/AlUlkesh/stable-diffusion-webui-images-browser)
|
||||
|
||||
### **Collab**
|
||||
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
let ws;
|
||||
let url;
|
||||
const el = {
|
||||
folders: undefined,
|
||||
files: undefined,
|
||||
image: undefined,
|
||||
search: undefined,
|
||||
status: undefined,
|
||||
};
|
||||
|
||||
// HTML Elements
|
||||
|
||||
class GalleryFolder extends HTMLElement {
|
||||
constructor(name) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.shadow = this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.gallery-folder {
|
||||
cursor: pointer;
|
||||
padding: 8px 6px 8px 6px;
|
||||
}
|
||||
.gallery-folder:hover {
|
||||
background-color: var(--button-primary-background-fill-hover);
|
||||
}
|
||||
`;
|
||||
this.shadow.appendChild(style);
|
||||
const div = document.createElement('div');
|
||||
div.className = 'gallery-folder';
|
||||
div.textContent = `\uf44a ${this.name}`;
|
||||
div.addEventListener('click', fetchFiles); // eslint-disable-line no-use-before-define
|
||||
this.shadow.appendChild(div);
|
||||
}
|
||||
}
|
||||
|
||||
class GalleryImage extends HTMLElement {
|
||||
constructor(folder, name, size, mtime) {
|
||||
super();
|
||||
this.folder = folder;
|
||||
this.name = name;
|
||||
this.size = size;
|
||||
this.mtime = mtime;
|
||||
this.shadow = this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.gallery-image {
|
||||
text-align: center;
|
||||
}
|
||||
.gallery-image > img {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
max-width: 100%;
|
||||
max-height: 60vh;
|
||||
}
|
||||
.gallery-image-text {
|
||||
text-align: left;
|
||||
padding: 8px;
|
||||
line-height: 1.3em;
|
||||
}
|
||||
`;
|
||||
this.shadow.appendChild(style);
|
||||
const div = document.createElement('div');
|
||||
div.className = 'gallery-image';
|
||||
|
||||
const text = document.createElement('div');
|
||||
text.className = 'gallery-image-text';
|
||||
text.innerHTML = `
|
||||
<b>Folder:</b> ${this.folder}<br>
|
||||
<b>File:</b> ${this.name}<br>
|
||||
<b>Resolution:</b> <span id="gallery-resolution"></span><br>
|
||||
<b>Size:</b> ${this.size.toLocaleString()} bytes<br>
|
||||
<b>Modified:</b> ${this.mtime.toLocaleString()}<br>
|
||||
<br>
|
||||
<span id="gallery-exif"></span>
|
||||
`;
|
||||
|
||||
const img = document.createElement('img');
|
||||
img.id = 'gallery-image';
|
||||
img.onload = async () => {
|
||||
const resolutionEl = this.shadow.getElementById('gallery-resolution');
|
||||
if (resolutionEl) resolutionEl.innerText = `${img.naturalWidth} x ${img.naturalHeight}`;
|
||||
const exifData = await getExif(img);
|
||||
const exifEl = this.shadow.getElementById('gallery-exif');
|
||||
if (exifEl) exifEl.innerHTML = exifData;
|
||||
};
|
||||
img.loading = 'lazy';
|
||||
img.src = `file=${this.folder}/${this.name}`;
|
||||
img.title = `Folder: ${this.folder}\nFile: ${this.name}\nResolution: ${img.naturalWidth} x ${img.naturalHeight}\nSize: ${this.size.toLocaleString()} bytes\nModified: ${this.mtime.toLocaleString()}`;
|
||||
img.addEventListener('click', galleryClickEventHandler, true);
|
||||
div.appendChild(img);
|
||||
div.appendChild(text);
|
||||
this.shadow.appendChild(div);
|
||||
}
|
||||
}
|
||||
|
||||
class GalleryFile extends HTMLElement {
|
||||
constructor({ folder, file, size, mtime }) {
|
||||
super();
|
||||
this.folder = folder;
|
||||
this.name = file;
|
||||
this.size = size;
|
||||
this.mtime = new Date(1000 * mtime);
|
||||
this.hash = undefined;
|
||||
this.exif = '';
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
this.shadow = this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
const ext = this.name.split('.').pop().toLowerCase();
|
||||
if (!['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(ext)) return;
|
||||
this.hash = await getHash(`${this.folder}/${this.name}/${this.size}/${this.mtime}`); // eslint-disable-line no-use-before-define
|
||||
const style = document.createElement('style');
|
||||
const width = opts.browser_fixed_width ? `${opts.extra_networks_card_size}px` : 'unset';
|
||||
style.textContent = `
|
||||
.gallery-file {
|
||||
object-fit: contain;
|
||||
cursor: pointer;
|
||||
height: ${opts.extra_networks_card_size}px;
|
||||
width: ${width};
|
||||
}
|
||||
.gallery-file:hover {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
`;
|
||||
|
||||
this.shadow.appendChild(style);
|
||||
const img = document.createElement('img');
|
||||
img.className = 'gallery-file';
|
||||
img.loading = 'lazy';
|
||||
img.title = `Folder: ${this.folder}\nFile: ${this.name}\nSize: ${this.size.toLocaleString()} bytes\nModified: ${this.mtime.toLocaleString()}`;
|
||||
img.onload = async () => {
|
||||
this.width = img.naturalWidth;
|
||||
this.height = img.naturalHeight;
|
||||
img.title += `\nResolution: ${this.width} x ${this.height}`;
|
||||
this.title = img.title;
|
||||
// let exif = await getExif(img);
|
||||
// if (exif) this.exif = exif.replaceAll('<br>', '\n').replace(/<\/?[^>]+(>|$)/g, "");
|
||||
};
|
||||
img.src = `file=${this.folder}/${this.name}`;
|
||||
img.onclick = () => {
|
||||
el.image.innerHTML = '';
|
||||
const image = new GalleryImage(this.folder, this.name, this.size, this.mtime);
|
||||
el.image.appendChild(image);
|
||||
};
|
||||
this.title = img.title;
|
||||
this.style.display = this.title.toLowerCase().includes(el.search.value.toLowerCase()) ? 'unset' : 'none';
|
||||
this.shadow.appendChild(img);
|
||||
}
|
||||
}
|
||||
|
||||
// methods
|
||||
|
||||
async function getHash(str, algo = 'SHA-256') {
|
||||
const strBuf = new TextEncoder().encode(str);
|
||||
const hash = await crypto.subtle.digest(algo, strBuf);
|
||||
let hex = '';
|
||||
const view = new DataView(hash);
|
||||
for (let i = 0; i < hash.byteLength; i += 4) hex += (`00000000${view.getUint32(i).toString(16)}`).slice(-8);
|
||||
return hex;
|
||||
}
|
||||
|
||||
async function wsConnect(socket, timeout = 2000) {
|
||||
const intrasleep = 100;
|
||||
const ttl = timeout / intrasleep;
|
||||
const isOpened = () => (socket.readyState === WebSocket.OPEN);
|
||||
if (socket.readyState !== WebSocket.CONNECTING) return isOpened();
|
||||
|
||||
let loop = 0;
|
||||
while (socket.readyState === WebSocket.CONNECTING && loop < ttl) {
|
||||
await new Promise((resolve) => setTimeout(resolve, intrasleep)); // eslint-disable-line no-promise-executor-return
|
||||
loop++;
|
||||
}
|
||||
return isOpened();
|
||||
}
|
||||
|
||||
async function gallerySearch(evt) {
|
||||
if (el.search.busy) clearTimeout(el.search.busy);
|
||||
el.search.busy = setTimeout(async () => {
|
||||
let numFound = 0;
|
||||
const all = Array.from(el.files.children);
|
||||
const str = el.search.value.toLowerCase();
|
||||
const r = /^(.+)([=<>])(.*)/;
|
||||
const t0 = performance.now();
|
||||
for (const f of all) {
|
||||
if (r.test(str)) {
|
||||
const match = str.match(r);
|
||||
const key = match[1].trim();
|
||||
const op = match[2].trim();
|
||||
let val = match[3].trim();
|
||||
if (key === 'mtime') val = new Date(val);
|
||||
console.log('HERE', key, op, val, f[key]);
|
||||
if (((op === '=') && (f[key] === val)) || ((op === '>') && (f[key] > val)) || ((op === '<') && (f[key] < val))) {
|
||||
f.style.display = 'unset';
|
||||
numFound++;
|
||||
} else {
|
||||
f.style.display = 'none';
|
||||
}
|
||||
} else if (f.title?.toLowerCase().includes(str) || f.exif?.toLowerCase().includes(str)) {
|
||||
f.style.display = 'unset';
|
||||
numFound++;
|
||||
} else {
|
||||
f.style.display = 'none';
|
||||
}
|
||||
const t1 = performance.now();
|
||||
el.status.innerText = `Filter | ${f.folder} | ${numFound.toLocaleString()}/${all.length.toLocaleString()} images | ${Math.floor(t1 - t0).toLocaleString()}ms`;
|
||||
}
|
||||
}, 250);
|
||||
}
|
||||
|
||||
async function gallerySort(btn) {
|
||||
const t0 = performance.now();
|
||||
document.querySelectorAll('.gallery-separator').forEach((node) => el.files.removeChild(node)); // cannot sort separators
|
||||
const arr = Array.from(el.files.children);
|
||||
switch (btn.charCodeAt(0)) {
|
||||
case 61789:
|
||||
arr
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
.forEach((node) => el.files.appendChild(node));
|
||||
break;
|
||||
case 61790:
|
||||
arr
|
||||
.sort((b, a) => a.name.localeCompare(b.name))
|
||||
.forEach((node) => el.files.appendChild(node));
|
||||
break;
|
||||
case 61792:
|
||||
arr
|
||||
.sort((a, b) => a.size - b.size)
|
||||
.forEach((node) => el.files.appendChild(node));
|
||||
break;
|
||||
case 61793:
|
||||
arr
|
||||
.sort((b, a) => a.size - b.size)
|
||||
.forEach((node) => el.files.appendChild(node));
|
||||
break;
|
||||
case 61794:
|
||||
arr
|
||||
.sort((a, b) => a.width * a.height - b.width * b.height)
|
||||
.forEach((node) => el.files.appendChild(node));
|
||||
break;
|
||||
case 61795:
|
||||
arr
|
||||
.sort((b, a) => a.width * a.height - b.width * b.height)
|
||||
.forEach((node) => el.files.appendChild(node));
|
||||
break;
|
||||
case 61662:
|
||||
arr
|
||||
.sort((a, b) => a.mtime - b.mtime)
|
||||
.forEach((node) => el.files.appendChild(node));
|
||||
break;
|
||||
case 61661:
|
||||
arr
|
||||
.sort((b, a) => a.mtime - b.mtime)
|
||||
.forEach((node) => el.files.appendChild(node));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
const t1 = performance.now();
|
||||
el.status.innerText = `Sort | ${arr.length.toLocaleString()} images | ${Math.floor(t1 - t0).toLocaleString()}ms`;
|
||||
}
|
||||
|
||||
async function fetchFiles(evt) { // fetch file-by-file list over websockets
|
||||
el.files.innerHTML = '';
|
||||
if (!url) return;
|
||||
if (ws && ws.readyState === WebSocket.OPEN) ws.close(); // abort previous request
|
||||
ws = new WebSocket(`${url}/sdapi/v1/browser/files`);
|
||||
await wsConnect(ws);
|
||||
let numFiles = 0;
|
||||
el.status.innerText = `Folder | ${evt.target.name}`;
|
||||
const t0 = performance.now();
|
||||
let t1 = performance.now();
|
||||
let lastDir;
|
||||
ws.onmessage = (event) => { // time is 20% list 80% create item
|
||||
numFiles++;
|
||||
t1 = performance.now();
|
||||
if (event.data === '#END#') {
|
||||
ws.close();
|
||||
} else {
|
||||
const json = JSON.parse(event.data);
|
||||
const dir = json.file.match(/(.*)[\/\\]/) || '';
|
||||
if (dir?.[1] !== lastDir) { // create separator
|
||||
lastDir = dir[1];
|
||||
const sep = document.createElement('div');
|
||||
sep.className = 'gallery-separator';
|
||||
sep.innerText = lastDir;
|
||||
sep.title = lastDir;
|
||||
el.files.appendChild(sep);
|
||||
}
|
||||
const file = new GalleryFile(json);
|
||||
el.files.appendChild(file);
|
||||
el.status.innerText = `Folder | ${evt.target.name} | ${numFiles.toLocaleString()} images | ${Math.floor(t1 - t0).toLocaleString()}ms`;
|
||||
}
|
||||
};
|
||||
ws.onclose = (event) => {
|
||||
// log('gallery ws file enum', event);
|
||||
};
|
||||
ws.onerror = (event) => {
|
||||
log('gallery ws error', event);
|
||||
};
|
||||
ws.send(evt.target.name);
|
||||
}
|
||||
|
||||
async function galleryVisible() {
|
||||
// if (el.folders.children.length > 0) return;
|
||||
const res = await fetch('/sdapi/v1/browser/folders');
|
||||
if (!res || res.status !== 200) return;
|
||||
el.folders.innerHTML = '';
|
||||
url = res.url.split('/sdapi')[0].replace('http', 'ws'); // update global url as ws need fqdn
|
||||
const folders = await res.json();
|
||||
for (const folder of folders) {
|
||||
const f = new GalleryFolder(folder);
|
||||
el.folders.appendChild(f);
|
||||
}
|
||||
}
|
||||
|
||||
async function galleryHidden() { /**/ }
|
||||
|
||||
async function galleryObserve() { // triggered on gradio change to monitor when ui gets sufficiently constructed
|
||||
log('initBrowser');
|
||||
el.folders = gradioApp().getElementById('tab-gallery-folders');
|
||||
el.files = gradioApp().getElementById('tab-gallery-files');
|
||||
el.image = gradioApp().getElementById('tab-gallery-image');
|
||||
el.status = gradioApp().getElementById('tab-gallery-status');
|
||||
el.search = gradioApp().querySelector('#tab-gallery-search textarea');
|
||||
el.search.addEventListener('input', gallerySearch);
|
||||
|
||||
const intersectionObserver = new IntersectionObserver((entries) => {
|
||||
if (entries[0].intersectionRatio <= 0) galleryHidden();
|
||||
if (entries[0].intersectionRatio > 0) galleryVisible();
|
||||
});
|
||||
intersectionObserver.observe(el.folders);
|
||||
}
|
||||
|
||||
// register on startup
|
||||
|
||||
customElements.define('gallery-folder', GalleryFolder);
|
||||
customElements.define('gallery-file', GalleryFile);
|
||||
customElements.define('gallery-image', GalleryImage);
|
||||
onUiLoaded(galleryObserve);
|
||||
+4
-1
@@ -5,7 +5,7 @@ from fastapi import FastAPI, APIRouter, Depends, Request
|
||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||
from fastapi.exceptions import HTTPException
|
||||
from modules import errors, shared, postprocessing
|
||||
from modules.api import models, endpoints, script, helpers, server, nvml, generate, process, control
|
||||
from modules.api import models, endpoints, script, helpers, server, nvml, generate, process, control, gallery
|
||||
|
||||
|
||||
errors.install()
|
||||
@@ -83,6 +83,9 @@ class Api:
|
||||
self.add_api_route("/sdapi/v1/reload-checkpoint", endpoints.post_reload_checkpoint, methods=["POST"])
|
||||
self.add_api_route("/sdapi/v1/refresh-vae", endpoints.post_refresh_vae, methods=["POST"])
|
||||
|
||||
# gallery api
|
||||
gallery.register_api(app)
|
||||
|
||||
def add_api_route(self, path: str, endpoint, **kwargs):
|
||||
if (shared.cmd_opts.auth or shared.cmd_opts.auth_file) and shared.cmd_opts.api_only:
|
||||
return self.app.add_api_route(path, endpoint, dependencies=[Depends(self.auth)], **kwargs)
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import os
|
||||
from typing import List
|
||||
from fastapi import FastAPI
|
||||
from fastapi.responses import JSONResponse
|
||||
from starlette.websockets import WebSocket, WebSocketState, WebSocketDisconnect
|
||||
from pydantic import BaseModel, Field # pylint: disable=no-name-in-module
|
||||
from modules import shared, files_cache
|
||||
|
||||
|
||||
debug = shared.log.debug if os.environ.get('SD_BROWSER_DEBUG', None) is not None else lambda *args, **kwargs: None
|
||||
|
||||
|
||||
OPTS_FOLDERS = [
|
||||
"outdir_samples",
|
||||
"outdir_txt2img_samples",
|
||||
"outdir_img2img_samples",
|
||||
"outdir_control_samples",
|
||||
"outdir_extras_samples",
|
||||
"outdir_save"
|
||||
"outdir_video",
|
||||
"outdir_init_images",
|
||||
"outdir_grids",
|
||||
"outdir_txt2img_grids",
|
||||
"outdir_img2img_grids",
|
||||
"outdir_control_grids",
|
||||
]
|
||||
|
||||
### class definitions
|
||||
|
||||
class ReqFiles(BaseModel):
|
||||
folder: str = Field(title="Folder")
|
||||
|
||||
### ws connection manager
|
||||
|
||||
class ConnectionManager:
|
||||
def __init__(self):
|
||||
self.active: list[WebSocket] = []
|
||||
|
||||
async def connect(self, ws: WebSocket):
|
||||
await ws.accept()
|
||||
agent = ws._headers.get("user-agent", "") # pylint: disable=protected-access
|
||||
debug(f'Browser WS connect: client={ws.client.host} agent="{agent}"')
|
||||
self.active.append(ws)
|
||||
|
||||
def disconnect(self, ws: WebSocket):
|
||||
debug(f'Browser WS disconnect: client={ws.client.host}')
|
||||
self.active.remove(ws)
|
||||
|
||||
async def send(self, ws: WebSocket, data: str|dict|bytes):
|
||||
# debug(f'Browser WS send: client={ws.client.host} data={type(data)}')
|
||||
if ws.client_state != WebSocketState.CONNECTED:
|
||||
return
|
||||
if isinstance(data, bytes):
|
||||
await ws.send_bytes(data)
|
||||
elif isinstance(data, dict):
|
||||
await ws.send_json(data)
|
||||
elif isinstance(data, str):
|
||||
await ws.send_text(data)
|
||||
else:
|
||||
debug(f'Browser WS send: client={ws.client.host} data={type(data)} unknown')
|
||||
|
||||
async def broadcast(self, data: str|dict|bytes):
|
||||
for ws in self.active:
|
||||
await self.send(ws, data)
|
||||
|
||||
### api definitions
|
||||
|
||||
def register_api(app: FastAPI): # register api
|
||||
manager = ConnectionManager()
|
||||
|
||||
@app.get('/sdapi/v1/browser/folders', response_model=List[str])
|
||||
def get_folders():
|
||||
folders = [shared.opts.data.get(f, '') for f in OPTS_FOLDERS]
|
||||
folders += list(shared.opts.browser_folders.split(','))
|
||||
folders = [f.strip() for f in folders if f != '']
|
||||
folders = list(dict.fromkeys(folders)) # filter duplicates
|
||||
folders = [f for f in folders if os.path.isdir(f)]
|
||||
if shared.demo is not None:
|
||||
for f in folders:
|
||||
if os.path.isabs(f) and f not in shared.demo.allowed_paths:
|
||||
debug(f'Browser folders allow: {f}')
|
||||
shared.demo.allowed_paths.append(f)
|
||||
debug(f'Browser folders: {folders}')
|
||||
return JSONResponse(content=folders)
|
||||
|
||||
@app.websocket("/sdapi/v1/browser/files")
|
||||
async def ws_files(ws: WebSocket):
|
||||
try:
|
||||
await manager.connect(ws)
|
||||
folder = await ws.receive_text()
|
||||
debug(f'Browser WS folder: {folder}')
|
||||
for f in files_cache.directory_files(folder, recursive=True):
|
||||
file = os.path.relpath(f, folder)
|
||||
stat = os.stat(f)
|
||||
dct = {
|
||||
'folder': folder,
|
||||
'file': file,
|
||||
'size': stat.st_size,
|
||||
'mtime': stat.st_mtime,
|
||||
}
|
||||
await manager.send(ws, dct)
|
||||
await manager.send(ws, '#END#')
|
||||
except WebSocketDisconnect:
|
||||
debug('Browser WS unexpected disconnect')
|
||||
manager.disconnect(ws)
|
||||
|
||||
@app.websocket("/sdapi/v1/browser/file/{file}")
|
||||
async def ws_file(ws: WebSocket, file: str):
|
||||
try:
|
||||
await manager.connect(ws)
|
||||
with open(file, 'rb') as f: # noqa: ASYNC101
|
||||
await manager.send(ws, f.read())
|
||||
except WebSocketDisconnect:
|
||||
manager.disconnect(ws)
|
||||
@@ -154,6 +154,11 @@ def create_ui(startup_timer = None):
|
||||
ui_models.create_ui()
|
||||
timer.startup.record("ui-models")
|
||||
|
||||
with gr.Blocks(analytics_enabled=False) as gallery_interface:
|
||||
from modules import ui_gallery
|
||||
ui_gallery.create_ui()
|
||||
timer.startup.record("ui-gallery")
|
||||
|
||||
def create_setting_component(key, is_quicksettings=False):
|
||||
def fun():
|
||||
return opts.data[key] if key in opts.data else opts.data_labels[key].default
|
||||
@@ -370,6 +375,7 @@ def create_ui(startup_timer = None):
|
||||
interfaces += [(img2img_interface, "Image", "img2img")]
|
||||
interfaces += [(control_interface, "Control", "control")] if control_interface is not None else []
|
||||
interfaces += [(extras_interface, "Process", "process")]
|
||||
interfaces += [(gallery_interface, "Gallery", "gallery")]
|
||||
interfaces += [(models_interface, "Models", "models")]
|
||||
interfaces += script_callbacks.ui_tabs_callback()
|
||||
interfaces += [(settings_interface, "System", "system")]
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import os
|
||||
import gradio as gr
|
||||
from modules import shared, ui_symbols
|
||||
from modules.ui_components import ToolButton
|
||||
|
||||
|
||||
debug = shared.log.debug if os.environ.get('SD_GALLERY_DEBUG', None) is not None else lambda *args, **kwargs: None
|
||||
|
||||
|
||||
def create_ui():
|
||||
with gr.Blocks() as tab:
|
||||
with gr.Row():
|
||||
sort_buttons = []
|
||||
sort_buttons.append(ToolButton(value=ui_symbols.sort_alpha_asc, show_label=False))
|
||||
sort_buttons.append(ToolButton(value=ui_symbols.sort_alpha_dsc, show_label=False))
|
||||
sort_buttons.append(ToolButton(value=ui_symbols.sort_size_asc, show_label=False))
|
||||
sort_buttons.append(ToolButton(value=ui_symbols.sort_size_dsc, show_label=False))
|
||||
sort_buttons.append(ToolButton(value=ui_symbols.sort_num_asc, show_label=False))
|
||||
sort_buttons.append(ToolButton(value=ui_symbols.sort_num_dsc, show_label=False))
|
||||
sort_buttons.append(ToolButton(value=ui_symbols.sort_time_asc, show_label=False))
|
||||
sort_buttons.append(ToolButton(value=ui_symbols.sort_time_dsc, show_label=False))
|
||||
gr.Textbox(show_label=False, placeholder='Search', elem_id='tab-gallery-search')
|
||||
gr.HTML('', elem_id='tab-gallery-status')
|
||||
for btn in sort_buttons:
|
||||
btn.click(fn=None, _js='gallerySort', inputs=[btn], outputs=[])
|
||||
with gr.Row():
|
||||
with gr.Column():
|
||||
gr.HTML('', elem_id='tab-gallery-folders')
|
||||
with gr.Column():
|
||||
gr.HTML('', elem_id='tab-gallery-files')
|
||||
with gr.Column():
|
||||
gr.HTML('', elem_id='tab-gallery-image')
|
||||
return [(tab, 'Gallery', 'tab-gallery')]
|
||||
Reference in New Issue
Block a user