// UI components for controller list (Client-Side) import { contextProvider } from "../core/contextProvider.js"; import { eventBus } from "../core/eventBus.js"; // Create a simple debounce function const debounce = (fn, delay) => { let timeoutId; return (...args) => { if (timeoutId) clearTimeout(timeoutId); timeoutId = setTimeout(() => { fn(...args); timeoutId = null; }, delay); }; }; /** * Loads and displays controller data * @param {HTMLElement} panel - The panel to render controllers in */ export function loadControllerData(panel) { // Set a flag on the panel to prevent multiple simultaneous updates if (panel.isUpdating) return; panel.isUpdating = true; panel.innerHTML = '
🎮Loading controllers...
'; try { // Get controllers from context const midiController = contextProvider.get('midiController'); const gamepadController = contextProvider.get('gamepadController'); // Clear the panel panel.innerHTML = ''; // --- MIDI Controllers --- const midiSection = document.createElement('div'); midiSection.className = 'controller-section midi-section'; panel.appendChild(midiSection); const midiHeader = document.createElement('h3'); midiHeader.className = 'cf-section-header'; midiHeader.innerHTML = '🎹 MIDI Controllers'; midiSection.appendChild(midiHeader); // Ensure midiController is initialized before accessing inputs if (midiController && midiController.initialized) { const midiInputs = midiController.getInputs(); if (midiInputs.length === 0) { midiSection.appendChild(createNoDevicesMessage("No MIDI input devices detected.")); } else { // Create a container for the cards const midiCardsContainer = document.createElement('div'); midiCardsContainer.className = 'cf-cards-container'; midiSection.appendChild(midiCardsContainer); midiInputs.forEach(device => { createMidiDeviceCard(midiCardsContainer, device); }); } } else { midiSection.appendChild(createNoDevicesMessage("Web MIDI API not supported, not enabled, or still initializing.")); } // --- Gamepad Controllers --- const gamepadSection = document.createElement('div'); gamepadSection.className = 'controller-section gamepad-section'; panel.appendChild(gamepadSection); const gamepadHeader = document.createElement('h3'); gamepadHeader.className = 'cf-section-header'; gamepadHeader.innerHTML = '🎮 Gamepad Controllers'; gamepadSection.appendChild(gamepadHeader); // Ensure gamepadController is initialized if (gamepadController) { const gamepads = gamepadController.getGamepads(); if (gamepads.length === 0) { gamepadSection.appendChild(createNoDevicesMessage("No gamepads detected. Connect a gamepad and press a button.")); } else { // Create a container for the cards const gamepadCardsContainer = document.createElement('div'); gamepadCardsContainer.className = 'cf-cards-container'; gamepadSection.appendChild(gamepadCardsContainer); gamepads.forEach(device => { createGamepadDeviceCard(gamepadCardsContainer, device); }); } } else { gamepadSection.appendChild(createNoDevicesMessage("Gamepad API not available or not initialized.")); } // --- Add listeners to update UI on state change --- // Create a debounced update function to avoid excessive refreshes const debouncedUpdate = debounce(() => { if (panel.parentNode) { // Only update if still in DOM panel.isUpdating = false; // Reset the flag before calling update loadControllerData(panel); } }, 250); // 250ms debounce // Set up event listeners const midiUnsubscribe = eventBus.on('midi:stateChanged', () => { debouncedUpdate(); }); const gamepadUnsubscribe = eventBus.on('gamepad:stateChanged', () => { debouncedUpdate(); }); // Store unsubscribe functions on the panel for cleanup panel.controllerListCleanup = () => { midiUnsubscribe(); gamepadUnsubscribe(); }; } catch (error) { console.error('Error loading controller data:', error); // Create error panel panel.innerHTML = `

Error loading controllers

${error.message}

${error.stack || "No stack trace available"}
`; } // Reset the updating flag panel.isUpdating = false; } /** Helper to create "No devices" message */ function createNoDevicesMessage(text) { const noDevices = document.createElement('div'); noDevices.className = 'cf-no-devices'; noDevices.innerHTML = `
🔍

${text}

`; return noDevices; } /** * Creates a MIDI device card UI component * @param {HTMLElement} container - The container to add the card to * @param {Object} device - The MIDI device data from WebMidiController */ function createMidiDeviceCard(container, device) { const deviceCard = document.createElement('div'); deviceCard.className = 'controller-card midi-card'; container.appendChild(deviceCard); // MIDI devices from Web MIDI API have a state property const isConnected = device.state === 'connected'; // Add device header with status badge const deviceHeader = document.createElement('div'); deviceHeader.className = 'controller-card-header'; const deviceName = document.createElement('h4'); deviceName.className = 'controller-card-title'; deviceName.textContent = device.name; deviceName.title = `ID: ${device.id}\nManufacturer: ${device.manufacturer || 'N/A'}`; deviceHeader.appendChild(deviceName); const statusBadge = document.createElement('span'); statusBadge.className = `controller-status ${isConnected ? 'connected' : 'disconnected'}`; statusBadge.textContent = isConnected ? 'Connected' : 'Disconnected'; deviceHeader.appendChild(statusBadge); deviceCard.appendChild(deviceHeader); // Device info (MIDI devices don't typically report button/axis counts) const deviceInfo = document.createElement('div'); deviceInfo.className = 'device-info'; deviceInfo.innerHTML = `
🎹 MIDI Device
${device.state}
`; deviceCard.appendChild(deviceInfo); // Connect/disconnect controls const actions = document.createElement('div'); actions.className = 'controller-actions'; if (isConnected) { const disconnectBtn = document.createElement('button'); disconnectBtn.className = 'disconnect-btn'; disconnectBtn.innerHTML = 'Disconnect'; disconnectBtn.onclick = () => { eventBus.emit('midi:disconnect', device.id); }; actions.appendChild(disconnectBtn); } else { const connectBtn = document.createElement('button'); connectBtn.className = 'connect-btn'; connectBtn.innerHTML = 'Connect'; connectBtn.onclick = () => { eventBus.emit('midi:connect', device.id); }; actions.appendChild(connectBtn); } deviceCard.appendChild(actions); } /** * Creates a Gamepad device card UI component * @param {HTMLElement} container - The container to add the card to * @param {Object} device - The Gamepad device data from GamepadController */ function createGamepadDeviceCard(container, device) { const deviceCard = document.createElement('div'); deviceCard.className = 'controller-card gamepad-card'; container.appendChild(deviceCard); // Check if the gamepad is explicitly connected for input const isConnected = device.state === 'connected'; // Add device header with status badge const deviceHeader = document.createElement('div'); deviceHeader.className = 'controller-card-header'; const deviceName = document.createElement('h4'); deviceName.className = 'controller-card-title'; deviceName.textContent = device.name; deviceName.title = `ID: ${device.id}`; deviceHeader.appendChild(deviceName); const statusBadge = document.createElement('span'); statusBadge.className = `controller-status ${isConnected ? 'connected' : 'detected'}`; statusBadge.textContent = isConnected ? 'Connected' : 'Detected'; deviceHeader.appendChild(statusBadge); deviceCard.appendChild(deviceHeader); // Device info const deviceInfo = document.createElement('div'); deviceInfo.className = 'device-info'; if (device.buttons && device.axes) { deviceInfo.innerHTML = `
🎮 Gamepad
${device.buttons} buttons ${device.axes} axes
`; } else { deviceInfo.innerHTML = `
🎮 Gamepad
`; } deviceCard.appendChild(deviceInfo); // Connect/disconnect controls const actions = document.createElement('div'); actions.className = 'controller-actions'; if (isConnected) { const disconnectBtn = document.createElement('button'); disconnectBtn.className = 'disconnect-btn'; disconnectBtn.innerHTML = 'Disconnect'; disconnectBtn.onclick = () => { eventBus.emit('gamepad:disconnect', device.id); }; actions.appendChild(disconnectBtn); } else { const connectBtn = document.createElement('button'); connectBtn.className = 'connect-btn'; connectBtn.innerHTML = 'Connect'; connectBtn.onclick = () => { eventBus.emit('gamepad:connect', device.id); }; actions.appendChild(connectBtn); } deviceCard.appendChild(actions); }