From 8ca6eddc878f0d08a26324c391f3fff088eb5728 Mon Sep 17 00:00:00 2001 From: AlexsandrSnytkin Date: Mon, 21 Sep 2026 16:27:08 +0700 Subject: [PATCH] feat(ui): add scale toggle to each block --- ui_server.py | 93 ++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 64 insertions(+), 29 deletions(-) diff --git a/ui_server.py b/ui_server.py index ea4f561..72435b6 100644 --- a/ui_server.py +++ b/ui_server.py @@ -1287,8 +1287,8 @@ HTML = """ .config-scale-control input { width: 74px; height: 16px; margin: 0; accent-color: var(--ui-accent); cursor: pointer; } .config-scale-control input:disabled { opacity: .45; cursor: default; } .config-scale-control output { min-width: 34px; color: var(--ui-text); font-variant-numeric: tabular-nums; text-align: right; } - .config-scale-mode { min-height: 26px !important; padding: 0 8px !important; white-space: nowrap; } - .config-scale-mode[aria-pressed="true"] { color: #fff !important; background: var(--ui-accent-bg) !important; border-color: var(--ui-accent) !important; box-shadow: 0 0 0 2px color-mix(in srgb, var(--ui-accent) 16%, transparent); } + .block-scale-toggle { flex: 0 0 auto !important; width: auto !important; min-width: 48px !important; min-height: 23px !important; height: 23px !important; margin-left: auto; padding: 0 6px !important; border: 1px solid var(--ui-border) !important; border-radius: 6px !important; background: color-mix(in srgb, var(--ui-accent) 8%, var(--ui-panel)) !important; color: var(--ui-muted) !important; font: 800 9px/1 ui-sans-serif, system-ui, sans-serif !important; letter-spacing: .02em; cursor: pointer !important; } + .block-scale-toggle:hover, .block-scale-toggle[aria-pressed="true"] { color: #fff !important; background: var(--ui-accent-bg) !important; border-color: var(--ui-accent) !important; } .config-layout-state { min-width: 0; color: var(--ui-muted); font-size: 10px; } .config-workspace { display: grid; grid-template-columns: minmax(0, 1fr); min-width: 0; min-height: 0; } .config-workspace.has-structure { grid-template-columns: minmax(190px, 260px) minmax(0, 1fr); align-items: start; } @@ -1329,7 +1329,7 @@ HTML = """ .config-canvas > .config-custom-block { border-style: dashed !important; } .config-block > summary { position: sticky; top: 0; z-index: 2; background: color-mix(in srgb, var(--ui-panel) 92%, var(--ui-accent)); border-bottom: 1px solid color-mix(in srgb, var(--ui-border) 80%, transparent); } .config-block > summary::after { display: none !important; } - .block-title { min-width: 0; display: inline-flex; align-items: center; gap: 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .block-title { min-width: 0; flex: 1 1 auto; display: inline-flex; align-items: center; gap: 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .block-title::before { content: "✦"; flex: 0 0 auto; color: var(--ui-accent); font-size: .85em; opacity: .8; } .config-block > summary { cursor: grab; touch-action: none; } .config-block > summary:active, .config-block.is-dragging > summary { cursor: grabbing; } @@ -1496,6 +1496,8 @@ HTML = """ .network-custom-block { border-style: dashed !important; } .network-block.is-deleted, .network-block.is-hidden, .network-canvas > .network-block[data-block-state="hidden"], .network-canvas > .network-block[data-block-state="deleted"] { display: none !important; } .network-summary { display: flex; align-items: center; gap: 6px; margin: -2px 0 8px; padding-bottom: 8px; border-bottom: 1px solid color-mix(in srgb, var(--ui-border) 80%, transparent); color: var(--ui-text); font-size: 12px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; } + .network-summary .block-title::before { display: none; } + .network-summary .block-scale-toggle { margin-left: auto; } .network-summary::before { content: "✦"; color: var(--ui-accent); font-size: 11px; } .network-block.settings-group { --block-scale: 1; padding: calc(14px * var(--block-scale)); gap: calc(8px * var(--block-scale)); } .network-block.settings-group label, .network-block.settings-group .model-state { gap: calc(3px * var(--block-scale)); font-size: clamp(8px, calc(11px * var(--block-scale)), 22px); } @@ -1605,7 +1607,6 @@ HTML = """ - Ctrl/Cmd + клик — выбрать несколько · перетаскивайте группу @@ -1832,7 +1833,6 @@ HTML = """ - Ctrl/Cmd + клик — выбрать несколько · перетаскивайте группу @@ -2097,7 +2097,6 @@ HTML = """ let deletedPanelOpen = false; let configZIndex = 1; let configContentScale = 1; - let configScaleWheelEnabled = false; let configBlockObserver = null; let selectedConfigBlock = null; let selectedConfigBlocks = new Set(); @@ -2141,15 +2140,16 @@ HTML = """ setValue('configContentScale', Math.round(configContentScale * 100)); setText('configContentScaleValue', `${Math.round(configContentScale * 100)}%`); updateConfigBlockScale(block); + updateConfigScaleToggle(block); saveConfigLayout(); } - function setConfigScaleMode(enabled) { - configScaleWheelEnabled = !!enabled && !!selectedConfigBlock; - const button = $('configScaleMode'); + function updateConfigScaleToggle(block) { + const button = block?.querySelector(':scope > summary .block-scale-toggle'); if (!button) return; - button.disabled = !selectedConfigBlock; - button.setAttribute('aria-pressed', String(configScaleWheelEnabled)); - button.textContent = configScaleWheelEnabled ? 'масштаб колесом: вкл' : 'масштаб колесом: выкл'; + const value = Math.round(Number(configLayout[block.dataset.section]?.scale) || 100); + button.textContent = `↕ ${value}%`; + button.setAttribute('aria-pressed', String(!!block._configScaleWheelEnabled)); + button.title = block._configScaleWheelEnabled ? 'Масштаб колесом включён · нажмите, чтобы выключить' : 'Включить масштаб колесом для этого блока'; } function selectConfigBlock(block) { selectedConfigBlocks = new Set(block && !configBlockState(block) ? [block] : []); @@ -2165,7 +2165,6 @@ HTML = """ if (!selectedConfigBlock) { if (control) control.disabled = true; if (remove) remove.disabled = true; - setConfigScaleMode(false); return; } const entry = configLayout[selectedConfigBlock.dataset.section] || {}; @@ -2175,7 +2174,6 @@ HTML = """ setText('configContentScaleValue', `${value}%`); if (control) control.disabled = selectedConfigBlocks.size !== 1; if (remove) remove.disabled = !selectedConfigBlocks.size; - setConfigScaleMode(configScaleWheelEnabled); updateConfigBlockScale(selectedConfigBlock); if (selectedConfigBlocks.size > 1) setText('configLayoutState', `выбрано ${selectedConfigBlocks.size} блока · перетаскивайте группу`); } @@ -2576,7 +2574,21 @@ HTML = """ const titleNode = document.createElement('span'); titleNode.className = 'block-title'; titleNode.textContent = title; - summary.appendChild(titleNode); + const scaleToggle = document.createElement('button'); + scaleToggle.className = 'block-scale-toggle'; + scaleToggle.type = 'button'; + scaleToggle.setAttribute('aria-label', `Масштаб блока «${title}» колесом`); + scaleToggle.addEventListener('pointerdown', event => { event.preventDefault(); event.stopPropagation(); }); + scaleToggle.addEventListener('click', event => { + event.preventDefault(); + event.stopPropagation(); + selectConfigBlock(block); + bringConfigBlockFront(block); + block._configScaleWheelEnabled = !block._configScaleWheelEnabled; + updateConfigScaleToggle(block); + }); + summary.append(titleNode, scaleToggle); + updateConfigScaleToggle(block); summary.setAttribute('aria-label', `${title}. Перетаскивайте заголовок для перемещения`); summary.setAttribute('aria-expanded', 'true'); summary.addEventListener('click', event => { @@ -2653,7 +2665,7 @@ HTML = """ } }); block.addEventListener('wheel', event => { - if (!configScaleWheelEnabled || configBlockState(block) || !event.deltaY) return; + if (!block._configScaleWheelEnabled || configBlockState(block) || !event.deltaY) return; event.preventDefault(); event.stopPropagation(); if (selectedConfigBlock !== block) selectConfigBlock(block); @@ -2779,7 +2791,6 @@ HTML = """ selectConfigBlock(configBlocks.find(block => !configBlockState(block)) || null); $('configResetLayout')?.addEventListener('click', resetConfigLayout); $('configDeleteSelected')?.addEventListener('click', deleteSelectedConfigBlocks); - $('configScaleMode')?.addEventListener('click', () => setConfigScaleMode(!configScaleWheelEnabled)); $('configRestoreToggle')?.addEventListener('click', () => { restorePanelOpen = !restorePanelOpen; renderConfigRestorePanel(); @@ -2835,7 +2846,6 @@ HTML = """ let selectedNetworkBlocks = new Set(); let networkStructureOpen = false; let networkContentScale = 1; - let networkScaleWheelEnabled = false; function readNetworkLayout() { try { const saved = JSON.parse(localStorage.getItem('fpv-network-layout-v1') || '{}'); @@ -2851,7 +2861,11 @@ HTML = """ function networkMinWidth() { return window.innerWidth <= 760 ? 220 : 240; } function networkCanvasWidth() { return Math.max(networkMinWidth(), (networkCanvas?.clientWidth || 0) - 28); } function networkState(block) { return block?.dataset.blockState || ''; } - function networkTitle(block) { return block?.querySelector(':scope > .network-summary')?.textContent?.trim() || block?.dataset.section || 'Блок'; } + function networkTitle(block) { + const summary = block?.querySelector(':scope > .network-summary'); + const direct = summary ? [...summary.childNodes].filter(node => node.nodeType === Node.TEXT_NODE).map(node => node.textContent).join('').trim() : ''; + return block?.querySelector(':scope > .network-summary .block-title')?.textContent?.trim() || direct || block?.dataset.section || 'Блок'; + } function networkTemplateKey(block) { return block?.dataset.custom === 'true' ? (block.dataset.template || 'model') : block?.dataset.section; } function nextNetworkKey() { let index = 1; @@ -2882,7 +2896,6 @@ HTML = """ if (!networkSelectedBlock) { if (control) control.disabled = true; if (remove) remove.disabled = true; - setNetworkScaleMode(false); return; } const value = Math.max(50, Math.min(200, Math.round(Number(networkLayout[networkSelectedBlock.dataset.section]?.scale) || 100))); @@ -2891,7 +2904,6 @@ HTML = """ setText('networkContentScaleValue', `${value}%`); if (control) control.disabled = selectedNetworkBlocks.size !== 1; if (remove) remove.disabled = !selectedNetworkBlocks.size; - setNetworkScaleMode(networkScaleWheelEnabled); updateNetworkBlockScale(networkSelectedBlock); if (selectedNetworkBlocks.size > 1) setText('networkLayoutState', `выбрано ${selectedNetworkBlocks.size} блока · перетаскивайте группу`); } @@ -2912,15 +2924,16 @@ HTML = """ networkLayout[block.dataset.section] = entry; setText('networkContentScaleValue', `${Math.round(networkContentScale * 100)}%`); updateNetworkBlockScale(block); + updateNetworkScaleToggle(block); saveNetworkLayout(); } - function setNetworkScaleMode(enabled) { - networkScaleWheelEnabled = !!enabled && !!networkSelectedBlock; - const button = $('networkScaleMode'); + function updateNetworkScaleToggle(block) { + const button = block?.querySelector(':scope > .network-summary .block-scale-toggle'); if (!button) return; - button.disabled = !networkSelectedBlock; - button.setAttribute('aria-pressed', String(networkScaleWheelEnabled)); - button.textContent = networkScaleWheelEnabled ? 'масштаб колесом: вкл' : 'масштаб колесом: выкл'; + const value = Math.round(Number(networkLayout[block.dataset.section]?.scale) || 100); + button.textContent = `↕ ${value}%`; + button.setAttribute('aria-pressed', String(!!block._networkScaleWheelEnabled)); + button.title = block._networkScaleWheelEnabled ? 'Масштаб колесом включён · нажмите, чтобы выключить' : 'Включить масштаб колесом для этого блока'; } function captureNetworkLayout(block) { const key = block?.dataset.section; @@ -3233,6 +3246,29 @@ HTML = """ function makeNetworkBlockInteractive(block) { if (!block || block._networkInteractive) return; block._networkInteractive = true; + const summary = block.querySelector(':scope > .network-summary'); + if (summary) { + const title = summary.textContent.trim(); + summary.textContent = ''; + const titleNode = document.createElement('span'); + titleNode.className = 'block-title'; + titleNode.textContent = title; + const scaleToggle = document.createElement('button'); + scaleToggle.className = 'block-scale-toggle'; + scaleToggle.type = 'button'; + scaleToggle.setAttribute('aria-label', `Масштаб блока «${networkTitle(block)}» колесом`); + scaleToggle.addEventListener('pointerdown', event => { event.preventDefault(); event.stopPropagation(); }); + scaleToggle.addEventListener('click', event => { + event.preventDefault(); + event.stopPropagation(); + selectNetworkBlock(block); + bringNetworkBlockFront(block); + block._networkScaleWheelEnabled = !block._networkScaleWheelEnabled; + updateNetworkScaleToggle(block); + }); + summary.append(titleNode, scaleToggle); + updateNetworkScaleToggle(block); + } block.addEventListener('pointerdown', event => { const additive = event.ctrlKey || event.metaKey; const edge = networkResizeEdge(event, block); @@ -3297,7 +3333,7 @@ HTML = """ } }); block.addEventListener('wheel', event => { - if (!networkScaleWheelEnabled || networkState(block) || !event.deltaY) return; + if (!block._networkScaleWheelEnabled || networkState(block) || !event.deltaY) return; event.preventDefault(); event.stopPropagation(); if (networkSelectedBlock !== block) selectNetworkBlock(block); @@ -3357,7 +3393,6 @@ HTML = """ selectNetworkBlock(networkBlocks.find(block => !networkState(block)) || null); $('networkResetLayout')?.addEventListener('click', resetNetworkLayout); $('networkDeleteSelected')?.addEventListener('click', deleteSelectedNetworkBlocks); - $('networkScaleMode')?.addEventListener('click', () => setNetworkScaleMode(!networkScaleWheelEnabled)); $('networkStructureToggle')?.addEventListener('click', () => { networkStructureOpen = !networkStructureOpen; renderNetworkStructure();