fix(entry): resolve floating trigger position, boundary clamping, and refresh flicker

This commit is contained in:
DemonGatanjieu
2026-09-21 02:02:50 +08:00
parent dd1bb8639c
commit 09b06d1dc7
7 changed files with 343 additions and 109 deletions
+5 -5
View File
@@ -85,12 +85,12 @@ DOM or live LiteGraph state.
- `model_policies.py` owns shared backend rename and protected-category policy.
- `model_identity.py` owns file SHA-256 evidence shared with the standalone scanner.
### Frontend
- `web/main.js` coordinates extension registration. `browser.js` owns the shared
- `web/main.js` coordinates extension registration (with `?v=...` versioned module imports busting aggressive browser ES Module caching and unconditional legacy storage key purging). `browser.js` owns the shared
browser class and extracted-method wiring; `browser_entry.js` owns the single
browser instance plus floating/topbar/menu entry behavior; and
`interface_settings.js` owns language and theme preferences.
browser instance plus floating/topbar/menu entry behavior (with Dual-Binding PointerEvents drag capture, `lostpointercapture` fail-safe listeners, zero-drift viewport boundary clamping, flicker-free pre-mount coordinate binding and `anomalous-trigger-initializing` smooth opacity fade reveal, default safe placement in the top-left canvas area at `top: 80px; left: 80px;` gracefully avoiding the left sidebar dock, and versioned `anomalous_trigger_pos_v3` atomic JSON coordinate persistence); `entry_controls.js`
owns entry mode, trigger sizing/styling normalization, mathematical viewport-safe boundary
clamping (`clampFloatingTriggerPosition` with minimum safe boundary `minX=70` preventing left sidebar dock entrapment), clean `loadSavedTriggerPosition`/`saveTriggerPosition` storage drivers, and non-distorting coordinate validation
(`isValidSavedTriggerPosition`); `api/__init__.py` injects an aiohttp no-cache middleware (`Cache-Control: no-cache, no-store, must-revalidate`) for extension static files to eliminate browser memory/disk cache desynchronization across regular page refreshes; and `interface_settings.js` owns language and theme preferences.
- `ui_sidebar.js` creates the browser shell and folder navigation.
`ui_settings_hub.js` owns settings and model-card preferences;
`ui_toolbox.js` owns the tool catalog, fixed shortcut bar, and tool dispatch;
+13
View File
@@ -8,7 +8,20 @@ from .config import *
from .notebooks import *
from .parameters import *
from aiohttp import web
@web.middleware
async def no_cache_extension_middleware(request, handler):
response = await handler(request)
if request.path.startswith('/extensions/Anomalous_Model_Browser'):
response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate, max-age=0'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '0'
return response
def setup_routes(app):
if hasattr(app, 'middlewares') and no_cache_extension_middleware not in app.middlewares:
app.middlewares.append(no_cache_extension_middleware)
app.router.add_get('/anomalous/folders', model_catalog.api_get_folders)
app.router.add_get('/anomalous/all_folder_types', folder_types.api_get_all_folder_types)
app.router.add_get('/anomalous/models', model_catalog.api_get_models)
+8 -1
View File
@@ -1,5 +1,12 @@
try {
if (typeof localStorage !== 'undefined') {
localStorage.removeItem('anomalous_btn_x');
localStorage.removeItem('anomalous_btn_y');
}
} catch (_) {}
import { app } from "../../scripts/app.js";
import { createBrowserEntry } from "./modules/browser_entry.js";
import { createBrowserEntry } from "./modules/browser_entry.js?v=20260921-flicker-free-1";
import { createInterfaceSettings, getCurrentLanguage, setAbyssalScarletTheme, t } from "./modules/interface_settings.js";
export { setAbyssalScarletTheme };
+192 -89
View File
@@ -1,11 +1,24 @@
try {
if (typeof localStorage !== 'undefined') {
localStorage.removeItem('anomalous_btn_x');
localStorage.removeItem('anomalous_btn_y');
}
} catch (_) {}
import { app } from '../../../scripts/app.js';
import { AnomalousBrowser } from './browser.js';
import {
clampFloatingTriggerPosition,
clearSavedTriggerPosition,
DEFAULT_SAFE_LEFT,
DEFAULT_SAFE_TOP,
isValidSavedTriggerPosition,
loadSavedTriggerPosition,
normalizeEntryMode,
normalizeFloatingTriggerSize,
normalizeFloatingTriggerStyle
} from './entry_controls.js';
normalizeFloatingTriggerStyle,
saveTriggerPosition
} from './entry_controls.js?v=20260921-flicker-free-1';
import {
createShortcutSettingControl,
DEFAULT_BROWSER_SHORTCUT,
@@ -107,18 +120,18 @@ export function createBrowserEntry({ translate, getCurrentLanguage }) {
triggerButton.replaceChildren(icon, label);
triggerButton.title = t('mainOpenTitle');
triggerButton.setAttribute('aria-label', t('mainOpenTitle'));
requestAnimationFrame(() => triggerBoundsUpdater?.());
if (loadSavedTriggerPosition()) {
requestAnimationFrame(() => triggerBoundsUpdater?.());
}
}
function resetPosition() {
localStorage.removeItem('anomalous_btn_x');
localStorage.removeItem('anomalous_btn_y');
clearSavedTriggerPosition();
if (!triggerButton) return;
triggerButton.style.left = '';
triggerButton.style.top = '';
triggerButton.style.right = '30px';
triggerButton.style.bottom = '30px';
requestAnimationFrame(() => triggerBoundsUpdater?.());
triggerButton.style.right = '';
triggerButton.style.bottom = '';
}
function ensureBrowser() {
@@ -241,7 +254,7 @@ export function createBrowserEntry({ translate, getCurrentLanguage }) {
});
async function setup() {
const cssUrl = '/extensions/Anomalous_Model_Browser/styles.css?v=' + Date.now();
const cssUrl = '/extensions/Anomalous_Model_Browser/styles.css?v=20260921-flicker-free-1';
if (!document.querySelector('link[href^="/extensions/Anomalous_Model_Browser/styles.css"]')) {
const link = document.createElement('link');
link.rel = 'stylesheet';
@@ -250,86 +263,6 @@ export function createBrowserEntry({ translate, getCurrentLanguage }) {
document.head.appendChild(link);
}
const btn = document.createElement('button');
btn.id = 'anomalous-trigger-btn';
btn.setAttribute('aria-label', 'Anomalous Model Browser');
triggerButton = btn;
if (browserInstance) browserInstance.triggerButton = btn;
btn.title = t('mainOpenTitle');
let isDragging = false;
let startX;
let startY;
let initialX;
let initialY;
btn.addEventListener('mousedown', event => {
isDragging = true;
startX = event.clientX;
startY = event.clientY;
const rect = btn.getBoundingClientRect();
initialX = rect.left;
initialY = rect.top;
btn.style.transition = 'none';
});
window.addEventListener('mousemove', event => {
if (!isDragging) return;
event.preventDefault();
const nextPosition = clampFloatingTriggerPosition({
x: initialX + (event.clientX - startX),
y: initialY + (event.clientY - startY),
width: btn.offsetWidth,
height: btn.offsetHeight,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
margin: 0
});
btn.style.left = nextPosition.x + 'px';
btn.style.top = nextPosition.y + 'px';
btn.style.right = 'auto';
btn.style.bottom = 'auto';
});
window.addEventListener('mouseup', event => {
if (!isDragging) return;
isDragging = false;
btn.style.transition = 'transform 0.15s, box-shadow 0.15s';
localStorage.setItem('anomalous_btn_x', btn.style.left);
localStorage.setItem('anomalous_btn_y', btn.style.top);
if (Math.abs(event.clientX - startX) < 5 && Math.abs(event.clientY - startY) < 5) open();
});
let savedX = localStorage.getItem('anomalous_btn_x');
let savedY = localStorage.getItem('anomalous_btn_y');
const updateBtnBounds = () => {
savedX = localStorage.getItem('anomalous_btn_x');
savedY = localStorage.getItem('anomalous_btn_y');
const position = clampFloatingTriggerPosition({
x: btn.style.left || savedX,
y: btn.style.top || savedY,
width: btn.offsetWidth,
height: btn.offsetHeight,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight
});
btn.style.left = position.x + 'px';
btn.style.top = position.y + 'px';
btn.style.right = 'auto';
btn.style.bottom = 'auto';
};
triggerBoundsUpdater = updateBtnBounds;
if (savedX && savedY && savedX !== 'NaN' && savedY !== 'NaN') {
btn.style.right = 'auto';
btn.style.bottom = 'auto';
btn.style.left = savedX;
btn.style.top = savedY;
}
setTimeout(updateBtnBounds, 200);
window.addEventListener('resize', () => {
if (btn.style.left) updateBtnBounds();
syncVisibility();
});
document.body.appendChild(btn);
try {
const registeredSettings = app.extensionManager?.setting;
entryMode = normalizeEntryMode(registeredSettings?.get(ENTRY_MODE_SETTING_ID));
@@ -338,11 +271,181 @@ export function createBrowserEntry({ translate, getCurrentLanguage }) {
} catch (error) {
console.warn('[Anomalous Model Browser] Unable to read entry preferences:', error);
}
const btn = document.createElement('button');
btn.id = 'anomalous-trigger-btn';
btn.setAttribute('aria-label', 'Anomalous Model Browser');
// Prevent FOUC: Start hidden until presentation & exact coordinates are bound
btn.classList.add('anomalous-trigger-initializing');
triggerButton = btn;
if (browserInstance) browserInstance.triggerButton = btn;
btn.title = t('mainOpenTitle');
// Pre-configure content & presentation BEFORE mounting
applyPresentation();
// Pre-calculate and assign exact target coordinates BEFORE appending to DOM
const savedInitialPos = loadSavedTriggerPosition();
if (savedInitialPos) {
const estSize = floatingTriggerSize === 'small' ? 44 : (floatingTriggerSize === 'large' ? 76 : 60);
const initialClamped = clampFloatingTriggerPosition({
x: savedInitialPos.x,
y: savedInitialPos.y,
width: estSize,
height: estSize,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
minX: 70
});
btn.style.position = 'fixed';
btn.style.left = initialClamped.x + 'px';
btn.style.top = initialClamped.y + 'px';
btn.style.right = 'auto';
btn.style.bottom = 'auto';
}
let isDragging = false;
let hasMoved = false;
let startPointerX = 0;
let startPointerY = 0;
let startBtnLeft = 0;
let startBtnTop = 0;
let currentDragX = null;
let currentDragY = null;
let activePointerId = null;
const onPointerMove = (event) => {
if (!isDragging) return;
const dx = event.clientX - startPointerX;
const dy = event.clientY - startPointerY;
if (!hasMoved && Math.hypot(dx, dy) >= 4) {
hasMoved = true;
}
if (!hasMoved) return;
event.preventDefault();
const btnW = btn.offsetWidth || 60;
const btnH = btn.offsetHeight || 60;
const clamped = clampFloatingTriggerPosition({
x: startBtnLeft + dx,
y: startBtnTop + dy,
width: btnW,
height: btnH,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
minX: 70
});
currentDragX = clamped.x;
currentDragY = clamped.y;
btn.style.left = clamped.x + 'px';
btn.style.top = clamped.y + 'px';
btn.style.right = 'auto';
btn.style.bottom = 'auto';
};
const finishDrag = (event) => {
if (!isDragging) return;
isDragging = false;
btn.removeEventListener('pointermove', onPointerMove);
btn.removeEventListener('pointerup', finishDrag);
btn.removeEventListener('pointercancel', finishDrag);
btn.removeEventListener('lostpointercapture', finishDrag);
window.removeEventListener('pointermove', onPointerMove);
window.removeEventListener('pointerup', finishDrag);
window.removeEventListener('pointercancel', finishDrag);
if (activePointerId != null) {
try {
if (btn.hasPointerCapture(activePointerId)) {
btn.releasePointerCapture(activePointerId);
}
} catch (_) {}
activePointerId = null;
}
btn.style.transition = '';
if (hasMoved && currentDragX != null && currentDragY != null) {
saveTriggerPosition({ x: currentDragX, y: currentDragY });
} else if (!hasMoved) {
open();
}
};
btn.addEventListener('pointerdown', (event) => {
if (event.button !== 0) return;
isDragging = true;
hasMoved = false;
startPointerX = event.clientX;
startPointerY = event.clientY;
activePointerId = event.pointerId;
const rect = btn.getBoundingClientRect();
startBtnLeft = rect.left;
startBtnTop = rect.top;
currentDragX = rect.left;
currentDragY = rect.top;
btn.style.transition = 'none';
try {
btn.setPointerCapture(event.pointerId);
} catch (_) {}
btn.addEventListener('pointermove', onPointerMove, { passive: false });
btn.addEventListener('pointerup', finishDrag);
btn.addEventListener('pointercancel', finishDrag);
btn.addEventListener('lostpointercapture', finishDrag);
window.addEventListener('pointermove', onPointerMove, { passive: false });
window.addEventListener('pointerup', finishDrag);
window.addEventListener('pointercancel', finishDrag);
});
const updateBtnBounds = () => {
const saved = loadSavedTriggerPosition();
if (saved) {
const estFallback = floatingTriggerSize === 'small' ? 44 : (floatingTriggerSize === 'large' ? 76 : 60);
const btnW = btn.offsetWidth || estFallback;
const btnH = btn.offsetHeight || btnW;
const clamped = clampFloatingTriggerPosition({
x: saved.x,
y: saved.y,
width: btnW,
height: btnH,
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight,
minX: 70
});
btn.style.left = clamped.x + 'px';
btn.style.top = clamped.y + 'px';
btn.style.right = 'auto';
btn.style.bottom = 'auto';
} else {
btn.style.left = '';
btn.style.top = '';
btn.style.right = '';
btn.style.bottom = '';
}
};
triggerBoundsUpdater = updateBtnBounds;
window.addEventListener('resize', () => {
updateBtnBounds();
syncVisibility();
});
// Mount pre-positioned and styled element to DOM
document.body.appendChild(btn);
syncVisibility();
ensureBrowser();
installMaterialsShortcutFallback();
// Reveal smoothly on first animation frame without any positional jump or flicker
requestAnimationFrame(() => {
updateBtnBounds();
btn.classList.remove('anomalous-trigger-initializing');
});
window.anomalousDragGhostImg = new Image();
window.anomalousDragGhostImg.src = "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='80' height='80'><rect width='76' height='76' x='2' y='2' fill='%23140812' fill-opacity='0.85' rx='16' stroke='%23f59e0b' stroke-width='2'/><text x='40' y='50' font-family='sans-serif' font-size='32' font-weight='bold' fill='%23f59e0b' text-anchor='middle'>W</text></svg>";
}
+93 -7
View File
@@ -1,3 +1,11 @@
// Unconditionally purge legacy contaminated storage keys on module load
try {
if (typeof localStorage !== 'undefined') {
localStorage.removeItem('anomalous_btn_x');
localStorage.removeItem('anomalous_btn_y');
}
} catch (_) {}
export const FLOATING_TRIGGER_SIZES = new Set(['small', 'medium', 'large']);
export const FLOATING_TRIGGER_STYLES = new Set(['icon', 'pill']);
export const ENTRY_MODES = new Set(['floating', 'topbar', 'menu']);
@@ -14,6 +22,74 @@ export function normalizeEntryMode(value) {
return ENTRY_MODES.has(value) ? value : 'floating';
}
export const DEFAULT_SAFE_TOP = 80;
export const DEFAULT_SAFE_LEFT = 80; // Safe position outside ComfyUI left sidebar dock
export const DEFAULT_SAFE_MARGIN_RIGHT = 24;
export const DEFAULT_MIN_SAFE_X = 70; // Avoid ComfyUI left sidebar dock
export const DEFAULT_TRIGGER_POSITION = Object.freeze({ top: DEFAULT_SAFE_TOP, left: DEFAULT_SAFE_LEFT });
export const TRIGGER_POSITION_STORAGE_KEY = 'anomalous_trigger_pos_v3';
export const LEGACY_TRIGGER_X_KEY = 'anomalous_btn_x';
export const LEGACY_TRIGGER_Y_KEY = 'anomalous_btn_y';
export function isValidSavedTriggerPosition(x, y) {
if (x == null || y == null) return false;
const px = Number.parseFloat(x);
const py = Number.parseFloat(y);
// Must be a finite number, py >= 0, and px >= DEFAULT_MIN_SAFE_X (70px) to prevent docking inside sidebar
return Number.isFinite(px) && Number.isFinite(py) && px >= DEFAULT_MIN_SAFE_X && py >= 0;
}
export function loadSavedTriggerPosition(storage = (typeof localStorage !== 'undefined' ? localStorage : null)) {
if (!storage) return null;
try {
const raw = storage.getItem(TRIGGER_POSITION_STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw);
if (isValidSavedTriggerPosition(parsed?.x, parsed?.y)) {
return { x: Math.round(Number(parsed.x)), y: Math.round(Number(parsed.y)) };
} else {
storage.removeItem(TRIGGER_POSITION_STORAGE_KEY);
}
}
} catch (_) {}
return null;
}
export function saveTriggerPosition(pos, storage = (typeof localStorage !== 'undefined' ? localStorage : null)) {
if (!storage || !pos) return;
const x = Number.parseFloat(pos.x);
const y = Number.parseFloat(pos.y);
if (!isValidSavedTriggerPosition(x, y)) return;
try {
storage.setItem(TRIGGER_POSITION_STORAGE_KEY, JSON.stringify({
x: Math.round(x),
y: Math.round(y)
}));
storage.removeItem(LEGACY_TRIGGER_X_KEY);
storage.removeItem(LEGACY_TRIGGER_Y_KEY);
storage.removeItem('anomalous_trigger_pos_v2');
} catch (_) {}
}
export function clearSavedTriggerPosition(storage = (typeof localStorage !== 'undefined' ? localStorage : null)) {
if (!storage) return;
try {
storage.removeItem(TRIGGER_POSITION_STORAGE_KEY);
storage.removeItem(LEGACY_TRIGGER_X_KEY);
storage.removeItem(LEGACY_TRIGGER_Y_KEY);
storage.removeItem('anomalous_trigger_pos_v2');
} catch (_) {}
}
export function sanitizeSavedTriggerPosition(x, y) {
if (!isValidSavedTriggerPosition(x, y)) return null;
return { x: Number.parseFloat(x), y: Number.parseFloat(y) };
}
export function normalizeSavedTriggerPosition(x, y) {
return sanitizeSavedTriggerPosition(x, y);
}
export function clampFloatingTriggerPosition({
x,
y,
@@ -21,19 +97,29 @@ export function clampFloatingTriggerPosition({
height,
viewportWidth,
viewportHeight,
margin = 30
minX = DEFAULT_MIN_SAFE_X,
margin = 0
}) {
const safeWidth = Math.max(1, Number(width) || 60);
const safeHeight = Math.max(1, Number(height) || 60);
const maxX = Math.max(0, (Number(viewportWidth) || safeWidth) - safeWidth);
const maxY = Math.max(0, (Number(viewportHeight) || safeHeight) - safeHeight);
const fallbackX = Math.max(0, maxX - margin);
const fallbackY = Math.max(0, maxY - margin);
const rawVw = Number(viewportWidth) || (typeof window !== 'undefined' ? window.innerWidth : 1024) || 1024;
const rawVh = Number(viewportHeight) || (typeof window !== 'undefined' ? window.innerHeight : 768) || 768;
const safeVw = rawVw >= 300 ? rawVw : 1024;
const safeVh = rawVh >= 200 ? rawVh : 768;
const maxX = Math.max(minX, safeVw - safeWidth);
const maxY = Math.max(0, safeVh - safeHeight);
const parsedX = Number.parseFloat(x);
const parsedY = Number.parseFloat(y);
const fallbackX = Math.max(minX, Math.min(maxX, DEFAULT_SAFE_LEFT));
const fallbackY = Math.min(maxY, DEFAULT_SAFE_TOP);
const targetX = Number.isFinite(parsedX) ? parsedX : fallbackX;
const targetY = Number.isFinite(parsedY) ? parsedY : fallbackY;
return {
x: Math.min(maxX, Math.max(0, Number.isFinite(parsedX) ? parsedX : fallbackX)),
y: Math.min(maxY, Math.max(0, Number.isFinite(parsedY) ? parsedY : fallbackY))
x: Math.min(maxX, Math.max(minX, Math.round(targetX))),
y: Math.min(maxY, Math.max(0, Math.round(targetY)))
};
}
+1 -1
View File
@@ -1,4 +1,4 @@
@import url("./styles/00-foundation-models.css?v=20260918-model-sources-1");
@import url("./styles/00-foundation-models.css?v=20260921-flicker-free-1");
@import url("./styles/01-shell-tools.css?v=20260918-model-sources-1");
@import url("./styles/02-notebooks.css?v=20260918-model-sources-1");
@import url("./styles/03-gallery-recipes.css?v=20260918-model-sources-1");
+31 -6
View File
@@ -77,8 +77,10 @@ html.theme-abyssal-scarlet,
--anomalous-trigger-font-size: 28px;
--anomalous-trigger-pill-padding: 18px;
position: fixed;
bottom: 30px;
right: 30px;
top: 80px;
left: 80px;
right: auto;
bottom: auto;
width: var(--anomalous-trigger-size);
height: var(--anomalous-trigger-size);
border-radius: 50%;
@@ -93,15 +95,20 @@ html.theme-abyssal-scarlet,
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.12);
display: flex !important;
visibility: visible !important;
opacity: 1 !important;
opacity: 1;
align-items: center !important;
justify-content: center !important;
user-select: none;
transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
transition: opacity 0.18s ease-out, transform 0.2s cubic-bezier(0.16, 1, 0.3, 1), box-shadow 0.2s ease, background 0.2s ease, border-color 0.2s ease;
gap: 8px;
white-space: nowrap;
}
#anomalous-trigger-btn.anomalous-trigger-initializing {
opacity: 0 !important;
pointer-events: none !important;
}
#anomalous-trigger-btn .anomalous-trigger-icon {
display: flex;
align-items: center;
@@ -195,12 +202,30 @@ body:has(.anomalous-prompt-studio-drawer) #anomalous-trigger-btn {
box-shadow: none;
}
.anomalous-topbar-entry {
.anomalous-topbar-entry,
button:has(.anomalous-topbar-entry),
[data-testid="anomalous-topbar-entry"],
.side-bar-button:has(.anomalous-topbar-entry) {
display: none !important;
visibility: hidden !important;
width: 0 !important;
height: 0 !important;
padding: 0 !important;
margin: 0 !important;
border: none !important;
pointer-events: none !important;
overflow: hidden !important;
}
html.anomalous-topbar-entry-enabled .anomalous-topbar-entry {
html.anomalous-topbar-entry-enabled .anomalous-topbar-entry,
html.anomalous-topbar-entry-enabled button:has(.anomalous-topbar-entry),
html.anomalous-topbar-entry-enabled .side-bar-button:has(.anomalous-topbar-entry) {
display: inline-flex !important;
visibility: visible !important;
width: auto !important;
height: auto !important;
pointer-events: auto !important;
overflow: visible !important;
}
html:not(.anomalous-floating-entry-enabled) [data-setting-id="Anomalous.ModelBrowser.FloatingTriggerSize"],