FIX: Carla sử dụng bridge
This commit is contained in:
+201
-22
@@ -5279,7 +5279,7 @@ const AIConfigModal = ({
|
||||
}, loading ? 'Đang lưu...' : 'Lưu Cấu Hình AI'))))));
|
||||
};
|
||||
|
||||
const PluginManagerModal = ({ isOpen, onClose, pluginsData }) => {
|
||||
const PluginManagerModal = ({ isOpen, onClose, pluginsData, onInsertInstrument }) => {
|
||||
if (!isOpen) return null;
|
||||
const [localData, setLocalData] = React.useState(pluginsData);
|
||||
const [sfUploadStatus, setSfUploadStatus] = React.useState('');
|
||||
@@ -5288,6 +5288,12 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData }) => {
|
||||
const [pmScanning, setPmScanning] = React.useState(false);
|
||||
const [pmScanResult, setPmScanResult] = React.useState('');
|
||||
const [pmScanData, setPmScanData] = React.useState(null); // { vst_found, soundfonts }
|
||||
// Instrument bên trong mỗi soundfont (expand) — "Chèn vào Synth" qua onInsertInstrument
|
||||
const [pmSfExpanded, setPmSfExpanded] = React.useState({});
|
||||
const [pmSfInstruments, setPmSfInstruments] = React.useState({});
|
||||
const [pmSfLoading, setPmSfLoading] = React.useState({});
|
||||
// Force re-render sau khi định vị Carla (capabilities đổi)
|
||||
const [pmCarlaVersion, setPmCarlaVersion] = React.useState(0);
|
||||
React.useEffect(() => {
|
||||
if (isOpen) {
|
||||
window.SonicAPI.listPlugins()
|
||||
@@ -5365,6 +5371,51 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData }) => {
|
||||
const removePluginDir = (dir) => {
|
||||
setPmDirs(prev => prev.filter(d => d !== dir));
|
||||
};
|
||||
// Expand 1 soundfont → đọc danh sách instrument (bank/program/name) bên trong
|
||||
// qua API soundfont-instruments/{id} → hiển thị nút "Chèn vào Synth".
|
||||
const toggleSfInstruments = async (sf) => {
|
||||
const baseId = String(sf.id || '').replace('sf_', '');
|
||||
setPmSfExpanded(prev => ({ ...prev, [baseId]: !prev[baseId] }));
|
||||
if (!pmSfInstruments[baseId] && !pmSfLoading[baseId]) {
|
||||
setPmSfLoading(prev => ({ ...prev, [baseId]: true }));
|
||||
try {
|
||||
const r = await window.SonicAPI.listSoundfontInstruments(baseId);
|
||||
setPmSfInstruments(prev => ({ ...prev, [baseId]: (r && r.presets) || [] }));
|
||||
} catch (e) {
|
||||
setPmSfInstruments(prev => ({ ...prev, [baseId]: [] }));
|
||||
} finally {
|
||||
setPmSfLoading(prev => ({ ...prev, [baseId]: false }));
|
||||
}
|
||||
}
|
||||
};
|
||||
// Định vị Carla.exe — bản Windows là zip portable: KHÔNG cài đặt, KHÔNG dùng
|
||||
// biến môi trường PATH nên heuristic không tìm thấy → user tự chọn thư mục
|
||||
// chứa carla.exe (folder picker native) → lưu config phía server.
|
||||
const locateCarla = async () => {
|
||||
try {
|
||||
let picked = null;
|
||||
try {
|
||||
const d = await window.SonicAPI.pickPluginDir();
|
||||
if (d && typeof d.path === 'string' && d.path) picked = d.path;
|
||||
} catch (e) { /* fallthrough */ }
|
||||
if (!picked && window.__TAURI__ && window.__TAURI__.core && window.__TAURI__.core.invoke) {
|
||||
try {
|
||||
const sel = await window.__TAURI__.core.invoke('plugin:dialog|open', { options: { directory: true, multiple: false } });
|
||||
if (typeof sel === 'string' && sel) picked = sel;
|
||||
} catch (e) { /* fallthrough */ }
|
||||
}
|
||||
if (!picked) { showToast('Không mở được hộp thoại chọn thư mục', 'error'); return; }
|
||||
const r = await window.SonicAPI.setCarlaPath(picked);
|
||||
if (r && r.success && r.carla_path) {
|
||||
window.SonicRuntime.capabilities = r;
|
||||
document.documentElement.dataset.carla = r.features && r.features.carla_local ? '1' : '0';
|
||||
setPmCarlaVersion(v => v + 1);
|
||||
showToast('Đã định vị Carla: ' + r.carla_path, 'success');
|
||||
} else {
|
||||
showToast('Không tìm thấy carla.exe trong thư mục đã chọn', 'error');
|
||||
}
|
||||
} catch (err) { showToast('Lỗi định vị Carla: ' + (err.message || err), 'error'); }
|
||||
};
|
||||
// Lưu dirs (user override) + scan cả 2 thư mục → refresh list + catalog.
|
||||
const saveAndScanDirs = async () => {
|
||||
setPmScanning(true);
|
||||
@@ -5545,28 +5596,78 @@ const PluginManagerModal = ({ isOpen, onClose, pluginsData }) => {
|
||||
) :
|
||||
(localData.soundfonts?.length === 0 ?
|
||||
React.createElement('div', { className: 'flex items-center justify-center h-32 text-zinc-500 text-xs' }, 'No SoundFonts found. Upload one below.') :
|
||||
localData.soundfonts.map((sf, i) =>
|
||||
React.createElement('div', {
|
||||
localData.soundfonts.map((sf, i) => {
|
||||
const baseId = String(sf.id || '').replace('sf_', '');
|
||||
const expanded = !!pmSfExpanded[baseId];
|
||||
const insts = pmSfInstruments[baseId] || [];
|
||||
const loading = !!pmSfLoading[baseId];
|
||||
return React.createElement('div', {
|
||||
key: i,
|
||||
className: 'flex items-center justify-between bg-[#252525] px-4 py-3 rounded-lg border border-[#333] hover:border-amber-800/50 transition group'
|
||||
className: 'bg-[#252525] rounded-lg border border-[#333] hover:border-amber-800/50 transition group'
|
||||
},
|
||||
React.createElement('div', { className: 'flex items-center gap-3' },
|
||||
React.createElement('div', { className: 'w-8 h-8 rounded bg-amber-900/30 flex items-center justify-center' },
|
||||
React.createElement('i', { 'data-lucide': 'music', className: 'w-4 h-4 text-amber-400' })
|
||||
React.createElement('div', {
|
||||
className: 'flex items-center justify-between px-4 py-3 cursor-pointer',
|
||||
onClick: () => toggleSfInstruments(sf)
|
||||
},
|
||||
React.createElement('div', { className: 'flex items-center gap-3' },
|
||||
React.createElement('div', { className: 'w-8 h-8 rounded bg-amber-900/30 flex items-center justify-center' },
|
||||
React.createElement('i', { 'data-lucide': 'music', className: 'w-4 h-4 text-amber-400' })
|
||||
),
|
||||
React.createElement('div', null,
|
||||
React.createElement('div', { className: 'text-xs font-semibold text-slate-200' }, sf.display || sf.name || sf.id),
|
||||
React.createElement('div', { className: 'text-[10px] text-zinc-500' }, (sf.file || sf.name) + (insts.length ? ' — ' + insts.length + ' instruments' : ''))
|
||||
)
|
||||
),
|
||||
React.createElement('div', null,
|
||||
React.createElement('div', { className: 'text-xs font-semibold text-slate-200' }, sf.display || sf.name || sf.id),
|
||||
React.createElement('div', { className: 'text-[10px] text-zinc-500' }, sf.file || sf.name)
|
||||
React.createElement('div', { className: 'flex items-center gap-2' },
|
||||
React.createElement('span', { className: 'text-[10px] text-zinc-500' }, expanded ? '▾' : '▸'),
|
||||
React.createElement('button', {
|
||||
onClick: (e) => { e.stopPropagation(); setSfToDelete(sf); },
|
||||
className: 'text-[10px] text-zinc-500 hover:text-red-400 opacity-0 group-hover:opacity-100 transition px-2 py-1'
|
||||
}, 'Delete')
|
||||
)
|
||||
),
|
||||
React.createElement('div', { className: 'flex items-center gap-2' },
|
||||
React.createElement('button', {
|
||||
onClick: () => setSfToDelete(sf),
|
||||
className: 'text-[10px] text-zinc-500 hover:text-red-400 opacity-0 group-hover:opacity-100 transition px-2 py-1'
|
||||
}, 'Delete')
|
||||
expanded && React.createElement('div', { className: 'border-t border-[#333] px-3 py-1 max-h-40 overflow-y-auto' },
|
||||
loading ?
|
||||
React.createElement('p', { className: 'text-[10px] text-zinc-500 italic py-1' }, 'Đang đọc instruments...') :
|
||||
insts.length === 0 ?
|
||||
React.createElement('p', { className: 'text-[10px] text-zinc-500 italic py-1' }, 'Không có instrument (SF3 cần chuyển đổi trước)') :
|
||||
insts.map((p, pi) => React.createElement('div', { key: 'si_' + pi, className: 'flex items-center gap-2 py-1 text-[11px]' },
|
||||
React.createElement('span', { className: 'text-zinc-500 font-mono w-24 shrink-0 text-[9px]' }, 'B' + (p.bank || 0) + ' P' + (p.program || 0)),
|
||||
React.createElement('span', { className: 'flex-1 truncate text-zinc-300' }, p.name || ('Program ' + p.program)),
|
||||
React.createElement('button', {
|
||||
onClick: () => onInsertInstrument && onInsertInstrument({
|
||||
instrumentId: 'sf_' + baseId,
|
||||
bank: p.bank || 0,
|
||||
program: p.program || 0,
|
||||
name: p.name || ('Program ' + p.program),
|
||||
displayName: (sf.display || sf.name || sf.id) + ' — ' + (p.name || ('Program ' + p.program))
|
||||
}),
|
||||
className: 'text-[10px] bg-amber-800 hover:bg-amber-700 text-white px-2 py-0.5 rounded transition shrink-0'
|
||||
}, 'Chèn vào Synth')
|
||||
))
|
||||
)
|
||||
)
|
||||
))
|
||||
);
|
||||
})
|
||||
)
|
||||
),
|
||||
// ── Carla Bridge section (desktop) — định vị carla.exe portable ──
|
||||
(window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.runtime === 'desktop') && React.createElement('div', { className: 'pt-4 mt-4 border-t border-[#383838]' },
|
||||
React.createElement('h4', { className: 'text-xs font-bold text-teal-400 uppercase mb-2' }, 'Carla Bridge (VSTi native GUI)'),
|
||||
React.createElement('p', { className: 'text-[10px] text-zinc-500 mb-2 break-all' },
|
||||
(window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_path) ?
|
||||
'Đã định vị: ' + window.SonicRuntime.capabilities.features.carla_path :
|
||||
'Chưa tìm thấy Carla. Bản Windows là zip portable (không cài đặt, không dùng PATH) — nhấn "Định vị Carla..." và chọn thư mục chứa carla.exe.'
|
||||
),
|
||||
React.createElement('div', { className: 'flex gap-2' },
|
||||
React.createElement('button', {
|
||||
onClick: locateCarla,
|
||||
className: 'px-3 py-1.5 bg-teal-800 hover:bg-teal-700 text-white text-xs font-semibold rounded transition flex items-center gap-1'
|
||||
}, React.createElement('i', { 'data-lucide': 'folder-search', className: 'w-3 h-3' }), 'Định vị Carla...'),
|
||||
(window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local) && React.createElement('button', {
|
||||
onClick: () => { window.SonicAPI.openInCarla().then(function (r) { if (r && r.success) showToast('Đã mở Carla', 'success'); }).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); }); },
|
||||
className: 'px-3 py-1.5 bg-zinc-800 hover:bg-zinc-700 text-teal-300 text-xs font-semibold rounded transition flex items-center gap-1'
|
||||
}, React.createElement('i', { 'data-lucide': 'play', className: 'w-3 h-3' }), 'Mở Carla')
|
||||
)
|
||||
),
|
||||
// Plugin directories section (folder picker + save + scan)
|
||||
React.createElement('div', {
|
||||
@@ -14279,6 +14380,11 @@ const MediaExplorerPanel = ({ height, clipboardRef, active }) => {
|
||||
<div className={`flex items-center gap-1 px-2 py-1 cursor-pointer hover:bg-blue-100 ${!synthInst ? 'bg-slate-200' : ''}`} onClick={() => selectSynthInst(null)}>
|
||||
<i className="fa-solid fa-ban text-slate-400"></i> None (mặc định)
|
||||
</div>
|
||||
{window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local && (
|
||||
<div className="flex items-center gap-1 px-2 py-1 cursor-pointer hover:bg-teal-100 font-semibold text-teal-700 border-t border-[#e0e0e0]" onClick={() => { setSynthOpen(false); window.SonicAPI.openInCarla().then(r => { if (r && r.success) showToast('Đã mở Carla — chỉnh preset rồi Upload trong app', 'success'); }).catch(err => showToast('Lỗi mở Carla: ' + (err.message || err), 'error')); }}>
|
||||
<i className="fa-solid fa-sliders text-teal-500"></i> 🎛 Carla Bridge (mở Carla.exe)
|
||||
</div>
|
||||
)}
|
||||
{!synthLoading && filteredSynthList && filteredSynthList.map(group => (
|
||||
<div key={group.sf.id || group.sf.name}>
|
||||
<div className="px-2 py-1 bg-[#f0f0f0] font-semibold text-slate-600 border-t border-[#e0e0e0] truncate">{group.sf.display || group.sf.name || group.sf.id}</div>
|
||||
@@ -15520,6 +15626,27 @@ const App = () => {
|
||||
setAiPrompt(newText);
|
||||
};
|
||||
|
||||
// Gán preset VST3 (.vstpreset từ thư viện storage/presets — cầu nối
|
||||
// Carla → pedalboard) vào track: preset_id nằm trong synth_engine → render
|
||||
// engine tải qua load_preset khi render → âm render = âm đã chỉnh trong Carla.
|
||||
const setTrackPreset = (trackId, presetId) => {
|
||||
if (!trackId || !presetId) return;
|
||||
var mt = activeTracksRef.current || tracks;
|
||||
var cur = null;
|
||||
for (var ci = 0; ci < mt.length; ci++) { if (mt[ci].id === trackId) { cur = mt[ci]; break; } }
|
||||
if (!cur) return;
|
||||
var se = cur.synth_engine || {};
|
||||
if (!se.plugin_id || String(se.type || '').indexOf('vst') === -1) {
|
||||
showToast('Chọn VST instrument trước khi gán preset', 'warning');
|
||||
return;
|
||||
}
|
||||
updateActiveTracks(prev => prev.map(t => t.id === trackId ? { ...t, synth_engine: { ...(t.synth_engine || {}), preset_id: presetId } } : t));
|
||||
setInstrumentDropdownTrackId(null);
|
||||
setInstrumentDropdownBtnRect(null);
|
||||
var p = ((window.SonicRuntime && window.SonicRuntime.presets) || []).find(function (x) { return x.id === presetId; });
|
||||
showToast('Đã gán preset: ' + ((p && p.name) || presetId), 'success');
|
||||
};
|
||||
|
||||
const setTrackInstrumentWithUndo = (trackId, instrumentId, displayName, bankNumber, programNumber) => {
|
||||
const track = activeTracks.find(t => t.id === trackId);
|
||||
if (!track) return;
|
||||
@@ -29802,7 +29929,16 @@ STRICT CONSTRAINTS:
|
||||
}), /*#__PURE__*/React.createElement(PluginManagerModal, {
|
||||
isOpen: pluginManagerModalOpen,
|
||||
onClose: () => setPluginManagerModalOpen(false),
|
||||
pluginsData: pluginsData
|
||||
pluginsData: pluginsData,
|
||||
onInsertInstrument: (inst) => {
|
||||
// Chèn instrument (soundfont bank/program) vào track đang chọn — nút Synth
|
||||
const tid = selectedTrackId || (activeTracks && activeTracks[0] && activeTracks[0].id);
|
||||
if (tid && inst && inst.instrumentId) {
|
||||
setTrackInstrumentWithProgram(tid, inst.instrumentId, inst.program, inst.displayName || inst.name, inst.bank);
|
||||
showToast('Đã chèn nhạc cụ: ' + (inst.displayName || inst.name), 'success');
|
||||
}
|
||||
setPluginManagerModalOpen(false);
|
||||
}
|
||||
}), /*#__PURE__*/React.createElement(AIPresetModal, {
|
||||
isOpen: aiPresetModalOpen,
|
||||
onClose: () => { setAiPresetModalOpen(false); setAiPresetVersion(v => v + 1); }
|
||||
@@ -29943,6 +30079,14 @@ STRICT CONSTRAINTS:
|
||||
onClick: () => setTrackInstrumentWithUndo(instrumentDropdownTrackId, null),
|
||||
className: "w-full text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-zinc-700 text-zinc-400"
|
||||
}, "None (Default Synth)"),
|
||||
window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local ? /*#__PURE__*/React.createElement("button", {
|
||||
onClick: () => {
|
||||
setInstrumentDropdownTrackId(null); setInstrumentDropdownBtnRect(null);
|
||||
window.SonicAPI.openInCarla().then(function (r) { if (r && r.success) { showToast('Đã mở Carla — chọn VSTi, chỉnh âm, Save preset (.vstpreset) rồi Upload trong app', 'success'); } }).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); });
|
||||
},
|
||||
className: "w-full text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-teal-800 text-teal-300 font-semibold flex items-center justify-between",
|
||||
title: "Mở Carla.exe trên hệ thống (native GUI VSTi) — không cần scan VST trong app"
|
||||
}, "\uD83C\uDF9B Carla Bridge (m\u1EDF Carla.exe)", /*#__PURE__*/React.createElement("span", { className: "text-[9px] text-teal-500 shrink-0 ml-1" }, "GUI")) : null,
|
||||
filteredInstruments.soundfonts.length > 0 && /*#__PURE__*/React.createElement("div", { className: "text-[10px] text-zinc-500 px-3 py-1 mt-1 uppercase font-bold" }, "SoundFonts"),
|
||||
filteredInstruments.soundfonts.map((sf, i) => /*#__PURE__*/React.createElement("button", {
|
||||
key: "sfd_" + i,
|
||||
@@ -29950,11 +30094,46 @@ STRICT CONSTRAINTS:
|
||||
className: "w-full text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-amber-800 text-zinc-200 flex items-center justify-between"
|
||||
}, /*#__PURE__*/React.createElement("span", { className: "truncate" }, sf.display || sf.name || sf.id), /*#__PURE__*/React.createElement("span", { className: "text-[10px] text-amber-400 shrink-0 ml-1" }, "SF"))),
|
||||
filteredInstruments.vst.length > 0 && /*#__PURE__*/React.createElement("div", { className: "text-[10px] text-zinc-500 px-3 py-1 mt-1 uppercase font-bold" }, "VST Instruments"),
|
||||
filteredInstruments.vst.map((v, i) => /*#__PURE__*/React.createElement("button", {
|
||||
filteredInstruments.vst.map((v, i) => /*#__PURE__*/React.createElement("div", {
|
||||
key: "vstd_" + i,
|
||||
onClick: () => { setInstrumentDropdownTrackId(null); setInstrumentDropdownBtnRect(null); setTrackInstrumentWithUndo(instrumentDropdownTrackId, v.id, v.name || v.id); },
|
||||
className: "w-full text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-violet-800 text-zinc-200 flex items-center justify-between"
|
||||
}, /*#__PURE__*/React.createElement("span", { className: "truncate" }, v.name || v.id), /*#__PURE__*/React.createElement("span", { className: "text-[10px] text-cyan-400 shrink-0 ml-1" }, v.type || "VST"))),
|
||||
className: "flex items-stretch"
|
||||
},
|
||||
/*#__PURE__*/React.createElement("button", {
|
||||
onClick: () => { setInstrumentDropdownTrackId(null); setInstrumentDropdownBtnRect(null); setTrackInstrumentWithUndo(instrumentDropdownTrackId, v.id, v.name || v.id); },
|
||||
className: "flex-1 min-w-0 text-left px-3 py-1.5 text-xs bg-zinc-800 hover:bg-violet-800 text-zinc-200 flex items-center justify-between"
|
||||
}, /*#__PURE__*/React.createElement("span", { className: "truncate" }, v.name || v.id), /*#__PURE__*/React.createElement("span", { className: "text-[10px] text-cyan-400 shrink-0 ml-1" }, v.type || "VST")),
|
||||
window.SonicRuntime && window.SonicRuntime.capabilities && window.SonicRuntime.capabilities.features && window.SonicRuntime.capabilities.features.carla_local ? /*#__PURE__*/React.createElement("button", {
|
||||
onClick: (e) => { e.stopPropagation(); window.SonicAPI.openInCarla(v.id).then(function (r) { if (r && r.success) { showToast('Đã mở Carla: ' + (v.name || v.id), 'success'); } }).catch(function (err) { showToast('Lỗi mở Carla: ' + (err.message || err), 'error'); }); },
|
||||
className: "shrink-0 px-2 text-xs bg-zinc-800 hover:bg-teal-700 text-teal-300 border-l border-zinc-700",
|
||||
title: "Mở trong Carla (native GUI)"
|
||||
}, "\uD83C\uDF9B") : null
|
||||
)),
|
||||
filteredInstruments.vst.length > 0 && /*#__PURE__*/React.createElement("div", { className: "text-[10px] text-zinc-500 px-3 py-1 mt-1 uppercase font-bold" }, "VST Presets (.vstpreset)"),
|
||||
(window.SonicRuntime && window.SonicRuntime.presets || []).map((p, i) => /*#__PURE__*/React.createElement("button", {
|
||||
key: "psd_" + i,
|
||||
onClick: () => setTrackPreset(instrumentDropdownTrackId, p.id),
|
||||
className: "w-full text-left px-3 py-1 text-xs bg-zinc-800 hover:bg-emerald-800 text-zinc-300 flex items-center justify-between"
|
||||
}, /*#__PURE__*/React.createElement("span", { className: "truncate" }, p.name || p.id), /*#__PURE__*/React.createElement("span", { className: "text-[10px] text-emerald-400 shrink-0 ml-1" }, "PRESET"))),
|
||||
/*#__PURE__*/React.createElement("button", {
|
||||
onClick: () => {
|
||||
const inp = document.createElement('input');
|
||||
inp.type = 'file';
|
||||
inp.accept = '.vstpreset,.fxp,.fxb,.dspreset';
|
||||
inp.onchange = () => {
|
||||
const f = inp.files && inp.files[0];
|
||||
if (!f) return;
|
||||
window.SonicAPI.uploadPreset(f).then(function (r) {
|
||||
if (r && r.success) {
|
||||
if (window.SonicRuntime) window.SonicRuntime.refreshPresets();
|
||||
showToast('Đã upload preset: ' + (r.original_name || r.name || ''), 'success');
|
||||
} else { showToast('Upload preset thất bại', 'error'); }
|
||||
}).catch(function (err) { showToast('Upload lỗi: ' + (err.message || err), 'error'); });
|
||||
};
|
||||
inp.click();
|
||||
},
|
||||
className: "w-full text-left px-3 py-1 text-xs bg-zinc-800 hover:bg-teal-800 text-teal-300 border-t border-zinc-700",
|
||||
title: "Upload preset .vstpreset xuất từ Carla (native GUI)"
|
||||
}, "\u2B06 Upload preset (t\u1EEB Carla...)"),
|
||||
(!filteredInstruments.soundfonts.length && !filteredInstruments.vst.length) && /*#__PURE__*/React.createElement("p", { className: "text-xs text-zinc-500 py-4 text-center" }, "Kh\u00f4ng t\u00ecm th\u1ea5y nh\u1ea1c c\u1ee5 n\u00e0o")
|
||||
)));
|
||||
};
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -17,6 +17,8 @@ window.API_BASE_URL = window.API_BASE_URL || window.location.origin;
|
||||
async function apiRequest(endpoint, options = {}) {
|
||||
const url = `${window.API_BASE_URL}${endpoint}`;
|
||||
const headers = { ...getAuthHeaders(), ...options.headers };
|
||||
// FormData: browser tự đặt Content-Type kèm boundary — không được ép JSON
|
||||
if (options.body instanceof FormData) delete headers['Content-Type'];
|
||||
const response = await fetch(url, { ...options, headers });
|
||||
|
||||
if (response.status === 401) {
|
||||
@@ -67,6 +69,24 @@ 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' }),
|
||||
// Runtime capabilities — frontend gọi lúc boot để biết môi trường
|
||||
// (desktop Windows / docker headless) và bật/tắt tính năng
|
||||
getCapabilities: () => apiRequest('/api/v1/system/capabilities', { method: 'GET' }),
|
||||
// Định vị Carla.exe (bản portable zip không cài đặt/PATH) — lưu config
|
||||
setCarlaPath: (path) => apiRequest('/api/v1/system/carla-path', { method: 'POST', body: JSON.stringify({ carla_path: path }) }),
|
||||
// Mở native GUI VSTi trong Carla (chỉ khi runtime=desktop + có Carla local)
|
||||
openInCarla: (pluginName, pluginPath) => apiRequest('/api/v1/plugins/open-in-carla', { method: 'POST', body: JSON.stringify({ plugin_name: pluginName, plugin_path: pluginPath }) }),
|
||||
// Quick-render preview VSTi (âm thật = âm export, cùng code path)
|
||||
previewInstrument: (payload) => apiRequest('/api/v1/plugins/preview', { method: 'POST', body: JSON.stringify(payload) }),
|
||||
// Thư viện preset VST3 (.vstpreset) — cầu nối Carla → pedalboard
|
||||
listPresets: () => apiRequest('/api/v1/presets', { method: 'GET' }),
|
||||
uploadPreset: (file, pluginHint) => {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
if (pluginHint) fd.append('plugin_hint', pluginHint);
|
||||
return apiRequest('/api/v1/presets/upload', { method: 'POST', body: fd });
|
||||
},
|
||||
deletePreset: (presetId) => apiRequest(`/api/v1/presets/${presetId}`, { method: 'DELETE' }),
|
||||
// Native folder picker (Explorer qua Tauri bridge / PowerShell) —
|
||||
// user yêu cầu dùng window explorer, không nhập tay
|
||||
pickPluginDir: () => apiRequest('/api/v1/plugins/pick-dir', { method: 'POST' }),
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
// SonicForge Runtime service — phát hiện môi trường chạy (desktop Windows /
|
||||
// docker headless) qua /api/v1/system/capabilities, bật/tắt tính năng theo đó.
|
||||
// - data-runtime trên <html>: "desktop" | "headless"
|
||||
// - data-carla="1": có Carla local (hiện nút "Mở trong Carla")
|
||||
// - Phần tử có thuộc tính data-carla-only sẽ bị ẩn khi không có Carla local.
|
||||
// - Thư viện preset (.vstpreset) cache trong SonicRuntime.presets — dùng cho
|
||||
// dropdown gán preset vào track (Carla → pedalboard bridge).
|
||||
window.SonicRuntime = window.SonicRuntime || { loaded: false, capabilities: null, presets: null };
|
||||
|
||||
(function () {
|
||||
function getHeaders() {
|
||||
const token = localStorage.getItem('sonic_token') || '';
|
||||
return token ? { 'Authorization': 'Bearer ' + token } : {};
|
||||
}
|
||||
|
||||
function load() {
|
||||
return fetch(window.API_BASE_URL + '/api/v1/system/capabilities')
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (data) {
|
||||
var c = data && data.success ? data : { features: {} };
|
||||
window.SonicRuntime.capabilities = c;
|
||||
window.SonicRuntime.loaded = true;
|
||||
var html = document.documentElement;
|
||||
html.dataset.runtime = c.runtime || 'unknown';
|
||||
html.dataset.platform = c.platform || '';
|
||||
html.dataset.carla = (c.features && c.features.carla_local) ? '1' : '0';
|
||||
if (c.features && c.features.carla_local === false) {
|
||||
document.querySelectorAll('[data-carla-only]').forEach(function (el) {
|
||||
el.style.display = 'none';
|
||||
});
|
||||
}
|
||||
// Cache sẵn danh sách preset (static, ít thay đổi)
|
||||
listPresets().catch(function () {});
|
||||
return c;
|
||||
})
|
||||
.catch(function () { return null; });
|
||||
}
|
||||
|
||||
function listPresets() {
|
||||
if (window.SonicRuntime.presets) return Promise.resolve(window.SonicRuntime.presets);
|
||||
return fetch(window.API_BASE_URL + '/api/v1/presets', { headers: getHeaders() })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (d) {
|
||||
window.SonicRuntime.presets = (d && d.presets) || [];
|
||||
return window.SonicRuntime.presets;
|
||||
})
|
||||
.catch(function () { return []; });
|
||||
}
|
||||
|
||||
window.SonicRuntime.load = load;
|
||||
window.SonicRuntime.listPresets = listPresets;
|
||||
window.SonicRuntime.refreshPresets = function () {
|
||||
window.SonicRuntime.presets = null;
|
||||
return window.SonicRuntime.listPresets();
|
||||
};
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', load);
|
||||
} else {
|
||||
load();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user