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 }}" 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", diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 782bce0b..79b78393 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -362,7 +362,10 @@ "show_original": "Show original", "show_translation": "Show translation", "show_original_translated_from": "Show original (translated from {{language}})", - "hide_original": "Hide original" + "hide_original": "Hide original", + "review_from_blocked_user": "Review from blocked user", + "show": "Show", + "hide": "Hide" }, "activation": { "title": "Activate Hydra", diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json index 863b8332..adf25e33 100644 --- a/src/locales/es/translation.json +++ b/src/locales/es/translation.json @@ -361,7 +361,10 @@ "you_seemed_to_enjoy_this_game": "Parece que has disfrutado de este juego", "language": "Idioma", "caption": "Subtítulo", - "audio": "Audio" + "audio": "Audio", + "review_from_blocked_user": "Reseña de usuario bloqueado", + "show": "Mostrar", + "hide": "Ocultar" }, "activation": { "title": "Activar Hydra", diff --git a/src/locales/pt-BR/translation.json b/src/locales/pt-BR/translation.json index 5bfc2af3..42743a64 100755 --- a/src/locales/pt-BR/translation.json +++ b/src/locales/pt-BR/translation.json @@ -349,7 +349,10 @@ "show_translation": "Mostrar tradução", "show_original_translated_from": "Mostrar original (traduzido do {{language}})", "hide_original": "Ocultar original", - "rating_count": "Avaliação" + "rating_count": "Avaliação", + "review_from_blocked_user": "Avaliação de usuário bloqueado", + "show": "Mostrar", + "hide": "Ocultar" }, "activation": { "title": "Ativação", diff --git a/src/locales/pt-PT/translation.json b/src/locales/pt-PT/translation.json index 2894cf65..6c1963cc 100644 --- a/src/locales/pt-PT/translation.json +++ b/src/locales/pt-PT/translation.json @@ -180,7 +180,10 @@ "download_error_not_cached_on_torbox": "Este download não está disponível no TorBox e a verificação do status do download não está disponível.", "game_removed_from_favorites": "Jogo removido dos favoritos", "game_added_to_favorites": "Jogo adicionado aos favoritos", - "create_start_menu_shortcut": "Criar atalho no Menu Iniciar" + "create_start_menu_shortcut": "Criar atalho no Menu Iniciar", + "review_from_blocked_user": "Avaliação de utilizador bloqueado", + "show": "Mostrar", + "hide": "Ocultar" }, "activation": { "title": "Ativação", diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json index 15a9c9cb..6f4d4b92 100644 --- a/src/locales/ru/translation.json +++ b/src/locales/ru/translation.json @@ -351,6 +351,8 @@ "audio": "Аудио", "filter_by_source": "Фильтр по источнику", "no_repacks_found": "Источники для этой игры не найдены", + "show": "Показать", + "hide": "Скрыть", "delete_review": "Удалить отзыв", "remove_review": "Удалить отзыв", "delete_review_modal_title": "Вы уверены, что хотите удалить свой отзыв?", @@ -361,7 +363,8 @@ "show_original": "Показать оригинал", "show_translation": "Показать перевод", "show_original_translated_from": "Показать оригинал (переведено с {{language}})", - "hide_original": "Скрыть оригинал" + "hide_original": "Скрыть оригинал", + "review_from_blocked_user": "Отзыв от заблокированного пользователя" }, "activation": { "title": "Активировать Hydra", 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..a669a363 100644 --- a/src/main/services/library-sync/update-game-playtime.ts +++ b/src/main/services/library-sync/update-game-playtime.ts @@ -1,12 +1,16 @@ import type { Game } from "@types"; import { HydraApi } from "../hydra-api"; -export const updateGamePlaytime = async ( +export const trackGamePlaytime = async ( game: Game, deltaInMillis: number, lastTimePlayed: Date ) => { - return HydraApi.put(`/profile/games/${game.remoteId}`, { + if (game.shop === "custom") { + return; + } + + 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 6408c30d..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"; @@ -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,8 +215,15 @@ function onOpenGame(game: Game) { }) .catch(() => {}); + if (game.shop === "custom") return; + + AchievementWatcherManager.firstSyncWithRemoteIfNeeded( + game.shop, + game.objectId + ); + if (game.remoteId) { - updateGamePlaytime( + trackGamePlaytime( game, game.unsyncedDeltaPlayTimeInMilliseconds ?? 0, new Date() @@ -255,43 +257,46 @@ 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 + (game.unsyncedDeltaPlayTimeInMilliseconds ?? 0); const gamePromise = game.remoteId - ? updateGamePlaytime(game, deltaToSync, game.lastTimePlayed!) + ? trackGamePlaytime(game, deltaToSync, game.lastTimePlayed!) : createGame(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!) + return trackGamePlaytime(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, }); }); diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index 673bf1a0..b11b4a9b 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; @@ -302,46 +296,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, }; } @@ -387,7 +393,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(); diff --git a/src/renderer/src/components/header/header.tsx b/src/renderer/src/components/header/header.tsx index e61f3954..6f97729f 100644 --- a/src/renderer/src/components/header/header.tsx +++ b/src/renderer/src/components/header/header.tsx @@ -60,7 +60,7 @@ export function Header() { }; const handleSearch = (value: string) => { - dispatch(setFilters({ title: value })); + dispatch(setFilters({ title: value.slice(0, 255) })); if (!location.pathname.startsWith("/catalogue")) { navigate("/catalogue"); diff --git a/src/renderer/src/context/game-details/game-details.context.tsx b/src/renderer/src/context/game-details/game-details.context.tsx index c5b88607..bc1a6351 100644 --- a/src/renderer/src/context/game-details/game-details.context.tsx +++ b/src/renderer/src/context/game-details/game-details.context.tsx @@ -293,6 +293,8 @@ export function GameDetailsContextProvider({ }, [objectId, shop, userDetails]); useEffect(() => { + if (shop === "custom") return; + const fetchDownloadSources = async () => { try { const sources = await window.electron.getDownloadSources(); diff --git a/src/renderer/src/pages/catalogue/catalogue.tsx b/src/renderer/src/pages/catalogue/catalogue.tsx index bbeda906..b9eb3c24 100644 --- a/src/renderer/src/pages/catalogue/catalogue.tsx +++ b/src/renderer/src/pages/catalogue/catalogue.tsx @@ -35,7 +35,7 @@ export default function Catalogue() { const { steamDevelopers, steamPublishers, downloadSources } = useCatalogue(); - const { steamGenres, steamUserTags } = useAppSelector( + const { steamGenres, steamUserTags, filters, page } = useAppSelector( (state) => state.catalogueSearch ); @@ -47,8 +47,6 @@ export default function Catalogue() { const { formatNumber } = useFormat(); - const { filters, page } = useAppSelector((state) => state.catalogueSearch); - const dispatch = useAppDispatch(); const { t, i18n } = useTranslation("catalogue"); diff --git a/src/renderer/src/pages/game-details/game-reviews.tsx b/src/renderer/src/pages/game-details/game-reviews.tsx index 1a6fc675..2dfd8864 100644 --- a/src/renderer/src/pages/game-details/game-reviews.tsx +++ b/src/renderer/src/pages/game-details/game-reviews.tsx @@ -163,7 +163,6 @@ export function GameReviews({ take: "20", skip: skip.toString(), sortBy: reviewsSortBy, - language: i18n.language, }); const response = await window.electron.hydraApi.get( diff --git a/src/renderer/src/pages/game-details/review-item.tsx b/src/renderer/src/pages/game-details/review-item.tsx index 82d7128c..6b03e7ea 100644 --- a/src/renderer/src/pages/game-details/review-item.tsx +++ b/src/renderer/src/pages/game-details/review-item.tsx @@ -66,25 +66,18 @@ export function ReviewItem({ const [showOriginal, setShowOriginal] = useState(false); - // Check if this is the user's own review const isOwnReview = userDetailsId === review.user.id; - // Helper to get base language code (e.g., "pt" from "pt-BR") - const getBaseLanguage = (lang: string) => lang.split("-")[0]; + const getBaseLanguage = (lang: string | null) => lang?.split("-")[0] || ""; - // Check if the review is in a different language (comparing base language codes) const isDifferentLanguage = getBaseLanguage(review.detectedLanguage) !== getBaseLanguage(i18n.language); - // Check if translation is available and needed (but not for own reviews) const needsTranslation = - !isOwnReview && - isDifferentLanguage && - review.translations && - review.translations[i18n.language]; + !isOwnReview && isDifferentLanguage && review.translations[i18n.language]; - // Get the full language name using Intl.DisplayNames - const getLanguageName = (languageCode: string) => { + const getLanguageName = (languageCode: string | null) => { + if (!languageCode) return ""; try { const displayNames = new Intl.DisplayNames([i18n.language], { type: "language", @@ -118,12 +111,12 @@ export function ReviewItem({ return (