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) {