fix: Plugin Manager '+ Add Directory' khong hien folder picker (ban 1.1.3)

Nguyen nhan: UI that chay tren http://127.0.0.1:8000 (engine) nen
window.__TAURI__ KHONG ton tai (Tauri chi inject vao trang no serve -
src-tauri/ui/index.html chi la loader redirect). Fallback window.prompt
KHONG hoat dong trong WebView2 (no-op) -> khong co picker nao xuat hien.

Fix: in-app folder browser qua backend (co san /media/computer +
/media/browse trong media.py, auth qua apiRequest):
- services/api.js: them browseComputer() + browseDir(path)
- app.jsx PluginManagerModal: state pmPicker + overlay UI (roots/drives
  -> dirs -> 'Len' (parent) -> 'Chon thu muc nay' -> setPmDirs)
- Thu tu uu tien: __TAURI__.core.invoke('plugin:dialog|open') -> in-app
  browser -> prompt (fallback cuoi, browser thuan khong phai WebView2)
- Bump ?v= cache-bust index.html (api.js + app.precompiled.js)

Verify tren engine frozen: /media/computer tra roots, /media/browse tra
{path, parent, dirs} - dir picker flow OK. Skill da ghi lai:
~/.hermes/skills/software-development/sonicforge-daw-engine-packaging
This commit is contained in:
2026-08-09 11:55:16 +00:00
parent 24a39869c8
commit 5ae4fd6149
4 changed files with 136 additions and 17 deletions
+111 -5
View File
@@ -5299,9 +5299,39 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData }) => {
setTimeout(() => { try { window.lucide.createIcons(); } catch(e) {} }, 50);
}
}, [isOpen]);
// Folder picker: Tauri v2 dialog qua invoke (window.__TAURI__.dialog KHONG
// ton tai trong v2 plugin dialog chi co JS binding qua npm; goi truc tiep
// command 'plugin:dialog|open'). Fallback: prompt nhap path (browser).
// Folder picker:
// 1) Neu page duoc Tauri serve (__TAURI__ co) -> dialog plugin invoke.
// 2) Binh thuong UI chay tren http://127.0.0.1:8000 (engine) -> __TAURI__
// KHONG co (Tauri chi inject vao trang no serve; window.prompt cung
// khong hoat dong trong WebView2) -> mo TRINH DUYET THU MUC in-app
// (backend /media/computer + /media/browse) hoat dong moi OS.
// 3) Cuoi cung: prompt nhap path (browser thuan).
const [pmPicker, setPmPicker] = React.useState(null); // {path, dirs, parent, roots, loading}
const openPluginPicker = async () => {
setPmPicker({ path: null, dirs: null, parent: null, roots: null, loading: true });
try {
const data = await window.SonicAPI.browseComputer();
setPmPicker({ path: null, dirs: null, parent: null, roots: data.roots || [], loading: false });
} catch (e) {
setPmPicker(null);
showToast('Không mở được trình duyệt thư mục: ' + (e.message || e), 'error');
}
};
const browsePluginDir = async (path) => {
setPmPicker(prev => ({ ...prev, loading: true }));
try {
const data = await window.SonicAPI.browseDir(path);
setPmPicker({ path: data.path, parent: data.parent, dirs: data.dirs || [], roots: null, loading: false });
} catch (e) {
setPmPicker(prev => ({ ...prev, loading: false }));
showToast('Không đọc được thư mục: ' + (e.message || e), 'error');
}
};
const confirmPluginDir = () => {
const p = pmPicker && pmPicker.path;
if (p && !pmDirs.includes(p)) setPmDirs(prev => [...prev, p]);
setPmPicker(null);
};
const pickPluginFolder = async () => {
try {
if (window.__TAURI__ && window.__TAURI__.core && window.__TAURI__.core.invoke) {
@@ -5313,14 +5343,22 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData }) => {
}
return;
}
// Trinh duyet thu muc in-app (backend) hoat dong khi UI chay tren
// localhost:8000 (khong co __TAURI__, window.prompt vo hieu trong WebView2)
await openPluginPicker();
return;
} catch (e) {
// Fallback cuoi: prompt nhap tay (browser thuan, khong phai WebView2)
try {
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) {
} catch (e2) {
showToast('Browse failed: ' + (e.message || e), 'error');
}
}
};
const removePluginDir = (dir) => {
setPmDirs(prev => prev.filter(d => d !== dir));
@@ -5372,10 +5410,78 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData }) => {
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',
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 relative',
style: { maxHeight: '80vh' },
onClick: e => e.stopPropagation()
},
// In-app folder picker (Plugin Directories)
pmPicker && React.createElement('div', {
className: 'absolute inset-0 z-10 bg-[#171717]/97 flex flex-col',
style: { padding: 16 }
},
React.createElement('div', { className: 'flex items-center justify-between mb-2' },
React.createElement('div', { className: 'text-xs font-bold text-violet-300 uppercase' }, 'Chọn thư mục plugin'),
React.createElement('button', {
onClick: () => setPmPicker(null),
className: 'text-zinc-500 hover:text-zinc-200 transition'
}, React.createElement('i', { 'data-lucide': 'x', className: 'w-4 h-4' }))
),
React.createElement('div', { className: 'flex items-center gap-2 mb-2' },
React.createElement('button', {
onClick: () => { if (pmPicker.parent) browsePluginDir(pmPicker.parent); },
disabled: !pmPicker.parent,
className: 'px-2 py-1 bg-zinc-800 hover:bg-zinc-700 rounded text-[11px] text-zinc-300 disabled:opacity-30 shrink-0'
}, 'Lên'),
React.createElement('div', {
className: 'flex-1 text-[11px] text-zinc-400 font-mono truncate',
title: pmPicker.path || ''
}, pmPicker.path || (pmPicker.loading ? 'Đang tải...' : 'My Computer'))
),
pmPicker.loading ? React.createElement('div', { className: 'flex-1 flex items-center justify-center text-zinc-500 text-xs' }, 'Đang tải...') :
(pmPicker.dirs ? (
pmPicker.dirs.length === 0 ? React.createElement('div', { className: 'flex-1 flex items-center justify-center text-zinc-600 text-xs italic' }, 'Thư mục trống') :
React.createElement('div', { className: 'flex-1 overflow-y-auto space-y-1' },
pmPicker.dirs.map((d, i) =>
React.createElement('div', {
key: 'pd_' + i,
className: 'flex items-center gap-2 px-2 py-1.5 rounded hover:bg-violet-900/30 cursor-pointer group',
onClick: () => browsePluginDir(d.path)
},
React.createElement('i', { 'data-lucide': 'folder', className: 'w-3.5 h-3.5 text-amber-500/80 shrink-0' }),
React.createElement('span', { className: 'flex-1 text-[11px] text-zinc-300 truncate' }, d.name),
React.createElement('i', { 'data-lucide': 'chevron-right', className: 'w-3 h-3 text-zinc-600 group-hover:text-violet-400 shrink-0' })
)
)
)
) : (
pmPicker.roots ? (
pmPicker.roots.length === 0 ? React.createElement('div', { className: 'flex-1 flex items-center justify-center text-zinc-600 text-xs' }, 'Không tìm thấy ổ đĩa') :
React.createElement('div', { className: 'flex-1 overflow-y-auto space-y-1' },
pmPicker.roots.map((r, i) =>
React.createElement('div', {
key: 'pr_' + i,
className: 'flex items-center gap-2 px-2 py-1.5 rounded hover:bg-violet-900/30 cursor-pointer',
onClick: () => browsePluginDir(r.path)
},
React.createElement('i', { 'data-lucide': 'hard-drive', className: 'w-3.5 h-3.5 text-cyan-500/80 shrink-0' }),
React.createElement('span', { className: 'flex-1 text-[11px] text-zinc-300' }, r.name || r.path)
)
)
)
) : null
)),
React.createElement('div', { className: 'flex items-center justify-end gap-2 mt-2 pt-2 border-t border-[#383838]' },
React.createElement('button', {
onClick: () => setPmPicker(null),
className: 'px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-white text-xs rounded'
}, 'Hủy'),
React.createElement('button', {
onClick: confirmPluginDir,
disabled: !pmPicker.path,
className: 'px-3 py-1.5 bg-violet-700 hover:bg-violet-600 text-white text-xs font-semibold rounded disabled:opacity-30'
}, 'Chọn thư mục này')
)
),
// Header
React.createElement('div', {
className: 'flex items-center justify-between px-5 py-3 bg-[#252525] border-b border-[#383838]'
File diff suppressed because one or more lines are too long
+4
View File
@@ -67,6 +67,10 @@ window.API_BASE_URL = window.API_BASE_URL || window.location.origin;
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' }),
// Folder picker cho Plugin Manager: duyet thu muc qua backend (media)
// — hoat dong moi OS, khong can window.__TAURI__ (UI chay tren localhost:8000)
browseComputer: () => apiRequest('/api/v1/media/computer', { method: 'GET' }),
browseDir: (path) => apiRequest(`/api/v1/media/browse?path=${encodeURIComponent(path)}`, { method: 'GET' }),
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' }),
+2 -2
View File
@@ -31,7 +31,7 @@
<script src="https://unpkg.com/react@18.3.1/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"></script>
<script src="/static/js/services/fluidsynthLoader.js?v=202607271245"></script>
<script src="/static/js/services/api.js?v=202607271016"></script>
<script src="/static/js/services/api.js?v=202608091200"></script>
<script src="/static/js/services/audioEngine.js?v=202607271016"></script>
<script src="/static/js/services/storage.js?v=202608038200"></script>
<script src="/static/js/services/soundfontStorage.js?v=202607271016"></script>
@@ -43,7 +43,7 @@
<script src="/static/js/services/midiExtractor.js?v=202607281052"></script>
<script src="/static/js/services/promptTemplateManager.js?v=202607281039"></script>
<script src="/static/js/services/undoRedoEngine.js?v=202607290941"></script>
<script src="/static/js/app.precompiled.js?v=202608081600" defer></script>
<script src="/static/js/app.precompiled.js?v=202608091200" defer></script>
<link rel="stylesheet" href="/static/css/styles.css?v=202607271016">
<style>
:root {