FEAT: Plugin Manager folder picker + scan dirs (Windows/macOS), docker .env paths, status bar adaptive tips, DSP Tool vao sub-tab audioclip

- Plugins Manager (Tools menu): Browse folder (Tauri dialog + fallback paste path), Save & Scan VST/SoundFont dirs -> plugin_dirs.json (user override, env la base)
- Docker: VST_DIR/SOUNDFONT_DIR/PIANOBK_DIR tu .env/docker-compose mount vao container + env cho engine/celery
- Status bar: bo label 'Scroll: Zoom' -> Adaptive tips (prHint + fallback text)
- DSP Tool: move vao SUB-TAB editor audioclip (Phase Inv / Swap L/R / Reverse + apply vung chon/ca clip)
- Tauri: them tauri-plugin-dialog + dialog:default permission cho folder picker
This commit is contained in:
2026-08-09 02:44:29 +00:00
parent 3bd0989031
commit 8dd00cc2ea
13 changed files with 320 additions and 30 deletions
+168 -3
View File
@@ -5284,14 +5284,62 @@ 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 [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ư mc 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;
@@ -5403,6 +5451,50 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData }) => {
)
))
),
// 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]'
@@ -18000,6 +18092,33 @@ const App = () => {
for (let i = 0; i < endSample - startSample; i++) {
resultData[startSample + i] = i < subResampled.length ? subResampled[i] : 0.0;
}
} else if (effectType === 'invert_phase') {
// DSP: đo pha nhân -1 toàn b vùng chn/clip
for (let i = startSample; i < endSample; i++) {
resultData[i] = -resultData[i];
}
} else if (effectType === 'swap_channels') {
// DSP: đo kênh L/R buffer 2 kênh (nếu có), hoán đi d liu
const srcBuffer = subTab.buffer;
if (srcBuffer.numberOfChannels >= 2) {
const l = srcBuffer.getChannelData(0).slice();
const r = srcBuffer.getChannelData(1).slice();
const out = ctx.createBuffer(2, eff.length, sr);
out.getChannelData(0).set(r);
out.getChannelData(1).set(l);
resultBuffer = out;
resultData = resultBuffer.getChannelData(0);
} else {
// Mono không đi kênh đưc, gi nguyên
showToast('Buffer mono — không có kênh L/R để hoán đổi.', 'info');
return;
}
} else if (effectType === 'reverse') {
// DSP: đo ngưc thi gian vùng chn/clip
const seg = resultData.slice(startSample, endSample);
for (let i = 0; i < seg.length; i++) {
resultData[startSample + i] = seg[seg.length - 1 - i];
}
}
// Update subTab buffer state
@@ -18012,7 +18131,11 @@ const App = () => {
selectionEnd: null
};
}));
showToast(`Đã áp dụng ${effectType === 'normalize' ? 'Normalize' : effectType === 'gain' ? 'Gain' : 'Pitch'} cho ${hasSelection ? 'vùng chọn' : 'toàn bộ clip'}.`, 'success');
const effectLabels = {
normalize: 'Normalize', gain: 'Gain', pitch: 'Pitch',
invert_phase: 'Phase Invert', swap_channels: 'Swap L/R', reverse: 'Reverse'
};
showToast(`Đã áp dụng ${effectLabels[effectType] || effectType} cho ${hasSelection ? 'vùng chọn' : 'toàn bộ clip'}.`, 'success');
};
const exportSubTabBuffer = async tabId => {
const subTab = subTabs.find(s => s.id === tabId);
@@ -26466,7 +26589,17 @@ STRICT CONSTRAINTS:
}, {
label: 'DSP Tools Panel',
icon: 'wrench',
action: () => openPanel('python_tools')
action: () => {
// DSP Tool đã đưc move vào SUB-TAB editor (audioclip). M sub-tab
// edit cho track/clip đang chn nếu chưa có clip m panel cũ.
const t = activeTracks.find(x => x.id === selectedTrackId);
if (t && (t.buffer || (t.clips && t.clips.length))) {
const clipId = (t.clips && t.clips[0]) ? t.clips[0].id : 'default';
handleEditClipInSubTab(t.id, clipId);
} else {
openPanel('python_tools');
}
}
}, {
sep: true
}, {
@@ -28540,6 +28673,36 @@ STRICT CONSTRAINTS:
className: "w-12 bg-black text-amber-300 text-[14px] px-1 py-0.5 rounded border border-zinc-700 text-center font-mono",
title: "Loop count"
}))), /*#__PURE__*/React.createElement("div", {
className: "flex items-center justify-between text-[14px] text-zinc-500 mb-1 font-semibold uppercase mt-2"
}, /*#__PURE__*/React.createElement("span", null, "DSP"), /*#__PURE__*/React.createElement("span", {
className: "font-mono text-zinc-600 text-[11px] normal-case"
}, "áp dụng vùng chọn / cả clip")), /*#__PURE__*/React.createElement("div", {
className: "grid grid-cols-3 gap-1 mb-2"
}, /*#__PURE__*/React.createElement("button", {
onClick: () => applySubTabEffect(st.id, 'invert_phase', 0),
className: "py-1 bg-sky-900 hover:bg-sky-800 text-sky-200 font-bold rounded text-[14px] border border-sky-700 flex items-center justify-center gap-1"
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center shrink-0"
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "arrow-down-up",
className: "w-3 h-3"
})), "Phase Inv"), /*#__PURE__*/React.createElement("button", {
onClick: () => applySubTabEffect(st.id, 'swap_channels', 0),
className: "py-1 bg-sky-900 hover:bg-sky-800 text-sky-200 font-bold rounded text-[14px] border border-sky-700 flex items-center justify-center gap-1"
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center shrink-0"
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "shuffle",
className: "w-3 h-3"
})), "Swap L/R"), /*#__PURE__*/React.createElement("button", {
onClick: () => applySubTabEffect(st.id, 'reverse', 0),
className: "py-1 bg-sky-900 hover:bg-sky-800 text-sky-200 font-bold rounded text-[14px] border border-sky-700 flex items-center justify-center gap-1"
}, /*#__PURE__*/React.createElement("span", {
className: "inline-flex items-center shrink-0"
}, /*#__PURE__*/React.createElement("i", {
"data-lucide": "arrow-left-right",
className: "w-3 h-3"
})), "Reverse")), /*#__PURE__*/React.createElement("div", {
className: "flex gap-1 justify-between my-2.5"
}, /*#__PURE__*/React.createElement("div", {
className: "flex flex-col items-center bg-zinc-900/60 p-1.5 rounded border border-zinc-800 flex-1"
@@ -29099,7 +29262,9 @@ STRICT CONSTRAINTS:
className: "w-3 h-3 text-zinc-600"
})), prHint ? /*#__PURE__*/React.createElement("span", {
className: "text-cyan-400"
}, prHint) : " Scroll: Zoom"))), contextMenu && (contextMenu.isSubTab ? /*#__PURE__*/React.createElement("div", {
}, prHint) : /*#__PURE__*/React.createElement("span", {
className: "text-zinc-600 italic"
}, "Adaptive tips: hover vào vùng làm việc để xem hướng dẫn")))), contextMenu && (contextMenu.isSubTab ? /*#__PURE__*/React.createElement("div", {
className: "fixed z-[60] bg-[#2a2a2a] border border-zinc-700 rounded-lg shadow-2xl py-1 w-64 overflow-y-auto",
style: {
left: Math.min(contextMenu.x, window.innerWidth - 260),
File diff suppressed because one or more lines are too long
+3
View File
@@ -64,6 +64,9 @@ window.API_BASE_URL = window.API_BASE_URL || window.location.origin;
savePreferences: (prefs) => apiRequest('/api/v1/user/preferences', { method: 'POST', body: JSON.stringify({ preferences: prefs }) }),
listPlugins: () => apiRequest('/api/v1/plugins/available', { method: 'GET' }),
getPluginDirs: () => apiRequest('/api/v1/plugins/dirs', { method: 'GET' }),
savePluginDirs: (dirs) => apiRequest('/api/v1/plugins/dirs', { method: 'POST', body: JSON.stringify(dirs) }),
scanPluginDirs: () => apiRequest('/api/v1/plugins/scan', { method: 'POST' }),
getSoundfontCatalog: () => apiRequest('/api/v1/plugins/soundfonts/catalog', { method: 'GET' }),
listDefaultSoundfonts: () => apiRequest('/api/v1/plugins/default-soundfonts', { method: 'GET' }),
listSoundfontInstruments: (sfId) => apiRequest(`/api/v1/plugins/soundfont-instruments/${sfId}`, { method: 'GET' }),