import { gradioApp, getUICurrentTabContent } from './script'; import { log } from './logger'; import { authFetch } from './authWrap'; import { quickApplyStyle, quickSaveStyle } from './extraNetworks'; interface ContextMenuItem { id: string; name: string; func: () => void; primary: boolean; } const contextMenuInit = () => { let eventListenerApplied = false; const menuSpecs = new Map(); const uid = () => Date.now().toString(36) + Math.random().toString(36).substring(2); function showContextMenu(event: MouseEvent, _element: Element, menuEntries: ContextMenuItem[]): void { const posx = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft; const posy = event.clientY + document.body.scrollTop + document.documentElement.scrollTop; const oldMenu = gradioApp().querySelector('#context-menu'); if (oldMenu) oldMenu.remove(); const contextMenu = document.createElement('nav'); contextMenu.id = 'context-menu'; 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((entry) => { const contextMenuEntry = document.createElement('a'); contextMenuEntry.innerHTML = entry.name; contextMenuEntry.addEventListener('click', () => entry.func()); contextMenuList.append(contextMenuEntry); }); gradioApp().appendChild(contextMenu); const menuWidth = contextMenu.offsetWidth + 4; const menuHeight = contextMenu.offsetHeight + 4; const windowWidth = window.innerWidth; const 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: string, entryName: string, entryFunction: () => void, primary = false): string { let currentItems = menuSpecs.get(targetElementSelector); if (!currentItems) { currentItems = []; menuSpecs.set(targetElementSelector, currentItems); } const newItem = { id: `${targetElementSelector}_${uid()}`, name: entryName, func: entryFunction, primary, // isNew: true, }; currentItems.push(newItem); return newItem.id; } function removeContextMenuOption(id: string): void { menuSpecs.forEach((v, k) => { let index = -1; v.forEach((e, ei) => { if (e.id === id) { index = ei; } }); if (index >= 0) v.splice(index, 1); }); } window.appendContextMenuOption = appendContextMenuOption; window.removeContextMenuOption = removeContextMenuOption; async function addContextMenuEventListener(): Promise { if (eventListenerApplied) return; log('initContextMenu'); gradioApp().addEventListener('click', (e: Event) => { const mouseEvent = e as MouseEvent; if (!mouseEvent.isTrusted) return; const oldMenu = gradioApp().querySelector('#context-menu'); if (oldMenu) oldMenu.remove(); menuSpecs.forEach((v, k) => { const items = v.filter((item) => item.primary); const target = mouseEvent.target as Element | null; if (!target) return; const matched = target.closest(k); if (items.length > 0 && matched) { showContextMenu(mouseEvent, matched, items); mouseEvent.preventDefault(); } }); }); gradioApp().addEventListener('contextmenu', (e: Event) => { const mouseEvent = e as MouseEvent; const oldMenu = gradioApp().querySelector('#context-menu'); if (oldMenu) oldMenu.remove(); menuSpecs.forEach((v, k) => { const items = v.filter((item) => !item.primary); const target = mouseEvent.target as Element | null; if (!target) return; const matched = target.closest(k); if (items.length > 0 && matched) { showContextMenu(mouseEvent, matched, items); mouseEvent.preventDefault(); } }); }); eventListenerApplied = true; } return [appendContextMenuOption, removeContextMenuOption, addContextMenuEventListener]; }; const initContextResponse = contextMenuInit(); const appendContextMenuOption = initContextResponse[0]; const removeContextMenuOption = initContextResponse[1]; const addContextMenuEventListener = initContextResponse[2] as () => void; let generateOnRepeatInterval: ReturnType | null = null; export const generateForever = (genbuttonid: string): void => { if (generateOnRepeatInterval) { log('generateForever: cancel'); clearInterval(generateOnRepeatInterval); generateOnRepeatInterval = null; } else { const genbutton = gradioApp().querySelector(genbuttonid); if (!(genbutton instanceof HTMLElement)) return; const isBusy = () => { let busy = document.getElementById('progressbar')?.style.display === 'block'; if (!busy) { // Also check in Modern UI const outerButton = genbutton.parentElement.closest('button'); busy = outerButton?.classList.contains('generate') && outerButton?.classList.contains('active'); } return busy; }; log('generateForever: start'); if (!isBusy()) genbutton.click(); generateOnRepeatInterval = setInterval(() => { if (!isBusy()) genbutton.click(); }, 500); } }; window.generateForever = generateForever; const reprocessClick = (tabId: string, state: string): void => { const btn = document.getElementById(`${tabId}_${state}`); window.submit_state = state; if (btn) btn.click(); }; const getStatus = async () => { const headers = new Headers(); const body = JSON.stringify({ id_task: -1, id_live_preview: false }); headers.set('Content-Type', 'application/json'); const tab = getUICurrentTabContent()?.id.replace('tab_', '') || ''; const el = gradioApp().querySelector(`#html_log_${tab} .performance p`); let res; let data; res = await fetch('./internal/progress', { method: 'POST', headers, body }); if (res?.ok) { data = await res.json(); log('progressInternal:', data); if (el) el.innerText += `\nProgress internal:\n${JSON.stringify(data, null, 2)}`; } res = await authFetch('./sdapi/v1/progress?skip_current_image=true', { method: 'GET', headers }); if (res?.ok) { data = await res.json(); log('progressAPI:', data); if (el) el.innerText += `\nProgress API:\n${JSON.stringify(data, null, 2)}`; } }; export async function initContextMenu() { for (const tab of ['txt2img', 'img2img', 'control', 'video']) { appendContextMenuOption(`#${tab}_generate`, 'Get server status', getStatus); appendContextMenuOption(`#${tab}_generate`, 'Copy prompt to clipboard', () => navigator.clipboard.writeText(document.querySelector(`#${tab}_prompt > label > textarea`).value)); appendContextMenuOption(`#${tab}_generate`, 'Generate forever', () => generateForever(`#${tab}_generate`)); appendContextMenuOption(`#${tab}_generate`, 'Apply selected style', quickApplyStyle); appendContextMenuOption(`#${tab}_generate`, 'Quick save style', quickSaveStyle); appendContextMenuOption(`#${tab}_reprocess`, 'Decode full quality', () => reprocessClick(tab, 'reprocess_decode'), true); appendContextMenuOption(`#${tab}_reprocess`, 'Refine & HiRes pass', () => reprocessClick(tab, 'reprocess_refine'), true); appendContextMenuOption(`#${tab}_reprocess`, 'Detailer pass', () => reprocessClick(tab, 'reprocess_detail'), true); } // Right-click send-to-control button for prompt/params-only transfer. for (const tab of ['gallery', 'txt2img', 'img2img', 'extras']) { appendContextMenuOption(`#${tab}_tabitem #control_tab`, 'Transfer only prompt to Images tab', () => { document.querySelector(`#image_buttons_${tab} #control_tab_prompt`)?.click(); document.getElementById('control_nav')?.click(); }); appendContextMenuOption(`#${tab}_tabitem #control_tab`, 'Transfer all parameters to Images tab', () => { document.querySelector(`#image_buttons_${tab} #control_tab_params`)?.click(); document.getElementById('control_nav')?.click(); }); } addContextMenuEventListener(); }