Compare commits

...

83 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
ee4639e041 Merge pull request #1222 from bankov4eto/main
Some checks failed
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-latest) (push) Has been cancelled
PR: [translation] bulgarian
2024-11-10 23:37:00 -03:00
Zamitto
6e6469d90f fix: format ignore case 2024-11-10 20:55:23 -03:00
bankov4eto
a53793a76b Merge branch 'hydralauncher:main' into main 2024-11-10 12:50:23 +02:00
bankov4eto
d046f1ed21 Update index.ts 2024-11-10 09:25:19 +02:00
Zamitto
da1ac788fb Update translation.json 2024-11-09 20:58:21 -03:00
Chubby Granny Chaser
1980560a2d Merge branch 'main' of github.com:hydralauncher/hydra
Some checks failed
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-latest) (push) Has been cancelled
2024-11-09 22:14:29 +00:00
bankov4eto
7166f66a9e Update translation.json 2024-11-09 17:29:32 +02:00
bankov4eto
cc3fc10ddf Merge pull request #1 from bankov4eto/bankov4eto-patch-1
Create translation.json
2024-11-09 15:41:30 +02:00
bankov4eto
15ecba1f6e Create translation.json
Bulgarian translation added
2024-11-09 15:36:16 +02:00
Zamitto
2828640ed7 feat: add intercom user id 2024-11-09 02:54:23 -03:00
Chubby Granny Chaser
341903fc3e ci: pushing builds to r2 2024-11-09 04:26:01 +00:00
Zamitto
20c001914a Merge pull request #1217 from hydralauncher/feat/intercom-userid
Some checks are pending
Release / build (ubuntu-latest) (push) Waiting to run
Release / build (windows-latest) (push) Waiting to run
feat: add userid to intercom and post to logout
2024-11-08 23:16:48 -03:00
Zamitto
77e9de704d feat: open checkout with user language 2024-11-08 22:40:43 -03:00
Zamitto
34ec8467ec feat: temp removing userId intercom 2024-11-08 22:34:58 -03:00
Zamitto
eeae140a41 chore: remove unused file 2024-11-08 20:17:12 -03:00
Zamitto
e277af0e4b feat: add staging info on bottom panel 2024-11-08 20:03:22 -03:00
Zamitto
8a67492cf8 feat: add userid to itercom and post to logout 2024-11-08 19:47:28 -03:00
Eight
981dbceb93 Merge pull request #1207 from hydralauncher/feature/disable-nsfw-popup
Some checks failed
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-latest) (push) Has been cancelled
Feature/disable nsfw alert
2024-11-06 21:51:24 -03:00
Hachi-R
aaee27732d refactor: rename all "popup" strings to "alert" 2024-11-06 20:44:56 -03:00
Hachi-R
e7acce2dfc feat: add localization strings 2024-11-06 03:01:28 -03:00
Hachi-R
56b15bf52a lint 2024-11-06 02:58:10 -03:00
Hachi-R
2f8fe67f9f feat: add option to disable NSFW warning 2024-11-06 02:57:52 -03:00
Chubby Granny Chaser
b09e91a1cf fix: fixing analytics for ddl
Some checks failed
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-latest) (push) Has been cancelled
2024-11-05 20:29:44 +00:00
Zamitto
9418c4acf7 fix: achievement filter 2024-11-05 17:09:46 -03:00
Chubby Granny Chaser
0451bc55aa chore: more relaxed CSP 2024-11-05 19:59:41 +00:00
Chubby Granny Chaser
2840110a21 ci: removing bump 2024-11-05 19:30:09 +00:00
Chubby Granny Chaser
8c4eacf045 chore: bump version 2024-11-05 19:18:13 +00:00
Chubby Granny Chaser
179785432d Merge pull request #1206 from hydralauncher/ci/adding-intercom-app-id
Ci/adding intercom app
2024-11-05 19:17:46 +00:00
Chubby Granny Chaser
c3da39205f ci: adding intercom app id 2024-11-05 19:12:12 +00:00
Chubby Granny Chaser
f071d1006b ci: adding intercom app id 2024-11-05 19:11:03 +00:00
Chubby Granny Chaser
6da1832799 ci: adding intercom app id 2024-11-05 19:08:37 +00:00
Chubby Granny Chaser
df0e124c3a Merge pull request #1204 from hydralauncher/feat/adding-intercom
Feat/adding intercom
2024-11-05 18:24:06 +00:00
Chubby Granny Chaser
25f1a72b48 Merge branch 'main' into feat/adding-intercom 2024-11-05 18:17:37 +00:00
Chubby Granny Chaser
2b5e76ffdd feat: adding intercom 2024-11-05 18:15:17 +00:00
Chubby Granny Chaser
9d75e3ad6f feat: adding intercom 2024-11-05 18:13:32 +00:00
56 changed files with 1003 additions and 1187 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

View File

@@ -2,6 +2,9 @@ name: Build
on: pull_request
env:
AWS_REGION: us-east-1
jobs:
build:
strategy:
@@ -44,6 +47,8 @@ jobs:
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
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
@@ -54,6 +59,8 @@ jobs:
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
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

@@ -46,8 +46,9 @@ jobs:
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
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'
run: yarn build:win
@@ -56,8 +57,9 @@ jobs:
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_AUTH_URL }}
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
with:

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

View File

@@ -0,0 +1,381 @@
{
"language_name": "Български",
"app": {
"successfully_signed_in": "Успешно вписване"
},
"home": {
"featured": "Препоръчани",
"surprise_me": "Изненадай ме",
"no_results": "Не са намерени резултати",
"start_typing": "Търсене...",
"hot": "Актуално сега",
"weekly": "📅 Най-доброто от седмицата",
"achievements": "🏆 Игри, които да победите"
},
"sidebar": {
"catalogue": "Каталог",
"downloads": "Изтегляния",
"settings": "Настройки",
"my_library": "Моята библиотека",
"downloading_metadata": "{{title}} (Сваляне на метаданни…)",
"paused": "{{title}} (Пауза)",
"downloading": "{{title}} ({{percentage}} - Изтегляне…)",
"filter": "Търсене по име",
"home": "Начало",
"queued": "{{title}} (Опашка)",
"game_has_no_executable": "Играта няма избран изпълним файл",
"sign_in": "Вписване",
"friends": "Приятели",
"need_help": "Имате нужда от помощ??"
},
"header": {
"search": "Търсене",
"home": "Начало",
"catalogue": "Каталог",
"downloads": "Изтегляния",
"search_results": "Резултати от търсене",
"settings": "Настройки",
"version_available_install": "Версия {{version}} е налична. Кликни тук, за да рестартирате и инсталирате.",
"version_available_download": "Версия {{version}} е налична. Кликни тук за изтегляне."
},
"bottom_panel": {
"no_downloads_in_progress": "Няма изтегляния в ход",
"downloading_metadata": "Сваляне на {{title}} метадата…",
"downloading": "Изтегляне на {{title}}… ({{percentage}} готово) - Остават {{eta}} - {{speed}}",
"calculating_eta": "Изтегляне на {{title}}… ({{percentage}} готово) - Изчисляване на оставащо време…",
"checking_files": "Проверка на {{title}} файловете… ({{percentage}} готово)"
},
"catalogue": {
"next_page": "Следваща страница",
"previous_page": "Предишна страница"
},
"game_details": {
"open_download_options": "Варианти за изтегляне",
"download_options_zero": "Няма варианти за изтегляне",
"download_options_one": "{{count}} варианти за изтегляне",
"download_options_other": "{{count}} варианти за изтегляне",
"updated_at": "Обновено на {{updated_at}}",
"install": "Инсталирай",
"resume": "Продължи",
"pause": "Пауза",
"cancel": "Отказ",
"remove": "Премахни",
"space_left_on_disk": "{{space}} място на диска",
"eta": "Заклчение {{eta}}",
"calculating_eta": "Калкулиране на оставащо време…",
"downloading_metadata": "Изтегляне на метадата…",
"filter": "Филтрирай repacks",
"requirements": "Системни изисквания",
"minimum": "Минимални",
"recommended": "Препоръчителни",
"paused": "Паузирано",
"release_date": "Издадено на {{date}}",
"publisher": "Публикувано от {{publisher}}",
"hours": "часове",
"minutes": "минути",
"amount_hours": "{{amount}} часа",
"amount_minutes": "{{amount}} минути",
"accuracy": "{{accuracy}}% точност",
"add_to_library": "Добави в библиотеката",
"remove_from_library": "Премахни от библиотеката",
"no_downloads": "Няма налични изтегляния",
"play_time": "Игрално време {{amount}}",
"last_time_played": "Последно пускане {{period}}",
"not_played_yet": "Не сте играли {{title}} все още",
"next_suggestion": "Следващо предложение",
"play": "Пускане",
"deleting": "Изтриване на инсталация…",
"close": "Затвори",
"playing_now": "Играй сега",
"change": "Промяна",
"repacks_modal_description": "Избери repack който искаш да изтеглиш",
"select_folder_hint": "За да промените стандартната папка отидете в <0>Настройки</0>",
"download_now": "Изтегли сега",
"no_shop_details": "Не може да се извлекат данни за магазина.",
"download_options": "Опции за сваляне",
"download_path": "Път за сваляне",
"previous_screenshot": "Предишна снимка",
"next_screenshot": "Следваща снимка",
"screenshot": "Снимка {{number}}",
"open_screenshot": "Отвори снимки {{number}}",
"download_settings": "Настройки за сваляне",
"downloader": "Downloader",
"select_executable": "Избери",
"no_executable_selected": "Няма избран стартиращ файл",
"open_folder": "Отвори папка",
"open_download_location": "Виж свалените файлове",
"create_shortcut": "Пряк път на Десктопа",
"remove_files": "Премахни файловете",
"remove_from_library_title": "Сигурен ли си?",
"remove_from_library_description": "Това ще премахне {{game}} от Библиотеката",
"options": "Опции",
"executable_section_title": "Стартиращ файл",
"executable_section_description": "Пътят на файла, който ще се изпълни, когато се щракне върху \"Пускане\"",
"downloads_secion_title": "Свалени",
"downloads_section_description": "Вижте актуализации или други версии на тази игра",
"danger_zone_section_title": "Опасна зона",
"danger_zone_section_description": "Премахнете тази игра от библиотеката си или от файловете, изтеглени от Hydra",
"download_in_progress": "Изтегляне в ход",
"download_paused": "Изтеглянето е паузирано",
"last_downloaded_option": "Опция от последно изтегляне",
"create_shortcut_success": "Прекият път е създаден успешно",
"create_shortcut_error": "Грешка при създаването на пряк път",
"nsfw_content_title": "Тази игра съдържа неподходящо съдържание",
"nsfw_content_description": "{{title}} съдържа съдържание, което може да не е подходящо за всички възрасти. Сигурни ли сте, че искате да продължите?",
"allow_nsfw_content": "Продължи",
"refuse_nsfw_content": "Назад",
"stats": "Статистики",
"download_count": "Сваляния",
"player_count": "Активни играчи",
"download_error": "Тази опция за изтегляне не е налична",
"download": "Свали",
"executable_path_in_use": "Изпълнимият файл вече се използва от \"{{game}}\"",
"warning": "Внимание:",
"hydra_needs_to_remain_open": "за това изтегляне, Hydra трябва да остане отворена, когато е завършено. Ако Hydra се затвори преди завършването, ще загубите напредъка си..",
"achievements": "Постижения",
"achievements_count": "Постижения {{unlockedCount}}/{{achievementsCount}}",
"cloud_save": "Запазване в облака",
"cloud_save_description": "Запазете напредъка си в облака и продължете да играете на всяко устройство",
"backups": "Резервни копия",
"install_backup": "Инсталирай",
"delete_backup": "Изтрий",
"create_backup": "Ново копие",
"last_backup_date": "Последно копие от {{date}}",
"no_backup_preview": "Не бяха намерени запазени игри за това заглавие",
"restoring_backup": "Възстановяване на резервно копие ({{progress}} готово)…",
"uploading_backup": "Качване на резервно копие…",
"no_backups": "Все още не сте създали резервни копия за тази игра",
"backup_uploaded": "Качено резервно копие",
"backup_deleted": "Изтрито резервно копие",
"backup_restored": "Възстановен бекъп",
"see_all_achievements": "Вижте всички постижения",
"sign_in_to_see_achievements": "Влезте, за да видите постиженията",
"mapping_method_automatic": "Автоматично",
"mapping_method_manual": "Ръчно",
"mapping_method_label": "Метод на картографиране",
"files_automatically_mapped": "Автоматично картографиране на файлове",
"no_backups_created": "Не са създадени резервни копия за тази игра",
"manage_files": "Управление на файлове",
"loading_save_preview": "Търсене на запазени игри…",
"wine_prefix": "Wine Префикс",
"wine_prefix_description": "Wine prefix използван за тази игра",
"no_download_option_info": "Няма налични данни",
"backup_deletion_failed": "Неуспешно изтриване на резервно копие",
"max_number_of_artifacts_reached": "Достигнат максимален брой резервни копия за тази игра",
"achievements_not_sync": "Постиженията не са синхронизирани",
"manage_files_description": "Управлявайте кои файлове ще бъдат архивирани и възстановени",
"select_folder": "Избери папка",
"backup_from": "Резервно копие от {{date}}",
"custom_backup_location_set": "Задаване на персонализирано местоположение за архивиране"
},
"activation": {
"title": "Активирай Hydra",
"installation_id": "Идентификатор на инсталацията:",
"enter_activation_code": "Въведете кода за активиране",
"message": "Ако не знаете къде да попитате за това, значи не трябва да го имате..",
"activate": "Активирай",
"loading": "Зареждане…"
},
"downloads": {
"resume": "Продължи",
"pause": "Пауза",
"eta": "Conclusion {{eta}}",
"paused": "Паузирано",
"verifying": "Проверка…",
"completed": "Готово",
"removed": "Не е изтеглен",
"cancel": "Отказ",
"filter": "Филтриране на изтеглени игри",
"remove": "Премахни",
"downloading_metadata": "Изтегляне на метаданни…",
"deleting": "Изтриване на инсталатора…",
"delete": "Премахване на инсталатора",
"delete_modal_title": "Сигурени ли сте?",
"delete_modal_description": "Това ще премахне всички инсталационни файлове от компютъра ви.",
"install": "Инсталирай",
"download_in_progress": "В процес на изпълнение",
"queued_downloads": "Изтеглени файлове в опашката",
"downloads_completed": "Приключени",
"queued": "В опашка",
"no_downloads_title": "Толкова е празно",
"no_downloads_description": "Все още не сте изтеглили нищо с Hydra, но никога не е късно да започнете...",
"checking_files": "Проверка на файлове…"
},
"settings": {
"downloads_path": "Инсталационен път",
"change": "Актуализиране",
"notifications": "Известия",
"enable_download_notifications": "Когато изтеглянето е завършено",
"enable_repack_list_notifications": "Когато се добави нов repack",
"real_debrid_api_token_label": "Real-Debrid API токен",
"quit_app_instead_hiding": "Не скривайте Hydra при затваряне",
"launch_with_system": "Стартиране на Hydra при стартиране на системата",
"general": "Общ",
"behavior": "Поведение",
"download_sources": "Източници за изтегляне",
"language": "Език",
"real_debrid_api_token": "API Токен",
"enable_real_debrid": "Включи Real-Debrid",
"real_debrid_description": "Real-Debrid е неограничен даунлоудър, който ви позволява бързо да изтегляте файлове, ограничени само от скоростта на интернет..",
"real_debrid_invalid_token": "Невалиден API токен",
"real_debrid_api_token_hint": "Вземете своя API токен <0>тук</0>",
"real_debrid_free_account_error": "Акаунтът \"{{username}}\" е безплатен акаунт. Моля абонирай се за Real-Debrid",
"real_debrid_linked_message": "Акаунтът \"{{username}}\" е свързан",
"save_changes": "Запази промените",
"changes_saved": "Промените са успешно запазни",
"download_sources_description": "Hydra ще извлича връзките за изтегляне от тези източници. URL адресът на източника трябва да е директна връзка към .json файл, съдържащ връзките за изтегляне.",
"validate_download_source": "Валидиране",
"remove_download_source": "Премахни",
"add_download_source": "Добави източник",
"download_count_zero": "Няма опции за сваляне",
"download_count_one": "{{countFormatted}} опции за сваляне",
"download_count_other": "{{countFormatted}} опции за сваляне",
"download_source_url": "URL адрес на източника за изтегляне",
"add_download_source_description": "Вмъкнете URL адреса на файла .json",
"download_source_up_to_date": "Актуален",
"download_source_errored": "Сгрешен",
"sync_download_sources": "Синхронизирай източниците",
"removed_download_source": "Източника за сваляне е премахнат",
"added_download_source": "Добавен източник за сваляне",
"download_sources_synced": "Всички източници за сваляне са синхронизирани",
"insert_valid_json_url": "Добавете ваиден JSON линк",
"found_download_option_zero": "Няма намерени опции за сваляне",
"found_download_option_one": "Намерени {{countFormatted}} опции за сваляне",
"found_download_option_other": "Намерени {{countFormatted}} опции за сваляне",
"import": "Внеси",
"public": "Публичен",
"private": "Личен",
"friends_only": "Само за приятели",
"privacy": "Поверителност",
"profile_visibility": "Видимост на профила",
"profile_visibility_description": "Изберете кой може да вижда вашия профил и библиотека",
"required_field": "Това поле е задължително",
"source_already_exists": "Този източник вече е добавен",
"must_be_valid_url": "Източникът трябва да е валиден URL адрес.",
"blocked_users": "Блокирани потребители",
"user_unblocked": "Потребителят е бил деблокиран",
"enable_achievement_notifications": "Когато е отключено постижение",
"launch_minimized": "Стартиране на Hydra минимизирано",
"disable_nsfw_alert": "Деактивиране на предупреждението NSFW"
},
"notifications": {
"download_complete": "Изтеглянето е завършено",
"game_ready_to_install": "{{title}} е готово за инсталиране",
"repack_list_updated": "Repack лист е обновен",
"repack_count_one": "{{count}} repack е добавен",
"repack_count_other": "{{count}} repacks добавени",
"new_update_available": "Версия {{version}} е налична",
"restart_to_install_update": "Рестартирайте Hydra, за да инсталирате актуализацията",
"notification_achievement_unlocked_title": "Отключено постижение за {{game}}",
"notification_achievement_unlocked_body": "{{achievement}} и други {{count}} са отклщчени"
},
"system_tray": {
"open": "Отвори Hydra",
"quit": "Изход"
},
"game_card": {
"no_downloads": "Няма налични изтегляния"
},
"binary_not_found_modal": {
"title": "Не инсталирани програми",
"description": "Wine или Lutris изпълними файлове не бяха открити на вашата система",
"instructions": "Проверете правилния начин за инсталиране на някоя от тях на вашата дистрибуция на Linux, за да може играта да работи нормално"
},
"modal": {
"close": "Бутон за затваряне"
},
"forms": {
"toggle_password_visibility": "Превключване на видимостта на паролата"
},
"user_profile": {
"amount_hours": "{{amount}} часове",
"amount_minutes": "{{amount}} минути",
"last_time_played": "Последно играно {{period}}",
"activity": "Скорошна активност",
"library": "Библиотека",
"total_play_time": "Общо време за игра: {{amount}}",
"no_recent_activity_title": "Хмм… няма нищо тук",
"no_recent_activity_description": "Не сте играли игри напоследък. Време е да промените това.!",
"display_name": "Показване на името",
"saving": "Запазване",
"save": "Запис",
"edit_profile": "Редактиране на профила",
"saved_successfully": "Запазено успешно",
"try_again": "Моля, опитайте пак",
"sign_out_modal_title": "Сигурни ли сте?",
"cancel": "Отказ",
"successfully_signed_out": "Успешно се отписахте",
"sign_out": "Отписване",
"playing_for": "В игра от {{amount}}",
"sign_out_modal_text": "Вашата библиотека е свързана с текущата ви сметка. Когато се отпишете, библиотеката ви вече няма да е видима и напредъкът няма да бъде запазен. Продължете с отписването?",
"add_friends": "Добави приятели",
"add": "Добави",
"friend_code": "Приятелски код",
"see_profile": "Виж профила",
"sending": "Изпращане",
"friend_request_sent": "Изпратена покана за приятелство",
"friends": "Приятели",
"friends_list": "Списък с приятели",
"user_not_found": "Не е намерен потребител",
"block_user": "Блокирай потребител",
"add_friend": "Добави приятел",
"request_sent": "Изпратена покана",
"request_received": "Получена покана",
"accept_request": "Приеми поканата",
"ignore_request": "Игнирирай поканата",
"cancel_request": "Откажи поканата",
"undo_friendship": "Отмяна на приятелството",
"request_accepted": "Поканата е приета",
"user_blocked_successfully": "Потребителят е блокиран успешно",
"user_block_modal_text": "Това ще блокира {{displayName}}",
"blocked_users": "Блокирани потребители",
"unblock": "Отблокирай",
"no_friends_added": "Не сте добавили приятели",
"pending": "Чакащи",
"no_pending_invites": "Нямате чакащи покани",
"no_blocked_users": "Нямате блокирани потребители",
"friend_code_copied": "Приятелския код е копиран",
"undo_friendship_modal_text": "Това ще отмени приятелството ви с {{displayName}}",
"privacy_hint": "За да настроите кой може да вижда това, отидете в <0>Настройки</0>",
"locked_profile": "Този профил е личен",
"image_process_failure": "Грешка при обработката на изображението",
"required_field": "Това поле е задължително",
"displayname_min_length": "Името трябва да е дълго поне 3 символа",
"displayname_max_length": "Името трябва да е с дължина не повече от 50 символа.",
"report_profile": "Докладвай този профил",
"report_reason": "Защо докладвате този профил?",
"report_description": "Допълнителна информация",
"report_description_placeholder": "Допълнителна информация",
"report": "Докладвай",
"report_reason_hate": "Омразна реч",
"report_reason_sexual_content": "Сексуално съдържание",
"report_reason_violence": "Насилия",
"report_reason_spam": "Спам",
"report_reason_other": "Друго",
"profile_reported": "Профилът е докладван",
"your_friend_code": "Вашия приятелски код:",
"upload_banner": "Качи банер",
"uploading_banner": "Качване на банер…",
"background_image_updated": "Обновено фоново изображение"
},
"achievement": {
"achievement_unlocked": "Постижението е отключено",
"user_achievements": "Постиженията на {{displayName}} ",
"your_achievements": "Вашите Постижения",
"unlocked_at": "Отключено на:",
"subscription_needed": "Необходим е абонамент за Hydra Cloud, за да видите това съдържание",
"new_achievements_unlocked": "Отключени {{achievementCount}} нови постижения от {{gameCount}} игра",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} постижения",
"achievements_unlocked_for_game": "Отключени {{achievementCount}} нови постижения за {{gameTitle}}"
},
"tour": {
"subscription_tour_title": "Hydra Cloud Абонамент",
"subscribe_now": "Абонирай се сега",
"cloud_saving": "Запазване в облака",
"cloud_achievements": "Запазете постиженията си в облака",
"animated_profile_picture": "Анимирана профилна снимка",
"premium_support": "Премиум поддръжка",
"show_and_compare_achievements": "Показвайте и сравнявайте постиженията си с тези на други потребители",
"animated_profile_banner": "Анимиран профилен банер"
}
}

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

@@ -25,7 +25,8 @@
"queued": "{{title}} (Queued)",
"game_has_no_executable": "Game has no executable selected",
"sign_in": "Sign in",
"friends": "Friends"
"friends": "Friends",
"need_help": "Need help?"
},
"header": {
"search": "Search games",
@@ -104,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",
@@ -165,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",
@@ -254,7 +257,8 @@
"blocked_users": "Blocked users",
"user_unblocked": "User has been unblocked",
"enable_achievement_notifications": "When an achievement is unlocked",
"launch_minimized": "Launch Hydra minimized"
"launch_minimized": "Launch Hydra minimized",
"disable_nsfw_alert": "Disable NSFW alert"
},
"notifications": {
"download_complete": "Download complete",

View File

@@ -25,7 +25,8 @@
"queued": "{{title}} (En cola)",
"game_has_no_executable": "El juego no tiene un ejecutable seleccionado",
"sign_in": "Iniciar sesión",
"friends": "Amigos"
"friends": "Amigos",
"need_help": "¿Necesitas ayuda?"
},
"header": {
"search": "Buscar juegos",
@@ -99,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",
@@ -165,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",
@@ -253,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",
@@ -360,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

@@ -24,6 +24,7 @@ import kk from "./kk/translation.json";
import cs from "./cs/translation.json";
import nb from "./nb/translation.json";
import et from "./et/translation.json";
import bg from "./bg/translation.json";
export default {
"pt-BR": ptBR,
@@ -48,6 +49,7 @@ export default {
fa,
ro,
ca,
bg,
kk,
cs,
nb,

View File

@@ -25,7 +25,8 @@
"queued": "{{title}} (Na fila)",
"game_has_no_executable": "Jogo não possui executável selecionado",
"sign_in": "Login",
"friends": "Amigos"
"friends": "Amigos",
"need_help": "Precisa de ajuda?"
},
"header": {
"search": "Buscar jogos",
@@ -161,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",
@@ -250,7 +253,8 @@
"blocked_users": "Usuários bloqueados",
"user_unblocked": "Usuário desbloqueado",
"enable_achievement_notifications": "Quando uma conquista é desbloqueada",
"launch_minimized": "Iniciar o Hydra minimizado"
"launch_minimized": "Iniciar o Hydra minimizado",
"disable_nsfw_alert": "Desativar alerta de conteúdo inapropriado"
},
"notifications": {
"download_complete": "Download concluído",

View File

@@ -24,7 +24,8 @@
"queued": "{{title}} (В очереди)",
"game_has_no_executable": "Файл запуска игры не выбран",
"sign_in": "Войти",
"friends": "Друзья"
"friends": "Друзья",
"need_help": "Нужна помощь?"
},
"header": {
"search": "Поиск",

View File

@@ -5,12 +5,12 @@ export const LUDUSAVI_MANIFEST_URL = "https://cdn.losbroxas.org/manifest.yaml";
export const defaultDownloadsPath = app.getPath("downloads");
export const isStaging = import.meta.env.MAIN_VITE_API_URL.includes("staging");
export const databaseDirectory = path.join(app.getPath("appData"), "hydra");
export const databasePath = path.join(
databaseDirectory,
import.meta.env.MAIN_VITE_API_URL.includes("staging")
? "hydra_test.db"
: "hydra.db"
isStaging ? "hydra_test.db" : "hydra.db"
);
export const logsPath = path.join(app.getPath("appData"), "hydra", "logs");
@@ -25,4 +25,4 @@ export const achievementSoundPath = app.isPackaged
export const backupsPath = path.join(app.getPath("userData"), "Backups");
export const appVersion = app.getVersion();
export const appVersion = app.getVersion() + (isStaging ? "-staging" : "");

View File

@@ -38,6 +38,9 @@ export class UserPreferences {
@Column("boolean", { default: false })
startMinimized: boolean;
@Column("boolean", { default: false })
disableNsfwAlert: boolean;
@CreateDateColumn()
createdAt: Date;

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,10 +1,16 @@
import { shell } from "electron";
import { registerEvent } from "../register-event";
import { userAuthRepository } from "@main/repository";
import {
userAuthRepository,
userPreferencesRepository,
} from "@main/repository";
import { HydraApi } from "@main/services";
const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => {
const userAuth = await userAuthRepository.findOne({ where: { id: 1 } });
const [userAuth, userPreferences] = await Promise.all([
userAuthRepository.findOne({ where: { id: 1 } }),
userPreferencesRepository.findOne({ where: { id: 1 } }),
]);
if (!userAuth) {
return;
@@ -16,6 +22,7 @@ const openCheckout = async (_event: Electron.IpcMainInvokeEvent) => {
const params = new URLSearchParams({
token: paymentToken,
lng: userPreferences?.language || "en",
});
shell.openExternal(

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,11 +89,6 @@ const startGameDownload = async (
logger.error("Failed to create game download", err);
});
const { infoHash } = await parseTorrent(payload.uri);
if (infoHash) {
HydraAnalytics.postDownload(infoHash).catch(() => {});
}
await DownloadManager.cancelDownload(updatedGame!.id);
await DownloadManager.startDownload(updatedGame!);

View File

@@ -12,6 +12,7 @@ import { CreateUserSubscription } from "./migrations/20241015235142_create_user_
import { AddBackgroundImageUrl } from "./migrations/20241016100249_add_background_image_url";
import { AddWinePrefixToGame } from "./migrations/20241019081648_add_wine_prefix_to_game";
import { AddStartMinimizedColumn } from "./migrations/20241030171454_add_start_minimized_column";
import { AddDisableNsfwAlertColumn } from "./migrations/20241106053733_add_disable_nsfw_alert_column";
export type HydraMigration = Knex.Migration & { name: string };
class MigrationSource implements Knex.MigrationSource<HydraMigration> {
@@ -28,6 +29,7 @@ class MigrationSource implements Knex.MigrationSource<HydraMigration> {
AddBackgroundImageUrl,
AddWinePrefixToGame,
AddStartMinimizedColumn,
AddDisableNsfwAlertColumn,
]);
}
getMigrationName(migration: HydraMigration): string {

View File

@@ -0,0 +1,17 @@
import type { HydraMigration } from "@main/knex-client";
import type { Knex } from "knex";
export const AddDisableNsfwAlertColumn: HydraMigration = {
name: "AddDisableNsfwAlertColumn",
up: (knex: Knex) => {
return knex.schema.alterTable("user_preferences", (table) => {
return table.boolean("disableNsfwAlert").notNullable().defaultTo(0);
});
},
down: async (knex: Knex) => {
return knex.schema.alterTable("user_preferences", (table) => {
return table.dropColumn("disableNsfwAlert");
});
},
};

View File

@@ -102,7 +102,7 @@ export const mergeAchievements = async (
);
});
})
.filter((achievement) => achievement)
.filter((achievement) => Boolean(achievement))
.map((achievement) => {
return {
displayName: achievement!.displayName,

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) {
@@ -112,6 +111,8 @@ export class HydraApi {
expirationTimestamp: 0,
subscription: null,
};
this.post("/auth/logout", {}, { needsAuth: false }).catch(() => {});
}
static async setupApi() {
@@ -152,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 },
@@ -56,6 +56,7 @@ export const getUserData = () => {
id: loggedUser.userId,
username: "",
bio: "",
email: null,
profileVisibility: "PUBLIC" as ProfileVisibility,
subscription: loggedUser.subscription
? {

View File

@@ -85,6 +85,14 @@ export class WindowManager {
return callback(details);
}
if (details.url.includes("featurebase")) {
return callback(details);
}
if (details.url.includes("chatwoot")) {
return callback(details);
}
const headers = {
"access-control-allow-origin": ["*"],
"access-control-allow-methods": ["GET, POST, PUT, DELETE, OPTIONS"],

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 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: local: *; media-src 'self' local: data: *;"
content="default-src 'self' 'unsafe-inline' * data: local:;"
/>
</head>
<body>

View File

@@ -126,3 +126,9 @@ export const titleBar = style({
zIndex: "4",
borderBottom: `1px solid ${vars.color.border}`,
} as ComplexStyleRule);
export const cloudText = style({
background: "linear-gradient(270deg, #16B195 50%, #3E62C0 100%)",
backgroundClip: "text",
color: "transparent",
});

View File

@@ -54,8 +54,13 @@ export function App() {
hideFriendsModal,
} = useUserDetails();
const { userDetails, fetchUserDetails, updateUserDetails, clearUserDetails } =
useUserDetails();
const {
userDetails,
hasActiveSubscription,
fetchUserDetails,
updateUserDetails,
clearUserDetails,
} = useUserDetails();
const dispatch = useAppDispatch();
@@ -109,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(() => {
@@ -204,7 +218,7 @@ export function App() {
useEffect(() => {
new MutationObserver(() => {
const modal = document.body.querySelector("[role=dialog]");
const modal = document.body.querySelector("[data-hydra-dialog]");
dispatch(toggleDraggingDisabled(Boolean(modal)));
}).observe(document.body, {
@@ -270,7 +284,12 @@ export function App() {
<>
{window.electron.platform === "win32" && (
<div className={styles.titleBar}>
<h4>Hydra</h4>
<h4>
Hydra
{hasActiveSubscription && (
<span className={styles.cloudText}> Cloud</span>
)}
</h4>
</div>
)}

View File

@@ -1,843 +0,0 @@
{
"v": "4.8.0",
"meta": { "g": "LottieFiles AE 3.5.6", "a": "", "k": "", "d": "", "tc": "" },
"fr": 60,
"ip": 0,
"op": 120,
"w": 714,
"h": 678,
"nm": "Pre-comp 1",
"ddd": 0,
"assets": [
{
"id": "comp_0",
"layers": [
{
"ddd": 0,
"ind": 1,
"ty": 4,
"nm": "centro",
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100, "ix": 11 },
"r": { "a": 0, "k": 0, "ix": 10 },
"p": {
"a": 1,
"k": [
{
"i": { "x": 0.214, "y": 1 },
"o": { "x": 0.462, "y": 0 },
"t": 0,
"s": [450, 907, 0],
"to": [0, 0, 0],
"ti": [0, 0, 0]
},
{ "t": 30, "s": [450, 1513, 0] }
],
"ix": 2
},
"a": { "a": 0, "k": [-348, -169, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100, 100], "ix": 6 }
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[0, 0],
[0, 0]
],
"o": [
[0, 0],
[0, 0]
],
"v": [
[-348, -420],
[-348, -30]
],
"c": false
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "st",
"c": {
"a": 0,
"k": [0.854901960784, 0.858823529412, 0.882352941176, 1],
"ix": 3
},
"o": { "a": 0, "k": 100, "ix": 4 },
"w": { "a": 0, "k": 77, "ix": 5 },
"lc": 2,
"lj": 1,
"ml": 4,
"bm": 0,
"nm": "Stroke 1",
"mn": "ADBE Vector Graphic - Stroke",
"hd": false
},
{
"ty": "tr",
"p": { "a": 0, "k": [-348, -164], "ix": 2 },
"a": { "a": 0, "k": [-348, -156], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Shape 1",
"np": 3,
"cix": 2,
"bm": 0,
"ix": 1,
"mn": "ADBE Vector Group",
"hd": false
}
],
"ip": 0,
"op": 120,
"st": 0,
"bm": 0
},
{
"ddd": 0,
"ind": 2,
"ty": 4,
"nm": "esquerdo",
"parent": 1,
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100, "ix": 11 },
"r": {
"a": 1,
"k": [
{
"i": { "x": [0.298], "y": [1] },
"o": { "x": [0.448], "y": [0] },
"t": 6,
"s": [43.5]
},
{ "t": 36, "s": [-1] }
],
"ix": 10
},
"p": { "a": 0, "k": [-348.39, -36.55, 0], "ix": 2 },
"a": { "a": 0, "k": [-2, 84, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100, 100], "ix": 6 }
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[0, 0],
[0, 0]
],
"o": [
[0, 0],
[0, 0]
],
"v": [
[-178, -102],
[-2, 84]
],
"c": false
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "st",
"c": {
"a": 0,
"k": [0.854901960784, 0.858823529412, 0.882352941176, 1],
"ix": 3
},
"o": { "a": 0, "k": 100, "ix": 4 },
"w": { "a": 0, "k": 77, "ix": 5 },
"lc": 2,
"lj": 1,
"ml": 4,
"bm": 0,
"nm": "Stroke 1",
"mn": "ADBE Vector Graphic - Stroke",
"hd": false
},
{
"ty": "tr",
"p": { "a": 0, "k": [0, 0], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Shape 1",
"np": 3,
"cix": 2,
"bm": 0,
"ix": 1,
"mn": "ADBE Vector Group",
"hd": false
},
{
"ty": "tm",
"s": { "a": 0, "k": 8, "ix": 1 },
"e": { "a": 0, "k": 100, "ix": 2 },
"o": { "a": 0, "k": 0, "ix": 3 },
"m": 1,
"ix": 2,
"nm": "Trim Paths 1",
"mn": "ADBE Vector Filter - Trim",
"hd": false
}
],
"ip": 0,
"op": 120,
"st": 0,
"bm": 0
},
{
"ddd": 0,
"ind": 3,
"ty": 4,
"nm": "direito",
"parent": 1,
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100, "ix": 11 },
"r": {
"a": 1,
"k": [
{
"i": { "x": [0.265], "y": [1] },
"o": { "x": [0.53], "y": [0] },
"t": 6,
"s": [-43.5]
},
{ "t": 36, "s": [1] }
],
"ix": 10
},
"p": { "a": 0, "k": [-348.39, -36.55, 0], "ix": 2 },
"a": { "a": 0, "k": [-2, 84, 0], "ix": 1 },
"s": { "a": 0, "k": [-100, 100, 100], "ix": 6 }
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[0, 0],
[0, 0]
],
"o": [
[0, 0],
[0, 0]
],
"v": [
[-178, -102],
[-2, 84]
],
"c": false
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "tm",
"s": { "a": 0, "k": 8, "ix": 1 },
"e": { "a": 0, "k": 100, "ix": 2 },
"o": { "a": 0, "k": 0, "ix": 3 },
"m": 1,
"ix": 2,
"nm": "Trim Paths 1",
"mn": "ADBE Vector Filter - Trim",
"hd": false
},
{
"ty": "st",
"c": {
"a": 0,
"k": [0.854901960784, 0.858823529412, 0.882352941176, 1],
"ix": 3
},
"o": { "a": 0, "k": 100, "ix": 4 },
"w": { "a": 0, "k": 77, "ix": 5 },
"lc": 2,
"lj": 1,
"ml": 4,
"bm": 0,
"nm": "Stroke 1",
"mn": "ADBE Vector Graphic - Stroke",
"hd": false
},
{
"ty": "tr",
"p": { "a": 0, "k": [0, 0], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Shape 1",
"np": 4,
"cix": 2,
"bm": 0,
"ix": 1,
"mn": "ADBE Vector Group",
"hd": false
}
],
"ip": 0,
"op": 120,
"st": 0,
"bm": 0
}
]
},
{
"id": "comp_1",
"layers": [
{
"ddd": 0,
"ind": 1,
"ty": 4,
"nm": "centro",
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100, "ix": 11 },
"r": { "a": 0, "k": 0, "ix": 10 },
"p": {
"a": 1,
"k": [
{
"i": { "x": 0.569, "y": 1 },
"o": { "x": 0.809, "y": 0 },
"t": 0,
"s": [450, 391, 0],
"to": [0, 0, 0],
"ti": [0, 0, 0]
},
{ "t": 30, "s": [450, 997, 0] }
],
"ix": 2
},
"a": { "a": 0, "k": [-348, -169, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100, 100], "ix": 6 }
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[0, 0],
[0, 0]
],
"o": [
[0, 0],
[0, 0]
],
"v": [
[-348, -420],
[-348, -30]
],
"c": false
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "st",
"c": {
"a": 0,
"k": [0.854901960784, 0.858823529412, 0.882352941176, 1],
"ix": 3
},
"o": { "a": 0, "k": 100, "ix": 4 },
"w": { "a": 0, "k": 77, "ix": 5 },
"lc": 2,
"lj": 1,
"ml": 4,
"bm": 0,
"nm": "Stroke 1",
"mn": "ADBE Vector Graphic - Stroke",
"hd": false
},
{
"ty": "tr",
"p": { "a": 0, "k": [-348, -164], "ix": 2 },
"a": { "a": 0, "k": [-348, -156], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Shape 1",
"np": 3,
"cix": 2,
"bm": 0,
"ix": 1,
"mn": "ADBE Vector Group",
"hd": false
}
],
"ip": 0,
"op": 120,
"st": 0,
"bm": 0
},
{
"ddd": 0,
"ind": 2,
"ty": 4,
"nm": "esquerdo",
"parent": 1,
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100, "ix": 11 },
"r": {
"a": 1,
"k": [
{
"i": { "x": [0.552], "y": [1] },
"o": { "x": [0.702], "y": [0] },
"t": 0,
"s": [-1]
},
{ "t": 30, "s": [43.5] }
],
"ix": 10
},
"p": { "a": 0, "k": [-348.39, -36.55, 0], "ix": 2 },
"a": { "a": 0, "k": [-2, 84, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100, 100], "ix": 6 }
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[0, 0],
[0, 0]
],
"o": [
[0, 0],
[0, 0]
],
"v": [
[-178, -102],
[-2, 84]
],
"c": false
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "st",
"c": {
"a": 0,
"k": [0.854901960784, 0.858823529412, 0.882352941176, 1],
"ix": 3
},
"o": { "a": 0, "k": 100, "ix": 4 },
"w": { "a": 0, "k": 77, "ix": 5 },
"lc": 2,
"lj": 1,
"ml": 4,
"bm": 0,
"nm": "Stroke 1",
"mn": "ADBE Vector Graphic - Stroke",
"hd": false
},
{
"ty": "tr",
"p": { "a": 0, "k": [0, 0], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Shape 1",
"np": 3,
"cix": 2,
"bm": 0,
"ix": 1,
"mn": "ADBE Vector Group",
"hd": false
},
{
"ty": "tm",
"s": { "a": 0, "k": 8, "ix": 1 },
"e": { "a": 0, "k": 100, "ix": 2 },
"o": { "a": 0, "k": 0, "ix": 3 },
"m": 1,
"ix": 2,
"nm": "Trim Paths 1",
"mn": "ADBE Vector Filter - Trim",
"hd": false
}
],
"ip": 0,
"op": 120,
"st": 0,
"bm": 0
},
{
"ddd": 0,
"ind": 3,
"ty": 4,
"nm": "direito",
"parent": 1,
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100, "ix": 11 },
"r": {
"a": 1,
"k": [
{
"i": { "x": [0.47], "y": [1] },
"o": { "x": [0.735], "y": [0] },
"t": 0,
"s": [1]
},
{ "t": 30, "s": [-43.5] }
],
"ix": 10
},
"p": { "a": 0, "k": [-348.39, -36.55, 0], "ix": 2 },
"a": { "a": 0, "k": [-2, 84, 0], "ix": 1 },
"s": { "a": 0, "k": [-100, 100, 100], "ix": 6 }
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[0, 0],
[0, 0]
],
"o": [
[0, 0],
[0, 0]
],
"v": [
[-178, -102],
[-2, 84]
],
"c": false
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "tm",
"s": { "a": 0, "k": 8, "ix": 1 },
"e": { "a": 0, "k": 100, "ix": 2 },
"o": { "a": 0, "k": 0, "ix": 3 },
"m": 1,
"ix": 2,
"nm": "Trim Paths 1",
"mn": "ADBE Vector Filter - Trim",
"hd": false
},
{
"ty": "st",
"c": {
"a": 0,
"k": [0.854901960784, 0.858823529412, 0.882352941176, 1],
"ix": 3
},
"o": { "a": 0, "k": 100, "ix": 4 },
"w": { "a": 0, "k": 77, "ix": 5 },
"lc": 2,
"lj": 1,
"ml": 4,
"bm": 0,
"nm": "Stroke 1",
"mn": "ADBE Vector Graphic - Stroke",
"hd": false
},
{
"ty": "tr",
"p": { "a": 0, "k": [0, 0], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Shape 1",
"np": 4,
"cix": 2,
"bm": 0,
"ix": 1,
"mn": "ADBE Vector Group",
"hd": false
}
],
"ip": 0,
"op": 120,
"st": 0,
"bm": 0
}
]
}
],
"layers": [
{
"ddd": 0,
"ind": 1,
"ty": 0,
"nm": "seta 2",
"refId": "comp_0",
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100, "ix": 11 },
"r": { "a": 0, "k": 0, "ix": 10 },
"p": { "a": 0, "k": [357, -247, 0], "ix": 2 },
"a": { "a": 0, "k": [450, 960, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100, 100], "ix": 6 }
},
"ao": 0,
"w": 900,
"h": 1920,
"ip": 30,
"op": 120,
"st": 30,
"bm": 0
},
{
"ddd": 0,
"ind": 2,
"ty": 0,
"nm": "seta",
"refId": "comp_1",
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100, "ix": 11 },
"r": { "a": 0, "k": 0, "ix": 10 },
"p": { "a": 0, "k": [357, 258, 0], "ix": 2 },
"a": { "a": 0, "k": [450, 345, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100, 100], "ix": 6 }
},
"ao": 0,
"w": 900,
"h": 690,
"ip": 0,
"op": 120,
"st": 0,
"bm": 0
},
{
"ddd": 0,
"ind": 3,
"ty": 4,
"nm": "base Outlines",
"sr": 1,
"ks": {
"o": { "a": 0, "k": 100, "ix": 11 },
"r": { "a": 0, "k": 0, "ix": 10 },
"p": { "a": 0, "k": [357, 548.713, 0], "ix": 2 },
"a": { "a": 0, "k": [357.81, 129.934, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100, 100], "ix": 6 }
},
"ao": 0,
"shapes": [
{
"ty": "gr",
"it": [
{
"ind": 0,
"ty": "sh",
"ix": 1,
"ks": {
"a": 0,
"k": {
"i": [
[0, 0],
[0, 50.043],
[0, 0],
[-21.158, 0],
[0, -21.447],
[0, 0],
[-7.049, 0],
[0, 0],
[0, 7.149],
[0, 0],
[-21.158, 0],
[0, -21.447],
[0, 0],
[49.368, 0]
],
"o": [
[-49.369, 0],
[0, 0],
[0, -21.447],
[21.158, 0],
[0, 0],
[0, 7.145],
[0, 0],
[7.053, 0],
[0, 0],
[0, -21.447],
[21.158, 0],
[0, 0],
[0, 50.043],
[0, 0]
],
"v": [
[-268.169, 129.445],
[-357.559, 38.834],
[-357.559, -90.61],
[-319.249, -129.445],
[-280.939, -90.61],
[-280.939, 38.834],
[-268.169, 51.778],
[268.169, 51.778],
[280.939, 38.834],
[280.939, -90.61],
[319.249, -129.445],
[357.559, -90.61],
[357.559, 38.834],
[268.169, 129.445]
],
"c": true
},
"ix": 2
},
"nm": "Path 1",
"mn": "ADBE Vector Shape - Group",
"hd": false
},
{
"ty": "fl",
"c": {
"a": 0,
"k": [0.865977448108, 0.86824388691, 0.890449075138, 1],
"ix": 4
},
"o": { "a": 0, "k": 100, "ix": 5 },
"r": 1,
"bm": 0,
"nm": "Fill 1",
"mn": "ADBE Vector Graphic - Fill",
"hd": false
},
{
"ty": "tr",
"p": { "a": 0, "k": [357.809, 129.695], "ix": 2 },
"a": { "a": 0, "k": [0, 0], "ix": 1 },
"s": { "a": 0, "k": [100, 100], "ix": 3 },
"r": { "a": 0, "k": 0, "ix": 6 },
"o": { "a": 0, "k": 100, "ix": 7 },
"sk": { "a": 0, "k": 0, "ix": 4 },
"sa": { "a": 0, "k": 0, "ix": 5 },
"nm": "Transform"
}
],
"nm": "Group 1",
"np": 2,
"cix": 2,
"bm": 0,
"ix": 1,
"mn": "ADBE Vector Group",
"hd": false
}
],
"ip": 0,
"op": 120,
"st": 0,
"bm": 0
}
],
"markers": []
}

View File

@@ -4,7 +4,7 @@
color: globals.$muted-color;
font-size: 10px;
padding: calc(globals.$spacing-unit / 2) globals.$spacing-unit;
border: solid 1px globals.$border-color;
border: solid 1px globals.$muted-color;
border-radius: 4px;
display: flex;
align-items: center;

View File

@@ -1,7 +1,7 @@
@use "../../scss/globals.scss";
.bottom-panel {
width: "100%";
width: 100%;
border-top: solid 1px globals.$border-color;
background-color: globals.$background-color;
padding: calc(globals.$spacing-unit / 2) calc(globals.$spacing-unit * 2);

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

@@ -13,6 +13,7 @@ export const sidebar = recipe({
borderRight: `solid 1px ${vars.color.border}`,
position: "relative",
overflow: "hidden",
justifyContent: "space-between",
},
variants: {
resizing: {
@@ -124,3 +125,28 @@ export const section = style({
flexDirection: "column",
paddingBottom: `${SPACING_UNIT}px`,
});
export const helpButton = style({
color: vars.color.muted,
padding: `${SPACING_UNIT}px ${SPACING_UNIT * 2}px`,
gap: "9px",
display: "flex",
alignItems: "center",
cursor: "pointer",
borderTop: `solid 1px ${vars.color.border}`,
transition: "background-color ease 0.1s",
":hover": {
backgroundColor: "rgba(255, 255, 255, 0.15)",
},
});
export const helpButtonIcon = style({
background: "linear-gradient(0deg, #16B195 50%, #3E62C0 100%)",
width: "24px",
height: "24px",
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "#fff",
borderRadius: "50%",
});

View File

@@ -5,7 +5,12 @@ import { useLocation, useNavigate } from "react-router-dom";
import type { LibraryGame } from "@types";
import { TextField } from "@renderer/components";
import { useDownload, useLibrary, useToast } from "@renderer/hooks";
import {
useDownload,
useLibrary,
useToast,
useUserDetails,
} from "@renderer/hooks";
import { routes } from "./routes";
@@ -15,6 +20,7 @@ import { buildGameDetailsPath } from "@renderer/helpers";
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
import { SidebarProfile } from "./sidebar-profile";
import { sortBy } from "lodash-es";
import { CommentDiscussionIcon } from "@primer/octicons-react";
const SIDEBAR_MIN_WIDTH = 200;
const SIDEBAR_INITIAL_WIDTH = 250;
@@ -42,6 +48,8 @@ export function Sidebar() {
return sortBy(library, (game) => game.title);
}, [library]);
const { hasActiveSubscription } = useUserDetails();
const { lastPacket, progress } = useDownload();
const { showWarningToast } = useToast();
@@ -166,77 +174,95 @@ export function Sidebar() {
maxWidth: sidebarWidth,
}}
>
<SidebarProfile />
<div
style={{ display: "flex", flexDirection: "column", overflow: "hidden" }}
>
<SidebarProfile />
<div className={styles.content}>
<section className={styles.section}>
<ul className={styles.menu}>
{routes.map(({ nameKey, path, render }) => (
<li
key={nameKey}
className={styles.menuItem({
active: location.pathname === path,
})}
>
<button
type="button"
className={styles.menuItemButton}
onClick={() => handleSidebarItemClick(path)}
<div className={styles.content}>
<section className={styles.section}>
<ul className={styles.menu}>
{routes.map(({ nameKey, path, render }) => (
<li
key={nameKey}
className={styles.menuItem({
active: location.pathname === path,
})}
>
{render()}
<span>{t(nameKey)}</span>
</button>
</li>
))}
</ul>
</section>
<button
type="button"
className={styles.menuItemButton}
onClick={() => handleSidebarItemClick(path)}
>
{render()}
<span>{t(nameKey)}</span>
</button>
</li>
))}
</ul>
</section>
<section className={styles.section}>
<small className={styles.sectionTitle}>{t("my_library")}</small>
<section className={styles.section}>
<small className={styles.sectionTitle}>{t("my_library")}</small>
<TextField
ref={filterRef}
placeholder={t("filter")}
onChange={handleFilter}
theme="dark"
/>
<TextField
ref={filterRef}
placeholder={t("filter")}
onChange={handleFilter}
theme="dark"
/>
<ul className={styles.menu}>
{filteredLibrary.map((game) => (
<li
key={game.id}
className={styles.menuItem({
active:
location.pathname === `/game/${game.shop}/${game.objectID}`,
muted: game.status === "removed",
})}
>
<button
type="button"
className={styles.menuItemButton}
onClick={(event) => handleSidebarGameClick(event, game)}
<ul className={styles.menu}>
{filteredLibrary.map((game) => (
<li
key={game.id}
className={styles.menuItem({
active:
location.pathname ===
`/game/${game.shop}/${game.objectID}`,
muted: game.status === "removed",
})}
>
{game.iconUrl ? (
<img
className={styles.gameIcon}
src={game.iconUrl}
alt={game.title}
loading="lazy"
/>
) : (
<SteamLogo className={styles.gameIcon} />
)}
<button
type="button"
className={styles.menuItemButton}
onClick={(event) => handleSidebarGameClick(event, game)}
>
{game.iconUrl ? (
<img
className={styles.gameIcon}
src={game.iconUrl}
alt={game.title}
loading="lazy"
/>
) : (
<SteamLogo className={styles.gameIcon} />
)}
<span className={styles.menuItemButtonLabel}>
{getGameTitle(game)}
</span>
</button>
</li>
))}
</ul>
</section>
<span className={styles.menuItemButtonLabel}>
{getGameTitle(game)}
</span>
</button>
</li>
))}
</ul>
</section>
</div>
</div>
{hasActiveSubscription && (
<button
type="button"
className={styles.helpButton}
data-open-support-chat
>
<div className={styles.helpButtonIcon}>
<CommentDiscussionIcon size={14} />
</div>
<span>{t("need_help")}</span>
</button>
)}
<button
type="button"
className={styles.handle}

View File

@@ -147,7 +147,8 @@ export function GameDetailsContextProvider({
if (
result?.content_descriptors.ids.includes(
SteamContentDescriptor.AdultOnlySexualContent
)
) &&
!userPreferences?.disableNsfwAlert
) {
setHasNSFWContentBlocked(true);
}
@@ -180,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

@@ -10,12 +10,22 @@ export interface HowLongToBeatEntry {
updatedAt: Date;
}
export interface CatalogueCache {
id?: number;
category: string;
games: { objectId: string; shop: GameShop }[];
createdAt: Date;
updatedAt: Date;
expiresAt: Date;
}
export const db = new Dexie("Hydra");
db.version(4).stores({
db.version(5).stores({
repacks: `++id, title, uris, fileSize, uploadDate, downloadSourceId, repacker, createdAt, updatedAt`,
downloadSources: `++id, url, name, etag, downloadCount, status, createdAt, updatedAt`,
howLongToBeatEntries: `++id, categories, [shop+objectId], createdAt, updatedAt`,
catalogueCache: `++id, category, games, createdAt, updatedAt, expiresAt`,
});
export const downloadSourcesTable = db.table("downloadSources");
@@ -24,4 +34,6 @@ export const howLongToBeatEntriesTable = db.table<HowLongToBeatEntry>(
"howLongToBeatEntries"
);
export const catalogueCacheTable = db.table<CatalogueCache>("catalogueCache");
db.open();

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

@@ -15,6 +15,14 @@ import * as styles from "./home.css";
import { SPACING_UNIT, vars } from "@renderer/theme.css";
import { buildGameDetailsPath } from "@renderer/helpers";
import { CatalogueCategory } from "@shared";
import { catalogueCacheTable, db } from "@renderer/dexie";
import { add } from "date-fns";
const categoryCacheDurationInSeconds = {
[CatalogueCategory.Hot]: 60 * 60 * 2,
[CatalogueCategory.Weekly]: 60 * 60 * 24,
[CatalogueCategory.Achievements]: 60 * 60 * 24,
};
export default function Home() {
const { t } = useTranslation("home");
@@ -36,19 +44,43 @@ export default function Home() {
[CatalogueCategory.Achievements]: [],
});
const getCatalogue = useCallback((category: CatalogueCategory) => {
setCurrentCatalogueCategory(category);
setIsLoading(true);
const getCatalogue = useCallback(async (category: CatalogueCategory) => {
try {
const catalogueCache = await catalogueCacheTable
.where("expiresAt")
.above(new Date())
.and((cache) => cache.category === category)
.first();
window.electron
.getCatalogue(category)
.then((catalogue) => {
setCatalogue((prev) => ({ ...prev, [category]: catalogue }));
})
.catch(() => {})
.finally(() => {
setIsLoading(false);
setCurrentCatalogueCategory(category);
setIsLoading(true);
if (catalogueCache)
return setCatalogue((prev) => ({
...prev,
[category]: catalogueCache.games,
}));
const catalogue = await window.electron.getCatalogue(category);
db.transaction("rw", catalogueCacheTable, async () => {
await catalogueCacheTable.where("category").equals(category).delete();
await catalogueCacheTable.add({
category,
games: catalogue,
createdAt: new Date(),
updatedAt: new Date(),
expiresAt: add(new Date(), {
seconds: categoryCacheDurationInSeconds[category],
}),
});
});
setCatalogue((prev) => ({ ...prev, [category]: catalogue }));
} finally {
setIsLoading(false);
}
}, []);
const getRandomGame = useCallback(() => {

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

@@ -18,6 +18,7 @@ export function SettingsBehavior() {
preferQuitInsteadOfHiding: false,
runAtStartup: false,
startMinimized: false,
disableNsfwAlert: false,
});
const { t } = useTranslation("settings");
@@ -28,6 +29,7 @@ export function SettingsBehavior() {
preferQuitInsteadOfHiding: userPreferences.preferQuitInsteadOfHiding,
runAtStartup: userPreferences.runAtStartup,
startMinimized: userPreferences.startMinimized,
disableNsfwAlert: userPreferences.disableNsfwAlert,
});
}
}, [userPreferences]);
@@ -86,6 +88,14 @@ export function SettingsBehavior() {
/>
</div>
)}
<CheckboxField
label={t("disable_nsfw_alert")}
checked={form.disableNsfwAlert}
onChange={() =>
handleChange({ disableNsfwAlert: !form.disableNsfwAlert })
}
/>
</>
);
}

View File

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

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

@@ -161,6 +161,7 @@ export interface UserPreferences {
preferQuitInsteadOfHiding: boolean;
runAtStartup: boolean;
startMinimized: boolean;
disableNsfwAlert: boolean;
}
export interface Steam250Game {
@@ -240,11 +241,13 @@ export interface Subscription {
status: SubscriptionStatus;
plan: { id: string; name: string };
expiresAt: string | null;
paymentMethod: "pix" | "paypal";
}
export interface UserDetails {
id: string;
username: string;
email: string | null;
displayName: string;
profileImageUrl: string | null;
backgroundImageUrl: string | null;
@@ -257,6 +260,7 @@ export interface UserProfile {
id: string;
displayName: string;
profileImageUrl: string | null;
email: string | null;
backgroundImageUrl: string | null;
profileVisibility: ProfileVisibility;
libraryGames: UserGame[];
@@ -373,4 +377,4 @@ export interface ComparedAchievements {
export * from "./steam.types";
export * from "./real-debrid.types";
export * from "./ludusavi.types";
export * from "./howlongtobeat.types";
export * from "./how-long-to-beat.types";