Reusable gradient definitions with built-in presets (rainbow, sunset, ocean, etc.) and user-created gradients. Includes model, JSON store, Pydantic schemas, REST routes (list/create/update/clone/delete), and backup/restore integration.
154 lines
5.4 KiB
Python
154 lines
5.4 KiB
Python
"""Gradient routes: CRUD for reusable gradient definitions."""
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
|
|
from wled_controller.api.auth import AuthRequired
|
|
from wled_controller.api.dependencies import (
|
|
fire_entity_event,
|
|
get_color_strip_store,
|
|
get_gradient_store,
|
|
)
|
|
from wled_controller.api.schemas.gradients import (
|
|
GradientCreate,
|
|
GradientListResponse,
|
|
GradientResponse,
|
|
GradientUpdate,
|
|
)
|
|
from wled_controller.storage.gradient import Gradient
|
|
from wled_controller.storage.gradient_store import GradientStore
|
|
from wled_controller.storage.color_strip_store import ColorStripStore
|
|
from wled_controller.storage.base_store import EntityNotFoundError
|
|
from wled_controller.utils import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def _to_response(gradient: Gradient) -> GradientResponse:
|
|
return GradientResponse(
|
|
id=gradient.id,
|
|
name=gradient.name,
|
|
stops=[{"position": s["position"], "color": s["color"]} for s in gradient.stops],
|
|
is_builtin=gradient.is_builtin,
|
|
description=gradient.description,
|
|
tags=gradient.tags,
|
|
created_at=gradient.created_at,
|
|
updated_at=gradient.updated_at,
|
|
)
|
|
|
|
|
|
@router.get("/api/v1/gradients", response_model=GradientListResponse, tags=["Gradients"])
|
|
async def list_gradients(
|
|
_auth: AuthRequired,
|
|
store: GradientStore = Depends(get_gradient_store),
|
|
):
|
|
"""List all gradients (built-in + user-created)."""
|
|
gradients = store.get_all_gradients()
|
|
return GradientListResponse(
|
|
gradients=[_to_response(g) for g in gradients],
|
|
count=len(gradients),
|
|
)
|
|
|
|
|
|
@router.post("/api/v1/gradients", response_model=GradientResponse, status_code=201, tags=["Gradients"])
|
|
async def create_gradient(
|
|
data: GradientCreate,
|
|
_auth: AuthRequired,
|
|
store: GradientStore = Depends(get_gradient_store),
|
|
):
|
|
"""Create a new user-defined gradient."""
|
|
try:
|
|
gradient = store.create_gradient(
|
|
name=data.name,
|
|
stops=[s.model_dump() for s in data.stops],
|
|
description=data.description,
|
|
tags=data.tags,
|
|
)
|
|
fire_entity_event("gradient", "created", gradient.id)
|
|
return _to_response(gradient)
|
|
except ValueError as e:
|
|
raise HTTPException(status_code=400, detail=str(e))
|
|
|
|
|
|
@router.get("/api/v1/gradients/{gradient_id}", response_model=GradientResponse, tags=["Gradients"])
|
|
async def get_gradient(
|
|
gradient_id: str,
|
|
_auth: AuthRequired,
|
|
store: GradientStore = Depends(get_gradient_store),
|
|
):
|
|
"""Get a gradient by ID."""
|
|
try:
|
|
gradient = store.get_gradient(gradient_id)
|
|
return _to_response(gradient)
|
|
except (ValueError, EntityNotFoundError) as e:
|
|
raise HTTPException(status_code=404, detail=str(e))
|
|
|
|
|
|
@router.put("/api/v1/gradients/{gradient_id}", response_model=GradientResponse, tags=["Gradients"])
|
|
async def update_gradient(
|
|
gradient_id: str,
|
|
data: GradientUpdate,
|
|
_auth: AuthRequired,
|
|
store: GradientStore = Depends(get_gradient_store),
|
|
):
|
|
"""Update a gradient (built-in gradients are read-only)."""
|
|
try:
|
|
stops = [s.model_dump() for s in data.stops] if data.stops is not None else None
|
|
gradient = store.update_gradient(
|
|
gradient_id=gradient_id,
|
|
name=data.name,
|
|
stops=stops,
|
|
description=data.description,
|
|
tags=data.tags,
|
|
)
|
|
fire_entity_event("gradient", "updated", gradient_id)
|
|
return _to_response(gradient)
|
|
except (ValueError, EntityNotFoundError) as e:
|
|
status = 404 if "not found" in str(e).lower() else 400
|
|
raise HTTPException(status_code=status, detail=str(e))
|
|
|
|
|
|
@router.post("/api/v1/gradients/{gradient_id}/clone", response_model=GradientResponse, status_code=201, tags=["Gradients"])
|
|
async def clone_gradient(
|
|
gradient_id: str,
|
|
_auth: AuthRequired,
|
|
store: GradientStore = Depends(get_gradient_store),
|
|
):
|
|
"""Clone a gradient (useful for customizing built-in gradients)."""
|
|
try:
|
|
original = store.get_gradient(gradient_id)
|
|
clone = store.create_gradient(
|
|
name=f"{original.name} (copy)",
|
|
stops=original.stops,
|
|
description=original.description,
|
|
tags=original.tags,
|
|
)
|
|
fire_entity_event("gradient", "created", clone.id)
|
|
return _to_response(clone)
|
|
except (ValueError, EntityNotFoundError) as e:
|
|
status = 404 if "not found" in str(e).lower() else 400
|
|
raise HTTPException(status_code=status, detail=str(e))
|
|
|
|
|
|
@router.delete("/api/v1/gradients/{gradient_id}", status_code=204, tags=["Gradients"])
|
|
async def delete_gradient(
|
|
gradient_id: str,
|
|
_auth: AuthRequired,
|
|
store: GradientStore = Depends(get_gradient_store),
|
|
css_store: ColorStripStore = Depends(get_color_strip_store),
|
|
):
|
|
"""Delete a gradient (fails if built-in or referenced by sources)."""
|
|
try:
|
|
# Check references
|
|
for source in css_store.get_all_sources():
|
|
if getattr(source, "gradient_id", None) == gradient_id:
|
|
raise ValueError(
|
|
f"Cannot delete: referenced by color strip source '{source.name}'"
|
|
)
|
|
store.delete_gradient(gradient_id)
|
|
fire_entity_event("gradient", "deleted", gradient_id)
|
|
except (ValueError, EntityNotFoundError) as e:
|
|
status = 404 if "not found" in str(e).lower() else 400
|
|
raise HTTPException(status_code=status, detail=str(e))
|