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,
+ })}
+
+
+
+ {removeExecutableResult.removedGames.map((game, index) => (
+ -
+
+ {game.title}
+
+
+ ))}
+
+ >
+ ) : (
+
+ {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 && (
-
)}
+ {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 && (
{t("scan_games_start")}
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() {
setShowScanModal(true)}
data-tooltip-id={scanButtonTooltipId}
diff --git a/src/renderer/src/components/header/scan-games-modal.tsx b/src/renderer/src/components/header/scan-games-modal.tsx
index 1bfaa9979e..4634098d71 100644
--- a/src/renderer/src/components/header/scan-games-modal.tsx
+++ b/src/renderer/src/components/header/scan-games-modal.tsx
@@ -108,7 +108,7 @@ export function ScanGamesModal({
clickOutsideToClose={!isScanning && !isRemovingExecutables}
>
- {!scanResult && !isScanning && (
+ {!scanResult && !isScanning && !isRemovingExecutables && (
<>
{isWindows && (
@@ -284,12 +284,10 @@ export function ScanGamesModal({
? t("scan_games_hide")
: t("scan_games_cancel")}
- {!scanResult && (
+ {!scanResult && !isRemovingExecutables && !isScanning && (
{t("scan_games_start")}
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
- ? t("scan_games_hide")
- : t("scan_games_close")
- : isScanning || isRemovingExecutables
- ? t("scan_games_hide")
- : t("scan_games_cancel")}
+ {closeButtonLabel}
{!scanResult && !isRemovingExecutables && !isScanning && (