fix(ui): master chain panel - xoa toolbar Delta Listen, GUI VST mo mac dinh spawn fresh moi lan mo panel, busy spinner + banner loi + nut Thu lai khi spawn GUI cham/fail
This commit is contained in:
+139
-56
@@ -13384,7 +13384,7 @@ const ExportProgressModal = ({ open, exportProgress, isExporting, onClose }) =>
|
||||
// gửi pointer/key về bridge qua /input. Dùng chung MasteringModal + FXRackModal.
|
||||
// url = embed_url từ API fx-gui open (embed:true); onClose khi user đóng;
|
||||
// onDead khi bridge chết (ping fail) → parent trả graph + toast.
|
||||
const VstGuiEmbed = ({ url, onClose, onDead }) => {
|
||||
const VstGuiEmbed = ({ url, onClose, onDead, closeOnUnmount, onUnmounting }) => {
|
||||
const imgRef = React.useRef(null);
|
||||
const boxRef = React.useRef(null);
|
||||
const deadRef = React.useRef(false);
|
||||
@@ -13396,7 +13396,38 @@ const ExportProgressModal = ({ open, exportProgress, isExporting, onClose }) =>
|
||||
deadRef.current = false;
|
||||
setDead(false);
|
||||
setFrameLoaded(false);
|
||||
const tick = setInterval(() => { const img = imgRef.current; if (img && img.complete) img.src = url + '/frame?t=' + Date.now(); }, 100);
|
||||
// Frame pump (fix flash): tải frame kế vào Image() ẩn, swap vào <img>
|
||||
// hiển thị chỉ khi load THÀNH CÔNG -> khong bao gio trang/broken giua
|
||||
// 2 frame. Loi frame (bridge ban): giu frame cu, retry cham hon.
|
||||
let stopped = false;
|
||||
let loader = null;
|
||||
let pumpTimer = null;
|
||||
const pump = () => {
|
||||
if (stopped) return;
|
||||
try {
|
||||
const im = new Image();
|
||||
loader = im;
|
||||
const u = url + '/frame?t=' + Date.now();
|
||||
im.onload = () => {
|
||||
if (stopped) return;
|
||||
const img = imgRef.current;
|
||||
if (img) img.src = u;
|
||||
setFrameLoaded(true);
|
||||
// Cap ~12fps: bridge JPEG-encode moi /frame (khong cache) — pump
|
||||
// nhanh hon chi dot CPU + decode main-thread -> audio WS (cung main
|
||||
// thread) tre -> gi?t am khi play + GUI mo.
|
||||
pumpTimer = window.setTimeout(pump, 80);
|
||||
};
|
||||
im.onerror = () => {
|
||||
if (stopped) return;
|
||||
pumpTimer = window.setTimeout(pump, 350);
|
||||
};
|
||||
im.src = u;
|
||||
} catch (e) {
|
||||
pumpTimer = window.setTimeout(pump, 350);
|
||||
}
|
||||
};
|
||||
pump();
|
||||
const ping = setInterval(() => {
|
||||
fetch(url + '/ping').then(r => { if (!r.ok) throw new Error('bad'); }).catch(() => {
|
||||
if (!deadRef.current) { deadRef.current = true; setDead(true); onDead && onDead(); }
|
||||
@@ -13407,10 +13438,22 @@ const ExportProgressModal = ({ open, exportProgress, isExporting, onClose }) =>
|
||||
// rect refresh below re-anchors it over the panel.
|
||||
try { fetch(url + '/show', { method: 'POST' }).catch(() => {}); } catch (e) {}
|
||||
return () => {
|
||||
clearInterval(tick); clearInterval(ping);
|
||||
// Panel closed -> hide the native window (bridge stays alive so
|
||||
// reopening is fast). The "Đóng GUI" button still POSTs /close (exit).
|
||||
try { fetch(url + '/hide', { method: 'POST' }).catch(() => {}); } catch (e) {}
|
||||
stopped = true;
|
||||
if (loader) { loader.onload = null; loader.onerror = null; loader = null; }
|
||||
if (pumpTimer) clearTimeout(pumpTimer);
|
||||
clearInterval(ping);
|
||||
// closeOnUnmount (master chain panel dong): dong HAN bridge GUI thay vi
|
||||
// hide — lan mo panel sau spawn lai fresh (instance cu treo/khong hien
|
||||
// thi se khong con duoc tai su dung). Capture preset cuoi truoc khi dong.
|
||||
if (closeOnUnmount) {
|
||||
try {
|
||||
fetch(url + '/preset').then(r => r.json()).then(d => { try { if (onUnmounting) onUnmounting((d && d.preset_b64) || ''); } catch (e) {} }).catch(() => { try { if (onUnmounting) onUnmounting(''); } catch (e) {} }).then(() => { try { fetch(url + '/close', { method: 'POST' }).catch(() => {}); } catch (e) {} });
|
||||
} catch (e) { try { if (onUnmounting) onUnmounting(''); } catch (e2) {} try { fetch(url + '/close', { method: 'POST' }).catch(() => {}); } catch (e3) {} }
|
||||
} else {
|
||||
// Track FX rack panel: hide (bridge stays alive — reopen nhanh).
|
||||
// The "Đóng GUI" button still POSTs /close (exit).
|
||||
try { fetch(url + '/hide', { method: 'POST' }).catch(() => {}); } catch (e) {}
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [url]);
|
||||
@@ -14229,6 +14272,13 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
|
||||
const masterGuiOpenRef = React.useRef(null);
|
||||
masterGuiOpenRef.current = masterGuiOpen;
|
||||
const masterGuiBusyRef = React.useRef(false); // request openFxGui đang bay (chưa có verdict 20-70s)
|
||||
// Busy/error render-state: GUI embed spawn chậm (Ozone ~30-60s) — UI phải
|
||||
// hiện trạng thái thay vì im lặng ("GUI không hiện"). Error + nút Thử lại.
|
||||
const [masterGuiBusy, setMasterGuiBusy] = React.useState(false);
|
||||
const [masterGuiError, setMasterGuiError] = React.useState(null);
|
||||
const masterGuiAttemptRef = React.useRef(null); // {path,name} cho nút Thử lại
|
||||
const beginMasterGuiBusy = () => { masterGuiBusyRef.current = true; setMasterGuiBusy(true); setMasterGuiError(null); };
|
||||
const endMasterGuiBusy = () => { masterGuiBusyRef.current = false; setMasterGuiBusy(false); };
|
||||
const closeMasterGui = () => {
|
||||
const g = masterGuiOpenRef.current;
|
||||
if (g) {
|
||||
@@ -14242,9 +14292,20 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
|
||||
delete _fxGuiBaselineByPath[g.path];
|
||||
}
|
||||
setActiveMasterVstPath(null);
|
||||
setMasterGuiError(null);
|
||||
setOzState(prev => { const n = { ...(prev || {}) }; delete n.gui_open_path; delete n.gui_open_name; return n; });
|
||||
};
|
||||
React.useEffect(() => () => { const g = masterGuiOpenRef.current; if (g) { try { fetch(g.url + '/hide', { method: 'POST' }).catch(() => {}); } catch (e) {} const _mk = 'master:' + g.path; if (_presetRestartTimers[_mk]) { clearTimeout(_presetRestartTimers[_mk]); delete _presetRestartTimers[_mk]; } if (g.path) delete _fxGuiBaselineByPath[g.path]; } }, []);
|
||||
// Panel (modal) dong -> VstGuiEmbed unmount -> bridge GUI da /close (dung
|
||||
// closeOnUnmount). Reset state GUI de lan mo panel sau SPAWN LAI fresh (y/c:
|
||||
// "mo master chain panel phai load lai GUI cua VST FX") — nhung GIU
|
||||
// gui_open_path + preset da capture de effect auto-reopen ben duoi chay.
|
||||
const closeMasterGuiForPanel = (presetB64) => {
|
||||
const g = masterGuiOpenRef.current;
|
||||
if (g && g.path && presetB64) _capturedPresetByPath[g.path] = presetB64;
|
||||
setMasterGuiOpen(null);
|
||||
setActiveMasterVstPath(null);
|
||||
};
|
||||
React.useEffect(() => () => { const g = masterGuiOpenRef.current; if (g) { const _mk = 'master:' + g.path; if (_presetRestartTimers[_mk]) { clearTimeout(_presetRestartTimers[_mk]); delete _presetRestartTimers[_mk]; } if (g.path) delete _fxGuiBaselineByPath[g.path]; } }, []);
|
||||
// Capture GUI knob tweaks while the embedded GUI is open (bridge /preset).
|
||||
React.useEffect(() => {
|
||||
if (!masterGuiOpen || !masterGuiOpen.url) return;
|
||||
@@ -14255,17 +14316,36 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [masterGuiOpen]);
|
||||
// Reopen the saved GUI when the panel is opened after a project restore.
|
||||
// MỞ MẶC ĐỊNH khi mở panel: GUI VST load lại fresh mỗi lần (bridge cũ đã
|
||||
// /close khi đóng panel — không tái dùng instance cũ). Chạy đúng lúc panel
|
||||
// vừa mở (isOpen false->true) — không tự mở lại sau khi user bấm "Đóng GUI"
|
||||
// ngay trong phiên. Nếu chưa có gui_open_path (chưa từng mở thành công) thì
|
||||
// fallback mở VST đầu tiên trong master chain.
|
||||
const masterPanelWasOpenRef = React.useRef(false);
|
||||
React.useEffect(() => {
|
||||
if (!isOpen || !ozState || !ozState.gui_open_path || masterGuiOpen || masterGuiBusyRef.current) return;
|
||||
const s = (ozState.vstFxChain || []).find(x => x.path === ozState.gui_open_path);
|
||||
const justOpened = !!isOpen && !masterPanelWasOpenRef.current;
|
||||
masterPanelWasOpenRef.current = !!isOpen;
|
||||
if (!isOpen || !justOpened) return;
|
||||
if (masterGuiOpen || masterGuiBusyRef.current) return;
|
||||
let path = ozState && ozState.gui_open_path;
|
||||
let name = ozState && ozState.gui_open_name;
|
||||
let s = (ozState.vstFxChain || []).find(x => x && x.path === path);
|
||||
if (!s) {
|
||||
const first = (ozState.vstFxChain || []).find(x => x && x.path);
|
||||
if (!first) return;
|
||||
s = first; path = first.path; name = first.name || first.path;
|
||||
}
|
||||
if (!s || !window.SonicAPI || !window.SonicAPI.openFxGui) return;
|
||||
masterGuiBusyRef.current = true;
|
||||
window.SonicAPI.openFxGui({ path: s.path, name: ozState.gui_open_name || s.name || s.path, embed: true, shm: fxRt.sessionId || '', preset_b64: _capturedPresetByPath[s.path] || s.preset_b64 || '' }).then(r => {
|
||||
masterGuiBusyRef.current = false;
|
||||
if (r && r.embed_url) setMasterGuiOpen({ path: s.path, url: r.embed_url });
|
||||
}).catch(() => { masterGuiBusyRef.current = false; });
|
||||
masterGuiAttemptRef.current = { path: s.path, name: name || s.name || s.path };
|
||||
beginMasterGuiBusy();
|
||||
window.SonicAPI.openFxGui({ path: s.path, name: name || s.name || s.path, embed: true, shm: fxRt.sessionId || '', preset_b64: _capturedPresetByPath[s.path] || s.preset_b64 || '' }).then(r => {
|
||||
endMasterGuiBusy();
|
||||
if (r && r.embed_url) { setMasterGuiOpen({ path: s.path, url: r.embed_url }); setOzState(prev => ({ ...(prev || {}), gui_open_path: s.path, gui_open_name: name || s.name || s.path })); }
|
||||
else if (r && r.already_running && r.embed_url) { setMasterGuiOpen({ path: s.path, url: r.embed_url }); setOzState(prev => ({ ...(prev || {}), gui_open_path: s.path, gui_open_name: name || s.name || s.path })); }
|
||||
else setMasterGuiError('Không mở được GUI embed cho ' + (name || s.name || s.path) + ' — plugin có thể crash khi tải. Bấm Thử lại.');
|
||||
}).catch(err => { endMasterGuiBusy(); setMasterGuiError('Lỗi mở GUI: ' + (err.message || err)); });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [isOpen, ozState && ozState.gui_open_path]);
|
||||
}, [isOpen]);
|
||||
const setMasterVstChain = (next) => setOzState(prev => {
|
||||
const chain = prev.chain || [];
|
||||
const out = [];
|
||||
@@ -14296,14 +14376,22 @@ const MasteringModal = ({ isOpen, onClose, masteringSettings, setMasteringSettin
|
||||
if (masterGuiOpen) closeMasterGui();
|
||||
if (s.path) setActiveMasterVstPath(s.path);
|
||||
if (!window.SonicAPI || !window.SonicAPI.openFxGui) { window.showToast && window.showToast('Máy này không hỗ trợ mở GUI VST', 'warning'); return; }
|
||||
masterGuiBusyRef.current = true;
|
||||
masterGuiAttemptRef.current = { path: s.path, name: s.name || s.path };
|
||||
beginMasterGuiBusy();
|
||||
window.SonicAPI.openFxGui({ path: s.path, name: s.name || s.path, embed: true, shm: fxRt.sessionId || '', preset_b64: _capturedPresetByPath[s.path] || s.preset_b64 || '' }).then(r => {
|
||||
masterGuiBusyRef.current = false;
|
||||
endMasterGuiBusy();
|
||||
if (r && r.embed_url) { setMasterGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); setOzState(prev => ({ ...(prev || {}), gui_open_path: s.path, gui_open_name: s.name || s.path })); }
|
||||
else if (r && r.already_running && r.embed_url) { setMasterGuiOpen({ path: s.path, url: r.embed_url, name: s.name || s.path }); setOzState(prev => ({ ...(prev || {}), gui_open_path: s.path, gui_open_name: s.name || s.path })); }
|
||||
else if (r && r.already_running) window.showToast && window.showToast('GUI của plugin này đang mở sẵn (cửa sổ ngoài)', 'info');
|
||||
else window.showToast && window.showToast('Không mở được GUI embed', 'error');
|
||||
}).catch(err => { masterGuiBusyRef.current = false; window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error'); });
|
||||
else { setMasterGuiError('Không mở được GUI embed cho ' + (s.name || s.path) + ' — plugin có thể crash khi tải. Bấm Thử lại để spawn lại.'); window.showToast && window.showToast('Không mở được GUI embed', 'error'); }
|
||||
}).catch(err => { endMasterGuiBusy(); setMasterGuiError('Lỗi mở GUI: ' + (err.message || err)); window.showToast && window.showToast('Lỗi mở GUI: ' + (err.message || err), 'error'); });
|
||||
};
|
||||
// Nút "Thử lại" trên banner lỗi — spawn lại GUI fresh cho lần mở gần nhất.
|
||||
const retryMasterGui = () => {
|
||||
const t = masterGuiAttemptRef.current;
|
||||
if (!t || !t.path) return;
|
||||
if (masterGuiOpen) return;
|
||||
openMasterVstGui({ path: t.path, name: t.name || t.path });
|
||||
};
|
||||
const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.path === path); if (!fx) return; setMasterVstChain([...masterVstChain, { type: 'vst3', path: fx.path, name: fx.name || fx.id, preset_b64: '', bypass: false }]);
|
||||
openMasterVstGui({ path: fx.path, name: fx.name || fx.id }); };
|
||||
@@ -15116,40 +15204,31 @@ const addMasterVst = (path) => { const fx = (masterFxList || []).find(f => f.pat
|
||||
{/* LEFT: MODULE VIEWS */}
|
||||
{masterGuiOpen ? (
|
||||
<div className="flex-1 flex flex-col bg-slate-950 relative min-h-0">
|
||||
<VstGuiEmbed url={masterGuiOpen.url} onClose={closeMasterGui}
|
||||
<VstGuiEmbed url={masterGuiOpen.url} onClose={closeMasterGui} closeOnUnmount
|
||||
onUnmounting={closeMasterGuiForPanel}
|
||||
onDead={() => { window.showToast && window.showToast('Bridge VST GUI đã mất kết nối — trả về graph', 'error'); closeMasterGui(); }} />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-1 flex flex-col bg-slate-950 relative overflow-y-auto oz-scrollbar">
|
||||
|
||||
{/* SUB HEADER TOOLBAR */}
|
||||
<div className="h-9 bg-slate-900/60 border-b border-slate-800/80 px-4 flex items-center justify-between text-xs oz-font-mono shrink-0">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-slate-400 flex items-center gap-1.5"><i data-lucide="headphones" className="w-3 h-3 text-cyan-400"></i> Delta Listen</span>
|
||||
<select className="bg-slate-950 border border-slate-800 rounded px-2 py-0.5 text-[11px] text-slate-300 outline-none">
|
||||
<option value="irc4">IRC IV - Classic</option>
|
||||
<option value="irc3">IRC III - Balanced</option>
|
||||
<option value="irc2">IRC II - Crisp</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-[11px]">
|
||||
{ozState.activeModule === 'eq' && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-slate-400">EQ Presets:</span>
|
||||
<select
|
||||
value={ozState.eqPreset || 'flat'}
|
||||
onChange={e => applyEQPreset(e.target.value)}
|
||||
className="bg-slate-950 border border-slate-700 text-cyan-300 rounded px-2 py-0.5 text-[11px] outline-none focus:border-cyan-500">
|
||||
{Object.keys(EQ_PRESET_LIBRARY).map(k => (
|
||||
<option key={k} value={k}>{EQ_PRESET_LIBRARY[k].name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{/* STATUS GUI VST — thay toolbar "Delta Listen" (không cần: GUI VST FX load mặc định) */}
|
||||
{(masterGuiBusy || masterGuiError) && (
|
||||
<div className={`h-9 px-4 flex items-center gap-2 text-[11px] oz-font-mono shrink-0 border-b ${masterGuiError ? 'bg-red-950/40 border-red-900/60 text-red-300' : 'bg-cyan-950/40 border-cyan-900/60 text-cyan-300'}`}>
|
||||
{masterGuiBusy && !masterGuiError && (
|
||||
<>
|
||||
<span className="w-3 h-3 border-2 border-cyan-400 border-t-transparent rounded-full animate-spin inline-block shrink-0"></span>
|
||||
<span className="truncate">Đang mở GUI VST — plugin khởi tạo có thể mất 30-60 giây, xin chờ...</span>
|
||||
</>
|
||||
)}
|
||||
{masterGuiError && (
|
||||
<>
|
||||
<span className="truncate flex-1">{masterGuiError}</span>
|
||||
<button onClick={retryMasterGui} className="px-2 py-0.5 bg-red-800 hover:bg-red-700 text-white rounded text-[10px] font-bold shrink-0">Thử lại</button>
|
||||
<button onClick={() => setMasterGuiError(null)} title="Ẩn" className="text-red-400 hover:text-red-200 shrink-0"><i data-lucide="x" className="w-3 h-3"></i></button>
|
||||
</>
|
||||
)}
|
||||
<span className="text-slate-400">Learn Input Gain:</span>
|
||||
<button className="bg-cyan-950 text-cyan-300 border border-cyan-800/80 px-2 py-0.5 rounded font-bold hover:bg-cyan-900 transition-colors">-11.0 LUFS</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* VIEW: DYNAMIC EQ */}
|
||||
<div className={`flex-1 flex flex-col p-4 gap-4 ${ozState.activeModule === 'eq' ? '' : 'hidden'}`}>
|
||||
@@ -23772,17 +23851,21 @@ const App = () => {
|
||||
fetch(API_BASE_URL, { signal: ctl.signal })
|
||||
.then(r => { if (!cancelled) { setServerStatus(r.ok ? 'connected' : 'error'); offlineCount = 0; } })
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
setServerStatus('offline');
|
||||
// Self-heal: engine chết/mất kết nối liên tục (~9s) → reload trang
|
||||
// để bootstrap chạy lại: ưu tiên port engine của chính app
|
||||
// (engine_port.txt) + fallback quét 8000-8010. Trước đây kẹt
|
||||
// OFFLINE vĩnh viễn khi engine bị instance khác giết port.
|
||||
offlineCount++;
|
||||
if (offlineCount >= 3) {
|
||||
offlineCount = 0;
|
||||
if (cancelled) return;
|
||||
offlineCount++;
|
||||
// Pill chi chuyen OFFLINE sau >=2 fail lien tiep (1 fail le do webview
|
||||
// nghen/timeout khong lam "SERVER OFFLINE/CONNECTED" doi lien tuc).
|
||||
// Reload sau 4 fail (~12s) + guard chong reload-loop (toi da 1 lan/15s).
|
||||
setServerStatus(offlineCount >= 2 ? 'offline' : 'checking...');
|
||||
if (offlineCount >= 4) {
|
||||
offlineCount = 0;
|
||||
const now = Date.now();
|
||||
let last = 0;
|
||||
try { last = parseInt(sessionStorage.getItem('sf_reload_ts') || '0', 10) || 0; } catch (e) {}
|
||||
if (now - last > 15000) {
|
||||
try { sessionStorage.setItem('sf_reload_ts', String(now)); } catch (e) {}
|
||||
try { window.location.reload(); } catch (e) {}
|
||||
}
|
||||
} else { setServerStatus('offline'); }
|
||||
}
|
||||
})
|
||||
.finally(() => { busy = false; clearTimeout(t); });
|
||||
|
||||
+2781
-2757
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user