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:
@@ -15,3 +15,11 @@ DEFAULT_ADMIN_PASSWORD=thay-mat-khau-admin
|
||||
|
||||
# Storage (đường dẫn trong container)
|
||||
STORAGE_DIR=/app/app/storage
|
||||
|
||||
# ── Plugin directories ──
|
||||
# Đường dẫn HOST tới thư mục chứa VST / SoundFont / Pianobook — dùng trong
|
||||
# docker-compose.yml để mount vào container (đổi theo máy chạy Docker).
|
||||
# Mặc định: /home/locpham/daw_assets/...
|
||||
VST_DIR=/home/locpham/daw_assets/vst3
|
||||
SOUNDFONT_DIR=/home/locpham/daw_assets/soundfonts
|
||||
PIANOBK_DIR=/home/locpham/daw_assets/pianobook
|
||||
|
||||
+89
-4
@@ -16,7 +16,89 @@ router = APIRouter()
|
||||
UPLOAD_SF_DIR = os.path.join(settings.STORAGE_DIR, "soundfonts")
|
||||
os.makedirs(UPLOAD_SF_DIR, exist_ok=True)
|
||||
|
||||
SYSTEM_SF_DIR = "/opt/daw_engine/soundfonts"
|
||||
SYSTEM_SF_DIR = settings.SOUNDFONT_DIR
|
||||
SYSTEM_VST_DIR = settings.VST_DIR
|
||||
|
||||
# User dirs (Windows/macOS — người dùng chọn qua folder picker trong
|
||||
# Plugins Manager). File global (không per-user): desktop app 1 user.
|
||||
PLUGIN_DIRS_FILE = os.path.join(settings.STORAGE_DIR, "plugin_dirs.json")
|
||||
|
||||
def _load_plugin_dirs() -> dict:
|
||||
if os.path.exists(PLUGIN_DIRS_FILE):
|
||||
try:
|
||||
with open(PLUGIN_DIRS_FILE, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
except Exception:
|
||||
pass
|
||||
return {}
|
||||
|
||||
def _save_plugin_dirs(dirs: dict):
|
||||
os.makedirs(settings.STORAGE_DIR, exist_ok=True)
|
||||
with open(PLUGIN_DIRS_FILE, "w", encoding="utf-8") as f:
|
||||
json.dump(dirs, f, indent=2)
|
||||
|
||||
def _effective_dirs() -> dict:
|
||||
"""Env/.env (Docker) là base; user dirs (file) override nếu khai báo."""
|
||||
user = _load_plugin_dirs()
|
||||
return {
|
||||
"vst_dir": user.get("vst_dir") or settings.VST_DIR,
|
||||
"soundfont_dir": user.get("soundfont_dir") or settings.SOUNDFONT_DIR,
|
||||
"vst_dir_user_set": bool(user.get("vst_dir")),
|
||||
"soundfont_dir_user_set": bool(user.get("soundfont_dir")),
|
||||
}
|
||||
|
||||
class DirsRequest(BaseModel):
|
||||
vst_dir: Optional[str] = None
|
||||
soundfont_dir: Optional[str] = None
|
||||
|
||||
@router.get("/dirs")
|
||||
async def get_plugin_dirs():
|
||||
return {"success": True, **(_effective_dirs())}
|
||||
|
||||
@router.post("/dirs")
|
||||
async def save_plugin_dirs(req: DirsRequest, current_user: dict = Depends(get_current_user)):
|
||||
enforce_password_changed(current_user)
|
||||
user = _load_plugin_dirs()
|
||||
if req.vst_dir is not None:
|
||||
user["vst_dir"] = req.vst_dir.strip()
|
||||
if req.soundfont_dir is not None:
|
||||
user["soundfont_dir"] = req.soundfont_dir.strip()
|
||||
_save_plugin_dirs(user)
|
||||
return {"success": True, **(_effective_dirs())}
|
||||
|
||||
@router.post("/scan")
|
||||
async def scan_plugin_dirs(background_tasks: BackgroundTasks = None,
|
||||
current_user: dict = Depends(get_current_user)):
|
||||
"""Scan các dir hiệu lực (env + user override): cập nhật catalog
|
||||
soundfont (inspector/scanner) + liệt kê VST. Trả về số lượng tìm thấy."""
|
||||
enforce_password_changed(current_user)
|
||||
dirs = _effective_dirs()
|
||||
vst_dir = dirs["vst_dir"]
|
||||
sf_dir = dirs["soundfont_dir"]
|
||||
# SoundFont: quét + inspect vào catalog (scan_once dùng dir hiệu lực)
|
||||
scanner = SoundFontAutoScanner(system_sf_dir=sf_dir, upload_sf_dir=UPLOAD_SF_DIR)
|
||||
if background_tasks:
|
||||
background_tasks.add_task(scanner.scan_once)
|
||||
else:
|
||||
scanner.scan_once()
|
||||
catalog = scanner.get_catalog()
|
||||
# VST: liệt kê thư mục (walk .vst3/.so)
|
||||
vst_found = []
|
||||
if os.path.isdir(vst_dir):
|
||||
for root, _dirs, files in os.walk(vst_dir):
|
||||
for f in files:
|
||||
if f.endswith(".vst3") or f.endswith(".so") or f.endswith(".dll"):
|
||||
vst_found.append({"name": os.path.splitext(f)[0],
|
||||
"path": os.path.join(root, f),
|
||||
"type": "VST3" if f.endswith(".vst3") else "VST2"})
|
||||
return {
|
||||
"success": True,
|
||||
"vst_dir": vst_dir,
|
||||
"soundfont_dir": sf_dir,
|
||||
"vst_found": vst_found,
|
||||
"vst_count": len(vst_found),
|
||||
"soundfont_count": len(catalog),
|
||||
}
|
||||
|
||||
_inspector = None
|
||||
_scanner = None
|
||||
@@ -24,20 +106,23 @@ _scanner = None
|
||||
def get_inspector():
|
||||
global _inspector
|
||||
if _inspector is None:
|
||||
_inspector = SoundFontInspector(system_sf_dir=SYSTEM_SF_DIR, upload_sf_dir=UPLOAD_SF_DIR)
|
||||
d = _effective_dirs()
|
||||
_inspector = SoundFontInspector(system_sf_dir=d["soundfont_dir"], upload_sf_dir=UPLOAD_SF_DIR)
|
||||
return _inspector
|
||||
|
||||
def get_scanner():
|
||||
global _scanner
|
||||
if _scanner is None:
|
||||
_scanner = SoundFontAutoScanner(system_sf_dir=SYSTEM_SF_DIR, upload_sf_dir=UPLOAD_SF_DIR)
|
||||
d = _effective_dirs()
|
||||
_scanner = SoundFontAutoScanner(system_sf_dir=d["soundfont_dir"], upload_sf_dir=UPLOAD_SF_DIR)
|
||||
_scanner.scan_once()
|
||||
return _scanner
|
||||
|
||||
|
||||
@router.get("/available")
|
||||
async def list_plugins(current_user: dict = Depends(get_current_user)):
|
||||
pm = PluginManager(upload_sf_dir=UPLOAD_SF_DIR)
|
||||
d = _effective_dirs()
|
||||
pm = PluginManager(vst_dir=d["vst_dir"], sf_dir=d["soundfont_dir"], upload_sf_dir=UPLOAD_SF_DIR)
|
||||
return pm.list_available()
|
||||
|
||||
|
||||
|
||||
@@ -29,4 +29,9 @@ class Settings:
|
||||
UPLOADS_DIR: str = os.path.join(STORAGE_DIR, "uploads")
|
||||
PROCESSED_DIR: str = os.path.join(STORAGE_DIR, "processed")
|
||||
|
||||
# Plugin dirs — người dùng khai báo qua .env / docker-compose (Docker)
|
||||
# hoặc qua Plugins Manager (Windows/macOS, lưu theo user).
|
||||
VST_DIR: str = os.getenv("VST_DIR", "/opt/daw_engine/vst3")
|
||||
SOUNDFONT_DIR: str = os.getenv("SOUNDFONT_DIR", "/opt/daw_engine/soundfonts")
|
||||
|
||||
settings = Settings()
|
||||
|
||||
@@ -8,7 +8,7 @@ from app.config import settings
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
TRACK_FILE = os.path.join(settings.STORAGE_DIR, "sf_scan_state.json")
|
||||
SYSTEM_SF_DIR = "/opt/daw_engine/soundfonts"
|
||||
SYSTEM_SF_DIR = settings.SOUNDFONT_DIR
|
||||
UPLOAD_SF_DIR = os.path.join(settings.STORAGE_DIR, "soundfonts")
|
||||
|
||||
|
||||
|
||||
+11
-5
@@ -99,8 +99,13 @@ _PLUGIN_MANAGER_INSTANCE = None
|
||||
_PLUGIN_MANAGER_ARGS = None
|
||||
_SF_INSTRUMENTS_CACHE = {} # sf_id → list[presets]
|
||||
|
||||
def get_plugin_manager(vst_dir="/opt/daw_engine/vst3", sf_dir="/opt/daw_engine/soundfonts", upload_sf_dir=None) -> "PluginManager":
|
||||
"""Singleton: reuse PluginManager when args match, else create new."""
|
||||
def get_plugin_manager(vst_dir=None, sf_dir=None, upload_sf_dir=None) -> "PluginManager":
|
||||
"""Singleton: reuse PluginManager when args match, else create new.
|
||||
Default dirs từ settings (env/.env/docker-compose hoặc user override)."""
|
||||
from app.config import settings as _st
|
||||
vst_dir = vst_dir or _st.VST_DIR
|
||||
sf_dir = sf_dir or _st.SOUNDFONT_DIR
|
||||
upload_sf_dir = upload_sf_dir or _st.STORAGE_DIR + "/soundfonts"
|
||||
global _PLUGIN_MANAGER_INSTANCE, _PLUGIN_MANAGER_ARGS
|
||||
args = (vst_dir, sf_dir, upload_sf_dir)
|
||||
if _PLUGIN_MANAGER_INSTANCE is not None and _PLUGIN_MANAGER_ARGS == args:
|
||||
@@ -155,9 +160,10 @@ def release_soundfont(path: str):
|
||||
_FLUID_CACHE[path] = (fl, ref - 1)
|
||||
|
||||
class PluginManager:
|
||||
def __init__(self, vst_dir="/opt/daw_engine/vst3", sf_dir="/opt/daw_engine/soundfonts", upload_sf_dir=None):
|
||||
self.vst_dir = vst_dir
|
||||
self.sf_dir = sf_dir
|
||||
def __init__(self, vst_dir=None, sf_dir=None, upload_sf_dir=None):
|
||||
from app.config import settings as _st
|
||||
self.vst_dir = vst_dir or _st.VST_DIR
|
||||
self.sf_dir = sf_dir or _st.SOUNDFONT_DIR
|
||||
self.upload_sf_dir = upload_sf_dir
|
||||
self._sf_scan_cache = None # cache for _scan_soundfonts()
|
||||
|
||||
|
||||
+168
-3
@@ -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ư mục → 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 chọn/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ữ liệu
|
||||
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 thời gian vùng chọn/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 chọn — 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
@@ -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' }),
|
||||
|
||||
+15
-9
@@ -10,13 +10,15 @@ services:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- .:/app
|
||||
- /home/locpham/daw_assets/vst3:/opt/daw_engine/vst3
|
||||
- /home/locpham/daw_assets/soundfonts:/opt/daw_engine/soundfonts
|
||||
- /home/locpham/daw_assets/pianobook:/opt/daw_engine/samples/pianobook
|
||||
- ${VST_DIR:-/home/locpham/daw_assets/vst3}:/opt/daw_engine/vst3
|
||||
- ${SOUNDFONT_DIR:-/home/locpham/daw_assets/soundfonts}:/opt/daw_engine/soundfonts
|
||||
- ${PIANOBK_DIR:-/home/locpham/daw_assets/pianobook}:/opt/daw_engine/samples/pianobook
|
||||
environment:
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||
- VST_DIR=/opt/daw_engine/vst3
|
||||
- SOUNDFONT_DIR=/opt/daw_engine/soundfonts
|
||||
depends_on:
|
||||
- redis
|
||||
|
||||
@@ -25,13 +27,15 @@ services:
|
||||
command: celery -A app.tasks.worker.celery_app worker --loglevel=info
|
||||
volumes:
|
||||
- .:/app
|
||||
- /home/locpham/daw_assets/vst3:/opt/daw_engine/vst3
|
||||
- /home/locpham/daw_assets/soundfonts:/opt/daw_engine/soundfonts
|
||||
- /home/locpham/daw_assets/pianobook:/opt/daw_engine/samples/pianobook
|
||||
- ${VST_DIR:-/home/locpham/daw_assets/vst3}:/opt/daw_engine/vst3
|
||||
- ${SOUNDFONT_DIR:-/home/locpham/daw_assets/soundfonts}:/opt/daw_engine/soundfonts
|
||||
- ${PIANOBK_DIR:-/home/locpham/daw_assets/pianobook}:/opt/daw_engine/samples/pianobook
|
||||
environment:
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||
- VST_DIR=/opt/daw_engine/vst3
|
||||
- SOUNDFONT_DIR=/opt/daw_engine/soundfonts
|
||||
depends_on:
|
||||
- redis
|
||||
|
||||
@@ -40,12 +44,14 @@ services:
|
||||
command: celery -A app.tasks.worker.celery_app beat --loglevel=info
|
||||
volumes:
|
||||
- .:/app
|
||||
- /home/locpham/daw_assets/vst3:/opt/daw_engine/vst3
|
||||
- /home/locpham/daw_assets/soundfonts:/opt/daw_engine/soundfonts
|
||||
- /home/locpham/daw_assets/pianobook:/opt/daw_engine/samples/pianobook
|
||||
- ${VST_DIR:-/home/locpham/daw_assets/vst3}:/opt/daw_engine/vst3
|
||||
- ${SOUNDFONT_DIR:-/home/locpham/daw_assets/soundfonts}:/opt/daw_engine/soundfonts
|
||||
- ${PIANOBK_DIR:-/home/locpham/daw_assets/pianobook}:/opt/daw_engine/samples/pianobook
|
||||
environment:
|
||||
- REDIS_URL=redis://redis:6379/0
|
||||
- CELERY_BROKER_URL=redis://redis:6379/0
|
||||
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
||||
- VST_DIR=/opt/daw_engine/vst3
|
||||
- SOUNDFONT_DIR=/opt/daw_engine/soundfonts
|
||||
depends_on:
|
||||
- redis
|
||||
|
||||
@@ -14,6 +14,7 @@ tauri-build = { version = "2", features = [] }
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
|
||||
|
||||
@@ -3,5 +3,5 @@
|
||||
"identifier": "default",
|
||||
"description": "Default capability for the main window",
|
||||
"windows": ["main"],
|
||||
"permissions": ["core:default"]
|
||||
"permissions": ["core:default", "dialog:default"]
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ struct EngineProcess(Mutex<Option<CommandChild>>);
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.setup(|app| {
|
||||
// 1. Spawn sidecar daw_engine.exe (PyInstaller bundle)
|
||||
let sidecar_command = app
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"devUrl": "http://localhost:8000"
|
||||
},
|
||||
"app": {
|
||||
"withGlobalTauri": true,
|
||||
"windows": [
|
||||
{
|
||||
"title": "Sonic Forge DAW - Professional Desktop Studio",
|
||||
|
||||
Reference in New Issue
Block a user