mirror of
https://github.com/hydralauncher/hydra.git
synced 2025-02-09 03:37:45 +03:00
Merge branch 'main' into feature/better-repack-modal
This commit is contained in:
commit
0d089bb5c4
@ -6,7 +6,7 @@ module.exports = {
|
|||||||
"plugin:react-hooks/recommended",
|
"plugin:react-hooks/recommended",
|
||||||
"plugin:jsx-a11y/recommended",
|
"plugin:jsx-a11y/recommended",
|
||||||
"@electron-toolkit/eslint-config-ts/recommended",
|
"@electron-toolkit/eslint-config-ts/recommended",
|
||||||
"prettier",
|
"plugin:prettier/recommended",
|
||||||
],
|
],
|
||||||
rules: {
|
rules: {
|
||||||
"@typescript-eslint/explicit-function-return-type": "off",
|
"@typescript-eslint/explicit-function-return-type": "off",
|
||||||
|
5
.github/workflows/lint.yml
vendored
5
.github/workflows/lint.yml
vendored
@ -1,6 +1,6 @@
|
|||||||
name: Lint
|
name: Lint
|
||||||
|
|
||||||
on: [pull_request, push]
|
on: [pull_request]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
@ -21,9 +21,6 @@ jobs:
|
|||||||
- name: Validate current commit (last commit) with commitlint
|
- name: Validate current commit (last commit) with commitlint
|
||||||
run: npx commitlint --last --verbose
|
run: npx commitlint --last --verbose
|
||||||
|
|
||||||
- name: Check formatting
|
|
||||||
run: yarn format:check
|
|
||||||
|
|
||||||
- name: Typecheck
|
- name: Typecheck
|
||||||
run: yarn typecheck
|
run: yarn typecheck
|
||||||
|
|
||||||
|
@ -12,7 +12,6 @@
|
|||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"format:check": "prettier --check .",
|
|
||||||
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
"lint": "eslint . --ext .js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix",
|
||||||
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
"typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
|
||||||
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
|
"typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
|
||||||
|
@ -88,7 +88,6 @@
|
|||||||
"repacks_modal_description": "Абярыце рэпак, які хочаце сьцягнуць",
|
"repacks_modal_description": "Абярыце рэпак, які хочаце сьцягнуць",
|
||||||
"downloads_path": "Шлях сьцягваньня",
|
"downloads_path": "Шлях сьцягваньня",
|
||||||
"select_folder_hint": "Каб зьмяніць папку па змоўчаньні, адкрыйце",
|
"select_folder_hint": "Каб зьмяніць папку па змоўчаньні, адкрыйце",
|
||||||
"settings": "Налады Hydra",
|
|
||||||
"download_now": "Сьцягнуць зараз",
|
"download_now": "Сьцягнуць зараз",
|
||||||
"installation_instructions": "Інструкцыя ўсталёўкі",
|
"installation_instructions": "Інструкцыя ўсталёўкі",
|
||||||
"installation_instructions_description": "Усталёўка гэтай гульні патрабуе дадатковых крокаў",
|
"installation_instructions_description": "Усталёўка гэтай гульні патрабуе дадатковых крокаў",
|
||||||
|
174
src/locales/da/translation.json
Normal file
174
src/locales/da/translation.json
Normal file
@ -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</0>",
|
||||||
|
"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</0>.",
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
}
|
@ -86,7 +86,6 @@
|
|||||||
"playing_now": "Playing now",
|
"playing_now": "Playing now",
|
||||||
"change": "Change",
|
"change": "Change",
|
||||||
"repacks_modal_description": "Choose the repack you want to download",
|
"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</0>",
|
"select_folder_hint": "To change the default folder, go to the <0>Settings</0>",
|
||||||
"download_now": "Download now",
|
"download_now": "Download now",
|
||||||
"installation_instructions": "Installation Instructions",
|
"installation_instructions": "Installation Instructions",
|
||||||
@ -98,7 +97,14 @@
|
|||||||
"copied_to_clipboard": "Copied",
|
"copied_to_clipboard": "Copied",
|
||||||
"got_it": "Got it",
|
"got_it": "Got it",
|
||||||
"multi_language": "Multi Language",
|
"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": {
|
"activation": {
|
||||||
"title": "Activate Hydra",
|
"title": "Activate Hydra",
|
||||||
@ -141,7 +147,7 @@
|
|||||||
"enable_repack_list_notifications": "When a new repack is added",
|
"enable_repack_list_notifications": "When a new repack is added",
|
||||||
"telemetry": "Telemetry",
|
"telemetry": "Telemetry",
|
||||||
"telemetry_description": "Enable anonymous usage statistics",
|
"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",
|
"quit_app_instead_hiding": "Quit Hydra instead of minimizing to tray",
|
||||||
"launch_with_system": "Launch Hydra on system start-up",
|
"launch_with_system": "Launch Hydra on system start-up",
|
||||||
"general": "General",
|
"general": "General",
|
||||||
|
@ -85,7 +85,6 @@
|
|||||||
"repacks_modal_description": "Selecciona el repack que quieres descargar",
|
"repacks_modal_description": "Selecciona el repack que quieres descargar",
|
||||||
"downloads_path": "Ruta de descarga",
|
"downloads_path": "Ruta de descarga",
|
||||||
"select_folder_hint": "Para cambiar la carpeta predeterminada, accede a",
|
"select_folder_hint": "Para cambiar la carpeta predeterminada, accede a",
|
||||||
"settings": "Ajustes",
|
|
||||||
"download_now": "Descargar ahora",
|
"download_now": "Descargar ahora",
|
||||||
"installation_instructions": "Instrucciones de instalación",
|
"installation_instructions": "Instrucciones de instalación",
|
||||||
"installation_instructions_description": "Se requieren de pasos adicionales para instalar este juego",
|
"installation_instructions_description": "Se requieren de pasos adicionales para instalar este juego",
|
||||||
|
@ -88,7 +88,6 @@
|
|||||||
"repacks_modal_description": "Pilih repack yang kamu ingin unduh",
|
"repacks_modal_description": "Pilih repack yang kamu ingin unduh",
|
||||||
"downloads_path": "Lokasi Unduhan",
|
"downloads_path": "Lokasi Unduhan",
|
||||||
"select_folder_hint": "Untuk merubah folder bawaan, akses melalui",
|
"select_folder_hint": "Untuk merubah folder bawaan, akses melalui",
|
||||||
"settings": "Pengaturan",
|
|
||||||
"download_now": "Unduh sekarang",
|
"download_now": "Unduh sekarang",
|
||||||
"installation_instructions": "Instruksi Instalasi",
|
"installation_instructions": "Instruksi Instalasi",
|
||||||
"installation_instructions_description": "Langkah tambahan dibutuhkan untuk meng-instal game ini",
|
"installation_instructions_description": "Langkah tambahan dibutuhkan untuk meng-instal game ini",
|
||||||
|
@ -11,3 +11,4 @@ export { default as tr } from "./tr/translation.json";
|
|||||||
export { default as be } from "./be/translation.json";
|
export { default as be } from "./be/translation.json";
|
||||||
export { default as uk } from "./uk/translation.json";
|
export { default as uk } from "./uk/translation.json";
|
||||||
export { default as id } from "./id/translation.json";
|
export { default as id } from "./id/translation.json";
|
||||||
|
export { default as da } from "./da/translation.json";
|
||||||
|
@ -88,7 +88,6 @@
|
|||||||
"repacks_modal_description": "Scegli il repack che vuoi scaricare",
|
"repacks_modal_description": "Scegli il repack che vuoi scaricare",
|
||||||
"downloads_path": "Percorso dei download",
|
"downloads_path": "Percorso dei download",
|
||||||
"select_folder_hint": "Per cambiare la cartella predefinita, accedi alle",
|
"select_folder_hint": "Per cambiare la cartella predefinita, accedi alle",
|
||||||
"settings": "Impostazioni",
|
|
||||||
"download_now": "Scarica ora",
|
"download_now": "Scarica ora",
|
||||||
"installation_instructions": "Istruzioni di installazione",
|
"installation_instructions": "Istruzioni di installazione",
|
||||||
"installation_instructions_description": "Sono necessari passaggi aggiuntivi per installare questo gioco",
|
"installation_instructions_description": "Sono necessari passaggi aggiuntivi per installare questo gioco",
|
||||||
|
@ -139,7 +139,7 @@
|
|||||||
"enable_repack_list_notifications": "Wanneer een nieuwe herverpakking wordt toegevoegd",
|
"enable_repack_list_notifications": "Wanneer een nieuwe herverpakking wordt toegevoegd",
|
||||||
"telemetry": "Telemetrie",
|
"telemetry": "Telemetrie",
|
||||||
"telemetry_description": "Schakel anonieme gebruiksstatistieken in",
|
"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",
|
"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",
|
"launch_with_system": "Start Hydra bij het opstarten van het systeem",
|
||||||
"general": "Algemeen",
|
"general": "Algemeen",
|
||||||
|
@ -82,7 +82,6 @@
|
|||||||
"playing_now": "Jogando agora",
|
"playing_now": "Jogando agora",
|
||||||
"change": "Mudar",
|
"change": "Mudar",
|
||||||
"repacks_modal_description": "Escolha o repack do jogo que deseja baixar",
|
"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</0>",
|
"select_folder_hint": "Para trocar a pasta padrão, acesse a <0>Tela de Configurações</0>",
|
||||||
"download_now": "Baixe agora",
|
"download_now": "Baixe agora",
|
||||||
"installation_instructions": "Instruções de Instalação",
|
"installation_instructions": "Instruções de Instalação",
|
||||||
@ -94,7 +93,14 @@
|
|||||||
"copied_to_clipboard": "Copiado",
|
"copied_to_clipboard": "Copiado",
|
||||||
"got_it": "Entendi",
|
"got_it": "Entendi",
|
||||||
"multi_language": "Multi Idioma",
|
"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": {
|
"activation": {
|
||||||
"title": "Ativação",
|
"title": "Ativação",
|
||||||
@ -125,7 +131,9 @@
|
|||||||
"delete_modal_description": "Isso removerá todos os arquivos de instalação do seu computador",
|
"delete_modal_description": "Isso removerá todos os arquivos de instalação do seu computador",
|
||||||
"delete_modal_title": "Tem certeza?",
|
"delete_modal_title": "Tem certeza?",
|
||||||
"deleting": "Excluindo instalador…",
|
"deleting": "Excluindo instalador…",
|
||||||
"install": "Instalar"
|
"install": "Instalar",
|
||||||
|
"torrent": "Torrent",
|
||||||
|
"real_debrid": "Real Debrid"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"downloads_path": "Diretório dos downloads",
|
"downloads_path": "Diretório dos downloads",
|
||||||
@ -135,6 +143,7 @@
|
|||||||
"enable_repack_list_notifications": "Quando a lista de repacks for atualizada",
|
"enable_repack_list_notifications": "Quando a lista de repacks for atualizada",
|
||||||
"telemetry": "Telemetria",
|
"telemetry": "Telemetria",
|
||||||
"telemetry_description": "Habilitar estatísticas de uso anônimas",
|
"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",
|
"quit_app_instead_hiding": "Fechar o aplicativo em vez de minimizá-lo",
|
||||||
"launch_with_system": "Iniciar aplicativo na inicialização do sistema",
|
"launch_with_system": "Iniciar aplicativo na inicialização do sistema",
|
||||||
"general": "Geral",
|
"general": "Geral",
|
||||||
|
@ -88,7 +88,6 @@
|
|||||||
"repacks_modal_description": "Выберите репак для загрузки",
|
"repacks_modal_description": "Выберите репак для загрузки",
|
||||||
"downloads_path": "Путь загрузок",
|
"downloads_path": "Путь загрузок",
|
||||||
"select_folder_hint": "Изменить папку по умолчанию",
|
"select_folder_hint": "Изменить папку по умолчанию",
|
||||||
"settings": "Настройки Hydra",
|
|
||||||
"download_now": "Загрузить сейчас",
|
"download_now": "Загрузить сейчас",
|
||||||
"installation_instructions": "Инструкция по установке",
|
"installation_instructions": "Инструкция по установке",
|
||||||
"installation_instructions_description": "Для установки этой игры требуются дополнительные шаги",
|
"installation_instructions_description": "Для установки этой игры требуются дополнительные шаги",
|
||||||
|
@ -88,7 +88,6 @@
|
|||||||
"repacks_modal_description": "İndirmek istediğiiniz repacki seçin",
|
"repacks_modal_description": "İndirmek istediğiiniz repacki seçin",
|
||||||
"downloads_path": "İndirme yolu",
|
"downloads_path": "İndirme yolu",
|
||||||
"select_folder_hint": "Varsayılan klasörü değiştirmek için ulaşmanız gereken ayar",
|
"select_folder_hint": "Varsayılan klasörü değiştirmek için ulaşmanız gereken ayar",
|
||||||
"settings": "Ayarlar",
|
|
||||||
"download_now": "Şimdi",
|
"download_now": "Şimdi",
|
||||||
"installation_instructions": "Kurulum",
|
"installation_instructions": "Kurulum",
|
||||||
"installation_instructions_description": "Bu oyunu kurmak için ek adımlar gerekiyor",
|
"installation_instructions_description": "Bu oyunu kurmak için ek adımlar gerekiyor",
|
||||||
|
@ -88,7 +88,6 @@
|
|||||||
"repacks_modal_description": "Виберіть репак, який хочете завантажити",
|
"repacks_modal_description": "Виберіть репак, який хочете завантажити",
|
||||||
"downloads_path": "Шлях завантажень",
|
"downloads_path": "Шлях завантажень",
|
||||||
"select_folder_hint": "Щоб змінити теку за замовчуванням, відкрийте",
|
"select_folder_hint": "Щоб змінити теку за замовчуванням, відкрийте",
|
||||||
"settings": "Налаштування Hydra",
|
|
||||||
"download_now": "Завантажити зараз",
|
"download_now": "Завантажити зараз",
|
||||||
"installation_instructions": "Інструкція зі встановлення",
|
"installation_instructions": "Інструкція зі встановлення",
|
||||||
"installation_instructions_description": "Для встановлення цієї гри потрібні додаткові кроки",
|
"installation_instructions_description": "Для встановлення цієї гри потрібні додаткові кроки",
|
||||||
|
@ -1,10 +1,32 @@
|
|||||||
import { gameShopCacheRepository } from "@main/repository";
|
import { gameShopCacheRepository, steamGameRepository } from "@main/repository";
|
||||||
import { getSteamAppDetails } from "@main/services";
|
import { getSteamAppDetails } from "@main/services";
|
||||||
|
|
||||||
import type { ShopDetails, GameShop, SteamAppDetails } from "@types";
|
import type { ShopDetails, GameShop, SteamAppDetails } from "@types";
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { searchRepacks } from "../helpers/search-games";
|
|
||||||
|
const getLocalizedSteamAppDetails = (
|
||||||
|
objectID: string,
|
||||||
|
language: string
|
||||||
|
): Promise<ShopDetails | null> => {
|
||||||
|
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 (
|
const getGameShopDetails = async (
|
||||||
_event: Electron.IpcMainInvokeEvent,
|
_event: Electron.IpcMainInvokeEvent,
|
||||||
@ -17,27 +39,21 @@ const getGameShopDetails = async (
|
|||||||
where: { objectID, language },
|
where: { objectID, language },
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = Promise.all([
|
const appDetails = getLocalizedSteamAppDetails(objectID, language).then(
|
||||||
getSteamAppDetails(objectID, "english"),
|
(result) => {
|
||||||
getSteamAppDetails(objectID, language),
|
|
||||||
]).then(([appDetails, localizedAppDetails]) => {
|
|
||||||
if (appDetails && localizedAppDetails) {
|
|
||||||
gameShopCacheRepository.upsert(
|
gameShopCacheRepository.upsert(
|
||||||
{
|
{
|
||||||
objectID,
|
objectID,
|
||||||
shop: "steam",
|
shop: "steam",
|
||||||
language,
|
language,
|
||||||
serializedData: JSON.stringify({
|
serializedData: JSON.stringify(result),
|
||||||
...localizedAppDetails,
|
|
||||||
name: appDetails.name,
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
["objectID"]
|
["objectID"]
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
return [appDetails, localizedAppDetails];
|
return result;
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const cachedGame = cachedData?.serializedData
|
const cachedGame = cachedData?.serializedData
|
||||||
? (JSON.parse(cachedData?.serializedData) as SteamAppDetails)
|
? (JSON.parse(cachedData?.serializedData) as SteamAppDetails)
|
||||||
@ -46,21 +62,11 @@ const getGameShopDetails = async (
|
|||||||
if (cachedGame) {
|
if (cachedGame) {
|
||||||
return {
|
return {
|
||||||
...cachedGame,
|
...cachedGame,
|
||||||
repacks: searchRepacks(cachedGame.name),
|
|
||||||
objectID,
|
objectID,
|
||||||
} as ShopDetails;
|
} as ShopDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.then(([appDetails, localizedAppDetails]) => {
|
return Promise.resolve(appDetails);
|
||||||
if (!appDetails || !localizedAppDetails) return null;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...localizedAppDetails,
|
|
||||||
name: appDetails.name,
|
|
||||||
repacks: searchRepacks(appDetails.name),
|
|
||||||
objectID,
|
|
||||||
} as ShopDetails;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error("Not implemented");
|
throw new Error("Not implemented");
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
import { shuffle } from "lodash-es";
|
import { shuffle } from "lodash-es";
|
||||||
|
|
||||||
import { Steam250Game, getSteam250List } from "@main/services";
|
import { getSteam250List } from "@main/services";
|
||||||
|
|
||||||
import { registerEvent } from "../register-event";
|
import { registerEvent } from "../register-event";
|
||||||
import { searchGames, searchRepacks } from "../helpers/search-games";
|
import { searchGames, searchRepacks } from "../helpers/search-games";
|
||||||
|
import type { Steam250Game } from "@types";
|
||||||
|
|
||||||
const state = { games: Array<Steam250Game>(), index: 0 };
|
const state = { games: Array<Steam250Game>(), index: 0 };
|
||||||
|
|
||||||
@ -25,8 +26,6 @@ const getRandomGame = async (_event: Electron.IpcMainInvokeEvent) => {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
const resultObjectId = state.games[state.index].objectID;
|
|
||||||
|
|
||||||
state.index += 1;
|
state.index += 1;
|
||||||
|
|
||||||
if (state.index == state.games.length) {
|
if (state.index == state.games.length) {
|
||||||
@ -34,7 +33,7 @@ const getRandomGame = async (_event: Electron.IpcMainInvokeEvent) => {
|
|||||||
state.games = shuffle(state.games);
|
state.games = shuffle(state.games);
|
||||||
}
|
}
|
||||||
|
|
||||||
return resultObjectId;
|
return state.games[state.index];
|
||||||
};
|
};
|
||||||
|
|
||||||
registerEvent(getRandomGame, {
|
registerEvent(getRandomGame, {
|
||||||
|
14
src/main/events/catalogue/search-game-repacks.ts
Normal file
14
src/main/events/catalogue/search-game-repacks.ts
Normal file
@ -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,
|
||||||
|
});
|
@ -8,6 +8,7 @@ import "./catalogue/get-how-long-to-beat";
|
|||||||
import "./catalogue/get-random-game";
|
import "./catalogue/get-random-game";
|
||||||
import "./catalogue/search-games";
|
import "./catalogue/search-games";
|
||||||
import "./catalogue/repacks/get-magnet-health";
|
import "./catalogue/repacks/get-magnet-health";
|
||||||
|
import "./catalogue/search-game-repacks";
|
||||||
import "./hardware/get-disk-free-space";
|
import "./hardware/get-disk-free-space";
|
||||||
import "./library/add-game-to-library";
|
import "./library/add-game-to-library";
|
||||||
import "./library/close-game";
|
import "./library/close-game";
|
||||||
|
@ -12,8 +12,7 @@ export class RealDebridClient {
|
|||||||
private static instance: AxiosInstance;
|
private static instance: AxiosInstance;
|
||||||
|
|
||||||
static async addMagnet(magnet: string) {
|
static async addMagnet(magnet: string) {
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams({ magnet });
|
||||||
searchParams.append("magnet", magnet);
|
|
||||||
|
|
||||||
const response = await this.instance.post<RealDebridAddMagnet>(
|
const response = await this.instance.post<RealDebridAddMagnet>(
|
||||||
"/torrents/addMagnet",
|
"/torrents/addMagnet",
|
||||||
@ -31,8 +30,7 @@ export class RealDebridClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async selectAllFiles(id: string) {
|
static async selectAllFiles(id: string) {
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams({ files: "all" });
|
||||||
searchParams.append("files", "all");
|
|
||||||
|
|
||||||
await this.instance.post(
|
await this.instance.post(
|
||||||
`/torrents/selectFiles/${id}`,
|
`/torrents/selectFiles/${id}`,
|
||||||
@ -41,8 +39,7 @@ export class RealDebridClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static async unrestrictLink(link: string) {
|
static async unrestrictLink(link: string) {
|
||||||
const searchParams = new URLSearchParams();
|
const searchParams = new URLSearchParams({ link });
|
||||||
searchParams.append("link", link);
|
|
||||||
|
|
||||||
const response = await this.instance.post<RealDebridUnrestrictLink>(
|
const response = await this.instance.post<RealDebridUnrestrictLink>(
|
||||||
"/unrestrict/link",
|
"/unrestrict/link",
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { JSDOM } from "jsdom";
|
import { JSDOM } from "jsdom";
|
||||||
|
|
||||||
export interface Steam250Game {
|
import type { Steam250Game } from "@types";
|
||||||
title: string;
|
|
||||||
objectID: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const requestSteam250 = async (path: string) => {
|
export const requestSteam250 = async (path: string) => {
|
||||||
return axios
|
return axios
|
||||||
|
@ -52,6 +52,8 @@ contextBridge.exposeInMainWorld("electron", {
|
|||||||
ipcRenderer.invoke("getHowLongToBeat", objectID, shop, title),
|
ipcRenderer.invoke("getHowLongToBeat", objectID, shop, title),
|
||||||
getGames: (take?: number, prevCursor?: number) =>
|
getGames: (take?: number, prevCursor?: number) =>
|
||||||
ipcRenderer.invoke("getGames", take, prevCursor),
|
ipcRenderer.invoke("getGames", take, prevCursor),
|
||||||
|
searchGameRepacks: (query: string) =>
|
||||||
|
ipcRenderer.invoke("searchGameRepacks", query),
|
||||||
|
|
||||||
/* User preferences */
|
/* User preferences */
|
||||||
getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"),
|
getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"),
|
||||||
|
@ -1,9 +1,7 @@
|
|||||||
import { style } from "@vanilla-extract/css";
|
import { style } from "@vanilla-extract/css";
|
||||||
import { recipe } from "@vanilla-extract/recipes";
|
|
||||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||||
|
|
||||||
export const card = recipe({
|
export const card = style({
|
||||||
base: {
|
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "180px",
|
height: "180px",
|
||||||
boxShadow: "0px 0px 15px 0px #000000",
|
boxShadow: "0px 0px 15px 0px #000000",
|
||||||
@ -16,17 +14,6 @@ export const card = recipe({
|
|||||||
":active": {
|
":active": {
|
||||||
opacity: vars.opacity.active,
|
opacity: vars.opacity.active,
|
||||||
},
|
},
|
||||||
},
|
|
||||||
variants: {
|
|
||||||
disabled: {
|
|
||||||
true: {
|
|
||||||
pointerEvents: "none",
|
|
||||||
boxShadow: "none",
|
|
||||||
opacity: vars.opacity.disabled,
|
|
||||||
filter: "grayscale(50%)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const backdrop = style({
|
export const backdrop = style({
|
||||||
@ -48,7 +35,7 @@ export const cover = style({
|
|||||||
zIndex: "-1",
|
zIndex: "-1",
|
||||||
transition: "all ease 0.2s",
|
transition: "all ease 0.2s",
|
||||||
selectors: {
|
selectors: {
|
||||||
[`${card({})}:hover &`]: {
|
[`${card}:hover &`]: {
|
||||||
transform: "scale(1.05)",
|
transform: "scale(1.05)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -64,7 +51,7 @@ export const content = style({
|
|||||||
transition: "all ease 0.2s",
|
transition: "all ease 0.2s",
|
||||||
transform: "translateY(24px)",
|
transform: "translateY(24px)",
|
||||||
selectors: {
|
selectors: {
|
||||||
[`${card({})}:hover &`]: {
|
[`${card}:hover &`]: {
|
||||||
transform: "translateY(0px)",
|
transform: "translateY(0px)",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -14,7 +14,6 @@ export interface GameCardProps
|
|||||||
HTMLButtonElement
|
HTMLButtonElement
|
||||||
> {
|
> {
|
||||||
game: CatalogueEntry;
|
game: CatalogueEntry;
|
||||||
disabled?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const shopIcon = {
|
const shopIcon = {
|
||||||
@ -22,7 +21,7 @@ const shopIcon = {
|
|||||||
steam: <SteamLogo className={styles.shopIcon} />,
|
steam: <SteamLogo className={styles.shopIcon} />,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function GameCard({ game, disabled, ...props }: GameCardProps) {
|
export function GameCard({ game, ...props }: GameCardProps) {
|
||||||
const { t } = useTranslation("game_card");
|
const { t } = useTranslation("game_card");
|
||||||
|
|
||||||
const repackersFriendlyNames = useAppSelector(
|
const repackersFriendlyNames = useAppSelector(
|
||||||
@ -34,12 +33,7 @@ export function GameCard({ game, disabled, ...props }: GameCardProps) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button {...props} type="button" className={styles.card}>
|
||||||
{...props}
|
|
||||||
type="button"
|
|
||||||
className={styles.card({ disabled })}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<div className={styles.backdrop}>
|
<div className={styles.backdrop}>
|
||||||
<img src={game.cover} alt={game.title} className={styles.cover} />
|
<img src={game.cover} alt={game.title} className={styles.cover} />
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ export const hero = style({
|
|||||||
height: "280px",
|
height: "280px",
|
||||||
minHeight: "280px",
|
minHeight: "280px",
|
||||||
maxHeight: "280px",
|
maxHeight: "280px",
|
||||||
borderRadius: "8px",
|
borderRadius: "4px",
|
||||||
color: "#DADBE1",
|
color: "#DADBE1",
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
boxShadow: "0px 0px 15px 0px #000000",
|
boxShadow: "0px 0px 15px 0px #000000",
|
||||||
@ -45,6 +45,7 @@ export const description = style({
|
|||||||
textAlign: "left",
|
textAlign: "left",
|
||||||
fontFamily: "'Fira Sans', sans-serif",
|
fontFamily: "'Fira Sans', sans-serif",
|
||||||
lineHeight: "20px",
|
lineHeight: "20px",
|
||||||
|
marginTop: `${SPACING_UNIT * 2}px`,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const content = style({
|
export const content = style({
|
||||||
|
@ -2,20 +2,28 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import * as styles from "./hero.css";
|
import * as styles from "./hero.css";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { ShopDetails } from "@types";
|
import { ShopDetails } from "@types";
|
||||||
import { getSteamLanguage, steamUrlBuilder } from "@renderer/helpers";
|
import {
|
||||||
|
buildGameDetailsPath,
|
||||||
|
getSteamLanguage,
|
||||||
|
steamUrlBuilder,
|
||||||
|
} from "@renderer/helpers";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
const FEATURED_GAME_TITLE = "Horizon Forbidden West™ Complete Edition";
|
||||||
const FEATURED_GAME_ID = "2420110";
|
const FEATURED_GAME_ID = "2420110";
|
||||||
|
|
||||||
export function Hero() {
|
export function Hero() {
|
||||||
const [featuredGameDetails, setFeaturedGameDetails] =
|
const [featuredGameDetails, setFeaturedGameDetails] =
|
||||||
useState<ShopDetails | null>(null);
|
useState<ShopDetails | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
window.electron
|
window.electron
|
||||||
.getGameShopDetails(
|
.getGameShopDetails(
|
||||||
FEATURED_GAME_ID,
|
FEATURED_GAME_ID,
|
||||||
@ -24,19 +32,30 @@ export function Hero() {
|
|||||||
)
|
)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
setFeaturedGameDetails(result);
|
setFeaturedGameDetails(result);
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setIsLoading(false);
|
||||||
});
|
});
|
||||||
}, [i18n.language]);
|
}, [i18n.language]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => navigate(`/game/steam/${FEATURED_GAME_ID}`)}
|
onClick={() =>
|
||||||
|
navigate(
|
||||||
|
buildGameDetailsPath({
|
||||||
|
title: FEATURED_GAME_TITLE,
|
||||||
|
objectID: FEATURED_GAME_ID,
|
||||||
|
shop: "steam",
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
className={styles.hero}
|
className={styles.hero}
|
||||||
>
|
>
|
||||||
<div className={styles.backdrop}>
|
<div className={styles.backdrop}>
|
||||||
<img
|
<img
|
||||||
src="https://cdn2.steamgriddb.com/hero/4ef10445b952a8b3c93a9379d581146a.jpg"
|
src="https://cdn2.steamgriddb.com/hero/4ef10445b952a8b3c93a9379d581146a.jpg"
|
||||||
alt={featuredGameDetails?.name}
|
alt={FEATURED_GAME_TITLE}
|
||||||
className={styles.heroMedia}
|
className={styles.heroMedia}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -44,13 +63,14 @@ export function Hero() {
|
|||||||
<img
|
<img
|
||||||
src={steamUrlBuilder.logo(FEATURED_GAME_ID)}
|
src={steamUrlBuilder.logo(FEATURED_GAME_ID)}
|
||||||
width="250px"
|
width="250px"
|
||||||
alt={featuredGameDetails?.name}
|
alt={FEATURED_GAME_TITLE}
|
||||||
style={{ marginBottom: 16 }}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{!isLoading && featuredGameDetails && (
|
||||||
<p className={styles.description}>
|
<p className={styles.description}>
|
||||||
{featuredGameDetails?.short_description}
|
{featuredGameDetails?.short_description}
|
||||||
</p>
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
@ -15,6 +15,7 @@ import XLogo from "@renderer/assets/x-icon.svg?react";
|
|||||||
|
|
||||||
import * as styles from "./sidebar.css";
|
import * as styles from "./sidebar.css";
|
||||||
import { GameStatus, GameStatusHelper } from "@shared";
|
import { GameStatus, GameStatusHelper } from "@shared";
|
||||||
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
|
|
||||||
const SIDEBAR_MIN_WIDTH = 200;
|
const SIDEBAR_MIN_WIDTH = 200;
|
||||||
const SIDEBAR_INITIAL_WIDTH = 250;
|
const SIDEBAR_INITIAL_WIDTH = 250;
|
||||||
@ -209,9 +210,7 @@ export function Sidebar() {
|
|||||||
type="button"
|
type="button"
|
||||||
className={styles.menuItemButton}
|
className={styles.menuItemButton}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleSidebarItemClick(
|
handleSidebarItemClick(buildGameDetailsPath(game))
|
||||||
`/game/${game.shop}/${game.objectID}`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
|
5
src/renderer/src/declaration.d.ts
vendored
5
src/renderer/src/declaration.d.ts
vendored
@ -2,9 +2,11 @@ import type {
|
|||||||
CatalogueCategory,
|
CatalogueCategory,
|
||||||
CatalogueEntry,
|
CatalogueEntry,
|
||||||
Game,
|
Game,
|
||||||
|
GameRepack,
|
||||||
GameShop,
|
GameShop,
|
||||||
HowLongToBeatCategory,
|
HowLongToBeatCategory,
|
||||||
ShopDetails,
|
ShopDetails,
|
||||||
|
Steam250Game,
|
||||||
TorrentProgress,
|
TorrentProgress,
|
||||||
UserPreferences,
|
UserPreferences,
|
||||||
} from "@types";
|
} from "@types";
|
||||||
@ -40,7 +42,7 @@ declare global {
|
|||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
language: string
|
language: string
|
||||||
) => Promise<ShopDetails | null>;
|
) => Promise<ShopDetails | null>;
|
||||||
getRandomGame: () => Promise<string>;
|
getRandomGame: () => Promise<Steam250Game>;
|
||||||
getHowLongToBeat: (
|
getHowLongToBeat: (
|
||||||
objectID: string,
|
objectID: string,
|
||||||
shop: GameShop,
|
shop: GameShop,
|
||||||
@ -50,6 +52,7 @@ declare global {
|
|||||||
take?: number,
|
take?: number,
|
||||||
prevCursor?: number
|
prevCursor?: number
|
||||||
) => Promise<{ results: CatalogueEntry[]; cursor: number }>;
|
) => Promise<{ results: CatalogueEntry[]; cursor: number }>;
|
||||||
|
searchGameRepacks: (query: string) => Promise<GameRepack[]>;
|
||||||
|
|
||||||
/* Library */
|
/* Library */
|
||||||
addGameToLibrary: (
|
addGameToLibrary: (
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import type { CatalogueEntry } from "@types";
|
||||||
|
|
||||||
export const steamUrlBuilder = {
|
export const steamUrlBuilder = {
|
||||||
library: (objectID: string) =>
|
library: (objectID: string) =>
|
||||||
`https://steamcdn-a.akamaihd.net/steam/apps/${objectID}/header.jpg`,
|
`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("it")) return "italian";
|
||||||
if (language.startsWith("hu")) return "hungarian";
|
if (language.startsWith("hu")) return "hungarian";
|
||||||
if (language.startsWith("pl")) return "polish";
|
if (language.startsWith("pl")) return "polish";
|
||||||
|
if (language.startsWith("da")) return "danish";
|
||||||
|
|
||||||
return "english";
|
return "english";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const buildGameDetailsPath = (
|
||||||
|
game: Pick<CatalogueEntry, "title" | "shop" | "objectID">,
|
||||||
|
params: Record<string, string> = {}
|
||||||
|
) => {
|
||||||
|
const searchParams = new URLSearchParams({ title: game.title, ...params });
|
||||||
|
return `/game/${game.shop}/${game.objectID}?${searchParams.toString()}`;
|
||||||
|
};
|
||||||
|
@ -1,6 +1,18 @@
|
|||||||
import { formatDistance } from "date-fns";
|
import { formatDistance } from "date-fns";
|
||||||
import type { FormatDistanceOptions } 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";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export function useDate() {
|
export function useDate() {
|
||||||
@ -18,6 +30,7 @@ export function useDate() {
|
|||||||
if (language.startsWith("ru")) return ru;
|
if (language.startsWith("ru")) return ru;
|
||||||
if (language.startsWith("it")) return it;
|
if (language.startsWith("it")) return it;
|
||||||
if (language.startsWith("be")) return be;
|
if (language.startsWith("be")) return be;
|
||||||
|
if (language.startsWith("da")) return da;
|
||||||
|
|
||||||
return enUS;
|
return enUS;
|
||||||
};
|
};
|
||||||
|
@ -11,6 +11,7 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import * as styles from "../home/home.css";
|
import * as styles from "../home/home.css";
|
||||||
import { ArrowLeftIcon, ArrowRightIcon } from "@primer/octicons-react";
|
import { ArrowLeftIcon, ArrowRightIcon } from "@primer/octicons-react";
|
||||||
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
|
|
||||||
export function Catalogue() {
|
export function Catalogue() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -31,7 +32,7 @@ export function Catalogue() {
|
|||||||
|
|
||||||
const handleGameClick = (game: CatalogueEntry) => {
|
const handleGameClick = (game: CatalogueEntry) => {
|
||||||
dispatch(clearSearch());
|
dispatch(clearSearch());
|
||||||
navigate(`/game/${game.shop}/${game.objectID}`);
|
navigate(buildGameDetailsPath(game));
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -11,7 +11,7 @@ import * as styles from "./game-details.css";
|
|||||||
const OPEN_HYDRA_URL = "https://open.hydralauncher.site";
|
const OPEN_HYDRA_URL = "https://open.hydralauncher.site";
|
||||||
|
|
||||||
export interface DescriptionHeaderProps {
|
export interface DescriptionHeaderProps {
|
||||||
gameDetails: ShopDetails | null;
|
gameDetails: ShopDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DescriptionHeader({ gameDetails }: DescriptionHeaderProps) {
|
export function DescriptionHeader({ gameDetails }: DescriptionHeaderProps) {
|
||||||
@ -64,7 +64,7 @@ export function DescriptionHeader({ gameDetails }: DescriptionHeaderProps) {
|
|||||||
date: gameDetails?.release_date.date,
|
date: gameDetails?.release_date.date,
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
<p>{t("publisher", { publisher: gameDetails?.publishers[0] })}</p>
|
<p>{t("publisher", { publisher: gameDetails.publishers[0] })}</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { recipe } from "@vanilla-extract/recipes";
|
||||||
import { SPACING_UNIT, vars } from "../../theme.css";
|
import { SPACING_UNIT, vars } from "../../theme.css";
|
||||||
import { style } from "@vanilla-extract/css";
|
import { style } from "@vanilla-extract/css";
|
||||||
|
|
||||||
@ -13,7 +14,7 @@ export const gallerySliderMedia = style({
|
|||||||
width: "100%",
|
width: "100%",
|
||||||
height: "100%",
|
height: "100%",
|
||||||
display: "block",
|
display: "block",
|
||||||
flexShrink: 0,
|
flexShrink: "0",
|
||||||
flexGrow: "0",
|
flexGrow: "0",
|
||||||
transition: "translate 0.3s ease-in-out",
|
transition: "translate 0.3s ease-in-out",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
@ -54,42 +55,77 @@ export const gallerySliderPreview = style({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const gallerySliderMediaPreview = style({
|
export const mediaPreviewButton = recipe({
|
||||||
|
base: {
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
width: "20%",
|
width: "20%",
|
||||||
height: "20%",
|
height: "20%",
|
||||||
display: "block",
|
display: "block",
|
||||||
flexShrink: 0,
|
flexShrink: "0",
|
||||||
flexGrow: 0,
|
flexGrow: "0",
|
||||||
opacity: 0.3,
|
opacity: "0.3",
|
||||||
transition: "translate 0.3s ease-in-out, opacity 0.2s ease",
|
transition: "translate 0.3s ease-in-out, opacity 0.2s ease",
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
border: `solid 1px ${vars.color.border}`,
|
border: `solid 1px ${vars.color.border}`,
|
||||||
":hover": {
|
":hover": {
|
||||||
|
opacity: "0.8",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
active: {
|
||||||
|
true: {
|
||||||
opacity: "1",
|
opacity: "1",
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
|
|
||||||
export const gallerySliderMediaPreviewActive = style({
|
|
||||||
opacity: 1,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const gallerySliderButton = style({
|
|
||||||
all: "unset",
|
|
||||||
display: "block",
|
|
||||||
position: "absolute",
|
|
||||||
top: 0,
|
|
||||||
bottom: 0,
|
|
||||||
padding: "1rem",
|
|
||||||
cursor: "pointer",
|
|
||||||
transition: "background-color 100ms ease-in-out",
|
|
||||||
":hover": {
|
|
||||||
backgroundColor: "rgb(0, 0, 0, 0.2)",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const gallerySliderIcons = style({
|
export const mediaPreview = style({
|
||||||
fill: vars.color.muted,
|
width: "100%",
|
||||||
width: "2rem",
|
height: "100%",
|
||||||
height: "2rem",
|
display: "flex",
|
||||||
|
flex: "1",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const gallerySliderButton = recipe({
|
||||||
|
base: {
|
||||||
|
position: "absolute",
|
||||||
|
alignSelf: "center",
|
||||||
|
cursor: "pointer",
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.4)",
|
||||||
|
transition: "all 0.2s ease-in-out",
|
||||||
|
borderRadius: "50%",
|
||||||
|
color: vars.color.muted,
|
||||||
|
width: "48px",
|
||||||
|
height: "48px",
|
||||||
|
":hover": {
|
||||||
|
backgroundColor: "rgba(0, 0, 0, 0.6)",
|
||||||
|
},
|
||||||
|
":active": {
|
||||||
|
transform: "scale(0.95)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
variants: {
|
||||||
|
direction: {
|
||||||
|
left: {
|
||||||
|
left: "0",
|
||||||
|
marginLeft: `${SPACING_UNIT}px`,
|
||||||
|
transform: `translateX(${-(48 + SPACING_UNIT)}px)`,
|
||||||
|
},
|
||||||
|
right: {
|
||||||
|
right: "0",
|
||||||
|
marginRight: `${SPACING_UNIT}px`,
|
||||||
|
transform: `translateX(${48 + SPACING_UNIT}px)`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
visible: {
|
||||||
|
true: {
|
||||||
|
transform: "translateX(0)",
|
||||||
|
opacity: "1",
|
||||||
|
},
|
||||||
|
false: {
|
||||||
|
opacity: "0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
@ -1,17 +1,25 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { ShopDetails, SteamMovies, SteamScreenshot } from "@types";
|
|
||||||
import { ChevronRightIcon, ChevronLeftIcon } from "@primer/octicons-react";
|
import { ChevronRightIcon, ChevronLeftIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
|
import type { ShopDetails } from "@types";
|
||||||
|
|
||||||
import * as styles from "./gallery-slider.css";
|
import * as styles from "./gallery-slider.css";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export interface GallerySliderProps {
|
export interface GallerySliderProps {
|
||||||
gameDetails: ShopDetails | null;
|
gameDetails: ShopDetails;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GallerySlider({ gameDetails }: GallerySliderProps) {
|
export function GallerySlider({ gameDetails }: GallerySliderProps) {
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const mediaContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
|
const hasScreenshots = gameDetails && gameDetails.screenshots.length;
|
||||||
|
const hasMovies = gameDetails && gameDetails.movies?.length;
|
||||||
|
|
||||||
const [mediaCount] = useState<number>(() => {
|
const [mediaCount] = useState<number>(() => {
|
||||||
if (gameDetails) {
|
|
||||||
if (gameDetails.screenshots && gameDetails.movies) {
|
if (gameDetails.screenshots && gameDetails.movies) {
|
||||||
return gameDetails.screenshots.length + gameDetails.movies.length;
|
return gameDetails.screenshots.length + gameDetails.movies.length;
|
||||||
} else if (gameDetails.movies) {
|
} else if (gameDetails.movies) {
|
||||||
@ -19,13 +27,12 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) {
|
|||||||
} else if (gameDetails.screenshots) {
|
} else if (gameDetails.screenshots) {
|
||||||
return gameDetails.screenshots.length;
|
return gameDetails.screenshots.length;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
const [mediaIndex, setMediaIndex] = useState<number>(0);
|
const [mediaIndex, setMediaIndex] = useState<number>(0);
|
||||||
const [arrowShow, setArrowShow] = useState(false);
|
const [showArrows, setShowArrows] = useState(false);
|
||||||
|
|
||||||
const showNextImage = () => {
|
const showNextImage = () => {
|
||||||
setMediaIndex((index: number) => {
|
setMediaIndex((index: number) => {
|
||||||
@ -47,6 +54,20 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) {
|
|||||||
setMediaIndex(0);
|
setMediaIndex(0);
|
||||||
}, [gameDetails]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
if (scrollContainerRef.current) {
|
if (scrollContainerRef.current) {
|
||||||
const container = scrollContainerRef.current;
|
const container = scrollContainerRef.current;
|
||||||
@ -57,92 +78,107 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) {
|
|||||||
}
|
}
|
||||||
}, [gameDetails, mediaIndex, mediaCount]);
|
}, [gameDetails, mediaIndex, mediaCount]);
|
||||||
|
|
||||||
const hasScreenshots = gameDetails && gameDetails.screenshots.length;
|
const previews = useMemo(() => {
|
||||||
const hasMovies = gameDetails && gameDetails.movies?.length;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{hasScreenshots && (
|
{hasScreenshots && (
|
||||||
<div className={styles.gallerySliderContainer}>
|
<div className={styles.gallerySliderContainer}>
|
||||||
<div
|
<div
|
||||||
onMouseEnter={() => setArrowShow(true)}
|
onMouseEnter={() => setShowArrows(true)}
|
||||||
onMouseLeave={() => setArrowShow(false)}
|
onMouseLeave={() => setShowArrows(false)}
|
||||||
className={styles.gallerySliderAnimationContainer}
|
className={styles.gallerySliderAnimationContainer}
|
||||||
|
ref={mediaContainerRef}
|
||||||
>
|
>
|
||||||
{gameDetails.movies &&
|
{gameDetails.movies &&
|
||||||
gameDetails.movies.map((video: SteamMovies) => (
|
gameDetails.movies.map((video) => (
|
||||||
<video
|
<video
|
||||||
key={video.id}
|
key={video.id}
|
||||||
controls
|
controls
|
||||||
className={styles.gallerySliderMedia}
|
className={styles.gallerySliderMedia}
|
||||||
poster={video.thumbnail}
|
poster={video.thumbnail}
|
||||||
style={{ translate: `${-100 * mediaIndex}%` }}
|
style={{ translate: `${-100 * mediaIndex}%` }}
|
||||||
autoPlay
|
|
||||||
loop
|
loop
|
||||||
muted
|
muted
|
||||||
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<source src={video.webm.max.replace("http", "https")} />
|
<source src={video.mp4.max.replace("http", "https")} />
|
||||||
</video>
|
</video>
|
||||||
))}
|
))}
|
||||||
{gameDetails.screenshots &&
|
|
||||||
gameDetails.screenshots.map(
|
{hasScreenshots &&
|
||||||
(image: SteamScreenshot, i: number) => (
|
gameDetails.screenshots.map((image, i) => (
|
||||||
<img
|
<img
|
||||||
key={"image-" + i}
|
key={image.id}
|
||||||
className={styles.gallerySliderMedia}
|
className={styles.gallerySliderMedia}
|
||||||
src={image.path_full}
|
src={image.path_full}
|
||||||
style={{ translate: `${-100 * mediaIndex}%` }}
|
style={{ translate: `${-100 * mediaIndex}%` }}
|
||||||
|
alt={t("screenshot", { number: i + 1 })}
|
||||||
/>
|
/>
|
||||||
)
|
))}
|
||||||
)}
|
|
||||||
{arrowShow && (
|
|
||||||
<>
|
|
||||||
<button
|
<button
|
||||||
onClick={showPrevImage}
|
onClick={showPrevImage}
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.gallerySliderButton}
|
className={styles.gallerySliderButton({
|
||||||
style={{ left: 0 }}
|
visible: showArrows,
|
||||||
|
direction: "left",
|
||||||
|
})}
|
||||||
|
aria-label={t("previous_screenshot")}
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<ChevronLeftIcon className={styles.gallerySliderIcons} />
|
<ChevronLeftIcon size={36} />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={showNextImage}
|
onClick={showNextImage}
|
||||||
type="button"
|
type="button"
|
||||||
className={styles.gallerySliderButton}
|
className={styles.gallerySliderButton({
|
||||||
style={{ right: 0 }}
|
visible: showArrows,
|
||||||
|
direction: "right",
|
||||||
|
})}
|
||||||
|
aria-label={t("next_screenshot")}
|
||||||
|
tabIndex={0}
|
||||||
>
|
>
|
||||||
<ChevronRightIcon className={styles.gallerySliderIcons} />
|
<ChevronRightIcon size={36} />
|
||||||
</button>
|
</button>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.gallerySliderPreview} ref={scrollContainerRef}>
|
<div className={styles.gallerySliderPreview} ref={scrollContainerRef}>
|
||||||
{hasMovies &&
|
{previews.map((media, i) => (
|
||||||
gameDetails.movies?.map((video: SteamMovies, i: number) => (
|
<button
|
||||||
<img
|
key={media.id}
|
||||||
key={video.id}
|
type="button"
|
||||||
|
className={styles.mediaPreviewButton({
|
||||||
|
active: mediaIndex === i,
|
||||||
|
})}
|
||||||
onClick={() => setMediaIndex(i)}
|
onClick={() => setMediaIndex(i)}
|
||||||
src={video.thumbnail}
|
aria-label={t("open_screenshot", { number: i + 1 })}
|
||||||
className={`${styles.gallerySliderMediaPreview} ${mediaIndex === i ? styles.gallerySliderMediaPreviewActive : ""}`}
|
>
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{gameDetails.screenshots &&
|
|
||||||
gameDetails.screenshots.map(
|
|
||||||
(image: SteamScreenshot, i: number) => (
|
|
||||||
<img
|
<img
|
||||||
key={"image-thumb-" + i}
|
src={media.thumbnail}
|
||||||
onClick={() =>
|
className={styles.mediaPreview}
|
||||||
setMediaIndex(
|
alt={t("screenshot", { number: i + 1 })}
|
||||||
i + (gameDetails.movies ? gameDetails.movies.length : 0)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
className={`${styles.gallerySliderMediaPreview} ${mediaIndex === i + (gameDetails.movies ? gameDetails.movies.length : 0) ? styles.gallerySliderMediaPreviewActive : ""}`}
|
|
||||||
src={image.path_full}
|
|
||||||
/>
|
/>
|
||||||
)
|
</button>
|
||||||
)}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -1,7 +1,10 @@
|
|||||||
import Skeleton from "react-loading-skeleton";
|
import Skeleton from "react-loading-skeleton";
|
||||||
|
|
||||||
import { Button } from "@renderer/components";
|
import { Button } from "@renderer/components";
|
||||||
|
|
||||||
import * as styles from "./game-details.css";
|
import * as styles from "./game-details.css";
|
||||||
|
import * as sidebarStyles from "./sidebar/sidebar.css";
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { ShareAndroidIcon } from "@primer/octicons-react";
|
import { ShareAndroidIcon } from "@primer/octicons-react";
|
||||||
|
|
||||||
@ -43,41 +46,41 @@ export function GameDetailsSkeleton() {
|
|||||||
<Skeleton />
|
<Skeleton />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.contentSidebar}>
|
<div className={sidebarStyles.contentSidebar}>
|
||||||
<div className={styles.contentSidebarTitle}>
|
<div className={sidebarStyles.contentSidebarTitle}>
|
||||||
<h3>HowLongToBeat</h3>
|
<h3>HowLongToBeat</h3>
|
||||||
</div>
|
</div>
|
||||||
<ul className={styles.howLongToBeatCategoriesList}>
|
<ul className={sidebarStyles.howLongToBeatCategoriesList}>
|
||||||
{Array.from({ length: 3 }).map((_, index) => (
|
{Array.from({ length: 3 }).map((_, index) => (
|
||||||
<Skeleton
|
<Skeleton
|
||||||
key={index}
|
key={index}
|
||||||
className={styles.howLongToBeatCategorySkeleton}
|
className={sidebarStyles.howLongToBeatCategorySkeleton}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<div
|
<div
|
||||||
className={styles.contentSidebarTitle}
|
className={sidebarStyles.contentSidebarTitle}
|
||||||
style={{ border: "none" }}
|
style={{ border: "none" }}
|
||||||
>
|
>
|
||||||
<h3>{t("requirements")}</h3>
|
<h3>{t("requirements")}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.requirementButtonContainer}>
|
<div className={sidebarStyles.requirementButtonContainer}>
|
||||||
<Button
|
<Button
|
||||||
className={styles.requirementButton}
|
className={sidebarStyles.requirementButton}
|
||||||
theme="primary"
|
theme="primary"
|
||||||
disabled
|
disabled
|
||||||
>
|
>
|
||||||
{t("minimum")}
|
{t("minimum")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className={styles.requirementButton}
|
className={sidebarStyles.requirementButton}
|
||||||
theme="outline"
|
theme="outline"
|
||||||
disabled
|
disabled
|
||||||
>
|
>
|
||||||
{t("recommended")}
|
{t("recommended")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.requirementsDetailsSkeleton}>
|
<div className={sidebarStyles.requirementsDetailsSkeleton}>
|
||||||
{Array.from({ length: 6 }).map((_, index) => (
|
{Array.from({ length: 6 }).map((_, index) => (
|
||||||
<Skeleton key={index} height={20} />
|
<Skeleton key={index} height={20} />
|
||||||
))}
|
))}
|
||||||
|
@ -79,62 +79,6 @@ export const descriptionContent = style({
|
|||||||
height: "100%",
|
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({
|
export const description = style({
|
||||||
userSelect: "text",
|
userSelect: "text",
|
||||||
lineHeight: "22px",
|
lineHeight: "22px",
|
||||||
@ -183,34 +127,6 @@ export const descriptionHeaderInfo = style({
|
|||||||
flexDirection: "column",
|
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({
|
export const randomizerButton = style({
|
||||||
animationName: slideIn,
|
animationName: slideIn,
|
||||||
animationDuration: "0.2s",
|
animationDuration: "0.2s",
|
||||||
@ -260,8 +176,3 @@ globalStyle(`${description} img`, {
|
|||||||
globalStyle(`${description} a`, {
|
globalStyle(`${description} a`, {
|
||||||
color: vars.color.bodyText,
|
color: vars.color.bodyText,
|
||||||
});
|
});
|
||||||
|
|
||||||
globalStyle(`${requirementsDetails} a`, {
|
|
||||||
display: "flex",
|
|
||||||
color: vars.color.bodyText,
|
|
||||||
});
|
|
||||||
|
@ -3,18 +3,21 @@ import { average } from "color.js";
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
import type {
|
import {
|
||||||
Game,
|
Steam250Game,
|
||||||
GameRepack,
|
type Game,
|
||||||
GameShop,
|
type GameRepack,
|
||||||
HowLongToBeatCategory,
|
type GameShop,
|
||||||
ShopDetails,
|
type ShopDetails,
|
||||||
SteamAppDetails,
|
|
||||||
} from "@types";
|
} from "@types";
|
||||||
|
|
||||||
import { Button } from "@renderer/components";
|
import { Button } from "@renderer/components";
|
||||||
import { setHeaderTitle } from "@renderer/features";
|
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 { useAppDispatch, useDownload } from "@renderer/hooks";
|
||||||
|
|
||||||
import starsAnimation from "@renderer/assets/lottie/stars.json";
|
import starsAnimation from "@renderer/assets/lottie/stars.json";
|
||||||
@ -26,7 +29,6 @@ import { DescriptionHeader } from "./description-header";
|
|||||||
import { GameDetailsSkeleton } from "./game-details-skeleton";
|
import { GameDetailsSkeleton } from "./game-details-skeleton";
|
||||||
import * as styles from "./game-details.css";
|
import * as styles from "./game-details.css";
|
||||||
import { HeroPanel } from "./hero";
|
import { HeroPanel } from "./hero";
|
||||||
import { HowLongToBeatSection } from "./how-long-to-beat-section";
|
|
||||||
import { RepacksModal } from "./repacks-modal";
|
import { RepacksModal } from "./repacks-modal";
|
||||||
|
|
||||||
import { vars } from "../../theme.css";
|
import { vars } from "../../theme.css";
|
||||||
@ -37,18 +39,16 @@ import {
|
|||||||
OnlineFixInstallationGuide,
|
OnlineFixInstallationGuide,
|
||||||
} from "./installation-guides";
|
} from "./installation-guides";
|
||||||
import { GallerySlider } from "./gallery-slider";
|
import { GallerySlider } from "./gallery-slider";
|
||||||
|
import { Sidebar } from "./sidebar/sidebar";
|
||||||
|
|
||||||
export function GameDetails() {
|
export function GameDetails() {
|
||||||
const { objectID, shop } = useParams();
|
const { objectID, shop } = useParams();
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isLoadingRandomGame, setIsLoadingRandomGame] = useState(false);
|
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
||||||
const [color, setColor] = useState({ dark: "", light: "" });
|
const [color, setColor] = useState({ dark: "", light: "" });
|
||||||
const [gameDetails, setGameDetails] = useState<ShopDetails | null>(null);
|
const [gameDetails, setGameDetails] = useState<ShopDetails | null>(null);
|
||||||
const [howLongToBeat, setHowLongToBeat] = useState<{
|
const [repacks, setRepacks] = useState<GameRepack[]>([]);
|
||||||
isLoading: boolean;
|
|
||||||
data: HowLongToBeatCategory[] | null;
|
|
||||||
}>({ isLoading: true, data: null });
|
|
||||||
|
|
||||||
const [game, setGame] = useState<Game | null>(null);
|
const [game, setGame] = useState<Game | null>(null);
|
||||||
const [isGamePlaying, setIsGamePlaying] = useState(false);
|
const [isGamePlaying, setIsGamePlaying] = useState(false);
|
||||||
@ -56,12 +56,12 @@ export function GameDetails() {
|
|||||||
null | "onlinefix" | "DODI"
|
null | "onlinefix" | "DODI"
|
||||||
>(null);
|
>(null);
|
||||||
|
|
||||||
const [activeRequirement, setActiveRequirement] =
|
|
||||||
useState<keyof SteamAppDetails["pc_requirements"]>("minimum");
|
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
|
const fromRandomizer = searchParams.get("fromRandomizer");
|
||||||
|
const title = searchParams.get("title")!;
|
||||||
|
|
||||||
const { t, i18n } = useTranslation("game_details");
|
const { t, i18n } = useTranslation("game_details");
|
||||||
|
|
||||||
const [showRepacksModal, setShowRepacksModal] = useState(false);
|
const [showRepacksModal, setShowRepacksModal] = useState(false);
|
||||||
@ -90,37 +90,35 @@ export function GameDetails() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getGame();
|
getGame();
|
||||||
}, [getGame, gameDownloading?.id]);
|
}, [getGame, gameDownloading?.id]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setGame(null);
|
setGame(null);
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setIsGamePlaying(false);
|
setIsGamePlaying(false);
|
||||||
dispatch(setHeaderTitle(""));
|
dispatch(setHeaderTitle(title));
|
||||||
|
|
||||||
window.electron
|
window.electron.getRandomGame().then((randomGame) => {
|
||||||
.getGameShopDetails(objectID!, "steam", getSteamLanguage(i18n.language))
|
setRandomGame(randomGame);
|
||||||
.then((result) => {
|
|
||||||
if (!result) {
|
|
||||||
navigate(-1);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.electron
|
|
||||||
.getHowLongToBeat(objectID!, "steam", result.name)
|
|
||||||
.then((data) => {
|
|
||||||
setHowLongToBeat({ isLoading: false, data });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
setGameDetails(result);
|
Promise.all([
|
||||||
dispatch(setHeaderTitle(result.name));
|
window.electron.getGameShopDetails(
|
||||||
setIsLoadingRandomGame(false);
|
objectID!,
|
||||||
|
"steam",
|
||||||
|
getSteamLanguage(i18n.language)
|
||||||
|
),
|
||||||
|
window.electron.searchGameRepacks(title),
|
||||||
|
])
|
||||||
|
.then(([appDetails, repacks]) => {
|
||||||
|
if (appDetails) setGameDetails(appDetails);
|
||||||
|
setRepacks(repacks);
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
getGame();
|
getGame();
|
||||||
setHowLongToBeat({ isLoading: true, data: null });
|
}, [getGame, dispatch, navigate, title, objectID, i18n.language]);
|
||||||
}, [getGame, dispatch, navigate, objectID, i18n.language]);
|
|
||||||
|
|
||||||
const isGameDownloading = gameDownloading?.id === game?.id;
|
const isGameDownloading = gameDownloading?.id === game?.id;
|
||||||
|
|
||||||
@ -154,11 +152,10 @@ export function GameDetails() {
|
|||||||
repack: GameRepack,
|
repack: GameRepack,
|
||||||
downloadPath: string
|
downloadPath: string
|
||||||
) => {
|
) => {
|
||||||
if (gameDetails) {
|
|
||||||
return startDownload(
|
return startDownload(
|
||||||
repack.id,
|
repack.id,
|
||||||
gameDetails.objectID,
|
objectID!,
|
||||||
gameDetails.name,
|
title,
|
||||||
shop as GameShop,
|
shop as GameShop,
|
||||||
downloadPath
|
downloadPath
|
||||||
).then(() => {
|
).then(() => {
|
||||||
@ -177,32 +174,27 @@ export function GameDetails() {
|
|||||||
setShowInstructionsModal("DODI");
|
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 (
|
return (
|
||||||
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
|
||||||
{gameDetails && (
|
|
||||||
<RepacksModal
|
<RepacksModal
|
||||||
visible={showRepacksModal}
|
visible={showRepacksModal}
|
||||||
gameDetails={gameDetails}
|
repacks={repacks}
|
||||||
startDownload={handleStartDownload}
|
startDownload={handleStartDownload}
|
||||||
onClose={() => setShowRepacksModal(false)}
|
onClose={() => setShowRepacksModal(false)}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
|
|
||||||
<OnlineFixInstallationGuide
|
<OnlineFixInstallationGuide
|
||||||
visible={showInstructionsModal === "onlinefix"}
|
visible={showInstructionsModal === "onlinefix"}
|
||||||
@ -240,7 +232,9 @@ export function GameDetails() {
|
|||||||
<HeroPanel
|
<HeroPanel
|
||||||
game={game}
|
game={game}
|
||||||
color={color.dark}
|
color={color.dark}
|
||||||
gameDetails={gameDetails}
|
objectID={objectID!}
|
||||||
|
title={title}
|
||||||
|
repacks={repacks}
|
||||||
openRepacksModal={() => setShowRepacksModal(true)}
|
openRepacksModal={() => setShowRepacksModal(true)}
|
||||||
getGame={getGame}
|
getGame={getGame}
|
||||||
isGamePlaying={isGamePlaying}
|
isGamePlaying={isGamePlaying}
|
||||||
@ -248,63 +242,22 @@ export function GameDetails() {
|
|||||||
|
|
||||||
<div className={styles.descriptionContainer}>
|
<div className={styles.descriptionContainer}>
|
||||||
<div className={styles.descriptionContent}>
|
<div className={styles.descriptionContent}>
|
||||||
<DescriptionHeader gameDetails={gameDetails} />
|
{gameDetails && <DescriptionHeader gameDetails={gameDetails} />}
|
||||||
|
{gameDetails && <GallerySlider gameDetails={gameDetails} />}
|
||||||
<GallerySlider gameDetails={gameDetails} />
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: gameDetails?.about_the_game ?? "",
|
__html: gameDetails?.about_the_game ?? t("no_shop_details"),
|
||||||
}}
|
}}
|
||||||
className={styles.description}
|
className={styles.description}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.contentSidebar}>
|
<Sidebar
|
||||||
<HowLongToBeatSection
|
objectID={objectID!}
|
||||||
howLongToBeatData={howLongToBeat.data}
|
title={title}
|
||||||
isLoading={howLongToBeat.isLoading}
|
gameDetails={gameDetails}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div
|
|
||||||
className={styles.contentSidebarTitle}
|
|
||||||
style={{ border: "none" }}
|
|
||||||
>
|
|
||||||
<h3>{t("requirements")}</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className={styles.requirementButtonContainer}>
|
|
||||||
<Button
|
|
||||||
className={styles.requirementButton}
|
|
||||||
onClick={() => setActiveRequirement("minimum")}
|
|
||||||
theme={
|
|
||||||
activeRequirement === "minimum" ? "primary" : "outline"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t("minimum")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className={styles.requirementButton}
|
|
||||||
onClick={() => setActiveRequirement("recommended")}
|
|
||||||
theme={
|
|
||||||
activeRequirement === "recommended" ? "primary" : "outline"
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{t("recommended")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className={styles.requirementsDetails}
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html:
|
|
||||||
gameDetails?.pc_requirements?.[activeRequirement] ??
|
|
||||||
t(`no_${activeRequirement}_requirements`, {
|
|
||||||
title: gameDetails?.name,
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
)}
|
)}
|
||||||
@ -314,7 +267,7 @@ export function GameDetails() {
|
|||||||
className={styles.randomizerButton}
|
className={styles.randomizerButton}
|
||||||
onClick={handleRandomizerClick}
|
onClick={handleRandomizerClick}
|
||||||
theme="outline"
|
theme="outline"
|
||||||
disabled={isLoadingRandomGame}
|
disabled={!randomGame}
|
||||||
>
|
>
|
||||||
<div style={{ width: 16, height: 16, position: "relative" }}>
|
<div style={{ width: 16, height: 16, position: "relative" }}>
|
||||||
<Lottie
|
<Lottie
|
||||||
|
@ -3,7 +3,7 @@ import { NoEntryIcon, PlusCircleIcon } from "@primer/octicons-react";
|
|||||||
|
|
||||||
import { Button } from "@renderer/components";
|
import { Button } from "@renderer/components";
|
||||||
import { useDownload, useLibrary } from "@renderer/hooks";
|
import { useDownload, useLibrary } from "@renderer/hooks";
|
||||||
import type { Game, ShopDetails } from "@types";
|
import type { Game, GameRepack } from "@types";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@ -11,9 +11,11 @@ import * as styles from "./hero-panel-actions.css";
|
|||||||
|
|
||||||
export interface HeroPanelActionsProps {
|
export interface HeroPanelActionsProps {
|
||||||
game: Game | null;
|
game: Game | null;
|
||||||
gameDetails: ShopDetails | null;
|
repacks: GameRepack[];
|
||||||
isGamePlaying: boolean;
|
isGamePlaying: boolean;
|
||||||
isGameDownloading: boolean;
|
isGameDownloading: boolean;
|
||||||
|
objectID: string;
|
||||||
|
title: string;
|
||||||
openRepacksModal: () => void;
|
openRepacksModal: () => void;
|
||||||
openBinaryNotFoundModal: () => void;
|
openBinaryNotFoundModal: () => void;
|
||||||
getGame: () => void;
|
getGame: () => void;
|
||||||
@ -21,9 +23,11 @@ export interface HeroPanelActionsProps {
|
|||||||
|
|
||||||
export function HeroPanelActions({
|
export function HeroPanelActions({
|
||||||
game,
|
game,
|
||||||
gameDetails,
|
|
||||||
isGamePlaying,
|
isGamePlaying,
|
||||||
isGameDownloading,
|
isGameDownloading,
|
||||||
|
repacks,
|
||||||
|
objectID,
|
||||||
|
title,
|
||||||
openRepacksModal,
|
openRepacksModal,
|
||||||
openBinaryNotFoundModal,
|
openBinaryNotFoundModal,
|
||||||
getGame,
|
getGame,
|
||||||
@ -69,12 +73,12 @@ export function HeroPanelActions({
|
|||||||
try {
|
try {
|
||||||
if (game) {
|
if (game) {
|
||||||
await removeGameFromLibrary(game.id);
|
await removeGameFromLibrary(game.id);
|
||||||
} else if (gameDetails) {
|
} else {
|
||||||
const gameExecutablePath = await selectGameExecutable();
|
const gameExecutablePath = await selectGameExecutable();
|
||||||
|
|
||||||
await window.electron.addGameToLibrary(
|
await window.electron.addGameToLibrary(
|
||||||
gameDetails.objectID,
|
objectID,
|
||||||
gameDetails.name,
|
title,
|
||||||
"steam",
|
"steam",
|
||||||
gameExecutablePath
|
gameExecutablePath
|
||||||
);
|
);
|
||||||
@ -123,7 +127,7 @@ export function HeroPanelActions({
|
|||||||
const toggleGameOnLibraryButton = (
|
const toggleGameOnLibraryButton = (
|
||||||
<Button
|
<Button
|
||||||
theme="outline"
|
theme="outline"
|
||||||
disabled={!gameDetails || toggleLibraryGameDisabled}
|
disabled={toggleLibraryGameDisabled}
|
||||||
onClick={toggleGameOnLibrary}
|
onClick={toggleGameOnLibrary}
|
||||||
className={styles.heroPanelAction}
|
className={styles.heroPanelAction}
|
||||||
>
|
>
|
||||||
@ -239,7 +243,7 @@ export function HeroPanelActions({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gameDetails && gameDetails.repacks.length) {
|
if (repacks.length) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{toggleGameOnLibraryButton}
|
{toggleGameOnLibraryButton}
|
||||||
|
@ -4,13 +4,13 @@ import { SPACING_UNIT, vars } from "../../../theme.css";
|
|||||||
export const panel = style({
|
export const panel = style({
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "72px",
|
height: "72px",
|
||||||
|
minHeight: "72px",
|
||||||
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
|
padding: `${SPACING_UNIT * 2}px ${SPACING_UNIT * 3}px`,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
transition: "all ease 0.2s",
|
transition: "all ease 0.2s",
|
||||||
borderBottom: `solid 1px ${vars.color.border}`,
|
borderBottom: `solid 1px ${vars.color.border}`,
|
||||||
boxShadow: "0px 0px 15px 0px #000000",
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const content = style({
|
export const content = style({
|
||||||
|
@ -3,7 +3,7 @@ import { useMemo, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { useDownload } from "@renderer/hooks";
|
import { useDownload } from "@renderer/hooks";
|
||||||
import type { Game, ShopDetails } from "@types";
|
import type { Game, GameRepack } from "@types";
|
||||||
|
|
||||||
import { formatDownloadProgress } from "@renderer/helpers";
|
import { formatDownloadProgress } from "@renderer/helpers";
|
||||||
import { HeroPanelActions } from "./hero-panel-actions";
|
import { HeroPanelActions } from "./hero-panel-actions";
|
||||||
@ -15,20 +15,24 @@ import { HeroPanelPlaytime } from "./hero-panel-playtime";
|
|||||||
|
|
||||||
export interface HeroPanelProps {
|
export interface HeroPanelProps {
|
||||||
game: Game | null;
|
game: Game | null;
|
||||||
gameDetails: ShopDetails | null;
|
|
||||||
color: string;
|
color: string;
|
||||||
isGamePlaying: boolean;
|
isGamePlaying: boolean;
|
||||||
|
objectID: string;
|
||||||
|
title: string;
|
||||||
|
repacks: GameRepack[];
|
||||||
openRepacksModal: () => void;
|
openRepacksModal: () => void;
|
||||||
getGame: () => void;
|
getGame: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HeroPanel({
|
export function HeroPanel({
|
||||||
game,
|
game,
|
||||||
gameDetails,
|
|
||||||
color,
|
color,
|
||||||
|
repacks,
|
||||||
|
objectID,
|
||||||
|
title,
|
||||||
|
isGamePlaying,
|
||||||
openRepacksModal,
|
openRepacksModal,
|
||||||
getGame,
|
getGame,
|
||||||
isGamePlaying,
|
|
||||||
}: HeroPanelProps) {
|
}: HeroPanelProps) {
|
||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
@ -58,8 +62,6 @@ export function HeroPanel({
|
|||||||
}, [game, isGameDownloading, gameDownloading]);
|
}, [game, isGameDownloading, gameDownloading]);
|
||||||
|
|
||||||
const getInfo = () => {
|
const getInfo = () => {
|
||||||
if (!gameDetails) return null;
|
|
||||||
|
|
||||||
if (isGameDeleting(game?.id ?? -1)) {
|
if (isGameDeleting(game?.id ?? -1)) {
|
||||||
return <p>{t("deleting")}</p>;
|
return <p>{t("deleting")}</p>;
|
||||||
}
|
}
|
||||||
@ -110,11 +112,11 @@ export function HeroPanel({
|
|||||||
return <HeroPanelPlaytime game={game} isGamePlaying={isGamePlaying} />;
|
return <HeroPanelPlaytime game={game} isGamePlaying={isGamePlaying} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [latestRepack] = gameDetails.repacks;
|
const [latestRepack] = repacks;
|
||||||
|
|
||||||
if (latestRepack) {
|
if (latestRepack) {
|
||||||
const lastUpdate = format(latestRepack.uploadDate!, "dd/MM/yyyy");
|
const lastUpdate = format(latestRepack.uploadDate!, "dd/MM/yyyy");
|
||||||
const repacksCount = gameDetails.repacks.length;
|
const repacksCount = repacks.length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -139,7 +141,9 @@ export function HeroPanel({
|
|||||||
<div className={styles.actions}>
|
<div className={styles.actions}>
|
||||||
<HeroPanelActions
|
<HeroPanelActions
|
||||||
game={game}
|
game={game}
|
||||||
gameDetails={gameDetails}
|
repacks={repacks}
|
||||||
|
objectID={objectID}
|
||||||
|
title={title}
|
||||||
getGame={getGame}
|
getGame={getGame}
|
||||||
openRepacksModal={openRepacksModal}
|
openRepacksModal={openRepacksModal}
|
||||||
openBinaryNotFoundModal={() => setShowBinaryNotFoundModal(true)}
|
openBinaryNotFoundModal={() => setShowBinaryNotFoundModal(true)}
|
||||||
|
@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import { Button, Modal, TextField } from "@renderer/components";
|
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";
|
import * as styles from "./repacks-modal.css";
|
||||||
|
|
||||||
@ -20,14 +20,14 @@ import { getRepackLanguageBasedOnRepacker } from "../../helpers/searcher";
|
|||||||
|
|
||||||
export interface RepacksModalProps {
|
export interface RepacksModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
gameDetails: ShopDetails;
|
repacks: GameRepack[];
|
||||||
startDownload: (repack: GameRepack, downloadPath: string) => Promise<void>;
|
startDownload: (repack: GameRepack, downloadPath: string) => Promise<void>;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RepacksModal({
|
export function RepacksModal({
|
||||||
visible,
|
visible,
|
||||||
gameDetails,
|
repacks,
|
||||||
startDownload,
|
startDownload,
|
||||||
onClose,
|
onClose,
|
||||||
}: RepacksModalProps) {
|
}: RepacksModalProps) {
|
||||||
@ -45,8 +45,8 @@ export function RepacksModal({
|
|||||||
const { t } = useTranslation("game_details");
|
const { t } = useTranslation("game_details");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setFilteredRepacks(gameDetails.repacks);
|
setFilteredRepacks(repacks);
|
||||||
}, [gameDetails.repacks, visible]);
|
}, [repacks, visible]);
|
||||||
|
|
||||||
const handleRepackClick = (repack: GameRepack) => {
|
const handleRepackClick = (repack: GameRepack) => {
|
||||||
setRepack(repack);
|
setRepack(repack);
|
||||||
@ -57,7 +57,7 @@ export function RepacksModal({
|
|||||||
const term = event.target.value.toLocaleLowerCase();
|
const term = event.target.value.toLocaleLowerCase();
|
||||||
|
|
||||||
setFilteredRepacks(
|
setFilteredRepacks(
|
||||||
gameDetails.repacks.filter((repack) => {
|
repacks.filter((repack) => {
|
||||||
const lowerCaseTitle = repack.title.toLowerCase();
|
const lowerCaseTitle = repack.title.toLowerCase();
|
||||||
const lowerCaseRepacker = repack.repacker.toLowerCase();
|
const lowerCaseRepacker = repack.repacker.toLowerCase();
|
||||||
|
|
||||||
@ -73,14 +73,13 @@ export function RepacksModal({
|
|||||||
<SelectFolderModal
|
<SelectFolderModal
|
||||||
visible={showSelectFolderModal}
|
visible={showSelectFolderModal}
|
||||||
onClose={() => setShowSelectFolderModal(false)}
|
onClose={() => setShowSelectFolderModal(false)}
|
||||||
gameDetails={gameDetails}
|
|
||||||
startDownload={startDownload}
|
startDownload={startDownload}
|
||||||
repack={repack}
|
repack={repack}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
visible={visible}
|
visible={visible}
|
||||||
title={`${gameDetails.name} Repacks`}
|
title={t("download_options")}
|
||||||
description={t("repacks_modal_description")}
|
description={t("repacks_modal_description")}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
>
|
>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Button, Link, Modal, TextField } from "@renderer/components";
|
import { Button, Link, Modal, TextField } from "@renderer/components";
|
||||||
import { GameRepack, ShopDetails } from "@types";
|
import type { GameRepack } from "@types";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@ -10,7 +10,6 @@ import { formatBytes } from "@shared";
|
|||||||
|
|
||||||
export interface SelectFolderModalProps {
|
export interface SelectFolderModalProps {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
gameDetails: ShopDetails;
|
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
startDownload: (repack: GameRepack, downloadPath: string) => Promise<void>;
|
startDownload: (repack: GameRepack, downloadPath: string) => Promise<void>;
|
||||||
repack: GameRepack | null;
|
repack: GameRepack | null;
|
||||||
@ -18,7 +17,6 @@ export interface SelectFolderModalProps {
|
|||||||
|
|
||||||
export function SelectFolderModal({
|
export function SelectFolderModal({
|
||||||
visible,
|
visible,
|
||||||
gameDetails,
|
|
||||||
onClose,
|
onClose,
|
||||||
startDownload,
|
startDownload,
|
||||||
repack,
|
repack,
|
||||||
@ -74,7 +72,7 @@ export function SelectFolderModal({
|
|||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
visible={visible}
|
visible={visible}
|
||||||
title={t("installation_folder", { name: gameDetails.name })}
|
title={t("download_path")}
|
||||||
description={t("space_left_on_disk", {
|
description={t("space_left_on_disk", {
|
||||||
space: formatBytes(diskFreeSpace?.free ?? 0),
|
space: formatBytes(diskFreeSpace?.free ?? 0),
|
||||||
})}
|
})}
|
||||||
@ -82,12 +80,7 @@ export function SelectFolderModal({
|
|||||||
>
|
>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<div className={styles.downloadsPathField}>
|
<div className={styles.downloadsPathField}>
|
||||||
<TextField
|
<TextField value={selectedPath} readOnly disabled />
|
||||||
label={t("downloads_path")}
|
|
||||||
value={selectedPath}
|
|
||||||
readOnly
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
style={{ alignSelf: "flex-end" }}
|
style={{ alignSelf: "flex-end" }}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
|
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import type { HowLongToBeatCategory } from "@types";
|
import type { HowLongToBeatCategory } from "@types";
|
||||||
import { vars } from "../../theme.css";
|
import { vars } from "../../../theme.css";
|
||||||
import * as styles from "./game-details.css";
|
import * as styles from "./sidebar.css";
|
||||||
|
|
||||||
const durationTranslation: Record<string, string> = {
|
const durationTranslation: Record<string, string> = {
|
||||||
Hours: "hours",
|
Hours: "hours",
|
92
src/renderer/src/pages/game-details/sidebar/sidebar.css.ts
Normal file
92
src/renderer/src/pages/game-details/sidebar/sidebar.css.ts
Normal file
@ -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,
|
||||||
|
});
|
84
src/renderer/src/pages/game-details/sidebar/sidebar.tsx
Normal file
84
src/renderer/src/pages/game-details/sidebar/sidebar.tsx
Normal file
@ -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<keyof SteamAppDetails["pc_requirements"]>("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 (
|
||||||
|
<aside className={styles.contentSidebar}>
|
||||||
|
<HowLongToBeatSection
|
||||||
|
howLongToBeatData={howLongToBeat.data}
|
||||||
|
isLoading={howLongToBeat.isLoading}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.contentSidebarTitle} style={{ border: "none" }}>
|
||||||
|
<h3>{t("requirements")}</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.requirementButtonContainer}>
|
||||||
|
<Button
|
||||||
|
className={styles.requirementButton}
|
||||||
|
onClick={() => setActiveRequirement("minimum")}
|
||||||
|
theme={activeRequirement === "minimum" ? "primary" : "outline"}
|
||||||
|
>
|
||||||
|
{t("minimum")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className={styles.requirementButton}
|
||||||
|
onClick={() => setActiveRequirement("recommended")}
|
||||||
|
theme={activeRequirement === "recommended" ? "primary" : "outline"}
|
||||||
|
>
|
||||||
|
{t("recommended")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={styles.requirementsDetails}
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html:
|
||||||
|
gameDetails?.pc_requirements?.[activeRequirement] ??
|
||||||
|
t(`no_${activeRequirement}_requirements`, {
|
||||||
|
title,
|
||||||
|
}),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
}
|
@ -1,17 +1,22 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
|
import Skeleton, { SkeletonTheme } from "react-loading-skeleton";
|
||||||
|
|
||||||
import { Button, GameCard, Hero } from "@renderer/components";
|
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 starsAnimation from "@renderer/assets/lottie/stars.json";
|
||||||
|
|
||||||
import * as styles from "./home.css";
|
import * as styles from "./home.css";
|
||||||
import { vars } from "../../theme.css";
|
import { vars } from "../../theme.css";
|
||||||
import Lottie from "lottie-react";
|
import Lottie from "lottie-react";
|
||||||
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
|
|
||||||
const categories: CatalogueCategory[] = ["trending", "recently_added"];
|
const categories: CatalogueCategory[] = ["trending", "recently_added"];
|
||||||
|
|
||||||
@ -20,8 +25,7 @@ export function Home() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [isLoadingRandomGame, setIsLoadingRandomGame] = useState(false);
|
const [randomGame, setRandomGame] = useState<Steam250Game | null>(null);
|
||||||
const randomGameObjectID = useRef<string | null>(null);
|
|
||||||
|
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
@ -56,24 +60,22 @@ export function Home() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const getRandomGame = useCallback(() => {
|
const getRandomGame = useCallback(() => {
|
||||||
setIsLoadingRandomGame(true);
|
window.electron.getRandomGame().then((game) => {
|
||||||
|
if (game) setRandomGame(game);
|
||||||
window.electron.getRandomGame().then((objectID) => {
|
|
||||||
if (objectID) {
|
|
||||||
randomGameObjectID.current = objectID;
|
|
||||||
setIsLoadingRandomGame(false);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleRandomizerClick = () => {
|
const handleRandomizerClick = () => {
|
||||||
const searchParams = new URLSearchParams({
|
if (randomGame) {
|
||||||
fromRandomizer: "1",
|
|
||||||
});
|
|
||||||
|
|
||||||
navigate(
|
navigate(
|
||||||
`/game/steam/${randomGameObjectID.current}?${searchParams.toString()}`
|
buildGameDetailsPath(
|
||||||
|
{ ...randomGame, shop: "steam" },
|
||||||
|
{
|
||||||
|
fromRandomizer: "1",
|
||||||
|
}
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -105,7 +107,7 @@ export function Home() {
|
|||||||
<Button
|
<Button
|
||||||
onClick={handleRandomizerClick}
|
onClick={handleRandomizerClick}
|
||||||
theme="outline"
|
theme="outline"
|
||||||
disabled={isLoadingRandomGame}
|
disabled={!randomGame}
|
||||||
>
|
>
|
||||||
<div style={{ width: 16, height: 16, position: "relative" }}>
|
<div style={{ width: 16, height: 16, position: "relative" }}>
|
||||||
<Lottie
|
<Lottie
|
||||||
@ -129,9 +131,7 @@ export function Home() {
|
|||||||
<GameCard
|
<GameCard
|
||||||
key={result.objectID}
|
key={result.objectID}
|
||||||
game={result}
|
game={result}
|
||||||
onClick={() =>
|
onClick={() => navigate(buildGameDetailsPath(result))}
|
||||||
navigate(`/game/${result.shop}/${result.objectID}`)
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</section>
|
</section>
|
||||||
|
@ -14,6 +14,7 @@ import { useEffect, useRef, useState } from "react";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||||
import * as styles from "./home.css";
|
import * as styles from "./home.css";
|
||||||
|
import { buildGameDetailsPath } from "@renderer/helpers";
|
||||||
|
|
||||||
export function SearchResults() {
|
export function SearchResults() {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
@ -30,7 +31,7 @@ export function SearchResults() {
|
|||||||
|
|
||||||
const handleGameClick = (game: CatalogueEntry) => {
|
const handleGameClick = (game: CatalogueEntry) => {
|
||||||
dispatch(clearSearch());
|
dispatch(clearSearch());
|
||||||
navigate(`/game/${game.shop}/${game.objectID}`);
|
navigate(buildGameDetailsPath(game));
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -57,7 +57,7 @@ export function SettingsRealDebrid({
|
|||||||
|
|
||||||
{form.useRealDebrid && (
|
{form.useRealDebrid && (
|
||||||
<TextField
|
<TextField
|
||||||
label={t("real_debrid_api_token_description")}
|
label={t("real_debrid_api_token_label")}
|
||||||
value={form.realDebridApiToken ?? ""}
|
value={form.realDebridApiToken ?? ""}
|
||||||
type="password"
|
type="password"
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
|
@ -69,7 +69,6 @@ export interface GameRepack {
|
|||||||
|
|
||||||
export type ShopDetails = SteamAppDetails & {
|
export type ShopDetails = SteamAppDetails & {
|
||||||
objectID: string;
|
objectID: string;
|
||||||
repacks: GameRepack[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface TorrentFile {
|
export interface TorrentFile {
|
||||||
@ -134,3 +133,8 @@ export interface HowLongToBeatCategory {
|
|||||||
duration: string;
|
duration: string;
|
||||||
accuracy: string;
|
accuracy: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Steam250Game {
|
||||||
|
title: string;
|
||||||
|
objectID: string;
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user