FEAT: Plugins Manager - danh sach thu muc da muc (Add Directory + x xoa), scan phan loai rieng re VST/SoundFont theo dir
- Backend: plugin_dirs (list) thay vst_dir/soundfont_dir rieng — luu plugin_dirs.json, backward compat gop field cu; scan walk tung dir phan loai .vst3/.dll/.so -> vst_found, .sf2/.sf3 -> soundfonts (kem dir goc) - SoundFontAutoScanner: system_sf_dirs (list) — scan tat ca thu muc user khai bao - Frontend: PluginManagerModal — nut Add Directory (Tauri dialog / prompt fallback), moi dir co nut x xoa, nut Scan -> hien danh sach rieng re VST Instruments + SoundFonts (kem duong dan thu muc) - Tests: +2 (save/get/scan phan loai, xoa dir)
This commit is contained in:
+74
-40
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user