2026-03-08 00:16:42 +01:00
|
|
|
const { app, BrowserWindow, session, shell, desktopCapturer, autoUpdater, dialog, ipcMain, Notification } = require('electron');
|
2026-03-07 02:40:59 +01:00
|
|
|
const path = require('path');
|
2026-03-08 00:26:31 +01:00
|
|
|
const fs = require('fs');
|
|
|
|
|
const os = require('os');
|
2026-03-07 02:40:59 +01:00
|
|
|
const { setupAdBlocker } = require('./ad-blocker');
|
|
|
|
|
|
|
|
|
|
// Handle Squirrel events (Windows installer)
|
|
|
|
|
try {
|
|
|
|
|
if (require('electron-squirrel-startup')) app.quit();
|
|
|
|
|
} catch {
|
|
|
|
|
// electron-squirrel-startup not installed, skip
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const HUB_URL = process.env.GAMING_HUB_URL || 'https://hub.daddelolymp.de';
|
2026-03-07 19:23:30 +01:00
|
|
|
const APP_VERSION = app.getVersion();
|
|
|
|
|
|
|
|
|
|
// Sync IPC: preload reads app version from package.json
|
|
|
|
|
ipcMain.on('get-app-version', (event) => {
|
|
|
|
|
event.returnValue = APP_VERSION;
|
|
|
|
|
});
|
2026-03-07 02:40:59 +01:00
|
|
|
|
2026-03-07 13:45:27 +01:00
|
|
|
function setupAutoUpdater() {
|
|
|
|
|
if (process.platform !== 'win32') return;
|
|
|
|
|
|
2026-03-07 19:23:30 +01:00
|
|
|
// Squirrel.Windows appends /RELEASES to the feed URL,
|
|
|
|
|
// so point to /downloads where RELEASES + nupkg files live
|
|
|
|
|
const updateURL = `${HUB_URL}/downloads`;
|
2026-03-07 13:45:27 +01:00
|
|
|
try {
|
|
|
|
|
autoUpdater.setFeedURL({ url: updateURL });
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.error('[AutoUpdater] setFeedURL failed:', e.message);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
autoUpdater.on('update-available', () => {
|
|
|
|
|
console.log('[AutoUpdater] Update available, downloading...');
|
2026-03-07 14:24:59 +01:00
|
|
|
if (mainWindow) mainWindow.webContents.send('update-available');
|
2026-03-07 13:45:27 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
autoUpdater.on('update-downloaded', (_event, releaseNotes, releaseName) => {
|
|
|
|
|
console.log('[AutoUpdater] Update downloaded:', releaseName || 'new version');
|
2026-03-07 13:53:26 +01:00
|
|
|
if (mainWindow) mainWindow.webContents.send('update-ready');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Handle install-update request from renderer
|
|
|
|
|
ipcMain.on('install-update', () => {
|
|
|
|
|
autoUpdater.quitAndInstall();
|
2026-03-07 13:45:27 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
autoUpdater.on('update-not-available', () => {
|
|
|
|
|
console.log('[AutoUpdater] App is up to date.');
|
2026-03-07 14:27:09 +01:00
|
|
|
if (mainWindow) mainWindow.webContents.send('update-not-available');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Manual check from renderer
|
|
|
|
|
ipcMain.on('check-for-updates', () => {
|
|
|
|
|
try { autoUpdater.checkForUpdates(); } catch (e) { console.error('[AutoUpdater]', e.message); }
|
2026-03-07 13:45:27 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
autoUpdater.on('error', (err) => {
|
|
|
|
|
console.error('[AutoUpdater] Error:', err.message);
|
2026-03-07 14:24:59 +01:00
|
|
|
if (mainWindow) mainWindow.webContents.send('update-error', err.message);
|
2026-03-07 13:45:27 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Check for updates after 5 seconds, then every 30 minutes
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
try { autoUpdater.checkForUpdates(); } catch (e) { console.error('[AutoUpdater]', e.message); }
|
|
|
|
|
}, 5000);
|
|
|
|
|
|
|
|
|
|
setInterval(() => {
|
|
|
|
|
try { autoUpdater.checkForUpdates(); } catch (e) { console.error('[AutoUpdater]', e.message); }
|
|
|
|
|
}, 30 * 60 * 1000);
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-07 02:40:59 +01:00
|
|
|
let mainWindow;
|
|
|
|
|
|
|
|
|
|
function createWindow() {
|
|
|
|
|
mainWindow = new BrowserWindow({
|
|
|
|
|
width: 1400,
|
|
|
|
|
height: 900,
|
|
|
|
|
minWidth: 900,
|
|
|
|
|
minHeight: 600,
|
|
|
|
|
title: 'Gaming Hub',
|
|
|
|
|
icon: path.join(__dirname, 'assets', 'icon.png'),
|
|
|
|
|
webPreferences: {
|
|
|
|
|
preload: path.join(__dirname, 'preload.js'),
|
|
|
|
|
contextIsolation: true,
|
|
|
|
|
nodeIntegration: false,
|
|
|
|
|
},
|
|
|
|
|
backgroundColor: '#1a1b1e',
|
|
|
|
|
autoHideMenuBar: true,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Setup ad blocker BEFORE loading URL
|
|
|
|
|
setupAdBlocker(session.defaultSession);
|
|
|
|
|
|
2026-03-08 00:16:42 +01:00
|
|
|
// Enable screen capture (getDisplayMedia) in Electron — always show picker
|
2026-03-08 00:26:31 +01:00
|
|
|
// IPC channel for picker result
|
|
|
|
|
const PICKER_CHANNEL = 'screen-picker-result';
|
|
|
|
|
|
2026-03-08 00:16:42 +01:00
|
|
|
session.defaultSession.setDisplayMediaRequestHandler(async (_request, callback) => {
|
|
|
|
|
const sources = await desktopCapturer.getSources({ types: ['screen', 'window'], thumbnailSize: { width: 320, height: 180 } });
|
|
|
|
|
if (sources.length === 0) {
|
|
|
|
|
callback({});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const sourceData = sources.map(s => ({
|
|
|
|
|
id: s.id,
|
|
|
|
|
name: s.name,
|
|
|
|
|
thumbnail: s.thumbnail.toDataURL(),
|
|
|
|
|
}));
|
|
|
|
|
|
2026-03-08 00:26:31 +01:00
|
|
|
// Write picker HTML to a temp file (data: URLs are blocked in Electron)
|
|
|
|
|
const pickerHtml = `<!DOCTYPE html>
|
2026-03-08 00:16:42 +01:00
|
|
|
<html><head><meta charset="utf-8"><style>
|
|
|
|
|
*{margin:0;padding:0;box-sizing:border-box}
|
|
|
|
|
body{background:#1a1b1e;color:#e0e0e0;font-family:system-ui,sans-serif;padding:16px;overflow-y:auto}
|
|
|
|
|
h2{font-size:16px;margin-bottom:12px;color:#ccc}
|
|
|
|
|
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:12px}
|
|
|
|
|
.item{background:#2a2a3e;border:2px solid transparent;border-radius:10px;cursor:pointer;overflow:hidden;transition:border-color .15s,transform .15s}
|
|
|
|
|
.item:hover{border-color:#7c5cff;transform:scale(1.03)}
|
|
|
|
|
.item img{width:100%;height:120px;object-fit:cover;display:block;background:#111}
|
|
|
|
|
.item .label{padding:8px 10px;font-size:13px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
|
|
|
.cancel-row{text-align:center;margin-top:14px}
|
|
|
|
|
.cancel-btn{background:#3a3a4e;color:#e0e0e0;border:none;padding:8px 24px;border-radius:6px;cursor:pointer;font-size:14px}
|
|
|
|
|
.cancel-btn:hover{background:#4a4a5e}
|
|
|
|
|
</style></head><body>
|
2026-03-08 00:26:31 +01:00
|
|
|
<h2>Bildschirm oder Fenster w\\u00e4hlen</h2>
|
2026-03-08 00:16:42 +01:00
|
|
|
<div class="grid" id="grid"></div>
|
2026-03-08 00:26:31 +01:00
|
|
|
<div class="cancel-row"><button class="cancel-btn" id="cancelBtn">Abbrechen</button></div>
|
2026-03-08 00:16:42 +01:00
|
|
|
<script>
|
|
|
|
|
const sources = ${JSON.stringify(sourceData)};
|
|
|
|
|
const grid = document.getElementById('grid');
|
|
|
|
|
sources.forEach(s => {
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
div.className = 'item';
|
2026-03-08 00:26:31 +01:00
|
|
|
const img = document.createElement('img');
|
|
|
|
|
img.src = s.thumbnail;
|
|
|
|
|
div.appendChild(img);
|
|
|
|
|
const label = document.createElement('div');
|
|
|
|
|
label.className = 'label';
|
|
|
|
|
label.textContent = s.name;
|
|
|
|
|
div.appendChild(label);
|
|
|
|
|
div.addEventListener('click', () => {
|
|
|
|
|
require('electron').ipcRenderer.send('${PICKER_CHANNEL}', s.id);
|
|
|
|
|
});
|
2026-03-08 00:16:42 +01:00
|
|
|
grid.appendChild(div);
|
|
|
|
|
});
|
2026-03-08 00:26:31 +01:00
|
|
|
document.getElementById('cancelBtn').addEventListener('click', () => {
|
|
|
|
|
require('electron').ipcRenderer.send('${PICKER_CHANNEL}', null);
|
|
|
|
|
});
|
2026-03-08 00:16:42 +01:00
|
|
|
</script></body></html>`;
|
|
|
|
|
|
2026-03-08 00:26:31 +01:00
|
|
|
const tmpFile = path.join(os.tmpdir(), 'gaming-hub-screen-picker.html');
|
|
|
|
|
fs.writeFileSync(tmpFile, pickerHtml, 'utf-8');
|
|
|
|
|
|
|
|
|
|
const picker = new BrowserWindow({
|
|
|
|
|
width: 680,
|
|
|
|
|
height: 520,
|
|
|
|
|
parent: mainWindow,
|
|
|
|
|
modal: true,
|
|
|
|
|
resizable: false,
|
|
|
|
|
minimizable: false,
|
|
|
|
|
maximizable: false,
|
|
|
|
|
title: 'Bildschirm / Fenster w\u00e4hlen',
|
|
|
|
|
backgroundColor: '#1a1b1e',
|
|
|
|
|
autoHideMenuBar: true,
|
|
|
|
|
webPreferences: {
|
|
|
|
|
contextIsolation: false,
|
|
|
|
|
nodeIntegration: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
picker.loadFile(tmpFile);
|
2026-03-08 00:16:42 +01:00
|
|
|
|
|
|
|
|
let resolved = false;
|
2026-03-08 00:26:31 +01:00
|
|
|
|
|
|
|
|
const onPickerResult = (_event, selectedId) => {
|
|
|
|
|
if (resolved) return;
|
2026-03-08 00:16:42 +01:00
|
|
|
resolved = true;
|
2026-03-08 00:26:31 +01:00
|
|
|
ipcMain.removeListener(PICKER_CHANNEL, onPickerResult);
|
2026-03-08 00:16:42 +01:00
|
|
|
picker.close();
|
2026-03-08 00:26:31 +01:00
|
|
|
try { fs.unlinkSync(tmpFile); } catch {}
|
|
|
|
|
|
2026-03-08 00:16:42 +01:00
|
|
|
if (!selectedId) {
|
|
|
|
|
callback({});
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const chosen = sources.find(s => s.id === selectedId);
|
|
|
|
|
if (chosen) {
|
|
|
|
|
callback({ video: chosen, audio: 'loopback' });
|
|
|
|
|
} else {
|
|
|
|
|
callback({});
|
|
|
|
|
}
|
2026-03-08 00:26:31 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ipcMain.on(PICKER_CHANNEL, onPickerResult);
|
2026-03-08 00:16:42 +01:00
|
|
|
|
|
|
|
|
picker.on('closed', () => {
|
2026-03-08 00:26:31 +01:00
|
|
|
try { fs.unlinkSync(tmpFile); } catch {}
|
|
|
|
|
if (!resolved) {
|
|
|
|
|
resolved = true;
|
|
|
|
|
ipcMain.removeListener(PICKER_CHANNEL, onPickerResult);
|
|
|
|
|
callback({});
|
|
|
|
|
}
|
2026-03-07 13:17:32 +01:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-07 02:40:59 +01:00
|
|
|
// Custom User-Agent to identify Electron app
|
|
|
|
|
const currentUA = mainWindow.webContents.getUserAgent();
|
2026-03-07 19:23:30 +01:00
|
|
|
mainWindow.webContents.setUserAgent(currentUA + ` GamingHubDesktop/${APP_VERSION}`);
|
2026-03-07 02:40:59 +01:00
|
|
|
|
|
|
|
|
mainWindow.loadURL(HUB_URL);
|
|
|
|
|
|
|
|
|
|
// Open external links in default browser
|
|
|
|
|
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
|
|
|
|
if (!url.startsWith(HUB_URL)) {
|
|
|
|
|
shell.openExternal(url);
|
|
|
|
|
return { action: 'deny' };
|
|
|
|
|
}
|
|
|
|
|
return { action: 'allow' };
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Handle navigation to external URLs
|
|
|
|
|
mainWindow.webContents.on('will-navigate', (event, url) => {
|
|
|
|
|
if (!url.startsWith(HUB_URL)) {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
shell.openExternal(url);
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-03-07 13:49:53 +01:00
|
|
|
|
2026-03-07 14:14:51 +01:00
|
|
|
// Track streaming status from renderer (synchronous — no async race)
|
|
|
|
|
let isStreaming = false;
|
|
|
|
|
ipcMain.on('streaming-status', (_event, active) => { isStreaming = active; });
|
|
|
|
|
|
2026-03-08 00:16:42 +01:00
|
|
|
// Windows toast notifications from renderer
|
|
|
|
|
ipcMain.on('show-notification', (_event, title, body) => {
|
|
|
|
|
if (Notification.isSupported()) {
|
|
|
|
|
const notif = new Notification({ title, body, icon: path.join(__dirname, 'assets', 'icon.png') });
|
|
|
|
|
notif.show();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2026-03-07 13:49:53 +01:00
|
|
|
// Warn before closing if a stream is active
|
|
|
|
|
mainWindow.on('close', (event) => {
|
2026-03-07 14:14:51 +01:00
|
|
|
if (!isStreaming) return;
|
|
|
|
|
const result = dialog.showMessageBoxSync(mainWindow, {
|
|
|
|
|
type: 'warning',
|
|
|
|
|
buttons: ['Beenden', 'Abbrechen'],
|
|
|
|
|
defaultId: 1,
|
|
|
|
|
cancelId: 1,
|
|
|
|
|
title: 'Stream laeuft noch!',
|
|
|
|
|
message: 'Ein Stream ist noch aktiv.\nBeim Beenden wird der Stream gestoppt.',
|
|
|
|
|
});
|
|
|
|
|
if (result !== 0) event.preventDefault();
|
2026-03-07 13:49:53 +01:00
|
|
|
});
|
2026-03-07 02:40:59 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-07 13:45:27 +01:00
|
|
|
app.whenReady().then(() => {
|
|
|
|
|
createWindow();
|
|
|
|
|
setupAutoUpdater();
|
|
|
|
|
});
|
2026-03-07 02:40:59 +01:00
|
|
|
|
|
|
|
|
app.on('window-all-closed', () => {
|
|
|
|
|
if (process.platform !== 'darwin') app.quit();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.on('activate', () => {
|
|
|
|
|
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
|
|
|
|
});
|