/** * LayerList Component * * Vertical list of layers with drag-to-reorder support. * Each row shows visibility toggle, lock, source name, and controls. */ import React, { useState, useCallback } from "react"; import type { Layer, LayerTransform, MediaSource } from "@livepeer-frameworks/streamcrafter-core"; import { useStudioTranslate } from "../context/StudioI18nContext"; // SVG Icons function EyeIcon() { return ( ); } function EyeOffIcon() { return ( ); } function CameraIcon() { return ( ); } function ScreenIcon() { return ( ); } function VideoIcon() { return ( ); } function GearIcon() { return ( ); } export interface LayerListProps { layers: Layer[]; sources: MediaSource[]; onVisibilityToggle: (layerId: string, visible: boolean) => void; onReorder: (layerIds: string[]) => void; onTransformEdit?: (layerId: string, transform: Partial) => void; onRemove?: (layerId: string) => void; onSelect?: (layerId: string | null) => void; selectedLayerId?: string | null; className?: string; } export function LayerList({ layers, sources, onVisibilityToggle, onReorder, onTransformEdit, onRemove, onSelect, selectedLayerId, className = "", }: LayerListProps) { const t = useStudioTranslate(); const [draggedId, setDraggedId] = useState(null); const [dragOverId, setDragOverId] = useState(null); const [editingLayerId, setEditingLayerId] = useState(null); // Sort layers by z-index (bottom to top, so higher z-index at top of list) const sortedLayers = [...layers].sort((a, b) => b.zIndex - a.zIndex); const getSourceLabel = useCallback( (sourceId: string): string => { const source = sources.find((s) => s.id === sourceId); return source?.label || sourceId; }, [sources] ); const getSourceIcon = useCallback( (sourceId: string): React.ReactNode => { const source = sources.find((s) => s.id === sourceId); switch (source?.type) { case "camera": return ; case "screen": return ; case "custom": return ; default: return ; } }, [sources] ); // Drag and drop handlers const handleDragStart = useCallback((e: React.DragEvent, layerId: string) => { setDraggedId(layerId); e.dataTransfer.effectAllowed = "move"; e.dataTransfer.setData("text/plain", layerId); }, []); const handleDragOver = useCallback((e: React.DragEvent, layerId: string) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; setDragOverId(layerId); }, []); const handleDragLeave = useCallback(() => { setDragOverId(null); }, []); const handleDrop = useCallback( (e: React.DragEvent, targetLayerId: string) => { e.preventDefault(); setDragOverId(null); if (!draggedId || draggedId === targetLayerId) { setDraggedId(null); return; } // Reorder: move dragged layer to target position const currentIds = sortedLayers.map((l) => l.id); const fromIndex = currentIds.indexOf(draggedId); const toIndex = currentIds.indexOf(targetLayerId); if (fromIndex === -1 || toIndex === -1) { setDraggedId(null); return; } // Create new order const newOrder = [...currentIds]; newOrder.splice(fromIndex, 1); newOrder.splice(toIndex, 0, draggedId); onReorder(newOrder); setDraggedId(null); }, [draggedId, sortedLayers, onReorder] ); const handleDragEnd = useCallback(() => { setDraggedId(null); setDragOverId(null); }, []); // Move layer up/down const handleMoveUp = useCallback( (layerId: string) => { const currentIds = sortedLayers.map((l) => l.id); const index = currentIds.indexOf(layerId); if (index <= 0) return; const newOrder = [...currentIds]; [newOrder[index - 1], newOrder[index]] = [newOrder[index], newOrder[index - 1]]; onReorder(newOrder); }, [sortedLayers, onReorder] ); const handleMoveDown = useCallback( (layerId: string) => { const currentIds = sortedLayers.map((l) => l.id); const index = currentIds.indexOf(layerId); if (index >= currentIds.length - 1) return; const newOrder = [...currentIds]; [newOrder[index], newOrder[index + 1]] = [newOrder[index + 1], newOrder[index]]; onReorder(newOrder); }, [sortedLayers, onReorder] ); // Opacity slider const handleOpacityChange = useCallback( (layerId: string, opacity: number) => { onTransformEdit?.(layerId, { opacity }); }, [onTransformEdit] ); return (
{t("layers")} {layers.length}
{sortedLayers.length === 0 ? (
{t("noLayers")}
) : ( sortedLayers.map((layer, index) => (
handleDragStart(e, layer.id)} onDragOver={(e) => handleDragOver(e, layer.id)} onDragLeave={handleDragLeave} onDrop={(e) => handleDrop(e, layer.id)} onDragEnd={handleDragEnd} onClick={() => onSelect?.(layer.id === selectedLayerId ? null : layer.id)} > {/* Visibility Toggle */} {/* Source Info */} {getSourceIcon(layer.sourceId)} {getSourceLabel(layer.sourceId)} {/* Opacity (shown when editing) */} {editingLayerId === layer.id && onTransformEdit && (
handleOpacityChange(layer.id, Number(e.target.value))} onClick={(e) => e.stopPropagation()} /> {Math.round(layer.transform.opacity * 100)}%
)} {/* Controls */}
{/* Move Up */} {/* Move Down */} {/* Edit Transform */} {onTransformEdit && ( )} {/* Remove */} {onRemove && ( )}
)) )}
); } export default LayerList;