feat: thêm tính năng lưu dự án bằng modal, Lưu dưới tên khác (Save As) và quản trị dự án/tệp tin trong Hồ sơ cá nhân
This commit is contained in:
@@ -2762,18 +2762,39 @@ const AIConfigModal = ({
|
||||
};
|
||||
const ProfileModal = ({
|
||||
isOpen,
|
||||
onClose
|
||||
onClose,
|
||||
tracks,
|
||||
setTracks,
|
||||
setSelectedTrackId,
|
||||
projectName,
|
||||
setProjectName,
|
||||
currentProjectId,
|
||||
setCurrentProjectId,
|
||||
showToast
|
||||
}) => {
|
||||
if (!isOpen) return null;
|
||||
const [activeTab, setActiveTab] = useState('account');
|
||||
const [profile, setProfile] = useState(null);
|
||||
const [oldPassword, setOldPassword] = useState('');
|
||||
const [newPassword, setNewPassword] = useState('');
|
||||
const [msg, setMsg] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
// Projects list state
|
||||
const [projectsList, setProjectsList] = useState([]);
|
||||
const [loadingProjects, setLoadingProjects] = useState(false);
|
||||
|
||||
// Files list state
|
||||
const [filesList, setFilesList] = useState([]);
|
||||
const [loadingFiles, setLoadingFiles] = useState(false);
|
||||
useEffect(() => {
|
||||
if (isOpen) fetchProfile();
|
||||
}, [isOpen]);
|
||||
if (isOpen) {
|
||||
fetchProfile();
|
||||
if (activeTab === 'projects') fetchProjects();
|
||||
if (activeTab === 'files') fetchFiles();
|
||||
}
|
||||
}, [isOpen, activeTab]);
|
||||
const fetchProfile = async () => {
|
||||
try {
|
||||
const data = await window.SonicAPI.getProfile();
|
||||
@@ -2782,6 +2803,96 @@ const ProfileModal = ({
|
||||
setError(e.message || 'Không thể tải thông tin profile');
|
||||
}
|
||||
};
|
||||
const fetchProjects = async () => {
|
||||
setLoadingProjects(true);
|
||||
try {
|
||||
const data = await window.SonicAPI.listCloudProjects();
|
||||
setProjectsList(data || []);
|
||||
} catch (e) {
|
||||
showToast(e.message || 'Không thể tải danh sách dự án', 'error');
|
||||
} finally {
|
||||
setLoadingProjects(false);
|
||||
}
|
||||
};
|
||||
const fetchFiles = async () => {
|
||||
setLoadingFiles(true);
|
||||
try {
|
||||
const activeFileIds = tracks.map(t => t.serverFileId).filter(Boolean);
|
||||
const data = await window.SonicAPI.listMyFiles(activeFileIds);
|
||||
setFilesList(data || []);
|
||||
} catch (e) {
|
||||
showToast(e.message || 'Không thể tải danh sách tệp tin', 'error');
|
||||
} finally {
|
||||
setLoadingFiles(false);
|
||||
}
|
||||
};
|
||||
const handleOpenProject = async projectId => {
|
||||
if (!confirm("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.")) return;
|
||||
try {
|
||||
const proj = await window.SonicAPI.getCloudProject(projectId);
|
||||
const parsed = JSON.parse(proj.data_json);
|
||||
const restored = (parsed.tracks || []).map(t => ({
|
||||
...t,
|
||||
buffer: null,
|
||||
channelInfo: t.channelInfo || null,
|
||||
clips: t.clips || [],
|
||||
serverFileId: t.serverFileId || null
|
||||
}));
|
||||
setTracks(restored);
|
||||
setSelectedTrackId(restored[0]?.id || '1');
|
||||
setProjectName(proj.name);
|
||||
setCurrentProjectId(proj.id);
|
||||
localStorage.setItem('sonic_project_name', proj.name);
|
||||
localStorage.setItem('sonic_project_id', proj.id);
|
||||
showToast(`Đã nạp dự án "${proj.name}" thành công!`, "success");
|
||||
onClose();
|
||||
} catch (e) {
|
||||
showToast(e.message || "Lỗi khi nạp dự án", "error");
|
||||
}
|
||||
};
|
||||
const handleDeleteProject = async (projectId, e) => {
|
||||
e.stopPropagation();
|
||||
if (!confirm("Bạn có chắc chắn muốn xóa dự án này khỏi Cloud?")) return;
|
||||
try {
|
||||
await window.SonicAPI.deleteCloudProject(projectId);
|
||||
showToast("Đã xóa dự án thành công!", "success");
|
||||
fetchProjects();
|
||||
fetchProfile();
|
||||
} catch (err) {
|
||||
showToast(err.message || "Lỗi khi xóa dự án", "error");
|
||||
}
|
||||
};
|
||||
const handleDeleteFile = async fileId => {
|
||||
if (!confirm(`Bạn có chắc chắn muốn xóa tệp tin ${fileId}?`)) return;
|
||||
try {
|
||||
await window.SonicAPI.deleteMyFile(fileId);
|
||||
showToast("Đã xóa tệp tin thành công!", "success");
|
||||
fetchFiles();
|
||||
fetchProfile();
|
||||
} catch (err) {
|
||||
showToast(err.message || "Lỗi khi xóa tệp tin", "error");
|
||||
}
|
||||
};
|
||||
const handleCleanUnusedFiles = async () => {
|
||||
const unusedFiles = filesList.filter(f => !f.is_in_use);
|
||||
if (unusedFiles.length === 0) {
|
||||
showToast("Không có tập tin rác nào để dọn dẹp.", "info");
|
||||
return;
|
||||
}
|
||||
if (!confirm(`Bạn có chắc chắn muốn xóa tất cả ${unusedFiles.length} tập tin rác để giải phóng dung lượng?`)) return;
|
||||
let successCount = 0;
|
||||
for (const file of unusedFiles) {
|
||||
try {
|
||||
await window.SonicAPI.deleteMyFile(file.file_id);
|
||||
successCount++;
|
||||
} catch (e) {
|
||||
console.error("Lỗi xóa file rác: ", file.file_id, e);
|
||||
}
|
||||
}
|
||||
showToast(`Đã dọn dẹp thành công ${successCount}/${unusedFiles.length} tập tin rác!`, "success");
|
||||
fetchFiles();
|
||||
fetchProfile();
|
||||
};
|
||||
const handleChangePassword = async e => {
|
||||
e.preventDefault();
|
||||
setMsg('');
|
||||
@@ -2801,55 +2912,83 @@ const ProfileModal = ({
|
||||
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-lg p-6 text-slate-200"
|
||||
className: "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]"
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex justify-between items-center pb-4 border-b border-[#383838]"
|
||||
className: "flex justify-between items-center pb-3 border-b border-[#383838] shrink-0"
|
||||
}, /*#__PURE__*/React.createElement("h3", {
|
||||
className: "text-lg font-bold text-teal-400"
|
||||
}, "👤 Hồ Sơ Cá Nhân & Hạn Mức Quota"), /*#__PURE__*/React.createElement("button", {
|
||||
className: "text-md font-bold text-teal-400 flex items-center gap-1.5"
|
||||
}, "👤 Hồ Sơ Cá Nhân & Quản Lý Dự Án"), /*#__PURE__*/React.createElement("button", {
|
||||
onClick: onClose,
|
||||
className: "text-slate-400 hover:text-slate-200"
|
||||
}, "✕")), profile && /*#__PURE__*/React.createElement("div", {
|
||||
className: "mt-4 space-y-4"
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
}, "✕")), /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex gap-2 border-b border-[#383838] py-2 shrink-0 text-xs font-semibold"
|
||||
}, [/*#__PURE__*/React.createElement("button", {
|
||||
key: "tab-acc",
|
||||
onClick: () => setActiveTab('account'),
|
||||
className: `px-3 py-1.5 rounded transition ${activeTab === 'account' ? 'bg-teal-950/60 text-teal-300 border border-teal-800' : 'text-slate-400 hover:text-slate-200'}`
|
||||
}, "Tài Khoản"), /*#__PURE__*/React.createElement("button", {
|
||||
key: "tab-proj",
|
||||
onClick: () => setActiveTab('projects'),
|
||||
className: `px-3 py-1.5 rounded transition ${activeTab === 'projects' ? 'bg-teal-950/60 text-teal-300 border border-teal-800' : 'text-slate-400 hover:text-slate-200'}`
|
||||
}, "Dự Án Cloud"), /*#__PURE__*/React.createElement("button", {
|
||||
key: "tab-files",
|
||||
onClick: () => setActiveTab('files'),
|
||||
className: `px-3 py-1.5 rounded transition ${activeTab === 'files' ? 'bg-teal-950/60 text-teal-300 border border-teal-800' : 'text-slate-400 hover:text-slate-200'}`
|
||||
}, "Tập Tin Của Tôi")]), /*#__PURE__*/React.createElement("div", {
|
||||
className: "flex-1 overflow-y-auto pt-4 space-y-4 pr-1 min-h-[300px]"
|
||||
}, activeTab === 'account' && profile ? [/*#__PURE__*/React.createElement("div", {
|
||||
key: "quota-info",
|
||||
className: "bg-[#1e1e1e] p-4 rounded-lg border border-[#333] grid grid-cols-2 gap-4 text-xs"
|
||||
}, /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("span", {
|
||||
}, [/*#__PURE__*/React.createElement("div", {
|
||||
key: "username"
|
||||
}, [/*#__PURE__*/React.createElement("span", {
|
||||
className: "text-slate-500 block"
|
||||
}, "Tên người dùng"), /*#__PURE__*/React.createElement("span", {
|
||||
className: "font-bold text-teal-300 text-sm"
|
||||
}, profile.username)), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("span", {
|
||||
}, profile.username)]), /*#__PURE__*/React.createElement("div", {
|
||||
key: "role"
|
||||
}, [/*#__PURE__*/React.createElement("span", {
|
||||
className: "text-slate-500 block"
|
||||
}, "Vai trò"), /*#__PURE__*/React.createElement("span", {
|
||||
className: "uppercase font-semibold text-amber-400"
|
||||
}, profile.role)), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("span", {
|
||||
}, profile.role)]), /*#__PURE__*/React.createElement("div", {
|
||||
key: "email"
|
||||
}, [/*#__PURE__*/React.createElement("span", {
|
||||
className: "text-slate-500 block"
|
||||
}, "Email"), /*#__PURE__*/React.createElement("span", null, profile.email)), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("span", {
|
||||
}, "Email"), /*#__PURE__*/React.createElement("span", null, profile.email)]), /*#__PURE__*/React.createElement("div", {
|
||||
key: "quota"
|
||||
}, [/*#__PURE__*/React.createElement("span", {
|
||||
className: "text-slate-500 block"
|
||||
}, "Dung lượng Quota"), /*#__PURE__*/React.createElement("span", {
|
||||
className: "font-semibold text-slate-200"
|
||||
}, profile.quota.used_mb, " MB / ", profile.quota.storage_limit_mb, " MB"))), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("div", {
|
||||
}, `${profile.quota.used_mb} MB / ${profile.quota.storage_limit_mb} MB`)])]), /*#__PURE__*/React.createElement("div", {
|
||||
key: "progress"
|
||||
}, [/*#__PURE__*/React.createElement("div", {
|
||||
className: "flex justify-between text-xs mb-1"
|
||||
}, /*#__PURE__*/React.createElement("span", {
|
||||
}, [/*#__PURE__*/React.createElement("span", {
|
||||
className: "text-slate-400"
|
||||
}, "Tiến trình sử dụng bộ nhớ Server"), /*#__PURE__*/React.createElement("span", {
|
||||
className: "font-bold text-teal-400"
|
||||
}, (profile.quota.used_mb / profile.quota.storage_limit_mb * 100).toFixed(1), "%")), /*#__PURE__*/React.createElement("div", {
|
||||
}, `${(profile.quota.used_mb / profile.quota.storage_limit_mb * 100).toFixed(1)}%`)]), /*#__PURE__*/React.createElement("div", {
|
||||
className: "w-full h-2 bg-slate-800 rounded-full overflow-hidden"
|
||||
}, /*#__PURE__*/React.createElement("div", {
|
||||
}, [/*#__PURE__*/React.createElement("div", {
|
||||
className: "h-full bg-teal-500 rounded-full transition-all duration-300",
|
||||
style: {
|
||||
width: `${Math.min(100, profile.quota.used_mb / profile.quota.storage_limit_mb * 100)}%`
|
||||
}
|
||||
}))), /*#__PURE__*/React.createElement("form", {
|
||||
})])]), /*#__PURE__*/React.createElement("form", {
|
||||
key: "pwd-form",
|
||||
onSubmit: handleChangePassword,
|
||||
className: "pt-4 border-t border-[#383838] space-y-3"
|
||||
}, /*#__PURE__*/React.createElement("h4", {
|
||||
}, [/*#__PURE__*/React.createElement("h4", {
|
||||
className: "text-xs font-bold text-slate-300 uppercase"
|
||||
}, "Thay Đổi Mật Khẩu"), msg && /*#__PURE__*/React.createElement("div", {
|
||||
className: "p-2 bg-emerald-950/60 border border-emerald-700 text-emerald-300 rounded text-xs"
|
||||
}, msg), error && /*#__PURE__*/React.createElement("div", {
|
||||
className: "p-2 bg-red-950/60 border border-red-700 text-red-300 rounded text-xs"
|
||||
}, error), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
|
||||
}, error), /*#__PURE__*/React.createElement("div", {
|
||||
key: "old"
|
||||
}, [/*#__PURE__*/React.createElement("label", {
|
||||
className: "block text-xs text-slate-400 mb-1"
|
||||
}, "Mật khẩu cũ"), /*#__PURE__*/React.createElement("input", {
|
||||
type: "password",
|
||||
@@ -2858,7 +2997,9 @@ const ProfileModal = ({
|
||||
value: oldPassword,
|
||||
onChange: e => setOldPassword(e.target.value),
|
||||
className: "w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs focus:outline-none focus:border-teal-500"
|
||||
})), /*#__PURE__*/React.createElement("div", null, /*#__PURE__*/React.createElement("label", {
|
||||
})]), /*#__PURE__*/React.createElement("div", {
|
||||
key: "new"
|
||||
}, [/*#__PURE__*/React.createElement("label", {
|
||||
className: "block text-xs text-slate-400 mb-1"
|
||||
}, "Mật khẩu mới"), /*#__PURE__*/React.createElement("input", {
|
||||
type: "password",
|
||||
@@ -2867,11 +3008,205 @@ const ProfileModal = ({
|
||||
value: newPassword,
|
||||
onChange: e => setNewPassword(e.target.value),
|
||||
className: "w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs focus:outline-none focus:border-teal-500"
|
||||
})), /*#__PURE__*/React.createElement("button", {
|
||||
})]), /*#__PURE__*/React.createElement("button", {
|
||||
type: "submit",
|
||||
disabled: loading,
|
||||
className: "w-full py-1.5 bg-teal-600 hover:bg-teal-500 text-white font-semibold text-xs rounded transition"
|
||||
}, loading ? 'Đang cập nhật...' : 'Cập Nhật Mật Khẩu')))));
|
||||
}, loading ? 'Đang cập nhật...' : 'Cập Nhật Mật Khẩu')])] : activeTab === 'projects' ? [loadingProjects ? /*#__PURE__*/React.createElement("div", {
|
||||
key: "loading",
|
||||
className: "text-center py-8 text-xs text-zinc-500"
|
||||
}, "Đang tải danh sách dự án...") : projectsList.length === 0 ? /*#__PURE__*/React.createElement("div", {
|
||||
key: "empty",
|
||||
className: "text-center py-8 text-xs text-zinc-500"
|
||||
}, "Bạn chưa có dự án nào lưu trên Cloud.") : /*#__PURE__*/React.createElement("div", {
|
||||
key: "list",
|
||||
className: "space-y-1.5"
|
||||
}, [projectsList.map(proj => /*#__PURE__*/React.createElement("div", {
|
||||
key: proj.id,
|
||||
onClick: () => handleOpenProject(proj.id),
|
||||
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"
|
||||
}, [/*#__PURE__*/React.createElement("div", {
|
||||
key: "meta"
|
||||
}, [/*#__PURE__*/React.createElement("div", {
|
||||
className: "font-bold text-slate-200 group-hover:text-teal-400"
|
||||
}, proj.name), /*#__PURE__*/React.createElement("div", {
|
||||
className: "text-[10px] text-zinc-500 mt-0.5"
|
||||
}, `Dung lượng: ${proj.size_mb} MB | Cập nhật: ${new Date(proj.updated_at * 1000).toLocaleString()}`)]), /*#__PURE__*/React.createElement("div", {
|
||||
key: "actions",
|
||||
className: "flex items-center gap-1.5"
|
||||
}, [/*#__PURE__*/React.createElement("button", {
|
||||
onClick: e => {
|
||||
e.stopPropagation();
|
||||
handleOpenProject(proj.id);
|
||||
},
|
||||
className: "px-2.5 py-1 bg-teal-800/80 hover:bg-teal-700 text-teal-200 rounded font-semibold text-[10px]"
|
||||
}, "MỞ"), /*#__PURE__*/React.createElement("button", {
|
||||
onClick: e => handleDeleteProject(proj.id, e),
|
||||
className: "px-2 py-1 bg-rose-950/60 hover:bg-rose-900/80 text-rose-300 rounded font-semibold text-[10px]"
|
||||
}, "XÓA")])]))])] : activeTab === 'files' ? [/*#__PURE__*/React.createElement("div", {
|
||||
key: "cleanup-header",
|
||||
className: "flex items-center justify-between bg-zinc-900/60 p-2.5 rounded border border-zinc-800 text-xs shrink-0 mb-3"
|
||||
}, [/*#__PURE__*/React.createElement("span", {
|
||||
className: "text-zinc-400 text-[10px]"
|
||||
}, "💡 Các tập tin không dùng trong dự án nào sẽ được đánh dấu 'Không dùng' và có thể xóa để giải phóng dung lượng."), /*#__PURE__*/React.createElement("button", {
|
||||
onClick: handleCleanUnusedFiles,
|
||||
className: "px-2.5 py-1 bg-rose-700 hover:bg-rose-600 text-white rounded font-bold text-[10px] uppercase flex items-center gap-1"
|
||||
}, "🧹 Dọn dẹp tệp rác")]), loadingFiles ? /*#__PURE__*/React.createElement("div", {
|
||||
key: "loading",
|
||||
className: "text-center py-8 text-xs text-zinc-500"
|
||||
}, "Đang tải danh sách tập tin...") : filesList.length === 0 ? /*#__PURE__*/React.createElement("div", {
|
||||
key: "empty",
|
||||
className: "text-center py-8 text-xs text-zinc-500"
|
||||
}, "Chưa có tập tin nào tải lên hoặc tạo ra.") : /*#__PURE__*/React.createElement("div", {
|
||||
key: "list",
|
||||
className: "space-y-1.5"
|
||||
}, [filesList.map(file => /*#__PURE__*/React.createElement("div", {
|
||||
key: file.file_id,
|
||||
className: "flex items-center justify-between p-3 bg-[#1e1e1e] rounded border border-zinc-800 text-xs"
|
||||
}, [/*#__PURE__*/React.createElement("div", {
|
||||
key: "meta",
|
||||
className: "max-w-[70%]"
|
||||
}, [/*#__PURE__*/React.createElement("div", {
|
||||
className: "font-mono font-semibold text-slate-300 truncate"
|
||||
}, file.file_id.replace(/^user_[^_]+_/, '')), /*#__PURE__*/React.createElement("div", {
|
||||
className: "text-[10px] text-zinc-500 mt-0.5"
|
||||
}, `Loại: ${file.type} | Dung lượng: ${file.size_mb} MB | Tạo lúc: ${new Date(file.created_at * 1000).toLocaleString()}`)]), /*#__PURE__*/React.createElement("div", {
|
||||
key: "actions",
|
||||
className: "flex items-center gap-2"
|
||||
}, [file.is_in_use ? /*#__PURE__*/React.createElement("span", {
|
||||
className: "px-2 py-0.5 bg-emerald-950 text-emerald-400 border border-emerald-900 rounded text-[9px] uppercase font-bold font-mono"
|
||||
}, "Đang dùng") : /*#__PURE__*/React.createElement("span", {
|
||||
className: "px-2 py-0.5 bg-zinc-850 text-zinc-400 border border-zinc-700 rounded text-[9px] uppercase font-bold font-mono"
|
||||
}, "Không dùng"), !file.is_in_use && /*#__PURE__*/React.createElement("button", {
|
||||
onClick: () => handleDeleteFile(file.file_id),
|
||||
className: "px-2 py-0.5 bg-rose-950 hover:bg-rose-900 text-rose-300 rounded font-semibold text-[10px] border border-rose-900"
|
||||
}, "Xóa")])]))])] : null)));
|
||||
};
|
||||
const SaveProjectModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSave
|
||||
}) => {
|
||||
if (!isOpen) return null;
|
||||
const [name, setName] = useState('');
|
||||
const handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
if (!name.trim()) return;
|
||||
onSave(name.trim());
|
||||
onClose();
|
||||
};
|
||||
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"
|
||||
}, /*#__PURE__*/React.createElement("h3", {
|
||||
className: "text-sm font-bold text-teal-400 mb-4 uppercase"
|
||||
}, "Đặt tên dự án"), /*#__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")])])));
|
||||
};
|
||||
const SaveAsModal = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
projectName,
|
||||
onSaveCloud,
|
||||
onSaveLocal
|
||||
}) => {
|
||||
if (!isOpen) return null;
|
||||
const [name, setName] = useState(projectName || '');
|
||||
const [saveType, setSaveType] = useState('cloud');
|
||||
const handleSubmit = e => {
|
||||
e.preventDefault();
|
||||
if (!name.trim()) return;
|
||||
if (saveType === 'cloud') {
|
||||
onSaveCloud(name.trim());
|
||||
} else {
|
||||
onSaveLocal(name.trim());
|
||||
}
|
||||
onClose();
|
||||
};
|
||||
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"
|
||||
}, /*#__PURE__*/React.createElement("h3", {
|
||||
className: "text-sm font-bold text-teal-400 mb-4 uppercase"
|
||||
}, "Lưu dưới tên khác (Save As...)"), /*#__PURE__*/React.createElement("form", {
|
||||
onSubmit: handleSubmit,
|
||||
className: "space-y-4"
|
||||
}, [/*#__PURE__*/React.createElement("div", {
|
||||
key: "name-block"
|
||||
}, [/*#__PURE__*/React.createElement("label", {
|
||||
className: "block text-[10px] text-zinc-400 uppercase font-bold mb-1"
|
||||
}, "Tên dự án mới"), /*#__PURE__*/React.createElement("input", {
|
||||
type: "text",
|
||||
placeholder: "Nhập tên mới...",
|
||||
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: "type-block"
|
||||
}, [/*#__PURE__*/React.createElement("label", {
|
||||
className: "block text-[10px] text-zinc-400 uppercase font-bold mb-1.5"
|
||||
}, "Phương thức lưu trữ"), /*#__PURE__*/React.createElement("div", {
|
||||
className: "grid grid-cols-2 gap-2 text-xs"
|
||||
}, [/*#__PURE__*/React.createElement("button", {
|
||||
key: "btn-cloud",
|
||||
type: "button",
|
||||
onClick: () => setSaveType('cloud'),
|
||||
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"
|
||||
}, "☁️ Lưu Cloud"), /*#__PURE__*/React.createElement("span", {
|
||||
key: "desc",
|
||||
className: "text-[9px] font-normal text-zinc-500"
|
||||
}, "Lưu lên server cá nhân")]), /*#__PURE__*/React.createElement("button", {
|
||||
key: "btn-local",
|
||||
type: "button",
|
||||
onClick: () => setSaveType('local'),
|
||||
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"
|
||||
}, "💾 Tải về máy (.sfs)"), /*#__PURE__*/React.createElement("span", {
|
||||
key: "desc",
|
||||
className: "text-[9px] font-normal text-zinc-500"
|
||||
}, "Tải tệp JSON dự án về máy")])])]), /*#__PURE__*/React.createElement("div", {
|
||||
key: "actions",
|
||||
className: "flex justify-end gap-2 text-xs pt-2"
|
||||
}, [/*#__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"
|
||||
}, "Thực hiện lưu")])])));
|
||||
};
|
||||
const SystemManagerModal = ({
|
||||
isOpen,
|
||||
@@ -3086,6 +3421,9 @@ const App = () => {
|
||||
const [subTabHeight, setSubTabHeight] = useState(96);
|
||||
const [isExporting, setIsExporting] = useState(false);
|
||||
const [projectName, setProjectName] = useState(() => localStorage.getItem('sonic_project_name') || '');
|
||||
const [currentProjectId, setCurrentProjectId] = useState(() => localStorage.getItem('sonic_project_id') || null);
|
||||
const [saveProjectModalOpen, setSaveProjectModalOpen] = useState(false);
|
||||
const [saveAsModalOpen, setSaveAsModalOpen] = useState(false);
|
||||
const [soloedTrackId, setSoloedTrackId] = useState(null);
|
||||
const [toastMessage, setToastMessage] = useState(null);
|
||||
const [showAIConfig, setShowAIConfig] = useState(false);
|
||||
@@ -6521,13 +6859,10 @@ const App = () => {
|
||||
setAuthModalOpen(true);
|
||||
return;
|
||||
}
|
||||
let name = projectName;
|
||||
if (!name) {
|
||||
name = prompt("Nhập tên dự án:", "Dự án SonicForge");
|
||||
if (!name) return;
|
||||
if (!projectName) {
|
||||
setSaveProjectModalOpen(true);
|
||||
return;
|
||||
}
|
||||
setProjectName(name);
|
||||
localStorage.setItem('sonic_project_name', name);
|
||||
const ss = arr => (arr || []).map(t => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
@@ -6546,28 +6881,32 @@ const App = () => {
|
||||
label: t.channelInfo.label
|
||||
} : null
|
||||
}));
|
||||
const dataJson = JSON.stringify({
|
||||
id: currentProjectId || 'project_' + Date.now(),
|
||||
name: projectName,
|
||||
tracks: ss(tracks)
|
||||
});
|
||||
try {
|
||||
const dataJson = JSON.stringify({
|
||||
id: 'project_' + Date.now(),
|
||||
name,
|
||||
tracks: ss(tracks)
|
||||
});
|
||||
await window.SonicAPI.saveCloudProject(name, dataJson);
|
||||
showToast(`Đã lưu "${name}" lên server!`, "success");
|
||||
if (currentProjectId) {
|
||||
await window.SonicAPI.updateCloudProject(currentProjectId, projectName, dataJson);
|
||||
showToast(`Đã cập nhật dự án "${projectName}" lên server!`, "success");
|
||||
} else {
|
||||
const res = await window.SonicAPI.saveCloudProject(projectName, dataJson);
|
||||
const newProjId = res.project_id;
|
||||
if (newProjId) {
|
||||
setCurrentProjectId(newProjId);
|
||||
localStorage.setItem('sonic_project_id', newProjId);
|
||||
}
|
||||
showToast(`Đã lưu dự án "${projectName}" mới lên server!`, "success");
|
||||
}
|
||||
} catch (err) {
|
||||
showToast(err.message || "Lỗi lưu server", "error");
|
||||
}
|
||||
};
|
||||
const handleSaveCloud = async () => {
|
||||
if (!currentUser) {
|
||||
setIsMandatoryLogin(false);
|
||||
setAuthMode('login');
|
||||
setAuthModalOpen(true);
|
||||
return;
|
||||
}
|
||||
const name = prompt("Nhập tên dự án để lưu lên Cloud:", "Dự án SonicForge");
|
||||
if (!name) return;
|
||||
const serializeSafe = arr => (arr || []).map(t => ({
|
||||
const handleSaveProjectWithName = async newName => {
|
||||
setProjectName(newName);
|
||||
localStorage.setItem('sonic_project_name', newName);
|
||||
const ss = arr => (arr || []).map(t => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
startTime: t.startTime,
|
||||
@@ -6585,19 +6924,64 @@ const App = () => {
|
||||
label: t.channelInfo.label
|
||||
} : null
|
||||
}));
|
||||
const dataJson = JSON.stringify({
|
||||
id: 'project_' + Date.now(),
|
||||
name: newName,
|
||||
tracks: ss(tracks)
|
||||
});
|
||||
try {
|
||||
const dataJson = JSON.stringify({
|
||||
id: 'cloud_project',
|
||||
name,
|
||||
tracks: serializeSafe(tracks)
|
||||
});
|
||||
await window.SonicAPI.saveCloudProject(name, dataJson);
|
||||
showToast("Đã lưu dự án lên Cloud thành công!", "success");
|
||||
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 Cloud", "error");
|
||||
showToast(err.message || "Lỗi lưu server", "error");
|
||||
}
|
||||
};
|
||||
const handleExportSFS = () => {
|
||||
const handleSaveAsCloud = async newName => {
|
||||
const ss = arr => (arr || []).map(t => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
startTime: t.startTime,
|
||||
height: t.height,
|
||||
volumeDb: t.volumeDb,
|
||||
pan: t.pan,
|
||||
muted: t.muted,
|
||||
solo: t.solo,
|
||||
color: t.color,
|
||||
markers: t.markers || [],
|
||||
serverFileId: t.serverFileId || null,
|
||||
channelInfo: t.channelInfo ? {
|
||||
channels: t.channelInfo.channels,
|
||||
isStereo: t.channelInfo.isStereo,
|
||||
label: t.channelInfo.label
|
||||
} : null
|
||||
}));
|
||||
const dataJson = JSON.stringify({
|
||||
id: 'project_' + Date.now(),
|
||||
name: newName,
|
||||
tracks: ss(tracks)
|
||||
});
|
||||
try {
|
||||
const res = await window.SonicAPI.saveCloudProject(newName, dataJson);
|
||||
const newProjId = res.project_id;
|
||||
setProjectName(newName);
|
||||
localStorage.setItem('sonic_project_name', newName);
|
||||
if (newProjId) {
|
||||
setCurrentProjectId(newProjId);
|
||||
localStorage.setItem('sonic_project_id', newProjId);
|
||||
}
|
||||
showToast(`Đã lưu dự án dưới tên mới "${newName}" lên server!`, "success");
|
||||
} catch (err) {
|
||||
showToast(err.message || "Lỗi Save As lên server", "error");
|
||||
}
|
||||
};
|
||||
const handleSaveCloud = handleSaveProject;
|
||||
const handleExportSFS = (customName = null) => {
|
||||
const finalName = customName || projectName || 'Dự án SonicForge';
|
||||
const serializeSafe = arr => (arr || []).map(t => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
@@ -6617,8 +7001,8 @@ const App = () => {
|
||||
} : null
|
||||
}));
|
||||
window.SonicStorage.exportProjectToSFS({
|
||||
id: 'proj_' + Date.now(),
|
||||
name: 'Dự án SonicForge',
|
||||
id: currentProjectId || 'proj_' + Date.now(),
|
||||
name: finalName,
|
||||
tracks: serializeSafe(tracks)
|
||||
});
|
||||
showToast("Đã xuất dự án (.sfs) thành công!", "success");
|
||||
@@ -6640,6 +7024,10 @@ const App = () => {
|
||||
}));
|
||||
if (restored.length > 0) {
|
||||
setTracks(restored);
|
||||
setProjectName(proj.name || 'Dự án mới');
|
||||
setCurrentProjectId(null);
|
||||
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");
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -8617,6 +9005,10 @@ const App = () => {
|
||||
serverFileId: null
|
||||
}]);
|
||||
setSelectedTrackId('1');
|
||||
setProjectName('');
|
||||
setCurrentProjectId(null);
|
||||
localStorage.removeItem('sonic_project_name');
|
||||
localStorage.removeItem('sonic_project_id');
|
||||
showToast('New project created', 'info');
|
||||
}
|
||||
}, {
|
||||
@@ -8633,7 +9025,7 @@ const App = () => {
|
||||
label: 'Save As...',
|
||||
icon: 'download',
|
||||
shortcut: 'Ctrl+Alt+S',
|
||||
action: () => handleExportSFS()
|
||||
action: () => setSaveAsModalOpen(true)
|
||||
}, {
|
||||
sep: true
|
||||
}, {
|
||||
@@ -11118,7 +11510,33 @@ const App = () => {
|
||||
onSuccess: handleAuthSuccess
|
||||
}), /*#__PURE__*/React.createElement(ProfileModal, {
|
||||
isOpen: profileModalOpen,
|
||||
onClose: () => setProfileModalOpen(false)
|
||||
onClose: () => setProfileModalOpen(false),
|
||||
tracks: tracks,
|
||||
setTracks: setTracks,
|
||||
setSelectedTrackId: setSelectedTrackId,
|
||||
projectName: projectName,
|
||||
setProjectName: setProjectName,
|
||||
currentProjectId: currentProjectId,
|
||||
setCurrentProjectId: setCurrentProjectId,
|
||||
showToast: showToast
|
||||
}), /*#__PURE__*/React.createElement(SaveProjectModal, {
|
||||
isOpen: saveProjectModalOpen,
|
||||
onClose: () => setSaveProjectModalOpen(false),
|
||||
onSave: newName => {
|
||||
handleSaveProjectWithName(newName);
|
||||
}
|
||||
}), /*#__PURE__*/React.createElement(SaveAsModal, {
|
||||
isOpen: saveAsModalOpen,
|
||||
onClose: () => setSaveAsModalOpen(false),
|
||||
projectName: projectName,
|
||||
onSaveCloud: newName => {
|
||||
handleSaveAsCloud(newName);
|
||||
},
|
||||
onSaveLocal: newName => {
|
||||
handleExportSFS(newName);
|
||||
setProjectName(newName);
|
||||
localStorage.setItem('sonic_project_name', newName);
|
||||
}
|
||||
}), /*#__PURE__*/React.createElement(AIConfigModal, {
|
||||
isOpen: aiConfigModalOpen,
|
||||
onClose: () => setAiConfigModalOpen(false),
|
||||
|
||||
Reference in New Issue
Block a user