diff --git a/css/ui/notifications.css b/css/ui/notifications.css new file mode 100644 index 0000000..a5b6dca --- /dev/null +++ b/css/ui/notifications.css @@ -0,0 +1,213 @@ +/** Notification area */ +.notification-area { + position: absolute; + + width: 250px; + min-width: 200px; + + z-index: 25; + + pointer-events: none; + + padding: 10px; +} + +.notification-area > * { + pointer-events: all; +} + +.notification-area.bottom-left { + left: 0px; + bottom: 0px; +} + +/** Notifications */ +.notification-area .notification { + position: relative; + + cursor: pointer; + + display: flex; + justify-content: space-between; + align-items: flex-start; + + border-radius: 5px; + border: solid 1px; + + padding: 5px; + margin-top: 5px; + + color: white; +} + +.notification-area .notification:hover { + filter: brightness(110%); +} + +.notification-area .notification:active { + filter: brightness(90%); +} + +.notification-area .notification-content { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + max-height: 100%; +} + +.notification-area .notification.expanded .notification-content { + white-space: normal !important; +} + +.notification .notification-closebtn { + position: relative; + + flex: 0; + + cursor: pointer; + + padding: 0; + border: 0; + + min-width: 20px; + max-width: 20px; + min-height: 20px; + max-height: 20px; + + border-radius: 10px; + + background-color: #0002; + + transition-duration: 20ms; +} + +.notification .notification-closebtn:hover { + background-color: #fff2; +} + +.notification .notification-closebtn:active { + background-color: #fff4; +} + +.notification .notification-closebtn::after { + content: ""; + position: absolute; + + cursor: pointer; + + top: 0; + right: 0; + + margin: 2px; + + width: 16px; + height: 16px; + + -webkit-mask-size: contain; + mask-size: contain; + + -webkit-mask-image: url("../../res/icons/x.svg"); + mask-image: url("../../res/icons/x.svg"); + + background-color: var(--c-text); +} + +/** Notification Types */ +.notification-area .notification.info { + background-color: #3976b399; + border-color: #12375c; +} + +.notification-area .notification.success { + background-color: #39b34999; + border-color: #1b5c12; +} + +.notification-area .notification.error { + background-color: #b3393999; + border-color: #5c1212; +} + +.notification-area .notification.warn { + background-color: #b3a13999; + border-color: #5c4e12; +} + +/** Dialog */ +.dialog-bg { + position: fixed; + + display: flex; + align-items: center; + justify-content: center; + + top: 0; + left: 0; + bottom: 0; + right: 0; + + backdrop-filter: blur(5px); + background-color: #fff6; + + z-index: 1000; +} + +.dialog-bg .dialog { + background-color: var(--c-primary); + color: var(--c-text); + + border-radius: 10px; + + position: absolute; + margin: auto; + + min-width: 200px; + min-height: 20px; + + max-width: 400px; +} + +.dialog .dialog-title { + margin: 10px; + font-weight: bold; +} + +.dialog .dialog-content { + margin: 10px; +} + +.dialog .dialog-choices { + display: flex; +} + +.dialog .dialog-choices > *:first-child { + border-bottom-left-radius: 10px; +} +.dialog .dialog-choices > *:last-child { + border-bottom-right-radius: 10px; +} + +.dialog .dialog-choices > * { + flex: 1; + + cursor: pointer; + + padding: 5px; + + background-color: transparent; + color: var(--c-text); + + border: 0px; + border-top: solid 1px var(--c-hover); + + transition-duration: 50ms; +} + +.dialog .dialog-choices > *:not(:first-child) { + border-left: solid 1px var(--c-hover); +} + +.dialog .dialog-choices > *:hover { + background-color: var(--c-hover); +} diff --git a/index.html b/index.html index d1a69eb..6379d17 100644 --- a/index.html +++ b/index.html @@ -12,6 +12,7 @@ + @@ -400,6 +401,7 @@ + diff --git a/js/config.js b/js/config.js index 5f55a75..488a311 100644 --- a/js/config.js +++ b/js/config.js @@ -38,6 +38,9 @@ const config = makeReadOnly( // Endpoint api: makeReadOnly({path: "/sdapi/v1/"}), + + // Default notification timeout + notificationTimeout: 8000, }, "config" ); diff --git a/js/index.js b/js/index.js index 90485d9..f098056 100644 --- a/js/index.js +++ b/js/index.js @@ -180,8 +180,8 @@ function setFixedHost(h, changePromptMessage) { hostInput.readOnly = true; hostInput.style.cursor = "default"; hostInput.style.backgroundColor = "#ddd"; - hostInput.addEventListener("dblclick", () => { - if (confirm(changePromptMessage)) { + hostInput.addEventListener("dblclick", async () => { + if (await notifications.dialog("Host is Locked", changePromptMessage)) { hostInput.style.backgroundColor = null; hostInput.style.cursor = null; hostInput.readOnly = false; @@ -341,16 +341,24 @@ async function testHostConnection() { ) => { const apiIssueResult = () => { setConnectionStatus("apiissue"); - const message = `The host is online, but the API seems to be disabled.\nHave you run the webui with the flag '--api', or is the flag '--gradio-debug' currently active?`; + const message = `The host is online, but the API seems to be disabled.
Have you run the webui with the flag '--api', or is the flag '--gradio-debug' currently active?`; console.error(message); - if (notify) alert(message); + if (notify) + notifications.notify(message, { + type: NotificationType.ERROR, + timeout: config.notificationTimeout * 2, + }); }; const offlineResult = () => { setConnectionStatus("offline"); const message = `The connection with the host returned an error: ${response.status} - ${response.statusText}`; console.error(message); - if (notify) alert(message); + if (notify) + notifications.notify(message, { + type: NotificationType.ERROR, + timeout: config.notificationTimeout * 2, + }); }; if (checkInProgress) throw new CheckInProgressError( @@ -384,9 +392,13 @@ async function testHostConnection() { ); const optionsdata = await response.json(); if (optionsdata["use_scale_latent_for_hires_fix"]) { - const message = `You are using an outdated version of A1111 webUI.\nThe HRfix options will not work until you update to at least commit ef27a18 or newer.\n(https://github.com/AUTOMATIC1111/stable-diffusion-webui/commit/ef27a18b6b7cb1a8eebdc9b2e88d25baf2c2414d)\nHRfix will fallback to half-resolution only.`; + const message = `You are using an outdated version of A1111 webUI.
The HRfix options will not work until you update to at least commit ef27a18 or newer.
(https://github.com/AUTOMATIC1111/stable-diffusion-webui/commit/ef27a18b6b7cb1a8eebdc9b2e88d25baf2c2414d)
HRfix will fallback to half-resolution only.`; console.warn(message); - if (notify) alert(message); + if (notify) + notifications.notify(message, { + type: NotificationType.WARN, + timeout: config.notificationTimeout * 4, + }); // Hide all new hrfix options document .querySelectorAll(".hrfix") @@ -428,14 +440,22 @@ async function testHostConnection() { setConnectionStatus("corsissue"); const message = `CORS is blocking our requests. Try running the webui with the flag '--cors-allow-origins=${window.location.protocol}//${window.location.host}/'`; console.error(message); - if (notify) alert(message); + if (notify) + notifications.notify(message, { + type: NotificationType.ERROR, + timeout: config.notificationTimeout * 2, + }); } catch (e) { setConnectionStatus("offline"); const message = `The server seems to be offline. Is host '${ document.getElementById("host").value }' correct?`; console.error(message); - if (notify) alert(message); + if (notify) + notifications.notify(message, { + type: NotificationType.ERROR, + timeout: config.notificationTimeout * 2, + }); } } checkInProgress = false; @@ -1062,13 +1082,17 @@ async function getModels(refresh = false) { body: JSON.stringify(payload), }); - alert(`Model changed to [${value}]`); + notifications.notify(`Model changed to [${value}]`, {type: "success"}); } catch (e) { console.warn("[index] Error changing model"); console.warn(e); - alert( - "Error changing model, please check console for additional information" + notifications.notify( + "Error changing model, please check console for additional information", + { + type: NotificationType.ERROR, + timeout: config.notificationTimeout * 2, + } ); } }); @@ -1082,13 +1106,16 @@ async function getModels(refresh = false) { These are highlighted as green in the model selector."; if (inpainting) { - message += `\n\nWe have found the inpainting model\n\n - ${inpainting.name}\n\navailable in the webui. Do you want to switch to it?`; - if (confirm(message)) { + message += `

We have found the inpainting model

- ${inpainting.name}

available in the webui. Do you want to switch to it?`; + if (await notifications.dialog("Automatic Model Switch", message)) { modelAutoComplete.value = inpainting.value; } } else { - message += `\n\nNo inpainting model seems to be available in the webui. It is recommended that you download an inpainting model, or outpainting results may not be optimal.`; - alert(message); + message += `

No inpainting model seems to be available in the webui. It is recommended that you download an inpainting model, or outpainting results may not be optimal.`; + notifications.notify(message, { + type: NotificationType.WARN, + timeout: null, + }); } } } @@ -1109,16 +1136,17 @@ async function getConfig() { // Check if img2img color correction is disabled and inpainting mask weight is set to one // TODO: API Seems bugged for retrieving inpainting mask weight - returning 0 for all values different than 1.0 if (data.img2img_color_correction) { - message += "\n - Image to Image Color Correction: false recommended"; + message += "
- Image to Image Color Correction: false recommended"; wrong = true; } if (data.inpainting_mask_weight < 1.0) { - message += `\n - Inpainting Conditioning Mask Strength: 1.0 recommended`; + message += `
- Inpainting Conditioning Mask Strength: 1.0 recommended`; wrong = true; } - message += "\n\nShould these values be changed to the recommended ones?"; + message += + "

Should these values be changed to the recommended ones?"; if (!wrong) { console.info("[index] WebUI Settings set as recommended."); @@ -1129,7 +1157,7 @@ async function getConfig() { "[index] WebUI Settings not set as recommended. Prompting for changing settings automatically." ); - if (!confirm(message)) return; + if (!(await notifications.dialog("Recommended Settings", message))) return; try { await fetch(url, { @@ -1331,8 +1359,13 @@ imageCollection.element.addEventListener( {passive: false} ); -function resetToDefaults() { - if (confirm("Are you sure you want to clear your settings?")) { +async function resetToDefaults() { + if ( + await notifications.dialog( + "Clear Settings", + "Are you sure you want to clear your settings?" + ) + ) { localStorage.clear(); } } diff --git a/js/initalize/workspace.populate.js b/js/initalize/workspace.populate.js index 0fe01a8..c285259 100644 --- a/js/initalize/workspace.populate.js +++ b/js/initalize/workspace.populate.js @@ -85,7 +85,9 @@ id = guid(); workspaces.add({id, name, workspace}).onsuccess = () => { listWorkspaces(id); - alert(`Workspace saved as '${name}'`); + notifications.notify(`Workspace saved as '${name}'`, { + type: "success", + }); }; } } else { @@ -93,7 +95,9 @@ const ws = e.target.result; if (ws) { workspaces.put({id, workspace}).onsuccess = () => { - alert(`Workspace saved as '${ws.value.name}'`); + notifications.notify(`Workspace saved as '${ws.value.name}'`, { + type: "success", + }); listWorkspaces(); }; } @@ -137,7 +141,7 @@ workspaces.get(id).onsuccess = (e) => { const workspace = e.target.result; const name = prompt( - `Please enter the new workspace name.\nOriginal is '${workspace.name}'` + `Please enter the new workspace name.
Original is '${workspace.name}'` ).trim(); if (!name) return; @@ -157,11 +161,12 @@ let id = workspaceAutocomplete.value; - workspaces.get(id).onsuccess = (e) => { + workspaces.get(id).onsuccess = async (e) => { const workspace = e.target.result; if ( - confirm( + await notifications.dialog( + "Delete Workspace", `Do you really want to delete the workspace '${workspace.name}'?` ) ) { diff --git a/js/lib/notifications.js b/js/lib/notifications.js new file mode 100644 index 0000000..e498590 --- /dev/null +++ b/js/lib/notifications.js @@ -0,0 +1,180 @@ +/** + * Enum representing the location of the notifications + * @readonly + * @enum {string} + */ +const NotificationLocation = { + TOPLEFT: "top-left", + TOPCENTER: "top-center", + TOPRIGHT: "top-right", + BOTTOMLEFT: "bottom-left", + BOTTOMCENTER: "bottom-center", + BOTTOMRIGHT: "bottom-right", +}; + +/** + * Enum representing notification types + * @readonly + * @enum {string} + */ +const NotificationType = { + INFO: "info", + ERROR: "error", + WARN: "warn", + SUCCESS: "success", +}; + +/** + * Responsible for the notification system + */ +const notifications = { + /** @type {NotificationLocation} */ + _location: NotificationLocation.BOTTOMLEFT, + /** @type {NotificationLocation} */ + get location() { + return this.location; + }, + /** @type {NotificationLocation} */ + set location(location) { + this._location = location; + }, + + // Notification Area Element + _areaEl: null, + + // Dialog BG Element + _dialogBGEl: null, + // Dialog Element + _dialogEl: null, + + /** + * Creates a simple notification for the user. Equivalent to alert() + * + * @param {string | HTMLElement} message Message to display to the user. + * @param {Object} options Extra options for the notification. + * @param {NotificationType} type Notification type + */ + notify(message, options = {}) { + defaultOpt(options, { + type: NotificationType.INFO, + timeout: config.notificationTimeout, + }); + + const notificationEl = document.createElement("div"); + notificationEl.classList.add("notification", "expanded", options.type); + notificationEl.title = new Date().toISOString(); + notificationEl.addEventListener("click", () => + notificationEl.classList.toggle("expanded") + ); + + const contentEl = document.createElement("div"); + contentEl.classList.add("notification-content"); + contentEl.innerHTML = message; + + notificationEl.append(contentEl); + + const closeBtn = document.createElement("button"); + closeBtn.classList.add("notification-closebtn"); + closeBtn.addEventListener("click", () => notificationEl.remove()); + + notificationEl.append(closeBtn); + + this._areaEl.prepend(notificationEl); + if (options.timeout) + setTimeout(() => { + if (this._areaEl.contains(notificationEl)) { + notificationEl.remove(); + } + }, options.timeout); + }, + + /** + * Creates a dialog box for the user with set options. + * + * @param {string} title The title of the dialog box to be displayed to the user. + * @param {string | HTMLElement} message The message to be displayed to the user. + * @param {Object} options Extra options for the dialog. + * @param {Array<{label: string, value: any}>} options.choices The choices to be displayed to the user. + */ + async dialog(title, message, options = {}) { + defaultOpt(options, { + // By default, it is a await notifications.dialogation dialog + choices: [ + {label: "No", value: false}, + {label: "Yes", value: true}, + ], + }); + + const titleEl = this._dialogEl.querySelector(".dialog-title"); + titleEl.textContent = title; + + const contentEl = this._dialogEl.querySelector(".dialog-content"); + contentEl.innerHTML = message; + + const choicesEl = this._dialogEl.querySelector(".dialog-choices"); + while (choicesEl.firstChild) { + choicesEl.firstChild.remove(); + } + + return new Promise((resolve, reject) => { + options.choices.forEach((choice) => { + const choiceBtn = document.createElement("button"); + choiceBtn.textContent = choice.label; + choiceBtn.addEventListener("click", () => { + this._dialogBGEl.style.display = "none"; + this._dialogEl.style.display = "none"; + + resolve(choice.value); + }); + + choicesEl.append(choiceBtn); + }); + + this._dialogBGEl.style.display = "flex"; + this._dialogEl.style.display = "block"; + }); + }, +}; +var k = 0; + +window.addEventListener("DOMContentLoaded", () => { + // Creates the notification area + const notificationArea = document.createElement("div"); + notificationArea.classList.add( + "notification-area", + NotificationLocation.BOTTOMLEFT + ); + + notifications._areaEl = notificationArea; + + document.body.appendChild(notificationArea); + + // Creates the dialog box element + const dialogBG = document.createElement("div"); + dialogBG.classList.add("dialog-bg"); + dialogBG.style.display = "none"; + + const dialogEl = document.createElement("div"); + dialogEl.classList.add("dialog"); + dialogEl.style.display = "none"; + + const titleEl = document.createElement("div"); + titleEl.classList.add("dialog-title"); + + const contentEl = document.createElement("div"); + contentEl.classList.add("dialog-content"); + + const choicesEl = document.createElement("div"); + choicesEl.classList.add("dialog-choices"); + + dialogEl.append(titleEl); + dialogEl.append(contentEl); + dialogEl.append(choicesEl); + + dialogBG.append(dialogEl); + + notifications._dialogEl = dialogEl; + notifications._dialogBGEl = dialogBG; + + document.body.appendChild(dialogBG); +}); diff --git a/js/ui/tool/dream.js b/js/ui/tool/dream.js index 154193c..41c02b0 100644 --- a/js/ui/tool/dream.js +++ b/js/ui/tool/dream.js @@ -439,8 +439,12 @@ const _generate = async (endpoint, request, bb, options = {}) => { stopDrawingStatus = true; at = 1; } catch (e) { - alert( - `Error generating images. Please try again or see console for more details` + notifications.notify( + `Error generating images. Please try again or see console for more details`, + { + type: NotificationType.ERROR, + timeout: config.notificationTimeout * 2, + } ); console.warn(`[dream] Error generating images:`); console.warn(e); @@ -576,8 +580,12 @@ const _generate = async (endpoint, request, bb, options = {}) => { imageindextxt.textContent = `${at}/${images.length - 1}`; } catch (e) { if (alertCount < 2) { - alert( - `Error generating images. Please try again or see console for more details` + notifications.notify( + `Error generating images. Please try again or see console for more details`, + { + type: NotificationType.ERROR, + timeout: config.notificationTimeout * 2, + } ); } else { eagerGenerateCount = 0; diff --git a/js/ui/tool/select.js b/js/ui/tool/select.js index d50a039..a86cbd9 100644 --- a/js/ui/tool/select.js +++ b/js/ui/tool/select.js @@ -735,21 +735,11 @@ const selectTransformTool = () => saveVisibleSelectionButton.textContent = "Save Vis."; saveVisibleSelectionButton.title = "Saves Visible Selection"; saveVisibleSelectionButton.onclick = () => { - const canvas = uil.getVisible( - { - x: - state._selected._position.x - - state._selected.canvas.width / 2, - y: - state._selected._position.y - - state._selected.canvas.height / 2, - w: state._selected.canvas.width, - h: state._selected.canvas.height, - }, - { - categories: ["image", "user", "select-display"], - } - ); + console.debug(state.selected); + console.debug(state.selected.bb); + const canvas = uil.getVisible(state._selected.bb, { + categories: ["image", "user", "select-display"], + }); downloadCanvas({ cropToContent: false, canvas, @@ -763,21 +753,9 @@ const selectTransformTool = () => createVisibleResourceButton.title = "Saves Visible Selection as a Resource"; createVisibleResourceButton.onclick = () => { - const canvas = uil.getVisible( - { - x: - state._selected._position.x - - state._selected.canvas.width / 2, - y: - state._selected._position.y - - state._selected.canvas.height / 2, - w: state._selected.canvas.width, - h: state._selected.canvas.height, - }, - { - categories: ["image", "user", "select-display"], - } - ); + const canvas = uil.getVisible(state._selected.bb, { + categories: ["image", "user", "select-display"], + }); const image = document.createElement("img"); image.src = canvas.toDataURL(); image.onload = () => { diff --git a/js/webui.js b/js/webui.js index de934f6..64a3d69 100644 --- a/js/webui.js +++ b/js/webui.js @@ -104,7 +104,7 @@ if (data.host) setFixedHost( data.host, - `Are you sure you want to modify the host?\nThis configuration was provided by the hosting page\n - ${parentWindow.document.title} (${origin})` + `Are you sure you want to modify the host?
This configuration was provided by the hosting page:
- ${parentWindow.document.title} (${origin})` ); if (data.destinations) webui.destinations = data.destinations;