update: overhauled firmware ui. Added public flash page.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useNavigate, useLocation } from "react-router-dom";
|
||||
import api from "../api/client";
|
||||
|
||||
const MAX_NOTES = 16;
|
||||
@@ -106,10 +106,48 @@ function playStep(audioCtx, stepValue, noteDurationMs) {
|
||||
}
|
||||
}
|
||||
|
||||
function csvToSteps(csv) {
|
||||
if (!csv || !csv.trim()) return null;
|
||||
return csv.trim().split(",").map((token) => {
|
||||
const parts = token.split("+");
|
||||
let val = 0;
|
||||
for (const p of parts) {
|
||||
const n = parseInt(p.trim(), 10);
|
||||
if (!isNaN(n) && n >= 1 && n <= 16) val |= (1 << (n - 1));
|
||||
}
|
||||
return val;
|
||||
});
|
||||
}
|
||||
|
||||
export default function MelodyComposer() {
|
||||
const navigate = useNavigate();
|
||||
const [steps, setSteps] = useState(Array.from({ length: 16 }, () => 0));
|
||||
const [noteCount, setNoteCount] = useState(8);
|
||||
const { state: routeState } = useLocation();
|
||||
const loadedArchetype = routeState?.archetype || null;
|
||||
|
||||
const initialSteps = () => {
|
||||
if (loadedArchetype?.steps) {
|
||||
const parsed = csvToSteps(loadedArchetype.steps);
|
||||
if (parsed?.length) return parsed;
|
||||
}
|
||||
return Array.from({ length: 16 }, () => 0);
|
||||
};
|
||||
|
||||
const [steps, setSteps] = useState(initialSteps);
|
||||
const [noteCount, setNoteCount] = useState(() => {
|
||||
if (loadedArchetype?.steps) {
|
||||
const parsed = csvToSteps(loadedArchetype.steps);
|
||||
if (parsed?.length) {
|
||||
let maxBit = 0;
|
||||
for (const v of parsed) {
|
||||
for (let b = 15; b >= 0; b--) {
|
||||
if (v & (1 << b)) { maxBit = Math.max(maxBit, b + 1); break; }
|
||||
}
|
||||
}
|
||||
return Math.max(8, maxBit);
|
||||
}
|
||||
}
|
||||
return 8;
|
||||
});
|
||||
const [stepDelayMs, setStepDelayMs] = useState(280);
|
||||
const [noteDurationMs, setNoteDurationMs] = useState(110);
|
||||
const [measureEvery, setMeasureEvery] = useState(4);
|
||||
@@ -123,6 +161,7 @@ export default function MelodyComposer() {
|
||||
const [deployPid, setDeployPid] = useState("");
|
||||
const [deployError, setDeployError] = useState("");
|
||||
const [deploying, setDeploying] = useState(false);
|
||||
const [deployMode, setDeployMode] = useState("new"); // "new" | "update"
|
||||
const [noteColors, setNoteColors] = useState([]);
|
||||
const [stepMenuIndex, setStepMenuIndex] = useState(null);
|
||||
|
||||
@@ -290,10 +329,18 @@ export default function MelodyComposer() {
|
||||
scheduleStep(0);
|
||||
};
|
||||
|
||||
const openDeployModal = () => {
|
||||
const openDeployModal = (mode = "new") => {
|
||||
setError("");
|
||||
setSuccessMsg("");
|
||||
setDeployError("");
|
||||
setDeployMode(mode);
|
||||
if (mode === "update" && loadedArchetype) {
|
||||
setDeployName(loadedArchetype.name || "");
|
||||
setDeployPid(loadedArchetype.pid || "");
|
||||
} else {
|
||||
setDeployName("");
|
||||
setDeployPid("");
|
||||
}
|
||||
setShowDeployModal(true);
|
||||
};
|
||||
|
||||
@@ -306,45 +353,34 @@ export default function MelodyComposer() {
|
||||
const handleDeploy = async () => {
|
||||
const name = deployName.trim();
|
||||
const pid = deployPid.trim();
|
||||
if (!name) {
|
||||
setDeployError("Name is required.");
|
||||
return;
|
||||
}
|
||||
if (!pid) {
|
||||
setDeployError("PID is required.");
|
||||
return;
|
||||
}
|
||||
if (!name) { setDeployError("Name is required."); return; }
|
||||
if (!pid) { setDeployError("PID is required."); return; }
|
||||
|
||||
setDeploying(true);
|
||||
setDeployError("");
|
||||
setSuccessMsg("");
|
||||
try {
|
||||
const existing = await api.get("/builder/melodies");
|
||||
const list = existing.melodies || [];
|
||||
const dupName = list.find((m) => m.name.toLowerCase() === name.toLowerCase());
|
||||
if (dupName) {
|
||||
setDeployError(`An archetype with the name "${name}" already exists.`);
|
||||
return;
|
||||
}
|
||||
const dupPid = list.find((m) => m.pid && m.pid.toLowerCase() === pid.toLowerCase());
|
||||
if (dupPid) {
|
||||
setDeployError(`An archetype with the PID "${pid}" already exists.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const stepsStr = steps.map(stepToNotation).join(",");
|
||||
const created = await api.post("/builder/melodies", {
|
||||
name,
|
||||
pid,
|
||||
steps: stepsStr,
|
||||
});
|
||||
|
||||
setSuccessMsg(`Archetype "${name}" deployed successfully.`);
|
||||
setShowDeployModal(false);
|
||||
setDeployName("");
|
||||
setDeployPid("");
|
||||
if (created?.id) {
|
||||
navigate(`/melodies/archetypes/${created.id}`);
|
||||
if (deployMode === "update" && loadedArchetype?.id) {
|
||||
const updated = await api.put(`/builder/melodies/${loadedArchetype.id}`, { name, pid, steps: stepsStr });
|
||||
setSuccessMsg(`Archetype "${name}" updated successfully.`);
|
||||
setShowDeployModal(false);
|
||||
if (updated?.id) navigate(`/melodies/archetypes/${updated.id}`);
|
||||
} else {
|
||||
const existing = await api.get("/builder/melodies");
|
||||
const list = existing.melodies || [];
|
||||
const dupName = list.find((m) => m.name.toLowerCase() === name.toLowerCase());
|
||||
if (dupName) { setDeployError(`An archetype with the name "${name}" already exists.`); return; }
|
||||
const dupPid = list.find((m) => m.pid && m.pid.toLowerCase() === pid.toLowerCase());
|
||||
if (dupPid) { setDeployError(`An archetype with the PID "${pid}" already exists.`); return; }
|
||||
|
||||
const created = await api.post("/builder/melodies", { name, pid, steps: stepsStr });
|
||||
setSuccessMsg(`Archetype "${name}" deployed successfully.`);
|
||||
setShowDeployModal(false);
|
||||
setDeployName("");
|
||||
setDeployPid("");
|
||||
if (created?.id) navigate(`/melodies/archetypes/${created.id}`);
|
||||
}
|
||||
} catch (err) {
|
||||
setDeployError(err.message);
|
||||
@@ -375,6 +411,24 @@ export default function MelodyComposer() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{loadedArchetype && (
|
||||
<div className="rounded-lg border px-4 py-3 flex items-center gap-3"
|
||||
style={{ backgroundColor: "rgba(139,92,246,0.08)", borderColor: "rgba(139,92,246,0.3)" }}>
|
||||
<span className="text-xs font-semibold uppercase tracking-wide" style={{ color: "#a78bfa" }}>Editing Archetype</span>
|
||||
<span className="text-sm font-medium" style={{ color: "var(--text-heading)" }}>{loadedArchetype.name}</span>
|
||||
<span className="text-xs" style={{ color: "var(--text-muted)" }}>·</span>
|
||||
<span className="text-xs font-mono" style={{ color: "var(--text-muted)" }}>{loadedArchetype.id}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate("/melodies/composer", { replace: true, state: null })}
|
||||
className="ml-auto text-xs px-2 py-1 rounded"
|
||||
style={{ backgroundColor: "var(--bg-card-hover)", color: "var(--text-muted)", border: "1px solid var(--border-primary)" }}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div
|
||||
className="text-sm rounded-md p-3 border"
|
||||
@@ -422,7 +476,14 @@ export default function MelodyComposer() {
|
||||
)}
|
||||
<span>{steps.length} steps, {noteCount} notes</span>
|
||||
</div>
|
||||
<button type="button" onClick={openDeployModal} className="px-3 py-1.5 rounded-md text-sm whitespace-nowrap" style={{ backgroundColor: "var(--text-link)", color: "var(--text-white)" }}>Deploy Archetype</button>
|
||||
{loadedArchetype ? (
|
||||
<>
|
||||
<button type="button" onClick={() => openDeployModal("new")} className="px-3 py-1.5 rounded-md text-sm whitespace-nowrap" style={{ backgroundColor: "var(--btn-neutral)", color: "var(--text-white)" }}>Deploy as New Archetype</button>
|
||||
<button type="button" onClick={() => openDeployModal("update")} className="px-3 py-1.5 rounded-md text-sm whitespace-nowrap" style={{ backgroundColor: "var(--text-link)", color: "var(--text-white)" }}>Update Current Archetype</button>
|
||||
</>
|
||||
) : (
|
||||
<button type="button" onClick={() => openDeployModal("new")} className="px-3 py-1.5 rounded-md text-sm whitespace-nowrap" style={{ backgroundColor: "var(--text-link)", color: "var(--text-white)" }}>Deploy Archetype</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -666,10 +727,12 @@ export default function MelodyComposer() {
|
||||
>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold" style={{ color: "var(--text-heading)" }}>
|
||||
Deploy Archetype
|
||||
{deployMode === "update" ? "Update Archetype" : "Deploy Archetype"}
|
||||
</h2>
|
||||
<p className="text-xs mt-0.5" style={{ color: "var(--text-muted)" }}>
|
||||
Create a new archetype from this composer pattern.
|
||||
{deployMode === "update"
|
||||
? "Rebuild the existing archetype with the current composer pattern."
|
||||
: "Create a new archetype from this composer pattern."}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -744,7 +807,7 @@ export default function MelodyComposer() {
|
||||
style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)" }}
|
||||
disabled={deploying}
|
||||
>
|
||||
{deploying ? "Deploying..." : "Deploy"}
|
||||
{deploying ? (deployMode === "update" ? "Updating..." : "Deploying...") : (deployMode === "update" ? "Update" : "Deploy")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user