diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index cef82a4b..995b427f 100755 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -542,7 +542,9 @@ "hidden": "Hidden", "test_notification": "Test notification", "notification_preview": "Achievement Notification Preview", - "enable_friend_start_game_notifications": "When a friend starts playing a game" + "enable_friend_start_game_notifications": "When a friend starts playing a game", + "autoplay_trailers_on_game_page": "Automatically start playing trailers on game page", + "hide_to_tray_on_game_start": "Hide Hydra to tray on game startup" }, "notifications": { "download_complete": "Download complete", diff --git a/src/main/events/catalogue/get-game-assets.ts b/src/main/events/catalogue/get-game-assets.ts index de1d2b1f..0e45f886 100644 --- a/src/main/events/catalogue/get-game-assets.ts +++ b/src/main/events/catalogue/get-game-assets.ts @@ -6,6 +6,10 @@ import { gamesShopAssetsSublevel, levelKeys } from "@main/level"; const LOCAL_CACHE_EXPIRATION = 1000 * 60 * 60 * 8; // 8 hours export const getGameAssets = async (objectId: string, shop: GameShop) => { + if (shop === "custom") { + return null; + } + const cachedAssets = await gamesShopAssetsSublevel.get( levelKeys.game(shop, objectId) ); diff --git a/src/main/events/catalogue/get-game-shop-details.ts b/src/main/events/catalogue/get-game-shop-details.ts index d6d27b9c..1a7fc455 100644 --- a/src/main/events/catalogue/get-game-shop-details.ts +++ b/src/main/events/catalogue/get-game-shop-details.ts @@ -26,6 +26,8 @@ const getGameShopDetails = async ( shop: GameShop, language: string ): Promise => { + if (shop === "custom") return null; + if (shop === "steam") { const [cachedData, cachedAssets] = await Promise.all([ gamesShopCacheSublevel.get( diff --git a/src/main/events/catalogue/get-game-stats.ts b/src/main/events/catalogue/get-game-stats.ts index b836531d..b7b7125c 100644 --- a/src/main/events/catalogue/get-game-stats.ts +++ b/src/main/events/catalogue/get-game-stats.ts @@ -10,6 +10,10 @@ const getGameStats = async ( objectId: string, shop: GameShop ) => { + if (shop === "custom") { + return null; + } + const cachedStats = await gamesStatsCacheSublevel.get( levelKeys.game(shop, objectId) ); diff --git a/src/main/events/library/add-game-to-favorites.ts b/src/main/events/library/add-game-to-favorites.ts index 68c81abb..53985a09 100644 --- a/src/main/events/library/add-game-to-favorites.ts +++ b/src/main/events/library/add-game-to-favorites.ts @@ -13,7 +13,9 @@ const addGameToFavorites = async ( const game = await gamesSublevel.get(gameKey); if (!game) return; - HydraApi.put(`/profile/games/${shop}/${objectId}/favorite`).catch(() => {}); + if (shop !== "custom") { + HydraApi.put(`/profile/games/${shop}/${objectId}/favorite`).catch(() => {}); + } try { await gamesSublevel.put(gameKey, { diff --git a/src/main/events/library/remove-game-from-favorites.ts b/src/main/events/library/remove-game-from-favorites.ts index f06f55ce..7c79cbf4 100644 --- a/src/main/events/library/remove-game-from-favorites.ts +++ b/src/main/events/library/remove-game-from-favorites.ts @@ -13,7 +13,11 @@ const removeGameFromFavorites = async ( const game = await gamesSublevel.get(gameKey); if (!game) return; - HydraApi.put(`/profile/games/${shop}/${objectId}/unfavorite`).catch(() => {}); + if (shop !== "custom") { + HydraApi.put(`/profile/games/${shop}/${objectId}/unfavorite`).catch( + () => {} + ); + } try { await gamesSublevel.put(gameKey, { diff --git a/src/main/events/library/remove-game-from-library.ts b/src/main/events/library/remove-game-from-library.ts index fbb60ab2..95133c70 100644 --- a/src/main/events/library/remove-game-from-library.ts +++ b/src/main/events/library/remove-game-from-library.ts @@ -84,7 +84,7 @@ const removeGameFromLibrary = async ( await resetShopAssets(gameKey); } - if (game?.remoteId) { + if (game.remoteId) { HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {}); } diff --git a/src/main/services/achievements/get-game-achievement-data.ts b/src/main/services/achievements/get-game-achievement-data.ts index ffbfac1a..69437801 100644 --- a/src/main/services/achievements/get-game-achievement-data.ts +++ b/src/main/services/achievements/get-game-achievement-data.ts @@ -27,6 +27,10 @@ export const getGameAchievementData = async ( shop: GameShop, useCachedData: boolean ) => { + if (shop === "custom") { + return []; + } + const gameKey = levelKeys.game(shop, objectId); const cachedAchievements = await gameAchievementsSublevel.get(gameKey); diff --git a/src/main/services/process-watcher.ts b/src/main/services/process-watcher.ts index 06f5f7d8..6408c30d 100644 --- a/src/main/services/process-watcher.ts +++ b/src/main/services/process-watcher.ts @@ -1,10 +1,10 @@ import { WindowManager } from "./window-manager"; import { createGame, updateGamePlaytime } from "./library-sync"; -import type { Game, GameRunning } from "@types"; +import type { Game, GameRunning, UserPreferences } from "@types"; import { PythonRPC } from "./python-rpc"; import axios from "axios"; import { ProcessPayload } from "./download/types"; -import { gamesSublevel, levelKeys } from "@main/level"; +import { db, gamesSublevel, levelKeys } from "@main/level"; import { CloudSync } from "./cloud-sync"; import { logger } from "./logger"; import path from "path"; @@ -209,6 +209,17 @@ function onOpenGame(game: Game) { lastSyncTick: now, }); + // Hide Hydra to tray on game startup if enabled + db.get(levelKeys.userPreferences, { + valueEncoding: "json", + }) + .then((userPreferences) => { + if (userPreferences?.hideToTrayOnGameStart) { + WindowManager.mainWindow?.hide(); + } + }) + .catch(() => {}); + if (game.remoteId) { updateGamePlaytime( game, diff --git a/src/main/services/window-manager.ts b/src/main/services/window-manager.ts index 7055fc09..118ff98b 100644 --- a/src/main/services/window-manager.ts +++ b/src/main/services/window-manager.ts @@ -462,6 +462,7 @@ export class WindowManager { editorWindow.once("ready-to-show", () => { editorWindow.show(); + this.mainWindow?.webContents.openDevTools(); if (!app.isPackaged || isStaging) { editorWindow.webContents.openDevTools(); } @@ -469,11 +470,12 @@ export class WindowManager { editorWindow.webContents.on("before-input-event", (_event, input) => { if (input.key === "F12") { - editorWindow.webContents.toggleDevTools(); + this.mainWindow?.webContents.toggleDevTools(); } }); editorWindow.on("close", () => { + this.mainWindow?.webContents.closeDevTools(); this.editorWindows.delete(themeId); }); } diff --git a/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx b/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx index b94c94d7..abc359e9 100644 --- a/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx +++ b/src/renderer/src/context/cloud-sync/cloud-sync.context.tsx @@ -98,6 +98,11 @@ export function CloudSyncContextProvider({ ); const getGameArtifacts = useCallback(async () => { + if (shop === "custom") { + setArtifacts([]); + return; + } + const params = new URLSearchParams({ objectId, shop, 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 14e5d587..3706b02e 100644 --- a/src/renderer/src/context/game-details/game-details.context.tsx +++ b/src/renderer/src/context/game-details/game-details.context.tsx @@ -142,10 +142,12 @@ export function GameDetailsContextProvider({ } }); - window.electron.getGameStats(objectId, shop).then((result) => { - if (abortController.signal.aborted) return; - setStats(result); - }); + if (shop !== "custom") { + window.electron.getGameStats(objectId, shop).then((result) => { + if (abortController.signal.aborted) return; + setStats(result); + }); + } const assetsPromise = window.electron.getGameAssets(objectId, shop); @@ -167,7 +169,7 @@ export function GameDetailsContextProvider({ setIsLoading(false); }); - if (userDetails) { + if (userDetails && shop !== "custom") { window.electron .getUnlockedAchievements(objectId, shop) .then((achievements) => { diff --git a/src/renderer/src/pages/catalogue/pagination.scss b/src/renderer/src/pages/catalogue/pagination.scss index 141dfe54..cac10211 100644 --- a/src/renderer/src/pages/catalogue/pagination.scss +++ b/src/renderer/src/pages/catalogue/pagination.scss @@ -1,3 +1,5 @@ +@use "../../scss/globals.scss"; + .pagination { display: flex; gap: 4px; @@ -18,4 +20,31 @@ font-size: 16px; } } + + &__page-input { + box-sizing: border-box; + width: 40px; + min-width: 40px; + max-width: 40px; + min-height: 40px; + border-radius: 8px; + border: solid 1px globals.$border-color; + background-color: transparent; + color: globals.$muted-color; + text-align: center; + font-size: 12px; + padding: 0 6px; + outline: none; + } + + &__double-chevron { + display: flex; + align-items: center; + justify-content: center; + font-size: 0; // remove whitespace node width between SVGs + } + + &__double-chevron > svg + svg { + margin-left: -8px; // pull the second chevron closer + } } diff --git a/src/renderer/src/pages/catalogue/pagination.tsx b/src/renderer/src/pages/catalogue/pagination.tsx index dfae6164..9febc8f8 100644 --- a/src/renderer/src/pages/catalogue/pagination.tsx +++ b/src/renderer/src/pages/catalogue/pagination.tsx @@ -1,8 +1,51 @@ import { Button } from "@renderer/components/button/button"; import { ChevronLeftIcon, ChevronRightIcon } from "@primer/octicons-react"; import { useFormat } from "@renderer/hooks/use-format"; +import { useEffect, useRef, useState } from "react"; +import type { ChangeEvent, KeyboardEvent, RefObject } from "react"; import "./pagination.scss"; +interface JumpControlProps { + isOpen: boolean; + value: string; + totalPages: number; + inputRef: RefObject; + onOpen: () => void; + onClose: () => void; + onChange: (e: ChangeEvent) => void; + onKeyDown: (e: KeyboardEvent) => void; +} + +function JumpControl({ + isOpen, + value, + totalPages, + inputRef, + onOpen, + onClose, + onChange, + onKeyDown, +}: JumpControlProps) { + return isOpen ? ( + + ) : ( + + ); +} + interface PaginationProps { page: number; totalPages: number; @@ -16,20 +59,82 @@ export function Pagination({ }: PaginationProps) { const { formatNumber } = useFormat(); + const [isJumpOpen, setIsJumpOpen] = useState(false); + const [jumpValue, setJumpValue] = useState(""); + const jumpInputRef = useRef(null); + + useEffect(() => { + if (isJumpOpen) { + setJumpValue(""); + setTimeout(() => jumpInputRef.current?.focus(), 0); + } + }, [isJumpOpen, page]); + if (totalPages <= 1) return null; const visiblePages = 3; + const isLastThree = totalPages > 3 && page >= totalPages - 2; let startPage = Math.max(1, page - 1); let endPage = startPage + visiblePages - 1; - if (endPage > totalPages) { + if (isLastThree) { + startPage = Math.max(1, totalPages - 2); + endPage = totalPages; + } else if (endPage > totalPages) { endPage = totalPages; startPage = Math.max(1, endPage - visiblePages + 1); } + const onJumpChange = (e: ChangeEvent) => { + const val = e.target.value; + if (val === "") { + setJumpValue(""); + return; + } + const num = Number(val); + if (Number.isNaN(num)) { + return; + } + if (num < 1) { + setJumpValue("1"); + return; + } + if (num > totalPages) { + setJumpValue(String(totalPages)); + return; + } + setJumpValue(val); + }; + + const onJumpKeyDown = (e: KeyboardEvent) => { + if (e.key === "Enter") { + if (jumpValue.trim() === "") return; + const parsed = Number(jumpValue); + 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); + } + }; + return (
+ {startPage > 1 && ( + + )} + - {page > 2 && ( + {isLastThree && startPage > 1 && ( <> - -
- ... -
+ setIsJumpOpen(true)} + onClose={() => setIsJumpOpen(false)} + onChange={onJumpChange} + onKeyDown={onJumpKeyDown} + /> )} @@ -70,11 +180,18 @@ export function Pagination({ ))} - {page < totalPages - 1 && ( + {!isLastThree && page < totalPages - 1 && ( <> -
- ... -
+ setIsJumpOpen(true)} + onClose={() => setIsJumpOpen(false)} + onChange={onJumpChange} + onKeyDown={onJumpKeyDown} + /> + + {endPage < totalPages && ( + + )}
); } diff --git a/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.tsx b/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.tsx index 4bf8dc48..c9658636 100644 --- a/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.tsx +++ b/src/renderer/src/pages/game-details/gallery-slider/gallery-slider.tsx @@ -7,11 +7,16 @@ import { } from "@primer/octicons-react"; import useEmblaCarousel from "embla-carousel-react"; import { gameDetailsContext } from "@renderer/context"; +import { useAppSelector } from "@renderer/hooks"; import "./gallery-slider.scss"; export function GallerySlider() { const { shopDetails } = useContext(gameDetailsContext); const { t } = useTranslation("game_details"); + const userPreferences = useAppSelector( + (state) => state.userPreferences.value + ); + const autoplayEnabled = userPreferences?.autoplayGameTrailers !== false; const hasScreenshots = shopDetails && shopDetails.screenshots?.length; @@ -164,7 +169,7 @@ export function GallerySlider() { poster={item.poster} loop muted - autoPlay + autoPlay={autoplayEnabled} tabIndex={-1} > diff --git a/src/renderer/src/pages/game-details/game-details-content.tsx b/src/renderer/src/pages/game-details/game-details-content.tsx index ab51a212..63c4c974 100644 --- a/src/renderer/src/pages/game-details/game-details-content.tsx +++ b/src/renderer/src/pages/game-details/game-details-content.tsx @@ -228,7 +228,7 @@ export function GameDetailsContent() { )} - {game?.shop !== "custom" && shop && objectId && ( + {shop !== "custom" && shop && objectId && ( - {game?.shop !== "custom" && } + {shop !== "custom" && } diff --git a/src/renderer/src/pages/game-details/game-reviews.tsx b/src/renderer/src/pages/game-details/game-reviews.tsx index f70c84b2..1a6fc675 100644 --- a/src/renderer/src/pages/game-details/game-reviews.tsx +++ b/src/renderer/src/pages/game-details/game-reviews.tsx @@ -117,7 +117,7 @@ export function GameReviews({ }); const checkUserReview = useCallback(async () => { - if (!objectId || !userDetailsId) return; + if (!objectId || !userDetailsId || shop === "custom") return; try { const response = await window.electron.hydraApi.get<{ @@ -147,7 +147,7 @@ export function GameReviews({ const loadReviews = useCallback( async (reset = false) => { - if (!objectId) return; + if (!objectId || shop === "custom") return; if (abortControllerRef.current) { abortControllerRef.current.abort(); diff --git a/src/renderer/src/pages/settings/settings-behavior.tsx b/src/renderer/src/pages/settings/settings-behavior.tsx index 64df52d7..c5698ef7 100644 --- a/src/renderer/src/pages/settings/settings-behavior.tsx +++ b/src/renderer/src/pages/settings/settings-behavior.tsx @@ -27,6 +27,8 @@ export function SettingsBehavior() { showDownloadSpeedInMegabytes: false, extractFilesByDefault: true, enableSteamAchievements: false, + autoplayGameTrailers: true, + hideToTrayOnGameStart: false, }); const { t } = useTranslation("settings"); @@ -49,6 +51,8 @@ export function SettingsBehavior() { extractFilesByDefault: userPreferences.extractFilesByDefault ?? true, enableSteamAchievements: userPreferences.enableSteamAchievements ?? false, + autoplayGameTrailers: userPreferences.autoplayGameTrailers ?? true, + hideToTrayOnGameStart: userPreferences.hideToTrayOnGameStart ?? false, }); } }, [userPreferences]); @@ -76,6 +80,16 @@ export function SettingsBehavior() { } /> + + handleChange({ + hideToTrayOnGameStart: !form.hideToTrayOnGameStart, + }) + } + /> + {showRunAtStartup && ( )} + + handleChange({ autoplayGameTrailers: !form.autoplayGameTrailers }) + } + /> +