=== FXScreen (block library) === function FXScreen({ state, connected }) { const [fxList, setFxList] = useState(null); const [loading, setLoading] = useState(true); const [showAdd, setShowAdd] = useState(false); const [paramSchemas, setParamSchemas] = useState({}); // Load block param schemas const loadParams = useCallback(async (fxType) => { if (paramSchemas[fxType]) return paramSchemas[fxType]; try { const data = await apiGet(`/api/block-params/${fxType}`); setParamSchemas(prev => ({ ...prev, [fxType]: data.params || [] })); return data.params || []; } catch (e) { return []; } }, [paramSchemas]); useEffect(() => { if (!connected || !state?.current_preset) { // Offline: show a simple empty state setFxList([]); setLoading(false); return; } // Load active preset's chain from presets API apiGet('/api/presets').then(async data => { const banks = data.banks || []; const cp = state.current_preset; const preset = banks[cp.bank]?.presets?.[cp.program]; if (preset?.chain) { const blocks = await Promise.all(preset.chain.map(async (b, i) => { const params = await loadParams(b.fx_type); return { id: i + 1, title: FX_TYPE_LABELS[b.fx_type] || b.fx_type.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()), type: b.fx_type, bypassed: b.bypass || !b.enabled, params: params, }; })); setFxList(blocks); } setLoading(false); }).catch(() => { setFxList([]); setLoading(false); }); }, [connected, state, loadParams]); const toggle = (id) => setFxList(prev => prev ? prev.map(f => f.id === id ? { ...f, bypassed: !f.bypassed } : f) : prev); const addBlock = async (fxType) => { setShowAdd(false); const params = await loadParams(fxType); setFxList(prev => [...(prev || []), { id: Date.now(), title: FX_TYPE_LABELS[fxType] || fxType, type: fxType, bypassed: false, params: params, }]); }; if (loading) return
; return (
FX Chain {(fxList?.length || 0) > 0 ? `(${fxList.length})` : ''}
{/* Signal path */} {fxList && fxList.length > 0 && (
{["IN", ...fxList.map(f => f.type.toUpperCase()), "OUT"].map((s, i, arr) => (
{s}
{i < arr.length - 1 &&
}
))}
)} {(!fxList || fxList.length === 0) ? (
No FX blocks in the chain.
) : ( fxList.map(f => ( toggle(f.id)} /> )) )}
{/* Add Block slide panel */} {showAdd && ( setShowAdd(false)}>
Add FX Block
{Object.entries(FX_TYPE_LABELS).map(([key, label]) => (
addBlock(key)} style={{ borderRadius: 6 }}> {key.split('_')[0]} {label} +
))}
)}
); } // 3. MODELS SCREEN ───────────────────────────────────────────── === ModelsScreen === function ModelsScreen({ state, connected, refresh }) { const [models, setModels] = useState([]); const [current, setCurrent] = useState(null); const [loading, setLoading] = useState(true); const [searching, setSearching] = useState(false); const [searchQ, setSearchQ] = useState(''); const [searchResults, setSearchResults] = useState([]); const [showSearch, setShowSearch] = useState(false); const [uploading, setUploading] = useState(false); const fileRef = useRef(null); const loadModels = useCallback(async () => { if (!connected) { setLoading(false); return; } try { const data = await apiGet('/api/models'); setModels(data.models || []); setCurrent(data.current); } catch (e) { /* ignore */ } setLoading(false); }, [connected]); useEffect(() => { loadModels(); }, [loadModels]); const handleLoad = async (path) => { try { await apiPost('/api/models/load', { path }); await loadModels(); refresh(); } catch (e) { alert('Failed to load model'); } }; const handleUnload = async () => { try { await apiPost('/api/models/unload'); setCurrent(null); await loadModels(); refresh(); } catch (e) { alert('Failed to unload'); } }; const handleUpload = async (e) => { const file = e.target.files?.[0]; if (!file) return; setUploading(true); try { await apiUpload('/api/models/upload', file); await loadModels(); } catch (err) { alert('Upload failed'); } setUploading(false); }; const handleSearch = useCallback(async () => { if (!searchQ.trim()) return; setSearching(true); try { const data = await apiGet(`/api/models/tonedownload/search?q=${encodeURIComponent(searchQ)}`); setSearchResults(data.results || []); } catch (e) { alert('Search failed'); } setSearching(false); }, [searchQ]); const handleInstall = async (result) => { try { await apiPost('/api/models/tonedownload/install', { download_url: result.download_url, name: result.name || result.filename, id: result.id, size: result.size, pack_name: result.pack_name, display_name: result.name, }); await loadModels(); refresh(); } catch (e) { alert('Install failed'); } }; if (loading) return
; return (
NAM Models ({models.length})
{showSearch && (
🔍 setSearchQ(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleSearch()} />
{searchResults.length > 0 && (
{searchResults.map(r => (
handleInstall(r)} style={{ borderBottom: `1px solid ${T.border}`, borderRadius: 0 }}>
{r.name}
{r.author} · {r.size_display} · {r.architecture}
))}
)}
)}
{current && (
Loaded Model
{current}
)} {models.length === 0 ? (
No NAM models found.
Upload a .nam file or search Tone3000.
) : ( models.map(m => (
{m.name}
{m.architecture} · {m.size_mb} MB
{m.loaded ? ( Loaded ) : ( )}
)) )}
); } // 4. IRS SCREEN ──────────────────────────────────────────────── === IRsScreen === function IRsScreen({ state, connected, refresh }) { const [irs, setIrs] = useState([]); const [current, setCurrent] = useState(null); const [loading, setLoading] = useState(true); const [searching, setSearching] = useState(false); const [searchQ, setSearchQ] = useState(''); const [searchResults, setSearchResults] = useState([]); const [showSearch, setShowSearch] = useState(false); const [uploading, setUploading] = useState(false); const fileRef = useRef(null); const loadIrs = useCallback(async () => { if (!connected) { setLoading(false); return; } try { const data = await apiGet('/api/irs'); setIrs(data.irs || []); setCurrent(data.current); } catch (e) { /* ignore */ } setLoading(false); }, [connected]); useEffect(() => { loadIrs(); }, [loadIrs]); const handleLoad = async (path) => { try { await apiPost('/api/irs/load', { path }); await loadIrs(); refresh(); } catch (e) { alert('Failed to load IR'); } }; const handleUnload = async () => { try { await apiPost('/api/irs/unload'); setCurrent(null); await loadIrs(); refresh(); } catch (e) { alert('Failed to unload'); } }; const handleUpload = async (e) => { const file = e.target.files?.[0]; if (!file) return; setUploading(true); try { await apiUpload('/api/irs/upload', file); await loadIrs(); } catch (err) { alert('Upload failed'); } setUploading(false); }; const handleSearch = useCallback(async () => { if (!searchQ.trim()) return; setSearching(true); try { const data = await apiGet(`/api/irs/tonedownload/search?q=${encodeURIComponent(searchQ)}`); setSearchResults(data.results || []); } catch (e) { alert('Search failed'); } setSearching(false); }, [searchQ]); const handleInstall = async (result) => { try { await apiPost('/api/irs/tonedownload/install', { download_url: result.download_url, name: result.name || result.filename, id: result.id, size: result.size, display_name: result.name, }); await loadIrs(); refresh(); } catch (e) { alert('Install failed'); } }; if (loading) return
; return (
IR Files ({irs.length})
{showSearch && (
🔍 setSearchQ(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleSearch()} />
{searchResults.length > 0 && (
{searchResults.map(r => (
handleInstall(r)} style={{ borderBottom: `1px solid ${T.border}`, borderRadius: 0 }}>
{r.name}
{r.author} · {r.size_display}
))}
)}
)}
{current && (
Loaded IR
{current}
)} {irs.length === 0 ? (
No IR files found.
Upload a .wav file or search Tone3000.
) : ( irs.map(ir => (
{ir.name}
{ir.sample_rate} Hz · {ir.length_ms} ms · {ir.num_taps} taps
{ir.loaded ? ( Loaded ) : ( )}
)) )}
); } // 5. PRESETS SCREEN ──────────────────────────────────────────── === PresetsScreen === function PresetsScreen({ state, connected, refresh }) { const [banks, setBanks] = useState([]); const [selectedBank, setSelectedBank] = useState(0); const [loading, setLoading] = useState(true); const [saveName, setSaveName] = useState(''); const [showSave, setShowSave] = useState(false); const [deleting, setDeleting] = useState(null); const loadPresets = useCallback(async () => { if (!connected) { setBanks([{ name: "Default", number: 0, presets: [ { name: "Edge of Breakup", bank: 0, program: 0 }, { name: "Modern High Gain", bank: 0, program: 1 }, null, null, ]}]); setLoading(false); return; } try { const data = await apiGet('/api/presets'); setBanks(data.banks || []); } catch (e) { /* ignore */ } setLoading(false); }, [connected]); useEffect(() => { loadPresets(); }, [loadPresets]); const handleActivate = async (bank, program) => { try { await apiPost(`/api/presets/${bank}/${program}/activate`); await loadPresets(); refresh(); } catch (e) { alert('Failed to load preset'); } }; const handleSave = async () => { if (!saveName.trim()) return; const currentBank = state?.current_preset?.bank ?? 0; const currentProgram = state?.current_preset?.program ?? 0; try { await apiPut(`/api/presets/${currentBank}/${currentProgram}`, { name: saveName }); setShowSave(false); await loadPresets(); } catch (e) { alert('Failed to save'); } }; const handleNewPreset = async () => { // Find the first empty slot, or create at next program number const bank = selectedBank; const bankData = banks[bank]; const presets = bankData?.presets || []; let nextProg = presets.findIndex(p => p == null); if (nextProg === -1) nextProg = presets.length; const name = `New Preset ${nextProg + 1}`; try { await apiPut(`/api/presets/${bank}/${nextProg}`, { name }); await loadPresets(); await apiPost(`/api/presets/${bank}/${nextProg}/activate`); refresh(); } catch (e) { alert('Failed to create preset'); } }; const handleDelete = async (bank, program) => { try { await apiDelete(`/api/presets/${bank}/${program}`); setDeleting(null); await loadPresets(); } catch (e) { alert('Failed to delete'); } }; if (loading) return
; const currentPreset = state?.current_preset; const bankData = banks[selectedBank]; const presets = bankData?.presets || []; return (
Presets
{/* Bank selector */} {banks.length > 1 && (
{banks.map((b, i) => ( ))}
)} {/* Current preset indicator */} {currentPreset && (
Current
{currentPreset.name} Bank {currentPreset.bank} · Prog {currentPreset.program}
)} {/* Preset list */} {presets.length === 0 ? (
No presets in this bank.
) : ( presets.map((p, i) => { if (!p) return null; const isActive = currentPreset?.bank === p.bank && currentPreset?.program === p.program; const isDeleting = deleting === `${p.bank}-${p.program}`; return (
!isDeleting && handleActivate(p.bank, p.program)}>
{p.program + 1}
{p.name}
Bank {p.bank} · Program {p.program}
{isActive && } {isDeleting ? (
) : ( )}
); }) )}
{/* Save dialog */} {showSave && ( setShowSave(false)}>
Save Preset
setSaveName(e.target.value)} autoFocus onKeyDown={e => e.key === 'Enter' && handleSave()} />
)}
); } // 6. SETTINGS SCREEN ───────────────────────────────────────────