diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 564daa84..e23081dc 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -6,7 +6,7 @@ module.exports = { "plugin:react-hooks/recommended", "plugin:jsx-a11y/recommended", "@electron-toolkit/eslint-config-ts/recommended", - "prettier", + "plugin:prettier/recommended", ], rules: { "@typescript-eslint/explicit-function-return-type": "off", diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 293a898b..af4cbae7 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: @@ -21,9 +21,6 @@ jobs: - name: Validate current commit (last commit) with commitlint run: npx commitlint --last --verbose - - name: Check formatting - run: yarn format:check - - name: Typecheck run: yarn typecheck diff --git a/package.json b/package.json index b36269ec..0ee6cdb6 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "type": "module", "scripts": { "format": "prettier --write .", - "format:check": "prettier --check .", "lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix", "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false", "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false", 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/da/translation.json b/src/locales/da/translation.json new file mode 100644 index 00000000..dca04160 --- /dev/null +++ b/src/locales/da/translation.json @@ -0,0 +1,174 @@ +{ + "home": { + "featured": "Anbefalet", + "recently_added": "Nyligt tilføjet", + "trending": "Trender", + "surprise_me": "Overrask mig", + "no_results": "Ingen resultater fundet" + }, + "sidebar": { + "catalogue": "Katalog", + "downloads": "Downloads", + "settings": "Indstillinger", + "my_library": "Mit bibliotek", + "downloading_metadata": "{{title}} (Downloader metadata…)", + "checking_files": "{{title}} ({{percentage}} - Tjekker filer…)", + "paused": "{{title}} (Paused)", + "downloading": "{{title}} ({{percentage}} - Downloading…)", + "filter": "Filtrer bibliotek", + "follow_us": "Følg os", + "home": "Hjem", + "discord": "Tilslut dig vores Discord", + "telegram": "Tilslut dig vores Telegram", + "x": "Følg på X", + "github": "Bidrag på GitHub" + }, + "header": { + "search": "Søg spil", + "home": "Hjem", + "catalogue": "Katalog", + "downloads": "Downloads", + "search_results": "Søge resultater", + "settings": "Indstillinger" + }, + "bottom_panel": { + "no_downloads_in_progress": "Ingen downloads igang", + "downloading_metadata": "Downloader {{title}} metadata…", + "checking_files": "Tjekker {{title}} filer… ({{percentage}} færdig)", + "downloading": "Downloader {{title}}… ({{percentage}} færdig) - Konklusion {{eta}} - {{speed}}" + }, + "catalogue": { + "next_page": "Næste side", + "previous_page": "Tidligere side" + }, + "game_details": { + "open_download_options": "Åben download muligheder", + "download_options_zero": "Ingen download mulighed", + "download_options_one": "{{count}} download mulighed", + "download_options_other": "{{count}} download muligheder", + "updated_at": "Opdateret {{updated_at}}", + "install": "Installér", + "resume": "Fortsæt", + "pause": "Pause", + "cancel": "Annullér", + "remove": "Fjern", + "remove_from_list": "Fjern", + "space_left_on_disk": "{{space}} tilbage på harddisken", + "eta": "Konklusion {{eta}}", + "downloading_metadata": "Downloader metadata…", + "checking_files": "Tjekker filer…", + "filter": "Filtrer repacks", + "requirements": "System behov", + "minimum": "Mindste", + "recommended": "Anbefalet", + "no_minimum_requirements": "{{title}} angiver ikke mindste behov informationer", + "no_recommended_requirements": "{{title}} angiver ikke anbefalet behov informationer", + "paused_progress": "{{progress}} (Pauset)", + "release_date": "Offentliggjort den {{date}}", + "publisher": "Udgivet af {{publisher}}", + "copy_link_to_clipboard": "Kopier link", + "copied_link_to_clipboard": "Link kopieret", + "hours": "timer", + "minutes": "minutter", + "amount_hours": "{{amount}} timer", + "amount_minutes": "{{amount}} minutter", + "accuracy": "{{accuracy}}% nøjagtighed", + "add_to_library": "Tilføj til bibliotek", + "remove_from_library": "Fjern fra bibliotek", + "no_downloads": "Ingen downloads tilgængelige", + "play_time": "Spillet i {{amount}}", + "last_time_played": "Sidst spillet {{period}}", + "not_played_yet": "Du har ikke spillet {{title}} endnu", + "next_suggestion": "Næste forslag", + "play": "Spil", + "deleting": "Sletter installatør…", + "close": "Luk", + "playing_now": "Spiller nu", + "change": "Ændré", + "repacks_modal_description": "Vælg den repack du vil downloade", + "downloads_path": "Downloads sti", + "select_folder_hint": "For at ændre standard mappen, gå til <0>Instillingerne", + "download_now": "Download nu", + "installation_instructions": "Installations Instrukser", + "installation_instructions_description": "Yderligere skridt er krævet for at installere dette spil", + "online_fix_instruction": "OnlineFix spil kræver et kodeord for at kunne blive udpakket. Når krævet, brug det følgende kodeord:", + "dodi_installation_instruction": "Når du åbner DODI installatør, tryk på op-knappen på dit tastatur <0 /> for at starte installations processen:", + "dont_show_it_again": "Vis ikke igen", + "copy_to_clipboard": "Kopier", + "copied_to_clipboard": "Kopieret", + "got_it": "Forstået" + }, + "activation": { + "title": "Aktivér Hydra", + "installation_id": "Installations ID:", + "enter_activation_code": "Indtast din aktiverings kode", + "message": "Hvis du ikke ved hvor du skal spørge om dette, burde du ikke have dette.", + "activate": "Aktivér", + "loading": "Loader…" + }, + "downloads": { + "resume": "Fortsæt", + "pause": "Pause", + "eta": "Konklusion {{eta}}", + "paused": "Pauset", + "verifying": "Verificerer…", + "completed_at": "Færdiggjort på {{date}}", + "completed": "Færdigt", + "cancelled": "Annulleret", + "download_again": "Download igen", + "cancel": "Annullér", + "filter": "Filtrer downloadet spil", + "remove": "Fjern", + "downloading_metadata": "Downloader metadata…", + "checking_files": "Tjekker filer…", + "starting_download": "Starter download…", + "deleting": "Sletter installatør…", + "delete": "Fjern installatør", + "remove_from_list": "Fjern", + "delete_modal_title": "Er du sikker?", + "delete_modal_description": "Dette vil fjerne alle installations filerne fra din computer", + "install": "Installér", + "real_debrid": "Real Debrid", + "torrent": "Torrent" + }, + "settings": { + "downloads_path": "Downloads sti", + "change": "Opdatering", + "notifications": "Notifikationer", + "enable_download_notifications": "Når et download bliver færdigt", + "enable_repack_list_notifications": "Når en ny repack bliver tilføjet", + "telemetry": "Telemetri", + "telemetry_description": "Slå anonymt brugs statistik til", + "real_debrid_api_token_description": "Real Debrid API token", + "quit_app_instead_hiding": "Afslut Hydra instedet for at minimere til processlinjen", + "launch_with_system": "Åben Hydra ved start af systemet", + "general": "Generelt", + "behavior": "Opførsel", + "enable_real_debrid": "Slå Real Debrid til", + "real_debrid": "Real Debrid", + "real_debrid_api_token_hint": "Du kan få din API nøgle <0>her.", + "save_changes": "Gem ændringer" + }, + "notifications": { + "download_complete": "Download færdig", + "game_ready_to_install": "{{title}} er klar til at installeret", + "repack_list_updated": "Repack liste opdateret", + "repack_count_one": "{{count}} repack tilføjet", + "repack_count_other": "{{count}} repacks tilføjet" + }, + "system_tray": { + "open": "Åben Hydra", + "quit": "Afslut" + }, + "game_card": { + "no_downloads": "Ingen downloads tilgængelig" + }, + "binary_not_found_modal": { + "title": "Programmer ikke installeret", + "description": "Wine eller Lutris eksekverbare blev ikke fundet på dit system", + "instructions": "Tjek den korrekte måde at installere nogle af dem, på din Linux distribution, så spillet kan køre normalt" + }, + "modal": { + "close": "Luk knap" + } +} diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index 8c7e704d..19efb4d8 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", @@ -98,7 +97,14 @@ "copied_to_clipboard": "Copied", "got_it": "Got it", "multi_language": "Multi Language", - "multiplayer": "Multi Player" + "multiplayer": "Multi Player", + "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", @@ -141,7 +147,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 d2a7d553..f9eea8f5 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/index.ts b/src/locales/index.ts index f97cace3..bf0eaab0 100644 --- a/src/locales/index.ts +++ b/src/locales/index.ts @@ -11,3 +11,4 @@ export { default as tr } from "./tr/translation.json"; export { default as be } from "./be/translation.json"; export { default as uk } from "./uk/translation.json"; export { default as id } from "./id/translation.json"; +export { default as da } from "./da/translation.json"; diff --git a/src/locales/it/translation.json b/src/locales/it/translation.json index 030c28c1..554001d3 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/pt/translation.json b/src/locales/pt/translation.json index 528cd8c7..d14e480e 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", @@ -94,7 +93,14 @@ "copied_to_clipboard": "Copiado", "got_it": "Entendi", "multi_language": "Multi Idioma", - "multiplayer": "Multijogador" + "multiplayer": "Multijogador", + "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", @@ -125,7 +131,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", @@ -135,6 +143,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 94258442..770f098e 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 efeb5fd1..c9d1479b 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 4e49dd1b..b2b9228b 100644 --- a/src/main/events/index.ts +++ b/src/main/events/index.ts @@ -8,6 +8,7 @@ import "./catalogue/get-how-long-to-beat"; import "./catalogue/get-random-game"; import "./catalogue/search-games"; import "./catalogue/repacks/get-magnet-health"; +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.ts b/src/preload/index.ts index 2ba23e61..326af0ae 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 51e708a0..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`, @@ -25,6 +27,15 @@ export const getSteamLanguage = (language: string) => { if (language.startsWith("it")) return "italian"; if (language.startsWith("hu")) return "hungarian"; if (language.startsWith("pl")) return "polish"; + if (language.startsWith("da")) return "danish"; 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/hooks/use-date.ts b/src/renderer/src/hooks/use-date.ts index a8face46..bce73186 100644 --- a/src/renderer/src/hooks/use-date.ts +++ b/src/renderer/src/hooks/use-date.ts @@ -1,6 +1,18 @@ import { formatDistance } from "date-fns"; import type { FormatDistanceOptions } from "date-fns"; -import { ptBR, enUS, es, fr, pl, hu, tr, ru, it, be } from "date-fns/locale"; +import { + ptBR, + enUS, + es, + fr, + pl, + hu, + tr, + ru, + it, + be, + da, +} from "date-fns/locale"; import { useTranslation } from "react-i18next"; export function useDate() { @@ -18,6 +30,7 @@ export function useDate() { if (language.startsWith("ru")) return ru; if (language.startsWith("it")) return it; if (language.startsWith("be")) return be; + if (language.startsWith("da")) return da; return enUS; }; 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] })}

- - - )} + {hasScreenshots && + gameDetails.screenshots.map((image, i) => ( + {t("screenshot", + ))} + + + +
- {hasMovies && - gameDetails.movies?.map((video: SteamMovies, i: number) => ( + {previews.map((media, i) => ( + + ))}
)} 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")}

    -
    +
    -
    +
    {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() {