- Add API specification documentation - Add system specification document - Add UI mockups and documentation - Add utility modules (wifi)
969 lines
35 KiB
HTML
969 lines
35 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>LED Driver - Presets</title>
|
||
<link rel="stylesheet" href="color-picker.css">
|
||
<style>
|
||
* {
|
||
margin: 0;
|
||
padding: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
min-height: 100vh;
|
||
padding: 20px;
|
||
color: #333;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.header {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
margin-bottom: 24px;
|
||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.header h1 {
|
||
color: #667eea;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.header p {
|
||
color: #666;
|
||
}
|
||
|
||
.controls {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
margin-bottom: 24px;
|
||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||
display: flex;
|
||
gap: 16px;
|
||
flex-wrap: wrap;
|
||
align-items: center;
|
||
}
|
||
|
||
.search-box {
|
||
flex: 1;
|
||
min-width: 200px;
|
||
padding: 12px;
|
||
border: 1px solid #e0e0e0;
|
||
border-radius: 8px;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.search-box:focus {
|
||
outline: none;
|
||
border-color: #667eea;
|
||
}
|
||
|
||
.filter-group {
|
||
display: flex;
|
||
gap: 12px;
|
||
align-items: center;
|
||
}
|
||
|
||
.filter-select {
|
||
padding: 12px;
|
||
border: 1px solid #e0e0e0;
|
||
border-radius: 8px;
|
||
font-size: 1rem;
|
||
background: white;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.filter-select:focus {
|
||
outline: none;
|
||
border-color: #667eea;
|
||
}
|
||
|
||
.view-toggle {
|
||
display: flex;
|
||
gap: 8px;
|
||
border: 1px solid #e0e0e0;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.view-btn {
|
||
padding: 8px 16px;
|
||
border: none;
|
||
background: white;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.view-btn.active {
|
||
background: #667eea;
|
||
color: white;
|
||
}
|
||
|
||
.presets-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||
gap: 24px;
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.preset-card {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 24px;
|
||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||
transition: all 0.3s;
|
||
cursor: pointer;
|
||
border: 3px solid transparent;
|
||
}
|
||
|
||
.preset-card:hover {
|
||
transform: translateY(-4px);
|
||
box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15);
|
||
border-color: #667eea;
|
||
}
|
||
|
||
.preset-card.selected {
|
||
border-color: #667eea;
|
||
background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%);
|
||
}
|
||
|
||
.preset-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: start;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.preset-name {
|
||
font-size: 1.5rem;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.pattern-badge {
|
||
display: inline-block;
|
||
padding: 4px 12px;
|
||
border-radius: 12px;
|
||
font-size: 0.75rem;
|
||
font-weight: 600;
|
||
background: #667eea;
|
||
color: white;
|
||
margin-bottom: 12px;
|
||
}
|
||
|
||
.pattern-badge.on { background: #4caf50; }
|
||
.pattern-badge.off { background: #757575; }
|
||
.pattern-badge.blink { background: #ff9800; }
|
||
.pattern-badge.chase { background: #f44336; }
|
||
.pattern-badge.circle { background: #00bcd4; }
|
||
.pattern-badge.pulse { background: #e91e63; }
|
||
.pattern-badge.rainbow { background: linear-gradient(90deg, #ff0000, #ff7f00, #ffff00, #00ff00, #0000ff, #4b0082, #9400d3); }
|
||
.pattern-badge.transition { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }
|
||
|
||
.color-preview {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-bottom: 16px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.color-swatch {
|
||
width: 40px;
|
||
height: 40px;
|
||
border-radius: 8px;
|
||
border: 2px solid #e0e0e0;
|
||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.preset-info {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 12px;
|
||
margin-bottom: 16px;
|
||
font-size: 0.875rem;
|
||
color: #666;
|
||
}
|
||
|
||
.info-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.info-label {
|
||
font-weight: 500;
|
||
}
|
||
|
||
.info-value {
|
||
color: #667eea;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.preset-actions {
|
||
display: flex;
|
||
gap: 8px;
|
||
margin-top: 16px;
|
||
padding-top: 16px;
|
||
border-top: 1px solid #e0e0e0;
|
||
}
|
||
|
||
.btn {
|
||
padding: 10px 20px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
font-size: 0.875rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
flex: 1;
|
||
}
|
||
|
||
.btn-primary {
|
||
background: #667eea;
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background: #5568d3;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background: #e0e0e0;
|
||
color: #333;
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background: #d0d0d0;
|
||
}
|
||
|
||
.btn-danger {
|
||
background: #f44336;
|
||
color: white;
|
||
}
|
||
|
||
.btn-danger:hover {
|
||
background: #d32f2f;
|
||
}
|
||
|
||
.btn-icon {
|
||
padding: 8px;
|
||
min-width: 40px;
|
||
}
|
||
|
||
.btn-large {
|
||
padding: 16px 32px;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
/* Modal Styles */
|
||
.modal {
|
||
display: none;
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.5);
|
||
z-index: 1000;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 20px;
|
||
}
|
||
|
||
.modal.active {
|
||
display: flex;
|
||
}
|
||
|
||
.modal-content {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 32px;
|
||
max-width: 600px;
|
||
width: 100%;
|
||
max-height: 90vh;
|
||
overflow-y: auto;
|
||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.modal-header {
|
||
margin-bottom: 24px;
|
||
}
|
||
|
||
.modal-header h2 {
|
||
color: #667eea;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.form-group label {
|
||
display: block;
|
||
margin-bottom: 8px;
|
||
font-weight: 500;
|
||
color: #333;
|
||
}
|
||
|
||
.form-group input,
|
||
.form-group select {
|
||
width: 100%;
|
||
padding: 12px;
|
||
border: 1px solid #e0e0e0;
|
||
border-radius: 8px;
|
||
font-size: 1rem;
|
||
}
|
||
|
||
.form-group input:focus,
|
||
.form-group select:focus {
|
||
outline: none;
|
||
border-color: #667eea;
|
||
}
|
||
|
||
.form-group small {
|
||
display: block;
|
||
margin-top: 4px;
|
||
color: #666;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.color-inputs {
|
||
display: flex;
|
||
gap: 12px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.color-input-wrapper {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 8px;
|
||
}
|
||
|
||
.color-input-wrapper {
|
||
display: inline-block;
|
||
}
|
||
|
||
.params-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 12px;
|
||
}
|
||
|
||
.n-value-input {
|
||
width: 100%;
|
||
}
|
||
|
||
.n-value-input:focus {
|
||
border-color: #667eea;
|
||
outline: none;
|
||
}
|
||
|
||
.param-input {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.param-input label {
|
||
font-size: 0.75rem;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.param-input input {
|
||
padding: 8px;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
.modal-actions {
|
||
display: flex;
|
||
gap: 12px;
|
||
margin-top: 24px;
|
||
padding-top: 24px;
|
||
border-top: 1px solid #e0e0e0;
|
||
}
|
||
|
||
.empty-state {
|
||
background: white;
|
||
border-radius: 12px;
|
||
padding: 60px 40px;
|
||
text-align: center;
|
||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.empty-state-icon {
|
||
font-size: 4rem;
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.empty-state h2 {
|
||
color: #667eea;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.empty-state p {
|
||
color: #666;
|
||
margin-bottom: 24px;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<div class="header">
|
||
<div>
|
||
<h1>Preset Management</h1>
|
||
<p>Save and manage your favorite LED pattern configurations</p>
|
||
</div>
|
||
<div style="display: flex; gap: 12px; align-items: center;">
|
||
<button class="btn btn-secondary btn-large" onclick="syncPresets()" title="Sync all presets to all devices">🔄 Sync Presets to All Devices</button>
|
||
<button class="btn btn-primary btn-large" onclick="showCreateModal()">+ Create Preset</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="controls">
|
||
<input type="text" class="search-box" placeholder="Search presets..." id="search-input">
|
||
<div class="filter-group">
|
||
<select class="filter-select" id="pattern-filter">
|
||
<option value="">All Patterns</option>
|
||
<option value="on">On</option>
|
||
<option value="off">Off</option>
|
||
<option value="blink">Blink</option>
|
||
<option value="chase">Chase</option>
|
||
<option value="circle">Circle</option>
|
||
<option value="pulse">Pulse</option>
|
||
<option value="rainbow">Rainbow</option>
|
||
<option value="transition">Transition</option>
|
||
</select>
|
||
<select class="filter-select" id="sort-select">
|
||
<option value="name">Sort by Name</option>
|
||
<option value="recent">Recently Used</option>
|
||
<option value="created">Recently Created</option>
|
||
</select>
|
||
<div class="view-toggle">
|
||
<button class="view-btn active" onclick="setView('grid')" id="view-grid">Grid</button>
|
||
<button class="view-btn" onclick="setView('list')" id="view-list">List</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="presets-grid" id="presets-container">
|
||
<!-- Preset Card 1 -->
|
||
<div class="preset-card" data-pattern="rainbow" data-name="Fast Rainbow">
|
||
<div class="preset-header">
|
||
<div>
|
||
<div class="preset-name">Fast Rainbow</div>
|
||
<span class="pattern-badge rainbow">Rainbow</span>
|
||
</div>
|
||
</div>
|
||
<div class="color-preview">
|
||
<div class="color-swatch" style="background: #FF0000;"></div>
|
||
<div class="color-swatch" style="background: #00FF00;"></div>
|
||
<div class="color-swatch" style="background: #0000FF;"></div>
|
||
</div>
|
||
<div class="preset-info">
|
||
<div class="info-item">
|
||
<span class="info-label">Delay:</span>
|
||
<span class="info-value">30ms</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">N1:</span>
|
||
<span class="info-value">10</span>
|
||
</div>
|
||
</div>
|
||
<div class="preset-actions">
|
||
<button class="btn btn-primary" onclick="applyPreset('Fast Rainbow')">Apply</button>
|
||
<button class="btn btn-secondary btn-icon" onclick="editPreset('Fast Rainbow')" title="Edit">✏️</button>
|
||
<button class="btn btn-danger btn-icon" onclick="deletePreset('Fast Rainbow')" title="Delete">🗑️</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Preset Card 2 -->
|
||
<div class="preset-card" data-pattern="pulse" data-name="Slow Pulse">
|
||
<div class="preset-header">
|
||
<div>
|
||
<div class="preset-name">Slow Pulse</div>
|
||
<span class="pattern-badge pulse">Pulse</span>
|
||
</div>
|
||
</div>
|
||
<div class="color-preview">
|
||
<div class="color-swatch" style="background: #FF0000;"></div>
|
||
<div class="color-swatch" style="background: #0000FF;"></div>
|
||
</div>
|
||
<div class="preset-info">
|
||
<div class="info-item">
|
||
<span class="info-label">Delay:</span>
|
||
<span class="info-value">200ms</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">N1:</span>
|
||
<span class="info-value">500</span>
|
||
</div>
|
||
</div>
|
||
<div class="preset-actions">
|
||
<button class="btn btn-primary" onclick="applyPreset('Slow Pulse')">Apply</button>
|
||
<button class="btn btn-secondary btn-icon" onclick="editPreset('Slow Pulse')" title="Edit">✏️</button>
|
||
<button class="btn btn-danger btn-icon" onclick="deletePreset('Slow Pulse')" title="Delete">🗑️</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Preset Card 3 -->
|
||
<div class="preset-card" data-pattern="chase" data-name="Red Blue Chase">
|
||
<div class="preset-header">
|
||
<div>
|
||
<div class="preset-name">Red Blue Chase</div>
|
||
<span class="pattern-badge chase">Chase</span>
|
||
</div>
|
||
</div>
|
||
<div class="color-preview">
|
||
<div class="color-swatch" style="background: #FF0000;"></div>
|
||
<div class="color-swatch" style="background: #0000FF;"></div>
|
||
</div>
|
||
<div class="preset-info">
|
||
<div class="info-item">
|
||
<span class="info-label">Delay:</span>
|
||
<span class="info-value">100ms</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">N1:</span>
|
||
<span class="info-value">5</span>
|
||
</div>
|
||
</div>
|
||
<div class="preset-actions">
|
||
<button class="btn btn-primary" onclick="applyPreset('Red Blue Chase')">Apply</button>
|
||
<button class="btn btn-secondary btn-icon" onclick="editPreset('Red Blue Chase')" title="Edit">✏️</button>
|
||
<button class="btn btn-danger btn-icon" onclick="deletePreset('Red Blue Chase')" title="Delete">🗑️</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Preset Card 4 -->
|
||
<div class="preset-card" data-pattern="circle" data-name="Loading Circle">
|
||
<div class="preset-header">
|
||
<div>
|
||
<div class="preset-name">Loading Circle</div>
|
||
<span class="pattern-badge circle">Circle</span>
|
||
</div>
|
||
</div>
|
||
<div class="color-preview">
|
||
<div class="color-swatch" style="background: #00FF00;"></div>
|
||
</div>
|
||
<div class="preset-info">
|
||
<div class="info-item">
|
||
<span class="info-label">Delay:</span>
|
||
<span class="info-value">50ms</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">N1:</span>
|
||
<span class="info-value">50</span>
|
||
</div>
|
||
</div>
|
||
<div class="preset-actions">
|
||
<button class="btn btn-primary" onclick="applyPreset('Loading Circle')">Apply</button>
|
||
<button class="btn btn-secondary btn-icon" onclick="editPreset('Loading Circle')" title="Edit">✏️</button>
|
||
<button class="btn btn-danger btn-icon" onclick="deletePreset('Loading Circle')" title="Delete">🗑️</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Preset Card 5 -->
|
||
<div class="preset-card" data-pattern="blink" data-name="Party Blink">
|
||
<div class="preset-header">
|
||
<div>
|
||
<div class="preset-name">Party Blink</div>
|
||
<span class="pattern-badge blink">Blink</span>
|
||
</div>
|
||
</div>
|
||
<div class="color-preview">
|
||
<div class="color-swatch" style="background: #FF00FF;"></div>
|
||
<div class="color-swatch" style="background: #00FFFF;"></div>
|
||
<div class="color-swatch" style="background: #FFFF00;"></div>
|
||
</div>
|
||
<div class="preset-info">
|
||
<div class="info-item">
|
||
<span class="info-label">Delay:</span>
|
||
<span class="info-value">150ms</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">N1:</span>
|
||
<span class="info-value">0</span>
|
||
</div>
|
||
</div>
|
||
<div class="preset-actions">
|
||
<button class="btn btn-primary" onclick="applyPreset('Party Blink')">Apply</button>
|
||
<button class="btn btn-secondary btn-icon" onclick="editPreset('Party Blink')" title="Edit">✏️</button>
|
||
<button class="btn btn-danger btn-icon" onclick="deletePreset('Party Blink')" title="Delete">🗑️</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Preset Card 6 -->
|
||
<div class="preset-card" data-pattern="transition" data-name="Smooth Transition">
|
||
<div class="preset-header">
|
||
<div>
|
||
<div class="preset-name">Smooth Transition</div>
|
||
<span class="pattern-badge transition">Transition</span>
|
||
</div>
|
||
</div>
|
||
<div class="color-preview">
|
||
<div class="color-swatch" style="background: #FF0000;"></div>
|
||
<div class="color-swatch" style="background: #00FF00;"></div>
|
||
<div class="color-swatch" style="background: #0000FF;"></div>
|
||
<div class="color-swatch" style="background: #FFFF00;"></div>
|
||
</div>
|
||
<div class="preset-info">
|
||
<div class="info-item">
|
||
<span class="info-label">Delay:</span>
|
||
<span class="info-value">100ms</span>
|
||
</div>
|
||
<div class="info-item">
|
||
<span class="info-label">N1:</span>
|
||
<span class="info-value">0</span>
|
||
</div>
|
||
</div>
|
||
<div class="preset-actions">
|
||
<button class="btn btn-primary" onclick="applyPreset('Smooth Transition')">Apply</button>
|
||
<button class="btn btn-secondary btn-icon" onclick="editPreset('Smooth Transition')" title="Edit">✏️</button>
|
||
<button class="btn btn-danger btn-icon" onclick="deletePreset('Smooth Transition')" title="Delete">🗑️</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Create/Edit Preset Modal -->
|
||
<div class="modal" id="preset-modal">
|
||
<div class="modal-content">
|
||
<div class="modal-header">
|
||
<h2 id="modal-title">Create Preset</h2>
|
||
<p>Configure your preset settings</p>
|
||
</div>
|
||
<form id="preset-form">
|
||
<div class="form-group">
|
||
<label for="preset-name">Preset Name *</label>
|
||
<input type="text" id="preset-name" required placeholder="Enter preset name">
|
||
<small>Unique identifier for this preset</small>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="preset-pattern">Pattern *</label>
|
||
<select id="preset-pattern" required>
|
||
<option value="on">On</option>
|
||
<option value="off">Off</option>
|
||
<option value="blink">Blink</option>
|
||
<option value="chase">Chase</option>
|
||
<option value="circle">Circle</option>
|
||
<option value="pulse">Pulse</option>
|
||
<option value="rainbow">Rainbow</option>
|
||
<option value="transition">Transition</option>
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>Colors *</label>
|
||
<div class="color-inputs" id="color-inputs">
|
||
<!-- Color pickers will be added here -->
|
||
</div>
|
||
<button type="button" class="btn btn-secondary" onclick="addColorPicker()" style="margin-top: 8px;">+ Add Color</button>
|
||
<small>Minimum 2 colors required</small>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="preset-delay">
|
||
Delay (ms) *
|
||
<span id="delay-value-display" style="margin-left: 12px; color: #667eea; font-weight: 600;">100</span>
|
||
</label>
|
||
<input type="range" id="preset-delay" min="10" max="1000" value="100" step="10" required>
|
||
<small>Animation speed (10-1000 milliseconds)</small>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="step-offset">Step Offset</label>
|
||
<input type="number" id="step-offset" value="0" min="-1000" max="1000">
|
||
<small>Step offset for group synchronization. Applied per device when preset is used in a group.</small>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="step-increment">Step Increment</label>
|
||
<input type="number" id="step-increment" value="1" min="1" max="255">
|
||
<small>Amount step counter increments per cycle. Controls pattern advancement speed.</small>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label>Pattern Parameters (N1-N8)</label>
|
||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">
|
||
<small style="margin: 0;">Pattern-specific parameters (0-255, varies by pattern)</small>
|
||
<button type="button" class="btn btn-secondary" onclick="setAllNValues(0)" style="padding: 6px 12px; font-size: 0.75rem;">Reset All to 0</button>
|
||
</div>
|
||
<div class="params-grid">
|
||
<div class="param-input">
|
||
<label>N1</label>
|
||
<input type="number" id="n1" min="0" max="255" value="0" class="n-value-input">
|
||
</div>
|
||
<div class="param-input">
|
||
<label>N2</label>
|
||
<input type="number" id="n2" min="0" max="255" value="0" class="n-value-input">
|
||
</div>
|
||
<div class="param-input">
|
||
<label>N3</label>
|
||
<input type="number" id="n3" min="0" max="255" value="0" class="n-value-input">
|
||
</div>
|
||
<div class="param-input">
|
||
<label>N4</label>
|
||
<input type="number" id="n4" min="0" max="255" value="0" class="n-value-input">
|
||
</div>
|
||
<div class="param-input">
|
||
<label>N5</label>
|
||
<input type="number" id="n5" min="0" max="255" value="0" class="n-value-input">
|
||
</div>
|
||
<div class="param-input">
|
||
<label>N6</label>
|
||
<input type="number" id="n6" min="0" max="255" value="0" class="n-value-input">
|
||
</div>
|
||
<div class="param-input">
|
||
<label>N7</label>
|
||
<input type="number" id="n7" min="0" max="255" value="0" class="n-value-input">
|
||
</div>
|
||
<div class="param-input">
|
||
<label>N8</label>
|
||
<input type="number" id="n8" min="0" max="255" value="0" class="n-value-input">
|
||
</div>
|
||
</div>
|
||
<div style="margin-top: 12px; display: flex; gap: 8px; flex-wrap: wrap;">
|
||
<button type="button" class="btn btn-secondary" onclick="setAllNValues(0)" style="padding: 8px 16px; font-size: 0.875rem;">Set All to 0</button>
|
||
<button type="button" class="btn btn-secondary" onclick="copyNValuesFromCurrent()" style="padding: 8px 16px; font-size: 0.875rem;">Copy from Current Settings</button>
|
||
<button type="button" class="btn btn-secondary" onclick="showNValueHelp()" style="padding: 8px 16px; font-size: 0.875rem;">ℹ️ Parameter Help</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-actions">
|
||
<button type="button" class="btn btn-secondary" onclick="closeModal()">Cancel</button>
|
||
<button type="submit" class="btn btn-primary">Save Preset</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let currentView = 'grid';
|
||
let editingPreset = null;
|
||
|
||
// Delay slider update
|
||
document.getElementById('preset-delay').addEventListener('input', function(e) {
|
||
document.getElementById('delay-value-display').textContent = e.target.value;
|
||
});
|
||
|
||
// Search functionality
|
||
document.getElementById('search-input').addEventListener('input', function(e) {
|
||
filterPresets();
|
||
});
|
||
|
||
// Filter functionality
|
||
document.getElementById('pattern-filter').addEventListener('change', function(e) {
|
||
filterPresets();
|
||
});
|
||
|
||
// Sort functionality
|
||
document.getElementById('sort-select').addEventListener('change', function(e) {
|
||
sortPresets(e.target.value);
|
||
});
|
||
|
||
function filterPresets() {
|
||
const search = document.getElementById('search-input').value.toLowerCase();
|
||
const patternFilter = document.getElementById('pattern-filter').value;
|
||
const cards = document.querySelectorAll('.preset-card');
|
||
|
||
cards.forEach(card => {
|
||
const name = card.dataset.name.toLowerCase();
|
||
const pattern = card.dataset.pattern;
|
||
const matchesSearch = name.includes(search);
|
||
const matchesPattern = !patternFilter || pattern === patternFilter;
|
||
|
||
if (matchesSearch && matchesPattern) {
|
||
card.style.display = '';
|
||
} else {
|
||
card.style.display = 'none';
|
||
}
|
||
});
|
||
}
|
||
|
||
function sortPresets(sortBy) {
|
||
const container = document.getElementById('presets-container');
|
||
const cards = Array.from(container.querySelectorAll('.preset-card'));
|
||
|
||
cards.sort((a, b) => {
|
||
if (sortBy === 'name') {
|
||
return a.dataset.name.localeCompare(b.dataset.name);
|
||
} else if (sortBy === 'recent') {
|
||
// In real implementation, would use actual usage data
|
||
return 0;
|
||
} else if (sortBy === 'created') {
|
||
// In real implementation, would use creation timestamps
|
||
return 0;
|
||
}
|
||
return 0;
|
||
});
|
||
|
||
cards.forEach(card => container.appendChild(card));
|
||
}
|
||
|
||
function setView(view) {
|
||
currentView = view;
|
||
const container = document.getElementById('presets-container');
|
||
const gridBtn = document.getElementById('view-grid');
|
||
const listBtn = document.getElementById('view-list');
|
||
|
||
if (view === 'grid') {
|
||
container.style.gridTemplateColumns = 'repeat(auto-fill, minmax(300px, 1fr))';
|
||
gridBtn.classList.add('active');
|
||
listBtn.classList.remove('active');
|
||
} else {
|
||
container.style.gridTemplateColumns = '1fr';
|
||
gridBtn.classList.remove('active');
|
||
listBtn.classList.add('active');
|
||
}
|
||
}
|
||
|
||
function showCreateModal() {
|
||
editingPreset = null;
|
||
document.getElementById('modal-title').textContent = 'Create Preset';
|
||
document.getElementById('preset-form').reset();
|
||
document.getElementById('preset-delay').value = 100;
|
||
document.getElementById('delay-value-display').textContent = '100';
|
||
// Reset to 2 colors
|
||
initializeColorPickers();
|
||
document.getElementById('preset-modal').classList.add('active');
|
||
}
|
||
|
||
// Initialize color pickers function (defined before showCreateModal)
|
||
const presetColorPickers = [];
|
||
|
||
function initializeColorPickers() {
|
||
const colorInputs = document.getElementById('color-inputs');
|
||
colorInputs.innerHTML = '';
|
||
presetColorPickers.length = 0;
|
||
addColorPicker('#FF0000');
|
||
addColorPicker('#0000FF');
|
||
}
|
||
|
||
function addColorPicker(color = '#00FF00') {
|
||
const colorInputs = document.getElementById('color-inputs');
|
||
const wrapper = document.createElement('div');
|
||
wrapper.className = 'color-input-wrapper';
|
||
colorInputs.appendChild(wrapper);
|
||
|
||
const picker = new ColorPicker(wrapper, {
|
||
initialColor: color,
|
||
onColorChange: (newColor) => {
|
||
console.log('Preset color changed:', newColor);
|
||
}
|
||
});
|
||
|
||
presetColorPickers.push(picker);
|
||
return picker;
|
||
}
|
||
|
||
function editPreset(name) {
|
||
editingPreset = name;
|
||
document.getElementById('modal-title').textContent = 'Edit Preset';
|
||
// In real implementation, would load preset data
|
||
document.getElementById('preset-name').value = name;
|
||
document.getElementById('preset-modal').classList.add('active');
|
||
}
|
||
|
||
function closeModal() {
|
||
document.getElementById('preset-modal').classList.remove('active');
|
||
editingPreset = null;
|
||
}
|
||
|
||
|
||
function applyPreset(name) {
|
||
alert(`Applying preset: ${name}\n(In real implementation, this would send preset configuration to device(s))`);
|
||
}
|
||
|
||
function deletePreset(name) {
|
||
if (confirm(`Delete preset "${name}"?`)) {
|
||
alert(`Preset "${name}" deleted\n(In real implementation, this would remove the preset from storage)`);
|
||
}
|
||
}
|
||
|
||
function syncPresets() {
|
||
if (confirm('Sync all presets to all devices?\nThis will send all presets from master to all devices via ESPNow.')) {
|
||
alert('Syncing presets to all devices...\n(In real implementation, this would send all presets via ESPNow to all devices)');
|
||
}
|
||
}
|
||
|
||
function setAllNValues(value) {
|
||
for (let i = 1; i <= 8; i++) {
|
||
document.getElementById(`n${i}`).value = value;
|
||
}
|
||
}
|
||
|
||
function copyNValuesFromCurrent() {
|
||
// In real implementation, this would copy from current device settings
|
||
alert('Copying N values from current device settings...\n(In real implementation, this would load current N1-N8 values from the active device)');
|
||
// Example: would set values like this:
|
||
// document.getElementById('n1').value = currentSettings.n1;
|
||
// ... etc
|
||
}
|
||
|
||
function showNValueHelp() {
|
||
const helpText = `
|
||
Pattern Parameter Guide:
|
||
|
||
Rainbow:
|
||
N1: Step increment (1-255, default: 1)
|
||
|
||
Pulse:
|
||
N1: Attack time in ms (0-255)
|
||
N2: Hold time in ms (0-255)
|
||
N3: Decay time in ms (0-255)
|
||
|
||
Chase:
|
||
N1: LEDs of color 0 (1-255)
|
||
N2: LEDs of color 1 (1-255)
|
||
N3: Step movement on odd steps (can be negative)
|
||
N4: Step movement on even steps (can be negative)
|
||
|
||
Circle:
|
||
N1: Head moves per second (1-255)
|
||
N2: Max length in LEDs (1-255)
|
||
N3: Tail moves per second (1-255)
|
||
N4: Min length in LEDs (0-255)
|
||
|
||
Other patterns:
|
||
N1-N8: Reserved for future pattern enhancements
|
||
|
||
All values range from 0-255 unless otherwise specified.
|
||
`;
|
||
alert(helpText);
|
||
}
|
||
|
||
// Form submission
|
||
document.getElementById('preset-form').addEventListener('submit', function(e) {
|
||
e.preventDefault();
|
||
const name = document.getElementById('preset-name').value;
|
||
const action = editingPreset ? 'updated' : 'created';
|
||
alert(`Preset "${name}" ${action}!\n(In real implementation, this would save the preset to storage)`);
|
||
closeModal();
|
||
});
|
||
|
||
// Close modal on outside click
|
||
document.getElementById('preset-modal').addEventListener('click', function(e) {
|
||
if (e.target === this) {
|
||
closeModal();
|
||
}
|
||
});
|
||
</script>
|
||
<script src="color-picker.js"></script>
|
||
</body>
|
||
</html>
|
||
|