Merge pull request #687 from cmdr2/beta

Undo/redo buttons in the image editor, Drag handle to reorder tasks, Pause button to pause all the tasks
This commit is contained in:
cmdr2 2022-12-22 13:23:50 +05:30 committed by GitHub
commit 9d201f82f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 270 additions and 72 deletions

View File

@ -27,6 +27,7 @@
- Support loading models in the safetensor format, for improved safety
### Detailed changelog
* 2.4.19 - 17 Dec 2022 - Add Undo/Redo buttons in the Image Editor. Thanks @JeLuf
* 2.4.19 - 10 Dec 2022 - Show init img in task list
* 2.4.19 - 7 Dec 2022 - Use pre-trained hypernetworks while generating images. Thanks @C0bra5
* 2.4.19 - 6 Dec 2022 - Allow processing new tasks first. Thanks @madrang

View File

@ -25,7 +25,7 @@
<div id="logo">
<h1>
Stable Diffusion UI
<small>v2.4.19 <span id="updateBranchLabel"></span></small>
<small>v2.4.20 <span id="updateBranchLabel"></span></small>
</h1>
</div>
<div id="server-status">
@ -100,7 +100,11 @@
</div>
<button id="makeImage" class="primaryButton">Make Image</button>
<button id="stopImage" class="secondaryButton">Stop All</button>
<div id="render-buttons">
<button id="stopImage" class="secondaryButton">Stop All</button>
<button id="pause"><i class="fa-solid fa-pause"></i> Pause All</button>
<button id="resume"><i class="fa-solid fa-play"></i> Resume</button>
</div>
</div>
<span class="line-separator"></span>

View File

@ -136,6 +136,10 @@
background: var(--background-color3);
}
.editor-controls-right .image-editor-button {
margin-bottom: 4px;
}
#init_image_button_inpaint .input-toggle {
position: absolute;
left: 16px;
@ -208,4 +212,4 @@
}
.image-editor-popup h4 {
text-align: left;
}
}

View File

@ -191,15 +191,29 @@ code {
background: rgb(132, 8, 0);
border: 2px solid rgb(122, 29, 0);
color: rgb(255, 221, 255);
width: 100%;
height: 30pt;
border-radius: 6px;
display: none;
margin-top: 2pt;
flex-grow: 2;
}
#stopImage:hover {
background: rgb(177, 27, 0);
}
div#render-buttons {
gap: 3px;
margin-top: 4px;
display: none;
}
button#pause {
flex-grow: 1;
background: var(--accent-color);
}
button#resume {
flex-grow: 1;
background: var(--accent-color);
display: none;
}
.flex-container {
display: flex;
width: 100%;
@ -265,7 +279,7 @@ img {
}
.preview-prompt {
font-size: 13pt;
margin-bottom: 10pt;
display: inline;
}
#coffeeButton {
height: 23px;
@ -391,14 +405,34 @@ img {
.imageTaskContainer > div > .collapsible-handle {
display: none;
}
.dropTargetBefore::before{
content: "";
border: 1px solid #fff;
margin-bottom: -2px;
display: block;
box-shadow: 0 0 5px #fff;
transform: translate(0px, -14px);
}
.dropTargetAfter::after{
content: "";
border: 1px solid #fff;
margin-bottom: -2px;
display: block;
box-shadow: 0 0 5px #fff;
transform: translate(0px, 14px);
}
.drag-handle {
margin-right: 6px;
cursor: move;
}
.taskStatusLabel {
float: left;
font-size: 8pt;
background:var(--background-color2);
border: 1px solid rgb(61, 62, 66);
padding: 2pt 4pt;
border-radius: 2pt;
margin-right: 5pt;
display: inline;
}
.activeTaskLabel {
background:rgb(0, 90, 30);
@ -448,6 +482,7 @@ img {
font-size: 10pt;
color: #aaa;
margin-bottom: 5pt;
margin-top: 5pt;
}
.img-batch {
display: inline;
@ -1068,7 +1103,19 @@ div.task-initimg > img {
}
div.task-fs-initimage {
display: none;
# position: absolute;
}
div.task-initimg:hover div.task-fs-initimage {
display: block;
position: absolute;
z-index: 9999;
box-shadow: 0 0 30px #000;
margin-top:-64px;
}
div.top-right {
position: absolute;
top: 8px;
right: 8px;
}
button#save-system-settings-btn {
@ -1080,3 +1127,50 @@ button#save-system-settings-btn {
#ip-info div {
line-height: 200%;
}
/* SCROLLBARS */
:root {
--scrollbar-width: 14px;
--scrollbar-radius: 10px;
}
.scrollbar-editor::-webkit-scrollbar {
width: 8px;
}
.scrollbar-editor::-webkit-scrollbar-track {
border-radius: 10px;
}
.scrollbar-editor::-webkit-scrollbar-thumb {
background: --background-color2;
border-radius: 10px;
}
::-webkit-scrollbar {
width: var(--scrollbar-width);
}
::-webkit-scrollbar-track {
box-shadow: inset 0 0 5px var(--input-border-color);
border-radius: var(--input-border-radius);
}
::-webkit-scrollbar-thumb {
background: var(--background-color2);
border-radius: var(--scrollbar-radius);
}
body.pause {
border: solid 12px var(--accent-color);
}
body.wait-pause {
animation: blinker 2s linear infinite;
}
@keyframes blinker {
0% { border: solid 12px var(--accent-color); }
50% { border: solid 12px var(--background-color1); }
100% { border: solid 12px var(--accent-color); }
}

View File

@ -10,8 +10,8 @@
const IDLE_COOLDOWN = 2500 // ms
const CONCURRENT_TASK_INTERVAL = 500 // ms
/** Connects to an endpoint and resumes connexion after reaching end of stream until all data is received.
* Allows closing the connexion while the server buffers more data.
/** Connects to an endpoint and resumes connection after reaching end of stream until all data is received.
* Allows closing the connection while the server buffers more data.
*/
class ChunkedStreamReader {
#bufferedString = '' // Data received waiting to be read.
@ -264,11 +264,11 @@
function isServerAvailable() {
if (typeof serverState !== 'object') {
console.error('serverState not set to a value. Connexion to server could be lost...')
console.error('serverState not set to a value. Connection to server could be lost...')
return false
}
if (Date.now() >= serverState.time + SERVER_STATE_VALIDITY_DURATION) {
console.warn('SERVER_STATE_VALIDITY_DURATION elapsed. Connexion to server could be lost...')
console.warn('SERVER_STATE_VALIDITY_DURATION elapsed. Connection to server could be lost...')
return false
}
switch (serverState.status) {
@ -306,7 +306,7 @@
if (await healthCheck() && isServerAvailable()) { // Force a recheck of server status before failure...
continue // Continue waiting if last healthCheck confirmed the server is still alive.
}
throw new Error('Connexion with server lost.')
throw new Error('Connection with server lost.')
}
}
if (Date.now() >= serverState.time + SERVER_STATE_VALIDITY_DURATION) {

View File

@ -105,7 +105,26 @@ const IMAGE_EDITOR_ACTIONS = [
icon: "fa-solid fa-xmark",
handler: (editor) => {
editor.ctx_current.clearRect(0, 0, editor.width, editor.height)
}
},
trackHistory: true
},
{
id: "undo",
name: "Undo",
icon: "fa-solid fa-rotate-left",
handler: (editor) => {
editor.history.undo()
},
trackHistory: false
},
{
id: "redo",
name: "Redo",
icon: "fa-solid fa-rotate-right",
handler: (editor) => {
editor.history.redo()
},
trackHistory: false
}
]
@ -436,13 +455,14 @@ class ImageEditor {
return
}
var max_size = Math.min(parseInt(window.innerWidth * 0.9), width, 768)
if (width > height) {
var max_size = Math.min(parseInt(window.innerWidth * 0.9), width, 768)
var multiplier = max_size / width
width = (multiplier * width).toFixed()
height = (multiplier * height).toFixed()
}
else {
var max_size = Math.min(parseInt(window.innerHeight * 0.9), height, 768)
var multiplier = max_size / height
width = (multiplier * width).toFixed()
height = (multiplier * height).toFixed()
@ -525,7 +545,9 @@ class ImageEditor {
}
runAction(action_id) {
var action = IMAGE_EDITOR_ACTIONS.find(a => a.id == action_id)
this.history.pushAction(action_id)
if (action.trackHistory) {
this.history.pushAction(action_id)
}
action.handler(this)
}
setBrush(layer = null, options = null) {

View File

@ -45,6 +45,9 @@ let streamImageProgressField = document.querySelector("#stream_image_progress")
let makeImageBtn = document.querySelector('#makeImage')
let stopImageBtn = document.querySelector('#stopImage')
let pauseBtn = document.querySelector('#pause')
let resumeBtn = document.querySelector('#resume')
let renderButtons = document.querySelector('#render-buttons')
let imagesContainer = document.querySelector('#current-images')
let initImagePreviewContainer = document.querySelector('#init_image_preview_container')
@ -101,6 +104,8 @@ imagePreview.addEventListener('drop', function(ev) {
}
})
let showConfigToggle = document.querySelector('#configToggleBtn')
// let configBox = document.querySelector('#config')
// let outputMsg = document.querySelector('#outputMsg')
@ -456,6 +461,10 @@ function makeImage() {
async function onIdle() {
const serverCapacity = SD.serverCapacity
if (pauseClient===true) {
await resumeClient()
}
for (const taskEntry of getUncompletedTaskEntries()) {
if (SD.activeTasks.size >= serverCapacity) {
break
@ -623,19 +632,18 @@ function onTaskCompleted(task, reqBody, instance, outputContainer, stepUpdate) {
task['stopTask'].innerHTML = '<i class="fa-solid fa-trash-can"></i> Remove'
task['taskStatusLabel'].style.display = 'none'
let time = Date.now() - task.startTime
time /= 1000
let time = millisecondsToStr( Date.now() - task.startTime )
if (task.batchesDone == task.batchCount) {
if (!task.outputMsg.innerText.toLowerCase().includes('error')) {
task.outputMsg.innerText = `Processed ${task.numOutputsTotal} images in ${time} seconds`
}
if (!task.outputMsg.innerText.toLowerCase().includes('error')) {
task.outputMsg.innerText = `Processed ${task.numOutputsTotal} images in ${time}`
}
task.progressBar.style.height = "0px"
task.progressBar.style.border = "0px solid var(--background-color3)"
task.progressBar.classList.remove("active")
setStatus('request', 'done', 'success')
} else {
task.outputMsg.innerText += `Task ended after ${time} seconds`
task.outputMsg.innerText += `Task ended after ${time}`
}
if (randomSeedField.checked) {
@ -650,7 +658,7 @@ function onTaskCompleted(task, reqBody, instance, outputContainer, stepUpdate) {
return
}
stopImageBtn.style.display = 'none'
renderButtons.style.display = 'none'
renameMakeImageButton()
if (isSoundEnabled()) {
@ -735,7 +743,7 @@ async function onTaskStart(task) {
)
setStatus('request', 'fetching..')
stopImageBtn.style.display = 'block'
renderButtons.style.display = 'flex'
renameMakeImageButton()
previewTools.style.display = 'block'
}
@ -743,23 +751,33 @@ async function onTaskStart(task) {
/* Hover effect for the init image in the task list */
function createInitImageHover(taskEntry) {
var $tooltip = $( taskEntry.querySelector('.task-fs-initimage') )
$( taskEntry.querySelector('div.task-initimg > img') ).on('mouseenter', function() {
var img = this,
$img = $(img),
offset = $img.offset();
$tooltip
.css({
'top': offset.top,
'left': offset.left,
'z-index': 99999,
'display': 'block'
})
.append($img.clone().css({width:"", height:""}));
})
$tooltip.on('mouseleave', function() {
$tooltip.empty().addClass('hidden');
var img = document.createElement('img')
img.src = taskEntry.querySelector('div.task-initimg > img').src
$tooltip.append(img)
$tooltip.append(`<div class="top-right"><button>Use as Input</button></div>`)
$tooltip.find('button').on('click', (e) => { onUseAsInputClick(null,img) } )
}
let startX, startY;
function onTaskEntryDragOver(event) {
imagePreview.querySelectorAll(".imageTaskContainer").forEach(itc => {
if(itc != event.target.closest(".imageTaskContainer")){
itc.classList.remove('dropTargetBefore','dropTargetAfter');
}
});
if(event.target.closest(".imageTaskContainer")){
if(startX && startY){
if(event.target.closest(".imageTaskContainer").offsetTop > startY){
event.target.closest(".imageTaskContainer").classList.add('dropTargetAfter');
}else if(event.target.closest(".imageTaskContainer").offsetTop < startY){
event.target.closest(".imageTaskContainer").classList.add('dropTargetBefore');
}else if (event.target.closest(".imageTaskContainer").offsetLeft > startX){
event.target.closest(".imageTaskContainer").classList.add('dropTargetAfter');
}else if (event.target.closest(".imageTaskContainer").offsetLeft < startX){
event.target.closest(".imageTaskContainer").classList.add('dropTargetBefore');
}
}
}
}
function createTask(task) {
@ -795,6 +813,7 @@ function createTask(task) {
taskEntry.id = `imageTaskContainer-${Date.now()}`
taskEntry.className = 'imageTaskContainer'
taskEntry.innerHTML = ` <div class="header-content panel collapsible active">
<i class="drag-handle fa-solid fa-grip"></i>
<div class="taskStatusLabel">Enqueued</div>
<button class="secondaryButton stopTask"><i class="fa-solid fa-trash-can"></i> Remove</button>
<button class="secondaryButton useSettings"><i class="fa-solid fa-redo"></i> Use these settings</button>
@ -808,6 +827,23 @@ function createTask(task) {
</div>`
createCollapsibles(taskEntry)
let draghandle = taskEntry.querySelector('.drag-handle')
draghandle.addEventListener('mousedown', (e) => { taskEntry.setAttribute('draggable',true)})
draghandle.addEventListener('mouseup', (e) => { taskEntry.setAttribute('draggable',false)})
taskEntry.addEventListener('dragend', (e) => {
taskEntry.setAttribute('draggable',false);
imagePreview.querySelectorAll(".imageTaskContainer").forEach(itc => {
itc.classList.remove('dropTargetBefore','dropTargetAfter');
});
imagePreview.removeEventListener("dragover", onTaskEntryDragOver );
})
taskEntry.addEventListener('dragstart', function(e) {
imagePreview.addEventListener("dragover", onTaskEntryDragOver );
e.dataTransfer.setData("text/plain", taskEntry.id);
startX = e.target.closest(".imageTaskContainer").offsetLeft;
startY = e.target.closest(".imageTaskContainer").offsetTop;
})
if (task.reqBody.init_image !== undefined) {
@ -841,24 +877,13 @@ function createTask(task) {
task.isProcessing = true
taskEntry = imagePreview.insertBefore(taskEntry, previewTools.nextSibling)
taskEntry.draggable = true
htmlTaskMap.set(taskEntry, task)
taskEntry.addEventListener('dragstart', function(ev) {
ev.dataTransfer.setData("text/plain", ev.target.id);
})
task.previewPrompt.innerText = task.reqBody.prompt
if (task.previewPrompt.innerText.trim() === '') {
task.previewPrompt.innerHTML = '&nbsp;' // allows the results to be collapsed
}
// Allow prompt text to be selected.
task.previewPrompt.addEventListener("mouseover", function() {
taskEntry.setAttribute("draggable", "false");
});
task.previewPrompt.addEventListener("mouseout", function() {
taskEntry.setAttribute("draggable", "true");
});
}
function getCurrentUserRequest() {
@ -929,20 +954,29 @@ function getPrompts(prompts) {
if (typeof prompts === 'undefined') {
prompts = promptField.value
}
if (prompts.trim() === '') {
if (prompts.trim() === '' && activeTags.length === 0) {
return ['']
}
prompts = prompts.split('\n')
prompts = prompts.map(prompt => prompt.trim())
prompts = prompts.filter(prompt => prompt !== '')
let promptsToMake = applyPermuteOperator(prompts)
promptsToMake = applySetOperator(promptsToMake)
let promptsToMake = []
if (prompts.trim() !== '') {
prompts = prompts.split('\n')
prompts = prompts.map(prompt => prompt.trim())
prompts = prompts.filter(prompt => prompt !== '')
promptsToMake = applyPermuteOperator(prompts)
promptsToMake = applySetOperator(promptsToMake)
}
const newTags = activeTags.filter(tag => tag.inactive === undefined || tag.inactive === false)
if (newTags.length > 0) {
const promptTags = newTags.map(x => x.name).join(", ")
promptsToMake = promptsToMake.map((prompt) => `${prompt}, ${promptTags}`)
if (promptsToMake.length > 0) {
promptsToMake = promptsToMake.map((prompt) => `${prompt}, ${promptTags}`)
}
else
{
promptsToMake.push(promptTags)
}
}
promptsToMake = applyPermuteOperator(promptsToMake)
@ -1380,6 +1414,7 @@ function selectTab(tab_id) {
tabInfo.tab.classList.toggle("active")
tabInfo.content.classList.toggle("active")
}
document.dispatchEvent(new CustomEvent('tabClick', { detail: tabInfo }))
}
function linkTabContents(tab) {
var name = tab.id.replace("tab-", "")
@ -1393,6 +1428,37 @@ function linkTabContents(tab) {
tab.addEventListener("click", event => selectTab(tab.id))
}
let pauseClient = false
function resumeClient() {
if (pauseClient) {
document.body.classList.remove('wait-pause')
document.body.classList.add('pause')
}
return new Promise(resolve => {
let playbuttonclick = function () {
resumeBtn.removeEventListener("click", playbuttonclick);
resolve("resolved");
}
resumeBtn.addEventListener("click", playbuttonclick)
})
}
pauseBtn.addEventListener("click", function () {
pauseClient = true
pauseBtn.style.display="none"
resumeBtn.style.display = "inline"
document.body.classList.add('wait-pause')
})
resumeBtn.addEventListener("click", function () {
pauseClient = false
resumeBtn.style.display = "none"
pauseBtn.style.display = "inline"
document.body.classList.remove('pause')
document.body.classList.remove('wait-pause')
})
document.querySelectorAll(".tab").forEach(linkTabContents)
window.addEventListener("beforeunload", function(e) {

View File

@ -45,6 +45,7 @@ function toggleCollapsible(element) {
handle.innerHTML = '&#x2796;' // minus
}
}
document.dispatchEvent(new CustomEvent('collapsibleClick', { detail: collapsibleHeader }))
if (COLLAPSIBLES_INITIALIZED && COLLAPSIBLE_PANELS.includes(element)) {
saveCollapsibles()

View File

@ -43,22 +43,22 @@
</style>
`)
const markedScript = document.createElement('script')
markedScript.src = '/media/js/marked.min.js'
markedScript.onload = async function() {
loadScript('/media/js/marked.min.js').then(async function() {
let appConfig = await fetch('/get/app_config')
if (!appConfig.ok) {
console.error('[release-notes] Failed to get app_config.')
return
}
appConfig = await appConfig.json()
let updateBranch = appConfig.update_branch || 'main'
const updateBranch = appConfig.update_branch || 'main'
let releaseNotes = await fetch(`https://raw.githubusercontent.com/cmdr2/stable-diffusion-ui/${updateBranch}/CHANGES.md`)
if (releaseNotes.status != 200) {
if (!releaseNotes.ok) {
console.error('[release-notes] Failed to get CHANGES.md.')
return
}
releaseNotes = await releaseNotes.text()
news.innerHTML = marked.parse(releaseNotes)
}
document.querySelector('body').appendChild(markedScript)
})
})()

View File

@ -822,7 +822,7 @@ def _txt2img(opt_W, opt_H, opt_n_samples, opt_ddim_steps, opt_scale, start_code,
move_to_cpu(thread_data.modelCS)
if thread_data.test_sd2 and sampler_name not in ('plms', 'ddim', 'dpm2'):
raise Exception('Only plms and ddim samplers are supported right now, in SD 2.0')
raise Exception('Only plms, ddim and dpm2 samplers are supported right now, in SD 2.0')
# samples, _ = sampler.sample(S=opt.steps,

View File

@ -314,9 +314,15 @@ def getUIPlugins():
return plugins
def getIPConfig():
ips = socket.gethostbyname_ex(socket.gethostname())
ips[2].append(ips[0])
return ips[2]
try:
ips = socket.gethostbyname_ex(socket.gethostname())
ips[2].append(ips[0])
return ips[2]
except Exception as e:
print(e)
print(traceback.format_exc())
return []
@app.get('/get/{key:path}')
def read_web_data(key:str=None):