- {existingFiles.binary_url && (
-
- {assignedBinaryName ? (
- <>
-
{assignedBinaryName} has been assigned. Selecting a new file will replace it.
- {assignedBinaryPid && (
-
- filename: {assignedBinaryPid}.bsm
-
- )}
- >
- ) : (
-
Current file uploaded. Selecting a new file will replace it.
- )}
-
+ {(() => {
+ const binaryUrl = existingFiles.binary_url || url || null;
+ const fallback = assignedBinaryPid ? `${assignedBinaryPid}.bsm` : (pid ? `${pid}.bsm` : "Click to Download");
+ const binaryName = resolveFilename(binaryUrl, fallback);
+ return (
+
+ );
+ })()}
+
setBinaryFile(e.target.files[0] || null)}
+ className="hidden"
+ />
+
+ {binaryFile && (
+
+ selected: {binaryFile.name}
+
)}
-
setBinaryFile(e.target.files[0] || null)} className="w-full text-sm" style={mutedStyle} />
);
}
+
+
+
+
+
diff --git a/frontend/src/melodies/MelodyList.jsx b/frontend/src/melodies/MelodyList.jsx
index 66e6463..333e9f0 100644
--- a/frontend/src/melodies/MelodyList.jsx
+++ b/frontend/src/melodies/MelodyList.jsx
@@ -29,8 +29,7 @@ const ALL_COLUMNS = [
{ key: "description", label: "Description", defaultOn: false },
{ key: "type", label: "Type", defaultOn: true },
{ key: "tone", label: "Tone", defaultOn: true },
- { key: "totalNotes", label: "Total Notes", defaultOn: true },
- { key: "totalActiveBells", label: "Total Active Bells", defaultOn: true },
+ { key: "totalActiveBells", label: "Unique Bells", defaultOn: true },
{ key: "minSpeed", label: "Min Speed", defaultOn: false },
{ key: "maxSpeed", label: "Max Speed", defaultOn: false },
{ key: "tags", label: "Tags", defaultOn: false },
@@ -42,7 +41,7 @@ const ALL_COLUMNS = [
{ key: "noteAssignments", label: "Note Assignments", defaultOn: false },
{ key: "binaryFile", label: "Binary File", defaultOn: false },
{ key: "dateCreated", label: "Date Created", defaultOn: false },
- { key: "dateEdited", label: "Date Edited", defaultOn: false },
+ { key: "dateEdited", label: "Last Edited", defaultOn: false },
{ key: "createdBy", label: "Created By", defaultOn: false },
{ key: "lastEditedBy", label: "Last Edited By", defaultOn: false },
{ key: "isTrueRing", label: "True Ring", defaultOn: true },
@@ -102,6 +101,49 @@ function parseDateValue(isoValue) {
return Number.isNaN(time) ? 0 : time;
}
+function formatDurationVerbose(seconds) {
+ const total = Number(seconds || 0);
+ const mins = Math.floor(total / 60);
+ const secs = total % 60;
+ return `${mins} min ${secs} sec`;
+}
+
+function formatDateTwoLine(isoValue) {
+ const d = new Date(isoValue);
+ if (Number.isNaN(d.getTime())) return { date: "-", time: "-" };
+ const dd = String(d.getDate()).padStart(2, "0");
+ const mm = String(d.getMonth() + 1).padStart(2, "0");
+ const yyyy = d.getFullYear();
+ const hh = String(d.getHours()).padStart(2, "0");
+ const min = String(d.getMinutes()).padStart(2, "0");
+ return { date: `${dd}/${mm}/${yyyy}`, time: `${hh}:${min}` };
+}
+
+function formatRelativeTime(isoValue) {
+ if (!isoValue) return "-";
+ const ts = new Date(isoValue).getTime();
+ if (Number.isNaN(ts)) return "-";
+ const now = Date.now();
+ const diffSec = Math.max(1, Math.floor((now - ts) / 1000));
+ if (diffSec < 60) return `${diffSec} sec ago`;
+ const diffMin = Math.floor(diffSec / 60);
+ if (diffMin < 60) return `${diffMin} min ago`;
+ const diffHours = Math.floor(diffMin / 60);
+ if (diffHours < 24) return `${diffHours} hour${diffHours === 1 ? "" : "s"} ago`;
+ const diffDays = Math.floor(diffHours / 24);
+ if (diffDays < 30) return `${diffDays} day${diffDays === 1 ? "" : "s"} ago`;
+ const diffMonths = Math.floor(diffDays / 30);
+ if (diffMonths < 12) return `${diffMonths} month${diffMonths === 1 ? "" : "s"} ago`;
+ const diffYears = Math.floor(diffMonths / 12);
+ return `${diffYears} year${diffYears === 1 ? "" : "s"} ago`;
+}
+
+function hueForDepth(index, count) {
+ const safeCount = Math.max(1, count);
+ const t = Math.max(0, Math.min(1, index / safeCount));
+ return 190 + (15 - 190) * t; // high notes blue-ish -> deep notes warm red
+}
+
function getBinaryUrl(row) {
const candidate = row?.url;
if (!candidate || typeof candidate !== "string") return null;
@@ -160,6 +202,20 @@ export default function MelodyList() {
});
}, []);
+ useEffect(() => {
+ setVisibleColumns((prev) => {
+ const known = new Set(ALL_COLUMNS.map((c) => c.key));
+ let next = prev.filter((k) => known.has(k));
+ for (const col of ALL_COLUMNS) {
+ if (col.alwaysOn && !next.includes(col.key)) next.push(col.key);
+ }
+ if (JSON.stringify(next) !== JSON.stringify(prev)) {
+ localStorage.setItem("melodyListColumns", JSON.stringify(next));
+ }
+ return next;
+ });
+ }, []);
+
// Close dropdowns on outside click
useEffect(() => {
const handleClick = (e) => {
@@ -279,6 +335,19 @@ export default function MelodyList() {
});
};
+ const moveColumn = (key, direction) => {
+ setVisibleColumns((prev) => {
+ const idx = prev.indexOf(key);
+ if (idx < 0) return prev;
+ const swapIdx = direction === "up" ? idx - 1 : idx + 1;
+ if (swapIdx < 0 || swapIdx >= prev.length) return prev;
+ const next = [...prev];
+ [next[idx], next[swapIdx]] = [next[swapIdx], next[idx]];
+ localStorage.setItem("melodyListColumns", JSON.stringify(next));
+ return next;
+ });
+ };
+
const toggleCreator = (creator) => {
setCreatedByFilter((prev) =>
prev.includes(creator) ? prev.filter((c) => c !== creator) : [...prev, creator]
@@ -416,8 +485,6 @@ export default function MelodyList() {
}
case "tone":
return
{info.melodyTone || "-"};
- case "totalNotes":
- return info.totalNotes ?? "-";
case "totalActiveBells":
return info.totalActiveBells ?? "-";
case "minSpeed":
@@ -499,7 +566,7 @@ export default function MelodyList() {
return (
- {formatDuration(ds.duration)}
+ {formatDurationVerbose(ds.duration)}
0 ? (
{ds.noteAssignments.map((assignedBell, noteIdx) => (
+ (() => {
+ const noteHue = hueForDepth(noteIdx, ds.noteAssignments.length - 1);
+ const bellDepthIdx = Math.max(0, Math.min(15, (assignedBell || 1) - 1));
+ const bellHue = hueForDepth(bellDepthIdx, 15);
+ return (
-
+
{NOTE_LABELS[noteIdx]}
-
+ 0 ? `hsl(${bellHue}, 80%, 70%)` : "var(--text-muted)" }}>
{assignedBell > 0 ? assignedBell : "—"}
+ );
+ })()
))}
) : (
@@ -562,23 +637,47 @@ export default function MelodyList() {
case "binaryFile": {
const binaryUrl = getBinaryUrl(row);
const filename = getBinaryFilename(row);
- if (!binaryUrl) return
-;
+ const totalNotes = info.totalNotes ?? 0;
+ if (!binaryUrl) {
+ return (
+
+ -
+ {totalNotes} active notes
+
+ );
+ }
return (
-
downloadBinary(e, row)}
- className="underline text-xs text-left"
- style={{ color: "var(--accent)", background: "none", border: "none", padding: 0 }}
- title={binaryUrl}
- >
- {filename}
-
+
+ downloadBinary(e, row)}
+ className="underline text-xs text-left"
+ style={{ color: "var(--accent)", background: "none", border: "none", padding: 0 }}
+ title={binaryUrl}
+ >
+ {filename || "Click to Download"}
+
+ {totalNotes} active notes
+
);
}
case "dateCreated":
- return metadata.dateCreated ? new Date(metadata.dateCreated).toLocaleString() : "-";
+ if (!metadata.dateCreated) return "-";
+ {
+ const parts = formatDateTwoLine(metadata.dateCreated);
+ return (
+
+ {parts.date}
+ {parts.time}
+
+ );
+ }
case "dateEdited":
- return metadata.dateEdited ? new Date(metadata.dateEdited).toLocaleString() : "-";
+ return metadata.dateEdited ? (
+
+ {formatRelativeTime(metadata.dateEdited)}
+
+ ) : "-";
case "createdBy":
return metadata.createdBy || "-";
case "lastEditedBy":
@@ -604,16 +703,19 @@ export default function MelodyList() {
}
};
- // Build visible column list (description is rendered inside name, not as its own column)
- const activeColumns = ALL_COLUMNS.filter((c) => c.key !== "description" && isVisible(c.key));
+ // Build visible column list in user-defined order (description is rendered inside name)
+ const activeColumns = visibleColumns
+ .filter((key) => key !== "description")
+ .map((key) => ALL_COLUMNS.find((c) => c.key === key))
+ .filter(Boolean);
const languages = melodySettings?.available_languages || ["en"];
const selectClass = "px-3 py-2 rounded-md text-sm cursor-pointer border";
return (
-
-
+
+
Melodies
{canEdit && (
-
+
- {ALL_COLUMNS.map((col) => (
+ {ALL_COLUMNS.map((col) => {
+ const orderIdx = visibleColumns.indexOf(col.key);
+ const canMove = orderIdx >= 0;
+ return (
- {col.label}
+ {col.label}
+ {canMove && (
+ e.stopPropagation()}>
+ moveColumn(col.key, "up")}
+ className="text-[10px] px-1 rounded border"
+ style={{ borderColor: "var(--border-primary)", color: "var(--text-muted)", background: "transparent" }}
+ title="Move left"
+ >
+ ←
+
+ moveColumn(col.key, "down")}
+ className="text-[10px] px-1 rounded border"
+ style={{ borderColor: "var(--border-primary)", color: "var(--text-muted)", background: "transparent" }}
+ title="Move right"
+ >
+ →
+
+
+ )}
- ))}
+ );
+ })}
)}
@@ -808,14 +936,14 @@ export default function MelodyList() {
) : (
-
-
+
+
{activeColumns.map((col) => (
@@ -942,3 +1070,4 @@ export default function MelodyList() {
);
}
+