// MacroPad PWA Application class MacroPadApp { constructor() { this.macros = {}; this.tabs = []; this.currentTab = 'All'; this.ws = null; this.editingMacroId = null; this.commands = []; this.init(); } async init() { await this.loadTabs(); await this.loadMacros(); this.setupWebSocket(); this.setupEventListeners(); this.checkInstallPrompt(); } // API Methods async loadTabs() { try { const response = await fetch('/api/tabs'); const data = await response.json(); this.tabs = data.tabs || []; this.renderTabs(); } catch (error) { console.error('Error loading tabs:', error); this.showToast('Error loading tabs', 'error'); } } async loadMacros() { try { const url = this.currentTab === 'All' ? '/api/macros' : `/api/macros/${encodeURIComponent(this.currentTab)}`; const response = await fetch(url); const data = await response.json(); this.macros = data.macros || {}; this.renderMacros(); } catch (error) { console.error('Error loading macros:', error); this.showToast('Error loading macros', 'error'); } } async executeMacro(macroId) { try { const card = document.querySelector(`[data-macro-id="${macroId}"]`); if (card) card.classList.add('executing'); const response = await fetch('/api/execute', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ macro_id: macroId }) }); if (!response.ok) throw new Error('Execution failed'); setTimeout(() => { if (card) card.classList.remove('executing'); }, 300); } catch (error) { console.error('Error executing macro:', error); this.showToast('Error executing macro', 'error'); } } async saveMacro() { const name = document.getElementById('macro-name').value.trim(); const category = document.getElementById('macro-category').value.trim(); if (!name) { this.showToast('Please enter a macro name', 'error'); return; } if (this.commands.length === 0) { this.showToast('Please add at least one command', 'error'); return; } const macroData = { name, category, commands: this.commands }; try { let response; if (this.editingMacroId) { response = await fetch(`/api/macros/${this.editingMacroId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(macroData) }); } else { response = await fetch('/api/macros', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(macroData) }); } if (!response.ok) throw new Error('Save failed'); this.closeModal(); await this.loadTabs(); await this.loadMacros(); this.showToast('Macro saved successfully', 'success'); } catch (error) { console.error('Error saving macro:', error); this.showToast('Error saving macro', 'error'); } } async deleteMacro(macroId) { if (!confirm('Are you sure you want to delete this macro?')) return; try { const response = await fetch(`/api/macros/${macroId}`, { method: 'DELETE' }); if (!response.ok) throw new Error('Delete failed'); await this.loadTabs(); await this.loadMacros(); this.showToast('Macro deleted', 'success'); } catch (error) { console.error('Error deleting macro:', error); this.showToast('Error deleting macro', 'error'); } } // WebSocket setupWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const wsUrl = `${protocol}//${window.location.host}/ws`; try { this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { this.updateConnectionStatus(true); }; this.ws.onclose = () => { this.updateConnectionStatus(false); setTimeout(() => this.setupWebSocket(), 3000); }; this.ws.onmessage = (event) => { const data = JSON.parse(event.data); this.handleWebSocketMessage(data); }; this.ws.onerror = () => { this.updateConnectionStatus(false); }; } catch (error) { console.error('WebSocket error:', error); } } handleWebSocketMessage(data) { switch (data.type) { case 'macro_created': case 'macro_updated': case 'macro_deleted': this.loadTabs(); this.loadMacros(); break; case 'executed': const card = document.querySelector(`[data-macro-id="${data.macro_id}"]`); if (card) { card.classList.add('executing'); setTimeout(() => card.classList.remove('executing'), 300); } break; } } updateConnectionStatus(connected) { const dot = document.querySelector('.status-dot'); const text = document.querySelector('.connection-status span'); if (dot) { dot.classList.toggle('connected', connected); } if (text) { text.textContent = connected ? 'Connected' : 'Disconnected'; } } // Rendering renderTabs() { const container = document.getElementById('tabs-container'); if (!container) return; container.innerHTML = this.tabs.map(tab => ` `).join(''); } renderMacros() { const container = document.getElementById('macro-grid'); if (!container) return; const macroEntries = Object.entries(this.macros); if (macroEntries.length === 0) { container.innerHTML = `

No macros found

`; return; } container.innerHTML = macroEntries.map(([id, macro]) => { const imageSrc = macro.image_path ? `/api/image/${macro.image_path}` : null; const firstChar = macro.name.charAt(0).toUpperCase(); return `
${imageSrc ? `${macro.name}` : '' }
${firstChar}
${macro.name}
`; }).join(''); } renderCommandList() { const container = document.getElementById('command-list'); if (!container) return; if (this.commands.length === 0) { container.innerHTML = '

No commands added yet

'; return; } container.innerHTML = this.commands.map((cmd, index) => { let displayValue = ''; switch (cmd.type) { case 'text': displayValue = cmd.value || ''; break; case 'key': displayValue = cmd.value || ''; break; case 'hotkey': displayValue = (cmd.keys || []).join(' + '); break; case 'wait': displayValue = `${cmd.ms || 0}ms`; break; case 'app': displayValue = cmd.command || ''; break; } return `
${cmd.type} ${displayValue}
`; }).join(''); } // Command Builder addCommand(type) { let command = { type }; switch (type) { case 'text': const text = prompt('Enter text to type:'); if (!text) return; command.value = text; break; case 'key': const key = prompt('Enter key to press (e.g., enter, tab, escape, space):'); if (!key) return; command.value = key.toLowerCase(); break; case 'hotkey': const keys = prompt('Enter key combination (comma separated, e.g., ctrl,c):'); if (!keys) return; command.keys = keys.split(',').map(k => k.trim().toLowerCase()); break; case 'wait': const ms = prompt('Enter delay in milliseconds:'); if (!ms) return; command.ms = parseInt(ms, 10) || 0; break; case 'app': const appCmd = prompt('Enter application command:'); if (!appCmd) return; command.command = appCmd; break; } this.commands.push(command); this.renderCommandList(); } removeCommand(index) { this.commands.splice(index, 1); this.renderCommandList(); } moveCommand(index, direction) { const newIndex = index + direction; if (newIndex < 0 || newIndex >= this.commands.length) return; [this.commands[index], this.commands[newIndex]] = [this.commands[newIndex], this.commands[index]]; this.renderCommandList(); } // Modal openAddModal() { this.editingMacroId = null; this.commands = []; document.getElementById('macro-name').value = ''; document.getElementById('macro-category').value = ''; document.getElementById('modal-title').textContent = 'Add Macro'; document.getElementById('delete-btn').style.display = 'none'; this.renderCommandList(); document.getElementById('modal-overlay').style.display = 'flex'; } async openEditModal(macroId) { this.editingMacroId = macroId; const macro = this.macros[macroId]; if (!macro) return; document.getElementById('macro-name').value = macro.name || ''; document.getElementById('macro-category').value = macro.category || ''; document.getElementById('modal-title').textContent = 'Edit Macro'; document.getElementById('delete-btn').style.display = 'block'; this.commands = JSON.parse(JSON.stringify(macro.commands || [])); this.renderCommandList(); document.getElementById('modal-overlay').style.display = 'flex'; } closeModal() { document.getElementById('modal-overlay').style.display = 'none'; this.editingMacroId = null; this.commands = []; } // Event Listeners setupEventListeners() { // Tab clicks document.getElementById('tabs-container')?.addEventListener('click', (e) => { if (e.target.classList.contains('tab')) { this.currentTab = e.target.dataset.tab; this.renderTabs(); this.loadMacros(); } }); // Modal close on overlay click document.getElementById('modal-overlay')?.addEventListener('click', (e) => { if (e.target.id === 'modal-overlay') { this.closeModal(); } }); // Escape key to close modal document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { this.closeModal(); } }); } // Toast notifications showToast(message, type = 'info') { const container = document.getElementById('toast-container'); if (!container) return; const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.textContent = message; container.appendChild(toast); setTimeout(() => { toast.remove(); }, 3000); } // PWA Install Prompt checkInstallPrompt() { let deferredPrompt; window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferredPrompt = e; this.showInstallBanner(deferredPrompt); }); } showInstallBanner(deferredPrompt) { const banner = document.createElement('div'); banner.className = 'install-banner'; banner.innerHTML = ` Install MacroPad for quick access
`; document.body.insertBefore(banner, document.body.firstChild); this.deferredPrompt = deferredPrompt; } async installPWA() { if (!this.deferredPrompt) return; this.deferredPrompt.prompt(); const { outcome } = await this.deferredPrompt.userChoice; if (outcome === 'accepted') { document.querySelector('.install-banner')?.remove(); } this.deferredPrompt = null; } // Refresh refresh() { this.loadTabs(); this.loadMacros(); } } // Initialize app let app; document.addEventListener('DOMContentLoaded', () => { app = new MacroPadApp(); }); // Register service worker if ('serviceWorker' in navigator) { window.addEventListener('load', () => { navigator.serviceWorker.register('/service-worker.js') .then((registration) => { console.log('SW registered:', registration.scope); }) .catch((error) => { console.log('SW registration failed:', error); }); }); }