import { useState, useEffect } from "react"; import { useNavigate, useParams } from "react-router-dom"; import api from "../api/client"; import TranslationModal from "./TranslationModal"; import SelectBuiltMelodyModal from "./builder/SelectBuiltMelodyModal"; import BuildOnTheFlyModal from "./builder/BuildOnTheFlyModal"; import SpeedCalculatorModal from "./SpeedCalculatorModal"; import { getLocalizedValue, getLanguageName, normalizeColor, formatDuration, parseLocalizedString, serializeLocalizedString, } from "./melodyUtils"; const MELODY_TYPES = ["orthodox", "catholic", "all"]; const MELODY_TONES = ["normal", "festive", "cheerful", "lamentation"]; const defaultInfo = { name: "", description: "", melodyTone: "normal", customTags: [], minSpeed: 0, maxSpeed: 0, totalNotes: 1, steps: 0, color: "", isTrueRing: false, previewURL: "", }; const defaultSettings = { speed: 50, duration: 0, totalRunDuration: 0, pauseDuration: 0, infiniteLoop: false, echoRing: [], noteAssignments: [], }; // Dark-themed styles const sectionStyle = { backgroundColor: "var(--bg-card)", borderColor: "var(--border-primary)", }; const headingStyle = { color: "var(--text-heading)" }; const labelStyle = { color: "var(--text-secondary)" }; const mutedStyle = { color: "var(--text-muted)" }; export default function MelodyForm() { const { id } = useParams(); const isEdit = Boolean(id); const navigate = useNavigate(); const [information, setInformation] = useState({ ...defaultInfo }); const [settings, setSettings] = useState({ ...defaultSettings }); const [type, setType] = useState("all"); const [url, setUrl] = useState(""); const [uid, setUid] = useState(""); const [pid, setPid] = useState(""); const [melodyStatus, setMelodyStatus] = useState("draft"); const [binaryFile, setBinaryFile] = useState(null); const [previewFile, setPreviewFile] = useState(null); const [existingFiles, setExistingFiles] = useState({}); const [uploading, setUploading] = useState(false); const [loading, setLoading] = useState(false); const [saving, setSaving] = useState(false); const [error, setError] = useState(""); const [tagInput, setTagInput] = useState(""); const [melodySettings, setMelodySettings] = useState(null); const [editLang, setEditLang] = useState("en"); const [translationModal, setTranslationModal] = useState({ open: false, field: "", fieldKey: "", multiline: false, }); const [showSelectBuilt, setShowSelectBuilt] = useState(false); const [showBuildOnTheFly, setShowBuildOnTheFly] = useState(false); const [showSpeedCalc, setShowSpeedCalc] = useState(false); const [builtMelody, setBuiltMelody] = useState(null); useEffect(() => { api.get("/settings/melody").then((ms) => { setMelodySettings(ms); setEditLang(ms.primary_language || "en"); }); }, []); useEffect(() => { if (isEdit) loadMelody(); }, [id]); useEffect(() => { const count = information.totalNotes || 1; setSettings((prev) => { const current = [...(prev.noteAssignments || [])]; if (current.length < count) { while (current.length < count) current.push(0); } else if (current.length > count) { current.length = count; } return { ...prev, noteAssignments: current }; }); }, [information.totalNotes]); const loadMelody = async () => { setLoading(true); try { const [melody, files] = await Promise.all([ api.get(`/melodies/${id}`), api.get(`/melodies/${id}/files`), ]); setInformation({ ...defaultInfo, ...melody.information }); setSettings({ ...defaultSettings, ...melody.default_settings }); setType(melody.type || "all"); setUrl(melody.url || ""); setUid(melody.uid || ""); setPid(melody.pid || ""); setMelodyStatus(melody.status || "published"); setExistingFiles(files); // Load built melody assignment (non-fatal) 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 updateInfo = (field, value) => setInformation((prev) => ({ ...prev, [field]: value })); const updateSettings = (field, value) => setSettings((prev) => ({ ...prev, [field]: value })); const addTag = () => { const tag = tagInput.trim(); if (tag && !information.customTags.includes(tag)) updateInfo("customTags", [...information.customTags, tag]); setTagInput(""); }; const removeTag = (tag) => updateInfo("customTags", information.customTags.filter((t) => t !== tag)); const parseIntList = (str) => { if (!str.trim()) return []; return str.split(",").map((s) => parseInt(s.trim(), 10)).filter((n) => !isNaN(n)); }; const updateLocalizedField = (fieldKey, text) => { const dict = parseLocalizedString(information[fieldKey]); dict[editLang] = text; updateInfo(fieldKey, serializeLocalizedString(dict)); }; const buildBody = () => { const { notes, ...infoWithoutNotes } = information; return { information: infoWithoutNotes, default_settings: settings, type, url, uid, pid, }; }; const uploadFiles = async (melodyId) => { if (binaryFile || previewFile) { setUploading(true); if (binaryFile) await api.upload(`/melodies/${melodyId}/upload/binary`, binaryFile); if (previewFile) await api.upload(`/melodies/${melodyId}/upload/preview`, previewFile); setUploading(false); } }; const handleSave = async (publish) => { setSaving(true); setError(""); try { const body = buildBody(); let melodyId = id; if (isEdit) { await api.put(`/melodies/${id}`, body); } else { const created = await api.post(`/melodies?publish=${publish}`, body); melodyId = created.id; } await uploadFiles(melodyId); navigate(`/melodies/${melodyId}`); } catch (err) { setError(err.message); setUploading(false); } finally { setSaving(false); } }; const handlePublishAction = async () => { setSaving(true); setError(""); try { const body = buildBody(); await api.put(`/melodies/${id}`, body); await uploadFiles(id); await api.post(`/melodies/${id}/publish`); navigate(`/melodies/${id}`); } catch (err) { setError(err.message); setUploading(false); } finally { setSaving(false); } }; const handleUnpublishAction = async () => { setSaving(true); setError(""); try { await api.post(`/melodies/${id}/unpublish`); navigate(`/melodies/${id}`); } catch (err) { setError(err.message); } finally { setSaving(false); } }; const handleSubmit = async (e) => { e.preventDefault(); // Default form submit: save as draft for new, update for existing if (isEdit) { await handleSave(false); } else { await handleSave(false); } }; if (loading) { return
Loading...
; } const languages = melodySettings?.available_languages || ["en"]; const durationValues = melodySettings?.duration_values || [0]; const quickColors = melodySettings?.quick_colors || []; const durationIndex = durationValues.indexOf(settings.duration); const currentDurationIdx = durationIndex >= 0 ? durationIndex : 0; const inputClass = "w-full px-3 py-2 rounded-md text-sm border"; return (

{isEdit ? "Edit Melody" : "Add Melody"}

{isEdit && ( )} {!isEdit ? ( <> ) : melodyStatus === "draft" ? ( <> ) : ( <> )}
{error && (
{error}
)}
{/* ===== Left Column ===== */}
{/* --- Melody Info Section --- */}

Melody Information

{/* Name (localized) */}
{languages.length > 1 && ( )}
updateLocalizedField("name", e.target.value)} className={inputClass} />
{/* Description (localized) */}