From 5067cf163e5bf65122858ce1446a2edb57d15fa1 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Sun, 2 Nov 2025 18:22:37 +0200 Subject: [PATCH] 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; }