Merge branch 'main' of github.com:hydralauncher/hydra

This commit is contained in:
Chubby Granny Chaser 2024-09-13 00:04:07 +01:00
commit 8f0003298f
No known key found for this signature in database
23 changed files with 614 additions and 78 deletions

View File

@ -22,6 +22,7 @@
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md) [![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md)
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md) [![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md) [![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
![Hydra Catalogue](./docs/screenshot.png) ![Hydra Catalogue](./docs/screenshot.png)

185
README.cs.md Normal file
View File

@ -0,0 +1,185 @@
<br>
<div align="center">
[<img src="./resources/icon.png" width="144"/>](https://hydralauncher.site)
<h1 align="center">Hydra Launcher</h1>
<p align="center">
<strong>Hydra je herní launcher s vlastním vestavěným Bittorrent klientem.</strong>
</p>
[![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)
[![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)
[![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)
[![be](https://img.shields.io/badge/lang-be-orange)](README.be.md)
[![es](https://img.shields.io/badge/lang-es-red)](README.es.md)
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md)
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
![Hydra Katalog](./docs/screenshot.png)
</div>
## Seznam obsahu
- [Seznam obsahu](#seznam-obsahu)
- [O projektu](#o-projektu)
- [Funkce](#funkce)
- [Instalace](#instalace)
- [Přispívání](#přispívání)
- [Připoj se na náš telegram](#připoj-se-na-náš-telegram)
- [Vytvořte fork a naklonujte svůj repozitář](#vytvořte-fork-a-naklonujte-svůj-repozitář)
- [Způsoby jak můžete přispět](#způsoby-jak-můžete-přispět)
- [Struktura projektu](#struktura-projektu)
- [Sestavení ze zdroje](#sestavení-ze-zdroje)
- [Instalace Node.js](#instalace-nodejs)
- [Instalace Yarn](#instalace-yarn)
- [Instalace Požadavků pro Node.js](#instalace-požadavků-pro-nodejs)
- [Instalace Pythonu 3.9](#instalace-pythonu-39)
- [Instalace Požadavků pro Python](#instalace-požadavků-pro-python)
- [Proměnné prostředí](#proměnné-prostředí)
- [Spuštění](#spuštění)
- [Sestavení](#sestavení)
- [Sestavení bittorrent klientu](#sestavení-bittorrent-klientu)
- [Sestavení electron aplikace](#sestavení-electron-aplikace)
- [Přispěvatelé](#přispěvatelé)
- [Licence](#licence)
## O projektu
**Hydra** je **Herní Launcher** s jeho vlastním vestavěným **BitTorrent Klientem**.
<br>
Launcher je napsán v TypeScriptu (Electron) a Pythonu, který má na starosti torrentovací systém za pomocí knihovny libtorrent.
## Funkce
- Vlastní vestavěný BitTorrent klient
- How Long To Beat (HLTB) integrace na stránce hry
- Vlastní místa pro uložení hry
- Windows a Linux podpora
- Časté aktualizace
- A další ...
## Instalace
Následuj kroky:
1. Stáhni nejnovější verzi Hydry ze stránky [Vydání](https://github.com/hydralauncher/hydra/releases/latest).
- Stáhni .exe, pokud chceš instalovat Hydru na Windows.
- Stáhni .deb nebo .rpm nebo .zip, pokud chceš instalovat Hydru na Linux. (záleží na tvé Linux distribuci)
2. Spusť stažený instalační soubor.
3. Užívej Hydru!
## <a name="contributing"> Přispívání
### <a name="join-our-telegram"></a> Připoj se na náš telegram
Vedeme diskuzi v našem [Telegramovém](https://t.me/hydralauncher) kanálu.
### Vytvořte fork a naklonujte svůj repozitář
1. Vytvoř fork repozitáře [(klikni sem pro vytvoření forku)](https://github.com/hydralauncher/hydra/fork)
2. Naklonuj kód forku `git clone https://github.com/tvoje_jméno/hydra`
3. Vytvoř nové odvětví (branch)
4. Odešli svoje změny
5. Odešli nový Pull Request
### Způsoby jak můžete přispět
- Překládání: Chceme, aby Hydra byla co nejvíce dostupná. Můžete přispět novým jazykem, nebo úpravou současného!
- Kód: Hydra je postavena na Typescriptu, Electronu a trochou Pythonu. Pokud chceš přispět, připoj se na náš [Telegram](https://t.me/hydralauncher)!
### Struktura projektu
- torrent-client: Používáme libtorrent, Pythonovou knihovnu, pro správu torrent stahování
- src/renderer: uživatelské rozhraní aplikace (UI)
- src/main: celá logika projektu
## Sestavení ze zdroje
### Instalace Node.js
Ujistěte se, že máte Node.js nainstalován na svém zařízení. Pokud ne, stáhněte ho, a nainstalujte z [nodejs.org](https://nodejs.org/).
### Instalace Yarn
Yarn je balíčkový správce pro Node.js. Pokud ještě nemáte yarn, můžete ho stáhnout za pomoci pokynů na [yarnpkg.com](https://classic.yarnpkg.com/lang/en/docs/install/).
### Instalace Požadavků pro Node.js
Jděte do složky projektu, otevřte v ní konzole a nainstalujte požadavky pro Node pomocí Yarn:
```bash
cd hydra
yarn
```
### Instalace Pythonu 3.9
Ujistěte se, že máte Python 3.9 nainstalován na svém zařízení. Můžete ho stáhnout z [python.org](https://www.python.org/downloads/release/python-3913/).
### Instalace Požadavků pro Python
Nainstalujte požadavky pro Python za pomoci pip:
```bash
pip install -r requirements.txt
```
## Proměnné prostředí
Budete potřebovat SteamGridDB API klíč, abyste mohli načítat ikony u her.
Jakmile ho máte, můžete zkopírovat, nebo přejmenovat `.env.example` soubor na `.env` a dát ho do `STEAMGRIDDB_API_KEY`.
## Spuštění
Jakmile máte vše nastaveno, můžete spustit jak Electron proces tak bittorrent client:
```bash
yarn dev
```
## Sestavení
### Sestavení bittorrent klientu
Sestavit bittorrent klient můžete pomocí:
```bash
python torrent-client/setup.py build
```
### Sestavení electron aplikace
Sestavit Electron aplikaci můžete pomocí následujících kroků:
Na Windows:
```bash
yarn build:win
```
Na Linux:
```bash
yarn build:linux
```
## Přispěvatelé
<a href="https://github.com/hydralauncher/hydra/graphs/contributors">
<img src="https://contrib.rocks/image?repo=hydralauncher/hydra" />
</a>
## Licence
Hydra je licencována pod [MIT Licencí](LICENSE).

View File

@ -22,6 +22,7 @@
[![es](https://img.shields.io/badge/lang-es-red)](README.es.md) [![es](https://img.shields.io/badge/lang-es-red)](README.es.md)
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md) [![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md)
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md) [![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
![Hydra Katalog](./docs/screenshot.png) ![Hydra Katalog](./docs/screenshot.png)

View File

@ -22,6 +22,7 @@
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md) [![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md)
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md) [![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md) [![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
![Hydra Catalogue](./docs/screenshot.png) ![Hydra Catalogue](./docs/screenshot.png)

View File

@ -22,6 +22,7 @@
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md) [![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md)
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md) [![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md) [![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
![Catalogue Hydra](./docs/screenshot.png) ![Catalogue Hydra](./docs/screenshot.png)

View File

@ -22,6 +22,7 @@
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md) [![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md)
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md) [![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md) [![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
![Hydra Catalogue](./docs/screenshot.png) ![Hydra Catalogue](./docs/screenshot.png)

View File

@ -22,6 +22,7 @@
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md) [![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md)
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md) [![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md) [![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
![Hydra Catalogue](./docs/screenshot.png) ![Hydra Catalogue](./docs/screenshot.png)

View File

@ -22,6 +22,7 @@
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md) [![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md)
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md) [![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md) [![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
![Hydra Catalogue](./docs/screenshot.png) ![Hydra Catalogue](./docs/screenshot.png)

View File

@ -22,6 +22,7 @@
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md) [![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md)
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md) [![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md) [![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
![Hydra Catalogue](./docs/screenshot.png) ![Hydra Catalogue](./docs/screenshot.png)

View File

@ -22,6 +22,7 @@
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md) [![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md)
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md) [![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md) [![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
![Hydra Catalogue](./docs/screenshot.png) ![Hydra Catalogue](./docs/screenshot.png)

View File

@ -22,6 +22,7 @@
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md) [![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md)
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md) [![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md) [![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
![Hydra Catalogue](./docs/screenshot.png) ![Hydra Catalogue](./docs/screenshot.png)

View File

@ -0,0 +1,277 @@
{
"language_name": "Čeština",
"app": {
"successfully_signed_in": "Úspěšně přihlášen"
},
"home": {
"featured": "Doporučené",
"trending": "Trendy",
"surprise_me": "Překvap mě",
"no_results": "Výsledek nenalezen"
},
"sidebar": {
"catalogue": "Katalog",
"downloads": "Stažené",
"settings": "Nastavení",
"my_library": "Moje knihovna",
"downloading_metadata": "{{title}} (Stahuji metadata…)",
"paused": "{{title}} (Pozastaveno)",
"downloading": "{{title}} ({{percentage}} - Stahuji…)",
"filter": "Filtrovat knihovnu",
"home": "Domov",
"queued": "{{title}} (V řadě)",
"game_has_no_executable": "Hra nemá zvolen žádný spustitelný soubor",
"sign_in": "Přihlásit se"
},
"header": {
"search": "Vyhledat hry",
"home": "Domov",
"catalogue": "Katalog",
"downloads": "Stažené",
"search_results": "Výsledky vyhledávání",
"settings": "Nastavení",
"version_available_install": "Je dostupná nová verze: {{version}}. Klikni sem pro restart a instalaci.",
"version_available_download": "Je dostupná nová verze: {{version}}. Klikni sem pro stažení."
},
"bottom_panel": {
"no_downloads_in_progress": "Neprobíhá žádné stahování",
"downloading_metadata": "Stahuji metadata: {{title}}…",
"downloading": "Stahuji {{title}}… ({{percentage}} staženo) - Odhadovaný čas {{eta}} - {{speed}}",
"calculating_eta": "Stahuji {{title}}… ({{percentage}} staženo) - Počítám zbývající čas…",
"checking_files": "Kontroluji soubory: {{title}}… ({{percentage}} ověřeno)"
},
"catalogue": {
"next_page": "Další strana",
"previous_page": "Předchozí strana"
},
"game_details": {
"open_download_options": "Otevřít možnosti stahování",
"download_options_zero": "Žádné možnosti stahování",
"download_options_one": "{{count}} možnost stažení",
"download_options_other": "{{count}} možnosti stažení",
"updated_at": "Aktualizováno {{updated_at}}",
"install": "Instalovat",
"resume": "Obnovit",
"pause": "Pozastavit",
"cancel": "Zrušit",
"remove": "Odebrat",
"space_left_on_disk": "{{space}} zbývá na disku",
"eta": "Odhadovaný čas: {{eta}}",
"calculating_eta": "Počítám zbývající čas…",
"downloading_metadata": "Stahuji metadata…",
"filter": "Filtrovat repacky",
"requirements": "Systémové požadavky",
"minimum": "Minimální",
"recommended": "Doporučené",
"paused": "Pozastaveno",
"release_date": "Datum vydání: {{date}}",
"publisher": "Publikováno: {{publisher}}",
"hours": "hodiny",
"minutes": "minuty",
"amount_hours": "{{amount}} hodin",
"amount_minutes": "{{amount}} minut",
"accuracy": "Přesnost {{accuracy}}%",
"add_to_library": "Přidat do knihovny",
"remove_from_library": "Odebrat z knihovny",
"no_downloads": "Žádné možnosti stahování nejsou dostupné",
"play_time": "Odehraný čas: {{amount}}",
"last_time_played": "Naposledy hráno {{period}}",
"not_played_yet": "Ješte jste nehráli {{title}}",
"next_suggestion": "Další doporučení",
"play": "Hrát",
"deleting": "Odstraňuji instalační program…",
"close": "Zavřít",
"playing_now": "Právě hraje",
"change": "Změnit",
"repacks_modal_description": "Vyber repack který chceš stáhnout",
"select_folder_hint": "Pro změnu základní složky, jdi do <0>Nastavení</0>",
"download_now": "Stáhnout",
"no_shop_details": "Nepodařilo se mi načíst informace o obchodu.",
"download_options": "Možnosti stahování",
"download_path": "Umístění stahování",
"previous_screenshot": "Předchozí snímek obrazovky",
"next_screenshot": "Následující snímek obrazovky",
"screenshot": "Snímek obrazovky {{number}}",
"open_screenshot": "Otevřít snímek obrazovky {{number}}",
"download_settings": "Nastavení stahování",
"downloader": "Správce stahování",
"select_executable": "Vybrat",
"no_executable_selected": "Nebyl vybrán spustitelný soubor",
"open_folder": "Otevřít složku",
"open_download_location": "Zobrazit stažené soubory",
"create_shortcut": "Vytvořit zástupce na ploše",
"remove_files": "Odebrat soubory",
"remove_from_library_title": "Jste si jisti?",
"remove_from_library_description": "Tohle odstraní {{game}} z vaší knihovny",
"options": "Možnosti",
"executable_section_title": "Spustitelné",
"executable_section_description": "Umístění souboru který bude spuštěn při kliknutí na \"Hrát\"",
"downloads_secion_title": "Stažené soubory",
"downloads_section_description": "Zkontrolovat jestli není nová / odlišná verze hry",
"danger_zone_section_title": "Nebezpečná zóna",
"danger_zone_section_description": "Odebrat hru z knihovny / soubory stažené Hydrou",
"download_in_progress": "Probíhá stahování",
"download_paused": "Stahování pozastaveno",
"last_downloaded_option": "Poslední stažená možnost",
"create_shortcut_success": "Zástupce vytvořen úspěšně",
"create_shortcut_error": "Chyba při pokusu vytvořit zástupce"
},
"activation": {
"title": "Aktivovat hydru",
"installation_id": "ID instalace:",
"enter_activation_code": "Zadej svůj aktivační kód",
"message": "Pokud nevíš, kde ten kód sehnat, tak by jsi k tomuhle neměl mít přístup.",
"activate": "Aktivovat",
"loading": "Načítání…"
},
"downloads": {
"resume": "Pokračovat",
"pause": "Pozastavit",
"eta": "Odhadovaný čas: {{eta}}",
"paused": "Pozastaveno",
"verifying": "Ověřuji…",
"completed": "Hotovo",
"removed": "Není staženo",
"cancel": "Zrušit",
"filter": "Filtrovat stažené hry",
"remove": "Odebrat",
"downloading_metadata": "Stahuji metadata…",
"deleting": "Odstraňuji instalační program…",
"delete": "Odebrat instalační program",
"delete_modal_title": "Jste si jisti?",
"delete_modal_description": "Tohle odstraní všechny instalační soubory",
"install": "Instalovat",
"download_in_progress": "Probíhá stahování",
"queued_downloads": "Stahování v řadě",
"downloads_completed": "Dokončeno",
"queued": "V řadě",
"no_downloads_title": "Prázdno..",
"no_downloads_description": "Ještě jsi zatím nic nestáhl přes Hydru, ale furt není pozdě začít.",
"checking_files": "Kontroluji soubory…"
},
"settings": {
"downloads_path": "Umístění stahování",
"change": "Aktualizovat",
"notifications": "Upozornění",
"enable_download_notifications": "Až bude stahování dokončeno",
"enable_repack_list_notifications": "Když bude přidán nový repack",
"real_debrid_api_token_label": "Real-Debrid API token",
"quit_app_instead_hiding": "Nezavírat Hydru při zavření okna",
"launch_with_system": "Spustit Hydru při startu systému",
"general": "Hlavní",
"behavior": "Chování",
"download_sources": "Zdroje stahování",
"language": "Jazyk",
"real_debrid_api_token": "API Token",
"enable_real_debrid": "Povolit Real-Debrid",
"real_debrid_description": "Real-Debrid je neomezený správce stahování, který umožňuje stahovat soubory v nejvyšší rychlosti vašeho internetu.",
"real_debrid_invalid_token": "Neplatný API token",
"real_debrid_api_token_hint": "API token můžeš sehnat <0>zde</0>",
"real_debrid_free_account_error": "Účet \"{{username}}\" má základní úroveň. Prosím předplaťte si Real-Debrid",
"real_debrid_linked_message": "Účet \"{{username}}\" je propojen",
"save_changes": "Uložit změny",
"changes_saved": "Změny úspěšně uloženy",
"download_sources_description": "Hydra bude odsud sbírat soubory. Zdrojový odkaz musí být .json soubor obsahující odkazy na soubory.",
"validate_download_source": "Ověřit",
"remove_download_source": "Odebrat",
"add_download_source": "Přidat zdroj",
"download_count_zero": "Žádná možnost stažení",
"download_count_one": "{{countFormatted}} možnost stažení",
"download_count_other": "{{countFormatted}} možnosti stažení",
"download_source_url": "Stáhnout zdrojový odkaz",
"add_download_source_description": "Zadej odkaz odkazující na .json soubor",
"download_source_up_to_date": "Aktuální",
"download_source_errored": "Chyba",
"sync_download_sources": "Synchronizovat zdroje",
"removed_download_source": "Zdroj odebrán",
"added_download_source": "Zdroj přidán",
"download_sources_synced": "Všechny zdroje jsou synchronizovány",
"insert_valid_json_url": "Zadej platnou JSON adresu",
"found_download_option_zero": "Nenalezena žádná možnost stahování",
"found_download_option_one": "Nalezena {{countFormatted}} možnost stahování",
"found_download_option_other": "Nalezeny {{countFormatted}} možnosti stahování",
"import": "Importovat"
},
"notifications": {
"download_complete": "Stahování dokončeno",
"game_ready_to_install": "{{title}} je připraveno k instalaci",
"repack_list_updated": "Seznam repacků byl aktualizován",
"repack_count_one": "{{count}} repack přidán",
"repack_count_other": "{{count}} repacky přidány",
"new_update_available": "Version {{version}} je dostupná",
"restart_to_install_update": "Restartuj Hydru pro aktualizaci"
},
"system_tray": {
"open": "Otevřít Hydru",
"quit": "Odejít"
},
"game_card": {
"no_downloads": "Žádné možnosti stahování nenalezeny"
},
"binary_not_found_modal": {
"title": "Programy nenainstalovány",
"description": "Spustitelné soubory Wine nebo Lutris nebyly nalezeny ve vašem systému",
"instructions": "Zkonstroluj oficiální cestu jak je nainstalovat na tvoji Linux Distribuci, aby hry mohly běžet normálně"
},
"modal": {
"close": "Tlačítko zavřít"
},
"forms": {
"toggle_password_visibility": "Přepnout viditelnost hesla"
},
"user_profile": {
"amount_hours": "{{amount}} hodin",
"amount_minutes": "{{amount}} minut",
"last_time_played": "Naposledy hráno {{period}}",
"activity": "Nedávná aktivita",
"library": "Knihovna",
"total_play_time": "Celkový odehraný čas: {{amount}}",
"no_recent_activity_title": "Hmmm… nic tu není",
"no_recent_activity_description": "V poslední době si nehrál žádnout hru, můžeš to ale napravit!",
"display_name": "Zobrazované jméno",
"saving": "Ukládání",
"save": "Uložit",
"edit_profile": "Upravit profil",
"saved_successfully": "Úspěšně uloženo",
"try_again": "Prosím, zkuste to znovu",
"sign_out_modal_title": "Jste si jisti?",
"cancel": "Zrušit",
"successfully_signed_out": "Úspěšně odhlášeno",
"sign_out": "Odhlásit se",
"playing_for": "Hraje po: {{amount}}",
"sign_out_modal_text": "Vaše knihovna je propojena s vaším současným účtem. Po odhlášení vaše knihovna již nebude vidět, a postup nebude uložen. Pokračovat?",
"add_friends": "Přidat přátele",
"add": "Přidat",
"friend_code": "Kód přítele",
"see_profile": "Zobrazit profil",
"sending": "Odesílání",
"friend_request_sent": "Žádost odeslána",
"friends": "Přátelé",
"friends_list": "Seznam přátel",
"user_not_found": "Uživatel nenalezen",
"block_user": "Zablokovat uživatele",
"add_friend": "Přidat přítele",
"request_sent": "Žádost odeslána",
"request_received": "Žádost obdržena",
"accept_request": "Přijmout žádost",
"ignore_request": "Ignorovat žádost",
"cancel_request": "Zrušit žádost",
"undo_friendship": "Odvolat přátelství",
"request_accepted": "Žádost přijata",
"user_blocked_successfully": "Uživatel úspěšně zablokován",
"user_block_modal_text": "Tohle zablokuje {{displayName}}",
"settings": "Nastavení",
"public": "Veřejné",
"private": "Soukromé",
"friends_only": "Pouze přátelé",
"privacy": "Soukromí",
"blocked_users": "Zablokovaní uživatelé",
"unblock": "Odblokovat",
"no_friends_added": "Nemáš přidané žádné přátele",
"pending": "Odchozí",
"no_pending_invites": "Nemáte žádné příchozí žádosti",
"no_blocked_users": "Nemáte nikoho zablokovaného",
"friend_code_copied": "Kód přítele zkopírován",
"undo_friendship_modal_text": "Tímto zrušíte své přátelství s {{displayName}}"
}
}

View File

@ -7,7 +7,8 @@
"featured": "Featured", "featured": "Featured",
"trending": "Trending", "trending": "Trending",
"surprise_me": "Surprise me", "surprise_me": "Surprise me",
"no_results": "No results found" "no_results": "No results found",
"start_typing": "Starting typing to search..."
}, },
"sidebar": { "sidebar": {
"catalogue": "Catalogue", "catalogue": "Catalogue",

View File

@ -21,6 +21,7 @@ import fa from "./fa/translation.json";
import ro from "./ro/translation.json"; import ro from "./ro/translation.json";
import ca from "./ca/translation.json"; import ca from "./ca/translation.json";
import kk from "./kk/translation.json"; import kk from "./kk/translation.json";
import cs from "./cs/translation.json";
export default { export default {
"pt-BR": ptBR, "pt-BR": ptBR,
@ -46,4 +47,5 @@ export default {
ro, ro,
ca, ca,
kk, kk,
cs,
}; };

View File

@ -9,7 +9,8 @@
"hot": "🔥 Populares agora", "hot": "🔥 Populares agora",
"weekly": "📅 Mais baixados da semana", "weekly": "📅 Mais baixados da semana",
"surprise_me": "Surpreenda-me", "surprise_me": "Surpreenda-me",
"no_results": "Nenhum resultado encontrado" "no_results": "Nenhum resultado encontrado",
"start_typing": "Comece a digitar para pesquisar…"
}, },
"sidebar": { "sidebar": {
"catalogue": "Catálogo", "catalogue": "Catálogo",

View File

@ -7,7 +7,8 @@
"featured": "Destaques", "featured": "Destaques",
"trending": "Populares", "trending": "Populares",
"surprise_me": "Surpreende-me", "surprise_me": "Surpreende-me",
"no_results": "Nenhum resultado encontrado" "no_results": "Nenhum resultado encontrado",
"start_typing": "Comece a digitar para pesquisar…"
}, },
"sidebar": { "sidebar": {
"catalogue": "Catálogo", "catalogue": "Catálogo",

View File

@ -3,7 +3,7 @@ import { shuffle } from "lodash-es";
import { getSteam250List } from "@main/services"; import { getSteam250List } from "@main/services";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { searchSteamGames } from "../helpers/search-games"; import { getSteamGameById } from "../helpers/search-games";
import type { Steam250Game } from "@types"; import type { Steam250Game } from "@types";
const state = { games: Array<Steam250Game>(), index: 0 }; const state = { games: Array<Steam250Game>(), index: 0 };
@ -12,14 +12,10 @@ const filterGames = async (games: Steam250Game[]) => {
const results: Steam250Game[] = []; const results: Steam250Game[] = [];
for (const game of games) { for (const game of games) {
const catalogue = await searchSteamGames({ query: game.title }); const steamGame = await getSteamGameById(game.objectID);
if (catalogue.length) { if (steamGame?.repacks.length) {
const [steamGame] = catalogue; results.push(game);
if (steamGame.repacks.length) {
results.push(game);
}
} }
} }

View File

@ -1,10 +1,25 @@
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { searchSteamGames } from "../helpers/search-games"; import { convertSteamGameToCatalogueEntry } from "../helpers/search-games";
import { CatalogueEntry } from "@types"; import { CatalogueEntry } from "@types";
import { HydraApi, RepacksManager } from "@main/services";
const searchGamesEvent = async ( const searchGamesEvent = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
query: string query: string
): Promise<CatalogueEntry[]> => searchSteamGames({ query, limit: 12 }); ): Promise<CatalogueEntry[]> => {
const games = await HydraApi.get<
{ objectId: string; title: string; shop: string }[]
>("/games/search", { title: query, take: 12, skip: 0 }, { needsAuth: false });
const steamGames = games.map((game) => {
return convertSteamGameToCatalogueEntry({
id: Number(game.objectId),
name: game.title,
clientIcon: null,
});
});
return RepacksManager.findRepacksForCatalogueEntries(steamGames);
};
registerEvent("searchGames", searchGamesEvent); registerEvent("searchGames", searchGamesEvent);

View File

@ -1,6 +1,3 @@
import { orderBy } from "lodash-es";
import flexSearch from "flexsearch";
import type { GameShop, CatalogueEntry, SteamGame } from "@types"; import type { GameShop, CatalogueEntry, SteamGame } from "@types";
import { steamGamesWorker } from "@main/workers"; import { steamGamesWorker } from "@main/workers";
@ -23,20 +20,18 @@ export const convertSteamGameToCatalogueEntry = (
repacks: [], repacks: [],
}); });
export const searchSteamGames = async ( export const getSteamGameById = async (
options: flexSearch.SearchOptions objectId: string
): Promise<CatalogueEntry[]> => { ): Promise<CatalogueEntry | null> => {
const steamGames = (await steamGamesWorker.run(options, { const steamGame = await steamGamesWorker.run(Number(objectId), {
name: "search", name: "getById",
})) as SteamGame[]; });
const result = RepacksManager.findRepacksForCatalogueEntries( if (!steamGame) return null;
steamGames.map((game) => convertSteamGameToCatalogueEntry(game))
);
return orderBy( const catalogueEntry = convertSteamGameToCatalogueEntry(steamGame);
result,
[({ repacks }) => repacks.length, "repacks"], const result = RepacksManager.findRepacksForCatalogueEntry(catalogueEntry);
["desc"]
); return result;
}; };

View File

@ -7,6 +7,10 @@ import { clearGamesRemoteIds } from "./library-sync/clear-games-remote-id";
import { logger } from "./logger"; import { logger } from "./logger";
import { UserNotLoggedInError } from "@shared"; import { UserNotLoggedInError } from "@shared";
interface HydraApiOptions {
needsAuth: boolean;
}
export class HydraApi { export class HydraApi {
private static instance: AxiosInstance; private static instance: AxiosInstance;
@ -204,50 +208,76 @@ export class HydraApi {
throw err; throw err;
}; };
static async get<T = any>(url: string, params?: any) { static async get<T = any>(
if (!this.isLoggedIn()) throw new UserNotLoggedInError(); url: string,
params?: any,
options?: HydraApiOptions
) {
if (!options || options.needsAuth) {
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
await this.revalidateAccessTokenIfExpired();
}
await this.revalidateAccessTokenIfExpired();
return this.instance return this.instance
.get<T>(url, { params, ...this.getAxiosConfig() }) .get<T>(url, { params, ...this.getAxiosConfig() })
.then((response) => response.data) .then((response) => response.data)
.catch(this.handleUnauthorizedError); .catch(this.handleUnauthorizedError);
} }
static async post<T = any>(url: string, data?: any) { static async post<T = any>(
if (!this.isLoggedIn()) throw new UserNotLoggedInError(); url: string,
data?: any,
options?: HydraApiOptions
) {
if (!options || options.needsAuth) {
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
await this.revalidateAccessTokenIfExpired();
}
await this.revalidateAccessTokenIfExpired();
return this.instance return this.instance
.post<T>(url, data, this.getAxiosConfig()) .post<T>(url, data, this.getAxiosConfig())
.then((response) => response.data) .then((response) => response.data)
.catch(this.handleUnauthorizedError); .catch(this.handleUnauthorizedError);
} }
static async put<T = any>(url: string, data?: any) { static async put<T = any>(
if (!this.isLoggedIn()) throw new UserNotLoggedInError(); url: string,
data?: any,
options?: HydraApiOptions
) {
if (!options || options.needsAuth) {
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
await this.revalidateAccessTokenIfExpired();
}
await this.revalidateAccessTokenIfExpired();
return this.instance return this.instance
.put<T>(url, data, this.getAxiosConfig()) .put<T>(url, data, this.getAxiosConfig())
.then((response) => response.data) .then((response) => response.data)
.catch(this.handleUnauthorizedError); .catch(this.handleUnauthorizedError);
} }
static async patch<T = any>(url: string, data?: any) { static async patch<T = any>(
if (!this.isLoggedIn()) throw new UserNotLoggedInError(); url: string,
data?: any,
options?: HydraApiOptions
) {
if (!options || options.needsAuth) {
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
await this.revalidateAccessTokenIfExpired();
}
await this.revalidateAccessTokenIfExpired();
return this.instance return this.instance
.patch<T>(url, data, this.getAxiosConfig()) .patch<T>(url, data, this.getAxiosConfig())
.then((response) => response.data) .then((response) => response.data)
.catch(this.handleUnauthorizedError); .catch(this.handleUnauthorizedError);
} }
static async delete<T = any>(url: string) { static async delete<T = any>(url: string, options?: HydraApiOptions) {
if (!this.isLoggedIn()) throw new UserNotLoggedInError(); if (!options || options.needsAuth) {
if (!this.isLoggedIn()) throw new UserNotLoggedInError();
await this.revalidateAccessTokenIfExpired();
}
await this.revalidateAccessTokenIfExpired();
return this.instance return this.instance
.delete<T>(url, this.getAxiosConfig()) .delete<T>(url, this.getAxiosConfig())
.then((response) => response.data) .then((response) => response.data)

View File

@ -49,6 +49,11 @@ export class RepacksManager {
.map((index) => this.repacks[index]); .map((index) => this.repacks[index]);
} }
public static findRepacksForCatalogueEntry(entry: CatalogueEntry) {
const repacks = this.search({ query: formatName(entry.title) });
return { ...entry, repacks };
}
public static findRepacksForCatalogueEntries(entries: CatalogueEntry[]) { public static findRepacksForCatalogueEntries(entries: CatalogueEntry[]) {
return entries.map((entry) => { return entries.map((entry) => {
const repacks = this.search({ query: formatName(entry.title) }); const repacks = this.search({ query: formatName(entry.title) });

View File

@ -1,36 +1,15 @@
import { SteamGame } from "@types"; import { SteamGame } from "@types";
import { orderBy, slice } from "lodash-es"; import { slice } from "lodash-es";
import flexSearch from "flexsearch";
import fs from "node:fs"; import fs from "node:fs";
import { formatName } from "@shared";
import { workerData } from "node:worker_threads"; import { workerData } from "node:worker_threads";
const steamGamesIndex = new flexSearch.Index({
tokenize: "reverse",
});
const { steamGamesPath } = workerData; const { steamGamesPath } = workerData;
const data = fs.readFileSync(steamGamesPath, "utf-8"); const data = fs.readFileSync(steamGamesPath, "utf-8");
const steamGames = JSON.parse(data) as SteamGame[]; const steamGames = JSON.parse(data) as SteamGame[];
for (let i = 0; i < steamGames.length; i++) {
const steamGame = steamGames[i];
const formattedName = formatName(steamGame.name);
steamGamesIndex.add(i, formattedName);
}
export const search = (options: flexSearch.SearchOptions) => {
const results = steamGamesIndex.search(options);
const games = results.map((index) => steamGames[index]);
return orderBy(games, ["name"], ["asc"]);
};
export const getById = (id: number) => export const getById = (id: number) =>
steamGames.find((game) => game.id === id); steamGames.find((game) => game.id === id);

View File

@ -6,7 +6,7 @@ import type { CatalogueEntry } from "@types";
import type { DebouncedFunc } from "lodash"; import type { DebouncedFunc } from "lodash";
import { debounce } from "lodash"; import { debounce } from "lodash";
import { InboxIcon } from "@primer/octicons-react"; import { InboxIcon, SearchIcon } from "@primer/octicons-react";
import { clearSearch } from "@renderer/features"; import { clearSearch } from "@renderer/features";
import { useAppDispatch } from "@renderer/hooks"; import { useAppDispatch } from "@renderer/hooks";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
@ -25,8 +25,10 @@ export function SearchResults() {
const [searchResults, setSearchResults] = useState<CatalogueEntry[]>([]); const [searchResults, setSearchResults] = useState<CatalogueEntry[]>([]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [showTypingMessage, setShowTypingMessage] = useState(false);
const debouncedFunc = useRef<DebouncedFunc<() => void> | null>(null); const debouncedFunc = useRef<DebouncedFunc<() => void> | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);
const navigate = useNavigate(); const navigate = useNavigate();
@ -38,21 +40,64 @@ export function SearchResults() {
useEffect(() => { useEffect(() => {
setIsLoading(true); setIsLoading(true);
if (debouncedFunc.current) debouncedFunc.current.cancel(); if (debouncedFunc.current) debouncedFunc.current.cancel();
if (abortControllerRef.current) abortControllerRef.current.abort();
const abortController = new AbortController();
abortControllerRef.current = abortController;
debouncedFunc.current = debounce(() => { debouncedFunc.current = debounce(() => {
const query = searchParams.get("query") ?? "";
if (query.length < 3) {
setIsLoading(false);
setShowTypingMessage(true);
setSearchResults([]);
return;
}
setShowTypingMessage(false);
window.electron window.electron
.searchGames(searchParams.get("query") ?? "") .searchGames(query)
.then((results) => { .then((results) => {
if (abortController.signal.aborted) return;
setSearchResults(results); setSearchResults(results);
setIsLoading(false);
}) })
.finally(() => { .catch(() => {
setIsLoading(false); setIsLoading(false);
}); });
}, 300); }, 500);
debouncedFunc.current(); debouncedFunc.current();
}, [searchParams, dispatch]); }, [searchParams, dispatch]);
const noResultsContent = () => {
if (isLoading) return null;
if (showTypingMessage) {
return (
<div className={styles.noResults}>
<SearchIcon size={56} />
<p>{t("start_typing")}</p>
</div>
);
}
if (searchResults.length === 0) {
return (
<div className={styles.noResults}>
<InboxIcon size={56} />
<p>{t("no_results")}</p>
</div>
);
}
return null;
};
return ( return (
<SkeletonTheme baseColor={vars.color.background} highlightColor="#444"> <SkeletonTheme baseColor={vars.color.background} highlightColor="#444">
<section className={styles.content}> <section className={styles.content}>
@ -75,13 +120,7 @@ export function SearchResults() {
)} )}
</section> </section>
{!isLoading && searchResults.length === 0 && ( {noResultsContent()}
<div className={styles.noResults}>
<InboxIcon size={56} />
<p>{t("no_results")}</p>
</div>
)}
</section> </section>
</SkeletonTheme> </SkeletonTheme>
); );