=== 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)}>
{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 (
{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 && (
)}
{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 (
{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 && (
)}
{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)}>
)}
);
}
// 6. SETTINGS SCREEN ───────────────────────────────────────────