CODEX - Probably fixed the download buttons
This commit is contained in:
@@ -532,8 +532,13 @@ export default function MelodyDetail() {
|
|||||||
</Field>
|
</Field>
|
||||||
<Field label="Binary File">
|
<Field label="Binary File">
|
||||||
{(() => {
|
{(() => {
|
||||||
// Prefer the uploaded file URL, fall back to melody.url (legacy/firebase storage URL)
|
// Common source of truth: assigned archetype binary first, then melody URL, then uploaded file URL.
|
||||||
const binaryUrl = normalizeFileUrl(files.binary_url || melody.url || null);
|
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>;
|
if (!binaryUrl) return <span style={{ color: "var(--text-muted)" }}>Not uploaded</span>;
|
||||||
|
|
||||||
const binaryPid = builtMelody?.pid || melody.pid || "binary";
|
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
|
// Derive a display name: for firebase URLs extract the filename portion
|
||||||
let downloadName = binaryFilename;
|
let downloadName = binaryFilename;
|
||||||
if (!files.binary_url && melody.url) {
|
if (!builtMelody?.binary_url && !files.binary_url && melody.url) {
|
||||||
try {
|
try {
|
||||||
const urlPath = decodeURIComponent(new URL(melody.url).pathname);
|
const urlPath = decodeURIComponent(new URL(melody.url).pathname);
|
||||||
const parts = urlPath.split("/");
|
const parts = urlPath.split("/");
|
||||||
@@ -577,7 +582,7 @@ export default function MelodyDetail() {
|
|||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(objectUrl);
|
URL.revokeObjectURL(objectUrl);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message);
|
window.open(binaryUrl, "_blank", "noopener,noreferrer");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -607,6 +612,9 @@ export default function MelodyDetail() {
|
|||||||
>
|
>
|
||||||
View
|
View
|
||||||
</button>
|
</button>
|
||||||
|
<span className="text-xs" style={{ color: "var(--text-muted)" }}>
|
||||||
|
{info.totalNotes ?? 0} active notes
|
||||||
|
</span>
|
||||||
{!files.binary_url && melody.url && (
|
{!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)" }}>
|
<span className="text-xs px-1.5 py-0.5 rounded" style={{ backgroundColor: "var(--bg-card-hover)", color: "var(--text-muted)" }}>
|
||||||
via URL
|
via URL
|
||||||
@@ -626,9 +634,6 @@ export default function MelodyDetail() {
|
|||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className="text-xs" style={{ color: "var(--text-muted)" }}>
|
|
||||||
{info.totalNotes ?? 0} active notes
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ export default function MelodyForm() {
|
|||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(objectUrl);
|
URL.revokeObjectURL(objectUrl);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err.message);
|
window.open(fileUrl, "_blank", "noopener,noreferrer");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -751,20 +751,25 @@ export default function MelodyForm() {
|
|||||||
<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 / .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 fallback = assignedBinaryPid ? `${assignedBinaryPid}.bsm` : (pid ? `${pid}.bsm` : "Click to Download");
|
||||||
const binaryName = resolveFilename(binaryUrl, fallback);
|
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 ? (
|
||||||
<span className="text-sm" style={{ color: "var(--text-secondary)" }}>
|
<span className="text-sm mr-4" style={{ color: "var(--text-secondary)" }}>
|
||||||
{binaryName}
|
{binaryName}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span>No binary uploaded</span>
|
<span>No binary uploaded</span>
|
||||||
)}
|
)}
|
||||||
{binaryUrl && (
|
{binaryUrl && (
|
||||||
<div className="mt-1 inline-flex gap-1.5">
|
<div className="inline-flex items-center gap-2.5">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={(e) => downloadExistingFile(binaryUrl, binaryName, e)}
|
onClick={(e) => downloadExistingFile(binaryUrl, binaryName, e)}
|
||||||
@@ -781,9 +786,9 @@ export default function MelodyForm() {
|
|||||||
>
|
>
|
||||||
View
|
View
|
||||||
</button>
|
</button>
|
||||||
|
<span style={{ color: "var(--text-muted)" }}>{information.totalNotes ?? 0} active notes</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="mt-0.5">{information.totalNotes ?? 0} active notes</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
@@ -794,25 +799,19 @@ export default function MelodyForm() {
|
|||||||
onChange={(e) => setBinaryFile(e.target.files[0] || null)}
|
onChange={(e) => setBinaryFile(e.target.files[0] || null)}
|
||||||
className="hidden"
|
className="hidden"
|
||||||
/>
|
/>
|
||||||
<button
|
<div className="flex flex-wrap gap-2 items-center">
|
||||||
type="button"
|
<button
|
||||||
onClick={() => binaryInputRef.current?.click()}
|
type="button"
|
||||||
className="px-3 py-1.5 text-xs rounded-md transition-colors"
|
onClick={() => binaryInputRef.current?.click()}
|
||||||
style={{ backgroundColor: "var(--bg-card-hover)", color: "var(--text-secondary)", border: "1px solid var(--border-primary)" }}
|
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>
|
Choose Binary File
|
||||||
{binaryFile && (
|
</button>
|
||||||
<p className="text-xs mt-1" style={mutedStyle}>
|
|
||||||
selected: {binaryFile.name}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<div className="flex gap-2 mt-2">
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (!isEdit && !savedMelodyId) {
|
if (!isEdit && !savedMelodyId) {
|
||||||
// Auto-save draft first for new melodies
|
|
||||||
setSaving(true); setError("");
|
setSaving(true); setError("");
|
||||||
try {
|
try {
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
@@ -852,6 +851,11 @@ 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>
|
||||||
|
|||||||
@@ -140,31 +140,6 @@ function formatRelativeTime(isoValue) {
|
|||||||
return `${diffYears} year${diffYears === 1 ? "" : "s"} ago`;
|
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") {
|
function toSafeCppSymbol(input, fallback = "melody") {
|
||||||
const base = String(input || "").trim().toLowerCase();
|
const base = String(input || "").trim().toLowerCase();
|
||||||
const cleaned = base.replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "");
|
const cleaned = base.replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "");
|
||||||
@@ -288,6 +263,7 @@ export default function MelodyList() {
|
|||||||
const [showOfflineModal, setShowOfflineModal] = useState(false);
|
const [showOfflineModal, setShowOfflineModal] = useState(false);
|
||||||
const [builtInSavingId, setBuiltInSavingId] = useState(null);
|
const [builtInSavingId, setBuiltInSavingId] = useState(null);
|
||||||
const [viewRow, setViewRow] = useState(null);
|
const [viewRow, setViewRow] = useState(null);
|
||||||
|
const [builtMap, setBuiltMap] = useState({});
|
||||||
const columnPickerRef = useRef(null);
|
const columnPickerRef = useRef(null);
|
||||||
const creatorPickerRef = useRef(null);
|
const creatorPickerRef = useRef(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -355,6 +331,62 @@ export default function MelodyList() {
|
|||||||
fetchMelodies();
|
fetchMelodies();
|
||||||
}, [search, typeFilter, toneFilter, statusFilter]);
|
}, [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 () => {
|
const handleDelete = async () => {
|
||||||
if (!deleteTarget) return;
|
if (!deleteTarget) return;
|
||||||
try {
|
try {
|
||||||
@@ -396,7 +428,8 @@ export default function MelodyList() {
|
|||||||
|
|
||||||
const downloadBinary = async (e, row) => {
|
const downloadBinary = async (e, row) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const binaryUrl = getBinaryUrl(row);
|
const resolved = resolveEffectiveBinary(row);
|
||||||
|
const binaryUrl = resolved.url;
|
||||||
if (!binaryUrl) return;
|
if (!binaryUrl) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -422,11 +455,16 @@ export default function MelodyList() {
|
|||||||
const objectUrl = URL.createObjectURL(blob);
|
const objectUrl = URL.createObjectURL(blob);
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
a.href = objectUrl;
|
a.href = objectUrl;
|
||||||
a.download = getBinaryFilename(row) || "melody.bsm";
|
a.download = resolved.filename || "melody.bsm";
|
||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(objectUrl);
|
URL.revokeObjectURL(objectUrl);
|
||||||
} catch (err) {
|
} 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": {
|
case "binaryFile": {
|
||||||
const binaryUrl = getBinaryUrl(row);
|
const resolved = resolveEffectiveBinary(row);
|
||||||
const filename = getBinaryFilename(row);
|
const binaryUrl = resolved.url;
|
||||||
|
const filename = resolved.filename;
|
||||||
const totalNotes = info.totalNotes ?? 0;
|
const totalNotes = info.totalNotes ?? 0;
|
||||||
if (!binaryUrl) {
|
if (!binaryUrl) {
|
||||||
return (
|
return (
|
||||||
@@ -820,7 +859,14 @@ export default function MelodyList() {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<span className="inline-flex flex-col">
|
<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">
|
<span className="mt-1 inline-flex gap-1.5">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1330,8 +1376,8 @@ export default function MelodyList() {
|
|||||||
<BinaryTableModal
|
<BinaryTableModal
|
||||||
open={!!viewRow}
|
open={!!viewRow}
|
||||||
melody={viewRow || null}
|
melody={viewRow || null}
|
||||||
builtMelody={null}
|
builtMelody={viewRow ? (builtMap[viewRow.id] || null) : null}
|
||||||
files={viewRow ? { binary_url: getBinaryUrl(viewRow) } : null}
|
files={viewRow ? { binary_url: resolveEffectiveBinary(viewRow).url } : null}
|
||||||
archetypeCsv={viewRow?.information?.archetype_csv || null}
|
archetypeCsv={viewRow?.information?.archetype_csv || null}
|
||||||
onClose={() => setViewRow(null)}
|
onClose={() => setViewRow(null)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -320,7 +320,6 @@ export default function PlaybackModal({ open, melody, builtMelody, files, archet
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-xs" style={mutedStyle}>{totalSteps} steps · {allBellsUsed.size} bell{allBellsUsed.size !== 1 ? "s" : ""}</span>
|
<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>
|
</div>
|
||||||
|
|
||||||
{noteAssignments.length > 0 && (
|
{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" />
|
<input type="checkbox" checked={loopEnabled} onChange={(e) => setLoopEnabled(e.target.checked)} className="h-3.5 w-3.5 rounded" />
|
||||||
Loop
|
Loop
|
||||||
</label>
|
</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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
Reference in New Issue
Block a user