feat: initial Club Launcher MVP
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
import { app, BrowserWindow, ipcMain, dialog, shell, globalShortcut } from 'electron'
|
||||
import { join } from 'path'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { detectSteamGames, SteamGame } from './steam'
|
||||
import { readCustomGames, writeCustomGames, CustomGame } from './config'
|
||||
import { launchSteamGame, launchExe } from './launcher'
|
||||
|
||||
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, // allow loading steam CDN images and local file:// images
|
||||
},
|
||||
})
|
||||
|
||||
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 Handlers ────────────────────────────────────────────────────────────
|
||||
|
||||
ipcMain.handle('games:get-all', async () => {
|
||||
const [steamGames, customGames] = await Promise.all([
|
||||
detectSteamGames(),
|
||||
Promise.resolve(readCustomGames()),
|
||||
])
|
||||
return [...steamGames, ...customGames]
|
||||
})
|
||||
|
||||
ipcMain.handle('games:launch', (_event, id: string) => {
|
||||
if (id.startsWith('steam_')) {
|
||||
const appid = id.replace('steam_', '')
|
||||
launchSteamGame(appid)
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
const customs = readCustomGames()
|
||||
const game = customs.find((g) => g.id === id)
|
||||
if (!game) return { ok: false, error: 'Game not found' }
|
||||
|
||||
try {
|
||||
launchExe(game.exe, game.args)
|
||||
return { ok: true }
|
||||
} catch (e) {
|
||||
return { ok: false, error: String(e) }
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('admin:add-game', (_event, game: Omit<CustomGame, 'id' | 'source'>) => {
|
||||
const customs = readCustomGames()
|
||||
const newGame: CustomGame = {
|
||||
...game,
|
||||
id: uuidv4(),
|
||||
source: 'custom',
|
||||
args: game.args ?? [],
|
||||
image: game.image ?? '',
|
||||
category: game.category ?? 'Other',
|
||||
}
|
||||
writeCustomGames([...customs, newGame])
|
||||
return newGame
|
||||
})
|
||||
|
||||
ipcMain.handle('admin:remove-game', (_event, id: string) => {
|
||||
const customs = readCustomGames()
|
||||
writeCustomGames(customs.filter((g) => g.id !== id))
|
||||
return { ok: true }
|
||||
})
|
||||
|
||||
ipcMain.handle('admin:update-game', (_event, updated: CustomGame) => {
|
||||
const customs = readCustomGames()
|
||||
writeCustomGames(customs.map((g) => (g.id === updated.id ? { ...g, ...updated } : g)))
|
||||
return { ok: true }
|
||||
})
|
||||
|
||||
ipcMain.handle('dialog:pick-exe', async () => {
|
||||
const result = await dialog.showOpenDialog({
|
||||
title: 'Выберите исполняемый файл',
|
||||
filters: [{ name: 'Executable', extensions: ['exe', 'bat', 'cmd'] }],
|
||||
properties: ['openFile'],
|
||||
})
|
||||
return result.canceled ? null : result.filePaths[0]
|
||||
})
|
||||
|
||||
ipcMain.handle('dialog:pick-image', async () => {
|
||||
const result = await dialog.showOpenDialog({
|
||||
title: 'Выберите обложку',
|
||||
filters: [{ name: 'Image', extensions: ['png', 'jpg', 'jpeg', 'webp', 'gif'] }],
|
||||
properties: ['openFile'],
|
||||
})
|
||||
return result.canceled ? null : result.filePaths[0]
|
||||
})
|
||||
|
||||
// ── App lifecycle ────────────────────────────────────────────────────────────
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow()
|
||||
|
||||
// Ctrl+Alt+A → open admin panel
|
||||
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()
|
||||
})
|
||||
Reference in New Issue
Block a user