feat(ui): patterns list and create form layout
Made-with: Cursor
This commit is contained in:
@@ -55,8 +55,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
if (mode === 'create') {
|
if (mode === 'create') {
|
||||||
if (labelEl) {
|
if (labelEl) {
|
||||||
labelEl.textContent = '';
|
labelEl.textContent = `${key}:`;
|
||||||
labelEl.style.display = 'none';
|
labelEl.style.display = '';
|
||||||
}
|
}
|
||||||
if (inputEl) {
|
if (inputEl) {
|
||||||
inputEl.value = '';
|
inputEl.value = '';
|
||||||
@@ -203,6 +203,17 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** on/off are implemented in driver firmware (presets.py), not as OTA ``.py`` files. */
|
||||||
|
const FIRMWARE_BUILTIN_PATTERNS = new Set(['on', 'off']);
|
||||||
|
|
||||||
|
const isFirmwareBuiltinPattern = (patternName) => {
|
||||||
|
const id = String(patternName || '')
|
||||||
|
.trim()
|
||||||
|
.replace(/\.py$/i, '')
|
||||||
|
.toLowerCase();
|
||||||
|
return FIRMWARE_BUILTIN_PATTERNS.has(id);
|
||||||
|
};
|
||||||
|
|
||||||
const sendPatternToDevices = async (patternName) => {
|
const sendPatternToDevices = async (patternName) => {
|
||||||
const response = await fetch(`/patterns/${encodeURIComponent(patternName)}/send`, {
|
const response = await fetch(`/patterns/${encodeURIComponent(patternName)}/send`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -281,7 +292,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`/patterns/ota/file/${encodeURIComponent(patternName)}.py`, {
|
const raw = String(patternName || '').trim();
|
||||||
|
const fileSegment = /\.py$/i.test(raw) ? raw : `${raw}.py`;
|
||||||
|
const response = await fetch(`/patterns/ota/file/${encodeURIComponent(fileSegment)}`, {
|
||||||
headers: { Accept: 'text/plain' },
|
headers: { Accept: 'text/plain' },
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
@@ -315,39 +328,41 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const label = document.createElement('span');
|
const label = document.createElement('span');
|
||||||
label.textContent = patternName;
|
label.textContent = patternName;
|
||||||
|
|
||||||
const details = document.createElement('span');
|
|
||||||
const minDelay = data && data.min_delay !== undefined ? data.min_delay : '-';
|
|
||||||
const maxDelay = data && data.max_delay !== undefined ? data.max_delay : '-';
|
|
||||||
details.textContent = `${minDelay}–${maxDelay} ms`;
|
|
||||||
details.style.color = '#aaa';
|
|
||||||
details.style.fontSize = '0.85em';
|
|
||||||
|
|
||||||
const sendBtn = document.createElement('button');
|
|
||||||
sendBtn.className = 'btn btn-primary btn-small';
|
|
||||||
sendBtn.textContent = 'Send';
|
|
||||||
sendBtn.addEventListener('click', async () => {
|
|
||||||
try {
|
|
||||||
await sendPatternToDevices(patternName);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Send pattern failed:', error);
|
|
||||||
alert(error.message || 'Failed to send pattern.');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const editBtn = document.createElement('button');
|
|
||||||
editBtn.className = 'btn btn-secondary btn-small';
|
|
||||||
editBtn.textContent = 'Edit';
|
|
||||||
editBtn.addEventListener('click', async () => {
|
|
||||||
if (patternEditorModal) {
|
|
||||||
patternEditorModal.classList.add('active');
|
|
||||||
}
|
|
||||||
await loadPatternIntoEditor(patternName, data || {});
|
|
||||||
});
|
|
||||||
|
|
||||||
row.appendChild(label);
|
row.appendChild(label);
|
||||||
row.appendChild(details);
|
|
||||||
row.appendChild(editBtn);
|
if (isFirmwareBuiltinPattern(patternName)) {
|
||||||
row.appendChild(sendBtn);
|
const note = document.createElement('span');
|
||||||
|
note.className = 'muted-text';
|
||||||
|
note.style.fontSize = '0.85em';
|
||||||
|
note.textContent = 'Built-in (no OTA module)';
|
||||||
|
row.appendChild(note);
|
||||||
|
} else {
|
||||||
|
const sendBtn = document.createElement('button');
|
||||||
|
sendBtn.className = 'btn btn-primary btn-small';
|
||||||
|
sendBtn.textContent = 'Send';
|
||||||
|
sendBtn.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
await sendPatternToDevices(patternName);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Send pattern failed:', error);
|
||||||
|
alert(error.message || 'Failed to send pattern.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const editBtn = document.createElement('button');
|
||||||
|
editBtn.className = 'btn btn-secondary btn-small';
|
||||||
|
editBtn.textContent = 'Edit';
|
||||||
|
editBtn.addEventListener('click', async () => {
|
||||||
|
if (patternEditorModal) {
|
||||||
|
patternEditorModal.classList.add('active');
|
||||||
|
}
|
||||||
|
await loadPatternIntoEditor(patternName, data || {});
|
||||||
|
});
|
||||||
|
|
||||||
|
row.appendChild(editBtn);
|
||||||
|
row.appendChild(sendBtn);
|
||||||
|
}
|
||||||
|
|
||||||
patternsList.appendChild(row);
|
patternsList.appendChild(row);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1288,6 +1288,12 @@ body.preset-ui-run .edit-mode-only {
|
|||||||
justify-content: stretch;
|
justify-content: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#pattern-editor-modal .n-param-group:has(.pattern-n-readable-input) label {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-width: 2.25rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
#pattern-editor-modal .pattern-n-readable-input {
|
#pattern-editor-modal .pattern-n-readable-input {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
@@ -259,14 +259,6 @@
|
|||||||
<label for="pattern-create-name" style="min-width: 7rem;">Name</label>
|
<label for="pattern-create-name" style="min-width: 7rem;">Name</label>
|
||||||
<input type="text" id="pattern-create-name" class="preset-name-like" placeholder="e.g. sparkle" pattern="[a-zA-Z_][a-zA-Z0-9_]*" style="flex: 1; min-width: 12rem;" autocomplete="off">
|
<input type="text" id="pattern-create-name" class="preset-name-like" placeholder="e.g. sparkle" pattern="[a-zA-Z_][a-zA-Z0-9_]*" style="flex: 1; min-width: 12rem;" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
<div class="profiles-row pattern-editor-meta-row" style="flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.5rem;">
|
|
||||||
<label for="pattern-create-min-delay" style="min-width: 7rem;">Min delay (ms)</label>
|
|
||||||
<input type="number" id="pattern-create-min-delay" min="0" value="10">
|
|
||||||
<label for="pattern-create-max-delay">Max delay (ms)</label>
|
|
||||||
<input type="number" id="pattern-create-max-delay" min="0" value="10000">
|
|
||||||
<label for="pattern-create-max-colors">Max colours</label>
|
|
||||||
<input type="number" id="pattern-create-max-colors" min="0" value="10">
|
|
||||||
</div>
|
|
||||||
<div id="pattern-create-n-section" class="n-params-section" style="margin-bottom: 0.5rem;">
|
<div id="pattern-create-n-section" class="n-params-section" style="margin-bottom: 0.5rem;">
|
||||||
<h3 class="muted-text">Readable parameter names</h3>
|
<h3 class="muted-text">Readable parameter names</h3>
|
||||||
<p id="pattern-create-n-empty" class="muted-text" style="display: none; margin: 0 0 0.5rem 0;">No parameter names are stored for this pattern.</p>
|
<p id="pattern-create-n-empty" class="muted-text" style="display: none; margin: 0 0 0.5rem 0;">No parameter names are stored for this pattern.</p>
|
||||||
@@ -305,6 +297,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="profiles-row pattern-editor-meta-row" style="flex-wrap: wrap; gap: 0.5rem; margin-bottom: 0.5rem;">
|
||||||
|
<label for="pattern-create-min-delay" style="min-width: 7rem;">Min delay (ms)</label>
|
||||||
|
<input type="number" id="pattern-create-min-delay" min="0" value="10">
|
||||||
|
<label for="pattern-create-max-delay">Max delay (ms)</label>
|
||||||
|
<input type="number" id="pattern-create-max-delay" min="0" value="10000">
|
||||||
|
<label for="pattern-create-max-colors">Max colours</label>
|
||||||
|
<input type="number" id="pattern-create-max-colors" min="0" value="10">
|
||||||
|
</div>
|
||||||
<div class="profiles-row" style="flex-direction: column; align-items: stretch; gap: 0.35rem; margin-bottom: 0.5rem;">
|
<div class="profiles-row" style="flex-direction: column; align-items: stretch; gap: 0.35rem; margin-bottom: 0.5rem;">
|
||||||
<label for="pattern-create-file">Pattern file</label>
|
<label for="pattern-create-file">Pattern file</label>
|
||||||
<input type="file" id="pattern-create-file" accept=".py,text/x-python,.PY">
|
<input type="file" id="pattern-create-file" accept=".py,text/x-python,.PY">
|
||||||
|
|||||||
Reference in New Issue
Block a user