diff --git a/frontend/src/melodies/MelodyDetail.jsx b/frontend/src/melodies/MelodyDetail.jsx index 5f1c6bc..5718f2b 100644 --- a/frontend/src/melodies/MelodyDetail.jsx +++ b/frontend/src/melodies/MelodyDetail.jsx @@ -556,6 +556,10 @@ export default function MelodyDetail() { const handleDownload = async (e) => { e.preventDefault(); + if (binaryUrl.startsWith("http")) { + window.open(binaryUrl, "_blank", "noopener,noreferrer"); + return; + } try { const token = localStorage.getItem("access_token"); let res = null; diff --git a/frontend/src/melodies/MelodyForm.jsx b/frontend/src/melodies/MelodyForm.jsx index 131a390..f3cd04f 100644 --- a/frontend/src/melodies/MelodyForm.jsx +++ b/frontend/src/melodies/MelodyForm.jsx @@ -92,7 +92,6 @@ export default function MelodyForm() { const [pid, setPid] = useState(""); const [melodyStatus, setMelodyStatus] = useState("draft"); - const [binaryFile, setBinaryFile] = useState(null); const [previewFile, setPreviewFile] = useState(null); const [existingFiles, setExistingFiles] = useState({}); const [uploading, setUploading] = useState(false); @@ -120,7 +119,6 @@ export default function MelodyForm() { const [builtMelody, setBuiltMelody] = useState(null); const [assignedBinaryName, setAssignedBinaryName] = useState(null); const [assignedBinaryPid, setAssignedBinaryPid] = useState(null); - const binaryInputRef = useRef(null); const previewInputRef = useRef(null); // 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) => { e?.preventDefault?.(); if (!fileUrl) return; @@ -233,7 +245,8 @@ export default function MelodyForm() { } } 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}`); const blob = await res.blob(); @@ -282,9 +295,8 @@ export default function MelodyForm() { }; const uploadFiles = async (melodyId) => { - if (binaryFile || previewFile) { + if (previewFile) { setUploading(true); - if (binaryFile) await api.upload(`/melodies/${melodyId}/upload/binary`, binaryFile); if (previewFile) await api.upload(`/melodies/${melodyId}/upload/preview`, previewFile); setUploading(false); } @@ -749,16 +761,9 @@ export default function MelodyForm() {
- + {(() => { - const binaryUrl = normalizeFileUrl( - (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); + const { effectiveUrl: binaryUrl, effectiveName: binaryName } = getEffectiveBinary(); return (
{binaryUrl ? ( @@ -792,22 +797,7 @@ export default function MelodyForm() {
); })()} - setBinaryFile(e.target.files[0] || null)} - className="hidden" - />
-
- {binaryFile && ( -

- selected: {binaryFile.name} -

- )}
@@ -968,7 +953,7 @@ export default function MelodyForm() { open={showPlayback} melody={{ id: id || savedMelodyId || "preview", information, default_settings: settings, type, url, uid, pid }} builtMelody={builtMelody} - files={{ binary_url: normalizeFileUrl(existingFiles.binary_url || url || null) }} + files={{ binary_url: getEffectiveBinary().effectiveUrl }} archetypeCsv={information.archetype_csv || null} onClose={() => setShowPlayback(false)} /> @@ -976,7 +961,7 @@ export default function MelodyForm() { open={showBinaryView} melody={{ id: id || savedMelodyId || "preview", information, default_settings: settings, type, url, uid, pid }} builtMelody={builtMelody} - files={{ binary_url: normalizeFileUrl(existingFiles.binary_url || url || null) }} + files={{ binary_url: getEffectiveBinary().effectiveUrl }} archetypeCsv={information.archetype_csv || null} onClose={() => setShowBinaryView(false)} /> @@ -1020,9 +1005,11 @@ export default function MelodyForm() { Promise.all([ api.get(`/melodies/${mid}/files`), api.get(`/melodies/${mid}`), - ]).then(([files, m]) => { + api.get(`/builder/melodies/for-melody/${mid}`), + ]).then(([files, m, bm]) => { setExistingFiles(files); if (m.url) setUrl(m.url); + setBuiltMelody(bm || null); }).catch(() => {}); } } @@ -1052,9 +1039,11 @@ export default function MelodyForm() { Promise.all([ api.get(`/melodies/${mid}/files`), api.get(`/melodies/${mid}`), - ]).then(([files, m]) => { + api.get(`/builder/melodies/for-melody/${mid}`), + ]).then(([files, m, bm]) => { setExistingFiles(files); if (m.url) setUrl(m.url); + setBuiltMelody(bm || null); }).catch(() => {}); } } diff --git a/frontend/src/melodies/MelodyList.jsx b/frontend/src/melodies/MelodyList.jsx index 031ef7c..07520ad 100644 --- a/frontend/src/melodies/MelodyList.jsx +++ b/frontend/src/melodies/MelodyList.jsx @@ -589,6 +589,14 @@ export default function MelodyList() { }); }, [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 handleSortClick = (columnKey) => { @@ -943,7 +951,7 @@ export default function MelodyList() { const selectClass = "px-3 py-2 rounded-md text-sm cursor-pointer border"; return ( -
+

Melodies

{canEdit && ( @@ -962,7 +970,7 @@ export default function MelodyList() { onSearch={setSearch} placeholder="Search by name, description, or tags..." /> -
+
setFile(e.target.files?.[0] || null)} + className="w-full px-3 py-2 rounded-md text-sm border" + disabled={submitting} + /> + {file && ( +

{file.name}

+ )} +
+
+ + setName(e.target.value)} + className="w-full px-3 py-2 rounded-md text-sm border" + placeholder="e.g. Doxology Festive Upload" + disabled={submitting} + /> +
+
+ + setPid(e.target.value)} + className="w-full px-3 py-2 rounded-md text-sm border" + placeholder="e.g. builtin_doxology_upload" + disabled={submitting} + /> +
+
+ +
+ + +
+
+
+ ); +} + function CodeSnippetModal({ melody, onClose }) { const [copied, setCopied] = useState(false); if (!melody) return null; @@ -127,6 +279,7 @@ export default function ArchetypeList() { const [assignedModal, setAssignedModal] = useState(null); // { archetype, melodyDetails } const [loadingAssigned, setLoadingAssigned] = useState(false); const [primaryLang, setPrimaryLang] = useState("en"); + const [showUploadModal, setShowUploadModal] = useState(false); useEffect(() => { api.get("/settings/melody").then((ms) => setPrimaryLang(ms.primary_language || "en")).catch(() => {}); @@ -259,13 +412,22 @@ export default function ArchetypeList() { Add Archetypes, and Build binary (.bsm) files and firmware PROGMEM code.

- +
+ + +
{error && ( @@ -448,6 +610,13 @@ export default function ArchetypeList() { primaryLang={primaryLang} onClose={() => setAssignedModal(null)} /> + + setShowUploadModal(false)} + onUploaded={() => loadArchetypes()} + /> ); }