From e92dfa11988f215cd756723a857310c2a479bfed Mon Sep 17 00:00:00 2001 From: IsaiahBideshi Date: Sun, 14 Jun 2026 10:08:49 -0400 Subject: [PATCH 1/5] feat: Sync library executable state during installed games scan Add a step during installed game scan that resets stale install metadata and disables automatic cloud sync when a game executable no longer exists. Expose in preload. Add UI elements that show which executables were cleared. --- src/locales/en/translation.json | 10 ++-- src/main/events/library/index.ts | 1 + .../remove-uninstalled-game-executables.ts | 49 +++++++++++++++++++ src/preload/index.ts | 2 + src/renderer/src/components/header/header.tsx | 10 ++++ .../components/header/scan-games-modal.tsx | 36 ++++++++++++++ src/renderer/src/declaration.d.ts | 4 ++ 7 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 src/main/events/library/remove-uninstalled-game-executables.ts diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 768e86ad63..277789a8ea 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -129,9 +129,9 @@ "settings": "Settings", "version_available_install": "Version {{version}} available. Click here to restart and install.", "version_available_download": "Version {{version}} available. Click here to download.", - "scan_games_tooltip": "Scan PC for installed games", - "scan_games_title": "Scan PC for installed games", - "scan_games_description": "This will scan your disks for known game executables.\nThis may take several minutes.", + "scan_games_tooltip": "Sync Installed Games with Library", + "scan_games_title": "Sync Installed Games with Library", + "scan_games_description": "This will: \n 1. Scan your disks for known game executables \n 2. Remove uninstalled executables from the library.\nThis may take several minutes.", "scan_games_start": "Start Scan", "scan_games_cancel": "Cancel", "scan_games_result": "Found {{found}} of {{total}} games without executable path", @@ -147,7 +147,9 @@ "scan_games_add_folder": "Add folder", "scan_games_remove_folder": "Remove folder", "scan_games_folders_hint_manual": "Add at least one folder to scan for games.", - "scan_games_detection_warning": "Detection relies on a community-maintained list of known games and may be outdated, so some games might not be found." + "scan_games_detection_warning": "Detection relies on a community-maintained list of known games and may be outdated, so some games might not be found.", + "remove_executables_result": "Removed {{removed}} of {{total}} uninstalled game executables", + "remove_executables_no_results": "No uninstalled game executables found." }, "bottom_panel": { "no_downloads_in_progress": "No downloads in progress", diff --git a/src/main/events/library/index.ts b/src/main/events/library/index.ts index 20f0e7e35f..1cb1016153 100644 --- a/src/main/events/library/index.ts +++ b/src/main/events/library/index.ts @@ -37,6 +37,7 @@ import "./remove-game-from-library"; import "./remove-game"; import "./reset-game-achievements"; import "./scan-installed-games"; +import "./remove-uninstalled-game-executables"; import "./select-game-proton-path"; import "./select-game-wine-prefix"; import "./toggle-automatic-cloud-sync"; diff --git a/src/main/events/library/remove-uninstalled-game-executables.ts b/src/main/events/library/remove-uninstalled-game-executables.ts new file mode 100644 index 0000000000..240d68b69e --- /dev/null +++ b/src/main/events/library/remove-uninstalled-game-executables.ts @@ -0,0 +1,49 @@ +import fs from "node:fs"; +import { registerEvent } from "../register-event"; +import { updateGameExecutablePath } from "@main/helpers/update-executable-path"; +import { gamesSublevel } from "@main/level"; +import { logger, WindowManager } from "@main/services"; + +interface RemovedGame { + title: string; +} + +const removeUninstalledGameExecutables = async () => { + const games = await gamesSublevel + .iterator() + .all() + .then((results) => + results + .filter(([_key, game]) => game.isDeleted === false) + .map(([key, game]) => ({ key, game })) + ); + + const removedGames: RemovedGame[] = []; + const gamesToCheck = games.filter((g) => g.game.executablePath); + + for (const { key, game } of gamesToCheck) { + const exePath = game.executablePath!; + if (!fs.existsSync(exePath)) { + await gamesSublevel.put(key, { + ...updateGameExecutablePath(game, null), + installedSizeInBytes: null, + automaticCloudSync: false, + }); + + logger.info( + `[RemoveUninstalledGameExecutables] Removed executable for ${game.objectId}: ${exePath}` + ); + removedGames.push({ title: game.title }); + } + } + + if (removedGames.length > 0) { + WindowManager.sendToAppWindows("on-library-batch-complete"); + } + return { removedGames: removedGames, total: gamesToCheck.length }; +}; + +registerEvent( + "removeUninstalledGameExecutables", + removeUninstalledGameExecutables +); diff --git a/src/preload/index.ts b/src/preload/index.ts index 55509b80db..bc58f5de58 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -636,6 +636,8 @@ contextBridge.exposeInMainWorld("electron", { additionalDirectories, includeDefaultDirectories ), + removeUninstalledGameExecutables: () => + ipcRenderer.invoke("removeUninstalledGameExecutables"), getDefaultWinePrefixSelectionPath: () => ipcRenderer.invoke("getDefaultWinePrefixSelectionPath"), createSteamShortcut: ( diff --git a/src/renderer/src/components/header/header.tsx b/src/renderer/src/components/header/header.tsx index 6588e0b712..0b4c0859b9 100644 --- a/src/renderer/src/components/header/header.tsx +++ b/src/renderer/src/components/header/header.tsx @@ -108,6 +108,10 @@ export function Header() { foundGames: { title: string; executablePath: string }[]; total: number; } | null>(null); + const [removeExeResult, setRemoveExeResult] = useState<{ + removedGames: { title: string }[]; + total: number; + } | null>(null); const { t } = useTranslation("header"); @@ -323,6 +327,7 @@ export function Header() { setIsScanning(true); setScanResult(null); + setRemoveExeResult(null); try { const result = await window.electron.scanInstalledGames( @@ -330,6 +335,9 @@ export function Header() { includeDefaultDirectories ); setScanResult(result); + const exeResult = + await window.electron.removeUninstalledGameExecutables(); + setRemoveExeResult(exeResult); } finally { setIsScanning(false); } @@ -337,6 +345,7 @@ export function Header() { const handleClearScanResult = () => { setScanResult(null); + setRemoveExeResult(null); }; useEffect(() => { @@ -480,6 +489,7 @@ export function Header() { onClose={() => setShowScanModal(false)} isScanning={isScanning} scanResult={scanResult} + removeExecutableResult={removeExeResult} onStartScan={handleStartScan} onClearResult={handleClearScanResult} /> diff --git a/src/renderer/src/components/header/scan-games-modal.tsx b/src/renderer/src/components/header/scan-games-modal.tsx index 7f1dfaa875..515b47686a 100644 --- a/src/renderer/src/components/header/scan-games-modal.tsx +++ b/src/renderer/src/components/header/scan-games-modal.tsx @@ -24,11 +24,17 @@ interface ScanResult { total: number; } +interface RemoveExecutableResult { + removedGames: { title: string }[]; + total: number; +} + export interface ScanGamesModalProps { visible: boolean; onClose: () => void; isScanning: boolean; scanResult: ScanResult | null; + removeExecutableResult: RemoveExecutableResult | null; onStartScan: ( additionalDirectories: string[], includeDefaultDirectories: boolean @@ -41,6 +47,7 @@ export function ScanGamesModal({ onClose, isScanning, scanResult, + removeExecutableResult, onStartScan, onClearResult, }: Readonly) { @@ -224,6 +231,35 @@ export function ScanGamesModal({ {t("scan_games_no_results")}

)} + + {removeExecutableResult && + (removeExecutableResult.removedGames.length > 0 ? ( + <> +

+ {t("remove_executables_result", { + removed: removeExecutableResult.removedGames.length, + total: removeExecutableResult.total, + })} +

+ + + + ) : ( +

+ {t("remove_executables_no_results")} +

+ ))} )} diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 8fc29cb1c8..c974ed325f 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -528,6 +528,10 @@ declare global { foundGames: { title: string; executablePath: string }[]; total: number; }>; + removeUninstalledGameExecutables: () => Promise<{ + removedGames: { title: string }[]; + total: number; + }>; onExtractionComplete: ( cb: (shop: GameShop, objectId: string) => void ) => () => Electron.IpcRenderer; From d419ccf7f14ec186b2861bbf91d596451caf879d Mon Sep 17 00:00:00 2001 From: IsaiahBideshi Date: Sun, 14 Jun 2026 10:51:16 -0400 Subject: [PATCH 2/5] fix: executable cleanup feedback Add status to removing executables. Filter out custom add games. --- src/locales/en/translation.json | 1 + .../remove-uninstalled-game-executables.ts | 4 +++- src/renderer/src/components/header/header.tsx | 8 +++++++- .../components/header/scan-games-modal.tsx | 19 ++++++++++++++++--- 4 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 277789a8ea..e9daa550b3 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -148,6 +148,7 @@ "scan_games_remove_folder": "Remove folder", "scan_games_folders_hint_manual": "Add at least one folder to scan for games.", "scan_games_detection_warning": "Detection relies on a community-maintained list of known games and may be outdated, so some games might not be found.", + "remove_executables_in_progress": "Checking for uninstalled game executables...", "remove_executables_result": "Removed {{removed}} of {{total}} uninstalled game executables", "remove_executables_no_results": "No uninstalled game executables found." }, diff --git a/src/main/events/library/remove-uninstalled-game-executables.ts b/src/main/events/library/remove-uninstalled-game-executables.ts index 240d68b69e..5a927b1bc5 100644 --- a/src/main/events/library/remove-uninstalled-game-executables.ts +++ b/src/main/events/library/remove-uninstalled-game-executables.ts @@ -14,7 +14,9 @@ const removeUninstalledGameExecutables = async () => { .all() .then((results) => results - .filter(([_key, game]) => game.isDeleted === false) + .filter( + ([_key, game]) => game.isDeleted === false && game.shop !== "custom" + ) .map(([key, game]) => ({ key, game })) ); diff --git a/src/renderer/src/components/header/header.tsx b/src/renderer/src/components/header/header.tsx index 0b4c0859b9..545db6c38e 100644 --- a/src/renderer/src/components/header/header.tsx +++ b/src/renderer/src/components/header/header.tsx @@ -104,6 +104,7 @@ export function Header() { }); const [showScanModal, setShowScanModal] = useState(false); const [isScanning, setIsScanning] = useState(false); + const [isRemovingExecutables, setIsRemovingExecutables] = useState(false); const [scanResult, setScanResult] = useState<{ foundGames: { title: string; executablePath: string }[]; total: number; @@ -323,9 +324,10 @@ export function Header() { additionalDirectories: string[] = [], includeDefaultDirectories = true ) => { - if (isScanning) return; + if (isScanning || isRemovingExecutables) return; setIsScanning(true); + setIsRemovingExecutables(false); setScanResult(null); setRemoveExeResult(null); @@ -335,11 +337,14 @@ export function Header() { includeDefaultDirectories ); setScanResult(result); + setIsScanning(false); + setIsRemovingExecutables(true); const exeResult = await window.electron.removeUninstalledGameExecutables(); setRemoveExeResult(exeResult); } finally { setIsScanning(false); + setIsRemovingExecutables(false); } }; @@ -488,6 +493,7 @@ export function Header() { visible={showScanModal} onClose={() => setShowScanModal(false)} isScanning={isScanning} + isRemovingExecutables={isRemovingExecutables} scanResult={scanResult} removeExecutableResult={removeExeResult} onStartScan={handleStartScan} diff --git a/src/renderer/src/components/header/scan-games-modal.tsx b/src/renderer/src/components/header/scan-games-modal.tsx index 515b47686a..a27aa1e515 100644 --- a/src/renderer/src/components/header/scan-games-modal.tsx +++ b/src/renderer/src/components/header/scan-games-modal.tsx @@ -33,6 +33,7 @@ export interface ScanGamesModalProps { visible: boolean; onClose: () => void; isScanning: boolean; + isRemovingExecutables: boolean; scanResult: ScanResult | null; removeExecutableResult: RemoveExecutableResult | null; onStartScan: ( @@ -46,6 +47,7 @@ export function ScanGamesModal({ visible, onClose, isScanning, + isRemovingExecutables, scanResult, removeExecutableResult, onStartScan, @@ -103,7 +105,7 @@ export function ScanGamesModal({ visible={visible} title={t("scan_games_title")} onClose={handleClose} - clickOutsideToClose={!isScanning} + clickOutsideToClose={!isScanning && !isRemovingExecutables} >
{!scanResult && !isScanning && ( @@ -232,6 +234,15 @@ export function ScanGamesModal({

)} + {isRemovingExecutables && !removeExecutableResult && ( +
+ +

+ {t("remove_executables_in_progress")} +

+
+ )} + {removeExecutableResult && (removeExecutableResult.removedGames.length > 0 ? ( <> @@ -266,7 +277,9 @@ export function ScanGamesModal({
)} {scanResult && ( - )} From 378fae9e6e9fbbbb2ab4d77d58021d1317e2a323 Mon Sep 17 00:00:00 2001 From: IsaiahBideshi Date: Mon, 15 Jun 2026 08:03:42 -0400 Subject: [PATCH 3/5] fix: scan excludes stale game executables -> clear paths before scan. --- src/renderer/src/components/header/header.tsx | 16 +++++++------ .../components/header/scan-games-modal.tsx | 24 ++++++++++--------- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/renderer/src/components/header/header.tsx b/src/renderer/src/components/header/header.tsx index 545db6c38e..46509f24b1 100644 --- a/src/renderer/src/components/header/header.tsx +++ b/src/renderer/src/components/header/header.tsx @@ -326,22 +326,24 @@ export function Header() { ) => { if (isScanning || isRemovingExecutables) return; - setIsScanning(true); - setIsRemovingExecutables(false); + setIsScanning(false); + setIsRemovingExecutables(true); setScanResult(null); setRemoveExeResult(null); try { + const exeResult = + await window.electron.removeUninstalledGameExecutables(); + setRemoveExeResult(exeResult); + + setIsRemovingExecutables(false); + setIsScanning(true); + const result = await window.electron.scanInstalledGames( additionalDirectories, includeDefaultDirectories ); setScanResult(result); - setIsScanning(false); - setIsRemovingExecutables(true); - const exeResult = - await window.electron.removeUninstalledGameExecutables(); - setRemoveExeResult(exeResult); } finally { setIsScanning(false); setIsRemovingExecutables(false); diff --git a/src/renderer/src/components/header/scan-games-modal.tsx b/src/renderer/src/components/header/scan-games-modal.tsx index a27aa1e515..1bfaa9979e 100644 --- a/src/renderer/src/components/header/scan-games-modal.tsx +++ b/src/renderer/src/components/header/scan-games-modal.tsx @@ -201,6 +201,15 @@ export function ScanGamesModal({
)} + {isRemovingExecutables && !removeExecutableResult && !scanResult && ( +
+ +

+ {t("remove_executables_in_progress")} +

+
+ )} + {scanResult && (
{scanResult.foundGames.length > 0 ? ( @@ -234,15 +243,6 @@ export function ScanGamesModal({

)} - {isRemovingExecutables && !removeExecutableResult && ( -
- -

- {t("remove_executables_in_progress")} -

-
- )} - {removeExecutableResult && (removeExecutableResult.removedGames.length > 0 ? ( <> @@ -280,14 +280,16 @@ export function ScanGamesModal({ ? isRemovingExecutables ? t("scan_games_hide") : t("scan_games_close") - : isScanning + : isScanning || isRemovingExecutables ? t("scan_games_hide") : t("scan_games_cancel")} {!scanResult && ( From 6b368627f98622780e32aa09b4a51ebf1f089b1d Mon Sep 17 00:00:00 2001 From: IsaiahBideshi Date: Wed, 17 Jun 2026 07:21:27 -0400 Subject: [PATCH 4/5] fix: disable scan options and remove scan button while syncing --- src/renderer/src/components/header/header.tsx | 3 ++- src/renderer/src/components/header/scan-games-modal.tsx | 8 +++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/components/header/header.tsx b/src/renderer/src/components/header/header.tsx index 46509f24b1..f4817146a9 100644 --- a/src/renderer/src/components/header/header.tsx +++ b/src/renderer/src/components/header/header.tsx @@ -408,7 +408,8 @@ export function Header() { - {!scanResult && ( + {!scanResult && !isRemovingExecutables && !isScanning && ( From 683131e78f2f2f2942b4f6246daa1f3266cfe0ff Mon Sep 17 00:00:00 2001 From: IsaiahBideshi Date: Mon, 22 Jun 2026 08:45:11 -0400 Subject: [PATCH 5/5] removed total from output, sonar fixes. --- src/locales/en/translation.json | 2 +- .../remove-uninstalled-game-executables.ts | 2 +- src/renderer/src/components/header/header.tsx | 3 +-- .../components/header/scan-games-modal.tsx | 22 +++++++++++-------- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index cdf33a19cc..fd2786bf18 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -153,7 +153,7 @@ "scan_games_folders_hint_manual": "Add at least one folder to scan for games.", "scan_games_detection_warning": "Detection relies on a community-maintained list of known games and may be outdated, so some games might not be found.", "remove_executables_in_progress": "Checking for uninstalled game executables...", - "remove_executables_result": "Removed {{removed}} of {{total}} uninstalled game executables", + "remove_executables_result": "Removed {{removed}} stale game executable paths", "remove_executables_no_results": "No uninstalled game executables found." }, "bottom_panel": { diff --git a/src/main/events/library/remove-uninstalled-game-executables.ts b/src/main/events/library/remove-uninstalled-game-executables.ts index 5a927b1bc5..102af04c8a 100644 --- a/src/main/events/library/remove-uninstalled-game-executables.ts +++ b/src/main/events/library/remove-uninstalled-game-executables.ts @@ -42,7 +42,7 @@ const removeUninstalledGameExecutables = async () => { if (removedGames.length > 0) { WindowManager.sendToAppWindows("on-library-batch-complete"); } - return { removedGames: removedGames, total: gamesToCheck.length }; + return { removedGames: removedGames }; }; registerEvent( diff --git a/src/renderer/src/components/header/header.tsx b/src/renderer/src/components/header/header.tsx index f4817146a9..a89bd015ff 100644 --- a/src/renderer/src/components/header/header.tsx +++ b/src/renderer/src/components/header/header.tsx @@ -111,7 +111,6 @@ export function Header() { } | null>(null); const [removeExeResult, setRemoveExeResult] = useState<{ removedGames: { title: string }[]; - total: number; } | null>(null); const { t } = useTranslation("header"); @@ -333,7 +332,7 @@ export function Header() { try { const exeResult = - await window.electron.removeUninstalledGameExecutables(); + await globalThis.electron.removeUninstalledGameExecutables(); setRemoveExeResult(exeResult); setIsRemovingExecutables(false); diff --git a/src/renderer/src/components/header/scan-games-modal.tsx b/src/renderer/src/components/header/scan-games-modal.tsx index 4634098d71..f4b48d4349 100644 --- a/src/renderer/src/components/header/scan-games-modal.tsx +++ b/src/renderer/src/components/header/scan-games-modal.tsx @@ -26,7 +26,6 @@ interface ScanResult { interface RemoveExecutableResult { removedGames: { title: string }[]; - total: number; } export interface ScanGamesModalProps { @@ -100,6 +99,18 @@ export function ScanGamesModal({ setSelectedFolders((prev) => prev.filter((item) => item !== folder)); }; + let closeButtonLabel: string; + + if (scanResult) { + closeButtonLabel = isRemovingExecutables + ? t("scan_games_hide") + : t("scan_games_close"); + } else if (isScanning || isRemovingExecutables) { + closeButtonLabel = t("scan_games_hide"); + } else { + closeButtonLabel = t("scan_games_cancel"); + } + return ( {t("remove_executables_result", { removed: removeExecutableResult.removedGames.length, - total: removeExecutableResult.total, })}

@@ -276,13 +286,7 @@ export function ScanGamesModal({
{!scanResult && !isRemovingExecutables && !isScanning && (