mirror of
https://github.com/Azornes/Comfyui-LayerForge.git
synced 2026-03-25 22:35:43 -03:00
feat: add notification system with deduplication
Implemented a comprehensive notification system with smart deduplication for LayerForge's "Paste Image" operations. The system prevents duplicate error/warning notifications while providing clear feedback for all clipboard operations including success, failure, and edge cases.
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
import { createModuleLogger } from "./LoggerUtils.js";
|
import { createModuleLogger } from "./LoggerUtils.js";
|
||||||
import { showNotification, showInfoNotification } from "./NotificationUtils.js";
|
import { showNotification, showInfoNotification, showErrorNotification, showWarningNotification } from "./NotificationUtils.js";
|
||||||
import { withErrorHandling, createValidationError, createNetworkError, createFileError } from "../ErrorHandler.js";
|
import { withErrorHandling, createValidationError, createNetworkError, createFileError } from "../ErrorHandler.js";
|
||||||
import { safeClipspacePaste } from "./ClipspaceUtils.js";
|
import { safeClipspacePaste } from "./ClipspaceUtils.js";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
@@ -18,6 +18,7 @@ export class ClipboardManager {
|
|||||||
if (this.canvas.canvasLayers.internalClipboard.length > 0) {
|
if (this.canvas.canvasLayers.internalClipboard.length > 0) {
|
||||||
log.info("Found layers in internal clipboard, pasting layers");
|
log.info("Found layers in internal clipboard, pasting layers");
|
||||||
this.canvas.canvasLayers.pasteLayers();
|
this.canvas.canvasLayers.pasteLayers();
|
||||||
|
showInfoNotification("Layers pasted from internal clipboard");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (preference === 'clipspace') {
|
if (preference === 'clipspace') {
|
||||||
@@ -27,9 +28,20 @@ export class ClipboardManager {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
log.info("No image found in ComfyUI Clipspace");
|
log.info("No image found in ComfyUI Clipspace");
|
||||||
|
// Don't show error here, will try system clipboard next
|
||||||
}
|
}
|
||||||
log.info("Attempting paste from system clipboard");
|
log.info("Attempting paste from system clipboard");
|
||||||
return await this.trySystemClipboardPaste(addMode);
|
const systemSuccess = await this.trySystemClipboardPaste(addMode);
|
||||||
|
if (!systemSuccess) {
|
||||||
|
// No valid image found in any clipboard
|
||||||
|
if (preference === 'clipspace') {
|
||||||
|
showWarningNotification("No valid image found in Clipspace or system clipboard");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
showWarningNotification("No valid image found in clipboard");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return systemSuccess;
|
||||||
}, 'ClipboardManager.handlePaste');
|
}, 'ClipboardManager.handlePaste');
|
||||||
/**
|
/**
|
||||||
* Attempts to paste from ComfyUI Clipspace
|
* Attempts to paste from ComfyUI Clipspace
|
||||||
@@ -51,6 +63,7 @@ export class ClipboardManager {
|
|||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = async () => {
|
img.onload = async () => {
|
||||||
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
||||||
|
showInfoNotification("Image pasted from Clipspace");
|
||||||
};
|
};
|
||||||
img.src = clipspaceImage.src;
|
img.src = clipspaceImage.src;
|
||||||
return true;
|
return true;
|
||||||
@@ -96,6 +109,7 @@ export class ClipboardManager {
|
|||||||
img.onload = async () => {
|
img.onload = async () => {
|
||||||
log.info("Successfully loaded image from backend response");
|
log.info("Successfully loaded image from backend response");
|
||||||
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
||||||
|
showInfoNotification("Image loaded from file path");
|
||||||
resolve(true);
|
resolve(true);
|
||||||
};
|
};
|
||||||
img.onerror = () => {
|
img.onerror = () => {
|
||||||
@@ -131,6 +145,7 @@ export class ClipboardManager {
|
|||||||
img.onload = async () => {
|
img.onload = async () => {
|
||||||
log.info("Successfully loaded image from system clipboard");
|
log.info("Successfully loaded image from system clipboard");
|
||||||
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
||||||
|
showInfoNotification("Image pasted from system clipboard");
|
||||||
};
|
};
|
||||||
if (event.target?.result) {
|
if (event.target?.result) {
|
||||||
img.src = event.target.result;
|
img.src = event.target.result;
|
||||||
@@ -252,10 +267,12 @@ export class ClipboardManager {
|
|||||||
img.onload = async () => {
|
img.onload = async () => {
|
||||||
log.info("Successfully loaded image from URL");
|
log.info("Successfully loaded image from URL");
|
||||||
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
||||||
|
showInfoNotification("Image loaded from URL");
|
||||||
resolve(true);
|
resolve(true);
|
||||||
};
|
};
|
||||||
img.onerror = () => {
|
img.onerror = () => {
|
||||||
log.warn("Failed to load image from URL:", filePath);
|
log.warn("Failed to load image from URL:", filePath);
|
||||||
|
showErrorNotification(`Failed to load image from URL\nThe link might be incorrect or may not point to an image file.: ${filePath}`, 5000, true);
|
||||||
resolve(false);
|
resolve(false);
|
||||||
};
|
};
|
||||||
img.src = filePath;
|
img.src = filePath;
|
||||||
@@ -313,6 +330,7 @@ export class ClipboardManager {
|
|||||||
img.onload = async () => {
|
img.onload = async () => {
|
||||||
log.info("Successfully loaded image from file picker");
|
log.info("Successfully loaded image from file picker");
|
||||||
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
||||||
|
showInfoNotification("Image loaded from selected file");
|
||||||
resolve(true);
|
resolve(true);
|
||||||
};
|
};
|
||||||
img.onerror = () => {
|
img.onerror = () => {
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { createModuleLogger } from "./LoggerUtils.js";
|
import { createModuleLogger } from "./LoggerUtils.js";
|
||||||
const log = createModuleLogger('NotificationUtils');
|
const log = createModuleLogger('NotificationUtils');
|
||||||
|
// Store active notifications for deduplication
|
||||||
|
const activeNotifications = new Map();
|
||||||
/**
|
/**
|
||||||
* Utility functions for showing notifications to the user
|
* Utility functions for showing notifications to the user
|
||||||
*/
|
*/
|
||||||
@@ -8,10 +10,50 @@ const log = createModuleLogger('NotificationUtils');
|
|||||||
* @param message - The message to show
|
* @param message - The message to show
|
||||||
* @param backgroundColor - Background color (default: #4a6cd4)
|
* @param backgroundColor - Background color (default: #4a6cd4)
|
||||||
* @param duration - Duration in milliseconds (default: 3000)
|
* @param duration - Duration in milliseconds (default: 3000)
|
||||||
|
* @param type - Type of notification
|
||||||
|
* @param deduplicate - If true, will not show duplicate messages and will refresh existing ones (default: false)
|
||||||
*/
|
*/
|
||||||
export function showNotification(message, backgroundColor = "#4a6cd4", duration = 3000, type = "info") {
|
export function showNotification(message, backgroundColor = "#4a6cd4", duration = 3000, type = "info", deduplicate = false) {
|
||||||
// Remove any existing prefix to avoid double prefixing
|
// Remove any existing prefix to avoid double prefixing
|
||||||
message = message.replace(/^\[Layer Forge\]\s*/, "");
|
message = message.replace(/^\[Layer Forge\]\s*/, "");
|
||||||
|
// If deduplication is enabled, check if this message already exists
|
||||||
|
if (deduplicate) {
|
||||||
|
const existingNotification = activeNotifications.get(message);
|
||||||
|
if (existingNotification) {
|
||||||
|
log.debug(`Notification already exists, refreshing timer: ${message}`);
|
||||||
|
// Clear existing timeout
|
||||||
|
if (existingNotification.timeout !== null) {
|
||||||
|
clearTimeout(existingNotification.timeout);
|
||||||
|
}
|
||||||
|
// Find the progress bar and restart its animation
|
||||||
|
const progressBar = existingNotification.element.querySelector('div[style*="animation"]');
|
||||||
|
if (progressBar) {
|
||||||
|
// Reset animation
|
||||||
|
progressBar.style.animation = 'none';
|
||||||
|
// Force reflow
|
||||||
|
void progressBar.offsetHeight;
|
||||||
|
// Restart animation
|
||||||
|
progressBar.style.animation = `lf-progress ${duration / 1000}s linear`;
|
||||||
|
}
|
||||||
|
// Set new timeout
|
||||||
|
const newTimeout = window.setTimeout(() => {
|
||||||
|
const notification = existingNotification.element;
|
||||||
|
notification.style.animation = 'lf-fadeout 0.3s ease-out forwards';
|
||||||
|
notification.addEventListener('animationend', () => {
|
||||||
|
if (notification.parentNode) {
|
||||||
|
notification.parentNode.removeChild(notification);
|
||||||
|
activeNotifications.delete(message);
|
||||||
|
const container = document.getElementById('lf-notification-container');
|
||||||
|
if (container && container.children.length === 0) {
|
||||||
|
container.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, duration);
|
||||||
|
existingNotification.timeout = newTimeout;
|
||||||
|
return; // Don't create a new notification
|
||||||
|
}
|
||||||
|
}
|
||||||
// Type-specific config
|
// Type-specific config
|
||||||
const config = {
|
const config = {
|
||||||
success: { icon: "✔️", title: "Success", bg: "#1fd18b" },
|
success: { icon: "✔️", title: "Success", bg: "#1fd18b" },
|
||||||
@@ -148,6 +190,10 @@ export function showNotification(message, backgroundColor = "#4a6cd4", duration
|
|||||||
body.classList.add('notification-scrollbar');
|
body.classList.add('notification-scrollbar');
|
||||||
let dismissTimeout = null;
|
let dismissTimeout = null;
|
||||||
const closeNotification = () => {
|
const closeNotification = () => {
|
||||||
|
// Remove from active notifications map if deduplicate is enabled
|
||||||
|
if (deduplicate) {
|
||||||
|
activeNotifications.delete(message);
|
||||||
|
}
|
||||||
notification.style.animation = 'lf-fadeout 0.3s ease-out forwards';
|
notification.style.animation = 'lf-fadeout 0.3s ease-out forwards';
|
||||||
notification.addEventListener('animationend', () => {
|
notification.addEventListener('animationend', () => {
|
||||||
if (notification.parentNode) {
|
if (notification.parentNode) {
|
||||||
@@ -171,40 +217,77 @@ export function showNotification(message, backgroundColor = "#4a6cd4", duration
|
|||||||
progressBar.style.transform = computedStyle.transform;
|
progressBar.style.transform = computedStyle.transform;
|
||||||
progressBar.style.animation = 'lf-progress-rewind 0.5s ease-out forwards';
|
progressBar.style.animation = 'lf-progress-rewind 0.5s ease-out forwards';
|
||||||
};
|
};
|
||||||
notification.addEventListener('mouseenter', pauseAndRewindTimer);
|
notification.addEventListener('mouseenter', () => {
|
||||||
notification.addEventListener('mouseleave', startDismissTimer);
|
pauseAndRewindTimer();
|
||||||
|
// Update stored timeout if deduplicate is enabled
|
||||||
|
if (deduplicate) {
|
||||||
|
const stored = activeNotifications.get(message);
|
||||||
|
if (stored) {
|
||||||
|
stored.timeout = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
notification.addEventListener('mouseleave', () => {
|
||||||
|
startDismissTimer();
|
||||||
|
// Update stored timeout if deduplicate is enabled
|
||||||
|
if (deduplicate) {
|
||||||
|
const stored = activeNotifications.get(message);
|
||||||
|
if (stored) {
|
||||||
|
stored.timeout = dismissTimeout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
startDismissTimer();
|
startDismissTimer();
|
||||||
|
// Store notification if deduplicate is enabled
|
||||||
|
if (deduplicate) {
|
||||||
|
activeNotifications.set(message, { element: notification, timeout: dismissTimeout });
|
||||||
|
}
|
||||||
log.debug(`Notification shown: [Layer Forge] ${message}`);
|
log.debug(`Notification shown: [Layer Forge] ${message}`);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Shows a success notification
|
* Shows a success notification
|
||||||
|
* @param message - The message to show
|
||||||
|
* @param duration - Duration in milliseconds (default: 3000)
|
||||||
|
* @param deduplicate - If true, will not show duplicate messages (default: false)
|
||||||
*/
|
*/
|
||||||
export function showSuccessNotification(message, duration = 3000) {
|
export function showSuccessNotification(message, duration = 3000, deduplicate = false) {
|
||||||
showNotification(message, undefined, duration, "success");
|
showNotification(message, undefined, duration, "success", deduplicate);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Shows an error notification
|
* Shows an error notification
|
||||||
|
* @param message - The message to show
|
||||||
|
* @param duration - Duration in milliseconds (default: 5000)
|
||||||
|
* @param deduplicate - If true, will not show duplicate messages (default: false)
|
||||||
*/
|
*/
|
||||||
export function showErrorNotification(message, duration = 5000) {
|
export function showErrorNotification(message, duration = 5000, deduplicate = false) {
|
||||||
showNotification(message, undefined, duration, "error");
|
showNotification(message, undefined, duration, "error", deduplicate);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Shows an info notification
|
* Shows an info notification
|
||||||
|
* @param message - The message to show
|
||||||
|
* @param duration - Duration in milliseconds (default: 3000)
|
||||||
|
* @param deduplicate - If true, will not show duplicate messages (default: false)
|
||||||
*/
|
*/
|
||||||
export function showInfoNotification(message, duration = 3000) {
|
export function showInfoNotification(message, duration = 3000, deduplicate = false) {
|
||||||
showNotification(message, undefined, duration, "info");
|
showNotification(message, undefined, duration, "info", deduplicate);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Shows a warning notification
|
* Shows a warning notification
|
||||||
|
* @param message - The message to show
|
||||||
|
* @param duration - Duration in milliseconds (default: 3000)
|
||||||
|
* @param deduplicate - If true, will not show duplicate messages (default: false)
|
||||||
*/
|
*/
|
||||||
export function showWarningNotification(message, duration = 3000) {
|
export function showWarningNotification(message, duration = 3000, deduplicate = false) {
|
||||||
showNotification(message, undefined, duration, "warning");
|
showNotification(message, undefined, duration, "warning", deduplicate);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Shows an alert notification
|
* Shows an alert notification
|
||||||
|
* @param message - The message to show
|
||||||
|
* @param duration - Duration in milliseconds (default: 3000)
|
||||||
|
* @param deduplicate - If true, will not show duplicate messages (default: false)
|
||||||
*/
|
*/
|
||||||
export function showAlertNotification(message, duration = 3000) {
|
export function showAlertNotification(message, duration = 3000, deduplicate = false) {
|
||||||
showNotification(message, undefined, duration, "alert");
|
showNotification(message, undefined, duration, "alert", deduplicate);
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* Shows a sequence of all notification types for debugging purposes.
|
* Shows a sequence of all notification types for debugging purposes.
|
||||||
@@ -214,7 +297,7 @@ export function showAllNotificationTypes(message) {
|
|||||||
types.forEach((type, index) => {
|
types.forEach((type, index) => {
|
||||||
const notificationMessage = message || `This is a '${type}' notification.`;
|
const notificationMessage = message || `This is a '${type}' notification.`;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
showNotification(notificationMessage, undefined, 3000, type);
|
showNotification(notificationMessage, undefined, 3000, type, false);
|
||||||
}, index * 400); // Stagger the notifications
|
}, index * 400); // Stagger the notifications
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {createModuleLogger} from "./LoggerUtils.js";
|
import {createModuleLogger} from "./LoggerUtils.js";
|
||||||
import { showNotification, showInfoNotification } from "./NotificationUtils.js";
|
import { showNotification, showInfoNotification, showErrorNotification, showWarningNotification } from "./NotificationUtils.js";
|
||||||
import { withErrorHandling, createValidationError, createNetworkError, createFileError } from "../ErrorHandler.js";
|
import { withErrorHandling, createValidationError, createNetworkError, createFileError } from "../ErrorHandler.js";
|
||||||
import { safeClipspacePaste } from "./ClipspaceUtils.js";
|
import { safeClipspacePaste } from "./ClipspaceUtils.js";
|
||||||
|
|
||||||
@@ -34,6 +34,7 @@ export class ClipboardManager {
|
|||||||
if (this.canvas.canvasLayers.internalClipboard.length > 0) {
|
if (this.canvas.canvasLayers.internalClipboard.length > 0) {
|
||||||
log.info("Found layers in internal clipboard, pasting layers");
|
log.info("Found layers in internal clipboard, pasting layers");
|
||||||
this.canvas.canvasLayers.pasteLayers();
|
this.canvas.canvasLayers.pasteLayers();
|
||||||
|
showInfoNotification("Layers pasted from internal clipboard");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,10 +45,22 @@ export class ClipboardManager {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
log.info("No image found in ComfyUI Clipspace");
|
log.info("No image found in ComfyUI Clipspace");
|
||||||
|
// Don't show error here, will try system clipboard next
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("Attempting paste from system clipboard");
|
log.info("Attempting paste from system clipboard");
|
||||||
return await this.trySystemClipboardPaste(addMode);
|
const systemSuccess = await this.trySystemClipboardPaste(addMode);
|
||||||
|
|
||||||
|
if (!systemSuccess) {
|
||||||
|
// No valid image found in any clipboard
|
||||||
|
if (preference === 'clipspace') {
|
||||||
|
showWarningNotification("No valid image found in Clipspace or system clipboard");
|
||||||
|
} else {
|
||||||
|
showWarningNotification("No valid image found in clipboard");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return systemSuccess;
|
||||||
}, 'ClipboardManager.handlePaste');
|
}, 'ClipboardManager.handlePaste');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -72,6 +85,7 @@ export class ClipboardManager {
|
|||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.onload = async () => {
|
img.onload = async () => {
|
||||||
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
||||||
|
showInfoNotification("Image pasted from Clipspace");
|
||||||
};
|
};
|
||||||
img.src = clipspaceImage.src;
|
img.src = clipspaceImage.src;
|
||||||
return true;
|
return true;
|
||||||
@@ -105,6 +119,7 @@ export class ClipboardManager {
|
|||||||
img.onload = async () => {
|
img.onload = async () => {
|
||||||
log.info("Successfully loaded image from system clipboard");
|
log.info("Successfully loaded image from system clipboard");
|
||||||
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
||||||
|
showInfoNotification("Image pasted from system clipboard");
|
||||||
};
|
};
|
||||||
if (event.target?.result) {
|
if (event.target?.result) {
|
||||||
img.src = event.target.result as string;
|
img.src = event.target.result as string;
|
||||||
@@ -240,15 +255,17 @@ export class ClipboardManager {
|
|||||||
const img = new Image();
|
const img = new Image();
|
||||||
img.crossOrigin = 'anonymous';
|
img.crossOrigin = 'anonymous';
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
img.onload = async () => {
|
img.onload = async () => {
|
||||||
log.info("Successfully loaded image from URL");
|
log.info("Successfully loaded image from URL");
|
||||||
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
||||||
resolve(true);
|
showInfoNotification("Image loaded from URL");
|
||||||
};
|
resolve(true);
|
||||||
img.onerror = () => {
|
};
|
||||||
log.warn("Failed to load image from URL:", filePath);
|
img.onerror = () => {
|
||||||
resolve(false);
|
log.warn("Failed to load image from URL:", filePath);
|
||||||
};
|
showErrorNotification(`Failed to load image from URL\nThe link might be incorrect or may not point to an image file.: ${filePath}`, 5000, true);
|
||||||
|
resolve(false);
|
||||||
|
};
|
||||||
img.src = filePath;
|
img.src = filePath;
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -326,6 +343,7 @@ export class ClipboardManager {
|
|||||||
img.onload = async () => {
|
img.onload = async () => {
|
||||||
log.info("Successfully loaded image from backend response");
|
log.info("Successfully loaded image from backend response");
|
||||||
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
||||||
|
showInfoNotification("Image loaded from file path");
|
||||||
resolve(true);
|
resolve(true);
|
||||||
};
|
};
|
||||||
img.onerror = () => {
|
img.onerror = () => {
|
||||||
@@ -366,6 +384,7 @@ export class ClipboardManager {
|
|||||||
img.onload = async () => {
|
img.onload = async () => {
|
||||||
log.info("Successfully loaded image from file picker");
|
log.info("Successfully loaded image from file picker");
|
||||||
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
await this.canvas.canvasLayers.addLayerWithImage(img, {}, addMode);
|
||||||
|
showInfoNotification("Image loaded from selected file");
|
||||||
resolve(true);
|
resolve(true);
|
||||||
};
|
};
|
||||||
img.onerror = () => {
|
img.onerror = () => {
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ import { createModuleLogger } from "./LoggerUtils.js";
|
|||||||
|
|
||||||
const log = createModuleLogger('NotificationUtils');
|
const log = createModuleLogger('NotificationUtils');
|
||||||
|
|
||||||
|
// Store active notifications for deduplication
|
||||||
|
const activeNotifications = new Map<string, { element: HTMLDivElement, timeout: number | null }>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Utility functions for showing notifications to the user
|
* Utility functions for showing notifications to the user
|
||||||
*/
|
*/
|
||||||
@@ -11,16 +14,62 @@ const log = createModuleLogger('NotificationUtils');
|
|||||||
* @param message - The message to show
|
* @param message - The message to show
|
||||||
* @param backgroundColor - Background color (default: #4a6cd4)
|
* @param backgroundColor - Background color (default: #4a6cd4)
|
||||||
* @param duration - Duration in milliseconds (default: 3000)
|
* @param duration - Duration in milliseconds (default: 3000)
|
||||||
|
* @param type - Type of notification
|
||||||
|
* @param deduplicate - If true, will not show duplicate messages and will refresh existing ones (default: false)
|
||||||
*/
|
*/
|
||||||
export function showNotification(
|
export function showNotification(
|
||||||
message: string,
|
message: string,
|
||||||
backgroundColor: string = "#4a6cd4",
|
backgroundColor: string = "#4a6cd4",
|
||||||
duration: number = 3000,
|
duration: number = 3000,
|
||||||
type: "success" | "error" | "info" | "warning" | "alert" = "info"
|
type: "success" | "error" | "info" | "warning" | "alert" = "info",
|
||||||
|
deduplicate: boolean = false
|
||||||
): void {
|
): void {
|
||||||
// Remove any existing prefix to avoid double prefixing
|
// Remove any existing prefix to avoid double prefixing
|
||||||
message = message.replace(/^\[Layer Forge\]\s*/, "");
|
message = message.replace(/^\[Layer Forge\]\s*/, "");
|
||||||
|
|
||||||
|
// If deduplication is enabled, check if this message already exists
|
||||||
|
if (deduplicate) {
|
||||||
|
const existingNotification = activeNotifications.get(message);
|
||||||
|
if (existingNotification) {
|
||||||
|
log.debug(`Notification already exists, refreshing timer: ${message}`);
|
||||||
|
|
||||||
|
// Clear existing timeout
|
||||||
|
if (existingNotification.timeout !== null) {
|
||||||
|
clearTimeout(existingNotification.timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the progress bar and restart its animation
|
||||||
|
const progressBar = existingNotification.element.querySelector('div[style*="animation"]') as HTMLDivElement;
|
||||||
|
if (progressBar) {
|
||||||
|
// Reset animation
|
||||||
|
progressBar.style.animation = 'none';
|
||||||
|
// Force reflow
|
||||||
|
void progressBar.offsetHeight;
|
||||||
|
// Restart animation
|
||||||
|
progressBar.style.animation = `lf-progress ${duration / 1000}s linear`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set new timeout
|
||||||
|
const newTimeout = window.setTimeout(() => {
|
||||||
|
const notification = existingNotification.element;
|
||||||
|
notification.style.animation = 'lf-fadeout 0.3s ease-out forwards';
|
||||||
|
notification.addEventListener('animationend', () => {
|
||||||
|
if (notification.parentNode) {
|
||||||
|
notification.parentNode.removeChild(notification);
|
||||||
|
activeNotifications.delete(message);
|
||||||
|
const container = document.getElementById('lf-notification-container');
|
||||||
|
if (container && container.children.length === 0) {
|
||||||
|
container.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, duration);
|
||||||
|
|
||||||
|
existingNotification.timeout = newTimeout;
|
||||||
|
return; // Don't create a new notification
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Type-specific config
|
// Type-specific config
|
||||||
const config = {
|
const config = {
|
||||||
success: { icon: "✔️", title: "Success", bg: "#1fd18b" },
|
success: { icon: "✔️", title: "Success", bg: "#1fd18b" },
|
||||||
@@ -172,6 +221,11 @@ export function showNotification(
|
|||||||
|
|
||||||
let dismissTimeout: number | null = null;
|
let dismissTimeout: number | null = null;
|
||||||
const closeNotification = () => {
|
const closeNotification = () => {
|
||||||
|
// Remove from active notifications map if deduplicate is enabled
|
||||||
|
if (deduplicate) {
|
||||||
|
activeNotifications.delete(message);
|
||||||
|
}
|
||||||
|
|
||||||
notification.style.animation = 'lf-fadeout 0.3s ease-out forwards';
|
notification.style.animation = 'lf-fadeout 0.3s ease-out forwards';
|
||||||
notification.addEventListener('animationend', () => {
|
notification.addEventListener('animationend', () => {
|
||||||
if (notification.parentNode) {
|
if (notification.parentNode) {
|
||||||
@@ -198,46 +252,86 @@ export function showNotification(
|
|||||||
progressBar.style.animation = 'lf-progress-rewind 0.5s ease-out forwards';
|
progressBar.style.animation = 'lf-progress-rewind 0.5s ease-out forwards';
|
||||||
};
|
};
|
||||||
|
|
||||||
notification.addEventListener('mouseenter', pauseAndRewindTimer);
|
notification.addEventListener('mouseenter', () => {
|
||||||
notification.addEventListener('mouseleave', startDismissTimer);
|
pauseAndRewindTimer();
|
||||||
|
// Update stored timeout if deduplicate is enabled
|
||||||
|
if (deduplicate) {
|
||||||
|
const stored = activeNotifications.get(message);
|
||||||
|
if (stored) {
|
||||||
|
stored.timeout = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
notification.addEventListener('mouseleave', () => {
|
||||||
|
startDismissTimer();
|
||||||
|
// Update stored timeout if deduplicate is enabled
|
||||||
|
if (deduplicate) {
|
||||||
|
const stored = activeNotifications.get(message);
|
||||||
|
if (stored) {
|
||||||
|
stored.timeout = dismissTimeout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
startDismissTimer();
|
startDismissTimer();
|
||||||
|
|
||||||
|
// Store notification if deduplicate is enabled
|
||||||
|
if (deduplicate) {
|
||||||
|
activeNotifications.set(message, { element: notification, timeout: dismissTimeout });
|
||||||
|
}
|
||||||
|
|
||||||
log.debug(`Notification shown: [Layer Forge] ${message}`);
|
log.debug(`Notification shown: [Layer Forge] ${message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows a success notification
|
* Shows a success notification
|
||||||
|
* @param message - The message to show
|
||||||
|
* @param duration - Duration in milliseconds (default: 3000)
|
||||||
|
* @param deduplicate - If true, will not show duplicate messages (default: false)
|
||||||
*/
|
*/
|
||||||
export function showSuccessNotification(message: string, duration: number = 3000): void {
|
export function showSuccessNotification(message: string, duration: number = 3000, deduplicate: boolean = false): void {
|
||||||
showNotification(message, undefined, duration, "success");
|
showNotification(message, undefined, duration, "success", deduplicate);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows an error notification
|
* Shows an error notification
|
||||||
|
* @param message - The message to show
|
||||||
|
* @param duration - Duration in milliseconds (default: 5000)
|
||||||
|
* @param deduplicate - If true, will not show duplicate messages (default: false)
|
||||||
*/
|
*/
|
||||||
export function showErrorNotification(message: string, duration: number = 5000): void {
|
export function showErrorNotification(message: string, duration: number = 5000, deduplicate: boolean = false): void {
|
||||||
showNotification(message, undefined, duration, "error");
|
showNotification(message, undefined, duration, "error", deduplicate);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows an info notification
|
* Shows an info notification
|
||||||
|
* @param message - The message to show
|
||||||
|
* @param duration - Duration in milliseconds (default: 3000)
|
||||||
|
* @param deduplicate - If true, will not show duplicate messages (default: false)
|
||||||
*/
|
*/
|
||||||
export function showInfoNotification(message: string, duration: number = 3000): void {
|
export function showInfoNotification(message: string, duration: number = 3000, deduplicate: boolean = false): void {
|
||||||
showNotification(message, undefined, duration, "info");
|
showNotification(message, undefined, duration, "info", deduplicate);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows a warning notification
|
* Shows a warning notification
|
||||||
|
* @param message - The message to show
|
||||||
|
* @param duration - Duration in milliseconds (default: 3000)
|
||||||
|
* @param deduplicate - If true, will not show duplicate messages (default: false)
|
||||||
*/
|
*/
|
||||||
export function showWarningNotification(message: string, duration: number = 3000): void {
|
export function showWarningNotification(message: string, duration: number = 3000, deduplicate: boolean = false): void {
|
||||||
showNotification(message, undefined, duration, "warning");
|
showNotification(message, undefined, duration, "warning", deduplicate);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows an alert notification
|
* Shows an alert notification
|
||||||
|
* @param message - The message to show
|
||||||
|
* @param duration - Duration in milliseconds (default: 3000)
|
||||||
|
* @param deduplicate - If true, will not show duplicate messages (default: false)
|
||||||
*/
|
*/
|
||||||
export function showAlertNotification(message: string, duration: number = 3000): void {
|
export function showAlertNotification(message: string, duration: number = 3000, deduplicate: boolean = false): void {
|
||||||
showNotification(message, undefined, duration, "alert");
|
showNotification(message, undefined, duration, "alert", deduplicate);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -248,7 +342,7 @@ export function showAllNotificationTypes(message?: string): void {
|
|||||||
types.forEach((type, index) => {
|
types.forEach((type, index) => {
|
||||||
const notificationMessage = message || `This is a '${type}' notification.`;
|
const notificationMessage = message || `This is a '${type}' notification.`;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
showNotification(notificationMessage, undefined, 3000, type);
|
showNotification(notificationMessage, undefined, 3000, type, false);
|
||||||
}, index * 400); // Stagger the notifications
|
}, index * 400); // Stagger the notifications
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user