From 9c2db0f1e9436c3cb5fe98e92a8b7e52f0efb970 Mon Sep 17 00:00:00 2001 From: DemonGatanjieu <1027066412@qq.com> Date: Mon, 21 Sep 2026 01:03:15 +0800 Subject: [PATCH] fix(entry): resolve drag persistence collapse, dock entrapment, and regular refresh cache desync --- ARCHITECTURE.md | 8 +- api/__init__.py | 13 +++ web/main.js | 9 +- web/modules/browser_entry.js | 142 +++++++++++++++++----------- web/modules/entry_controls.js | 44 ++++++--- web/styles/00-foundation-models.css | 22 ++++- 6 files changed, 162 insertions(+), 76 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index a5c91ca..173dd28 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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. diff --git a/api/__init__.py b/api/__init__.py index fd0e758..afd667d 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -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) diff --git a/web/main.js b/web/main.js index 267bfe8..8e20d0d 100644 --- a/web/main.js +++ b/web/main.js @@ -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 }; diff --git a/web/modules/browser_entry.js b/web/modules/browser_entry.js index 913bff3..03e5c70 100644 --- a/web/modules/browser_entry.js +++ b/web/modules/browser_entry.js @@ -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,W"; diff --git a/web/modules/entry_controls.js b/web/modules/entry_controls.js index f894ff2..8f2dec9 100644 --- a/web/modules/entry_controls.js +++ b/web/modules/entry_controls.js @@ -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))) }; } diff --git a/web/styles/00-foundation-models.css b/web/styles/00-foundation-models.css index c465f7d..d39e444 100644 --- a/web/styles/00-foundation-models.css +++ b/web/styles/00-foundation-models.css @@ -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"],