feat: add scan installed games functionality with UI integration

This commit is contained in:
Moyasee
2026-01-19 15:17:27 +02:00
parent c9801644ac
commit 88b2581797
9 changed files with 457 additions and 3 deletions

View File

@@ -24,6 +24,7 @@ import "./remove-game-from-favorites";
import "./remove-game-from-library";
import "./remove-game";
import "./reset-game-achievements";
import "./scan-installed-games";
import "./select-game-wine-prefix";
import "./toggle-automatic-cloud-sync";
import "./toggle-game-pin";

View File

@@ -0,0 +1,129 @@
import path from "node:path";
import fs from "node:fs";
import { registerEvent } from "../register-event";
import { gamesSublevel, levelKeys } from "@main/level";
import { GameExecutables, logger, WindowManager } from "@main/services";
const SCAN_DIRECTORIES = [
"C:\\Games",
"D:\\Games",
"C:\\Program Files (x86)\\Steam\\steamapps\\common",
"C:\\Program Files\\Steam\\steamapps\\common",
"C:\\Program Files (x86)\\DODI-Repacks",
];
interface FoundGame {
title: string;
executablePath: string;
}
interface ScanResult {
foundGames: FoundGame[];
total: number;
}
const scanInstalledGames = async (
_event: Electron.IpcMainInvokeEvent
): Promise<ScanResult> => {
const games = await gamesSublevel
.iterator()
.all()
.then((results) =>
results
.filter(
([_key, game]) => game.isDeleted === false && game.shop !== "custom"
)
.map(([key, game]) => ({ key, game }))
);
const foundGames: FoundGame[] = [];
for (const { key, game } of games) {
if (game.executablePath) {
continue;
}
const executableNames = GameExecutables.getExecutablesForGame(
game.objectId
);
if (!executableNames || executableNames.length === 0) {
continue;
}
const normalizedNames = new Set(
executableNames.map((name) => name.toLowerCase())
);
let foundPath: string | null = null;
for (const scanDir of SCAN_DIRECTORIES) {
if (!fs.existsSync(scanDir)) {
continue;
}
foundPath = await findExecutableInFolder(scanDir, normalizedNames);
if (foundPath) {
break;
}
}
if (foundPath) {
await gamesSublevel.put(key, {
...game,
executablePath: foundPath,
});
logger.info(
`[ScanInstalledGames] Found executable for ${game.objectId}: ${foundPath}`
);
foundGames.push({
title: game.title,
executablePath: foundPath,
});
}
}
WindowManager.mainWindow?.webContents.send("on-library-batch-complete");
return {
foundGames,
total: games.filter((g) => !g.game.executablePath).length,
};
};
async function findExecutableInFolder(
folderPath: string,
executableNames: Set<string>
): Promise<string | null> {
try {
const entries = await fs.promises.readdir(folderPath, {
withFileTypes: true,
recursive: true,
});
for (const entry of entries) {
if (!entry.isFile()) continue;
const fileName = entry.name.toLowerCase();
if (executableNames.has(fileName)) {
const parentPath =
"parentPath" in entry ? (entry.parentPath as string) : folderPath;
return path.join(parentPath, entry.name);
}
}
} catch (err) {
logger.error(
`[ScanInstalledGames] Error reading folder ${folderPath}:`,
err
);
}
return null;
}
registerEvent("scanInstalledGames", scanInstalledGames);