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;