450 lines
14 KiB
JavaScript
450 lines
14 KiB
JavaScript
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();
|
|
}
|
|
});
|