CODEX - Probably fixed the download buttons

This commit is contained in:
2026-02-23 11:03:03 +02:00
parent 3a2362b7fd
commit be0b3a5a5a
4 changed files with 130 additions and 61 deletions

View File

@@ -532,8 +532,13 @@ export default function MelodyDetail() {
</Field>
<Field label="Binary File">
{(() => {
// Prefer the uploaded file URL, fall back to melody.url (legacy/firebase storage URL)
const binaryUrl = normalizeFileUrl(files.binary_url || melody.url || null);
// Common source of truth: assigned archetype binary first, then melody URL, then uploaded file URL.
const binaryUrl = normalizeFileUrl(
(builtMelody?.binary_url ? `/api${builtMelody.binary_url}` : null) ||
melody.url ||
files.binary_url ||
null
);
if (!binaryUrl) return <span style={{ color: "var(--text-muted)" }}>Not uploaded</span>;
const binaryPid = builtMelody?.pid || melody.pid || "binary";
@@ -541,7 +546,7 @@ export default function MelodyDetail() {
// Derive a display name: for firebase URLs extract the filename portion
let downloadName = binaryFilename;
if (!files.binary_url && melody.url) {
if (!builtMelody?.binary_url && !files.binary_url && melody.url) {
try {
const urlPath = decodeURIComponent(new URL(melody.url).pathname);
const parts = urlPath.split("/");
@@ -577,7 +582,7 @@ export default function MelodyDetail() {
a.click();
URL.revokeObjectURL(objectUrl);
} catch (err) {
setError(err.message);
window.open(binaryUrl, "_blank", "noopener,noreferrer");
}
};
@@ -607,6 +612,9 @@ export default function MelodyDetail() {
>
View
</button>
<span className="text-xs" style={{ color: "var(--text-muted)" }}>
{info.totalNotes ?? 0} active notes
</span>
{!files.binary_url && melody.url && (
<span className="text-xs px-1.5 py-0.5 rounded" style={{ backgroundColor: "var(--bg-card-hover)", color: "var(--text-muted)" }}>
via URL
@@ -626,9 +634,6 @@ export default function MelodyDetail() {
</a>
</span>
)}
<span className="text-xs" style={{ color: "var(--text-muted)" }}>
{info.totalNotes ?? 0} active notes
</span>
</span>
);
})()}

View File

@@ -244,7 +244,7 @@ export default function MelodyForm() {
a.click();
URL.revokeObjectURL(objectUrl);
} catch (err) {
setError(err.message);
window.open(fileUrl, "_blank", "noopener,noreferrer");
}
};
@@ -751,20 +751,25 @@ export default function MelodyForm() {
<div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>Binary File (.bsm / .bin)</label>
{(() => {
const binaryUrl = normalizeFileUrl(existingFiles.binary_url || url || null);
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);
return (
<div className="text-xs mb-2" style={{ color: "var(--text-muted)" }}>
{binaryUrl ? (
<span className="text-sm" style={{ color: "var(--text-secondary)" }}>
<span className="text-sm mr-4" style={{ color: "var(--text-secondary)" }}>
{binaryName}
</span>
) : (
<span>No binary uploaded</span>
)}
{binaryUrl && (
<div className="mt-1 inline-flex gap-1.5">
<div className="inline-flex items-center gap-2.5">
<button
type="button"
onClick={(e) => downloadExistingFile(binaryUrl, binaryName, e)}
@@ -781,9 +786,9 @@ export default function MelodyForm() {
>
View
</button>
<span style={{ color: "var(--text-muted)" }}>{information.totalNotes ?? 0} active notes</span>
</div>
)}
<div className="mt-0.5">{information.totalNotes ?? 0} active notes</div>
</div>
);
})()}
@@ -794,6 +799,7 @@ export default function MelodyForm() {
onChange={(e) => setBinaryFile(e.target.files[0] || null)}
className="hidden"
/>
<div className="flex flex-wrap gap-2 items-center">
<button
type="button"
onClick={() => binaryInputRef.current?.click()}
@@ -802,17 +808,10 @@ export default function MelodyForm() {
>
Choose Binary File
</button>
{binaryFile && (
<p className="text-xs mt-1" style={mutedStyle}>
selected: {binaryFile.name}
</p>
)}
<div className="flex gap-2 mt-2">
<button
type="button"
onClick={async () => {
if (!isEdit && !savedMelodyId) {
// Auto-save draft first for new melodies
setSaving(true); setError("");
try {
const now = new Date().toISOString();
@@ -852,6 +851,11 @@ export default function MelodyForm() {
Build on the Fly
</button>
</div>
{binaryFile && (
<p className="text-xs mt-1" style={mutedStyle}>
selected: {binaryFile.name}
</p>
)}
</div>
<div>
<label className="block text-sm font-medium mb-1" style={labelStyle}>Audio Preview (.mp3)</label>

View File

@@ -140,31 +140,6 @@ function formatRelativeTime(isoValue) {
return `${diffYears} year${diffYears === 1 ? "" : "s"} ago`;
}
function getBinaryUrl(row) {
const candidate = row?.url;
if (!candidate || typeof candidate !== "string") return null;
if (candidate.startsWith("http") || candidate.startsWith("/api")) return candidate;
if (candidate.startsWith("/")) return `/api${candidate}`;
return `/api/${candidate}`;
}
function getBinaryFilename(row) {
const url = getBinaryUrl(row);
if (!url) return null;
try {
const parsed = new URL(url, window.location.origin);
const path = decodeURIComponent(parsed.pathname || "");
const parts = path.split("/");
const name = parts[parts.length - 1];
if (name && name.toLowerCase().endsWith(".bsm")) return name;
} catch {
// fallback below
}
return row?.pid ? `${row.pid}.bsm` : "melody.bsm";
}
function toSafeCppSymbol(input, fallback = "melody") {
const base = String(input || "").trim().toLowerCase();
const cleaned = base.replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "");
@@ -288,6 +263,7 @@ export default function MelodyList() {
const [showOfflineModal, setShowOfflineModal] = useState(false);
const [builtInSavingId, setBuiltInSavingId] = useState(null);
const [viewRow, setViewRow] = useState(null);
const [builtMap, setBuiltMap] = useState({});
const columnPickerRef = useRef(null);
const creatorPickerRef = useRef(null);
const navigate = useNavigate();
@@ -355,6 +331,62 @@ export default function MelodyList() {
fetchMelodies();
}, [search, typeFilter, toneFilter, statusFilter]);
useEffect(() => {
let canceled = false;
const loadBuiltAssignments = async () => {
if (!melodies.length) {
setBuiltMap({});
return;
}
const pairs = await Promise.all(
melodies.map(async (m) => {
try {
const bm = await api.get(`/builder/melodies/for-melody/${m.id}`);
return [m.id, bm || null];
} catch {
return [m.id, null];
}
})
);
if (canceled) return;
const next = {};
for (const [id, bm] of pairs) next[id] = bm;
setBuiltMap(next);
};
loadBuiltAssignments();
return () => { canceled = true; };
}, [melodies]);
const resolveEffectiveBinary = (row) => {
const built = builtMap[row?.id] || null;
const candidate = built?.binary_url
? `/api${built.binary_url}`
: row?.url || null;
const url = candidate
? (candidate.startsWith("http") || candidate.startsWith("/api")
? candidate
: candidate.startsWith("/")
? `/api${candidate}`
: `/api/${candidate}`)
: null;
const source = built?.binary_url ? "Archetype" : (url ? "Melody URL" : null);
const filename = (() => {
if (!url) return null;
try {
const parsed = new URL(url, window.location.origin);
const path = decodeURIComponent(parsed.pathname || "");
const parts = path.split("/");
const name = parts[parts.length - 1];
if (name) return name;
} catch {
// fallback below
}
if (built?.pid) return `${built.pid}.bsm`;
return row?.pid ? `${row.pid}.bsm` : "melody.bsm";
})();
return { url, filename, source, built };
};
const handleDelete = async () => {
if (!deleteTarget) return;
try {
@@ -396,7 +428,8 @@ export default function MelodyList() {
const downloadBinary = async (e, row) => {
e.stopPropagation();
const binaryUrl = getBinaryUrl(row);
const resolved = resolveEffectiveBinary(row);
const binaryUrl = resolved.url;
if (!binaryUrl) return;
try {
@@ -422,12 +455,17 @@ export default function MelodyList() {
const objectUrl = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = objectUrl;
a.download = getBinaryFilename(row) || "melody.bsm";
a.download = resolved.filename || "melody.bsm";
a.click();
URL.revokeObjectURL(objectUrl);
} catch (err) {
const fallbackUrl = resolveEffectiveBinary(row).url;
if (fallbackUrl) {
window.open(fallbackUrl, "_blank", "noopener,noreferrer");
} else {
setError(err.message);
}
}
};
const openBinaryView = (e, row) => {
@@ -807,8 +845,9 @@ export default function MelodyList() {
);
}
case "binaryFile": {
const binaryUrl = getBinaryUrl(row);
const filename = getBinaryFilename(row);
const resolved = resolveEffectiveBinary(row);
const binaryUrl = resolved.url;
const filename = resolved.filename;
const totalNotes = info.totalNotes ?? 0;
if (!binaryUrl) {
return (
@@ -820,7 +859,14 @@ export default function MelodyList() {
}
return (
<span className="inline-flex flex-col">
<span className="inline-flex items-center gap-2">
<span className="text-xs" style={{ color: "var(--text-secondary)" }}>{filename || "binary.bsm"}</span>
{resolved.source && (
<span className="text-[10px] px-1.5 py-0.5 rounded" style={{ color: "var(--text-muted)", backgroundColor: "var(--bg-card-hover)" }}>
{resolved.source}
</span>
)}
</span>
<span className="mt-1 inline-flex gap-1.5">
<button
type="button"
@@ -1330,8 +1376,8 @@ export default function MelodyList() {
<BinaryTableModal
open={!!viewRow}
melody={viewRow || null}
builtMelody={null}
files={viewRow ? { binary_url: getBinaryUrl(viewRow) } : null}
builtMelody={viewRow ? (builtMap[viewRow.id] || null) : null}
files={viewRow ? { binary_url: resolveEffectiveBinary(viewRow).url } : null}
archetypeCsv={viewRow?.information?.archetype_csv || null}
onClose={() => setViewRow(null)}
/>

View File

@@ -320,7 +320,6 @@ export default function PlaybackModal({ open, melody, builtMelody, files, archet
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-xs" style={mutedStyle}>{totalSteps} steps · {allBellsUsed.size} bell{allBellsUsed.size !== 1 ? "s" : ""}</span>
{currentStep >= 0 && <span className="text-xs font-mono" style={{ color: "var(--accent)" }}>Step {currentStep + 1} / {totalSteps}</span>}
</div>
{noteAssignments.length > 0 && (
@@ -392,6 +391,21 @@ export default function PlaybackModal({ open, melody, builtMelody, files, archet
<input type="checkbox" checked={loopEnabled} onChange={(e) => setLoopEnabled(e.target.checked)} className="h-3.5 w-3.5 rounded" />
Loop
</label>
{currentStep >= 0 && (
<div className="ml-auto min-w-[180px]">
<div className="flex items-center justify-end gap-2">
<span className="text-xs font-mono" style={{ color: "var(--accent)" }}>
Step {currentStep + 1} / {totalSteps}
</span>
</div>
<div className="mt-1 h-1.5 rounded-full" style={{ backgroundColor: "var(--bg-primary)", border: "1px solid var(--border-primary)" }}>
<div
className="h-full rounded-full"
style={{ width: `${Math.max(0, Math.min(100, ((currentStep + 1) / totalSteps) * 100))}%`, backgroundColor: "var(--accent)" }}
/>
</div>
</div>
)}
</div>
<div>