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 6855100e..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>Settings", "download_now": "Download now", "installation_instructions": "Installation Instructions", @@ -97,6 +96,9 @@ "copy_to_clipboard": "Copy", "copied_to_clipboard": "Copied", "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}}", @@ -143,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 5ad27c07..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ções", "download_now": "Baixe agora", "installation_instructions": "Instruções de Instalação", @@ -93,6 +92,9 @@ "copy_to_clipboard": "Copiar", "copied_to_clipboard": "Copiado", "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}}", @@ -127,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", @@ -137,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/game-card/game-card.css.ts b/src/renderer/src/components/game-card/game-card.css.ts index 9f2f0654..1f45c106 100644 --- a/src/renderer/src/components/game-card/game-card.css.ts +++ b/src/renderer/src/components/game-card/game-card.css.ts @@ -1,31 +1,18 @@ import { style } from "@vanilla-extract/css"; -import { recipe } from "@vanilla-extract/recipes"; import { SPACING_UNIT, vars } from "../../theme.css"; -export const card = recipe({ - base: { - width: "100%", - height: "180px", - boxShadow: "0px 0px 15px 0px #000000", - overflow: "hidden", - borderRadius: "4px", - transition: "all ease 0.2s", - border: `solid 1px ${vars.color.border}`, - cursor: "pointer", - zIndex: "1", - ":active": { - opacity: vars.opacity.active, - }, - }, - variants: { - disabled: { - true: { - pointerEvents: "none", - boxShadow: "none", - opacity: vars.opacity.disabled, - filter: "grayscale(50%)", - }, - }, +export const card = style({ + width: "100%", + height: "180px", + boxShadow: "0px 0px 15px 0px #000000", + overflow: "hidden", + borderRadius: "4px", + transition: "all ease 0.2s", + border: `solid 1px ${vars.color.border}`, + cursor: "pointer", + zIndex: "1", + ":active": { + opacity: vars.opacity.active, }, }); @@ -48,7 +35,7 @@ export const cover = style({ zIndex: "-1", transition: "all ease 0.2s", selectors: { - [`${card({})}:hover &`]: { + [`${card}:hover &`]: { transform: "scale(1.05)", }, }, @@ -64,7 +51,7 @@ export const content = style({ transition: "all ease 0.2s", transform: "translateY(24px)", selectors: { - [`${card({})}:hover &`]: { + [`${card}:hover &`]: { transform: "translateY(0px)", }, }, diff --git a/src/renderer/src/components/game-card/game-card.tsx b/src/renderer/src/components/game-card/game-card.tsx index f7f6ffe4..b3ac3fa2 100644 --- a/src/renderer/src/components/game-card/game-card.tsx +++ b/src/renderer/src/components/game-card/game-card.tsx @@ -14,7 +14,6 @@ export interface GameCardProps HTMLButtonElement > { game: CatalogueEntry; - disabled?: boolean; } const shopIcon = { @@ -22,7 +21,7 @@ const shopIcon = { steam: , }; -export function GameCard({ game, disabled, ...props }: GameCardProps) { +export function GameCard({ game, ...props }: GameCardProps) { const { t } = useTranslation("game_card"); const repackersFriendlyNames = useAppSelector( @@ -34,12 +33,7 @@ export function GameCard({ game, disabled, ...props }: GameCardProps) { ); return ( - diff --git a/src/renderer/src/components/sidebar/sidebar.tsx b/src/renderer/src/components/sidebar/sidebar.tsx index 8e5b5df5..ffce96ae 100644 --- a/src/renderer/src/components/sidebar/sidebar.tsx +++ b/src/renderer/src/components/sidebar/sidebar.tsx @@ -15,6 +15,7 @@ import XLogo from "@renderer/assets/x-icon.svg?react"; import * as styles from "./sidebar.css"; import { GameStatus, GameStatusHelper } from "@shared"; +import { buildGameDetailsPath } from "@renderer/helpers"; const SIDEBAR_MIN_WIDTH = 200; const SIDEBAR_INITIAL_WIDTH = 250; @@ -209,9 +210,7 @@ export function Sidebar() { type="button" className={styles.menuItemButton} onClick={() => - handleSidebarItemClick( - `/game/${game.shop}/${game.objectID}` - ) + handleSidebarItemClick(buildGameDetailsPath(game)) } > Promise; - getRandomGame: () => Promise; + getRandomGame: () => Promise; getHowLongToBeat: ( objectID: string, shop: GameShop, @@ -50,6 +52,7 @@ declare global { take?: number, prevCursor?: number ) => Promise<{ results: CatalogueEntry[]; cursor: number }>; + searchGameRepacks: (query: string) => Promise; /* Library */ addGameToLibrary: ( diff --git a/src/renderer/src/helpers.ts b/src/renderer/src/helpers.ts index 847b7673..b51c9927 100644 --- a/src/renderer/src/helpers.ts +++ b/src/renderer/src/helpers.ts @@ -1,3 +1,5 @@ +import type { CatalogueEntry } from "@types"; + export const steamUrlBuilder = { library: (objectID: string) => `https://steamcdn-a.akamaihd.net/steam/apps/${objectID}/header.jpg`, @@ -29,3 +31,11 @@ export const getSteamLanguage = (language: string) => { return "english"; }; + +export const buildGameDetailsPath = ( + game: Pick, + params: Record = {} +) => { + const searchParams = new URLSearchParams({ title: game.title, ...params }); + return `/game/${game.shop}/${game.objectID}?${searchParams.toString()}`; +}; diff --git a/src/renderer/src/main.tsx b/src/renderer/src/main.tsx index d9b7821e..1ffc5099 100644 --- a/src/renderer/src/main.tsx +++ b/src/renderer/src/main.tsx @@ -57,6 +57,7 @@ i18n }, }) .then(() => { + i18n.changeLanguage("pt-BR"); window.electron.updateUserPreferences({ language: i18n.language }); }); diff --git a/src/renderer/src/pages/catalogue/catalogue.tsx b/src/renderer/src/pages/catalogue/catalogue.tsx index a809e246..6720b83f 100644 --- a/src/renderer/src/pages/catalogue/catalogue.tsx +++ b/src/renderer/src/pages/catalogue/catalogue.tsx @@ -11,6 +11,7 @@ import { useEffect, useRef, useState } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import * as styles from "../home/home.css"; import { ArrowLeftIcon, ArrowRightIcon } from "@primer/octicons-react"; +import { buildGameDetailsPath } from "@renderer/helpers"; export function Catalogue() { const dispatch = useAppDispatch(); @@ -31,7 +32,7 @@ export function Catalogue() { const handleGameClick = (game: CatalogueEntry) => { dispatch(clearSearch()); - navigate(`/game/${game.shop}/${game.objectID}`); + navigate(buildGameDetailsPath(game)); }; useEffect(() => { diff --git a/src/renderer/src/pages/game-details/description-header.tsx b/src/renderer/src/pages/game-details/description-header.tsx index df1455e9..860e8025 100644 --- a/src/renderer/src/pages/game-details/description-header.tsx +++ b/src/renderer/src/pages/game-details/description-header.tsx @@ -11,7 +11,7 @@ import * as styles from "./game-details.css"; const OPEN_HYDRA_URL = "https://open.hydralauncher.site"; export interface DescriptionHeaderProps { - gameDetails: ShopDetails | null; + gameDetails: ShopDetails; } export function DescriptionHeader({ gameDetails }: DescriptionHeaderProps) { @@ -64,7 +64,7 @@ export function DescriptionHeader({ gameDetails }: DescriptionHeaderProps) { date: gameDetails?.release_date.date, })}

-

{t("publisher", { publisher: gameDetails?.publishers[0] })}

+

{t("publisher", { publisher: gameDetails.publishers[0] })}

-
+
{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 4513892f..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)} + /> setShowRepacksModal(true)} getGame={getGame} isGamePlaying={isGamePlaying} @@ -248,63 +242,22 @@ export function GameDetails() {
- - - + {gameDetails && } + {gameDetails && }
-
- - -
-

{t("requirements")}

-
- -
- - -
- -
-
+
)} @@ -314,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 = ( + + +
+ +
+ + ); +} 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() {