import { useState, useEffect } from "react"; import { useParams, useNavigate } from "react-router-dom"; import api from "../api/client"; import { useAuth } from "../auth/AuthContext"; import ConfirmDialog from "../components/ConfirmDialog"; import SpeedCalculatorModal from "./SpeedCalculatorModal"; import { getLocalizedValue, getLanguageName, normalizeColor, formatDuration, } from "./melodyUtils"; function Field({ label, children }) { return (
{label}
{children || "-"}
); } export default function MelodyDetail() { const { id } = useParams(); const navigate = useNavigate(); const { hasPermission } = useAuth(); const canEdit = hasPermission("melodies", "edit"); const [melody, setMelody] = useState(null); const [files, setFiles] = useState({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); const [showDelete, setShowDelete] = useState(false); const [showUnpublish, setShowUnpublish] = useState(false); const [actionLoading, setActionLoading] = useState(false); const [displayLang, setDisplayLang] = useState("en"); const [melodySettings, setMelodySettings] = useState(null); const [builtMelody, setBuiltMelody] = useState(null); const [codeCopied, setCodeCopied] = useState(false); const [showSpeedCalc, setShowSpeedCalc] = useState(false); useEffect(() => { api.get("/settings/melody").then((ms) => { setMelodySettings(ms); setDisplayLang(ms.primary_language || "en"); }); }, []); useEffect(() => { loadData(); }, [id]); const loadData = async () => { setLoading(true); try { const [m, f] = await Promise.all([ api.get(`/melodies/${id}`), api.get(`/melodies/${id}/files`), ]); setMelody(m); setFiles(f); // Load built melody assignment (non-fatal if it fails) try { const bm = await api.get(`/builder/melodies/for-melody/${id}`); setBuiltMelody(bm || null); } catch { setBuiltMelody(null); } } catch (err) { setError(err.message); } finally { setLoading(false); } }; const handleDelete = async () => { try { await api.delete(`/melodies/${id}`); navigate("/melodies"); } catch (err) { setError(err.message); setShowDelete(false); } }; const handlePublish = async () => { setActionLoading(true); try { await api.post(`/melodies/${id}/publish`); await loadData(); } catch (err) { setError(err.message); } finally { setActionLoading(false); } }; const handleUnpublish = async () => { setActionLoading(true); try { await api.post(`/melodies/${id}/unpublish`); setShowUnpublish(false); await loadData(); } catch (err) { setError(err.message); setShowUnpublish(false); } finally { setActionLoading(false); } }; if (loading) { return
Loading...
; } if (error) { return (
{error}
); } if (!melody) return null; const info = melody.information || {}; const settings = melody.default_settings || {}; const languages = melodySettings?.available_languages || ["en"]; const displayName = getLocalizedValue(info.name, displayLang, "Untitled Melody"); const badgeStyle = (active) => ({ backgroundColor: active ? "var(--success-bg)" : "var(--bg-card-hover)", color: active ? "var(--success-text)" : "var(--text-muted)", }); return (

{displayName}

{melody.status === "published" ? "LIVE" : "DRAFT"} {languages.length > 1 && ( )}
{canEdit && (
{melody.status === "draft" ? ( ) : ( )}
)}
{/* Left column */}
{/* Melody Information */}

Melody Information

{melody.type} {info.melodyTone} {info.steps} {info.totalNotes} {info.minSpeed} {info.maxSpeed} {info.color ? ( {info.color} ) : ( "-" )} {info.isTrueRing ? "Yes" : "No"}
{getLocalizedValue(info.description, displayLang)}
{info.customTags?.length > 0 ? (
{info.customTags.map((tag) => ( {tag} ))}
) : ( "-" )}
{/* Identifiers */}

Identifiers

{melody.id} {melody.pid} {melody.uid}
{melody.url}
{/* Right column */}
{/* Default Settings */}

Default Settings

{settings.speed}% {formatDuration(settings.duration)} {settings.totalRunDuration} {settings.pauseDuration} {settings.infiniteLoop ? "Yes" : "No"}
{settings.echoRing?.length > 0 ? settings.echoRing.join(", ") : "-"}
{settings.noteAssignments?.length > 0 ? settings.noteAssignments.join(", ") : "-"}
{/* Files */}

Files

{files.binary_url ? ( Download binary ) : ( Not uploaded )} {files.preview_url ? ( ) : ( Not uploaded )}
{/* Firmware Code section — only shown if a built melody with PROGMEM code is assigned */} {builtMelody?.progmem_code && (

Firmware Code

PROGMEM code for built-in firmware playback  ·  PID: {builtMelody.pid}

            {builtMelody.progmem_code}
          
)} setShowSpeedCalc(false)} onSaved={() => { setShowSpeedCalc(false); loadData(); }} /> setShowDelete(false)} /> setShowUnpublish(false)} />
); }