b84a7bdf29
- electron/settings.ts: persist steamPath + gameFolders in settings.json - electron/scanner.ts: scan folders 2 levels deep for .exe files, filter out known non-game binaries (uninstall/setup/redist/crash handlers) - electron/steam.ts: detectSteamGames(customPath?) — uses user path when set - electron/main.ts: new IPC handlers — dialog:pick-folder, settings:get/set-steam-path/ add-folder/remove-folder, scanner:scan/import - src/components/SettingsPanel.tsx: drawer UI — Steam path picker with auto-resolved path display, game folders list, per-folder scan with checkbox selection, one-click import of selected executables - src/App.tsx: SlidersHorizontal button opens SettingsPanel, reload on import Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
167 lines
5.7 KiB
TypeScript
167 lines
5.7 KiB
TypeScript
import { app, BrowserWindow, ipcMain, dialog, globalShortcut } from 'electron'
|
|
import { join } from 'path'
|
|
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<CustomGame, 'id' | 'source'>) => 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<AppEntry, 'id' | 'source'>) => 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) })
|
|
})
|
|
|
|
// Scanner
|
|
ipcMain.handle('scanner:scan', (_e, folder: string) => scanFolder(folder))
|
|
|
|
ipcMain.handle('scanner:import', (_e, exes: Array<{ name: string; exe: string }>) => {
|
|
const imported = exes.map((e) =>
|
|
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()
|
|
})
|