From a2ef0f304daa827d29aeaca4a13e92a4fe87b5fb Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Thu, 30 Oct 2025 07:35:49 -0300 Subject: [PATCH 01/11] fix: playtime count and custom games request on process watcher --- .../achievement-watcher-manager.ts | 2 + src/main/services/library-sync/create-game.ts | 4 ++ .../library-sync/update-game-playtime.ts | 4 ++ src/main/services/process-watcher.ts | 44 +++++++++++++------ 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/main/services/achievements/achievement-watcher-manager.ts b/src/main/services/achievements/achievement-watcher-manager.ts index b862abbe..dd65165a 100644 --- a/src/main/services/achievements/achievement-watcher-manager.ts +++ b/src/main/services/achievements/achievement-watcher-manager.ts @@ -167,6 +167,8 @@ export class AchievementWatcherManager { shop: GameShop, objectId: string ) { + if (shop === "custom") return; + const gameKey = levelKeys.game(shop, objectId); if (this.alreadySyncedGames.get(gameKey)) return; diff --git a/src/main/services/library-sync/create-game.ts b/src/main/services/library-sync/create-game.ts index a346d3b4..e9ec9612 100644 --- a/src/main/services/library-sync/create-game.ts +++ b/src/main/services/library-sync/create-game.ts @@ -3,6 +3,10 @@ import { HydraApi } from "../hydra-api"; import { gamesSublevel, levelKeys } from "@main/level"; export const createGame = async (game: Game) => { + if (game.shop === "custom") { + return; + } + return HydraApi.post(`/profile/games`, { objectId: game.objectId, playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds ?? 0), diff --git a/src/main/services/library-sync/update-game-playtime.ts b/src/main/services/library-sync/update-game-playtime.ts index 3689b302..b53ebebc 100644 --- a/src/main/services/library-sync/update-game-playtime.ts +++ b/src/main/services/library-sync/update-game-playtime.ts @@ -6,6 +6,10 @@ export const updateGamePlaytime = async ( deltaInMillis: number, lastTimePlayed: Date ) => { + if (game.shop === "custom") { + return; + } + return HydraApi.put(`/profile/games/${game.remoteId}`, { playTimeDeltaInSeconds: Math.trunc(deltaInMillis / 1000), lastTimePlayed, diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts index 6408c30d..a1449255 100644 --- a/src/main/services/process-watcher.ts +++ b/src/main/services/process-watcher.ts @@ -198,11 +198,6 @@ export const watchProcesses = async () => { function onOpenGame(game: Game) { const now = performance.now(); - AchievementWatcherManager.firstSyncWithRemoteIfNeeded( - game.shop, - game.objectId - ); - gamesPlaytime.set(levelKeys.game(game.shop, game.objectId), { lastTick: now, firstTick: now, @@ -220,6 +215,13 @@ function onOpenGame(game: Game) { }) .catch(() => {}); + if (game.shop === "custom") return; + + AchievementWatcherManager.firstSyncWithRemoteIfNeeded( + game.shop, + game.objectId + ); + if (game.remoteId) { updateGamePlaytime( game, @@ -255,18 +257,20 @@ function onTickGame(game: Game) { const delta = now - gamePlaytime.lastTick; - gamesSublevel.put(levelKeys.game(game.shop, game.objectId), { + const updatedGame: Game = { ...game, playTimeInMilliseconds: (game.playTimeInMilliseconds ?? 0) + delta, lastTimePlayed: new Date(), - }); + }; + + gamesSublevel.put(levelKeys.game(game.shop, game.objectId), updatedGame); gamesPlaytime.set(levelKeys.game(game.shop, game.objectId), { ...gamePlaytime, lastTick: now, }); - if (currentTick % TICKS_TO_UPDATE_API === 0) { + if (currentTick % TICKS_TO_UPDATE_API === 0 && game.shop !== "custom") { const deltaToSync = now - gamePlaytime.lastSyncTick + @@ -279,19 +283,20 @@ function onTickGame(game: Game) { gamePromise .then(() => { gamesSublevel.put(levelKeys.game(game.shop, game.objectId), { - ...game, + ...updatedGame, unsyncedDeltaPlayTimeInMilliseconds: 0, }); }) .catch(() => { gamesSublevel.put(levelKeys.game(game.shop, game.objectId), { - ...game, + ...updatedGame, unsyncedDeltaPlayTimeInMilliseconds: deltaToSync, }); }) .finally(() => { gamesPlaytime.set(levelKeys.game(game.shop, game.objectId), { ...gamePlaytime, + lastTick: now, lastSyncTick: now, }); }); @@ -299,11 +304,24 @@ function onTickGame(game: Game) { } const onCloseGame = (game: Game) => { + const now = performance.now(); const gamePlaytime = gamesPlaytime.get( levelKeys.game(game.shop, game.objectId) )!; gamesPlaytime.delete(levelKeys.game(game.shop, game.objectId)); + const delta = now - gamePlaytime.lastTick; + + const updatedGame: Game = { + ...game, + playTimeInMilliseconds: (game.playTimeInMilliseconds ?? 0) + delta, + lastTimePlayed: new Date(), + }; + + gamesSublevel.put(levelKeys.game(game.shop, game.objectId), updatedGame); + + if (game.shop === "custom") return; + if (game.remoteId) { if (game.automaticCloudSync) { CloudSync.uploadSaveGame( @@ -315,20 +333,20 @@ const onCloseGame = (game: Game) => { } const deltaToSync = - performance.now() - + now - gamePlaytime.lastSyncTick + (game.unsyncedDeltaPlayTimeInMilliseconds ?? 0); return updateGamePlaytime(game, deltaToSync, game.lastTimePlayed!) .then(() => { return gamesSublevel.put(levelKeys.game(game.shop, game.objectId), { - ...game, + ...updatedGame, unsyncedDeltaPlayTimeInMilliseconds: 0, }); }) .catch(() => { return gamesSublevel.put(levelKeys.game(game.shop, game.objectId), { - ...game, + ...updatedGame, unsyncedDeltaPlayTimeInMilliseconds: deltaToSync, }); }); From aadf648a2bfe7a52db7036d90ec7b418663f68c6 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Thu, 30 Oct 2025 07:58:43 -0300 Subject: [PATCH 02/11] chore: unnecessary casting --- .../src/pages/settings/settings-download-sources.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/renderer/src/pages/settings/settings-download-sources.tsx b/src/renderer/src/pages/settings/settings-download-sources.tsx index 75f0cc73..675919e3 100644 --- a/src/renderer/src/pages/settings/settings-download-sources.tsx +++ b/src/renderer/src/pages/settings/settings-download-sources.tsx @@ -89,7 +89,7 @@ export function SettingsDownloadSources() { try { await window.electron.removeDownloadSource(false, downloadSource.id); const sources = await window.electron.getDownloadSources(); - setDownloadSources(sources as DownloadSource[]); + setDownloadSources(sources); showSuccessToast(t("removed_download_source")); } catch (error) { logger.error("Failed to remove download source:", error); @@ -104,7 +104,7 @@ export function SettingsDownloadSources() { try { await window.electron.removeDownloadSource(true); const sources = await window.electron.getDownloadSources(); - setDownloadSources(sources as DownloadSource[]); + setDownloadSources(sources); showSuccessToast(t("removed_all_download_sources")); } catch (error) { logger.error("Failed to remove all download sources:", error); @@ -117,7 +117,7 @@ export function SettingsDownloadSources() { const handleAddDownloadSource = async () => { try { const sources = await window.electron.getDownloadSources(); - setDownloadSources(sources as DownloadSource[]); + setDownloadSources(sources); } catch (error) { logger.error("Failed to refresh download sources:", error); } @@ -128,7 +128,7 @@ export function SettingsDownloadSources() { try { await window.electron.syncDownloadSources(); const sources = await window.electron.getDownloadSources(); - setDownloadSources(sources as DownloadSource[]); + setDownloadSources(sources); showSuccessToast(t("download_sources_synced_successfully")); } finally { From 80e0adcd4969ffda3eb25c8006009a22f1e879f7 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Thu, 30 Oct 2025 23:33:07 +0200 Subject: [PATCH 03/11] fix: removed ability to enter non-number symbols to pagination --- .../src/pages/catalogue/pagination.tsx | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/renderer/src/pages/catalogue/pagination.tsx b/src/renderer/src/pages/catalogue/pagination.tsx index 9febc8f8..8dd85cab 100644 --- a/src/renderer/src/pages/catalogue/pagination.tsx +++ b/src/renderer/src/pages/catalogue/pagination.tsx @@ -29,9 +29,11 @@ function JumpControl({ return isOpen ? ( ) => { - const val = e.target.value; - if (val === "") { + const raw = e.target.value; + const digitsOnly = raw.replace(/\D+/g, ""); + if (digitsOnly === "") { setJumpValue(""); return; } - const num = Number(val); + const num = parseInt(digitsOnly, 10); if (Number.isNaN(num)) { + setJumpValue(""); return; } if (num < 1) { @@ -104,19 +108,38 @@ export function Pagination({ setJumpValue(String(totalPages)); return; } - setJumpValue(val); + setJumpValue(String(num)); }; const onJumpKeyDown = (e: KeyboardEvent) => { + // Allow common control keys + const controlKeys = [ + "Backspace", + "Delete", + "Tab", + "ArrowLeft", + "ArrowRight", + "Home", + "End", + ]; + + if (controlKeys.includes(e.key) || e.ctrlKey || e.metaKey) { + return; + } + if (e.key === "Enter") { - if (jumpValue.trim() === "") return; - const parsed = Number(jumpValue); + const sanitized = jumpValue.replace(/\D+/g, ""); + if (sanitized.trim() === "") return; + const parsed = parseInt(sanitized, 10); if (Number.isNaN(parsed)) return; const target = Math.max(1, Math.min(totalPages, parsed)); onPageChange(target); setIsJumpOpen(false); } else if (e.key === "Escape") { setIsJumpOpen(false); + } else if (!/^\d$/.test(e.key)) { + // Block any non-digit input (e.g., '.', ',') + e.preventDefault(); } }; From bbbf861594575fa8bfa685fecc3dd6e3ca967134 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Thu, 30 Oct 2025 23:36:41 +0200 Subject: [PATCH 04/11] fix: deleted comments --- src/renderer/src/pages/catalogue/pagination.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/renderer/src/pages/catalogue/pagination.tsx b/src/renderer/src/pages/catalogue/pagination.tsx index 8dd85cab..c41635d0 100644 --- a/src/renderer/src/pages/catalogue/pagination.tsx +++ b/src/renderer/src/pages/catalogue/pagination.tsx @@ -112,7 +112,6 @@ export function Pagination({ }; const onJumpKeyDown = (e: KeyboardEvent) => { - // Allow common control keys const controlKeys = [ "Backspace", "Delete", @@ -138,7 +137,6 @@ export function Pagination({ } else if (e.key === "Escape") { setIsJumpOpen(false); } else if (!/^\d$/.test(e.key)) { - // Block any non-digit input (e.g., '.', ',') e.preventDefault(); } }; From bd059cc7fa3800276f053f9650bdd355a85677a9 Mon Sep 17 00:00:00 2001 From: Moyasee Date: Thu, 30 Oct 2025 23:45:29 +0200 Subject: [PATCH 05/11] feat: update cursorrules --- .cursorrules | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.cursorrules b/.cursorrules index 0b0c009c..5015ab7e 100644 --- a/.cursorrules +++ b/.cursorrules @@ -27,3 +27,11 @@ - Follow TypeScript strict mode conventions - Use async/await instead of promises when possible - Prefer named exports over default exports for utilities and services + +## Comments + +- Keep comments concise and purposeful; avoid verbose explanations. +- Focus on the "why" or non-obvious context, not restating the code. +- Prefer self-explanatory naming and structure over excessive comments. +- Do not comment every line or obvious behavior; remove stale comments. +- Use docblocks only where they add value (public APIs, complex logic). From aadbda770b5a6479688c40ae6c6f36cc904e06ac Mon Sep 17 00:00:00 2001 From: Moyasee Date: Fri, 31 Oct 2025 00:19:49 +0200 Subject: [PATCH 06/11] fix: linting issue, marked props as read-only --- src/renderer/src/pages/catalogue/pagination.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/renderer/src/pages/catalogue/pagination.tsx b/src/renderer/src/pages/catalogue/pagination.tsx index c41635d0..ecc2afe3 100644 --- a/src/renderer/src/pages/catalogue/pagination.tsx +++ b/src/renderer/src/pages/catalogue/pagination.tsx @@ -58,7 +58,7 @@ export function Pagination({ page, totalPages, onPageChange, -}: PaginationProps) { +}: Readonly) { const { formatNumber } = useFormat(); const [isJumpOpen, setIsJumpOpen] = useState(false); @@ -90,12 +90,12 @@ export function Pagination({ const onJumpChange = (e: ChangeEvent) => { const raw = e.target.value; - const digitsOnly = raw.replace(/\D+/g, ""); + const digitsOnly = raw.replaceAll(/\D+/g, ""); if (digitsOnly === "") { setJumpValue(""); return; } - const num = parseInt(digitsOnly, 10); + const num = Number.parseInt(digitsOnly, 10); if (Number.isNaN(num)) { setJumpValue(""); return; @@ -127,9 +127,9 @@ export function Pagination({ } if (e.key === "Enter") { - const sanitized = jumpValue.replace(/\D+/g, ""); + const sanitized = jumpValue.replaceAll(/\D+/g, ""); if (sanitized.trim() === "") return; - const parsed = parseInt(sanitized, 10); + const parsed = Number.parseInt(sanitized, 10); if (Number.isNaN(parsed)) return; const target = Math.max(1, Math.min(totalPages, parsed)); onPageChange(target); From c71f5947ba8dfb7856625477289bb9b2f44346f2 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Fri, 31 Oct 2025 10:20:11 -0300 Subject: [PATCH 07/11] feat: use new ep to track game playtime --- src/main/services/library-sync/update-game-playtime.ts | 4 ++-- src/main/services/process-watcher.ts | 8 ++++---- src/main/services/window-manager.ts | 8 +------- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/main/services/library-sync/update-game-playtime.ts b/src/main/services/library-sync/update-game-playtime.ts index b53ebebc..a669a363 100644 --- a/src/main/services/library-sync/update-game-playtime.ts +++ b/src/main/services/library-sync/update-game-playtime.ts @@ -1,7 +1,7 @@ import type { Game } from "@types"; import { HydraApi } from "../hydra-api"; -export const updateGamePlaytime = async ( +export const trackGamePlaytime = async ( game: Game, deltaInMillis: number, lastTimePlayed: Date @@ -10,7 +10,7 @@ export const updateGamePlaytime = async ( return; } - return HydraApi.put(`/profile/games/${game.remoteId}`, { + return HydraApi.put(`/profile/games/${game.shop}/${game.objectId}`, { playTimeDeltaInSeconds: Math.trunc(deltaInMillis / 1000), lastTimePlayed, }); diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts index a1449255..db5bbee1 100644 --- a/src/main/services/process-watcher.ts +++ b/src/main/services/process-watcher.ts @@ -1,5 +1,5 @@ import { WindowManager } from "./window-manager"; -import { createGame, updateGamePlaytime } from "./library-sync"; +import { createGame, trackGamePlaytime } from "./library-sync"; import type { Game, GameRunning, UserPreferences } from "@types"; import { PythonRPC } from "./python-rpc"; import axios from "axios"; @@ -223,7 +223,7 @@ function onOpenGame(game: Game) { ); if (game.remoteId) { - updateGamePlaytime( + trackGamePlaytime( game, game.unsyncedDeltaPlayTimeInMilliseconds ?? 0, new Date() @@ -277,7 +277,7 @@ function onTickGame(game: Game) { (game.unsyncedDeltaPlayTimeInMilliseconds ?? 0); const gamePromise = game.remoteId - ? updateGamePlaytime(game, deltaToSync, game.lastTimePlayed!) + ? trackGamePlaytime(game, deltaToSync, game.lastTimePlayed!) : createGame(game); gamePromise @@ -337,7 +337,7 @@ const onCloseGame = (game: Game) => { gamePlaytime.lastSyncTick + (game.unsyncedDeltaPlayTimeInMilliseconds ?? 0); - return updateGamePlaytime(game, deltaToSync, game.lastTimePlayed!) + return trackGamePlaytime(game, deltaToSync, game.lastTimePlayed!) .then(() => { return gamesSublevel.put(levelKeys.game(game.shop, game.objectId), { ...updatedGame, diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index 673bf1a0..2484e8e7 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -289,12 +289,6 @@ export class WindowManager { } } - private static loadNotificationWindowURL() { - if (this.notificationWindow) { - this.loadWindowURL(this.notificationWindow, "achievement-notification"); - } - } - private static readonly NOTIFICATION_WINDOW_WIDTH = 360; private static readonly NOTIFICATION_WINDOW_HEIGHT = 140; @@ -387,7 +381,7 @@ export class WindowManager { this.notificationWindow.setIgnoreMouseEvents(true); this.notificationWindow.setAlwaysOnTop(true, "screen-saver", 1); - this.loadNotificationWindowURL(); + this.loadWindowURL(this.notificationWindow, "achievement-notification"); if (!app.isPackaged || isStaging) { this.notificationWindow.webContents.openDevTools(); From 138120460cf0957eaf584162172f8791c2c318e1 Mon Sep 17 00:00:00 2001 From: jarexe Date: Fri, 31 Oct 2025 10:57:44 -0300 Subject: [PATCH 08/11] fix: correct achievement notification positioning on multi-monitor setups --- src/main/services/window-manager.ts | 38 +++++++++++++++++++---------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index 673bf1a0..834bf7ab 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -302,46 +302,58 @@ export class WindowManager { position: AchievementCustomNotificationPosition | undefined ) { const display = screen.getPrimaryDisplay(); - const { width, height } = display.workAreaSize; + const { + x: displayX, + y: displayY, + width: displayWidth, + height: displayHeight, + } = display.bounds; if (position === "bottom-left") { return { - x: 0, - y: height - this.NOTIFICATION_WINDOW_HEIGHT, + x: displayX, + y: displayY + displayHeight - this.NOTIFICATION_WINDOW_HEIGHT, }; } if (position === "bottom-center") { return { - x: (width - this.NOTIFICATION_WINDOW_WIDTH) / 2, - y: height - this.NOTIFICATION_WINDOW_HEIGHT, + x: displayX + (displayWidth - this.NOTIFICATION_WINDOW_WIDTH) / 2, + y: displayY + displayHeight - this.NOTIFICATION_WINDOW_HEIGHT, }; } if (position === "bottom-right") { return { - x: width - this.NOTIFICATION_WINDOW_WIDTH, - y: height - this.NOTIFICATION_WINDOW_HEIGHT, + x: displayX + displayWidth - this.NOTIFICATION_WINDOW_WIDTH, + y: displayY + displayHeight - this.NOTIFICATION_WINDOW_HEIGHT, + }; + } + + if (position === "top-left") { + return { + x: displayX, + y: displayY, }; } if (position === "top-center") { return { - x: (width - this.NOTIFICATION_WINDOW_WIDTH) / 2, - y: 0, + x: displayX + (displayWidth - this.NOTIFICATION_WINDOW_WIDTH) / 2, + y: displayY, }; } if (position === "top-right") { return { - x: width - this.NOTIFICATION_WINDOW_WIDTH, - y: 0, + x: displayX + displayWidth - this.NOTIFICATION_WINDOW_WIDTH, + y: displayY, }; } return { - x: 0, - y: 0, + x: displayX, + y: displayY, }; } From 51c4e4f5b38b411f0ac92d13a1b15d8b676ceecc Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Fri, 31 Oct 2025 13:07:06 -0300 Subject: [PATCH 09/11] chore: bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5d84e763..ee039574 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "hydralauncher", - "version": "3.7.2", + "version": "3.7.3", "description": "Hydra", "main": "./out/main/index.js", "author": "Los Broxas", From d167628ed44e22c6090f352ee57cdb5710b32811 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Fri, 31 Oct 2025 13:57:15 -0300 Subject: [PATCH 10/11] fix: prevent crash when detectedLanguage is null --- src/renderer/src/pages/game-details/review-item.tsx | 5 +++-- src/types/index.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/renderer/src/pages/game-details/review-item.tsx b/src/renderer/src/pages/game-details/review-item.tsx index 7e407e20..c411e0cb 100644 --- a/src/renderer/src/pages/game-details/review-item.tsx +++ b/src/renderer/src/pages/game-details/review-item.tsx @@ -81,7 +81,8 @@ export function ReviewItem({ const needsTranslation = !isOwnReview && isDifferentLanguage && review.translations[i18n.language]; - const getLanguageName = (languageCode: string) => { + const getLanguageName = (languageCode: string | null) => { + if (!languageCode) return ""; try { const displayNames = new Intl.DisplayNames([i18n.language], { type: "language", @@ -182,7 +183,7 @@ export function ReviewItem({ {showOriginal ? t("hide_original") : t("show_original_translated_from", { - language: getLanguageName(review.detectedLanguage!), + language: getLanguageName(review.detectedLanguage), })} {showOriginal && ( diff --git a/src/types/index.ts b/src/types/index.ts index 4b13c496..c04b6232 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -252,7 +252,7 @@ export interface GameReview { translations: { [key: string]: string; }; - detectedLanguage: string; + detectedLanguage: string | null; } export interface TrendingGame extends ShopAssets { From 19bf99ff119f65394bcdaed84d0a1423472b1d88 Mon Sep 17 00:00:00 2001 From: Zamitto <167933696+zamitto@users.noreply.github.com> Date: Fri, 31 Oct 2025 16:16:03 -0300 Subject: [PATCH 11/11] chore: add sleep to aur script --- .github/workflows/update-aur.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/update-aur.yml b/.github/workflows/update-aur.yml index 52fe907e..fa12b500 100644 --- a/.github/workflows/update-aur.yml +++ b/.github/workflows/update-aur.yml @@ -95,6 +95,8 @@ jobs: - name: Update PKGBUILD and .SRCINFO if: steps.check-update.outputs.update_needed == 'true' run: | + # sleeps for 1 minute to be sure GH updated the release info + sleep 60 # Update pkgver in PKGBUILD cd hydra-launcher-bin NEW_VERSION="${{ steps.get-version.outputs.version }}"