diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 49aa25e2..564daa84 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -4,6 +4,7 @@ module.exports = {
"plugin:react/recommended",
"plugin:react/jsx-runtime",
"plugin:react-hooks/recommended",
+ "plugin:jsx-a11y/recommended",
"@electron-toolkit/eslint-config-ts/recommended",
"prettier",
],
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 293a898b..5d5fc277 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -1,6 +1,6 @@
name: Lint
-on: [pull_request, push]
+on: [pull_request]
jobs:
lint:
diff --git a/package.json b/package.json
index 5830a320..85043c80 100644
--- a/package.json
+++ b/package.json
@@ -87,6 +87,7 @@
"electron-builder": "^24.9.1",
"electron-vite": "^2.0.0",
"eslint": "^8.56.0",
+ "eslint-plugin-jsx-a11y": "^6.8.0",
"eslint-plugin-react": "^7.33.2",
"eslint-plugin-react-hooks": "^4.6.0",
"husky": "^9.0.11",
diff --git a/resources/hydra.db b/resources/hydra.db
deleted file mode 100644
index e69de29b..00000000
diff --git a/src/locales/be/translation.json b/src/locales/be/translation.json
index ccada6a7..c55ec394 100644
--- a/src/locales/be/translation.json
+++ b/src/locales/be/translation.json
@@ -88,7 +88,6 @@
"repacks_modal_description": "Абярыце рэпак, які хочаце сьцягнуць",
"downloads_path": "Шлях сьцягваньня",
"select_folder_hint": "Каб зьмяніць папку па змоўчаньні, адкрыйце",
- "settings": "Налады Hydra",
"download_now": "Сьцягнуць зараз",
"installation_instructions": "Інструкцыя ўсталёўкі",
"installation_instructions_description": "Усталёўка гэтай гульні патрабуе дадатковых крокаў",
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index 0674d1b5..7b54b889 100644
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -86,7 +86,6 @@
"playing_now": "Playing now",
"change": "Change",
"repacks_modal_description": "Choose the repack you want to download",
- "downloads_path": "Downloads path",
"select_folder_hint": "To change the default folder, go to the <0>Settings0>",
"download_now": "Download now",
"installation_instructions": "Installation Instructions",
@@ -96,7 +95,14 @@
"dont_show_it_again": "Don't show it again",
"copy_to_clipboard": "Copy",
"copied_to_clipboard": "Copied",
- "got_it": "Got it"
+ "got_it": "Got it",
+ "no_shop_details": "Could not retrieve shop details.",
+ "download_options": "Download options",
+ "download_path": "Download path",
+ "previous_screenshot": "Previous screenshot",
+ "next_screenshot": "Next screenshot",
+ "screenshot": "Screenshot {{number}}",
+ "open_screenshot": "Open screenshot {{number}}"
},
"activation": {
"title": "Activate Hydra",
@@ -139,7 +145,7 @@
"enable_repack_list_notifications": "When a new repack is added",
"telemetry": "Telemetry",
"telemetry_description": "Enable anonymous usage statistics",
- "real_debrid_api_token_description": "Real Debrid API token",
+ "real_debrid_api_token_label": "Real Debrid API token",
"quit_app_instead_hiding": "Quit Hydra instead of minimizing to tray",
"launch_with_system": "Launch Hydra on system start-up",
"general": "General",
diff --git a/src/locales/es/translation.json b/src/locales/es/translation.json
index a692fd16..1c97adae 100644
--- a/src/locales/es/translation.json
+++ b/src/locales/es/translation.json
@@ -85,7 +85,6 @@
"repacks_modal_description": "Selecciona el repack que quieres descargar",
"downloads_path": "Ruta de descarga",
"select_folder_hint": "Para cambiar la carpeta predeterminada, accede a",
- "settings": "Ajustes",
"download_now": "Descargar ahora",
"installation_instructions": "Instrucciones de instalación",
"installation_instructions_description": "Se requieren de pasos adicionales para instalar este juego",
diff --git a/src/locales/id/translation.json b/src/locales/id/translation.json
index 49d3a991..60de327a 100644
--- a/src/locales/id/translation.json
+++ b/src/locales/id/translation.json
@@ -88,7 +88,6 @@
"repacks_modal_description": "Pilih repack yang kamu ingin unduh",
"downloads_path": "Lokasi Unduhan",
"select_folder_hint": "Untuk merubah folder bawaan, akses melalui",
- "settings": "Pengaturan",
"download_now": "Unduh sekarang",
"installation_instructions": "Instruksi Instalasi",
"installation_instructions_description": "Langkah tambahan dibutuhkan untuk meng-instal game ini",
diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json
index 9889dd0c..57706dc9 100644
--- a/src/locales/it/translation.json
+++ b/src/locales/it/translation.json
@@ -88,7 +88,6 @@
"repacks_modal_description": "Scegli il repack che vuoi scaricare",
"downloads_path": "Percorso dei download",
"select_folder_hint": "Per cambiare la cartella predefinita, accedi alle",
- "settings": "Impostazioni",
"download_now": "Scarica ora",
"installation_instructions": "Istruzioni di installazione",
"installation_instructions_description": "Sono necessari passaggi aggiuntivi per installare questo gioco",
diff --git a/src/locales/nl/translation.json b/src/locales/nl/translation.json
index 22cb1d90..4be69007 100644
--- a/src/locales/nl/translation.json
+++ b/src/locales/nl/translation.json
@@ -139,7 +139,7 @@
"enable_repack_list_notifications": "Wanneer een nieuwe herverpakking wordt toegevoegd",
"telemetry": "Telemetrie",
"telemetry_description": "Schakel anonieme gebruiksstatistieken in",
- "real_debrid_api_token_description": "Real Debrid API token",
+ "real_debrid_api_token_label": "Real Debrid API token",
"quit_app_instead_hiding": "Sluit Hydra af in plaats van te minimaliseren naar de lade",
"launch_with_system": "Start Hydra bij het opstarten van het systeem",
"general": "Algemeen",
diff --git a/src/locales/pl/translation.json b/src/locales/pl/translation.json
index 5623c74e..5214019e 100644
--- a/src/locales/pl/translation.json
+++ b/src/locales/pl/translation.json
@@ -82,7 +82,6 @@
"repacks_modal_description": "Wybierz repack, który chcesz pobrać",
"downloads_path": "Ścieżka pobierania",
"select_folder_hint": "Aby zmienić domyślny folder, przejdź do",
- "settings": "Ustawienia Hydra",
"download_now": "Pobierz teraz"
},
"activation": {
diff --git a/src/locales/pt/translation.json b/src/locales/pt/translation.json
index dda53065..57ec0470 100644
--- a/src/locales/pt/translation.json
+++ b/src/locales/pt/translation.json
@@ -82,7 +82,6 @@
"playing_now": "Jogando agora",
"change": "Mudar",
"repacks_modal_description": "Escolha o repack do jogo que deseja baixar",
- "downloads_path": "Diretório do download",
"select_folder_hint": "Para trocar a pasta padrão, acesse a <0>Tela de Configurações0>",
"download_now": "Baixe agora",
"installation_instructions": "Instruções de Instalação",
@@ -92,7 +91,14 @@
"dont_show_it_again": "Não mostrar novamente",
"copy_to_clipboard": "Copiar",
"copied_to_clipboard": "Copiado",
- "got_it": "Entendi"
+ "got_it": "Entendi",
+ "no_shop_details": "Não foi possível obter os detalhes da loja.",
+ "download_options": "Opções de download",
+ "download_path": "Diretório de download",
+ "previous_screenshot": "Captura de tela anterior",
+ "next_screenshot": "Próxima captura de tela",
+ "screenshot": "Captura de tela {{number}}",
+ "open_screenshot": "Ver captura de tela {{number}}"
},
"activation": {
"title": "Ativação",
@@ -123,7 +129,9 @@
"delete_modal_description": "Isso removerá todos os arquivos de instalação do seu computador",
"delete_modal_title": "Tem certeza?",
"deleting": "Excluindo instalador…",
- "install": "Instalar"
+ "install": "Instalar",
+ "torrent": "Torrent",
+ "real_debrid": "Real Debrid"
},
"settings": {
"downloads_path": "Diretório dos downloads",
@@ -133,6 +141,7 @@
"enable_repack_list_notifications": "Quando a lista de repacks for atualizada",
"telemetry": "Telemetria",
"telemetry_description": "Habilitar estatísticas de uso anônimas",
+ "real_debrid_api_token_label": "Token de API do Real Debrid",
"quit_app_instead_hiding": "Fechar o aplicativo em vez de minimizá-lo",
"launch_with_system": "Iniciar aplicativo na inicialização do sistema",
"general": "Geral",
diff --git a/src/locales/ru/translation.json b/src/locales/ru/translation.json
index efeaba37..c2c30cd5 100644
--- a/src/locales/ru/translation.json
+++ b/src/locales/ru/translation.json
@@ -88,7 +88,6 @@
"repacks_modal_description": "Выберите репак для загрузки",
"downloads_path": "Путь загрузок",
"select_folder_hint": "Изменить папку по умолчанию",
- "settings": "Настройки Hydra",
"download_now": "Загрузить сейчас",
"installation_instructions": "Инструкция по установке",
"installation_instructions_description": "Для установки этой игры требуются дополнительные шаги",
diff --git a/src/locales/tr/translation.json b/src/locales/tr/translation.json
index 6a7033b3..be40e013 100644
--- a/src/locales/tr/translation.json
+++ b/src/locales/tr/translation.json
@@ -88,7 +88,6 @@
"repacks_modal_description": "İndirmek istediğiiniz repacki seçin",
"downloads_path": "İndirme yolu",
"select_folder_hint": "Varsayılan klasörü değiştirmek için ulaşmanız gereken ayar",
- "settings": "Ayarlar",
"download_now": "Şimdi",
"installation_instructions": "Kurulum",
"installation_instructions_description": "Bu oyunu kurmak için ek adımlar gerekiyor",
diff --git a/src/locales/uk/translation.json b/src/locales/uk/translation.json
index e8d1c117..e0f6af7b 100644
--- a/src/locales/uk/translation.json
+++ b/src/locales/uk/translation.json
@@ -88,7 +88,6 @@
"repacks_modal_description": "Виберіть репак, який хочете завантажити",
"downloads_path": "Шлях завантажень",
"select_folder_hint": "Щоб змінити теку за замовчуванням, відкрийте",
- "settings": "Налаштування Hydra",
"download_now": "Завантажити зараз",
"installation_instructions": "Інструкція зі встановлення",
"installation_instructions_description": "Для встановлення цієї гри потрібні додаткові кроки",
diff --git a/src/main/events/catalogue/get-game-shop-details.ts b/src/main/events/catalogue/get-game-shop-details.ts
index 61629242..bbc3b08a 100644
--- a/src/main/events/catalogue/get-game-shop-details.ts
+++ b/src/main/events/catalogue/get-game-shop-details.ts
@@ -1,10 +1,32 @@
-import { gameShopCacheRepository } from "@main/repository";
+import { gameShopCacheRepository, steamGameRepository } from "@main/repository";
import { getSteamAppDetails } from "@main/services";
import type { ShopDetails, GameShop, SteamAppDetails } from "@types";
import { registerEvent } from "../register-event";
-import { searchRepacks } from "../helpers/search-games";
+
+const getLocalizedSteamAppDetails = (
+ objectID: string,
+ language: string
+): Promise => {
+ if (language === "english") {
+ return getSteamAppDetails(objectID, language);
+ }
+
+ return Promise.all([
+ steamGameRepository.findOne({ where: { id: Number(objectID) } }),
+ getSteamAppDetails(objectID, language),
+ ]).then(([steamGame, localizedAppDetails]) => {
+ if (steamGame && localizedAppDetails) {
+ return {
+ ...localizedAppDetails,
+ name: steamGame.name,
+ };
+ }
+
+ return null;
+ });
+};
const getGameShopDetails = async (
_event: Electron.IpcMainInvokeEvent,
@@ -17,27 +39,21 @@ const getGameShopDetails = async (
where: { objectID, language },
});
- const result = Promise.all([
- getSteamAppDetails(objectID, "english"),
- getSteamAppDetails(objectID, language),
- ]).then(([appDetails, localizedAppDetails]) => {
- if (appDetails && localizedAppDetails) {
+ const appDetails = getLocalizedSteamAppDetails(objectID, language).then(
+ (result) => {
gameShopCacheRepository.upsert(
{
objectID,
shop: "steam",
language,
- serializedData: JSON.stringify({
- ...localizedAppDetails,
- name: appDetails.name,
- }),
+ serializedData: JSON.stringify(result),
},
["objectID"]
);
- }
- return [appDetails, localizedAppDetails];
- });
+ return result;
+ }
+ );
const cachedGame = cachedData?.serializedData
? (JSON.parse(cachedData?.serializedData) as SteamAppDetails)
@@ -46,21 +62,11 @@ const getGameShopDetails = async (
if (cachedGame) {
return {
...cachedGame,
- repacks: searchRepacks(cachedGame.name),
objectID,
} as ShopDetails;
}
- return result.then(([appDetails, localizedAppDetails]) => {
- if (!appDetails || !localizedAppDetails) return null;
-
- return {
- ...localizedAppDetails,
- name: appDetails.name,
- repacks: searchRepacks(appDetails.name),
- objectID,
- } as ShopDetails;
- });
+ return Promise.resolve(appDetails);
}
throw new Error("Not implemented");
diff --git a/src/main/events/catalogue/get-random-game.ts b/src/main/events/catalogue/get-random-game.ts
index 72f9cd90..dd3741e3 100644
--- a/src/main/events/catalogue/get-random-game.ts
+++ b/src/main/events/catalogue/get-random-game.ts
@@ -1,9 +1,10 @@
import { shuffle } from "lodash-es";
-import { Steam250Game, getSteam250List } from "@main/services";
+import { getSteam250List } from "@main/services";
import { registerEvent } from "../register-event";
import { searchGames, searchRepacks } from "../helpers/search-games";
+import type { Steam250Game } from "@types";
const state = { games: Array(), index: 0 };
@@ -25,8 +26,6 @@ const getRandomGame = async (_event: Electron.IpcMainInvokeEvent) => {
return "";
}
- const resultObjectId = state.games[state.index].objectID;
-
state.index += 1;
if (state.index == state.games.length) {
@@ -34,7 +33,7 @@ const getRandomGame = async (_event: Electron.IpcMainInvokeEvent) => {
state.games = shuffle(state.games);
}
- return resultObjectId;
+ return state.games[state.index];
};
registerEvent(getRandomGame, {
diff --git a/src/main/events/catalogue/search-game-repacks.ts b/src/main/events/catalogue/search-game-repacks.ts
new file mode 100644
index 00000000..448c6daf
--- /dev/null
+++ b/src/main/events/catalogue/search-game-repacks.ts
@@ -0,0 +1,14 @@
+import { searchRepacks } from "../helpers/search-games";
+import { registerEvent } from "../register-event";
+
+const searchGameRepacks = (
+ _event: Electron.IpcMainInvokeEvent,
+ query: string
+) => {
+ return searchRepacks(query);
+};
+
+registerEvent(searchGameRepacks, {
+ name: "searchGameRepacks",
+ memoize: true,
+});
diff --git a/src/main/events/index.ts b/src/main/events/index.ts
index 822cb9d5..ab35ff79 100644
--- a/src/main/events/index.ts
+++ b/src/main/events/index.ts
@@ -7,6 +7,7 @@ import "./catalogue/get-games";
import "./catalogue/get-how-long-to-beat";
import "./catalogue/get-random-game";
import "./catalogue/search-games";
+import "./catalogue/search-game-repacks";
import "./hardware/get-disk-free-space";
import "./library/add-game-to-library";
import "./library/close-game";
diff --git a/src/main/services/real-debrid.ts b/src/main/services/real-debrid.ts
index 44798062..355a59b3 100644
--- a/src/main/services/real-debrid.ts
+++ b/src/main/services/real-debrid.ts
@@ -12,8 +12,7 @@ export class RealDebridClient {
private static instance: AxiosInstance;
static async addMagnet(magnet: string) {
- const searchParams = new URLSearchParams();
- searchParams.append("magnet", magnet);
+ const searchParams = new URLSearchParams({ magnet });
const response = await this.instance.post(
"/torrents/addMagnet",
@@ -31,8 +30,7 @@ export class RealDebridClient {
}
static async selectAllFiles(id: string) {
- const searchParams = new URLSearchParams();
- searchParams.append("files", "all");
+ const searchParams = new URLSearchParams({ files: "all" });
await this.instance.post(
`/torrents/selectFiles/${id}`,
@@ -41,8 +39,7 @@ export class RealDebridClient {
}
static async unrestrictLink(link: string) {
- const searchParams = new URLSearchParams();
- searchParams.append("link", link);
+ const searchParams = new URLSearchParams({ link });
const response = await this.instance.post(
"/unrestrict/link",
diff --git a/src/main/services/steam-250.ts b/src/main/services/steam-250.ts
index db505b47..9833c278 100644
--- a/src/main/services/steam-250.ts
+++ b/src/main/services/steam-250.ts
@@ -1,10 +1,7 @@
import axios from "axios";
import { JSDOM } from "jsdom";
-export interface Steam250Game {
- title: string;
- objectID: string;
-}
+import type { Steam250Game } from "@types";
export const requestSteam250 = async (path: string) => {
return axios
diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts
deleted file mode 100644
index 51163f1a..00000000
--- a/src/preload/index.d.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-// See the Electron documentation for details on how to use preload scripts:
-// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
-import { contextBridge, ipcRenderer } from "electron";
-
-import type {
- CatalogueCategory,
- GameShop,
- TorrentProgress,
- UserPreferences,
-} from "@types";
-
-contextBridge.exposeInMainWorld("electron", {
- /* Torrenting */
- startGameDownload: (
- repackId: number,
- objectID: string,
- title: string,
- shop: GameShop
- ) => ipcRenderer.invoke("startGameDownload", repackId, objectID, title, shop),
- cancelGameDownload: (gameId: number) =>
- ipcRenderer.invoke("cancelGameDownload", gameId),
- pauseGameDownload: (gameId: number) =>
- ipcRenderer.invoke("pauseGameDownload", gameId),
- resumeGameDownload: (gameId: number) =>
- ipcRenderer.invoke("resumeGameDownload", gameId),
- onDownloadProgress: (cb: (value: TorrentProgress) => void) => {
- const listener = (
- _event: Electron.IpcRendererEvent,
- value: TorrentProgress
- ) => cb(value);
- ipcRenderer.on("on-download-progress", listener);
- return () => ipcRenderer.removeListener("on-download-progress", listener);
- },
-
- /* Catalogue */
- searchGames: (query: string) => ipcRenderer.invoke("searchGames", query),
- getCatalogue: (category: CatalogueCategory) =>
- ipcRenderer.invoke("getCatalogue", category),
- getGameShopDetails: (objectID: string, shop: GameShop, language: string) =>
- ipcRenderer.invoke("getGameShopDetails", objectID, shop, language),
- getRandomGame: () => ipcRenderer.invoke("getRandomGame"),
- getHowLongToBeat: (objectID: string, shop: GameShop, title: string) =>
- ipcRenderer.invoke("getHowLongToBeat", objectID, shop, title),
- getGames: (take?: number, prevCursor?: number) =>
- ipcRenderer.invoke("getGames", take, prevCursor),
-
- /* User preferences */
- getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"),
- updateUserPreferences: (preferences: UserPreferences) =>
- ipcRenderer.invoke("updateUserPreferences", preferences),
- autoLaunch: (enabled: boolean) => ipcRenderer.invoke("autoLaunch", enabled),
-
- /* Library */
- addGameToLibrary: (
- objectID: string,
- title: string,
- shop: GameShop,
- executablePath: string
- ) =>
- ipcRenderer.invoke(
- "addGameToLibrary",
- objectID,
- title,
- shop,
- executablePath
- ),
- getLibrary: () => ipcRenderer.invoke("getLibrary"),
- getRepackersFriendlyNames: () =>
- ipcRenderer.invoke("getRepackersFriendlyNames"),
- openGameInstaller: (gameId: number) =>
- ipcRenderer.invoke("openGameInstaller", gameId),
- openGame: (gameId: number, executablePath: string) =>
- ipcRenderer.invoke("openGame", gameId, executablePath),
- closeGame: (gameId: number) => ipcRenderer.invoke("closeGame", gameId),
- removeGameFromLibrary: (gameId: number) =>
- ipcRenderer.invoke("removeGameFromLibrary", gameId),
- deleteGameFolder: (gameId: number) =>
- ipcRenderer.invoke("deleteGameFolder", gameId),
- getGameByObjectID: (objectID: string) =>
- ipcRenderer.invoke("getGameByObjectID", objectID),
- onPlaytime: (cb: (gameId: number) => void) => {
- const listener = (_event: Electron.IpcRendererEvent, gameId: number) =>
- cb(gameId);
- ipcRenderer.on("on-playtime", listener);
- return () => ipcRenderer.removeListener("on-playtime", listener);
- },
- onGameClose: (cb: (gameId: number) => void) => {
- const listener = (_event: Electron.IpcRendererEvent, gameId: number) =>
- cb(gameId);
- ipcRenderer.on("on-game-close", listener);
- return () => ipcRenderer.removeListener("on-game-close", listener);
- },
-
- /* Hardware */
- getDiskFreeSpace: () => ipcRenderer.invoke("getDiskFreeSpace"),
-
- /* Misc */
- ping: () => ipcRenderer.invoke("ping"),
- getVersion: () => ipcRenderer.invoke("getVersion"),
- getDefaultDownloadsPath: () => ipcRenderer.invoke("getDefaultDownloadsPath"),
- openExternal: (src: string) => ipcRenderer.invoke("openExternal", src),
- showOpenDialog: (options: Electron.OpenDialogOptions) =>
- ipcRenderer.invoke("showOpenDialog", options),
- platform: process.platform,
-});
diff --git a/src/preload/index.ts b/src/preload/index.ts
index 7d5eb7fe..9151942f 100644
--- a/src/preload/index.ts
+++ b/src/preload/index.ts
@@ -52,6 +52,8 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("getHowLongToBeat", objectID, shop, title),
getGames: (take?: number, prevCursor?: number) =>
ipcRenderer.invoke("getGames", take, prevCursor),
+ searchGameRepacks: (query: string) =>
+ ipcRenderer.invoke("searchGameRepacks", query),
/* User preferences */
getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"),
diff --git a/src/renderer/src/components/bottom-panel/bottom-panel.tsx b/src/renderer/src/components/bottom-panel/bottom-panel.tsx
index 6cce070e..44d125cd 100644
--- a/src/renderer/src/components/bottom-panel/bottom-panel.tsx
+++ b/src/renderer/src/components/bottom-panel/bottom-panel.tsx
@@ -64,7 +64,7 @@ export function BottomPanel() {
{status}
-
+
v{version} "{VERSION_CODENAME}"
diff --git a/src/renderer/src/components/checkbox-field/checkbox-field.tsx b/src/renderer/src/components/checkbox-field/checkbox-field.tsx
index 9a7e71d5..bb81a910 100644
--- a/src/renderer/src/components/checkbox-field/checkbox-field.tsx
+++ b/src/renderer/src/components/checkbox-field/checkbox-field.tsx
@@ -24,7 +24,7 @@ export function CheckboxField({ label, ...props }: CheckboxFieldProps) {
/>
{props.checked && }
-
- {t("publisher", { publisher: gameDetails?.publishers[0] })}
+ {t("publisher", { publisher: gameDetails.publishers[0] })}
(null);
+ const mediaContainerRef = useRef(null);
+
+ const { t } = useTranslation("game_details");
+
+ const hasScreenshots = gameDetails && gameDetails.screenshots.length;
+ const hasMovies = gameDetails && gameDetails.movies?.length;
const [mediaCount] = useState(() => {
- if (gameDetails) {
- if (gameDetails.screenshots && gameDetails.movies) {
- return gameDetails.screenshots.length + gameDetails.movies.length;
- } else if (gameDetails.movies) {
- return gameDetails.movies.length;
- } else if (gameDetails.screenshots) {
- return gameDetails.screenshots.length;
- }
+ if (gameDetails.screenshots && gameDetails.movies) {
+ return gameDetails.screenshots.length + gameDetails.movies.length;
+ } else if (gameDetails.movies) {
+ return gameDetails.movies.length;
+ } else if (gameDetails.screenshots) {
+ return gameDetails.screenshots.length;
}
return 0;
});
const [mediaIndex, setMediaIndex] = useState(0);
- const [arrowShow, setArrowShow] = useState(false);
+ const [showArrows, setShowArrows] = useState(false);
const showNextImage = () => {
setMediaIndex((index: number) => {
@@ -47,6 +54,20 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) {
setMediaIndex(0);
}, [gameDetails]);
+ useEffect(() => {
+ if (hasMovies && mediaContainerRef.current) {
+ mediaContainerRef.current.childNodes.forEach((node, index) => {
+ if (node instanceof HTMLVideoElement) {
+ if (index == mediaIndex) {
+ node.play();
+ } else {
+ node.pause();
+ }
+ }
+ });
+ }
+ }, [hasMovies, mediaContainerRef, mediaIndex]);
+
useEffect(() => {
if (scrollContainerRef.current) {
const container = scrollContainerRef.current;
@@ -57,92 +78,107 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) {
}
}, [gameDetails, mediaIndex, mediaCount]);
- const hasScreenshots = gameDetails && gameDetails.screenshots.length;
- const hasMovies = gameDetails && gameDetails.movies?.length;
+ const previews = useMemo(() => {
+ const screenshotPreviews =
+ gameDetails?.screenshots.map(({ id, path_thumbnail }) => ({
+ id,
+ thumbnail: path_thumbnail,
+ })) ?? [];
+
+ if (gameDetails?.movies) {
+ const moviePreviews = gameDetails.movies.map(({ id, thumbnail }) => ({
+ id,
+ thumbnail,
+ }));
+
+ return [...moviePreviews, ...screenshotPreviews];
+ }
+
+ return screenshotPreviews;
+ }, [gameDetails]);
return (
<>
{hasScreenshots && (
setArrowShow(true)}
- onMouseLeave={() => setArrowShow(false)}
+ onMouseEnter={() => setShowArrows(true)}
+ onMouseLeave={() => setShowArrows(false)}
className={styles.gallerySliderAnimationContainer}
+ ref={mediaContainerRef}
>
{gameDetails.movies &&
- gameDetails.movies.map((video: SteamMovies) => (
+ gameDetails.movies.map((video) => (
))}
- {gameDetails.screenshots &&
- gameDetails.screenshots.map(
- (image: SteamScreenshot, i: number) => (
-

- )
- )}
- {arrowShow && (
- <>
-
-
-
-
-
-
- >
- )}
+ {hasScreenshots &&
+ gameDetails.screenshots.map((image, i) => (
+

+ ))}
+
+
+
+
+
+
+
+
- {hasMovies &&
- gameDetails.movies?.map((video: SteamMovies, i: number) => (
+ {previews.map((media, i) => (
+
setMediaIndex(i)}
+ aria-label={t("open_screenshot", { number: i + 1 })}
+ >
setMediaIndex(i)}
- src={video.thumbnail}
- className={`${styles.gallerySliderMediaPreview} ${mediaIndex === i ? styles.gallerySliderMediaPreviewActive : ""}`}
+ src={media.thumbnail}
+ className={styles.mediaPreview}
+ alt={t("screenshot", { number: i + 1 })}
/>
- ))}
- {gameDetails.screenshots &&
- gameDetails.screenshots.map(
- (image: SteamScreenshot, i: number) => (
-
- setMediaIndex(
- i + (gameDetails.movies ? gameDetails.movies.length : 0)
- )
- }
- className={`${styles.gallerySliderMediaPreview} ${mediaIndex === i + (gameDetails.movies ? gameDetails.movies.length : 0) ? styles.gallerySliderMediaPreviewActive : ""}`}
- src={image.path_full}
- />
- )
- )}
+
+ ))}
)}
diff --git a/src/renderer/src/pages/game-details/game-details-skeleton.tsx b/src/renderer/src/pages/game-details/game-details-skeleton.tsx
index 1334362b..be481247 100644
--- a/src/renderer/src/pages/game-details/game-details-skeleton.tsx
+++ b/src/renderer/src/pages/game-details/game-details-skeleton.tsx
@@ -1,7 +1,10 @@
import Skeleton from "react-loading-skeleton";
import { Button } from "@renderer/components";
+
import * as styles from "./game-details.css";
+import * as sidebarStyles from "./sidebar/sidebar.css";
+
import { useTranslation } from "react-i18next";
import { ShareAndroidIcon } from "@primer/octicons-react";
@@ -43,41 +46,41 @@ export function GameDetailsSkeleton() {
-
-
+
+
HowLongToBeat
-
+
{Array.from({ length: 3 }).map((_, index) => (
))}
{t("requirements")}
-
+
{t("minimum")}
{t("recommended")}
-
+
{Array.from({ length: 6 }).map((_, index) => (
))}
diff --git a/src/renderer/src/pages/game-details/game-details.css.ts b/src/renderer/src/pages/game-details/game-details.css.ts
index 72c5e4d3..dadfb641 100644
--- a/src/renderer/src/pages/game-details/game-details.css.ts
+++ b/src/renderer/src/pages/game-details/game-details.css.ts
@@ -79,62 +79,6 @@ export const descriptionContent = style({
height: "100%",
});
-export const contentSidebar = style({
- borderLeft: `solid 1px ${vars.color.border};`,
- width: "100%",
- height: "100%",
- "@media": {
- "(min-width: 768px)": {
- width: "100%",
- maxWidth: "200px",
- },
- "(min-width: 1024px)": {
- maxWidth: "300px",
- width: "100%",
- },
- "(min-width: 1280px)": {
- width: "100%",
- maxWidth: "400px",
- },
- },
-});
-
-export const contentSidebarTitle = style({
- height: "72px",
- padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
- display: "flex",
- alignItems: "center",
- backgroundColor: vars.color.background,
-});
-
-export const requirementButtonContainer = style({
- width: "100%",
- display: "flex",
-});
-
-export const requirementButton = style({
- border: `solid 1px ${vars.color.border};`,
- borderLeft: "none",
- borderRight: "none",
- borderRadius: "0",
- width: "100%",
-});
-
-export const requirementsDetails = style({
- padding: `${SPACING_UNIT * 2}px`,
- lineHeight: "22px",
- fontFamily: "'Fira Sans', sans-serif",
- fontSize: "16px",
-});
-
-export const requirementsDetailsSkeleton = style({
- display: "flex",
- flexDirection: "column",
- gap: "8px",
- padding: `${SPACING_UNIT * 2}px`,
- fontSize: "16px",
-});
-
export const description = style({
userSelect: "text",
lineHeight: "22px",
@@ -183,34 +127,6 @@ export const descriptionHeaderInfo = style({
flexDirection: "column",
});
-export const howLongToBeatCategoriesList = style({
- margin: "0",
- padding: "16px",
- display: "flex",
- flexDirection: "column",
- gap: "16px",
-});
-
-export const howLongToBeatCategory = style({
- display: "flex",
- flexDirection: "column",
- gap: "4px",
- backgroundColor: vars.color.background,
- borderRadius: "8px",
- padding: `8px 16px`,
- border: `solid 1px ${vars.color.border}`,
-});
-
-export const howLongToBeatCategoryLabel = style({
- color: vars.color.muted,
-});
-
-export const howLongToBeatCategorySkeleton = style({
- border: `solid 1px ${vars.color.border}`,
- borderRadius: "8px",
- height: "76px",
-});
-
export const randomizerButton = style({
animationName: slideIn,
animationDuration: "0.2s",
@@ -260,8 +176,3 @@ globalStyle(`${description} img`, {
globalStyle(`${description} a`, {
color: vars.color.bodyText,
});
-
-globalStyle(`${requirementsDetails} a`, {
- display: "flex",
- color: vars.color.bodyText,
-});
diff --git a/src/renderer/src/pages/game-details/game-details.tsx b/src/renderer/src/pages/game-details/game-details.tsx
index 02c880d8..1b481104 100644
--- a/src/renderer/src/pages/game-details/game-details.tsx
+++ b/src/renderer/src/pages/game-details/game-details.tsx
@@ -3,18 +3,21 @@ import { average } from "color.js";
import { useCallback, useEffect, useState } from "react";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
-import type {
- Game,
- GameRepack,
- GameShop,
- HowLongToBeatCategory,
- ShopDetails,
- SteamAppDetails,
+import {
+ Steam250Game,
+ type Game,
+ type GameRepack,
+ type GameShop,
+ type ShopDetails,
} from "@types";
import { Button } from "@renderer/components";
import { setHeaderTitle } from "@renderer/features";
-import { getSteamLanguage, steamUrlBuilder } from "@renderer/helpers";
+import {
+ buildGameDetailsPath,
+ getSteamLanguage,
+ steamUrlBuilder,
+} from "@renderer/helpers";
import { useAppDispatch, useDownload } from "@renderer/hooks";
import starsAnimation from "@renderer/assets/lottie/stars.json";
@@ -26,7 +29,6 @@ import { DescriptionHeader } from "./description-header";
import { GameDetailsSkeleton } from "./game-details-skeleton";
import * as styles from "./game-details.css";
import { HeroPanel } from "./hero";
-import { HowLongToBeatSection } from "./how-long-to-beat-section";
import { RepacksModal } from "./repacks-modal";
import { vars } from "../../theme.css";
@@ -37,18 +39,16 @@ import {
OnlineFixInstallationGuide,
} from "./installation-guides";
import { GallerySlider } from "./gallery-slider";
+import { Sidebar } from "./sidebar/sidebar";
export function GameDetails() {
const { objectID, shop } = useParams();
const [isLoading, setIsLoading] = useState(false);
- const [isLoadingRandomGame, setIsLoadingRandomGame] = useState(false);
+ const [randomGame, setRandomGame] = useState(null);
const [color, setColor] = useState({ dark: "", light: "" });
const [gameDetails, setGameDetails] = useState(null);
- const [howLongToBeat, setHowLongToBeat] = useState<{
- isLoading: boolean;
- data: HowLongToBeatCategory[] | null;
- }>({ isLoading: true, data: null });
+ const [repacks, setRepacks] = useState([]);
const [game, setGame] = useState(null);
const [isGamePlaying, setIsGamePlaying] = useState(false);
@@ -56,12 +56,12 @@ export function GameDetails() {
null | "onlinefix" | "DODI"
>(null);
- const [activeRequirement, setActiveRequirement] =
- useState("minimum");
-
const navigate = useNavigate();
const [searchParams] = useSearchParams();
+ const fromRandomizer = searchParams.get("fromRandomizer");
+ const title = searchParams.get("title")!;
+
const { t, i18n } = useTranslation("game_details");
const [showRepacksModal, setShowRepacksModal] = useState(false);
@@ -90,37 +90,35 @@ export function GameDetails() {
useEffect(() => {
getGame();
}, [getGame, gameDownloading?.id]);
+
useEffect(() => {
setGame(null);
setIsLoading(true);
setIsGamePlaying(false);
- dispatch(setHeaderTitle(""));
+ dispatch(setHeaderTitle(title));
- window.electron
- .getGameShopDetails(objectID!, "steam", getSteamLanguage(i18n.language))
- .then((result) => {
- if (!result) {
- navigate(-1);
- return;
- }
+ window.electron.getRandomGame().then((randomGame) => {
+ setRandomGame(randomGame);
+ });
- window.electron
- .getHowLongToBeat(objectID!, "steam", result.name)
- .then((data) => {
- setHowLongToBeat({ isLoading: false, data });
- });
-
- setGameDetails(result);
- dispatch(setHeaderTitle(result.name));
- setIsLoadingRandomGame(false);
+ Promise.all([
+ window.electron.getGameShopDetails(
+ objectID!,
+ "steam",
+ getSteamLanguage(i18n.language)
+ ),
+ window.electron.searchGameRepacks(title),
+ ])
+ .then(([appDetails, repacks]) => {
+ if (appDetails) setGameDetails(appDetails);
+ setRepacks(repacks);
})
.finally(() => {
setIsLoading(false);
});
getGame();
- setHowLongToBeat({ isLoading: true, data: null });
- }, [getGame, dispatch, navigate, objectID, i18n.language]);
+ }, [getGame, dispatch, navigate, title, objectID, i18n.language]);
const isGameDownloading = gameDownloading?.id === game?.id;
@@ -154,55 +152,49 @@ export function GameDetails() {
repack: GameRepack,
downloadPath: string
) => {
- if (gameDetails) {
- return startDownload(
- repack.id,
- gameDetails.objectID,
- gameDetails.name,
- shop as GameShop,
- downloadPath
- ).then(() => {
- getGame();
- setShowRepacksModal(false);
+ return startDownload(
+ repack.id,
+ objectID!,
+ title,
+ shop as GameShop,
+ downloadPath
+ ).then(() => {
+ getGame();
+ setShowRepacksModal(false);
- if (
- repack.repacker === "onlinefix" &&
- !window.localStorage.getItem(DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY)
- ) {
- setShowInstructionsModal("onlinefix");
- } else if (
- repack.repacker === "DODI" &&
- !window.localStorage.getItem(DONT_SHOW_DODI_INSTRUCTIONS_KEY)
- ) {
- setShowInstructionsModal("DODI");
- }
- });
+ if (
+ repack.repacker === "onlinefix" &&
+ !window.localStorage.getItem(DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY)
+ ) {
+ setShowInstructionsModal("onlinefix");
+ } else if (
+ repack.repacker === "DODI" &&
+ !window.localStorage.getItem(DONT_SHOW_DODI_INSTRUCTIONS_KEY)
+ ) {
+ setShowInstructionsModal("DODI");
+ }
+ });
+ };
+
+ const handleRandomizerClick = () => {
+ if (randomGame) {
+ navigate(
+ buildGameDetailsPath(
+ { ...randomGame, shop: "steam" },
+ { fromRandomizer: "1" }
+ )
+ );
}
};
- const handleRandomizerClick = async () => {
- setIsLoadingRandomGame(true);
- const randomGameObjectID = await window.electron.getRandomGame();
-
- const searchParams = new URLSearchParams({
- fromRandomizer: "1",
- });
-
- navigate(`/game/steam/${randomGameObjectID}?${searchParams.toString()}`);
- };
-
- const fromRandomizer = searchParams.get("fromRandomizer");
-
return (
- {gameDetails && (
- setShowRepacksModal(false)}
- />
- )}
+ setShowRepacksModal(false)}
+ />
@@ -239,7 +232,9 @@ export function GameDetails() {
setShowRepacksModal(true)}
getGame={getGame}
isGamePlaying={isGamePlaying}
@@ -247,63 +242,22 @@ export function GameDetails() {
-
-
-
+ {gameDetails &&
}
+ {gameDetails &&
}
-
-
-
-
-
{t("requirements")}
-
-
-
- setActiveRequirement("minimum")}
- theme={
- activeRequirement === "minimum" ? "primary" : "outline"
- }
- >
- {t("minimum")}
-
- setActiveRequirement("recommended")}
- theme={
- activeRequirement === "recommended" ? "primary" : "outline"
- }
- >
- {t("recommended")}
-
-
-
-
-
+
)}
@@ -313,7 +267,7 @@ export function GameDetails() {
className={styles.randomizerButton}
onClick={handleRandomizerClick}
theme="outline"
- disabled={isLoadingRandomGame}
+ disabled={!randomGame}
>
void;
openBinaryNotFoundModal: () => void;
getGame: () => void;
@@ -21,9 +23,11 @@ export interface HeroPanelActionsProps {
export function HeroPanelActions({
game,
- gameDetails,
isGamePlaying,
isGameDownloading,
+ repacks,
+ objectID,
+ title,
openRepacksModal,
openBinaryNotFoundModal,
getGame,
@@ -69,12 +73,12 @@ export function HeroPanelActions({
try {
if (game) {
await removeGameFromLibrary(game.id);
- } else if (gameDetails) {
+ } else {
const gameExecutablePath = await selectGameExecutable();
await window.electron.addGameToLibrary(
- gameDetails.objectID,
- gameDetails.name,
+ objectID,
+ title,
"steam",
gameExecutablePath
);
@@ -123,7 +127,7 @@ export function HeroPanelActions({
const toggleGameOnLibraryButton = (
@@ -239,7 +243,7 @@ export function HeroPanelActions({
);
}
- if (gameDetails && gameDetails.repacks.length) {
+ if (repacks.length) {
return (
<>
{toggleGameOnLibraryButton}
diff --git a/src/renderer/src/pages/game-details/hero/hero-panel.css.ts b/src/renderer/src/pages/game-details/hero/hero-panel.css.ts
index 1fe8f23f..640196e6 100644
--- a/src/renderer/src/pages/game-details/hero/hero-panel.css.ts
+++ b/src/renderer/src/pages/game-details/hero/hero-panel.css.ts
@@ -4,13 +4,13 @@ import { SPACING_UNIT, vars } from "../../../theme.css";
export const panel = style({
width: "100%",
height: "72px",
+ minHeight: "72px",
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
transition: "all ease 0.2s",
borderBottom: `solid 1px ${vars.color.border}`,
- boxShadow: "0px 0px 15px 0px #000000",
});
export const content = style({
diff --git a/src/renderer/src/pages/game-details/hero/hero-panel.tsx b/src/renderer/src/pages/game-details/hero/hero-panel.tsx
index 9dafc720..87a4b0ee 100644
--- a/src/renderer/src/pages/game-details/hero/hero-panel.tsx
+++ b/src/renderer/src/pages/game-details/hero/hero-panel.tsx
@@ -3,7 +3,7 @@ import { useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useDownload } from "@renderer/hooks";
-import type { Game, ShopDetails } from "@types";
+import type { Game, GameRepack } from "@types";
import { formatDownloadProgress } from "@renderer/helpers";
import { HeroPanelActions } from "./hero-panel-actions";
@@ -15,20 +15,24 @@ import { HeroPanelPlaytime } from "./hero-panel-playtime";
export interface HeroPanelProps {
game: Game | null;
- gameDetails: ShopDetails | null;
color: string;
isGamePlaying: boolean;
+ objectID: string;
+ title: string;
+ repacks: GameRepack[];
openRepacksModal: () => void;
getGame: () => void;
}
export function HeroPanel({
game,
- gameDetails,
color,
+ repacks,
+ objectID,
+ title,
+ isGamePlaying,
openRepacksModal,
getGame,
- isGamePlaying,
}: HeroPanelProps) {
const { t } = useTranslation("game_details");
@@ -58,8 +62,6 @@ export function HeroPanel({
}, [game, isGameDownloading, gameDownloading]);
const getInfo = () => {
- if (!gameDetails) return null;
-
if (isGameDeleting(game?.id ?? -1)) {
return {t("deleting")}
;
}
@@ -110,11 +112,11 @@ export function HeroPanel({
return ;
}
- const [latestRepack] = gameDetails.repacks;
+ const [latestRepack] = repacks;
if (latestRepack) {
const lastUpdate = format(latestRepack.uploadDate!, "dd/MM/yyyy");
- const repacksCount = gameDetails.repacks.length;
+ const repacksCount = repacks.length;
return (
<>
@@ -139,7 +141,9 @@ export function HeroPanel({
setShowBinaryNotFoundModal(true)}
diff --git a/src/renderer/src/pages/game-details/repacks-modal.tsx b/src/renderer/src/pages/game-details/repacks-modal.tsx
index 0e441520..3ee3631a 100644
--- a/src/renderer/src/pages/game-details/repacks-modal.tsx
+++ b/src/renderer/src/pages/game-details/repacks-modal.tsx
@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button, Modal, TextField } from "@renderer/components";
-import type { GameRepack, ShopDetails } from "@types";
+import type { GameRepack } from "@types";
import * as styles from "./repacks-modal.css";
@@ -13,14 +13,14 @@ import { SelectFolderModal } from "./select-folder-modal";
export interface RepacksModalProps {
visible: boolean;
- gameDetails: ShopDetails;
+ repacks: GameRepack[];
startDownload: (repack: GameRepack, downloadPath: string) => Promise;
onClose: () => void;
}
export function RepacksModal({
visible,
- gameDetails,
+ repacks,
startDownload,
onClose,
}: RepacksModalProps) {
@@ -35,8 +35,8 @@ export function RepacksModal({
const { t } = useTranslation("game_details");
useEffect(() => {
- setFilteredRepacks(gameDetails.repacks);
- }, [gameDetails.repacks, visible]);
+ setFilteredRepacks(repacks);
+ }, [repacks, visible]);
const handleRepackClick = (repack: GameRepack) => {
setRepack(repack);
@@ -47,7 +47,7 @@ export function RepacksModal({
const term = event.target.value.toLocaleLowerCase();
setFilteredRepacks(
- gameDetails.repacks.filter((repack) => {
+ repacks.filter((repack) => {
const lowerCaseTitle = repack.title.toLowerCase();
const lowerCaseRepacker = repack.repacker.toLowerCase();
@@ -63,14 +63,13 @@ export function RepacksModal({
setShowSelectFolderModal(false)}
- gameDetails={gameDetails}
startDownload={startDownload}
repack={repack}
/>
diff --git a/src/renderer/src/pages/game-details/select-folder-modal.tsx b/src/renderer/src/pages/game-details/select-folder-modal.tsx
index 97567c98..d43990dc 100644
--- a/src/renderer/src/pages/game-details/select-folder-modal.tsx
+++ b/src/renderer/src/pages/game-details/select-folder-modal.tsx
@@ -1,5 +1,5 @@
import { Button, Link, Modal, TextField } from "@renderer/components";
-import { GameRepack, ShopDetails } from "@types";
+import type { GameRepack } from "@types";
import { useEffect, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
@@ -10,7 +10,6 @@ import { formatBytes } from "@shared";
export interface SelectFolderModalProps {
visible: boolean;
- gameDetails: ShopDetails;
onClose: () => void;
startDownload: (repack: GameRepack, downloadPath: string) => Promise;
repack: GameRepack | null;
@@ -18,7 +17,6 @@ export interface SelectFolderModalProps {
export function SelectFolderModal({
visible,
- gameDetails,
onClose,
startDownload,
repack,
@@ -74,7 +72,7 @@ export function SelectFolderModal({
return (
-
+
= {
Hours: "hours",
diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.css.ts b/src/renderer/src/pages/game-details/sidebar/sidebar.css.ts
new file mode 100644
index 00000000..cae988ab
--- /dev/null
+++ b/src/renderer/src/pages/game-details/sidebar/sidebar.css.ts
@@ -0,0 +1,92 @@
+import { globalStyle, style } from "@vanilla-extract/css";
+
+import { SPACING_UNIT, vars } from "../../../theme.css";
+
+export const contentSidebar = style({
+ borderLeft: `solid 1px ${vars.color.border};`,
+ width: "100%",
+ height: "100%",
+ "@media": {
+ "(min-width: 768px)": {
+ width: "100%",
+ maxWidth: "200px",
+ },
+ "(min-width: 1024px)": {
+ maxWidth: "300px",
+ width: "100%",
+ },
+ "(min-width: 1280px)": {
+ width: "100%",
+ maxWidth: "400px",
+ },
+ },
+});
+
+export const contentSidebarTitle = style({
+ height: "72px",
+ padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 2}px`,
+ display: "flex",
+ alignItems: "center",
+ backgroundColor: vars.color.background,
+});
+
+export const requirementButtonContainer = style({
+ width: "100%",
+ display: "flex",
+});
+
+export const requirementButton = style({
+ border: `solid 1px ${vars.color.border};`,
+ borderLeft: "none",
+ borderRight: "none",
+ borderRadius: "0",
+ width: "100%",
+});
+
+export const requirementsDetails = style({
+ padding: `${SPACING_UNIT * 2}px`,
+ lineHeight: "22px",
+ fontFamily: "'Fira Sans', sans-serif",
+ fontSize: "16px",
+});
+
+export const requirementsDetailsSkeleton = style({
+ display: "flex",
+ flexDirection: "column",
+ gap: "8px",
+ padding: `${SPACING_UNIT * 2}px`,
+ fontSize: "16px",
+});
+
+export const howLongToBeatCategoriesList = style({
+ margin: "0",
+ padding: "16px",
+ display: "flex",
+ flexDirection: "column",
+ gap: "16px",
+});
+
+export const howLongToBeatCategory = style({
+ display: "flex",
+ flexDirection: "column",
+ gap: "4px",
+ backgroundColor: vars.color.background,
+ borderRadius: "8px",
+ padding: `8px 16px`,
+ border: `solid 1px ${vars.color.border}`,
+});
+
+export const howLongToBeatCategoryLabel = style({
+ color: vars.color.muted,
+});
+
+export const howLongToBeatCategorySkeleton = style({
+ border: `solid 1px ${vars.color.border}`,
+ borderRadius: "8px",
+ height: "76px",
+});
+
+globalStyle(`${requirementsDetails} a`, {
+ display: "flex",
+ color: vars.color.bodyText,
+});
diff --git a/src/renderer/src/pages/game-details/sidebar/sidebar.tsx b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx
new file mode 100644
index 00000000..ec9e12c7
--- /dev/null
+++ b/src/renderer/src/pages/game-details/sidebar/sidebar.tsx
@@ -0,0 +1,84 @@
+import { useEffect, useState } from "react";
+import { HowLongToBeatSection } from "./how-long-to-beat-section";
+import type {
+ HowLongToBeatCategory,
+ ShopDetails,
+ SteamAppDetails,
+} from "@types";
+import { useTranslation } from "react-i18next";
+import { Button } from "@renderer/components";
+
+import * as styles from "./sidebar.css";
+
+export interface SidebarProps {
+ objectID: string;
+ title: string;
+ gameDetails: ShopDetails | null;
+}
+
+export function Sidebar({ objectID, title, gameDetails }: SidebarProps) {
+ const [howLongToBeat, setHowLongToBeat] = useState<{
+ isLoading: boolean;
+ data: HowLongToBeatCategory[] | null;
+ }>({ isLoading: true, data: null });
+
+ const [activeRequirement, setActiveRequirement] =
+ useState("minimum");
+
+ const { t } = useTranslation("game_details");
+
+ useEffect(() => {
+ setHowLongToBeat({ isLoading: true, data: null });
+
+ window.electron
+ .getHowLongToBeat(objectID, "steam", title)
+ .then((howLongToBeat) => {
+ setHowLongToBeat({ isLoading: false, data: howLongToBeat });
+ })
+ .catch(() => {
+ setHowLongToBeat({ isLoading: false, data: null });
+ });
+ }, [objectID, title]);
+
+ return (
+
+ );
+}
diff --git a/src/renderer/src/pages/home/home.tsx b/src/renderer/src/pages/home/home.tsx
index 7230e51e..85f72250 100644
--- a/src/renderer/src/pages/home/home.tsx
+++ b/src/renderer/src/pages/home/home.tsx
@@ -1,17 +1,22 @@
-import { useCallback, useEffect, useRef, useState } from "react";
+import { useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useSearchParams } from "react-router-dom";
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
import { Button, GameCard, Hero } from "@renderer/components";
-import type { CatalogueCategory, CatalogueEntry } from "@types";
+import {
+ Steam250Game,
+ type CatalogueCategory,
+ type CatalogueEntry,
+} from "@types";
import starsAnimation from "@renderer/assets/lottie/stars.json";
import * as styles from "./home.css";
import { vars } from "../../theme.css";
import Lottie from "lottie-react";
+import { buildGameDetailsPath } from "@renderer/helpers";
const categories: CatalogueCategory[] = ["trending", "recently_added"];
@@ -20,8 +25,7 @@ export function Home() {
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
- const [isLoadingRandomGame, setIsLoadingRandomGame] = useState(false);
- const randomGameObjectID = useRef(null);
+ const [randomGame, setRandomGame] = useState(null);
const [searchParams] = useSearchParams();
@@ -56,24 +60,22 @@ export function Home() {
};
const getRandomGame = useCallback(() => {
- setIsLoadingRandomGame(true);
-
- window.electron.getRandomGame().then((objectID) => {
- if (objectID) {
- randomGameObjectID.current = objectID;
- setIsLoadingRandomGame(false);
- }
+ window.electron.getRandomGame().then((game) => {
+ if (game) setRandomGame(game);
});
}, []);
const handleRandomizerClick = () => {
- const searchParams = new URLSearchParams({
- fromRandomizer: "1",
- });
-
- navigate(
- `/game/steam/${randomGameObjectID.current}?${searchParams.toString()}`
- );
+ if (randomGame) {
+ navigate(
+ buildGameDetailsPath(
+ { ...randomGame, shop: "steam" },
+ {
+ fromRandomizer: "1",
+ }
+ )
+ );
+ }
};
useEffect(() => {
@@ -105,7 +107,7 @@ export function Home() {
- navigate(`/game/${result.shop}/${result.objectID}`)
- }
+ onClick={() => navigate(buildGameDetailsPath(result))}
/>
))}
diff --git a/src/renderer/src/pages/home/search-results.tsx b/src/renderer/src/pages/home/search-results.tsx
index 2def747d..be461918 100644
--- a/src/renderer/src/pages/home/search-results.tsx
+++ b/src/renderer/src/pages/home/search-results.tsx
@@ -14,6 +14,7 @@ import { useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { useNavigate, useSearchParams } from "react-router-dom";
import * as styles from "./home.css";
+import { buildGameDetailsPath } from "@renderer/helpers";
export function SearchResults() {
const dispatch = useAppDispatch();
@@ -30,7 +31,7 @@ export function SearchResults() {
const handleGameClick = (game: CatalogueEntry) => {
dispatch(clearSearch());
- navigate(`/game/${game.shop}/${game.objectID}`);
+ navigate(buildGameDetailsPath(game));
};
useEffect(() => {
diff --git a/src/renderer/src/pages/settings/settings-real-debrid.tsx b/src/renderer/src/pages/settings/settings-real-debrid.tsx
index 0f5017ca..f64a6c8d 100644
--- a/src/renderer/src/pages/settings/settings-real-debrid.tsx
+++ b/src/renderer/src/pages/settings/settings-real-debrid.tsx
@@ -57,7 +57,7 @@ export function SettingsRealDebrid({
{form.useRealDebrid && (
diff --git a/src/types/index.ts b/src/types/index.ts
index 94361564..c4d1e4ee 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -69,7 +69,6 @@ export interface GameRepack {
export type ShopDetails = SteamAppDetails & {
objectID: string;
- repacks: GameRepack[];
};
export interface TorrentFile {
@@ -134,3 +133,8 @@ export interface HowLongToBeatCategory {
duration: string;
accuracy: string;
}
+
+export interface Steam250Game {
+ title: string;
+ objectID: string;
+}
diff --git a/yarn.lock b/yarn.lock
index 5f4dac26..fd67cd6f 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2023,6 +2023,13 @@ argparse@^2.0.1:
resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
+aria-query@^5.3.0:
+ version "5.3.0"
+ resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e"
+ integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==
+ dependencies:
+ dequal "^2.0.3"
+
array-buffer-byte-length@^1.0.1:
version "1.0.1"
resolved "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz"
@@ -2125,6 +2132,11 @@ assert-plus@^1.0.0:
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==
+ast-types-flow@^0.0.8:
+ version "0.0.8"
+ resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6"
+ integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==
+
astral-regex@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
@@ -2168,6 +2180,11 @@ available-typed-arrays@^1.0.7:
dependencies:
possible-typed-array-names "^1.0.0"
+axe-core@=4.7.0:
+ version "4.7.0"
+ resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf"
+ integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==
+
axios@^1.6.8:
version "1.6.8"
resolved "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz"
@@ -2177,6 +2194,13 @@ axios@^1.6.8:
form-data "^4.0.0"
proxy-from-env "^1.1.0"
+axobject-query@^3.2.1:
+ version "3.2.1"
+ resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a"
+ integrity sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==
+ dependencies:
+ dequal "^2.0.3"
+
balanced-match@^1.0.0:
version "1.0.2"
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
@@ -2715,6 +2739,11 @@ csstype@^3.0.2, csstype@^3.0.7:
resolved "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
+damerau-levenshtein@^1.0.8:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
+ integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==
+
dargs@^8.0.0:
version "8.1.0"
resolved "https://registry.yarnpkg.com/dargs/-/dargs-8.1.0.tgz#a34859ea509cbce45485e5aa356fef70bfcc7272"
@@ -2842,6 +2871,11 @@ delayed-stream@~1.0.0:
resolved "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
+dequal@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
+ integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
+
detect-libc@^2.0.0:
version "2.0.3"
resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz"
@@ -3120,7 +3154,7 @@ es-errors@^1.1.0, es-errors@^1.2.1, es-errors@^1.3.0:
resolved "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz"
integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
-es-iterator-helpers@^1.0.17:
+es-iterator-helpers@^1.0.15, es-iterator-helpers@^1.0.17:
version "1.0.19"
resolved "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.19.tgz"
integrity sha512-zoMwbCcH5hwUkKJkT8kDIBZSz9I6mVG//+lDCinLCGov4+r7NIy0ld8o03M0cJxl2spVf6ESYVS6/gpIfq1FFw==
@@ -3255,6 +3289,28 @@ eslint-config-prettier@^9.1.0:
resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz"
integrity sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==
+eslint-plugin-jsx-a11y@^6.8.0:
+ version "6.8.0"
+ resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz#2fa9c701d44fcd722b7c771ec322432857fcbad2"
+ integrity sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==
+ dependencies:
+ "@babel/runtime" "^7.23.2"
+ aria-query "^5.3.0"
+ array-includes "^3.1.7"
+ array.prototype.flatmap "^1.3.2"
+ ast-types-flow "^0.0.8"
+ axe-core "=4.7.0"
+ axobject-query "^3.2.1"
+ damerau-levenshtein "^1.0.8"
+ emoji-regex "^9.2.2"
+ es-iterator-helpers "^1.0.15"
+ hasown "^2.0.0"
+ jsx-ast-utils "^3.3.5"
+ language-tags "^1.0.9"
+ minimatch "^3.1.2"
+ object.entries "^1.1.7"
+ object.fromentries "^2.0.7"
+
eslint-plugin-prettier@^5.0.1:
version "5.1.3"
resolved "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz"
@@ -4488,7 +4544,7 @@ jsonparse@^1.2.0:
resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==
-"jsx-ast-utils@^2.4.1 || ^3.0.0":
+"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.5:
version "3.3.5"
resolved "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz"
integrity sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==
@@ -4510,6 +4566,18 @@ kuler@^2.0.0:
resolved "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz"
integrity sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==
+language-subtag-registry@^0.3.20:
+ version "0.3.22"
+ resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d"
+ integrity sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==
+
+language-tags@^1.0.9:
+ version "1.0.9"
+ resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.9.tgz#1ffdcd0ec0fafb4b1be7f8b11f306ad0f9c08777"
+ integrity sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==
+ dependencies:
+ language-subtag-registry "^0.3.20"
+
lazy-val@^1.0.4, lazy-val@^1.0.5:
version "1.0.5"
resolved "https://registry.npmjs.org/lazy-val/-/lazy-val-1.0.5.tgz"