CODEX - Probably fixed the download buttons
This commit is contained in:
@@ -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>
|
||||
);
|
||||
})()}
|
||||
|
||||
@@ -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,25 +799,19 @@ export default function MelodyForm() {
|
||||
onChange={(e) => setBinaryFile(e.target.files[0] || null)}
|
||||
className="hidden"
|
||||
/>
|
||||
<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>
|
||||
{binaryFile && (
|
||||
<p className="text-xs mt-1" style={mutedStyle}>
|
||||
selected: {binaryFile.name}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex gap-2 mt-2">
|
||||
<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
|
||||
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>
|
||||
|
||||
@@ -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,11 +455,16 @@ 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) {
|
||||
setError(err.message);
|
||||
const fallbackUrl = resolveEffectiveBinary(row).url;
|
||||
if (fallbackUrl) {
|
||||
window.open(fallbackUrl, "_blank", "noopener,noreferrer");
|
||||
} else {
|
||||
setError(err.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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="text-xs" style={{ color: "var(--text-secondary)" }}>{filename || "binary.bsm"}</span>
|
||||
<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)}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user