Add drag-and-drop for presets and colors, max_colors validation, and 2D grid layout
- Add drag-and-drop to reorder presets in tabs (2D grid layout) - Add drag-and-drop to reorder colors within presets - Add max_colors field to pattern definitions - Hide color section when max_colors is 0 - Validate color count against pattern max_colors limit - Store presets in 2D grid format (3 columns) - Remove left panel from tab content, show only presets - Update color palette to show swatches instead of hex codes - Improve preset editor UI with visual color swatches
This commit is contained in:
@@ -1,46 +1,54 @@
|
|||||||
{
|
{
|
||||||
"on": {
|
"on": {
|
||||||
"min_delay": 10,
|
"min_delay": 10,
|
||||||
"max_delay": 10000
|
"max_delay": 10000,
|
||||||
|
"max_colors": 1
|
||||||
},
|
},
|
||||||
"off": {
|
"off": {
|
||||||
"min_delay": 10,
|
"min_delay": 10,
|
||||||
"max_delay": 10000
|
"max_delay": 10000,
|
||||||
},
|
"max_colors": 0
|
||||||
"rainbow": {
|
},
|
||||||
"Step Rate": "n1",
|
"rainbow": {
|
||||||
"min_delay": 10,
|
"n1": "Step Rate",
|
||||||
"max_delay": 10000
|
"min_delay": 10,
|
||||||
},
|
"max_delay": 10000,
|
||||||
"transition": {
|
"max_colors": 0
|
||||||
"min_delay": 10,
|
},
|
||||||
"max_delay": 10000
|
"transition": {
|
||||||
},
|
"min_delay": 10,
|
||||||
"chase": {
|
"max_delay": 10000,
|
||||||
"Colour 1 Length": "n1",
|
"max_colors": 10
|
||||||
"Colour 2 Length": "n2",
|
},
|
||||||
"Step 1": "n3",
|
"chase": {
|
||||||
"Step 2": "n4",
|
"n1": "Colour 1 Length",
|
||||||
"min_delay": 10,
|
"n2": "Colour 2 Length",
|
||||||
"max_delay": 10000
|
"n3": "Step 1",
|
||||||
},
|
"n4": "Step 2",
|
||||||
"pulse": {
|
"min_delay": 10,
|
||||||
"Attack": "n1",
|
"max_delay": 10000,
|
||||||
"Hold": "n2",
|
"max_colors": 2
|
||||||
"Decay": "n3",
|
},
|
||||||
"min_delay": 10,
|
"pulse": {
|
||||||
"max_delay": 10000
|
"n1": "Attack",
|
||||||
},
|
"n2": "Hold",
|
||||||
"circle": {
|
"n3": "Decay",
|
||||||
"Head Rate": "n1",
|
"min_delay": 10,
|
||||||
"Max Length": "n2",
|
"max_delay": 10000,
|
||||||
"Tail Rate": "n3",
|
"max_colors": 10
|
||||||
"Min Length": "n4",
|
},
|
||||||
"min_delay": 10,
|
"circle": {
|
||||||
"max_delay": 10000
|
"n1": "Head Rate",
|
||||||
},
|
"n2": "Max Length",
|
||||||
"blink": {
|
"n3": "Tail Rate",
|
||||||
"min_delay": 10,
|
"n4": "Min Length",
|
||||||
"max_delay": 10000
|
"min_delay": 10,
|
||||||
}
|
"max_delay": 10000,
|
||||||
}
|
"max_colors": 2
|
||||||
|
},
|
||||||
|
"blink": {
|
||||||
|
"min_delay": 10,
|
||||||
|
"max_delay": 10000,
|
||||||
|
"max_colors": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -163,88 +163,17 @@ async def tab_content_fragment(request, session, id):
|
|||||||
return send_file('templates/index.html')
|
return send_file('templates/index.html')
|
||||||
|
|
||||||
tab_name = tab.get('name', 'Tab ' + str(id))
|
tab_name = tab.get('name', 'Tab ' + str(id))
|
||||||
device_ids = ', '.join(tab.get('names', []))
|
|
||||||
|
|
||||||
html = (
|
html = (
|
||||||
'<div class="left-panel">'
|
'<div class="presets-section" data-tab-id="' + str(id) + '">'
|
||||||
'<div class="left-panel-header">'
|
|
||||||
'<div class="ids-display">'
|
|
||||||
'<label>IDs: </label>'
|
|
||||||
'<span id="current-ids">' + device_ids + '</span>'
|
|
||||||
'</div>'
|
|
||||||
'<button id="toggle-left-panel" class="btn btn-small left-panel-toggle" title="Collapse/expand controls">◀</button>'
|
|
||||||
'</div>'
|
|
||||||
'<div class="left-panel-body">'
|
|
||||||
'<div class="color-palette-section">'
|
|
||||||
'<h3>Color Palette</h3>'
|
|
||||||
'<div id="color-palette" class="color-palette" data-tab-id="' + str(id) + '">'
|
|
||||||
'<!-- Colors will be loaded here -->'
|
|
||||||
'</div>'
|
|
||||||
'<div class="palette-actions">'
|
|
||||||
'<input type="color" id="tab-color-input" value="#ffffff">'
|
|
||||||
'<button class="btn btn-small" id="tab-color-add-btn">Add Color</button>'
|
|
||||||
'<button class="btn btn-small" id="tab-color-add-from-palette-btn">Add from Palette</button>'
|
|
||||||
'</div>'
|
|
||||||
'</div>'
|
|
||||||
'<div class="controls-section">'
|
|
||||||
'<div class="control-group">'
|
|
||||||
'<label for="brightness-slider">Brightness:</label>'
|
|
||||||
'<input type="range" id="brightness-slider" min="0" max="255" value="127" class="slider">'
|
|
||||||
'<span id="brightness-value" class="slider-value">127</span>'
|
|
||||||
'</div>'
|
|
||||||
'<div class="control-group">'
|
|
||||||
'<label for="delay-slider">Delay:</label>'
|
|
||||||
'<input type="range" id="delay-slider" min="0" max="1000" value="0" class="slider">'
|
|
||||||
'<span id="delay-value" class="slider-value">100 ms</span>'
|
|
||||||
'</div>'
|
|
||||||
'</div>'
|
|
||||||
'<div class="n-params-section">'
|
|
||||||
'<h3>N Parameters</h3>'
|
|
||||||
'<div class="n-params-grid">'
|
|
||||||
'<div class="n-param-group">'
|
|
||||||
'<label for="n1-input" id="n1-label">n1:</label>'
|
|
||||||
'<input type="number" id="n1-input" min="0" max="255" value="10" class="n-input">'
|
|
||||||
'</div>'
|
|
||||||
'<div class="n-param-group">'
|
|
||||||
'<label for="n2-input" id="n2-label">n2:</label>'
|
|
||||||
'<input type="number" id="n2-input" min="0" max="255" value="10" class="n-input">'
|
|
||||||
'</div>'
|
|
||||||
'<div class="n-param-group">'
|
|
||||||
'<label for="n3-input" id="n3-label">n3:</label>'
|
|
||||||
'<input type="number" id="n3-input" min="0" max="255" value="10" class="n-input">'
|
|
||||||
'</div>'
|
|
||||||
'<div class="n-param-group">'
|
|
||||||
'<label for="n4-input" id="n4-label">n4:</label>'
|
|
||||||
'<input type="number" id="n4-input" min="0" max="255" value="10" class="n-input">'
|
|
||||||
'</div>'
|
|
||||||
'<div class="n-param-group">'
|
|
||||||
'<label for="n5-input" id="n5-label">n5:</label>'
|
|
||||||
'<input type="number" id="n5-input" min="0" max="255" value="10" class="n-input">'
|
|
||||||
'</div>'
|
|
||||||
'<div class="n-param-group">'
|
|
||||||
'<label for="n6-input" id="n6-label">n6:</label>'
|
|
||||||
'<input type="number" id="n6-input" min="0" max="255" value="10" class="n-input">'
|
|
||||||
'</div>'
|
|
||||||
'<div class="n-param-group">'
|
|
||||||
'<label for="n7-input" id="n7-label">n7:</label>'
|
|
||||||
'<input type="number" id="n7-input" min="0" max="255" value="10" class="n-input">'
|
|
||||||
'</div>'
|
|
||||||
'<div class="n-param-group">'
|
|
||||||
'<label for="n8-input" id="n8-label">n8:</label>'
|
|
||||||
'<input type="number" id="n8-input" min="0" max="255" value="10" class="n-input">'
|
|
||||||
'</div>'
|
|
||||||
'</div>'
|
|
||||||
'</div>'
|
|
||||||
'</div>'
|
|
||||||
'</div>'
|
|
||||||
'<div class="right-panel">'
|
|
||||||
'<div class="presets-section">'
|
|
||||||
'<h3>Presets</h3>'
|
'<h3>Presets</h3>'
|
||||||
|
'<div class="profiles-actions" style="margin-bottom: 1rem;">'
|
||||||
|
'<button class="btn btn-primary" id="preset-add-btn-tab">Add Preset</button>'
|
||||||
|
'</div>'
|
||||||
'<div id="presets-list-tab" class="presets-list">'
|
'<div id="presets-list-tab" class="presets-list">'
|
||||||
'<!-- Presets will be loaded here -->'
|
'<!-- Presets will be loaded here -->'
|
||||||
'</div>'
|
'</div>'
|
||||||
'</div>'
|
'</div>'
|
||||||
'</div>'
|
|
||||||
)
|
)
|
||||||
return html, 200, {'Content-Type': 'text/html'}
|
return html, 200, {'Content-Type': 'text/html'}
|
||||||
|
|
||||||
|
|||||||
@@ -28,27 +28,35 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const row = document.createElement('div');
|
const row = document.createElement('div');
|
||||||
row.className = 'profiles-row';
|
row.className = 'profiles-row';
|
||||||
row.dataset.color = color;
|
row.dataset.color = color;
|
||||||
|
row.style.cssText = 'display: flex; align-items: center; gap: 1rem;';
|
||||||
|
// Ensure no text content
|
||||||
|
row.textContent = '';
|
||||||
|
|
||||||
const swatch = document.createElement('div');
|
const swatch = document.createElement('div');
|
||||||
swatch.style.width = '28px';
|
swatch.style.cssText = `
|
||||||
swatch.style.height = '28px';
|
width: 64px;
|
||||||
swatch.style.borderRadius = '4px';
|
height: 64px;
|
||||||
swatch.style.backgroundColor = color;
|
border-radius: 8px;
|
||||||
swatch.style.border = '1px solid #4a4a4a';
|
background-color: ${color};
|
||||||
|
border: 2px solid #4a4a4a;
|
||||||
const label = document.createElement('span');
|
cursor: pointer;
|
||||||
label.textContent = color;
|
flex-shrink: 0;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.3);
|
||||||
|
`;
|
||||||
|
swatch.title = color; // Show hex code on hover only
|
||||||
|
swatch.setAttribute('aria-label', `Color ${color}`);
|
||||||
|
|
||||||
const removeButton = document.createElement('button');
|
const removeButton = document.createElement('button');
|
||||||
removeButton.className = 'btn btn-danger btn-small';
|
removeButton.className = 'btn btn-danger btn-small';
|
||||||
removeButton.textContent = 'Remove';
|
removeButton.textContent = 'Remove';
|
||||||
removeButton.addEventListener('click', async () => {
|
removeButton.style.fontSize = '0.8rem'; // Restore font size for button
|
||||||
|
removeButton.addEventListener('click', async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
const updated = currentPalette.filter((_, i) => i !== index);
|
const updated = currentPalette.filter((_, i) => i !== index);
|
||||||
await savePalette(updated);
|
await savePalette(updated);
|
||||||
});
|
});
|
||||||
|
|
||||||
row.appendChild(swatch);
|
row.appendChild(swatch);
|
||||||
row.appendChild(label);
|
|
||||||
row.appendChild(removeButton);
|
row.appendChild(removeButton);
|
||||||
paletteContainer.appendChild(row);
|
paletteContainer.appendChild(row);
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -364,7 +364,7 @@ header h1 {
|
|||||||
|
|
||||||
.presets-list {
|
.presets-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
grid-template-columns: repeat(3, 1fr);
|
||||||
gap: 0.75rem;
|
gap: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -129,10 +129,12 @@
|
|||||||
<option value="">Pattern</option>
|
<option value="">Pattern</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<label>Colors (comma-separated hex)</label>
|
<label>Colors</label>
|
||||||
<div class="profiles-actions">
|
<div id="preset-colors-container" class="preset-colors-container"></div>
|
||||||
<input type="text" id="preset-colors-input" placeholder="#FF0000,#00FF00,#0000FF">
|
<div class="profiles-actions" style="margin-top: 0.5rem;">
|
||||||
<button class="btn btn-secondary" id="preset-add-from-palette-btn">Add from Palette</button>
|
<input type="color" id="preset-new-color" value="#ffffff">
|
||||||
|
<button class="btn btn-secondary btn-small" id="preset-add-color-btn">Add Color</button>
|
||||||
|
<button class="btn btn-secondary btn-small" id="preset-add-from-palette-btn">Add from Palette</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="profiles-actions">
|
<div class="profiles-actions">
|
||||||
<input type="number" id="preset-brightness-input" placeholder="Brightness" min="0" max="255" value="0">
|
<input type="number" id="preset-brightness-input" placeholder="Brightness" min="0" max="255" value="0">
|
||||||
@@ -268,6 +270,34 @@
|
|||||||
background-color: #3a3a3a;
|
background-color: #3a3a3a;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
/* Hide any text content in palette rows - only show color swatches */
|
||||||
|
#palette-container .profiles-row {
|
||||||
|
font-size: 0; /* Hide any text nodes */
|
||||||
|
}
|
||||||
|
#palette-container .profiles-row > * {
|
||||||
|
font-size: 1rem; /* Restore font size for buttons */
|
||||||
|
}
|
||||||
|
#palette-container .profiles-row > span:not(.btn),
|
||||||
|
#palette-container .profiles-row > label,
|
||||||
|
#palette-container .profiles-row::before,
|
||||||
|
#palette-container .profiles-row::after {
|
||||||
|
display: none !important;
|
||||||
|
content: none !important;
|
||||||
|
}
|
||||||
|
/* Preset colors container */
|
||||||
|
#preset-colors-container {
|
||||||
|
min-height: 80px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
#preset-colors-container .muted-text {
|
||||||
|
color: #888;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
.muted-text {
|
.muted-text {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
color: #888;
|
color: #888;
|
||||||
@@ -285,6 +315,38 @@
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
}
|
||||||
|
/* Drag and drop styles for presets */
|
||||||
|
.draggable-preset {
|
||||||
|
cursor: move;
|
||||||
|
transition: opacity 0.2s, transform 0.2s;
|
||||||
|
}
|
||||||
|
.draggable-preset.dragging {
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
.draggable-preset:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
/* Drag and drop styles for color swatches */
|
||||||
|
.draggable-color-swatch {
|
||||||
|
transition: opacity 0.2s, transform 0.2s;
|
||||||
|
}
|
||||||
|
.draggable-color-swatch.dragging-color {
|
||||||
|
opacity: 0.5;
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
.draggable-color-swatch.drag-over-color {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
.color-swatches-container {
|
||||||
|
min-height: 80px;
|
||||||
|
}
|
||||||
|
/* Ensure presets list uses grid layout */
|
||||||
|
#presets-list-tab {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<script src="/static/color_palette.js"></script>
|
<script src="/static/color_palette.js"></script>
|
||||||
<script src="/static/profiles.js"></script>
|
<script src="/static/profiles.js"></script>
|
||||||
|
|||||||
Reference in New Issue
Block a user