feat: adding bypass for region blocks

This commit is contained in:
Hydra 2024-05-12 23:43:00 +01:00
parent a2790190e6
commit 1c7911c531
No known key found for this signature in database
38 changed files with 942 additions and 979 deletions

View File

@ -2,7 +2,7 @@
<div align="center"> <div align="center">
[<img src="./resources/icon.png" width="144"/>](https://hydralauncher.site) [<img src="./resources/icon.png" width="144"/>](https://hydralauncher.site)
<h1 align="center">Hydra Launcher</h1> <h1 align="center">Hydra Launcher</h1>
@ -10,15 +10,15 @@
<strong>Hydra é um Launcher de Jogos com seu próprio cliente de bittorrent integrado e um wrapper autogerenciado para busca de repacks.</strong> <strong>Hydra é um Launcher de Jogos com seu próprio cliente de bittorrent integrado e um wrapper autogerenciado para busca de repacks.</strong>
</p> </p>
[![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions) [![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions)
[![release](https://img.shields.io/github/package-json/v/hydralauncher/hydra)](https://github.com/hydralauncher/hydra/releases) [![release](https://img.shields.io/github/package-json/v/hydralauncher/hydra)](https://github.com/hydralauncher/hydra/releases)
[![pt-BR](https://img.shields.io/badge/lang-pt--BR-green.svg)](README.pt-BR.md) [![pt-BR](https://img.shields.io/badge/lang-pt--BR-green.svg)](README.pt-BR.md)
[![en](https://img.shields.io/badge/lang-en-red.svg)](README.md) [![en](https://img.shields.io/badge/lang-en-red.svg)](README.md)
[![ru](https://img.shields.io/badge/lang-ru-yellow.svg)](README.ru.md) [![ru](https://img.shields.io/badge/lang-ru-yellow.svg)](README.ru.md)
[![uk-UA](https://img.shields.io/badge/lang-uk--UA-blue)](README.uk-UA.md) [![uk-UA](https://img.shields.io/badge/lang-uk--UA-blue)](README.uk-UA.md)
![Hydra Catalogue](./docs/screenshot.png) ![Hydra Catalogue](./docs/screenshot.png)
</div> </div>

View File

@ -2,7 +2,7 @@
<div align="center"> <div align="center">
[<img src="./resources/icon.png" width="144"/>](https://hydralauncher.site) [<img src="./resources/icon.png" width="144"/>](https://hydralauncher.site)
<h1 align="center">Hydra Launcher</h1> <h1 align="center">Hydra Launcher</h1>
@ -10,15 +10,15 @@
<strong>Hydra - это игровой лаунчер с собственным встроенным клиентом BitTorrent и самостоятельным scraper`ом для репаков.</strong> <strong>Hydra - это игровой лаунчер с собственным встроенным клиентом BitTorrent и самостоятельным scraper`ом для репаков.</strong>
</p> </p>
[![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions) [![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions)
[![release](https://img.shields.io/github/package-json/v/hydralauncher/hydra)](https://github.com/hydralauncher/hydra/releases) [![release](https://img.shields.io/github/package-json/v/hydralauncher/hydra)](https://github.com/hydralauncher/hydra/releases)
[![pt-BR](https://img.shields.io/badge/lang-pt--BR-green.svg)](README.pt-BR.md) [![pt-BR](https://img.shields.io/badge/lang-pt--BR-green.svg)](README.pt-BR.md)
[![en](https://img.shields.io/badge/lang-en-red.svg)](README.md) [![en](https://img.shields.io/badge/lang-en-red.svg)](README.md)
[![ru](https://img.shields.io/badge/lang-ru-yellow.svg)](README.ru.md) [![ru](https://img.shields.io/badge/lang-ru-yellow.svg)](README.ru.md)
[![uk-UA](https://img.shields.io/badge/lang-uk--UA-blue)](README.uk-UA.md) [![uk-UA](https://img.shields.io/badge/lang-uk--UA-blue)](README.uk-UA.md)
![Hydra Catalogue](./docs/screenshot.png) ![Hydra Catalogue](./docs/screenshot.png)
</div> </div>
@ -67,8 +67,8 @@
Чтобы установить, выполните следующие шаги: Чтобы установить, выполните следующие шаги:
1. Скачайте последнюю версию Hydra с [страницы релизов](https://github.com/hydralauncher/hydra/releases/latest). 1. Скачайте последнюю версию Hydra с [страницы релизов](https://github.com/hydralauncher/hydra/releases/latest).
- Загрузите только .exe, если хотите установить Hydra на Windows. - Загрузите только .exe, если хотите установить Hydra на Windows.
- Загрузите .deb или .rpm или .zip, если хотите установить Hydra на Linux (в зависимости от вашего дистрибутива Linux). - Загрузите .deb или .rpm или .zip, если хотите установить Hydra на Linux (в зависимости от вашего дистрибутива Linux).
2. Запустите скачанный файл. 2. Запустите скачанный файл.
3. Наслаждайтесь Hydra! 3. Наслаждайтесь Hydra!

View File

@ -2,7 +2,7 @@
<div align="center"> <div align="center">
[<img src="./resources/icon.png" width="144"/>](https://hydralauncher.site) [<img src="./resources/icon.png" width="144"/>](https://hydralauncher.site)
<h1 align="center">Hydra Launcher</h1> <h1 align="center">Hydra Launcher</h1>
@ -10,15 +10,15 @@
<strong>Hydra - це ігровий лаунчер з власним вбудованим bittorrent-клієнтом і самокерованим збирачем репаків.</strong> <strong>Hydra - це ігровий лаунчер з власним вбудованим bittorrent-клієнтом і самокерованим збирачем репаків.</strong>
</p> </p>
[![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions) [![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions)
[![release](https://img.shields.io/github/package-json/v/hydralauncher/hydra)](https://github.com/hydralauncher/hydra/releases) [![release](https://img.shields.io/github/package-json/v/hydralauncher/hydra)](https://github.com/hydralauncher/hydra/releases)
[![pt-BR](https://img.shields.io/badge/lang-pt--BR-green.svg)](README.pt-BR.md) [![pt-BR](https://img.shields.io/badge/lang-pt--BR-green.svg)](README.pt-BR.md)
[![en](https://img.shields.io/badge/lang-en-red.svg)](README.md) [![en](https://img.shields.io/badge/lang-en-red.svg)](README.md)
[![ru](https://img.shields.io/badge/lang-ru-yellow.svg)](README.ru.md) [![ru](https://img.shields.io/badge/lang-ru-yellow.svg)](README.ru.md)
[![uk-UA](https://img.shields.io/badge/lang-uk--UA-blue)](README.uk-UA.md) [![uk-UA](https://img.shields.io/badge/lang-uk--UA-blue)](README.uk-UA.md)
![Hydra Catalogue](./docs/screenshot.png) ![Hydra Catalogue](./docs/screenshot.png)
</div> </div>

View File

View File

@ -96,7 +96,8 @@
"dont_show_it_again": "Don't show it again", "dont_show_it_again": "Don't show it again",
"copy_to_clipboard": "Copy", "copy_to_clipboard": "Copy",
"copied_to_clipboard": "Copied", "copied_to_clipboard": "Copied",
"got_it": "Got it" "got_it": "Got it",
"no_shop_details": "Could not retrieve shop details."
}, },
"activation": { "activation": {
"title": "Activate Hydra", "title": "Activate Hydra",

View File

@ -1,175 +1,174 @@
{ {
"home": { "home": {
"featured": "Uitgelicht", "featured": "Uitgelicht",
"recently_added": "Recent Toegevoegd", "recently_added": "Recent Toegevoegd",
"trending": "Trending", "trending": "Trending",
"surprise_me": "Verrasing", "surprise_me": "Verrasing",
"no_results": "Geen resultaten gevonden" "no_results": "Geen resultaten gevonden"
}, },
"sidebar": { "sidebar": {
"catalogue": "catalogus", "catalogue": "catalogus",
"downloads": "Downloads", "downloads": "Downloads",
"settings": "Instellingen", "settings": "Instellingen",
"my_library": "Mijn Bibliotheek", "my_library": "Mijn Bibliotheek",
"downloading_metadata": "{{title}} (Downloading metadata…)", "downloading_metadata": "{{title}} (Downloading metadata…)",
"checking_files": "{{title}} ({{percentage}} - Folders checken…)", "checking_files": "{{title}} ({{percentage}} - Folders checken…)",
"paused": "{{title}} (Gepauzeerd)", "paused": "{{title}} (Gepauzeerd)",
"downloading": "{{title}} ({{percentage}} - Downloading…)", "downloading": "{{title}} ({{percentage}} - Downloading…)",
"filter": "Filter Bibliotheek", "filter": "Filter Bibliotheek",
"follow_us": "volg ons", "follow_us": "volg ons",
"home": "Home", "home": "Home",
"discord": "Volg onze Discord", "discord": "Volg onze Discord",
"telegram": "Volg onze Telegram", "telegram": "Volg onze Telegram",
"x": "Volg ons op X", "x": "Volg ons op X",
"github": "Contribute op GitHub" "github": "Contribute op GitHub"
}, },
"header": { "header": {
"search": "Zoek spellen", "search": "Zoek spellen",
"home": "Home", "home": "Home",
"catalogue": "Bibliotheek", "catalogue": "Bibliotheek",
"downloads": "Downloads", "downloads": "Downloads",
"search_results": "Zoek resultaten", "search_results": "Zoek resultaten",
"settings": "Instellingen" "settings": "Instellingen"
}, },
"bottom_panel": { "bottom_panel": {
"no_downloads_in_progress": "Geen Downloads bezig", "no_downloads_in_progress": "Geen Downloads bezig",
"downloading_metadata": "Downloading {{title}} metadata…", "downloading_metadata": "Downloading {{title}} metadata…",
"checking_files": "Checking {{title}} files… ({{percentage}} complete)", "checking_files": "Checking {{title}} files… ({{percentage}} complete)",
"downloading": "Downloading {{title}}… ({{percentage}} complete) - Conclusion {{eta}} - {{speed}}" "downloading": "Downloading {{title}}… ({{percentage}} complete) - Conclusion {{eta}} - {{speed}}"
}, },
"catalogue": { "catalogue": {
"next_page": "Volgende Pagina", "next_page": "Volgende Pagina",
"previous_page": "Vorige Pagina" "previous_page": "Vorige Pagina"
}, },
"game_details": { "game_details": {
"open_download_options": "Open download Instellingen", "open_download_options": "Open download Instellingen",
"download_options_zero": "Geen download Instellingen", "download_options_zero": "Geen download Instellingen",
"download_options_one": "{{count}} download Instellingen", "download_options_one": "{{count}} download Instellingen",
"download_options_other": "{{count}} download Instellingen", "download_options_other": "{{count}} download Instellingen",
"updated_at": "Geupdate {{updated_at}}", "updated_at": "Geupdate {{updated_at}}",
"install": "Instaleer", "install": "Instaleer",
"resume": "Verder gaan", "resume": "Verder gaan",
"pause": "Pauze", "pause": "Pauze",
"cancel": "Stoppen", "cancel": "Stoppen",
"remove": "Verwijderen", "remove": "Verwijderen",
"remove_from_list": "Verwijdere van lijst", "remove_from_list": "Verwijdere van lijst",
"space_left_on_disk": "{{space}} Over op schijf", "space_left_on_disk": "{{space}} Over op schijf",
"eta": "Conclusie {{eta}}", "eta": "Conclusie {{eta}}",
"downloading_metadata": "Downloading metadata…", "downloading_metadata": "Downloading metadata…",
"checking_files": "Files nakijken…", "checking_files": "Files nakijken…",
"filter": "Filter repacks", "filter": "Filter repacks",
"requirements": "Systeem vereisten", "requirements": "Systeem vereisten",
"minimum": "Minimaal", "minimum": "Minimaal",
"recommended": "Aanbevolen", "recommended": "Aanbevolen",
"no_minimum_requirements": "{{title}} biedt geen informatie over de minimale vereisten", "no_minimum_requirements": "{{title}} biedt geen informatie over de minimale vereisten",
"no_recommended_requirements": "{{title}} biedt geen informatie over aanbevolen vereisten", "no_recommended_requirements": "{{title}} biedt geen informatie over aanbevolen vereisten",
"paused_progress": "{{progress}} (Paused)", "paused_progress": "{{progress}} (Paused)",
"release_date": "Uitgebracht op {{date}}", "release_date": "Uitgebracht op {{date}}",
"publisher": "Gepubliceerd door {{publisher}}", "publisher": "Gepubliceerd door {{publisher}}",
"copy_link_to_clipboard": "Kopieer link", "copy_link_to_clipboard": "Kopieer link",
"copied_link_to_clipboard": "Link Gekopieerd", "copied_link_to_clipboard": "Link Gekopieerd",
"hours": "uren", "hours": "uren",
"minutes": "minuten", "minutes": "minuten",
"amount_hours": "{{amount}} uren", "amount_hours": "{{amount}} uren",
"amount_minutes": "{{amount}} minuten", "amount_minutes": "{{amount}} minuten",
"accuracy": "{{accuracy}}% nauwkeurigheid", "accuracy": "{{accuracy}}% nauwkeurigheid",
"add_to_library": "Toevoegen aan bibliotheek", "add_to_library": "Toevoegen aan bibliotheek",
"remove_from_library": "Verwijderen uit bibliotheek", "remove_from_library": "Verwijderen uit bibliotheek",
"no_downloads": "Geen downloads beschikbaar", "no_downloads": "Geen downloads beschikbaar",
"play_time": "Voor gespeeld {{amount}}", "play_time": "Voor gespeeld {{amount}}",
"last_time_played": "Laatst gespeeld {{period}}", "last_time_played": "Laatst gespeeld {{period}}",
"not_played_yet": "Je hebt nog niet gespeeld {{title}}", "not_played_yet": "Je hebt nog niet gespeeld {{title}}",
"next_suggestion": "Volgende suggestie", "next_suggestion": "Volgende suggestie",
"play": "Speel", "play": "Speel",
"deleting": "Installatieprogramma verwijderen…", "deleting": "Installatieprogramma verwijderen…",
"close": "Sluiten", "close": "Sluiten",
"playing_now": "Speel nu", "playing_now": "Speel nu",
"change": "Verander", "change": "Verander",
"repacks_modal_description": "Kies de herverpakking die u wilt downloaden", "repacks_modal_description": "Kies de herverpakking die u wilt downloaden",
"downloads_path": "Downloads path", "downloads_path": "Downloads path",
"select_folder_hint": "Om de standaardmap te wijzigen, gaat u naar <0>instellingen</0>", "select_folder_hint": "Om de standaardmap te wijzigen, gaat u naar <0>instellingen</0>",
"download_now": "Download nu", "download_now": "Download nu",
"installation_instructions": "Installatie instructies", "installation_instructions": "Installatie instructies",
"installation_instructions_description": "Er zijn extra stappen vereist om deze game te installeren", "installation_instructions_description": "Er zijn extra stappen vereist om deze game te installeren",
"online_fix_instruction": "OnlineFix-spellen vereisen dat een wachtwoord wordt uitgepakt. Gebruik indien nodig het volgende wachtwoord:", "online_fix_instruction": "OnlineFix-spellen vereisen dat een wachtwoord wordt uitgepakt. Gebruik indien nodig het volgende wachtwoord:",
"dodi_installation_instruction": "Wanneer u het DODI-installatieprogramma opent, drukt u op de toets omhoog <0 /> op uw toetsenbord om het installatieproces te starten:", "dodi_installation_instruction": "Wanneer u het DODI-installatieprogramma opent, drukt u op de toets omhoog <0 /> op uw toetsenbord om het installatieproces te starten:",
"dont_show_it_again": "Laat het niet meer zien", "dont_show_it_again": "Laat het niet meer zien",
"copy_to_clipboard": "Kopiëren", "copy_to_clipboard": "Kopiëren",
"copied_to_clipboard": "Gekopieerd", "copied_to_clipboard": "Gekopieerd",
"got_it": "Begrepen" "got_it": "Begrepen"
}, },
"activation": { "activation": {
"title": "Activeer Hydra", "title": "Activeer Hydra",
"installation_id": "Installatie-ID:", "installation_id": "Installatie-ID:",
"enter_activation_code": "Voer uw activatiecode in", "enter_activation_code": "Voer uw activatiecode in",
"message": "Als je niet weet waar je dit moet vragen, dan moet je dit niet hebben.", "message": "Als je niet weet waar je dit moet vragen, dan moet je dit niet hebben.",
"activate": "Activeren", "activate": "Activeren",
"loading": "Bezig met laden…" "loading": "Bezig met laden…"
}, },
"downloads": { "downloads": {
"resume": "Hervatten", "resume": "Hervatten",
"pause": "Pauze", "pause": "Pauze",
"eta": "Conclusie{{eta}}", "eta": "Conclusie{{eta}}",
"paused": "Gepauzeerd", "paused": "Gepauzeerd",
"verifying": "Verifiëren…", "verifying": "Verifiëren…",
"completed_at": "Voltooid binnen {{date}}", "completed_at": "Voltooid binnen {{date}}",
"completed": "Voltooid", "completed": "Voltooid",
"cancelled": "Geannuleerd", "cancelled": "Geannuleerd",
"download_again": "Opnieuw downloaden", "download_again": "Opnieuw downloaden",
"cancel": "Annuleren", "cancel": "Annuleren",
"filter": "Filter gedownloade games", "filter": "Filter gedownloade games",
"remove": "Verwijderen", "remove": "Verwijderen",
"downloading_metadata": "Metagegevens downloaden", "downloading_metadata": "Metagegevens downloaden",
"checking_files": "Bestanden controleren", "checking_files": "Bestanden controleren",
"starting_download": "download starten", "starting_download": "download starten",
"deleting": "Installatieprogramma verwijderen…", "deleting": "Installatieprogramma verwijderen…",
"delete": "Installatieprogramma verwijderen", "delete": "Installatieprogramma verwijderen",
"remove_from_list": "Verwijderen", "remove_from_list": "Verwijderen",
"delete_modal_title": "Weet je het zeker?", "delete_modal_title": "Weet je het zeker?",
"delete_modal_description": "Hiermee worden alle installatiebestanden van uw computer verwijderd", "delete_modal_description": "Hiermee worden alle installatiebestanden van uw computer verwijderd",
"install": "Installeren", "install": "Installeren",
"real_debrid": "Real Debrid", "real_debrid": "Real Debrid",
"torrent": "Torrent" "torrent": "Torrent"
}, },
"settings": { "settings": {
"downloads_path": "Downloadpad", "downloads_path": "Downloadpad",
"change": "Update", "change": "Update",
"notifications": "Meldingen", "notifications": "Meldingen",
"enable_download_notifications": "Wanneer een download voltooid is", "enable_download_notifications": "Wanneer een download voltooid is",
"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_description": "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",
"behavior": "Gedrag", "behavior": "Gedrag",
"enable_real_debrid": "Enable Real Debrid", "enable_real_debrid": "Enable Real Debrid",
"real_debrid": "Real Debrid", "real_debrid": "Real Debrid",
"real_debrid_api_token_hint": "U kunt uw API-sleutel <0>hier</0> verkrijgen.", "real_debrid_api_token_hint": "U kunt uw API-sleutel <0>hier</0> verkrijgen.",
"save_changes": "Wijzigingen opslaan" "save_changes": "Wijzigingen opslaan"
}, },
"notifications": { "notifications": {
"download_complete": "Download compleet", "download_complete": "Download compleet",
"game_ready_to_install": "{{title}} is klaar om te installeren", "game_ready_to_install": "{{title}} is klaar om te installeren",
"repack_list_updated": "Herpaklijst bijgewerkt", "repack_list_updated": "Herpaklijst bijgewerkt",
"repack_count_one": "{{count}} herverpakking toegevoegd", "repack_count_one": "{{count}} herverpakking toegevoegd",
"repack_count_other": "{{count}} herverpakkingen toegevoegd" "repack_count_other": "{{count}} herverpakkingen toegevoegd"
}, },
"system_tray": { "system_tray": {
"open": "Open Hydra", "open": "Open Hydra",
"quit": "Verlaten" "quit": "Verlaten"
}, },
"game_card": { "game_card": {
"no_downloads": "Geen downloads beschikbaar" "no_downloads": "Geen downloads beschikbaar"
}, },
"binary_not_found_modal": { "binary_not_found_modal": {
"title": "Programma's niet geïnstalleerd", "title": "Programma's niet geïnstalleerd",
"description": "Er zijn geen uitvoerbare bestanden van Wine of Lutris gevonden op uw systeem", "description": "Er zijn geen uitvoerbare bestanden van Wine of Lutris gevonden op uw systeem",
"instructions": "Controleer de juiste manier om ze op je Linux-distro te installeren, zodat de game normaal kan werken" "instructions": "Controleer de juiste manier om ze op je Linux-distro te installeren, zodat de game normaal kan werken"
}, },
"modal": { "modal": {
"close": "Knop Sluiten" "close": "Knop Sluiten"
}
} }
}

View File

@ -1,165 +1,164 @@
{ {
"home": { "home": {
"featured": "Öne çıkan", "featured": "Öne çıkan",
"recently_added": "Son eklenen", "recently_added": "Son eklenen",
"trending": "Popüler", "trending": "Popüler",
"surprise_me": "Şaşırt beni", "surprise_me": "Şaşırt beni",
"no_results": "Sonuç bulunamadı" "no_results": "Sonuç bulunamadı"
}, },
"sidebar": { "sidebar": {
"catalogue": "Katalog", "catalogue": "Katalog",
"downloads": "İndirmeler", "downloads": "İndirmeler",
"settings": "Ayarlar", "settings": "Ayarlar",
"my_library": "Kütüphane", "my_library": "Kütüphane",
"downloading_metadata": "{{title}} (Metadata indiriliyor…)", "downloading_metadata": "{{title}} (Metadata indiriliyor…)",
"checking_files": "{{title}} ({{percentage}} - Dosyalar kontrol ediliyor…)", "checking_files": "{{title}} ({{percentage}} - Dosyalar kontrol ediliyor…)",
"paused": "{{title}} (Duraklatıldı)", "paused": "{{title}} (Duraklatıldı)",
"downloading": "{{title}} ({{percentage}} - İndiriliyor…)", "downloading": "{{title}} ({{percentage}} - İndiriliyor…)",
"filter": "Kütüphaneyi filtrele", "filter": "Kütüphaneyi filtrele",
"follow_us": "Bizi takip et", "follow_us": "Bizi takip et",
"home": "Ana menü", "home": "Ana menü",
"discord": "Discord'umuza katıl", "discord": "Discord'umuza katıl",
"telegram": "Telegram'umuza katıl", "telegram": "Telegram'umuza katıl",
"x": "X'te bizi takip et", "x": "X'te bizi takip et",
"github": "GitHub'da bize katkı yap" "github": "GitHub'da bize katkı yap"
}, },
"header": { "header": {
"search": "Ara", "search": "Ara",
"home": "Ana menü", "home": "Ana menü",
"catalogue": "Katalog", "catalogue": "Katalog",
"downloads": "İndirmeler", "downloads": "İndirmeler",
"search_results": "Arama sonuçları", "search_results": "Arama sonuçları",
"settings": "Ayarlar" "settings": "Ayarlar"
}, },
"bottom_panel": { "bottom_panel": {
"no_downloads_in_progress": "İndirilen bir şey yok", "no_downloads_in_progress": "İndirilen bir şey yok",
"downloading_metadata": "{{title}} metadatası indiriliyor…", "downloading_metadata": "{{title}} metadatası indiriliyor…",
"checking_files": "{{title}} dosyaları kontrol ediliyor… ({{percentage}} tamamlandı)", "checking_files": "{{title}} dosyaları kontrol ediliyor… ({{percentage}} tamamlandı)",
"downloading": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Bitiş {{eta}} - {{speed}}" "downloading": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Bitiş {{eta}} - {{speed}}"
}, },
"catalogue": { "catalogue": {
"next_page": "Sonraki sayfa", "next_page": "Sonraki sayfa",
"previous_page": "Önceki sayfa" "previous_page": "Önceki sayfa"
}, },
"game_details": { "game_details": {
"open_download_options": "İndirme seçeneklerini aç", "open_download_options": "İndirme seçeneklerini aç",
"download_options_zero": "İndirme seçeneği yok", "download_options_zero": "İndirme seçeneği yok",
"download_options_one": "{{count}} indirme seçeneği", "download_options_one": "{{count}} indirme seçeneği",
"download_options_other": "{{count}} indirme seçeneği", "download_options_other": "{{count}} indirme seçeneği",
"updated_at": "{{updated_at}} güncellendi", "updated_at": "{{updated_at}} güncellendi",
"install": "İndir", "install": "İndir",
"resume": "Devam et", "resume": "Devam et",
"pause": "Duraklat", "pause": "Duraklat",
"cancel": "İptal et", "cancel": "İptal et",
"remove": "Sil", "remove": "Sil",
"remove_from_list": "Sil", "remove_from_list": "Sil",
"space_left_on_disk": "Diskte {{space}} yer kaldı", "space_left_on_disk": "Diskte {{space}} yer kaldı",
"eta": "Bitiş {{eta}}", "eta": "Bitiş {{eta}}",
"downloading_metadata": "Metadata indiriliyor…", "downloading_metadata": "Metadata indiriliyor…",
"checking_files": "Dosyalar kontrol ediliyor…", "checking_files": "Dosyalar kontrol ediliyor…",
"filter": "Repackleri filtrele", "filter": "Repackleri filtrele",
"requirements": "Sistem gereksinimleri", "requirements": "Sistem gereksinimleri",
"minimum": "Minimum", "minimum": "Minimum",
"recommended": "Önerilen", "recommended": "Önerilen",
"no_minimum_requirements": "{{title}} minimum sistem gereksinim bilgilerini karşılamıyor", "no_minimum_requirements": "{{title}} minimum sistem gereksinim bilgilerini karşılamıyor",
"no_recommended_requirements": "{{title}} önerilen sistem gereksinim bilgilerini karşılamıyor", "no_recommended_requirements": "{{title}} önerilen sistem gereksinim bilgilerini karşılamıyor",
"paused_progress": "{{progress}} (Duraklatıldı)", "paused_progress": "{{progress}} (Duraklatıldı)",
"release_date": "{{date}} tarihinde çıktı", "release_date": "{{date}} tarihinde çıktı",
"publisher": "{{publisher}} tarihinde yayınlandı", "publisher": "{{publisher}} tarihinde yayınlandı",
"copy_link_to_clipboard": "Link'i kopyala", "copy_link_to_clipboard": "Link'i kopyala",
"copied_link_to_clipboard": "Link kopyalandı", "copied_link_to_clipboard": "Link kopyalandı",
"hours": "saatler", "hours": "saatler",
"minutes": "dakikalar", "minutes": "dakikalar",
"amount_hours": "{{amount}} saat", "amount_hours": "{{amount}} saat",
"amount_minutes": "{{amount}} dakika", "amount_minutes": "{{amount}} dakika",
"accuracy": "%{{accuracy}} doğruluk", "accuracy": "%{{accuracy}} doğruluk",
"add_to_library": "Kütüphaneye ekle", "add_to_library": "Kütüphaneye ekle",
"remove_from_library": "Kütüphaneden kaldır", "remove_from_library": "Kütüphaneden kaldır",
"no_downloads": "İndirme yok", "no_downloads": "İndirme yok",
"play_time": "{{amount}} oynandı", "play_time": "{{amount}} oynandı",
"last_time_played": "Son oynanan {{period}}", "last_time_played": "Son oynanan {{period}}",
"not_played_yet": "Bu {{title}} hiç oynanmadı", "not_played_yet": "Bu {{title}} hiç oynanmadı",
"next_suggestion": "Sıradaki öneri", "next_suggestion": "Sıradaki öneri",
"play": "Oyna", "play": "Oyna",
"deleting": "Installer siliniyor…", "deleting": "Installer siliniyor…",
"close": "Kapat", "close": "Kapat",
"playing_now": "Şimdi oynanıyor", "playing_now": "Şimdi oynanıyor",
"change": "Değiştir", "change": "Değiştir",
"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", "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",
"online_fix_instruction": "OnlineFix oyunlarını ayıklamak için parola gerekiyor. Gerekli olduğunda bu parolayı kullanın:", "online_fix_instruction": "OnlineFix oyunlarını ayıklamak için parola gerekiyor. Gerekli olduğunda bu parolayı kullanın:",
"dodi_installation_instruction": "Dodi installerını açtığınızda, kurulumu başlatmak için bu tuşa basın <0 />:", "dodi_installation_instruction": "Dodi installerını açtığınızda, kurulumu başlatmak için bu tuşa basın <0 />:",
"dont_show_it_again": "Tekrar gösterme", "dont_show_it_again": "Tekrar gösterme",
"copy_to_clipboard": "Kopyala", "copy_to_clipboard": "Kopyala",
"copied_to_clipboard": "Kopyalandı", "copied_to_clipboard": "Kopyalandı",
"got_it": "Tamam" "got_it": "Tamam"
}, },
"activation": { "activation": {
"title": "Hydra'yı aktif et", "title": "Hydra'yı aktif et",
"installation_id": "Kurulum ID'si:", "installation_id": "Kurulum ID'si:",
"enter_activation_code": "Aktifleştirme kodunuzu girin", "enter_activation_code": "Aktifleştirme kodunuzu girin",
"message": "Bunu nerede soracağınızı bilmiyorsanız, buna sahip olmamanız gerekiyor.", "message": "Bunu nerede soracağınızı bilmiyorsanız, buna sahip olmamanız gerekiyor.",
"activate": "Aktif et", "activate": "Aktif et",
"loading": "Yükleniyor…" "loading": "Yükleniyor…"
}, },
"downloads": { "downloads": {
"resume": "Devam et", "resume": "Devam et",
"pause": "Duraklat", "pause": "Duraklat",
"eta": "Bitiş {{eta}}", "eta": "Bitiş {{eta}}",
"paused": "Duraklatıldı", "paused": "Duraklatıldı",
"verifying": "Doğrulanıyor…", "verifying": "Doğrulanıyor…",
"completed_at": "{{date}} tarihinde tamamlanacak", "completed_at": "{{date}} tarihinde tamamlanacak",
"completed": "Tamamlandı", "completed": "Tamamlandı",
"cancelled": "İptal edildi", "cancelled": "İptal edildi",
"download_again": "Tekrar indir", "download_again": "Tekrar indir",
"cancel": "İptal et", "cancel": "İptal et",
"filter": "Yüklü oyunları filtrele", "filter": "Yüklü oyunları filtrele",
"remove": "Kaldır", "remove": "Kaldır",
"downloading_metadata": "Metadata indiriliyor…", "downloading_metadata": "Metadata indiriliyor…",
"checking_files": "Dosyalar kontrol ediliyor…", "checking_files": "Dosyalar kontrol ediliyor…",
"starting_download": "İndirme başlatılıyor…", "starting_download": "İndirme başlatılıyor…",
"deleting": "Installer siliniyor…", "deleting": "Installer siliniyor…",
"delete": "Installer'ı sil", "delete": "Installer'ı sil",
"remove_from_list": "Kaldır", "remove_from_list": "Kaldır",
"delete_modal_title": "Emin misiniz?", "delete_modal_title": "Emin misiniz?",
"delete_modal_description": "Bu bilgisayarınızdan tüm kurulum dosyalarını silecek", "delete_modal_description": "Bu bilgisayarınızdan tüm kurulum dosyalarını silecek",
"install": "Kur" "install": "Kur"
}, },
"settings": { "settings": {
"downloads_path": "İndirme yolu", "downloads_path": "İndirme yolu",
"change": "Güncelle", "change": "Güncelle",
"notifications": "Bildirimler", "notifications": "Bildirimler",
"enable_download_notifications": "Bir indirme bittiğinde", "enable_download_notifications": "Bir indirme bittiğinde",
"enable_repack_list_notifications": "Yeni bir repack eklendiğinde", "enable_repack_list_notifications": "Yeni bir repack eklendiğinde",
"telemetry": "Telemetri", "telemetry": "Telemetri",
"telemetry_description": "Anonim kullanım istatistiklerini aktifleştir" "telemetry_description": "Anonim kullanım istatistiklerini aktifleştir"
}, },
"notifications": { "notifications": {
"download_complete": "İndirme tamamlandı", "download_complete": "İndirme tamamlandı",
"game_ready_to_install": "{{title}} kuruluma hazır", "game_ready_to_install": "{{title}} kuruluma hazır",
"repack_list_updated": "Repack listesi güncellendi", "repack_list_updated": "Repack listesi güncellendi",
"repack_count_one": "{{count}} yeni repack eklendi", "repack_count_one": "{{count}} yeni repack eklendi",
"repack_count_other": "{{count}} yeni repack eklendi" "repack_count_other": "{{count}} yeni repack eklendi"
}, },
"system_tray": { "system_tray": {
"open": "Hydra'yı aç", "open": "Hydra'yı aç",
"quit": ık" "quit": ık"
}, },
"game_card": { "game_card": {
"no_downloads": "İndirme mevcut değil" "no_downloads": "İndirme mevcut değil"
}, },
"binary_not_found_modal": { "binary_not_found_modal": {
"title": "Programlar yüklü değil", "title": "Programlar yüklü değil",
"description": "Sisteminizde Wine veya Lutris çalıştırılabiliri bulunamadı", "description": "Sisteminizde Wine veya Lutris çalıştırılabiliri bulunamadı",
"instructions": "Oyunları düzgün şekilde çalıştırmak için Linux distronuza bunlardan birini nasıl yükleyebileceğinize bakın" "instructions": "Oyunları düzgün şekilde çalıştırmak için Linux distronuza bunlardan birini nasıl yükleyebileceğinize bakın"
}, },
"modal": { "modal": {
"close": "Kapat tuşu" "close": "Kapat tuşu"
}
} }
}

View File

@ -1,167 +1,167 @@
{ {
"home": { "home": {
"featured": "Рекомендоване", "featured": "Рекомендоване",
"recently_added": "Нове", "recently_added": "Нове",
"trending": "У тренді", "trending": "У тренді",
"surprise_me": "Здивуй мене", "surprise_me": "Здивуй мене",
"no_results": "Результатів не знайдено" "no_results": "Результатів не знайдено"
}, },
"sidebar": { "sidebar": {
"catalogue": "Каталог", "catalogue": "Каталог",
"downloads": "Завантаження", "downloads": "Завантаження",
"settings": "Налаштування", "settings": "Налаштування",
"my_library": "Бібліотека", "my_library": "Бібліотека",
"downloading_metadata": "{{title}} (Завантаження метаданих…)", "downloading_metadata": "{{title}} (Завантаження метаданих…)",
"checking_files": "{{title}} ({{percentage}} - Перевірка файлів…)", "checking_files": "{{title}} ({{percentage}} - Перевірка файлів…)",
"paused": "{{title}} (Призупинено)", "paused": "{{title}} (Призупинено)",
"downloading": "{{title}} ({{percentage}} - Завантаження…)", "downloading": "{{title}} ({{percentage}} - Завантаження…)",
"filter": "Фільтр бібліотеки", "filter": "Фільтр бібліотеки",
"follow_us": "Підписуйтесь на нас", "follow_us": "Підписуйтесь на нас",
"home": "Головна", "home": "Головна",
"discord": "Приєднуйтесь до Discord", "discord": "Приєднуйтесь до Discord",
"telegram": "Приєднуйтесь до Telegram", "telegram": "Приєднуйтесь до Telegram",
"x": "Підписуйтесь на X", "x": "Підписуйтесь на X",
"github": "Зробіть свій внесок на GitHub" "github": "Зробіть свій внесок на GitHub"
}, },
"header": { "header": {
"search": "Пошук", "search": "Пошук",
"home": "Головна", "home": "Головна",
"catalogue": "Каталог", "catalogue": "Каталог",
"downloads": "Завантаження", "downloads": "Завантаження",
"search_results": "Результати пошуку", "search_results": "Результати пошуку",
"settings": "Налаштування" "settings": "Налаштування"
}, },
"bottom_panel": { "bottom_panel": {
"no_downloads_in_progress": "Немає активних завантажень", "no_downloads_in_progress": "Немає активних завантажень",
"downloading_metadata": "Завантаження метаданих {{title}}…", "downloading_metadata": "Завантаження метаданих {{title}}…",
"checking_files": "Перевірка файлів {{title}}… ({{percentage}} завершено)", "checking_files": "Перевірка файлів {{title}}… ({{percentage}} завершено)",
"downloading": "Завантаження {{title}}… ({{percentage}} завершено) - Закінчення {{eta}} - {{speed}}" "downloading": "Завантаження {{title}}… ({{percentage}} завершено) - Закінчення {{eta}} - {{speed}}"
}, },
"catalogue": { "catalogue": {
"next_page": "Наступна сторінка", "next_page": "Наступна сторінка",
"previous_page": "Попередня сторінка" "previous_page": "Попередня сторінка"
}, },
"game_details": { "game_details": {
"open_download_options": "Відкрити варіанти завантаження", "open_download_options": "Відкрити варіанти завантаження",
"download_options_zero": "Немає варіантів завантаження", "download_options_zero": "Немає варіантів завантаження",
"download_options_one": "{{count}} варіант завантаження", "download_options_one": "{{count}} варіант завантаження",
"download_options_other": "{{count}} варіантів завантаження", "download_options_other": "{{count}} варіантів завантаження",
"updated_at": "Оновлено {{updated_at}}", "updated_at": "Оновлено {{updated_at}}",
"install": "Встановити", "install": "Встановити",
"resume": "Відновити", "resume": "Відновити",
"pause": "Призупинити", "pause": "Призупинити",
"cancel": "Скасувати", "cancel": "Скасувати",
"remove": "Видалити", "remove": "Видалити",
"remove_from_list": "Видалити", "remove_from_list": "Видалити",
"space_left_on_disk": "{{space}} вільно на диску", "space_left_on_disk": "{{space}} вільно на диску",
"eta": "Закінчення {{eta}}", "eta": "Закінчення {{eta}}",
"downloading_metadata": "Завантаження метаданих…", "downloading_metadata": "Завантаження метаданих…",
"checking_files": "Перевірка файлів…", "checking_files": "Перевірка файлів…",
"filter": "Фільтр репаків", "filter": "Фільтр репаків",
"requirements": "Системні вимоги", "requirements": "Системні вимоги",
"minimum": "Мінімальні", "minimum": "Мінімальні",
"recommended": "Рекомендовані", "recommended": "Рекомендовані",
"no_minimum_requirements": "Для {{title}} не вказані мінімальні вимоги", "no_minimum_requirements": "Для {{title}} не вказані мінімальні вимоги",
"no_recommended_requirements": "Для {{title}} не вказані рекомендовані вимоги", "no_recommended_requirements": "Для {{title}} не вказані рекомендовані вимоги",
"paused_progress": "{{progress}} (Призупинено)", "paused_progress": "{{progress}} (Призупинено)",
"release_date": "Випущено {{date}}", "release_date": "Випущено {{date}}",
"publisher": "Видавець {{publisher}}", "publisher": "Видавець {{publisher}}",
"copy_link_to_clipboard": "Скопіювати посилання", "copy_link_to_clipboard": "Скопіювати посилання",
"copied_link_to_clipboard": "Посилання скопійовано", "copied_link_to_clipboard": "Посилання скопійовано",
"hours": "годин", "hours": "годин",
"minutes": "хвилин", "minutes": "хвилин",
"amount_hours": "{{amount}} годин", "amount_hours": "{{amount}} годин",
"amount_minutes": "{{amount}} хвилин", "amount_minutes": "{{amount}} хвилин",
"accuracy": "{{accuracy}}% точність", "accuracy": "{{accuracy}}% точність",
"add_to_library": "Додати до бібліотеки", "add_to_library": "Додати до бібліотеки",
"remove_from_library": "Видалити з бібліотеки", "remove_from_library": "Видалити з бібліотеки",
"no_downloads": "Немає доступних завантажень", "no_downloads": "Немає доступних завантажень",
"play_time": "Час гри: {{amount}}", "play_time": "Час гри: {{amount}}",
"last_time_played": "Востаннє зіграно: {{period}}", "last_time_played": "Востаннє зіграно: {{period}}",
"not_played_yet": "Ви ще не грали в {{title}}", "not_played_yet": "Ви ще не грали в {{title}}",
"next_suggestion": "Наступна пропозиція", "next_suggestion": "Наступна пропозиція",
"play": "Грати", "play": "Грати",
"deleting": "Видалення інсталятора…", "deleting": "Видалення інсталятора…",
"close": "Закрити", "close": "Закрити",
"playing_now": "Поточна гра", "playing_now": "Поточна гра",
"change": "Змінити", "change": "Змінити",
"repacks_modal_description": "Виберіть репак, який хочете завантажити", "repacks_modal_description": "Виберіть репак, який хочете завантажити",
"downloads_path": "Шлях завантажень", "downloads_path": "Шлях завантажень",
"select_folder_hint": "Щоб змінити теку за замовчуванням, відкрийте", "select_folder_hint": "Щоб змінити теку за замовчуванням, відкрийте",
"settings": "Налаштування Hydra", "settings": "Налаштування Hydra",
"download_now": "Завантажити зараз", "download_now": "Завантажити зараз",
"installation_instructions": "Інструкція зі встановлення", "installation_instructions": "Інструкція зі встановлення",
"installation_instructions_description": "Для встановлення цієї гри потрібні додаткові кроки", "installation_instructions_description": "Для встановлення цієї гри потрібні додаткові кроки",
"online_fix_instruction": "В іграх з OnlineFix потрібно ввести пароль для вилучення. За необхідності використовуйте наступний пароль:", "online_fix_instruction": "В іграх з OnlineFix потрібно ввести пароль для вилучення. За необхідності використовуйте наступний пароль:",
"dodi_installation_instruction": "Коли ви відкриєте інсталятор DODI, натисніть на клавіатурі клавішу 'вгору' <0 />, щоб почати процес встановлення:", "dodi_installation_instruction": "Коли ви відкриєте інсталятор DODI, натисніть на клавіатурі клавішу 'вгору' <0 />, щоб почати процес встановлення:",
"dont_show_it_again": "Не показувати це знову", "dont_show_it_again": "Не показувати це знову",
"copy_to_clipboard": "Копіювати", "copy_to_clipboard": "Копіювати",
"copied_to_clipboard": "Скопійовано", "copied_to_clipboard": "Скопійовано",
"got_it": "Зрозуміло" "got_it": "Зрозуміло"
}, },
"activation": { "activation": {
"title": "Активувати Hydra", "title": "Активувати Hydra",
"installation_id": "ID установки:", "installation_id": "ID установки:",
"enter_activation_code": "Введіть ваш активаційний код", "enter_activation_code": "Введіть ваш активаційний код",
"message": "Якщо ви не знаєте, де його запросити, то не повинні мати цього.", "message": "Якщо ви не знаєте, де його запросити, то не повинні мати цього.",
"activate": "Активувати", "activate": "Активувати",
"loading": "Завантаження…" "loading": "Завантаження…"
}, },
"downloads": { "downloads": {
"resume": "Продовжити", "resume": "Продовжити",
"pause": "Призупинити", "pause": "Призупинити",
"eta": "Закінчення {{eta}}", "eta": "Закінчення {{eta}}",
"paused": "Призупинено", "paused": "Призупинено",
"verifying": "Перевірка…", "verifying": "Перевірка…",
"completed_at": "Завершено в {{date}}", "completed_at": "Завершено в {{date}}",
"completed": "Завершено", "completed": "Завершено",
"cancelled": "Скасовано", "cancelled": "Скасовано",
"download_again": "Завантажити знову", "download_again": "Завантажити знову",
"cancel": "Скасувати", "cancel": "Скасувати",
"filter": "Фільтр завантажених ігор", "filter": "Фільтр завантажених ігор",
"remove": "Видалити", "remove": "Видалити",
"downloading_metadata": "Завантаження метаданих…", "downloading_metadata": "Завантаження метаданих…",
"checking_files": "Перевірка файлів…", "checking_files": "Перевірка файлів…",
"starting_download": "Початок завантаження…", "starting_download": "Початок завантаження…",
"deleting": "Видалення інсталятора…", "deleting": "Видалення інсталятора…",
"delete": "Видалити інсталятор", "delete": "Видалити інсталятор",
"remove_from_list": "Видалити", "remove_from_list": "Видалити",
"delete_modal_title": "Ви впевнені?", "delete_modal_title": "Ви впевнені?",
"delete_modal_description": "Це видалить усі інсталяційні файли з вашого комп'ютера", "delete_modal_description": "Це видалить усі інсталяційні файли з вашого комп'ютера",
"install": "Встановити" "install": "Встановити"
}, },
"settings": { "settings": {
"downloads_path": "Тека завантажень", "downloads_path": "Тека завантажень",
"change": "Змінити", "change": "Змінити",
"notifications": "Повідомлення", "notifications": "Повідомлення",
"enable_download_notifications": "Після завершення завантаження", "enable_download_notifications": "Після завершення завантаження",
"enable_repack_list_notifications": "Коли додається новий репак", "enable_repack_list_notifications": "Коли додається новий репак",
"telemetry": "Телеметрія", "telemetry": "Телеметрія",
"telemetry_description": "Відправляти анонімну статистику використання", "telemetry_description": "Відправляти анонімну статистику використання",
"behavior": "Поведінка", "behavior": "Поведінка",
"quit_app_instead_hiding": "Закривати програму замість того, щоб згортати її в трей", "quit_app_instead_hiding": "Закривати програму замість того, щоб згортати її в трей",
"launch_with_system": "Запускати програми із запуском комп'ютера" "launch_with_system": "Запускати програми із запуском комп'ютера"
}, },
"notifications": { "notifications": {
"download_complete": "Завантаження завершено", "download_complete": "Завантаження завершено",
"game_ready_to_install": "{{title}} готова до встановлення", "game_ready_to_install": "{{title}} готова до встановлення",
"repack_list_updated": "Список репаків оновлено", "repack_list_updated": "Список репаків оновлено",
"repack_count_one": "{{count}} репак додано", "repack_count_one": "{{count}} репак додано",
"repack_count_other": "{{count}} репаків додано" "repack_count_other": "{{count}} репаків додано"
}, },
"system_tray": { "system_tray": {
"open": "Відкрити Hydra", "open": "Відкрити Hydra",
"quit": "Вийти" "quit": "Вийти"
}, },
"game_card": { "game_card": {
"no_downloads": "Немає доступних завантажень" "no_downloads": "Немає доступних завантажень"
}, },
"binary_not_found_modal": { "binary_not_found_modal": {
"title": "Програми не встановлені", "title": "Програми не встановлені",
"description": "Виконувані файли Wine або Lutris не знайдено у вашій системі", "description": "Виконувані файли Wine або Lutris не знайдено у вашій системі",
"instructions": "Дізнайтеся правильний спосіб встановити будь-який з них на ваш дистрибутив Linux, щоб гра могла нормально працювати" "instructions": "Дізнайтеся правильний спосіб встановити будь-який з них на ваш дистрибутив Linux, щоб гра могла нормально працювати"
}, },
"modal": { "modal": {
"close": "Закрити" "close": "Закрити"
}
} }
}

View File

@ -4,7 +4,29 @@ 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> => {
const englishAppDetails = getSteamAppDetails(objectID, "english");
if (language === "english") return englishAppDetails;
return Promise.all([
englishAppDetails,
getSteamAppDetails(objectID, language),
]).then(([appDetails, localizedAppDetails]) => {
if (appDetails && localizedAppDetails) {
return {
...localizedAppDetails,
name: appDetails.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");

View 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,
});

View File

@ -7,6 +7,7 @@ import "./catalogue/get-games";
import "./catalogue/get-how-long-to-beat"; 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/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";

View File

@ -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",

105
src/preload/index.d.ts vendored
View File

@ -1,105 +0,0 @@
// See the Electron documentation for details on how to use preload scripts:
// https://www.electronjs.org/docs/latest/tutorial/process-model#preload-scripts
import { contextBridge, ipcRenderer } from "electron";
import type {
CatalogueCategory,
GameShop,
TorrentProgress,
UserPreferences,
} from "@types";
contextBridge.exposeInMainWorld("electron", {
/* Torrenting */
startGameDownload: (
repackId: number,
objectID: string,
title: string,
shop: GameShop
) => ipcRenderer.invoke("startGameDownload", repackId, objectID, title, shop),
cancelGameDownload: (gameId: number) =>
ipcRenderer.invoke("cancelGameDownload", gameId),
pauseGameDownload: (gameId: number) =>
ipcRenderer.invoke("pauseGameDownload", gameId),
resumeGameDownload: (gameId: number) =>
ipcRenderer.invoke("resumeGameDownload", gameId),
onDownloadProgress: (cb: (value: TorrentProgress) => void) => {
const listener = (
_event: Electron.IpcRendererEvent,
value: TorrentProgress
) => cb(value);
ipcRenderer.on("on-download-progress", listener);
return () => ipcRenderer.removeListener("on-download-progress", listener);
},
/* Catalogue */
searchGames: (query: string) => ipcRenderer.invoke("searchGames", query),
getCatalogue: (category: CatalogueCategory) =>
ipcRenderer.invoke("getCatalogue", category),
getGameShopDetails: (objectID: string, shop: GameShop, language: string) =>
ipcRenderer.invoke("getGameShopDetails", objectID, shop, language),
getRandomGame: () => ipcRenderer.invoke("getRandomGame"),
getHowLongToBeat: (objectID: string, shop: GameShop, title: string) =>
ipcRenderer.invoke("getHowLongToBeat", objectID, shop, title),
getGames: (take?: number, prevCursor?: number) =>
ipcRenderer.invoke("getGames", take, prevCursor),
/* User preferences */
getUserPreferences: () => ipcRenderer.invoke("getUserPreferences"),
updateUserPreferences: (preferences: UserPreferences) =>
ipcRenderer.invoke("updateUserPreferences", preferences),
autoLaunch: (enabled: boolean) => ipcRenderer.invoke("autoLaunch", enabled),
/* Library */
addGameToLibrary: (
objectID: string,
title: string,
shop: GameShop,
executablePath: string
) =>
ipcRenderer.invoke(
"addGameToLibrary",
objectID,
title,
shop,
executablePath
),
getLibrary: () => ipcRenderer.invoke("getLibrary"),
getRepackersFriendlyNames: () =>
ipcRenderer.invoke("getRepackersFriendlyNames"),
openGameInstaller: (gameId: number) =>
ipcRenderer.invoke("openGameInstaller", gameId),
openGame: (gameId: number, executablePath: string) =>
ipcRenderer.invoke("openGame", gameId, executablePath),
closeGame: (gameId: number) => ipcRenderer.invoke("closeGame", gameId),
removeGameFromLibrary: (gameId: number) =>
ipcRenderer.invoke("removeGameFromLibrary", gameId),
deleteGameFolder: (gameId: number) =>
ipcRenderer.invoke("deleteGameFolder", gameId),
getGameByObjectID: (objectID: string) =>
ipcRenderer.invoke("getGameByObjectID", objectID),
onPlaytime: (cb: (gameId: number) => void) => {
const listener = (_event: Electron.IpcRendererEvent, gameId: number) =>
cb(gameId);
ipcRenderer.on("on-playtime", listener);
return () => ipcRenderer.removeListener("on-playtime", listener);
},
onGameClose: (cb: (gameId: number) => void) => {
const listener = (_event: Electron.IpcRendererEvent, gameId: number) =>
cb(gameId);
ipcRenderer.on("on-game-close", listener);
return () => ipcRenderer.removeListener("on-game-close", listener);
},
/* Hardware */
getDiskFreeSpace: () => ipcRenderer.invoke("getDiskFreeSpace"),
/* Misc */
ping: () => ipcRenderer.invoke("ping"),
getVersion: () => ipcRenderer.invoke("getVersion"),
getDefaultDownloadsPath: () => ipcRenderer.invoke("getDefaultDownloadsPath"),
openExternal: (src: string) => ipcRenderer.invoke("openExternal", src),
showOpenDialog: (options: Electron.OpenDialogOptions) =>
ipcRenderer.invoke("showOpenDialog", options),
platform: process.platform,
});

View File

@ -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"),

View File

@ -1,31 +1,18 @@
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", overflow: "hidden",
overflow: "hidden", borderRadius: "4px",
borderRadius: "4px", transition: "all ease 0.2s",
transition: "all ease 0.2s", border: `solid 1px ${vars.color.border}`,
border: `solid 1px ${vars.color.border}`, cursor: "pointer",
cursor: "pointer", zIndex: "1",
zIndex: "1", ":active": {
":active": { opacity: vars.opacity.active,
opacity: vars.opacity.active,
},
},
variants: {
disabled: {
true: {
pointerEvents: "none",
boxShadow: "none",
opacity: vars.opacity.disabled,
filter: "grayscale(50%)",
},
},
}, },
}); });
@ -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)",
}, },
}, },

View File

@ -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} />

View File

@ -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({

View File

@ -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 }}
/> />
<p className={styles.description}> {!isLoading && featuredGameDetails && (
{featuredGameDetails?.short_description} <p className={styles.description}>
</p> {featuredGameDetails?.short_description}
</p>
)}
</div> </div>
</div> </div>
</button> </button>

View File

@ -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

View File

@ -2,6 +2,7 @@ import type {
CatalogueCategory, CatalogueCategory,
CatalogueEntry, CatalogueEntry,
Game, Game,
GameRepack,
GameShop, GameShop,
HowLongToBeatCategory, HowLongToBeatCategory,
ShopDetails, ShopDetails,
@ -50,6 +51,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: (

View File

@ -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`,
@ -28,3 +30,11 @@ export const getSteamLanguage = (language: string) => {
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()}`;
};

View File

@ -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(() => {

View File

@ -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

View File

@ -4,21 +4,19 @@ import { ChevronRightIcon, ChevronLeftIcon } from "@primer/octicons-react";
import * as styles from "./gallery-slider.css"; import * as styles from "./gallery-slider.css";
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 [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) { return gameDetails.movies.length;
return gameDetails.movies.length; } else if (gameDetails.screenshots) {
} else if (gameDetails.screenshots) { return gameDetails.screenshots.length;
return gameDetails.screenshots.length;
}
} }
return 0; return 0;
@ -57,8 +55,8 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) {
} }
}, [gameDetails, mediaIndex, mediaCount]); }, [gameDetails, mediaIndex, mediaCount]);
const hasScreenshots = gameDetails && gameDetails.screenshots.length; const hasScreenshots = gameDetails.screenshots.length;
const hasMovies = gameDetails && gameDetails.movies?.length; const hasMovies = gameDetails.movies?.length;
return ( return (
<> <>
@ -84,7 +82,7 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) {
<source src={video.webm.max.replace("http", "https")} /> <source src={video.webm.max.replace("http", "https")} />
</video> </video>
))} ))}
{gameDetails.screenshots && {hasScreenshots &&
gameDetails.screenshots.map( gameDetails.screenshots.map(
(image: SteamScreenshot, i: number) => ( (image: SteamScreenshot, i: number) => (
<img <img
@ -128,7 +126,8 @@ export function GallerySlider({ gameDetails }: GallerySliderProps) {
className={`${styles.gallerySliderMediaPreview} ${mediaIndex === i ? styles.gallerySliderMediaPreviewActive : ""}`} className={`${styles.gallerySliderMediaPreview} ${mediaIndex === i ? styles.gallerySliderMediaPreviewActive : ""}`}
/> />
))} ))}
{gameDetails.screenshots &&
{hasScreenshots &&
gameDetails.screenshots.map( gameDetails.screenshots.map(
(image: SteamScreenshot, i: number) => ( (image: SteamScreenshot, i: number) => (
<img <img

View File

@ -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} />
))} ))}

View File

@ -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,
});

View File

@ -3,14 +3,7 @@ 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 type { Game, GameRepack, GameShop, ShopDetails } from "@types";
Game,
GameRepack,
GameShop,
HowLongToBeatCategory,
ShopDetails,
SteamAppDetails,
} from "@types";
import { Button } from "@renderer/components"; import { Button } from "@renderer/components";
import { setHeaderTitle } from "@renderer/features"; import { setHeaderTitle } from "@renderer/features";
@ -26,7 +19,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,6 +29,7 @@ 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();
@ -45,10 +38,7 @@ export function GameDetails() {
const [isLoadingRandomGame, setIsLoadingRandomGame] = useState(false); const [isLoadingRandomGame, setIsLoadingRandomGame] = useState(false);
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 +46,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,28 +80,24 @@ 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 Promise.all([
.getGameShopDetails(objectID!, "steam", getSteamLanguage(i18n.language)) window.electron.getGameShopDetails(
.then((result) => { objectID!,
if (!result) { "steam",
navigate(-1); getSteamLanguage(i18n.language)
return; ),
} window.electron.searchGameRepacks(title),
])
window.electron .then(([appDetails, repacks]) => {
.getHowLongToBeat(objectID!, "steam", result.name) if (appDetails) setGameDetails(appDetails);
.then((data) => { setRepacks(repacks);
setHowLongToBeat({ isLoading: false, data });
});
setGameDetails(result);
dispatch(setHeaderTitle(result.name));
setIsLoadingRandomGame(false); setIsLoadingRandomGame(false);
}) })
.finally(() => { .finally(() => {
@ -119,8 +105,7 @@ export function GameDetails() {
}); });
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,30 +139,28 @@ export function GameDetails() {
repack: GameRepack, repack: GameRepack,
downloadPath: string downloadPath: string
) => { ) => {
if (gameDetails) { return startDownload(
return startDownload( repack.id,
repack.id, objectID!,
gameDetails.objectID, title,
gameDetails.name, shop as GameShop,
shop as GameShop, downloadPath
downloadPath ).then(() => {
).then(() => { getGame();
getGame(); setShowRepacksModal(false);
setShowRepacksModal(false);
if ( if (
repack.repacker === "onlinefix" && repack.repacker === "onlinefix" &&
!window.localStorage.getItem(DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY) !window.localStorage.getItem(DONT_SHOW_ONLINE_FIX_INSTRUCTIONS_KEY)
) { ) {
setShowInstructionsModal("onlinefix"); setShowInstructionsModal("onlinefix");
} else if ( } else if (
repack.repacker === "DODI" && repack.repacker === "DODI" &&
!window.localStorage.getItem(DONT_SHOW_DODI_INSTRUCTIONS_KEY) !window.localStorage.getItem(DONT_SHOW_DODI_INSTRUCTIONS_KEY)
) { ) {
setShowInstructionsModal("DODI"); setShowInstructionsModal("DODI");
} }
}); });
}
}; };
const handleRandomizerClick = async () => { const handleRandomizerClick = async () => {
@ -191,18 +174,14 @@ export function GameDetails() {
navigate(`/game/steam/${randomGameObjectID}?${searchParams.toString()}`); 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} repacks={repacks}
gameDetails={gameDetails} startDownload={handleStartDownload}
startDownload={handleStartDownload} onClose={() => setShowRepacksModal(false)}
onClose={() => setShowRepacksModal(false)} />
/>
)}
<OnlineFixInstallationGuide <OnlineFixInstallationGuide
visible={showInstructionsModal === "onlinefix"} visible={showInstructionsModal === "onlinefix"}
@ -239,7 +218,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}
@ -247,63 +228,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>
)} )}

View File

@ -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}

View File

@ -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({

View File

@ -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,7 +62,7 @@ export function HeroPanel({
}, [game, isGameDownloading, gameDownloading]); }, [game, isGameDownloading, gameDownloading]);
const getInfo = () => { const getInfo = () => {
if (!gameDetails) return null; if (!repacks.length) return null;
if (isGameDeleting(game?.id ?? -1)) { if (isGameDeleting(game?.id ?? -1)) {
return <p>{t("deleting")}</p>; return <p>{t("deleting")}</p>;
@ -110,11 +114,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 +143,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)}

View File

@ -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";
@ -13,14 +13,14 @@ import { SelectFolderModal } from "./select-folder-modal";
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) {
@ -35,8 +35,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);
@ -47,7 +47,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();
@ -63,14 +63,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={`Download options`}
description={t("repacks_modal_description")} description={t("repacks_modal_description")}
onClose={onClose} onClose={onClose}
> >

View File

@ -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("installation_folder")}
description={t("space_left_on_disk", { description={t("space_left_on_disk", {
space: formatBytes(diskFreeSpace?.free ?? 0), space: formatBytes(diskFreeSpace?.free ?? 0),
})} })}

View File

@ -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",

View 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,
});

View 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>
);
}

View File

@ -12,6 +12,7 @@ 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"];
@ -129,9 +130,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>

View File

@ -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(() => {

View File

@ -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 {