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
39 changed files with 358 additions and 304 deletions

View File

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

View File

@@ -22,16 +22,6 @@ jobs:
with: with:
node-version: 20.18.0 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 - name: Install dependencies
run: yarn run: yarn
@@ -58,6 +48,7 @@ jobs:
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }} MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_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_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 }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build Windows - name: Build Windows
@@ -69,6 +60,7 @@ jobs:
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }} MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_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_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 }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create artifact - name: Create artifact

View File

@@ -47,6 +47,7 @@ jobs:
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }} MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_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_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 }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Build Windows - name: Build Windows
if: matrix.os == 'windows-latest' if: matrix.os == 'windows-latest'
@@ -57,6 +58,7 @@ jobs:
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }} MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
MAIN_VITE_ANALYTICS_API_URL: ${{ vars.MAIN_VITE_ANALYTICS_API_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_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 }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create artifact - name: Create artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4

View File

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

View File

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

View File

@@ -6,7 +6,11 @@
"home": { "home": {
"featured": "Doporučené", "featured": "Doporučené",
"surprise_me": "Překvap mě", "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": { "sidebar": {
"catalogue": "Katalog", "catalogue": "Katalog",
@@ -20,7 +24,9 @@
"home": "Domov", "home": "Domov",
"queued": "{{title}} (V řadě)", "queued": "{{title}} (V řadě)",
"game_has_no_executable": "Hra nemá zvolen žádný spustitelný soubor", "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": { "header": {
"search": "Vyhledat hry", "search": "Vyhledat hry",
@@ -113,7 +119,54 @@
"download_paused": "Stahování pozastaveno", "download_paused": "Stahování pozastaveno",
"last_downloaded_option": "Poslední stažená možnost", "last_downloaded_option": "Poslední stažená možnost",
"create_shortcut_success": "Zástupce vytvořen úspěšně", "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": { "activation": {
"title": "Aktivovat hydru", "title": "Aktivovat hydru",
@@ -189,7 +242,21 @@
"found_download_option_zero": "Nenalezena žádná možnost stahování", "found_download_option_zero": "Nenalezena žádná možnost stahování",
"found_download_option_one": "Nalezena {{countFormatted}} možnost stahování", "found_download_option_one": "Nalezena {{countFormatted}} možnost stahování",
"found_download_option_other": "Nalezeny {{countFormatted}} možnosti 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": { "notifications": {
"download_complete": "Stahování dokončeno", "download_complete": "Stahování dokončeno",
@@ -198,7 +265,9 @@
"repack_count_one": "{{count}} repack přidán", "repack_count_one": "{{count}} repack přidán",
"repack_count_other": "{{count}} repacky přidány", "repack_count_other": "{{count}} repacky přidány",
"new_update_available": "Version {{version}} je dostupná", "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": { "system_tray": {
"open": "Otevřít Hydru", "open": "Otevřít Hydru",
@@ -266,6 +335,47 @@
"no_pending_invites": "Nemáte žádné příchozí žádosti", "no_pending_invites": "Nemáte žádné příchozí žádosti",
"no_blocked_users": "Nemáte nikoho zablokovaného", "no_blocked_users": "Nemáte nikoho zablokovaného",
"friend_code_copied": "Kód přítele zkopírován", "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_folder": "Open folder",
"open_download_location": "See downloaded files", "open_download_location": "See downloaded files",
"create_shortcut": "Create desktop shortcut", "create_shortcut": "Create desktop shortcut",
"clear": "Clear",
"remove_files": "Remove files", "remove_files": "Remove files",
"remove_from_library_title": "Are you sure?", "remove_from_library_title": "Are you sure?",
"remove_from_library_description": "This will remove {{game}} from your library", "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", "manage_files_description": "Manage which files will be backed up and restored",
"select_folder": "Select folder", "select_folder": "Select folder",
"backup_from": "Backup from {{date}}", "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": { "activation": {
"title": "Activate Hydra", "title": "Activate Hydra",

View File

@@ -100,7 +100,7 @@
"open_screenshot": "Abrir captura {{number}}", "open_screenshot": "Abrir captura {{number}}",
"download_settings": "Ajustes de descarga", "download_settings": "Ajustes de descarga",
"downloader": "Método de descarga", "downloader": "Método de descarga",
"select_executable": "Seleccionar ejecutable", "select_executable": "Seleccionar",
"no_executable_selected": "No se seleccionó un ejecutable", "no_executable_selected": "No se seleccionó un ejecutable",
"open_folder": "Abrir carpeta", "open_folder": "Abrir carpeta",
"open_download_location": "Ver archivos descargados", "open_download_location": "Ver archivos descargados",
@@ -166,7 +166,9 @@
"manage_files_description": "Gestiona los archivos que serán respaldados y restaurados", "manage_files_description": "Gestiona los archivos que serán respaldados y restaurados",
"select_folder": "Seleccionar carpeta", "select_folder": "Seleccionar carpeta",
"backup_from": "Copia de seguridad de {{date}}", "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": { "activation": {
"title": "Activar Hydra", "title": "Activar Hydra",
@@ -254,7 +256,9 @@
"must_be_valid_url": "La fuente debe ser una URL válida.", "must_be_valid_url": "La fuente debe ser una URL válida.",
"blocked_users": "Usuarios bloqueados", "blocked_users": "Usuarios bloqueados",
"user_unblocked": "El usuario ha sido desbloqueado", "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": { "notifications": {
"download_complete": "Descarga completada", "download_complete": "Descarga completada",
@@ -361,8 +365,10 @@
"user_achievements": "Logros de {{displayName}}", "user_achievements": "Logros de {{displayName}}",
"your_achievements": "Tus Logros", "your_achievements": "Tus Logros",
"unlocked_at": "Desbloqueado el:", "unlocked_at": "Desbloqueado el:",
"subscription_needed": "Se necesita una suscripción a Hydra Cloud se necesita para ver este contenido", "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" "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": { "tour": {
"subscription_tour_title": "Suscripción Hydra Cloud", "subscription_tour_title": "Suscripción Hydra Cloud",

View File

@@ -162,7 +162,9 @@
"backup_from": "Backup de {{date}}", "backup_from": "Backup de {{date}}",
"custom_backup_location_set": "Localização customizada selecionada", "custom_backup_location_set": "Localização customizada selecionada",
"select_folder": "Selecione a pasta", "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": { "activation": {
"title": "Ativação", "title": "Ativação",

View File

@@ -1,23 +1,21 @@
import type { HowLongToBeatCategory } from "@types"; import type { GameShop, HowLongToBeatCategory } from "@types";
import { getHowLongToBeatGame, searchHowLongToBeat } from "@main/services";
import { registerEvent } from "../register-event"; import { registerEvent } from "../register-event";
import { formatName } from "@shared"; import { HydraApi } from "@main/services";
const getHowLongToBeat = async ( const getHowLongToBeat = async (
_event: Electron.IpcMainInvokeEvent, _event: Electron.IpcMainInvokeEvent,
title: string objectId: string,
shop: GameShop
): Promise<HowLongToBeatCategory[] | null> => { ): Promise<HowLongToBeatCategory[] | null> => {
const response = await searchHowLongToBeat(title); const params = new URLSearchParams({
objectId,
const game = response.data.find((game) => { shop,
return formatName(game.game_name) === formatName(title);
}); });
if (!game) return null; return HydraApi.get(`/games/how-long-to-beat?${params.toString()}`, null, {
const howLongToBeat = await getHowLongToBeatGame(String(game.game_id)); needsAuth: false,
});
return howLongToBeat;
}; };
registerEvent("getHowLongToBeat", getHowLongToBeat); 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 { ipcMain } from "electron";
import "./catalogue/get-catalogue"; import "./catalogue/get-catalogue";
@@ -72,5 +72,6 @@ import "./misc/show-item-in-folder";
ipcMain.handle("ping", () => "pong"); ipcMain.handle("ping", () => "pong");
ipcMain.handle("getVersion", () => appVersion); ipcMain.handle("getVersion", () => appVersion);
ipcMain.handle("isStaging", () => isStaging);
ipcMain.handle("isPortableVersion", () => isPortableVersion()); ipcMain.handle("isPortableVersion", () => isPortableVersion());
ipcMain.handle("getDefaultDownloadsPath", () => defaultDownloadsPath); ipcMain.handle("getDefaultDownloadsPath", () => defaultDownloadsPath);

View File

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

View File

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

View File

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

View File

@@ -174,8 +174,10 @@ export class PythonInstance {
.then((response) => response.data); .then((response) => response.data);
} }
private static async handleRpcError(_error: unknown) { private static async handleRpcError(error: unknown) {
await this.rpc.get("/healthcheck").catch(() => { logger.error(error);
return this.rpc.get("/healthcheck").catch(() => {
logger.error( logger.error(
"RPC healthcheck failed. Killing process and starting again" "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 { omit } from "lodash-es";
import { appVersion } from "@main/constants"; import { appVersion } from "@main/constants";
import { getUserData } from "./user/get-user-data"; import { getUserData } from "./user/get-user-data";
import { isFuture, isToday } from "date-fns";
interface HydraApiOptions { interface HydraApiOptions {
needsAuth?: boolean; needsAuth?: boolean;
@@ -45,10 +46,8 @@ export class HydraApi {
} }
private static hasActiveSubscription() { private static hasActiveSubscription() {
return ( const expiresAt = this.userAuth.subscription?.expiresAt;
this.userAuth.subscription?.expiresAt && return expiresAt && (isFuture(expiresAt) || isToday(expiresAt));
this.userAuth.subscription.expiresAt > new Date()
);
} }
static async handleExternalAuth(uri: string) { static async handleExternalAuth(uri: string) {
@@ -154,21 +153,26 @@ export class HydraApi {
(error) => { (error) => {
logger.error(" ---- RESPONSE ERROR -----"); logger.error(" ---- RESPONSE ERROR -----");
const { config } = error; const { config } = error;
const data = JSON.parse(config.data);
logger.error( logger.error(
config.method, config.method,
config.baseURL, config.baseURL,
config.url, config.url,
config.headers, omit(config.headers, ["accessToken", "refreshToken"]),
config.data Array.isArray(data)
? data
: omit(data, ["accessToken", "refreshToken"])
); );
if (error.response) { if (error.response) {
logger.error( logger.error(
"Response", "Response error:",
error.response.status, error.response.status,
error.response.data error.response.data
); );
} else if (error.request) { } else if (error.request) {
logger.error("Request", error.request); const errorData = error.toJSON();
logger.error("Request error:", errorData.message);
} else { } else {
logger.error("Error", error.message); logger.error("Error", error.message);
} }

View File

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

View File

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

View File

@@ -85,7 +85,11 @@ export class WindowManager {
return callback(details); 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); return callback(details);
} }

View File

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

View File

@@ -6,7 +6,7 @@
<title>Hydra</title> <title>Hydra</title>
<meta <meta
http-equiv="Content-Security-Policy" 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> </head>
<body> <body>

View File

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

View File

@@ -107,6 +107,7 @@ export function Modal({
aria-labelledby={title} aria-labelledby={title}
aria-describedby={description} aria-describedby={description}
ref={modalContentRef} ref={modalContentRef}
data-hydra-dialog
> >
<div className={styles.modalHeader}> <div className={styles.modalHeader}>
<div style={{ display: "flex", gap: 4, flexDirection: "column" }}> <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 { sortBy } from "lodash-es";
import { CommentDiscussionIcon } from "@primer/octicons-react"; import { CommentDiscussionIcon } from "@primer/octicons-react";
import { show, update } from "@intercom/messenger-js-sdk";
const SIDEBAR_MIN_WIDTH = 200; const SIDEBAR_MIN_WIDTH = 200;
const SIDEBAR_INITIAL_WIDTH = 250; const SIDEBAR_INITIAL_WIDTH = 250;
const SIDEBAR_MAX_WIDTH = 450; const SIDEBAR_MAX_WIDTH = 450;
@@ -50,20 +48,7 @@ export function Sidebar() {
return sortBy(library, (game) => game.title); return sortBy(library, (game) => game.title);
}, [library]); }, [library]);
const { userDetails, hasActiveSubscription } = useUserDetails(); const { 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 { lastPacket, progress } = useDownload(); const { lastPacket, progress } = useDownload();
@@ -266,7 +251,11 @@ export function Sidebar() {
</div> </div>
{hasActiveSubscription && ( {hasActiveSubscription && (
<button type="button" className={styles.helpButton} onClick={show}> <button
type="button"
className={styles.helpButton}
data-open-support-chat
>
<div className={styles.helpButtonIcon}> <div className={styles.helpButtonIcon}>
<CommentDiscussionIcon size={14} /> <CommentDiscussionIcon size={14} />
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -46,7 +46,7 @@ export const removeSymbolsFromName = (name: string) =>
export const removeSpecialEditionFromName = (name: string) => export const removeSpecialEditionFromName = (name: string) =>
name.replace( 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, replaceUnderscoreWithSpace,
replaceDotsWithSpace, replaceDotsWithSpace,
replaceNbspWithSpace, replaceNbspWithSpace,
(str) => str.replace(/DIRECTOR'S CUT/g, ""), (str) => str.replace(/DIRECTOR'S CUT/gi, ""),
(str) => str.replace(/Friend's Pass/gi, ""),
removeSymbolsFromName, removeSymbolsFromName,
removeDuplicateSpaces, removeDuplicateSpaces,
(str) => str.trim() (str) => str.trim()

View File

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

View File

@@ -1066,11 +1066,6 @@
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3"
integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== 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": "@isaacs/cliui@^8.0.2":
version "8.0.2" version "8.0.2"
resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz" resolved "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz"