feat: add structured preset button in AI Preset Manager
Add handleNewStructured fn + emerald button left of Quay lại pre-fills form with comprehensive Vietnamese template sections
This commit is contained in:
+458
-89
@@ -4573,7 +4573,71 @@ const ProfileModal = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenProject = async (projectId) => {
|
||||
const loadAudioBuffersForTracks = async (tracksList) => {
|
||||
let hasLoadedAny = false;
|
||||
const updatedTracks = await Promise.all(tracksList.map(async t => {
|
||||
let trackBuffer = t.buffer;
|
||||
let trackChannelInfo = t.channelInfo;
|
||||
if (t.serverFileId && !trackBuffer) {
|
||||
try {
|
||||
const res = await fetch('/static/audio/uploads/' + t.serverFileId);
|
||||
if (res.ok) {
|
||||
const blob = await res.blob();
|
||||
const { audioBuffer, channelInfo } = await window.SonicAudio.decodeAudioFile(blob);
|
||||
trackBuffer = audioBuffer;
|
||||
trackChannelInfo = channelInfo;
|
||||
hasLoadedAny = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to autoload track buffer for " + t.serverFileId, e);
|
||||
}
|
||||
}
|
||||
|
||||
let clipsUpdated = false;
|
||||
const updatedClips = await Promise.all((t.clips || []).map(async c => {
|
||||
let clipBuffer = c.buffer;
|
||||
const targetFileId = c.serverFileId || t.serverFileId;
|
||||
if (targetFileId && !clipBuffer) {
|
||||
try {
|
||||
const res = await fetch('/static/audio/uploads/' + targetFileId);
|
||||
if (res.ok) {
|
||||
const blob = await res.blob();
|
||||
const { audioBuffer } = await window.SonicAudio.decodeAudioFile(blob);
|
||||
clipBuffer = audioBuffer;
|
||||
clipsUpdated = true;
|
||||
hasLoadedAny = true;
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to autoload clip buffer for " + targetFileId, e);
|
||||
}
|
||||
}
|
||||
return { ...c, buffer: clipBuffer };
|
||||
}));
|
||||
|
||||
if (trackBuffer && updatedClips.length === 0) {
|
||||
const clipId = `default_${t.id}`;
|
||||
return {
|
||||
...t,
|
||||
buffer: trackBuffer,
|
||||
channelInfo: trackChannelInfo,
|
||||
clips: [{ id: clipId, buffer: trackBuffer, startTime: t.startTime || 0, name: t.name, speed: 1.0 }]
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...t,
|
||||
buffer: trackBuffer,
|
||||
channelInfo: trackChannelInfo,
|
||||
clips: updatedClips
|
||||
};
|
||||
}));
|
||||
|
||||
if (hasLoadedAny) {
|
||||
setTracks(updatedTracks);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenProject = async (projectId, projectName) => {
|
||||
setAppWarningModal({
|
||||
title: "Mở dự án",
|
||||
message: "Bạn có muốn mở dự án này? Các thay đổi chưa lưu trên workspace hiện tại sẽ bị mất.",
|
||||
@@ -4630,6 +4694,7 @@ const ProfileModal = ({
|
||||
});
|
||||
}
|
||||
setTracks(restoredTracks);
|
||||
loadAudioBuffersForTracks(restoredTracks);
|
||||
setBpm(restoredBpm.toString());
|
||||
setSelectedTrackId(restoredTracks[0]?.id || '1');
|
||||
setProjectName(proj.name);
|
||||
@@ -4741,10 +4806,10 @@ const ProfileModal = ({
|
||||
}, /*#__PURE__*/React.createElement("button", {
|
||||
onClick: () => setConfirmModal(null),
|
||||
className: "px-3 py-1.5 bg-zinc-700 hover:bg-zinc-600 text-slate-300 rounded text-xs font-semibold"
|
||||
}, "Hủy"), /*#__PURE__*/React.createElement("button", {
|
||||
}, confirmModal.cancelText || "Hủy"), /*#__PURE__*/React.createElement("button", {
|
||||
onClick: () => { const fn = confirmModal.onConfirm; setConfirmModal(null); fn(); },
|
||||
className: "px-3 py-1.5 bg-rose-700 hover:bg-rose-600 text-white rounded text-xs font-semibold"
|
||||
}, "Xác nhận xóa")))), /*#__PURE__*/React.createElement("div", {
|
||||
}, confirmModal.confirmText || "Xác nhận xóa")))), /*#__PURE__*/React.createElement("div", {
|
||||
key: "dialog",
|
||||
className: "fixed z-50 bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-2xl p-6 text-slate-200 flex flex-col max-h-[85vh]",
|
||||
style: modalStyle
|
||||
@@ -4903,59 +4968,183 @@ const ProfileModal = ({
|
||||
])
|
||||
] : null)));
|
||||
};
|
||||
|
||||
const SaveProjectModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave
|
||||
onSaveCloud,
|
||||
onSaveLocal,
|
||||
projectName
|
||||
}) => {
|
||||
if (!isOpen) return null;
|
||||
const [name, setName] = useState('');
|
||||
const [name, setName] = useState(projectName || '');
|
||||
const [saveType, setSaveType] = useState(localStorage.getItem('sonic_token') ? 'cloud' : 'local');
|
||||
const [cloudProjects, setCloudProjects] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedExisting, setSelectedExisting] = useState(null);
|
||||
const [confirmOverwriteProject, setConfirmOverwriteProject] = useState(null);
|
||||
React.useEffect(function() {
|
||||
if (!isOpen) return;
|
||||
if (saveType === 'cloud' && window.SonicAPI && window.SonicAPI.listCloudProjects) {
|
||||
setLoading(true);
|
||||
window.SonicAPI.listCloudProjects().then(function(data) { setCloudProjects(data || []); }).catch(function() { setCloudProjects([]); }).finally(function() { setLoading(false); });
|
||||
}
|
||||
}, [isOpen, saveType]);
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
if (!name.trim()) return;
|
||||
onSave(name.trim());
|
||||
onClose();
|
||||
var matched = null;
|
||||
for (var i = 0; i < cloudProjects.length; i++) { if (cloudProjects[i].name === name.trim()) { matched = cloudProjects[i]; break; } }
|
||||
if (matched) {
|
||||
setConfirmOverwriteProject(matched);
|
||||
} else {
|
||||
if (saveType === 'cloud') { onSaveCloud(name.trim(), null); onClose(); }
|
||||
else { onSaveLocal(name.trim()); onClose(); }
|
||||
}
|
||||
};
|
||||
if (confirmOverwriteProject) {
|
||||
return React.createElement("div", {
|
||||
className: "fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"
|
||||
}, React.createElement("div", {
|
||||
className: "bg-[#262626] border border-[#383838] rounded-lg shadow-2xl p-5 max-w-sm w-full text-slate-200"
|
||||
}, React.createElement("h4", {
|
||||
className: "text-sm font-bold text-amber-400 mb-2"
|
||||
}, "Ghi \u0111\u00E8 d\u1EF1 \u00E1n"), React.createElement("p", {
|
||||
className: "text-xs text-slate-300 mb-4"
|
||||
}, "D\u1EF1 \u00E1n \"", confirmOverwriteProject.name, "\" \u0111\u00E3 t\u1ED3n t\u1EA1i tr\u00EAn Cloud. B\u1EA1n c\u00F3 ch\u1EAFc ch\u1EAFn mu\u1ED1n ghi \u0111\u00E8 n\u00F3 kh\u00F4ng?"), React.createElement("div", {
|
||||
className: "flex justify-end gap-2"
|
||||
}, React.createElement("button", {
|
||||
type: "button", onClick: function() { setConfirmOverwriteProject(null); },
|
||||
className: "px-3 py-1.5 bg-zinc-700 hover:bg-zinc-600 text-slate-300 rounded text-xs font-semibold"
|
||||
}, "Quay l\u1EA1i"), React.createElement("button", {
|
||||
type: "button", onClick: function() {
|
||||
setConfirmOverwriteProject(null);
|
||||
onSaveCloud(name.trim(), confirmOverwriteProject.id);
|
||||
onClose();
|
||||
},
|
||||
className: "px-3 py-1.5 bg-rose-700 hover:bg-rose-600 text-white rounded text-xs font-semibold"
|
||||
}, "X\u00E1c nh\u1EADn ghi \u0111\u00E8"))));
|
||||
}
|
||||
var filtered = cloudProjects.filter(function(p) { return p.name.toLowerCase().includes(name.toLowerCase()); });
|
||||
return /*#__PURE__*/React.createElement("div", {
|
||||
className: "fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-md p-6 text-slate-200"
|
||||
className: "bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-lg p-6 text-slate-200"
|
||||
}, /*#__PURE__*/React.createElement("h3", {
|
||||
className: "text-sm font-bold text-teal-400 mb-4 uppercase"
|
||||
}, "Đặt tên dự án"), /*#__PURE__*/React.createElement("form", {
|
||||
}, "L\u01B0u d\u1EF1 \u00E1n"), /*#__PURE__*/React.createElement("form", {
|
||||
onSubmit: handleSubmit,
|
||||
className: "space-y-4"
|
||||
}, [
|
||||
/*#__PURE__*/React.createElement("input", {
|
||||
key: "name-input",
|
||||
type: "text",
|
||||
placeholder: "Nhập tên dự án...",
|
||||
required: true,
|
||||
value: name,
|
||||
onChange: e => setName(e.target.value),
|
||||
className: "w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs text-slate-200 focus:outline-none focus:border-teal-500 font-bold",
|
||||
autoFocus: true
|
||||
}),
|
||||
/*#__PURE__*/React.createElement("div", {
|
||||
key: "actions",
|
||||
className: "flex justify-end gap-2 text-xs"
|
||||
}, [
|
||||
/*#__PURE__*/React.createElement("button", {
|
||||
key: "cancel",
|
||||
type: "button",
|
||||
onClick: onClose,
|
||||
className: "px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded"
|
||||
}, "Hủy"),
|
||||
/*#__PURE__*/React.createElement("button", {
|
||||
key: "save",
|
||||
type: "submit",
|
||||
className: "px-3 py-1.5 bg-teal-600 hover:bg-teal-500 text-white rounded font-semibold"
|
||||
}, "Lưu")
|
||||
])
|
||||
/*#__PURE__*/React.createElement("div", { key: "type-block" }, [
|
||||
/*#__PURE__*/React.createElement("div", { className: "grid grid-cols-2 gap-2 text-xs" }, [
|
||||
/*#__PURE__*/React.createElement("button", {
|
||||
key: "btn-cloud", type: "button", onClick: function() { setSaveType('cloud'); setSelectedExisting(null); },
|
||||
className: "py-2 rounded border flex flex-col items-center gap-1 font-semibold transition " + (saveType === 'cloud' ? 'bg-teal-950/60 border-teal-500 text-teal-300' : 'bg-zinc-800 border-zinc-700 hover:bg-zinc-750 text-zinc-400')
|
||||
}, [/*#__PURE__*/React.createElement("span", { key: "title" }, "\u2601\uFE0F L\u01B0u Cloud"), /*#__PURE__*/React.createElement("span", { key: "desc", className: "text-[9px] font-normal text-zinc-500" }, "L\u01B0u l\u00EAn server")]),
|
||||
/*#__PURE__*/React.createElement("button", {
|
||||
key: "btn-local", type: "button", onClick: function() { setSaveType('local'); setSelectedExisting(null); },
|
||||
className: "py-2 rounded border flex flex-col items-center gap-1 font-semibold transition " + (saveType === 'local' ? 'bg-teal-950/60 border-teal-500 text-teal-300' : 'bg-zinc-800 border-zinc-700 hover:bg-zinc-750 text-zinc-400')
|
||||
}, [/*#__PURE__*/React.createElement("span", { key: "title" }, "\U0001F5C2\uFE0F L\u01B0u Local"), /*#__PURE__*/React.createElement("span", { key: "desc", className: "text-[9px] font-normal text-zinc-500" }, "L\u01B0u xu\u1ED1ng t\u1EC7p .sfs")])
|
||||
])
|
||||
]),
|
||||
/*#__PURE__*/React.createElement("div", { key: "name-block" }, [
|
||||
/*#__PURE__*/React.createElement("label", { className: "block text-[10px] text-zinc-400 uppercase font-bold mb-1" }, "T\u00EAn d\u1EF1 \u00E1n"),
|
||||
/*#__PURE__*/React.createElement("input", {
|
||||
key: "name-input", type: "text", placeholder: "Nh\u1EADp t\u00EAn d\u1EF1 \u00E1n...", required: true,
|
||||
value: name,
|
||||
onChange: function(e) { setName(e.target.value); setSelectedExisting(null); },
|
||||
className: "w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs text-slate-200 focus:outline-none focus:border-teal-500 font-bold", autoFocus: true
|
||||
})
|
||||
]),
|
||||
saveType === 'cloud' && /*#__PURE__*/React.createElement("div", { key: "cloud-list", className: "max-h-40 overflow-y-auto -mx-1" },
|
||||
loading ? [/*#__PURE__*/React.createElement("div", { key: "l", className: "text-center py-3 text-xs text-zinc-500" }, "\u0110ang t\u1EA3i danh s\u00E1ch d\u1EF1 \u00E1n...")]
|
||||
: filtered.length === 0 ? [/*#__PURE__*/React.createElement("div", { key: "e", className: "text-center py-3 text-xs text-zinc-500" }, "Ch\u01B0a c\u00F3 d\u1EF1 \u00E1n n\u00E0o.")]
|
||||
: filtered.map(function(p) {
|
||||
var isSelected = selectedExisting && selectedExisting.id === p.id;
|
||||
var isExactMatch = p.name === name.trim();
|
||||
return /*#__PURE__*/React.createElement("div", {
|
||||
key: p.id,
|
||||
onClick: function() { setName(p.name); setSelectedExisting(p); },
|
||||
className: "flex items-center justify-between px-2 py-1.5 rounded cursor-pointer text-xs border transition " + (isSelected ? 'bg-amber-950/60 border-amber-600 text-amber-300' : 'bg-[#1e1e1e] hover:bg-zinc-800 border-transparent text-slate-300')
|
||||
}, [/*#__PURE__*/React.createElement("span", { key: "n", className: "font-semibold truncate" }, p.name), /*#__PURE__*/React.createElement("span", { key: "t", className: "text-[9px] text-zinc-500 shrink-0 ml-2" }, isExactMatch ? "S\u1EBD ghi \u0111\u00E8" : "Ch\u1ECDn")]);
|
||||
})
|
||||
),
|
||||
/*#__PURE__*/React.createElement("div", { key: "actions", className: "flex justify-end gap-2 text-xs" }, [
|
||||
/*#__PURE__*/React.createElement("button", { key: "cancel", type: "button", onClick: onClose, className: "px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded" }, "H\u1EE7y"),
|
||||
selectedExisting ? /*#__PURE__*/React.createElement("button", { key: "back", type: "button", onClick: function() { setName(''); setSelectedExisting(null); }, className: "px-3 py-1.5 bg-zinc-700 hover:bg-zinc-600 text-slate-300 rounded text-xs font-semibold" }, "Quay l\u1EA1i") : null,
|
||||
/*#__PURE__*/React.createElement("button", { key: "save", type: "submit", className: "px-3 py-1.5 bg-teal-600 hover:bg-teal-500 text-white rounded font-semibold" }, selectedExisting ? "Ghi \u0111\u00E8" : "L\u01B0u")
|
||||
])
|
||||
])));
|
||||
};
|
||||
|
||||
const OpenProjectModal = ({ isOpen, onClose, onOpenCloud, onOpenLocal }) => {
|
||||
if (!isOpen) return null;
|
||||
const [tab, setTab] = useState('cloud');
|
||||
const [projects, setProjects] = useState([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
React.useEffect(function() {
|
||||
if (!isOpen) return;
|
||||
if (tab === 'cloud') {
|
||||
setLoading(true);
|
||||
var api = window.SonicAPI;
|
||||
if (api && api.listCloudProjects) {
|
||||
api.listCloudProjects().then(function(data) { setProjects(data || []); }).catch(function() { setProjects([]); }).finally(function() { setLoading(false); });
|
||||
} else { setLoading(false); }
|
||||
}
|
||||
}, [isOpen, tab]);
|
||||
return React.createElement("div", {
|
||||
className: "fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"
|
||||
}, React.createElement("div", {
|
||||
className: "bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-lg p-6 text-slate-200"
|
||||
}, React.createElement("h3", {
|
||||
className: "text-sm font-bold text-teal-400 mb-4 uppercase"
|
||||
}, "Mở dự án"), React.createElement("div", {
|
||||
className: "grid grid-cols-2 gap-2 text-xs mb-4"
|
||||
}, [
|
||||
React.createElement("button", {
|
||||
key: "cloud-tab", type: "button", onClick: function() { setTab('cloud'); },
|
||||
className: "py-2 rounded border flex flex-col items-center gap-1 font-semibold transition " + (tab === 'cloud' ? 'bg-teal-950/60 border-teal-500 text-teal-300' : 'bg-zinc-800 border-zinc-700 hover:bg-zinc-750 text-zinc-400')
|
||||
}, [React.createElement("span", { key: "t" }, "☁️ Cloud"), React.createElement("span", { key: "d", className: "text-[9px] font-normal text-zinc-500" }, "Dự án trên server")]),
|
||||
React.createElement("button", {
|
||||
key: "local-tab", type: "button", onClick: function() { setTab('local'); },
|
||||
className: "py-2 rounded border flex flex-col items-center gap-1 font-semibold transition " + (tab === 'local' ? 'bg-teal-950/60 border-teal-500 text-teal-300' : 'bg-zinc-800 border-zinc-700 hover:bg-zinc-750 text-zinc-400')
|
||||
}, [React.createElement("span", { key: "t" }, "💾 Local"), React.createElement("span", { key: "d", className: "text-[9px] font-normal text-zinc-500" }, "Tập tin .sfs trên máy")])
|
||||
]),
|
||||
tab === 'cloud' ?
|
||||
React.createElement("div", { className: "space-y-1.5 max-h-64 overflow-y-auto" },
|
||||
loading ? [React.createElement("div", { key: "l", className: "text-center py-8 text-xs text-zinc-500" }, "Đang tải danh sách dự án...")]
|
||||
: projects.length === 0 ? [React.createElement("div", { key: "e", className: "text-center py-8 text-xs text-zinc-500" }, "Bạn chưa có dự án nào trên Cloud.")]
|
||||
: projects.map(function(p) {
|
||||
return React.createElement("div", {
|
||||
key: p.id,
|
||||
onClick: function() { onOpenCloud(p.id, p.name); },
|
||||
className: "flex items-center justify-between p-3 bg-[#1e1e1e] hover:bg-zinc-800 rounded border border-zinc-800 transition cursor-pointer text-xs group"
|
||||
}, [
|
||||
React.createElement("div", { key: "meta" }, [
|
||||
React.createElement("div", { className: "font-bold text-slate-200 group-hover:text-teal-400" }, p.name),
|
||||
React.createElement("div", { className: "text-[10px] text-zinc-500 mt-0.5" }, "Dung lượng: " + (p.size_mb || 0) + " MB | Cập nhật: " + new Date((p.updated_at || 0) * 1000).toLocaleString())
|
||||
]),
|
||||
React.createElement("button", {
|
||||
onClick: function(e) { e.stopPropagation(); onOpenCloud(p.id, p.name); },
|
||||
className: "px-2.5 py-1 bg-teal-800/80 hover:bg-teal-700 text-teal-200 rounded font-semibold text-[10px]"
|
||||
}, "MỞ")
|
||||
]);
|
||||
})
|
||||
)
|
||||
: React.createElement("div", { className: "py-4 text-center text-xs text-zinc-400 space-y-3" },
|
||||
[React.createElement("div", { key: "d", className: "text-zinc-500" }, "Chọn tệp .sfs để mở dự án từ Local."),
|
||||
React.createElement("button", {
|
||||
key: "b", onClick: function() { onOpenLocal(); },
|
||||
className: "px-4 py-2 bg-teal-600 hover:bg-teal-500 text-white rounded font-semibold text-xs transition"
|
||||
}, "Chọn tệp .sfs ...")]
|
||||
),
|
||||
React.createElement("div", { className: "flex justify-end gap-2 text-xs mt-4 pt-3 border-t border-zinc-800" },
|
||||
React.createElement("button", {
|
||||
type: "button", onClick: onClose,
|
||||
className: "px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded"
|
||||
}, "Hủy"))));
|
||||
};
|
||||
|
||||
const SaveAsModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
@@ -5256,6 +5445,46 @@ const AIPresetModal = ({ isOpen, onClose }) => {
|
||||
setFormTemplate('');
|
||||
};
|
||||
|
||||
const handleNewStructured = () => {
|
||||
setEditingPreset('new');
|
||||
setFormName('');
|
||||
setFormKeywords('');
|
||||
setFormCategory('Orchestral / Film Score');
|
||||
setFormBars(8);
|
||||
setFormBpm(120);
|
||||
setFormScale('C Minor');
|
||||
setFormTemplate(`[Mô tả thể loại / phong cách]
|
||||
Ví dụ: Nhạc phim epic, dàn nhạc giao hưởng, tempo 130 BPM, giọng Cm.
|
||||
|
||||
[Cấu trúc bố cục]
|
||||
- Intro (2 ô nhịp): ...
|
||||
- Phát triển (4 ô nhịp): ...
|
||||
- Climax / Cao trào (2 ô nhịp): ...
|
||||
|
||||
[Yêu cầu về nhạc cụ / Track]
|
||||
1. Track 1 - Strings:
|
||||
- Vai trò: ...
|
||||
- Quãng: ...
|
||||
2. Track 2 - Brass:
|
||||
- Vai trò: ...
|
||||
- Quãng: ...
|
||||
3. Track 3 - Percussion:
|
||||
- Vai trò: ...
|
||||
|
||||
[Yêu cầu hòa âm / Giai điệu]
|
||||
- Tiến trình hợp âm: ...
|
||||
- Pattern giai điệu: ...
|
||||
|
||||
[Hiệu ứng / Sắc thái]
|
||||
- Dynamics: ...
|
||||
- Articulation: ...
|
||||
|
||||
[Lưu ý kỹ thuật MIDI]
|
||||
- Viết note liên tục trên toàn bộ số ô nhịp.
|
||||
- Dùng whole note, half note, quarter note.
|
||||
- Tránh chồng note quá dày.`);
|
||||
};
|
||||
|
||||
const handleToggleFav = (id) => {
|
||||
mgr.toggleFavorite(id);
|
||||
setPresets([...mgr.getPresets()]);
|
||||
@@ -5498,8 +5727,14 @@ const AIPresetModal = ({ isOpen, onClose }) => {
|
||||
rows: 6,
|
||||
className: "flex-1 bg-zinc-900 border border-zinc-700 rounded p-2.5 text-xs outline-none focus:border-purple-600 text-zinc-200 font-mono resize-none"
|
||||
})), /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex items-center justify-end gap-2 shrink-0 pt-2 border-t border-zinc-800"
|
||||
className: "flex items-center gap-2 shrink-0 pt-2 border-t border-zinc-800"
|
||||
}, /*#__PURE__*/React.createElement("button", {
|
||||
type: "button",
|
||||
onClick: handleNewStructured,
|
||||
className: "px-3 py-1.5 bg-emerald-800 hover:bg-emerald-700 text-emerald-300 border border-emerald-700 rounded text-xs transition"
|
||||
}, "Tạo preset với cấu trúc"), /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex-1"
|
||||
}), /*#__PURE__*/React.createElement("button", {
|
||||
type: "button",
|
||||
onClick: () => setEditingPreset(null),
|
||||
className: "px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-400 border border-zinc-700 rounded text-xs transition"
|
||||
@@ -7266,7 +7501,9 @@ const beatSec = 60.0 / (parseInt(bpm) || 120);
|
||||
const serializeTracksList = (tracksList, secondsPerBar) => {
|
||||
return (tracksList || []).map(t => {
|
||||
let trackType = "AUDIO";
|
||||
if (t.sections && t.sections.length > 0) trackType = "SECTION";
|
||||
if (t.type === 'MIDI' || t.type === 'soundfont' || t.type === 'vst3') trackType = "MIDI";
|
||||
else if (t.type === 'SECTION') trackType = "SECTION";
|
||||
else if (t.sections && t.sections.length > 0) trackType = "SECTION";
|
||||
else if (t.midiItems && t.midiItems.length > 0) trackType = "MIDI";
|
||||
|
||||
const items = [];
|
||||
@@ -7399,18 +7636,27 @@ const deserializeTracksList = (schemaTracks, secondsPerBar, sectionStore) => {
|
||||
return {
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
type: t.type === 'MIDI' ? 'MIDI' : (t.type === 'SECTION' ? 'SECTION' : 'audio'),
|
||||
volumeDb: t.volume_db || 0.0,
|
||||
pan: t.pan || 0.0,
|
||||
muted: t.mute || false,
|
||||
solo: t.solo || false,
|
||||
color: t.id === '1' ? '#0f766e' : '#1d4ed8',
|
||||
markers: [],
|
||||
serverFileId: t.items && t.items.find(i => i.type === "AUDIO_ITEM")?.source_data?.audio_file_url?.split("/").pop() || null,
|
||||
color: t.color || (t.id === '1' ? '#0f766e' : '#1d4ed8'),
|
||||
startTime: t.start_time || 0,
|
||||
height: t.height || 140,
|
||||
markers: t.markers || [],
|
||||
serverFileId: t.server_file_id || (t.items && t.items.find(function(i) { return i.type === 'AUDIO_ITEM'; })?.source_data?.audio_file_url?.split('/').pop()) || null,
|
||||
channelInfo: t.channel_info || null,
|
||||
isArmed: t.is_armed || false,
|
||||
monitoringEnabled: t.monitoring_enabled !== false,
|
||||
inputSource: t.input_source ? { deviceType: t.input_source.device_type || 'NONE', deviceId: t.input_source.device_id || '' } : { deviceType: 'NONE', deviceId: '' },
|
||||
midiChannel: t.midi_channel != null ? t.midi_channel : undefined,
|
||||
is_percussion: t.is_percussion || false,
|
||||
clips: clips,
|
||||
sections: sections,
|
||||
midiItems: midiItems,
|
||||
instrumentId: t.instrument_id || null,
|
||||
instrumentProgram: t.instrument_program !== null ? t.instrument_program : undefined,
|
||||
instrumentId: t.instrumentId != null ? t.instrumentId : (t.instrument_id || null),
|
||||
instrumentProgram: t.instrumentProgram !== undefined && t.instrumentProgram !== null ? t.instrumentProgram : (t.instrument_program !== null ? t.instrument_program : undefined),
|
||||
instrumentName: t.instrument_name || null,
|
||||
instrument_source: t.instrument_source || null,
|
||||
soundfont_id: t.soundfont_id || null,
|
||||
@@ -8805,6 +9051,7 @@ const App = () => {
|
||||
const [currentProjectId, setCurrentProjectId] = useState(() => localStorage.getItem('sonic_project_id') || null);
|
||||
const [saveProjectModalOpen, setSaveProjectModalOpen] = useState(false);
|
||||
const [saveAsModalOpen, setSaveAsModalOpen] = useState(false);
|
||||
const [openProjectModalOpen, setOpenProjectModalOpen] = useState(false);
|
||||
const soloedTrack = tracks.find(t => t.solo);
|
||||
const soloedTrackId = soloedTrack ? soloedTrack.id : null;
|
||||
const [toastMessage, setToastMessage] = useState(null);
|
||||
@@ -9548,8 +9795,69 @@ const App = () => {
|
||||
applyMasteringSettings(masteringSettings);
|
||||
}
|
||||
}, [masteringSettings]);
|
||||
|
||||
const [pluginManagerModalOpen, setPluginManagerModalOpen] = useState(false);
|
||||
const [pluginsData, setPluginsData] = useState(null);
|
||||
|
||||
const restoreLastSessionProject = async () => {
|
||||
var pendingWasNull = !window.__pendingSfsProject;
|
||||
loadPendingSfsProject();
|
||||
if (!pendingWasNull) return;
|
||||
|
||||
var lastId = localStorage.getItem('sonic_project_id');
|
||||
if (!lastId) return;
|
||||
var lastName = localStorage.getItem('sonic_project_name') || 'Dự án';
|
||||
try {
|
||||
var parsed = null;
|
||||
if (lastId.startsWith('local_')) {
|
||||
var localData = localStorage.getItem('sonic_local_project_data');
|
||||
if (localData) parsed = JSON.parse(localData);
|
||||
} else {
|
||||
var proj = await window.SonicAPI.getCloudProject(lastId);
|
||||
if (proj) parsed = JSON.parse(proj.data_json);
|
||||
}
|
||||
if (!parsed) return;
|
||||
var restoredBpm = bpm;
|
||||
var restoredTracks = [];
|
||||
var restoredSessionTabs = [];
|
||||
var restoredSubTabs = [];
|
||||
if (parsed.main_session) {
|
||||
var result = deserializeProjectFromSchema(parsed);
|
||||
restoredTracks = result.tracks;
|
||||
restoredBpm = result.bpm;
|
||||
restoredSessionTabs = result.sessionTabs;
|
||||
restoredSubTabs = result.subTabs;
|
||||
if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
|
||||
} else {
|
||||
restoredTracks = (parsed.tracks || []).map(function(t) {
|
||||
var rest = Object.assign({}, t); delete rest.height;
|
||||
return Object.assign({}, rest, { buffer: null, channelInfo: t.channelInfo || null, clips: t.clips || [], serverFileId: t.serverFileId || null });
|
||||
});
|
||||
}
|
||||
setTracks(restoredTracks);
|
||||
loadAudioBuffersForTracks(restoredTracks);
|
||||
setBpm(restoredBpm.toString());
|
||||
setSelectedTrackId(restoredTracks[0]?.id || '1');
|
||||
setProjectName(lastName);
|
||||
setCurrentProjectId(lastId);
|
||||
localStorage.setItem('sonic_project_id', lastId);
|
||||
setSessionTabs(restoredSessionTabs);
|
||||
setSubTabs(restoredSubTabs);
|
||||
var restoredItemCount = 0;
|
||||
restoredTracks.forEach(function(rt) {
|
||||
if (rt.clips) restoredItemCount += rt.clips.length;
|
||||
if (rt.midiItems) restoredItemCount += rt.midiItems.length;
|
||||
if (rt.sections) restoredItemCount += rt.sections.length;
|
||||
});
|
||||
showToast('Đã khôi phục dự án "' + lastName + '" (' + restoredItemCount + ' items).', 'info');
|
||||
} catch(e) {
|
||||
if (!lastId.startsWith('local_')) {
|
||||
localStorage.removeItem('sonic_project_id');
|
||||
localStorage.removeItem('sonic_project_name');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuthStatus = async () => {
|
||||
const savedToken = localStorage.getItem('sonic_token');
|
||||
@@ -9570,6 +9878,7 @@ const App = () => {
|
||||
} else {
|
||||
setIsMandatoryLogin(false);
|
||||
setAuthModalOpen(false);
|
||||
restoreLastSessionProject();
|
||||
}
|
||||
} catch (err) {
|
||||
const cached = localStorage.getItem('sonic_user');
|
||||
@@ -9577,6 +9886,7 @@ const App = () => {
|
||||
try { setCurrentUser(JSON.parse(cached)); } catch (_) { }
|
||||
setIsMandatoryLogin(false);
|
||||
setAuthModalOpen(false);
|
||||
restoreLastSessionProject();
|
||||
} else {
|
||||
localStorage.removeItem('sonic_token');
|
||||
localStorage.removeItem('sonic_user');
|
||||
@@ -9589,6 +9899,7 @@ const App = () => {
|
||||
};
|
||||
checkAuthStatus();
|
||||
}, []);
|
||||
|
||||
const loadPendingSfsProject = () => {
|
||||
const proj = window.__pendingSfsProject;
|
||||
if (!proj) return;
|
||||
@@ -9666,7 +9977,7 @@ const App = () => {
|
||||
} else {
|
||||
setIsMandatoryLogin(false);
|
||||
setAuthModalOpen(false);
|
||||
loadPendingSfsProject();
|
||||
restoreLastSessionProject();
|
||||
const loadPrefs = (p) => {
|
||||
if (!p) return;
|
||||
if (p.showAIPanel !== undefined) setShowAIPanel(p.showAIPanel);
|
||||
@@ -10373,13 +10684,7 @@ const App = () => {
|
||||
}
|
||||
if (ctrl && !alt && e.key === 's') {
|
||||
e.preventDefault();
|
||||
var curTab = activeTabRef.current;
|
||||
if (curTab.startsWith('session_')) {
|
||||
handleSaveSectionTabRef.current(curTab);
|
||||
showToast('Đã lưu Section', 'success');
|
||||
} else {
|
||||
handleExportSFS();
|
||||
}
|
||||
handleSaveProjectRef.current();
|
||||
return;
|
||||
}
|
||||
if (ctrl && alt && e.key === 's' || ctrl && e.shiftKey && e.key === 's') {
|
||||
@@ -10473,6 +10778,8 @@ const App = () => {
|
||||
e.preventDefault();
|
||||
const curTab = activeTabRef.current;
|
||||
if (curTab === 'main') {
|
||||
// handled by main handler
|
||||
} else if (curTab.startsWith('session_')) {
|
||||
// Main session: save project + save all dirty sub-tabs
|
||||
handleSaveProject();
|
||||
subTabsRef.current.filter(s => s.isDirty).forEach(st => {
|
||||
@@ -15454,49 +15761,79 @@ const App = () => {
|
||||
clientSideExport(exportTracks);
|
||||
}
|
||||
};
|
||||
const handleSaveProject = async () => {
|
||||
if (!currentUser) { setIsMandatoryLogin(false); setAuthMode('login'); setAuthModalOpen(true); return; }
|
||||
if (!projectName) {
|
||||
setSaveProjectModalOpen(true);
|
||||
return;
|
||||
const handleSaveLocalProject = (name) => {
|
||||
const finalName = name || projectName || 'Dự án mới';
|
||||
const localId = currentProjectId && currentProjectId.startsWith('local_') ? currentProjectId : 'local_' + Date.now();
|
||||
const projectSchemaObj = serializeProjectToSchema(localId, finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings);
|
||||
const dataStr = JSON.stringify(projectSchemaObj);
|
||||
localStorage.setItem('sonic_local_project_data', dataStr);
|
||||
localStorage.setItem('sonic_project_id', localId);
|
||||
localStorage.setItem('sonic_project_name', finalName);
|
||||
setCurrentProjectId(localId);
|
||||
setProjectName(finalName);
|
||||
window.SonicStorage.exportProjectToSFS(projectSchemaObj);
|
||||
showToast(`Đã lưu dự án local "${finalName}" thành công!`, "success");
|
||||
};
|
||||
|
||||
const handleSaveCloudProject = async (name, existingProjectId) => {
|
||||
const finalName = name || projectName || 'Dự án mới';
|
||||
var useProjectId = existingProjectId || currentProjectId;
|
||||
const projectSchemaObj = serializeProjectToSchema(useProjectId || 'project_' + Date.now(), finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings);
|
||||
// Diagnostic: count source vs serialized items
|
||||
var srcMidi = 0, srcClip = 0, srcSec = 0;
|
||||
tracks.forEach(function(t) { srcMidi += (t.midiItems||[]).length; srcClip += (t.clips||[]).length; srcSec += (t.sections||[]).length; });
|
||||
var serItems = 0;
|
||||
if (projectSchemaObj.main_session && projectSchemaObj.main_session.tracks) {
|
||||
projectSchemaObj.main_session.tracks.forEach(function(st) { if (st.items) serItems += st.items.length; });
|
||||
}
|
||||
const projectSchemaObj = serializeProjectToSchema(currentProjectId || 'project_' + Date.now(), projectName, bpm, tracks, subTabs, sessionTabs, masteringSettings);
|
||||
Object.keys(projectSchemaObj.section_store || {}).forEach(function(sk) {
|
||||
(projectSchemaObj.section_store[sk].tracks || []).forEach(function(st) { if (st.items) serItems += st.items.length; });
|
||||
});
|
||||
const dataJson = JSON.stringify(projectSchemaObj);
|
||||
try {
|
||||
if (currentProjectId) {
|
||||
await window.SonicAPI.updateCloudProject(currentProjectId, projectName, dataJson);
|
||||
showToast(`Đã cập nhật dự án "${projectName}" lên server!`, "success");
|
||||
if (existingProjectId) {
|
||||
await window.SonicAPI.updateCloudProject(existingProjectId, finalName, dataJson);
|
||||
setProjectName(finalName);
|
||||
setCurrentProjectId(existingProjectId);
|
||||
localStorage.setItem('sonic_project_name', finalName);
|
||||
localStorage.setItem('sonic_project_id', existingProjectId);
|
||||
showToast('Đã ghi đè Cloud "' + finalName + '" (src midi=' + srcMidi + ' clip=' + srcClip + ' sec=' + srcSec + ' | ser=' + serItems + ').', 'success');
|
||||
} else if (currentProjectId && !currentProjectId.startsWith('local_')) {
|
||||
await window.SonicAPI.updateCloudProject(currentProjectId, finalName, dataJson);
|
||||
setProjectName(finalName);
|
||||
localStorage.setItem('sonic_project_name', finalName);
|
||||
localStorage.setItem('sonic_project_id', currentProjectId);
|
||||
showToast('Đã lưu Cloud "' + finalName + '" (src midi=' + srcMidi + ' clip=' + srcClip + ' sec=' + srcSec + ' | ser=' + serItems + ' id=' + currentProjectId + ').', serItems === 0 ? 'warning' : 'success');
|
||||
} else {
|
||||
const res = await window.SonicAPI.saveCloudProject(projectName, dataJson);
|
||||
const res = await window.SonicAPI.saveCloudProject(finalName, dataJson);
|
||||
const newProjId = res.project_id;
|
||||
setProjectName(finalName);
|
||||
localStorage.setItem('sonic_project_name', finalName);
|
||||
if (newProjId) {
|
||||
setCurrentProjectId(newProjId);
|
||||
localStorage.setItem('sonic_project_id', newProjId);
|
||||
}
|
||||
showToast(`Đã lưu dự án "${projectName}" mới lên server!`, "success");
|
||||
showToast('Đã lưu Cloud mới "' + finalName + '" (src midi=' + srcMidi + ' clip=' + srcClip + ' sec=' + srcSec + ' | ser=' + serItems + ').', serItems === 0 ? 'warning' : 'success');
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(err.message || "Lỗi lưu server", "error");
|
||||
showToast(err.message || "Lỗi lưu dự án lên Cloud", "error");
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveProjectWithName = async (newName) => {
|
||||
setProjectName(newName);
|
||||
localStorage.setItem('sonic_project_name', newName);
|
||||
const projectSchemaObj = serializeProjectToSchema('project_' + Date.now(), newName, bpm, tracks, subTabs, sessionTabs, masteringSettings);
|
||||
const dataJson = JSON.stringify(projectSchemaObj);
|
||||
try {
|
||||
const res = await window.SonicAPI.saveCloudProject(newName, dataJson);
|
||||
const newProjId = res.project_id;
|
||||
if (newProjId) {
|
||||
setCurrentProjectId(newProjId);
|
||||
localStorage.setItem('sonic_project_id', newProjId);
|
||||
}
|
||||
showToast(`Đã lưu dự án "${newName}" mới lên server!`, "success");
|
||||
} catch (err) {
|
||||
showToast(err.message || "Lỗi lưu server", "error");
|
||||
const handleSaveProject = async () => {
|
||||
if (!currentProjectId) {
|
||||
setSaveProjectModalOpen(true);
|
||||
return;
|
||||
}
|
||||
if (currentProjectId.startsWith('local_')) {
|
||||
handleSaveLocalProject(projectName);
|
||||
} else {
|
||||
if (!currentUser) { showToast('Cần đăng nhập để lưu Cloud. currentUser=' + (currentUser ? 'OK' : 'NULL') + ' token=' + (localStorage.getItem('sonic_token') ? 'exists' : 'missing'), 'warning'); return; }
|
||||
handleSaveCloudProject(projectName);
|
||||
}
|
||||
};
|
||||
const handleSaveProjectRef = useRef(handleSaveProject);
|
||||
handleSaveProjectRef.current = handleSaveProject;
|
||||
|
||||
const handleSaveAsCloud = async (newName) => {
|
||||
const projectSchemaObj = serializeProjectToSchema('project_' + Date.now(), newName, bpm, tracks, subTabs, sessionTabs, masteringSettings);
|
||||
@@ -15531,20 +15868,37 @@ const App = () => {
|
||||
if (!e.target.files[0]) return;
|
||||
try {
|
||||
const proj = await window.SonicStorage.importProjectFromSFSFile(e.target.files[0]);
|
||||
const restored = (proj.tracks || []).map(t => ({
|
||||
...t,
|
||||
buffer: null,
|
||||
channelInfo: t.channelInfo || null,
|
||||
clips: t.clips || [],
|
||||
serverFileId: t.serverFileId || null
|
||||
}));
|
||||
let restored = [];
|
||||
let restoredBpm = bpm;
|
||||
let restoredSessionTabs = [];
|
||||
let restoredSubTabs = [];
|
||||
if (proj.main_session) {
|
||||
const result = deserializeProjectFromSchema(proj);
|
||||
restored = result.tracks;
|
||||
restoredBpm = result.bpm;
|
||||
restoredSessionTabs = result.sessionTabs;
|
||||
restoredSubTabs = result.subTabs;
|
||||
if (result.masteringSettings) setMasteringSettings(result.masteringSettings);
|
||||
} else {
|
||||
restored = (proj.tracks || []).map(t => ({
|
||||
...t,
|
||||
buffer: null,
|
||||
channelInfo: t.channelInfo || null,
|
||||
clips: t.clips || [],
|
||||
serverFileId: t.serverFileId || null
|
||||
}));
|
||||
}
|
||||
if (restored.length > 0) {
|
||||
setTracks(restored);
|
||||
loadAudioBuffersForTracks(restored);
|
||||
setBpm(restoredBpm.toString());
|
||||
setProjectName(proj.name || 'Dự án mới');
|
||||
setCurrentProjectId(null);
|
||||
if (restoredSessionTabs.length > 0) setSessionTabs(restoredSessionTabs);
|
||||
if (restoredSubTabs.length > 0) setSubTabs(restoredSubTabs);
|
||||
localStorage.setItem('sonic_project_name', proj.name || 'Dự án mới');
|
||||
localStorage.removeItem('sonic_project_id');
|
||||
showToast(`Đã nạp dự án "${proj.name}" từ tệp .sfs thành công!`, "success");
|
||||
showToast(`Đã nạp dự án "${proj.name || 'Dự án mới'}" từ tệp .sfs thành công!`, "success");
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(err.message || "Lỗi mở tệp .sfs", "error");
|
||||
@@ -17701,7 +18055,7 @@ const App = () => {
|
||||
label: 'Open Project...',
|
||||
icon: 'folder-open',
|
||||
shortcut: 'Ctrl+O',
|
||||
action: () => handleImportSFS()
|
||||
action: () => setOpenProjectModalOpen(true)
|
||||
}, {
|
||||
label: 'Save Project',
|
||||
icon: 'upload-cloud',
|
||||
@@ -20789,8 +21143,12 @@ const App = () => {
|
||||
}), /*#__PURE__*/React.createElement(SaveProjectModal, {
|
||||
isOpen: saveProjectModalOpen,
|
||||
onClose: () => setSaveProjectModalOpen(false),
|
||||
onSave: (newName) => {
|
||||
handleSaveProjectWithName(newName);
|
||||
projectName: projectName,
|
||||
onSaveCloud: (newName, existingId) => {
|
||||
handleSaveCloudProject(newName, existingId);
|
||||
},
|
||||
onSaveLocal: (newName) => {
|
||||
handleSaveLocalProject(newName);
|
||||
}
|
||||
}), /*#__PURE__*/React.createElement(SaveAsModal, {
|
||||
isOpen: saveAsModalOpen,
|
||||
@@ -20804,6 +21162,17 @@ const App = () => {
|
||||
setProjectName(newName);
|
||||
localStorage.setItem('sonic_project_name', newName);
|
||||
}
|
||||
}), /*#__PURE__*/React.createElement(OpenProjectModal, {
|
||||
isOpen: openProjectModalOpen,
|
||||
onClose: () => setOpenProjectModalOpen(false),
|
||||
onOpenCloud: (projId, projName) => {
|
||||
setOpenProjectModalOpen(false);
|
||||
handleOpenProject(projId, projName);
|
||||
},
|
||||
onOpenLocal: () => {
|
||||
setOpenProjectModalOpen(false);
|
||||
handleImportSFS();
|
||||
}
|
||||
}), /*#__PURE__*/React.createElement(AIConfigModal, {
|
||||
isOpen: aiConfigModalOpen,
|
||||
onClose: () => setAiConfigModalOpen(false),
|
||||
|
||||
@@ -848,3 +848,9 @@
|
||||
- **Tóm tắt thay đổi:** (1) Refine `MasterStripConsole` theo spec `md/47`: top button "MASTERING PANEL", pan knob pointer-drag (L/R/center label), mute/mono buttons toggle `.btn-mute-active`/`.btn-mono-active`, VU meter dùng synthetic post-fader simulation khi muted. (2) Tạo `TrackStripConsole` từ spec `md/48_TRACK_STRIP_COMPONENT.md`: `w-[145px]` với top color accent bar, pan rotary dial, peak dB fader rail + LED VU canvas, button stack (M/S/Route/FX/Power/Auto/Phase Ø), record arm row, track name, color footer index. (3) CSS mới: `.strip-bg-track`, `.fader-track-bg`, `.btn-solo-active`, `.btn-arm-active`, `@keyframes pulse-red`, cập nhật `.btn-daw:active`/`fader-slider` thumb từ spec. (4) Replace `MixerStrip` → `TrackStripConsole` trong mixer panel.
|
||||
- **Các file ảnh hưởng:** `app/templates/index.html`, `app/static/js/app.jsx`
|
||||
- **Ghi chú/Test (nếu có):** `npm run build` — build passes.
|
||||
|
||||
### [2026-07-30 20:32] Task: Add "Tạo preset với cấu trúc" button in AI Preset Manager
|
||||
- **Tóm tắt thay đổi:** Thêm `handleNewStructured` — tạo preset mới với template có cấu trúc (mục: thể loại, bố cục, nhạc cụ, hòa âm, hiệu ứng, MIDI). Button xanh lục "Tạo preset với cấu trúc" bên trái "Quay lại" trong form edit.
|
||||
- **Các file ảnh hưởng:** `app/static/js/app.jsx`
|
||||
- **Ghi chú/Test (nếu có):** Nhấn "Tạo mới" → form edit hiện button xanh "Tạo preset với cấu trúc" → click → pre-fill template.
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user