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": {
|
||||
"min_delay": 10,
|
||||
"max_delay": 10000
|
||||
"max_delay": 10000,
|
||||
"max_colors": 1
|
||||
},
|
||||
"off": {
|
||||
"min_delay": 10,
|
||||
"max_delay": 10000
|
||||
},
|
||||
"rainbow": {
|
||||
"Step Rate": "n1",
|
||||
"min_delay": 10,
|
||||
"max_delay": 10000
|
||||
},
|
||||
"transition": {
|
||||
"min_delay": 10,
|
||||
"max_delay": 10000
|
||||
},
|
||||
"chase": {
|
||||
"Colour 1 Length": "n1",
|
||||
"Colour 2 Length": "n2",
|
||||
"Step 1": "n3",
|
||||
"Step 2": "n4",
|
||||
"min_delay": 10,
|
||||
"max_delay": 10000
|
||||
},
|
||||
"pulse": {
|
||||
"Attack": "n1",
|
||||
"Hold": "n2",
|
||||
"Decay": "n3",
|
||||
"min_delay": 10,
|
||||
"max_delay": 10000
|
||||
},
|
||||
"circle": {
|
||||
"Head Rate": "n1",
|
||||
"Max Length": "n2",
|
||||
"Tail Rate": "n3",
|
||||
"Min Length": "n4",
|
||||
"min_delay": 10,
|
||||
"max_delay": 10000
|
||||
},
|
||||
"blink": {
|
||||
"min_delay": 10,
|
||||
"max_delay": 10000
|
||||
}
|
||||
}
|
||||
"off": {
|
||||
"min_delay": 10,
|
||||
"max_delay": 10000,
|
||||
"max_colors": 0
|
||||
},
|
||||
"rainbow": {
|
||||
"n1": "Step Rate",
|
||||
"min_delay": 10,
|
||||
"max_delay": 10000,
|
||||
"max_colors": 0
|
||||
},
|
||||
"transition": {
|
||||
"min_delay": 10,
|
||||
"max_delay": 10000,
|
||||
"max_colors": 10
|
||||
},
|
||||
"chase": {
|
||||
"n1": "Colour 1 Length",
|
||||
"n2": "Colour 2 Length",
|
||||
"n3": "Step 1",
|
||||
"n4": "Step 2",
|
||||
"min_delay": 10,
|
||||
"max_delay": 10000,
|
||||
"max_colors": 2
|
||||
},
|
||||
"pulse": {
|
||||
"n1": "Attack",
|
||||
"n2": "Hold",
|
||||
"n3": "Decay",
|
||||
"min_delay": 10,
|
||||
"max_delay": 10000,
|
||||
"max_colors": 10
|
||||
},
|
||||
"circle": {
|
||||
"n1": "Head Rate",
|
||||
"n2": "Max Length",
|
||||
"n3": "Tail Rate",
|
||||
"n4": "Min Length",
|
||||
"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')
|
||||
|
||||
tab_name = tab.get('name', 'Tab ' + str(id))
|
||||
device_ids = ', '.join(tab.get('names', []))
|
||||
|
||||
html = (
|
||||
'<div class="left-panel">'
|
||||
'<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">'
|
||||
'<div class="presets-section" data-tab-id="' + str(id) + '">'
|
||||
'<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">'
|
||||
'<!-- Presets will be loaded here -->'
|
||||
'</div>'
|
||||
'</div>'
|
||||
'</div>'
|
||||
)
|
||||
return html, 200, {'Content-Type': 'text/html'}
|
||||
|
||||
|
||||
@@ -28,27 +28,35 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'profiles-row';
|
||||
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');
|
||||
swatch.style.width = '28px';
|
||||
swatch.style.height = '28px';
|
||||
swatch.style.borderRadius = '4px';
|
||||
swatch.style.backgroundColor = color;
|
||||
swatch.style.border = '1px solid #4a4a4a';
|
||||
|
||||
const label = document.createElement('span');
|
||||
label.textContent = color;
|
||||
swatch.style.cssText = `
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 8px;
|
||||
background-color: ${color};
|
||||
border: 2px solid #4a4a4a;
|
||||
cursor: pointer;
|
||||
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');
|
||||
removeButton.className = 'btn btn-danger btn-small';
|
||||
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);
|
||||
await savePalette(updated);
|
||||
});
|
||||
|
||||
row.appendChild(swatch);
|
||||
row.appendChild(label);
|
||||
row.appendChild(removeButton);
|
||||
paletteContainer.appendChild(row);
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -364,7 +364,7 @@ header h1 {
|
||||
|
||||
.presets-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -129,10 +129,12 @@
|
||||
<option value="">Pattern</option>
|
||||
</select>
|
||||
</div>
|
||||
<label>Colors (comma-separated hex)</label>
|
||||
<div class="profiles-actions">
|
||||
<input type="text" id="preset-colors-input" placeholder="#FF0000,#00FF00,#0000FF">
|
||||
<button class="btn btn-secondary" id="preset-add-from-palette-btn">Add from Palette</button>
|
||||
<label>Colors</label>
|
||||
<div id="preset-colors-container" class="preset-colors-container"></div>
|
||||
<div class="profiles-actions" style="margin-top: 0.5rem;">
|
||||
<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 class="profiles-actions">
|
||||
<input type="number" id="preset-brightness-input" placeholder="Brightness" min="0" max="255" value="0">
|
||||
@@ -268,6 +270,34 @@
|
||||
background-color: #3a3a3a;
|
||||
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 {
|
||||
text-align: center;
|
||||
color: #888;
|
||||
@@ -285,6 +315,38 @@
|
||||
border-radius: 4px;
|
||||
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>
|
||||
<script src="/static/color_palette.js"></script>
|
||||
<script src="/static/profiles.js"></script>
|
||||
|
||||
Reference in New Issue
Block a user