|
|
|
|
@@ -3,6 +3,55 @@ import { useNavigate } from "react-router-dom";
|
|
|
|
|
import api from "../../api/client";
|
|
|
|
|
import ConfirmDialog from "../../components/ConfirmDialog";
|
|
|
|
|
|
|
|
|
|
function CodeSnippetModal({ melody, onClose }) {
|
|
|
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
|
if (!melody) return null;
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
className="fixed inset-0 flex items-center justify-center z-50"
|
|
|
|
|
style={{ backgroundColor: "rgba(0,0,0,0.7)" }}
|
|
|
|
|
onClick={(e) => e.target === e.currentTarget && onClose()}
|
|
|
|
|
>
|
|
|
|
|
<div
|
|
|
|
|
className="w-full rounded-lg border shadow-xl"
|
|
|
|
|
style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)", maxWidth: "720px", maxHeight: "80vh", display: "flex", flexDirection: "column" }}
|
|
|
|
|
>
|
|
|
|
|
<div className="flex items-center justify-between px-6 py-4 border-b flex-shrink-0" style={{ borderColor: "var(--border-primary)" }}>
|
|
|
|
|
<div>
|
|
|
|
|
<h2 className="text-base font-semibold" style={{ color: "var(--text-heading)" }}>Firmware Code Snippet</h2>
|
|
|
|
|
<p className="text-xs mt-0.5" style={{ color: "var(--text-muted)" }}>{melody.name} · PID: <span className="font-mono">{melody.pid || "—"}</span></p>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-3">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => {
|
|
|
|
|
navigator.clipboard.writeText(melody.progmem_code).then(() => {
|
|
|
|
|
setCopied(true);
|
|
|
|
|
setTimeout(() => setCopied(false), 2000);
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
className="px-3 py-1.5 text-xs rounded transition-colors"
|
|
|
|
|
style={{
|
|
|
|
|
backgroundColor: copied ? "var(--success-bg)" : "var(--bg-card-hover)",
|
|
|
|
|
color: copied ? "var(--success-text)" : "var(--text-secondary)",
|
|
|
|
|
border: "1px solid var(--border-primary)",
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{copied ? "Copied!" : "Copy"}
|
|
|
|
|
</button>
|
|
|
|
|
<button onClick={onClose} className="text-xl leading-none" style={{ color: "var(--text-muted)" }}>×</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<pre
|
|
|
|
|
className="flex-1 overflow-auto p-5 text-xs"
|
|
|
|
|
style={{ fontFamily: "monospace", color: "var(--text-primary)", backgroundColor: "var(--bg-primary)", whiteSpace: "pre", margin: 0 }}
|
|
|
|
|
>
|
|
|
|
|
{melody.progmem_code}
|
|
|
|
|
</pre>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sectionStyle = { backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" };
|
|
|
|
|
|
|
|
|
|
export default function BuilderList() {
|
|
|
|
|
@@ -11,6 +60,8 @@ export default function BuilderList() {
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
|
const [error, setError] = useState("");
|
|
|
|
|
const [deleteTarget, setDeleteTarget] = useState(null);
|
|
|
|
|
const [deleteWarningConfirmed, setDeleteWarningConfirmed] = useState(false);
|
|
|
|
|
const [codeSnippetMelody, setCodeSnippetMelody] = useState(null);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
loadMelodies();
|
|
|
|
|
@@ -46,6 +97,22 @@ export default function BuilderList() {
|
|
|
|
|
return stepsStr.split(",").length;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleDownloadBinary = async (e, m) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
const token = localStorage.getItem("access_token");
|
|
|
|
|
const res = await fetch(`/api/builder/melodies/${m.id}/download`, {
|
|
|
|
|
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
|
|
|
});
|
|
|
|
|
if (!res.ok) return;
|
|
|
|
|
const blob = await res.blob();
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
const a = document.createElement("a");
|
|
|
|
|
a.href = url;
|
|
|
|
|
a.download = `${m.name || m.id}.bsm`;
|
|
|
|
|
a.click();
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div>
|
|
|
|
|
<div className="flex items-center justify-between mb-6">
|
|
|
|
|
@@ -54,7 +121,7 @@ export default function BuilderList() {
|
|
|
|
|
Archetype Builder
|
|
|
|
|
</h1>
|
|
|
|
|
<p className="text-sm mt-1" style={{ color: "var(--text-muted)" }}>
|
|
|
|
|
Build binary (.bsm) files and firmware PROGMEM code from melody step notation.
|
|
|
|
|
Add Archetypes, and Build binary (.bsm) files and firmware PROGMEM code.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<button
|
|
|
|
|
@@ -121,7 +188,12 @@ export default function BuilderList() {
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-4 py-3 text-center">
|
|
|
|
|
{m.binary_path ? (
|
|
|
|
|
<span className="px-2 py-0.5 text-xs rounded-full" style={{ backgroundColor: "var(--success-bg)", color: "var(--success-text)" }}>
|
|
|
|
|
<span
|
|
|
|
|
title="Click to Download"
|
|
|
|
|
onClick={(e) => handleDownloadBinary(e, m)}
|
|
|
|
|
className="px-2 py-0.5 text-xs rounded-full cursor-pointer transition-opacity hover:opacity-70"
|
|
|
|
|
style={{ backgroundColor: "var(--success-bg)", color: "var(--success-text)" }}
|
|
|
|
|
>
|
|
|
|
|
Built
|
|
|
|
|
</span>
|
|
|
|
|
) : (
|
|
|
|
|
@@ -132,7 +204,12 @@ export default function BuilderList() {
|
|
|
|
|
</td>
|
|
|
|
|
<td className="px-4 py-3 text-center">
|
|
|
|
|
{m.progmem_code ? (
|
|
|
|
|
<span className="px-2 py-0.5 text-xs rounded-full" style={{ backgroundColor: "var(--success-bg)", color: "var(--success-text)" }}>
|
|
|
|
|
<span
|
|
|
|
|
title="Click to View Code Snippet"
|
|
|
|
|
onClick={(e) => { e.stopPropagation(); setCodeSnippetMelody(m); }}
|
|
|
|
|
className="px-2 py-0.5 text-xs rounded-full cursor-pointer transition-opacity hover:opacity-70"
|
|
|
|
|
style={{ backgroundColor: "var(--success-bg)", color: "var(--success-text)" }}
|
|
|
|
|
>
|
|
|
|
|
Generated
|
|
|
|
|
</span>
|
|
|
|
|
) : (
|
|
|
|
|
@@ -169,12 +246,48 @@ export default function BuilderList() {
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* Two-stage delete: warn if assigned, then confirm */}
|
|
|
|
|
{deleteTarget && !deleteWarningConfirmed && (deleteTarget.assigned_melody_ids?.length || 0) > 0 && (
|
|
|
|
|
<div className="fixed inset-0 flex items-center justify-center z-50" style={{ backgroundColor: "rgba(0,0,0,0.6)" }}>
|
|
|
|
|
<div className="w-full max-w-md rounded-lg border shadow-xl p-6 space-y-4" style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}>
|
|
|
|
|
<h3 className="text-base font-semibold" style={{ color: "var(--text-heading)" }}>Archetype In Use</h3>
|
|
|
|
|
<div className="rounded-md p-3 border text-sm" style={{ backgroundColor: "var(--warning-bg, rgba(245,158,11,0.1))", borderColor: "var(--warning, #f59e0b)", color: "var(--warning-text, #f59e0b)" }}>
|
|
|
|
|
<strong>"{deleteTarget.name}"</strong> is currently assigned to{" "}
|
|
|
|
|
<strong>{deleteTarget.assigned_melody_ids.length} melody{deleteTarget.assigned_melody_ids.length !== 1 ? "ies" : "y"}</strong>.
|
|
|
|
|
Deleting it will remove the archetype and its binary file, but existing melodies will keep their uploaded binary.
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-sm" style={{ color: "var(--text-muted)" }}>Do you still want to delete this archetype?</p>
|
|
|
|
|
<div className="flex justify-end gap-3">
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => { setDeleteTarget(null); setDeleteWarningConfirmed(false); }}
|
|
|
|
|
className="px-4 py-2 text-sm rounded-md transition-colors"
|
|
|
|
|
style={{ backgroundColor: "var(--bg-card-hover)", color: "var(--text-primary)" }}
|
|
|
|
|
>
|
|
|
|
|
Cancel
|
|
|
|
|
</button>
|
|
|
|
|
<button
|
|
|
|
|
onClick={() => setDeleteWarningConfirmed(true)}
|
|
|
|
|
className="px-4 py-2 text-sm rounded-md transition-colors font-medium"
|
|
|
|
|
style={{ backgroundColor: "var(--danger)", color: "#fff" }}
|
|
|
|
|
>
|
|
|
|
|
Yes, Delete Anyway
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<ConfirmDialog
|
|
|
|
|
open={Boolean(deleteTarget)}
|
|
|
|
|
title="Delete Built Melody"
|
|
|
|
|
message={`Are you sure you want to delete "${deleteTarget?.name}"? This will also delete the .bsm binary file if it exists. This action cannot be undone.`}
|
|
|
|
|
open={Boolean(deleteTarget) && (deleteWarningConfirmed || (deleteTarget?.assigned_melody_ids?.length || 0) === 0)}
|
|
|
|
|
title="Delete Archetype"
|
|
|
|
|
message={`Are you sure you want to permanently delete "${deleteTarget?.name}"? This will also delete the .bsm binary file. This action cannot be undone.`}
|
|
|
|
|
onConfirm={handleDelete}
|
|
|
|
|
onCancel={() => setDeleteTarget(null)}
|
|
|
|
|
onCancel={() => { setDeleteTarget(null); setDeleteWarningConfirmed(false); }}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<CodeSnippetModal
|
|
|
|
|
melody={codeSnippetMelody}
|
|
|
|
|
onClose={() => setCodeSnippetMelody(null)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|