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:
2026-07-30 20:33:22 +07:00
parent 64f43cd5ad
commit 3386a2b4c2
2 changed files with 464 additions and 89 deletions
+448 -79
View File
@@ -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("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ập tên dự án...",
required: true,
key: "name-input", type: "text", placeholder: "Nh\u1EADp t\u00EAn d\u1EF1 \u00E1n...", 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")
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]
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);
const handleSaveProject = async () => {
if (!currentProjectId) {
setSaveProjectModalOpen(true);
return;
}
showToast(`Đã lưu dự án "${newName}" mới lên server!`, "success");
} catch (err) {
showToast(err.message || "Lỗi lưu server", "error");
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 => ({
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),
+6
View File
@@ -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.
---