From cf4f9b64e7e8ac01d68a2e4c1bf25749fca1099c Mon Sep 17 00:00:00 2001 From: AlexsandrSnytkin Date: Fri, 18 Sep 2026 17:42:56 +0700 Subject: [PATCH] feat(ui): scale blocks independently --- ui_server.py | 63 +++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 10 deletions(-) diff --git a/ui_server.py b/ui_server.py index c798053..84e937c 100644 --- a/ui_server.py +++ b/ui_server.py @@ -1284,6 +1284,7 @@ HTML = """ .config-toolbar button:hover, .config-toolbar button:focus-visible, .config-restore-panel button:hover, .config-restore-panel button:focus-visible { outline: 0; border-color: var(--ui-accent); color: var(--ui-accent); transform: translateY(-1px); } .config-scale-control { display: inline-flex !important; align-items: center; gap: 6px !important; min-width: 148px; color: var(--ui-muted) !important; font-size: 10px !important; white-space: nowrap; } .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-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; } @@ -1314,6 +1315,7 @@ HTML = """ .config-canvas > .config-block { position: absolute; box-sizing: border-box; container: config-block / inline-size; margin: 0; min-width: 240px; min-height: 110px; border: 1px solid var(--ui-border) !important; border-radius: 10px; background: var(--ui-panel) !important; box-shadow: 0 6px 18px rgba(0, 0, 0, .1); overflow: auto; transition: border-color .2s ease, box-shadow .2s ease, opacity .2s ease; } .config-canvas > .config-block { cursor: grab; touch-action: none; } .config-canvas > .config-block:hover { border-color: color-mix(in srgb, var(--ui-accent) 45%, var(--ui-border)) !important; } + .config-canvas > .config-block.is-selected { border-color: color-mix(in srgb, var(--ui-accent) 70%, var(--ui-border)) !important; box-shadow: 0 0 0 2px color-mix(in srgb, var(--ui-accent) 12%, transparent), 0 6px 18px rgba(0, 0, 0, .1); } .config-canvas > .config-block.is-dragging, .config-canvas > .config-block.is-resizing { z-index: 5; border-color: var(--ui-accent) !important; box-shadow: 0 10px 28px color-mix(in srgb, var(--ui-accent) 16%, transparent); transition: none; } .config-canvas > .config-custom-block { border-style: dashed !important; } .config-block > summary { position: sticky; top: 0; z-index: 2; background: var(--ui-panel); } @@ -1359,6 +1361,7 @@ HTML = """ .config-canvas > .config-block { min-width: 220px; max-width: calc(100% - 8px); } .config-structure-create { align-items: stretch; } .config-structure-create label, .config-structure-create label:first-child { min-width: min(100%, 220px); flex: 1 1 180px; } + .config-scale-control { flex: 1 1 100%; justify-content: space-between; min-width: 0; } .config-trash { bottom: 10px; } } @keyframes form-field-in { from { opacity: .35; transform: translateY(3px); } to { opacity: 1; transform: none; } } @@ -1539,7 +1542,7 @@ HTML = """ Блоки конфигурации - + перетаскивайте за заголовок или свободное место
@@ -2008,6 +2011,7 @@ HTML = """ let configZIndex = 1; let configContentScale = 1; let configBlockObserver = null; + let selectedConfigBlock = null; function readConfigLayout() { try { const saved = JSON.parse(localStorage.getItem('fpv-config-layout-v1') || '{}'); @@ -2031,15 +2035,43 @@ HTML = """ const width = Math.max(configMinWidth(), block.clientWidth || block.offsetWidth || configMinWidth()); const height = Math.max(110, block.clientHeight || block.offsetHeight || 110); const area = Math.max(.56, Math.min(1.85, (width / 720) * (height / 320))); - const scale = Math.max(.74, Math.min(1.35, Math.sqrt(area) * configContentScale)); + const entry = configLayout[block.dataset.section] || {}; + const userScale = Number.isFinite(Number(block._configUserScale)) ? block._configUserScale : (Number(entry.scale) || 100) / 100; + block._configUserScale = Math.max(.8, Math.min(1.25, userScale)); + const scale = Math.max(.74, Math.min(1.35, Math.sqrt(area) * block._configUserScale)); block.style.setProperty('--block-scale', scale.toFixed(3)); } function applyConfigContentScale(value) { configContentScale = Math.max(.8, Math.min(1.25, (Number(value) || 100) / 100)); + const block = selectedConfigBlock; + if (!block || configBlockState(block)) return; + block._configUserScale = configContentScale; + const entry = configLayout[block.dataset.section] || {}; + entry.scale = Math.round(configContentScale * 100); + configLayout[block.dataset.section] = entry; setValue('configContentScale', Math.round(configContentScale * 100)); setText('configContentScaleValue', `${Math.round(configContentScale * 100)}%`); - configBlocks.forEach(updateConfigBlockScale); - try { localStorage.setItem('fpv-config-content-scale', String(Math.round(configContentScale * 100))); } catch (_error) {} + updateConfigBlockScale(block); + saveConfigLayout(); + } + function selectConfigBlock(block) { + if (!block || configBlockState(block)) { + selectedConfigBlock = null; + configBlocks.forEach(other => other.classList.remove('is-selected')); + const control = $('configContentScale'); + if (control) control.disabled = true; + return; + } + selectedConfigBlock = block; + configBlocks.forEach(other => other.classList.toggle('is-selected', other === block)); + const entry = configLayout[block.dataset.section] || {}; + const value = Math.round((Number(entry.scale) || 100)); + configContentScale = Math.max(.8, Math.min(1.25, value / 100)); + setValue('configContentScale', value); + setText('configContentScaleValue', `${value}%`); + const control = $('configContentScale'); + if (control) control.disabled = false; + updateConfigBlockScale(block); } function configTitle(block) { return block.querySelector(':scope > summary .block-title')?.textContent || block.dataset.section || 'Блок'; @@ -2085,6 +2117,8 @@ HTML = """ entry.width = Math.round(block.offsetWidth); if (block.style.height && block.style.height !== 'auto') entry.height = Math.round(block.offsetHeight); else delete entry.height; + if (Number.isFinite(Number(block._configUserScale)) && Math.abs(block._configUserScale - 1) > .001) entry.scale = Math.round(block._configUserScale * 100); + else delete entry.scale; if (block.style.zIndex) entry.z = Number(block.style.zIndex); configLayout[key] = entry; } @@ -2186,12 +2220,13 @@ HTML = """ saveConfigLayout(); applyStoredConfigLayout(); renderConfigRestorePanel(); + selectConfigBlock(configBlocks.find(candidate => !configBlockState(candidate)) || null); } function focusConfigBlock(block, item) { if (!block || configBlockState(block)) return; configBlocks.forEach(other => other.classList.remove('is-selected')); document.querySelectorAll('.config-restore-item.is-selected').forEach(node => node.classList.remove('is-selected')); - block.classList.add('is-selected'); + selectConfigBlock(block); item?.classList.add('is-selected'); bringConfigBlockFront(block); block.scrollIntoView?.({behavior: 'smooth', block: 'nearest', inline: 'nearest'}); @@ -2281,6 +2316,7 @@ HTML = """ if (key) delete configLayout[key]; saveConfigLayout(); renderConfigRestorePanel(); + selectConfigBlock(configBlocks.find(candidate => !configBlockState(candidate)) || null); setText('configLayoutState', 'блок удалён навсегда'); } function nextCustomConfigKey() { @@ -2362,6 +2398,12 @@ HTML = """ const trash = $('configTrash'); if (trash) trash.classList.toggle('is-over', trashDrop(event)); } + function showConfigTrash() { + const trash = $('configTrash'); + if (!trash) return; + trash.hidden = false; + trash.classList.remove('is-over'); + } function finishConfigDrag(block, event, dragged) { const trash = $('configTrash'); const droppedOnTrash = trashDrop(event); @@ -2400,6 +2442,7 @@ HTML = """ block.open = true; }); block.addEventListener('pointerdown', event => { + selectConfigBlock(block); bringConfigBlockFront(block); const edge = configResizeEdge(event, block); if (!edge || configDragBlocked(block, event)) return; @@ -2483,6 +2526,7 @@ HTML = """ const startLeft = parseFloat(block.style.left) || block.offsetLeft || 0; const startTop = parseFloat(block.style.top) || block.offsetTop || 0; let dragged = false; + showConfigTrash(); block.setPointerCapture?.(event.pointerId); const move = moveEvent => { if (!dragged && Math.hypot(moveEvent.clientX - startX, moveEvent.clientY - startY) < 4) return; @@ -2490,8 +2534,6 @@ HTML = """ dragged = true; event.preventDefault(); block.classList.add('is-dragging'); - const trash = $('configTrash'); - if (trash) { trash.hidden = false; trash.classList.remove('is-over'); } } event.preventDefault(); const maxLeft = Math.max(0, configCanvasWidth() - block.offsetWidth); @@ -2521,6 +2563,7 @@ HTML = """ block.style.width = ''; block.style.height = ''; block.style.zIndex = ''; + block._configUserScale = 1; block.open = true; block.dataset.blockState = ''; block.classList.remove('config-block-hidden', 'config-block-deleted'); @@ -2532,6 +2575,7 @@ HTML = """ saveConfigLayout(); applyStoredConfigLayout(); renderConfigRestorePanel(); + selectConfigBlock(configBlocks.find(block => !configBlockState(block)) || null); setText('configLayoutState', 'раскладка сброшена'); } function initConfigCanvas() { @@ -2555,6 +2599,7 @@ HTML = """ }); applyStoredConfigLayout(); renderConfigRestorePanel(); + selectConfigBlock(configBlocks.find(block => !configBlockState(block)) || null); $('configResetLayout')?.addEventListener('click', resetConfigLayout); $('configRestoreToggle')?.addEventListener('click', () => { restorePanelOpen = !restorePanelOpen; @@ -2586,9 +2631,7 @@ HTML = """ }); configBlocks.forEach(block => configBlockObserver.observe(block)); } - let savedScale = 100; - try { savedScale = Number(localStorage.getItem('fpv-config-content-scale') || 100); } catch (_error) {} - applyConfigContentScale(savedScale); + selectConfigBlock(selectedConfigBlock); $('configContentScale')?.addEventListener('input', event => applyConfigContentScale(event.target.value)); } function parseFrameSize(value) {