From d0b7261671be2c830a5074cdab24757c8dd666cb Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Tue, 30 Jun 2026 00:27:52 +0200 Subject: [PATCH] Refactor notification progress to use requestAnimationFrame and fix db imports - Replace CSS animation-based notification progress bar with requestAnimationFrame-driven animation - Centralize getCanvasState/setCanvasState imports at top-level in CanvasView (remove dynamic import()) - Clean up animation frame lifecycle tracking to prevent orphaned rAF callbacks - Set local debug log level in python/config.py for development diagnostics --- js/CanvasView.js | 4 +- js/utils/NotificationUtils.js | 66 ++++++++++++++++++++++++++++----- python/config.py | 2 +- src/CanvasView.ts | 4 +- src/utils/NotificationUtils.ts | 67 ++++++++++++++++++++++++++++------ 5 files changed, 115 insertions(+), 28 deletions(-) diff --git a/js/CanvasView.js b/js/CanvasView.js index 9967114..320074f 100644 --- a/js/CanvasView.js +++ b/js/CanvasView.js @@ -6,7 +6,7 @@ import { ChangeTracker } from "../../scripts/changeTracker.js"; import { $el } from "../../scripts/ui.js"; import { addStylesheet, getUrl, loadTemplate } from "./utils/ResourceManager.js"; import { Canvas } from "./Canvas.js"; -import { clearAllCanvasStates } from "./db.js"; +import { clearAllCanvasStates, getCanvasState, setCanvasState } from "./db.js"; import { ImageCache } from "./ImageCache.js"; import { createCanvas } from "./utils/CommonUtils.js"; import { createModuleLogger } from "./utils/LoggerUtils.js"; @@ -1248,7 +1248,6 @@ app.registerExtension({ // Copy the canvas state now that the widget is initialized setTimeout(async () => { try { - const { getCanvasState, setCanvasState } = await import('./db.js'); let sourceState = await getCanvasState(String(sourceNodeId)); // If source node doesn't exist (cross-workflow paste), try clipboard if (!sourceState) { @@ -1444,7 +1443,6 @@ app.registerExtension({ // This happens async but that's fine since paste happens later (async () => { try { - const { getCanvasState, setCanvasState } = await import('./db.js'); const sourceState = await getCanvasState(String(this.id)); if (sourceState) { // Store in a special "clipboard" entry diff --git a/js/utils/NotificationUtils.js b/js/utils/NotificationUtils.js index 04d4610..2bafa1c 100644 --- a/js/utils/NotificationUtils.js +++ b/js/utils/NotificationUtils.js @@ -25,15 +25,25 @@ export function showNotification(message, backgroundColor = "#4a6cd4", duration if (existingNotification.timeout !== null) { clearTimeout(existingNotification.timeout); } + if (existingNotification.animationFrame !== null) { + cancelAnimationFrame(existingNotification.animationFrame); + existingNotification.animationFrame = null; + } // Find the progress bar and restart its animation - const progressBar = existingNotification.element.querySelector('div[style*="animation"]'); + const progressBar = existingNotification.element.querySelector('div[style*="lf-progress"], div[style*="scaleX"]'); if (progressBar) { - // Reset animation progressBar.style.animation = 'none'; - // Force reflow - void progressBar.offsetHeight; - // Restart animation - progressBar.style.animation = `lf-progress ${duration / 1000}s linear`; + progressBar.style.transition = 'none'; + progressBar.style.transform = 'scaleX(1)'; + const startedAt = performance.now(); + const updateProgress = (now) => { + const progress = Math.max(0, 1 - ((now - startedAt) / duration)); + progressBar.style.transform = `scaleX(${progress})`; + if (progress > 0) { + existingNotification.animationFrame = requestAnimationFrame(updateProgress); + } + }; + existingNotification.animationFrame = requestAnimationFrame(updateProgress); } // Set new timeout const newTimeout = window.setTimeout(() => { @@ -42,6 +52,10 @@ export function showNotification(message, backgroundColor = "#4a6cd4", duration notification.addEventListener('animationend', () => { if (notification.parentNode) { notification.parentNode.removeChild(notification); + const stored = activeNotifications.get(message); + if (stored?.animationFrame !== null && stored?.animationFrame !== undefined) { + cancelAnimationFrame(stored.animationFrame); + } activeNotifications.delete(message); const container = document.getElementById('lf-notification-container'); if (container && container.children.length === 0) { @@ -146,7 +160,7 @@ export function showNotification(message, backgroundColor = "#4a6cd4", duration body.appendChild(msgSpan); // --- Progress Bar --- const progressBar = document.createElement('div'); - progressBar.style.cssText = `height: 4px; width: 100%; background: ${config.bg}; box-shadow: 0 0 12px ${config.bg}; transform-origin: left; animation: lf-progress ${duration / 1000}s linear; flex-shrink: 0;`; + progressBar.style.cssText = `height: 4px; width: 100%; background: ${config.bg}; box-shadow: 0 0 12px ${config.bg}; transform-origin: left; transform: scaleX(1); transition: none; flex-shrink: 0;`; // --- Assemble Notification --- notification.appendChild(leftBar); notification.appendChild(header); @@ -189,7 +203,16 @@ export function showNotification(message, backgroundColor = "#4a6cd4", duration } body.classList.add('notification-scrollbar'); let dismissTimeout = null; + let progressAnimationFrame = null; const closeNotification = () => { + if (dismissTimeout !== null) { + clearTimeout(dismissTimeout); + dismissTimeout = null; + } + if (progressAnimationFrame !== null) { + cancelAnimationFrame(progressAnimationFrame); + progressAnimationFrame = null; + } // Remove from active notifications map if deduplicate is enabled if (deduplicate) { activeNotifications.delete(message); @@ -206,16 +229,37 @@ export function showNotification(message, backgroundColor = "#4a6cd4", duration }; closeBtn.onclick = closeNotification; const startDismissTimer = () => { + if (dismissTimeout !== null) + clearTimeout(dismissTimeout); + if (progressAnimationFrame !== null) + cancelAnimationFrame(progressAnimationFrame); dismissTimeout = window.setTimeout(closeNotification, duration); - progressBar.style.animation = `lf-progress ${duration / 1000}s linear`; + progressBar.style.animation = 'none'; + progressBar.style.transition = 'none'; + progressBar.style.transform = 'scaleX(1)'; + const startedAt = performance.now(); + const updateProgress = (now) => { + const progress = Math.max(0, 1 - ((now - startedAt) / duration)); + progressBar.style.transform = `scaleX(${progress})`; + if (progress > 0) { + progressAnimationFrame = requestAnimationFrame(updateProgress); + } + }; + progressAnimationFrame = requestAnimationFrame(updateProgress); }; const pauseAndRewindTimer = () => { if (dismissTimeout !== null) clearTimeout(dismissTimeout); dismissTimeout = null; + if (progressAnimationFrame !== null) + cancelAnimationFrame(progressAnimationFrame); + progressAnimationFrame = null; const computedStyle = window.getComputedStyle(progressBar); + progressBar.style.transition = 'none'; progressBar.style.transform = computedStyle.transform; - progressBar.style.animation = 'lf-progress-rewind 0.5s ease-out forwards'; + void progressBar.offsetHeight; + progressBar.style.transition = 'transform 0.5s ease-out'; + progressBar.style.transform = 'scaleX(1)'; }; notification.addEventListener('mouseenter', () => { pauseAndRewindTimer(); @@ -224,6 +268,7 @@ export function showNotification(message, backgroundColor = "#4a6cd4", duration const stored = activeNotifications.get(message); if (stored) { stored.timeout = null; + stored.animationFrame = null; } } }); @@ -234,13 +279,14 @@ export function showNotification(message, backgroundColor = "#4a6cd4", duration const stored = activeNotifications.get(message); if (stored) { stored.timeout = dismissTimeout; + stored.animationFrame = progressAnimationFrame; } } }); startDismissTimer(); // Store notification if deduplicate is enabled if (deduplicate) { - activeNotifications.set(message, { element: notification, timeout: dismissTimeout }); + activeNotifications.set(message, { element: notification, timeout: dismissTimeout, animationFrame: progressAnimationFrame }); } log.debug(`Notification shown: [Layer Forge] ${message}`); } diff --git a/python/config.py b/python/config.py index 0230b97..ebb3593 100644 --- a/python/config.py +++ b/python/config.py @@ -1,3 +1,3 @@ # Log level for development. # Possible values: 'DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE' -LOG_LEVEL = 'NONE' +LOG_LEVEL = 'DEBUG' diff --git a/src/CanvasView.ts b/src/CanvasView.ts index 3faabf7..c671da7 100644 --- a/src/CanvasView.ts +++ b/src/CanvasView.ts @@ -12,7 +12,7 @@ import {$el} from "../../scripts/ui.js"; import { addStylesheet, getUrl, loadTemplate } from "./utils/ResourceManager.js"; import {Canvas} from "./Canvas.js"; -import {clearAllCanvasStates} from "./db.js"; +import {clearAllCanvasStates, getCanvasState, setCanvasState} from "./db.js"; import {ImageCache} from "./ImageCache.js"; import {generateUniqueFileName, createCanvas} from "./utils/CommonUtils.js"; import {createModuleLogger} from "./utils/LoggerUtils.js"; @@ -1446,7 +1446,6 @@ app.registerExtension({ // Copy the canvas state now that the widget is initialized setTimeout(async () => { try { - const { getCanvasState, setCanvasState } = await import('./db.js'); let sourceState = await getCanvasState(String(sourceNodeId)); // If source node doesn't exist (cross-workflow paste), try clipboard @@ -1665,7 +1664,6 @@ app.registerExtension({ // This happens async but that's fine since paste happens later (async () => { try { - const { getCanvasState, setCanvasState } = await import('./db.js'); const sourceState = await getCanvasState(String(this.id)); if (sourceState) { // Store in a special "clipboard" entry diff --git a/src/utils/NotificationUtils.ts b/src/utils/NotificationUtils.ts index 5b75208..9b7e92d 100644 --- a/src/utils/NotificationUtils.ts +++ b/src/utils/NotificationUtils.ts @@ -3,7 +3,7 @@ import { createModuleLogger } from "./LoggerUtils.js"; const log = createModuleLogger('NotificationUtils'); // Store active notifications for deduplication -const activeNotifications = new Map(); +const activeNotifications = new Map(); /** * Utility functions for showing notifications to the user @@ -37,16 +37,26 @@ export function showNotification( if (existingNotification.timeout !== null) { clearTimeout(existingNotification.timeout); } + if (existingNotification.animationFrame !== null) { + cancelAnimationFrame(existingNotification.animationFrame); + existingNotification.animationFrame = null; + } // Find the progress bar and restart its animation - const progressBar = existingNotification.element.querySelector('div[style*="animation"]') as HTMLDivElement; + const progressBar = existingNotification.element.querySelector('div[style*="lf-progress"], div[style*="scaleX"]') 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`; + progressBar.style.transition = 'none'; + progressBar.style.transform = 'scaleX(1)'; + const startedAt = performance.now(); + const updateProgress = (now: number) => { + const progress = Math.max(0, 1 - ((now - startedAt) / duration)); + progressBar.style.transform = `scaleX(${progress})`; + if (progress > 0) { + existingNotification.animationFrame = requestAnimationFrame(updateProgress); + } + }; + existingNotification.animationFrame = requestAnimationFrame(updateProgress); } // Set new timeout @@ -56,6 +66,10 @@ export function showNotification( notification.addEventListener('animationend', () => { if (notification.parentNode) { notification.parentNode.removeChild(notification); + const stored = activeNotifications.get(message); + if (stored?.animationFrame !== null && stored?.animationFrame !== undefined) { + cancelAnimationFrame(stored.animationFrame); + } activeNotifications.delete(message); const container = document.getElementById('lf-notification-container'); if (container && container.children.length === 0) { @@ -172,7 +186,7 @@ export function showNotification( // --- Progress Bar --- const progressBar = document.createElement('div'); - progressBar.style.cssText = `height: 4px; width: 100%; background: ${config.bg}; box-shadow: 0 0 12px ${config.bg}; transform-origin: left; animation: lf-progress ${duration / 1000}s linear; flex-shrink: 0;`; + progressBar.style.cssText = `height: 4px; width: 100%; background: ${config.bg}; box-shadow: 0 0 12px ${config.bg}; transform-origin: left; transform: scaleX(1); transition: none; flex-shrink: 0;`; // --- Assemble Notification --- notification.appendChild(leftBar); @@ -220,7 +234,16 @@ export function showNotification( body.classList.add('notification-scrollbar'); let dismissTimeout: number | null = null; + let progressAnimationFrame: number | null = null; const closeNotification = () => { + if (dismissTimeout !== null) { + clearTimeout(dismissTimeout); + dismissTimeout = null; + } + if (progressAnimationFrame !== null) { + cancelAnimationFrame(progressAnimationFrame); + progressAnimationFrame = null; + } // Remove from active notifications map if deduplicate is enabled if (deduplicate) { activeNotifications.delete(message); @@ -240,16 +263,36 @@ export function showNotification( closeBtn.onclick = closeNotification; const startDismissTimer = () => { + if (dismissTimeout !== null) clearTimeout(dismissTimeout); + if (progressAnimationFrame !== null) cancelAnimationFrame(progressAnimationFrame); + dismissTimeout = window.setTimeout(closeNotification, duration); - progressBar.style.animation = `lf-progress ${duration / 1000}s linear`; + progressBar.style.animation = 'none'; + progressBar.style.transition = 'none'; + progressBar.style.transform = 'scaleX(1)'; + + const startedAt = performance.now(); + const updateProgress = (now: number) => { + const progress = Math.max(0, 1 - ((now - startedAt) / duration)); + progressBar.style.transform = `scaleX(${progress})`; + if (progress > 0) { + progressAnimationFrame = requestAnimationFrame(updateProgress); + } + }; + progressAnimationFrame = requestAnimationFrame(updateProgress); }; const pauseAndRewindTimer = () => { if (dismissTimeout !== null) clearTimeout(dismissTimeout); dismissTimeout = null; + if (progressAnimationFrame !== null) cancelAnimationFrame(progressAnimationFrame); + progressAnimationFrame = null; const computedStyle = window.getComputedStyle(progressBar); + progressBar.style.transition = 'none'; progressBar.style.transform = computedStyle.transform; - progressBar.style.animation = 'lf-progress-rewind 0.5s ease-out forwards'; + void progressBar.offsetHeight; + progressBar.style.transition = 'transform 0.5s ease-out'; + progressBar.style.transform = 'scaleX(1)'; }; notification.addEventListener('mouseenter', () => { @@ -259,6 +302,7 @@ export function showNotification( const stored = activeNotifications.get(message); if (stored) { stored.timeout = null; + stored.animationFrame = null; } } }); @@ -270,6 +314,7 @@ export function showNotification( const stored = activeNotifications.get(message); if (stored) { stored.timeout = dismissTimeout; + stored.animationFrame = progressAnimationFrame; } } }); @@ -278,7 +323,7 @@ export function showNotification( // Store notification if deduplicate is enabled if (deduplicate) { - activeNotifications.set(message, { element: notification, timeout: dismissTimeout }); + activeNotifications.set(message, { element: notification, timeout: dismissTimeout, animationFrame: progressAnimationFrame }); } log.debug(`Notification shown: [Layer Forge] ${message}`);