diff --git a/.gitignore b/.gitignore index 57e31e9..baeabd5 100644 --- a/.gitignore +++ b/.gitignore @@ -30,7 +30,8 @@ build_chb.py /interrogate /user.css /.idea -notification.mp3 +/notification.ogg +/notification.mp3 /SwinIR /textual_inversion .vscode diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..d94ec87 --- /dev/null +++ b/css/style.css @@ -0,0 +1,28 @@ +/* based on https://github.com/AUTOMATIC1111/stable-diffusion-webui/blob/v1.6.0/style.css */ + +#context-menu{ + z-index:9999; + position:absolute; + display:block; + padding:0px 0; + border:2px solid #a55000; + border-radius:8px; + box-shadow:1px 1px 2px #CE6400; + width: 200px; +} + +.context-menu-items{ + list-style: none; + margin: 0; + padding: 0; +} + +.context-menu-items a{ + display:block; + padding:5px; + cursor:pointer; +} + +.context-menu-items a:hover{ + background: #a55000; +} diff --git a/javascript/contextMenus.js b/javascript/contextMenus.js new file mode 100644 index 0000000..cf76993 --- /dev/null +++ b/javascript/contextMenus.js @@ -0,0 +1,171 @@ +// based on https://github.com/AUTOMATIC1111/stable-diffusion-webui/blob/v1.6.0/javascript/contextMenus.js + +var contextMenuInit = function() { + let eventListenerApplied = false; + let menuSpecs = new Map(); + + const uid = function() { + return Date.now().toString(36) + Math.random().toString(36).substring(2); + }; + + function showContextMenu(event, element, menuEntries) { + let posx = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; + let posy = event.clientY + document.body.scrollTop + document.documentElement.scrollTop; + + let oldMenu = gradioApp().querySelector('#context-menu'); + if (oldMenu) { + oldMenu.remove(); + } + + let baseStyle = window.getComputedStyle(gradioApp().querySelector('button.selected')); + + const contextMenu = document.createElement('nav'); + contextMenu.id = "context-menu"; + contextMenu.style.background = baseStyle.background; + contextMenu.style.color = baseStyle.color; + contextMenu.style.fontFamily = baseStyle.fontFamily; + contextMenu.style.top = posy + 'px'; + contextMenu.style.left = posx + 'px'; + + const contextMenuList = document.createElement('ul'); + contextMenuList.className = 'context-menu-items'; + contextMenu.append(contextMenuList); + + menuEntries.forEach(function(entry) { + let contextMenuEntry = document.createElement('a'); + contextMenuEntry.innerHTML = entry['name']; + contextMenuEntry.addEventListener("click", function() { + entry['func'](); + }); + contextMenuList.append(contextMenuEntry); + + }); + + gradioApp().appendChild(contextMenu); + + let menuWidth = contextMenu.offsetWidth + 4; + let menuHeight = contextMenu.offsetHeight + 4; + + let windowWidth = window.innerWidth; + let windowHeight = window.innerHeight; + + if ((windowWidth - posx) < menuWidth) { + contextMenu.style.left = windowWidth - menuWidth + "px"; + } + + if ((windowHeight - posy) < menuHeight) { + contextMenu.style.top = windowHeight - menuHeight + "px"; + } + + } + + function appendContextMenuOption(targetElementSelector, entryName, entryFunction) { + + var currentItems = menuSpecs.get(targetElementSelector); + + if (!currentItems) { + currentItems = []; + menuSpecs.set(targetElementSelector, currentItems); + } + let newItem = { + id: targetElementSelector + '_' + uid(), + name: entryName, + func: entryFunction, + isNew: true + }; + + currentItems.push(newItem); + return newItem['id']; + } + + function removeContextMenuOption(uid) { + menuSpecs.forEach(function(v) { + let index = -1; + v.forEach(function(e, ei) { + if (e['id'] == uid) { + index = ei; + } + }); + if (index >= 0) { + v.splice(index, 1); + } + }); + } + + function addContextMenuEventListener() { + if (eventListenerApplied) { + return; + } + gradioApp().addEventListener("click", function(e) { + if (!e.isTrusted) { + return; + } + + let oldMenu = gradioApp().querySelector('#context-menu'); + if (oldMenu) { + oldMenu.remove(); + } + }); + gradioApp().addEventListener("contextmenu", function(e) { + let oldMenu = gradioApp().querySelector('#context-menu'); + if (oldMenu) { + oldMenu.remove(); + } + menuSpecs.forEach(function(v, k) { + if (e.composedPath()[0].matches(k)) { + showContextMenu(e, e.composedPath()[0], v); + e.preventDefault(); + } + }); + }); + eventListenerApplied = true; + + } + + return [appendContextMenuOption, removeContextMenuOption, addContextMenuEventListener]; +}; + +var initResponse = contextMenuInit(); +var appendContextMenuOption = initResponse[0]; +var removeContextMenuOption = initResponse[1]; +var addContextMenuEventListener = initResponse[2]; + +(function() { + //Start example Context Menu Items + let generateOnRepeat = function(genbuttonid, interruptbuttonid) { + let genbutton = gradioApp().querySelector(genbuttonid); + let interruptbutton = gradioApp().querySelector(interruptbuttonid); + if (!interruptbutton.offsetParent) { + genbutton.click(); + } + clearInterval(window.generateOnRepeatInterval); + window.generateOnRepeatInterval = setInterval(function() { + if (!interruptbutton.offsetParent) { + genbutton.click(); + } + }, + 500); + }; + + let generateOnRepeatForButtons = function() { + generateOnRepeat('#generate_button', '#stop_button'); + }; + + appendContextMenuOption('#generate_button', 'Generate forever', generateOnRepeatForButtons); + appendContextMenuOption('#stop_button', 'Generate forever', generateOnRepeatForButtons); + + let cancelGenerateForever = function() { + clearInterval(window.generateOnRepeatInterval); + }; + + appendContextMenuOption('#stop_button', 'Cancel generate forever', cancelGenerateForever); + appendContextMenuOption('#generate_button', 'Cancel generate forever', cancelGenerateForever); + +})(); +//End example Context Menu Items + +document.onreadystatechange = function () { + if (document.readyState == "complete") { + addContextMenuEventListener(); + } +}; diff --git a/javascript/script.js b/javascript/script.js new file mode 100644 index 0000000..1098aff --- /dev/null +++ b/javascript/script.js @@ -0,0 +1,33 @@ +// based on https://github.com/AUTOMATIC1111/stable-diffusion-webui/blob/v1.6.0/script.js + +function gradioApp() { + const elems = document.getElementsByTagName('gradio-app'); + const elem = elems.length == 0 ? document : elems[0]; + + if (elem !== document) { + elem.getElementById = function(id) { + return document.getElementById(id); + }; + } + return elem.shadowRoot ? elem.shadowRoot : elem; +} + +function playNotification() { + gradioApp().querySelector('#audio_notification audio')?.play(); +} + +document.addEventListener('keydown', function(e) { + var handled = false; + if (e.key !== undefined) { + if ((e.key == "Enter" && (e.metaKey || e.ctrlKey || e.altKey))) handled = true; + } else if (e.keyCode !== undefined) { + if ((e.keyCode == 13 && (e.metaKey || e.ctrlKey || e.altKey))) handled = true; + } + if (handled) { + var button = gradioApp().querySelector('button[id=generate_button]'); + if (button) { + button.click(); + } + e.preventDefault(); + } +}); diff --git a/modules/ui_gradio_extensions.py b/modules/ui_gradio_extensions.py new file mode 100644 index 0000000..9c90857 --- /dev/null +++ b/modules/ui_gradio_extensions.py @@ -0,0 +1,46 @@ +# based on https://github.com/AUTOMATIC1111/stable-diffusion-webui/blob/v1.6.0/modules/ui_gradio_extensions.py + +import os +import gradio as gr + +GradioTemplateResponseOriginal = gr.routes.templates.TemplateResponse + +modules_path = os.path.dirname(os.path.realpath(__file__)) +script_path = os.path.dirname(modules_path) + + +def webpath(fn): + if fn.startswith(script_path): + web_path = os.path.relpath(fn, script_path).replace('\\', '/') + else: + web_path = os.path.abspath(fn) + + return f'file={web_path}?{os.path.getmtime(fn)}' + + +def javascript_html(): + script_js_path = webpath('javascript/script.js') + context_menus_js_path = webpath('javascript/contextMenus.js') + head = f'\n' + head += f'\n' + return head + + +def css_html(): + style_css_path = webpath('css/style.css') + head = f'' + return head + + +def reload_javascript(): + js = javascript_html() + css = css_html() + + def template_response(*args, **kwargs): + res = GradioTemplateResponseOriginal(*args, **kwargs) + res.body = res.body.replace(b'', f'{js}'.encode("utf8")) + res.body = res.body.replace(b'', f'{css}'.encode("utf8")) + res.init_headers() + return res + + gr.routes.templates.TemplateResponse = template_response diff --git a/notification-example.ogg b/notification-example.ogg new file mode 100644 index 0000000..fe4291d Binary files /dev/null and b/notification-example.ogg differ diff --git a/webui.py b/webui.py index b5e09c0..908ae68 100644 --- a/webui.py +++ b/webui.py @@ -15,6 +15,8 @@ import args_manager from modules.sdxl_styles import legal_style_names, aspect_ratios from modules.private_logger import get_current_html_path +from modules.ui_gradio_extensions import reload_javascript +from os.path import exists def generate_clicked(*args): @@ -47,6 +49,8 @@ def generate_clicked(*args): return +reload_javascript() + shared.gradio_root = gr.Blocks( title=f'Fooocus {fooocus_version.version} ' + ('' if args_manager.args.preset is None else args_manager.args.preset), css=modules.html.css).queue() @@ -63,9 +67,9 @@ with shared.gradio_root: value=modules.path.default_positive_prompt, container=False, autofocus=True, elem_classes='type_row', lines=1024) with gr.Column(scale=0.15, min_width=0): - run_button = gr.Button(label="Generate", value="Generate", elem_classes='type_row', visible=True) + generate_button = gr.Button(label="Generate", value="Generate", elem_classes='type_row', elem_id='generate_button', visible=True) skip_button = gr.Button(label="Skip", value="Skip", elem_classes='type_row_half', visible=False) - stop_button = gr.Button(label="Stop", value="Stop", elem_classes='type_row_half', visible=False) + stop_button = gr.Button(label="Stop", value="Stop", elem_classes='type_row_half', elem_id='stop_button', visible=False) def stop_clicked(): import fcbh.model_management as model_management @@ -337,12 +341,16 @@ with shared.gradio_root: ctrls += [outpaint_selections, inpaint_input_image] ctrls += ip_ctrls - run_button.click(lambda: (gr.update(visible=True, interactive=True), gr.update(visible=True, interactive=True), gr.update(visible=False), []), outputs=[stop_button, skip_button, run_button, gallery])\ - .then(fn=refresh_seed, inputs=[seed_random, image_seed], outputs=image_seed)\ - .then(advanced_parameters.set_all_advanced_parameters, inputs=adps)\ - .then(fn=generate_clicked, inputs=ctrls, outputs=[progress_html, progress_window, gallery])\ - .then(lambda: (gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)), outputs=[run_button, stop_button, skip_button]) + generate_button.click(lambda: (gr.update(visible=True, interactive=True), gr.update(visible=True, interactive=True), gr.update(visible=False), []), outputs=[stop_button, skip_button, generate_button, gallery]) \ + .then(fn=refresh_seed, inputs=[seed_random, image_seed], outputs=image_seed) \ + .then(advanced_parameters.set_all_advanced_parameters, inputs=adps) \ + .then(fn=generate_clicked, inputs=ctrls, outputs=[progress_html, progress_window, gallery]) \ + .then(lambda: (gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)), outputs=[generate_button, stop_button, skip_button]) \ + .then(fn=None, _js='playNotification') + notification_file = 'notification.ogg' if exists('notification.ogg') else 'notification.mp3' if exists('notification.mp3') else None + if notification_file != None: + gr.Audio(interactive=False, value=notification_file, elem_id='audio_notification', visible=False) shared.gradio_root.launch( inbrowser=args_manager.args.auto_launch,