import { app, BrowserWindow, ipcMain, dialog, Tray, Menu, nativeImage } from 'electron'; import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; import { LoupedeckDevice } from './src/loupedeck/device.js'; import { ConfigManager } from './src/loupedeck/config.js'; import { PageManager } from './src/loupedeck/pages.js'; import { StreamDeckPedalDevice } from './src/streamdeck/device.js'; import { PedalPageManager } from './src/streamdeck/pages.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); function getTrayIcon() { const iconPath = path.join(__dirname, 'assets', 'icons', 'Icon.png'); return nativeImage.createFromPath(iconPath); } let mainWindow; let tray; let loupedeckDevice; let configManager; let pageManager; let deviceStatus = { connected: false }; let pedalDevice; let pedalPageManager; let pedalStatus = { connected: false }; function createWindow() { mainWindow = new BrowserWindow({ width: 1580, height: 800, minWidth: 1380, minHeight: 600, frame: false, icon: path.join(__dirname, 'assets', 'icons', 'Icon.png'), webPreferences: { preload: path.join(__dirname, 'preload.cjs'), contextIsolation: true, nodeIntegration: false }, backgroundColor: '#1a1a2e', title: 'Packed PackControl - Loupedeck Live' }); mainWindow.loadFile('src/renderer/index.html'); mainWindow.webContents.on('did-finish-load', () => { mainWindow.webContents.send('device-status', deviceStatus); }); mainWindow.on('close', (event) => { if (!app.isQuitting) { event.preventDefault(); mainWindow.hide(); } }); } function createTray() { tray = new Tray(getTrayIcon()); const contextMenu = Menu.buildFromTemplate([ { label: 'Open', click: () => mainWindow.show() }, { label: 'Quit', click: () => { app.isQuitting = true; app.quit(); } } ]); tray.setToolTip('Packed'); tray.setContextMenu(contextMenu); tray.on('click', () => mainWindow.show()); } async function initializeLoupedeck() { configManager = new ConfigManager(); await configManager.load(); pageManager = new PageManager(configManager); loupedeckDevice = new LoupedeckDevice(pageManager, configManager); loupedeckDevice.on('connected', () => { deviceStatus = { connected: true }; mainWindow.webContents.send('device-status', { connected: true }); }); loupedeckDevice.on('disconnected', () => { deviceStatus = { connected: false }; mainWindow.webContents.send('device-status', { connected: false }); }); loupedeckDevice.on('button-press', (data) => { mainWindow.webContents.send('button-press', data); }); loupedeckDevice.on('knob-rotate', (data) => { mainWindow.webContents.send('knob-rotate', data); }); loupedeckDevice.on('page-changed', (pageIndex) => { mainWindow.webContents.send('page-changed', pageIndex); }); loupedeckDevice.on('metric-update', (updates) => { mainWindow.webContents.send('metric-update', updates); }); loupedeckDevice.on('button-toggle', (data) => { mainWindow.webContents.send('button-toggle', data); }); await loupedeckDevice.connect(); } async function initializePedal() { pedalPageManager = new PedalPageManager(configManager); pedalDevice = new StreamDeckPedalDevice(pedalPageManager, configManager); pedalDevice.on('connected', () => { pedalStatus = { connected: true }; mainWindow.webContents.send('pedal-status', { connected: true }); }); pedalDevice.on('disconnected', () => { pedalStatus = { connected: false }; mainWindow.webContents.send('pedal-status', { connected: false }); }); pedalDevice.on('button-press', (data) => { mainWindow.webContents.send('pedal-button-press', data); }); await pedalDevice.connect(); } // Pedal-IPC-Handler ipcMain.handle('get-pedal-pages', () => pedalPageManager?.getPages() ?? []); ipcMain.handle('get-pedal-current-page', () => pedalPageManager?.getCurrentPageIndex() ?? 0); ipcMain.handle('get-pedal-status', () => pedalStatus); ipcMain.handle('set-pedal-button-config', async (event, { pageIndex, buttonIndex, config }) => { if (!pedalPageManager) return false; pedalPageManager.setButtonConfig(pageIndex, buttonIndex, config); await configManager.save(); return true; }); ipcMain.handle('reset-pedal-button-config', async (event, { pageIndex, buttonIndex }) => { if (!pedalPageManager) return null; const defaultConfig = pedalPageManager.resetButtonConfig(pageIndex, buttonIndex); if (!defaultConfig) return null; await configManager.save(); return defaultConfig; }); ipcMain.handle('add-pedal-page', async () => { if (!pedalPageManager) return null; const newPage = pedalPageManager.addPage(); await configManager.save(); return newPage; }); ipcMain.handle('rename-pedal-page', async (event, { pageIndex, name }) => { if (!pedalPageManager) return false; pedalPageManager.renamePage(pageIndex, name); await configManager.save(); return true; }); ipcMain.handle('delete-pedal-page', async (event, pageIndex) => { if (!pedalPageManager) return false; pedalPageManager.deletePage(pageIndex); await configManager.save(); return true; }); ipcMain.handle('switch-pedal-page', async (event, pageIndex) => { if (!pedalPageManager) return false; pedalPageManager.switchPage(pageIndex); return true; }); ipcMain.handle('reconnect-pedal', async () => { if (!pedalDevice) return; await pedalDevice.disconnect(); await pedalDevice.connect(); }); // IPC-Handler ipcMain.handle('get-config', () => configManager.getConfig()); ipcMain.handle('get-pages', () => pageManager.getPages()); ipcMain.handle('get-current-page', () => pageManager.getCurrentPageIndex()); ipcMain.handle('get-device-status', () => deviceStatus); ipcMain.handle('set-button-config', async (event, { pageIndex, buttonIndex, config }) => { pageManager.setButtonConfig(pageIndex, buttonIndex, config); await configManager.save(); if (pageIndex === pageManager.getCurrentPageIndex()) { await loupedeckDevice.renderButtonIndex(buttonIndex, config); } return true; }); ipcMain.handle('reset-button-config', async (event, { pageIndex, buttonIndex }) => { const defaultConfig = pageManager.resetButtonConfig(pageIndex, buttonIndex); if (!defaultConfig) return null; await configManager.save(); if (pageIndex === pageManager.getCurrentPageIndex()) { await loupedeckDevice.renderButtonIndex(buttonIndex, defaultConfig); } return defaultConfig; }); ipcMain.handle('swap-button-configs', async (event, { pageIndex, sourceIndex, targetIndex }) => { const result = pageManager.swapButtonConfigs(pageIndex, sourceIndex, targetIndex); if (!result) return null; await configManager.save(); if (pageIndex === pageManager.getCurrentPageIndex()) { await loupedeckDevice.renderButtonIndex(sourceIndex, result.source); await loupedeckDevice.renderButtonIndex(targetIndex, result.target); } return result; }); ipcMain.handle('set-knob-config', async (event, { knobIndex, config }) => { pageManager.setKnobConfig(knobIndex, config); await configManager.save(); return true; }); ipcMain.handle('reset-knob-config', async (event, { knobIndex }) => { const defaultConfig = pageManager.resetKnobConfig(knobIndex); if (!defaultConfig) return null; await configManager.save(); if (loupedeckDevice && loupedeckDevice.connected) { await loupedeckDevice.renderSideDisplays(); } return defaultConfig; }); ipcMain.handle('add-page', async () => { const newPage = pageManager.addPage(); await configManager.save(); return newPage; }); ipcMain.handle('rename-page', async (event, { pageIndex, name }) => { pageManager.renamePage(pageIndex, name); await configManager.save(); return true; }); ipcMain.handle('set-page-focus-config', async (event, { pageIndex, config }) => { const updated = pageManager.setPageFocusConfig(pageIndex, config); if (!updated) return false; await configManager.save(); return true; }); ipcMain.handle('delete-page', async (event, pageIndex) => { pageManager.deletePage(pageIndex); await configManager.save(); pageManager.renderCurrentPage(loupedeckDevice); return true; }); ipcMain.handle('switch-page', async (event, pageIndex) => { pageManager.switchPage(pageIndex); await pageManager.renderCurrentPage(loupedeckDevice); return true; }); ipcMain.handle('select-image', async () => { const result = await dialog.showOpenDialog(mainWindow, { properties: ['openFile'], filters: [{ name: 'Images', extensions: ['png', 'jpg', 'jpeg', 'gif'] }] }); if (!result.canceled && result.filePaths.length > 0) { return result.filePaths[0]; } return null; }); ipcMain.handle('read-image-data', async (event, filePath) => { if (!filePath) return null; try { const ext = path.extname(filePath).toLowerCase(); const mime = ext === '.png' ? 'image/png' : (ext === '.jpg' || ext === '.jpeg' ? 'image/jpeg' : (ext === '.gif' ? 'image/gif' : 'application/octet-stream')); const data = await fs.readFile(filePath); return `data:${mime};base64,${data.toString('base64')}`; } catch (error) { console.error('Could not read image:', error.message); return null; } }); ipcMain.handle('export-config', async () => { const result = await dialog.showSaveDialog(mainWindow, { defaultPath: 'packcontrol-config.json', filters: [{ name: 'JSON', extensions: ['json'] }] }); if (!result.canceled) { await configManager.exportTo(result.filePath); return true; } return false; }); ipcMain.handle('import-config', async () => { const result = await dialog.showOpenDialog(mainWindow, { properties: ['openFile'], filters: [{ name: 'JSON', extensions: ['json'] }] }); if (!result.canceled && result.filePaths.length > 0) { await configManager.importFrom(result.filePaths[0]); pageManager.reload(configManager); pageManager.renderCurrentPage(loupedeckDevice); return configManager.getConfig(); } return null; }); ipcMain.handle('reconnect-device', async () => { await loupedeckDevice.disconnect(); await loupedeckDevice.connect(); }); ipcMain.handle('set-setting', async (event, { key, value }) => { const previousAccent = key === 'accentColor' ? configManager.getSetting('accentColor') : null; configManager.setSetting(key, value); if (key === 'accentColor') { const config = configManager.getConfig(); if (Array.isArray(config.pages)) { config.pages.forEach((page) => { if (!page || !Array.isArray(page.buttons)) return; page.buttons.forEach((button) => { if (!button) return; if (button.borderColorAuto === undefined) { if (button.borderColor && previousAccent && button.borderColor.toLowerCase() !== previousAccent.toLowerCase()) { button.borderColorAuto = false; return; } button.borderColorAuto = true; } if (button.borderColorAuto !== false) { button.borderColor = value; } }); }); config.pages.forEach((page) => { if (!page || !page.knobs) return; Object.values(page.knobs).forEach((knob) => { if (!knob) return; if (knob.knobColorAuto === undefined) { if (knob.knobColor && previousAccent && knob.knobColor.toLowerCase() !== previousAccent.toLowerCase()) { knob.knobColorAuto = false; return; } knob.knobColorAuto = true; } if (knob.knobColorAuto !== false) { knob.knobColor = value; } }); }); } } await configManager.save(); if (key === 'accentColor' && loupedeckDevice) { await loupedeckDevice.renderCurrentPage(); } return true; }); ipcMain.handle('set-circle-button-config', async (event, { index, config }) => { pageManager.setCircleButtonConfig(index, config); await configManager.save(); // Farbe am Geraet direkt aktualisieren if (loupedeckDevice && loupedeckDevice.connected) { const isActive = config.pageIndex === pageManager.getCurrentPageIndex(); const color = loupedeckDevice.getCircleButtonColor(config.pageIndex, isActive); try { await loupedeckDevice.device.setButtonColor({ id: index, color }); } catch (e) { // Ignorieren } } return true; }); ipcMain.handle('render-page', async () => { if (loupedeckDevice) { await loupedeckDevice.renderCurrentPage(); } }); // Fenstersteuerung ipcMain.handle('window-minimize', () => { mainWindow.minimize(); }); ipcMain.handle('window-maximize', () => { if (mainWindow.isMaximized()) { mainWindow.unmaximize(); } else { mainWindow.maximize(); } }); ipcMain.handle('window-close', () => { mainWindow.close(); }); app.whenReady().then(async () => { createWindow(); // Tray nur bauen, wenn das Icon da ist try { createTray(); } catch (e) { console.log('Tray icon not found, skipping tray'); } await initializeLoupedeck(); await initializePedal(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); app.on('before-quit', async () => { app.isQuitting = true; if (loupedeckDevice) { await loupedeckDevice.disconnect(); } if (pedalDevice) { await pedalDevice.disconnect(); } });