diff --git a/app/api/v1/plugins.py b/app/api/v1/plugins.py index 7d2fab5..9decd2c 100644 --- a/app/api/v1/plugins.py +++ b/app/api/v1/plugins.py @@ -38,18 +38,35 @@ def _save_plugin_dirs(dirs: dict): json.dump(dirs, f, indent=2) def _effective_dirs() -> dict: - """Env/.env (Docker) là base; user dirs (file) override nếu khai báo.""" + """Env/.env (Docker) là base; user dirs (file) override nếu khai báo. + + plugin_dirs: list thư mục user thêm trong Plugins Manager (mỗi thư mục + có thể chứa cả VST lẫn SoundFont — scan tự phân loại). Nếu user chưa + khai báo → fallback env VST_DIR + SOUNDFONT_DIR. + """ user = _load_plugin_dirs() + plugin_dirs = [d for d in (user.get("plugin_dirs") or []) if d] + vst_dir = settings.VST_DIR + soundfont_dir = settings.SOUNDFONT_DIR + # Backward compat: file cũ lưu vst_dir/soundfont_dir riêng → gộp vào list. + if not plugin_dirs: + if user.get("vst_dir"): + plugin_dirs.append(user["vst_dir"]) + if user.get("soundfont_dir"): + plugin_dirs.append(user["soundfont_dir"]) + if not plugin_dirs: + plugin_dirs = [vst_dir, soundfont_dir] return { - "vst_dir": user.get("vst_dir") or settings.VST_DIR, - "soundfont_dir": user.get("soundfont_dir") or settings.SOUNDFONT_DIR, - "vst_dir_user_set": bool(user.get("vst_dir")), - "soundfont_dir_user_set": bool(user.get("soundfont_dir")), + "plugin_dirs": plugin_dirs, + "vst_dir": vst_dir, + "soundfont_dir": soundfont_dir, + "plugin_dirs_user_set": bool(user.get("plugin_dirs")), } class DirsRequest(BaseModel): vst_dir: Optional[str] = None soundfont_dir: Optional[str] = None + plugin_dirs: Optional[list] = None @router.get("/dirs") async def get_plugin_dirs(): @@ -59,10 +76,16 @@ async def get_plugin_dirs(): async def save_plugin_dirs(req: DirsRequest, current_user: dict = Depends(get_current_user)): enforce_password_changed(current_user) user = _load_plugin_dirs() - if req.vst_dir is not None: - user["vst_dir"] = req.vst_dir.strip() - if req.soundfont_dir is not None: - user["soundfont_dir"] = req.soundfont_dir.strip() + if req.plugin_dirs is not None: + user["plugin_dirs"] = [d.strip() for d in req.plugin_dirs if d and d.strip()] + # Xóa field cũ (đã gộp vào plugin_dirs) tránh nhầm lẫn + user.pop("vst_dir", None) + user.pop("soundfont_dir", None) + else: + if req.vst_dir is not None: + user["vst_dir"] = req.vst_dir.strip() + if req.soundfont_dir is not None: + user["soundfont_dir"] = req.soundfont_dir.strip() _save_plugin_dirs(user) return {"success": True, **(_effective_dirs())} @@ -70,34 +93,43 @@ async def save_plugin_dirs(req: DirsRequest, current_user: dict = Depends(get_cu async def scan_plugin_dirs(background_tasks: BackgroundTasks = None, current_user: dict = Depends(get_current_user)): """Scan các dir hiệu lực (env + user override): cập nhật catalog - soundfont (inspector/scanner) + liệt kê VST. Trả về số lượng tìm thấy.""" + soundfont (inspector/scanner) + liệt kê VST. Trả về danh sách riêng rẽ + VST (vst_found) + SoundFont (soundfonts) theo từng thư mục user khai báo.""" enforce_password_changed(current_user) dirs = _effective_dirs() - vst_dir = dirs["vst_dir"] - sf_dir = dirs["soundfont_dir"] + plugin_dirs = dirs["plugin_dirs"] # SoundFont: quét + inspect vào catalog (scan_once dùng dir hiệu lực) - scanner = SoundFontAutoScanner(system_sf_dir=sf_dir, upload_sf_dir=UPLOAD_SF_DIR) + scanner = SoundFontAutoScanner(system_sf_dirs=plugin_dirs, upload_sf_dir=UPLOAD_SF_DIR) if background_tasks: background_tasks.add_task(scanner.scan_once) else: scanner.scan_once() catalog = scanner.get_catalog() - # VST: liệt kê thư mục (walk .vst3/.so) + # VST + SoundFont: walk từng thư mục, phân loại riêng rẽ theo extension vst_found = [] - if os.path.isdir(vst_dir): - for root, _dirs, files in os.walk(vst_dir): + sf_found = [] + for d in plugin_dirs: + if not os.path.isdir(d): + continue + for root, _dirs, files in os.walk(d): for f in files: - if f.endswith(".vst3") or f.endswith(".so") or f.endswith(".dll"): + low = f.lower() + if low.endswith(".vst3") or low.endswith(".dll") or low.endswith(".so"): vst_found.append({"name": os.path.splitext(f)[0], "path": os.path.join(root, f), - "type": "VST3" if f.endswith(".vst3") else "VST2"}) + "dir": d, + "type": "VST3" if low.endswith(".vst3") else "VST2"}) + elif low.endswith(".sf2") or low.endswith(".sf3"): + sf_found.append({"name": os.path.splitext(f)[0], + "path": os.path.join(root, f), + "dir": d}) return { "success": True, - "vst_dir": vst_dir, - "soundfont_dir": sf_dir, + "plugin_dirs": plugin_dirs, "vst_found": vst_found, "vst_count": len(vst_found), - "soundfont_count": len(catalog), + "soundfonts": sf_found, + "soundfont_count": len(sf_found), } _inspector = None @@ -107,14 +139,15 @@ def get_inspector(): global _inspector if _inspector is None: d = _effective_dirs() - _inspector = SoundFontInspector(system_sf_dir=d["soundfont_dir"], upload_sf_dir=UPLOAD_SF_DIR) + _inspector = SoundFontInspector(d["plugin_dirs"][0] if d["plugin_dirs"] else settings.SOUNDFONT_DIR, + upload_sf_dir=UPLOAD_SF_DIR) return _inspector def get_scanner(): global _scanner if _scanner is None: d = _effective_dirs() - _scanner = SoundFontAutoScanner(system_sf_dir=d["soundfont_dir"], upload_sf_dir=UPLOAD_SF_DIR) + _scanner = SoundFontAutoScanner(system_sf_dirs=d["plugin_dirs"], upload_sf_dir=UPLOAD_SF_DIR) _scanner.scan_once() return _scanner diff --git a/app/core/soundfont_scanner.py b/app/core/soundfont_scanner.py index 3616345..3d70a59 100644 --- a/app/core/soundfont_scanner.py +++ b/app/core/soundfont_scanner.py @@ -19,8 +19,11 @@ def _file_sig(path: str) -> tuple: class SoundFontAutoScanner: - def __init__(self, system_sf_dir=SYSTEM_SF_DIR, upload_sf_dir=UPLOAD_SF_DIR): - self.system_sf_dir = system_sf_dir + def __init__(self, system_sf_dir=SYSTEM_SF_DIR, upload_sf_dir=UPLOAD_SF_DIR, + system_sf_dirs=None): + # system_sf_dirs (list) — nhiều thư mục user khai báo trong Plugins + # Manager. Fallback system_sf_dir (env/.env) nếu list rỗng. + self.system_sf_dirs = [d for d in (system_sf_dirs or []) if d] or [system_sf_dir] self.upload_sf_dir = upload_sf_dir self._catalog = {} self._lock = threading.Lock() @@ -50,6 +53,15 @@ class SoundFontAutoScanner: out.append((fname, os.path.join(directory, fname))) return out + def _all_sf_files(self) -> list: + """Gộp file .sf2/.sf3 từ TẤT CẢ thư mục hiệu lực (user dirs + upload).""" + out = [] + for d in self.system_sf_dirs: + out.extend(self._sf_files(d)) + if self.upload_sf_dir and os.path.isdir(self.upload_sf_dir): + out.extend(self._sf_files(self.upload_sf_dir)) + return out + def _inspect_single(self, fname: str, full: str, inspector) -> dict: if fname.lower().endswith(".sf2"): sf_info = inspector.inspect_sf2_file(full) or {} @@ -68,9 +80,9 @@ class SoundFontAutoScanner: def scan_once(self) -> bool: from app.core.soundfont_inspector import SoundFontInspector - inspector = SoundFontInspector(self.system_sf_dir, self.upload_sf_dir) + inspector = SoundFontInspector(self.system_sf_dirs[0], self.upload_sf_dir) found_new = False - dirs = [(self.system_sf_dir, "system")] + dirs = [(d, "system") for d in self.system_sf_dirs] if self.upload_sf_dir and os.path.isdir(self.upload_sf_dir): dirs.append((self.upload_sf_dir, "upload")) diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index ed4063c..bcf1dff 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -5284,47 +5284,52 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData }) => { const [localData, setLocalData] = React.useState(pluginsData); const [sfUploadStatus, setSfUploadStatus] = React.useState(''); const [sfToDelete, setSfToDelete] = React.useState(null); - const [pmVstDir, setPmVstDir] = React.useState(''); - const [pmSfDir, setPmSfDir] = React.useState(''); + const [pmDirs, setPmDirs] = React.useState([]); const [pmScanning, setPmScanning] = React.useState(false); const [pmScanResult, setPmScanResult] = React.useState(''); + const [pmScanData, setPmScanData] = React.useState(null); // { vst_found, soundfonts } React.useEffect(() => { if (isOpen) { window.SonicAPI.listPlugins() .then(data => setLocalData(data)) .catch(() => setLocalData({ vst_instruments: [], soundfonts: [] })); window.SonicAPI.getPluginDirs() - .then(d => { - setPmVstDir(d.vst_dir || ''); - setPmSfDir(d.soundfont_dir || ''); - }) + .then(d => setPmDirs(d.plugin_dirs || [])) .catch(() => {}); setTimeout(() => { try { window.lucide.createIcons(); } catch(e) {} }, 50); } }, [isOpen]); // Folder picker: Tauri dialog (desktop) nếu có; fallback: paste path. - const pickPluginFolder = async (which) => { + const pickPluginFolder = async () => { try { if (window.__TAURI__ && window.__TAURI__.dialog) { const sel = await window.__TAURI__.dialog.open({ directory: true, multiple: false }); if (typeof sel === 'string' && sel) { - if (which === 'vst') setPmVstDir(sel); - else setPmSfDir(sel); + if (!pmDirs.includes(sel)) setPmDirs(prev => [...prev, sel]); } return; } - showToast('Desktop build: dùng nút Browse. Browser: dán đường dẫn vào ô.', 'info'); + const manual = window.prompt('Nhập đường dẫn thư mục plugin (VST / SoundFont):'); + if (manual && manual.trim()) { + const p = manual.trim(); + if (!pmDirs.includes(p)) setPmDirs(prev => [...prev, p]); + } } catch (e) { showToast('Browse failed: ' + (e.message || e), 'error'); } }; + const removePluginDir = (dir) => { + setPmDirs(prev => prev.filter(d => d !== dir)); + }; // Lưu dirs (user override) + scan cả 2 thư mục → refresh list + catalog. const saveAndScanDirs = async () => { setPmScanning(true); setPmScanResult(''); + setPmScanData(null); try { - await window.SonicAPI.savePluginDirs({ vst_dir: pmVstDir, soundfont_dir: pmSfDir }); + await window.SonicAPI.savePluginDirs({ plugin_dirs: pmDirs }); const scan = await window.SonicAPI.scanPluginDirs(); + setPmScanData({ vst_found: scan.vst_found || [], soundfonts: scan.soundfonts || [] }); const data = await window.SonicAPI.listPlugins(); setLocalData(data); try { @@ -5455,44 +5460,73 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData }) => { React.createElement('div', { className: 'pt-4 mt-4 border-t border-[#383838]' }, - React.createElement('h4', { className: 'text-xs font-bold text-zinc-400 mb-3 uppercase' }, - 'Plugin Directories (VST / SoundFont)'), - React.createElement('div', { className: 'flex gap-2 mb-2' }, - React.createElement('input', { - type: 'text', - value: pmVstDir, - onChange: e => setPmVstDir(e.target.value), - placeholder: 'VST directory (e.g. C:\\VSTs or /opt/daw_engine/vst3)', - className: 'flex-1 bg-zinc-800 border border-zinc-700 rounded px-3 py-2 text-xs text-zinc-300 focus:outline-none focus:border-violet-600 font-mono' - }), + React.createElement('div', { className: 'flex items-center justify-between mb-2' }, + React.createElement('h4', { className: 'text-xs font-bold text-zinc-400 uppercase' }, + 'Plugin Directories'), React.createElement('button', { - className: 'px-3 py-2 bg-zinc-700 hover:bg-zinc-600 text-white text-xs font-semibold rounded transition shrink-0', - title: 'Browse folder (desktop) - fallback: paste path', - onClick: () => pickPluginFolder('vst') - }, 'Browse...') + className: 'px-3 py-1.5 bg-violet-800 hover:bg-violet-700 text-white text-xs font-semibold rounded transition flex items-center gap-1 shrink-0', + title: 'Add plugin directory (VST / SoundFont)', + onClick: pickPluginFolder + }, + React.createElement('i', { 'data-lucide': 'plus', className: 'w-3 h-3' }), 'Add Directory') ), - React.createElement('div', { className: 'flex gap-2 mb-3' }, - React.createElement('input', { - type: 'text', - value: pmSfDir, - onChange: e => setPmSfDir(e.target.value), - placeholder: 'SoundFont directory (e.g. C:\\SoundFonts or /opt/daw_engine/soundfonts)', - className: 'flex-1 bg-zinc-800 border border-zinc-700 rounded px-3 py-2 text-xs text-zinc-300 focus:outline-none focus:border-amber-600 font-mono' - }), - React.createElement('button', { - className: 'px-3 py-2 bg-zinc-700 hover:bg-zinc-600 text-white text-xs font-semibold rounded transition shrink-0', - title: 'Browse folder (desktop) - fallback: paste path', - onClick: () => pickPluginFolder('soundfont') - }, 'Browse...') + pmDirs.length === 0 && + React.createElement('p', { className: 'text-[10px] text-zinc-600 mb-2 italic' }, + 'Chưa có thư mục nào. Nhấn Add Directory để chọn thư mục chứa VST / SoundFont.'), + pmDirs.map((dir, idx) => + React.createElement('div', { + key: 'pdir_' + idx, + className: 'flex items-center gap-2 mb-1.5 bg-zinc-800/70 border border-zinc-700 rounded px-2 py-1.5' + }, + React.createElement('button', { + className: 'text-zinc-500 hover:text-red-400 transition shrink-0', + title: 'Remove directory', + onClick: () => removePluginDir(dir) + }, + React.createElement('i', { 'data-lucide': 'x', className: 'w-3.5 h-3.5' })), + React.createElement('span', { + className: 'flex-1 text-[11px] text-zinc-300 font-mono truncate', + title: dir + }, dir), + React.createElement('i', { + 'data-lucide': 'folder', + className: 'w-3 h-3 text-zinc-600 shrink-0' + }) + ) ), - React.createElement('div', { className: 'flex gap-2 items-center' }, + React.createElement('div', { className: 'flex gap-2 items-center mt-2' }, React.createElement('button', { className: 'px-4 py-2 bg-emerald-800 hover:bg-emerald-700 text-white text-xs font-semibold rounded transition flex items-center gap-1', onClick: saveAndScanDirs }, - React.createElement('i', { 'data-lucide': 'save', className: 'w-3 h-3' }), 'Save & Scan'), + React.createElement('i', { 'data-lucide': 'search', className: 'w-3 h-3' }), 'Scan'), pmScanning && React.createElement('span', { className: 'text-[10px] text-emerald-400' }, 'Scanning...'), pmScanResult && React.createElement('span', { className: 'text-[10px] text-zinc-400' }, pmScanResult) + ), + pmScanData && React.createElement('div', { className: 'mt-3 space-y-2 max-h-40 overflow-y-auto' }, + React.createElement('div', { className: 'text-[10px] font-bold text-violet-300 uppercase flex items-center gap-1' }, + React.createElement('i', { 'data-lucide': 'cpu', className: 'w-3 h-3' }), + 'VST Instruments (' + pmScanData.vst_found.length + ')'), + pmScanData.vst_found.length === 0 ? + React.createElement('p', { className: 'text-[10px] text-zinc-600 italic' }, 'Không tìm thấy VST.') : + pmScanData.vst_found.map((v, i) => + React.createElement('div', { key: 'sv_' + i, className: 'flex items-center gap-2 text-[11px] text-zinc-300' }, + React.createElement('span', { className: 'w-16 shrink-0 text-zinc-500 font-mono text-[9px] truncate' }, v.type || 'VST'), + React.createElement('span', { className: 'truncate' }, v.name), + React.createElement('span', { className: 'text-[9px] text-zinc-600 font-mono truncate ml-auto' }, v.dir) + ) + ), + React.createElement('div', { className: 'text-[10px] font-bold text-amber-300 uppercase flex items-center gap-1 mt-2' }, + React.createElement('i', { 'data-lucide': 'music', className: 'w-3 h-3' }), + 'SoundFonts (' + pmScanData.soundfonts.length + ')'), + pmScanData.soundfonts.length === 0 ? + React.createElement('p', { className: 'text-[10px] text-zinc-600 italic' }, 'Không tìm thấy SoundFont.') : + pmScanData.soundfonts.map((s, i) => + React.createElement('div', { key: 'ss_' + i, className: 'flex items-center gap-2 text-[11px] text-zinc-300' }, + React.createElement('span', { className: 'truncate' }, s.name), + React.createElement('span', { className: 'text-[9px] text-zinc-600 font-mono truncate ml-auto' }, s.dir) + ) + ) ) ), // Upload section (bottom of right panel) diff --git a/app/static/js/app.precompiled.js b/app/static/js/app.precompiled.js index e2cfd01..8d2be4f 100644 --- a/app/static/js/app.precompiled.js +++ b/app/static/js/app.precompiled.js @@ -362,15 +362,16 @@ const GraphEditorCanvas=({buffer,zoom,timelineWidth,volumeNodes,panningNodes,fad // username/mật khẩu; sau khi đã đổi → ẩn vĩnh viễn. useEffect(()=>{if(!isOpen)return;if(window.SonicAPI&&window.SonicAPI.apiRequest){window.SonicAPI.apiRequest('/api/v1/auth/first-time',{method:'GET'}).then(function(d){setFirstTime(!!(d&&d.first_time));})// Fail-closed: endpoint lỗi/404 (backend chưa restart) → ẨN gợi ý // (không hiện — user đã yêu cầu bỏ gợi ý sau lần đầu). -.catch(function(){setFirstTime(false);});}else{setFirstTime(false);}},[isOpen]);const handleSubmit=async e=>{e.preventDefault();setError('');setLoading(true);try{if(activeTab==='login'){const targetUsername=username.trim()||'admin';const targetPwd=password.trim()||'admin123';const res=await window.SonicAPI.login(targetUsername,targetPwd);localStorage.setItem('sonic_token',res.access_token);localStorage.setItem('sonic_user',JSON.stringify(res.user));if(res.user&&res.user.must_change_password){setOldPassword(targetPwd);}onSuccess(res.user,res.access_token);}else if(activeTab==='register'){const res=await window.SonicAPI.register(username.trim(),email.trim(),password.trim());localStorage.setItem('sonic_token',res.access_token);localStorage.setItem('sonic_user',JSON.stringify(res.user));onSuccess(res.user,res.access_token);}else if(activeTab==='force_change'){const res=await window.SonicAPI.changePassword(oldPassword.trim(),newPassword.trim());localStorage.setItem('sonic_token',res.access_token);const user=JSON.parse(localStorage.getItem('sonic_user')||'{}');user.must_change_password=false;localStorage.setItem('sonic_user',JSON.stringify(user));onSuccess(user,res.access_token);}}catch(err){setError(err.message||(activeTab==='login'?'Tài khoản hoặc mật khẩu không chính xác. (Nếu bạn đã đổi mật khẩu trước đó, vui lòng nhập mật khẩu mới mà bạn đã tạo)':'Thao tác không thành công'));}finally{setLoading(false);}};const isForceMode=activeTab==='force_change';const canClose=!forceMandatory&&!isForceMode;return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-md"},/*#__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("div",{className:"flex justify-between items-center pb-4 border-b border-[#383838]"},/*#__PURE__*/React.createElement("h3",{className:"text-lg font-bold text-teal-400"},isForceMode?'⚠️ Bắt Buộc Đổi Mật Khẩu Khởi Tạo':activeTab==='login'?'🔐 Đăng Nhập Hệ Thống':'📝 Đăng Ký Tài Khoản'),canClose&&/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"text-slate-400 hover:text-slate-200"},"✕")),error&&/*#__PURE__*/React.createElement("div",{className:"mt-4 p-3 bg-red-900/40 border border-red-700 rounded-lg text-red-200 text-sm"},error),/*#__PURE__*/React.createElement("form",{onSubmit:handleSubmit,className:"mt-4 space-y-4"},isForceMode?/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("p",{className:"text-xs text-amber-400 bg-amber-950/60 p-2.5 border border-amber-800/80 rounded leading-relaxed"},"🔒 Tài khoản của bạn đang dùng mật khẩu khởi tạo mặc định. Để bảo mật hệ thống, bạn phải đổi mật khẩu mới trước khi tiếp tục."),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold mb-1 text-slate-400"},"Mật khẩu hiện tại (Mặc định: admin123)"),/*#__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-2 text-sm focus:outline-none focus:border-teal-500"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold mb-1 text-slate-400"},"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-2 text-sm focus:outline-none focus:border-teal-500"}))):/*#__PURE__*/React.createElement(React.Fragment,null,activeTab==='login'?/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold mb-1 text-slate-400"},"Tên đăng nhập ",firstTime&&/*#__PURE__*/React.createElement("span",{className:"text-teal-400 font-normal"},"(Tùy chọn - Admin có thể bỏ trống)")),/*#__PURE__*/React.createElement("input",{type:"text",placeholder:"Mặc định: admin",value:username,onChange:e=>setUsername(e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-2 text-sm focus:outline-none focus:border-teal-500"})):/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold mb-1 text-slate-400"},"Tên đăng nhập"),/*#__PURE__*/React.createElement("input",{type:"text",required:true,value:username,onChange:e=>setUsername(e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-2 text-sm focus:outline-none focus:border-teal-500"})),activeTab==='register'&&/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold mb-1 text-slate-400"},"Email"),/*#__PURE__*/React.createElement("input",{type:"email",required:true,value:email,onChange:e=>setEmail(e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-2 text-sm focus:outline-none focus:border-teal-500"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold mb-1 text-slate-400"},"Mật khẩu ",activeTab==='login'&&firstTime&&/*#__PURE__*/React.createElement("span",{className:"text-amber-400 font-normal"},"(Lần đầu: admin123)")),/*#__PURE__*/React.createElement("input",{type:"password",autoComplete:activeTab==='login'?'current-password':'new-password',required:true,value:password,onChange:e=>setPassword(e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-2 text-sm focus:outline-none focus:border-teal-500"}))),/*#__PURE__*/React.createElement("button",{type:"submit",disabled:loading,className:"w-full py-2 bg-teal-600 hover:bg-teal-500 text-white font-semibold rounded-lg shadow transition duration-150"},loading?'Đang xác thực...':isForceMode?'Đổi Mật Khẩu Ngay':activeTab==='login'?'Đăng Nhập System':'Tạo Tài Khoản Mới'),activeTab==='login'&&!isForceMode&&firstTime&&/*#__PURE__*/React.createElement("button",{type:"button",onClick:()=>{setUsername('admin');setPassword('admin123');setError('');},className:"w-full mt-2 py-1.5 bg-amber-950/60 hover:bg-amber-900/80 text-amber-300 border border-amber-800/60 text-xs font-semibold rounded-lg transition flex items-center justify-center gap-1.5"},"🔑 Điền Nhanh Tài Khoản Admin (admin / admin123)")),!isForceMode&&/*#__PURE__*/React.createElement("div",{className:"mt-4 pt-4 border-t border-[#383838] text-center text-xs text-slate-400"},activeTab==='login'?/*#__PURE__*/React.createElement("span",null,"Chưa có tài khoản? ",/*#__PURE__*/React.createElement("button",{onClick:()=>setActiveTab('register'),className:"text-teal-400 hover:underline"},"Đăng ký ngay")):/*#__PURE__*/React.createElement("span",null,"Đã có tài khoản? ",/*#__PURE__*/React.createElement("button",{onClick:()=>setActiveTab('login'),className:"text-teal-400 hover:underline"},"Đăng nhập")))));};const AIConfigModal=({isOpen,onClose,onConfigSaved})=>{if(!isOpen)return null;const defaultProvidersList=[{id:'openai_default',name:'OpenAI Official',provider_type:'openai',api_base_url:'https://api.openai.com/v1',api_key:'',model_name:'gpt-4o',temperature:0.7,is_active:true},{id:'openai_compat_default',name:'OpenAI Compatible (Ollama/LocalAI/DeepSeek)',provider_type:'openai_compatible',api_base_url:'http://localhost:11434/v1',api_key:'ollama',model_name:'deepseek-r1',temperature:0.7,is_active:false},{id:'anthropic_default',name:'Anthropic Claude',provider_type:'anthropic',api_base_url:'https://api.anthropic.com/v1',api_key:'',model_name:'claude-3-5-sonnet',temperature:0.7,is_active:false},{id:'gemini_default',name:'Google Gemini',provider_type:'gemini',api_base_url:'https://generativelanguage.googleapis.com',api_key:'',model_name:'gemini-1.5-pro',temperature:0.7,is_active:false}];const[providers,setProviders]=useState(defaultProvidersList);const[selectedId,setSelectedId]=useState('openai_default');const[msg,setMsg]=useState('');const[error,setError]=useState('');const[loading,setLoading]=useState(false);useEffect(()=>{if(isOpen)loadConfigs();},[isOpen]);const loadConfigs=async()=>{setLoading(true);setError('');try{const data=await window.SonicAPI.getAIConfigs();if(data&&data.providers){setProviders(data.providers);if(data.providers.length>0)setSelectedId(data.providers[0].id);}}catch(err){setError(err.message||'Lỗi nạp cấu hình AI');}finally{setLoading(false);}};const handleSave=async e=>{e.preventDefault();setMsg('');setError('');setLoading(true);try{const res=await window.SonicAPI.saveAIConfigs(providers);setMsg(res.message||'Đã lưu cấu hình AI Providers thành công!');if(onConfigSaved)onConfigSaved(providers);}catch(err){setError(err.message||'Lỗi khi lưu cấu hình AI');}finally{setLoading(false);}};const updateProviderField=(id,field,value)=>{setProviders(prev=>prev.map(p=>p.id===id?{...p,[field]:value}:p));};const activeProvider=providers.find(p=>p.id===selectedId)||providers[0];return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm select-none"},/*#__PURE__*/React.createElement("div",{className:"bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-2xl p-6 text-slate-200"},/*#__PURE__*/React.createElement("div",{className:"flex justify-between items-center pb-4 border-b border-[#383838]"},/*#__PURE__*/React.createElement("h3",{className:"text-lg font-bold text-cyan-400 flex items-center gap-2"},"🤖 Quản Lý & Cấu Hình AI Providers"),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"text-slate-400 hover:text-slate-200"},"✕")),msg&&/*#__PURE__*/React.createElement("div",{className:"mt-3 p-2 bg-emerald-950/60 border border-emerald-700 text-emerald-300 rounded text-xs"},msg),error&&/*#__PURE__*/React.createElement("div",{className:"mt-3 p-2 bg-red-950/60 border border-red-700 text-red-300 rounded text-xs"},error),/*#__PURE__*/React.createElement("div",{className:"mt-4 grid grid-cols-3 gap-4"},/*#__PURE__*/React.createElement("div",{className:"space-y-1.5 border-r border-[#383838] pr-3"},/*#__PURE__*/React.createElement("span",{className:"text-xs uppercase font-bold text-slate-400 block mb-2"},"Providers"),providers.map(p=>/*#__PURE__*/React.createElement("button",{key:p.id,tabIndex:0,onClick:()=>setSelectedId(p.id),onKeyDown:e=>{if(e.key===' '){e.preventDefault();updateProviderField(p.id,'is_active',!p.is_active);}},className:`w-full text-left px-3 py-2 rounded-lg text-xs font-semibold flex items-center justify-between transition ${selectedId===p.id?'bg-cyan-600 text-white shadow':'bg-[#1e1e1e] text-slate-300 hover:bg-[#2e2e2e]'}`},/*#__PURE__*/React.createElement("span",{className:"truncate"},p.name),p.is_active&&/*#__PURE__*/React.createElement("span",{className:"w-2 h-2 rounded-full bg-emerald-400"}))),/*#__PURE__*/React.createElement("div",{className:"flex gap-1 mt-2"},/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();const rearrangeNewId='provider_'+Date.now();setProviders(prev=>[...prev,{id:rearrangeNewId,name:'New Provider',provider_type:'openai_compatible',api_base_url:'https://api.openai.com/v1',api_key:'',model_name:'gpt-4o-mini',temperature:0.7,is_active:false}]);setSelectedId(rearrangeNewId);},className:"flex-1 px-2 py-1 bg-emerald-700 hover:bg-emerald-600 text-white text-xs font-bold rounded"},"+ Thêm"),providers.length>1&&/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();if(confirm(`Xóa provider "${providers.find(p=>p.id===selectedId)?.name}"?`)){setProviders(prev=>{const filtered=prev.filter(p=>p.id!==selectedId);if(filtered.length>0)setSelectedId(filtered[0].id);return filtered;});}},className:"px-2 py-1 bg-red-800 hover:bg-red-700 text-white text-xs font-bold rounded"},"Xóa"),providers.length>1&&/*#__PURE__*/React.createElement("button",{onClick:function(e){e.stopPropagation();var provs=providers.slice();var pos=-1;for(var pi=0;pi0){var tmp=provs[pos];provs[pos]=provs[pos-1];provs[pos-1]=tmp;setProviders(provs);}},className:"px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-white text-xs font-bold rounded",title:"Di chuyển lên"},"▲"),providers.length>1&&/*#__PURE__*/React.createElement("button",{onClick:function(e){e.stopPropagation();var provs=providers.slice();var pos=-1;for(var pi=0;piupdateProviderField(activeProvider.id,'name',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-cyan-500"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold text-slate-400 mb-1"},"API Base URL (Endpoint)"),/*#__PURE__*/React.createElement("input",{type:"text",value:activeProvider.api_base_url||'',onChange:e=>updateProviderField(activeProvider.id,'api_base_url',e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs font-mono text-cyan-300 focus:outline-none focus:border-cyan-500",placeholder:"https://api.openai.com/v1"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold text-slate-400 mb-1"},"API Key Cá Nhân"),/*#__PURE__*/React.createElement("input",{type:"password",value:activeProvider.api_key||'',onChange:e=>updateProviderField(activeProvider.id,'api_key',e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs font-mono text-slate-200 focus:outline-none focus:border-cyan-500",placeholder:"sk-..."})),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-3"},/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold text-slate-400 mb-1"},"Model Name"),/*#__PURE__*/React.createElement("input",{type:"text",value:activeProvider.model_name||'',onChange:e=>updateProviderField(activeProvider.id,'model_name',e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs font-mono text-amber-300 focus:outline-none focus:border-cyan-500"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold text-slate-400 mb-1"},"Temperature"),/*#__PURE__*/React.createElement("input",{type:"number",step:"0.1",min:"0",max:"2",value:activeProvider.temperature??0.7,onChange:e=>updateProviderField(activeProvider.id,'temperature',parseFloat(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-cyan-500"}))),/*#__PURE__*/React.createElement("div",{className:"pt-2 flex items-center justify-between"},/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-2 cursor-pointer text-xs text-slate-300"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:activeProvider.is_active,onChange:e=>updateProviderField(activeProvider.id,'is_active',e.target.checked),className:"rounded accent-cyan-500"}),"Kích hoạt Provider này"),/*#__PURE__*/React.createElement("button",{type:"submit",disabled:loading,className:"px-4 py-1.5 bg-cyan-600 hover:bg-cyan-500 text-white font-semibold text-xs rounded-lg shadow transition"},loading?'Đang lưu...':'Lưu Cấu Hình AI'))))));};const PluginManagerModal=({isOpen,onClose,pluginsData})=>{if(!isOpen)return null;const[localData,setLocalData]=React.useState(pluginsData);const[sfUploadStatus,setSfUploadStatus]=React.useState('');const[sfToDelete,setSfToDelete]=React.useState(null);const[pmVstDir,setPmVstDir]=React.useState('');const[pmSfDir,setPmSfDir]=React.useState('');const[pmScanning,setPmScanning]=React.useState(false);const[pmScanResult,setPmScanResult]=React.useState('');React.useEffect(()=>{if(isOpen){window.SonicAPI.listPlugins().then(data=>setLocalData(data)).catch(()=>setLocalData({vst_instruments:[],soundfonts:[]}));window.SonicAPI.getPluginDirs().then(d=>{setPmVstDir(d.vst_dir||'');setPmSfDir(d.soundfont_dir||'');}).catch(()=>{});setTimeout(()=>{try{window.lucide.createIcons();}catch(e){}},50);}},[isOpen]);// Folder picker: Tauri dialog (desktop) nếu có; fallback: paste path. -const pickPluginFolder=async which=>{try{if(window.__TAURI__&&window.__TAURI__.dialog){const sel=await window.__TAURI__.dialog.open({directory:true,multiple:false});if(typeof sel==='string'&&sel){if(which==='vst')setPmVstDir(sel);else setPmSfDir(sel);}return;}showToast('Desktop build: dùng nút Browse. Browser: dán đường dẫn vào ô.','info');}catch(e){showToast('Browse failed: '+(e.message||e),'error');}};// Lưu dirs (user override) + scan cả 2 thư mục → refresh list + catalog. -const saveAndScanDirs=async()=>{setPmScanning(true);setPmScanResult('');try{await window.SonicAPI.savePluginDirs({vst_dir:pmVstDir,soundfont_dir:pmSfDir});const scan=await window.SonicAPI.scanPluginDirs();const data=await window.SonicAPI.listPlugins();setLocalData(data);try{const cat=await window.SonicAPI.getSoundfontCatalog();window.__soundfontCatalog=cat;}catch(_){}setPmScanResult(`VST: ${scan.vst_count||0} | SoundFonts: ${scan.soundfont_count||0}`);showToast(`Scan xong: ${scan.vst_count||0} VST, ${scan.soundfont_count||0} SoundFonts.`,'success');}catch(err){setPmScanResult('Scan failed: '+(err.message||err));showToast('Scan failed: '+(err.message||err),'error');}finally{setPmScanning(false);}};const handleUploadSF=async e=>{const file=e.target.files?.[0];if(!file)return;setSfUploadStatus('Uploading...');try{const result=await window.SonicAPI.uploadSoundFont(file);setSfUploadStatus('Uploaded: '+result.name);// Refresh plugin list and catalog +.catch(function(){setFirstTime(false);});}else{setFirstTime(false);}},[isOpen]);const handleSubmit=async e=>{e.preventDefault();setError('');setLoading(true);try{if(activeTab==='login'){const targetUsername=username.trim()||'admin';const targetPwd=password.trim()||'admin123';const res=await window.SonicAPI.login(targetUsername,targetPwd);localStorage.setItem('sonic_token',res.access_token);localStorage.setItem('sonic_user',JSON.stringify(res.user));if(res.user&&res.user.must_change_password){setOldPassword(targetPwd);}onSuccess(res.user,res.access_token);}else if(activeTab==='register'){const res=await window.SonicAPI.register(username.trim(),email.trim(),password.trim());localStorage.setItem('sonic_token',res.access_token);localStorage.setItem('sonic_user',JSON.stringify(res.user));onSuccess(res.user,res.access_token);}else if(activeTab==='force_change'){const res=await window.SonicAPI.changePassword(oldPassword.trim(),newPassword.trim());localStorage.setItem('sonic_token',res.access_token);const user=JSON.parse(localStorage.getItem('sonic_user')||'{}');user.must_change_password=false;localStorage.setItem('sonic_user',JSON.stringify(user));onSuccess(user,res.access_token);}}catch(err){setError(err.message||(activeTab==='login'?'Tài khoản hoặc mật khẩu không chính xác. (Nếu bạn đã đổi mật khẩu trước đó, vui lòng nhập mật khẩu mới mà bạn đã tạo)':'Thao tác không thành công'));}finally{setLoading(false);}};const isForceMode=activeTab==='force_change';const canClose=!forceMandatory&&!isForceMode;return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-md"},/*#__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("div",{className:"flex justify-between items-center pb-4 border-b border-[#383838]"},/*#__PURE__*/React.createElement("h3",{className:"text-lg font-bold text-teal-400"},isForceMode?'⚠️ Bắt Buộc Đổi Mật Khẩu Khởi Tạo':activeTab==='login'?'🔐 Đăng Nhập Hệ Thống':'📝 Đăng Ký Tài Khoản'),canClose&&/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"text-slate-400 hover:text-slate-200"},"✕")),error&&/*#__PURE__*/React.createElement("div",{className:"mt-4 p-3 bg-red-900/40 border border-red-700 rounded-lg text-red-200 text-sm"},error),/*#__PURE__*/React.createElement("form",{onSubmit:handleSubmit,className:"mt-4 space-y-4"},isForceMode?/*#__PURE__*/React.createElement(React.Fragment,null,/*#__PURE__*/React.createElement("p",{className:"text-xs text-amber-400 bg-amber-950/60 p-2.5 border border-amber-800/80 rounded leading-relaxed"},"🔒 Tài khoản của bạn đang dùng mật khẩu khởi tạo mặc định. Để bảo mật hệ thống, bạn phải đổi mật khẩu mới trước khi tiếp tục."),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold mb-1 text-slate-400"},"Mật khẩu hiện tại (Mặc định: admin123)"),/*#__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-2 text-sm focus:outline-none focus:border-teal-500"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold mb-1 text-slate-400"},"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-2 text-sm focus:outline-none focus:border-teal-500"}))):/*#__PURE__*/React.createElement(React.Fragment,null,activeTab==='login'?/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold mb-1 text-slate-400"},"Tên đăng nhập ",firstTime&&/*#__PURE__*/React.createElement("span",{className:"text-teal-400 font-normal"},"(Tùy chọn - Admin có thể bỏ trống)")),/*#__PURE__*/React.createElement("input",{type:"text",placeholder:"Mặc định: admin",value:username,onChange:e=>setUsername(e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-2 text-sm focus:outline-none focus:border-teal-500"})):/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold mb-1 text-slate-400"},"Tên đăng nhập"),/*#__PURE__*/React.createElement("input",{type:"text",required:true,value:username,onChange:e=>setUsername(e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-2 text-sm focus:outline-none focus:border-teal-500"})),activeTab==='register'&&/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold mb-1 text-slate-400"},"Email"),/*#__PURE__*/React.createElement("input",{type:"email",required:true,value:email,onChange:e=>setEmail(e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-2 text-sm focus:outline-none focus:border-teal-500"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold mb-1 text-slate-400"},"Mật khẩu ",activeTab==='login'&&firstTime&&/*#__PURE__*/React.createElement("span",{className:"text-amber-400 font-normal"},"(Lần đầu: admin123)")),/*#__PURE__*/React.createElement("input",{type:"password",autoComplete:activeTab==='login'?'current-password':'new-password',required:true,value:password,onChange:e=>setPassword(e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-2 text-sm focus:outline-none focus:border-teal-500"}))),/*#__PURE__*/React.createElement("button",{type:"submit",disabled:loading,className:"w-full py-2 bg-teal-600 hover:bg-teal-500 text-white font-semibold rounded-lg shadow transition duration-150"},loading?'Đang xác thực...':isForceMode?'Đổi Mật Khẩu Ngay':activeTab==='login'?'Đăng Nhập System':'Tạo Tài Khoản Mới'),activeTab==='login'&&!isForceMode&&firstTime&&/*#__PURE__*/React.createElement("button",{type:"button",onClick:()=>{setUsername('admin');setPassword('admin123');setError('');},className:"w-full mt-2 py-1.5 bg-amber-950/60 hover:bg-amber-900/80 text-amber-300 border border-amber-800/60 text-xs font-semibold rounded-lg transition flex items-center justify-center gap-1.5"},"🔑 Điền Nhanh Tài Khoản Admin (admin / admin123)")),!isForceMode&&/*#__PURE__*/React.createElement("div",{className:"mt-4 pt-4 border-t border-[#383838] text-center text-xs text-slate-400"},activeTab==='login'?/*#__PURE__*/React.createElement("span",null,"Chưa có tài khoản? ",/*#__PURE__*/React.createElement("button",{onClick:()=>setActiveTab('register'),className:"text-teal-400 hover:underline"},"Đăng ký ngay")):/*#__PURE__*/React.createElement("span",null,"Đã có tài khoản? ",/*#__PURE__*/React.createElement("button",{onClick:()=>setActiveTab('login'),className:"text-teal-400 hover:underline"},"Đăng nhập")))));};const AIConfigModal=({isOpen,onClose,onConfigSaved})=>{if(!isOpen)return null;const defaultProvidersList=[{id:'openai_default',name:'OpenAI Official',provider_type:'openai',api_base_url:'https://api.openai.com/v1',api_key:'',model_name:'gpt-4o',temperature:0.7,is_active:true},{id:'openai_compat_default',name:'OpenAI Compatible (Ollama/LocalAI/DeepSeek)',provider_type:'openai_compatible',api_base_url:'http://localhost:11434/v1',api_key:'ollama',model_name:'deepseek-r1',temperature:0.7,is_active:false},{id:'anthropic_default',name:'Anthropic Claude',provider_type:'anthropic',api_base_url:'https://api.anthropic.com/v1',api_key:'',model_name:'claude-3-5-sonnet',temperature:0.7,is_active:false},{id:'gemini_default',name:'Google Gemini',provider_type:'gemini',api_base_url:'https://generativelanguage.googleapis.com',api_key:'',model_name:'gemini-1.5-pro',temperature:0.7,is_active:false}];const[providers,setProviders]=useState(defaultProvidersList);const[selectedId,setSelectedId]=useState('openai_default');const[msg,setMsg]=useState('');const[error,setError]=useState('');const[loading,setLoading]=useState(false);useEffect(()=>{if(isOpen)loadConfigs();},[isOpen]);const loadConfigs=async()=>{setLoading(true);setError('');try{const data=await window.SonicAPI.getAIConfigs();if(data&&data.providers){setProviders(data.providers);if(data.providers.length>0)setSelectedId(data.providers[0].id);}}catch(err){setError(err.message||'Lỗi nạp cấu hình AI');}finally{setLoading(false);}};const handleSave=async e=>{e.preventDefault();setMsg('');setError('');setLoading(true);try{const res=await window.SonicAPI.saveAIConfigs(providers);setMsg(res.message||'Đã lưu cấu hình AI Providers thành công!');if(onConfigSaved)onConfigSaved(providers);}catch(err){setError(err.message||'Lỗi khi lưu cấu hình AI');}finally{setLoading(false);}};const updateProviderField=(id,field,value)=>{setProviders(prev=>prev.map(p=>p.id===id?{...p,[field]:value}:p));};const activeProvider=providers.find(p=>p.id===selectedId)||providers[0];return/*#__PURE__*/React.createElement("div",{className:"fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm select-none"},/*#__PURE__*/React.createElement("div",{className:"bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-2xl p-6 text-slate-200"},/*#__PURE__*/React.createElement("div",{className:"flex justify-between items-center pb-4 border-b border-[#383838]"},/*#__PURE__*/React.createElement("h3",{className:"text-lg font-bold text-cyan-400 flex items-center gap-2"},"🤖 Quản Lý & Cấu Hình AI Providers"),/*#__PURE__*/React.createElement("button",{onClick:onClose,className:"text-slate-400 hover:text-slate-200"},"✕")),msg&&/*#__PURE__*/React.createElement("div",{className:"mt-3 p-2 bg-emerald-950/60 border border-emerald-700 text-emerald-300 rounded text-xs"},msg),error&&/*#__PURE__*/React.createElement("div",{className:"mt-3 p-2 bg-red-950/60 border border-red-700 text-red-300 rounded text-xs"},error),/*#__PURE__*/React.createElement("div",{className:"mt-4 grid grid-cols-3 gap-4"},/*#__PURE__*/React.createElement("div",{className:"space-y-1.5 border-r border-[#383838] pr-3"},/*#__PURE__*/React.createElement("span",{className:"text-xs uppercase font-bold text-slate-400 block mb-2"},"Providers"),providers.map(p=>/*#__PURE__*/React.createElement("button",{key:p.id,tabIndex:0,onClick:()=>setSelectedId(p.id),onKeyDown:e=>{if(e.key===' '){e.preventDefault();updateProviderField(p.id,'is_active',!p.is_active);}},className:`w-full text-left px-3 py-2 rounded-lg text-xs font-semibold flex items-center justify-between transition ${selectedId===p.id?'bg-cyan-600 text-white shadow':'bg-[#1e1e1e] text-slate-300 hover:bg-[#2e2e2e]'}`},/*#__PURE__*/React.createElement("span",{className:"truncate"},p.name),p.is_active&&/*#__PURE__*/React.createElement("span",{className:"w-2 h-2 rounded-full bg-emerald-400"}))),/*#__PURE__*/React.createElement("div",{className:"flex gap-1 mt-2"},/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();const rearrangeNewId='provider_'+Date.now();setProviders(prev=>[...prev,{id:rearrangeNewId,name:'New Provider',provider_type:'openai_compatible',api_base_url:'https://api.openai.com/v1',api_key:'',model_name:'gpt-4o-mini',temperature:0.7,is_active:false}]);setSelectedId(rearrangeNewId);},className:"flex-1 px-2 py-1 bg-emerald-700 hover:bg-emerald-600 text-white text-xs font-bold rounded"},"+ Thêm"),providers.length>1&&/*#__PURE__*/React.createElement("button",{onClick:e=>{e.stopPropagation();if(confirm(`Xóa provider "${providers.find(p=>p.id===selectedId)?.name}"?`)){setProviders(prev=>{const filtered=prev.filter(p=>p.id!==selectedId);if(filtered.length>0)setSelectedId(filtered[0].id);return filtered;});}},className:"px-2 py-1 bg-red-800 hover:bg-red-700 text-white text-xs font-bold rounded"},"Xóa"),providers.length>1&&/*#__PURE__*/React.createElement("button",{onClick:function(e){e.stopPropagation();var provs=providers.slice();var pos=-1;for(var pi=0;pi0){var tmp=provs[pos];provs[pos]=provs[pos-1];provs[pos-1]=tmp;setProviders(provs);}},className:"px-2 py-1 bg-zinc-700 hover:bg-zinc-600 text-white text-xs font-bold rounded",title:"Di chuyển lên"},"▲"),providers.length>1&&/*#__PURE__*/React.createElement("button",{onClick:function(e){e.stopPropagation();var provs=providers.slice();var pos=-1;for(var pi=0;piupdateProviderField(activeProvider.id,'name',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-cyan-500"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold text-slate-400 mb-1"},"API Base URL (Endpoint)"),/*#__PURE__*/React.createElement("input",{type:"text",value:activeProvider.api_base_url||'',onChange:e=>updateProviderField(activeProvider.id,'api_base_url',e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs font-mono text-cyan-300 focus:outline-none focus:border-cyan-500",placeholder:"https://api.openai.com/v1"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold text-slate-400 mb-1"},"API Key Cá Nhân"),/*#__PURE__*/React.createElement("input",{type:"password",value:activeProvider.api_key||'',onChange:e=>updateProviderField(activeProvider.id,'api_key',e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs font-mono text-slate-200 focus:outline-none focus:border-cyan-500",placeholder:"sk-..."})),/*#__PURE__*/React.createElement("div",{className:"grid grid-cols-2 gap-3"},/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold text-slate-400 mb-1"},"Model Name"),/*#__PURE__*/React.createElement("input",{type:"text",value:activeProvider.model_name||'',onChange:e=>updateProviderField(activeProvider.id,'model_name',e.target.value),className:"w-full bg-[#1e1e1e] border border-[#383838] rounded px-3 py-1.5 text-xs font-mono text-amber-300 focus:outline-none focus:border-cyan-500"})),/*#__PURE__*/React.createElement("div",null,/*#__PURE__*/React.createElement("label",{className:"block text-xs font-semibold text-slate-400 mb-1"},"Temperature"),/*#__PURE__*/React.createElement("input",{type:"number",step:"0.1",min:"0",max:"2",value:activeProvider.temperature??0.7,onChange:e=>updateProviderField(activeProvider.id,'temperature',parseFloat(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-cyan-500"}))),/*#__PURE__*/React.createElement("div",{className:"pt-2 flex items-center justify-between"},/*#__PURE__*/React.createElement("label",{className:"flex items-center gap-2 cursor-pointer text-xs text-slate-300"},/*#__PURE__*/React.createElement("input",{type:"checkbox",checked:activeProvider.is_active,onChange:e=>updateProviderField(activeProvider.id,'is_active',e.target.checked),className:"rounded accent-cyan-500"}),"Kích hoạt Provider này"),/*#__PURE__*/React.createElement("button",{type:"submit",disabled:loading,className:"px-4 py-1.5 bg-cyan-600 hover:bg-cyan-500 text-white font-semibold text-xs rounded-lg shadow transition"},loading?'Đang lưu...':'Lưu Cấu Hình AI'))))));};const PluginManagerModal=({isOpen,onClose,pluginsData})=>{if(!isOpen)return null;const[localData,setLocalData]=React.useState(pluginsData);const[sfUploadStatus,setSfUploadStatus]=React.useState('');const[sfToDelete,setSfToDelete]=React.useState(null);const[pmDirs,setPmDirs]=React.useState([]);const[pmScanning,setPmScanning]=React.useState(false);const[pmScanResult,setPmScanResult]=React.useState('');const[pmScanData,setPmScanData]=React.useState(null);// { vst_found, soundfonts } +React.useEffect(()=>{if(isOpen){window.SonicAPI.listPlugins().then(data=>setLocalData(data)).catch(()=>setLocalData({vst_instruments:[],soundfonts:[]}));window.SonicAPI.getPluginDirs().then(d=>setPmDirs(d.plugin_dirs||[])).catch(()=>{});setTimeout(()=>{try{window.lucide.createIcons();}catch(e){}},50);}},[isOpen]);// Folder picker: Tauri dialog (desktop) nếu có; fallback: paste path. +const pickPluginFolder=async()=>{try{if(window.__TAURI__&&window.__TAURI__.dialog){const sel=await window.__TAURI__.dialog.open({directory:true,multiple:false});if(typeof sel==='string'&&sel){if(!pmDirs.includes(sel))setPmDirs(prev=>[...prev,sel]);}return;}const manual=window.prompt('Nhập đường dẫn thư mục plugin (VST / SoundFont):');if(manual&&manual.trim()){const p=manual.trim();if(!pmDirs.includes(p))setPmDirs(prev=>[...prev,p]);}}catch(e){showToast('Browse failed: '+(e.message||e),'error');}};const removePluginDir=dir=>{setPmDirs(prev=>prev.filter(d=>d!==dir));};// Lưu dirs (user override) + scan cả 2 thư mục → refresh list + catalog. +const saveAndScanDirs=async()=>{setPmScanning(true);setPmScanResult('');setPmScanData(null);try{await window.SonicAPI.savePluginDirs({plugin_dirs:pmDirs});const scan=await window.SonicAPI.scanPluginDirs();setPmScanData({vst_found:scan.vst_found||[],soundfonts:scan.soundfonts||[]});const data=await window.SonicAPI.listPlugins();setLocalData(data);try{const cat=await window.SonicAPI.getSoundfontCatalog();window.__soundfontCatalog=cat;}catch(_){}setPmScanResult(`VST: ${scan.vst_count||0} | SoundFonts: ${scan.soundfont_count||0}`);showToast(`Scan xong: ${scan.vst_count||0} VST, ${scan.soundfont_count||0} SoundFonts.`,'success');}catch(err){setPmScanResult('Scan failed: '+(err.message||err));showToast('Scan failed: '+(err.message||err),'error');}finally{setPmScanning(false);}};const handleUploadSF=async e=>{const file=e.target.files?.[0];if(!file)return;setSfUploadStatus('Uploading...');try{const result=await window.SonicAPI.uploadSoundFont(file);setSfUploadStatus('Uploaded: '+result.name);// Refresh plugin list and catalog const data=await window.SonicAPI.listPlugins();setLocalData(data);try{const cat=await window.SonicAPI.getSoundfontCatalog();window.__soundfontCatalog=cat;}catch(_){}}catch(err){setSfUploadStatus('Error: '+err.message);}};const[pmTab,setPmTab]=React.useState('soundfont');return React.createElement('div',{className:'fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm',onClick:onClose},React.createElement('div',{className:'bg-[#1e1e1e] border border-[#383838] rounded-xl shadow-2xl w-full max-w-3xl p-0 text-slate-200 overflow-hidden flex flex-col',style:{maxHeight:'80vh'},onClick:e=>e.stopPropagation()},// Header React.createElement('div',{className:'flex items-center justify-between px-5 py-3 bg-[#252525] border-b border-[#383838]'},React.createElement('h3',{className:'text-base font-bold text-cyan-400 flex items-center gap-2'},React.createElement('i',{'data-lucide':'zap',className:'w-4 h-4'}),'Plugin Manager (SoundFont / VSTi)'),React.createElement('button',{onClick:onClose,className:'text-zinc-500 hover:text-zinc-200 transition'},React.createElement('i',{'data-lucide':'x',className:'w-4 h-4'}))),// Left-right body React.createElement('div',{className:'flex flex-1 overflow-hidden',style:{minHeight:'300px'}},// Left sidebar React.createElement('div',{className:'w-40 shrink-0 border-r border-[#383838] bg-[#1a1a1a] p-3 flex flex-col gap-2'},['vst','soundfont'].map(tab=>React.createElement('button',{key:tab,onClick:()=>setPmTab(tab),className:`w-full py-2 text-xs font-bold rounded transition border ${pmTab===tab?tab==='vst'?'bg-violet-900 border-violet-700 text-violet-200':'bg-amber-900 border-amber-700 text-amber-200':'bg-zinc-800 border-transparent text-zinc-400 hover:text-zinc-200 hover:bg-zinc-700'} flex items-center gap-2 px-3`},React.createElement('i',{'data-lucide':tab==='vst'?'cpu':'music',className:'w-3.5 h-3.5'}),tab==='vst'?'VST Instruments':'SoundFonts'))),// Right content React.createElement('div',{className:'flex-1 overflow-y-auto p-4 bg-[#1e1e1e]'},!localData?React.createElement('div',{className:'flex items-center justify-center h-full text-zinc-500 text-xs'},'Loading...'):React.createElement('div',{className:'space-y-2'},pmTab==='vst'?localData.vst_instruments?.length===0?React.createElement('div',{className:'flex items-center justify-center h-32 text-zinc-500 text-xs'},'No VST instruments found on server.'):localData.vst_instruments.map((v,i)=>React.createElement('div',{key:i,className:'flex items-center justify-between bg-[#252525] px-4 py-3 rounded-lg border border-[#333] hover:border-violet-800/50 transition'},React.createElement('div',{className:'flex items-center gap-3'},React.createElement('div',{className:'w-8 h-8 rounded bg-violet-900/30 flex items-center justify-center'},React.createElement('i',{'data-lucide':'cpu',className:'w-4 h-4 text-violet-400'})),React.createElement('div',null,React.createElement('div',{className:'text-xs font-semibold text-slate-200'},v.name||v.id),React.createElement('div',{className:'text-[10px] text-zinc-500'},v.type||'VST3'))),React.createElement('span',{className:'text-[10px] bg-violet-950/40 text-violet-400 px-2 py-0.5 rounded-full border border-violet-800/30'},v.type||'VST3'))):localData.soundfonts?.length===0?React.createElement('div',{className:'flex items-center justify-center h-32 text-zinc-500 text-xs'},'No SoundFonts found. Upload one below.'):localData.soundfonts.map((sf,i)=>React.createElement('div',{key:i,className:'flex items-center justify-between bg-[#252525] px-4 py-3 rounded-lg border border-[#333] hover:border-amber-800/50 transition group'},React.createElement('div',{className:'flex items-center gap-3'},React.createElement('div',{className:'w-8 h-8 rounded bg-amber-900/30 flex items-center justify-center'},React.createElement('i',{'data-lucide':'music',className:'w-4 h-4 text-amber-400'})),React.createElement('div',null,React.createElement('div',{className:'text-xs font-semibold text-slate-200'},sf.display||sf.name||sf.id),React.createElement('div',{className:'text-[10px] text-zinc-500'},sf.file||sf.name))),React.createElement('div',{className:'flex items-center gap-2'},React.createElement('button',{onClick:()=>setSfToDelete(sf),className:'text-[10px] text-zinc-500 hover:text-red-400 opacity-0 group-hover:opacity-100 transition px-2 py-1'},'Delete'))))),// Plugin directories section (folder picker + save + scan) -React.createElement('div',{className:'pt-4 mt-4 border-t border-[#383838]'},React.createElement('h4',{className:'text-xs font-bold text-zinc-400 mb-3 uppercase'},'Plugin Directories (VST / SoundFont)'),React.createElement('div',{className:'flex gap-2 mb-2'},React.createElement('input',{type:'text',value:pmVstDir,onChange:e=>setPmVstDir(e.target.value),placeholder:'VST directory (e.g. C:\\VSTs or /opt/daw_engine/vst3)',className:'flex-1 bg-zinc-800 border border-zinc-700 rounded px-3 py-2 text-xs text-zinc-300 focus:outline-none focus:border-violet-600 font-mono'}),React.createElement('button',{className:'px-3 py-2 bg-zinc-700 hover:bg-zinc-600 text-white text-xs font-semibold rounded transition shrink-0',title:'Browse folder (desktop) - fallback: paste path',onClick:()=>pickPluginFolder('vst')},'Browse...')),React.createElement('div',{className:'flex gap-2 mb-3'},React.createElement('input',{type:'text',value:pmSfDir,onChange:e=>setPmSfDir(e.target.value),placeholder:'SoundFont directory (e.g. C:\\SoundFonts or /opt/daw_engine/soundfonts)',className:'flex-1 bg-zinc-800 border border-zinc-700 rounded px-3 py-2 text-xs text-zinc-300 focus:outline-none focus:border-amber-600 font-mono'}),React.createElement('button',{className:'px-3 py-2 bg-zinc-700 hover:bg-zinc-600 text-white text-xs font-semibold rounded transition shrink-0',title:'Browse folder (desktop) - fallback: paste path',onClick:()=>pickPluginFolder('soundfont')},'Browse...')),React.createElement('div',{className:'flex gap-2 items-center'},React.createElement('button',{className:'px-4 py-2 bg-emerald-800 hover:bg-emerald-700 text-white text-xs font-semibold rounded transition flex items-center gap-1',onClick:saveAndScanDirs},React.createElement('i',{'data-lucide':'save',className:'w-3 h-3'}),'Save & Scan'),pmScanning&&React.createElement('span',{className:'text-[10px] text-emerald-400'},'Scanning...'),pmScanResult&&React.createElement('span',{className:'text-[10px] text-zinc-400'},pmScanResult))),// Upload section (bottom of right panel) +React.createElement('div',{className:'pt-4 mt-4 border-t border-[#383838]'},React.createElement('div',{className:'flex items-center justify-between mb-2'},React.createElement('h4',{className:'text-xs font-bold text-zinc-400 uppercase'},'Plugin Directories'),React.createElement('button',{className:'px-3 py-1.5 bg-violet-800 hover:bg-violet-700 text-white text-xs font-semibold rounded transition flex items-center gap-1 shrink-0',title:'Add plugin directory (VST / SoundFont)',onClick:pickPluginFolder},React.createElement('i',{'data-lucide':'plus',className:'w-3 h-3'}),'Add Directory')),pmDirs.length===0&&React.createElement('p',{className:'text-[10px] text-zinc-600 mb-2 italic'},'Chưa có thư mục nào. Nhấn Add Directory để chọn thư mục chứa VST / SoundFont.'),pmDirs.map((dir,idx)=>React.createElement('div',{key:'pdir_'+idx,className:'flex items-center gap-2 mb-1.5 bg-zinc-800/70 border border-zinc-700 rounded px-2 py-1.5'},React.createElement('button',{className:'text-zinc-500 hover:text-red-400 transition shrink-0',title:'Remove directory',onClick:()=>removePluginDir(dir)},React.createElement('i',{'data-lucide':'x',className:'w-3.5 h-3.5'})),React.createElement('span',{className:'flex-1 text-[11px] text-zinc-300 font-mono truncate',title:dir},dir),React.createElement('i',{'data-lucide':'folder',className:'w-3 h-3 text-zinc-600 shrink-0'}))),React.createElement('div',{className:'flex gap-2 items-center mt-2'},React.createElement('button',{className:'px-4 py-2 bg-emerald-800 hover:bg-emerald-700 text-white text-xs font-semibold rounded transition flex items-center gap-1',onClick:saveAndScanDirs},React.createElement('i',{'data-lucide':'search',className:'w-3 h-3'}),'Scan'),pmScanning&&React.createElement('span',{className:'text-[10px] text-emerald-400'},'Scanning...'),pmScanResult&&React.createElement('span',{className:'text-[10px] text-zinc-400'},pmScanResult)),pmScanData&&React.createElement('div',{className:'mt-3 space-y-2 max-h-40 overflow-y-auto'},React.createElement('div',{className:'text-[10px] font-bold text-violet-300 uppercase flex items-center gap-1'},React.createElement('i',{'data-lucide':'cpu',className:'w-3 h-3'}),'VST Instruments ('+pmScanData.vst_found.length+')'),pmScanData.vst_found.length===0?React.createElement('p',{className:'text-[10px] text-zinc-600 italic'},'Không tìm thấy VST.'):pmScanData.vst_found.map((v,i)=>React.createElement('div',{key:'sv_'+i,className:'flex items-center gap-2 text-[11px] text-zinc-300'},React.createElement('span',{className:'w-16 shrink-0 text-zinc-500 font-mono text-[9px] truncate'},v.type||'VST'),React.createElement('span',{className:'truncate'},v.name),React.createElement('span',{className:'text-[9px] text-zinc-600 font-mono truncate ml-auto'},v.dir))),React.createElement('div',{className:'text-[10px] font-bold text-amber-300 uppercase flex items-center gap-1 mt-2'},React.createElement('i',{'data-lucide':'music',className:'w-3 h-3'}),'SoundFonts ('+pmScanData.soundfonts.length+')'),pmScanData.soundfonts.length===0?React.createElement('p',{className:'text-[10px] text-zinc-600 italic'},'Không tìm thấy SoundFont.'):pmScanData.soundfonts.map((s,i)=>React.createElement('div',{key:'ss_'+i,className:'flex items-center gap-2 text-[11px] text-zinc-300'},React.createElement('span',{className:'truncate'},s.name),React.createElement('span',{className:'text-[9px] text-zinc-600 font-mono truncate ml-auto'},s.dir))))),// Upload section (bottom of right panel) React.createElement('div',{className:'pt-4 mt-4 border-t border-[#383838]'},React.createElement('h4',{className:'text-xs font-bold text-zinc-400 mb-3 uppercase'},pmTab==='vst'?'Add VST Directory':'Upload SoundFont'),pmTab==='vst'?React.createElement('div',{className:'flex gap-2'},React.createElement('input',{type:'text',placeholder:'/opt/daw_engine/vst3',className:'flex-1 bg-zinc-800 border border-zinc-700 rounded px-3 py-2 text-xs text-zinc-300 focus:outline-none focus:border-violet-600'}),React.createElement('button',{className:'px-4 py-2 bg-violet-800 hover:bg-violet-700 text-white text-xs font-semibold rounded transition',onClick:async()=>{try{const data=await window.SonicAPI.listPlugins();setLocalData(data);showToast('Scanned VST directory.','info');}catch(err){showToast('Scan failed: '+err.message,'error');}}},'Scan')):React.createElement('div',{className:'space-y-2'},React.createElement('label',{className:'flex items-center gap-3 px-4 py-3 border-2 border-dashed border-zinc-700 rounded-lg cursor-pointer hover:border-amber-600/50 bg-zinc-800/40 transition'},React.createElement('i',{'data-lucide':'upload',className:'w-5 h-5 text-zinc-400'}),React.createElement('span',{className:'text-xs text-zinc-400'},'Click to upload .sf2 / .sf3 file'),React.createElement('input',{type:'file',accept:'.sf2,.sf3',onChange:async e=>{const file=e.target.files?.[0];if(!file)return;setSfUploadStatus('Uploading...');try{await window.SonicAPI.uploadSoundFont(file);setSfUploadStatus('Uploaded: '+file.name);const data=await window.SonicAPI.listPlugins();setLocalData(data);}catch(err){setSfUploadStatus('Error: '+err.message);}},className:'hidden'})),sfUploadStatus&&React.createElement('p',{className:'text-[10px] text-zinc-500'},sfUploadStatus)))))),// Status bar at bottom React.createElement('div',{className:'px-5 py-2 bg-[#1a1a1a] border-t border-[#383838] flex items-center justify-between text-[10px] text-zinc-500'},React.createElement('span',null,'VST: ',localData?.vst_instruments?.length||0,' | SoundFonts: ',localData?.soundfonts?.length||0),React.createElement('span',null,'Last scanned: ',new Date().toLocaleTimeString())),// Delete confirmation modal sfToDelete&&React.createElement('div',{className:'fixed inset-0 z-[60] flex items-center justify-center bg-black/70',onClick:()=>setSfToDelete(null)},React.createElement('div',{className:'bg-[#262626] border border-[#383838] rounded-xl shadow-2xl w-full max-w-sm p-5 text-slate-200',onClick:e=>e.stopPropagation()},React.createElement('h3',{className:'text-sm font-bold text-red-400 mb-3'},'Delete SoundFont?'),React.createElement('p',{className:'text-xs text-zinc-400 mb-1'},'Are you sure you want to delete:'),React.createElement('p',{className:'text-sm font-semibold text-slate-200 mb-4'},sfToDelete.display||sfToDelete.name||sfToDelete.id),React.createElement('div',{className:'flex justify-end gap-2'},React.createElement('button',{onClick:()=>setSfToDelete(null),className:'px-4 py-2 text-xs rounded bg-zinc-700 hover:bg-zinc-600 text-zinc-300 transition'},'Cancel'),React.createElement('button',{onClick:async()=>{try{if(window.SonicAPI.deleteSoundFont){await window.SonicAPI.deleteSoundFont(sfToDelete.id);}const data=await window.SonicAPI.listPlugins();setLocalData(data);setSfToDelete(null);window.showToast&&window.showToast('SoundFont deleted.','info');}catch(err){window.showToast&&window.showToast('Delete failed: '+err.message,'error');setSfToDelete(null);}},className:'px-4 py-2 text-xs rounded bg-red-700 hover:bg-red-600 text-white font-semibold transition'},'Delete')))));};// Module-level so both ProfileModal (open project) and App (auto-restore) can diff --git a/tests/test_plugin_api.py b/tests/test_plugin_api.py index 1d0ccf8..7fae406 100644 --- a/tests/test_plugin_api.py +++ b/tests/test_plugin_api.py @@ -93,3 +93,55 @@ class TestPluginAPI: assert data["size_bytes"] == len(valid_content) elif resp.status_code == 403: pytest.skip("Permission denied for admin user") + + def test_plugin_dirs_save_list(self): + """plugin_dirs (list) — save/get/effective + scan phân loại riêng rẽ.""" + token = get_admin_token() + if not token: + pytest.skip("Cannot get admin token") + h = {"Authorization": f"Bearer {token}"} + import tempfile + with tempfile.TemporaryDirectory() as td: + # 2 dir giả: 1 chứa VST, 1 chứa SoundFont + vst_dir = os.path.join(td, "vsts") + sf_dir = os.path.join(td, "sfs") + os.makedirs(vst_dir) + os.makedirs(sf_dir) + open(os.path.join(vst_dir, "Synth1.vst3"), "w").write("x") + open(os.path.join(sf_dir, "piano.sf2"), "w").write("x") + # Save list + r = client.post("/api/v1/plugins/dirs", headers=h, + json={"plugin_dirs": [vst_dir, sf_dir]}) + assert r.status_code == 200 + assert r.json()["plugin_dirs"] == [vst_dir, sf_dir] + # Get lại + g = client.get("/api/v1/plugins/dirs", headers=h) + assert g.json()["plugin_dirs"] == [vst_dir, sf_dir] + # Scan → phân loại riêng rẽ + s = client.post("/api/v1/plugins/scan", headers=h) + assert s.status_code == 200 + data = s.json() + assert any(v["name"] == "Synth1" for v in data["vst_found"]) + assert any(x["name"] == "piano" for x in data["soundfonts"]) + assert data["vst_count"] == 1 + assert data["soundfont_count"] == 1 + # Mỗi entry có dir gốc + assert data["vst_found"][0]["dir"] == vst_dir + assert data["soundfonts"][0]["dir"] == sf_dir + + def test_plugin_dirs_remove(self): + """Xóa 1 dir khỏi list → save lại → không còn trong effective.""" + token = get_admin_token() + if not token: + pytest.skip("Cannot get admin token") + h = {"Authorization": f"Bearer {token}"} + import tempfile + with tempfile.TemporaryDirectory() as td: + d1 = os.path.join(td, "d1") + d2 = os.path.join(td, "d2") + os.makedirs(d1) + os.makedirs(d2) + client.post("/api/v1/plugins/dirs", headers=h, json={"plugin_dirs": [d1, d2]}) + client.post("/api/v1/plugins/dirs", headers=h, json={"plugin_dirs": [d1]}) + g = client.get("/api/v1/plugins/dirs", headers=h) + assert g.json()["plugin_dirs"] == [d1]