PEDAL-UI: Footswitch modes (stomp/preset) + mode selector in status bar + SVG fx icons from pipedal project
This commit is contained in:
+239
-35
@@ -1,6 +1,8 @@
|
||||
import { useState, useRef, useEffect, useCallback, useMemo } from "react";
|
||||
import BlockChain from "./BlockChain.jsx";
|
||||
import FootswitchBar from "./FootswitchBar.jsx";
|
||||
import TunerScreen from "./Tuner.jsx";
|
||||
import FocusView from "./FocusView.jsx";
|
||||
|
||||
// ── API Layer ──────────────────────────────────────────────────────
|
||||
const API_BASE = "";
|
||||
@@ -23,12 +25,14 @@ const API = {
|
||||
listModels: () => api("GET", "/api/models"),
|
||||
loadModel: (path) => api("POST", "/api/models/load", { path }),
|
||||
listPresets: () => api("GET", "/api/presets"),
|
||||
loadPreset: (bank, program) => api("GET", "/api/presets/{bank}/{program}".replace("{bank}",bank).replace("{program}",program)),
|
||||
getPreset: (bank, program) => api("GET", `/api/presets/${bank}/${program}`),
|
||||
activatePreset:(bank, program) => api("POST", `/api/presets/${bank}/${program}/activate`),
|
||||
savePreset: (bank, program, name, tags) => api("PUT", "/api/presets", { bank, program, name, tags }),
|
||||
searchModels: (q) => api("GET", `/api/tonehub/search?q=${encodeURIComponent(q)}`),
|
||||
searchIrs: (q) => api("GET", `/api/tonehub/search/irs?q=${encodeURIComponent(q)}`),
|
||||
installModel: (url, name) => api("POST", "/api/models/tonedownload/install", { download_url: url, name }),
|
||||
installIr: (url, name) => api("POST", "/api/irs/tonedownload/install", { download_url: url, name }),
|
||||
tunerToggle: (enabled) => api("POST", "/api/tuner", { enabled }),
|
||||
};
|
||||
|
||||
// ── Design tokens ──────────────────────────────────────────────
|
||||
@@ -49,6 +53,13 @@ const T = {
|
||||
};
|
||||
|
||||
// ── Block parameter definitions ─────────────────────────────────
|
||||
const SPLIT_TYPES = [
|
||||
{ value:'y', label:'Y — Split evenly', pan:true },
|
||||
{ value:'ab', label:'A/B — Route to A or B', pan:false },
|
||||
{ value:'cascade', label:'Cascade — A feeds B', pan:true },
|
||||
{ value:'a_to_b', label:'A→B — Serial extension', pan:false },
|
||||
];
|
||||
|
||||
const BLOCK_PARAMS = {
|
||||
overdrive: [{ key:'drive', label:'Drive', min:0, max:100, def:30 }, { key:'tone', label:'Tone', min:0, max:100, def:50 }, { key:'level', label:'Level', min:0, max:100, def:70 }],
|
||||
distortion: [{ key:'drive', label:'Drive', min:0, max:100, def:50 }, { key:'tone', label:'Tone', min:0, max:100, def:40 }, { key:'level', label:'Level', min:0, max:100, def:60 }],
|
||||
@@ -64,14 +75,36 @@ const BLOCK_PARAMS = {
|
||||
eq: [{ key:'low', label:'Low', min:-12, max:12, def:0 }, { key:'mid', label:'Mid', min:-12, max:12, def:0 }, { key:'high', label:'High', min:-12, max:12, def:0 }, { key:'level', label:'Level', min:0, max:100, def:70 }],
|
||||
cabinet: [{ key:'level', label:'Level', min:0, max:100, def:75 }],
|
||||
ir: [{ key:'level', label:'Level', min:0, max:100, def:75 }],
|
||||
split: [{ key:'splitType', label:'Split Type', type:'select', options:SPLIT_TYPES, def:'y' },
|
||||
{ key:'panA', label:'Pan A', min:0, max:100, def:50, conditional: (p) => p.splitType !== 'ab' },
|
||||
{ key:'panB', label:'Pan B', min:0, max:100, def:50, conditional: (p) => p.splitType !== 'ab' },
|
||||
{ key:'activePath', label:'Active Path', type:'select', options:[{value:'A',label:'A'},{value:'B',label:'B'}], def:'A', conditional: (p) => p.splitType === 'ab' }],
|
||||
merge: [{ key:'blend', label:'Blend', min:0, max:100, def:100 },
|
||||
{ key:'levelA', label:'Level A', min:0, max:100, def:100 },
|
||||
{ key:'panA', label:'Pan A', min:-100, max:100, def:0 },
|
||||
{ key:'levelB', label:'Level B', min:0, max:100, def:100 },
|
||||
{ key:'panB', label:'Pan B', min:-100, max:100, def:0 }],
|
||||
};
|
||||
|
||||
const BLOCK_COLORS = {
|
||||
overdrive: T.amber, distortion: T.red, fuzz: '#C07030',
|
||||
delay: T.blue, echo: T.blue, reverb: '#60B0D0',
|
||||
chorus: T.green, flanger: '#50C080', phaser: '#80C050',
|
||||
compressor: '#D09040', gate: T.textSec, eq: '#7080D0',
|
||||
cabinet: '#D07090', ir: '#D07090',
|
||||
tremolo: T.green, compressor: '#D09040', gate: T.textSec, eq: '#7080D0',
|
||||
cabinet: '#D07090', ir: '#D07090', nam: '#E0A040', boost: T.amber,
|
||||
wah: '#D0A060', volume: T.textSec, tuner: '#80D0A0',
|
||||
split: '#B080C0', merge: '#80B0C0',
|
||||
};
|
||||
|
||||
const BLOCK_DISPLAY_NAMES = {
|
||||
overdrive: "OD-808", distortion: "RAT", fuzz: "Big Muff",
|
||||
delay: "Digital Delay", echo: "Tape Echo", reverb: "Hall Reverb",
|
||||
chorus: "Chorus", flanger: "Flanger", phaser: "Phase 90",
|
||||
tremolo: "Tremolo", compressor: "Compressor", gate: "Noise Gate",
|
||||
eq: "EQ", cabinet: "Cabinet", ir: "IR Loader", nam: "NAM Model",
|
||||
boost: "Clean Boost", wah: "Wah", volume: "Volume", tuner: "Tuner",
|
||||
loop: "FX Loop",
|
||||
split: "Split", merge: "Merge",
|
||||
};
|
||||
|
||||
function getBlockParams(type) {
|
||||
@@ -80,6 +113,20 @@ function getBlockParams(type) {
|
||||
function getBlockColor(type) {
|
||||
return BLOCK_COLORS[(type || '').toLowerCase()] || T.amber;
|
||||
}
|
||||
function getBlockDisplayName(block) {
|
||||
const key = (block.type || '').toLowerCase();
|
||||
// If it has a NAM model path, show the model filename
|
||||
if (block.nam_model_path) {
|
||||
const parts = block.nam_model_path.split('/').pop()?.replace(/\.(nam|wav|aiff?)$/i, '') || '';
|
||||
return parts || 'NAM Model';
|
||||
}
|
||||
// If it has an IR file path, show the filename
|
||||
if (block.ir_file_path) {
|
||||
const parts = block.ir_file_path.split('/').pop()?.replace(/\.(wav|aiff?|ir)$/i, '') || '';
|
||||
return parts || 'IR Loader';
|
||||
}
|
||||
return BLOCK_DISPLAY_NAMES[key] || key.charAt(0).toUpperCase() + key.slice(1);
|
||||
}
|
||||
|
||||
// ── CSS ─────────────────────────────────────────────────────
|
||||
const css = `
|
||||
@@ -264,26 +311,70 @@ function ParamSlider({ label,value=50,onChange,onChangeEnd,min=0,max=100,color=T
|
||||
);
|
||||
}
|
||||
|
||||
// ── Select Param ────────────────────────────────────────────
|
||||
function SelectParam({ def, value, color, onChange }) {
|
||||
return (
|
||||
<div className="param-row">
|
||||
<div className="param-header">
|
||||
<span className="param-label" style={{color}}>{def.label}</span>
|
||||
</div>
|
||||
<div style={{
|
||||
display: "flex", gap: 4, flexWrap: "wrap",
|
||||
background: "#0A0A10", borderRadius: 6, padding: 4,
|
||||
}}>
|
||||
{(def.options || []).map(opt => (
|
||||
<button key={opt.value}
|
||||
onClick={() => onChange?.(opt.value)}
|
||||
style={{
|
||||
flex: 1, padding: "5px 8px", borderRadius: 4, border: "none",
|
||||
fontSize: 10, fontWeight: 600, cursor: "pointer",
|
||||
background: value === opt.value ? color : "transparent",
|
||||
color: value === opt.value ? "#000" : "#8888A0",
|
||||
transition: "all .1s", letterSpacing: ".03em",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ── Parameter Panel ──────────────────────────────────────────
|
||||
function ParameterPanel({ block,params,onParamChange,onParamChangeEnd,viewMode,onToggleView }) {
|
||||
function ParameterPanel({ block,params,onParamChange,onParamChangeEnd,viewMode,onToggleView,onFocus }) {
|
||||
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>
|
||||
<button className="btn-icon" style={{width:26,height:26,fontSize:11,flexShrink:0}}
|
||||
onClick={onToggleView}>{viewMode==='slider'?'◉':'▦'}</button>
|
||||
<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>}
|
||||
<button className="btn-icon" style={{width:26,height:26,fontSize:11,flexShrink:0}}
|
||||
onClick={onToggleView}>{viewMode==='slider'?'◉':'▦'}</button>
|
||||
</div>
|
||||
</div>
|
||||
{viewMode==='slider'?defs.map(p=>(
|
||||
<ParamSlider key={p.key} label={p.label}
|
||||
value={params[p.key]!=null?params[p.key]:p.def} min={p.min} max={p.max} color={color}
|
||||
onChange={v=>onParamChange?.(p.key,v)} onChangeEnd={v=>onParamChangeEnd?.(p.key,v)}/>
|
||||
)):(
|
||||
<div className="param-knobs">{defs.map(p=>(
|
||||
<Knob key={p.key} label={p.label}
|
||||
value={params[p.key]!=null?params[p.key]:p.def} min={p.min} max={p.max} size={48} color={color}
|
||||
{viewMode==='slider'?visibleDefs.map(p=>(
|
||||
p.type === 'select' ? (
|
||||
<SelectParam key={p.key} def={p} value={params[p.key]!=null?params[p.key]:p.def}
|
||||
color={color}
|
||||
onChange={v=>{onParamChange?.(p.key,v);onParamChangeEnd?.(p.key,v);}}/>
|
||||
))}</div>
|
||||
) : (
|
||||
<ParamSlider key={p.key} label={p.label}
|
||||
value={params[p.key]!=null?params[p.key]:p.def} min={p.min} max={p.max} color={color}
|
||||
onChange={v=>onParamChange?.(p.key,v)} onChangeEnd={v=>onParamChangeEnd?.(p.key,v)}/>
|
||||
)
|
||||
)):(
|
||||
visibleDefs.filter(p => p.type !== 'select').length > 0 && (
|
||||
<div className="param-knobs">{visibleDefs.filter(p => p.type !== 'select').map(p=>(
|
||||
<Knob key={p.key} label={p.label}
|
||||
value={params[p.key]!=null?params[p.key]:p.def} min={p.min} max={p.max} size={48} color={color}
|
||||
onChange={v=>{onParamChange?.(p.key,v);onParamChangeEnd?.(p.key,v);}}/>
|
||||
))}</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -329,10 +420,11 @@ function CapturesScreen({onClose}){
|
||||
// ── MOCK DATA (used when pedal not connected) ────────────────
|
||||
const MOCK_BLOCKS=[
|
||||
{id:"gate",type:"gate",name:"Noise Gate",bypassed:false,params:{threshold:40,release:25,depth:70,mix:100}},
|
||||
{id:"drive",type:"overdrive",name:"OD-808",bypassed:false,params:{drive:65,tone:55,level:70}},
|
||||
{id:"dist",type:"distortion",name:"RAT",bypassed:true,params:{drive:75,filter:50,level:65}},
|
||||
{id:"mod",type:"phaser",name:"Phase 90",bypassed:false,params:{rate:35,depth:60,feedback:40,mix:50}},
|
||||
{id:"delay",type:"delay",name:"Digital Delay",bypassed:false,params:{time:45,feedback:25,mix:40}},
|
||||
{id:"split",type:"split",name:"Split",bypassed:false,params:{splitType:"y",panA:50,panB:50,activePath:"A"}},
|
||||
{id:"drive_a",type:"overdrive",name:"OD-808",path:"A",bypassed:false,params:{drive:65,tone:55,level:70}},
|
||||
{id:"delay_a",type:"delay",name:"Digital Delay",path:"A",bypassed:false,params:{time:45,feedback:25,mix:40}},
|
||||
{id:"fuzz_b",type:"fuzz",name:"Big Muff",path:"B",bypassed:false,params:{fuzz:75,tone:40,level:65}},
|
||||
{id:"merge",type:"merge",name:"Merge",bypassed:false,params:{blend:100,levelA:100,panA:0,levelB:100,panB:0}},
|
||||
{id:"reverb",type:"reverb",name:"Hall Reverb",bypassed:false,params:{decay:60,mix:35,tone:50}},
|
||||
];
|
||||
|
||||
@@ -357,19 +449,25 @@ export default function App(){
|
||||
const[presets,setPresets]=useState(MOCK_PRESETS);
|
||||
const[globalBypass,setGlobalBypass]=useState(false);
|
||||
const[loadingPreset,setLoadingPreset]=useState(false);
|
||||
const[focusBlockId,setFocusBlockId]=useState(null);
|
||||
const[footswitchMode,setFootswitchMode]=useState("stomp");
|
||||
|
||||
// ── Poll pedal state from API ──
|
||||
const userChangedPreset = useRef(false);
|
||||
const userChangedBlock = useRef(false);
|
||||
|
||||
const loadState=useCallback(async()=>{
|
||||
try{
|
||||
const s=await API.getState();
|
||||
setState(s);
|
||||
if(s.input_level!=null)setVuLevel(s.input_level);
|
||||
// If API has preset info, use it
|
||||
if(s.current_preset?.name){
|
||||
if(s.current_preset?.name&&!userChangedPreset.current){
|
||||
setCurrentPreset(s.current_preset.program!=null?s.current_preset.program+1:currentPreset);
|
||||
}
|
||||
// If API has blocks, use them
|
||||
if(s.blocks&&s.blocks.length>0){setBlocks(s.blocks);}
|
||||
// Only update blocks from API if user didn't just toggle/change them
|
||||
if(s.blocks&&s.blocks.length>0&&!userChangedBlock.current){
|
||||
setBlocks(s.blocks);
|
||||
}
|
||||
}catch{}
|
||||
},[currentPreset]);
|
||||
|
||||
@@ -391,14 +489,49 @@ export default function App(){
|
||||
|
||||
// ── Change preset ──
|
||||
const handleLoadPreset=useCallback(async(bank,program)=>{
|
||||
userChangedPreset.current = true;
|
||||
setLoadingPreset(true);
|
||||
try{
|
||||
const p=await API.loadPreset(bank,program);
|
||||
if(p?.chain)setBlocks(p.chain.map((b,i)=>({...b,id:b.id||(`block_${i}`),bypassed:!b.enabled,params:b.params||{}})));
|
||||
setCurrentPreset(program);
|
||||
// Activate on the backend first
|
||||
await API.activatePreset(bank,program);
|
||||
// Short delay for backend to apply
|
||||
await new Promise(r => setTimeout(r, 300));
|
||||
// Get updated state
|
||||
const s=await API.getState();
|
||||
setState(s);
|
||||
if(s.current_preset?.name){
|
||||
setCurrentPreset(s.current_preset.program!=null?s.current_preset.program+1:program+1);
|
||||
}
|
||||
// Get preset chain data and build blocks
|
||||
try{
|
||||
const p=await API.getPreset(bank,program);
|
||||
if(p?.chain&&p.chain.length>0){
|
||||
const newBlocks=p.chain.map((b,i)=>{
|
||||
const raw = { type: b.fx_type || "fx",
|
||||
enabled: b.enabled !== false, params: b.params || {},
|
||||
nam_model_path: b.nam_model_path, ir_file_path: b.ir_file_path,
|
||||
};
|
||||
return {
|
||||
id: b.id || `block_${b.fx_type || 'fx'}_${i}`,
|
||||
type: raw.type,
|
||||
name: getBlockDisplayName(raw),
|
||||
bypassed: !b.enabled,
|
||||
enabled: raw.enabled,
|
||||
params: raw.params,
|
||||
nam_model_path: raw.nam_model_path,
|
||||
ir_file_path: raw.ir_file_path,
|
||||
};
|
||||
});
|
||||
setBlocks(newBlocks);
|
||||
userChangedBlock.current = true;
|
||||
}
|
||||
}catch(e){console.warn("Could not fetch chain:",e);}
|
||||
setSelectedBlockId(null);
|
||||
}catch(e){console.warn("Preset load failed:",e);}
|
||||
}catch(e){console.warn("Preset activate failed:",e);}
|
||||
setLoadingPreset(false);
|
||||
// Re-enable polling sync after 5s
|
||||
setTimeout(() => { userChangedPreset.current = false; }, 5000);
|
||||
setTimeout(() => { userChangedBlock.current = false; }, 5000);
|
||||
},[]);
|
||||
|
||||
const handlePresetClick=useCallback(async(num)=>{
|
||||
@@ -421,12 +554,12 @@ export default function App(){
|
||||
},[selectedBlockId,selectedBlock,blockParams]);
|
||||
|
||||
const handleSelectBlock=useCallback((id)=>{setSelectedBlockId(prev=>prev===id?null:id);},[]);
|
||||
const handleToggleBlock=useCallback((id)=>{setBlocks(prev=>prev.map(b=>b.id===id?{...b,bypassed:!b.bypassed}:b));API.toggleBlock(id,false).catch(()=>{});},[]);
|
||||
const handleToggleBlock=useCallback((id)=>{userChangedBlock.current=true;setBlocks(prev=>prev.map(b=>b.id===id?{...b,bypassed:!b.bypassed}:b));API.toggleBlock(id,false).catch(()=>{});setTimeout(()=>{userChangedBlock.current=false;},3000);},[]);
|
||||
const handleParamChange=useCallback((id,key,value)=>{setBlockParams(prev=>({...prev,[id]:{...(prev[id]||{}),[key]:value}}));},[]);
|
||||
const handleParamChangeEnd=useCallback((id,key,value)=>{API.updateBlock(id,{[key]:value}).catch(()=>{});},[]);
|
||||
const handleReorder=useCallback((fromIdx,toIdx)=>{setBlocks(prev=>{const arr=[...prev];const[m]=arr.splice(fromIdx,1);arr.splice(toIdx,0,m);return arr;});},[]);
|
||||
const handleAddBlock=useCallback(()=>{const id="block_"+Date.now();setBlocks(prev=>[...prev,{id,type:"overdrive",name:"New Drive",bypassed:false,params:{drive:30,tone:50,level:70}}]);},[]);
|
||||
const handleRemoveBlock=useCallback((id)=>{setBlocks(prev=>prev.filter(b=>b.id!==id));if(selectedBlockId===id)setSelectedBlockId(null);},[selectedBlockId]);
|
||||
const handleAddBlock=useCallback(()=>{userChangedBlock.current=true;const id="block_"+Date.now();setBlocks(prev=>[...prev,{id,type:"overdrive",name:"New Drive",bypassed:false,params:{drive:30,tone:50,level:70}}]);setTimeout(()=>{userChangedBlock.current=false;},3000);},[]);
|
||||
const handleRemoveBlock=useCallback((id)=>{userChangedBlock.current=true;setBlocks(prev=>prev.filter(b=>b.id!==id));if(selectedBlockId===id)setSelectedBlockId(null);setTimeout(()=>{userChangedBlock.current=false;},3000);},[selectedBlockId]);
|
||||
|
||||
// ── Bank navigation ──
|
||||
const currentBankPresets=useMemo(()=>presets.filter(p=>p.bank===currentBank),[presets,currentBank]);
|
||||
@@ -435,23 +568,47 @@ export default function App(){
|
||||
const handleBankUp=useCallback(()=>{
|
||||
const next=Math.min(currentBank+1,8);
|
||||
setCurrentBank(next);
|
||||
// Load first preset of new bank
|
||||
const bp=presets.filter(p=>p.bank===next);
|
||||
if(bp.length>0)handlePresetClick(bp[0].num);
|
||||
},[currentBank,presets,handlePresetClick]);
|
||||
if(bp.length>0)handleLoadPreset(bp[0].bank,bp[0].num-1);
|
||||
},[currentBank,presets,handleLoadPreset]);
|
||||
|
||||
const handleBankDown=useCallback(()=>{
|
||||
const next=Math.max(currentBank-1,1);
|
||||
setCurrentBank(next);
|
||||
const bp=presets.filter(p=>p.bank===next);
|
||||
if(bp.length>0)handlePresetClick(bp[0].num);
|
||||
},[currentBank,presets,handlePresetClick]);
|
||||
if(bp.length>0)handleLoadPreset(bp[0].bank,bp[0].num-1);
|
||||
},[currentBank,presets,handleLoadPreset]);
|
||||
|
||||
const handleGlobalBypass=useCallback(()=>{
|
||||
setGlobalBypass(p=>!p);
|
||||
API.bypassToggle().catch(()=>{});
|
||||
},[]);
|
||||
|
||||
// ── Tuner toggle ──
|
||||
const handleTunerToggle=useCallback(()=>{
|
||||
const newState = !state.tuner_enabled;
|
||||
setState(prev=>({...prev, tuner_enabled: newState}));
|
||||
API.tunerToggle(newState).catch(()=>{});
|
||||
if (newState) {
|
||||
setView("main"); // Return to main view when enabling tuner (or keep current)
|
||||
}
|
||||
},[state.tuner_enabled]);
|
||||
|
||||
// ── Focus View handlers ──
|
||||
const handleFocusViewBypass=useCallback(()=>{
|
||||
userChangedBlock.current=true;
|
||||
const id=focusBlockId;
|
||||
if(!id)return;
|
||||
setBlocks(prev=>prev.map(b=>b.id===id?{...b,bypassed:!b.bypassed}:b));
|
||||
API.toggleBlock(id,false).catch(()=>{});
|
||||
setTimeout(()=>{userChangedBlock.current=false;},3000);
|
||||
},[focusBlockId]);
|
||||
|
||||
const handleOpenFocusModelBrowser=useCallback(()=>{
|
||||
setView("captures");
|
||||
setFocusBlockId(null);
|
||||
},[]);
|
||||
|
||||
// ── Render ──
|
||||
return(
|
||||
<><style>{css}</style>
|
||||
@@ -483,6 +640,21 @@ export default function App(){
|
||||
<div style={{width:1,height:12,background:T.border}}/>
|
||||
<button className="btn-icon" style={{width:24,height:24,fontSize:10}}onClick={()=>setView("captures")}title="Downloads">📁</button>
|
||||
<button className="btn-icon" style={{width:24,height:24,fontSize:10}}onClick={()=>setView("presets")}title="Presets">⭐</button>
|
||||
<div style={{display:"flex",gap:2,alignItems:"center",background:T.surface,borderRadius:4,border:`1px solid ${T.border}`,padding:2,height:24}}>
|
||||
{["stomp","preset"].map(m=>(<button key={m} onClick={()=>setFootswitchMode(m)} style={{
|
||||
padding:"1px 5px",borderRadius:3,border:"none",height:20,
|
||||
fontSize:8,fontWeight:700,letterSpacing:".04em",textTransform:"uppercase",
|
||||
background:footswitchMode===m?T.amber:"transparent",
|
||||
color:footswitchMode===m?"#000":T.textDim,
|
||||
cursor:"pointer",lineHeight:1.5,minWidth:34,display:"flex",alignItems:"center",justifyContent:"center",
|
||||
}}>{m==="stomp"?"SM":"PR"}</button>))}
|
||||
</div>
|
||||
<button className="btn-icon" style={{width:24,height:24,fontSize:10,
|
||||
background:state.tuner_enabled?'rgba(128,208,160,.15)':T.surface,
|
||||
borderColor:state.tuner_enabled?T.green:T.border,
|
||||
color:state.tuner_enabled?T.green:T.textPrimary}}
|
||||
onClick={handleTunerToggle} title={state.tuner_enabled?"Exit Tuner":"Tuner"}>♪</button>
|
||||
<button className="btn-icon" style={{width:24,height:24,fontSize:10,background:'rgba(232,160,48,.12)',borderColor:T.amber,color:T.amber}}onClick={()=>{if(!document.fullscreenElement){document.documentElement.requestFullscreen()}else{document.exitFullscreen()}}}title="Fullscreen">⛶</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -551,6 +723,7 @@ export default function App(){
|
||||
onToggleView={()=>setParamView(p=>p==='slider'?'knob':'slider')}
|
||||
onParamChange={(key,value)=>handleParamChange(selectedBlockId,key,value)}
|
||||
onParamChangeEnd={(key,value)=>handleParamChangeEnd(selectedBlockId,key,value)}
|
||||
onFocus={()=>setFocusBlockId(selectedBlockId)}
|
||||
/>
|
||||
):(
|
||||
<div style={{flex:1,display:"flex",alignItems:"center",justifyContent:"center",background:T.bg,color:T.textDim,fontSize:11}}>
|
||||
@@ -562,19 +735,37 @@ export default function App(){
|
||||
|
||||
{/* ══════ FOOTSWITCH BAR ══════ */}
|
||||
<FootswitchBar
|
||||
mode={footswitchMode}
|
||||
blocks={blocks}
|
||||
selectedBlockId={selectedBlockId}
|
||||
currentBank={bankLetter}
|
||||
currentPreset={currentPreset}
|
||||
presets={currentBankPresets}
|
||||
onSelectBlock={handleSelectBlock}
|
||||
onToggleBlock={handleToggleBlock}
|
||||
onBankUp={handleBankUp}
|
||||
onBankDown={handleBankDown}
|
||||
onGlobalBypass={handleGlobalBypass}
|
||||
globalBypass={globalBypass}
|
||||
onPresetClick={(idx)=>{const p=currentBankPresets[parseInt(idx)];if(p)handleLoadPreset(p.bank,p.num-1);}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* ── Focus View overlay ── */}
|
||||
{focusBlockId&&blocks.find(b=>b.id===focusBlockId)&&blockParams[focusBlockId]&&(
|
||||
<FocusView
|
||||
block={blocks.find(b=>b.id===focusBlockId)}
|
||||
params={blockParams[focusBlockId]||{}}
|
||||
viewMode={paramView}
|
||||
onClose={()=>setFocusBlockId(null)}
|
||||
onParamChange={(key,value)=>handleParamChange(focusBlockId,key,value)}
|
||||
onParamChangeEnd={(key,value)=>handleParamChangeEnd(focusBlockId,key,value)}
|
||||
onToggleView={()=>setParamView(p=>p==='slider'?'knob':'slider')}
|
||||
onToggleBypass={handleFocusViewBypass}
|
||||
onOpenModelBrowser={handleOpenFocusModelBrowser}
|
||||
/>
|
||||
)}
|
||||
{/* ── Overlays ── */}
|
||||
{view==="captures"&&<CapturesScreen onClose={()=>setView("main")}/>}
|
||||
{view==="presets"&&(
|
||||
@@ -594,6 +785,19 @@ export default function App(){
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── Tuner overlay ── */}
|
||||
{state.tuner_enabled&&(
|
||||
<TunerScreen
|
||||
tunerData={state}
|
||||
connected={state.connected}
|
||||
onExit={()=>{
|
||||
setState(prev=>({...prev, tuner_enabled: false}));
|
||||
API.tunerToggle(false).catch(()=>{});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
</div></>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user