Compare commits

..

50 Commits

Author SHA1 Message Date
Zamitto
d8b59cae05 feat: differ staging cookie
Some checks failed
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-latest) (push) Has been cancelled
2024-12-09 19:29:31 -03:00
Zamitto
45f2727415 chore: bump version 2024-12-09 17:58:46 -03:00
Zamitto
7e44b3dedb Merge pull request #1282 from hydralauncher/fix/intercepting-cookies
Fix/intercepting cookies
2024-12-09 17:57:30 -03:00
Zamitto
0dea700479 feat: adjustments on clear button 2024-12-09 17:26:20 -03:00
Zamitto
56247eaf7b Merge branch 'main' into fix/intercepting-cookies 2024-12-09 16:52:47 -03:00
Zamitto
590a15e534 Merge pull request #1254 from JarEXE/feature/clearpaths
Some checks are pending
Release / build (ubuntu-latest) (push) Waiting to run
Release / build (windows-latest) (push) Waiting to run
feat: allow clearing game executable path and wine prefix
2024-12-09 16:51:18 -03:00
Zamitto
fb1cc9b82c Merge branch 'main' into feature/clearpaths 2024-12-09 16:29:19 -03:00
Zamitto
7595f19af3 feat: remove console log 2024-12-08 22:50:39 -03:00
Zamitto
e978d84f5f feat: intercepting cookies 2024-12-08 22:21:20 -03:00
Zamitto
0157d546e4 chore: bump version
Some checks are pending
Release / build (ubuntu-latest) (push) Waiting to run
Release / build (windows-latest) (push) Waiting to run
2024-12-08 10:57:39 -03:00
Chubby Granny Chaser
4f5dc51a68 Merge pull request #1281 from hydralauncher/fix/fixing-local-images
fix: fixing local images
2024-12-08 13:17:25 +00:00
Chubby Granny Chaser
938650dbde Merge branch 'main' into fix/fixing-local-images 2024-12-08 13:15:52 +00:00
Chubby Granny Chaser
bae9e57fcd fix: fixing local images 2024-12-08 13:12:38 +00:00
Zamitto
6a673f0c6b Merge pull request #1280 from hydralauncher/fix/headers-and-logs
Some checks are pending
Release / build (ubuntu-latest) (push) Waiting to run
Release / build (windows-latest) (push) Waiting to run
Fix/headers and logs
2024-12-08 01:26:55 -03:00
Zamitto
a962d839a8 fix: chatwoot and featurebase headers 2024-12-08 01:04:17 -03:00
Zamitto
173fb41e63 feat: refactor error logs 2024-12-08 00:59:53 -03:00
Zamitto
26cbeee5af Merge pull request #1277 from GearCzech/main
Updated czech translation [translation]
2024-12-08 00:42:33 -03:00
Zamitto
f6b5263814 Merge branch 'main' into main 2024-12-08 00:29:40 -03:00
Zamitto
3b7ddd0170 Merge pull request #1279 from bankov4eto/main
Some checks are pending
Release / build (ubuntu-latest) (push) Waiting to run
Release / build (windows-latest) (push) Waiting to run
PR: [translation]
2024-12-07 10:36:18 -03:00
bankov4eto
600cbfe861 Update translation.json
Updated translation and corrections
2024-12-07 10:57:02 +02:00
bankov4eto
994f4568a4 Update translation.json
Updated translations and corrections
2024-12-07 10:50:06 +02:00
Gear
1de973ebd0 Fixed by prettier now 2024-12-05 22:36:12 +01:00
Gear
4f6d3d7057 fixed the last line 2024-12-05 22:32:31 +01:00
Gear
124e38c782 Updated czech translation 2024-12-05 22:13:28 +01:00
Zamitto
c9eed85a00 Merge pull request #1275 from hydralauncher/fix/set-userDetails-after-login
Some checks failed
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-latest) (push) Has been cancelled
fix: set window.userDetails after login
2024-12-04 21:11:13 -03:00
Zamitto
988c01f506 fix: set window.userDetails after login 2024-12-04 12:11:14 -03:00
Zamitto
5450443022 Merge pull request #1274 from hydralauncher/chore/remove-intercom-dependency
Some checks are pending
Release / build (ubuntu-latest) (push) Waiting to run
Release / build (windows-latest) (push) Waiting to run
chore: remove intercom dependency
2024-12-04 09:04:35 -03:00
Zamitto
590a1b354d chore: remove intercom dependency 2024-12-04 01:57:47 -03:00
Zamitto
2dcb629c2d Merge pull request #1269 from Lianela/main
Some checks failed
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-latest) (push) Has been cancelled
feature: updated spanish translation
2024-12-02 23:59:48 -03:00
Lianela
96feaf8d42 Update translation.json 2024-12-02 17:43:00 -06:00
Lianela
4cb3258e17 feat: updated spanish translation
added missing strings
2024-12-02 17:38:13 -06:00
Chubby Granny Chaser
9b9b3f73d0 fix: fixing data: links on CSP
Some checks are pending
Release / build (ubuntu-latest) (push) Waiting to run
Release / build (windows-latest) (push) Waiting to run
2024-12-02 20:37:43 +00:00
Chubby Granny Chaser
de36965017 fix: prevent loading external resources twice 2024-12-02 19:37:32 +00:00
Chubby Granny Chaser
e9fddd2456 Merge branch 'main' of github.com:hydralauncher/hydra 2024-12-02 19:13:41 +00:00
Chubby Granny Chaser
8b4791f1f4 chore: bump version 2024-12-02 19:12:39 +00:00
Chubby Granny Chaser
5f9397f6db Merge branch 'main' into feature/clearpaths 2024-12-02 18:47:19 +00:00
Chubby Granny Chaser
870e45991b Merge pull request #1268 from hydralauncher/fix/migrating-hltb
Fix/migrating hltb
2024-12-02 18:47:08 +00:00
Chubby Granny Chaser
38e94c92d7 fix: cache-busting external resources 2024-12-02 18:44:03 +00:00
Chubby Granny Chaser
93fbf7657d fix: returning hydra api call directly 2024-12-02 18:15:45 +00:00
Chubby Granny Chaser
0fc6d69851 fix: migrating hltb to api 2024-12-02 17:58:13 +00:00
Chubby Granny Chaser
7f600a0cbf feat: adding csp update 2024-12-02 17:10:13 +00:00
Zamitto
5bc424796a Merge pull request #1219 from hydralauncher/feat/intercom-user-id
Some checks failed
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-latest) (push) Has been cancelled
feat: add intercom user id
2024-11-28 11:27:57 -03:00
Zamitto
7f33b63bed Merge branch 'main' into feat/intercom-user-id 2024-11-28 09:55:15 -03:00
Zamitto
57e0fb493f Merge pull request #1261 from hydralauncher/fix/correct-subscription-date-validation
Some checks are pending
Release / build (ubuntu-latest) (push) Waiting to run
Release / build (windows-latest) (push) Waiting to run
fix: subscription date validation
2024-11-27 13:37:32 -03:00
Zamitto
730ea4f2b9 fix: subscription date validation 2024-11-27 13:27:13 -03:00
jarome
948965dda5 QoL allow clearing game executable path and wine prefix 2024-11-23 21:39:45 -03:00
Zamitto
8cfe5b4d34 fix: add friend's pass to format name 2024-11-11 10:35:33 -03:00
Zamitto
a2e41b81a3 Merge branch 'main' into feat/intercom-user-id 2024-11-10 23:38:07 -03:00
Zamitto
6e6469d90f fix: format ignore case 2024-11-10 20:55:23 -03:00
Zamitto
2828640ed7 feat: add intercom user id 2024-11-09 02:54:23 -03:00
40 changed files with 358 additions and 487 deletions

View File

@@ -1,4 +1,3 @@
MAIN_VITE_API_URL=API_URL
MAIN_VITE_AUTH_URL=AUTH_URL
MAIN_VITE_STEAMGRIDDB_API_KEY=YOUR_API_KEY
RENDERER_VITE_INTERCOM_APP_ID=YOUR_APP_ID

View File

@@ -22,16 +22,6 @@ jobs:
with:
node-version: 20.18.0
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.R2_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.R2_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Push build to R2
run: aws s3 sync ./docs s3://${{ vars.BUILDS_BUCKET_NAME }}
- name: Install dependencies
run: yarn
@@ -58,6 +48,7 @@ jobs:
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.RENDERER_VITE_EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build Windows
@@ -69,6 +60,7 @@ jobs:
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.RENDERER_VITE_EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create artifact

View File

@@ -47,6 +47,7 @@ jobs:
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.RENDERER_VITE_EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build Windows
if: matrix.os == 'windows-latest'
@@ -57,6 +58,7 @@ jobs:
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_URL }}
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.RENDERER_VITE_EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create artifact
uses: actions/upload-artifact@v4

View File

@@ -1,183 +0,0 @@
<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 è un game launcher con il proprio client bittorrent e autogestore di repacks.</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)
[![be](https://img.shields.io/badge/lang-be-orange)](README.be.md)
[![pl](https://img.shields.io/badge/lang-pl-white)](README.pl.md)
[![pt-BR](https://img.shields.io/badge/lang-pt--BR-green.svg)](README.pt-BR.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)
[![es](https://img.shields.io/badge/lang-es-red)](README.es.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
![Hydra Catalogue](./docs/screenshot.png)
</div>
## Table of Contents
- [A proposito](#a-proposito)
- [Caratteristiche](#caratteristiche)
- [Installazione](#installazione)
- [Contribuire](#contribuire)
- [Uscisciti su Telegram](#unisciti-su-telegram)
- [Forka e Clona la tua repository](#forka-e-clona-la-tua-repository)
- [Modi in cui contribuire](#modi-in-cui-contribuire)
- [Struttura del Progetto](#struttura-del-progetto)
- [Compila il codice sorgente](#compila-il-codice-sorgente)
- [Installa Node.js](#installa-nodejs)
- [Installa Yarn](#installa-yarn)
- [Installa le Node Dependencies](#installa-node-dependencies)
- [Installa Python 3.9](#installa-python-39)
- [Installa le Python Dependencies](#installa-python-dependencies)
- [Variabili d'Ambiente](#variabili-di-ambiente)
- [Esecuzione](#esecuzione)
- [Compilazione](#compilazione)
- [Compilare il client bittorrent](#compilazione-client-bittorrent)
- [Compilare l'applicazione Electron](#compilazione-applicazione-electron)
- [Collaboratori](#collaboratori)
## A proposito
**Hydra** è un **Game Launcher** con il proprio **Client BitTorrent** e **autogestore di repack**.
<br>
Il launcher è scritto in TypeScript (Electron) and Python, che gestisce il sistema di torrenting appoggiandosi a libtorrent.
## Caratteristiche
- Motore di ricerca automatizzato sulle fonti di repack dal [Megathread]("https://www.reddit.com/r/Piracy/wiki/megathread/")
- Client Bittorrent integrato
- Integrazione How Long To Beat (HLTB) nella pagina del gioco
- Percorso del download Personalizzato
- Notifiche di aggiornamenti sulla list dei repacks
- Supporto Windows e Linux
- Costantemente Aggiornato
- E molto altro ...
## Installazione
Segui i seguenti passi:
1. Scarica l'ultima versione di Hydra dalla pagina [Releases](https://github.com/hydralauncher/hydra/releases/latest).
- Scarica solo il file .exe per installare Hydra su Windows.
- Scarica il file .deb o .rpm o .zip per Linux. (Dipende dalla tua distro Linux)
2. Esegui il file scaricato.
3. Goditi Hydra!
## <a name="contribuire"> Contribuire
### <a name="unisciti-su-telegram"></a> Unisciti su Telegram
Puoi unirti alle nostre conversazioni sul canale [Telegram](https://t.me/hydralauncher).
### Forka e Clona la repository
1. Forka la repository [(clicca qui per forkare)](https://github.com/hydralauncher/hydra/fork)
2. Clona il tuo codice forkato `git clone https://github.com/your_username/hydra`
3. Crea un nuovo branch
4. Aggiungi le modifiche (push)
5. Invia la richiesta di pull
### Modi in cui contribuire
- Traduzione: Vogliamo rendere Hydra disponibile a più persone possibile. Sentiti libero di tradurre in altre lingue o aggiornare e migliorare quelle già disponibili su Hydra.
- Programmazione: Hydra è programmato in TypeScript, Electron e un po' di Python. Se intendi contribuire unisciti al nostro [Telegram](https://t.me/hydralauncher)!
### Struttura del Progetto
- client-torrent: Usiamo libtorrent, una libreria Python, per gestire i download dei torrent
- src/renderer: l'UI dell'applicazione
- src/main: tutta la logica qui.
## Compilazione
### Installa Node.js
Assicurati di avere Node.js installato sulla tua macchina. Scaricalo e installalo da [nodejs.org](https://nodejs.org/).
### Installa Yarn
Yarn è un gestore di pacchetti per Node.js. Se non hai ancora installato Yarn segui le istruzioni su [yarnpkg.com](https://classic.yarnpkg.com/lang/en/docs/install/).
### Installa le dipendenze Node
Naviga alla cartella del progetto e installa le dipendenze Node con Yarn:
```bash
cd hydra
yarn
```
### Installa Python 3.9
Assicurati di avere Python 3.9 installato. Puoi scaricarlo da [python.org](https://www.python.org/downloads/release/python-3913/).
### Installa le Dipendenze Python
Installa le dipendenze con pip:
```bash
pip install -r requirements.txt
```
## Variabili d'ambiente
Avrai bisogno di una chiave API SteamGridDB per poter caricare le icone di gioco.
Se intendi avere onlinefix come repacker dovrai aggiungere le tue credenziali al file .env
Una volta ottenuta, puoi copiare e rinominare il file `.env.example` a `.env` e metterlo in `STEAMGRIDDB_API_KEY`, `ONLINEFIX_USERNAME`, `ONLINEFIX_PASSWORD`.
## Esecuzione
Una volta impostato tutto, puoi eseguire il seguente comando per avviare il processo Electron e il client bittorrent:
```bash
yarn dev
```
## Compilazione
### Compila il bittorrent
Usa il comando:
```bash
python torrent-client/setup.py build
```
### Compila l'applicazione Electron
Usa il comando:
Per Windows:
```bash
yarn build:win
```
Per Linux:
```bash
yarn build:linux
```
## Collaboratori
<a href="https://github.com/hydralauncher/hydra/graphs/contributors">
<img src="https://contrib.rocks/image?repo=hydralauncher/hydra" />
</a>
## Licenza
Hydra è concesso in licenza secondo la [MIT License](LICENSE).

View File

@@ -1,6 +1,6 @@
{
"name": "hydralauncher",
"version": "3.0.5",
"version": "3.0.8",
"description": "Hydra",
"main": "./out/main/index.js",
"author": "Los Broxas",
@@ -36,7 +36,6 @@
"@electron-toolkit/utils": "^3.0.0",
"@fontsource/noto-sans": "^5.0.22",
"@hookform/resolvers": "^3.9.0",
"@intercom/messenger-js-sdk": "^0.0.14",
"@primer/octicons-react": "^19.9.0",
"@reduxjs/toolkit": "^2.2.3",
"@vanilla-extract/css": "^1.14.2",

View File

@@ -29,7 +29,7 @@
"need_help": "Имате нужда от помощ??"
},
"header": {
"search": "Търси игри",
"search": "Търсене",
"home": "Начало",
"catalogue": "Каталог",
"downloads": "Изтегляния",
@@ -65,7 +65,7 @@
"calculating_eta": "Калкулиране на оставащо време…",
"downloading_metadata": "Изтегляне на метадата…",
"filter": "Филтрирай repacks",
"requirements": "Състемни изисквания",
"requirements": "Системни изисквания",
"minimum": "Минимални",
"recommended": "Препоръчителни",
"paused": "Паузирано",
@@ -79,8 +79,8 @@
"add_to_library": "Добави в библиотеката",
"remove_from_library": "Премахни от библиотеката",
"no_downloads": "Няма налични изтегляния",
"play_time": "Играно {{amount}}",
"last_time_played": "Последно играно {{period}}",
"play_time": "Игрално време {{amount}}",
"last_time_played": "Последно пускане {{period}}",
"not_played_yet": "Не сте играли {{title}} все още",
"next_suggestion": "Следващо предложение",
"play": "Пускане",
@@ -110,7 +110,7 @@
"remove_from_library_description": "Това ще премахне {{game}} от Библиотеката",
"options": "Опции",
"executable_section_title": "Стартиращ файл",
"executable_section_description": "Пътят на файла, който ще се изпълни, когато се щракне върху \"Играй\"",
"executable_section_description": "Пътят на файла, който ще се изпълни, когато се щракне върху \"Пускане\"",
"downloads_secion_title": "Свалени",
"downloads_section_description": "Вижте актуализации или други версии на тази игра",
"danger_zone_section_title": "Опасна зона",
@@ -162,7 +162,7 @@
"no_download_option_info": "Няма налични данни",
"backup_deletion_failed": "Неуспешно изтриване на резервно копие",
"max_number_of_artifacts_reached": "Достигнат максимален брой резервни копия за тази игра",
"achievements_not_sync": "Постиженията ви не са синхронизирани",
"achievements_not_sync": "Постиженията не са синхронизирани",
"manage_files_description": "Управлявайте кои файлове ще бъдат архивирани и възстановени",
"select_folder": "Избери папка",
"backup_from": "Резервно копие от {{date}}",
@@ -198,7 +198,7 @@
"downloads_completed": "Приключени",
"queued": "В опашка",
"no_downloads_title": "Толкова е празно",
"no_downloads_description": "Все още не сте изтеглили нищо с Hydra, но никога не е късно да започнете..",
"no_downloads_description": "Все още не сте изтеглили нищо с Hydra, но никога не е късно да започнете...",
"checking_files": "Проверка на файлове…"
},
"settings": {
@@ -331,7 +331,7 @@
"blocked_users": "Блокирани потребители",
"unblock": "Отблокирай",
"no_friends_added": "Не сте добавили приятели",
"pending": "Чакащо",
"pending": "Чакащи",
"no_pending_invites": "Нямате чакащи покани",
"no_blocked_users": "Нямате блокирани потребители",
"friend_code_copied": "Приятелския код е копиран",

View File

@@ -6,7 +6,11 @@
"home": {
"featured": "Doporučené",
"surprise_me": "Překvap mě",
"no_results": "Výsledek nenalezen"
"no_results": "Výsledek nenalezen",
"start_typing": "Začni psát pro vyhledávání...",
"hot": "Teď populární",
"weekly": "📅 Nejlepší hry týdne",
"achievements": "🏆 Hry k překonání"
},
"sidebar": {
"catalogue": "Katalog",
@@ -20,7 +24,9 @@
"home": "Domov",
"queued": "{{title}} (V řadě)",
"game_has_no_executable": "Hra nemá zvolen žádný spustitelný soubor",
"sign_in": "Přihlásit se"
"sign_in": "Přihlásit se",
"friends": "Přátelé",
"need_help": "Potřebujete pomoc?"
},
"header": {
"search": "Vyhledat hry",
@@ -113,7 +119,54 @@
"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"
"create_shortcut_error": "Chyba při pokusu vytvořit zástupce",
"nsfw_content_title": "Tahle hra obsahuje nevhodný obsah",
"nsfw_content_description": "{{title}} obsahuje obsah, který by nemusel být vhodný pro všechny věkové skupiny. Jste si jisti, že chcete pokračovat?",
"allow_nsfw_content": "Pokračovat",
"refuse_nsfw_content": "Jít zpět",
"stats": "Statistiky",
"download_count": "Stažení",
"player_count": "Aktivní hráči",
"download_error": "Tahle možnost stažení není dostupná",
"download": "Stáhnout",
"executable_path_in_use": "Spustitelný soubor již používá \"{{game}}\"",
"warning": "Varování",
"hydra_needs_to_remain_open": "Pro tohle stažení, musí Hydra zůstat otevřená až do konce stahování. Pokud Hydru zavřete dříve, postup stahování bude ztracen.",
"achievements": "Achievementy",
"achievements_count": "Achievementy {{unlockedCount}}/{{achievementsCount}}",
"cloud_save": "Uložení v cloudu",
"cloud_save_description": "Uložte si svůj postup v cloud a pokračujte v hraní na jakémkoliv zářízení",
"backups": "Zálohy",
"install_backup": "Nainstalovat",
"delete_backup": "Smazat",
"create_backup": "Vytvořit zálohu",
"last_backup_date": "Poslední záloha vytvořena {{date}}",
"no_backup_preview": "Žádné zálohy nebyly nalezeny pro tuhle hru",
"restoring_backup": "Obnovuji zálohu ({{progress}} hotovo)...",
"uploading_backup": "Nahrávání zálohy...",
"no_backups": "Nemáte zatím vytvořeny žádné zálohy pro tuto hru",
"backup_uploaded": "Záloha nahrána",
"backup_deleted": "Záloha odstraněna",
"backup_restored": "Záloha obnovena",
"see_all_achievements": "Zobrazit všechny achievementy",
"sign_in_to_see_achievements": "Musíte se přihlásit pro zobrazení achievementů",
"mapping_method_automatic": "Automaticky",
"mapping_method_manual": "Manuálně",
"mapping_method_label": "Metoda mapování",
"files_automatically_mapped": "Soubory automaticky zmapovány",
"no_backups_created": "Žádné zálohy nebyly vytvořeny pro tuto hru",
"manage_files": "Spravovat soubory",
"loading_save_preview": "Hledání uložených her...",
"wine_prefix": "Wine Prefix",
"wine_prefix_description": "Wine Prefix použit pro spuštění této hry",
"no_download_option_info": "Žádné informace nejsou dostupny",
"backup_deletion_failed": "Nepovedlo se odstranit zálohu",
"max_number_of_artifacts_reached": "Dosáhli jste maximálního počtu záloh pro tuto hru",
"achievements_not_sync": "Vaše achievementy nejsou synchronizovány",
"manage_files_description": "Spravovat, které soubory budou zálohovány a obnoveny",
"select_folder": "Vybrat složku",
"backup_from": "Zálohy z {{date}}",
"custom_backup_location_set": "Vlastní umístění záloh nastaveno"
},
"activation": {
"title": "Aktivovat hydru",
@@ -189,7 +242,21 @@
"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"
"import": "Importovat",
"public": "Veřejné",
"private": "Soukromé",
"friends_only": "Pouze přátelé",
"privacy": "Soukromí",
"profile_visibility": "Viditelnost profilu",
"profile_visibility_description": "Vyberte si, kdo může vidět váš profil a knihovnu",
"required_field": "Toto pole je povinné",
"source_already_exists": "Tento zdroj byl již přidán",
"must_be_valid_url": "Zdroj musí být platký odkaz URL",
"blocked_users": "Zablokovaní uživatelé",
"user_unblocked": "Uživatel byl odblokován",
"enable_achievement_notifications": "Když je odemknut achievement",
"launch_minimized": "Spustit v minimalizovaném režimu",
"disable_nsfw_alert": "Deaktivovat upozornění na nevhodný obsah"
},
"notifications": {
"download_complete": "Stahování dokončeno",
@@ -198,7 +265,9 @@
"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"
"restart_to_install_update": "Restartuj Hydru pro aktualizaci",
"notification_achievement_unlocked_title": "Achievement pro {{game}} byl odemknut",
"notification_achievement_unlocked_body": "{{achievement}} a dalších {{count}} byly odemknuty"
},
"system_tray": {
"open": "Otevřít Hydru",
@@ -266,6 +335,47 @@
"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}}"
"undo_friendship_modal_text": "Tímto zrušíte své přátelství s {{displayName}}",
"privacy_hint": "Pro změnu toho, kdo tohle může vidět, jděte do <0>Nastavení</0>",
"locked_profile": "Tento profil je soukromý",
"image_process_failure": "Nastala chyba při zpracování obrázku",
"required_field": "Toto pole je povinné",
"displayname_min_length": "Uživatelské jméno musí být minimálně 3 znaky dlouhé",
"displayname_max_length": "Uživatelské jméno musí být maximálně 50 znaků dlouhé",
"report_profile": "Nahlásit profil",
"report_reason": "Proč nahlašujete tento profil?",
"report_description": "Přídavné informace",
"report_description_placeholder": "Přídavné informace",
"report": "Nahlásit",
"report_reason_hate": "Nenávistné projevy",
"report_reason_sexual_content": "Sexuální obsah",
"report_reason_violence": "Násilí",
"report_reason_spam": "Spam",
"report_reason_other": "Ostatní",
"profile_reported": "Profil nahlášen",
"your_friend_code": "Tvůj kód přítele:",
"upload_banner": "Nahrát banner profilu",
"uploading_banner": "Nahrávání banneru",
"background_image_updated": "Obrázek pozadí byl změněn"
},
"achievement": {
"achievement_unlocked": "Achievement odemčen",
"user_achievements": "Achievementy uživatele {{displayName}}",
"your_achievements": "Vaše achievementy",
"unlocked_at": "Odemčeno:",
"subscription_needed": "Je vyžadováno předplatné Hydra Cloud pro zobrazení tohoto obsahu",
"new_achievements_unlocked": "Odemčeno {{achievementCount}} nových achievementů z {{gameCount}} her",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} achievementů",
"achievements_unlocked_for_game": "Odemčeno {{achievementCount}} nových achievementů pro {{gameTitle}}"
},
"tour": {
"subscription_tour_title": "Předplatné Hydra Cloud",
"subscribe_now": "Připojit se",
"cloud_saving": "Ukládání v cloudu",
"cloud_achievements": "Ukládejte vaše achievementy do cloudu",
"animated_profile_picture": "Animované profilové obrázky",
"premium_support": "Prémiová podpora",
"show_and_compare_achievements": "Zobraz a porovnej achievementy s ostatními uživateli",
"animated_profile_banner": "Animovaný banner na profilu"
}
}

View File

@@ -105,6 +105,7 @@
"open_folder": "Open folder",
"open_download_location": "See downloaded files",
"create_shortcut": "Create desktop shortcut",
"clear": "Clear",
"remove_files": "Remove files",
"remove_from_library_title": "Are you sure?",
"remove_from_library_description": "This will remove {{game}} from your library",
@@ -166,7 +167,8 @@
"manage_files_description": "Manage which files will be backed up and restored",
"select_folder": "Select folder",
"backup_from": "Backup from {{date}}",
"custom_backup_location_set": "Custom backup location set"
"custom_backup_location_set": "Custom backup location set",
"no_directory_selected": "No directory selected"
},
"activation": {
"title": "Activate Hydra",

View File

@@ -100,7 +100,7 @@
"open_screenshot": "Abrir captura {{number}}",
"download_settings": "Ajustes de descarga",
"downloader": "Método de descarga",
"select_executable": "Seleccionar ejecutable",
"select_executable": "Seleccionar",
"no_executable_selected": "No se seleccionó un ejecutable",
"open_folder": "Abrir carpeta",
"open_download_location": "Ver archivos descargados",
@@ -166,7 +166,9 @@
"manage_files_description": "Gestiona los archivos que serán respaldados y restaurados",
"select_folder": "Seleccionar carpeta",
"backup_from": "Copia de seguridad de {{date}}",
"custom_backup_location_set": "Se configuró la carpeta de copia de seguridad"
"custom_backup_location_set": "Se configuró la carpeta de copia de seguridad",
"clear": "Limpiar",
"no_directory_selected": "No se seleccionó un directório"
},
"activation": {
"title": "Activar Hydra",
@@ -254,7 +256,9 @@
"must_be_valid_url": "La fuente debe ser una URL válida.",
"blocked_users": "Usuarios bloqueados",
"user_unblocked": "El usuario ha sido desbloqueado",
"enable_achievement_notifications": "Cuando un logro se desbloquea"
"enable_achievement_notifications": "Cuando un logro se desbloquea",
"launch_minimized": "Iniciar Hydra minimizado",
"disable_nsfw_alert": "Desactivar alerta NSFW"
},
"notifications": {
"download_complete": "Descarga completada",
@@ -361,8 +365,10 @@
"user_achievements": "Logros de {{displayName}}",
"your_achievements": "Tus Logros",
"unlocked_at": "Desbloqueado el:",
"subscription_needed": "Se necesita una suscripción a Hydra Cloud se necesita para ver este contenido",
"new_achievements_unlocked": "Desbloqueados {{achievementCount}} nuevos logros de {{gameCount}} juegos"
"subscription_needed": "Se necesita una suscripción a Hydra Cloud necesita para ver este contenido",
"new_achievements_unlocked": "Desbloqueados {{achievementCount}} nuevos logros de {{gameCount}} juegos",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} logros",
"achievements_unlocked_for_game": "Se han desbloqueado {{achievementCount}} nuevos logros de {{gameTitle}}"
},
"tour": {
"subscription_tour_title": "Suscripción Hydra Cloud",

View File

@@ -162,7 +162,9 @@
"backup_from": "Backup de {{date}}",
"custom_backup_location_set": "Localização customizada selecionada",
"select_folder": "Selecione a pasta",
"manage_files_description": "Gerencie quais arquivos serão feitos backup"
"manage_files_description": "Gerencie quais arquivos serão feitos backup",
"clear": "Limpar",
"no_directory_selected": "Nenhum diretório selecionado"
},
"activation": {
"title": "Ativação",

View File

@@ -1,23 +1,21 @@
import type { HowLongToBeatCategory } from "@types";
import { getHowLongToBeatGame, searchHowLongToBeat } from "@main/services";
import type { GameShop, HowLongToBeatCategory } from "@types";
import { registerEvent } from "../register-event";
import { formatName } from "@shared";
import { HydraApi } from "@main/services";
const getHowLongToBeat = async (
_event: Electron.IpcMainInvokeEvent,
title: string
objectId: string,
shop: GameShop
): Promise<HowLongToBeatCategory[] | null> => {
const response = await searchHowLongToBeat(title);
const game = response.data.find((game) => {
return formatName(game.game_name) === formatName(title);
const params = new URLSearchParams({
objectId,
shop,
});
if (!game) return null;
const howLongToBeat = await getHowLongToBeatGame(String(game.game_id));
return howLongToBeat;
return HydraApi.get(`/games/how-long-to-beat?${params.toString()}`, null, {
needsAuth: false,
});
};
registerEvent("getHowLongToBeat", getHowLongToBeat);

View File

@@ -1,4 +1,4 @@
import { appVersion, defaultDownloadsPath } from "@main/constants";
import { appVersion, defaultDownloadsPath, isStaging } from "@main/constants";
import { ipcMain } from "electron";
import "./catalogue/get-catalogue";
@@ -72,5 +72,6 @@ import "./misc/show-item-in-folder";
ipcMain.handle("ping", () => "pong");
ipcMain.handle("getVersion", () => appVersion);
ipcMain.handle("isStaging", () => isStaging);
ipcMain.handle("isPortableVersion", () => isPortableVersion());
ipcMain.handle("getDefaultDownloadsPath", () => defaultDownloadsPath);

View File

@@ -5,9 +5,9 @@ import { registerEvent } from "../register-event";
const selectGameWinePrefix = async (
_event: Electron.IpcMainInvokeEvent,
id: number,
winePrefixPath: string
winePrefixPath: string | null
) => {
return gameRepository.update({ id }, { winePrefixPath });
return gameRepository.update({ id }, { winePrefixPath: winePrefixPath });
};
registerEvent("selectGameWinePrefix", selectGameWinePrefix);

View File

@@ -6,14 +6,18 @@ import { parseExecutablePath } from "../helpers/parse-executable-path";
const updateExecutablePath = async (
_event: Electron.IpcMainInvokeEvent,
id: number,
executablePath: string
executablePath: string | null
) => {
const parsedPath = executablePath
? parseExecutablePath(executablePath)
: null;
return gameRepository.update(
{
id,
},
{
executablePath: parseExecutablePath(executablePath),
executablePath: parsedPath,
}
);
};

View File

@@ -1,5 +1,4 @@
import { registerEvent } from "../register-event";
import parseTorrent from "parse-torrent";
import type { StartGameDownloadPayload } from "@types";
import { DownloadManager, HydraApi, logger } from "@main/services";
@@ -9,7 +8,6 @@ import { createGame } from "@main/services/library-sync";
import { steamUrlBuilder } from "@shared";
import { dataSource } from "@main/data-source";
import { DownloadQueue, Game } from "@main/entity";
import { HydraAnalytics } from "@main/services/hydra-analytics";
const startGameDownload = async (
_event: Electron.IpcMainInvokeEvent,
@@ -91,17 +89,6 @@ const startGameDownload = async (
logger.error("Failed to create game download", err);
});
if (uri.startsWith("magnet:")) {
try {
const { infoHash } = await parseTorrent(payload.uri);
if (infoHash) {
HydraAnalytics.postDownload(infoHash).catch(() => {});
}
} catch (err) {
logger.error("Failed to parse torrent", err);
}
}
await DownloadManager.cancelDownload(updatedGame!.id);
await DownloadManager.startDownload(updatedGame!);

View File

@@ -174,8 +174,10 @@ export class PythonInstance {
.then((response) => response.data);
}
private static async handleRpcError(_error: unknown) {
await this.rpc.get("/healthcheck").catch(() => {
private static async handleRpcError(error: unknown) {
logger.error(error);
return this.rpc.get("/healthcheck").catch(() => {
logger.error(
"RPC healthcheck failed. Killing process and starting again"
);

View File

@@ -1,108 +0,0 @@
import axios from "axios";
import { requestWebPage } from "@main/helpers";
import type {
HowLongToBeatCategory,
HowLongToBeatSearchResponse,
} from "@types";
import { formatName } from "@shared";
import { logger } from "./logger";
import UserAgent from "user-agents";
const state = {
apiKey: null as string | null,
};
const getHowLongToBeatSearchApiKey = async () => {
const userAgent = new UserAgent();
const document = await requestWebPage("https://howlongtobeat.com/");
const scripts = Array.from(document.querySelectorAll("script"));
const appScript = scripts.find((script) =>
script.src.startsWith("/_next/static/chunks/pages/_app")
);
if (!appScript) return null;
const response = await axios.get(
`https://howlongtobeat.com${appScript.src}`,
{
headers: {
"User-Agent": userAgent.toString(),
},
}
);
const results = /fetch\("\/api\/search\/"\.concat\("(.*?)"\)/gm.exec(
response.data
);
if (!results) return null;
return results[1];
};
export const searchHowLongToBeat = async (gameName: string) => {
state.apiKey = state.apiKey ?? (await getHowLongToBeatSearchApiKey());
if (!state.apiKey) return { data: [] };
const userAgent = new UserAgent();
const response = await axios
.post(
`https://howlongtobeat.com/api/search/${state.apiKey}`,
{
searchType: "games",
searchTerms: formatName(gameName).split(" "),
searchPage: 1,
size: 20,
},
{
headers: {
"User-Agent": userAgent.toString(),
Referer: "https://howlongtobeat.com/",
},
}
)
.catch((error) => {
logger.error("Error searching HowLongToBeat:", error?.response?.status);
return { data: { data: [] } };
});
return response.data as HowLongToBeatSearchResponse;
};
const parseListItems = ($lis: Element[]) => {
return $lis.map(($li) => {
const title = $li.querySelector("h4")?.textContent;
const [, accuracyClassName] = Array.from(($li as HTMLElement).classList);
const accuracy = accuracyClassName.split("time_").at(1);
return {
title: title ?? "",
duration: $li.querySelector("h5")?.textContent ?? "",
accuracy: accuracy ?? "",
};
});
};
export const getHowLongToBeatGame = async (
id: string
): Promise<HowLongToBeatCategory[]> => {
const document = await requestWebPage(`https://howlongtobeat.com/game/${id}`);
const $ul = document.querySelector(".shadow_shadow ul");
if (!$ul) return [];
const $lis = Array.from($ul.children);
const [$firstLi] = $lis;
if ($firstLi.tagName === "DIV") {
const $pcData = $lis.find(($li) => $li.textContent?.includes("PC"));
return parseListItems(Array.from($pcData?.querySelectorAll("li") ?? []));
}
return parseListItems($lis);
};

View File

@@ -1,34 +0,0 @@
import { userSubscriptionRepository } from "@main/repository";
import axios from "axios";
import { appVersion } from "@main/constants";
export class HydraAnalytics {
private static instance = axios.create({
baseURL: import.meta.env.MAIN_VITE_ANALYTICS_API_URL,
headers: { "User-Agent": `Hydra Launcher v${appVersion}` },
});
private static async hasActiveSubscription() {
const userSubscription = await userSubscriptionRepository.findOne({
where: { id: 1 },
});
return (
userSubscription?.expiresAt && userSubscription.expiresAt > new Date()
);
}
static async postDownload(hash: string) {
const hasSubscription = await this.hasActiveSubscription();
return this.instance
.post("/track", {
event: "download",
attributes: {
hash,
hasSubscription,
},
})
.then((response) => response.data);
}
}

View File

@@ -12,6 +12,7 @@ import { UserNotLoggedInError, SubscriptionRequiredError } from "@shared";
import { omit } from "lodash-es";
import { appVersion } from "@main/constants";
import { getUserData } from "./user/get-user-data";
import { isFuture, isToday } from "date-fns";
interface HydraApiOptions {
needsAuth?: boolean;
@@ -45,10 +46,8 @@ export class HydraApi {
}
private static hasActiveSubscription() {
return (
this.userAuth.subscription?.expiresAt &&
this.userAuth.subscription.expiresAt > new Date()
);
const expiresAt = this.userAuth.subscription?.expiresAt;
return expiresAt && (isFuture(expiresAt) || isToday(expiresAt));
}
static async handleExternalAuth(uri: string) {
@@ -154,21 +153,26 @@ export class HydraApi {
(error) => {
logger.error(" ---- RESPONSE ERROR -----");
const { config } = error;
const data = JSON.parse(config.data);
logger.error(
config.method,
config.baseURL,
config.url,
config.headers,
config.data
omit(config.headers, ["accessToken", "refreshToken"]),
Array.isArray(data)
? data
: omit(data, ["accessToken", "refreshToken"])
);
if (error.response) {
logger.error(
"Response",
"Response error:",
error.response.status,
error.response.data
);
} else if (error.request) {
logger.error("Request", error.request);
const errorData = error.toJSON();
logger.error("Request error:", errorData.message);
} else {
logger.error("Error", error.message);
}

View File

@@ -4,7 +4,6 @@ export * from "./steam-250";
export * from "./steam-grid";
export * from "./window-manager";
export * from "./download";
export * from "./how-long-to-beat";
export * from "./process-watcher";
export * from "./main-loop";
export * from "./hydra-api";

View File

@@ -44,7 +44,7 @@ export const getUserData = () => {
if (err instanceof UserNotLoggedInError) {
return null;
}
logger.error("Failed to get logged user", err);
logger.error("Failed to get logged user");
const loggedUser = await userAuthRepository.findOne({
where: { id: 1 },
relations: { subscription: true },

View File

@@ -85,7 +85,11 @@ export class WindowManager {
return callback(details);
}
if (details.url.includes("intercom.io")) {
if (details.url.includes("featurebase")) {
return callback(details);
}
if (details.url.includes("chatwoot")) {
return callback(details);
}

View File

@@ -42,8 +42,8 @@ contextBridge.exposeInMainWorld("electron", {
getGameShopDetails: (objectId: string, shop: GameShop, language: string) =>
ipcRenderer.invoke("getGameShopDetails", objectId, shop, language),
getRandomGame: () => ipcRenderer.invoke("getRandomGame"),
getHowLongToBeat: (title: string) =>
ipcRenderer.invoke("getHowLongToBeat", title),
getHowLongToBeat: (objectId: string, shop: GameShop) =>
ipcRenderer.invoke("getHowLongToBeat", objectId, shop),
getGames: (take?: number, skip?: number) =>
ipcRenderer.invoke("getGames", take, skip),
searchGameRepacks: (query: string) =>
@@ -87,9 +87,9 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("addGameToLibrary", objectId, title, shop),
createGameShortcut: (id: number) =>
ipcRenderer.invoke("createGameShortcut", id),
updateExecutablePath: (id: number, executablePath: string) =>
updateExecutablePath: (id: number, executablePath: string | null) =>
ipcRenderer.invoke("updateExecutablePath", id, executablePath),
selectGameWinePrefix: (id: number, winePrefixPath: string) =>
selectGameWinePrefix: (id: number, winePrefixPath: string | null) =>
ipcRenderer.invoke("selectGameWinePrefix", id, winePrefixPath),
verifyExecutablePathInUse: (executablePath: string) =>
ipcRenderer.invoke("verifyExecutablePathInUse", executablePath),
@@ -198,6 +198,7 @@ contextBridge.exposeInMainWorld("electron", {
ping: () => ipcRenderer.invoke("ping"),
getVersion: () => ipcRenderer.invoke("getVersion"),
getDefaultDownloadsPath: () => ipcRenderer.invoke("getDefaultDownloadsPath"),
isStaging: () => ipcRenderer.invoke("isStaging"),
isPortableVersion: () => ipcRenderer.invoke("isPortableVersion"),
openExternal: (src: string) => ipcRenderer.invoke("openExternal", src),
openCheckout: () => ipcRenderer.invoke("openCheckout"),

View File

@@ -6,7 +6,7 @@
<title>Hydra</title>
<meta
http-equiv="Content-Security-Policy"
content="default-src 'self'; script-src *; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: *; media-src 'self' local: data: *; connect-src *; font-src *;"
content="default-src 'self' 'unsafe-inline' * data: local:;"
/>
</head>
<body>

View File

@@ -2,8 +2,6 @@ import { useCallback, useContext, useEffect, useRef } from "react";
import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
import Intercom from "@intercom/messenger-js-sdk";
import {
useAppDispatch,
useAppSelector,
@@ -36,10 +34,6 @@ export interface AppProps {
children: React.ReactNode;
}
Intercom({
app_id: import.meta.env.RENDERER_VITE_INTERCOM_APP_ID,
});
export function App() {
const contentRef = useRef<HTMLDivElement>(null);
const { updateLibrary, library } = useLibrary();
@@ -120,12 +114,21 @@ export function App() {
dispatch(setProfileBackground(profileBackground));
}
fetchUserDetails().then((response) => {
if (response) {
updateUserDetails(response);
syncFriendRequests();
}
});
fetchUserDetails()
.then((response) => {
if (response) {
updateUserDetails(response);
syncFriendRequests();
}
})
.finally(() => {
if (document.getElementById("external-resources")) return;
const $script = document.createElement("script");
$script.id = "external-resources";
$script.src = `${import.meta.env.RENDERER_VITE_EXTERNAL_RESOURCES_URL}?t=${Date.now()}`;
document.head.appendChild($script);
});
}, [fetchUserDetails, syncFriendRequests, updateUserDetails, dispatch]);
const onSignIn = useCallback(() => {
@@ -215,9 +218,7 @@ export function App() {
useEffect(() => {
new MutationObserver(() => {
const modal = document.body.querySelector(
"[role=dialog]:not([data-intercom-frame='true'])"
);
const modal = document.body.querySelector("[data-hydra-dialog]");
dispatch(toggleDraggingDisabled(Boolean(modal)));
}).observe(document.body, {

View File

@@ -107,6 +107,7 @@ export function Modal({
aria-labelledby={title}
aria-describedby={description}
ref={modalContentRef}
data-hydra-dialog
>
<div className={styles.modalHeader}>
<div style={{ display: "flex", gap: 4, flexDirection: "column" }}>

View File

@@ -22,8 +22,6 @@ import { SidebarProfile } from "./sidebar-profile";
import { sortBy } from "lodash-es";
import { CommentDiscussionIcon } from "@primer/octicons-react";
import { show, update } from "@intercom/messenger-js-sdk";
const SIDEBAR_MIN_WIDTH = 200;
const SIDEBAR_INITIAL_WIDTH = 250;
const SIDEBAR_MAX_WIDTH = 450;
@@ -50,20 +48,7 @@ export function Sidebar() {
return sortBy(library, (game) => game.title);
}, [library]);
const { userDetails, hasActiveSubscription } = useUserDetails();
useEffect(() => {
if (userDetails) {
update({
name: userDetails.displayName,
Username: userDetails.username,
email: userDetails.email ?? undefined,
Email: userDetails.email,
"Subscription expiration date": userDetails?.subscription?.expiresAt,
"Payment status": userDetails?.subscription?.status,
});
}
}, [userDetails, hasActiveSubscription]);
const { hasActiveSubscription } = useUserDetails();
const { lastPacket, progress } = useDownload();
@@ -266,7 +251,11 @@ export function Sidebar() {
</div>
{hasActiveSubscription && (
<button type="button" className={styles.helpButton} onClick={show}>
<button
type="button"
className={styles.helpButton}
data-open-support-chat
>
<div className={styles.helpButtonIcon}>
<CommentDiscussionIcon size={14} />
</div>

View File

@@ -181,6 +181,7 @@ export function GameDetailsContextProvider({
shop,
i18n.language,
userDetails,
userPreferences,
]);
useEffect(() => {

View File

@@ -0,0 +1,46 @@
export function addCookieInterceptor(isStaging: boolean) {
const cookieKey = isStaging ? "cookies-staging" : "cookies";
Object.defineProperty(document, "cookie", {
enumerable: true,
configurable: true,
get() {
return localStorage.getItem(cookieKey) || "";
},
set(cookieString) {
try {
const [cookieName, cookieValue] = cookieString.split(";")[0].split("=");
const currentCookies = localStorage.getItem(cookieKey) || "";
const cookiesObject = parseCookieStringsToObjects(currentCookies);
cookiesObject[cookieName] = cookieValue;
const newString = Object.entries(cookiesObject)
.map(([key, value]) => {
return key + "=" + value;
})
.join("; ");
localStorage.setItem(cookieKey, newString);
} catch (err) {
console.error(err);
}
},
});
}
const parseCookieStringsToObjects = (
cookieStrings: string
): { [key: string]: string } => {
const result = {};
if (cookieStrings === "") return result;
cookieStrings.split(";").forEach((cookieString) => {
const [name, value] = cookieString.split("=");
result[name.trim()] = value.trim();
});
return result;
};

View File

@@ -60,7 +60,8 @@ declare global {
) => Promise<ShopDetails | null>;
getRandomGame: () => Promise<Steam250Game>;
getHowLongToBeat: (
title: string
objectId: string,
shop: GameShop
) => Promise<HowLongToBeatCategory[] | null>;
getGames: (take?: number, skip?: number) => Promise<CatalogueEntry[]>;
searchGameRepacks: (query: string) => Promise<GameRepack[]>;
@@ -79,8 +80,14 @@ declare global {
shop: GameShop
) => Promise<void>;
createGameShortcut: (id: number) => Promise<boolean>;
updateExecutablePath: (id: number, executablePath: string) => Promise<void>;
selectGameWinePrefix: (id: number, winePrefixPath: string) => Promise<void>;
updateExecutablePath: (
id: number,
executablePath: string | null
) => Promise<void>;
selectGameWinePrefix: (
id: number,
winePrefixPath: string | null
) => Promise<void>;
verifyExecutablePathInUse: (executablePath: string) => Promise<Game>;
getLibrary: () => Promise<LibraryGame[]>;
openGameInstaller: (gameId: number) => Promise<boolean>;
@@ -162,6 +169,7 @@ declare global {
openExternal: (src: string) => Promise<void>;
openCheckout: () => Promise<void>;
getVersion: () => Promise<string>;
isStaging: () => Promise<boolean>;
ping: () => string;
getDefaultDownloadsPath: () => Promise<string>;
isPortableVersion: () => Promise<boolean>;

View File

@@ -14,6 +14,7 @@ import type {
UserDetails,
} from "@types";
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
import { isFuture, isToday } from "date-fns";
export function useUserDetails() {
const dispatch = useAppDispatch();
@@ -55,6 +56,8 @@ export function useUserDetails() {
clearUserDetails();
}
window["userDetails"] = userDetails;
return userDetails;
});
}, [clearUserDetails]);
@@ -128,10 +131,8 @@ export function useUserDetails() {
const unblockUser = (userId: string) => window.electron.unblockUser(userId);
const hasActiveSubscription = useMemo(() => {
return (
userDetails?.subscription?.expiresAt &&
new Date(userDetails.subscription.expiresAt) > new Date()
);
const expiresAt = userDetails?.subscription?.expiresAt;
return expiresAt && (isFuture(expiresAt) || isToday(expiresAt));
}, [userDetails]);
return {

View File

@@ -20,6 +20,8 @@ import resources from "@locales";
import { RepacksContextProvider } from "./context";
import { SuspenseWrapper } from "./components";
import { logger } from "./logger";
import { addCookieInterceptor } from "./cookies";
const Home = React.lazy(() => import("./pages/home/home"));
const GameDetails = React.lazy(
@@ -34,6 +36,11 @@ const Achievements = React.lazy(
() => import("./pages/achievements/achievements")
);
console.log = logger.log;
const isStaging = await window.electron.isStaging();
addCookieInterceptor(isStaging);
i18n
.use(LanguageDetector)
.use(initReactI18next)

View File

@@ -75,7 +75,7 @@ export function CloudSyncFilesModal({
showSuccessToast(t("custom_backup_location_set"));
getGameBackupPreview();
}
}, [objectId, setValue, shop, showSuccessToast, getGameBackupPreview]);
}, [objectId, setValue, shop, showSuccessToast, getGameBackupPreview, t]);
const handleFileMappingMethodClick = useCallback(
(mappingOption: FileMappingMethod) => {

View File

@@ -95,6 +95,11 @@ export function GameOptionsModal({
await window.electron.openGameExecutablePath(game.id);
};
const handleClearExecutablePath = async () => {
await window.electron.updateExecutablePath(game.id, null);
updateGame();
};
const handleChangeWinePrefixPath = async () => {
const { filePaths } = await window.electron.showOpenDialog({
properties: ["openDirectory"],
@@ -106,6 +111,11 @@ export function GameOptionsModal({
}
};
const handleClearWinePrefixPath = async () => {
await window.electron.selectGameWinePrefix(game.id, null);
updateGame();
};
const shouldShowWinePrefixConfiguration =
window.electron.platform === "linux";
@@ -145,14 +155,21 @@ export function GameOptionsModal({
disabled
placeholder={t("no_executable_selected")}
rightContent={
<Button
type="button"
theme="outline"
onClick={handleChangeExecutableLocation}
>
<FileIcon />
{t("select_executable")}
</Button>
<>
<Button
type="button"
theme="outline"
onClick={handleChangeExecutableLocation}
>
<FileIcon />
{t("select_executable")}
</Button>
{game.executablePath && (
<Button onClick={handleClearExecutablePath} theme="outline">
{t("clear")}
</Button>
)}
</>
}
/>
@@ -186,14 +203,24 @@ export function GameOptionsModal({
disabled
placeholder={t("no_directory_selected")}
rightContent={
<Button
type="button"
theme="outline"
onClick={handleChangeWinePrefixPath}
>
<FileDirectoryIcon />
{t("select_executable")}
</Button>
<>
<Button
type="button"
theme="outline"
onClick={handleChangeWinePrefixPath}
>
<FileDirectoryIcon />
{t("select_executable")}
</Button>
{game.winePrefixPath && (
<Button
onClick={handleClearWinePrefixPath}
theme="outline"
>
{t("clear")}
</Button>
)}
</>
}
/>
</div>

View File

@@ -97,8 +97,10 @@ export function Sidebar() {
});
} else {
try {
const howLongToBeat =
await window.electron.getHowLongToBeat(gameTitle);
const howLongToBeat = await window.electron.getHowLongToBeat(
objectId,
shop
);
if (howLongToBeat) {
howLongToBeatEntriesTable.add({

View File

@@ -45,22 +45,25 @@ export function ProfileContent() {
return userProfile?.relation?.status === "ACCEPTED";
}, [userProfile]);
const buildUserGameDetailsPath = (game: UserGame) => {
if (!userProfile?.hasActiveSubscription || game.achievementCount === 0) {
return buildGameDetailsPath({
...game,
objectId: game.objectId,
});
}
const buildUserGameDetailsPath = useCallback(
(game: UserGame) => {
if (!userProfile?.hasActiveSubscription || game.achievementCount === 0) {
return buildGameDetailsPath({
...game,
objectId: game.objectId,
});
}
const userParams = userProfile
? {
userId: userProfile.id,
}
: undefined;
const userParams = userProfile
? {
userId: userProfile.id,
}
: undefined;
return buildGameAchievementPath({ ...game }, userParams);
};
return buildGameAchievementPath({ ...game }, userParams);
},
[userProfile]
);
const formatPlayTime = useCallback(
(playTimeInSeconds = 0) => {
@@ -259,6 +262,7 @@ export function ProfileContent() {
userStats,
numberFormatter,
t,
buildUserGameDetailsPath,
formatPlayTime,
navigate,
]);

View File

@@ -2,7 +2,7 @@
/// <reference types="vite-plugin-svgr/client" />
interface ImportMetaEnv {
readonly RENDERER_VITE_INTERCOM_APP_ID: string;
readonly RENDERER_VITE_EXTERNAL_RESOURCES_URL: string;
}
interface ImportMeta {

View File

@@ -46,7 +46,7 @@ export const removeSymbolsFromName = (name: string) =>
export const removeSpecialEditionFromName = (name: string) =>
name.replace(
/(The |Digital )?(GOTY|Deluxe|Standard|Ultimate|Definitive|Enhanced|Collector's|Premium|Digital|Limited|Game of the Year|Reloaded|[0-9]{4}) Edition/g,
/(The |Digital )?(GOTY|Deluxe|Standard|Ultimate|Definitive|Enhanced|Collector's|Premium|Digital|Limited|Game of the Year|Reloaded|[0-9]{4}) Edition/gi,
""
);
@@ -73,7 +73,8 @@ export const formatName = pipe<string>(
replaceUnderscoreWithSpace,
replaceDotsWithSpace,
replaceNbspWithSpace,
(str) => str.replace(/DIRECTOR'S CUT/g, ""),
(str) => str.replace(/DIRECTOR'S CUT/gi, ""),
(str) => str.replace(/Friend's Pass/gi, ""),
removeSymbolsFromName,
removeDuplicateSpaces,
(str) => str.trim()

View File

@@ -241,6 +241,7 @@ export interface Subscription {
status: SubscriptionStatus;
plan: { id: string; name: string };
expiresAt: string | null;
paymentMethod: "pix" | "paypal";
}
export interface UserDetails {

View File

@@ -1066,11 +1066,6 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3"
integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==
"@intercom/messenger-js-sdk@^0.0.14":
version "0.0.14"
resolved "https://registry.yarnpkg.com/@intercom/messenger-js-sdk/-/messenger-js-sdk-0.0.14.tgz#a27999370cc0a82a2a57a779426df25a57891863"
integrity sha512-2dH4BDAh9EI90K7hUkAdZ76W79LM45Sd1OBX7t6Vzy8twpNiQ5X+7sH9G5hlJlkSGnf+vFWlFcy9TOYAyEs1hA==
"@isaacs/cliui@^8.0.2":
version "8.0.2"
resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz"