fix(entry): resolve drag persistence collapse, dock entrapment, and regular refresh cache desync

This commit is contained in:
DemonGatanjieu
2026-09-21 01:03:15 +08:00
parent 364c5aff97
commit 9c2db0f1e9
6 changed files with 162 additions and 76 deletions
+4 -4
View File
@@ -88,12 +88,12 @@ DOM or live LiteGraph state.
### Frontend
- `web/main.js` coordinates extension registration (with `?v=...` versioned module imports busting aggressive browser ES Module caching). `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 (with Window-level PointerEvents drag capture, zero-drift viewport boundary clamping, mount-first DOM layout initialization, default safe placement in the top-right corner at `top: 80px; right: 24px;` avoiding both topbar menus and left sidebars, and versioned `anomalous_trigger_pos_v2` atomic JSON coordinate persistence); `entry_controls.js`
browser instance plus floating/topbar/menu entry behavior (with Dual-Binding PointerEvents drag capture, `lostpointercapture` fail-safe listeners, zero-drift viewport boundary clamping, mount-first DOM layout initialization with multi-stage rAF/timeout calibration, default safe placement in the top-right corner at `top: 80px; right: 24px;` avoiding both topbar menus and left sidebars, 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`), clean `loadSavedTriggerPosition`/`saveTriggerPosition` storage drivers, and non-distorting coordinate validation
(`isValidSavedTriggerPosition`); and `interface_settings.js` owns language and theme preferences.
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_domain_switcher.js` owns the dual-domain mode toggle (Visual Studio 🎨 vs Audio & Voice Studio 🎙️) embedded in the brand header.
- `ui_audio_studio.js` owns the Audio & Voice Studio workspace, presenting character voice cards, emotion slice playback, prompt syntax copying, real-time search, animated equalizers, and canvas drag-and-drop.
- `ui_audio_sidebar.js` owns the Audio Sidebar navigation, rendering trees for All Voices, Character Presets (e.g. Arona) with count badges, and Output History Vault.
+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?v=20260921-entry-v2";
import { createBrowserEntry } from "./modules/browser_entry.js?v=20260921-entry-v3";
import { createInterfaceSettings, getCurrentLanguage, setAbyssalScarletTheme, t } from "./modules/interface_settings.js";
export { setAbyssalScarletTheme };
+85 -57
View File
@@ -1,3 +1,10 @@
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 {
@@ -9,7 +16,7 @@ import {
normalizeFloatingTriggerSize,
normalizeFloatingTriggerStyle,
saveTriggerPosition
} from './entry_controls.js?v=20260921-entry-v2';
} from './entry_controls.js?v=20260921-entry-v3';
import {
createShortcutSettingControl,
DEFAULT_BROWSER_SHORTCUT,
@@ -111,7 +118,7 @@ export function createBrowserEntry({ translate, getCurrentLanguage }) {
triggerButton.replaceChildren(icon, label);
triggerButton.title = t('mainOpenTitle');
triggerButton.setAttribute('aria-label', t('mainOpenTitle'));
if (isValidSavedTriggerPosition(localStorage.getItem('anomalous_btn_x'), localStorage.getItem('anomalous_btn_y'))) {
if (loadSavedTriggerPosition()) {
requestAnimationFrame(() => triggerBoundsUpdater?.());
}
}
@@ -262,15 +269,18 @@ export function createBrowserEntry({ translate, getCurrentLanguage }) {
btn.title = t('mainOpenTitle');
let isDragging = false;
let hasMoved = false;
let startX = 0;
let startY = 0;
let initialX = 0;
let initialY = 0;
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 - startX;
const dy = event.clientY - startY;
const dx = event.clientX - startPointerX;
const dy = event.clientY - startPointerY;
if (!hasMoved && Math.hypot(dx, dy) >= 4) {
hasMoved = true;
}
@@ -278,69 +288,80 @@ export function createBrowserEntry({ translate, getCurrentLanguage }) {
event.preventDefault();
const btnW = btn.offsetWidth || 60;
const btnH = btn.offsetHeight || 60;
const maxW = Math.max(0, window.innerWidth - btnW);
const maxH = Math.max(0, window.innerHeight - btnH);
const nextX = Math.min(maxW, Math.max(0, initialX + dx));
const nextY = Math.min(maxH, Math.max(0, initialY + dy));
btn.style.left = nextX + 'px';
btn.style.top = nextY + 'px';
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 onPointerUp = (event) => {
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', onPointerUp);
window.removeEventListener('pointercancel', onPointerCancel);
try {
if (event && event.pointerId != null) {
btn.releasePointerCapture(event.pointerId);
}
} catch (_) {}
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) {
const curLeft = Number.parseFloat(btn.style.left);
const curTop = Number.parseFloat(btn.style.top);
if (Number.isFinite(curLeft) && Number.isFinite(curTop)) {
saveTriggerPosition({ x: curLeft, y: curTop });
}
} else {
if (hasMoved && currentDragX != null && currentDragY != null) {
saveTriggerPosition({ x: currentDragX, y: currentDragY });
} else if (!hasMoved) {
open();
}
};
const onPointerCancel = (event) => {
if (!isDragging) return;
isDragging = false;
window.removeEventListener('pointermove', onPointerMove);
window.removeEventListener('pointerup', onPointerUp);
window.removeEventListener('pointercancel', onPointerCancel);
try {
if (event && event.pointerId != null) {
btn.releasePointerCapture(event.pointerId);
}
} catch (_) {}
btn.style.transition = '';
};
btn.addEventListener('pointerdown', event => {
btn.addEventListener('pointerdown', (event) => {
if (event.button !== 0) return;
isDragging = true;
hasMoved = false;
startX = event.clientX;
startY = event.clientY;
startPointerX = event.clientX;
startPointerY = event.clientY;
activePointerId = event.pointerId;
const rect = btn.getBoundingClientRect();
initialX = rect.left;
initialY = rect.top;
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', onPointerUp);
window.addEventListener('pointercancel', onPointerCancel);
window.addEventListener('pointerup', finishDrag);
window.addEventListener('pointercancel', finishDrag);
});
const updateBtnBounds = () => {
@@ -348,12 +369,17 @@ export function createBrowserEntry({ translate, getCurrentLanguage }) {
if (saved) {
const btnW = btn.offsetWidth || 60;
const btnH = btn.offsetHeight || 60;
const maxW = Math.max(0, window.innerWidth - btnW);
const maxH = Math.max(0, window.innerHeight - btnH);
const px = Math.min(maxW, Math.max(0, saved.x));
const py = Math.min(maxH, Math.max(0, saved.y));
btn.style.left = px + 'px';
btn.style.top = py + 'px';
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 {
@@ -386,8 +412,10 @@ export function createBrowserEntry({ translate, getCurrentLanguage }) {
ensureBrowser();
installMaterialsShortcutFallback();
// Restore position after the element is in the DOM tree
// Restore position after the element is in the DOM tree, with multi-stage raf/timeout guards
updateBtnBounds();
requestAnimationFrame(() => updateBtnBounds());
setTimeout(() => updateBtnBounds(), 150);
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>";
+32 -12
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']);
@@ -16,8 +24,9 @@ export function normalizeEntryMode(value) {
export const DEFAULT_SAFE_TOP = 80;
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, right: DEFAULT_SAFE_MARGIN_RIGHT });
export const TRIGGER_POSITION_STORAGE_KEY = 'anomalous_trigger_pos_v2';
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';
@@ -25,7 +34,8 @@ export function isValidSavedTriggerPosition(x, y) {
if (x == null || y == null) return false;
const px = Number.parseFloat(x);
const py = Number.parseFloat(y);
return Number.isFinite(px) && Number.isFinite(py) && px >= 0 && py >= 0;
// 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)) {
@@ -35,7 +45,9 @@ export function loadSavedTriggerPosition(storage = (typeof localStorage !== 'und
if (raw) {
const parsed = JSON.parse(raw);
if (isValidSavedTriggerPosition(parsed?.x, parsed?.y)) {
return { x: Number(parsed.x), y: Number(parsed.y) };
return { x: Math.round(Number(parsed.x)), y: Math.round(Number(parsed.y)) };
} else {
storage.removeItem(TRIGGER_POSITION_STORAGE_KEY);
}
}
} catch (_) {}
@@ -43,14 +55,18 @@ export function loadSavedTriggerPosition(storage = (typeof localStorage !== 'und
}
export function saveTriggerPosition(pos, storage = (typeof localStorage !== 'undefined' ? localStorage : null)) {
if (!storage || !pos || !Number.isFinite(pos.x) || !Number.isFinite(pos.y)) return;
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(pos.x),
y: Math.round(pos.y)
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 (_) {}
}
@@ -60,6 +76,7 @@ export function clearSavedTriggerPosition(storage = (typeof localStorage !== 'un
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 (_) {}
}
@@ -79,26 +96,29 @@ export function clampFloatingTriggerPosition({
height,
viewportWidth,
viewportHeight,
minX = DEFAULT_MIN_SAFE_X,
margin = 0
}) {
const safeWidth = Math.max(1, Number(width) || 60);
const safeHeight = Math.max(1, Number(height) || 60);
const safeVw = Math.max(safeWidth + margin, Number(viewportWidth) || (typeof window !== 'undefined' ? window.innerWidth : 1024) || 1024);
const safeVh = Math.max(safeHeight + margin, Number(viewportHeight) || (typeof window !== 'undefined' ? window.innerHeight : 768) || 768);
const maxX = Math.max(0, safeVw - safeWidth);
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(0, maxX - DEFAULT_SAFE_MARGIN_RIGHT);
const fallbackX = Math.max(minX, maxX - DEFAULT_SAFE_MARGIN_RIGHT);
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, targetX)),
y: Math.min(maxY, Math.max(0, targetY))
x: Math.min(maxX, Math.max(minX, Math.round(targetX))),
y: Math.min(maxY, Math.max(0, Math.round(targetY)))
};
}
+20 -2
View File
@@ -197,12 +197,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"],