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:
2026-07-22 18:42:14 +07:00
parent 6f0fac9f2d
commit 9bf9f38864
5 changed files with 1140 additions and 181 deletions
+88 -4
View File
@@ -236,16 +236,17 @@ async def ai_scan_audio(req: AIScanRequest):
}
@router.post("/ai-cut")
async def ai_cut_audio(req: AICutRequest):
async def ai_cut_audio(req: AICutRequest, current_user: Optional[dict] = Depends(get_optional_user)):
"""
17_AI_SCAN.md Feature 2: Fade-Free AI Cut (Zero-Crossing Aligned Slicing).
Executes raw binary sample slice at exact zero-crossing coordinates.
"""
user_id = current_user["user_id"] if current_user else "anonymous"
from app.core.ai_dsp_engine import AIDSPEngine
import soundfile as sf
import numpy as np
output_file_id = f"ai_cut_{uuid.uuid4().hex[:8]}.wav"
output_file_id = f"user_{user_id}_ai_cut_{uuid.uuid4().hex[:8]}.wav"
out_path = os.path.join(settings.PROCESSED_DIR, output_file_id)
file_path = None
@@ -278,11 +279,12 @@ async def ai_cut_audio(req: AICutRequest):
}
@router.post("/python-tool")
async def run_python_dsp_tool(req: PythonToolRequest):
async def run_python_dsp_tool(req: PythonToolRequest, current_user: Optional[dict] = Depends(get_optional_user)):
"""
Non-AI Python DSP Tools endpoint.
Handles normalize peak, invert phase, swap channels, zero-crossing align, and synth wave generation.
"""
user_id = current_user["user_id"] if current_user else "anonymous"
from app.core.python_tools_engine import PythonToolsEngine
from app.core.ai_dsp_engine import AIDSPEngine
import soundfile as sf
@@ -290,7 +292,7 @@ async def run_python_dsp_tool(req: PythonToolRequest):
if req.tool_type == "synth_wave":
wave = PythonToolsEngine.generate_synth_wave(req.wave_type or "sine", req.freq or 440.0, req.duration or 2.0)
output_file_id = f"synth_{req.wave_type}_{uuid.uuid4().hex[:6]}.wav"
output_file_id = f"user_{user_id}_synth_{req.wave_type}_{uuid.uuid4().hex[:6]}.wav"
out_path = os.path.join(settings.PROCESSED_DIR, output_file_id)
sf.write(out_path, wave, 44100)
return {
@@ -311,3 +313,85 @@ async def run_python_dsp_tool(req: PythonToolRequest):
"success": True,
"message": f"Python Tool '{req.tool_type}' executed successfully for track {req.track_id}"
}
class MyFilesRequest(BaseModel):
active_file_ids: List[str] = []
@router.post("/my-files")
async def list_user_files(req: MyFilesRequest, current_user: dict = Depends(get_current_user)):
user_id = current_user["user_id"]
prefix = f"user_{user_id}_"
# Scan all user's projects to find referenced files
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute("SELECT data_json FROM projects WHERE user_id = ?", (user_id,))
rows = cursor.fetchall()
conn.close()
referenced_in_db = set()
for row in rows:
try:
proj = json.loads(row["data_json"])
for track in proj.get("tracks", []):
fid = track.get("serverFileId")
if fid:
referenced_in_db.add(fid)
except Exception:
pass
active_set = set(req.active_file_ids) | referenced_in_db
files_map = {}
def scan_dir(directory, type_label):
if not os.path.exists(directory):
return
for filename in os.listdir(directory):
if filename.startswith(prefix):
filepath = os.path.join(directory, filename)
if os.path.isfile(filepath):
stat = os.stat(filepath)
is_in_use = filename in active_set
if filename in files_map:
files_map[filename]["size_mb"] = round(files_map[filename]["size_mb"] + stat.st_size / (1024 * 1024), 2)
else:
files_map[filename] = {
"file_id": filename,
"size_mb": round(stat.st_size / (1024 * 1024), 2),
"created_at": stat.st_mtime,
"type": type_label,
"is_in_use": is_in_use
}
scan_dir(settings.UPLOADS_DIR, "Upload")
scan_dir(settings.PROCESSED_DIR, "Processed")
user_files = list(files_map.values())
user_files.sort(key=lambda x: x["created_at"], reverse=True)
return user_files
@router.delete("/my-files/{file_id}")
async def delete_user_file(file_id: str, current_user: dict = Depends(get_current_user)):
user_id = current_user["user_id"]
prefix = f"user_{user_id}_"
# Guard: only own files can be deleted
if not file_id.startswith(prefix):
raise HTTPException(status_code=403, detail="Bạn không có quyền xóa tệp này")
deleted = False
for directory in [settings.UPLOADS_DIR, settings.PROCESSED_DIR]:
filepath = os.path.join(directory, file_id)
if os.path.exists(filepath):
try:
os.remove(filepath)
deleted = True
except Exception:
pass
if not deleted:
raise HTTPException(status_code=404, detail="Không tìm thấy tệp trên server")
return {"success": True, "message": "Đã xóa tệp thành công"}
+506 -117
View File
@@ -2725,18 +2725,41 @@ 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();
@@ -2745,6 +2768,103 @@ 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('');
@@ -2761,80 +2881,300 @@ const ProfileModal = ({
setLoading(false);
}
};
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", {
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", { 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", { 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", { key: "email" }, [
/*#__PURE__*/React.createElement("span", { className: "text-slate-500 block" }, "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", { key: "progress" }, [
/*#__PURE__*/React.createElement("div", { className: "flex justify-between text-xs mb-1" }, [
/*#__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", { className: "w-full h-2 bg-slate-800 rounded-full overflow-hidden" }, [
/*#__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", {
key: "pwd-form",
onSubmit: handleChangePassword,
className: "pt-4 border-t border-[#383838] space-y-3"
}, [
/*#__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", { 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",
autoComplete: "current-password",
required: true,
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", { 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",
autoComplete: "new-password",
required: true,
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", {
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')
])
] : 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-[#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", {
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", {
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", {
className: "text-slate-500 block"
}, "Email"), /*#__PURE__*/React.createElement("span", null, profile.email)), /*#__PURE__*/React.createElement("div", null, /*#__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", {
className: "flex justify-between text-xs mb-1"
}, /*#__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", {
className: "w-full h-2 bg-slate-800 rounded-full overflow-hidden"
}, /*#__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)}%`
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());
}
}))), /*#__PURE__*/React.createElement("form", {
onSubmit: handleChangePassword,
className: "pt-4 border-t border-[#383838] space-y-3"
}, /*#__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", {
className: "block text-xs text-slate-400 mb-1"
}, "Mật khẩu cũ"), /*#__PURE__*/React.createElement("input", {
type: "password",
autoComplete: "current-password",
required: true,
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", {
className: "block text-xs text-slate-400 mb-1"
}, "Mật khẩu mới"), /*#__PURE__*/React.createElement("input", {
type: "password",
autoComplete: "new-password",
required: true,
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", {
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')))));
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,
@@ -3043,6 +3383,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);
@@ -6414,57 +6757,69 @@ const App = () => {
};
const handleSaveProject = async () => {
if (!currentUser) { setIsMandatoryLogin(false); setAuthMode('login'); setAuthModalOpen(true); return; }
let name = projectName;
if (!name) { name = prompt("Nhập tên dự án:", "Dự án SonicForge"); if (!name) return; }
setProjectName(name);
localStorage.setItem('sonic_project_name', name);
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 }));
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");
} catch (err) { showToast(err.message || "Lỗi lưu server", "error"); }
};
const handleSaveCloud = async () => {
if (!currentUser) {
setIsMandatoryLogin(false);
setAuthMode('login');
setAuthModalOpen(true);
if (!projectName) {
setSaveProjectModalOpen(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 => ({
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 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: currentProjectId || 'project_' + Date.now(), name: projectName, 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");
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 Cloud", "error");
showToast(err.message || "Lỗi lưu server", "error");
}
};
const handleExportSFS = () => {
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, 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;
if (newProjId) {
setCurrentProjectId(newProjId);
localStorage.setItem('sonic_project_id', newProjId);
}
showToast(`Đã lưu dự án "${newName}" mới lên server!`, "success");
} catch (err) {
showToast(err.message || "Lỗi lưu server", "error");
}
};
const 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,
@@ -6484,8 +6839,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");
@@ -6507,6 +6862,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) {
@@ -8170,6 +8529,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');
}
}, {
@@ -8186,7 +8549,7 @@ const App = () => {
label: 'Save As...',
icon: 'download',
shortcut: 'Ctrl+Alt+S',
action: () => handleExportSFS()
action: () => setSaveAsModalOpen(true)
}, {
sep: true
}, {
@@ -10603,7 +10966,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),
+478 -60
View File
@@ -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),
+5
View File
@@ -46,6 +46,11 @@ window.API_BASE_URL = window.API_BASE_URL || window.location.origin;
getTempProject: () => apiRequest('/api/v1/projects/temp', { method: 'GET' }),
saveCloudProject: (name, dataJson) => apiRequest('/api/v1/projects/cloud', { method: 'POST', body: JSON.stringify({ name, data_json: dataJson }) }),
listCloudProjects: () => apiRequest('/api/v1/projects/cloud', { method: 'GET' }),
getCloudProject: (projectId) => apiRequest(`/api/v1/projects/cloud/${projectId}`, { method: 'GET' }),
deleteCloudProject: (projectId) => apiRequest(`/api/v1/projects/cloud/${projectId}`, { method: 'DELETE' }),
updateCloudProject: (projectId, name, dataJson) => apiRequest(`/api/v1/projects/cloud/${projectId}`, { method: 'PUT', body: JSON.stringify({ name, data_json: dataJson }) }),
listMyFiles: (activeFileIds) => apiRequest('/api/v1/audio/my-files', { method: 'POST', body: JSON.stringify({ active_file_ids: activeFileIds }) }),
deleteMyFile: (fileId) => apiRequest(`/api/v1/audio/my-files/${fileId}`, { method: 'DELETE' }),
aiScan: (trackId, fileId, minLoopDuration = 2.0, maxLoopDuration = 6.0) => apiRequest('/api/v1/audio/ai-scan', { method: 'POST', body: JSON.stringify({ track_id: trackId, file_id: fileId, min_loop_duration: minLoopDuration, max_loop_duration: maxLoopDuration }) }),
aiCut: (sourceTrackId, fileId, selectionStart, selectionEnd) => apiRequest('/api/v1/audio/ai-cut', { method: 'POST', body: JSON.stringify({ source_track_id: sourceTrackId, file_id: fileId, selection_start: selectionStart, selection_end: selectionEnd }) }),
+63
View File
@@ -0,0 +1,63 @@
# Implementation Plan: Project Management, Save As, and File Management inside Profile
We will add robust cloud/local project management, a custom "Save Project" name modal, a "Save As..." dialog offering server/local options, and a comprehensive file and project manager inside the User Profile Modal.
## User Review Required
> [!IMPORTANT]
> The profile modal will now contain three tabs: Account, Cloud Projects, and My Uploaded Files. Unused files (those not in the current session tracks or any saved projects) can be deleted by the user to free up quota storage.
>
> **Save As...** will trigger a modal allowing the user to type a new name and save it to either the server or export locally as a `.sfs` file.
---
## Proposed Changes
### Backend APIs
#### [MODIFY] [projects.py](file:///home/locpham/SonicForgeStudio/app/api/v1/projects.py)
- **`GET /cloud/{project_id}`**: Retrieves a specific user cloud project.
- **`DELETE /cloud/{project_id}`**: Deletes a specific user cloud project.
- **`PUT /cloud/{project_id}`**: Updates/overwrites an existing user cloud project.
#### [MODIFY] [audio.py](file:///home/locpham/SonicForgeStudio/app/api/v1/audio.py)
- **`POST /upload`**, **`run_python_dsp_tool`** (for synth), and **`ai_cut_audio`**: Prefix file IDs with `user_{user_id}_` to establish file ownership and quota tracking securely.
- **`POST /my-files`**: Lists all files starting with `user_{user_id}_` on the server disk. Identifies if they are referenced in the active project session or any database project records to compute their `is_in_use` status.
- **`DELETE /my-files/{file_id}`**: Deletes a user's uploaded/generated file from the server uploads and processed directories after verifying ownership.
---
### Frontend Services & UI
#### [MODIFY] [api.js](file:///home/locpham/SonicForgeStudio/app/static/js/services/api.js)
- Expose APIs for fetching, deleting, and updating cloud projects.
- Expose APIs for listing and deleting user audio files.
#### [MODIFY] [app.jsx](file:///home/locpham/SonicForgeStudio/app/static/js/app.jsx)
- **State Additions**:
- `currentProjectId`: Tracks the ID of the loaded cloud project (synced with localStorage).
- `saveProjectModalOpen`, `saveAsModalOpen`: Controls the new custom modals.
- **Save Project Modal**:
- Modal with an input for project name, used when saving a project that doesn't have a name yet.
- **Save As Modal**:
- Allows choosing to save under a new name either locally (.sfs file) or on the server.
- **Profile Modal Extensions**:
- Add Tabs: **Account Settings**, **Cloud Projects**, **My Uploaded Files**.
- **Cloud Projects Tab**: Displays saved projects with load (open DAW project) and delete options.
- **My Uploaded Files Tab**: Displays files with sizes, creation dates, usage badges, individual delete buttons, and a global "Clean Up Unused Files" button.
---
## Verification Plan
### Automated Tests
- Run backend lint and sanity checks.
```bash
python -m flake8 app/api/v1/projects.py app/api/v1/audio.py
```
### Manual Verification
1. Create a new project, press Save, verify the custom input modal appears.
2. Upload some files, check the Profile -> My Uploaded Files tab. Verify the files are listed as "In Use".
3. Remove a track containing a file, verify the file changes to "Not In Use". Press delete to free up quota.
4. Click File -> Save As... and select either Cloud or Local .sfs and verify name updates and downloads.