feat: Model Browser — block library with category filter tabs, search, favorites, NAM/IR models

This commit is contained in:
2026-06-12 19:51:19 -04:00
parent f67e6bd8c7
commit 376249172f
3 changed files with 807 additions and 11 deletions
+57 -7
View File
@@ -3,6 +3,7 @@ import BlockChain from "./BlockChain.jsx";
import FootswitchBar from "./FootswitchBar.jsx";
import TunerScreen from "./Tuner.jsx";
import FocusView from "./FocusView.jsx";
import ModelBrowser from "./ModelBrowser.jsx";
// ── API Layer ──────────────────────────────────────────────────────
const API_BASE = "";
@@ -343,13 +344,17 @@ function SelectParam({ def, value, color, onChange }) {
}
// ── Parameter Panel ──────────────────────────────────────────
function ParameterPanel({ block,params,onParamChange,onParamChangeEnd,viewMode,onToggleView,onFocus }) {
function ParameterPanel({ block,params,onParamChange,onParamChangeEnd,viewMode,onToggleView,onFocus,onModelBrowser }) {
const defs=getBlockParams(block.type);const color=getBlockColor(block.type);
const visibleDefs = defs.filter(d => !d.conditional || d.conditional(params));
return (
<div className="param-panel">
<div className="param-panel-header">
<span className="param-panel-title"><span style={{color}}></span> {block.name||block.type||''}</span>
<span className="param-panel-title" style={{cursor:onModelBrowser?'pointer':'default'}}
onClick={()=>onModelBrowser?.(block.id)}>
<span style={{color}}></span> {block.name||block.type||''}
{onModelBrowser&&<span style={{marginLeft:4,fontSize:8,color:T.textDim}}></span>}
</span>
<div style={{display:'flex',gap:4,alignItems:'center'}}>
{onFocus&&<button className="btn" style={{padding:'4px 8px',fontSize:9,fontWeight:600,background:`${T.amber}22`,border:`1px solid ${T.amber}44`,color:T.amber,borderRadius:5,letterSpacing:'.04em',cursor:'pointer'}}
onClick={onFocus}>🔍 Focus</button>}
@@ -450,6 +455,7 @@ export default function App(){
const[globalBypass,setGlobalBypass]=useState(false);
const[loadingPreset,setLoadingPreset]=useState(false);
const[focusBlockId,setFocusBlockId]=useState(null);
const[modelBrowserBlockId,setModelBrowserBlockId]=useState(null);
const[footswitchMode,setFootswitchMode]=useState("stomp");
// ── Poll pedal state from API ──
@@ -604,10 +610,37 @@ export default function App(){
setTimeout(()=>{userChangedBlock.current=false;},3000);
},[focusBlockId]);
const handleOpenFocusModelBrowser=useCallback(()=>{
setView("captures");
setFocusBlockId(null);
},[]);
const handleOpenModelBrowser=useCallback((blockId)=>{
setModelBrowserBlockId(blockId);
if(focusBlockId) setFocusBlockId(null);
},[focusBlockId]);
const handleSelectModel=useCallback(({type, name, modelPath, isBuiltin})=>{
const id=modelBrowserBlockId;
if(!id)return;
userChangedBlock.current=true;
setBlocks(prev=>prev.map(b=>{
if(b.id!==id)return b;
const defs=getBlockParams(type);
const init={};
defs.forEach(p=>{init[p.key]=p.def;});
return {
...b,
type,
name: name || getBlockDisplayName({type}),
params: init,
nam_model_path: (!isBuiltin && modelPath && type==='nam') ? modelPath : undefined,
ir_file_path: (!isBuiltin && modelPath && type==='ir') ? modelPath : undefined,
};
}));
setModelBrowserBlockId(null);
setBlockParams(prev=>{
const next={...prev};
delete next[id];
return next;
});
setTimeout(()=>{userChangedBlock.current=false;},3000);
},[modelBrowserBlockId]);
// ── Render ──
return(
@@ -720,6 +753,7 @@ export default function App(){
onReorder={handleReorder}
onAddBlock={handleAddBlock}
onRemoveBlock={handleRemoveBlock}
onModelBrowser={handleOpenModelBrowser}
/>
</div>
@@ -733,6 +767,7 @@ export default function App(){
onParamChange={(key,value)=>handleParamChange(selectedBlockId,key,value)}
onParamChangeEnd={(key,value)=>handleParamChangeEnd(selectedBlockId,key,value)}
onFocus={()=>setFocusBlockId(selectedBlockId)}
onModelBrowser={handleOpenModelBrowser}
/>
):(
<div style={{flex:1,display:"flex",alignItems:"center",justifyContent:"center",background:T.bg,color:T.textDim,fontSize:11}}>
@@ -774,7 +809,7 @@ export default function App(){
onParamChangeEnd={(key,value)=>handleParamChangeEnd(focusBlockId,key,value)}
onToggleView={()=>setParamView(p=>p==='slider'?'knob':'slider')}
onToggleBypass={handleFocusViewBypass}
onOpenModelBrowser={handleOpenFocusModelBrowser}
onOpenModelBrowser={()=>handleOpenModelBrowser(focusBlockId)}
/>
)}
{/* ── Overlays ── */}
@@ -809,6 +844,21 @@ export default function App(){
/>
)}
{/* ── Model Browser ── */}
{modelBrowserBlockId&&(()=>{
const block=blocks.find(b=>b.id===modelBrowserBlockId);
if(!block)return null;
return (
<ModelBrowser
currentBlockType={block.type}
currentBlockName={block.name}
currentModelPath={block.nam_model_path||block.ir_file_path}
onSelectModel={handleSelectModel}
onClose={()=>setModelBrowserBlockId(null)}
/>
);
})()}
</div></>
);
}