fix(entry): resolve drag persistence collapse, dock entrapment, and regular refresh cache desync
This commit is contained in:
+4
-4
@@ -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.
|
||||
|
||||
@@ -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
@@ -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 };
|
||||
|
||||
@@ -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>";
|
||||
|
||||
@@ -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)))
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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"],
|
||||
|
||||
Reference in New Issue
Block a user