From 101bc3546086123c80dce7279bac273407992a86 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Thu, 30 Oct 2025 23:21:31 +0200 Subject: [PATCH 01/14] feat: sidebar badge on new game download option --- .../downloadSourcesCheckTimestamp.ts | 16 +++ src/main/level/sublevels/index.ts | 1 + src/main/level/sublevels/keys.ts | 1 + src/main/main.ts | 4 + src/main/services/download-sources-checker.ts | 127 ++++++++++++++++++ src/main/services/hydra-api.ts | 32 +++++ src/main/services/index.ts | 1 + src/preload/index.ts | 11 ++ src/renderer/src/app.tsx | 4 + .../components/sidebar/sidebar-game-item.tsx | 7 + .../src/components/sidebar/sidebar.scss | 20 +++ src/renderer/src/declaration.d.ts | 5 + src/renderer/src/features/library-slice.ts | 20 ++- src/renderer/src/hooks/index.ts | 1 + .../hooks/use-download-options-listener.ts | 19 +++ src/types/level.types.ts | 1 + 16 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 src/main/level/sublevels/downloadSourcesCheckTimestamp.ts create mode 100644 src/main/services/download-sources-checker.ts create mode 100644 src/renderer/src/hooks/use-download-options-listener.ts diff --git a/src/main/level/sublevels/downloadSourcesCheckTimestamp.ts b/src/main/level/sublevels/downloadSourcesCheckTimestamp.ts new file mode 100644 index 00000000..13dbf682 --- /dev/null +++ b/src/main/level/sublevels/downloadSourcesCheckTimestamp.ts @@ -0,0 +1,16 @@ +import { levelKeys } from "./keys"; +import { db } from "../level"; + +export const getLastDownloadSourcesCheck = async (): Promise => { + try { + const timestamp = await db.get(levelKeys.lastDownloadSourcesCheck); + return timestamp; + } catch (error) { + // Key doesn't exist yet + return null; + } +}; + +export const updateLastDownloadSourcesCheck = async (timestamp: string): Promise => { + await db.put(levelKeys.lastDownloadSourcesCheck, timestamp); +}; \ No newline at end of file diff --git a/src/main/level/sublevels/index.ts b/src/main/level/sublevels/index.ts index 3619ae26..4575bbc4 100644 --- a/src/main/level/sublevels/index.ts +++ b/src/main/level/sublevels/index.ts @@ -7,3 +7,4 @@ export * from "./game-achievements"; export * from "./keys"; export * from "./themes"; export * from "./download-sources"; +export * from "./downloadSourcesCheckTimestamp"; diff --git a/src/main/level/sublevels/keys.ts b/src/main/level/sublevels/keys.ts index a28690b2..536f9dca 100644 --- a/src/main/level/sublevels/keys.ts +++ b/src/main/level/sublevels/keys.ts @@ -18,4 +18,5 @@ export const levelKeys = { screenState: "screenState", rpcPassword: "rpcPassword", downloadSources: "downloadSources", + lastDownloadSourcesCheck: "lastDownloadSourcesCheck", }; diff --git a/src/main/main.ts b/src/main/main.ts index ffb8f8a9..8e1264d7 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -16,6 +16,7 @@ import { Ludusavi, Lock, DeckyPlugin, + DownloadSourcesChecker, } from "@main/services"; import { migrateDownloadSources } from "./helpers/migrate-download-sources"; @@ -56,6 +57,9 @@ export const loadState = async () => { const { syncDownloadSourcesFromApi } = await import("./services/user"); void syncDownloadSourcesFromApi(); + + // Check for new download options on startup + void DownloadSourcesChecker.checkForChanges(); // WSClient.connect(); }); diff --git a/src/main/services/download-sources-checker.ts b/src/main/services/download-sources-checker.ts new file mode 100644 index 00000000..f22f12fc --- /dev/null +++ b/src/main/services/download-sources-checker.ts @@ -0,0 +1,127 @@ +import { HydraApi } from "./hydra-api"; +import { gamesSublevel, getLastDownloadSourcesCheck, updateLastDownloadSourcesCheck, downloadSourcesSublevel } from "@main/level"; +import { logger } from "./logger"; +import { WindowManager } from "./window-manager"; +import type { Game } from "@types"; + +interface DownloadSourcesChangeResponse { + shop: string; + objectId: string; + newDownloadOptionsCount: number; + downloadSourceIds: string[]; +} + +export class DownloadSourcesChecker { + static async checkForChanges(): Promise { + logger.info("DownloadSourcesChecker.checkForChanges() called"); + + try { + // Get all installed games (excluding custom games) + const installedGames = await gamesSublevel.values().all(); + const nonCustomGames = installedGames.filter((game: Game) => game.shop !== 'custom'); + logger.info(`Found ${installedGames.length} total games, ${nonCustomGames.length} non-custom games`); + + if (nonCustomGames.length === 0) { + logger.info("No non-custom games found, skipping download sources check"); + return; + } + + // Get download sources + const downloadSources = await downloadSourcesSublevel.values().all(); + const downloadSourceIds = downloadSources.map(source => source.id); + logger.info(`Found ${downloadSourceIds.length} download sources: ${downloadSourceIds.join(', ')}`); + + if (downloadSourceIds.length === 0) { + logger.info("No download sources found, skipping download sources check"); + return; + } + + // Get last check timestamp or use a default (24 hours ago) + const lastCheck = await getLastDownloadSourcesCheck(); + const since = lastCheck || new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); + logger.info(`Last check: ${lastCheck}, using since: ${since}`); + + // Clear any previously stored new download option counts so badges don't persist across restarts + const previouslyFlaggedGames = nonCustomGames.filter( + (game: Game) => (game as Game).newDownloadOptionsCount && (game as Game).newDownloadOptionsCount! > 0 + ); + + const clearedPayload: { gameId: string; count: number }[] = []; + if (previouslyFlaggedGames.length > 0) { + logger.info(`Clearing stale newDownloadOptionsCount for ${previouslyFlaggedGames.length} games`); + for (const game of previouslyFlaggedGames) { + await gamesSublevel.put(`${game.shop}:${game.objectId}`, { + ...game, + newDownloadOptionsCount: undefined, + }); + clearedPayload.push({ gameId: `${game.shop}:${game.objectId}`, count: 0 }); + } + } + + // Prepare games array for API call (excluding custom games) + const games = nonCustomGames.map((game: Game) => ({ + shop: game.shop, + objectId: game.objectId + })); + + logger.info(`Checking download sources changes for ${games.length} non-custom games since ${since}`); + logger.info(`Making API call to HydraApi.checkDownloadSourcesChanges with:`, { + downloadSourceIds, + gamesCount: games.length, + since + }); + + // Call the API + const response = await HydraApi.checkDownloadSourcesChanges( + downloadSourceIds, + games, + since + ); + + logger.info("API call completed, response:", response); + + // Update the last check timestamp + await updateLastDownloadSourcesCheck(new Date().toISOString()); + + // Process the response and store newDownloadOptionsCount for games with new options + if (response && Array.isArray(response)) { + const gamesWithNewOptions: { gameId: string; count: number }[] = []; + + for (const gameUpdate of response as DownloadSourcesChangeResponse[]) { + if (gameUpdate.newDownloadOptionsCount > 0) { + const game = nonCustomGames.find(g => + g.shop === gameUpdate.shop && g.objectId === gameUpdate.objectId + ); + + if (game) { + // Store the new download options count in the game data + await gamesSublevel.put(`${game.shop}:${game.objectId}`, { + ...game, + newDownloadOptionsCount: gameUpdate.newDownloadOptionsCount + }); + + gamesWithNewOptions.push({ + gameId: `${game.shop}:${game.objectId}`, + count: gameUpdate.newDownloadOptionsCount + }); + + logger.info(`Game ${game.title} has ${gameUpdate.newDownloadOptionsCount} new download options`); + } + } + } + + // Send IPC event to renderer to clear stale badges and set fresh counts from response + const eventPayload = [...clearedPayload, ...gamesWithNewOptions]; + if (eventPayload.length > 0 && WindowManager.mainWindow) { + WindowManager.mainWindow.webContents.send("on-new-download-options", eventPayload); + } + + logger.info(`Found new download options for ${gamesWithNewOptions.length} games`); + } + + logger.info("Download sources check completed successfully"); + } catch (error) { + logger.error("Failed to check download sources changes:", error); + } + } +} \ No newline at end of file diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 12090df3..4f7091db 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -399,4 +399,36 @@ export class HydraApi { .then((response) => response.data) .catch(this.handleUnauthorizedError); } + + static async checkDownloadSourcesChanges( + downloadSourceIds: string[], + games: Array<{ shop: string; objectId: string }>, + since: string + ) { + logger.info("HydraApi.checkDownloadSourcesChanges called with:", { + downloadSourceIds, + gamesCount: games.length, + since, + isLoggedIn: this.isLoggedIn() + }); + + try { + const result = await this.post>("/download-sources/changes", { + downloadSourceIds, + games, + since, + }, { needsAuth: true }); + + logger.info("HydraApi.checkDownloadSourcesChanges completed successfully:", result); + return result; + } catch (error) { + logger.error("HydraApi.checkDownloadSourcesChanges failed:", error); + throw error; + } + } } diff --git a/src/main/services/index.ts b/src/main/services/index.ts index da4e6848..a3891dc6 100644 --- a/src/main/services/index.ts +++ b/src/main/services/index.ts @@ -19,3 +19,4 @@ export * from "./wine"; export * from "./lock"; export * from "./decky-plugin"; export * from "./user"; +export * from "./download-sources-checker"; diff --git a/src/preload/index.ts b/src/preload/index.ts index f89ec4db..a1a0d959 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -580,6 +580,17 @@ contextBridge.exposeInMainWorld("electron", { return () => ipcRenderer.removeListener("on-custom-theme-updated", listener); }, + onNewDownloadOptions: ( + cb: (gamesWithNewOptions: { gameId: string; count: number }[]) => void + ) => { + const listener = ( + _event: Electron.IpcRendererEvent, + gamesWithNewOptions: { gameId: string; count: number }[] + ) => cb(gamesWithNewOptions); + ipcRenderer.on("on-new-download-options", listener); + return () => + ipcRenderer.removeListener("on-new-download-options", listener); + }, closeEditorWindow: (themeId?: string) => ipcRenderer.invoke("closeEditorWindow", themeId), }); diff --git a/src/renderer/src/app.tsx b/src/renderer/src/app.tsx index 168a4435..274e95db 100644 --- a/src/renderer/src/app.tsx +++ b/src/renderer/src/app.tsx @@ -10,6 +10,7 @@ import { useToast, useUserDetails, } from "@renderer/hooks"; +import { useDownloadOptionsListener } from "@renderer/hooks/use-download-options-listener"; import { Outlet, useLocation, useNavigate } from "react-router-dom"; import { @@ -36,6 +37,9 @@ export function App() { const contentRef = useRef(null); const { updateLibrary, library } = useLibrary(); + // Listen for new download options updates + useDownloadOptionsListener(); + const { t } = useTranslation("app"); const { clearDownload, setLastPacket } = useDownload(); diff --git a/src/renderer/src/components/sidebar/sidebar-game-item.tsx b/src/renderer/src/components/sidebar/sidebar-game-item.tsx index 356aa913..add7e081 100644 --- a/src/renderer/src/components/sidebar/sidebar-game-item.tsx +++ b/src/renderer/src/components/sidebar/sidebar-game-item.tsx @@ -80,6 +80,13 @@ export function SidebarGameItem({ {getGameTitle(game)} + + {game.newDownloadOptionsCount && game.newDownloadOptionsCount > 0 && ( + +
+
+
{game.newDownloadOptionsCount}
+
+ )} diff --git a/src/renderer/src/components/sidebar/sidebar.scss b/src/renderer/src/components/sidebar/sidebar.scss index 49f6e007..9068e2b5 100644 --- a/src/renderer/src/components/sidebar/sidebar.scss +++ b/src/renderer/src/components/sidebar/sidebar.scss @@ -115,6 +115,26 @@ background-size: cover; } + &__game-badge { + background: rgba(255, 255, 255, 0.1);; + color: #fff; + font-size: 10px; + font-weight: bold; + padding: 4px 6px; + border-radius: 6px; + display: flex; + margin-left: auto; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + gap: calc(globals.$spacing-unit * 0.35); + } + + &__game-badge-plus, + &__game-badge-count { + display: flex; + align-items: center; + justify-content: center; + } + &__section-header { display: flex; justify-content: space-between; diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index fa4ab3d6..30b5c67a 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -414,6 +414,11 @@ declare global { openEditorWindow: (themeId: string) => Promise; onCustomThemeUpdated: (cb: () => void) => () => Electron.IpcRenderer; closeEditorWindow: (themeId?: string) => Promise; + + /* Download Options */ + onNewDownloadOptions: ( + cb: (gamesWithNewOptions: { gameId: string; count: number }[]) => void + ) => () => Electron.IpcRenderer; } interface Window { diff --git a/src/renderer/src/features/library-slice.ts b/src/renderer/src/features/library-slice.ts index 6c95aa79..e92e6a25 100644 --- a/src/renderer/src/features/library-slice.ts +++ b/src/renderer/src/features/library-slice.ts @@ -18,7 +18,25 @@ export const librarySlice = createSlice({ setLibrary: (state, action: PayloadAction) => { state.value = action.payload; }, + updateGameNewDownloadOptions: ( + state, + action: PayloadAction<{ gameId: string; count: number }> + ) => { + const game = state.value.find((g) => g.id === action.payload.gameId); + if (game) { + game.newDownloadOptionsCount = action.payload.count; + } + }, + clearNewDownloadOptions: ( + state, + action: PayloadAction<{ gameId: string }> + ) => { + const game = state.value.find((g) => g.id === action.payload.gameId); + if (game) { + game.newDownloadOptionsCount = undefined; + } + }, }, }); -export const { setLibrary } = librarySlice.actions; +export const { setLibrary, updateGameNewDownloadOptions, clearNewDownloadOptions } = librarySlice.actions; diff --git a/src/renderer/src/hooks/index.ts b/src/renderer/src/hooks/index.ts index 73733e2b..2d2ee02f 100644 --- a/src/renderer/src/hooks/index.ts +++ b/src/renderer/src/hooks/index.ts @@ -6,3 +6,4 @@ export * from "./redux"; export * from "./use-user-details"; export * from "./use-format"; export * from "./use-feature"; +export * from "./use-download-options-listener"; diff --git a/src/renderer/src/hooks/use-download-options-listener.ts b/src/renderer/src/hooks/use-download-options-listener.ts new file mode 100644 index 00000000..f0268335 --- /dev/null +++ b/src/renderer/src/hooks/use-download-options-listener.ts @@ -0,0 +1,19 @@ +import { useEffect } from "react"; +import { useAppDispatch } from "./redux"; +import { updateGameNewDownloadOptions } from "@renderer/features"; + +export function useDownloadOptionsListener() { + const dispatch = useAppDispatch(); + + useEffect(() => { + const unsubscribe = window.electron.onNewDownloadOptions( + (gamesWithNewOptions) => { + gamesWithNewOptions.forEach(({ gameId, count }) => { + dispatch(updateGameNewDownloadOptions({ gameId, count })); + }); + } + ); + + return unsubscribe; + }, [dispatch]); +} \ No newline at end of file diff --git a/src/types/level.types.ts b/src/types/level.types.ts index 1df55b9e..ff602ac9 100644 --- a/src/types/level.types.ts +++ b/src/types/level.types.ts @@ -59,6 +59,7 @@ export interface Game { pinnedDate?: Date | null; automaticCloudSync?: boolean; hasManuallyUpdatedPlaytime?: boolean; + newDownloadOptionsCount?: number; } export interface Download { From 4dd3c9de76e0765a3b001c01577bbf2cf56d7a33 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Thu, 30 Oct 2025 23:26:22 +0200 Subject: [PATCH 02/14] fix: formatting --- .../downloadSourcesCheckTimestamp.ts | 6 +- src/main/main.ts | 2 +- src/main/services/download-sources-checker.ts | 106 ++++++++++++------ src/main/services/hydra-api.ts | 35 +++--- .../components/sidebar/sidebar-game-item.tsx | 4 +- .../src/components/sidebar/sidebar.scss | 2 +- src/renderer/src/features/library-slice.ts | 6 +- .../hooks/use-download-options-listener.ts | 2 +- 8 files changed, 108 insertions(+), 55 deletions(-) diff --git a/src/main/level/sublevels/downloadSourcesCheckTimestamp.ts b/src/main/level/sublevels/downloadSourcesCheckTimestamp.ts index 13dbf682..f7071932 100644 --- a/src/main/level/sublevels/downloadSourcesCheckTimestamp.ts +++ b/src/main/level/sublevels/downloadSourcesCheckTimestamp.ts @@ -11,6 +11,8 @@ export const getLastDownloadSourcesCheck = async (): Promise => { } }; -export const updateLastDownloadSourcesCheck = async (timestamp: string): Promise => { +export const updateLastDownloadSourcesCheck = async ( + timestamp: string +): Promise => { await db.put(levelKeys.lastDownloadSourcesCheck, timestamp); -}; \ No newline at end of file +}; diff --git a/src/main/main.ts b/src/main/main.ts index 8e1264d7..50173390 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -57,7 +57,7 @@ export const loadState = async () => { const { syncDownloadSourcesFromApi } = await import("./services/user"); void syncDownloadSourcesFromApi(); - + // Check for new download options on startup void DownloadSourcesChecker.checkForChanges(); // WSClient.connect(); diff --git a/src/main/services/download-sources-checker.ts b/src/main/services/download-sources-checker.ts index f22f12fc..d2ccb531 100644 --- a/src/main/services/download-sources-checker.ts +++ b/src/main/services/download-sources-checker.ts @@ -1,5 +1,10 @@ import { HydraApi } from "./hydra-api"; -import { gamesSublevel, getLastDownloadSourcesCheck, updateLastDownloadSourcesCheck, downloadSourcesSublevel } from "@main/level"; +import { + gamesSublevel, + getLastDownloadSourcesCheck, + updateLastDownloadSourcesCheck, + downloadSourcesSublevel, +} from "@main/level"; import { logger } from "./logger"; import { WindowManager } from "./window-manager"; import type { Game } from "@types"; @@ -14,62 +19,85 @@ interface DownloadSourcesChangeResponse { export class DownloadSourcesChecker { static async checkForChanges(): Promise { logger.info("DownloadSourcesChecker.checkForChanges() called"); - + try { // Get all installed games (excluding custom games) const installedGames = await gamesSublevel.values().all(); - const nonCustomGames = installedGames.filter((game: Game) => game.shop !== 'custom'); - logger.info(`Found ${installedGames.length} total games, ${nonCustomGames.length} non-custom games`); - + const nonCustomGames = installedGames.filter( + (game: Game) => game.shop !== "custom" + ); + logger.info( + `Found ${installedGames.length} total games, ${nonCustomGames.length} non-custom games` + ); + if (nonCustomGames.length === 0) { - logger.info("No non-custom games found, skipping download sources check"); + logger.info( + "No non-custom games found, skipping download sources check" + ); return; } // Get download sources const downloadSources = await downloadSourcesSublevel.values().all(); - const downloadSourceIds = downloadSources.map(source => source.id); - logger.info(`Found ${downloadSourceIds.length} download sources: ${downloadSourceIds.join(', ')}`); + const downloadSourceIds = downloadSources.map((source) => source.id); + logger.info( + `Found ${downloadSourceIds.length} download sources: ${downloadSourceIds.join(", ")}` + ); if (downloadSourceIds.length === 0) { - logger.info("No download sources found, skipping download sources check"); + logger.info( + "No download sources found, skipping download sources check" + ); return; } // Get last check timestamp or use a default (24 hours ago) const lastCheck = await getLastDownloadSourcesCheck(); - const since = lastCheck || new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); + const since = + lastCheck || new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); logger.info(`Last check: ${lastCheck}, using since: ${since}`); // Clear any previously stored new download option counts so badges don't persist across restarts const previouslyFlaggedGames = nonCustomGames.filter( - (game: Game) => (game as Game).newDownloadOptionsCount && (game as Game).newDownloadOptionsCount! > 0 + (game: Game) => + (game as Game).newDownloadOptionsCount && + (game as Game).newDownloadOptionsCount! > 0 ); const clearedPayload: { gameId: string; count: number }[] = []; if (previouslyFlaggedGames.length > 0) { - logger.info(`Clearing stale newDownloadOptionsCount for ${previouslyFlaggedGames.length} games`); + logger.info( + `Clearing stale newDownloadOptionsCount for ${previouslyFlaggedGames.length} games` + ); for (const game of previouslyFlaggedGames) { await gamesSublevel.put(`${game.shop}:${game.objectId}`, { ...game, newDownloadOptionsCount: undefined, }); - clearedPayload.push({ gameId: `${game.shop}:${game.objectId}`, count: 0 }); + clearedPayload.push({ + gameId: `${game.shop}:${game.objectId}`, + count: 0, + }); } } // Prepare games array for API call (excluding custom games) const games = nonCustomGames.map((game: Game) => ({ shop: game.shop, - objectId: game.objectId + objectId: game.objectId, })); - logger.info(`Checking download sources changes for ${games.length} non-custom games since ${since}`); - logger.info(`Making API call to HydraApi.checkDownloadSourcesChanges with:`, { - downloadSourceIds, - gamesCount: games.length, - since - }); + logger.info( + `Checking download sources changes for ${games.length} non-custom games since ${since}` + ); + logger.info( + `Making API call to HydraApi.checkDownloadSourcesChanges with:`, + { + downloadSourceIds, + gamesCount: games.length, + since, + } + ); // Call the API const response = await HydraApi.checkDownloadSourcesChanges( @@ -77,7 +105,7 @@ export class DownloadSourcesChecker { games, since ); - + logger.info("API call completed, response:", response); // Update the last check timestamp @@ -86,37 +114,45 @@ export class DownloadSourcesChecker { // Process the response and store newDownloadOptionsCount for games with new options if (response && Array.isArray(response)) { const gamesWithNewOptions: { gameId: string; count: number }[] = []; - + for (const gameUpdate of response as DownloadSourcesChangeResponse[]) { if (gameUpdate.newDownloadOptionsCount > 0) { - const game = nonCustomGames.find(g => - g.shop === gameUpdate.shop && g.objectId === gameUpdate.objectId + const game = nonCustomGames.find( + (g) => + g.shop === gameUpdate.shop && g.objectId === gameUpdate.objectId ); - + if (game) { // Store the new download options count in the game data await gamesSublevel.put(`${game.shop}:${game.objectId}`, { ...game, - newDownloadOptionsCount: gameUpdate.newDownloadOptionsCount + newDownloadOptionsCount: gameUpdate.newDownloadOptionsCount, }); - + gamesWithNewOptions.push({ gameId: `${game.shop}:${game.objectId}`, - count: gameUpdate.newDownloadOptionsCount + count: gameUpdate.newDownloadOptionsCount, }); - - logger.info(`Game ${game.title} has ${gameUpdate.newDownloadOptionsCount} new download options`); + + logger.info( + `Game ${game.title} has ${gameUpdate.newDownloadOptionsCount} new download options` + ); } } } - + // Send IPC event to renderer to clear stale badges and set fresh counts from response const eventPayload = [...clearedPayload, ...gamesWithNewOptions]; if (eventPayload.length > 0 && WindowManager.mainWindow) { - WindowManager.mainWindow.webContents.send("on-new-download-options", eventPayload); + WindowManager.mainWindow.webContents.send( + "on-new-download-options", + eventPayload + ); } - - logger.info(`Found new download options for ${gamesWithNewOptions.length} games`); + + logger.info( + `Found new download options for ${gamesWithNewOptions.length} games` + ); } logger.info("Download sources check completed successfully"); @@ -124,4 +160,4 @@ export class DownloadSourcesChecker { logger.error("Failed to check download sources changes:", error); } } -} \ No newline at end of file +} diff --git a/src/main/services/hydra-api.ts b/src/main/services/hydra-api.ts index 4f7091db..e7e93268 100644 --- a/src/main/services/hydra-api.ts +++ b/src/main/services/hydra-api.ts @@ -409,22 +409,31 @@ export class HydraApi { downloadSourceIds, gamesCount: games.length, since, - isLoggedIn: this.isLoggedIn() + isLoggedIn: this.isLoggedIn(), }); try { - const result = await this.post>("/download-sources/changes", { - downloadSourceIds, - games, - since, - }, { needsAuth: true }); - - logger.info("HydraApi.checkDownloadSourcesChanges completed successfully:", result); + const result = await this.post< + Array<{ + shop: string; + objectId: string; + newDownloadOptionsCount: number; + downloadSourceIds: string[]; + }> + >( + "/download-sources/changes", + { + downloadSourceIds, + games, + since, + }, + { needsAuth: true } + ); + + logger.info( + "HydraApi.checkDownloadSourcesChanges completed successfully:", + result + ); return result; } catch (error) { logger.error("HydraApi.checkDownloadSourcesChanges failed:", error); diff --git a/src/renderer/src/components/sidebar/sidebar-game-item.tsx b/src/renderer/src/components/sidebar/sidebar-game-item.tsx index add7e081..59698935 100644 --- a/src/renderer/src/components/sidebar/sidebar-game-item.tsx +++ b/src/renderer/src/components/sidebar/sidebar-game-item.tsx @@ -84,7 +84,9 @@ export function SidebarGameItem({ {game.newDownloadOptionsCount && game.newDownloadOptionsCount > 0 && (
+
-
{game.newDownloadOptionsCount}
+
+ {game.newDownloadOptionsCount} +
)} diff --git a/src/renderer/src/components/sidebar/sidebar.scss b/src/renderer/src/components/sidebar/sidebar.scss index 9068e2b5..fdb3872e 100644 --- a/src/renderer/src/components/sidebar/sidebar.scss +++ b/src/renderer/src/components/sidebar/sidebar.scss @@ -116,7 +116,7 @@ } &__game-badge { - background: rgba(255, 255, 255, 0.1);; + background: rgba(255, 255, 255, 0.1); color: #fff; font-size: 10px; font-weight: bold; diff --git a/src/renderer/src/features/library-slice.ts b/src/renderer/src/features/library-slice.ts index e92e6a25..9c399dbe 100644 --- a/src/renderer/src/features/library-slice.ts +++ b/src/renderer/src/features/library-slice.ts @@ -39,4 +39,8 @@ export const librarySlice = createSlice({ }, }); -export const { setLibrary, updateGameNewDownloadOptions, clearNewDownloadOptions } = librarySlice.actions; +export const { + setLibrary, + updateGameNewDownloadOptions, + clearNewDownloadOptions, +} = librarySlice.actions; diff --git a/src/renderer/src/hooks/use-download-options-listener.ts b/src/renderer/src/hooks/use-download-options-listener.ts index f0268335..34cc7bf7 100644 --- a/src/renderer/src/hooks/use-download-options-listener.ts +++ b/src/renderer/src/hooks/use-download-options-listener.ts @@ -16,4 +16,4 @@ export function useDownloadOptionsListener() { return unsubscribe; }, [dispatch]); -} \ No newline at end of file +} From efab242c745a8fa615f11c53a316fb1f8f1d21b6 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 31 Oct 2025 23:17:06 +0200 Subject: [PATCH 03/14] ci: showing new badge in repack-modal --- src/locales/en/translation.json | 1 + .../get-download-sources-check-baseline.ts | 13 ++ .../get-download-sources-since-value.ts | 13 ++ src/main/events/index.ts | 2 + .../downloadSourcesCheckTimestamp.ts | 51 ++++- src/main/level/sublevels/keys.ts | 3 +- src/main/services/download-sources-checker.ts | 180 +++++++++++------- src/preload/index.ts | 4 + src/renderer/src/declaration.d.ts | 2 + .../hooks/use-download-options-listener.ts | 4 +- .../game-details/modals/repacks-modal.scss | 15 ++ .../game-details/modals/repacks-modal.tsx | 48 ++++- src/types/index.ts | 1 + 13 files changed, 253 insertions(+), 84 deletions(-) create mode 100644 src/main/events/download-sources/get-download-sources-check-baseline.ts create mode 100644 src/main/events/download-sources/get-download-sources-since-value.ts diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 668f1547..7710066d 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -194,6 +194,7 @@ "download_in_progress": "Download in progress", "download_paused": "Download paused", "last_downloaded_option": "Last downloaded option", + "new_download_option": "New", "create_steam_shortcut": "Create Steam shortcut", "create_shortcut_success": "Shortcut created successfully", "you_might_need_to_restart_steam": "You might need to restart Steam to see the changes", diff --git a/src/main/events/download-sources/get-download-sources-check-baseline.ts b/src/main/events/download-sources/get-download-sources-check-baseline.ts new file mode 100644 index 00000000..2f3ab377 --- /dev/null +++ b/src/main/events/download-sources/get-download-sources-check-baseline.ts @@ -0,0 +1,13 @@ +import { getDownloadSourcesCheckBaseline } from "@main/level"; +import { registerEvent } from "../register-event"; + +const getDownloadSourcesCheckBaselineHandler = async ( + _event: Electron.IpcMainInvokeEvent +) => { + return await getDownloadSourcesCheckBaseline(); +}; + +registerEvent( + "getDownloadSourcesCheckBaseline", + getDownloadSourcesCheckBaselineHandler +); diff --git a/src/main/events/download-sources/get-download-sources-since-value.ts b/src/main/events/download-sources/get-download-sources-since-value.ts new file mode 100644 index 00000000..cbd06faf --- /dev/null +++ b/src/main/events/download-sources/get-download-sources-since-value.ts @@ -0,0 +1,13 @@ +import { getDownloadSourcesSinceValue } from "@main/level"; +import { registerEvent } from "../register-event"; + +const getDownloadSourcesSinceValueHandler = async ( + _event: Electron.IpcMainInvokeEvent +) => { + return await getDownloadSourcesSinceValue(); +}; + +registerEvent( + "getDownloadSourcesSinceValue", + getDownloadSourcesSinceValueHandler +); diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 0ab5499a..162b08d7 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -64,6 +64,8 @@ import "./user-preferences/authenticate-real-debrid"; import "./user-preferences/authenticate-torbox"; import "./download-sources/add-download-source"; import "./download-sources/sync-download-sources"; +import "./download-sources/get-download-sources-check-baseline"; +import "./download-sources/get-download-sources-since-value"; import "./auth/sign-out"; import "./auth/open-auth-window"; import "./auth/get-session-hash"; diff --git a/src/main/level/sublevels/downloadSourcesCheckTimestamp.ts b/src/main/level/sublevels/downloadSourcesCheckTimestamp.ts index f7071932..4b60b962 100644 --- a/src/main/level/sublevels/downloadSourcesCheckTimestamp.ts +++ b/src/main/level/sublevels/downloadSourcesCheckTimestamp.ts @@ -1,18 +1,59 @@ import { levelKeys } from "./keys"; import { db } from "../level"; +import { logger } from "@main/services"; -export const getLastDownloadSourcesCheck = async (): Promise => { +// Gets when we last started the app (for next API call's 'since') +export const getDownloadSourcesCheckBaseline = async (): Promise< + string | null +> => { try { - const timestamp = await db.get(levelKeys.lastDownloadSourcesCheck); + const timestamp = await db.get(levelKeys.downloadSourcesCheckBaseline); return timestamp; } catch (error) { - // Key doesn't exist yet + if (error instanceof Error && error.name === "NotFoundError") { + logger.debug("Download sources check baseline not found, returning null"); + } else { + logger.error( + "Unexpected error while getting download sources check baseline", + error + ); + } return null; } }; -export const updateLastDownloadSourcesCheck = async ( +// Updates to current time (when app starts) +export const updateDownloadSourcesCheckBaseline = async ( timestamp: string ): Promise => { - await db.put(levelKeys.lastDownloadSourcesCheck, timestamp); + const utcTimestamp = new Date(timestamp).toISOString(); + await db.put(levelKeys.downloadSourcesCheckBaseline, utcTimestamp); +}; + +// Gets the 'since' value the API used in the last check (for modal comparison) +export const getDownloadSourcesSinceValue = async (): Promise< + string | null +> => { + try { + const timestamp = await db.get(levelKeys.downloadSourcesSinceValue); + return timestamp; + } catch (error) { + if (error instanceof Error && error.name === "NotFoundError") { + logger.debug("Download sources since value not found, returning null"); + } else { + logger.error( + "Unexpected error while getting download sources since value", + error + ); + } + return null; + } +}; + +// Saves the 'since' value we used in the API call (for modal to compare against) +export const updateDownloadSourcesSinceValue = async ( + timestamp: string +): Promise => { + const utcTimestamp = new Date(timestamp).toISOString(); + await db.put(levelKeys.downloadSourcesSinceValue, utcTimestamp); }; diff --git a/src/main/level/sublevels/keys.ts b/src/main/level/sublevels/keys.ts index 536f9dca..89c33f8d 100644 --- a/src/main/level/sublevels/keys.ts +++ b/src/main/level/sublevels/keys.ts @@ -18,5 +18,6 @@ export const levelKeys = { screenState: "screenState", rpcPassword: "rpcPassword", downloadSources: "downloadSources", - lastDownloadSourcesCheck: "lastDownloadSourcesCheck", + downloadSourcesCheckBaseline: "downloadSourcesCheckBaseline", // When we last started the app + downloadSourcesSinceValue: "downloadSourcesSinceValue", // The 'since' value API used (for modal comparison) }; diff --git a/src/main/services/download-sources-checker.ts b/src/main/services/download-sources-checker.ts index d2ccb531..f8b853a7 100644 --- a/src/main/services/download-sources-checker.ts +++ b/src/main/services/download-sources-checker.ts @@ -1,8 +1,9 @@ import { HydraApi } from "./hydra-api"; import { gamesSublevel, - getLastDownloadSourcesCheck, - updateLastDownloadSourcesCheck, + getDownloadSourcesCheckBaseline, + updateDownloadSourcesCheckBaseline, + updateDownloadSourcesSinceValue, downloadSourcesSublevel, } from "@main/level"; import { logger } from "./logger"; @@ -17,6 +18,89 @@ interface DownloadSourcesChangeResponse { } export class DownloadSourcesChecker { + private static async clearStaleBadges( + nonCustomGames: Game[] + ): Promise<{ gameId: string; count: number }[]> { + const previouslyFlaggedGames = nonCustomGames.filter( + (game: Game) => + game.newDownloadOptionsCount && game.newDownloadOptionsCount > 0 + ); + + const clearedPayload: { gameId: string; count: number }[] = []; + if (previouslyFlaggedGames.length > 0) { + logger.info( + `Clearing stale newDownloadOptionsCount for ${previouslyFlaggedGames.length} games` + ); + for (const game of previouslyFlaggedGames) { + await gamesSublevel.put(`${game.shop}:${game.objectId}`, { + ...game, + newDownloadOptionsCount: undefined, + }); + clearedPayload.push({ + gameId: `${game.shop}:${game.objectId}`, + count: 0, + }); + } + } + + return clearedPayload; + } + + private static async processApiResponse( + response: unknown, + nonCustomGames: Game[] + ): Promise<{ gameId: string; count: number }[]> { + if (!response || !Array.isArray(response)) { + return []; + } + + const gamesWithNewOptions: { gameId: string; count: number }[] = []; + + for (const gameUpdate of response as DownloadSourcesChangeResponse[]) { + if (gameUpdate.newDownloadOptionsCount > 0) { + const game = nonCustomGames.find( + (g) => + g.shop === gameUpdate.shop && g.objectId === gameUpdate.objectId + ); + + if (game) { + await gamesSublevel.put(`${game.shop}:${game.objectId}`, { + ...game, + newDownloadOptionsCount: gameUpdate.newDownloadOptionsCount, + }); + + gamesWithNewOptions.push({ + gameId: `${game.shop}:${game.objectId}`, + count: gameUpdate.newDownloadOptionsCount, + }); + + logger.info( + `Game ${game.title} has ${gameUpdate.newDownloadOptionsCount} new download options` + ); + } + } + } + + return gamesWithNewOptions; + } + + private static sendNewDownloadOptionsEvent( + clearedPayload: { gameId: string; count: number }[], + gamesWithNewOptions: { gameId: string; count: number }[] + ): void { + const eventPayload = [...clearedPayload, ...gamesWithNewOptions]; + if (eventPayload.length > 0 && WindowManager.mainWindow) { + WindowManager.mainWindow.webContents.send( + "on-new-download-options", + eventPayload + ); + } + + logger.info( + `Found new download options for ${gamesWithNewOptions.length} games` + ); + } + static async checkForChanges(): Promise { logger.info("DownloadSourcesChecker.checkForChanges() called"); @@ -51,35 +135,16 @@ export class DownloadSourcesChecker { return; } - // Get last check timestamp or use a default (24 hours ago) - const lastCheck = await getLastDownloadSourcesCheck(); + // Get when we LAST started the app (for this check's 'since' parameter) + const previousBaseline = await getDownloadSourcesCheckBaseline(); const since = - lastCheck || new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); - logger.info(`Last check: ${lastCheck}, using since: ${since}`); + previousBaseline || + new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(); + + logger.info(`Using since: ${since} (from last app start)`); // Clear any previously stored new download option counts so badges don't persist across restarts - const previouslyFlaggedGames = nonCustomGames.filter( - (game: Game) => - (game as Game).newDownloadOptionsCount && - (game as Game).newDownloadOptionsCount! > 0 - ); - - const clearedPayload: { gameId: string; count: number }[] = []; - if (previouslyFlaggedGames.length > 0) { - logger.info( - `Clearing stale newDownloadOptionsCount for ${previouslyFlaggedGames.length} games` - ); - for (const game of previouslyFlaggedGames) { - await gamesSublevel.put(`${game.shop}:${game.objectId}`, { - ...game, - newDownloadOptionsCount: undefined, - }); - clearedPayload.push({ - gameId: `${game.shop}:${game.objectId}`, - count: 0, - }); - } - } + const clearedPayload = await this.clearStaleBadges(nonCustomGames); // Prepare games array for API call (excluding custom games) const games = nonCustomGames.map((game: Game) => ({ @@ -108,52 +173,25 @@ export class DownloadSourcesChecker { logger.info("API call completed, response:", response); - // Update the last check timestamp - await updateLastDownloadSourcesCheck(new Date().toISOString()); + // Save the 'since' value we just used (for modal to compare against) + await updateDownloadSourcesSinceValue(since); + logger.info(`Saved 'since' value: ${since} (for modal comparison)`); + + // Update baseline to NOW (for next app start's 'since') + const now = new Date().toISOString(); + await updateDownloadSourcesCheckBaseline(now); + logger.info( + `Updated baseline to: ${now} (will be 'since' on next app start)` + ); // Process the response and store newDownloadOptionsCount for games with new options - if (response && Array.isArray(response)) { - const gamesWithNewOptions: { gameId: string; count: number }[] = []; + const gamesWithNewOptions = await this.processApiResponse( + response, + nonCustomGames + ); - for (const gameUpdate of response as DownloadSourcesChangeResponse[]) { - if (gameUpdate.newDownloadOptionsCount > 0) { - const game = nonCustomGames.find( - (g) => - g.shop === gameUpdate.shop && g.objectId === gameUpdate.objectId - ); - - if (game) { - // Store the new download options count in the game data - await gamesSublevel.put(`${game.shop}:${game.objectId}`, { - ...game, - newDownloadOptionsCount: gameUpdate.newDownloadOptionsCount, - }); - - gamesWithNewOptions.push({ - gameId: `${game.shop}:${game.objectId}`, - count: gameUpdate.newDownloadOptionsCount, - }); - - logger.info( - `Game ${game.title} has ${gameUpdate.newDownloadOptionsCount} new download options` - ); - } - } - } - - // Send IPC event to renderer to clear stale badges and set fresh counts from response - const eventPayload = [...clearedPayload, ...gamesWithNewOptions]; - if (eventPayload.length > 0 && WindowManager.mainWindow) { - WindowManager.mainWindow.webContents.send( - "on-new-download-options", - eventPayload - ); - } - - logger.info( - `Found new download options for ${gamesWithNewOptions.length} games` - ); - } + // Send IPC event to renderer to clear stale badges and set fresh counts from response + this.sendNewDownloadOptionsEvent(clearedPayload, gamesWithNewOptions); logger.info("Download sources check completed successfully"); } catch (error) { diff --git a/src/preload/index.ts b/src/preload/index.ts index a1a0d959..3a879d70 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -103,6 +103,10 @@ contextBridge.exposeInMainWorld("electron", { ipcRenderer.invoke("removeDownloadSource", url, removeAll), getDownloadSources: () => ipcRenderer.invoke("getDownloadSources"), syncDownloadSources: () => ipcRenderer.invoke("syncDownloadSources"), + getDownloadSourcesCheckBaseline: () => + ipcRenderer.invoke("getDownloadSourcesCheckBaseline"), + getDownloadSourcesSinceValue: () => + ipcRenderer.invoke("getDownloadSourcesSinceValue"), /* Library */ toggleAutomaticCloudSync: ( diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index 30b5c67a..b5d2492d 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -214,6 +214,8 @@ declare global { ) => Promise; getDownloadSources: () => Promise; syncDownloadSources: () => Promise; + getDownloadSourcesCheckBaseline: () => Promise; + getDownloadSourcesSinceValue: () => Promise; /* Hardware */ getDiskFreeSpace: (path: string) => Promise; diff --git a/src/renderer/src/hooks/use-download-options-listener.ts b/src/renderer/src/hooks/use-download-options-listener.ts index 34cc7bf7..dfc9b3b4 100644 --- a/src/renderer/src/hooks/use-download-options-listener.ts +++ b/src/renderer/src/hooks/use-download-options-listener.ts @@ -8,9 +8,9 @@ export function useDownloadOptionsListener() { useEffect(() => { const unsubscribe = window.electron.onNewDownloadOptions( (gamesWithNewOptions) => { - gamesWithNewOptions.forEach(({ gameId, count }) => { + for (const { gameId, count } of gamesWithNewOptions) { dispatch(updateGameNewDownloadOptions({ gameId, count })); - }); + } } ); diff --git a/src/renderer/src/pages/game-details/modals/repacks-modal.scss b/src/renderer/src/pages/game-details/modals/repacks-modal.scss index ba9778fd..9bffc676 100644 --- a/src/renderer/src/pages/game-details/modals/repacks-modal.scss +++ b/src/renderer/src/pages/game-details/modals/repacks-modal.scss @@ -45,12 +45,27 @@ &__repack-title { color: globals.$muted-color; word-break: break-word; + display: flex; + align-items: center; + gap: calc(globals.$spacing-unit * 1); } &__repack-info { font-size: globals.$small-font-size; } + &__new-badge { + background-color: rgba(255, 255, 255, 0.1); + color: rgba(255, 255, 255, 0.7); + padding: 4px 8px; + border-radius: 6px; + font-size: 12px; + font-weight: 600; + min-width: 24px; + text-align: center; + flex-shrink: 0; + } + &__no-results { width: 100%; padding: calc(globals.$spacing-unit * 4) 0; diff --git a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx index 306e8647..1627d8e9 100644 --- a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx @@ -15,8 +15,7 @@ import { TextField, CheckboxField, } from "@renderer/components"; -import type { DownloadSource } from "@types"; -import type { GameRepack } from "@types"; +import type { DownloadSource, GameRepack } from "@types"; import { DownloadSettingsModal } from "./download-settings-modal"; import { gameDetailsContext } from "@renderer/context"; @@ -53,6 +52,10 @@ export function RepacksModal({ const [hashesInDebrid, setHashesInDebrid] = useState>( {} ); + const [lastCheckTimestamp, setLastCheckTimestamp] = useState( + null + ); + const [isLoadingTimestamp, setIsLoadingTimestamp] = useState(true); const { game, repacks } = useContext(gameDetailsContext); @@ -66,8 +69,8 @@ export function RepacksModal({ return null; } - const hashRegex = /xt=urn:btih:([a-zA-Z0-9]+)/i; - const match = magnet.match(hashRegex); + const hashRegex = /xt=urn:btih:([a-f0-9]+)/i; + const match = hashRegex.exec(magnet); return match ? match[1].toLowerCase() : null; }; @@ -97,6 +100,21 @@ export function RepacksModal({ fetchDownloadSources(); }, []); + useEffect(() => { + const fetchLastCheckTimestamp = async () => { + setIsLoadingTimestamp(true); + + const timestamp = await window.electron.getDownloadSourcesSinceValue(); + + setLastCheckTimestamp(timestamp); + setIsLoadingTimestamp(false); + }; + + if (visible) { + fetchLastCheckTimestamp(); + } + }, [visible, repacks]); + const sortedRepacks = useMemo(() => { return orderBy( repacks, @@ -158,6 +176,19 @@ export function RepacksModal({ return repack.uris.some((uri) => uri.includes(game.download!.uri)); }; + const isNewRepack = (repack: GameRepack): boolean => { + // Don't show badge while loading timestamp + if (isLoadingTimestamp) return false; + + if (!lastCheckTimestamp || !repack.createdAt) { + return false; + } + + const lastCheckUtc = new Date(lastCheckTimestamp).toISOString(); + + return repack.createdAt > lastCheckUtc; + }; + const [isFilterDrawerOpen, setIsFilterDrawerOpen] = useState(false); useEffect(() => { @@ -273,7 +304,14 @@ export function RepacksModal({ onClick={() => handleRepackClick(repack)} className="repacks-modal__repack-button" > -

{repack.title}

+

+ {repack.title} + {isNewRepack(repack) && ( + + {t("new_download_option")} + + )} +

{isLastDownloadedOption && ( {t("last_downloaded_option")} diff --git a/src/types/index.ts b/src/types/index.ts index 4b13c496..f435bc60 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -23,6 +23,7 @@ export interface GameRepack { uploadDate: string | null; downloadSourceId: string; downloadSourceName: string; + createdAt: string; } export interface DownloadSource { From 5067cf163e5bf65122858ce1446a2edb57d15fa1 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sun, 2 Nov 2025 18:22:37 +0200 Subject: [PATCH 04/14] feat: added new badge to repacks-modal, set up badge clearing --- src/main/events/index.ts | 1 + .../library/clear-new-download-options.ts | 27 ++++++++++++++++ src/main/services/download-sources-checker.ts | 13 -------- src/preload/index.ts | 2 ++ src/renderer/src/declaration.d.ts | 4 +++ .../game-details/modals/repacks-modal.scss | 10 +++--- .../game-details/modals/repacks-modal.tsx | 31 +++++++++++++++++-- 7 files changed, 67 insertions(+), 21 deletions(-) create mode 100644 src/main/events/library/clear-new-download-options.ts diff --git a/src/main/events/index.ts b/src/main/events/index.ts index 162b08d7..d75f8255 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -19,6 +19,7 @@ import "./library/delete-game-folder"; import "./library/get-game-by-object-id"; import "./library/get-library"; import "./library/extract-game-download"; +import "./library/clear-new-download-options"; import "./library/open-game"; import "./library/open-game-executable-path"; import "./library/open-game-installer"; diff --git a/src/main/events/library/clear-new-download-options.ts b/src/main/events/library/clear-new-download-options.ts new file mode 100644 index 00000000..55ebfd8f --- /dev/null +++ b/src/main/events/library/clear-new-download-options.ts @@ -0,0 +1,27 @@ +import { registerEvent } from "../register-event"; +import { gamesSublevel, levelKeys } from "@main/level"; +import { logger } from "@main/services"; +import type { GameShop } from "@types"; + +const clearNewDownloadOptions = async ( + _event: Electron.IpcMainInvokeEvent, + shop: GameShop, + objectId: string +) => { + const gameKey = levelKeys.game(shop, objectId); + + const game = await gamesSublevel.get(gameKey); + if (!game) return; + + try { + await gamesSublevel.put(gameKey, { + ...game, + newDownloadOptionsCount: undefined, + }); + logger.info(`Cleared newDownloadOptionsCount for game ${gameKey}`); + } catch (error) { + logger.error(`Failed to clear newDownloadOptionsCount: ${error}`); + } +}; + +registerEvent("clearNewDownloadOptions", clearNewDownloadOptions); diff --git a/src/main/services/download-sources-checker.ts b/src/main/services/download-sources-checker.ts index f8b853a7..928e3d52 100644 --- a/src/main/services/download-sources-checker.ts +++ b/src/main/services/download-sources-checker.ts @@ -73,10 +73,6 @@ export class DownloadSourcesChecker { gameId: `${game.shop}:${game.objectId}`, count: gameUpdate.newDownloadOptionsCount, }); - - logger.info( - `Game ${game.title} has ${gameUpdate.newDownloadOptionsCount} new download options` - ); } } } @@ -121,7 +117,6 @@ export class DownloadSourcesChecker { return; } - // Get download sources const downloadSources = await downloadSourcesSublevel.values().all(); const downloadSourceIds = downloadSources.map((source) => source.id); logger.info( @@ -135,7 +130,6 @@ export class DownloadSourcesChecker { return; } - // Get when we LAST started the app (for this check's 'since' parameter) const previousBaseline = await getDownloadSourcesCheckBaseline(); const since = previousBaseline || @@ -143,10 +137,8 @@ export class DownloadSourcesChecker { logger.info(`Using since: ${since} (from last app start)`); - // Clear any previously stored new download option counts so badges don't persist across restarts const clearedPayload = await this.clearStaleBadges(nonCustomGames); - // Prepare games array for API call (excluding custom games) const games = nonCustomGames.map((game: Game) => ({ shop: game.shop, objectId: game.objectId, @@ -164,7 +156,6 @@ export class DownloadSourcesChecker { } ); - // Call the API const response = await HydraApi.checkDownloadSourcesChanges( downloadSourceIds, games, @@ -173,24 +164,20 @@ export class DownloadSourcesChecker { logger.info("API call completed, response:", response); - // Save the 'since' value we just used (for modal to compare against) await updateDownloadSourcesSinceValue(since); logger.info(`Saved 'since' value: ${since} (for modal comparison)`); - // Update baseline to NOW (for next app start's 'since') const now = new Date().toISOString(); await updateDownloadSourcesCheckBaseline(now); logger.info( `Updated baseline to: ${now} (will be 'since' on next app start)` ); - // Process the response and store newDownloadOptionsCount for games with new options const gamesWithNewOptions = await this.processApiResponse( response, nonCustomGames ); - // Send IPC event to renderer to clear stale badges and set fresh counts from response this.sendNewDownloadOptionsEvent(clearedPayload, gamesWithNewOptions); logger.info("Download sources check completed successfully"); diff --git a/src/preload/index.ts b/src/preload/index.ts index 3a879d70..69cbb3d4 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -183,6 +183,8 @@ contextBridge.exposeInMainWorld("electron", { ipcRenderer.invoke("addGameToFavorites", shop, objectId), removeGameFromFavorites: (shop: GameShop, objectId: string) => ipcRenderer.invoke("removeGameFromFavorites", shop, objectId), + clearNewDownloadOptions: (shop: GameShop, objectId: string) => + ipcRenderer.invoke("clearNewDownloadOptions", shop, objectId), toggleGamePin: (shop: GameShop, objectId: string, pinned: boolean) => ipcRenderer.invoke("toggleGamePin", shop, objectId, pinned), updateLaunchOptions: ( diff --git a/src/renderer/src/declaration.d.ts b/src/renderer/src/declaration.d.ts index b5d2492d..54b1be51 100644 --- a/src/renderer/src/declaration.d.ts +++ b/src/renderer/src/declaration.d.ts @@ -142,6 +142,10 @@ declare global { shop: GameShop, objectId: string ) => Promise; + clearNewDownloadOptions: ( + shop: GameShop, + objectId: string + ) => Promise; toggleGamePin: ( shop: GameShop, objectId: string, diff --git a/src/renderer/src/pages/game-details/modals/repacks-modal.scss b/src/renderer/src/pages/game-details/modals/repacks-modal.scss index 9bffc676..3f78c82c 100644 --- a/src/renderer/src/pages/game-details/modals/repacks-modal.scss +++ b/src/renderer/src/pages/game-details/modals/repacks-modal.scss @@ -55,15 +55,15 @@ } &__new-badge { - background-color: rgba(255, 255, 255, 0.1); - color: rgba(255, 255, 255, 0.7); - padding: 4px 8px; + background-color: rgba(34, 197, 94, 0.15); + color: rgb(187, 247, 208); + padding: 2px 6px; border-radius: 6px; - font-size: 12px; + font-size: 10px; font-weight: 600; - min-width: 24px; text-align: center; flex-shrink: 0; + border: 1px solid rgba(34, 197, 94, 0.5); } &__no-results { diff --git a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx index 1627d8e9..fa9b04a1 100644 --- a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx @@ -21,7 +21,8 @@ import { DownloadSettingsModal } from "./download-settings-modal"; import { gameDetailsContext } from "@renderer/context"; import { Downloader } from "@shared"; import { orderBy } from "lodash-es"; -import { useDate, useFeature } from "@renderer/hooks"; +import { useDate, useFeature, useAppDispatch } from "@renderer/hooks"; +import { clearNewDownloadOptions } from "@renderer/features"; import "./repacks-modal.scss"; export interface RepacksModalProps { @@ -56,6 +57,9 @@ export function RepacksModal({ null ); const [isLoadingTimestamp, setIsLoadingTimestamp] = useState(true); + const [viewedRepackIds, setViewedRepackIds] = useState>( + new Set() + ); const { game, repacks } = useContext(gameDetailsContext); @@ -63,14 +67,15 @@ export function RepacksModal({ const { formatDate } = useDate(); const navigate = useNavigate(); + const dispatch = useAppDispatch(); const getHashFromMagnet = (magnet: string) => { if (!magnet || typeof magnet !== "string") { return null; } - const hashRegex = /xt=urn:btih:([a-f0-9]+)/i; - const match = hashRegex.exec(magnet); + const hashRegex = /xt=urn:btih:([a-zA-Z0-9]+)/i; + const match = magnet.match(hashRegex); return match ? match[1].toLowerCase() : null; }; @@ -115,6 +120,21 @@ export function RepacksModal({ } }, [visible, repacks]); + useEffect(() => { + if ( + visible && + game?.newDownloadOptionsCount && + game.newDownloadOptionsCount > 0 + ) { + // Clear the badge in the database + globalThis.electron.clearNewDownloadOptions(game.shop, game.objectId); + + // Clear the badge in Redux store + const gameId = `${game.shop}:${game.objectId}`; + dispatch(clearNewDownloadOptions({ gameId })); + } + }, [visible, game, dispatch]); + const sortedRepacks = useMemo(() => { return orderBy( repacks, @@ -157,6 +177,8 @@ export function RepacksModal({ const handleRepackClick = (repack: GameRepack) => { setRepack(repack); setShowSelectFolderModal(true); + // Mark this repack as viewed to hide the "NEW" badge + setViewedRepackIds((prev) => new Set(prev).add(repack.id)); }; const handleFilter: React.ChangeEventHandler = (event) => { @@ -180,6 +202,9 @@ export function RepacksModal({ // Don't show badge while loading timestamp if (isLoadingTimestamp) return false; + // Don't show badge if user has already clicked this repack in current session + if (viewedRepackIds.has(repack.id)) return false; + if (!lastCheckTimestamp || !repack.createdAt) { return false; } From 87d35da9fcbbed07dc155397f5737ae4bc79d475 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sun, 2 Nov 2025 18:24:10 +0200 Subject: [PATCH 05/14] fix: deleted comments --- src/renderer/src/pages/game-details/modals/repacks-modal.tsx | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx index fa9b04a1..91013da0 100644 --- a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx @@ -126,10 +126,8 @@ export function RepacksModal({ game?.newDownloadOptionsCount && game.newDownloadOptionsCount > 0 ) { - // Clear the badge in the database globalThis.electron.clearNewDownloadOptions(game.shop, game.objectId); - // Clear the badge in Redux store const gameId = `${game.shop}:${game.objectId}`; dispatch(clearNewDownloadOptions({ gameId })); } @@ -177,7 +175,6 @@ export function RepacksModal({ const handleRepackClick = (repack: GameRepack) => { setRepack(repack); setShowSelectFolderModal(true); - // Mark this repack as viewed to hide the "NEW" badge setViewedRepackIds((prev) => new Set(prev).add(repack.id)); }; @@ -199,10 +196,8 @@ export function RepacksModal({ }; const isNewRepack = (repack: GameRepack): boolean => { - // Don't show badge while loading timestamp if (isLoadingTimestamp) return false; - // Don't show badge if user has already clicked this repack in current session if (viewedRepackIds.has(repack.id)) return false; if (!lastCheckTimestamp || !repack.createdAt) { From 6f6b7d49ac120fa2bed6055f8e8f3a45a551e325 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sun, 2 Nov 2025 18:47:26 +0200 Subject: [PATCH 06/14] fix: removed void and converted conditional to boolean --- src/main/main.ts | 2 +- src/renderer/src/components/sidebar/sidebar-game-item.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/main.ts b/src/main/main.ts index 50173390..6fc2b216 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -59,7 +59,7 @@ export const loadState = async () => { void syncDownloadSourcesFromApi(); // Check for new download options on startup - void DownloadSourcesChecker.checkForChanges(); + DownloadSourcesChecker.checkForChanges(); // WSClient.connect(); }); diff --git a/src/renderer/src/components/sidebar/sidebar-game-item.tsx b/src/renderer/src/components/sidebar/sidebar-game-item.tsx index 59698935..ee16d418 100644 --- a/src/renderer/src/components/sidebar/sidebar-game-item.tsx +++ b/src/renderer/src/components/sidebar/sidebar-game-item.tsx @@ -81,7 +81,7 @@ export function SidebarGameItem({ {getGameTitle(game)} - {game.newDownloadOptionsCount && game.newDownloadOptionsCount > 0 && ( + {(game.newDownloadOptionsCount ?? 0) > 0 && (
+
From 44b24ab63d79255ff71f0bc62edb26b838249f08 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Tue, 11 Nov 2025 01:14:27 +0200 Subject: [PATCH 07/14] feat: checking updates only for games with executables --- src/main/services/download-sources-checker.ts | 8 ++++---- .../src/pages/game-details/modals/repacks-modal.tsx | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/main/services/download-sources-checker.ts b/src/main/services/download-sources-checker.ts index 928e3d52..b3675829 100644 --- a/src/main/services/download-sources-checker.ts +++ b/src/main/services/download-sources-checker.ts @@ -101,18 +101,18 @@ export class DownloadSourcesChecker { logger.info("DownloadSourcesChecker.checkForChanges() called"); try { - // Get all installed games (excluding custom games) + // Get all installed games (excluding custom games and games without executable) const installedGames = await gamesSublevel.values().all(); const nonCustomGames = installedGames.filter( - (game: Game) => game.shop !== "custom" + (game: Game) => game.shop !== "custom" && game.executablePath ); logger.info( - `Found ${installedGames.length} total games, ${nonCustomGames.length} non-custom games` + `Found ${installedGames.length} total games, ${nonCustomGames.length} non-custom games with executable path` ); if (nonCustomGames.length === 0) { logger.info( - "No non-custom games found, skipping download sources check" + "No non-custom games with executable path found, skipping download sources check" ); return; } diff --git a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx index 91013da0..a72e1323 100644 --- a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx @@ -200,6 +200,8 @@ export function RepacksModal({ if (viewedRepackIds.has(repack.id)) return false; + if (!game?.executablePath) return false; + if (!lastCheckTimestamp || !repack.createdAt) { return false; } From 860030a510cdbee4ec0884ac8265889a8cef3efe Mon Sep 17 00:00:00 2001 From: Moyasee Date: Tue, 11 Nov 2025 01:23:37 +0200 Subject: [PATCH 08/14] fix: merging conflict --- src/main/main.ts | 4 +++- src/renderer/src/features/library-slice.ts | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/main.ts b/src/main/main.ts index 6fc2b216..1c203889 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -17,6 +17,7 @@ import { Lock, DeckyPlugin, DownloadSourcesChecker, + WSClient, } from "@main/services"; import { migrateDownloadSources } from "./helpers/migrate-download-sources"; @@ -60,7 +61,8 @@ export const loadState = async () => { // Check for new download options on startup DownloadSourcesChecker.checkForChanges(); - // WSClient.connect(); + + WSClient.connect(); }); const downloads = await downloadsSublevel diff --git a/src/renderer/src/features/library-slice.ts b/src/renderer/src/features/library-slice.ts index 9c399dbe..9e671e34 100644 --- a/src/renderer/src/features/library-slice.ts +++ b/src/renderer/src/features/library-slice.ts @@ -5,10 +5,12 @@ import type { LibraryGame } from "@types"; export interface LibraryState { value: LibraryGame[]; + searchQuery: string; } const initialState: LibraryState = { value: [], + searchQuery: "", }; export const librarySlice = createSlice({ @@ -36,6 +38,9 @@ export const librarySlice = createSlice({ game.newDownloadOptionsCount = undefined; } }, + setLibrarySearchQuery: (state, action: PayloadAction) => { + state.searchQuery = action.payload; + }, }, }); @@ -43,4 +48,5 @@ export const { setLibrary, updateGameNewDownloadOptions, clearNewDownloadOptions, + setLibrarySearchQuery, } = librarySlice.actions; From 14eb0f8172dc8f1bea47f7f2a8d9b0253b078a7c Mon Sep 17 00:00:00 2001 From: Moyasee Date: Tue, 11 Nov 2025 01:27:24 +0200 Subject: [PATCH 09/14] reverting changes --- src/renderer/src/features/library-slice.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/renderer/src/features/library-slice.ts b/src/renderer/src/features/library-slice.ts index 9e671e34..9c399dbe 100644 --- a/src/renderer/src/features/library-slice.ts +++ b/src/renderer/src/features/library-slice.ts @@ -5,12 +5,10 @@ import type { LibraryGame } from "@types"; export interface LibraryState { value: LibraryGame[]; - searchQuery: string; } const initialState: LibraryState = { value: [], - searchQuery: "", }; export const librarySlice = createSlice({ @@ -38,9 +36,6 @@ export const librarySlice = createSlice({ game.newDownloadOptionsCount = undefined; } }, - setLibrarySearchQuery: (state, action: PayloadAction) => { - state.searchQuery = action.payload; - }, }, }); @@ -48,5 +43,4 @@ export const { setLibrary, updateGameNewDownloadOptions, clearNewDownloadOptions, - setLibrarySearchQuery, } = librarySlice.actions; From 1521d7c058e1ff1091baa246a7032efe2ecab0bd Mon Sep 17 00:00:00 2001 From: Moyasee Date: Tue, 11 Nov 2025 01:29:04 +0200 Subject: [PATCH 10/14] reverting changes --- src/main/main.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/main.ts b/src/main/main.ts index 1c203889..c6d54bc7 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -17,7 +17,6 @@ import { Lock, DeckyPlugin, DownloadSourcesChecker, - WSClient, } from "@main/services"; import { migrateDownloadSources } from "./helpers/migrate-download-sources"; @@ -61,8 +60,6 @@ export const loadState = async () => { // Check for new download options on startup DownloadSourcesChecker.checkForChanges(); - - WSClient.connect(); }); const downloads = await downloadsSublevel From a1eef4eab6110d06dead73153d58760ed82a82da Mon Sep 17 00:00:00 2001 From: Moyasee Date: Tue, 11 Nov 2025 16:30:23 +0200 Subject: [PATCH 11/14] feat: check updates for installed games --- src/main/services/download-sources-checker.ts | 59 +++++++++++-------- 1 file changed, 36 insertions(+), 23 deletions(-) diff --git a/src/main/services/download-sources-checker.ts b/src/main/services/download-sources-checker.ts index b3675829..c0997a0f 100644 --- a/src/main/services/download-sources-checker.ts +++ b/src/main/services/download-sources-checker.ts @@ -5,6 +5,7 @@ import { updateDownloadSourcesCheckBaseline, updateDownloadSourcesSinceValue, downloadSourcesSublevel, + levelKeys, } from "@main/level"; import { logger } from "./logger"; import { WindowManager } from "./window-manager"; @@ -47,34 +48,49 @@ export class DownloadSourcesChecker { } private static async processApiResponse( - response: unknown, - nonCustomGames: Game[] + response: unknown ): Promise<{ gameId: string; count: number }[]> { if (!response || !Array.isArray(response)) { return []; } const gamesWithNewOptions: { gameId: string; count: number }[] = []; + const responseArray = response as DownloadSourcesChangeResponse[]; + const gamesWithUpdates = responseArray.filter( + (update) => update.newDownloadOptionsCount > 0 + ); - for (const gameUpdate of response as DownloadSourcesChangeResponse[]) { - if (gameUpdate.newDownloadOptionsCount > 0) { - const game = nonCustomGames.find( - (g) => - g.shop === gameUpdate.shop && g.objectId === gameUpdate.objectId + logger.info( + `API returned ${gamesWithUpdates.length} games with new download options (out of ${responseArray.length} total updates)` + ); + + for (const gameUpdate of gamesWithUpdates) { + const gameKey = levelKeys.game(gameUpdate.shop, gameUpdate.objectId); + const game = await gamesSublevel.get(gameKey).catch(() => null); + + if (!game) { + logger.info( + `Skipping update for ${gameKey} - game not found in database` ); - - if (game) { - await gamesSublevel.put(`${game.shop}:${game.objectId}`, { - ...game, - newDownloadOptionsCount: gameUpdate.newDownloadOptionsCount, - }); - - gamesWithNewOptions.push({ - gameId: `${game.shop}:${game.objectId}`, - count: gameUpdate.newDownloadOptionsCount, - }); - } + continue; } + + if (game.shop === "custom") { + logger.info( + `Skipping update for ${gameKey} - custom games are excluded` + ); + continue; + } + + await gamesSublevel.put(gameKey, { + ...game, + newDownloadOptionsCount: gameUpdate.newDownloadOptionsCount, + }); + + gamesWithNewOptions.push({ + gameId: gameKey, + count: gameUpdate.newDownloadOptionsCount, + }); } return gamesWithNewOptions; @@ -173,10 +189,7 @@ export class DownloadSourcesChecker { `Updated baseline to: ${now} (will be 'since' on next app start)` ); - const gamesWithNewOptions = await this.processApiResponse( - response, - nonCustomGames - ); + const gamesWithNewOptions = await this.processApiResponse(response); this.sendNewDownloadOptionsEvent(clearedPayload, gamesWithNewOptions); From d59b96f44651d22a13b240afc9d12ae956a9c932 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Tue, 11 Nov 2025 16:31:53 +0200 Subject: [PATCH 12/14] fix: typescript error --- src/main/services/download-sources-checker.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/services/download-sources-checker.ts b/src/main/services/download-sources-checker.ts index c0997a0f..890abee8 100644 --- a/src/main/services/download-sources-checker.ts +++ b/src/main/services/download-sources-checker.ts @@ -9,10 +9,10 @@ import { } from "@main/level"; import { logger } from "./logger"; import { WindowManager } from "./window-manager"; -import type { Game } from "@types"; +import type { Game, GameShop } from "@types"; interface DownloadSourcesChangeResponse { - shop: string; + shop: GameShop; objectId: string; newDownloadOptionsCount: number; downloadSourceIds: string[]; From 8dc5be1bdf7ab95c43e7a17758d06f2145e9d932 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Tue, 11 Nov 2025 20:01:28 +0200 Subject: [PATCH 13/14] reverting changes --- src/main/services/download-sources-checker.ts | 71 ++++++++----------- .../game-details/modals/repacks-modal.tsx | 2 - 2 files changed, 29 insertions(+), 44 deletions(-) diff --git a/src/main/services/download-sources-checker.ts b/src/main/services/download-sources-checker.ts index 890abee8..928e3d52 100644 --- a/src/main/services/download-sources-checker.ts +++ b/src/main/services/download-sources-checker.ts @@ -5,14 +5,13 @@ import { updateDownloadSourcesCheckBaseline, updateDownloadSourcesSinceValue, downloadSourcesSublevel, - levelKeys, } from "@main/level"; import { logger } from "./logger"; import { WindowManager } from "./window-manager"; -import type { Game, GameShop } from "@types"; +import type { Game } from "@types"; interface DownloadSourcesChangeResponse { - shop: GameShop; + shop: string; objectId: string; newDownloadOptionsCount: number; downloadSourceIds: string[]; @@ -48,49 +47,34 @@ export class DownloadSourcesChecker { } private static async processApiResponse( - response: unknown + response: unknown, + nonCustomGames: Game[] ): Promise<{ gameId: string; count: number }[]> { if (!response || !Array.isArray(response)) { return []; } const gamesWithNewOptions: { gameId: string; count: number }[] = []; - const responseArray = response as DownloadSourcesChangeResponse[]; - const gamesWithUpdates = responseArray.filter( - (update) => update.newDownloadOptionsCount > 0 - ); - logger.info( - `API returned ${gamesWithUpdates.length} games with new download options (out of ${responseArray.length} total updates)` - ); - - for (const gameUpdate of gamesWithUpdates) { - const gameKey = levelKeys.game(gameUpdate.shop, gameUpdate.objectId); - const game = await gamesSublevel.get(gameKey).catch(() => null); - - if (!game) { - logger.info( - `Skipping update for ${gameKey} - game not found in database` + for (const gameUpdate of response as DownloadSourcesChangeResponse[]) { + if (gameUpdate.newDownloadOptionsCount > 0) { + const game = nonCustomGames.find( + (g) => + g.shop === gameUpdate.shop && g.objectId === gameUpdate.objectId ); - continue; + + if (game) { + await gamesSublevel.put(`${game.shop}:${game.objectId}`, { + ...game, + newDownloadOptionsCount: gameUpdate.newDownloadOptionsCount, + }); + + gamesWithNewOptions.push({ + gameId: `${game.shop}:${game.objectId}`, + count: gameUpdate.newDownloadOptionsCount, + }); + } } - - if (game.shop === "custom") { - logger.info( - `Skipping update for ${gameKey} - custom games are excluded` - ); - continue; - } - - await gamesSublevel.put(gameKey, { - ...game, - newDownloadOptionsCount: gameUpdate.newDownloadOptionsCount, - }); - - gamesWithNewOptions.push({ - gameId: gameKey, - count: gameUpdate.newDownloadOptionsCount, - }); } return gamesWithNewOptions; @@ -117,18 +101,18 @@ export class DownloadSourcesChecker { logger.info("DownloadSourcesChecker.checkForChanges() called"); try { - // Get all installed games (excluding custom games and games without executable) + // Get all installed games (excluding custom games) const installedGames = await gamesSublevel.values().all(); const nonCustomGames = installedGames.filter( - (game: Game) => game.shop !== "custom" && game.executablePath + (game: Game) => game.shop !== "custom" ); logger.info( - `Found ${installedGames.length} total games, ${nonCustomGames.length} non-custom games with executable path` + `Found ${installedGames.length} total games, ${nonCustomGames.length} non-custom games` ); if (nonCustomGames.length === 0) { logger.info( - "No non-custom games with executable path found, skipping download sources check" + "No non-custom games found, skipping download sources check" ); return; } @@ -189,7 +173,10 @@ export class DownloadSourcesChecker { `Updated baseline to: ${now} (will be 'since' on next app start)` ); - const gamesWithNewOptions = await this.processApiResponse(response); + const gamesWithNewOptions = await this.processApiResponse( + response, + nonCustomGames + ); this.sendNewDownloadOptionsEvent(clearedPayload, gamesWithNewOptions); diff --git a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx index a72e1323..91013da0 100644 --- a/src/renderer/src/pages/game-details/modals/repacks-modal.tsx +++ b/src/renderer/src/pages/game-details/modals/repacks-modal.tsx @@ -200,8 +200,6 @@ export function RepacksModal({ if (viewedRepackIds.has(repack.id)) return false; - if (!game?.executablePath) return false; - if (!lastCheckTimestamp || !repack.createdAt) { return false; } From aebf6d1cae5ab6916039e472fee25f5e99fb472e Mon Sep 17 00:00:00 2001 From: Chubby Granny Chaser Date: Tue, 11 Nov 2025 18:22:39 +0000 Subject: [PATCH 14/14] feat: adding translations for new label --- src/locales/es/translation.json | 1 + src/locales/pt-BR/translation.json | 1 + src/locales/ru/translation.json | 1 + .../src/components/sidebar/sidebar-game-item.tsx | 5 +---- src/renderer/src/components/sidebar/sidebar.scss | 14 ++++---------- .../pages/game-details/modals/repacks-modal.scss | 5 ++--- 6 files changed, 10 insertions(+), 17 deletions(-) diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index 12e7e7fe..5a65d3cf 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -193,6 +193,7 @@ "download_in_progress": "Descarga en progreso", "download_paused": "Descarga pausada", "last_downloaded_option": "Última opción de descarga", + "new_download_option": "Nuevo", "create_steam_shortcut": "Crear atajo de Steam", "create_shortcut_success": "Atajo creado con éxito", "you_might_need_to_restart_steam": "Probablemente necesités reiniciar Steam para ver cambios", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index fc0f4332..73d5e8fb 100755 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -183,6 +183,7 @@ "download_in_progress": "Download em andamento", "download_paused": "Download pausado", "last_downloaded_option": "Última opção baixada", + "new_download_option": "Novo", "create_steam_shortcut": "Criar atalho na Steam", "create_shortcut_success": "Atalho criado com sucesso", "you_might_need_to_restart_steam": "Você pode precisar reiniciar a Steam para ver as alterações", diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index c9527af8..b831ff2e 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -195,6 +195,7 @@ "download_in_progress": "Идёт загрузка", "download_paused": "Загрузка приостановлена", "last_downloaded_option": "Последний вариант загрузки", + "new_download_option": "Новый", "create_steam_shortcut": "Создать ярлык Steam", "create_shortcut_success": "Ярлык создан", "you_might_need_to_restart_steam": "Возможно, вам потребуется перезапустить Steam, чтобы увидеть изменения", diff --git a/src/renderer/src/components/sidebar/sidebar-game-item.tsx b/src/renderer/src/components/sidebar/sidebar-game-item.tsx index ee16d418..23223fc5 100644 --- a/src/renderer/src/components/sidebar/sidebar-game-item.tsx +++ b/src/renderer/src/components/sidebar/sidebar-game-item.tsx @@ -83,10 +83,7 @@ export function SidebarGameItem({ {(game.newDownloadOptionsCount ?? 0) > 0 && ( -
+
-
- {game.newDownloadOptionsCount} -
+ +{game.newDownloadOptionsCount}
)} diff --git a/src/renderer/src/components/sidebar/sidebar.scss b/src/renderer/src/components/sidebar/sidebar.scss index fdb3872e..312cba3c 100644 --- a/src/renderer/src/components/sidebar/sidebar.scss +++ b/src/renderer/src/components/sidebar/sidebar.scss @@ -116,24 +116,18 @@ } &__game-badge { - background: rgba(255, 255, 255, 0.1); - color: #fff; + background-color: rgba(34, 197, 94, 0.15); + color: rgb(187, 247, 208); font-size: 10px; - font-weight: bold; + font-weight: 600; padding: 4px 6px; border-radius: 6px; display: flex; margin-left: auto; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); - gap: calc(globals.$spacing-unit * 0.35); + border: 1px solid rgba(34, 197, 94, 0.5); } - &__game-badge-plus, - &__game-badge-count { - display: flex; - align-items: center; - justify-content: center; - } &__section-header { display: flex; diff --git a/src/renderer/src/pages/game-details/modals/repacks-modal.scss b/src/renderer/src/pages/game-details/modals/repacks-modal.scss index 3f78c82c..420029c7 100644 --- a/src/renderer/src/pages/game-details/modals/repacks-modal.scss +++ b/src/renderer/src/pages/game-details/modals/repacks-modal.scss @@ -57,10 +57,9 @@ &__new-badge { background-color: rgba(34, 197, 94, 0.15); color: rgb(187, 247, 208); - padding: 2px 6px; + padding: 2px 8px; border-radius: 6px; - font-size: 10px; - font-weight: 600; + font-size: 9px; text-align: center; flex-shrink: 0; border: 1px solid rgba(34, 197, 94, 0.5);