/** * FootswitchMode Context — provides the current mode and mode switching * to all footswitch components. */ import { createContext, useContext, useState, useCallback, useEffect, type ReactNode } from 'react'; import type { FootswitchMode, ScribbleStripData, PedalState, PedalBlock } from './types'; import { apiPost, activatePreset } from "./hooks/usePedalState"; // ── Context Shape ───────────────────────────────────────────── interface FootswitchModeContextValue { mode: FootswitchMode; setMode: (mode: FootswitchMode) => void; /** Scribble strip data for each footswitch position (0–7) */ scribbleData: ScribbleStripData[]; /** Handle a footswitch press at a given position */ handleFootswitch: (index: number) => void; /** Handle secondary action (double-tap / long-press) at a given position */ handleSecondary: (index: number) => void; /** Current banks/presets for preset mode */ currentBankIndex: number; currentPresetIndex: number; setBank: (bank: number) => void; setPreset: (preset: number) => void; /** Whether the preset has been modified */ dirty: boolean; } const FootswitchModeContext = createContext(null); // ── Mode-specific scribble strip generators ─────────────────── function stompScribbles( blocks: PedalBlock[], _state: PedalState | null, ): ScribbleStripData[] { const items: ScribbleStripData[] = []; for (let i = 0; i < 8; i++) { const block = blocks[i]; if (block) { items.push({ key: `stomp-${block.id}`, label: block.title.length > 6 ? block.title.slice(0, 6) : block.title, sublabel: block.bypassed ? 'BYP' : 'ON', color: block.bypassed ? 'off' : 'green', active: !block.bypassed, bypassed: block.bypassed, }); } else { items.push({ key: `stomp-empty-${i}`, label: '—', sublabel: '', color: 'off', active: false, bypassed: false, }); } } return items; } function presetScribbles( state: PedalState | null, bankIndex: number, bankNames: string[], ): ScribbleStripData[] { const items: ScribbleStripData[] = []; // FS 0-1: Bank navigation items.push({ key: 'bank-down', label: 'BANK', sublabel: bankIndex > 0 ? `${bankNames[bankIndex - 1] || '−'}` : '—', color: 'blue', active: bankIndex > 0, }); items.push({ key: 'bank-up', label: 'BANK', sublabel: bankIndex < bankNames.length - 1 ? `${bankNames[bankIndex + 1] || '+'}` : '—', color: 'blue', active: bankIndex < bankNames.length - 1, }); // FS 2-7: Preset slots for (let i = 0; i < 6; i++) { const presetIdx = i; const isActive = state?.current_preset?.program === presetIdx && state?.current_preset?.bank === bankIndex; items.push({ key: `preset-${presetIdx}`, label: String(presetIdx + 1), sublabel: state?.current_preset && state.current_preset.program === presetIdx ? state.current_preset.name : '', color: isActive ? 'green' : 'off', active: isActive, }); } return items; } function snapshotScribbles( snapshots: { name: string; active: boolean }[], ): ScribbleStripData[] { const items: ScribbleStripData[] = []; for (let i = 0; i < 8; i++) { const snap = snapshots[i]; if (snap) { items.push({ key: `snapshot-${i}`, label: snap.name.length > 6 ? snap.name.slice(0, 6) : (snap.name || `Snap ${i + 1}`), sublabel: snap.active ? 'ACTIVE' : '', color: snap.active ? 'amber' : 'off', active: snap.active, }); } else { items.push({ key: `snapshot-empty-${i}`, label: `Snap ${i + 1}`, sublabel: '', color: 'off', active: false, }); } } return items; } function comboScribbles( blocks: PedalBlock[], state: PedalState | null, bankNames: string[], bankIndex: number, ): ScribbleStripData[] { // Combo = first 4 stomp + bank/preset info on last 4 const stomp = stompScribbles(blocks.slice(0, 4), state); // FS 4: bank down, FS 5: bank up, FS 6-7: preset select return [ ...stomp.slice(0, 4), { key: 'bank-down', label: 'BANK', sublabel: bankIndex > 0 ? '▼' : '—', color: 'blue', active: bankIndex > 0, }, { key: 'bank-up', label: 'BANK', sublabel: bankIndex < bankNames.length - 1 ? '▲' : '—', color: 'blue', active: bankIndex < bankNames.length - 1, }, { key: 'preset-select', label: state?.current_preset?.name?.slice(0, 6) || '—', sublabel: state?.current_preset ? `B${bankIndex + 1}` : '', color: 'amber', active: true, }, { key: 'global-bypass', label: 'BYP', sublabel: state?.bypass ? 'ON' : 'OFF', color: state?.bypass ? 'red' : 'off', active: !!state?.bypass, }, ]; } // ── Provider ────────────────────────────────────────────────── interface FootswitchModeProviderProps { children: ReactNode; state: PedalState | null; connected: boolean; refresh: () => void; blocks: PedalBlock[]; bankNames: string[]; /** Current snapshot definitions */ snapshots: { name: string; active: boolean }[]; /** Called when a block toggle needs to happen */ onToggleBlock?: (blockId: number, bypassed: boolean) => void; } export function FootswitchModeProvider({ children, state, connected, refresh, blocks, bankNames, snapshots, onToggleBlock, }: FootswitchModeProviderProps) { const [mode, setMode] = useState('stomp'); const [currentBankIndex, setCurrentBankIndex] = useState(() => { try { const saved = localStorage.getItem('pedal-footswitch-bank'); return saved !== null ? parseInt(saved, 10) : 0; } catch { return 0; } }); const [currentPresetIndex, setCurrentPresetIndex] = useState(0); const [dirty, _setDirty] = useState(false); // Persist bank index to localStorage useEffect(() => { try { localStorage.setItem('pedal-footswitch-bank', String(currentBankIndex)); } catch { /* quota exceeded or storage disabled */ } }, [currentBankIndex]); // Generate scribble data based on mode let scribbleData: ScribbleStripData[]; switch (mode) { case 'stomp': scribbleData = stompScribbles(blocks, state); break; case 'preset': scribbleData = presetScribbles(state, currentBankIndex, bankNames); break; case 'snapshot': scribbleData = snapshotScribbles(snapshots); break; case 'combo': scribbleData = comboScribbles(blocks, state, bankNames, currentBankIndex); break; default: scribbleData = stompScribbles(blocks, state); } const handleFootswitch = useCallback((index: number) => { switch (mode) { case 'stomp': { const block = blocks[index]; if (block && onToggleBlock) { onToggleBlock(block.id, !block.bypassed); } break; } case 'preset': { // FS 0 = bank down, FS 1 = bank up, FS 2-7 = presets if (index === 0) { setCurrentBankIndex(prev => Math.max(0, prev - 1)); } else if (index === 1) { setCurrentBankIndex(prev => Math.min(bankNames.length - 1, prev + 1)); } else { const presetIndex = index - 2; if (connected) { activatePreset(currentBankIndex, presetIndex).then(() => refresh()); setCurrentPresetIndex(presetIndex); } } break; } case 'snapshot': { // Recall snapshot at index if (connected) { apiPost(`/api/snapshots/${index}/recall`).catch(() => {}); } break; } case 'combo': { // FS 0-3 = stomp toggles, FS 4 = bank down, FS 5 = bank up, // FS 6 = open preset browser, FS 7 = global bypass if (index < 4) { const block = blocks[index]; if (block && onToggleBlock) { onToggleBlock(block.id, !block.bypassed); } } else if (index === 4) { setCurrentBankIndex(prev => Math.max(0, prev - 1)); } else if (index === 5) { setCurrentBankIndex(prev => Math.min(bankNames.length - 1, prev + 1)); } else if (index === 6) { setMode('preset'); } else if (index === 7) { if (connected) { apiPost('/api/bypass', { bypass: !state?.bypass }).catch(() => {}); } } break; } } }, [mode, blocks, onToggleBlock, connected, currentBankIndex, bankNames, state, refresh]); const handleSecondary = useCallback((index: number) => { // Double-tap / long-press secondary actions per mode switch (mode) { case 'stomp': { // Secondary: toggle tuner on FS 7 (last footswitch), or reset block params if (index === 7 && connected) { apiPost('/api/tuner', { enabled: !state?.tuner_enabled }).catch(() => {}); } break; } case 'preset': { // Secondary on bank switches: jump to first/last bank if (index === 0) { setCurrentBankIndex(0); } else if (index === 1) { setCurrentBankIndex(bankNames.length - 1); } break; } case 'snapshot': { // Secondary: save current state to snapshot if (connected) { apiPost(`/api/snapshots/${index}/save`).catch(() => {}); } break; } case 'combo': { // Secondary on stomp blocks: edit block params (would open param panel) // Secondary on bypass: enter tuner mode if (index === 7 && connected) { apiPost('/api/tuner', { enabled: !state?.tuner_enabled }).catch(() => {}); } break; } } }, [mode, state, connected, bankNames]); const setBank = useCallback((bank: number) => { setCurrentBankIndex(bank); }, []); const setPreset = useCallback((preset: number) => { setCurrentPresetIndex(preset); if (connected) { activatePreset(currentBankIndex, preset).then(() => refresh()); } }, [connected, currentBankIndex, refresh]); const value: FootswitchModeContextValue = { mode, setMode, scribbleData, handleFootswitch, handleSecondary, currentBankIndex, currentPresetIndex, setBank, setPreset, dirty, }; return ( {children} ); } // ── Consumer Hook ───────────────────────────────────────────── export function useFootswitchMode(): FootswitchModeContextValue { const ctx = useContext(FootswitchModeContext); if (!ctx) { throw new Error('useFootswitchMode must be used within a FootswitchModeProvider'); } return ctx; }