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
This commit is contained in:
Dariusz L
2026-06-30 00:27:52 +02:00
parent 3f4e3f0392
commit d0b7261671
5 changed files with 115 additions and 28 deletions
+1 -3
View File
@@ -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
+56 -10
View File
@@ -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}`);
}
+1 -1
View File
@@ -1,3 +1,3 @@
# Log level for development.
# Possible values: 'DEBUG', 'INFO', 'WARN', 'ERROR', 'NONE'
LOG_LEVEL = 'NONE'
LOG_LEVEL = 'DEBUG'
+1 -3
View File
@@ -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
+56 -11
View File
@@ -3,7 +3,7 @@ import { createModuleLogger } from "./LoggerUtils.js";
const log = createModuleLogger('NotificationUtils');
// Store active notifications for deduplication
const activeNotifications = new Map<string, { element: HTMLDivElement, timeout: number | null }>();
const activeNotifications = new Map<string, { element: HTMLDivElement, timeout: number | null, animationFrame: number | null }>();
/**
* 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}`);