UI Cleanup
This commit is contained in:
@@ -23,8 +23,6 @@ function AppFooter() {
|
|||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const [appState, setAppState] = useRuntimeState();
|
const [appState, setAppState] = useRuntimeState();
|
||||||
const [presetName, setPresetName] = useState("");
|
|
||||||
const [selectedPresetName, setSelectedPresetName] = useState("");
|
|
||||||
const [expandedLayerIds, setExpandedLayerIds] = useState([]);
|
const [expandedLayerIds, setExpandedLayerIds] = useState([]);
|
||||||
const [dragLayerId, setDragLayerId] = useState(null);
|
const [dragLayerId, setDragLayerId] = useState(null);
|
||||||
const [dropTargetLayerId, setDropTargetLayerId] = useState(null);
|
const [dropTargetLayerId, setDropTargetLayerId] = useState(null);
|
||||||
@@ -34,16 +32,8 @@ function App() {
|
|||||||
const performance = appState?.performance ?? {};
|
const performance = appState?.performance ?? {};
|
||||||
const runtime = appState?.runtime ?? {};
|
const runtime = appState?.runtime ?? {};
|
||||||
const video = appState?.video ?? {};
|
const video = appState?.video ?? {};
|
||||||
|
const videoOutput = appState?.videoOutput ?? {};
|
||||||
const app = appState?.app ?? {};
|
const app = appState?.app ?? {};
|
||||||
const stackPresets = appState?.stackPresets ?? [];
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!selectedPresetName && stackPresets.length > 0) {
|
|
||||||
setSelectedPresetName(stackPresets[0]);
|
|
||||||
} else if (selectedPresetName && !stackPresets.includes(selectedPresetName)) {
|
|
||||||
setSelectedPresetName(stackPresets[0] ?? "");
|
|
||||||
}
|
|
||||||
}, [selectedPresetName, stackPresets]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const layerIds = new Set(layers.map((layer) => layer.id));
|
const layerIds = new Set(layers.map((layer) => layer.id));
|
||||||
@@ -70,7 +60,7 @@ function App() {
|
|||||||
<header className="app-header">
|
<header className="app-header">
|
||||||
<div>
|
<div>
|
||||||
<h2>Video Shader Toys</h2>
|
<h2>Video Shader Toys</h2>
|
||||||
<p className="muted">Live shader stack, DeckLink status, and runtime controls.</p>
|
<p className="muted">Live shader stack, video IO status, and runtime controls.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={`status-pill${runtime.compileSucceeded ? " status-pill--ready" : " status-pill--error"}`}>
|
<div className={`status-pill${runtime.compileSucceeded ? " status-pill--ready" : " status-pill--error"}`}>
|
||||||
{runtime.compileSucceeded ? "Ready" : "Compile Error"}
|
{runtime.compileSucceeded ? "Ready" : "Compile Error"}
|
||||||
@@ -88,8 +78,8 @@ function App() {
|
|||||||
<dd>{layers.length}</dd>
|
<dd>{layers.length}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div className="summary-item">
|
<div className="summary-item">
|
||||||
<dt>Signal</dt>
|
<dt>Output</dt>
|
||||||
<dd>{video.hasSignal ? "Present" : "Missing"}</dd>
|
<dd>{videoOutput.enabled ? "Enabled" : "Disabled"}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div className="summary-item">
|
<div className="summary-item">
|
||||||
<dt>Render</dt>
|
<dt>Render</dt>
|
||||||
@@ -99,14 +89,8 @@ function App() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="dashboard-grid">
|
<section className="dashboard-grid">
|
||||||
<StatusPanels app={app} performance={performance} runtime={runtime} video={video} />
|
<StatusPanels app={app} performance={performance} runtime={runtime} video={video} videoOutput={videoOutput} />
|
||||||
<StackPresetToolbar
|
<StackPresetToolbar />
|
||||||
presetName={presetName}
|
|
||||||
selectedPresetName={selectedPresetName}
|
|
||||||
stackPresets={stackPresets}
|
|
||||||
onPresetNameChange={setPresetName}
|
|
||||||
onSelectedPresetNameChange={setSelectedPresetName}
|
|
||||||
/>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<LayerStack
|
<LayerStack
|
||||||
|
|||||||
@@ -1,48 +1,16 @@
|
|||||||
import { Camera, FolderOpen, RefreshCw, Save } from "lucide-react";
|
import { RefreshCw } from "lucide-react";
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import { postJson } from "../api/controlApi";
|
import { postJson } from "../api/controlApi";
|
||||||
|
|
||||||
export function StackPresetToolbar({
|
export function StackPresetToolbar() {
|
||||||
presetName,
|
|
||||||
selectedPresetName,
|
|
||||||
stackPresets,
|
|
||||||
onPresetNameChange,
|
|
||||||
onSelectedPresetNameChange,
|
|
||||||
}) {
|
|
||||||
const [screenshotQueued, setScreenshotQueued] = useState(false);
|
|
||||||
const trimmedPresetName = presetName.trim();
|
|
||||||
const normalizedPresetName = trimmedPresetName
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^a-z0-9]+/g, "-")
|
|
||||||
.replace(/^-+|-+$/g, "");
|
|
||||||
const willOverwrite = normalizedPresetName ? stackPresets.includes(normalizedPresetName) : false;
|
|
||||||
|
|
||||||
async function requestScreenshot() {
|
|
||||||
setScreenshotQueued(true);
|
|
||||||
try {
|
|
||||||
await postJson("/api/screenshot", {});
|
|
||||||
} finally {
|
|
||||||
window.setTimeout(() => setScreenshotQueued(false), 1200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="panel stack-panel">
|
<div className="panel stack-panel">
|
||||||
<div className="panel__header stack-panel__header">
|
<div className="panel__header stack-panel__header">
|
||||||
<div>
|
<div>
|
||||||
<h3>Stack presets</h3>
|
<h3>Runtime controls</h3>
|
||||||
<p className="muted">Save or recall the current layer chain.</p>
|
<p className="muted">Rescan manifests and rebuild changed active shader layers.</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="stack-panel__actions">
|
<div className="stack-panel__actions">
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="button-with-icon stack-panel__screenshot"
|
|
||||||
onClick={requestScreenshot}
|
|
||||||
>
|
|
||||||
<Camera size={16} strokeWidth={1.9} aria-hidden="true" />
|
|
||||||
<span>{screenshotQueued ? "Queued" : "Screenshot"}</span>
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="button-with-icon stack-panel__reload"
|
className="button-with-icon stack-panel__reload"
|
||||||
@@ -53,84 +21,6 @@ export function StackPresetToolbar({
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="stack-panel__grid">
|
|
||||||
<div className="toolbar__group">
|
|
||||||
<label htmlFor="preset-name">Save stack</label>
|
|
||||||
<div className="toolbar__inline">
|
|
||||||
<input
|
|
||||||
id="preset-name"
|
|
||||||
type="text"
|
|
||||||
placeholder="Preset name"
|
|
||||||
value={presetName}
|
|
||||||
onChange={(event) => onPresetNameChange(event.target.value)}
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={`button-with-icon${willOverwrite ? " stack-panel__save--overwrite" : ""}`}
|
|
||||||
disabled={!trimmedPresetName}
|
|
||||||
onClick={async () => {
|
|
||||||
if (!trimmedPresetName) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const response = await postJson("/api/stack-presets/save", { presetName: trimmedPresetName });
|
|
||||||
if (response?.ok) {
|
|
||||||
onSelectedPresetNameChange(normalizedPresetName);
|
|
||||||
onPresetNameChange("");
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Save size={16} strokeWidth={1.9} aria-hidden="true" />
|
|
||||||
<span>{willOverwrite ? "Overwrite" : "Save"}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{trimmedPresetName ? (
|
|
||||||
<p className="muted toolbar__status" role="status">
|
|
||||||
{willOverwrite
|
|
||||||
? `This will overwrite the existing preset "${normalizedPresetName}".`
|
|
||||||
: `This will save as "${normalizedPresetName}".`}
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<p className="muted toolbar__status toolbar__status--placeholder" aria-hidden="true">
|
|
||||||
Preset status
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="toolbar__group">
|
|
||||||
<label htmlFor="preset-select">Recall stack</label>
|
|
||||||
<div className="toolbar__inline">
|
|
||||||
<select
|
|
||||||
id="preset-select"
|
|
||||||
value={selectedPresetName}
|
|
||||||
onChange={(event) => onSelectedPresetNameChange(event.target.value)}
|
|
||||||
>
|
|
||||||
{stackPresets.length === 0 ? <option value="">No presets</option> : null}
|
|
||||||
{stackPresets.map((preset) => (
|
|
||||||
<option key={preset} value={preset}>
|
|
||||||
{preset}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="button-with-icon"
|
|
||||||
disabled={!selectedPresetName}
|
|
||||||
onClick={() => {
|
|
||||||
if (selectedPresetName) {
|
|
||||||
postJson("/api/stack-presets/load", { presetName: selectedPresetName });
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<FolderOpen size={16} strokeWidth={1.9} aria-hidden="true" />
|
|
||||||
<span>Recall</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<p className="muted toolbar__status toolbar__status--placeholder" aria-hidden="true">
|
|
||||||
Preset status
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,21 @@ function formatNumber(value, digits = 3) {
|
|||||||
return Number(value ?? 0).toFixed(digits);
|
return Number(value ?? 0).toFixed(digits);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function StatusPanels({ app, performance, runtime, video }) {
|
function formatEndpoint(endpoint) {
|
||||||
|
const backend = endpoint?.backend || "none";
|
||||||
|
const device = endpoint?.device ? ` ${endpoint.device}` : "";
|
||||||
|
return `${backend}${device}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatVideoMode(endpoint) {
|
||||||
|
const resolution = endpoint?.resolution || "Unknown";
|
||||||
|
const frameRate = endpoint?.frameRate ? ` ${endpoint.frameRate}` : "";
|
||||||
|
return `${resolution}${frameRate}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function StatusPanels({ app, performance, runtime, video, videoOutput }) {
|
||||||
const budgetUsedPercent = Math.max(0, Math.min(100, Number(performance.budgetUsedPercent) || 0));
|
const budgetUsedPercent = Math.max(0, Math.min(100, Number(performance.budgetUsedPercent) || 0));
|
||||||
|
const outputEnabled = Boolean(videoOutput?.enabled);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -16,8 +29,8 @@ export function StatusPanels({ app, performance, runtime, video }) {
|
|||||||
<span className={`mini-status${runtime.compileSucceeded ? " mini-status--ready" : " mini-status--error"}`}>
|
<span className={`mini-status${runtime.compileSucceeded ? " mini-status--ready" : " mini-status--error"}`}>
|
||||||
{runtime.compileSucceeded ? "Ready" : "Error"}
|
{runtime.compileSucceeded ? "Ready" : "Error"}
|
||||||
</span>
|
</span>
|
||||||
<span className={`mini-status${video.hasSignal ? " mini-status--ready" : " mini-status--error"}`}>
|
<span className={`mini-status${outputEnabled ? " mini-status--ready" : " mini-status--error"}`}>
|
||||||
{video.hasSignal ? "Signal" : "No signal"}
|
{outputEnabled ? "Output" : "No output"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -51,9 +64,13 @@ export function StatusPanels({ app, performance, runtime, video }) {
|
|||||||
<KvList
|
<KvList
|
||||||
variant="rows"
|
variant="rows"
|
||||||
values={[
|
values={[
|
||||||
["Input mode", video.modeName || "Unknown"],
|
["Input", formatEndpoint(app.input)],
|
||||||
["Input size", `${video.width || 0} x ${video.height || 0}`],
|
["Input mode", formatVideoMode(app.input)],
|
||||||
["Output", `${app.outputVideoFormat || "Unknown"}${app.outputFrameRate ? ` ${app.outputFrameRate}` : ""}`],
|
["Output", formatEndpoint(app.output)],
|
||||||
|
["Output mode", formatVideoMode(app.output)],
|
||||||
|
["Output size", `${video.width || 0} x ${video.height || 0}`],
|
||||||
|
["Output status", videoOutput?.statusMessage || "Unknown"],
|
||||||
|
["Schedule failures", `${videoOutput?.scheduleFailures ?? 0}`],
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
Reference in New Issue
Block a user