CODEX - Moved the upload binary to the Archetype
This commit is contained in:
@@ -556,6 +556,10 @@ export default function MelodyDetail() {
|
|||||||
|
|
||||||
const handleDownload = async (e) => {
|
const handleDownload = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
if (binaryUrl.startsWith("http")) {
|
||||||
|
window.open(binaryUrl, "_blank", "noopener,noreferrer");
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const token = localStorage.getItem("access_token");
|
const token = localStorage.getItem("access_token");
|
||||||
let res = null;
|
let res = null;
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ export default function MelodyForm() {
|
|||||||
const [pid, setPid] = useState("");
|
const [pid, setPid] = useState("");
|
||||||
const [melodyStatus, setMelodyStatus] = useState("draft");
|
const [melodyStatus, setMelodyStatus] = useState("draft");
|
||||||
|
|
||||||
const [binaryFile, setBinaryFile] = useState(null);
|
|
||||||
const [previewFile, setPreviewFile] = useState(null);
|
const [previewFile, setPreviewFile] = useState(null);
|
||||||
const [existingFiles, setExistingFiles] = useState({});
|
const [existingFiles, setExistingFiles] = useState({});
|
||||||
const [uploading, setUploading] = useState(false);
|
const [uploading, setUploading] = useState(false);
|
||||||
@@ -120,7 +119,6 @@ export default function MelodyForm() {
|
|||||||
const [builtMelody, setBuiltMelody] = useState(null);
|
const [builtMelody, setBuiltMelody] = useState(null);
|
||||||
const [assignedBinaryName, setAssignedBinaryName] = useState(null);
|
const [assignedBinaryName, setAssignedBinaryName] = useState(null);
|
||||||
const [assignedBinaryPid, setAssignedBinaryPid] = useState(null);
|
const [assignedBinaryPid, setAssignedBinaryPid] = useState(null);
|
||||||
const binaryInputRef = useRef(null);
|
|
||||||
const previewInputRef = useRef(null);
|
const previewInputRef = useRef(null);
|
||||||
|
|
||||||
// Metadata / Admin Notes
|
// Metadata / Admin Notes
|
||||||
@@ -215,6 +213,20 @@ export default function MelodyForm() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getEffectiveBinary = () => {
|
||||||
|
const effectiveUrl = normalizeFileUrl(
|
||||||
|
(builtMelody?.binary_url ? `/api${builtMelody.binary_url}` : null) ||
|
||||||
|
url ||
|
||||||
|
existingFiles.binary_url ||
|
||||||
|
null
|
||||||
|
);
|
||||||
|
const fixedNameFromPid = builtMelody?.pid || assignedBinaryPid || pid || null;
|
||||||
|
const effectiveName = fixedNameFromPid
|
||||||
|
? `${fixedNameFromPid}.bsm`
|
||||||
|
: resolveFilename(effectiveUrl, "binary.bsm");
|
||||||
|
return { effectiveUrl, effectiveName };
|
||||||
|
};
|
||||||
|
|
||||||
const downloadExistingFile = async (fileUrl, fallbackName, e) => {
|
const downloadExistingFile = async (fileUrl, fallbackName, e) => {
|
||||||
e?.preventDefault?.();
|
e?.preventDefault?.();
|
||||||
if (!fileUrl) return;
|
if (!fileUrl) return;
|
||||||
@@ -233,7 +245,8 @@ export default function MelodyForm() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ((!res || !res.ok) && fileUrl.startsWith("http")) {
|
if ((!res || !res.ok) && fileUrl.startsWith("http")) {
|
||||||
res = await fetch(fileUrl);
|
window.open(fileUrl, "_blank", "noopener,noreferrer");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
if (!res.ok) throw new Error(`Download failed: ${res.statusText}`);
|
if (!res.ok) throw new Error(`Download failed: ${res.statusText}`);
|
||||||
const blob = await res.blob();
|
const blob = await res.blob();
|
||||||
@@ -282,9 +295,8 @@ export default function MelodyForm() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const uploadFiles = async (melodyId) => {
|
const uploadFiles = async (melodyId) => {
|
||||||
if (binaryFile || previewFile) {
|
if (previewFile) {
|
||||||
setUploading(true);
|
setUploading(true);
|
||||||
if (binaryFile) await api.upload(`/melodies/${melodyId}/upload/binary`, binaryFile);
|
|
||||||
if (previewFile) await api.upload(`/melodies/${melodyId}/upload/preview`, previewFile);
|
if (previewFile) await api.upload(`/melodies/${melodyId}/upload/preview`, previewFile);
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
}
|
}
|
||||||
@@ -749,16 +761,9 @@ export default function MelodyForm() {
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1" style={labelStyle}>Binary File (.bsm / .bin)</label>
|
<label className="block text-sm font-medium mb-1" style={labelStyle}>Binary File (.bsm)</label>
|
||||||
{(() => {
|
{(() => {
|
||||||
const binaryUrl = normalizeFileUrl(
|
const { effectiveUrl: binaryUrl, effectiveName: binaryName } = getEffectiveBinary();
|
||||||
(builtMelody?.binary_url ? `/api${builtMelody.binary_url}` : null) ||
|
|
||||||
url ||
|
|
||||||
existingFiles.binary_url ||
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const fallback = assignedBinaryPid ? `${assignedBinaryPid}.bsm` : (pid ? `${pid}.bsm` : "Click to Download");
|
|
||||||
const binaryName = resolveFilename(binaryUrl, fallback);
|
|
||||||
return (
|
return (
|
||||||
<div className="text-xs mb-2" style={{ color: "var(--text-muted)" }}>
|
<div className="text-xs mb-2" style={{ color: "var(--text-muted)" }}>
|
||||||
{binaryUrl ? (
|
{binaryUrl ? (
|
||||||
@@ -792,22 +797,7 @@ export default function MelodyForm() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
<input
|
|
||||||
ref={binaryInputRef}
|
|
||||||
type="file"
|
|
||||||
accept=".bin,.bsm"
|
|
||||||
onChange={(e) => setBinaryFile(e.target.files[0] || null)}
|
|
||||||
className="hidden"
|
|
||||||
/>
|
|
||||||
<div className="flex flex-wrap gap-2 items-center">
|
<div className="flex flex-wrap gap-2 items-center">
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => binaryInputRef.current?.click()}
|
|
||||||
className="px-3 py-1.5 text-xs rounded-md transition-colors"
|
|
||||||
style={{ backgroundColor: "var(--bg-card-hover)", color: "var(--text-secondary)", border: "1px solid var(--border-primary)" }}
|
|
||||||
>
|
|
||||||
Choose Binary File
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
@@ -851,11 +841,6 @@ export default function MelodyForm() {
|
|||||||
Build on the Fly
|
Build on the Fly
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{binaryFile && (
|
|
||||||
<p className="text-xs mt-1" style={mutedStyle}>
|
|
||||||
selected: {binaryFile.name}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1" style={labelStyle}>Audio Preview (.mp3)</label>
|
<label className="block text-sm font-medium mb-1" style={labelStyle}>Audio Preview (.mp3)</label>
|
||||||
@@ -968,7 +953,7 @@ export default function MelodyForm() {
|
|||||||
open={showPlayback}
|
open={showPlayback}
|
||||||
melody={{ id: id || savedMelodyId || "preview", information, default_settings: settings, type, url, uid, pid }}
|
melody={{ id: id || savedMelodyId || "preview", information, default_settings: settings, type, url, uid, pid }}
|
||||||
builtMelody={builtMelody}
|
builtMelody={builtMelody}
|
||||||
files={{ binary_url: normalizeFileUrl(existingFiles.binary_url || url || null) }}
|
files={{ binary_url: getEffectiveBinary().effectiveUrl }}
|
||||||
archetypeCsv={information.archetype_csv || null}
|
archetypeCsv={information.archetype_csv || null}
|
||||||
onClose={() => setShowPlayback(false)}
|
onClose={() => setShowPlayback(false)}
|
||||||
/>
|
/>
|
||||||
@@ -976,7 +961,7 @@ export default function MelodyForm() {
|
|||||||
open={showBinaryView}
|
open={showBinaryView}
|
||||||
melody={{ id: id || savedMelodyId || "preview", information, default_settings: settings, type, url, uid, pid }}
|
melody={{ id: id || savedMelodyId || "preview", information, default_settings: settings, type, url, uid, pid }}
|
||||||
builtMelody={builtMelody}
|
builtMelody={builtMelody}
|
||||||
files={{ binary_url: normalizeFileUrl(existingFiles.binary_url || url || null) }}
|
files={{ binary_url: getEffectiveBinary().effectiveUrl }}
|
||||||
archetypeCsv={information.archetype_csv || null}
|
archetypeCsv={information.archetype_csv || null}
|
||||||
onClose={() => setShowBinaryView(false)}
|
onClose={() => setShowBinaryView(false)}
|
||||||
/>
|
/>
|
||||||
@@ -1020,9 +1005,11 @@ export default function MelodyForm() {
|
|||||||
Promise.all([
|
Promise.all([
|
||||||
api.get(`/melodies/${mid}/files`),
|
api.get(`/melodies/${mid}/files`),
|
||||||
api.get(`/melodies/${mid}`),
|
api.get(`/melodies/${mid}`),
|
||||||
]).then(([files, m]) => {
|
api.get(`/builder/melodies/for-melody/${mid}`),
|
||||||
|
]).then(([files, m, bm]) => {
|
||||||
setExistingFiles(files);
|
setExistingFiles(files);
|
||||||
if (m.url) setUrl(m.url);
|
if (m.url) setUrl(m.url);
|
||||||
|
setBuiltMelody(bm || null);
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1052,9 +1039,11 @@ export default function MelodyForm() {
|
|||||||
Promise.all([
|
Promise.all([
|
||||||
api.get(`/melodies/${mid}/files`),
|
api.get(`/melodies/${mid}/files`),
|
||||||
api.get(`/melodies/${mid}`),
|
api.get(`/melodies/${mid}`),
|
||||||
]).then(([files, m]) => {
|
api.get(`/builder/melodies/for-melody/${mid}`),
|
||||||
|
]).then(([files, m, bm]) => {
|
||||||
setExistingFiles(files);
|
setExistingFiles(files);
|
||||||
if (m.url) setUrl(m.url);
|
if (m.url) setUrl(m.url);
|
||||||
|
setBuiltMelody(bm || null);
|
||||||
}).catch(() => {});
|
}).catch(() => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -589,6 +589,14 @@ export default function MelodyList() {
|
|||||||
});
|
});
|
||||||
}, [melodies, createdByFilter, sortBy, sortDir]); // eslint-disable-line react-hooks/exhaustive-deps
|
}, [melodies, createdByFilter, sortBy, sortDir]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||||
|
|
||||||
|
const offlineTaggedCount = useMemo(
|
||||||
|
() => displayRows.filter((row) => Boolean(row?.information?.available_offline)).length,
|
||||||
|
[displayRows]
|
||||||
|
);
|
||||||
|
const hasAnyFilter = Boolean(
|
||||||
|
search || typeFilter || toneFilter || statusFilter || createdByFilter.length > 0
|
||||||
|
);
|
||||||
|
|
||||||
const offlineCode = useMemo(() => buildOfflineCppCode(displayRows), [displayRows]);
|
const offlineCode = useMemo(() => buildOfflineCppCode(displayRows), [displayRows]);
|
||||||
|
|
||||||
const handleSortClick = (columnKey) => {
|
const handleSortClick = (columnKey) => {
|
||||||
@@ -943,7 +951,7 @@ export default function MelodyList() {
|
|||||||
const selectClass = "px-3 py-2 rounded-md text-sm cursor-pointer border";
|
const selectClass = "px-3 py-2 rounded-md text-sm cursor-pointer border";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full min-w-0">
|
<div className="w-full min-w-0 overflow-x-hidden">
|
||||||
<div className="flex items-center justify-between mb-6 w-full">
|
<div className="flex items-center justify-between mb-6 w-full">
|
||||||
<h1 className="text-2xl font-bold" style={{ color: "var(--text-heading)" }}>Melodies</h1>
|
<h1 className="text-2xl font-bold" style={{ color: "var(--text-heading)" }}>Melodies</h1>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
@@ -962,7 +970,7 @@ export default function MelodyList() {
|
|||||||
onSearch={setSearch}
|
onSearch={setSearch}
|
||||||
placeholder="Search by name, description, or tags..."
|
placeholder="Search by name, description, or tags..."
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-wrap gap-3 items-center">
|
<div className="flex flex-wrap gap-3 items-center w-full">
|
||||||
<select
|
<select
|
||||||
value={typeFilter}
|
value={typeFilter}
|
||||||
onChange={(e) => setTypeFilter(e.target.value)}
|
onChange={(e) => setTypeFilter(e.target.value)}
|
||||||
@@ -1131,12 +1139,19 @@ export default function MelodyList() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="ml-auto flex items-center gap-3">
|
<div className="ml-auto flex items-center gap-3 flex-nowrap">
|
||||||
|
<span className="text-sm whitespace-nowrap" style={{ color: "var(--text-muted)" }}>
|
||||||
|
<span className="inline-block max-w-[48vw] overflow-hidden text-ellipsis align-bottom">
|
||||||
|
{hasAnyFilter
|
||||||
|
? `Filtered - Showing ${displayRows.length} / ${melodies.length} Melodies | ${offlineTaggedCount} Melodies tagged for Offline`
|
||||||
|
: `Showing all (${melodies.length}) Melodies | ${offlineTaggedCount} Melodies tagged for Offline`}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
{canEdit && (
|
{canEdit && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setShowOfflineModal(true)}
|
onClick={() => setShowOfflineModal(true)}
|
||||||
className="px-3 py-2 text-sm rounded-md border transition-colors cursor-pointer"
|
className="px-3 py-2 text-sm rounded-md border transition-colors cursor-pointer whitespace-nowrap"
|
||||||
style={{
|
style={{
|
||||||
borderColor: "var(--border-primary)",
|
borderColor: "var(--border-primary)",
|
||||||
color: "var(--text-secondary)",
|
color: "var(--text-secondary)",
|
||||||
@@ -1146,9 +1161,6 @@ export default function MelodyList() {
|
|||||||
Build Offline List
|
Build Offline List
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<span className="flex items-center text-sm" style={{ color: "var(--text-muted)" }}>
|
|
||||||
{displayRows.length} / {total} {total === 1 ? "melody" : "melodies"}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,6 +23,158 @@ function copyText(text, onSuccess) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function stepValueToNotation(stepValue) {
|
||||||
|
const v = Number(stepValue || 0) & 0xffff;
|
||||||
|
if (!v) return "0";
|
||||||
|
const bells = [];
|
||||||
|
for (let bit = 0; bit < 16; bit++) {
|
||||||
|
if (v & (1 << bit)) bells.push(String(bit + 1));
|
||||||
|
}
|
||||||
|
return bells.join("+");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function decodeBinaryToStepsString(file) {
|
||||||
|
const buf = await file.arrayBuffer();
|
||||||
|
const view = new DataView(buf);
|
||||||
|
const values = [];
|
||||||
|
for (let i = 0; i + 1 < buf.byteLength; i += 2) {
|
||||||
|
values.push(view.getUint16(i, false));
|
||||||
|
}
|
||||||
|
return values.map(stepValueToNotation).join(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
function UploadArchetypeModal({ open, existingArchetypes, onClose, onUploaded }) {
|
||||||
|
const [file, setFile] = useState(null);
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [pid, setPid] = useState("");
|
||||||
|
const [submitting, setSubmitting] = useState(false);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
setFile(null);
|
||||||
|
setName("");
|
||||||
|
setPid("");
|
||||||
|
setSubmitting(false);
|
||||||
|
setError("");
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
if (!open) return null;
|
||||||
|
|
||||||
|
const submit = async () => {
|
||||||
|
setError("");
|
||||||
|
if (!file) { setError("Please choose a .bin or .bsm file."); return; }
|
||||||
|
if (!name.trim()) { setError("Friendly name is required."); return; }
|
||||||
|
if (!pid.trim()) { setError("PID is required."); return; }
|
||||||
|
|
||||||
|
const dupName = (existingArchetypes || []).find((a) => a.name?.toLowerCase() === name.trim().toLowerCase());
|
||||||
|
if (dupName) { setError(`An archetype with the name "${name.trim()}" already exists.`); return; }
|
||||||
|
const dupPid = (existingArchetypes || []).find((a) => a.pid?.toLowerCase() === pid.trim().toLowerCase());
|
||||||
|
if (dupPid) { setError(`An archetype with the PID "${pid.trim()}" already exists.`); return; }
|
||||||
|
|
||||||
|
setSubmitting(true);
|
||||||
|
try {
|
||||||
|
const steps = await decodeBinaryToStepsString(file);
|
||||||
|
const created = await api.post("/builder/melodies", {
|
||||||
|
name: name.trim(),
|
||||||
|
pid: pid.trim(),
|
||||||
|
steps,
|
||||||
|
});
|
||||||
|
await api.post(`/builder/melodies/${created.id}/build-binary`);
|
||||||
|
onUploaded?.();
|
||||||
|
onClose?.();
|
||||||
|
} catch (err) {
|
||||||
|
setError(err.message || "Failed to upload archetype.");
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 flex items-center justify-center z-50"
|
||||||
|
style={{ backgroundColor: "rgba(0,0,0,0.65)" }}
|
||||||
|
onClick={(e) => e.target === e.currentTarget && !submitting && onClose()}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-full max-w-md 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)" }}>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-base font-semibold" style={{ color: "var(--text-heading)" }}>Upload Archetype</h2>
|
||||||
|
<p className="text-xs mt-0.5" style={{ color: "var(--text-muted)" }}>Upload .bin/.bsm and save as selectable archetype</p>
|
||||||
|
</div>
|
||||||
|
<button onClick={onClose} className="text-xl leading-none" style={{ color: "var(--text-muted)" }} disabled={submitting}>×</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-6 py-5 space-y-4">
|
||||||
|
{error && (
|
||||||
|
<div className="text-sm rounded-md p-3 border" style={{ backgroundColor: "var(--danger-bg)", borderColor: "var(--danger)", color: "var(--danger-text)" }}>
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1" style={{ color: "var(--text-secondary)" }}>Binary File (.bin/.bsm) *</label>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
accept=".bin,.bsm,application/octet-stream"
|
||||||
|
onChange={(e) => setFile(e.target.files?.[0] || null)}
|
||||||
|
className="w-full px-3 py-2 rounded-md text-sm border"
|
||||||
|
disabled={submitting}
|
||||||
|
/>
|
||||||
|
{file && (
|
||||||
|
<p className="text-xs mt-1" style={{ color: "var(--text-muted)" }}>{file.name}</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1" style={{ color: "var(--text-secondary)" }}>Friendly Name *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 rounded-md text-sm border"
|
||||||
|
placeholder="e.g. Doxology Festive Upload"
|
||||||
|
disabled={submitting}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium mb-1" style={{ color: "var(--text-secondary)" }}>PID *</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={pid}
|
||||||
|
onChange={(e) => setPid(e.target.value)}
|
||||||
|
className="w-full px-3 py-2 rounded-md text-sm border"
|
||||||
|
placeholder="e.g. builtin_doxology_upload"
|
||||||
|
disabled={submitting}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-3 px-6 py-4 border-t" style={{ borderColor: "var(--border-primary)" }}>
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
disabled={submitting}
|
||||||
|
className="px-4 py-2 text-sm rounded-md transition-colors disabled:opacity-50"
|
||||||
|
style={{ backgroundColor: "var(--bg-card-hover)", color: "var(--text-primary)" }}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={submit}
|
||||||
|
disabled={submitting}
|
||||||
|
className="px-4 py-2 text-sm rounded-md transition-colors disabled:opacity-50"
|
||||||
|
style={{ backgroundColor: "var(--btn-primary)", color: "var(--text-white)" }}
|
||||||
|
>
|
||||||
|
{submitting ? "Uploading..." : "Upload Archetype"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function CodeSnippetModal({ melody, onClose }) {
|
function CodeSnippetModal({ melody, onClose }) {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
if (!melody) return null;
|
if (!melody) return null;
|
||||||
@@ -127,6 +279,7 @@ export default function ArchetypeList() {
|
|||||||
const [assignedModal, setAssignedModal] = useState(null); // { archetype, melodyDetails }
|
const [assignedModal, setAssignedModal] = useState(null); // { archetype, melodyDetails }
|
||||||
const [loadingAssigned, setLoadingAssigned] = useState(false);
|
const [loadingAssigned, setLoadingAssigned] = useState(false);
|
||||||
const [primaryLang, setPrimaryLang] = useState("en");
|
const [primaryLang, setPrimaryLang] = useState("en");
|
||||||
|
const [showUploadModal, setShowUploadModal] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
api.get("/settings/melody").then((ms) => setPrimaryLang(ms.primary_language || "en")).catch(() => {});
|
api.get("/settings/melody").then((ms) => setPrimaryLang(ms.primary_language || "en")).catch(() => {});
|
||||||
@@ -259,6 +412,14 @@ export default function ArchetypeList() {
|
|||||||
Add Archetypes, and Build binary (.bsm) files and firmware PROGMEM code.
|
Add Archetypes, and Build binary (.bsm) files and firmware PROGMEM code.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setShowUploadModal(true)}
|
||||||
|
className="px-4 py-2 text-sm rounded-md transition-colors"
|
||||||
|
style={{ backgroundColor: "var(--bg-card-hover)", color: "var(--text-primary)", border: "1px solid var(--border-primary)" }}
|
||||||
|
>
|
||||||
|
Upload Archetype
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => navigate("/melodies/archetypes/new")}
|
onClick={() => navigate("/melodies/archetypes/new")}
|
||||||
className="px-4 py-2 text-sm rounded-md transition-colors"
|
className="px-4 py-2 text-sm rounded-md transition-colors"
|
||||||
@@ -267,6 +428,7 @@ export default function ArchetypeList() {
|
|||||||
+ Add Archetype
|
+ Add Archetype
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="text-sm rounded-md p-3 mb-4 border" style={{ backgroundColor: "var(--danger-bg)", borderColor: "var(--danger)", color: "var(--danger-text)" }}>
|
<div className="text-sm rounded-md p-3 mb-4 border" style={{ backgroundColor: "var(--danger-bg)", borderColor: "var(--danger)", color: "var(--danger-text)" }}>
|
||||||
@@ -448,6 +610,13 @@ export default function ArchetypeList() {
|
|||||||
primaryLang={primaryLang}
|
primaryLang={primaryLang}
|
||||||
onClose={() => setAssignedModal(null)}
|
onClose={() => setAssignedModal(null)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<UploadArchetypeModal
|
||||||
|
open={showUploadModal}
|
||||||
|
existingArchetypes={archetypes}
|
||||||
|
onClose={() => setShowUploadModal(false)}
|
||||||
|
onUploaded={() => loadArchetypes()}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user