Split monolithic index.html and style.css for maintainability
- Extract 15 modals and 3 partials from index.html into Jinja2 templates (templates/modals/*.html, templates/partials/*.html) - Split style.css (3,712 lines) into 11 feature-scoped CSS files under static/css/ (base, layout, components, cards, modal, calibration, dashboard, streams, patterns, profiles, tutorials) - Switch root route from FileResponse to Jinja2Templates - Add jinja2 dependency - Consolidate duplicate @keyframes spin definition - Browser receives identical assembled HTML — zero JS changes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,7 @@ dependencies = [
|
|||||||
"python-json-logger>=3.1.0",
|
"python-json-logger>=3.1.0",
|
||||||
"python-dateutil>=2.9.0",
|
"python-dateutil>=2.9.0",
|
||||||
"python-multipart>=0.0.12",
|
"python-multipart>=0.0.12",
|
||||||
|
"jinja2>=3.1.0",
|
||||||
"wmi>=1.5.1; sys_platform == 'win32'",
|
"wmi>=1.5.1; sys_platform == 'win32'",
|
||||||
"zeroconf>=0.131.0",
|
"zeroconf>=0.131.0",
|
||||||
"pyserial>=3.5",
|
"pyserial>=3.5",
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ from pathlib import Path
|
|||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import JSONResponse, FileResponse
|
from fastapi.responses import JSONResponse
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
from fastapi.templating import Jinja2Templates
|
||||||
|
from starlette.requests import Request
|
||||||
|
|
||||||
from wled_controller import __version__
|
from wled_controller import __version__
|
||||||
from wled_controller.api import router
|
from wled_controller.api import router
|
||||||
@@ -262,6 +264,10 @@ if static_path.exists():
|
|||||||
else:
|
else:
|
||||||
logger.warning(f"Static files directory not found: {static_path}")
|
logger.warning(f"Static files directory not found: {static_path}")
|
||||||
|
|
||||||
|
# Jinja2 templates
|
||||||
|
templates_path = Path(__file__).parent / "templates"
|
||||||
|
templates = Jinja2Templates(directory=str(templates_path))
|
||||||
|
|
||||||
|
|
||||||
@app.exception_handler(Exception)
|
@app.exception_handler(Exception)
|
||||||
async def global_exception_handler(request, exc):
|
async def global_exception_handler(request, exc):
|
||||||
@@ -279,20 +285,9 @@ async def global_exception_handler(request, exc):
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/")
|
@app.get("/")
|
||||||
async def root():
|
async def root(request: Request):
|
||||||
"""Serve the web UI dashboard."""
|
"""Serve the web UI dashboard."""
|
||||||
static_path = Path(__file__).parent / "static" / "index.html"
|
return templates.TemplateResponse(request, "index.html")
|
||||||
if static_path.exists():
|
|
||||||
return FileResponse(static_path)
|
|
||||||
|
|
||||||
# Fallback to JSON if static files not found
|
|
||||||
return {
|
|
||||||
"name": "LED Grab",
|
|
||||||
"version": __version__,
|
|
||||||
"docs": "/docs",
|
|
||||||
"health": "/health",
|
|
||||||
"api": "/api/v1",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
61
server/src/wled_controller/static/css/base.css
Normal file
61
server/src/wled_controller/static/css/base.css
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--primary-color: #4CAF50;
|
||||||
|
--danger-color: #f44336;
|
||||||
|
--warning-color: #ff9800;
|
||||||
|
--info-color: #2196F3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark theme (default) */
|
||||||
|
[data-theme="dark"] {
|
||||||
|
--bg-color: #1a1a1a;
|
||||||
|
--card-bg: #2d2d2d;
|
||||||
|
--text-color: #e0e0e0;
|
||||||
|
--border-color: #404040;
|
||||||
|
--display-badge-bg: rgba(0, 0, 0, 0.4);
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light theme */
|
||||||
|
[data-theme="light"] {
|
||||||
|
--bg-color: #f5f5f5;
|
||||||
|
--card-bg: #ffffff;
|
||||||
|
--text-color: #333333;
|
||||||
|
--border-color: #e0e0e0;
|
||||||
|
--display-badge-bg: rgba(255, 255, 255, 0.85);
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Default to dark theme */
|
||||||
|
body {
|
||||||
|
background: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
background: var(--bg-color);
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
background: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.modal-open {
|
||||||
|
position: fixed;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
423
server/src/wled_controller/static/css/calibration.css
Normal file
423
server/src/wled_controller/static/css/calibration.css
Normal file
@@ -0,0 +1,423 @@
|
|||||||
|
/* Interactive Calibration Preview Edges */
|
||||||
|
.calibration-preview {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
margin: 40px auto 40px;
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
#calibration-preview-canvas {
|
||||||
|
position: absolute;
|
||||||
|
top: -40px;
|
||||||
|
left: -40px;
|
||||||
|
width: calc(100% + 80px);
|
||||||
|
height: calc(100% + 80px);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-screen {
|
||||||
|
position: absolute;
|
||||||
|
top: 37px;
|
||||||
|
left: 57px;
|
||||||
|
right: 57px;
|
||||||
|
bottom: 37px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 6px;
|
||||||
|
color: white;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-screen-border-width {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-screen-border-width label {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-screen-border-width input {
|
||||||
|
width: 52px;
|
||||||
|
padding: 2px 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.4);
|
||||||
|
border-radius: 3px;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
color: white;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-screen-border-width input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: rgba(255, 255, 255, 0.7);
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-screen-total {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
opacity: 0.9;
|
||||||
|
transition: color 0.2s;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-screen-total:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-screen-total.mismatch {
|
||||||
|
color: #FFC107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputs-dimmed .edge-led-input {
|
||||||
|
opacity: 0.2;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-screen-controls {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-edge {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: rgba(128, 128, 128, 0.15);
|
||||||
|
transition: background 0.2s;
|
||||||
|
z-index: 2;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Edge test toggle zones — positioned outside the container border */
|
||||||
|
.edge-toggle {
|
||||||
|
position: absolute;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 1;
|
||||||
|
background: rgba(128, 128, 128, 0.1);
|
||||||
|
border: 1px solid rgba(128, 128, 128, 0.35);
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: background 0.2s, box-shadow 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-toggle:hover {
|
||||||
|
background: rgba(128, 128, 128, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-edge.edge-disabled {
|
||||||
|
opacity: 0.25;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-edge.edge-disabled .edge-led-input {
|
||||||
|
pointer-events: auto;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-toggle.edge-disabled {
|
||||||
|
opacity: 0.15;
|
||||||
|
pointer-events: none;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-top {
|
||||||
|
top: -16px;
|
||||||
|
left: 56px;
|
||||||
|
right: 56px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-bottom {
|
||||||
|
bottom: -16px;
|
||||||
|
left: 56px;
|
||||||
|
right: 56px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-left {
|
||||||
|
left: -16px;
|
||||||
|
top: 36px;
|
||||||
|
bottom: 36px;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-right {
|
||||||
|
right: -16px;
|
||||||
|
top: 36px;
|
||||||
|
bottom: 36px;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-top {
|
||||||
|
top: 0;
|
||||||
|
left: 56px;
|
||||||
|
right: 56px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 6px 6px 0 0;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-bottom {
|
||||||
|
bottom: 0;
|
||||||
|
left: 56px;
|
||||||
|
right: 56px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 0 0 6px 6px;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-left {
|
||||||
|
left: 0;
|
||||||
|
top: 36px;
|
||||||
|
bottom: 36px;
|
||||||
|
width: 56px;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 6px 0 0 6px;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-right {
|
||||||
|
right: 0;
|
||||||
|
top: 36px;
|
||||||
|
bottom: 36px;
|
||||||
|
width: 56px;
|
||||||
|
flex-direction: column;
|
||||||
|
border-radius: 0 6px 6px 0;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-led-input {
|
||||||
|
width: 46px;
|
||||||
|
padding: 2px 2px;
|
||||||
|
font-size: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-height: 100%;
|
||||||
|
text-align: center;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 3px;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-led-input:focus {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-top .edge-led-input,
|
||||||
|
.edge-bottom .edge-led-input {
|
||||||
|
width: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide spinner arrows on edge inputs to save space */
|
||||||
|
.edge-led-input::-webkit-outer-spin-button,
|
||||||
|
.edge-led-input::-webkit-inner-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.edge-led-input[type=number] {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Edge span bars */
|
||||||
|
.edge-span-bar {
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(76, 175, 80, 0.3);
|
||||||
|
border: 1px solid rgba(76, 175, 80, 0.5);
|
||||||
|
border-radius: 2px;
|
||||||
|
cursor: grab;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-span-bar:hover {
|
||||||
|
background: rgba(76, 175, 80, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-span-bar:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Horizontal edges: bar spans left-right */
|
||||||
|
.edge-top .edge-span-bar,
|
||||||
|
.edge-bottom .edge-span-bar {
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vertical edges: bar spans top-bottom */
|
||||||
|
.edge-left .edge-span-bar,
|
||||||
|
.edge-right .edge-span-bar {
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Resize handles — large transparent hit area with narrow visible strip */
|
||||||
|
.edge-span-handle {
|
||||||
|
position: absolute;
|
||||||
|
background: transparent;
|
||||||
|
z-index: 3;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-span-handle::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(255, 255, 255, 0.75);
|
||||||
|
border: 1px solid rgba(76, 175, 80, 0.7);
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-span-bar:hover .edge-span-handle {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Horizontal handles */
|
||||||
|
.edge-top .edge-span-handle,
|
||||||
|
.edge-bottom .edge-span-handle {
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 16px;
|
||||||
|
cursor: ew-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-top .edge-span-handle::after,
|
||||||
|
.edge-bottom .edge-span-handle::after {
|
||||||
|
top: 3px;
|
||||||
|
bottom: 3px;
|
||||||
|
left: 6px;
|
||||||
|
width: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-top .edge-span-handle-start,
|
||||||
|
.edge-bottom .edge-span-handle-start {
|
||||||
|
left: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-top .edge-span-handle-end,
|
||||||
|
.edge-bottom .edge-span-handle-end {
|
||||||
|
right: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Vertical handles */
|
||||||
|
.edge-left .edge-span-handle,
|
||||||
|
.edge-right .edge-span-handle {
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 16px;
|
||||||
|
cursor: ns-resize;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-left .edge-span-handle::after,
|
||||||
|
.edge-right .edge-span-handle::after {
|
||||||
|
left: 3px;
|
||||||
|
right: 3px;
|
||||||
|
top: 6px;
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-left .edge-span-handle-start,
|
||||||
|
.edge-right .edge-span-handle-start {
|
||||||
|
top: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edge-left .edge-span-handle-end,
|
||||||
|
.edge-right .edge-span-handle-end {
|
||||||
|
bottom: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure LED input is above span bar */
|
||||||
|
.edge-led-input {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Corner start-position buttons */
|
||||||
|
.preview-corner {
|
||||||
|
position: absolute;
|
||||||
|
width: 56px;
|
||||||
|
height: 36px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1;
|
||||||
|
color: rgba(128, 128, 128, 0.4);
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 5;
|
||||||
|
transition: color 0.2s, transform 0.2s, text-shadow 0.2s;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-corner:hover {
|
||||||
|
color: rgba(76, 175, 80, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-corner.active:hover {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-corner.active {
|
||||||
|
color: #4CAF50;
|
||||||
|
text-shadow: 0 0 8px rgba(76, 175, 80, 0.6);
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.corner-top-left { top: 0; left: 0; }
|
||||||
|
.corner-top-right { top: 0; right: 0; }
|
||||||
|
.corner-bottom-left { bottom: 0; left: 0; }
|
||||||
|
.corner-bottom-right { bottom: 0; right: 0; }
|
||||||
|
|
||||||
|
/* Direction toggle inside screen */
|
||||||
|
.direction-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
height: 26px;
|
||||||
|
padding: 0 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 12px;
|
||||||
|
color: white;
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.direction-toggle:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.direction-toggle #direction-icon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-hint {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
592
server/src/wled_controller/static/css/cards.css
Normal file
592
server/src/wled_controller/static/css/cards.css
Normal file
@@ -0,0 +1,592 @@
|
|||||||
|
section {
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.displays-grid,
|
||||||
|
.devices-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.devices-grid > .loading,
|
||||||
|
.devices-grid > .loading-spinner {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-device-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px dashed var(--border-color);
|
||||||
|
background: transparent;
|
||||||
|
min-height: 160px;
|
||||||
|
transition: border-color 0.2s, background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-device-card:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
background: rgba(33, 150, 243, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-device-icon {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 300;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-device-card:hover .add-device-icon {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-device-label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 20px 20px;
|
||||||
|
position: relative;
|
||||||
|
transition: transform 0.2s, box-shadow 0.2s;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-tutorial-btn {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background: none;
|
||||||
|
border: 1.5px solid var(--border-color);
|
||||||
|
color: var(--text-muted, #777);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: bold;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: color 0.2s, background 0.2s, border-color 0.2s;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-tutorial-btn:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.card-top-actions {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-top-actions .card-remove-btn {
|
||||||
|
position: static;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-power-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #777;
|
||||||
|
font-size: 1rem;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: color 0.2s, background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-power-btn:hover {
|
||||||
|
color: var(--primary-color);
|
||||||
|
background: rgba(76, 175, 80, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-remove-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #777;
|
||||||
|
font-size: 1rem;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: color 0.2s, background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-remove-btn:hover {
|
||||||
|
color: var(--danger-color);
|
||||||
|
background: rgba(244, 67, 54, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-right: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-title {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-url-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: var(--border-color);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
font-family: monospace;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-url-badge:hover {
|
||||||
|
background: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-url-icon {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-subtitle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-meta {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #999;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-type-badge {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Device discovery */
|
||||||
|
.discovery-section {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.btn-block {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.discovery-list {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.discovery-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--card-bg);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
.discovery-item:not(.discovery-item--added):hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
.discovery-item--added {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.discovery-item-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
.discovery-badge {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: var(--border-color);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.discovery-type-badge {
|
||||||
|
font-size: 10px;
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 600;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
.modal-divider {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
margin: 12px 0;
|
||||||
|
}
|
||||||
|
.discovery-loading {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
.discovery-spinner {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
border-top-color: var(--primary-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-indicator {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 2px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-indicator .ch {
|
||||||
|
width: 8px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-card {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-index {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--info-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-star {
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Display Layout Visualization */
|
||||||
|
.layout-container {
|
||||||
|
position: relative;
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-display {
|
||||||
|
position: absolute;
|
||||||
|
border: 3px solid;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2);
|
||||||
|
transition: box-shadow 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-display:hover {
|
||||||
|
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-display.primary {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
background: linear-gradient(135deg, rgba(76, 175, 80, 0.15), rgba(76, 175, 80, 0.05));
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-display.secondary {
|
||||||
|
border-color: var(--border-color);
|
||||||
|
background: linear-gradient(135deg, rgba(128, 128, 128, 0.1), rgba(128, 128, 128, 0.05));
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-position-label {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 6px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-index-label {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 6px;
|
||||||
|
left: 6px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
|
background: var(--display-badge-bg);
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-display-label {
|
||||||
|
text-align: center;
|
||||||
|
padding: 5px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-display-label strong {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-display-label small {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.primary-indicator {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: 4px;
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
line-height: 1;
|
||||||
|
text-shadow: 0 0 4px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Card brightness slider */
|
||||||
|
.brightness-control {
|
||||||
|
padding: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brightness-slider {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brightness-loading .brightness-slider {
|
||||||
|
opacity: 0.3;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Static color picker — inline in card-subtitle */
|
||||||
|
.static-color-control {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.static-color-picker {
|
||||||
|
width: 22px;
|
||||||
|
height: 18px;
|
||||||
|
padding: 0;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
background: none;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-clear-color {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #777;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 2px;
|
||||||
|
line-height: 1;
|
||||||
|
border-radius: 3px;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-clear-color:hover {
|
||||||
|
color: var(--danger-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header h2 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-tip {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin: 0 0 15px 0;
|
||||||
|
line-height: 1.5;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: rgba(33, 150, 243, 0.08);
|
||||||
|
border-left: 3px solid var(--info-color, #2196F3);
|
||||||
|
border-radius: 0 6px 6px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-tip a {
|
||||||
|
color: var(--info-color, #2196F3);
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.section-tip {
|
||||||
|
list-style: disc;
|
||||||
|
padding-left: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul.section-tip li {
|
||||||
|
margin: 2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metrics-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 4px 12px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 3px 8px;
|
||||||
|
background: var(--bg-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-value {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-label {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Timing breakdown bar */
|
||||||
|
.timing-breakdown {
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: var(--bg-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timing-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timing-total {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.timing-bar {
|
||||||
|
display: flex;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
margin: 4px 0;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timing-seg {
|
||||||
|
min-width: 2px;
|
||||||
|
transition: flex 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timing-extract { background: #4CAF50; }
|
||||||
|
.timing-map { background: #FF9800; }
|
||||||
|
.timing-smooth { background: #2196F3; }
|
||||||
|
.timing-send { background: #E91E63; }
|
||||||
|
|
||||||
|
.timing-legend {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #999;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timing-legend-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timing-dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timing-dot.timing-extract { background: #4CAF50; }
|
||||||
|
.timing-dot.timing-map { background: #FF9800; }
|
||||||
|
.timing-dot.timing-smooth { background: #2196F3; }
|
||||||
|
.timing-dot.timing-send { background: #E91E63; }
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.displays-grid,
|
||||||
|
.devices-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
356
server/src/wled_controller/static/css/components.css
Normal file
356
server/src/wled_controller/static/css/components.css
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
.badge {
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.processing {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.idle {
|
||||||
|
background: var(--warning-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge.error {
|
||||||
|
background: var(--danger-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-content {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 0;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-row:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-label {
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-value {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-danger {
|
||||||
|
background: var(--danger-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--border-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon {
|
||||||
|
min-width: auto;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-icon:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-toggle-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-toggle {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
width: 34px;
|
||||||
|
height: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-toggle input {
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-toggle-slider {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: var(--border-color);
|
||||||
|
border-radius: 9px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-toggle-slider::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-toggle input:checked + .settings-toggle-slider {
|
||||||
|
background: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-toggle input:checked + .settings-toggle-slider::after {
|
||||||
|
transform: translateX(16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #999;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"],
|
||||||
|
input[type="url"],
|
||||||
|
input[type="number"],
|
||||||
|
input[type="password"],
|
||||||
|
select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s, opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="number"]:disabled,
|
||||||
|
input[type="password"]:disabled,
|
||||||
|
select:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="range"] {
|
||||||
|
width: 100%;
|
||||||
|
margin: 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Better password field appearance */
|
||||||
|
input[type="password"] {
|
||||||
|
letter-spacing: 0.15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
input:focus,
|
||||||
|
select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove browser autofill styling */
|
||||||
|
input:-webkit-autofill,
|
||||||
|
input:-webkit-autofill:hover,
|
||||||
|
input:-webkit-autofill:focus {
|
||||||
|
-webkit-box-shadow: 0 0 0 1000px var(--bg-color) inset;
|
||||||
|
-webkit-text-fill-color: var(--text-color);
|
||||||
|
transition: background-color 5000s ease-in-out 0s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px;
|
||||||
|
color: #999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loading-spinner::after {
|
||||||
|
content: '';
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border: 3px solid var(--border-color);
|
||||||
|
border-top-color: var(--primary-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: spin 0.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Full-page overlay spinner */
|
||||||
|
.overlay-spinner {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 9999;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-spinner .progress-container {
|
||||||
|
position: relative;
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-spinner .progress-ring {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-spinner .progress-ring-circle {
|
||||||
|
transition: stroke-dashoffset 0.1s linear;
|
||||||
|
stroke: var(--primary-color);
|
||||||
|
stroke-width: 4;
|
||||||
|
fill: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-spinner .progress-ring-bg {
|
||||||
|
stroke: rgba(255, 255, 255, 0.1);
|
||||||
|
stroke-width: 4;
|
||||||
|
fill: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-spinner .progress-content {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-spinner .progress-percentage {
|
||||||
|
color: white;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-spinner .spinner-text {
|
||||||
|
margin-top: 24px;
|
||||||
|
color: white;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-spinner-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font-size: 32px;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1;
|
||||||
|
padding: 4px 8px;
|
||||||
|
transition: color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overlay-spinner-close:hover {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 40px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%) translateY(100px);
|
||||||
|
padding: 16px 24px;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: white;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s, transform 0.3s;
|
||||||
|
z-index: 2001;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.6);
|
||||||
|
min-width: 300px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.show {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(-50%) translateY(0);
|
||||||
|
animation: toastShake 0.5s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes toastShake {
|
||||||
|
0%, 100% { transform: translateX(-50%) translateY(0); }
|
||||||
|
10%, 30%, 50%, 70%, 90% { transform: translateX(-50%) translateY(-5px); }
|
||||||
|
20%, 40%, 60%, 80% { transform: translateX(-50%) translateY(5px); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.success {
|
||||||
|
background: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.error {
|
||||||
|
background: var(--danger-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast.info {
|
||||||
|
background: var(--info-color);
|
||||||
|
}
|
||||||
281
server/src/wled_controller/static/css/dashboard.css
Normal file
281
server/src/wled_controller/static/css/dashboard.css
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
/* ── Dashboard ── */
|
||||||
|
|
||||||
|
.dashboard-section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-section-header {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-section-chevron {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
width: 10px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-section-count {
|
||||||
|
background: var(--border-color);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0 6px;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-subsection {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
padding-left: 16px;
|
||||||
|
}
|
||||||
|
.dashboard-subsection .dashboard-section-header {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-stop-all {
|
||||||
|
margin-left: auto;
|
||||||
|
padding: 2px 8px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-poll-wrap {
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-poll-slider {
|
||||||
|
width: 48px;
|
||||||
|
height: 12px;
|
||||||
|
accent-color: var(--primary-color);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-poll-value {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
min-width: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-target {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr auto auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-target-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-target-icon {
|
||||||
|
font-size: 1rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-target-name {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-target-name .health-dot {
|
||||||
|
margin-right: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-target-subtitle {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-target-metrics {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-metric {
|
||||||
|
text-align: center;
|
||||||
|
min-width: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-metric-value {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--primary-color);
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-metric-label {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-fps-metric {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-fps-sparkline {
|
||||||
|
position: relative;
|
||||||
|
width: 100px;
|
||||||
|
height: 36px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-fps-label {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 36px;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-fps-target {
|
||||||
|
font-weight: 400;
|
||||||
|
opacity: 0.5;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-target-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-status-dot {
|
||||||
|
font-size: 1rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-status-dot.active {
|
||||||
|
color: #4CAF50;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-no-targets {
|
||||||
|
text-align: center;
|
||||||
|
padding: 32px 16px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-badge-stopped {
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
background: var(--border-color);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-badge-active {
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
background: var(--success-color, #28a745);
|
||||||
|
color: #fff;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-profile .dashboard-target-metrics {
|
||||||
|
min-width: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.dashboard-target {
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-target-metrics {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== PERFORMANCE CHARTS ===== */
|
||||||
|
|
||||||
|
.perf-charts-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.perf-chart-card {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.perf-chart-wrap {
|
||||||
|
position: relative;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.perf-chart-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.perf-chart-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.perf-chart-value {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.perf-chart-value.cpu { color: #2196F3; }
|
||||||
|
.perf-chart-value.ram { color: #4CAF50; }
|
||||||
|
.perf-chart-value.gpu { color: #FF9800; }
|
||||||
|
|
||||||
|
.perf-chart-unavailable {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
209
server/src/wled_controller/static/css/layout.css
Normal file
209
server/src/wled_controller/static/css/layout.css
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 20px 0 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-link {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: color 0.2s, background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-link:hover {
|
||||||
|
color: var(--text-color);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
#server-version {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: var(--border-color);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
letter-spacing: 0.03em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.online {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-badge.offline {
|
||||||
|
color: var(--danger-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* WLED device health indicator */
|
||||||
|
.health-dot {
|
||||||
|
display: inline-block;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 8px;
|
||||||
|
vertical-align: middle;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-dot.health-online {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
box-shadow: 0 0 6px rgba(76, 175, 80, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-dot.health-offline {
|
||||||
|
background-color: var(--danger-color);
|
||||||
|
box-shadow: 0 0 6px rgba(244, 67, 54, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-dot.health-unknown {
|
||||||
|
background-color: #9E9E9E;
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-latency {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 400;
|
||||||
|
color: #4CAF50;
|
||||||
|
margin-left: auto;
|
||||||
|
padding-left: 8px;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.health-latency.offline {
|
||||||
|
color: var(--danger-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tabs */
|
||||||
|
.tab-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
border-bottom: 2px solid var(--border-color);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 10px 18px;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
transition: color 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn:hover {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn.active {
|
||||||
|
color: var(--primary-color);
|
||||||
|
border-bottom-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-panel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-panel.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme Toggle */
|
||||||
|
.theme-toggle {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.app-footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.footer-content strong {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
text-decoration: none;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-content a:hover {
|
||||||
|
opacity: 0.8;
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
450
server/src/wled_controller/static/css/modal.css
Normal file
450
server/src/wled_controller/static/css/modal.css
Normal file
@@ -0,0 +1,450 @@
|
|||||||
|
/* Modal Styles */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
z-index: 2000;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
animation: fadeIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 12px;
|
||||||
|
max-width: 500px;
|
||||||
|
width: 90%;
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
|
||||||
|
animation: slideUp 0.3s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
#template-modal .modal-content {
|
||||||
|
max-width: 500px !important;
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#test-template-modal .modal-content {
|
||||||
|
max-width: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from {
|
||||||
|
transform: translateY(20px);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding: 24px 24px 16px 24px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.modal-header-btn:hover {
|
||||||
|
background: rgba(128, 128, 128, 0.15);
|
||||||
|
}
|
||||||
|
.modal-header-btn:disabled {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #777;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: color 0.2s, background 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close-btn:hover {
|
||||||
|
color: var(--text-color);
|
||||||
|
background: rgba(128, 128, 128, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-description {
|
||||||
|
color: #999;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-input-wrapper input {
|
||||||
|
flex: 1;
|
||||||
|
padding-right: 45px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-toggle {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
padding: 8px;
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-toggle:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label-row label {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-toggle {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--text-secondary, #888);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0.6;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-toggle:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-toggle.active {
|
||||||
|
opacity: 1;
|
||||||
|
color: var(--primary-color, #4CAF50);
|
||||||
|
border-color: var(--primary-color, #4CAF50);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-hint {
|
||||||
|
display: block;
|
||||||
|
margin: 0 0 6px 0;
|
||||||
|
color: #666;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fps-hint {
|
||||||
|
display: block;
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--info-color, #2196F3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-row input[type="range"] {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-value {
|
||||||
|
min-width: 28px;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
background: rgba(244, 67, 54, 0.1);
|
||||||
|
border: 1px solid var(--danger-color);
|
||||||
|
color: var(--danger-color);
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 15px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
padding: 16px 24px 24px 24px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer .btn-icon {
|
||||||
|
min-width: 60px;
|
||||||
|
padding: 10px 20px;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-sm {
|
||||||
|
padding: 4px 10px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
min-width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-display-picker {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
transition: border-color 0.2s, box-shadow 0.2s;
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-display-picker:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content-wide {
|
||||||
|
width: fit-content;
|
||||||
|
min-width: 500px;
|
||||||
|
max-width: calc(100vw - 40px);
|
||||||
|
max-height: calc(100vh - 40px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content-wide .modal-body {
|
||||||
|
overflow-y: auto;
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.modal-content {
|
||||||
|
width: 95%;
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer .btn {
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Image Lightbox */
|
||||||
|
.lightbox {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.92);
|
||||||
|
z-index: 10000;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
cursor: zoom-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox.active {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-content {
|
||||||
|
position: relative;
|
||||||
|
max-width: 95%;
|
||||||
|
max-height: 95%;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-content img {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 90vh;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-close {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
right: 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background 0.2s;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-close:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-refresh-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 16px;
|
||||||
|
right: 64px;
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background 0.2s;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-refresh-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-refresh-btn.active {
|
||||||
|
background: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-stats {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 8px;
|
||||||
|
left: 8px;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
color: white;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-stats .stat-item {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-stats .stat-item span {
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lightbox-stats .stat-item strong {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Display Picker Lightbox */
|
||||||
|
.display-picker-content {
|
||||||
|
max-width: 900px;
|
||||||
|
width: 90%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-picker-title {
|
||||||
|
color: white;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.display-picker-canvas {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border: 2px dashed rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 24px;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-display-pickable {
|
||||||
|
cursor: pointer !important;
|
||||||
|
border: 2px solid var(--border-color) !important;
|
||||||
|
background: linear-gradient(135deg, rgba(128, 128, 128, 0.08), rgba(128, 128, 128, 0.03)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-display-pickable:hover {
|
||||||
|
box-shadow: 0 0 20px rgba(76, 175, 80, 0.4);
|
||||||
|
border-color: var(--primary-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.layout-display-pickable.selected {
|
||||||
|
border-color: var(--primary-color) !important;
|
||||||
|
box-shadow: 0 0 16px rgba(76, 175, 80, 0.5);
|
||||||
|
background: rgba(76, 175, 80, 0.12) !important;
|
||||||
|
}
|
||||||
337
server/src/wled_controller/static/css/patterns.css
Normal file
337
server/src/wled_controller/static/css/patterns.css
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
/* Static image stream styles */
|
||||||
|
.image-preview-container {
|
||||||
|
text-align: center;
|
||||||
|
margin: 12px 0;
|
||||||
|
padding: 12px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
.stream-image-preview {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 200px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
.stream-image-info {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.validation-status {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
margin-top: 4px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
.validation-status.success {
|
||||||
|
color: #4caf50;
|
||||||
|
}
|
||||||
|
.validation-status.error {
|
||||||
|
color: #f44336;
|
||||||
|
}
|
||||||
|
.validation-status.loading {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
.stream-card-props {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-card-prop {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: var(--border-color);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 180px;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-card-prop-full {
|
||||||
|
max-width: 100%;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: normal;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Key Colors target styles */
|
||||||
|
.kc-rect-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kc-rect-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px;
|
||||||
|
background: var(--bg-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kc-rect-row input[type="text"] {
|
||||||
|
flex: 2;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kc-rect-row input[type="number"] {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kc-rect-row .kc-rect-remove-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #777;
|
||||||
|
font-size: 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: color 0.2s, background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kc-rect-row .kc-rect-remove-btn:hover {
|
||||||
|
color: var(--danger-color);
|
||||||
|
background: rgba(244, 67, 54, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.kc-rect-labels {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0 8px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kc-rect-labels span:first-child {
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kc-rect-labels span {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kc-rect-labels span:last-child {
|
||||||
|
width: 28px;
|
||||||
|
flex: 0 0 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kc-add-rect-btn {
|
||||||
|
width: 100%;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 6px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kc-rect-empty {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
padding: 12px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kc-color-swatches {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kc-swatch {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kc-swatch-color {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 2px solid var(--border-color);
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kc-swatch-label {
|
||||||
|
font-size: 0.6rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
max-width: 40px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.kc-no-colors {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-style: italic;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pattern Template Visual Editor */
|
||||||
|
.pattern-canvas-container {
|
||||||
|
position: relative;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--bg-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
resize: both;
|
||||||
|
width: 820px;
|
||||||
|
min-width: 400px;
|
||||||
|
max-width: 100%;
|
||||||
|
min-height: 200px;
|
||||||
|
height: 450px;
|
||||||
|
max-height: calc(100vh - 400px);
|
||||||
|
}
|
||||||
|
|
||||||
|
#pattern-canvas {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pattern-canvas-toolbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 4px;
|
||||||
|
align-items: center;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 6px;
|
||||||
|
margin-top: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pattern-canvas-toolbar .btn {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-width: 32px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 1rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pattern-bg-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pattern-bg-row select {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pattern-capture-btn {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
min-width: 36px !important;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
padding: 0 !important;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
line-height: 36px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pattern-rect-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 8px;
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pattern-rect-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
background: var(--bg-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pattern-rect-row.selected {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
background: rgba(76, 175, 80, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pattern-rect-row input[type="text"] {
|
||||||
|
flex: 2;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 4px 6px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pattern-rect-row input[type="number"] {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
width: 55px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pattern-rect-row .pattern-rect-remove-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #777;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 2px 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
transition: color 0.2s, background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pattern-rect-row .pattern-rect-remove-btn:hover {
|
||||||
|
color: var(--danger-color);
|
||||||
|
background: rgba(244, 67, 54, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pattern-rect-labels {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 0 8px;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pattern-rect-labels span:first-child {
|
||||||
|
flex: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pattern-rect-labels span {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pattern-rect-labels span:last-child {
|
||||||
|
width: 24px;
|
||||||
|
flex: 0 0 24px;
|
||||||
|
}
|
||||||
185
server/src/wled_controller/static/css/profiles.css
Normal file
185
server/src/wled_controller/static/css/profiles.css
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
/* ===== PROFILES ===== */
|
||||||
|
|
||||||
|
.badge-profile-active {
|
||||||
|
background: var(--success-color, #28a745);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-profile-inactive {
|
||||||
|
background: var(--border-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-profile-disabled {
|
||||||
|
background: var(--border-color);
|
||||||
|
color: var(--text-muted);
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-status-disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-logic-label {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
padding: 0 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Profile condition editor rows */
|
||||||
|
.profile-condition-row {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 10px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
background: var(--bg-secondary, var(--bg-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.condition-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.condition-type-label {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-remove-condition {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--danger-color, #dc3545);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.condition-fields {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.condition-field label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
margin-bottom: 3px;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.condition-field select,
|
||||||
|
.condition-field textarea {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-family: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.condition-apps {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.condition-apps-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-browse-apps {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.2s, background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-browse-apps:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
background: rgba(33, 150, 243, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-picker {
|
||||||
|
margin-top: 6px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-picker-search {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
background: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
font-family: inherit;
|
||||||
|
outline: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-picker-list {
|
||||||
|
max-height: 160px;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-picker-item {
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.1s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-picker-item:hover {
|
||||||
|
background: rgba(33, 150, 243, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-picker-item.added {
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: default;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-picker-loading {
|
||||||
|
padding: 8px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Profile target checklist */
|
||||||
|
.profile-targets-checklist {
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-target-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-target-item:hover {
|
||||||
|
background: var(--bg-secondary, var(--bg-color));
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-target-item input[type="checkbox"] {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
611
server/src/wled_controller/static/css/streams.css
Normal file
611
server/src/wled_controller/static/css/streams.css
Normal file
@@ -0,0 +1,611 @@
|
|||||||
|
/* ===========================
|
||||||
|
Capture Templates Styles
|
||||||
|
=========================== */
|
||||||
|
|
||||||
|
.templates-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
transition: box-shadow 0.2s;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card:hover {
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-template-card {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 180px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px dashed var(--border-color);
|
||||||
|
background: transparent;
|
||||||
|
transition: border-color 0.2s, background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-template-card:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
background: rgba(33, 150, 243, 0.05);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-template-icon {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 300;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-template-card:hover .add-template-icon {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-template-label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-top: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card .template-card-header {
|
||||||
|
padding-right: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-name {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-description {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-config {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-chain {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 4px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-chain-item {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
background: var(--border-color);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-chain-arrow {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-config-details {
|
||||||
|
margin: 12px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-config-details summary {
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-config-details summary:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-no-config {
|
||||||
|
margin: 12px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-table {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: 8px;
|
||||||
|
border-collapse: collapse;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-table td {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-table tr:last-child td {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-key {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
width: 1%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-value {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Engine config grid (property name left, input right) */
|
||||||
|
.config-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr;
|
||||||
|
gap: 8px 12px;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-grid-label {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-grid-value input,
|
||||||
|
.config-grid-value select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card-actions .btn:not(.btn-icon) {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-card-actions .btn-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-muted {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* PP Filter List in Template Modal */
|
||||||
|
.pp-filter-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pp-filter-empty {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
text-align: center;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px dashed var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pp-filter-card {
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
padding: 10px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pp-filter-card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pp-filter-card.expanded .pp-filter-card-header {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pp-filter-card-chevron {
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pp-filter-card-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pp-filter-card-summary {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
margin-right: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pp-filter-card-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-filter-action {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
width: 26px;
|
||||||
|
height: 26px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-filter-action:hover:not(:disabled) {
|
||||||
|
background: var(--border-color);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-filter-action:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-filter-remove:hover:not(:disabled) {
|
||||||
|
background: rgba(239, 68, 68, 0.15);
|
||||||
|
border-color: rgba(239, 68, 68, 0.4);
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pp-filter-card-options {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pp-filter-option {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pp-filter-option label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pp-filter-option input[type="range"] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pp-filter-option-bool label {
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pp-filter-option-bool input[type="checkbox"] {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
width: 34px;
|
||||||
|
min-width: 34px;
|
||||||
|
height: 18px;
|
||||||
|
background: var(--border-color);
|
||||||
|
border-radius: 9px;
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
order: 1;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pp-filter-option-bool input[type="checkbox"]::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
left: 2px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pp-filter-option-bool input[type="checkbox"]:checked {
|
||||||
|
background: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pp-filter-option-bool input[type="checkbox"]:checked::after {
|
||||||
|
transform: translateX(16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pp-filter-option-bool span {
|
||||||
|
order: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pp-add-filter-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pp-add-filter-select {
|
||||||
|
flex: 1;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--card-bg);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pp-add-filter-btn {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--card-bg);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pp-add-filter-btn:hover {
|
||||||
|
background: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Template Test Section */
|
||||||
|
.template-test-section {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-test-section h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-results-container {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-preview-section,
|
||||||
|
.test-performance-section {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-preview-section:last-child,
|
||||||
|
.test-performance-section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-preview-section h4,
|
||||||
|
.test-performance-section h4 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-preview-image {
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-preview-image img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-performance-stats {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item span {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item strong {
|
||||||
|
color: var(--text-color);
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty state */
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: 40px 20px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stream type badges */
|
||||||
|
.badge-raw {
|
||||||
|
background: #1976d2;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge-processed {
|
||||||
|
background: #7b1fa2;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stream info panel in stream selector modal */
|
||||||
|
.stream-info-panel {
|
||||||
|
padding: 4px 0 0 0;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stream sub-tabs */
|
||||||
|
.stream-tab-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
border-bottom: 2px solid var(--border-color);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-tab-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 14px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 2px solid transparent;
|
||||||
|
margin-bottom: -2px;
|
||||||
|
transition: color 0.2s, border-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-tab-btn:hover {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-tab-btn.active {
|
||||||
|
color: var(--primary-color);
|
||||||
|
border-bottom-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-tab-count {
|
||||||
|
background: var(--border-color);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 0.7rem;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-tab-btn.active .stream-tab-count {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-tab-panel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-tab-panel.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sub-tab content sections */
|
||||||
|
.subtab-section {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtab-section:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtab-section-header {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.templates-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
197
server/src/wled_controller/static/css/tutorials.css
Normal file
197
server/src/wled_controller/static/css/tutorials.css
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
/* Tutorial System */
|
||||||
|
.tutorial-trigger-btn {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid var(--primary-color);
|
||||||
|
background: transparent;
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-trigger-btn:hover {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#calibration-modal .modal-body {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-overlay {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 100;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-overlay.active {
|
||||||
|
display: block;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.7);
|
||||||
|
transition: clip-path 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-ring {
|
||||||
|
position: absolute;
|
||||||
|
border: 2px solid var(--primary-color);
|
||||||
|
border-radius: 6px;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
animation: tutorial-pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tutorial-pulse {
|
||||||
|
0%, 100% { box-shadow: 0 0 0 0 rgba(76, 175, 80, 0.6); }
|
||||||
|
50% { box-shadow: 0 0 0 6px rgba(76, 175, 80, 0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
width: 260px;
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 2px solid var(--primary-color);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
|
||||||
|
z-index: 102;
|
||||||
|
pointer-events: auto;
|
||||||
|
animation: tutorial-tooltip-in 0.25s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes tutorial-tooltip-in {
|
||||||
|
from { opacity: 0; transform: translateY(-8px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-tooltip-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-step-counter {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-close-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #777;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: color 0.2s, background 0.2s;
|
||||||
|
padding: 0;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-close-btn:hover {
|
||||||
|
color: var(--text-color);
|
||||||
|
background: rgba(128, 128, 128, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-tooltip-text {
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-tooltip-nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-prev-btn,
|
||||||
|
.tutorial-next-btn {
|
||||||
|
flex: 1;
|
||||||
|
padding: 6px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: opacity 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-prev-btn {
|
||||||
|
background: var(--border-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-next-btn {
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-prev-btn:hover:not(:disabled),
|
||||||
|
.tutorial-next-btn:hover:not(:disabled) {
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-prev-btn:disabled,
|
||||||
|
.tutorial-next-btn:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-target {
|
||||||
|
position: relative;
|
||||||
|
z-index: 101 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fixed (viewport-level) tutorial overlay for device cards */
|
||||||
|
.tutorial-overlay-fixed {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-overlay-fixed .tutorial-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-overlay-fixed .tutorial-ring {
|
||||||
|
position: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tutorial-overlay-fixed .tutorial-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 10002;
|
||||||
|
animation: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* target z-index for fixed overlay is set inline via JS (target is outside overlay DOM) */
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
276
server/src/wled_controller/templates/index.html
Normal file
276
server/src/wled_controller/templates/index.html
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>LED Grab</title>
|
||||||
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='0.9em' font-size='90'>💡</text></svg>">
|
||||||
|
<link rel="stylesheet" href="/static/css/base.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/layout.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/components.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/cards.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/modal.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/calibration.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/dashboard.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/streams.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/patterns.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/profiles.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/tutorials.css">
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4/dist/chart.umd.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body style="visibility: hidden;">
|
||||||
|
<div class="container">
|
||||||
|
<header>
|
||||||
|
<div class="header-title">
|
||||||
|
<span id="server-status" class="status-badge">●</span>
|
||||||
|
<h1 data-i18n="app.title">LED Grab</h1>
|
||||||
|
<span id="server-version"><span id="version-number"></span></span>
|
||||||
|
</div>
|
||||||
|
<div class="server-info">
|
||||||
|
<a href="/docs" target="_blank" class="header-link" data-i18n-title="app.api_docs" title="API Docs">API</a>
|
||||||
|
<button class="theme-toggle" onclick="toggleTheme()" data-i18n-title="theme.toggle" title="Toggle theme">
|
||||||
|
<span id="theme-icon">🌙</span>
|
||||||
|
</button>
|
||||||
|
<select id="locale-select" onchange="changeLocale()" data-i18n-title="locale.change" title="Change language" style="margin-left: 10px; padding: 6px 12px; border: 1px solid var(--border-color); border-radius: 4px; background: var(--bg-color); color: var(--text-color); font-size: 0.9rem; cursor: pointer;">
|
||||||
|
<option value="en">English</option>
|
||||||
|
<option value="ru">Русский</option>
|
||||||
|
</select>
|
||||||
|
<button id="login-btn" class="btn btn-primary" onclick="showLogin()" style="display: none; padding: 6px 16px; font-size: 0.85rem; margin-left: 10px;">
|
||||||
|
🔑 <span data-i18n="auth.login">Login</span>
|
||||||
|
</button>
|
||||||
|
<button id="logout-btn" class="btn btn-danger" onclick="logout()" style="display: none; padding: 6px 16px; font-size: 0.85rem; margin-left: 10px;">
|
||||||
|
🚪
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="tabs">
|
||||||
|
<div class="tab-bar">
|
||||||
|
<button class="tab-btn" data-tab="dashboard" onclick="switchTab('dashboard')"><span data-i18n="dashboard.title">📊 Dashboard</span></button>
|
||||||
|
<button class="tab-btn" data-tab="profiles" onclick="switchTab('profiles')"><span data-i18n="profiles.title">📋 Profiles</span></button>
|
||||||
|
<button class="tab-btn" data-tab="targets" onclick="switchTab('targets')"><span data-i18n="targets.title">⚡ Targets</span></button>
|
||||||
|
<button class="tab-btn" data-tab="streams" onclick="switchTab('streams')"><span data-i18n="streams.title">📺 Sources</span></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-panel" id="tab-dashboard">
|
||||||
|
<div id="dashboard-content">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-panel" id="tab-profiles">
|
||||||
|
<div id="profiles-content">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-panel" id="tab-targets">
|
||||||
|
<div id="targets-panel-content">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tab-panel" id="tab-streams">
|
||||||
|
<div id="streams-list">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Apply saved tab immediately during parse to prevent visible jump
|
||||||
|
(function() {
|
||||||
|
var saved = localStorage.getItem('activeTab');
|
||||||
|
if (saved === 'devices') saved = 'targets';
|
||||||
|
if (!saved || !document.getElementById('tab-' + saved)) saved = 'dashboard';
|
||||||
|
document.querySelectorAll('.tab-btn').forEach(function(btn) { btn.classList.toggle('active', btn.dataset.tab === saved); });
|
||||||
|
document.querySelectorAll('.tab-panel').forEach(function(panel) { panel.classList.toggle('active', panel.id === 'tab-' + saved); });
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="app-footer">
|
||||||
|
<div class="footer-content">
|
||||||
|
<p>
|
||||||
|
Created by <strong>Alexei Dolgolyov</strong>
|
||||||
|
• <a href="mailto:dolgolyov.alexei@gmail.com">dolgolyov.alexei@gmail.com</a>
|
||||||
|
• <a href="https://git.dolgolyov-family.by/alexei.dolgolyov/wled-screen-controller-mixed" target="_blank" rel="noopener">Source Code</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="toast" class="toast"></div>
|
||||||
|
|
||||||
|
{% include 'modals/calibration.html' %}
|
||||||
|
{% include 'modals/device-settings.html' %}
|
||||||
|
{% include 'modals/target-editor.html' %}
|
||||||
|
{% include 'modals/kc-editor.html' %}
|
||||||
|
{% include 'modals/pattern-template.html' %}
|
||||||
|
{% include 'modals/api-key.html' %}
|
||||||
|
{% include 'modals/confirm.html' %}
|
||||||
|
{% include 'modals/add-device.html' %}
|
||||||
|
{% include 'modals/capture-template.html' %}
|
||||||
|
{% include 'modals/test-template.html' %}
|
||||||
|
{% include 'modals/test-stream.html' %}
|
||||||
|
{% include 'modals/test-pp-template.html' %}
|
||||||
|
{% include 'modals/stream.html' %}
|
||||||
|
{% include 'modals/pp-template.html' %}
|
||||||
|
{% include 'modals/profile-editor.html' %}
|
||||||
|
|
||||||
|
{% include 'partials/tutorial-overlay.html' %}
|
||||||
|
{% include 'partials/image-lightbox.html' %}
|
||||||
|
{% include 'partials/display-picker.html' %}
|
||||||
|
|
||||||
|
<script type="module" src="/static/js/app.js"></script>
|
||||||
|
<script>
|
||||||
|
// Initialize theme
|
||||||
|
const savedTheme = localStorage.getItem('theme') || 'dark';
|
||||||
|
document.documentElement.setAttribute('data-theme', savedTheme);
|
||||||
|
updateThemeIcon(savedTheme);
|
||||||
|
|
||||||
|
function updateThemeIcon(theme) {
|
||||||
|
const icon = document.getElementById('theme-icon');
|
||||||
|
icon.textContent = theme === 'dark' ? '☀️' : '🌙';
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleTheme() {
|
||||||
|
const currentTheme = document.documentElement.getAttribute('data-theme');
|
||||||
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||||
|
|
||||||
|
document.documentElement.setAttribute('data-theme', newTheme);
|
||||||
|
localStorage.setItem('theme', newTheme);
|
||||||
|
updateThemeIcon(newTheme);
|
||||||
|
showToast(`Switched to ${newTheme} theme`, 'info');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize auth state
|
||||||
|
function updateAuthUI() {
|
||||||
|
const apiKey = localStorage.getItem('wled_api_key');
|
||||||
|
const loginBtn = document.getElementById('login-btn');
|
||||||
|
const logoutBtn = document.getElementById('logout-btn');
|
||||||
|
const tabBar = document.querySelector('.tab-bar');
|
||||||
|
|
||||||
|
if (apiKey) {
|
||||||
|
loginBtn.style.display = 'none';
|
||||||
|
logoutBtn.style.display = 'inline-block';
|
||||||
|
if (tabBar) tabBar.style.display = '';
|
||||||
|
} else {
|
||||||
|
loginBtn.style.display = 'inline-block';
|
||||||
|
logoutBtn.style.display = 'none';
|
||||||
|
if (tabBar) tabBar.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showLogin() {
|
||||||
|
showApiKeyModal(t('auth.message'));
|
||||||
|
document.getElementById('modal-cancel-btn').style.display = 'inline-block';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
const confirmed = await showConfirm(t('auth.logout.confirm'));
|
||||||
|
if (!confirmed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.removeItem('wled_api_key');
|
||||||
|
if (window.setApiKey) window.setApiKey(null);
|
||||||
|
updateAuthUI();
|
||||||
|
showToast(t('auth.logout.success'), 'info');
|
||||||
|
|
||||||
|
// Stop background activity
|
||||||
|
if (window.stopDashboardWS) window.stopDashboardWS();
|
||||||
|
if (window.stopPerfPolling) window.stopPerfPolling();
|
||||||
|
if (window.stopUptimeTimer) window.stopUptimeTimer();
|
||||||
|
if (window.disconnectAllKCWebSockets) window.disconnectAllKCWebSockets();
|
||||||
|
|
||||||
|
// Clear all tab panels
|
||||||
|
const loginMsg = `<div class="loading">${t('auth.please_login')}</div>`;
|
||||||
|
document.getElementById('dashboard-content').innerHTML = loginMsg;
|
||||||
|
document.getElementById('profiles-content').innerHTML = loginMsg;
|
||||||
|
document.getElementById('targets-panel-content').innerHTML = loginMsg;
|
||||||
|
document.getElementById('streams-list').innerHTML = loginMsg;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize on load
|
||||||
|
updateAuthUI();
|
||||||
|
|
||||||
|
// Modal functions
|
||||||
|
function togglePasswordVisibility() {
|
||||||
|
const input = document.getElementById('api-key-input');
|
||||||
|
const button = document.querySelector('.password-toggle');
|
||||||
|
if (input.type === 'password') {
|
||||||
|
input.type = 'text';
|
||||||
|
button.textContent = '🙈';
|
||||||
|
} else {
|
||||||
|
input.type = 'password';
|
||||||
|
button.textContent = '👁️';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function showApiKeyModal(message, hideCancel = false) {
|
||||||
|
const modal = document.getElementById('api-key-modal');
|
||||||
|
const description = document.querySelector('.modal-description');
|
||||||
|
const input = document.getElementById('api-key-input');
|
||||||
|
const error = document.getElementById('api-key-error');
|
||||||
|
const cancelBtn = document.getElementById('modal-cancel-btn');
|
||||||
|
|
||||||
|
description.textContent = message || t('auth.message');
|
||||||
|
|
||||||
|
input.value = '';
|
||||||
|
input.placeholder = t('auth.placeholder');
|
||||||
|
error.style.display = 'none';
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
lockBody();
|
||||||
|
|
||||||
|
// Hide cancel button and close X if this is required login (no existing session)
|
||||||
|
cancelBtn.style.display = hideCancel ? 'none' : 'inline-block';
|
||||||
|
const closeXBtn = document.getElementById('modal-close-x-btn');
|
||||||
|
if (closeXBtn) closeXBtn.style.display = hideCancel ? 'none' : '';
|
||||||
|
|
||||||
|
// Hide login button while modal is open
|
||||||
|
document.getElementById('login-btn').style.display = 'none';
|
||||||
|
|
||||||
|
setTimeout(() => input.focus(), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeApiKeyModal() {
|
||||||
|
const modal = document.getElementById('api-key-modal');
|
||||||
|
modal.style.display = 'none';
|
||||||
|
unlockBody();
|
||||||
|
updateAuthUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitApiKey(event) {
|
||||||
|
if (event) {
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = document.getElementById('api-key-input');
|
||||||
|
const error = document.getElementById('api-key-error');
|
||||||
|
const key = input.value.trim();
|
||||||
|
|
||||||
|
if (!key) {
|
||||||
|
error.textContent = t('auth.error.required');
|
||||||
|
error.style.display = 'block';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the key
|
||||||
|
localStorage.setItem('wled_api_key', key);
|
||||||
|
if (window.setApiKey) window.setApiKey(key);
|
||||||
|
updateAuthUI();
|
||||||
|
|
||||||
|
closeApiKeyModal();
|
||||||
|
showToast(t('auth.success'), 'success');
|
||||||
|
|
||||||
|
// Reload data
|
||||||
|
loadServerInfo();
|
||||||
|
loadDisplays();
|
||||||
|
loadTargetsTab();
|
||||||
|
|
||||||
|
// Start auto-refresh
|
||||||
|
startAutoRefresh();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
89
server/src/wled_controller/templates/modals/add-device.html
Normal file
89
server/src/wled_controller/templates/modals/add-device.html
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<!-- Add Device Modal -->
|
||||||
|
<div id="add-device-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 data-i18n="devices.add">Add New Device</h2>
|
||||||
|
<div class="modal-header-actions">
|
||||||
|
<button type="button" class="modal-header-btn" id="scan-network-btn" onclick="scanForDevices()" data-i18n-title="device.scan" title="Auto Discovery">🔍</button>
|
||||||
|
<button class="modal-close-btn" onclick="closeAddDeviceModal()" title="Close">✕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div id="discovery-section" class="discovery-section" style="display: none;">
|
||||||
|
<div id="discovery-loading" class="discovery-loading" style="display: none;">
|
||||||
|
<span class="discovery-spinner"></span>
|
||||||
|
</div>
|
||||||
|
<div id="discovery-list" class="discovery-list"></div>
|
||||||
|
<div id="discovery-empty" style="display: none;">
|
||||||
|
<small data-i18n="device.scan.empty">No devices found</small>
|
||||||
|
</div>
|
||||||
|
<hr class="modal-divider">
|
||||||
|
</div>
|
||||||
|
<form id="add-device-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="device-type" data-i18n="device.type">Device Type:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="device.type.hint">Select the type of LED controller</small>
|
||||||
|
<select id="device-type" onchange="onDeviceTypeChanged()">
|
||||||
|
<option value="wled">WLED</option>
|
||||||
|
<option value="adalight">Adalight</option>
|
||||||
|
<option value="ambiled">AmbiLED</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="device-name" data-i18n="device.name">Device Name:</label>
|
||||||
|
<input type="text" id="device-name" data-i18n-placeholder="device.name.placeholder" placeholder="Living Room TV" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" id="device-url-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="device-url" id="device-url-label" data-i18n="device.url">URL:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" id="device-url-hint" data-i18n="device.url.hint">IP address or hostname of the device (e.g. http://192.168.1.100)</small>
|
||||||
|
<input type="text" id="device-url" data-i18n-placeholder="device.url.placeholder" placeholder="http://192.168.1.100" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" id="device-serial-port-group" style="display: none;">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="device-serial-port" id="device-serial-port-label" data-i18n="device.serial_port">Serial Port:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="device.serial_port.hint">Select the COM port of the Adalight device</small>
|
||||||
|
<select id="device-serial-port" onfocus="onSerialPortFocus()"></select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" id="device-led-count-group" style="display: none;">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="device-led-count" data-i18n="device.led_count">LED Count:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="device.led_count_manual.hint">Number of LEDs on the strip (must match your Arduino sketch)</small>
|
||||||
|
<input type="number" id="device-led-count" min="1" max="10000" placeholder="60" oninput="updateBaudFpsHint()">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" id="device-baud-rate-group" style="display: none;">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="device-baud-rate" data-i18n="device.baud_rate">Baud Rate:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="device.baud_rate.hint">Serial communication speed. Higher = more FPS but requires matching Arduino sketch.</small>
|
||||||
|
<select id="device-baud-rate" onchange="updateBaudFpsHint()">
|
||||||
|
<option value="115200">115200</option>
|
||||||
|
<option value="230400">230400</option>
|
||||||
|
<option value="460800">460800</option>
|
||||||
|
<option value="500000">500000</option>
|
||||||
|
<option value="921600">921600</option>
|
||||||
|
<option value="1000000">1000000</option>
|
||||||
|
<option value="1500000">1500000</option>
|
||||||
|
<option value="2000000">2000000</option>
|
||||||
|
</select>
|
||||||
|
<small id="baud-fps-hint" class="fps-hint" style="display:none"></small>
|
||||||
|
</div>
|
||||||
|
<div id="add-device-error" class="error-message" style="display: none;"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-icon btn-secondary" onclick="closeAddDeviceModal()" title="Cancel">✕</button>
|
||||||
|
<button class="btn btn-icon btn-primary" onclick="document.getElementById('add-device-form').requestSubmit()" title="Add Device">✓</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
40
server/src/wled_controller/templates/modals/api-key.html
Normal file
40
server/src/wled_controller/templates/modals/api-key.html
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<!-- Login Modal -->
|
||||||
|
<div id="api-key-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 data-i18n="auth.title">🔑 Login to LED Grab</h2>
|
||||||
|
<button class="modal-close-btn" id="modal-close-x-btn" onclick="closeApiKeyModal()" title="Close">✕</button>
|
||||||
|
</div>
|
||||||
|
<form id="api-key-form" onsubmit="submitApiKey(event)">
|
||||||
|
<div class="modal-body">
|
||||||
|
<p class="modal-description" data-i18n="auth.message">
|
||||||
|
Please enter your API key to authenticate and access the LED Grab.
|
||||||
|
</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="api-key-input" data-i18n="auth.label">API Key:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="auth.hint">Your API key will be stored securely in your browser's local storage.</small>
|
||||||
|
<div class="password-input-wrapper">
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
id="api-key-input"
|
||||||
|
data-i18n-placeholder="auth.placeholder"
|
||||||
|
placeholder="Enter your API key..."
|
||||||
|
autocomplete="off"
|
||||||
|
>
|
||||||
|
<button type="button" class="password-toggle" onclick="togglePasswordVisibility()">
|
||||||
|
👁️
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="api-key-error" class="error-message" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-icon btn-secondary" onclick="closeApiKeyModal()" id="modal-cancel-btn" title="Cancel">✕</button>
|
||||||
|
<button type="submit" class="btn btn-icon btn-primary" title="Login">✓</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
144
server/src/wled_controller/templates/modals/calibration.html
Normal file
144
server/src/wled_controller/templates/modals/calibration.html
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
<!-- Calibration Modal -->
|
||||||
|
<div id="calibration-modal" class="modal">
|
||||||
|
<div class="modal-content" style="max-width: 700px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 data-i18n="calibration.title">📐 LED Calibration</h2>
|
||||||
|
<button class="tutorial-trigger-btn" onclick="startCalibrationTutorial()" data-i18n-title="calibration.tutorial.start" title="Start tutorial">?</button>
|
||||||
|
<button class="modal-close-btn" onclick="closeCalibrationModal()" title="Close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="calibration-device-id">
|
||||||
|
<!-- Interactive Preview with integrated LED inputs and test toggles -->
|
||||||
|
<div style="margin-bottom: 12px; padding: 0 24px;">
|
||||||
|
<div class="calibration-preview">
|
||||||
|
<!-- Screen with direction toggle, total LEDs, and offset -->
|
||||||
|
<div class="preview-screen">
|
||||||
|
<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" onclick="toggleEdgeInputs()" title="Toggle edge LED inputs"><span id="cal-total-leds-inline">0</span> / <span id="cal-device-led-count-inline">0</span></div>
|
||||||
|
<div class="preview-screen-border-width">
|
||||||
|
<label for="cal-border-width" data-i18n="calibration.border_width">Border (px):</label>
|
||||||
|
<input type="number" id="cal-border-width" min="1" max="100" value="10">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edge bars with span controls and LED count inputs -->
|
||||||
|
<div class="preview-edge edge-top">
|
||||||
|
<div class="edge-span-bar" data-edge="top">
|
||||||
|
<div class="edge-span-handle edge-span-handle-start" data-edge="top" data-handle="start"></div>
|
||||||
|
<div class="edge-span-handle edge-span-handle-end" data-edge="top" data-handle="end"></div>
|
||||||
|
</div>
|
||||||
|
<input type="number" id="cal-top-leds" class="edge-led-input" min="0" value="0"
|
||||||
|
oninput="updateCalibrationPreview()">
|
||||||
|
</div>
|
||||||
|
<div class="preview-edge edge-right">
|
||||||
|
<div class="edge-span-bar" data-edge="right">
|
||||||
|
<div class="edge-span-handle edge-span-handle-start" data-edge="right" data-handle="start"></div>
|
||||||
|
<div class="edge-span-handle edge-span-handle-end" data-edge="right" data-handle="end"></div>
|
||||||
|
</div>
|
||||||
|
<input type="number" id="cal-right-leds" class="edge-led-input" min="0" value="0"
|
||||||
|
oninput="updateCalibrationPreview()">
|
||||||
|
</div>
|
||||||
|
<div class="preview-edge edge-bottom">
|
||||||
|
<div class="edge-span-bar" data-edge="bottom">
|
||||||
|
<div class="edge-span-handle edge-span-handle-start" data-edge="bottom" data-handle="start"></div>
|
||||||
|
<div class="edge-span-handle edge-span-handle-end" data-edge="bottom" data-handle="end"></div>
|
||||||
|
</div>
|
||||||
|
<input type="number" id="cal-bottom-leds" class="edge-led-input" min="0" value="0"
|
||||||
|
oninput="updateCalibrationPreview()">
|
||||||
|
</div>
|
||||||
|
<div class="preview-edge edge-left">
|
||||||
|
<div class="edge-span-bar" data-edge="left">
|
||||||
|
<div class="edge-span-handle edge-span-handle-start" data-edge="left" data-handle="start"></div>
|
||||||
|
<div class="edge-span-handle edge-span-handle-end" data-edge="left" data-handle="end"></div>
|
||||||
|
</div>
|
||||||
|
<input type="number" id="cal-left-leds" class="edge-led-input" min="0" value="0"
|
||||||
|
oninput="updateCalibrationPreview()">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Edge test toggle zones (outside container border, in tick area) -->
|
||||||
|
<div class="edge-toggle toggle-top" onclick="toggleTestEdge('top')"></div>
|
||||||
|
<div class="edge-toggle toggle-right" onclick="toggleTestEdge('right')"></div>
|
||||||
|
<div class="edge-toggle toggle-bottom" onclick="toggleTestEdge('bottom')"></div>
|
||||||
|
<div class="edge-toggle toggle-left" onclick="toggleTestEdge('left')"></div>
|
||||||
|
|
||||||
|
<!-- Corner start position buttons -->
|
||||||
|
<div class="preview-corner corner-top-left" onclick="setStartPosition('top_left')">●</div>
|
||||||
|
<div class="preview-corner corner-top-right" onclick="setStartPosition('top_right')">●</div>
|
||||||
|
<div class="preview-corner corner-bottom-left" onclick="setStartPosition('bottom_left')">●</div>
|
||||||
|
<div class="preview-corner corner-bottom-right" onclick="setStartPosition('bottom_right')">●</div>
|
||||||
|
|
||||||
|
<!-- Canvas overlay for ticks, arrows, start label -->
|
||||||
|
<canvas id="calibration-preview-canvas"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hidden selects (used by saveCalibration) -->
|
||||||
|
<div style="display: none;">
|
||||||
|
<select id="cal-start-position">
|
||||||
|
<option value="bottom_left">Bottom Left</option>
|
||||||
|
<option value="bottom_right">Bottom Right</option>
|
||||||
|
<option value="top_left">Top Left</option>
|
||||||
|
<option value="top_right">Top Right</option>
|
||||||
|
</select>
|
||||||
|
<select id="cal-layout">
|
||||||
|
<option value="clockwise">Clockwise</option>
|
||||||
|
<option value="counterclockwise">Counterclockwise</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- Offset & Skip LEDs -->
|
||||||
|
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; padding: 0 24px;">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="cal-offset" data-i18n="calibration.offset">LED Offset:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="calibration.offset.hint">Distance from physical LED 0 to the start corner (along strip direction)</small>
|
||||||
|
<input type="number" id="cal-offset" min="0" value="0" oninput="updateOffsetSkipLock(); updateCalibrationPreview()">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="cal-skip-start" data-i18n="calibration.skip_start">Skip LEDs (Start):</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="calibration.skip_start.hint">Number of LEDs to turn off at the beginning of the strip (0 = none)</small>
|
||||||
|
<input type="number" id="cal-skip-start" min="0" value="0" oninput="updateOffsetSkipLock(); updateCalibrationPreview()">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="cal-skip-end" data-i18n="calibration.skip_end">Skip LEDs (End):</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="calibration.skip_end.hint">Number of LEDs to turn off at the end of the strip (0 = none)</small>
|
||||||
|
<input type="number" id="cal-skip-end" min="0" value="0" oninput="updateOffsetSkipLock(); updateCalibrationPreview()">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tutorial Overlay -->
|
||||||
|
<div id="tutorial-overlay" class="tutorial-overlay">
|
||||||
|
<div class="tutorial-backdrop"></div>
|
||||||
|
<div class="tutorial-ring"></div>
|
||||||
|
<div class="tutorial-tooltip">
|
||||||
|
<div class="tutorial-tooltip-header">
|
||||||
|
<span class="tutorial-step-counter"></span>
|
||||||
|
<button class="tutorial-close-btn" onclick="closeTutorial()">×</button>
|
||||||
|
</div>
|
||||||
|
<p class="tutorial-tooltip-text"></p>
|
||||||
|
<div class="tutorial-tooltip-nav">
|
||||||
|
<button class="tutorial-prev-btn" onclick="tutorialPrev()">←</button>
|
||||||
|
<button class="tutorial-next-btn" onclick="tutorialNext()">→</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="calibration-error" class="error-message" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-icon btn-secondary" onclick="closeCalibrationModal()" title="Cancel">✕</button>
|
||||||
|
<button class="btn btn-icon btn-primary" onclick="saveCalibration()" title="Save">✓</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<!-- Template Modal -->
|
||||||
|
<div id="template-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 id="template-modal-title" data-i18n="templates.add">Add Capture Template</h2>
|
||||||
|
<button class="modal-close-btn" onclick="closeTemplateModal()" title="Close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="template-id">
|
||||||
|
<form id="template-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="template-name" data-i18n="templates.name">Template Name:</label>
|
||||||
|
<input type="text" id="template-name" data-i18n-placeholder="templates.name.placeholder" placeholder="My Custom Template" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="template-description" data-i18n="templates.description.label">Description (optional):</label>
|
||||||
|
<input type="text" id="template-description" data-i18n-placeholder="templates.description.placeholder" placeholder="Describe this template..." maxlength="500">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="template-engine" data-i18n="templates.engine">Capture Engine:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="templates.engine.hint">Select the screen capture technology to use</small>
|
||||||
|
<select id="template-engine" onchange="onEngineChange()" required>
|
||||||
|
</select>
|
||||||
|
<small id="engine-availability-hint" class="form-hint" style="display: none;"></small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="engine-config-section" style="display: none;">
|
||||||
|
<h3 data-i18n="templates.config">Configuration</h3>
|
||||||
|
<div id="engine-config-fields"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="template-error" class="error-message" style="display: none;"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-icon btn-secondary" onclick="closeTemplateModal()" title="Cancel">✕</button>
|
||||||
|
<button class="btn btn-icon btn-primary" onclick="saveTemplate()" title="Save">✓</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
16
server/src/wled_controller/templates/modals/confirm.html
Normal file
16
server/src/wled_controller/templates/modals/confirm.html
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!-- Confirmation Modal -->
|
||||||
|
<div id="confirm-modal" class="modal">
|
||||||
|
<div class="modal-content" style="max-width: 450px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 id="confirm-title">Confirm Action</h2>
|
||||||
|
<button class="modal-close-btn" onclick="closeConfirmModal(false)" title="Close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p id="confirm-message" class="modal-description"></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-danger" id="confirm-no-btn" onclick="closeConfirmModal(false)">No</button>
|
||||||
|
<button class="btn btn-secondary" id="confirm-yes-btn" onclick="closeConfirmModal(true)">Yes</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
<!-- General Settings Modal -->
|
||||||
|
<div id="device-settings-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 data-i18n="settings.general.title">⚙️ General Settings</h2>
|
||||||
|
<button class="modal-close-btn" onclick="closeDeviceSettingsModal()" title="Close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="device-settings-form">
|
||||||
|
<input type="hidden" id="settings-device-id">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="settings-device-name" data-i18n="device.name">Device Name:</label>
|
||||||
|
<input type="text" id="settings-device-name" data-i18n-placeholder="device.name.placeholder" placeholder="Living Room TV" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" id="settings-url-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="settings-device-url" data-i18n="device.url">URL:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="settings.url.hint">IP address or hostname of the device</small>
|
||||||
|
<input type="text" id="settings-device-url" data-i18n-placeholder="device.url.placeholder" placeholder="http://192.168.1.100" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" id="settings-serial-port-group" style="display: none;">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="settings-serial-port" data-i18n="device.serial_port">Serial Port:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="device.serial_port.hint">Select the COM port of the Adalight device</small>
|
||||||
|
<select id="settings-serial-port"></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" id="settings-led-count-group" style="display: none;">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="settings-led-count" data-i18n="device.led_count">LED Count:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="device.led_count_manual.hint">Number of LEDs on the strip (must match your Arduino sketch)</small>
|
||||||
|
<input type="number" id="settings-led-count" min="1" max="10000" oninput="updateSettingsBaudFpsHint()">
|
||||||
|
</div>
|
||||||
|
<div class="form-group" id="settings-baud-rate-group" style="display: none;">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="settings-baud-rate" data-i18n="device.baud_rate">Baud Rate:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="device.baud_rate.hint">Serial communication speed. Higher = more FPS but requires matching Arduino sketch.</small>
|
||||||
|
<select id="settings-baud-rate" onchange="updateSettingsBaudFpsHint()">
|
||||||
|
<option value="115200">115200</option>
|
||||||
|
<option value="230400">230400</option>
|
||||||
|
<option value="460800">460800</option>
|
||||||
|
<option value="500000">500000</option>
|
||||||
|
<option value="921600">921600</option>
|
||||||
|
<option value="1000000">1000000</option>
|
||||||
|
<option value="1500000">1500000</option>
|
||||||
|
<option value="2000000">2000000</option>
|
||||||
|
</select>
|
||||||
|
<small id="settings-baud-fps-hint" class="fps-hint" style="display:none"></small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="settings-health-interval" data-i18n="settings.health_interval">Health Check Interval (s):</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="settings.health_interval.hint">How often to check the device status (5-600 seconds)</small>
|
||||||
|
<input type="number" id="settings-health-interval" min="5" max="600" value="30">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group settings-toggle-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label data-i18n="settings.auto_shutdown">Auto Restore:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="settings.auto_shutdown.hint">Restore device to idle state when targets stop or server shuts down</small>
|
||||||
|
<label class="settings-toggle">
|
||||||
|
<input type="checkbox" id="settings-auto-shutdown">
|
||||||
|
<span class="settings-toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="settings-error" class="error-message" style="display: none;"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-icon btn-secondary" onclick="closeDeviceSettingsModal()" title="Cancel">✕</button>
|
||||||
|
<button class="btn btn-icon btn-primary" onclick="saveDeviceSettings()" title="Save">✓</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
80
server/src/wled_controller/templates/modals/kc-editor.html
Normal file
80
server/src/wled_controller/templates/modals/kc-editor.html
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
<!-- Key Colors Editor Modal -->
|
||||||
|
<div id="kc-editor-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 id="kc-editor-title" data-i18n="kc.add">🎨 Add Key Colors Target</h2>
|
||||||
|
<button class="modal-close-btn" onclick="closeKCEditorModal()" title="Close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="kc-editor-form">
|
||||||
|
<input type="hidden" id="kc-editor-id">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="kc-editor-name" data-i18n="kc.name">Target Name:</label>
|
||||||
|
<input type="text" id="kc-editor-name" data-i18n-placeholder="kc.name.placeholder" placeholder="My Key Colors Target" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="kc-editor-source" data-i18n="kc.source">Picture Source:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="kc.source.hint">Which picture source to extract colors from</small>
|
||||||
|
<select id="kc-editor-source"></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="kc-editor-pattern-template" data-i18n="kc.pattern_template">Pattern Template:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="kc.pattern_template.hint">Select the rectangle pattern to use for color extraction</small>
|
||||||
|
<select id="kc-editor-pattern-template"></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="kc-editor-fps" data-i18n="kc.fps">Extraction FPS:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="kc.fps.hint">How many times per second to extract colors (1-60)</small>
|
||||||
|
<div class="slider-row">
|
||||||
|
<input type="range" id="kc-editor-fps" min="1" max="60" value="10" oninput="document.getElementById('kc-editor-fps-value').textContent = this.value">
|
||||||
|
<span id="kc-editor-fps-value" class="slider-value">10</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="kc-editor-interpolation" data-i18n="kc.interpolation">Color Mode:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="kc.interpolation.hint">How to compute the key color from pixels in each rectangle</small>
|
||||||
|
<select id="kc-editor-interpolation">
|
||||||
|
<option value="average">Average</option>
|
||||||
|
<option value="median">Median</option>
|
||||||
|
<option value="dominant">Dominant</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="kc-editor-smoothing">
|
||||||
|
<span data-i18n="kc.smoothing">Smoothing:</span>
|
||||||
|
<span id="kc-editor-smoothing-value">0.3</span>
|
||||||
|
</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="kc.smoothing.hint">Temporal blending between extractions (0=none, 1=full)</small>
|
||||||
|
<input type="range" id="kc-editor-smoothing" min="0.0" max="1.0" step="0.05" value="0.3" oninput="document.getElementById('kc-editor-smoothing-value').textContent = this.value">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="kc-editor-error" class="error-message" style="display: none;"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-icon btn-secondary" onclick="closeKCEditorModal()" title="Cancel">✕</button>
|
||||||
|
<button class="btn btn-icon btn-primary" onclick="saveKCEditor()" title="Save">✓</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
<!-- Pattern Template Editor Modal -->
|
||||||
|
<div id="pattern-template-modal" class="modal">
|
||||||
|
<div class="modal-content modal-content-wide">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 id="pattern-template-title" data-i18n="pattern.add">📄 Add Pattern Template</h2>
|
||||||
|
<button class="modal-close-btn" onclick="closePatternTemplateModal()" title="Close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="pattern-template-form">
|
||||||
|
<input type="hidden" id="pattern-template-id">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="pattern-template-name" data-i18n="pattern.name">Template Name:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="pattern.name.hint">A descriptive name for this rectangle layout</small>
|
||||||
|
<input type="text" id="pattern-template-name" data-i18n-placeholder="pattern.name.placeholder" placeholder="My Pattern Template" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="pattern-template-description" data-i18n="pattern.description_label">Description (optional):</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="pattern.description.hint">Optional notes about where or how this pattern is used</small>
|
||||||
|
<input type="text" id="pattern-template-description" data-i18n-placeholder="pattern.description_placeholder" placeholder="Describe this pattern...">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Visual Editor -->
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label data-i18n="pattern.visual_editor">Visual Editor</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="pattern.visual_editor.hint">Click + buttons to add rectangles. Drag edges to resize, drag inside to move.</small>
|
||||||
|
<div class="pattern-bg-row">
|
||||||
|
<select id="pattern-bg-source"></select>
|
||||||
|
<button type="button" class="btn btn-icon btn-secondary pattern-capture-btn" onclick="capturePatternBackground()" title="Capture Background" data-i18n-title="pattern.capture_bg">📷</button>
|
||||||
|
</div>
|
||||||
|
<div class="pattern-canvas-container">
|
||||||
|
<canvas id="pattern-canvas"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Precise coordinate list -->
|
||||||
|
<div class="form-group">
|
||||||
|
<div id="pattern-rect-labels" class="pattern-rect-labels">
|
||||||
|
<span data-i18n="pattern.rect.name">Name</span>
|
||||||
|
<span data-i18n="pattern.rect.x">X</span>
|
||||||
|
<span data-i18n="pattern.rect.y">Y</span>
|
||||||
|
<span data-i18n="pattern.rect.width">W</span>
|
||||||
|
<span data-i18n="pattern.rect.height">H</span>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
<div id="pattern-rect-list" class="pattern-rect-list"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pattern-template-error" class="error-message" style="display: none;"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-icon btn-secondary" onclick="closePatternTemplateModal()" title="Cancel">✕</button>
|
||||||
|
<button class="btn btn-icon btn-primary" onclick="savePatternTemplate()" title="Save">✓</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
40
server/src/wled_controller/templates/modals/pp-template.html
Normal file
40
server/src/wled_controller/templates/modals/pp-template.html
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<!-- Processing Template Modal -->
|
||||||
|
<div id="pp-template-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 id="pp-template-modal-title" data-i18n="postprocessing.add">Add Processing Template</h2>
|
||||||
|
<button class="modal-close-btn" onclick="closePPTemplateModal()" title="Close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="pp-template-id">
|
||||||
|
<form id="pp-template-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="pp-template-name" data-i18n="postprocessing.name">Template Name:</label>
|
||||||
|
<input type="text" id="pp-template-name" data-i18n-placeholder="postprocessing.name.placeholder" placeholder="My Processing Template" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Dynamic filter list -->
|
||||||
|
<div id="pp-filter-list" class="pp-filter-list"></div>
|
||||||
|
|
||||||
|
<!-- Add filter control -->
|
||||||
|
<div class="pp-add-filter-row">
|
||||||
|
<select id="pp-add-filter-select" class="pp-add-filter-select">
|
||||||
|
<option value="" data-i18n="filters.select_type">Select filter type...</option>
|
||||||
|
</select>
|
||||||
|
<button type="button" class="pp-add-filter-btn" onclick="addFilterFromSelect()" title="Add Filter">+</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="pp-template-description" data-i18n="postprocessing.description_label">Description (optional):</label>
|
||||||
|
<input type="text" id="pp-template-description" data-i18n-placeholder="postprocessing.description_placeholder" placeholder="Describe this template...">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="pp-template-error" class="error-message" style="display: none;"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-icon btn-secondary" onclick="closePPTemplateModal()" title="Cancel">✕</button>
|
||||||
|
<button class="btn btn-icon btn-primary" onclick="savePPTemplate()" title="Save">✓</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
<!-- Profile Editor Modal -->
|
||||||
|
<div id="profile-editor-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 id="profile-editor-title" data-i18n="profiles.add">📋 Add Profile</h2>
|
||||||
|
<button class="modal-close-btn" onclick="closeProfileEditorModal()" title="Close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="profile-editor-form">
|
||||||
|
<input type="hidden" id="profile-editor-id">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="profile-editor-name" data-i18n="profiles.name">Name:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="profiles.name.hint">A descriptive name for this profile</small>
|
||||||
|
<input type="text" id="profile-editor-name" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group settings-toggle-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label data-i18n="profiles.enabled">Enabled:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="profiles.enabled.hint">Disabled profiles won't activate even when conditions are met</small>
|
||||||
|
<label class="settings-toggle">
|
||||||
|
<input type="checkbox" id="profile-editor-enabled" checked>
|
||||||
|
<span class="settings-toggle-slider"></span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="profile-editor-logic" data-i18n="profiles.condition_logic">Condition Logic:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="profiles.condition_logic.hint">How multiple conditions are combined: ANY (OR) or ALL (AND)</small>
|
||||||
|
<select id="profile-editor-logic">
|
||||||
|
<option value="or" data-i18n="profiles.condition_logic.or">Any condition (OR)</option>
|
||||||
|
<option value="and" data-i18n="profiles.condition_logic.and">All conditions (AND)</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label data-i18n="profiles.conditions">Conditions:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="profiles.conditions.hint">Rules that determine when this profile activates</small>
|
||||||
|
<div id="profile-conditions-list"></div>
|
||||||
|
<button type="button" class="btn btn-secondary btn-sm" onclick="addProfileCondition()" style="margin-top: 6px;">
|
||||||
|
+ <span data-i18n="profiles.conditions.add">Add Condition</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label data-i18n="profiles.targets">Targets:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="profiles.targets.hint">Targets to start when this profile activates</small>
|
||||||
|
<div id="profile-targets-list" class="profile-targets-checklist"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="profile-editor-error" class="error-message" style="display: none;"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-icon btn-secondary" onclick="closeProfileEditorModal()" title="Cancel">✕</button>
|
||||||
|
<button class="btn btn-icon btn-primary" onclick="saveProfileEditor()" title="Save">✓</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
102
server/src/wled_controller/templates/modals/stream.html
Normal file
102
server/src/wled_controller/templates/modals/stream.html
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<!-- Source Modal -->
|
||||||
|
<div id="stream-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 id="stream-modal-title" data-i18n="streams.add">Add Source</h2>
|
||||||
|
<button class="modal-close-btn" onclick="closeStreamModal()" title="Close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="stream-id">
|
||||||
|
<form id="stream-form">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="stream-name" data-i18n="streams.name">Source Name:</label>
|
||||||
|
<input type="text" id="stream-name" data-i18n-placeholder="streams.name.placeholder" placeholder="My Source" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input type="hidden" id="stream-type" value="raw">
|
||||||
|
|
||||||
|
<!-- Raw source fields -->
|
||||||
|
<div id="stream-raw-fields">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label data-i18n="streams.display">Display:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="streams.display.hint">Which screen to capture</small>
|
||||||
|
<input type="hidden" id="stream-display-index" value="">
|
||||||
|
<button type="button" class="btn btn-display-picker" id="stream-display-picker-btn" onclick="openDisplayPicker(onStreamDisplaySelected, document.getElementById('stream-display-index').value)">
|
||||||
|
<span id="stream-display-picker-label" data-i18n="displays.picker.select">Select display...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="stream-capture-template" data-i18n="streams.capture_template">Capture Template:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="streams.capture_template.hint">Engine template defining how the screen is captured</small>
|
||||||
|
<select id="stream-capture-template"></select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="stream-target-fps" data-i18n="streams.target_fps">Target FPS:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="streams.target_fps.hint">Target frames per second for capture (10-90)</small>
|
||||||
|
<div class="slider-row">
|
||||||
|
<input type="range" id="stream-target-fps" min="10" max="90" value="30" oninput="document.getElementById('stream-target-fps-value').textContent = this.value">
|
||||||
|
<span id="stream-target-fps-value" class="slider-value">30</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Processed source fields -->
|
||||||
|
<div id="stream-processed-fields" style="display: none;">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="stream-source" data-i18n="streams.source">Source:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="streams.source.hint">The source to apply processing filters to</small>
|
||||||
|
<select id="stream-source"></select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="stream-pp-template" data-i18n="streams.pp_template">Processing Template:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="streams.pp_template.hint">Filter template to apply to the source</small>
|
||||||
|
<select id="stream-pp-template"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Static image fields -->
|
||||||
|
<div id="stream-static-image-fields" style="display: none;">
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="stream-image-source" data-i18n="streams.image_source">Image Source:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="streams.image_source.hint">Enter a URL (http/https) or local file path to an image</small>
|
||||||
|
<input type="text" id="stream-image-source" data-i18n-placeholder="streams.image_source.placeholder" placeholder="https://example.com/image.jpg or C:\path\to\image.png">
|
||||||
|
</div>
|
||||||
|
<div id="stream-image-preview-container" class="image-preview-container" style="display: none;">
|
||||||
|
<img id="stream-image-preview" class="stream-image-preview" src="" alt="Preview">
|
||||||
|
<div id="stream-image-info" class="stream-image-info"></div>
|
||||||
|
</div>
|
||||||
|
<div id="stream-image-validation-status" class="validation-status" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="stream-description" data-i18n="streams.description_label">Description (optional):</label>
|
||||||
|
<input type="text" id="stream-description" data-i18n-placeholder="streams.description_placeholder" placeholder="Describe this source...">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="stream-error" class="error-message" style="display: none;"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-icon btn-secondary" onclick="closeStreamModal()" title="Cancel">✕</button>
|
||||||
|
<button class="btn btn-icon btn-primary" onclick="saveStream()" title="Save">✓</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
<!-- Target Editor Modal (name, device, source, settings) -->
|
||||||
|
<div id="target-editor-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 id="target-editor-title" data-i18n="targets.add">🎯 Add Target</h2>
|
||||||
|
<button class="modal-close-btn" onclick="closeTargetEditorModal()" title="Close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="target-editor-form">
|
||||||
|
<input type="hidden" id="target-editor-id">
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="target-editor-name" data-i18n="targets.name">Target Name:</label>
|
||||||
|
<input type="text" id="target-editor-name" data-i18n-placeholder="targets.name.placeholder" placeholder="My Target" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="target-editor-device" data-i18n="targets.device">Device:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="targets.device.hint">Select the LED device to send data to</small>
|
||||||
|
<select id="target-editor-device"></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="target-editor-source" data-i18n="targets.source">Picture Source:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="targets.source.hint">Select a source that defines what to capture</small>
|
||||||
|
<select id="target-editor-source"></select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="target-editor-fps" data-i18n="targets.fps">Target FPS:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="targets.fps.hint">Target frames per second for capture and LED updates (10-90)</small>
|
||||||
|
<div class="slider-row">
|
||||||
|
<input type="range" id="target-editor-fps" min="10" max="90" value="30" oninput="document.getElementById('target-editor-fps-value').textContent = this.value">
|
||||||
|
<span id="target-editor-fps-value" class="slider-value">30</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="target-editor-interpolation" data-i18n="targets.interpolation">Interpolation Mode:</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="targets.interpolation.hint">How to calculate LED color from sampled pixels</small>
|
||||||
|
<select id="target-editor-interpolation">
|
||||||
|
<option value="average">Average</option>
|
||||||
|
<option value="median">Median</option>
|
||||||
|
<option value="dominant">Dominant</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="target-editor-smoothing">
|
||||||
|
<span data-i18n="targets.smoothing">Smoothing:</span>
|
||||||
|
<span id="target-editor-smoothing-value">0.3</span>
|
||||||
|
</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="targets.smoothing.hint">Temporal blending between frames (0=none, 1=full). Reduces flicker.</small>
|
||||||
|
<input type="range" id="target-editor-smoothing" min="0.0" max="1.0" step="0.05" value="0.3" oninput="document.getElementById('target-editor-smoothing-value').textContent = this.value">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" id="target-editor-standby-group">
|
||||||
|
<div class="label-row">
|
||||||
|
<label for="target-editor-standby-interval">
|
||||||
|
<span data-i18n="targets.standby_interval">Standby Interval:</span>
|
||||||
|
<span id="target-editor-standby-interval-value">1.0</span><span>s</span>
|
||||||
|
</label>
|
||||||
|
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
|
||||||
|
</div>
|
||||||
|
<small class="input-hint" style="display:none" data-i18n="targets.standby_interval.hint">How often to resend the last frame when the screen is static, to keep the device in live mode (0.5-5.0s)</small>
|
||||||
|
<input type="range" id="target-editor-standby-interval" min="0.5" max="5.0" step="0.5" value="1.0" oninput="document.getElementById('target-editor-standby-interval-value').textContent = this.value">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="target-editor-error" class="error-message" style="display: none;"></div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button class="btn btn-icon btn-secondary" onclick="closeTargetEditorModal()" title="Cancel">✕</button>
|
||||||
|
<button class="btn btn-icon btn-primary" onclick="saveTargetEditor()" title="Save">✓</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<!-- Test PP Template Modal -->
|
||||||
|
<div id="test-pp-template-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 data-i18n="postprocessing.test.title">Test Processing Template</h2>
|
||||||
|
<button class="modal-close-btn" onclick="closeTestPPTemplateModal()" title="Close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label data-i18n="postprocessing.test.source_stream">Source:</label>
|
||||||
|
<select id="test-pp-source-stream"></select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="test-pp-duration">
|
||||||
|
<span data-i18n="streams.test.duration">Capture Duration (s):</span>
|
||||||
|
<span id="test-pp-duration-value">5</span>
|
||||||
|
</label>
|
||||||
|
<input type="range" id="test-pp-duration" min="1" max="10" step="1" value="5" oninput="updatePPTestDuration(this.value)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-primary" onclick="runPPTemplateTest()" style="margin-top: 16px;">
|
||||||
|
<span data-i18n="streams.test.run">🧪 Run</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
23
server/src/wled_controller/templates/modals/test-stream.html
Normal file
23
server/src/wled_controller/templates/modals/test-stream.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<!-- Test Source Modal -->
|
||||||
|
<div id="test-stream-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 data-i18n="streams.test.title">Test Source</h2>
|
||||||
|
<button class="modal-close-btn" onclick="closeTestStreamModal()" title="Close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="test-stream-duration">
|
||||||
|
<span data-i18n="streams.test.duration">Capture Duration (s):</span>
|
||||||
|
<span id="test-stream-duration-value">5</span>
|
||||||
|
</label>
|
||||||
|
<input type="range" id="test-stream-duration" min="1" max="10" step="1" value="5" oninput="updateStreamTestDuration(this.value)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-primary" onclick="runStreamTest()" style="margin-top: 16px;">
|
||||||
|
<span data-i18n="streams.test.run">🧪 Run</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<!-- Test Template Modal -->
|
||||||
|
<div id="test-template-modal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 data-i18n="templates.test.title">Test Capture Template</h2>
|
||||||
|
<button class="modal-close-btn" onclick="closeTestTemplateModal()" title="Close">✕</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group">
|
||||||
|
<label data-i18n="templates.test.display">Display:</label>
|
||||||
|
<input type="hidden" id="test-template-display" value="">
|
||||||
|
<button type="button" class="btn btn-display-picker" id="test-display-picker-btn" onclick="openDisplayPicker(onTestDisplaySelected, document.getElementById('test-template-display').value)">
|
||||||
|
<span id="test-display-picker-label" data-i18n="displays.picker.select">Select display...</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="test-template-duration">
|
||||||
|
<span data-i18n="templates.test.duration">Capture Duration (s):</span>
|
||||||
|
<span id="test-template-duration-value">5</span>
|
||||||
|
</label>
|
||||||
|
<input type="range" id="test-template-duration" min="1" max="10" step="1" value="5" oninput="updateCaptureDuration(this.value)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button type="button" class="btn btn-primary" onclick="runTemplateTest()" style="margin-top: 16px;">
|
||||||
|
<span data-i18n="templates.test.run">🧪 Run</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<!-- Display Picker Lightbox -->
|
||||||
|
<div id="display-picker-lightbox" class="lightbox" onclick="closeDisplayPicker(event)">
|
||||||
|
<button class="lightbox-close" onclick="closeDisplayPicker()" title="Close">✕</button>
|
||||||
|
<div class="lightbox-content display-picker-content">
|
||||||
|
<h3 class="display-picker-title" data-i18n="displays.picker.title">Select a Display</h3>
|
||||||
|
<div id="display-picker-canvas" class="display-picker-canvas">
|
||||||
|
<div class="loading-spinner"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<!-- Image Lightbox -->
|
||||||
|
<div id="image-lightbox" class="lightbox" onclick="closeLightbox(event)">
|
||||||
|
<button class="lightbox-close" onclick="closeLightbox()" title="Close">✕</button>
|
||||||
|
<button id="lightbox-auto-refresh" class="lightbox-refresh-btn" onclick="toggleKCTestAutoRefresh()" title="Auto-refresh" style="display:none">▶</button>
|
||||||
|
<div class="lightbox-content">
|
||||||
|
<img id="lightbox-image" src="" alt="Full size preview">
|
||||||
|
<div id="lightbox-stats" class="lightbox-stats" style="display: none;"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<!-- Device Tutorial Overlay (viewport-level) -->
|
||||||
|
<div id="device-tutorial-overlay" class="tutorial-overlay tutorial-overlay-fixed">
|
||||||
|
<div class="tutorial-backdrop"></div>
|
||||||
|
<div class="tutorial-ring"></div>
|
||||||
|
<div class="tutorial-tooltip">
|
||||||
|
<div class="tutorial-tooltip-header">
|
||||||
|
<span class="tutorial-step-counter"></span>
|
||||||
|
<button class="tutorial-close-btn" onclick="closeTutorial()">×</button>
|
||||||
|
</div>
|
||||||
|
<p class="tutorial-tooltip-text"></p>
|
||||||
|
<div class="tutorial-tooltip-nav">
|
||||||
|
<button class="tutorial-prev-btn" onclick="tutorialPrev()">←</button>
|
||||||
|
<button class="tutorial-next-btn" onclick="tutorialNext()">→</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -10,13 +10,11 @@ client = TestClient(app)
|
|||||||
|
|
||||||
|
|
||||||
def test_root_endpoint():
|
def test_root_endpoint():
|
||||||
"""Test root endpoint."""
|
"""Test root endpoint returns the HTML dashboard."""
|
||||||
response = client.get("/")
|
response = client.get("/")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
data = response.json()
|
assert "text/html" in response.headers["content-type"]
|
||||||
assert data["name"] == "WLED Screen Controller"
|
assert "LED Grab" in response.text
|
||||||
assert data["version"] == __version__
|
|
||||||
assert "/docs" in data["docs"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_health_check():
|
def test_health_check():
|
||||||
|
|||||||
Reference in New Issue
Block a user