Streamline calibration modal: inline controls, dynamic aspect ratio, offset fix
Some checks failed
Validate / validate (push) Failing after 8s

- Move total LEDs counter, direction toggle, and offset input into the
  screen area of the calibration preview
- Remove description paragraph, standalone offset form, and total LEDs banner
- Add mismatch warning (yellow + ⚠) when configured LEDs ≠ device count
- Use actual display aspect ratio for calibration preview
- Fix offset not updating tick labels (buildSegments now starts at offset)
- Remove max-width constraint on preview, add padding for breathing room
- Clean up unused i18n keys from both locale files

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-08 04:43:56 +03:00
parent cf770440c0
commit c34f10f7de
5 changed files with 95 additions and 44 deletions

View File

@@ -980,10 +980,11 @@ function closeConfirmModal(result) {
// Calibration functions
async function showCalibration(deviceId) {
try {
// Fetch current device data
const response = await fetch(`${API_BASE}/devices/${deviceId}`, {
headers: getHeaders()
});
// Fetch device data and displays in parallel
const [response, displaysResponse] = await Promise.all([
fetch(`${API_BASE}/devices/${deviceId}`, { headers: getHeaders() }),
fetch(`${API_BASE}/config/displays`, { headers: getHeaders() }),
]);
if (response.status === 401) {
handle401Error();
@@ -998,9 +999,24 @@ async function showCalibration(deviceId) {
const device = await response.json();
const calibration = device.calibration;
// Set aspect ratio from device's display
const preview = document.querySelector('.calibration-preview');
if (displaysResponse.ok) {
const displaysData = await displaysResponse.json();
const displayIndex = device.settings?.display_index ?? 0;
const display = (displaysData.displays || []).find(d => d.index === displayIndex);
if (display && display.width && display.height) {
preview.style.aspectRatio = `${display.width} / ${display.height}`;
} else {
preview.style.aspectRatio = '';
}
} else {
preview.style.aspectRatio = '';
}
// Store device ID and LED count
document.getElementById('calibration-device-id').value = device.id;
document.getElementById('cal-device-led-count').textContent = device.led_count;
document.getElementById('cal-device-led-count-inline').textContent = device.led_count;
// Set layout
document.getElementById('cal-start-position').value = calibration.start_position;
@@ -1083,7 +1099,14 @@ function updateCalibrationPreview() {
parseInt(document.getElementById('cal-right-leds').value || 0) +
parseInt(document.getElementById('cal-bottom-leds').value || 0) +
parseInt(document.getElementById('cal-left-leds').value || 0);
document.getElementById('cal-total-leds').textContent = total;
// Warning if total doesn't match device LED count
const totalEl = document.querySelector('.preview-screen-total');
const deviceCount = parseInt(document.getElementById('cal-device-led-count-inline').textContent || 0);
const mismatch = total !== deviceCount;
document.getElementById('cal-total-leds-inline').textContent = (mismatch ? '\u26A0 ' : '') + total;
if (totalEl) {
totalEl.classList.toggle('mismatch', mismatch);
}
// Update corner dot highlights for start position
const startPos = document.getElementById('cal-start-position').value;
@@ -1174,11 +1197,9 @@ function renderCalibrationCanvas() {
const segments = buildSegments(calibration);
if (segments.length === 0) return;
// Edge bar geometry (matches CSS: corner zones 56px × 36px proportional)
const cornerFracW = 56 / 500;
const cornerFracH = 36 / 312.5;
const cw = cornerFracW * cW;
const ch = cornerFracH * cH;
// Edge bar geometry (matches CSS: corner zones 56px × 36px fixed)
const cw = 56;
const ch = 36;
// Edge midlines (center of each edge bar) - in canvas coords
const edgeGeometry = {
@@ -1371,7 +1392,7 @@ async function clearTestMode(deviceId) {
async function saveCalibration() {
const deviceId = document.getElementById('calibration-device-id').value;
const deviceLedCount = parseInt(document.getElementById('cal-device-led-count').textContent);
const deviceLedCount = parseInt(document.getElementById('cal-device-led-count-inline').textContent);
const error = document.getElementById('calibration-error');
// Clear test mode before saving
@@ -1476,7 +1497,7 @@ function buildSegments(calibration) {
};
const segments = [];
let ledStart = 0;
let ledStart = calibration.offset || 0;
edgeOrder.forEach(edge => {
const count = edgeCounts[edge];

View File

@@ -83,19 +83,21 @@
</div>
<div class="modal-body">
<input type="hidden" id="calibration-device-id">
<p style="margin-bottom: 12px; color: var(--text-secondary);" data-i18n="calibration.description">
Configure how your LED strip is mapped to screen edges. Click an edge to toggle test mode.
</p>
<!-- Interactive Preview with integrated LED inputs and test toggles -->
<div style="margin-bottom: 12px;">
<div style="margin-bottom: 12px; padding: 0 24px;">
<div class="calibration-preview">
<!-- Screen with direction toggle -->
<!-- Screen with direction toggle, total LEDs, and offset -->
<div class="preview-screen">
<span data-i18n="calibration.preview.screen">Screen</span>
<button type="button" class="direction-toggle" onclick="toggleDirection()" title="Toggle direction">
<span id="direction-icon"></span> <span id="direction-label">CW</span>
</button>
<div class="preview-screen-total"><span id="cal-total-leds-inline">0</span> / <span id="cal-device-led-count-inline">0</span></div>
<div class="preview-screen-controls">
<button type="button" class="direction-toggle" onclick="toggleDirection()" title="Toggle direction">
<span id="direction-icon"></span> <span id="direction-label">CW</span>
</button>
<label class="offset-control" title="LED offset from LED 0 to start corner">
<span></span>
<input type="number" id="cal-offset" min="0" value="0" oninput="updateCalibrationPreview()">
</label>
</div>
</div>
<!-- Clickable edge bars with LED count inputs -->
@@ -142,15 +144,6 @@
</select>
</div>
<div class="form-group" style="margin-bottom: 12px;">
<label for="cal-offset" data-i18n="calibration.offset">LED Offset:</label>
<input type="number" id="cal-offset" min="0" value="0" oninput="updateCalibrationPreview()">
<small style="color: #aaa; display: block; margin-top: 4px;" data-i18n="calibration.offset_hint">LEDs from LED 0 to start corner (along strip)</small>
</div>
<div style="padding: 8px 10px; background: rgba(255, 193, 7, 0.1); border-left: 4px solid #FFC107; border-radius: 4px; margin-bottom: 12px;">
<strong data-i18n="calibration.total">Total LEDs:</strong> <span id="cal-total-leds">0</span> / <span id="cal-device-led-count">0</span>
</div>
<div id="calibration-error" class="error-message" style="display: none;"></div>
</div>

View File

@@ -90,8 +90,6 @@
"settings.saved": "Settings saved successfully",
"settings.failed": "Failed to save settings",
"calibration.title": "LED Calibration",
"calibration.description": "Configure how your LED strip is mapped to screen edges. Click an edge to toggle test mode.",
"calibration.preview.screen": "Screen",
"calibration.preview.click_hint": "Click an edge to toggle test LEDs on/off",
"calibration.start_position": "Starting Position:",
"calibration.position.bottom_left": "Bottom Left",
@@ -101,13 +99,10 @@
"calibration.direction": "Direction:",
"calibration.direction.clockwise": "Clockwise",
"calibration.direction.counterclockwise": "Counterclockwise",
"calibration.offset": "LED Offset:",
"calibration.offset_hint": "LEDs from LED 0 to start corner (along strip)",
"calibration.leds.top": "Top LEDs:",
"calibration.leds.right": "Right LEDs:",
"calibration.leds.bottom": "Bottom LEDs:",
"calibration.leds.left": "Left LEDs:",
"calibration.total": "Total LEDs:",
"calibration.button.cancel": "Cancel",
"calibration.button.save": "Save",
"calibration.saved": "Calibration saved successfully",

View File

@@ -90,8 +90,6 @@
"settings.saved": "Настройки успешно сохранены",
"settings.failed": "Не удалось сохранить настройки",
"calibration.title": "Калибровка Светодиодов",
"calibration.description": "Настройте как ваша светодиодная лента сопоставляется с краями экрана. Нажмите на край для теста.",
"calibration.preview.screen": "Экран",
"calibration.preview.click_hint": "Нажмите на край чтобы включить/выключить тест светодиодов",
"calibration.start_position": "Начальная Позиция:",
"calibration.position.bottom_left": "Нижний Левый",
@@ -101,13 +99,10 @@
"calibration.direction": "Направление:",
"calibration.direction.clockwise": "По Часовой Стрелке",
"calibration.direction.counterclockwise": "Против Часовой Стрелки",
"calibration.offset": "Смещение LED:",
"calibration.offset_hint": "Количество LED от LED 0 до начального угла (по ленте)",
"calibration.leds.top": "Светодиодов Сверху:",
"calibration.leds.right": "Светодиодов Справа:",
"calibration.leds.bottom": "Светодиодов Снизу:",
"calibration.leds.left": "Светодиодов Слева:",
"calibration.total": "Всего Светодиодов:",
"calibration.button.cancel": "Отмена",
"calibration.button.save": "Сохранить",
"calibration.saved": "Калибровка успешно сохранена",

View File

@@ -827,8 +827,7 @@ input:-webkit-autofill:focus {
.calibration-preview {
position: relative;
width: 100%;
max-width: 500px;
aspect-ratio: 16 / 10;
aspect-ratio: 16 / 9;
margin: 20px auto;
background: var(--card-bg);
border: 2px solid var(--border-color);
@@ -863,6 +862,54 @@ input:-webkit-autofill:focus {
font-size: 14px;
}
.preview-screen-total {
font-size: 16px;
font-weight: 600;
opacity: 0.9;
transition: color 0.2s;
}
.preview-screen-total.mismatch {
color: #FFC107;
}
.preview-screen-controls {
display: flex;
align-items: center;
gap: 6px;
}
.offset-control {
display: flex;
align-items: center;
gap: 3px;
padding: 4px 8px;
background: rgba(255, 255, 255, 0.15);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 12px;
font-size: 12px;
cursor: pointer;
user-select: none;
}
.offset-control input {
width: 36px;
padding: 0;
background: transparent;
border: none;
color: white;
font-size: 12px;
text-align: center;
outline: none;
-moz-appearance: textfield;
}
.offset-control input::-webkit-outer-spin-button,
.offset-control input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
.preview-edge {
position: absolute;
display: flex;