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({
|
setAppWarningModal({
|
||||||
title: "Mở dự án",
|
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.",
|
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);
|
setTracks(restoredTracks);
|
||||||
|
loadAudioBuffersForTracks(restoredTracks);
|
||||||
setBpm(restoredBpm.toString());
|
setBpm(restoredBpm.toString());
|
||||||
setSelectedTrackId(restoredTracks[0]?.id || '1');
|
setSelectedTrackId(restoredTracks[0]?.id || '1');
|
||||||
setProjectName(proj.name);
|
setProjectName(proj.name);
|
||||||
@@ -4741,10 +4806,10 @@ const ProfileModal = ({
|
|||||||
}, /*#__PURE__*/React.createElement("button", {
|
}, /*#__PURE__*/React.createElement("button", {
|
||||||
onClick: () => setConfirmModal(null),
|
onClick: () => setConfirmModal(null),
|
||||||
className: "px-3 py-1.5 bg-zinc-700 hover:bg-zinc-600 text-slate-300 rounded text-xs font-semibold"
|
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(); },
|
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"
|
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",
|
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]",
|
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
|
style: modalStyle
|
||||||
@@ -4903,59 +4968,183 @@ const ProfileModal = ({
|
|||||||
])
|
])
|
||||||
] : null)));
|
] : null)));
|
||||||
};
|
};
|
||||||
|
|
||||||
const SaveProjectModal = ({
|
const SaveProjectModal = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
onSave
|
onSaveCloud,
|
||||||
|
onSaveLocal,
|
||||||
|
projectName
|
||||||
}) => {
|
}) => {
|
||||||
if (!isOpen) return null;
|
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) => {
|
const handleSubmit = (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!name.trim()) return;
|
if (!name.trim()) return;
|
||||||
onSave(name.trim());
|
var matched = null;
|
||||||
onClose();
|
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", {
|
return /*#__PURE__*/React.createElement("div", {
|
||||||
className: "fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"
|
className: "fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"
|
||||||
}, /*#__PURE__*/React.createElement("div", {
|
}, /*#__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", {
|
}, /*#__PURE__*/React.createElement("h3", {
|
||||||
className: "text-sm font-bold text-teal-400 mb-4 uppercase"
|
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,
|
onSubmit: handleSubmit,
|
||||||
className: "space-y-4"
|
className: "space-y-4"
|
||||||
}, [
|
}, [
|
||||||
/*#__PURE__*/React.createElement("input", {
|
/*#__PURE__*/React.createElement("div", { key: "type-block" }, [
|
||||||
key: "name-input",
|
/*#__PURE__*/React.createElement("div", { className: "grid grid-cols-2 gap-2 text-xs" }, [
|
||||||
type: "text",
|
/*#__PURE__*/React.createElement("button", {
|
||||||
placeholder: "Nhập tên dự án...",
|
key: "btn-cloud", type: "button", onClick: function() { setSaveType('cloud'); setSelectedExisting(null); },
|
||||||
required: true,
|
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')
|
||||||
value: name,
|
}, [/*#__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")]),
|
||||||
onChange: e => setName(e.target.value),
|
/*#__PURE__*/React.createElement("button", {
|
||||||
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",
|
key: "btn-local", type: "button", onClick: function() { setSaveType('local'); setSelectedExisting(null); },
|
||||||
autoFocus: true
|
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: "actions",
|
]),
|
||||||
className: "flex justify-end gap-2 text-xs"
|
/*#__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("button", {
|
/*#__PURE__*/React.createElement("input", {
|
||||||
key: "cancel",
|
key: "name-input", type: "text", placeholder: "Nh\u1EADp t\u00EAn d\u1EF1 \u00E1n...", required: true,
|
||||||
type: "button",
|
value: name,
|
||||||
onClick: onClose,
|
onChange: function(e) { setName(e.target.value); setSelectedExisting(null); },
|
||||||
className: "px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-zinc-300 rounded"
|
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
|
||||||
}, "Hủy"),
|
})
|
||||||
/*#__PURE__*/React.createElement("button", {
|
]),
|
||||||
key: "save",
|
saveType === 'cloud' && /*#__PURE__*/React.createElement("div", { key: "cloud-list", className: "max-h-40 overflow-y-auto -mx-1" },
|
||||||
type: "submit",
|
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...")]
|
||||||
className: "px-3 py-1.5 bg-teal-600 hover:bg-teal-500 text-white rounded font-semibold"
|
: 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.")]
|
||||||
}, "Lưu")
|
: 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 = ({
|
const SaveAsModal = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -5256,6 +5445,46 @@ const AIPresetModal = ({ isOpen, onClose }) => {
|
|||||||
setFormTemplate('');
|
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) => {
|
const handleToggleFav = (id) => {
|
||||||
mgr.toggleFavorite(id);
|
mgr.toggleFavorite(id);
|
||||||
setPresets([...mgr.getPresets()]);
|
setPresets([...mgr.getPresets()]);
|
||||||
@@ -5498,8 +5727,14 @@ const AIPresetModal = ({ isOpen, onClose }) => {
|
|||||||
rows: 6,
|
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"
|
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", {
|
})), /*#__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", {
|
}, /*#__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",
|
type: "button",
|
||||||
onClick: () => setEditingPreset(null),
|
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"
|
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) => {
|
const serializeTracksList = (tracksList, secondsPerBar) => {
|
||||||
return (tracksList || []).map(t => {
|
return (tracksList || []).map(t => {
|
||||||
let trackType = "AUDIO";
|
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";
|
else if (t.midiItems && t.midiItems.length > 0) trackType = "MIDI";
|
||||||
|
|
||||||
const items = [];
|
const items = [];
|
||||||
@@ -7399,18 +7636,27 @@ const deserializeTracksList = (schemaTracks, secondsPerBar, sectionStore) => {
|
|||||||
return {
|
return {
|
||||||
id: t.id,
|
id: t.id,
|
||||||
name: t.name,
|
name: t.name,
|
||||||
|
type: t.type === 'MIDI' ? 'MIDI' : (t.type === 'SECTION' ? 'SECTION' : 'audio'),
|
||||||
volumeDb: t.volume_db || 0.0,
|
volumeDb: t.volume_db || 0.0,
|
||||||
pan: t.pan || 0.0,
|
pan: t.pan || 0.0,
|
||||||
muted: t.mute || false,
|
muted: t.mute || false,
|
||||||
solo: t.solo || false,
|
solo: t.solo || false,
|
||||||
color: t.id === '1' ? '#0f766e' : '#1d4ed8',
|
color: t.color || (t.id === '1' ? '#0f766e' : '#1d4ed8'),
|
||||||
markers: [],
|
startTime: t.start_time || 0,
|
||||||
serverFileId: t.items && t.items.find(i => i.type === "AUDIO_ITEM")?.source_data?.audio_file_url?.split("/").pop() || null,
|
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,
|
clips: clips,
|
||||||
sections: sections,
|
sections: sections,
|
||||||
midiItems: midiItems,
|
midiItems: midiItems,
|
||||||
instrumentId: t.instrument_id || null,
|
instrumentId: t.instrumentId != null ? t.instrumentId : (t.instrument_id || null),
|
||||||
instrumentProgram: t.instrument_program !== null ? t.instrument_program : undefined,
|
instrumentProgram: t.instrumentProgram !== undefined && t.instrumentProgram !== null ? t.instrumentProgram : (t.instrument_program !== null ? t.instrument_program : undefined),
|
||||||
instrumentName: t.instrument_name || null,
|
instrumentName: t.instrument_name || null,
|
||||||
instrument_source: t.instrument_source || null,
|
instrument_source: t.instrument_source || null,
|
||||||
soundfont_id: t.soundfont_id || null,
|
soundfont_id: t.soundfont_id || null,
|
||||||
@@ -8805,6 +9051,7 @@ const App = () => {
|
|||||||
const [currentProjectId, setCurrentProjectId] = useState(() => localStorage.getItem('sonic_project_id') || null);
|
const [currentProjectId, setCurrentProjectId] = useState(() => localStorage.getItem('sonic_project_id') || null);
|
||||||
const [saveProjectModalOpen, setSaveProjectModalOpen] = useState(false);
|
const [saveProjectModalOpen, setSaveProjectModalOpen] = useState(false);
|
||||||
const [saveAsModalOpen, setSaveAsModalOpen] = useState(false);
|
const [saveAsModalOpen, setSaveAsModalOpen] = useState(false);
|
||||||
|
const [openProjectModalOpen, setOpenProjectModalOpen] = useState(false);
|
||||||
const soloedTrack = tracks.find(t => t.solo);
|
const soloedTrack = tracks.find(t => t.solo);
|
||||||
const soloedTrackId = soloedTrack ? soloedTrack.id : null;
|
const soloedTrackId = soloedTrack ? soloedTrack.id : null;
|
||||||
const [toastMessage, setToastMessage] = useState(null);
|
const [toastMessage, setToastMessage] = useState(null);
|
||||||
@@ -9548,8 +9795,69 @@ const App = () => {
|
|||||||
applyMasteringSettings(masteringSettings);
|
applyMasteringSettings(masteringSettings);
|
||||||
}
|
}
|
||||||
}, [masteringSettings]);
|
}, [masteringSettings]);
|
||||||
|
|
||||||
const [pluginManagerModalOpen, setPluginManagerModalOpen] = useState(false);
|
const [pluginManagerModalOpen, setPluginManagerModalOpen] = useState(false);
|
||||||
const [pluginsData, setPluginsData] = useState(null);
|
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(() => {
|
useEffect(() => {
|
||||||
const checkAuthStatus = async () => {
|
const checkAuthStatus = async () => {
|
||||||
const savedToken = localStorage.getItem('sonic_token');
|
const savedToken = localStorage.getItem('sonic_token');
|
||||||
@@ -9570,6 +9878,7 @@ const App = () => {
|
|||||||
} else {
|
} else {
|
||||||
setIsMandatoryLogin(false);
|
setIsMandatoryLogin(false);
|
||||||
setAuthModalOpen(false);
|
setAuthModalOpen(false);
|
||||||
|
restoreLastSessionProject();
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const cached = localStorage.getItem('sonic_user');
|
const cached = localStorage.getItem('sonic_user');
|
||||||
@@ -9577,6 +9886,7 @@ const App = () => {
|
|||||||
try { setCurrentUser(JSON.parse(cached)); } catch (_) { }
|
try { setCurrentUser(JSON.parse(cached)); } catch (_) { }
|
||||||
setIsMandatoryLogin(false);
|
setIsMandatoryLogin(false);
|
||||||
setAuthModalOpen(false);
|
setAuthModalOpen(false);
|
||||||
|
restoreLastSessionProject();
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem('sonic_token');
|
localStorage.removeItem('sonic_token');
|
||||||
localStorage.removeItem('sonic_user');
|
localStorage.removeItem('sonic_user');
|
||||||
@@ -9589,6 +9899,7 @@ const App = () => {
|
|||||||
};
|
};
|
||||||
checkAuthStatus();
|
checkAuthStatus();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadPendingSfsProject = () => {
|
const loadPendingSfsProject = () => {
|
||||||
const proj = window.__pendingSfsProject;
|
const proj = window.__pendingSfsProject;
|
||||||
if (!proj) return;
|
if (!proj) return;
|
||||||
@@ -9666,7 +9977,7 @@ const App = () => {
|
|||||||
} else {
|
} else {
|
||||||
setIsMandatoryLogin(false);
|
setIsMandatoryLogin(false);
|
||||||
setAuthModalOpen(false);
|
setAuthModalOpen(false);
|
||||||
loadPendingSfsProject();
|
restoreLastSessionProject();
|
||||||
const loadPrefs = (p) => {
|
const loadPrefs = (p) => {
|
||||||
if (!p) return;
|
if (!p) return;
|
||||||
if (p.showAIPanel !== undefined) setShowAIPanel(p.showAIPanel);
|
if (p.showAIPanel !== undefined) setShowAIPanel(p.showAIPanel);
|
||||||
@@ -10373,13 +10684,7 @@ const App = () => {
|
|||||||
}
|
}
|
||||||
if (ctrl && !alt && e.key === 's') {
|
if (ctrl && !alt && e.key === 's') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var curTab = activeTabRef.current;
|
handleSaveProjectRef.current();
|
||||||
if (curTab.startsWith('session_')) {
|
|
||||||
handleSaveSectionTabRef.current(curTab);
|
|
||||||
showToast('Đã lưu Section', 'success');
|
|
||||||
} else {
|
|
||||||
handleExportSFS();
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (ctrl && alt && e.key === 's' || ctrl && e.shiftKey && e.key === 's') {
|
if (ctrl && alt && e.key === 's' || ctrl && e.shiftKey && e.key === 's') {
|
||||||
@@ -10473,6 +10778,8 @@ const App = () => {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const curTab = activeTabRef.current;
|
const curTab = activeTabRef.current;
|
||||||
if (curTab === 'main') {
|
if (curTab === 'main') {
|
||||||
|
// handled by main handler
|
||||||
|
} else if (curTab.startsWith('session_')) {
|
||||||
// Main session: save project + save all dirty sub-tabs
|
// Main session: save project + save all dirty sub-tabs
|
||||||
handleSaveProject();
|
handleSaveProject();
|
||||||
subTabsRef.current.filter(s => s.isDirty).forEach(st => {
|
subTabsRef.current.filter(s => s.isDirty).forEach(st => {
|
||||||
@@ -15454,49 +15761,79 @@ const App = () => {
|
|||||||
clientSideExport(exportTracks);
|
clientSideExport(exportTracks);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const handleSaveProject = async () => {
|
const handleSaveLocalProject = (name) => {
|
||||||
if (!currentUser) { setIsMandatoryLogin(false); setAuthMode('login'); setAuthModalOpen(true); return; }
|
const finalName = name || projectName || 'Dự án mới';
|
||||||
if (!projectName) {
|
const localId = currentProjectId && currentProjectId.startsWith('local_') ? currentProjectId : 'local_' + Date.now();
|
||||||
setSaveProjectModalOpen(true);
|
const projectSchemaObj = serializeProjectToSchema(localId, finalName, bpm, tracks, subTabs, sessionTabs, masteringSettings);
|
||||||
return;
|
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);
|
const dataJson = JSON.stringify(projectSchemaObj);
|
||||||
try {
|
try {
|
||||||
if (currentProjectId) {
|
if (existingProjectId) {
|
||||||
await window.SonicAPI.updateCloudProject(currentProjectId, projectName, dataJson);
|
await window.SonicAPI.updateCloudProject(existingProjectId, finalName, dataJson);
|
||||||
showToast(`Đã cập nhật dự án "${projectName}" lên server!`, "success");
|
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 {
|
} else {
|
||||||
const res = await window.SonicAPI.saveCloudProject(projectName, dataJson);
|
const res = await window.SonicAPI.saveCloudProject(finalName, dataJson);
|
||||||
const newProjId = res.project_id;
|
const newProjId = res.project_id;
|
||||||
|
setProjectName(finalName);
|
||||||
|
localStorage.setItem('sonic_project_name', finalName);
|
||||||
if (newProjId) {
|
if (newProjId) {
|
||||||
setCurrentProjectId(newProjId);
|
setCurrentProjectId(newProjId);
|
||||||
localStorage.setItem('sonic_project_id', 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) {
|
} 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) => {
|
const handleSaveProject = async () => {
|
||||||
setProjectName(newName);
|
if (!currentProjectId) {
|
||||||
localStorage.setItem('sonic_project_name', newName);
|
setSaveProjectModalOpen(true);
|
||||||
const projectSchemaObj = serializeProjectToSchema('project_' + Date.now(), newName, bpm, tracks, subTabs, sessionTabs, masteringSettings);
|
return;
|
||||||
const dataJson = JSON.stringify(projectSchemaObj);
|
}
|
||||||
try {
|
if (currentProjectId.startsWith('local_')) {
|
||||||
const res = await window.SonicAPI.saveCloudProject(newName, dataJson);
|
handleSaveLocalProject(projectName);
|
||||||
const newProjId = res.project_id;
|
} else {
|
||||||
if (newProjId) {
|
if (!currentUser) { showToast('Cần đăng nhập để lưu Cloud. currentUser=' + (currentUser ? 'OK' : 'NULL') + ' token=' + (localStorage.getItem('sonic_token') ? 'exists' : 'missing'), 'warning'); return; }
|
||||||
setCurrentProjectId(newProjId);
|
handleSaveCloudProject(projectName);
|
||||||
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 handleSaveProjectRef = useRef(handleSaveProject);
|
||||||
|
handleSaveProjectRef.current = handleSaveProject;
|
||||||
|
|
||||||
const handleSaveAsCloud = async (newName) => {
|
const handleSaveAsCloud = async (newName) => {
|
||||||
const projectSchemaObj = serializeProjectToSchema('project_' + Date.now(), newName, bpm, tracks, subTabs, sessionTabs, masteringSettings);
|
const projectSchemaObj = serializeProjectToSchema('project_' + Date.now(), newName, bpm, tracks, subTabs, sessionTabs, masteringSettings);
|
||||||
@@ -15531,20 +15868,37 @@ const App = () => {
|
|||||||
if (!e.target.files[0]) return;
|
if (!e.target.files[0]) return;
|
||||||
try {
|
try {
|
||||||
const proj = await window.SonicStorage.importProjectFromSFSFile(e.target.files[0]);
|
const proj = await window.SonicStorage.importProjectFromSFSFile(e.target.files[0]);
|
||||||
const restored = (proj.tracks || []).map(t => ({
|
let restored = [];
|
||||||
...t,
|
let restoredBpm = bpm;
|
||||||
buffer: null,
|
let restoredSessionTabs = [];
|
||||||
channelInfo: t.channelInfo || null,
|
let restoredSubTabs = [];
|
||||||
clips: t.clips || [],
|
if (proj.main_session) {
|
||||||
serverFileId: t.serverFileId || null
|
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) {
|
if (restored.length > 0) {
|
||||||
setTracks(restored);
|
setTracks(restored);
|
||||||
|
loadAudioBuffersForTracks(restored);
|
||||||
|
setBpm(restoredBpm.toString());
|
||||||
setProjectName(proj.name || 'Dự án mới');
|
setProjectName(proj.name || 'Dự án mới');
|
||||||
setCurrentProjectId(null);
|
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.setItem('sonic_project_name', proj.name || 'Dự án mới');
|
||||||
localStorage.removeItem('sonic_project_id');
|
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) {
|
} catch (err) {
|
||||||
showToast(err.message || "Lỗi mở tệp .sfs", "error");
|
showToast(err.message || "Lỗi mở tệp .sfs", "error");
|
||||||
@@ -17701,7 +18055,7 @@ const App = () => {
|
|||||||
label: 'Open Project...',
|
label: 'Open Project...',
|
||||||
icon: 'folder-open',
|
icon: 'folder-open',
|
||||||
shortcut: 'Ctrl+O',
|
shortcut: 'Ctrl+O',
|
||||||
action: () => handleImportSFS()
|
action: () => setOpenProjectModalOpen(true)
|
||||||
}, {
|
}, {
|
||||||
label: 'Save Project',
|
label: 'Save Project',
|
||||||
icon: 'upload-cloud',
|
icon: 'upload-cloud',
|
||||||
@@ -20789,8 +21143,12 @@ const App = () => {
|
|||||||
}), /*#__PURE__*/React.createElement(SaveProjectModal, {
|
}), /*#__PURE__*/React.createElement(SaveProjectModal, {
|
||||||
isOpen: saveProjectModalOpen,
|
isOpen: saveProjectModalOpen,
|
||||||
onClose: () => setSaveProjectModalOpen(false),
|
onClose: () => setSaveProjectModalOpen(false),
|
||||||
onSave: (newName) => {
|
projectName: projectName,
|
||||||
handleSaveProjectWithName(newName);
|
onSaveCloud: (newName, existingId) => {
|
||||||
|
handleSaveCloudProject(newName, existingId);
|
||||||
|
},
|
||||||
|
onSaveLocal: (newName) => {
|
||||||
|
handleSaveLocalProject(newName);
|
||||||
}
|
}
|
||||||
}), /*#__PURE__*/React.createElement(SaveAsModal, {
|
}), /*#__PURE__*/React.createElement(SaveAsModal, {
|
||||||
isOpen: saveAsModalOpen,
|
isOpen: saveAsModalOpen,
|
||||||
@@ -20804,6 +21162,17 @@ const App = () => {
|
|||||||
setProjectName(newName);
|
setProjectName(newName);
|
||||||
localStorage.setItem('sonic_project_name', 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, {
|
}), /*#__PURE__*/React.createElement(AIConfigModal, {
|
||||||
isOpen: aiConfigModalOpen,
|
isOpen: aiConfigModalOpen,
|
||||||
onClose: () => setAiConfigModalOpen(false),
|
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.
|
- **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`
|
- **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.
|
- **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