155 lines
6.1 KiB
JavaScript
155 lines
6.1 KiB
JavaScript
import { useState, useEffect } from "react";
|
|
import api from "../../api/client";
|
|
|
|
function computeStepsAndNotes(stepsStr) {
|
|
if (!stepsStr || !stepsStr.trim()) return { steps: 0, totalNotes: 0 };
|
|
const tokens = stepsStr.trim().split(",");
|
|
const bellSet = new Set();
|
|
for (const token of tokens) {
|
|
for (const part of token.split("+")) {
|
|
const n = parseInt(part.trim(), 10);
|
|
if (!isNaN(n) && n >= 1 && n <= 16) bellSet.add(n);
|
|
}
|
|
}
|
|
return { steps: tokens.length, totalNotes: bellSet.size || 1 };
|
|
}
|
|
|
|
export default function SelectBuiltMelodyModal({ open, melodyId, currentMelody, onClose, onSuccess }) {
|
|
const [melodies, setMelodies] = useState([]);
|
|
const [loading, setLoading] = useState(false);
|
|
const [assigning, setAssigning] = useState(null); // id of the one being assigned
|
|
const [error, setError] = useState("");
|
|
|
|
useEffect(() => {
|
|
if (open) loadMelodies();
|
|
}, [open]);
|
|
|
|
const loadMelodies = async () => {
|
|
setLoading(true);
|
|
setError("");
|
|
try {
|
|
const data = await api.get("/builder/melodies");
|
|
// Only show those with a built binary
|
|
setMelodies((data.melodies || []).filter((m) => m.binary_path));
|
|
} catch (err) {
|
|
setError(err.message);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleSelect = async (builtMelody) => {
|
|
setAssigning(builtMelody.id);
|
|
setError("");
|
|
try {
|
|
// 1. Fetch the .bsm file from the builder endpoint
|
|
const token = localStorage.getItem("access_token");
|
|
const res = await fetch(`/api${builtMelody.binary_url}`, {
|
|
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
});
|
|
if (!res.ok) throw new Error(`Failed to download binary: ${res.statusText}`);
|
|
const blob = await res.blob();
|
|
const file = new File([blob], `${builtMelody.name}.bsm`, { type: "application/octet-stream" });
|
|
|
|
// 2. Upload to Firebase Storage via the existing melody upload endpoint
|
|
await api.upload(`/melodies/${melodyId}/upload/binary`, file);
|
|
|
|
// 3. Mark this built melody as assigned to this Firestore melody
|
|
await api.post(`/builder/melodies/${builtMelody.id}/assign?firestore_melody_id=${melodyId}`);
|
|
|
|
// 4. Update the melody's information with archetype_csv, steps, and totalNotes
|
|
const csv = builtMelody.steps || "";
|
|
const { steps, totalNotes } = computeStepsAndNotes(csv);
|
|
if (currentMelody && csv) {
|
|
const existingInfo = currentMelody.information || {};
|
|
await api.put(`/melodies/${melodyId}`, {
|
|
information: {
|
|
...existingInfo,
|
|
archetype_csv: csv,
|
|
steps,
|
|
totalNotes,
|
|
},
|
|
default_settings: currentMelody.default_settings,
|
|
type: currentMelody.type,
|
|
url: currentMelody.url,
|
|
uid: currentMelody.uid,
|
|
pid: currentMelody.pid,
|
|
});
|
|
}
|
|
|
|
onSuccess({ name: builtMelody.name, pid: builtMelody.pid, steps, totalNotes, archetype_csv: csv });
|
|
} catch (err) {
|
|
setError(err.message);
|
|
} finally {
|
|
setAssigning(null);
|
|
}
|
|
};
|
|
|
|
if (!open) return null;
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 flex items-center justify-center z-50"
|
|
style={{ backgroundColor: "rgba(0,0,0,0.6)" }}
|
|
onClick={(e) => e.target === e.currentTarget && onClose()}
|
|
>
|
|
<div
|
|
className="w-full max-w-2xl rounded-lg border shadow-xl"
|
|
style={{ backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)" }}
|
|
>
|
|
<div className="flex items-center justify-between px-6 py-4 border-b" style={{ borderColor: "var(--border-primary)" }}>
|
|
<h2 className="text-lg font-semibold" style={{ color: "var(--text-heading)" }}>Select Built Melody</h2>
|
|
<button onClick={onClose} className="text-xl leading-none" style={{ color: "var(--text-muted)" }}>×</button>
|
|
</div>
|
|
|
|
<div className="p-6">
|
|
{error && (
|
|
<div className="text-sm rounded-md p-3 mb-4 border" style={{ backgroundColor: "var(--danger-bg)", borderColor: "var(--danger)", color: "var(--danger-text)" }}>
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{loading ? (
|
|
<div className="text-center py-8" style={{ color: "var(--text-muted)" }}>Loading...</div>
|
|
) : melodies.length === 0 ? (
|
|
<div className="text-center py-8 text-sm" style={{ color: "var(--text-muted)" }}>
|
|
No built binaries found. Go to <strong>Melody Builder</strong> to create one first.
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2 max-h-96 overflow-y-auto">
|
|
{melodies.map((m) => (
|
|
<div
|
|
key={m.id}
|
|
className="flex items-center justify-between rounded-lg px-4 py-3 border transition-colors"
|
|
style={{ borderColor: "var(--border-primary)", backgroundColor: "var(--bg-primary)" }}
|
|
>
|
|
<div>
|
|
<p className="text-sm font-medium" style={{ color: "var(--text-heading)" }}>{m.name}</p>
|
|
<p className="text-xs mt-0.5 font-mono" style={{ color: "var(--text-muted)" }}>
|
|
PID: {m.pid || "—"} · {m.steps?.split(",").length || 0} steps
|
|
</p>
|
|
</div>
|
|
<button
|
|
onClick={() => handleSelect(m)}
|
|
disabled={Boolean(assigning)}
|
|
className="px-3 py-1.5 text-xs rounded-md disabled:opacity-50 transition-colors font-medium"
|
|
style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)" }}
|
|
>
|
|
{assigning === m.id ? "Uploading..." : "Select & Upload"}
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className="flex justify-end px-6 py-4 border-t" style={{ borderColor: "var(--border-primary)" }}>
|
|
<button onClick={onClose} className="px-4 py-2 text-sm rounded-md transition-colors" style={{ backgroundColor: "var(--bg-card-hover)", color: "var(--text-primary)" }}>
|
|
Cancel
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|