## Features - Fullscreen button (⛶) in header to toggle fullscreen mode - Wake Lock API integration to keep screen on while using the app - Sun icon (☀) indicator shows wake lock status (bright = active) ## Wake Lock behavior - Automatically requests wake lock when page loads - Re-acquires wake lock when returning to the page - Visual indicator pulses when active - Gracefully hidden if Wake Lock API not supported ## Fullscreen - Works on Android Chrome and desktop browsers - iOS Safari has limited support (no fullscreen API) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
322 lines
9.7 KiB
JavaScript
322 lines
9.7 KiB
JavaScript
// MacroPad PWA Application (Execute-only)
|
|
|
|
class MacroPadApp {
|
|
constructor() {
|
|
this.macros = {};
|
|
this.tabs = [];
|
|
this.currentTab = 'All';
|
|
this.ws = null;
|
|
this.wakeLock = null;
|
|
|
|
this.init();
|
|
}
|
|
|
|
async init() {
|
|
await this.loadTabs();
|
|
await this.loadMacros();
|
|
this.setupWebSocket();
|
|
this.setupEventListeners();
|
|
this.setupWakeLock();
|
|
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');
|
|
}
|
|
}
|
|
|
|
// 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 => `
|
|
<button class="tab ${tab === this.currentTab ? 'active' : ''}"
|
|
data-tab="${tab}">${tab}</button>
|
|
`).join('');
|
|
}
|
|
|
|
renderMacros() {
|
|
const container = document.getElementById('macro-grid');
|
|
if (!container) return;
|
|
|
|
const macroEntries = Object.entries(this.macros);
|
|
|
|
if (macroEntries.length === 0) {
|
|
container.innerHTML = `
|
|
<div class="empty-state">
|
|
<p>No macros found</p>
|
|
<p class="hint">Create macros in the desktop app</p>
|
|
</div>
|
|
`;
|
|
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 `
|
|
<div class="macro-card" data-macro-id="${id}" onclick="app.executeMacro('${id}')">
|
|
${imageSrc
|
|
? `<img src="${imageSrc}" alt="${macro.name}" class="macro-image" onerror="this.style.display='none';this.nextElementSibling.style.display='flex'">`
|
|
: ''
|
|
}
|
|
<div class="macro-image-placeholder" ${imageSrc ? 'style="display:none"' : ''}>
|
|
${firstChar}
|
|
</div>
|
|
<span class="macro-name">${macro.name}</span>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
}
|
|
|
|
// 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();
|
|
}
|
|
});
|
|
}
|
|
|
|
// 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 = `
|
|
<span>Install MacroPad for quick access</span>
|
|
<div>
|
|
<button onclick="app.installPWA()">Install</button>
|
|
<button class="dismiss" onclick="this.parentElement.parentElement.remove()">X</button>
|
|
</div>
|
|
`;
|
|
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();
|
|
}
|
|
|
|
// Fullscreen
|
|
toggleFullscreen() {
|
|
if (!document.fullscreenElement) {
|
|
document.documentElement.requestFullscreen().catch(err => {
|
|
console.log('Fullscreen error:', err);
|
|
});
|
|
} else {
|
|
document.exitFullscreen();
|
|
}
|
|
}
|
|
|
|
// Wake Lock - prevents screen from sleeping
|
|
async setupWakeLock() {
|
|
if (!('wakeLock' in navigator)) {
|
|
console.log('Wake Lock API not supported');
|
|
document.getElementById('wake-lock-status')?.remove();
|
|
return;
|
|
}
|
|
|
|
// Request wake lock
|
|
await this.requestWakeLock();
|
|
|
|
// Re-acquire wake lock when page becomes visible again
|
|
document.addEventListener('visibilitychange', async () => {
|
|
if (document.visibilityState === 'visible') {
|
|
await this.requestWakeLock();
|
|
}
|
|
});
|
|
}
|
|
|
|
async requestWakeLock() {
|
|
try {
|
|
this.wakeLock = await navigator.wakeLock.request('screen');
|
|
this.updateWakeLockStatus(true);
|
|
|
|
this.wakeLock.addEventListener('release', () => {
|
|
this.updateWakeLockStatus(false);
|
|
});
|
|
} catch (err) {
|
|
console.log('Wake Lock error:', err);
|
|
this.updateWakeLockStatus(false);
|
|
}
|
|
}
|
|
|
|
updateWakeLockStatus(active) {
|
|
const status = document.getElementById('wake-lock-status');
|
|
if (status) {
|
|
status.classList.toggle('active', active);
|
|
status.title = active ? 'Screen will stay on' : 'Screen may sleep';
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
});
|
|
});
|
|
}
|