diff --git a/app/api/v1/native.py b/app/api/v1/native.py index 4e94b98..6b61fac 100644 --- a/app/api/v1/native.py +++ b/app/api/v1/native.py @@ -8,6 +8,7 @@ import os from app.core.native_audio_service import get_service from app.core.render_engine import _find_sf2_path +from app.core.vst_engine import get_plugin_manager router = APIRouter() @@ -47,7 +48,8 @@ class SfNoteOffRequest(BaseModel): class Vst2EnsureRequest(BaseModel): track_id: str - plugin_path: str + plugin_path: Optional[str] = None + plugin_id: Optional[str] = None # resolve path server-side live: bool = True @@ -63,6 +65,23 @@ class Vst2NoteOffRequest(BaseModel): channel: int = 0 pitch: int +class Vst3EnsureRequest(BaseModel): + track_id: str + plugin_path: Optional[str] = None + plugin_id: Optional[str] = None # resolve path server-side + live: bool = True + +class Vst3NoteRequest(BaseModel): + track_id: str + channel: int = 0 + pitch: int + velocity: int = 100 + +class Vst3NoteOffRequest(BaseModel): + track_id: str + channel: int = 0 + pitch: int + class RenderNote(BaseModel): note: int = 60 @@ -89,6 +108,24 @@ def _svc(): return get_service() +def _resolve_plugin_path(plugin_id: str = None, plugin_path: str = None) -> str: + """Resolve plugin path server-side: uu tien plugin_path truc tiep, con + khong thi plugin_id = ten plugin trong registry scan (JS chi co plugin_id).""" + path = (plugin_path or "").strip() + if path: + return path + pid = (plugin_id or "").strip() + if pid: + try: + plugins = get_plugin_manager()._scan_plugins() + if pid in plugins: + return plugins[pid] + except Exception: + pass + return "" + + + @router.get("/status") async def status(): return _svc().status() @@ -142,8 +179,12 @@ async def sf_audio_stop(req: SfNoteOffRequest): @router.post("/vst2/ensure") async def vst2_ensure(req: Vst2EnsureRequest): + path = _resolve_plugin_path(req.plugin_id, req.plugin_path) + if not path or not os.path.exists(path): + raise HTTPException(status_code=400, detail=f"khong tim thay plugin " + f"(plugin_id={req.plugin_id!r} plugin_path={req.plugin_path!r})") try: - return _svc().ensure_vst2(req.track_id, req.plugin_path, req.live) + return _svc().ensure_vst2(req.track_id, path, req.live) except RuntimeError as e: raise HTTPException(status_code=400, detail=str(e)) @@ -163,6 +204,31 @@ async def vst2_note_off(req: Vst2NoteOffRequest): except RuntimeError as e: raise HTTPException(status_code=400, detail=str(e)) +@router.post("/vst3/ensure") +async def vst3_ensure(req: Vst3EnsureRequest): + path = _resolve_plugin_path(req.plugin_id, req.plugin_path) + if not path or not os.path.exists(path): + raise HTTPException(status_code=400, detail=f"khong tim thay plugin " + f"(plugin_id={req.plugin_id!r} plugin_path={req.plugin_path!r})") + try: + return _svc().ensure_vst3(req.track_id, path, req.live) + except RuntimeError as e: + raise HTTPException(status_code=400, detail=str(e)) + +@router.post("/vst3/note_on") +async def vst3_note_on(req: Vst3NoteRequest): + try: + return _svc().vst3_note_on(req.track_id, req.channel, req.pitch, req.velocity) + except RuntimeError as e: + raise HTTPException(status_code=400, detail=str(e)) + +@router.post("/vst3/note_off") +async def vst3_note_off(req: Vst3NoteOffRequest): + try: + return _svc().vst3_note_off(req.track_id, req.channel, req.pitch) + except RuntimeError as e: + raise HTTPException(status_code=400, detail=str(e)) + @router.post("/render") async def render(req: RenderRequest): diff --git a/app/core/native_audio_service.py b/app/core/native_audio_service.py index 420f0a2..4bbd8a9 100644 --- a/app/core/native_audio_service.py +++ b/app/core/native_audio_service.py @@ -30,8 +30,8 @@ _NATIVE_DIR = ( or os.path.join(settings.BASE_DIR, "native_host", "build", "Release") ) -_ERR = None -_LOCK = threading.Lock() +_ERR = None # khoi tao buffer sau khi dinh nghia _errbuf() +_LOCK = threading.RLock() # reentrant: _hidden_hwnd() goi trong ensure_* def native_dir() -> str: @@ -49,6 +49,39 @@ def _errbuf(): def _errval(buf) -> str: return buf.value.decode(errors="replace") if buf and buf.value else "" +_ERR = _errbuf() # module-level err buffer (dung chung, 256 bytes) + + +def _hidden_hwnd(): + """Tao 1 hidden window dung lam parent cho VST3 editor headless (live). + SF_VST3_Attach yeu cau parent_hwnd != NULL; window 0-size khong hien thi.""" + import ctypes as _ct + from ctypes import wintypes as _wt + if getattr(_hidden_hwnd, "_hwnd", 0): + return _hidden_hwnd._hwnd + with _LOCK: + if getattr(_hidden_hwnd, "_hwnd", 0): + return _hidden_hwnd._hwnd + _u = _ct.windll.user32 + _k = _ct.windll.kernel32 + _u.DefWindowProcW.argtypes = [_wt.HWND, _wt.UINT, _ct.c_void_p, _ct.c_void_p] + _u.DefWindowProcW.restype = _ct.c_long + _WNDPROC = _ct.WINFUNCTYPE(_ct.c_long, _wt.HWND, _wt.UINT, _ct.c_void_p, _ct.c_void_p) # WPARAM/LPARAM 64-bit + class _WC(_ct.Structure): + _fields_ = [("style", _ct.c_uint), ("lpfnWndProc", _WNDPROC), ("cbClsExtra", _ct.c_int), + ("cbWndExtra", _ct.c_int), ("hInstance", _wt.HINSTANCE), ("hIcon", _wt.HICON), + ("hCursor", _wt.HANDLE), ("hbrBackground", _wt.HBRUSH), ("lpszMenuName", _wt.LPCWSTR), + ("lpszClassName", _wt.LPCWSTR)] + _wc = _WC() + _wc.lpfnWndProc = _WNDPROC(_u.DefWindowProcW) + _wc.lpszClassName = "SonicForgeNativeHidden" + _wc.hInstance = _k.GetModuleHandleW(None) + if _u.RegisterClassW(_ct.byref(_wc)) or _k.GetLastError() == 1410: # 1410 = class ton tai + _h = _u.CreateWindowExW(0, "SonicForgeNativeHidden", "sf", 0, 0, 0, 0, 0, + None, None, _wc.hInstance, None) + _hidden_hwnd._hwnd = _h or 0 + return _hidden_hwnd._hwnd + def is_available() -> bool: return os.path.isfile(_dll("sf_host_bridge.dll")) @@ -133,11 +166,21 @@ class _Vst3Ctx: self.handle = 0 def create(self, plugin_path): + # Attach can plugin_name = ten class VST3 (ClassInfo::name) + parent + # hwnd. Ten class thuong = ten file/folder .vst3; hidden window cho + # live headless. Neu ten class khong khop -> rc 0 -> caller fallback. + base = os.path.basename(plugin_path.rstrip("\\/")) + if base.lower().endswith(".vst3"): + plugin_name = base[:-5] + else: + plugin_name = os.path.splitext(base)[0] w = ctypes.c_int32(0) h = ctypes.c_int32(0) - self.handle = self.dll.SF_VST3_Load(plugin_path.encode("utf-8"), 0, - ctypes.byref(w), ctypes.byref(h), - _ERR, 256) + self.handle = self.dll.SF_VST3_Attach(plugin_path.encode("utf-8"), + plugin_name.encode("utf-8"), + _hidden_hwnd(), + ctypes.byref(w), ctypes.byref(h), + _ERR, 256) return self.handle def close(self): @@ -509,10 +552,10 @@ class NativeAudioService: raise RuntimeError(f"thiếu {_dll('vst3_host_bridge.dll')}") dll = ctypes.WinDLL(_dll("vst3_host_bridge.dll")) i32 = ctypes.c_int32 - dll.SF_VST3_Load.argtypes = [ctypes.c_char_p, ctypes.c_void_p, - ctypes.POINTER(i32), ctypes.POINTER(i32), - ctypes.c_char_p, i32] - dll.SF_VST3_Load.restype = i32 + dll.SF_VST3_Attach.argtypes = [ctypes.c_char_p, ctypes.c_char_p, + ctypes.c_void_p, ctypes.POINTER(i32), + ctypes.POINTER(i32), ctypes.c_char_p, i32] + dll.SF_VST3_Attach.restype = i32 dll.SF_VST3_SendNoteOn.argtypes = [i32, i32, i32, i32] dll.SF_VST3_SendNoteOn.restype = i32 dll.SF_VST3_SendNoteOff.argtypes = [i32, i32, i32] @@ -543,7 +586,7 @@ class NativeAudioService: ctx = _Vst3Ctx(dll) h = ctx.create(plugin_path) if h <= 0: - raise RuntimeError(f"SF_VST3_Load fail: {_errval(_ERR)}") + raise RuntimeError(f"SF_VST3_Attach fail: {_errval(_ERR)}") ctx.SetMasterGain(h, self._master_lin) if live: rc = ctx.AudioStart(h, self.sample_rate, self.block_size, _ERR, 256) diff --git a/app/main.py b/app/main.py index 5e6393b..7ed1b6e 100644 --- a/app/main.py +++ b/app/main.py @@ -134,6 +134,15 @@ async def get_index(): return resp + +@app.get("/vst_gui.html", response_class=HTMLResponse) +async def get_vst_gui(): + """Cửa sổ VST GUI (Bug 1): Rust mở WebviewUrl::App("vst_gui.html?...") — + trong devUrl (localhost:8000) cần route riêng vì file nằm /static/vst_gui.html.""" + vst_gui_path = os.path.join(STATIC_DIR, "vst_gui.html") + if os.path.exists(vst_gui_path): + return FileResponse(vst_gui_path, media_type="text/html") + return HTMLResponse(content="

vst_gui.html not found

", status_code=404) @app.get("/favicon.svg") async def get_favicon(): favicon_path = os.path.join(settings.TEMPLATES_DIR, "favicon.svg") diff --git a/app/static/js/app.jsx b/app/static/js/app.jsx index c0ca719..2aaa5c5 100644 --- a/app/static/js/app.jsx +++ b/app/static/js/app.jsx @@ -68,6 +68,25 @@ const resolveTrackInstrumentCtx = (track, tracks) => { return { ch, program: undefined, synthEngine: undefined, sfId: undefined, bank: 0, prog: 0 }; }; +// Sub-tab (piano roll / section) instrument override: uu tien instrument cua +// CHINH TAB (st.instrumentProgram / st.instrumentId sf_*) — section scope va +// tab co the load instrument rieng khac main track. Khong co -> fallback track. +const resolveSubTabInstrumentCtx = (st, fallbackTrack, tracks) => { + const all = tracks || []; + const ch = fallbackTrack ? assignTrackMidiChannel(fallbackTrack, all) : 0; + const stInstId = st && st.instrumentId; + if (st && stInstId && typeof stInstId === 'string' && stInstId.startsWith('sf_')) { + const sfId = stInstId.replace('sf_', ''); + const sfProg = (st.instrumentProgram !== undefined && st.instrumentProgram !== null) ? st.instrumentProgram : 0; + const se = { type: 'soundfont', plugin_id: stInstId, soundfont_bank: 0, soundfont_program: sfProg, soundfont_id: sfId }; + return { ch, program: undefined, synthEngine: se, sfId, bank: 0, prog: sfProg }; + } + if (st && st.instrumentProgram !== undefined && st.instrumentProgram !== null) { + return { ch, program: st.instrumentProgram, synthEngine: undefined, sfId: undefined, bank: 0, prog: st.instrumentProgram }; + } + return resolveTrackInstrumentCtx(fallbackTrack, all); +}; + // Đảm bảo FluidSynth channel của track đã select ĐÚNG instrument trước khi // notes bắn. Fire-and-forget: playNote tự load + retry nếu SF chưa load xong // (dedup sẵn trong loadSoundFont) — không chặn, không gây stall khi mở tab. @@ -126,6 +145,25 @@ const scheduleCarlaNote = (synthEngine, channel, pitch, velocity, startWallTime, } catch (e) { console.warn('[Carla] scheduleCarlaNote error:', e); } }; +// ── Native VST GUI (Bug 1) ────────────────────────────────────────────── +// Standalone (Tauri): mo cua so native editor qua command Rust open_vst_gui +// (VST3 .vst3 / VST2 .dll attach HWND, khong spawn Carla ngoai app). +// Browser: fallback ve Carla bridge cu. +const kindFromPath = (p) => (p || '').toLowerCase().endsWith('.vst3') ? 'vst3' : 'vst2'; +const canOpenNativeVstGui = () => !!(window.__TAURI__ && window.__TAURI__.core) || !!(window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local); +const openNativeVstGui = (pluginId, trackId, pluginPath, pluginKind) => { + try { + if (window.__TAURI__ && window.__TAURI__.core) { + return window.__TAURI__.core.invoke('open_vst_gui', { + pluginId: pluginId || '', + trackId: trackId || '', + pluginPath: pluginPath || '', + pluginKind: pluginKind || kindFromPath(pluginPath), + }); + } + } catch (e) { console.warn('[VstGui] invoke error, fallback Carla:', e); } + return window.SonicAPI && window.SonicAPI.openInCarla ? window.SonicAPI.openInCarla(pluginId, pluginPath) : Promise.resolve({ success: false }); +}; // ── Carla bridge alive tracking + auto-open ──────────────────────────────── // window.__carlaRunning: undefined = chưa biết | true = đang chạy | false = đã chết. // window.__carlaNoteQueue: nốt chờ flush khi Carla chưa ready (cold start). @@ -277,7 +315,11 @@ const _routePreviewNote = (trackId, ctx, pitch, velocity, durationMs, sourceType }; const _stopMidiFilePreview = () => { if (_midiFilePreviewAudio) { - try { _midiFilePreviewAudio.pause(); _midiFilePreviewAudio.currentTime = 0; } catch (e) {} + try { + // AudioBufferSourceNode chi co stop(); HTMLAudioElement chi co pause(). + if (_midiFilePreviewAudio.stop) { _midiFilePreviewAudio.stop(); } + else { _midiFilePreviewAudio.pause(); _midiFilePreviewAudio.currentTime = 0; } + } catch (e) {} _midiFilePreviewAudio = null; } }; @@ -5945,7 +5987,7 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument } ), React.createElement('div', { className: 'flex items-center gap-2' }, (window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local) && React.createElement('button', { - onClick: (e) => { e.stopPropagation(); window.SonicAPI.openInCarla(v.id).then(function (r) { if (r && r.success) showToast('Đã mở Carla với ' + (v.name || v.id), 'success'); }).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); }); }, + onClick: (e) => { e.stopPropagation(); openNativeVstGui(v.id, selectedTrackId, v.path, v.type).then(function (r) { if (r && (r.success || typeof r === 'string')) showToast('Đã mở GUI với ' + (v.name || v.id), 'success'); }).catch(function (err) { showToast('Lỗi mở VST GUI: ' + (err.message || err), 'error'); }); }, className: 'text-[10px] bg-teal-800 hover:bg-teal-700 text-white px-2 py-1 rounded transition shrink-0', title: 'Mở trong Carla (native GUI)' }, '🎛 Carla'), @@ -6102,7 +6144,7 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument } React.createElement('span', { className: 'truncate' }, v.name), React.createElement('span', { className: 'text-[9px] text-zinc-600 font-mono truncate ml-auto' }, v.dir), (window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local) && React.createElement('button', { - onClick: () => { window.SonicAPI.openInCarla(null, v.path).then(function (r) { if (r && r.success) showToast('Đã mở Carla với ' + v.name, 'success'); }).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); }); }, + onClick: () => { openNativeVstGui(v.name || v.id, selectedTrackId, v.path, v.type).then(function (r) { if (r && (r.success || typeof r === 'string')) showToast('Đã mở GUI với ' + v.name, 'success'); }).catch(function (err) { showToast('Lỗi mở VST GUI: ' + (err.message || err), 'error'); }); }, className: 'text-[9px] bg-teal-800 hover:bg-teal-700 text-white px-1.5 py-0.5 rounded transition shrink-0', title: 'Mở trong Carla (native GUI)' }, '🎛') @@ -8287,7 +8329,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos currentBeat < n.start_beat && newBeat >= n.start_beat ); var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; }); - var pvCtx = resolveTrackInstrumentCtx(pvTrk, activeTracks); + var pvCtx = resolveSubTabInstrumentCtx(st, pvTrk, activeTracks); ensureSonicInstrument(pvCtx); playing.forEach(n => { if (isStandaloneSf() && isSfTrackEngine(pvCtx.synthEngine) && !shouldRouteCarla(pvCtx.synthEngine)) { @@ -8660,7 +8702,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos if (window.SonicSF) { const ctx = getAudioContext(); var clTrk = activeTracks.find(function(t) { return t.id === st.trackId; }); - var clCtx = resolveTrackInstrumentCtx(clTrk, activeTracks); + var clCtx = resolveSubTabInstrumentCtx(st, clTrk, activeTracks); ensureSonicInstrument(clCtx); if (isStandaloneSf() && isSfTrackEngine(clCtx.synthEngine) && !shouldRouteCarla(clCtx.synthEngine)) { if (!_routePreviewNote(clTrk && clTrk.id, clCtx, notes[clickedNoteIdx].pitch, notes[clickedNoteIdx].velocity || 0.8, 300, 'CLICK')) { @@ -8925,15 +8967,15 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos previewNodesRef.current = null; } var dwTrk = activeTracks.find(function(t) { return t.id === st.trackId; }); - var dwCtx = resolveTrackInstrumentCtx(dwTrk, activeTracks); + var dwCtx = resolveSubTabInstrumentCtx(st, dwTrk, activeTracks); var dwCh = dwTrk ? assignTrackMidiChannel(dwTrk, activeTracks) : 0; var dwPitch = snapToScaleRef.current ? snapPitchToScale(pitch, selectedScaleRef.current) : pitch; var dwDurMs = Math.max(100, Math.round(initialDur * (60 / bpm) * 1000)); - if (isStandaloneSf() && isSfTrackEngine(dwTrk && dwTrk.synth_engine) && !shouldRouteCarla(dwTrk && dwTrk.synth_engine)) { + if (isStandaloneSf() && isSfTrackEngine(dwCtx.synthEngine) && !shouldRouteCarla(dwCtx.synthEngine)) { if (!_routePreviewNote(dwTrk && dwTrk.id, dwCtx, dwPitch, brushVelocityRef.current || 0.8, dwDurMs, 'DRAW')) { if (window.SonicSF && window.SonicSF.playNote) { const ctx = getAudioContext(); - window.SonicSF.playNote(dwPitch, Math.round(brushVelocityRef.current * 127), dwDurMs, ctx.currentTime, dwTrk ? dwTrk.instrumentProgram : undefined, null, dwCh, dwTrk ? dwTrk.synth_engine : undefined); + window.SonicSF.playNote(dwPitch, Math.round(brushVelocityRef.current * 127), dwDurMs, ctx.currentTime, dwCtx.program, null, dwCh, dwCtx.synthEngine); } } } else if (window.SonicSF && window.SonicSF.playNote) { @@ -8941,7 +8983,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos // playNote (FluidSynth — nhạc cụ THẬT của track). _playNoteFallback chỉ // là oscillator beep (sai âm với percussion/soundfont — user: note vẽ // mới nghe nhạc cụ track trước). - window.SonicSF.playNote(dwPitch, Math.round(brushVelocityRef.current * 127), dwDurMs, ctx.currentTime, dwTrk ? dwTrk.instrumentProgram : undefined, null, dwCh, dwTrk ? dwTrk.synth_engine : undefined); + window.SonicSF.playNote(dwPitch, Math.round(brushVelocityRef.current * 127), dwDurMs, ctx.currentTime, dwCtx.program, null, dwCh, dwCtx.synthEngine); } } }; @@ -9034,7 +9076,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos // mastering FX của main out khi chain bật) try { if (window.__ensureMasteringRouting) window.__ensureMasteringRouting(); } catch (er) {} var pvTrk = activeTracks.find(function(t) { return t.id === st.trackId; }); - var pvCtxInst = resolveTrackInstrumentCtx(pvTrk, activeTracks); + var pvCtxInst = resolveSubTabInstrumentCtx(st, pvTrk, activeTracks); if (isStandaloneSf() && isSfTrackEngine(pvCtxInst.synthEngine) && !shouldRouteCarla(pvCtxInst.synthEngine)) { if (!_routePreviewNote(pvTrk && pvTrk.id, pvCtxInst, p, brushVelocityRef.current || 0.8, durMs, 'DRAW')) { if (window.SonicSF && window.SonicSF.playNote) { @@ -9409,7 +9451,7 @@ const PianoRollTabEditor = ({ st, zoom, bpm, viewportWidth, activeTracks, onClos const renderKeybed = () => { var kbTrk = activeTracks.find(function(t) { return t.id === st.trackId; }); - var kbCtx = resolveTrackInstrumentCtx(kbTrk, activeTracks); + var kbCtx = resolveSubTabInstrumentCtx(st, kbTrk, activeTracks); ensureSonicInstrument(kbCtx); const keys = []; for (let pitch = 127; pitch >= PITCH_START; pitch--) { @@ -11823,10 +11865,10 @@ const FXRackModal = ({ track, onUpdateTrack, onClose }) => { + +
+
Đang tải tham số…
+ + + + diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index d748eba..d4d1293 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2880,6 +2880,8 @@ dependencies = [ "tauri-build", "tauri-plugin-dialog", "tauri-plugin-shell", + "webview2-com", + "windows-core 0.61.2", ] [[package]] diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 4249a46..26520fe 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -16,6 +16,8 @@ tauri = { version = "2", features = [] } tauri-plugin-shell = "2" tauri-plugin-dialog = "2" raw-window-handle = "0.6" +webview2-com = "0.38" +windows-core = "0.61" serde = { version = "1", features = ["derive"] } serde_json = "1" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9b5871c..d4f6474 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -18,6 +18,7 @@ use std::path::{Path, PathBuf}; use std::sync::Mutex; mod vst_gui; +mod permissions; struct EngineProcess(Mutex>); @@ -77,6 +78,9 @@ pub fn run() { .plugin(tauri_plugin_dialog::init()) .invoke_handler(tauri::generate_handler![vst_gui::open_vst_gui, vst_gui::set_vst_param, vst_gui::get_vst_params, vst_gui::close_vst_editor]) .setup(|app| { + permissions::install( + &app.get_webview_window("main").expect("main window missing"), + ); vst_gui::init(app.handle()); let res_dir = app .path() diff --git a/src-tauri/src/permissions.rs b/src-tauri/src/permissions.rs new file mode 100644 index 0000000..553bc40 --- /dev/null +++ b/src-tauri/src/permissions.rs @@ -0,0 +1,27 @@ +// Standalone permissions (Bug 5): WebView2 auto-ALLOW mọi permission request +// (MIDI input, microphone, File System Access) — không hiện popup hỏi quyền. +// WebView2 mặc định DENY khi không có handler ICoreWebView2::add_PermissionRequested. +#![cfg(windows)] + +use webview2_com::Microsoft::Web::WebView2::Win32::*; +use webview2_com::PermissionRequestedEventHandler; + +pub fn install(window: &tauri::WebviewWindow) { + let _ = window.with_webview(|webview| { + unsafe { + if let Ok(core) = webview.controller().CoreWebView2() { + let handler = PermissionRequestedEventHandler::create(Box::new( + |_sender: Option, + args: Option| { + if let Some(args) = args { + args.SetState(COREWEBVIEW2_PERMISSION_STATE_ALLOW)?; + } + Ok(()) + }, + )); + let mut token = 0i64; + let _ = core.add_PermissionRequested(&handler, &mut token); + } + } + }); +}