import { app, BrowserWindow, ipcMain, dialog, globalShortcut } from 'electron' import { join } from 'path' import { existsSync, mkdirSync, writeFileSync } from 'fs' import { createHash } from 'crypto' import { detectSteamGames, resolveSteamPath } from './steam' import { detectEpicGames } from './epic' import { CustomGame, AppEntry, readCustomGames, addCustomGame, removeCustomGame, updateCustomGame, readApps, addApp, removeApp, updateApp, } from './config' import { launchSteamGame, launchEpicGame, launchExe } from './launcher' import { getFavorites, toggleFavorite, getRecent, addRecent } from './favorites' import { readSettings, updateSettings } from './settings' import { scanFolder } from './scanner' let mainWindow: BrowserWindow | null = null function createWindow(): void { mainWindow = new BrowserWindow({ width: 1280, height: 800, minWidth: 960, minHeight: 600, backgroundColor: '#0f0f0f', title: 'Club Launcher', frame: true, autoHideMenuBar: true, webPreferences: { preload: join(__dirname, '../preload/preload.js'), contextIsolation: true, nodeIntegration: false, webSecurity: false, }, }) if (process.env['ELECTRON_RENDERER_URL']) { mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL']) mainWindow.webContents.openDevTools({ mode: 'detach' }) } else { mainWindow.loadFile(join(__dirname, '../renderer/index.html')) } mainWindow.on('closed', () => { mainWindow = null }) } // ── IPC ────────────────────────────────────────────────────────────────── ipcMain.handle('games:get-all', () => { const { steamPath } = readSettings() const steamGames = detectSteamGames(steamPath) const epicGames = detectEpicGames() const customGames = readCustomGames() const apps = readApps() return [...steamGames, ...epicGames, ...customGames, ...apps] }) ipcMain.handle('games:launch', (_event, id: string) => { if (id.startsWith('steam_')) { launchSteamGame(id.replace('steam_', '')) return { ok: true, recent: addRecent(id) } } if (id.startsWith('epic_')) { launchEpicGame(id.replace('epic_', '')) return { ok: true, recent: addRecent(id) } } const all = [...readCustomGames(), ...readApps()] const game = all.find((g) => g.id === id) if (!game) return { ok: false, error: 'Not found' } try { launchExe(game.exe, game.args) return { ok: true, recent: addRecent(id) } } catch (e) { return { ok: false, error: String(e) } } }) // Custom games ipcMain.handle('admin:add-game', (_e, d: Omit) => addCustomGame(d)) ipcMain.handle('admin:remove-game', (_e, id: string) => { removeCustomGame(id); return { ok: true } }) ipcMain.handle('admin:update-game', (_e, g: CustomGame) => { updateCustomGame(g); return { ok: true } }) // App entries ipcMain.handle('apps:add', (_e, d: Omit) => addApp(d)) ipcMain.handle('apps:remove', (_e, id: string) => { removeApp(id); return { ok: true } }) ipcMain.handle('apps:update', (_e, a: AppEntry) => { updateApp(a); return { ok: true } }) // Dialogs ipcMain.handle('dialog:pick-exe', async () => { const r = await dialog.showOpenDialog({ title: 'Выберите исполняемый файл', filters: [{ name: 'Executable', extensions: ['exe', 'bat', 'cmd', 'lnk'] }], properties: ['openFile'], }) return r.canceled ? null : r.filePaths[0] }) ipcMain.handle('dialog:pick-image', async () => { const r = await dialog.showOpenDialog({ title: 'Выберите обложку', filters: [{ name: 'Image', extensions: ['png', 'jpg', 'jpeg', 'webp', 'gif', 'ico'] }], properties: ['openFile'], }) return r.canceled ? null : r.filePaths[0] }) ipcMain.handle('dialog:pick-folder', async (_e, title = 'Выберите папку') => { const r = await dialog.showOpenDialog({ title, properties: ['openDirectory'] }) return r.canceled ? null : r.filePaths[0] }) // Settings ipcMain.handle('settings:get', () => { const s = readSettings() return { ...s, resolvedSteamPath: resolveSteamPath(s.steamPath), } }) ipcMain.handle('settings:set-steam-path', (_e, path: string | null) => { return updateSettings({ steamPath: path }) }) ipcMain.handle('settings:add-folder', (_e, folder: string) => { const s = readSettings() if (!s.gameFolders.includes(folder)) { return updateSettings({ gameFolders: [...s.gameFolders, folder] }) } return s }) ipcMain.handle('settings:remove-folder', (_e, folder: string) => { const s = readSettings() return updateSettings({ gameFolders: s.gameFolders.filter((f) => f !== folder) }) }) // Icon extraction helper async function extractExeIcon(exePath: string): Promise { if (!existsSync(exePath)) return '' try { const nativeImage = await app.getFileIcon(exePath, { size: 'large' }) if (nativeImage.isEmpty()) return '' const iconDir = join(app.getPath('userData'), 'icons') if (!existsSync(iconDir)) mkdirSync(iconDir, { recursive: true }) const hash = createHash('md5').update(exePath).digest('hex') const iconPath = join(iconDir, `${hash}.png`) writeFileSync(iconPath, nativeImage.toPNG()) return iconPath } catch { return '' } } // Icon IPC ipcMain.handle('icon:extract', (_e, exePath: string) => extractExeIcon(exePath)) // Scanner ipcMain.handle('scanner:scan', (_e, folder: string) => scanFolder(folder)) ipcMain.handle('scanner:import', async (_e, exes: Array<{ name: string; exe: string }>) => { const imported = await Promise.all( exes.map(async (e) => { const image = await extractExeIcon(e.exe) return addCustomGame({ name: e.name, exe: e.exe, args: [], image, category: 'Other' }) }) ) return imported }) // Favorites / recent ipcMain.handle('favorites:get', () => getFavorites()) ipcMain.handle('favorites:toggle', (_e, id: string) => toggleFavorite(id)) ipcMain.handle('recent:get', () => getRecent()) // ── App lifecycle ──────────────────────────────────────────────────────── app.whenReady().then(() => { createWindow() globalShortcut.register('CommandOrControl+Alt+A', () => { mainWindow?.webContents.send('admin:open') }) app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow() }) }) app.on('window-all-closed', () => { globalShortcut.unregisterAll() if (process.platform !== 'darwin') app.quit() })