Compare commits

..

140 Commits

Author SHA1 Message Date
Chubby Granny Chaser
7fd0894b56 Merge pull request #1527 from hydralauncher/feat/sync-download-sources
feat: sync download sources
2025-04-07 00:02:39 +01:00
Chubby Granny Chaser
3bb8c17b16 feat: disabling button when installing 2025-04-07 00:00:43 +01:00
Chubby Granny Chaser
417bb69b86 Merge branch 'feat/sync-download-sources' of github.com:hydralauncher/hydra into feat/sync-download-sources 2025-04-06 23:57:01 +01:00
Chubby Granny Chaser
a55b5dafa0 feat: disabling button when installing 2025-04-06 23:55:39 +01:00
Chubby Granny Chaser
b7bec47a2c Merge branch 'main' into feat/sync-download-sources 2025-04-06 23:54:21 +01:00
Zamitto
f565226a6d Merge pull request #1540 from slakgosh/main
Update translation.json
2025-04-06 19:53:55 -03:00
Chubby Granny Chaser
e7427b8184 feat: disabling button when installing 2025-04-06 23:50:00 +01:00
Chubby Granny Chaser
cc505acbdc feat: disabling button when installing 2025-04-06 23:24:47 +01:00
Chubby Granny Chaser
934831eaae Merge branch 'feat/sync-download-sources' of github.com:hydralauncher/hydra into feat/sync-download-sources 2025-04-06 23:05:35 +01:00
Chubby Granny Chaser
a35dae3e18 feat: disabling button when installing 2025-04-06 23:04:01 +01:00
Chubby Granny Chaser
5e58762151 Merge branch 'main' into feat/sync-download-sources 2025-04-06 22:51:00 +01:00
Chubby Granny Chaser
21535d8acc feat: disabling button when installing 2025-04-06 22:48:12 +01:00
Zamitto
6106325f0a Merge branch 'main' into main 2025-04-06 18:45:44 -03:00
Zamitto
3ff9dd705e Merge pull request #1531 from hydrasources/patch-14
Update translation.json
2025-04-06 18:44:37 -03:00
Chubby Granny Chaser
04f061afa3 feat: adding optional common redist install 2025-04-06 22:33:41 +01:00
Chubby Granny Chaser
f8ea4f29d9 feat: adding optional common redist install 2025-04-06 22:17:54 +01:00
Chubby Granny Chaser
1520e2b2ae feat: adding optional common redist install 2025-04-06 22:13:26 +01:00
slakgosh
d29fc50f68 Update translation.json 2025-04-05 15:33:39 +05:00
Chubby Granny Chaser
ede538392f feat: adding installation logs 2025-04-05 03:31:56 +01:00
Chubby Granny Chaser
f464850c38 feat: setting external common redist 2025-04-02 19:08:31 +01:00
Chubby Granny Chaser
379e90568a feat: setting external common redist 2025-04-02 19:06:21 +01:00
Chubby Granny Chaser
01d440590b feat: adding commonredist 2025-04-02 15:35:40 +01:00
Chubby Granny Chaser
56da86d63d feat: adding recursive automatic extraction 2025-04-02 11:34:57 +01:00
Chubby Granny Chaser
36cf2060c1 feat: adding recursive automatic extraction 2025-04-02 11:26:15 +01:00
Chubby Granny Chaser
5fa4d128c3 feat: adding recursive automatic extraction 2025-04-02 11:25:56 +01:00
Chubby Granny Chaser
d15ef33a86 fix: fixing translation 2025-04-01 23:42:24 +01:00
Chubby Granny Chaser
54ab719935 fix: fixing translation 2025-04-01 23:06:13 +01:00
Chubby Granny Chaser
9eb909cd17 feat: adding different 7z version 2025-04-01 22:36:25 +01:00
Chubby Granny Chaser
b2b4b1ff3c feat: adding different 7z version 2025-04-01 22:36:17 +01:00
Chubby Granny Chaser
f547b85df7 feat: adding different 7z version 2025-04-01 22:25:26 +01:00
Chubby Granny Chaser
0d75878b07 feat: adding initial download sources 2025-04-01 21:39:54 +01:00
hydrasources
9470068de6 Update translation.json 2025-04-01 23:09:38 +03:00
hydrasources
74a92f57bd Update translation.json 2025-04-01 21:57:28 +03:00
Chubby Granny Chaser
73f4b0e869 fix: removing async promise 2025-03-31 01:01:44 +01:00
Chubby Granny Chaser
e6fde48dbd feat: sync download sources 2025-03-31 00:14:07 +01:00
Chubby Granny Chaser
9b8a0af8e9 Merge pull request #1514 from hydralauncher/fix/disabling-debrid
fix: allowing debrid services to be disabled
2025-03-17 21:10:38 +00:00
Chubby Granny Chaser
31319b0811 fix: allowing debrid services to be disabled 2025-03-17 21:02:32 +00:00
Chubby Granny Chaser
9976812bf7 fix: allowing debrid services to be disabled 2025-03-17 20:24:14 +00:00
Zamitto
6b4d422b65 Merge pull request #1498 from hydralauncher/feat/auto-install-on-linux-and-friend-request-notification
feat: auto install update option on linux + new friend request notification
2025-03-15 19:38:02 -03:00
Zamitto
ec9db3a48f feat: refactor watcher for sync friend request 2025-03-15 13:48:07 -03:00
Zamitto
72cde368f5 Merge branch 'main' into feat/auto-install-on-linux-and-friend-request-notification
# Conflicts:
#	src/locales/pt-PT/translation.json
2025-03-15 11:43:35 -03:00
Zamitto
bd77aee3c2 Merge pull request #1510 from hydralauncher/feat/simplify-subscription-expires-at
feat: simplify subscription expires at
2025-03-15 11:41:29 -03:00
Zamitto
dafdda1bc7 Merge pull request #1508 from spydea-tr/translation-tr
[translation] missing and incorrect translations in TR have been fixed.
2025-03-15 11:41:08 -03:00
Zamitto
6c6a238c5f Merge pull request #1507 from lucascanero/translation-PT-pt
[translation]add missing translations on PT-pt
2025-03-15 11:40:41 -03:00
Zamitto
0df18022ed feat: replace darken with color.adjust 2025-03-15 11:05:05 -03:00
Zamitto
6626368613 feat: simplify subscription expires at comparison 2025-03-15 09:40:57 -03:00
Spydea
cff728bbbe Update src/locales/tr/translation.json
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-03-15 14:22:33 +03:00
Spydea
3961b53d0e Update src/locales/tr/translation.json
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-03-15 14:22:21 +03:00
Spydea
3f08e87eda Update src/locales/tr/translation.json
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-03-15 14:22:04 +03:00
Spydea
b84c34a69c Add files via upload 2025-03-15 14:14:19 +03:00
Lucas Cañero
c2e1a092c9 Update src/locales/pt-PT/translation.json
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-03-14 16:10:55 +00:00
Lucas Cañero
fe03894054 Update src/locales/pt-PT/translation.json
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-03-14 16:10:37 +00:00
Lucas Cañero
893695eda0 Update src/locales/pt-PT/translation.json
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-03-14 16:10:25 +00:00
Lucas Cañero
28727600fe Update src/locales/pt-PT/translation.json
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-03-14 16:10:01 +00:00
Lucas Cañero
d892ed4f34 Update src/locales/pt-PT/translation.json
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-03-14 16:09:54 +00:00
Lucas Cañero
ea618febe2 Update src/locales/pt-PT/translation.json
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-03-14 16:09:46 +00:00
Lucas Cañero
358eac061f Update src/locales/pt-PT/translation.json
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-03-14 16:09:38 +00:00
Lucas Cañero
33f9e5729b Update src/locales/pt-PT/translation.json
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-03-14 16:08:42 +00:00
Lucas Cañero
2a7feba5dc revert catalog missing entries 2025-03-14 16:03:46 +00:00
Lucas Cañero
1390f99895 add more missing translation to PT-pt 2025-03-14 16:00:10 +00:00
Lucas Cañero
daf8ee6ba2 add game details and activation PT-pt translations 2025-03-14 15:49:30 +00:00
Lucas Cañero
652fe69141 Update settings translation on Pt-pt 2025-03-14 15:38:14 +00:00
Zamitto
61b2710219 feat: code review 2025-03-14 07:16:13 -03:00
Zamitto
0e67e21223 Merge branch 'main' into feat/auto-install-on-linux-and-friend-request-notification 2025-03-14 06:22:19 -03:00
Zamitto
f12784b6e7 Merge pull request #1392 from Shisuiicaro/Feature/Game-Card-Sources
Feature/streamline repacker list in game cards
2025-03-14 06:04:48 -03:00
Shisuys
8795359c0b EN, ES, RU translation strings 2025-03-13 15:41:48 -03:00
Shisuys
bcc2127a6e Merge branch 'main' into Feature/Game-Card-Sources 2025-03-13 13:47:54 -03:00
Zamitto
766c5760d7 Merge branch 'main' into feat/auto-install-on-linux-and-friend-request-notification 2025-03-13 12:32:05 -03:00
Zamitto
3c8db34456 Merge pull request #1399 from KelvinDiasMoreira/feature/delete-all-dowload-sources
Feature/delete all dowload sources
2025-03-13 12:31:31 -03:00
Kelvin
433ebf55ac changed translation 2025-03-11 22:13:25 -03:00
Kelvin
b68bed97dd Merge branch 'feature/delete-all-dowload-sources' of https://github.com/KelvinDiasMoreira/hydra into feature/delete-all-dowload-sources 2025-03-11 22:04:36 -03:00
Kelvin
2a8600a841 merge branch 'main' of https://github.com/KelvinDiasMoreira/hydra into feature/delete-all-dowload-sources 2025-03-11 22:01:28 -03:00
Zamitto
3fd48eb784 chore: add trigger lp build on release published 2025-03-11 16:28:36 -03:00
Zamitto
aa19241b2b chore: add trigger lp build on release published 2025-03-11 15:43:11 -03:00
Zamitto
a16a75ff39 chore: add trigger lp build on release published 2025-03-11 15:42:03 -03:00
Zamitto
e4d97e9775 chore: add trigger lp build on release published 2025-03-11 15:37:36 -03:00
Zamitto
5688aaf0e8 Merge branch 'main' into feature/delete-all-dowload-sources 2025-03-11 06:24:05 -03:00
Zamitto
ad5eb22553 Merge branch 'main' into Feature/Game-Card-Sources 2025-03-11 06:23:17 -03:00
Zamitto
1c3a5f4800 Merge branch 'main' into feat/auto-install-on-linux-and-friend-request-notification 2025-03-11 06:21:18 -03:00
Zamitto
d8a7275512 Merge pull request #1446 from KelvinDiasMoreira/feature/screen-state
feat: screen state
2025-03-11 06:20:46 -03:00
Zamitto
489dd710fa small typing adjustments 2025-03-11 06:11:16 -03:00
Kelvin
0db93c7dad Merge branch 'main' into feature/delete-all-dowload-sources 2025-03-11 00:02:07 -03:00
Kelvin
d9fe3c709e resolved conflicts 2025-03-10 23:54:03 -03:00
Kelvin
03a3d734f1 resolved conflicts 2025-03-10 23:46:18 -03:00
Zamitto
106d66f1a2 Merge branch 'main' into feat/auto-install-on-linux-and-friend-request-notification 2025-03-10 20:35:55 -03:00
Zamitto
3ef6472825 Merge pull request #1485 from Shisuiicaro/Fix/Pixeldrain
Fix: Pixeldrain 403
2025-03-10 20:35:45 -03:00
Zamitto
df77d754ae Merge branch 'main' into Fix/Pixeldrain 2025-03-10 20:30:48 -03:00
Zamitto
6a8da74bcf feat: refactor and i18n 2025-03-10 20:29:12 -03:00
Zamitto
1aa3bb1ad2 Merge branch 'main' into feat/auto-install-on-linux-and-friend-request-notification 2025-03-10 20:23:53 -03:00
Zamitto
fdcf244f4e feat: i18n 2025-03-10 20:23:32 -03:00
Zamitto
d8a4eaaa66 feat: code adjustments 2025-03-10 20:21:17 -03:00
Zamitto
1c71c20b48 Merge pull request #1481 from Shisuiicaro/Fix/Datanodes
Fix: Datanodes
2025-03-10 20:17:45 -03:00
Zamitto
aefc5eca49 Update package.json 2025-03-10 20:07:54 -03:00
Zamitto
5f1b83a0d9 Merge branch 'main' into Fix/Datanodes 2025-03-10 20:00:31 -03:00
Zamitto
85efc23c25 feat: friend request notification adjustments 2025-03-10 19:50:53 -03:00
Zamitto
b344a1850a feat: auto install default false on linux and friend request notification 2025-03-10 18:57:13 -03:00
Chubby Granny Chaser
497f5e7742 Merge pull request #1491 from Lianela/main
feat: update es translation.json
2025-03-09 22:10:59 +00:00
Chubby Granny Chaser
199b0d5b19 Merge branch 'main' into main 2025-03-09 22:08:09 +00:00
Chubby Granny Chaser
2554dc4c69 feat: updating steam-games.json 2025-03-09 21:47:27 +00:00
Chubby Granny Chaser
c4c401e054 Merge pull request #1495 from hydralauncher/feat/automatic-cloud-sync
feat: adding automatic cloud sync
2025-03-09 21:38:13 +00:00
Chubby Granny Chaser
864ff0070f chore: bump version 2025-03-09 21:33:09 +00:00
Chubby Granny Chaser
da8c40d5dc Merge branch 'feat/automatic-cloud-sync' of github.com:hydralauncher/hydra into feat/automatic-cloud-sync 2025-03-09 19:36:14 +00:00
Chubby Granny Chaser
a32fdf3385 style: using fs promises 2025-03-09 19:28:47 +00:00
Chubby Granny Chaser
c9c1750afb Merge branch 'main' into feat/automatic-cloud-sync 2025-03-09 19:18:22 +00:00
Chubby Granny Chaser
857063d2c7 feat: adding automatic cloud sync 2025-03-09 19:17:59 +00:00
Chubby Granny Chaser
ed699a8dee feat: adding automatic cloud sync 2025-03-09 19:14:24 +00:00
Shisuys
a9ec1a62bf Merge branch 'main' into Fix/Datanodes 2025-03-07 12:49:14 -03:00
Lianela
778e921594 fix: translation.json
spacing
2025-03-05 22:53:34 -06:00
Lianela
206886c091 feat: update es translation.json 2025-03-05 20:26:58 -06:00
Shisuys
6832c11d06 Fix Pixeldrain/pdcybar Ratelimit 2025-03-03 13:25:29 -03:00
Shisuys
740cc2ec7c Update datanodes.ts 2025-03-02 15:59:06 -03:00
Shisuys
e12c6daa16 Merge branch 'main' into Feature/Game-Card-Sources 2025-02-28 00:07:53 -03:00
Shisuys
2ee8a64945 Update datanodes.ts 2025-02-27 00:36:54 -03:00
Shisuys
a54edd8b4a Merge branch 'main' into Fix/Datanodes 2025-02-27 00:15:25 -03:00
Shisuys
f7b6a91621 Update datanodes.ts 2025-02-27 00:05:53 -03:00
Shisuys
96dbfe8593 fix datanodes 2025-02-27 00:00:14 -03:00
Shisuys
dddc5bfd96 Update game-card.tsx 2025-02-11 19:17:58 -03:00
Shisuys
282bb45869 Update game-card.tsx 2025-02-11 19:15:20 -03:00
Shisuys
c982ebe33a Merge branch 'main' into Feature/Game-Card-Sources 2025-02-11 18:37:13 -03:00
Kelvin
08935ceabd refactor to levelDB 2025-02-05 22:50:02 -03:00
Chubby Granny Chaser
0e7a0d1afa Merge branch 'main' into feature/screen-state 2025-02-05 23:56:08 +00:00
Kelvin
38dc8bdea5 fix 2025-02-04 22:26:01 -03:00
Kelvin
c33aa3c874 fix 2025-02-04 00:50:18 -03:00
Kelvin
afa34c74d7 fix 2025-02-04 00:48:55 -03:00
Kelvin
cda9b8fb42 feat: screen state 2025-02-04 00:37:50 -03:00
Kelvin
026abd7909 change button to top 2025-02-03 21:48:07 -03:00
Kelvin
bc31a28eee resolved conflicts and prettier 2025-02-02 17:36:16 -03:00
Kelvin
f1624441b4 Merge branch 'main' into feature/delete-all-dowload-sources 2025-01-29 14:38:12 -03:00
Shisuys
358abde963 Merge branch 'main' into Feature/Game-Card-Sources 2025-01-26 10:47:01 -03:00
Kelvin
c65fc15d99 Merge branch 'main' into feature/delete-all-dowload-sources 2025-01-21 23:23:14 -03:00
Kelvin
b32226273e fix 2025-01-14 01:30:41 -03:00
Kelvin
2e7a2a05a5 added translations and logic fix 2025-01-14 01:00:51 -03:00
Kelvin
ab70ff10be running format 2025-01-13 23:43:34 -03:00
Kelvin
6a429f9f39 added logic and some translate 2025-01-13 01:38:29 -03:00
Shisuys
b03c69b185 Added pluralization 2025-01-12 15:52:02 -03:00
Shisuys
3fe77f4961 Update game-card.tsx 2025-01-11 11:56:17 -03:00
Shisuys
79be151de0 Merge branch 'Feature/Game-Card-Sources' of https://github.com/Shisuiicaro/hydra-pr into Feature/Game-Card-Sources 2025-01-11 11:52:17 -03:00
Shisuys
e804b92f30 Update game-card.tsx 2025-01-11 11:50:22 -03:00
Shisuys
cf31b36961 Merge branch 'hydralauncher:main' into Feature/Game-Card-Sources 2025-01-11 11:43:27 -03:00
Shisuys
3156baf78e Limit visible sources 2025-01-11 11:40:48 -03:00
115 changed files with 2401 additions and 915 deletions

View File

@@ -1,2 +1,4 @@
MAIN_VITE_API_URL=API_URL
MAIN_VITE_AUTH_URL=AUTH_URL
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID=
RENDERER_VITE_TORBOX_REFERRAL_CODE=

View File

@@ -47,12 +47,13 @@ jobs:
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
- name: Build Windows
if: matrix.os == 'windows-latest'
@@ -61,14 +62,15 @@ jobs:
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
RENDERER_VITE_INTERCOM_APP_ID: ${{ vars.RENDERER_VITE_INTERCOM_APP_ID }}
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
- name: Test Upload build
- name: Upload build
env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
S3_ENDPOINT: ${{ secrets.S3_ENDPOINT }}
@@ -78,7 +80,6 @@ jobs:
BUILDS_URL: ${{ secrets.BUILDS_URL }}
BUILD_WEBHOOK_URL: ${{ secrets.BUILD_WEBHOOK_URL }}
GITHUB_ACTOR: ${{ github.actor }}
run: node scripts/upload-build.cjs
- name: Create artifact

View File

@@ -49,13 +49,13 @@ jobs:
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
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.EXTERNAL_RESOURCES_URL }}
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
- name: Build Windows
if: matrix.os == 'windows-latest'
@@ -64,13 +64,13 @@ jobs:
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
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.EXTERNAL_RESOURCES_URL }}
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
- name: Create artifact
uses: actions/upload-artifact@v4

13
.github/workflows/trigger-lp.yml vendored Normal file
View File

@@ -0,0 +1,13 @@
name: Trigger Landing Page Build
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Trigger Landing Page build
run: curl --location --request POST '${{ secrets.LP_TRIGGER_DEPLOY_URL }}'

BIN
binaries/7z.dll Normal file

Binary file not shown.

BIN
binaries/7z.exe Normal file

Binary file not shown.

BIN
binaries/7zz Normal file

Binary file not shown.

BIN
binaries/7zzs Normal file

Binary file not shown.

View File

@@ -20,6 +20,9 @@ asarUnpack:
- resources/**
win:
executableName: Hydra
extraResources:
- from: binaries/7z.exe
- from: binaries/7z.dll
target:
- nsis
- portable
@@ -35,6 +38,8 @@ portable:
artifactName: ${name}-${version}-portable.${ext}
mac:
entitlementsInherit: build/entitlements.mac.plist
extraResources:
- from: binaries/7zz
extendInfo:
- NSCameraUsageDescription: Application requests access to the device's camera.
- NSMicrophoneUsageDescription: Application requests access to the device's microphone.
@@ -44,6 +49,8 @@ mac:
dmg:
artifactName: ${name}-${version}.${ext}
linux:
extraResources:
- from: binaries/7zzs
target:
- AppImage
- snap

View File

@@ -16,9 +16,6 @@ export default defineConfig(({ mode }) => {
main: {
build: {
sourcemap: true,
rollupOptions: {
external: ["better-sqlite3"],
},
},
resolve: {
alias: {

View File

@@ -1,6 +1,6 @@
{
"name": "hydralauncher",
"version": "3.2.3",
"version": "3.4.0",
"description": "Hydra",
"main": "./out/main/index.js",
"author": "Los Broxas",
@@ -28,8 +28,7 @@
"build:win": "electron-vite build && electron-builder --win",
"build:mac": "electron-vite build && electron-builder --mac",
"build:linux": "electron-vite build && electron-builder --linux",
"prepare": "husky",
"knex:migrate:make": "knex --knexfile src/main/knexfile.ts migrate:make --esm"
"prepare": "husky"
},
"dependencies": {
"@electron-toolkit/preload": "^3.0.0",
@@ -44,7 +43,7 @@
"@sentry/vite-plugin": "^2.22.7",
"auto-launch": "^5.0.6",
"axios": "^1.7.9",
"better-sqlite3": "^11.7.0",
"axios-cookiejar-support": "^5.0.5",
"classic-level": "^2.0.0",
"classnames": "^2.5.1",
"color": "^4.2.3",
@@ -61,7 +60,6 @@
"jsdom": "^24.0.0",
"jsonwebtoken": "^9.0.2",
"kill-port": "^2.0.1",
"knex": "^3.1.0",
"lodash-es": "^4.17.21",
"parse-torrent": "^11.0.17",
"piscina": "^4.7.0",
@@ -75,6 +73,7 @@
"sound-play": "^1.1.0",
"sudo-prompt": "^9.2.1",
"tar": "^7.4.3",
"tough-cookie": "^5.1.1",
"user-agents": "^1.1.387",
"yaml": "^2.6.1",
"yup": "^1.5.0",

File diff suppressed because one or more lines are too long

View File

@@ -255,6 +255,12 @@
"download_sources_description": "سيقوم Hydra بجلب روابط التنزيل من هذه المصادر. يجب أن يكون عنوان URL للمصدر رابطًا مباشرًا لملف .json يحتوي على روابط التنزيل.",
"validate_download_source": "تحقق",
"remove_download_source": "إزالة",
"removed_download_sources": "تمت إزالة مصادر التنزيل",
"cancel_button_confirmation_delete_all_sources": "لا",
"confirm_button_confirmation_delete_all_sources": "نعم، احذف كل شيء",
"description_confirmation_delete_all_sources": "سوف تقوم بحذف جميع مصادر التنزيل",
"title_confirmation_delete_all_sources": "احذف جميع مصادر التنزيل",
"button_delete_all_sources": "قم بإزالة جميع مصادر التنزيل",
"add_download_source": "إضافة مصدر",
"download_count_zero": "لا توجد خيارات تنزيل",
"download_count_one": "{{countFormatted}} خيار تنزيل",

View File

@@ -107,6 +107,12 @@
"notifications": "Апавяшчэнні",
"enable_download_notifications": "Па сканчэнні сцягванні",
"enable_repack_list_notifications": "Пры даданні новага рэпака",
"cancel_button_confirmation_delete_all_sources": "Няма",
"confirm_button_confirmation_delete_all_sources": "Так,выдаліць усё",
"description_confirmation_delete_all_sources": "Вы выдаліце ​​ўсе крыніцы сцягвання",
"title_confirmation_delete_all_sources": "Выдаліць усе крыніцы сцягвання",
"removed_download_sources": "Крыніцы сцягвання выдалены",
"button_delete_all_sources": "Премахнете всички източници на изтегляне",
"behavior": "Паводзіны",
"quit_app_instead_hiding": "Закрываць праграму замест таго, каб хаваць яе ў трэй",
"launch_with_system": "Запускаць праграму пры запуску сыстэмы"

View File

@@ -253,6 +253,12 @@
"download_source_errored": "Сгрешен",
"sync_download_sources": "Синхронизирай източниците",
"removed_download_source": "Източника за сваляне е премахнат",
"cancel_button_confirmation_delete_all_sources": "не",
"confirm_button_confirmation_delete_all_sources": "Да, удалить все",
"description_confirmation_delete_all_sources": "Вы удалите все источники загрузки",
"title_confirmation_delete_all_sources": "Удалить все источники загрузки",
"removed_download_sources": "Шрифты удалены",
"button_delete_all_sources": "Удалить все источники загрузки",
"added_download_source": "Добавен източник за сваляне",
"download_sources_synced": "Всички източници за сваляне са синхронизирани",
"insert_valid_json_url": "Добавете ваиден JSON линк",

View File

@@ -175,6 +175,12 @@
"download_sources_description": "Hydra buscarà els enllaços de descàrrega d'aquestes fonts. L'URL d'origen ha de ser un enllaç directe a un fitxer .json que contingui els enllaços de descàrrega.",
"validate_download_source": "Valida",
"remove_download_source": "Elimina",
"cancel_button_confirmation_delete_all_sources": "No",
"confirm_button_confirmation_delete_all_sources": "Sí, esborra-ho tot",
"description_confirmation_delete_all_sources": "Eliminareu totes les fonts de descàrrega",
"title_confirmation_delete_all_sources": "Suprimeix totes les fonts de baixada",
"removed_download_sources": "S'han eliminat les fonts de descàrrega",
"button_delete_all_sources": "Elimina totes les fonts de baixada",
"add_download_source": "Afegeix font",
"download_count_zero": "No hi ha baixades a la llista",
"download_count_one": "{{countFormatted}} a la llista de baixades",

View File

@@ -237,6 +237,12 @@
"download_source_errored": "Chyba",
"sync_download_sources": "Synchronizovat zdroje",
"removed_download_source": "Zdroj odebrán",
"cancel_button_confirmation_delete_all_sources": "Žádný",
"confirm_button_confirmation_delete_all_sources": "Ano, smazat vše",
"description_confirmation_delete_all_sources": "Smažete všechny zdroje stahování",
"title_confirmation_delete_all_sources": "Odstraňte všechny zdroje stahování",
"removed_download_sources": "Zdroje stahování byly odstraněny",
"button_delete_all_sources": "Odstraňte všechny zdroje stahování",
"added_download_source": "Zdroj přidán",
"download_sources_synced": "Všechny zdroje jsou synchronizovány",
"insert_valid_json_url": "Zadej platnou JSON adresu",

View File

@@ -201,6 +201,12 @@
"download_source_errored": "Fejlede",
"sync_download_sources": "Synkronisér kilder",
"removed_download_source": "Download kilde fjernet",
"cancel_button_confirmation_delete_all_sources": "Ingen",
"confirm_button_confirmation_delete_all_sources": "Ja, slet alt",
"description_confirmation_delete_all_sources": "Du vil slette alle downloadkilder",
"title_confirmation_delete_all_sources": "Slet alle downloadkilder",
"removed_download_sources": "Download kilder fjernet",
"button_delete_all_sources": "Fjern alle downloadkilder",
"added_download_source": "Tilføjede download kilde",
"download_sources_synced": "Alle download kilder er synkroniserede",
"insert_valid_json_url": "Indsæt en gyldig JSON url",

View File

@@ -185,6 +185,12 @@
"download_source_errored": "Fehlgeschlagen",
"sync_download_sources": "Quellen synchronisieren",
"removed_download_source": "Download-Quelle entfernt",
"cancel_button_confirmation_delete_all_sources": "Nein",
"confirm_button_confirmation_delete_all_sources": "Ja, alles löschen",
"description_confirmation_delete_all_sources": "Du löschen alle Downloadquellen",
"title_confirmation_delete_all_sources": "Löschen du alle Downloadquellen",
"removed_download_sources": "Download-Quellen entfernt",
"button_delete_all_sources": "Entfernen Sie alle Downloadquellen",
"added_download_source": "Download-Quelle hinzugefügt",
"download_sources_synced": "Alle Download-Quellen sind synchronisiert",
"insert_valid_json_url": "Füge eine gültige JSON URL ein",

View File

@@ -44,7 +44,10 @@
"downloading_metadata": "Downloading {{title}} metadata…",
"downloading": "Downloading {{title}}… ({{percentage}} complete) - Completion {{eta}} - {{speed}}",
"calculating_eta": "Downloading {{title}}… ({{percentage}} complete) - Calculating remaining time…",
"checking_files": "Checking {{title}} files… ({{percentage}} complete)"
"checking_files": "Checking {{title}} files… ({{percentage}} complete)",
"installing_common_redist": "{{log}}…",
"installation_complete": "Installation complete",
"installation_complete_message": "Common redistributables installed successfully"
},
"catalogue": {
"search": "Filter…",
@@ -178,6 +181,8 @@
"manage_files_description": "Manage which files will be backed up and restored",
"select_folder": "Select folder",
"backup_from": "Backup from {{date}}",
"automatic_backup_from": "Automatic backup from {{date}}",
"enable_automatic_cloud_sync": "Enable automatic cloud sync",
"custom_backup_location_set": "Custom backup location set",
"no_directory_selected": "No directory selected",
"no_write_permission": "Cannot download into this directory. Click here to learn more.",
@@ -191,7 +196,8 @@
"download_error_not_cached_in_real_debrid": "This download is not available on Real-Debrid and polling download status from Real-Debrid is not yet available.",
"download_error_not_cached_in_torbox": "This download is not available on Torbox and polling download status from Torbox is not yet available.",
"game_removed_from_favorites": "Game removed from favorites",
"game_added_to_favorites": "Game added to favorites"
"game_added_to_favorites": "Game added to favorites",
"automatically_extract_downloaded_files": "Automatically extract downloaded files"
},
"activation": {
"title": "Activate Hydra",
@@ -228,7 +234,9 @@
"seeding": "Seeding",
"stop_seeding": "Stop seeding",
"resume_seeding": "Resume seeding",
"options": "Manage"
"options": "Manage",
"extract": "Extract files",
"extracting": "Extracting files…"
},
"settings": {
"downloads_path": "Downloads path",
@@ -265,6 +273,12 @@
"download_source_errored": "Errored",
"sync_download_sources": "Sync sources",
"removed_download_source": "Download source removed",
"removed_download_sources": "Download sources removed",
"cancel_button_confirmation_delete_all_sources": "No",
"confirm_button_confirmation_delete_all_sources": "Yes, delete everything",
"title_confirmation_delete_all_sources": "Delete all download sources",
"description_confirmation_delete_all_sources": "You will delete all download sources",
"button_delete_all_sources": "Remove all",
"added_download_source": "Added download source",
"download_sources_synced": "All download sources are synced",
"insert_valid_json_url": "Insert a valid JSON url",
@@ -327,12 +341,20 @@
"enable_torbox": "Enable Torbox",
"torbox_description": "TorBox is your premium seedbox service rivaling even the best servers on the market.",
"torbox_account_linked": "TorBox account linked",
"create_real_debrid_account": "Click here if you don't have a Real-Debrid account yet",
"create_torbox_account": "Click here if you don't have a TorBox account yet",
"real_debrid_account_linked": "Real-Debrid account linked",
"name_min_length": "Theme name must be at least 3 characters long",
"import_theme": "Import theme",
"import_theme_description": "You will import {{theme}} from the theme store",
"error_importing_theme": "Error importing theme",
"theme_imported": "Theme imported successfully"
"theme_imported": "Theme imported successfully",
"enable_friend_request_notifications": "When a friend request is received",
"enable_auto_install": "Download updates automatically",
"common_redist": "Common redistributables",
"common_redist_description": "Common redistributables are required to run some games. Installing them is recommended to avoid issues.",
"install_common_redist": "Install",
"installing_common_redist": "Installing…"
},
"notifications": {
"download_complete": "Download complete",
@@ -343,13 +365,19 @@
"new_update_available": "Version {{version}} available",
"restart_to_install_update": "Restart Hydra to install the update",
"notification_achievement_unlocked_title": "Achievement unlocked for {{game}}",
"notification_achievement_unlocked_body": "{{achievement}} and other {{count}} were unlocked"
"notification_achievement_unlocked_body": "{{achievement}} and other {{count}} were unlocked",
"new_friend_request_description": "You have received a new friend request",
"new_friend_request_title": "New friend request",
"extraction_complete": "Extraction complete",
"game_extracted": "{{title}} extracted successfully"
},
"system_tray": {
"open": "Open Hydra",
"quit": "Quit"
},
"game_card": {
"available_one": "Available",
"available_other": "Available",
"no_downloads": "No downloads available"
},
"binary_not_found_modal": {

View File

@@ -174,6 +174,8 @@
"manage_files_description": "Gestiona los archivos que serán respaldados y restaurados",
"select_folder": "Seleccionar carpeta",
"backup_from": "Copia de seguridad de {{date}}",
"automatic_backup_from": "Copia de seguridad automática de {{date}}",
"enable_automatic_cloud_sync": "Habilitar sincronización automática en la nube",
"custom_backup_location_set": "Se configuró la carpeta de copia de seguridad",
"clear": "Limpiar",
"no_directory_selected": "No se seleccionó un directorio",
@@ -185,7 +187,13 @@
"reset_achievements_description": "Esto reiniciará todos los logros de {{game}}",
"reset_achievements_title": "¿Estás seguro?",
"reset_achievements_success": "Logros reiniciados exitosamente",
"reset_achievements_error": "Se produjo un error al reiniciar los logros"
"reset_achievements_error": "Se produjo un error al reiniciar los logros",
"download_error_gofile_quota_exceeded": "Has excedido la cuota mensual de Gofile. Por favor espera a que se reinicie la cuota.",
"download_error_real_debrid_account_not_authorized": "Tu cuenta de Real-Debrid no está autorizada para nueva descargas. Por favor, revisa los ajustes de tu cuenta e intenta de nuevo.",
"download_error_not_cached_in_real_debrid": "Esta descarga no está disponible en Real-Debrid y el estado de descarga del sondeo de Real-Debrid aún no está disponible.",
"download_error_not_cached_in_torbox": "Esta descarga no está disponible en Torbox y el estado de descarga del sondeo aún no está disponible.",
"game_added_to_favorites": "Juego añadido a favoritos",
"game_removed_from_favorites": "Juego removido de favoritos"
},
"activation": {
"title": "Activar Hydra",
@@ -259,6 +267,12 @@
"download_source_errored": "Error",
"sync_download_sources": "Sincronizar fuentes",
"removed_download_source": "Fuente de descarga eliminada",
"cancel_button_confirmation_delete_all_sources": "No",
"confirm_button_confirmation_delete_all_sources": "Sí, eliminar todo",
"description_confirmation_delete_all_sources": "Eliminarás todas las fuentes de descarga",
"title_confirmation_delete_all_sources": "Eliminar todas las fuentes de descarga",
"removed_download_sources": "Fuentes de descarga eliminadas",
"button_delete_all_sources": "Eliminar todas las fuentes de descarga",
"added_download_source": "Fuente de descarga añadida",
"download_sources_synced": "Todas las fuentes de descargas están actualizadas.",
"insert_valid_json_url": "Introduce una URL JSON válida",
@@ -290,14 +304,45 @@
"no_email_account": "No has configurado un correo aún",
"no_subscription": "Disfruta Hydra de la mejor manera",
"no_users_blocked": "No tienes usuarios bloqueados",
"notifications": "Notificaciones",
"renew_subscription": "Renovar Hydra Cloud",
"subscription_active_until": "Tu Hydra Cloud está activa hasta {{date}}",
"subscription_expired_at": "Tú suscripción expiró el {{date}}",
"subscription_renew_cancelled": "Está desactivada la renovación automática",
"subscription_renews_on": "Tú suscripción se renueva el {{date}}",
"update_email": "Actualizar correo",
"update_password": "Actualizar contraseña"
"update_password": "Actualizar contraseña",
"appearance": "Apariencia",
"become_subscriber": "Sé Hydra Cloud",
"cancel": "Cancelar",
"clear_themes": "Limpiar",
"create_theme": "Crear",
"create_theme_modal_description": "Crea un nuevo tema para personalizar la apariencia de Hydra",
"create_theme_modal_title": "Crear tema personalizado",
"delete_all_themes": "Eliminar todos los temas",
"delete_all_themes_description": "Esto eliminará todos tus temas personalizados",
"delete_theme": "Eliminar tema",
"delete_theme_description": "Esto eliminará el tema {{theme}}",
"edit_theme": "Editar tema",
"editor_tab_code": "Código",
"editor_tab_info": "Info",
"editor_tab_save": "Guardar",
"enable_torbox": "Habilitar Torbox",
"error_importing_theme": "Error al importar el tema",
"import_theme": "Importar tema",
"import_theme_description": "Vas a importar el tema {{theme}} desde la tienda de temas",
"insert_theme_name": "Introducí el nombre del tema",
"name_min_length": "El tema tiene que tener 3 carácteres de largo mínimo",
"no_themes": "Parece que no tenés ningún tema aún, pero no te preocupes, presiona acá para crear tu primer tema.",
"real_debrid_account_linked": "Cuenta de Real-Debrid vinculada",
"set_theme": "Establecer tema",
"theme_imported": "Tema importado exitosamente",
"theme_name": "Nombre",
"torbox_account_linked": "Cuenta de TorBox vinculada",
"torbox_description": "TorBox es tu servicio premium de seedbox que rivaliza incluso a los mejores servidores del mercado.",
"unset_theme": "Desactivar tema",
"web_store": "Tienda Web",
"enable_friend_request_notifications": "Cuando se recibe una solicitud de amistad",
"enable_auto_install": "Descargar actualizaciones automáticamente"
},
"notifications": {
"download_complete": "Descarga completada",
@@ -308,13 +353,17 @@
"new_update_available": "Version {{version}} disponible",
"restart_to_install_update": "Reinicia Hydra para instalar la actualización",
"notification_achievement_unlocked_title": "Logro desbloqueado de {{game}}",
"notification_achievement_unlocked_body": "{{achievement}} y otros {{count}} fueron desbloqueados"
"notification_achievement_unlocked_body": "{{achievement}} y otros {{count}} fueron desbloqueados",
"new_friend_request_title": "Nueva solicitud de amistad",
"new_friend_request_description": "Has recibido una nueva solicitud de amistad"
},
"system_tray": {
"open": "Abrir Hydra",
"quit": "Salir"
},
"game_card": {
"available_one": "Disponible",
"available_other": "Disponibles",
"no_downloads": "No hay descargas disponibles"
},
"binary_not_found_modal": {

View File

@@ -236,6 +236,12 @@
"download_source_errored": "Vigane",
"sync_download_sources": "Sünkroniseeri allikad",
"removed_download_source": "Allalaadimise allikas eemaldatud",
"cancel_button_confirmation_delete_all_sources": "Ei",
"confirm_button_confirmation_delete_all_sources": "Jah, kustuta kõik",
"description_confirmation_delete_all_sources": "Kustutate kõik allalaadimisallikad",
"title_confirmation_delete_all_sources": "Kustutage kõik allalaadimisallikad",
"removed_download_sources": "Allalaadimise allikas eemaldati",
"button_delete_all_sources": "Eemaldage kõik allalaadimisallikad",
"added_download_source": "Allalaadimise allikas lisatud",
"download_sources_synced": "Kõik allalaadimise allikad on sünkroniseeritud",
"insert_valid_json_url": "Sisesta kehtiv JSON url",

View File

@@ -111,6 +111,12 @@
"launch_with_system": "زمانی که سیستم روشن می‌شود، هایدرا را باز کن",
"general": "کلی",
"behavior": "رفتار",
"cancel_button_confirmation_delete_all_sources": "خیر",
"confirm_button_confirmation_delete_all_sources": "ہاں، سب کچھ حذف کر دیں۔",
"description_confirmation_delete_all_sources": "آپ ڈاؤن لوڈ کے تمام ذرائع کو حذف کر دیں گے۔",
"title_confirmation_delete_all_sources": "تمام منابع دانلود را حذف کنید",
"removed_download_sources": "منابع دانلود حذف شد",
"button_delete_all_sources": "تمام منابع دانلود را حذف کنید",
"enable_real_debrid": "فعال‌سازی Real-Debrid",
"debrid_api_token_hint": "کلید API خود را از <ب0>اینجا</0> بگیرید.",
"save_changes": "ذخیره تغییرات"

View File

@@ -99,7 +99,13 @@
"notifications": "Notifications",
"enable_download_notifications": "Quand un téléchargement est terminé",
"enable_repack_list_notifications": "Quand un nouveau repack est ajouté",
"language": "Langue"
"language": "Langue",
"cancel_button_confirmation_delete_all_sources": "Non",
"confirm_button_confirmation_delete_all_sources": "Oui, tout supprimer",
"description_confirmation_delete_all_sources": "Vous supprimerez toutes les sources de téléchargement",
"title_confirmation_delete_all_sources": "Supprimer toutes les sources de téléchargement",
"removed_download_sources": "Sources de téléchargement supprimées",
"button_delete_all_sources": "Supprimer toutes les sources de téléchargement"
},
"notifications": {
"download_complete": "Téléchargement terminé",

View File

@@ -106,6 +106,12 @@
"change": "Frissítés",
"notifications": "Értesítések",
"enable_download_notifications": "Amikor egy letöltés befejeződik",
"cancel_button_confirmation_delete_all_sources": "Nem",
"confirm_button_confirmation_delete_all_sources": "Igen, törölj mindent",
"description_confirmation_delete_all_sources": "Törölni fog minden letöltési forrást",
"title_confirmation_delete_all_sources": "Törölje az összes letöltési forrást",
"removed_download_sources": "Betűtípusok eltávolítva",
"button_delete_all_sources": "Távolítsa el az összes letöltési forrást",
"enable_repack_list_notifications": "Amikor egy új repack hozzáadásra kerül"
},
"notifications": {

View File

@@ -175,6 +175,12 @@
"download_sources_description": "Hydra akan mencari link unduhan dari sini. URL harus menuju file .json dengan link unduhan.",
"validate_download_source": "Validasi",
"remove_download_source": "Hapus",
"cancel_button_confirmation_delete_all_sources": "TIDAK",
"confirm_button_confirmation_delete_all_sources": "Ya, hapus semuanya",
"description_confirmation_delete_all_sources": "Anda akan menghapus semua sumber unduhan",
"title_confirmation_delete_all_sources": "Hapus semua sumber unduhan",
"removed_download_sources": "Font dihapus",
"button_delete_all_sources": "Hapus semua sumber unduhan",
"add_download_source": "Tambahkan sumber",
"download_count_zero": "Tidak ada unduhan dalam daftar",
"download_count_one": "{{countFormatted}} unduhan dalam daftar",

View File

@@ -120,6 +120,12 @@
"general": "Generale",
"behavior": "Comportamento",
"enable_real_debrid": "Abilita Real Debrid",
"cancel_button_confirmation_delete_all_sources": "NO",
"confirm_button_confirmation_delete_all_sources": "Sì, cancella tutto",
"description_confirmation_delete_all_sources": "Eliminerai tutte le fonti di download",
"title_confirmation_delete_all_sources": "Elimina tutte le fonti di download",
"removed_download_sources": "Fonti di download rimosse",
"button_delete_all_sources": "Rimuovi tutte le fonti di download",
"debrid_api_token_hint": "Puoi trovare la tua chiave API <0>here</0>",
"save_changes": "Salva modifiche"
},

View File

@@ -173,6 +173,12 @@
"download_sources_description": "Hydra осы көздерден жүктеу сілтемелерін алады. URL-да жүктеу сілтемелері бар .json файлына тікелей сілтеме болуы керек.",
"validate_download_source": "Тексеру",
"remove_download_source": "Жою",
"cancel_button_confirmation_delete_all_sources": "Жоқ",
"confirm_button_confirmation_delete_all_sources": "Иә, бәрін жойыңыз",
"description_confirmation_delete_all_sources": "Барлық жүктеу көздерін жоясыз",
"title_confirmation_delete_all_sources": "Барлық жүктеу көздерін жойыңыз",
"removed_download_sources": "Қаріптер жойылды",
"button_delete_all_sources": "Барлық жүктеу көздерін жойыңыз",
"add_download_source": "Жүктеу көзін қосу",
"download_count_zero": "Жүктеулер тізімінде жоқ",
"download_count_one": "{{countFormatted}} жүктеу тізімде",

View File

@@ -111,6 +111,12 @@
"launch_with_system": "컴퓨터가 시작되었을 때 Hydra 실행",
"general": "일반",
"behavior": "행동",
"cancel_button_confirmation_delete_all_sources": "아니요",
"confirm_button_confirmation_delete_all_sources": "네, 모두 삭제합니다",
"description_confirmation_delete_all_sources": "모든 다운로드 소스를 삭제합니다.",
"title_confirmation_delete_all_sources": "모든 다운로드 소스 삭제",
"removed_download_sources": "제거된 글꼴",
"button_delete_all_sources": "모든 다운로드 소스 제거",
"enable_real_debrid": "Real-Debrid 활성화",
"debrid_api_token_hint": "API 키를 <0>이곳</0>에서 얻으세요.",
"save_changes": "변경 사항 저장"

View File

@@ -201,6 +201,12 @@
"download_source_errored": "Mislyktes",
"sync_download_sources": "Synkroniser kilder",
"removed_download_source": "Nedlastingskilde fjernet",
"cancel_button_confirmation_delete_all_sources": "Ingen",
"confirm_button_confirmation_delete_all_sources": "Ja, slett alt",
"description_confirmation_delete_all_sources": "Du vil slette alle nedlastingskilder",
"title_confirmation_delete_all_sources": "Slett alle nedlastingskilder",
"removed_download_sources": "Fonter fjernet",
"button_delete_all_sources": "Fjern alle nedlastingskilder",
"added_download_source": "La til Nedlastingskilde",
"download_sources_synced": "Alle nedlastingskilder er synkroniserte",
"insert_valid_json_url": "Innsett en gyldig JSON url",

View File

@@ -112,6 +112,12 @@
"launch_with_system": "Start Hydra bij het opstarten van het systeem",
"general": "Algemeen",
"behavior": "Gedrag",
"cancel_button_confirmation_delete_all_sources": "Nee",
"confirm_button_confirmation_delete_all_sources": "Ja, verwijder alles",
"description_confirmation_delete_all_sources": "Je verwijdert alle downloadbronnen",
"title_confirmation_delete_all_sources": "Verwijder alle downloadbronnen",
"removed_download_sources": "Downloadbronnen verwijderd",
"button_delete_all_sources": "Verwijder alle downloadbronnen",
"enable_real_debrid": "Enable Real-Debrid",
"debrid_api_token_hint": "U kunt uw API-sleutel <0>hier</0> verkrijgen.",
"save_changes": "Wijzigingen opslaan"

View File

@@ -120,6 +120,11 @@
"general": "Ogólne",
"behavior": "Zachowania",
"language": "Język",
"cancel_button_confirmation_delete_all_sources": "NIE",
"confirm_button_confirmation_delete_all_sources": "Tak, usuń wszystko",
"description_confirmation_delete_all_sources": "Usuniesz wszystkie źródła pobierania",
"title_confirmation_delete_all_sources": "Usuń wszystkie źródła pobierania",
"button_delete_all_sources": "Usuń wszystkie źródła pobierania",
"enable_real_debrid": "Włącz Real-Debrid",
"debrid_api_token_hint": "Możesz uzyskać swój klucz API <0>tutaj</0>",
"save_changes": "Zapisz zmiany"

View File

@@ -31,7 +31,6 @@
},
"header": {
"search": "Buscar jogos",
"catalogue": "Catálogo",
"downloads": "Downloads",
"search_results": "Resultados da busca",
@@ -45,7 +44,10 @@
"downloading_metadata": "Baixando metadados de {{title}}…",
"downloading": "Baixando {{title}}… ({{percentage}} concluído) - Conclusão {{eta}} - {{speed}}",
"calculating_eta": "Baixando {{title}}… ({{percentage}} concluído) - Calculando tempo restante…",
"checking_files": "Verificando arquivos de {{title}}…"
"checking_files": "Verificando arquivos de {{title}}…",
"installing_common_redist": "{{log}}…",
"installation_complete": "Instalação concluída",
"installation_complete_message": "Componentes recomendados instalados com sucesso"
},
"game_details": {
"open_download_options": "Ver opções de download",
@@ -165,6 +167,8 @@
"max_number_of_artifacts_reached": "Número máximo de backups atingido para este jogo",
"achievements_not_sync": "Veja como exibir suas conquistas no perfil",
"backup_from": "Backup de {{date}}",
"automatic_backup_from": "Backup automático de {{date}}",
"enable_automatic_cloud_sync": "Habilitar sincronização automática na nuvem",
"custom_backup_location_set": "Localização customizada selecionada",
"select_folder": "Selecione a pasta",
"manage_files_description": "Gerencie quais arquivos serão feitos backup",
@@ -181,7 +185,8 @@
"download_error_not_cached_in_real_debrid": "Este download não está disponível no Real-Debrid e a verificação do status do download não está disponível.",
"download_error_not_cached_in_torbox": "Este download não está disponível no Torbox e a verificação do status do download não está disponível.",
"game_removed_from_favorites": "Jogo removido dos favoritos",
"game_added_to_favorites": "Jogo adicionado aos favoritos"
"game_added_to_favorites": "Jogo adicionado aos favoritos",
"automatically_extract_downloaded_files": "Extrair automaticamente os arquivos baixados"
},
"activation": {
"title": "Ativação",
@@ -218,7 +223,9 @@
"seeding": "Semeando",
"stop_seeding": "Parar de semear",
"resume_seeding": "Semear",
"options": "Gerenciar"
"options": "Gerenciar",
"extract": "Extrair arquivos",
"extracting": "Extraindo arquivos…"
},
"settings": {
"downloads_path": "Diretório dos downloads",
@@ -255,6 +262,12 @@
"download_source_errored": "Falhou",
"sync_download_sources": "Sincronizar",
"removed_download_source": "Fonte removida",
"removed_download_sources": "Fontes removidas",
"cancel_button_confirmation_delete_all_sources": "Não",
"confirm_button_confirmation_delete_all_sources": "Sim, excluir tudo",
"title_confirmation_delete_all_sources": "Remover todas as fontes de download",
"description_confirmation_delete_all_sources": "Você irá remover todas as fontes de download. Deseja prosseguir?",
"button_delete_all_sources": "Remover todas",
"added_download_source": "Fonte adicionada",
"download_sources_synced": "As fontes foram sincronizadas",
"insert_valid_json_url": "Insira a url de um JSON válido",
@@ -315,12 +328,20 @@
"enable_torbox": "Habilitar Torbox",
"torbox_description": "TorBox é o seu serviço de seedbox premium que rivaliza até com os melhores servidores do mercado.",
"torbox_account_linked": "Conta do TorBox vinculada",
"create_real_debrid_account": "Clique aqui se você ainda não tem uma conta do Real-Debrid",
"create_torbox_account": "Clique aqui se você ainda não tem uma conta do TorBox",
"real_debrid_account_linked": "Conta Real-Debrid associada",
"name_min_length": "O nome do tema deve ter pelo menos 3 caracteres",
"import_theme": "Importar tema",
"import_theme_description": "Você irá importar {{theme}} da loja de temas",
"error_importing_theme": "Erro ao importar tema",
"theme_imported": "Tema importado com sucesso"
"theme_imported": "Tema importado com sucesso",
"enable_friend_request_notifications": "Quando um pedido de amizade é recebido",
"enable_auto_install": "Baixar atualizações automaticamente",
"common_redist": "Componentes recomendados",
"common_redist_description": "Componentes recomendados são necessários para executar alguns jogos. A instalação deles é recomendada para evitar problemas.",
"install_common_redist": "Instalar",
"installing_common_redist": "Instalando…"
},
"notifications": {
"download_complete": "Download concluído",
@@ -329,13 +350,19 @@
"repack_count_one": "{{count}} novo repack",
"repack_count_other": "{{count}} novos repacks",
"new_update_available": "Versão {{version}} disponível",
"restart_to_install_update": "Reinicie o Hydra para instalar a nova versão"
"restart_to_install_update": "Reinicie o Hydra para instalar a nova versão",
"new_friend_request_title": "Novo pedido de amizade",
"new_friend_request_description": "Você recebeu um novo pedido de amizade",
"extraction_complete": "Extração concluída",
"game_extracted": "{{title}} extraído com sucesso"
},
"system_tray": {
"open": "Abrir Hydra",
"quit": "Fechar"
},
"game_card": {
"available_one": "Disponível",
"available_other": "Disponíveis",
"no_downloads": "Sem downloads disponíveis"
},
"binary_not_found_modal": {

View File

@@ -30,7 +30,6 @@
},
"header": {
"search": "Procurar jogos",
"catalogue": "Catálogo",
"downloads": "Transferências",
"search_results": "Resultados da pesquisa",
@@ -103,7 +102,7 @@
"open_download_location": "Ver ficheiros transferidos",
"create_shortcut": "Criar atalho no ambiente de trabalho",
"remove_files": "Remover ficheiros",
"options": "Gerir",
"options": "Opções",
"remove_from_library_description": "Isto vai remover {{game}} da tua biblioteca",
"remove_from_library_title": "Tens a certeza?",
"executable_section_title": "Executável",
@@ -159,7 +158,27 @@
"no_download_option_info": "Sem informações disponíveis",
"backup_deletion_failed": "Falha ao apagar o backup",
"max_number_of_artifacts_reached": "Número máximo de backups atingido para este jogo",
"achievements_not_sync": "As tuas conquistas não estão sincronizadas"
"achievements_not_sync": "As tuas conquistas não estão sincronizadas",
"backup_from": "Backup de {{date}}",
"automatic_backup_from": "Backup automático de {{date}}",
"enable_automatic_cloud_sync": "Ativar sincronização automática na nuvem",
"custom_backup_location_set": "Localização customizada selecionada",
"select_folder": "Selecione a pasta",
"manage_files_description": "Gerencie quais arquivos serão feitos backup",
"clear": "Limpar",
"no_directory_selected": "Nenhum diretório selecionado",
"reset_achievements": "Repor conquistas",
"reset_achievements_description": "Isto irá apagar todas as conquistas de {{game}}",
"reset_achievements_title": "Tem certeza?",
"reset_achievements_success": "Conquistas repostas com sucesso",
"reset_achievements_error": "Falha ao repor conquistas",
"no_write_permission": "Não é possível descarregar neste diretório. Clique aqui para saber mais.",
"download_error_gofile_quota_exceeded": "Você excedeu sua cota mensal do Gofile. Por favor, aguarde o reset da cota.",
"download_error_real_debrid_account_not_authorized": "A sua conta do Real-Debrid não está autorizada a fazer novos downloads. Por favor, verifique a sua assinatura e tente novamente.",
"download_error_not_cached_in_real_debrid": "Este download não está disponível no Real-Debrid e a verificação do status do download não está disponível.",
"download_error_not_cached_in_torbox": "Este download não está disponível no Torbox e a verificação do status do download não está disponível.",
"game_removed_from_favorites": "Jogo removido dos favoritos",
"game_added_to_favorites": "Jogo adicionado aos favoritos"
},
"activation": {
"title": "Ativação",
@@ -192,7 +211,11 @@
"queued": "Na fila",
"no_downloads_title": "Nada por aqui…",
"no_downloads_description": "Ainda não descarregaste nada pelo Hydra, mas nunca é tarde para começar.",
"checking_files": "A verificar ficheiros…"
"checking_files": "A verificar ficheiros…",
"seeding": "A semear",
"stop_seeding": "Parar de semear",
"resume_seeding": "Semear",
"options": "Opções"
},
"settings": {
"downloads_path": "Local das transferências",
@@ -232,6 +255,12 @@
"download_source_errored": "Falhou",
"sync_download_sources": "Sincronizar",
"removed_download_source": "Fonte removida",
"cancel_button_confirmation_delete_all_sources": "Não",
"confirm_button_confirmation_delete_all_sources": "Sim, apague tudo",
"description_confirmation_delete_all_sources": "Irá remover todas as fontes de download",
"title_confirmation_delete_all_sources": "Remover todas as fontes de download",
"removed_download_sources": "Fontes de download removidas",
"button_delete_all_sources": "Remover todas",
"added_download_source": "Fonte adicionada",
"download_sources_synced": "As fontes foram sincronizadas",
"insert_valid_json_url": "Insere o URL de um JSON válido",
@@ -250,7 +279,57 @@
"must_be_valid_url": "A fonte deve ser um URL válido",
"blocked_users": "Utilizadores bloqueados",
"user_unblocked": "Utilizador desbloqueado",
"enable_achievement_notifications": "Quando uma conquista é desbloqueada"
"enable_achievement_notifications": "Quando uma conquista é desbloqueada",
"enable_friend_request_notifications": "Quando um pedido de amizade é recebido",
"launch_minimized": "Iniciar Hydra minimizado",
"disable_nsfw_alert": "Desativar alertas NSFW",
"seed_after_download_complete": "Semear após concluir o download",
"show_hidden_achievement_description": "Mostrar descrição de conquistas ocultas antes de as desbloquear",
"account": "Conta",
"no_users_blocked": "Não tem utilizadores bloqueados",
"subscription_active_until": "O teu Hydra Cloud está ativo até {{date}}",
"manage_subscription": "Gerir subscrições",
"update_email": "Atualizar email",
"update_password": "Atualizar password",
"current_email": "Email atual:",
"no_email_account": "Ainda não adicionou nenhum email à sua conta",
"account_data_updated_successfully": "Dados da conta atualizados com sucesso",
"renew_subscription": "Renovar Hydra Cloud",
"subscription_expired_at": "A sua subscrição expirou a {{date}}",
"no_subscription": "Aproveite o Hydra da melhor forma possível",
"become_subscriber": "Subscreva o Hydra Cloud",
"subscription_renew_cancelled": "A renovação automática está desativada",
"subscription_renews_on": "Sua assinatura renova dia {{date}}",
"bill_sent_until": "A próxima cobrança será enviada até esse dia",
"no_themes": "Parece que ainda não tem nenhum tema. Não se preocupe, clique aqui para criar a sua primeira obra de arte.",
"editor_tab_code": "Código",
"editor_tab_info": "Info",
"editor_tab_save": "Guardar",
"web_store": "Loja de temas",
"clear_themes": "Limpar",
"create_theme": "Criar",
"create_theme_modal_title": "Criar tema customizado",
"create_theme_modal_description": "Criar um novo tema para customizar a aparência do Hydra",
"theme_name": "Nome",
"insert_theme_name": "Insira o nome do tema",
"set_theme": "Definir tema",
"unset_theme": "Redefinir tema",
"delete_theme": "Apagar tema",
"edit_theme": "Editar tema",
"delete_all_themes": "Apagar todos os temas",
"delete_all_themes_description": "Isto irá apagar todos os seus temas",
"delete_theme_description": "Isto irá apagar o tema {{theme}}",
"cancel": "Cancelar",
"appearance": "Aparência",
"enable_torbox": "Ativar Torbox",
"torbox_description": "TorBox é um serviço de seedbox premium sendo um dos melhores servidores do mercado.",
"torbox_account_linked": "Conta do TorBox associada",
"real_debrid_account_linked": "Conta Real-Debrid associada",
"name_min_length": "O nome do tema deve ter pelo menos 3 caracteres",
"import_theme": "Importar tema",
"import_theme_description": "Irá importar {{theme}} da loja de temas",
"error_importing_theme": "Erro ao importar tema",
"theme_imported": "Tema importado com sucesso"
},
"notifications": {
"download_complete": "Transferência concluída",
@@ -259,13 +338,17 @@
"repack_count_one": "{{count}} novo repack",
"repack_count_other": "{{count}} novos repacks",
"new_update_available": "Versão {{version}} disponível",
"restart_to_install_update": "Reinicia o Hydra para instalar a nova versão"
"restart_to_install_update": "Reinicia o Hydra para instalar a nova versão",
"new_friend_request_title": "Novo pedido de amizade",
"new_friend_request_description": "Recebeste um novo pedido de amizade"
},
"system_tray": {
"open": "Abrir o Hydra",
"quit": "Sair"
},
"game_card": {
"available_one": "Disponível",
"available_other": "Disponíveis",
"no_downloads": "Sem downloads disponíveis"
},
"binary_not_found_modal": {
@@ -275,7 +358,16 @@
},
"catalogue": {
"next_page": "Página seguinte",
"previous_page": "Página anterior"
"previous_page": "Página anterior",
"search": "Filtrar…",
"developers": "Desenvolvedores",
"genres": "Géneros",
"tags": "Marcadores",
"publishers": "Distribuidoras",
"download_sources": "Fontes de download",
"result_count": "{{resultCount}} resultados",
"filter_count": "{{filterCount}} disponíveis",
"clear_filters": "Limpar {{filterCount}} selecionados"
},
"modal": {
"close": "Botão de fechar"
@@ -352,7 +444,17 @@
"profile_reported": "Perfil denunciado",
"your_friend_code": "O teu código de amigo:",
"upload_banner": "Fazer upload do banner",
"uploading_banner": "A fazer upload do banner…"
"uploading_banner": "A fazer upload do banner…",
"background_image_updated": "Imagem de fundo salva",
"stats": "Estatísticas",
"achievements": "conquistas",
"games": "Jogos",
"ranking_updated_weekly": "O ranking é atualizado semanalmente",
"playing": "A jogar {{game}}",
"achievements_unlocked": "Conquistas desbloqueadas",
"earned_points": "Pontos ganhos",
"show_achievements_on_profile": "Mostre as suas conquistas no perfil",
"show_points_on_profile": "Mostre os seus pontos ganhos no perfil"
},
"achievement": {
"achievement_unlocked": "Conquista desbloqueada",
@@ -360,15 +462,25 @@
"user_achievements": "Conquistas de {{displayName}}",
"unlocked_at": "Desbloqueada em: {{date}}",
"subscription_needed": "Precisas de uma subscrição Hydra Cloud para visualizar este conteúdo",
"new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos"
"new_achievements_unlocked": "{{achievementCount}} novas conquistas de {{gameCount}} jogos",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} conquistas",
"achievements_unlocked_for_game": "Desbloqueadas {{achievementCount}} novas conquistas em {{gameTitle}}",
"hidden_achievement_tooltip": "Esta é uma conquista oculta",
"achievement_earn_points": "Ganhe {{points}} pontos com essa conquista",
"earned_points": "Pontos ganhos:",
"available_points": "Pontos disponíveis:",
"how_to_earn_achievements_points": "Como desbloquear pontos nas conquistas?"
},
"hydra_cloud": {
"subscription_tour_title": "Subscrição Hydra Cloud",
"subscribe_now": "Subscreve agora",
"cloud_achievements": "Gravação de conquistas na nuvem",
"animated_profile_picture": "Fotos de perfil animadas",
"premium_support": "Apoio Premium",
"premium_support": "Suporte Premium",
"show_and_compare_achievements": "Mostra e compara as tuas conquistas com as de outros utilizadores",
"animated_profile_banner": "Banner animado no perfil"
"animated_profile_banner": "Banner animado no perfil",
"cloud_saving": "Progresso dos jogos na nuvem",
"hydra_cloud_feature_found": "Descubriste uma funcionalidade Hydra Cloud!",
"learn_more": "Saber mais"
}
}

View File

@@ -127,6 +127,12 @@
"behavior": "Comportament",
"language": "Limbă",
"api_token": "Token API",
"cancel_button_confirmation_delete_all_sources": "Nu",
"confirm_button_confirmation_delete_all_sources": "Da, șterge totul",
"description_confirmation_delete_all_sources": "Veți șterge toate sursele de descărcare",
"title_confirmation_delete_all_sources": "Ștergeți toate sursele de descărcare",
"removed_download_sources": "Sursele de descărcare au fost eliminate",
"button_delete_all_sources": "Eliminați toate sursele de descărcare",
"enable_real_debrid": "Activează Real-Debrid",
"real_debrid_description": "Real-Debrid este un descărcător fără restricții care îți permite să descarci fișiere instantaneu și la cea mai bună viteză a internetului tău.",
"debrid_invalid_token": "Token API invalid",

View File

@@ -10,7 +10,7 @@
"hot": "Сейчас популярно",
"start_typing": "Начинаю вводить текст...",
"weekly": "📅 Лучшие игры недели",
"achievements": "🏆 Игры, в которых нужно победить"
"achievements": "🏆 Игры с достижениями"
},
"sidebar": {
"catalogue": "Каталог",
@@ -36,7 +36,7 @@
"downloads": "Загрузки",
"search_results": "Результаты поиска",
"settings": "Настройки",
"version_available_install": "Доступна версия {{version}}. Нажмите здесь для перезапуска и установки.",
"version_available_install": "Доступна версия {{version}}. Нажмите здесь для установки.",
"version_available_download": "Доступна версия {{version}}. Нажмите здесь для загрузки."
},
"bottom_panel": {
@@ -50,7 +50,7 @@
"search": "Фильтр…",
"developers": "Разработчики",
"genres": "Жанры",
"tags": "Маркеры",
"tags": "Теги",
"publishers": "Издательства",
"download_sources": "Источники загрузки",
"result_count": "{{resultCount}} результатов",
@@ -178,16 +178,20 @@
"manage_files_description": "Управляйте файлами, которые будут сохраняться и восстанавливаться",
"select_folder": "Выбрать папку",
"backup_from": "Резервная копия от {{date}}",
"automatic_backup_from": "Автоматическая резервная копия от {{date}}",
"enable_automatic_cloud_sync": "Включить автоматическую синхронизацию в облаке",
"custom_backup_location_set": "Установлено настраиваемое местоположение резервной копии",
"no_directory_selected": "Не выбран каталог",
"no_write_permission": "Невозможно загрузить в эту директорию. Нажмите здесь, чтобы узнать больше.",
"reset_achievements": "Сброс достижений",
"reset_achievements_description": "Это сбросит все достижения для {{game}}",
"reset_achievements_title": "Вы уверены?",
"reset_achievements_success": "Достижения успешно сброшены",
"reset_achievements_error": "Не удалось сбросить достижения",
"download_error_gofile_quota_exceeded": "Вы превысили месячную квоту Gofile. Пожалуйста, подождите, пока квота не будет восстановлена.",
"download_error_real_debrid_account_not_authorized": "Ваш аккаунт Real-Debrid не авторизован для осуществления новых загрузок. Пожалуйста, проверьте настройки учетной записи и повторите попытку.",
"download_error_not_cached_in_real_debrid": "Эта загрузка недоступна на Real-Debrid, а опрос статуса загрузки с Real-Debrid пока недоступен.",
"download_error_not_cached_in_torbox": "Эта загрузка недоступна на Torbox, и опросить статус загрузки с Torbox пока невозможно.",
"download_error_not_cached_in_real_debrid": "Эта загрузка недоступна на Real-Debrid, и получение статуса загрузки с Real-Debrid пока недоступно.",
"download_error_not_cached_in_torbox": "Эта загрузка недоступна на Torbox, и получить статус загрузки с Torbox пока невозможно.",
"game_added_to_favorites": "Игра добавлена в избранное",
"game_removed_from_favorites": "Игра удалена из избранного"
},
@@ -265,9 +269,15 @@
"download_source_up_to_date": "Обновлён",
"download_source_errored": "Ошибка",
"sync_download_sources": "Обновить источники",
"removed_download_source": "Источник загрузок удален",
"added_download_source": "Источник загрузок добавлен",
"download_sources_synced": "Все источники загрузок синхронизированы",
"removed_download_source": "Источник удален",
"cancel_button_confirmation_delete_all_sources": "Нет",
"confirm_button_confirmation_delete_all_sources": "Да, удалить все",
"description_confirmation_delete_all_sources": "Вы удалите все источники",
"title_confirmation_delete_all_sources": "Удалить все источники",
"removed_download_sources": "Источники удалены",
"button_delete_all_sources": "Удалить все источники",
"added_download_source": "Источник добавлен",
"download_sources_synced": "Все источники обновлены",
"insert_valid_json_url": "Вставьте действительный URL JSON-файла",
"found_download_option_zero": "Не найдено вариантов загрузки",
"found_download_option_one": "Найден {{countFormatted}} вариант загрузки",
@@ -275,7 +285,7 @@
"import": "Импортировать",
"blocked_users": "Заблокированные пользователи",
"friends_only": "Только для друзей",
"must_be_valid_url": "Источник должен быть действительным URL-адресом.",
"must_be_valid_url": "У источника должен быть правильный URL",
"privacy": "Конфиденциальность",
"private": "Частный",
"profile_visibility": "Видимость профиля",
@@ -326,11 +336,15 @@
"torbox_description": "TorBox - это ваш премиум-сервис, конкурирующий даже с лучшими серверами на рынке.",
"torbox_account_linked": "Аккаунт TorBox привязан",
"real_debrid_account_linked": "Аккаунт Real-Debrid привязан",
"create_real_debrid_account": "Нажмите здесь, если у вас еще нет аккаунта Real-Debrid",
"create_torbox_account": "Нажмите здесь, если у вас еще нет учетной записи TorBox",
"name_min_length": "Название темы должно содержать не менее 3 символов",
"import_theme": "Импортировать тему",
"import_theme_description": "Вы импортируете {{theme}} из магазина тем",
"error_importing_theme": "Ошибка при импорте темы",
"theme_imported": "Тема успешно импортирована"
"theme_imported": "Тема успешно импортирована",
"enable_friend_request_notifications": "При получении запроса на добавление в друзья",
"enable_auto_install": "Загружать обновления автоматически"
},
"notifications": {
"download_complete": "Загрузка завершена",
@@ -341,13 +355,17 @@
"new_update_available": "Доступна новая версия {{version}}",
"restart_to_install_update": "Перезапустите Hydra для установки обновления",
"notification_achievement_unlocked_title": "Достижение разблокировано для {{game}}",
"notification_achievement_unlocked_body": "были разблокированы {{achievement}} и другие {{count}}"
"notification_achievement_unlocked_body": "были разблокированы {{achievement}} и другие {{count}}",
"new_friend_request_title": "Новый запрос на добавление в друзья",
"new_friend_request_description": "Вы получили новый запрос на добавление в друзья"
},
"system_tray": {
"open": "Открыть Hydra",
"quit": "Выйти"
},
"game_card": {
"available_one": "Доступный",
"available_other": "Доступный",
"no_downloads": "Нет доступных источников"
},
"binary_not_found_modal": {
@@ -432,11 +450,13 @@
"uploading_banner": "Загрузка баннера...",
"background_image_updated": "Фоновое изображение обновлено",
"stats": "Статистика",
"achievements": "Достижения",
"games": "Игры",
"top_percentile": "Топ {{percentile}}%",
"ranking_updated_weekly": "Рейтинг обновляется еженедельно",
"playing": "Играет в {{game}}",
"achievements_unlocked": "Достижения разблокированы",
"earned_points": "Заработано очков:",
"show_achievements_on_profile": "Покажите свои достижения в профиле",
"show_points_on_profile": "Показывать заработанные очки в своем профиле"
},

View File

@@ -16,7 +16,7 @@
"catalogue": "Katalog",
"downloads": "İndirilenler",
"settings": "Ayarlar",
"my_library": "Kütüphanem",
"my_library": "Kütüphane",
"downloading_metadata": "{{title}} (Meta verileri indiriliyor…)",
"paused": "{{title}} (Durduruldu)",
"downloading": "{{title}} ({{percentage}} - İndiriliyor…)",
@@ -36,13 +36,13 @@
"downloads": "İndirilenler",
"search_results": "Arama sonuçları",
"settings": "Ayarlar",
"version_available_install": "Sürüm {{version}} mevcut. Yüklemek ve yeniden başlatmak için buraya tıklayın.",
"version_available_download": "Sürüm {{version}} mevcut. İndirmek için buraya tıklayın."
"version_available_install": "{{version}} sürümü mevcut. Yüklemek ve yeniden başlatmak için buraya tıklayın.",
"version_available_download": "{{version}} sürümü mevcut. İndirmek için buraya tıklayın."
},
"bottom_panel": {
"no_downloads_in_progress": "Devam eden indirme yok",
"downloading_metadata": "{{title}} meta verileri indiriliyor…",
"downloading": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Tamamlama: {{eta}} - Hız: {{speed}}",
"downloading": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Tamamlanma: {{eta}} - Hız: {{speed}}",
"calculating_eta": "{{title}} indiriliyor… ({{percentage}} tamamlandı) - Kalan süre hesaplanıyor…",
"checking_files": "{{title}} dosyaları kontrol ediliyor… ({{percentage}} tamamlandı)"
},
@@ -116,14 +116,14 @@
"clear": "Temizle",
"remove_files": "Dosyaları kaldır",
"remove_from_library_title": "Emin misiniz?",
"remove_from_library_description": "Bu işlem {{game}} oyununu kütüphanenizden kaldıracaktır",
"remove_from_library_description": "Bu işlem sonrasında {{game}} oyunu kütüphanenizden kaldıracaktır",
"options": "Seçenekler",
"executable_section_title": "Çalıştırılabilir dosya",
"executable_section_description": "\"Oyna\" tıklandığında çalıştırılacak dosyanın yolu",
"executable_section_description": "\"Oyna\" butonuna tıklandığında çalıştırılacak dosyanın yolu",
"downloads_secion_title": "İndirmeler",
"downloads_section_description": "Bu oyun için güncellemeleri veya diğer sürümleri kontrol edin",
"danger_zone_section_title": "Tehlike bölgesi",
"danger_zone_section_description": "Bu oyunu kütüphanenizden veya Hydra tarafından indirilen dosyaları kaldırın",
"danger_zone_section_description": "Bu oyunu kütüphanenizden kaldırın veya Hydra tarafından indirilen dosyaları silin.",
"download_in_progress": "İndirme devam ediyor",
"download_paused": "İndirme durduruldu",
"last_downloaded_option": "Son indirilen seçenek",
@@ -141,8 +141,8 @@
"executable_path_in_use": "\"{{game}}\" tarafından kullanılan çalıştırılabilir dosya",
"warning": "Uyarı:",
"hydra_needs_to_remain_open": "Bu indirmenin tamamlanması için Hydra açık kalmalıdır. Eğer Hydra kapanırsa, ilerleme kaydedilmez.",
"achievements": "Başarılar",
"achievements_count": "Başarılar {{unlockedCount}}/{{achievementsCount}}",
"achievements": "Başarımlar",
"achievements_count": "Başarımlar {{unlockedCount}}/{{achievementsCount}}",
"cloud_save": "Bulut kaydı",
"cloud_save_description": "İlerlemenizi buluta kaydedin ve herhangi bir cihazda oynamaya devam edin",
"backups": "Yedekler",
@@ -150,15 +150,15 @@
"delete_backup": "Sil",
"create_backup": "Yeni yedek oluştur",
"last_backup_date": "{{date}} tarihindeki son yedek",
"no_backup_preview": "Bu oyun için kayıtlı oyun bulunamadı",
"no_backup_preview": "Bu oyun için bir kayıt dosyası bulunamadı",
"restoring_backup": "Yedek geri yükleniyor ({{progress}} tamamlandı)…",
"uploading_backup": "Yedek yükleniyor…",
"no_backups": "Bu oyun için henüz bir yedek oluşturmadınız",
"backup_uploaded": "Yedek yüklendi",
"backup_deleted": "Yedek silindi",
"backup_restored": "Yedek geri yüklendi",
"see_all_achievements": "Tüm başarıları gör",
"sign_in_to_see_achievements": "Başarıları görmek için giriş yapın",
"see_all_achievements": "Tüm başarımları gör",
"sign_in_to_see_achievements": "Başarımları görmek için oturum açın",
"mapping_method_automatic": "Otomatik",
"mapping_method_manual": "Manuel",
"mapping_method_label": "Eşleme yöntemi",
@@ -169,29 +169,37 @@
"wine_prefix": "Wine Prefix",
"wine_prefix_description": "Bu oyunu çalıştırmak için kullanılan Wine Prefix",
"launch_options": "Başlatma Seçenekleri",
"launch_options_description": "İleri düzey kullanıcılar, başlatma seçeneklerine değişiklikler girebilir (deneysel özellik)",
"launch_options_description": "İleri düzey kullanıcılar, başlatma seçeneklerine parametreler girebilir (deneysel özellik)",
"launch_options_placeholder": "Belirtilen bir parametre yok",
"no_download_option_info": "Bilgi mevcut değil",
"backup_deletion_failed": "Yedek silinemedi",
"max_number_of_artifacts_reached": "Bu oyun için maksimum yedek sayısına ulaşıldı",
"achievements_not_sync": "Başarılarınızı senkronize etmeyi öğrenin",
"achievements_not_sync": "Başarımlarınızı senkronize etmeyi öğrenin",
"manage_files_description": "Hangi dosyaların yedeklenip geri yükleneceğini yönetin",
"select_folder": "Klasör seç",
"backup_from": "{{date}} tarihinden yedek",
"automatic_backup_from": "{{date}} tarihinden otomatik kayıt",
"enable_automatic_cloud_sync": "Otomatik bulut kaydı senkronizasyonunu aktifleştir",
"custom_backup_location_set": "Özel yedekleme konumu ayarlandı",
"no_directory_selected": "Bir dizin seçilmedi",
"no_write_permission": "Bu dizine indirme yapılamaz. Daha fazla bilgi için buraya tıklayın.",
"reset_achievements": "Başarıları sıfırla",
"reset_achievements_description": "Bu işlem {{game}} için tüm başarıları sıfırlar",
"reset_achievements": "Başarımları sıfırla",
"reset_achievements_description": "Bu işlem {{game}} için tüm başarımları sıfırlar",
"reset_achievements_title": "Emin misiniz?",
"reset_achievements_success": "Başarılar başarıyla sıfırlandı",
"reset_achievements_error": "Başarılar sıfırlanamadı"
"reset_achievements_success": "Başarımlar başarıyla sıfırlandı",
"reset_achievements_error": "Başarımlar sıfırlanamadı",
"download_error_gofile_quota_exceeded": "Gofile aylık kotanızı doldurdunuz. Kotanın yenilenmesini bekleyin.",
"download_error_real_debrid_account_not_authorized": "Real-Debrid hesabınız yeni indirme işlemleri yapmak için yetkilendirilmemiş. Lütfen hesap ayarlarınızı kontrol edip tekrar deneyin.",
"download_error_not_cached_in_real_debrid": "Bu indirme Real-Debrid üzerinde mevcut değil ve Real-Debrid'den indirme durumu henüz sorgulanamıyor.",
"download_error_not_cached_in_torbox": "Bu indirme Torbox'ta mevcut değil ve Torbox'tan indirme durumu henüz sorgulanamıyor.",
"game_removed_from_favorites": "Oyun favorilerden silindi",
"game_added_to_favorites": "Oyun favorilere eklendi"
},
"activation": {
"title": "Hydra'yı Aktive Et",
"installation_id": "Kurulum Kimliği:",
"enter_activation_code": "Aktivasyon kodunuzu girin",
"message": "Bunu nereden soracağınızı bilmiyorsanız, bu sizin için olmamalı.",
"message": "Bunu nasıl edineceğini bilmiyorsan, buna sahip olmamalısın.",
"activate": "Aktive Et",
"loading": "Yükleniyor…"
},
@@ -206,7 +214,7 @@
"cancel": "İptal Et",
"filter": "İndirilen oyunları filtrele",
"remove": "Kaldır",
"downloading_metadata": "Metadata indiriliyor…",
"downloading_metadata": "Meta verileri indiriliyor…",
"deleting": "Yükleyici siliniyor…",
"delete": "Yükleyiciyi kaldır",
"delete_modal_title": "Emin misiniz?",
@@ -231,7 +239,7 @@
"enable_download_notifications": "Bir indirme tamamlandığında",
"enable_repack_list_notifications": "Yeni bir repack eklendiğinde",
"real_debrid_api_token_label": "Real-Debrid API anahtarı",
"quit_app_instead_hiding": "Hydra'yı kapatırken gizlemeyin",
"quit_app_instead_hiding": "Hydra'yı kapatınca sistem tepsisine gitmesin",
"launch_with_system": "Hydra'yı sistem başlatıldığında çalıştır",
"general": "Genel",
"behavior": "Davranış",
@@ -250,6 +258,12 @@
"validate_download_source": "Doğrula",
"remove_download_source": "Kaldır",
"add_download_source": "Kaynak ekle",
"cancel_button_confirmation_delete_all_sources": "Hayır",
"confirm_button_confirmation_delete_all_sources": "Evet, her şeyi sil",
"description_confirmation_delete_all_sources": "Tüm indirme kaynaklarını sileceksiniz",
"title_confirmation_delete_all_sources": "Tüm indirme kaynaklarını sil",
"removed_download_sources": "Yazı tipleri kaldırıldı",
"button_delete_all_sources": "Tüm indirme kaynaklarını kaldır",
"download_count_zero": "İndirme seçeneği yok",
"download_count_one": "{{countFormatted}} indirme seçeneği",
"download_count_other": "{{countFormatted}} indirme seçeneği",
@@ -277,11 +291,56 @@
"must_be_valid_url": "Kaynak geçerli bir URL olmalıdır",
"blocked_users": "Engellenen kullanıcılar",
"user_unblocked": "Kullanıcının engeli kaldırıldı",
"enable_achievement_notifications": "Bir başarı kilidi açıldığında",
"enable_achievement_notifications": "Bir başarım kilidi açıldığında",
"launch_minimized": "Hydra'yı küçültülmüş başlat",
"disable_nsfw_alert": "NSFW uyarısını devre dışı bırak",
"seed_after_download_complete": "İndirme tamamlandıktan sonra paylaş",
"show_hidden_achievement_description": "Gizli başarııklamalarını kilitlenmeden önce göster"
"show_hidden_achievement_description": "Gizli başarımıklamalarını kilitlenmeden önce göster",
"account": "Hesap",
"no_users_blocked": "Hiçbir kullanıcıyı engellemediniz",
"subscription_active_until": "Hydra Cloud'unuz {{date}} tarihine kadar aktif",
"manage_subscription": "Aboneliği yönet",
"update_email": "E-posta'yı güncelle",
"update_password": "Şifreyi güncelle",
"current_email": "Aktif e-posta'nız",
"no_email_account": "Henüz ayarlanmış bir e-postanız yok",
"account_data_updated_successfully": "Hesap bilgileri başarıyla güncellendi",
"renew_subscription": "Hydra Cloud'u yenile",
"subscription_expired_at": "Aboneliğiniz {{date}} tarihinde sona erdi",
"no_subscription": "Hydra'yı en iyi şekilde deneyimleyin",
"become_subscriber": "Hydra Cloud'lu ol",
"subscription_renew_cancelled": "Otomatik yenileme devre dışı",
"subscription_renews_on": "Aboneliğiniz {{date}} tarihinde yenilenecek",
"bill_sent_until": "Bir sonraki faturanız bu tarihe kadar gönderilecek",
"no_themes": "Henüz bir temanız yok gibi görünüyor, ama endişelenmeyin, ilk şaheserinizi oluşturmak için buraya tıklayın.",
"editor_tab_code": "Kod",
"editor_tab_info": "Bilgi",
"editor_tab_save": "Kaydet",
"web_store": "İnternet mağazası",
"clear_themes": "Temizle",
"create_theme": "Oluştur",
"create_theme_modal_title": "Tema oluştur",
"create_theme_modal_description": "Hydra'nın görünümünü özelleştirmek için yeni bir tema oluştur",
"theme_name": "İsim",
"insert_theme_name": "Tema ismini gir",
"set_theme": "Temayı seç",
"unset_theme": "Tema seçimini kaldır",
"delete_theme": "Temayı sil",
"edit_theme": "Temayı düzenle",
"delete_all_themes": "Tüm temaları sil",
"delete_all_themes_description": "Bu tüm temalarınızı silecektir",
"delete_theme_description": "Bu {{theme}} temasını silecektir",
"cancel": "İptal",
"appearance": "Görünüm",
"enable_torbox": "Torbox'u etkinleştir",
"torbox_description": "TorBox, piyasadaki en iyi sunucularla bile rekabet edebilen premium seedbox hizmetinizdir.",
"torbox_account_linked": "TorBox hesabı bağlando",
"real_debrid_account_linked": "Real-Debrid hesabı bağlando",
"name_min_length": "Tema ismi en az 3 karakter uzunluğunda olmalıdır",
"import_theme": "Temayı içe aktar",
"import_theme_description": "{{theme}} teması, tema mağazasından içeri aktarılacak",
"error_importing_theme": "Temayı içe aktarmada bir sorun oluştu",
"theme_imported": "Tema başarıyla içe aktarıldı"
},
"notifications": {
"download_complete": "İndirme tamamlandı",
@@ -289,17 +348,19 @@
"repack_list_updated": "Repack listesi güncellendi",
"repack_count_one": "{{count}} repack eklendi",
"repack_count_other": "{{count}} repack eklendi",
"new_update_available": "Sürüm {{version}} mevcut",
"new_update_available": "{{version}} sürümü mevcut",
"restart_to_install_update": "Güncellemeyi yüklemek için Hydra'yı yeniden başlatın",
"notification_achievement_unlocked_title": "{{game}} için başarı kilidi açıldı",
"notification_achievement_unlocked_body": "{{achievement}} ve diğer {{count}} başarılarıldı"
"notification_achievement_unlocked_title": "{{game}} için başarım kilidi açıldı",
"notification_achievement_unlocked_body": "{{achievement}} ve diğer {{count}} başarımıldı"
},
"system_tray": {
"open": "Hydra'yı Aç",
"quit": ık"
},
"game_card": {
"no_downloads": "İndirilebilir içerik bulunmuyor"
"no_downloads": "İndirilebilir içerik bulunmuyor",
"available_one": "Mevcut",
"available_other": "Mevcut"
},
"binary_not_found_modal": {
"title": "Programlar Yüklü Değil",
@@ -332,7 +393,7 @@
"successfully_signed_out": "Başarıyla çıkış yapıldı",
"sign_out": ıkış yap",
"playing_for": "{{amount}} oynanıyor",
"sign_out_modal_text": "Kütüphaneniz mevcut hesabınıza bağlı. Çıkış yaptığınızda kütüphaneniz görünür olmayacak ve herhangi bir ilerleme kaydedilmeyecek. Çıkışa devam etmek istiyor musunuz?",
"sign_out_modal_text": "Kütüphaneniz mevcut hesabınıza bağlı. Oturumu kapattığınızda kütüphaneniz görünür olmayacak ve herhangi bir ilerleme kaydedilmeyecek. Oturumu kapatmaya devam etmek istiyor musunuz?",
"add_friends": "Arkadaş Ekle",
"add": "Ekle",
"friend_code": "Arkadaş kodu",
@@ -383,39 +444,39 @@
"uploading_banner": "Afiş yükleniyor…",
"background_image_updated": "Arka plan görüntüsü güncellendi",
"stats": "İstatistikler",
"achievements": "Başarılar",
"achievements": "Başarımlar",
"games": "Oyunlar",
"top_percentile": "En üst {{percentile}}%",
"ranking_updated_weekly": "Sıralama haftalık olarak güncellenir",
"playing": "{{game}} oynanıyor",
"achievements_unlocked": "Başarılar açıldı",
"achievements_unlocked": "Başarımlar açıldı",
"earned_points": "Kazanılan puanlar",
"show_achievements_on_profile": "Başarılarınızı profilinizde gösterin",
"show_achievements_on_profile": "Başarımlarınızı profilinizde gösterin",
"show_points_on_profile": "Kazandığınız puanları profilinizde gösterin"
},
"achievement": {
"achievement_unlocked": "Başarııldı",
"user_achievements": "{{displayName}}'in Başarıları",
"your_achievements": "Başarılarınız",
"achievement_unlocked": "Başarımıldı",
"user_achievements": "{{displayName}} oyununun Başarımları",
"your_achievements": "Başarımlarınız",
"unlocked_at": "Açılma zamanı: {{date}}",
"subscription_needed": "Bu içeriği görmek için bir Hydra Cloud aboneliği gereklidir",
"new_achievements_unlocked": "{{gameCount}} oyundan {{achievementCount}} yeni başarııldı",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} başarı",
"achievements_unlocked_for_game": "{{gameTitle}} oyunu için {{achievementCount}} yeni başarııldı",
"hidden_achievement_tooltip": "Bu gizli bir başarıdır",
"achievement_earn_points": "Bu başarı ile {{points}} puan kazanın",
"new_achievements_unlocked": "{{gameCount}} oyundan {{achievementCount}} yeni başarımıldı",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} başarım",
"achievements_unlocked_for_game": "{{gameTitle}} oyunu için {{achievementCount}} yeni başarımıldı",
"hidden_achievement_tooltip": "Bu gizli bir başarımdır",
"achievement_earn_points": "Bu başarım ile {{points}} puan kazanın",
"earned_points": "Kazanılan puanlar:",
"available_points": "Mevcut puanlar:",
"how_to_earn_achievements_points": "Başarı puanları nasıl kazanılır?"
"how_to_earn_achievements_points": "Başarım puanları nasıl kazanılır?"
},
"hydra_cloud": {
"subscription_tour_title": "Hydra Cloud Aboneliği",
"subscribe_now": "Şimdi abone olun",
"cloud_saving": "Bulut kaydetme",
"cloud_achievements": "Başarılarınızı buluta kaydedin",
"cloud_achievements": "Başarımlarınızı buluta kaydedin",
"animated_profile_picture": "Animasyonlu profil resimleri",
"premium_support": "Premium Destek",
"show_and_compare_achievements": "Başarılarınızı diğer kullanıcılarla karşılaştırın ve gösterin",
"show_and_compare_achievements": "Başarımlarınızı diğer kullanıcılarla karşılaştırın ve gösterin",
"animated_profile_banner": "Animasyonlu profil afişi",
"hydra_cloud": "Hydra Cloud",
"hydra_cloud_feature_found": "Bir Hydra Cloud özelliği keşfettiniz!",

View File

@@ -176,6 +176,12 @@
"import": "Імпортувати",
"insert_valid_json_url": "Вставте дійсний URL JSON-файлу",
"language": "Мова",
"cancel_button_confirmation_delete_all_sources": "немає",
"confirm_button_confirmation_delete_all_sources": "Так, видалити все",
"description_confirmation_delete_all_sources": "Ви видалите всі джерела завантаження",
"title_confirmation_delete_all_sources": "Видалити всі джерела завантаження",
"removed_download_sources": "Джерела завантажень видалено",
"button_delete_all_sources": "Видаліть усі джерела завантаження",
"api_token": "API-токен",
"debrid_api_token_hint": "API токен можливо отримати <0>тут</0>",
"real_debrid_api_token_label": "Real-Debrid API-токен",

View File

@@ -226,6 +226,12 @@
"download_sources_description": "Hydra 将从这些源获取下载链接。源 URL 必须是直接链接到包含下载链接的 .json 文件。",
"validate_download_source": "验证",
"remove_download_source": "移除",
"cancel_button_confirmation_delete_all_sources": "不",
"confirm_button_confirmation_delete_all_sources": "是的,删除所有内容",
"description_confirmation_delete_all_sources": "您将删除所有下载源",
"title_confirmation_delete_all_sources": "删除所有下载源",
"removed_download_sources": "已删除字体",
"button_delete_all_sources": "删除所有下载源",
"add_download_source": "添加源",
"download_count_zero": "列表中无下载",
"download_count_one": "列表中有 {{countFormatted}} 个下载",

View File

@@ -12,10 +12,9 @@ export const levelDatabasePath = path.join(
`hydra-db${isStaging ? "-staging" : ""}`
);
export const databaseDirectory = path.join(app.getPath("appData"), "hydra");
export const databasePath = path.join(
databaseDirectory,
isStaging ? "hydra_test.db" : "hydra.db"
export const commonRedistPath = path.join(
app.getPath("userData"),
"CommonRedist"
);
export const logsPath = path.join(app.getPath("userData"), "logs");
@@ -31,3 +30,5 @@ export const achievementSoundPath = app.isPackaged
export const backupsPath = path.join(app.getPath("userData"), "Backups");
export const appVersion = app.getVersion() + (isStaging ? "-staging" : "");
export const MAIN_LOOP_INTERVAL = 1500;

View File

@@ -4,11 +4,15 @@ import updater from "electron-updater";
const { autoUpdater } = updater;
const restartAndInstallUpdate = async (_event: Electron.IpcMainInvokeEvent) => {
export const restartAndInstallUpdate = () => {
autoUpdater.removeAllListeners();
if (app.isPackaged) {
autoUpdater.quitAndInstall(false);
}
};
registerEvent("restartAndInstallUpdate", restartAndInstallUpdate);
const restartAndInstallUpdateEvent = async (
_event: Electron.IpcMainInvokeEvent
) => restartAndInstallUpdate();
registerEvent("restartAndInstallUpdate", restartAndInstallUpdateEvent);

View File

@@ -1,44 +1,8 @@
import { HydraApi, logger, Ludusavi, WindowManager } from "@main/services";
import { CloudSync } from "@main/services";
import { registerEvent } from "../register-event";
import fs from "node:fs";
import path from "node:path";
import * as tar from "tar";
import crypto from "node:crypto";
import type { GameShop } from "@types";
import axios from "axios";
import os from "node:os";
import { backupsPath } from "@main/constants";
import { app } from "electron";
import { normalizePath } from "@main/helpers";
import { gamesSublevel, levelKeys } from "@main/level";
const bundleBackup = async (
shop: GameShop,
objectId: string,
winePrefix: string | null
) => {
const backupPath = path.join(backupsPath, `${shop}-${objectId}`);
// Remove existing backup
if (fs.existsSync(backupPath)) {
fs.rmSync(backupPath, { recursive: true });
}
await Ludusavi.backupGame(shop, objectId, backupPath, winePrefix);
const tarLocation = path.join(backupsPath, `${crypto.randomUUID()}.tar`);
await tar.create(
{
gzip: false,
file: tarLocation,
cwd: backupPath,
},
["."]
);
return tarLocation;
};
import i18next, { t } from "i18next";
import { formatDate } from "date-fns";
const uploadSaveGame = async (
_event: Electron.IpcMainInvokeEvent,
@@ -46,61 +10,17 @@ const uploadSaveGame = async (
shop: GameShop,
downloadOptionTitle: string | null
) => {
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
const { language } = i18next;
const bundleLocation = await bundleBackup(
shop,
return CloudSync.uploadSaveGame(
objectId,
game?.winePrefixPath ?? null
shop,
downloadOptionTitle,
t("backup_from", {
ns: "game_details",
date: formatDate(new Date(), language),
})
);
fs.stat(bundleLocation, async (err, stat) => {
if (err) {
logger.error("Failed to get zip file stats", err);
throw err;
}
const { uploadUrl } = await HydraApi.post<{
id: string;
uploadUrl: string;
}>("/profile/games/artifacts", {
artifactLengthInBytes: stat.size,
shop,
objectId,
hostname: os.hostname(),
homeDir: normalizePath(app.getPath("home")),
downloadOptionTitle,
platform: os.platform(),
});
fs.readFile(bundleLocation, async (err, fileBuffer) => {
if (err) {
logger.error("Failed to read zip file", err);
throw err;
}
await axios.put(uploadUrl, fileBuffer, {
headers: {
"Content-Type": "application/tar",
},
onUploadProgress: (progressEvent) => {
logger.log(progressEvent);
},
});
WindowManager.mainWindow?.webContents.send(
`on-upload-complete-${objectId}-${shop}`,
true
);
fs.rm(bundleLocation, (err) => {
if (err) {
logger.error("Failed to remove tar file", err);
throw err;
}
});
});
});
};
registerEvent("uploadSaveGame", uploadSaveGame);

View File

@@ -0,0 +1,13 @@
import { HydraApi } from "@main/services";
import { registerEvent } from "../register-event";
const createDownloadSources = async (
_event: Electron.IpcMainInvokeEvent,
urls: string[]
) => {
await HydraApi.post("/profile/download-sources", {
urls,
});
};
registerEvent("createDownloadSources", createDownloadSources);

View File

@@ -0,0 +1,8 @@
import { HydraApi } from "@main/services";
import { registerEvent } from "../register-event";
const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) => {
return HydraApi.get("/profile/download-sources");
};
registerEvent("getDownloadSources", getDownloadSources);

View File

@@ -0,0 +1,18 @@
import { HydraApi } from "@main/services";
import { registerEvent } from "../register-event";
const removeDownloadSource = async (
_event: Electron.IpcMainInvokeEvent,
url?: string,
removeAll = false
) => {
const params = new URLSearchParams({
all: removeAll.toString(),
});
if (url) params.set("url", url);
return HydraApi.delete(`/profile/download-sources?${params.toString()}`);
};
registerEvent("removeDownloadSource", removeDownloadSource);

View File

@@ -20,6 +20,7 @@ import "./library/close-game";
import "./library/delete-game-folder";
import "./library/get-game-by-object-id";
import "./library/get-library";
import "./library/extract-game-download";
import "./library/open-game";
import "./library/open-game-executable-path";
import "./library/open-game-installer";
@@ -31,11 +32,15 @@ import "./library/remove-game";
import "./library/remove-game-from-library";
import "./library/select-game-wine-prefix";
import "./library/reset-game-achievements";
import "./library/toggle-automatic-cloud-sync";
import "./misc/open-checkout";
import "./misc/open-external";
import "./misc/show-open-dialog";
import "./misc/get-features";
import "./misc/show-item-in-folder";
import "./misc/get-badges";
import "./misc/install-common-redist";
import "./misc/can-install-common-redist";
import "./torrenting/cancel-game-download";
import "./torrenting/pause-game-download";
import "./torrenting/resume-game-download";
@@ -58,6 +63,7 @@ import "./user/get-blocked-users";
import "./user/block-user";
import "./user/unblock-user";
import "./user/get-user-friends";
import "./user/get-auth";
import "./user/get-user-stats";
import "./user/report-user";
import "./user/get-unlocked-achievements";
@@ -87,7 +93,9 @@ import "./themes/get-custom-theme-by-id";
import "./themes/get-active-custom-theme";
import "./themes/close-editor-window";
import "./themes/toggle-custom-theme";
import "./misc/get-badges";
import "./download-sources/create-download-sources";
import "./download-sources/remove-download-source";
import "./download-sources/get-download-sources";
import { isPortableVersion } from "@main/helpers";
ipcMain.handle("ping", () => "pong");

View File

@@ -0,0 +1,46 @@
import { registerEvent } from "../register-event";
import { GameShop } from "@types";
import path from "node:path";
import { GameFilesManager } from "@main/services";
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
import { FILE_EXTENSIONS_TO_EXTRACT } from "@shared";
const extractGameDownload = async (
_event: Electron.IpcMainInvokeEvent,
shop: GameShop,
objectId: string
): Promise<boolean> => {
const gameKey = levelKeys.game(shop, objectId);
const [download, game] = await Promise.all([
downloadsSublevel.get(gameKey),
gamesSublevel.get(gameKey),
]);
if (!download || !game) return false;
await downloadsSublevel.put(gameKey, {
...download,
extracting: true,
});
const gameFilesManager = new GameFilesManager(shop, objectId);
if (
FILE_EXTENSIONS_TO_EXTRACT.some((ext) => download.folderName?.endsWith(ext))
) {
gameFilesManager.extractDownloadedFile();
} else {
gameFilesManager
.extractFilesInDirectory(
path.join(download.downloadPath, download.folderName!)
)
.then(() => {
gameFilesManager.setExtractionComplete(false);
});
}
return true;
};
registerEvent("extractGameDownload", extractGameDownload);

View File

@@ -0,0 +1,23 @@
import { registerEvent } from "../register-event";
import { levelKeys, gamesSublevel } from "@main/level";
import type { GameShop } from "@types";
const toggleAutomaticCloudSync = async (
_event: Electron.IpcMainInvokeEvent,
shop: GameShop,
objectId: string,
automaticCloudSync: boolean
) => {
const gameKey = levelKeys.game(shop, objectId);
const game = await gamesSublevel.get(gameKey);
if (!game) return;
await gamesSublevel.put(gameKey, {
...game,
automaticCloudSync,
});
};
registerEvent("toggleAutomaticCloudSync", toggleAutomaticCloudSync);

View File

@@ -21,6 +21,8 @@ const updateExecutablePath = async (
await gamesSublevel.put(gameKey, {
...game,
executablePath: parsedPath,
automaticCloudSync:
executablePath === null ? false : game.automaticCloudSync,
});
};

View File

@@ -0,0 +1,7 @@
import { registerEvent } from "../register-event";
import { CommonRedistManager } from "@main/services/common-redist-manager";
const canInstallCommonRedist = async (_event: Electron.IpcMainInvokeEvent) =>
CommonRedistManager.canInstallCommonRedist();
registerEvent("canInstallCommonRedist", canInstallCommonRedist);

View File

@@ -0,0 +1,10 @@
import { registerEvent } from "../register-event";
import { CommonRedistManager } from "@main/services/common-redist-manager";
const installCommonRedist = async (_event: Electron.IpcMainInvokeEvent) => {
if (await CommonRedistManager.canInstallCommonRedist()) {
CommonRedistManager.installCommonRedist();
}
};
registerEvent("installCommonRedist", installCommonRedist);

View File

@@ -1,17 +1,59 @@
import { MAIN_LOOP_INTERVAL } from "@main/constants";
import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services";
import { HydraApi, WindowManager } from "@main/services";
import { publishNewFriendRequestNotification } from "@main/services/notifications";
import { UserNotLoggedInError } from "@shared";
import type { FriendRequestSync } from "@types";
const syncFriendRequests = async (_event: Electron.IpcMainInvokeEvent) => {
return HydraApi.get<FriendRequestSync>(`/profile/friend-requests/sync`).catch(
(err) => {
if (err instanceof UserNotLoggedInError) {
return { friendRequests: [] };
}
throw err;
}
);
interface SyncState {
friendRequestCount: number | null;
tick: number;
}
const ticksToUpdate = (2 * 60 * 1000) / MAIN_LOOP_INTERVAL; // 2 minutes
const syncState: SyncState = {
friendRequestCount: null,
tick: 0,
};
registerEvent("syncFriendRequests", syncFriendRequests);
const syncFriendRequests = async () => {
return HydraApi.get<FriendRequestSync>(`/profile/friend-requests/sync`)
.then((res) => {
if (
syncState.friendRequestCount != null &&
syncState.friendRequestCount < res.friendRequestCount
) {
publishNewFriendRequestNotification();
}
syncState.friendRequestCount = res.friendRequestCount;
WindowManager.mainWindow?.webContents.send(
"on-sync-friend-requests",
res
);
return res;
})
.catch((err) => {
if (err instanceof UserNotLoggedInError) {
return { friendRequestCount: 0 } as FriendRequestSync;
}
throw err;
});
};
const syncFriendRequestsEvent = async (_event: Electron.IpcMainInvokeEvent) => {
return syncFriendRequests();
};
export const watchFriendRequests = async () => {
if (syncState.tick % ticksToUpdate === 0) {
await syncFriendRequests();
}
syncState.tick++;
};
registerEvent("syncFriendRequests", syncFriendRequestsEvent);

View File

@@ -12,7 +12,15 @@ const startGameDownload = async (
_event: Electron.IpcMainInvokeEvent,
payload: StartGameDownloadPayload
) => {
const { objectId, title, shop, downloadPath, downloader, uri } = payload;
const {
objectId,
title,
shop,
downloadPath,
downloader,
uri,
automaticallyExtract,
} = payload;
const gameKey = levelKeys.game(shop, objectId);
@@ -74,6 +82,8 @@ const startGameDownload = async (
shouldSeed: false,
timestamp: Date.now(),
queued: true,
extracting: false,
automaticallyExtract,
};
try {

View File

@@ -23,10 +23,6 @@ const updateUserPreferences = async (
patchUserProfile({ language: preferences.language }).catch(() => {});
}
if (!preferences.downloadsPath) {
preferences.downloadsPath = null;
}
await db.put<string, UserPreferences>(
levelKeys.userPreferences,
{

View File

@@ -0,0 +1,11 @@
import { db, levelKeys } from "@main/level";
import type { Auth } from "@types";
import { registerEvent } from "../register-event";
const getAuth = async (_event: Electron.IpcMainInvokeEvent) =>
db.get<string, Auth>(levelKeys.auth, {
valueEncoding: "json",
});
registerEvent("getAuth", getAuth);

View File

@@ -1,11 +0,0 @@
import knex from "knex";
import { databasePath } from "./constants";
import { app } from "electron";
export const knexClient = knex({
debug: !app.isPackaged,
client: "better-sqlite3",
connection: {
filename: databasePath,
},
});

View File

@@ -1,10 +0,0 @@
const config = {
development: {
migrations: {
extension: "ts",
stub: "migrations/migration.stub",
},
},
};
export default config;

View File

@@ -13,5 +13,5 @@ export const levelKeys = {
downloads: "downloads",
userPreferences: "userPreferences",
language: "language",
sqliteMigrationDone: "sqliteMigrationDone",
screenState: "screenState",
};

View File

@@ -1,4 +1,4 @@
import { DownloadManager, logger, Ludusavi, startMainLoop } from "./services";
import { DownloadManager, Ludusavi, startMainLoop } from "./services";
import { RealDebridClient } from "./services/download/real-debrid";
import { HydraApi } from "./services/hydra-api";
import { uploadGamesBatch } from "./services/library-sync";
@@ -6,26 +6,18 @@ import { Aria2 } from "./services/aria2";
import { downloadsSublevel } from "./level/sublevels/downloads";
import { sortBy } from "lodash-es";
import { Downloader } from "@shared";
import {
gameAchievementsSublevel,
gamesSublevel,
levelKeys,
db,
} from "./level";
import { Auth, User, type UserPreferences } from "@types";
import { knexClient } from "./knex-client";
import { levelKeys, db } from "./level";
import type { UserPreferences } from "@types";
import { TorBoxClient } from "./services/download/torbox";
import { CommonRedistManager } from "./services/common-redist-manager";
export const loadState = async () => {
const userPreferences = await migrateFromSqlite().then(async () => {
await db.put<string, boolean>(levelKeys.sqliteMigrationDone, true, {
const userPreferences = await db.get<string, UserPreferences | null>(
levelKeys.userPreferences,
{
valueEncoding: "json",
});
return db.get<string, UserPreferences | null>(levelKeys.userPreferences, {
valueEncoding: "json",
});
});
}
);
await import("./events");
@@ -52,6 +44,15 @@ export const loadState = async () => {
return sortBy(games, "timestamp", "DESC");
});
downloads.forEach((download) => {
if (download.extracting) {
downloadsSublevel.put(levelKeys.game(download.shop, download.objectId), {
...download,
extracting: false,
});
}
});
const [nextItemOnQueue] = downloads.filter((game) => game.queued);
const downloadsToSeed = downloads.filter(
@@ -62,143 +63,9 @@ export const loadState = async () => {
game.uri !== null
);
console.log("downloadsToSeed", downloadsToSeed);
await DownloadManager.startRPC(nextItemOnQueue, downloadsToSeed);
startMainLoop();
};
const migrateFromSqlite = async () => {
const sqliteMigrationDone = await db.get(levelKeys.sqliteMigrationDone);
if (sqliteMigrationDone) {
return;
}
const migrateGames = knexClient("game")
.select("*")
.then((games) => {
return gamesSublevel.batch(
games.map((game) => ({
type: "put",
key: levelKeys.game(game.shop, game.objectID),
value: {
objectId: game.objectID,
shop: game.shop,
title: game.title,
iconUrl: game.iconUrl,
playTimeInMilliseconds: game.playTimeInMilliseconds,
lastTimePlayed: game.lastTimePlayed,
remoteId: game.remoteId,
winePrefixPath: game.winePrefixPath,
launchOptions: game.launchOptions,
executablePath: game.executablePath,
isDeleted: game.isDeleted === 1,
},
}))
);
})
.then(() => {
logger.info("Games migrated successfully");
});
const migrateUserPreferences = knexClient("user_preferences")
.select("*")
.then(async (userPreferences) => {
if (userPreferences.length > 0) {
const { realDebridApiToken, ...rest } = userPreferences[0];
await db.put<string, UserPreferences>(
levelKeys.userPreferences,
{
...rest,
realDebridApiToken,
preferQuitInsteadOfHiding: rest.preferQuitInsteadOfHiding === 1,
runAtStartup: rest.runAtStartup === 1,
startMinimized: rest.startMinimized === 1,
disableNsfwAlert: rest.disableNsfwAlert === 1,
seedAfterDownloadComplete: rest.seedAfterDownloadComplete === 1,
showHiddenAchievementsDescription:
rest.showHiddenAchievementsDescription === 1,
downloadNotificationsEnabled:
rest.downloadNotificationsEnabled === 1,
repackUpdatesNotificationsEnabled:
rest.repackUpdatesNotificationsEnabled === 1,
achievementNotificationsEnabled:
rest.achievementNotificationsEnabled === 1,
},
{ valueEncoding: "json" }
);
if (rest.language) {
await db.put<string, string>(levelKeys.language, rest.language, {
valueEncoding: "utf-8",
});
}
}
})
.then(() => {
logger.info("User preferences migrated successfully");
});
const migrateAchievements = knexClient("game_achievement")
.select("*")
.then((achievements) => {
return gameAchievementsSublevel.batch(
achievements.map((achievement) => ({
type: "put",
key: levelKeys.game(achievement.shop, achievement.objectId),
value: {
achievements: JSON.parse(achievement.achievements),
unlockedAchievements: JSON.parse(achievement.unlockedAchievements),
},
}))
);
})
.then(() => {
logger.info("Achievements migrated successfully");
});
const migrateUser = knexClient("user_auth")
.select("*")
.then(async (users) => {
if (users.length > 0) {
await db.put<string, User>(
levelKeys.user,
{
id: users[0].userId,
displayName: users[0].displayName,
profileImageUrl: users[0].profileImageUrl,
backgroundImageUrl: users[0].backgroundImageUrl,
subscription: users[0].subscription,
},
{
valueEncoding: "json",
}
);
await db.put<string, Auth>(
levelKeys.auth,
{
accessToken: users[0].accessToken,
refreshToken: users[0].refreshToken,
tokenExpirationTimestamp: users[0].tokenExpirationTimestamp,
},
{
valueEncoding: "json",
}
);
}
})
.then(() => {
logger.info("User data migrated successfully");
});
return Promise.allSettled([
migrateGames,
migrateUserPreferences,
migrateAchievements,
migrateUser,
]);
CommonRedistManager.downloadCommonRedist();
};

76
src/main/services/7zip.ts Normal file
View File

@@ -0,0 +1,76 @@
import { app } from "electron";
import cp from "node:child_process";
import path from "node:path";
import { logger } from "./logger";
export const binaryName = {
linux: "7zzs",
darwin: "7zz",
win32: "7z.exe",
};
export class SevenZip {
private static readonly binaryPath = app.isPackaged
? path.join(process.resourcesPath, binaryName[process.platform])
: path.join(
__dirname,
"..",
"..",
"binaries",
binaryName[process.platform]
);
public static extractFile(
{
filePath,
outputPath,
cwd,
passwords = [],
}: {
filePath: string;
outputPath?: string;
cwd?: string;
passwords?: string[];
},
successCb: () => void,
errorCb: () => void
) {
const tryPassword = (index = -1) => {
const password = passwords[index] ?? "";
logger.info(`Trying password ${password} on ${filePath}`);
const args = ["x", filePath, "-y", "-p" + password];
if (outputPath) {
args.push("-o" + outputPath);
}
const child = cp.execFile(this.binaryPath, args, {
cwd,
});
child.once("exit", (code) => {
console.log("EXIT CALLED", code, filePath);
if (code === 0) {
successCb();
return;
}
if (index < passwords.length - 1) {
logger.info(
`Failed to extract file: ${filePath} with password: ${password}. Trying next password...`
);
tryPassword(index + 1);
} else {
logger.info(`Failed to extract file: ${filePath}`);
errorCb();
}
});
};
tryPassword();
}
}

View File

@@ -2,23 +2,26 @@ import path from "node:path";
import cp from "node:child_process";
import { app } from "electron";
export const startAria2 = () => {};
export class Aria2 {
private static process: cp.ChildProcess | null = null;
private static readonly binaryPath = app.isPackaged
? path.join(process.resourcesPath, "aria2", "aria2c")
: path.join(__dirname, "..", "..", "aria2", "aria2c");
public static spawn() {
const binaryPath = app.isPackaged
? path.join(process.resourcesPath, "aria2", "aria2c")
: path.join(__dirname, "..", "..", "aria2", "aria2c");
this.process = cp.spawn(
binaryPath,
this.binaryPath,
[
"--enable-rpc",
"--rpc-listen-all",
"--file-allocation=none",
"--allow-overwrite=true",
"-s",
"16",
"-x",
"16",
"-k",
"1M",
],
{ stdio: "inherit", windowsHide: true }
);

View File

@@ -0,0 +1,111 @@
import { levelKeys, gamesSublevel, db } from "@main/level";
import { app } from "electron";
import path from "node:path";
import * as tar from "tar";
import crypto from "node:crypto";
import fs from "node:fs";
import os from "node:os";
import type { GameShop, User } from "@types";
import { backupsPath } from "@main/constants";
import { HydraApi } from "./hydra-api";
import { normalizePath } from "@main/helpers";
import { logger } from "./logger";
import { WindowManager } from "./window-manager";
import axios from "axios";
import { Ludusavi } from "./ludusavi";
import { SubscriptionRequiredError } from "@shared";
export class CloudSync {
private static async bundleBackup(
shop: GameShop,
objectId: string,
winePrefix: string | null
) {
const backupPath = path.join(backupsPath, `${shop}-${objectId}`);
// Remove existing backup
if (fs.existsSync(backupPath)) {
fs.rmSync(backupPath, { recursive: true });
}
await Ludusavi.backupGame(shop, objectId, backupPath, winePrefix);
const tarLocation = path.join(backupsPath, `${crypto.randomUUID()}.tar`);
await tar.create(
{
gzip: false,
file: tarLocation,
cwd: backupPath,
},
["."]
);
return tarLocation;
}
public static async uploadSaveGame(
objectId: string,
shop: GameShop,
downloadOptionTitle: string | null,
label?: string
) {
const hasActiveSubscription = await db
.get<string, User>(levelKeys.user, { valueEncoding: "json" })
.then((user) => {
const expiresAt = new Date(user?.subscription?.expiresAt ?? 0);
return expiresAt > new Date();
});
if (!hasActiveSubscription) {
throw new SubscriptionRequiredError();
}
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
const bundleLocation = await this.bundleBackup(
shop,
objectId,
game?.winePrefixPath ?? null
);
const stat = await fs.promises.stat(bundleLocation);
const { uploadUrl } = await HydraApi.post<{
id: string;
uploadUrl: string;
}>("/profile/games/artifacts", {
artifactLengthInBytes: stat.size,
shop,
objectId,
hostname: os.hostname(),
homeDir: normalizePath(app.getPath("home")),
downloadOptionTitle,
platform: os.platform(),
label,
});
const fileBuffer = await fs.promises.readFile(bundleLocation);
await axios.put(uploadUrl, fileBuffer, {
headers: {
"Content-Type": "application/tar",
},
onUploadProgress: (progressEvent) => {
logger.log(progressEvent);
},
});
WindowManager.mainWindow?.webContents.send(
`on-upload-complete-${objectId}-${shop}`,
true
);
fs.rm(bundleLocation, (err) => {
if (err) {
logger.error("Failed to remove tar file", err);
throw err;
}
});
}
}

View File

@@ -0,0 +1,109 @@
import { commonRedistPath } from "@main/constants";
import axios from "axios";
import fs from "node:fs";
import cp from "node:child_process";
import path from "node:path";
import { logger } from "./logger";
import { app } from "electron";
import { WindowManager } from "./window-manager";
export class CommonRedistManager {
private static readonly redistributables = [
"dotNetFx40_Full_setup.exe",
"dxwebsetup.exe",
"oalinst.exe",
"install.bat",
"vcredist_2015-2019_x64.exe",
"vcredist_2015-2019_x86.exe",
"vcredist_x64.exe",
"vcredist_x86.exe",
"xnafx40_redist.msi",
];
private static readonly installationTimeout = 1000 * 60 * 5; // 5 minutes
private static readonly installationLog = path.join(
app.getPath("temp"),
"common_redist_install.log"
);
public static async installCommonRedist() {
const abortController = new AbortController();
const timeout = setTimeout(() => {
abortController.abort();
logger.error("Installation timed out");
WindowManager.mainWindow?.webContents.send("common-redist-progress", {
log: "Installation timed out",
complete: false,
});
}, this.installationTimeout);
const installationCompleteMessage = "Installation complete";
if (!fs.existsSync(this.installationLog)) {
await fs.promises.writeFile(this.installationLog, "");
}
fs.watch(this.installationLog, { signal: abortController.signal }, () => {
fs.readFile(this.installationLog, "utf-8", (err, data) => {
if (err) return logger.error("Error reading log file:", err);
const tail = data.split("\n").at(-2)?.trim();
if (tail?.includes(installationCompleteMessage)) {
clearTimeout(timeout);
if (!abortController.signal.aborted) {
abortController.abort();
}
}
WindowManager.mainWindow?.webContents.send("common-redist-progress", {
log: tail,
complete: tail?.includes(installationCompleteMessage),
});
});
});
cp.exec(
path.join(commonRedistPath, "install.bat"),
{
windowsHide: true,
},
(error) => {
if (error) {
logger.error("Failed to run install.bat", error);
}
}
);
}
public static async canInstallCommonRedist() {
return this.redistributables.every((redist) => {
const filePath = path.join(commonRedistPath, redist);
return fs.existsSync(filePath);
});
}
public static async downloadCommonRedist() {
if (!fs.existsSync(commonRedistPath)) {
await fs.promises.mkdir(commonRedistPath, { recursive: true });
}
for (const redist of this.redistributables) {
const filePath = path.join(commonRedistPath, redist);
if (fs.existsSync(filePath)) {
continue;
}
const response = await axios.get(
`https://github.com/hydralauncher/hydra-common-redist/raw/refs/heads/main/${redist}`,
{
responseType: "arraybuffer",
}
);
await fs.promises.writeFile(filePath, response.data);
}
}
}

View File

@@ -1,8 +1,14 @@
import { Downloader, DownloadError } from "@shared";
import { Downloader, DownloadError, FILE_EXTENSIONS_TO_EXTRACT } from "@shared";
import { WindowManager } from "../window-manager";
import { publishDownloadCompleteNotification } from "../notifications";
import type { Download, DownloadProgress, UserPreferences } from "@types";
import { GofileApi, QiwiApi, DatanodesApi, MediafireApi } from "../hosters";
import {
GofileApi,
QiwiApi,
DatanodesApi,
MediafireApi,
PixelDrainApi,
} from "../hosters";
import { PythonRPC } from "../python-rpc";
import {
LibtorrentPayload,
@@ -16,6 +22,7 @@ import { logger } from "../logger";
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
import { sortBy } from "lodash-es";
import { TorBoxClient } from "./torbox";
import { GameFilesManager } from "../game-files-manager";
export class DownloadManager {
private static downloadingGameId: string | null = null;
@@ -130,6 +137,8 @@ export class DownloadManager {
);
}
const shouldExtract = download.automaticallyExtract;
if (progress === 1 && download) {
publishDownloadCompleteNotification(game);
@@ -137,23 +146,48 @@ export class DownloadManager {
userPreferences?.seedAfterDownloadComplete &&
download.downloader === Downloader.Torrent
) {
downloadsSublevel.put(gameId, {
await downloadsSublevel.put(gameId, {
...download,
status: "seeding",
shouldSeed: true,
queued: false,
extracting: shouldExtract,
});
} else {
downloadsSublevel.put(gameId, {
await downloadsSublevel.put(gameId, {
...download,
status: "complete",
shouldSeed: false,
queued: false,
extracting: shouldExtract,
});
this.cancelDownload(gameId);
}
if (shouldExtract) {
const gameFilesManager = new GameFilesManager(
game.shop,
game.objectId
);
if (
FILE_EXTENSIONS_TO_EXTRACT.some((ext) =>
download.folderName?.endsWith(ext)
)
) {
gameFilesManager.extractDownloadedFile();
} else {
gameFilesManager
.extractFilesInDirectory(
path.join(download.downloadPath, download.folderName!)
)
.then(() => {
gameFilesManager.setExtractionComplete();
});
}
}
const downloads = await downloadsSublevel
.values()
.all()
@@ -283,11 +317,12 @@ export class DownloadManager {
}
case Downloader.PixelDrain: {
const id = download.uri.split("/").pop();
const downloadUrl = await PixelDrainApi.getDownloadUrl(id!);
return {
action: "start",
game_id: downloadId,
url: `https://cdn.pd5-gamedriveorg.workers.dev/api/file/${id}`,
url: downloadUrl,
save_path: download.downloadPath,
};
}

View File

@@ -6,6 +6,7 @@ import type {
TorBoxAddTorrentRequest,
TorBoxRequestLinkRequest,
} from "@types";
import { appVersion } from "@main/constants";
export class TorBoxClient {
private static instance: AxiosInstance;
@@ -18,6 +19,7 @@ export class TorBoxClient {
baseURL: this.baseURL,
headers: {
Authorization: `Bearer ${apiToken}`,
"User-Agent": `Hydra/${appVersion}`,
},
});
}

View File

@@ -0,0 +1,158 @@
import path from "node:path";
import fs from "node:fs";
import type { GameShop } from "@types";
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
import { FILE_EXTENSIONS_TO_EXTRACT } from "@shared";
import { SevenZip } from "./7zip";
import { WindowManager } from "./window-manager";
import { publishExtractionCompleteNotification } from "./notifications";
import { logger } from "./logger";
export class GameFilesManager {
constructor(
private readonly shop: GameShop,
private readonly objectId: string
) {}
private async clearExtractionState() {
const gameKey = levelKeys.game(this.shop, this.objectId);
const download = await downloadsSublevel.get(gameKey);
await downloadsSublevel.put(gameKey, {
...download!,
extracting: false,
});
WindowManager.mainWindow?.webContents.send(
"on-extraction-complete",
this.shop,
this.objectId
);
}
async extractFilesInDirectory(directoryPath: string) {
if (!fs.existsSync(directoryPath)) return;
const files = await fs.promises.readdir(directoryPath);
const compressedFiles = files.filter((file) =>
FILE_EXTENSIONS_TO_EXTRACT.some((ext) => file.endsWith(ext))
);
const filesToExtract = compressedFiles.filter(
(file) => /part1\.rar$/i.test(file) || !/part\d+\.rar$/i.test(file)
);
await Promise.all(
filesToExtract.map((file) => {
return new Promise((resolve, reject) => {
SevenZip.extractFile(
{
filePath: path.join(directoryPath, file),
cwd: directoryPath,
passwords: ["online-fix.me", "steamrip.com"],
},
() => {
resolve(true);
},
() => {
reject(new Error(`Failed to extract file: ${file}`));
this.clearExtractionState();
}
);
});
})
);
compressedFiles.forEach((file) => {
const extractionPath = path.join(directoryPath, file);
if (fs.existsSync(extractionPath)) {
fs.unlink(extractionPath, (err) => {
if (err) {
logger.error(`Failed to delete file: ${file}`, err);
this.clearExtractionState();
}
});
}
});
}
async setExtractionComplete(publishNotification = true) {
const gameKey = levelKeys.game(this.shop, this.objectId);
const [download, game] = await Promise.all([
downloadsSublevel.get(gameKey),
gamesSublevel.get(gameKey),
]);
await downloadsSublevel.put(gameKey, {
...download!,
extracting: false,
});
WindowManager.mainWindow?.webContents.send(
"on-extraction-complete",
this.shop,
this.objectId
);
if (publishNotification) {
publishExtractionCompleteNotification(game!);
}
}
async extractDownloadedFile() {
const gameKey = levelKeys.game(this.shop, this.objectId);
const [download, game] = await Promise.all([
downloadsSublevel.get(gameKey),
gamesSublevel.get(gameKey),
]);
if (!download || !game) return false;
const filePath = path.join(download.downloadPath, download.folderName!);
const extractionPath = path.join(
download.downloadPath,
path.parse(download.folderName!).name
);
SevenZip.extractFile(
{
filePath,
outputPath: extractionPath,
passwords: ["online-fix.me", "steamrip.com"],
},
async () => {
await this.extractFilesInDirectory(extractionPath);
if (fs.existsSync(extractionPath) && fs.existsSync(filePath)) {
fs.unlink(filePath, (err) => {
if (err) {
logger.error(
`Failed to delete file: ${download.folderName}`,
err
);
this.clearExtractionState();
}
});
}
await downloadsSublevel.put(gameKey, {
...download!,
folderName: path.parse(download.folderName!).name,
});
this.setExtractionComplete();
},
() => {
this.clearExtractionState();
}
);
return true;
}
}

View File

@@ -1,47 +1,71 @@
import axios, { AxiosResponse } from "axios";
import { wrapper } from "axios-cookiejar-support";
import { CookieJar } from "tough-cookie";
export class DatanodesApi {
private static readonly session = axios.create({});
private static readonly jar = new CookieJar();
private static readonly session = wrapper(
axios.create({
jar: DatanodesApi.jar,
withCredentials: true,
})
);
public static async getDownloadUrl(downloadUrl: string): Promise<string> {
const parsedUrl = new URL(downloadUrl);
const pathSegments = parsedUrl.pathname.split("/");
try {
const parsedUrl = new URL(downloadUrl);
const pathSegments = parsedUrl.pathname.split("/").filter(Boolean);
const fileCode = pathSegments[0];
const fileCode = decodeURIComponent(pathSegments[1]);
const fileName = decodeURIComponent(pathSegments[pathSegments.length - 1]);
await this.jar.setCookie("lang=english;", "https://datanodes.to");
const payload = new URLSearchParams({
op: "download2",
id: fileCode,
rand: "",
referer: "https://datanodes.to/download",
method_free: "Free Download >>",
method_premium: "",
adblock_detected: "",
});
const payload = new URLSearchParams({
op: "download2",
id: fileCode,
method_free: "Free Download >>",
dl: "1",
});
const response: AxiosResponse = await this.session.post(
"https://datanodes.to/download",
payload,
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Cookie: `lang=english; file_name=${fileName}; file_code=${fileCode};`,
Host: "datanodes.to",
Origin: "https://datanodes.to",
Referer: "https://datanodes.to/download",
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
},
maxRedirects: 0,
validateStatus: (status: number) => status === 302 || status < 400,
const response: AxiosResponse = await this.session.post(
"https://datanodes.to/download",
payload,
{
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0",
Referer: "https://datanodes.to/download",
Origin: "https://datanodes.to",
"Content-Type": "application/x-www-form-urlencoded",
},
maxRedirects: 0,
validateStatus: (status: number) => status === 302 || status < 400,
}
);
if (response.status === 302) {
return response.headers["location"];
}
);
if (response.status === 302) {
return response.headers["location"];
if (typeof response.data === "object" && response.data.url) {
return decodeURIComponent(response.data.url);
}
const htmlContent = String(response.data);
if (!htmlContent) {
throw new Error("Empty response received");
}
const downloadLinkRegex = /href=["'](https:\/\/[^"']+)["']/;
const downloadLinkMatch = downloadLinkRegex.exec(htmlContent);
if (downloadLinkMatch) {
return downloadLinkMatch[1];
}
throw new Error("Failed to get the download link");
} catch (error) {
console.error("Error fetching download URL:", error);
throw error;
}
return "";
}
}

View File

@@ -2,3 +2,4 @@ export * from "./gofile";
export * from "./qiwi";
export * from "./datanodes";
export * from "./mediafire";
export * from "./pixeldrain";

View File

@@ -0,0 +1,42 @@
import axios from "axios";
export class PixelDrainApi {
private static readonly browserHeaders = {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36",
Accept:
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.9",
"Accept-Encoding": "gzip, deflate, br",
DNT: "1",
Connection: "keep-alive",
"Sec-Fetch-Dest": "document",
"Sec-Fetch-Mode": "navigate",
"Sec-Fetch-Site": "none",
"Sec-Fetch-User": "?1",
};
public static async getDownloadUrl(fileId: string): Promise<string> {
try {
const response = await axios.get(`https://pd.cybar.xyz/${fileId}`, {
headers: this.browserHeaders,
maxRedirects: 0,
validateStatus: (status) =>
status === 301 || status === 302 || status === 200,
});
if (
response.headers.location ||
response.status === 301 ||
response.status === 302
) {
return response.headers.location;
}
throw new Error(`No redirect URL found (status: ${response.status})`);
} catch (error) {
console.error("Error fetching PixelDrain URL:", error);
throw error;
}
}
}

View File

@@ -8,7 +8,6 @@ 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";
import { db } from "@main/level";
import { levelKeys } from "@main/level/sublevels";
import type { Auth, User } from "@types";
@@ -47,8 +46,8 @@ export class HydraApi {
}
private static hasActiveSubscription() {
const expiresAt = this.userAuth.subscription?.expiresAt;
return expiresAt && (isFuture(expiresAt) || isToday(expiresAt));
const expiresAt = new Date(this.userAuth.subscription?.expiresAt ?? 0);
return expiresAt > new Date();
}
static async handleExternalAuth(uri: string) {

View File

@@ -7,3 +7,7 @@ export * from "./process-watcher";
export * from "./main-loop";
export * from "./hydra-api";
export * from "./ludusavi";
export * from "./cloud-sync";
export * from "./7zip";
export * from "./game-files-manager";
export * from "./common-redist-manager";

View File

@@ -3,18 +3,21 @@ import { DownloadManager } from "./download";
import { watchProcesses } from "./process-watcher";
import { AchievementWatcherManager } from "./achievements/achievement-watcher-manager";
import { UpdateManager } from "./update-manager";
import { watchFriendRequests } from "@main/events/profile/sync-friend-requests";
import { MAIN_LOOP_INTERVAL } from "@main/constants";
export const startMainLoop = async () => {
// eslint-disable-next-line no-constant-condition
while (true) {
await Promise.allSettled([
watchProcesses(),
watchFriendRequests(),
DownloadManager.watchDownloads(),
AchievementWatcherManager.watchAchievements(),
DownloadManager.getSeedStatus(),
UpdateManager.checkForUpdatePeriodically(),
]);
await sleep(1500);
await sleep(MAIN_LOOP_INTERVAL);
}
};

View File

@@ -12,6 +12,7 @@ import { logger } from "../logger";
import { WindowManager } from "../window-manager";
import type { Game, UserPreferences } from "@types";
import { db, levelKeys } from "@main/level";
import { restartAndInstallUpdate } from "@main/events/autoupdater/restart-and-install-update";
async function downloadImage(url: string | null) {
if (!url) return undefined;
@@ -72,10 +73,33 @@ export const publishNotificationUpdateReadyToInstall = async (
ns: "notifications",
}),
icon: trayIcon,
}).show();
})
.on("click", () => {
restartAndInstallUpdate();
})
.show();
};
export const publishNewFriendRequestNotification = async () => {};
export const publishNewFriendRequestNotification = async () => {
const userPreferences = await db.get<string, UserPreferences | null>(
levelKeys.userPreferences,
{
valueEncoding: "json",
}
);
if (!userPreferences?.friendRequestNotificationsEnabled) return;
new Notification({
title: t("new_friend_request_title", {
ns: "notifications",
}),
body: t("new_friend_request_description", {
ns: "notifications",
}),
icon: trayIcon,
}).show();
};
export const publishCombinedNewAchievementNotification = async (
achievementCount,
@@ -104,6 +128,17 @@ export const publishCombinedNewAchievementNotification = async (
}
};
export const publishExtractionCompleteNotification = async (game: Game) => {
new Notification({
title: t("extraction_complete", { ns: "notifications" }),
body: t("game_extracted", {
ns: "notifications",
title: game.title,
}),
icon: trayIcon,
}).show();
};
export const publishNewAchievementNotification = async (info: {
achievements: { displayName: string; iconUrl: string }[];
unlockedAchievementCount: number;

View File

@@ -6,6 +6,9 @@ import axios from "axios";
import { exec } from "child_process";
import { ProcessPayload } from "./download/types";
import { gamesSublevel, levelKeys } from "@main/level";
import i18next, { t } from "i18next";
import { CloudSync } from "./cloud-sync";
import { formatDate } from "date-fns";
const commands = {
findWineDir: `lsof -c wine 2>/dev/null | grep '/drive_c/windows$' | head -n 1 | awk '{for(i=9;i<=NF;i++) printf "%s ", $i; print ""}'`,
@@ -225,6 +228,20 @@ function onOpenGame(game: Game) {
if (game.remoteId) {
updateGamePlaytime(game, 0, new Date()).catch(() => {});
const { language } = i18next;
if (game.automaticCloudSync) {
CloudSync.uploadSaveGame(
game.objectId,
game.shop,
null,
t("automatic_backup_from", {
ns: "game_details",
date: formatDate(new Date(), language),
})
);
}
} else {
createGame({ ...game, lastTimePlayed: new Date() }).catch(() => {});
}
@@ -281,12 +298,26 @@ const onCloseGame = (game: Game) => {
)!;
gamesPlaytime.delete(levelKeys.game(game.shop, game.objectId));
const { language } = i18next;
if (game.remoteId) {
updateGamePlaytime(
game,
performance.now() - gamePlaytime.lastSyncTick,
game.lastTimePlayed!
).catch(() => {});
if (game.automaticCloudSync) {
CloudSync.uploadSaveGame(
game.objectId,
game.shop,
null,
t("automatic_backup_from", {
ns: "game_details",
date: formatDate(new Date(), language),
})
);
}
} else {
createGame(game).catch(() => {});
}

View File

@@ -1,14 +1,14 @@
import updater, { UpdateInfo } from "electron-updater";
import { logger, WindowManager } from "@main/services";
import { AppUpdaterEvent } from "@types";
import { AppUpdaterEvent, UserPreferences } from "@types";
import { app } from "electron";
import { publishNotificationUpdateReadyToInstall } from "@main/services/notifications";
const isAutoInstallAvailable =
process.platform !== "darwin" && process.env.PORTABLE_EXECUTABLE_FILE == null;
import { db, levelKeys } from "@main/level";
import { MAIN_LOOP_INTERVAL } from "@main/constants";
const { autoUpdater } = updater;
const sendEventsForDebug = false;
const ticksToUpdate = (50 * 60 * 1000) / MAIN_LOOP_INTERVAL; // 50 minutes
export class UpdateManager {
private static hasNotified = false;
@@ -16,7 +16,7 @@ export class UpdateManager {
private static checkTick = 0;
private static mockValuesForDebug() {
this.sendEvent({ type: "update-available", info: { version: "1.3.0" } });
this.sendEvent({ type: "update-available", info: { version: "3.3.1" } });
this.sendEvent({ type: "update-downloaded" });
}
@@ -24,7 +24,27 @@ export class UpdateManager {
WindowManager.mainWindow?.webContents.send("autoUpdaterEvent", event);
}
public static checkForUpdates() {
private static async isAutoInstallEnabled() {
if (process.platform === "darwin") return false;
if (process.platform === "win32") {
return process.env.PORTABLE_EXECUTABLE_FILE == null;
}
if (process.platform === "linux") {
const userPreferences = await db.get<string, UserPreferences | null>(
levelKeys.userPreferences,
{
valueEncoding: "json",
}
);
return userPreferences?.enableAutoInstall === true;
}
return false;
}
public static async checkForUpdates() {
autoUpdater
.once("update-available", (info: UpdateInfo) => {
this.sendEvent({ type: "update-available", info });
@@ -39,6 +59,8 @@ export class UpdateManager {
}
});
const isAutoInstallAvailable = await this.isAutoInstallEnabled();
if (app.isPackaged) {
autoUpdater.autoDownload = isAutoInstallAvailable;
autoUpdater.checkForUpdates().then((result) => {
@@ -52,7 +74,7 @@ export class UpdateManager {
}
public static checkForUpdatePeriodically() {
if (this.checkTick % 2000 == 0) {
if (this.checkTick % ticksToUpdate == 0) {
this.checkForUpdates();
}
this.checkTick++;

View File

@@ -17,7 +17,7 @@ import { HydraApi } from "./hydra-api";
import UserAgent from "user-agents";
import { db, gamesSublevel, levelKeys } from "@main/level";
import { slice, sortBy } from "lodash-es";
import type { UserPreferences } from "@types";
import type { ScreenState, UserPreferences } from "@types";
import { AuthPage } from "@shared";
import { isStaging } from "@main/constants";
@@ -26,27 +26,8 @@ export class WindowManager {
private static readonly editorWindows: Map<string, BrowserWindow> = new Map();
private static loadMainWindowURL(hash = "") {
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
this.mainWindow?.loadURL(
`${process.env["ELECTRON_RENDERER_URL"]}#/${hash}`
);
} else {
this.mainWindow?.loadFile(
path.join(__dirname, "../renderer/index.html"),
{
hash,
}
);
}
}
public static createMainWindow() {
if (this.mainWindow) return;
this.mainWindow = new BrowserWindow({
private static initialConfigInitializationMainWindow: Electron.BrowserWindowConstructorOptions =
{
width: 1200,
height: 720,
minWidth: 1024,
@@ -65,7 +46,65 @@ export class WindowManager {
sandbox: false,
},
show: false,
};
private static loadMainWindowURL(hash = "") {
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
this.mainWindow?.loadURL(
`${process.env["ELECTRON_RENDERER_URL"]}#/${hash}`
);
} else {
this.mainWindow?.loadFile(
path.join(__dirname, "../renderer/index.html"),
{
hash,
}
);
}
}
private static async saveScreenConfig(configScreenWhenClosed: ScreenState) {
await db.put(levelKeys.screenState, configScreenWhenClosed, {
valueEncoding: "json",
});
}
private static async loadScreenConfig() {
const data = await db.get<string, ScreenState | undefined>(
levelKeys.screenState,
{
valueEncoding: "json",
}
);
return data ?? { isMaximized: false, height: 720, width: 1200 };
}
private static updateInitialConfig(
newConfig: Partial<Electron.BrowserWindowConstructorOptions>
) {
this.initialConfigInitializationMainWindow = {
...this.initialConfigInitializationMainWindow,
...newConfig,
};
}
public static async createMainWindow() {
if (this.mainWindow) return;
const { isMaximized = false, ...configWithoutMaximized } =
await this.loadScreenConfig();
this.updateInitialConfig(configWithoutMaximized);
this.mainWindow = new BrowserWindow(
this.initialConfigInitializationMainWindow
);
if (isMaximized) {
this.mainWindow.maximize();
}
this.mainWindow.webContents.session.webRequest.onBeforeSendHeaders(
(details, callback) => {
@@ -143,9 +182,26 @@ export class WindowManager {
}
);
if (this.mainWindow) {
const lastBounds = this.mainWindow.getBounds();
const isMaximized = this.mainWindow.isMaximized() ?? false;
const screenConfig = isMaximized
? {
x: undefined,
y: undefined,
height: this.initialConfigInitializationMainWindow.height ?? 720,
width: this.initialConfigInitializationMainWindow.width ?? 1200,
isMaximized: true,
}
: { ...lastBounds, isMaximized };
await this.saveScreenConfig(screenConfig);
}
if (userPreferences?.preferQuitInsteadOfHiding) {
app.quit();
}
WindowManager.mainWindow?.setProgressBar(-1);
WindowManager.mainWindow = null;
});

View File

@@ -15,6 +15,7 @@ import type {
SeedingStatus,
GameAchievement,
Theme,
FriendRequestSync,
} from "@types";
import type { AuthPage, CatalogueCategory } from "@shared";
import type { AxiosProgressEvent } from "axios";
@@ -99,8 +100,24 @@ contextBridge.exposeInMainWorld("electron", {
/* Download sources */
putDownloadSource: (objectIds: string[]) =>
ipcRenderer.invoke("putDownloadSource", objectIds),
createDownloadSources: (urls: string[]) =>
ipcRenderer.invoke("createDownloadSources", urls),
removeDownloadSource: (url: string, removeAll?: boolean) =>
ipcRenderer.invoke("removeDownloadSource", url, removeAll),
getDownloadSources: () => ipcRenderer.invoke("getDownloadSources"),
/* Library */
toggleAutomaticCloudSync: (
shop: GameShop,
objectId: string,
automaticCloudSync: boolean
) =>
ipcRenderer.invoke(
"toggleAutomaticCloudSync",
shop,
objectId,
automaticCloudSync
),
addGameToLibrary: (shop: GameShop, objectId: string, title: string) =>
ipcRenderer.invoke("addGameToLibrary", shop, objectId, title),
createGameShortcut: (shop: GameShop, objectId: string) =>
@@ -161,6 +178,8 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("getGameByObjectId", shop, objectId),
resetGameAchievements: (shop: GameShop, objectId: string) =>
ipcRenderer.invoke("resetGameAchievements", shop, objectId),
extractGameDownload: (shop: GameShop, objectId: string) =>
ipcRenderer.invoke("extractGameDownload", shop, objectId),
onGamesRunning: (
cb: (
gamesRunning: Pick<GameRunning, "id" | "sessionDurationInMillis">[]
@@ -183,6 +202,15 @@ contextBridge.exposeInMainWorld("electron", {
return () =>
ipcRenderer.removeListener("on-achievement-unlocked", listener);
},
onExtractionComplete: (cb: (shop: GameShop, objectId: string) => void) => {
const listener = (
_event: Electron.IpcRendererEvent,
shop: GameShop,
objectId: string
) => cb(shop, objectId);
ipcRenderer.on("on-extraction-complete", listener);
return () => ipcRenderer.removeListener("on-extraction-complete", listener);
},
/* Hardware */
getDiskFreeSpace: (path: string) =>
@@ -267,6 +295,8 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("showItemInFolder", path),
getFeatures: () => ipcRenderer.invoke("getFeatures"),
getBadges: () => ipcRenderer.invoke("getBadges"),
canInstallCommonRedist: () => ipcRenderer.invoke("canInstallCommonRedist"),
installCommonRedist: () => ipcRenderer.invoke("installCommonRedist"),
platform: process.platform,
/* Auto update */
@@ -282,6 +312,16 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.removeListener("autoUpdaterEvent", listener);
};
},
onCommonRedistProgress: (
cb: (value: { log: string; complete: boolean }) => void
) => {
const listener = (
_event: Electron.IpcRendererEvent,
value: { log: string; complete: boolean }
) => cb(value);
ipcRenderer.on("common-redist-progress", listener);
return () => ipcRenderer.removeListener("common-redist-progress", listener);
},
checkForUpdates: () => ipcRenderer.invoke("checkForUpdates"),
restartAndInstallUpdate: () => ipcRenderer.invoke("restartAndInstallUpdate"),
@@ -295,6 +335,15 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("processProfileImage", imagePath),
getFriendRequests: () => ipcRenderer.invoke("getFriendRequests"),
syncFriendRequests: () => ipcRenderer.invoke("syncFriendRequests"),
onSyncFriendRequests: (cb: (friendRequests: FriendRequestSync) => void) => {
const listener = (
_event: Electron.IpcRendererEvent,
friendRequests: FriendRequestSync
) => cb(friendRequests);
ipcRenderer.on("on-sync-friend-requests", listener);
return () =>
ipcRenderer.removeListener("on-sync-friend-requests", listener);
},
updateFriendRequest: (userId: string, action: FriendRequestAction) =>
ipcRenderer.invoke("updateFriendRequest", userId, action),
sendFriendRequest: (userId: string) =>
@@ -326,6 +375,7 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("getUnlockedAchievements", objectId, shop),
/* Auth */
getAuth: () => ipcRenderer.invoke("getAuth"),
signOut: () => ipcRenderer.invoke("signOut"),
openAuthWindow: (page: AuthPage) =>
ipcRenderer.invoke("openAuthWindow", page),

View File

@@ -20,6 +20,7 @@ import {
setUserDetails,
setProfileBackground,
setGameRunning,
setFriendRequestCount,
} from "@renderer/features";
import { useTranslation } from "react-i18next";
import { UserFriendModal } from "./pages/shared-modals/user-friend-modal";
@@ -30,6 +31,7 @@ import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-m
import { injectCustomCss } from "./helpers";
import "./app.scss";
import { DownloadSource } from "@types";
export interface AppProps {
children: React.ReactNode;
@@ -51,7 +53,6 @@ export function App() {
isFriendsModalVisible,
friendRequetsModalTab,
friendModalUserId,
syncFriendRequests,
hideFriendsModal,
fetchUserDetails,
updateUserDetails,
@@ -123,7 +124,7 @@ export function App() {
.then((response) => {
if (response) {
updateUserDetails(response);
syncFriendRequests();
window.electron.syncFriendRequests();
}
})
.finally(() => {
@@ -134,24 +135,100 @@ export function App() {
$script.src = `${import.meta.env.RENDERER_VITE_EXTERNAL_RESOURCES_URL}/bundle.js?t=${Date.now()}`;
document.head.appendChild($script);
});
}, [fetchUserDetails, syncFriendRequests, updateUserDetails, dispatch]);
}, [fetchUserDetails, updateUserDetails, dispatch]);
const syncDownloadSources = useCallback(async () => {
const downloadSources = await window.electron.getDownloadSources();
const existingDownloadSources: DownloadSource[] =
await downloadSourcesTable.toArray();
window.electron.createDownloadSources(
existingDownloadSources.map((source) => source.url)
);
await Promise.allSettled(
downloadSources.map(async (source) => {
return new Promise((resolve) => {
const existingDownloadSource = existingDownloadSources.find(
(downloadSource) => downloadSource.url === source.url
);
if (!existingDownloadSource) {
const channel = new BroadcastChannel(
`download_sources:import:${source.url}`
);
downloadSourcesWorker.postMessage([
"IMPORT_DOWNLOAD_SOURCE",
source.url,
]);
channel.onmessage = () => {
resolve(true);
channel.close();
};
} else {
resolve(true);
}
});
})
);
updateRepacks();
const id = crypto.randomUUID();
const channel = new BroadcastChannel(`download_sources:sync:${id}`);
channel.onmessage = async (event: MessageEvent<number>) => {
const newRepacksCount = event.data;
window.electron.publishNewRepacksNotification(newRepacksCount);
updateRepacks();
const downloadSources = await downloadSourcesTable.toArray();
downloadSources
.filter((source) => !source.fingerprint)
.forEach(async (downloadSource) => {
const { fingerprint } = await window.electron.putDownloadSource(
downloadSource.objectIds
);
downloadSourcesTable.update(downloadSource.id, { fingerprint });
});
};
downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]);
}, [updateRepacks]);
const onSignIn = useCallback(() => {
fetchUserDetails().then((response) => {
if (response) {
updateUserDetails(response);
syncFriendRequests();
window.electron.syncFriendRequests();
showSuccessToast(t("successfully_signed_in"));
}
});
syncDownloadSources();
}, [
fetchUserDetails,
syncFriendRequests,
t,
showSuccessToast,
updateUserDetails,
syncDownloadSources,
]);
useEffect(() => {
const unsubscribe = window.electron.onSyncFriendRequests((result) => {
dispatch(setFriendRequestCount(result.friendRequestCount));
});
return () => {
unsubscribe();
};
}, [dispatch]);
useEffect(() => {
const unsubscribe = window.electron.onGamesRunning((gamesRunning) => {
if (gamesRunning.length) {
@@ -208,31 +285,8 @@ export function App() {
}, [dispatch, draggingDisabled]);
useEffect(() => {
updateRepacks();
const id = crypto.randomUUID();
const channel = new BroadcastChannel(`download_sources:sync:${id}`);
channel.onmessage = async (event: MessageEvent<number>) => {
const newRepacksCount = event.data;
window.electron.publishNewRepacksNotification(newRepacksCount);
updateRepacks();
const downloadSources = await downloadSourcesTable.toArray();
downloadSources
.filter((source) => !source.fingerprint)
.forEach(async (downloadSource) => {
const { fingerprint } = await window.electron.putDownloadSource(
downloadSource.objectIds
);
downloadSourcesTable.update(downloadSource.id, { fingerprint });
});
};
downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]);
}, [updateRepacks]);
syncDownloadSources();
}, [syncDownloadSources]);
useEffect(() => {
const loadAndApplyTheme = async () => {

View File

@@ -1,7 +1,12 @@
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { useDownload, useLibrary, useUserDetails } from "@renderer/hooks";
import {
useDownload,
useLibrary,
useToast,
useUserDetails,
} from "@renderer/hooks";
import "./bottom-panel.scss";
@@ -17,20 +22,52 @@ export function BottomPanel() {
const { library } = useLibrary();
const { showSuccessToast } = useToast();
const { lastPacket, progress, downloadSpeed, eta } = useDownload();
const [version, setVersion] = useState("");
const [sessionHash, setSessionHash] = useState<null | string>("");
const [commonRedistStatus, setCommonRedistStatus] = useState<string | null>(
null
);
useEffect(() => {
window.electron.getVersion().then((result) => setVersion(result));
}, []);
useEffect(() => {
const unlisten = window.electron.onCommonRedistProgress(
({ log, complete }) => {
if (log === "Installation timed out" || complete) {
setCommonRedistStatus(null);
if (complete) {
showSuccessToast(
t("installation_complete"),
t("installation_complete_message")
);
}
return;
}
setCommonRedistStatus(log);
}
);
return () => unlisten();
}, [t, showSuccessToast]);
useEffect(() => {
window.electron.getSessionHash().then((result) => setSessionHash(result));
}, [userDetails?.id]);
const status = useMemo(() => {
if (commonRedistStatus) {
return t("installing_common_redist", { log: commonRedistStatus });
}
const game = lastPacket
? library.find((game) => game.id === lastPacket?.gameId)
: undefined;
@@ -64,7 +101,15 @@ export function BottomPanel() {
}
return t("no_downloads_in_progress");
}, [t, library, lastPacket, progress, eta, downloadSpeed]);
}, [
t,
library,
lastPacket,
progress,
eta,
downloadSpeed,
commonRedistStatus,
]);
return (
<footer className="bottom-panel">

View File

@@ -7,7 +7,7 @@ export interface CheckboxFieldProps
React.InputHTMLAttributes<HTMLInputElement>,
HTMLInputElement
> {
label: string;
label: string | React.ReactNode;
}
export function CheckboxField({ label, ...props }: CheckboxFieldProps) {

View File

@@ -10,6 +10,8 @@ export interface ConfirmationModalProps extends Omit<ModalProps, "children"> {
onConfirm: () => void;
onCancel?: () => void;
buttonsIsDisabled?: boolean;
}
export function ConfirmationModal({
@@ -18,6 +20,7 @@ export function ConfirmationModal({
descriptionText,
onConfirm,
onCancel,
buttonsIsDisabled = false,
...props
}: ConfirmationModalProps) {
const handleCancelClick = () => {
@@ -38,7 +41,11 @@ export function ConfirmationModal({
<Button theme="outline" onClick={handleCancelClick}>
{cancelButtonLabel}
</Button>
<Button theme="danger" onClick={onConfirm}>
<Button
theme="danger"
disabled={buttonsIsDisabled}
onClick={onConfirm}
>
{confirmButtonLabel}
</Button>
</div>

View File

@@ -7,7 +7,7 @@ import "./game-card.scss";
import { useTranslation } from "react-i18next";
import { Badge } from "../badge/badge";
import { useCallback, useState } from "react";
import { useCallback, useState, useMemo } from "react";
import { useFormat, useRepacks } from "@renderer/hooks";
import { steamUrlBuilder } from "@shared";
@@ -45,6 +45,15 @@ export function GameCard({ game, ...props }: GameCardProps) {
const { numberFormatter } = useFormat();
const firstThreeRepackers = useMemo(
() => uniqueRepackers.slice(0, 3),
[uniqueRepackers]
);
const remainingCount = useMemo(
() => uniqueRepackers.length - 3,
[uniqueRepackers]
);
return (
<button
{...props}
@@ -68,15 +77,24 @@ export function GameCard({ game, ...props }: GameCardProps) {
{uniqueRepackers.length > 0 ? (
<ul className="game-card__download-options">
{uniqueRepackers.map((repacker) => (
{firstThreeRepackers.map((repacker) => (
<li key={repacker}>
<Badge>{repacker}</Badge>
</li>
))}
{remainingCount > 0 && (
<li>
<Badge>
+{remainingCount}{" "}
{t("game_card:available", { count: remainingCount })}
</Badge>
</li>
)}
</ul>
) : (
<p className="game-card__no-download-label">{t("no_downloads")}</p>
)}
<div className="game-card__specifics">
<div className="game-card__specifics-item">
<DownloadIcon />
@@ -84,11 +102,10 @@ export function GameCard({ game, ...props }: GameCardProps) {
{stats ? numberFormatter.format(stats.downloadCount) : "…"}
</span>
</div>
<div className="game-card__specifics-item">
<PeopleIcon />
<span>
{stats ? numberFormatter.format(stats?.playerCount) : "…"}
{stats ? numberFormatter.format(stats.playerCount) : "…"}
</span>
</div>
</div>

View File

@@ -1,7 +1,7 @@
import { useNavigate } from "react-router-dom";
import { PeopleIcon } from "@primer/octicons-react";
import { useAppSelector, useUserDetails } from "@renderer/hooks";
import { useEffect, useMemo } from "react";
import { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
@@ -9,19 +9,13 @@ import { Avatar } from "../avatar/avatar";
import { AuthPage } from "@shared";
import "./sidebar-profile.scss";
const LONG_POLLING_INTERVAL = 120_000;
export function SidebarProfile() {
const navigate = useNavigate();
const { t } = useTranslation("sidebar");
const {
userDetails,
friendRequestCount,
showFriendsModal,
syncFriendRequests,
} = useUserDetails();
const { userDetails, friendRequestCount, showFriendsModal } =
useUserDetails();
const { gameRunning } = useAppSelector((state) => state.gameRunning);
@@ -34,16 +28,6 @@ export function SidebarProfile() {
navigate(`/profile/${userDetails.id}`);
};
useEffect(() => {
const pollingInterval = setInterval(() => {
syncFriendRequests();
}, LONG_POLLING_INTERVAL);
return () => {
clearInterval(pollingInterval);
};
}, [syncFriendRequests]);
const friendsButton = useMemo(() => {
if (!userDetails) return null;

View File

@@ -31,6 +31,7 @@ import type {
TorBoxUser,
Theme,
Badge,
Auth,
} from "@types";
import type { AxiosProgressEvent } from "axios";
import type disk from "diskusage";
@@ -87,6 +88,11 @@ declare global {
getDevelopers: () => Promise<string[]>;
/* Library */
toggleAutomaticCloudSync: (
shop: GameShop,
objectId: string,
automaticCloudSync: boolean
) => Promise<void>;
addGameToLibrary: (
shop: GameShop,
objectId: string,
@@ -143,6 +149,8 @@ declare global {
onLibraryBatchComplete: (cb: () => void) => () => Electron.IpcRenderer;
resetGameAchievements: (shop: GameShop, objectId: string) => Promise<void>;
/* User preferences */
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
authenticateTorBox: (apiToken: string) => Promise<TorBoxUser>;
getUserPreferences: () => Promise<UserPreferences | null>;
updateUserPreferences: (
preferences: Partial<UserPreferences>
@@ -151,14 +159,21 @@ declare global {
enabled: boolean;
minimized: boolean;
}) => Promise<void>;
authenticateRealDebrid: (apiToken: string) => Promise<RealDebridUser>;
authenticateTorBox: (apiToken: string) => Promise<TorBoxUser>;
extractGameDownload: (shop: GameShop, objectId: string) => Promise<boolean>;
onAchievementUnlocked: (cb: () => void) => () => Electron.IpcRenderer;
onExtractionComplete: (
cb: (shop: GameShop, objectId: string) => void
) => () => Electron.IpcRenderer;
/* Download sources */
putDownloadSource: (
objectIds: string[]
) => Promise<{ fingerprint: string }>;
createDownloadSources: (urls: string[]) => Promise<void>;
removeDownloadSource: (url: string, removeAll?: boolean) => Promise<void>;
getDownloadSources: () => Promise<
Pick<DownloadSource, "url" | "createdAt" | "updatedAt">[]
>;
/* Hardware */
getDiskFreeSpace: (path: string) => Promise<disk.DiskUsage>;
@@ -219,6 +234,11 @@ declare global {
showItemInFolder: (path: string) => Promise<void>;
getFeatures: () => Promise<string[]>;
getBadges: () => Promise<Badge[]>;
canInstallCommonRedist: () => Promise<boolean>;
installCommonRedist: () => Promise<void>;
onCommonRedistProgress: (
cb: (value: { log: string; complete: boolean }) => void
) => () => Electron.IpcRenderer;
platform: NodeJS.Platform;
/* Auto update */
@@ -229,6 +249,7 @@ declare global {
restartAndInstallUpdate: () => Promise<void>;
/* Auth */
getAuth: () => Promise<Auth | null>;
signOut: () => Promise<void>;
openAuthWindow: (page: AuthPage) => Promise<void>;
getSessionHash: () => Promise<string | null>;
@@ -273,7 +294,10 @@ declare global {
path: string
) => Promise<{ imagePath: string; mimeType: string }>;
getFriendRequests: () => Promise<FriendRequest[]>;
syncFriendRequests: () => Promise<FriendRequestSync>;
syncFriendRequests: () => Promise<void>;
onSyncFriendRequests: (
cb: (friendRequests: FriendRequestSync) => void
) => () => Electron.IpcRenderer;
updateFriendRequest: (
userId: string,
action: FriendRequestAction

View File

@@ -21,9 +21,9 @@ export interface CatalogueCache {
export const db = new Dexie("Hydra");
db.version(8).stores({
db.version(9).stores({
repacks: `++id, title, uris, fileSize, uploadDate, downloadSourceId, repacker, objectIds, createdAt, updatedAt`,
downloadSources: `++id, url, name, etag, objectIds, downloadCount, status, fingerprint, createdAt, updatedAt`,
downloadSources: `++id, &url, name, etag, objectIds, downloadCount, status, fingerprint, createdAt, updatedAt`,
howLongToBeatEntries: `++id, categories, [shop+objectId], createdAt, updatedAt`,
});

View File

@@ -1,19 +1,7 @@
import { formatDate, getDateLocale } from "@shared";
import { format, formatDistance, subMilliseconds } from "date-fns";
import type { FormatDistanceOptions } from "date-fns";
import {
ptBR,
enUS,
es,
fr,
pl,
hu,
tr,
ru,
it,
be,
zhCN,
da,
} from "date-fns/locale";
import { enUS } from "date-fns/locale";
import { useTranslation } from "react-i18next";
export function useDate() {
@@ -21,22 +9,6 @@ export function useDate() {
const { language } = i18n;
const getDateLocale = () => {
if (language.startsWith("pt")) return ptBR;
if (language.startsWith("es")) return es;
if (language.startsWith("fr")) return fr;
if (language.startsWith("hu")) return hu;
if (language.startsWith("pl")) return pl;
if (language.startsWith("tr")) return tr;
if (language.startsWith("ru")) return ru;
if (language.startsWith("it")) return it;
if (language.startsWith("be")) return be;
if (language.startsWith("zh")) return zhCN;
if (language.startsWith("da")) return da;
return enUS;
};
return {
formatDistance: (
date: string | number | Date,
@@ -46,7 +18,7 @@ export function useDate() {
try {
return formatDistance(date, baseDate, {
...options,
locale: getDateLocale(),
locale: getDateLocale(language),
});
} catch (err) {
return "";
@@ -61,7 +33,7 @@ export function useDate() {
try {
return formatDistance(subMilliseconds(new Date(), millis), baseDate, {
...options,
locale: getDateLocale(),
locale: getDateLocale(language),
});
} catch (err) {
return "";
@@ -69,18 +41,13 @@ export function useDate() {
},
formatDateTime: (date: number | Date | string): string => {
const locale = getDateLocale();
const locale = getDateLocale(language);
return format(
date,
locale == enUS ? "MM/dd/yyyy - HH:mm" : "dd/MM/yyyy HH:mm"
);
},
formatDate: (date: number | Date | string): string => {
if (isNaN(new Date(date).getDate())) return "N/A";
const locale = getDateLocale();
return format(date, locale == enUS ? "MM/dd/yyyy" : "dd/MM/yyyy");
},
formatDate: (date: number | Date | string) => formatDate(date, language),
};
}

View File

@@ -6,7 +6,6 @@ import {
setFriendRequests,
setFriendsModalVisible,
setFriendsModalHidden,
setFriendRequestCount,
} from "@renderer/features";
import type {
FriendRequestAction,
@@ -15,7 +14,6 @@ import type {
} from "@types";
import * as Sentry from "@sentry/react";
import { UserFriendModalTab } from "@renderer/pages/shared-modals/user-friend-modal";
import { isFuture, isToday } from "date-fns";
export function useUserDetails() {
const dispatch = useAppDispatch();
@@ -89,24 +87,15 @@ export function useUserDetails() {
]
);
const syncFriendRequests = useCallback(async () => {
return window.electron
.syncFriendRequests()
.then((sync) => {
dispatch(setFriendRequestCount(sync.friendRequestCount));
})
.catch(() => {});
}, [dispatch]);
const fetchFriendRequests = useCallback(async () => {
return window.electron
.getFriendRequests()
.then((friendRequests) => {
syncFriendRequests();
window.electron.syncFriendRequests();
dispatch(setFriendRequests(friendRequests));
})
.catch(() => {});
}, [dispatch, syncFriendRequests]);
}, [dispatch]);
const showFriendsModal = useCallback(
(initialTab: UserFriendModalTab, userId: string) => {
@@ -146,8 +135,8 @@ export function useUserDetails() {
const unblockUser = (userId: string) => window.electron.unblockUser(userId);
const hasActiveSubscription = useMemo(() => {
const expiresAt = userDetails?.subscription?.expiresAt;
return expiresAt && (isFuture(expiresAt) || isToday(expiresAt));
const expiresAt = new Date(userDetails?.subscription?.expiresAt ?? 0);
return expiresAt > new Date();
}, [userDetails]);
return {
@@ -168,7 +157,6 @@ export function useUserDetails() {
patchUser,
sendFriendRequest,
fetchFriendRequests,
syncFriendRequests,
updateFriendRequestState,
blockUser,
unblockUser,

View File

@@ -18,14 +18,11 @@ export function Pagination({
if (totalPages <= 1) return null;
// Number of visible pages
const visiblePages = 3;
// Calculate the start and end of the visible range
let startPage = Math.max(1, page - 1); // Shift range slightly back
let startPage = Math.max(1, page - 1);
let endPage = startPage + visiblePages - 1;
// Adjust the range if we're near the start or end
if (endPage > totalPages) {
endPage = totalPages;
startPage = Math.max(1, endPage - visiblePages + 1);
@@ -33,7 +30,6 @@ export function Pagination({
return (
<div className="pagination">
{/* Previous Button */}
<Button
theme="outline"
onClick={() => onPageChange(page - 1)}
@@ -45,7 +41,6 @@ export function Pagination({
{page > 2 && (
<>
{/* initial page */}
<Button
theme="outline"
onClick={() => onPageChange(1)}
@@ -55,14 +50,12 @@ export function Pagination({
{1}
</Button>
{/* ellipsis */}
<div className="pagination__ellipsis">
<span className="pagination__ellipsis-text">...</span>
</div>
</>
)}
{/* Page Buttons */}
{Array.from(
{ length: endPage - startPage + 1 },
(_, i) => startPage + i
@@ -79,12 +72,10 @@ export function Pagination({
{page < totalPages - 1 && (
<>
{/* ellipsis */}
<div className="pagination__ellipsis">
<span className="pagination__ellipsis-text">...</span>
</div>
{/* last page */}
<Button
theme="outline"
onClick={() => onPageChange(totalPages)}
@@ -96,7 +87,6 @@ export function Pagination({
</>
)}
{/* Next Button */}
<Button
theme="outline"
onClick={() => onPageChange(page + 1)}

View File

@@ -10,11 +10,11 @@ import {
import { Downloader, formatBytes, steamUrlBuilder } from "@shared";
import { DOWNLOADER_NAME } from "@renderer/constants";
import { useAppSelector, useDownload } from "@renderer/hooks";
import { useAppSelector, useDownload, useLibrary } from "@renderer/hooks";
import "./download-group.scss";
import { useTranslation } from "react-i18next";
import { useMemo } from "react";
import { useCallback, useMemo } from "react";
import {
DropdownMenu,
DropdownMenuItem,
@@ -22,6 +22,7 @@ import {
import {
ColumnsIcon,
DownloadIcon,
FileDirectoryIcon,
LinkIcon,
PlayIcon,
QuestionIcon,
@@ -56,6 +57,8 @@ export function DownloadGroup({
(state) => state.userPreferences.value
);
const { updateLibrary } = useLibrary();
const {
lastPacket,
progress,
@@ -89,6 +92,14 @@ export function DownloadGroup({
return map;
}, [seedingStatus]);
const extractGameDownload = useCallback(
async (shop: GameShop, objectId: string) => {
await window.electron.extractGameDownload(shop, objectId);
updateLibrary();
},
[updateLibrary]
);
const getGameInfo = (game: LibraryGame) => {
const download = game.download!;
@@ -96,6 +107,10 @@ export function DownloadGroup({
const finalDownloadSize = getFinalDownloadSize(game);
const seedingStatus = seedingMap.get(game.id);
if (download.extracting) {
return <p>{t("extracting")}</p>;
}
if (isGameDeleting(game.id)) {
return <p>{t("deleting")}</p>;
}
@@ -197,6 +212,14 @@ export function DownloadGroup({
},
icon: <DownloadIcon />,
},
{
label: t("extract"),
disabled: game.download.extracting,
icon: <FileDirectoryIcon />,
onClick: () => {
extractGameDownload(game.shop, game.objectId);
},
},
{
label: t("stop_seeding"),
disabled: deleting,

View File

@@ -38,7 +38,13 @@ export default function Downloads() {
useEffect(() => {
window.electron.onSeedingStatus((value) => setSeedingStatus(value));
}, []);
const unsubscribe = window.electron.onExtractionComplete(() => {
updateLibrary();
});
return () => unsubscribe();
}, [updateLibrary]);
const handleOpenGameInstaller = (shop: GameShop, objectId: string) =>
window.electron.openGameInstaller(shop, objectId).then((isBinaryInPath) => {
@@ -67,7 +73,7 @@ export default function Downloads() {
if (!next.download) return prev;
/* Is downloading */
if (lastPacket?.gameId === next.id)
if (lastPacket?.gameId === next.id || next.download.extracting)
return { ...prev, downloading: [...prev.downloading, next] };
/* Is either queued or paused */

View File

@@ -4,7 +4,6 @@ import { cloudSyncContext, gameDetailsContext } from "@renderer/context";
import "./cloud-sync-modal.scss";
import { formatBytes } from "@shared";
import { format } from "date-fns";
import {
ClockIcon,
DeviceDesktopIcon,
@@ -14,7 +13,7 @@ import {
TrashIcon,
UploadIcon,
} from "@primer/octicons-react";
import { useAppSelector, useToast } from "@renderer/hooks";
import { useAppSelector, useDate, useToast } from "@renderer/hooks";
import { useTranslation } from "react-i18next";
import { AxiosProgressEvent } from "axios";
import { formatDownloadProgress } from "@renderer/helpers";
@@ -29,6 +28,8 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
const { t } = useTranslation("game_details");
const { formatDate, formatDateTime } = useDate();
const {
artifacts,
backupPreview,
@@ -203,9 +204,10 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
<div className="cloud-sync-modal__artifact-info">
<div className="cloud-sync-modal__artifact-header">
<h3>
{t("backup_from", {
date: format(artifact.createdAt, "dd/MM/yyyy"),
})}
{artifact.label ??
t("backup_from", {
date: formatDate(artifact.createdAt),
})}
</h3>
<small>{formatBytes(artifact.artifactLengthInBytes)}</small>
</div>
@@ -222,7 +224,7 @@ export function CloudSyncModal({ visible, onClose }: CloudSyncModalProps) {
<span className="cloud-sync-modal__artifact-meta">
<ClockIcon size={14} />
{format(artifact.createdAt, "dd/MM/yyyy HH:mm:ss")}
{formatDateTime(artifact.createdAt)}
</span>
</div>

View File

@@ -98,7 +98,8 @@ export default function GameDetails() {
const handleStartDownload = async (
repack: GameRepack,
downloader: Downloader,
downloadPath: string
downloadPath: string,
automaticallyExtract: boolean
) => {
const response = await startDownload({
repackId: repack.id,
@@ -108,6 +109,7 @@ export default function GameDetails() {
shop,
downloadPath,
uri: selectRepackUri(repack, downloader),
automaticallyExtract: automaticallyExtract,
});
if (response.ok) {

View File

@@ -1,6 +1,12 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { Trans, useTranslation } from "react-i18next";
import { Button, Link, Modal, TextField } from "@renderer/components";
import {
Button,
CheckboxField,
Link,
Modal,
TextField,
} from "@renderer/components";
import { CheckCircleFillIcon, DownloadIcon } from "@primer/octicons-react";
import { Downloader, formatBytes, getDownloadersForUris } from "@shared";
import type { GameRepack } from "@types";
@@ -14,7 +20,8 @@ export interface DownloadSettingsModalProps {
startDownload: (
repack: GameRepack,
downloader: Downloader,
downloadPath: string
downloadPath: string,
automaticallyExtract: boolean
) => Promise<{ ok: boolean; error?: string }>;
repack: GameRepack | null;
}
@@ -32,6 +39,8 @@ export function DownloadSettingsModal({
const [diskFreeSpace, setDiskFreeSpace] = useState<number | null>(null);
const [selectedPath, setSelectedPath] = useState("");
const [downloadStarting, setDownloadStarting] = useState(false);
const [automaticExtractionEnabled, setAutomaticExtractionEnabled] =
useState(true);
const [selectedDownloader, setSelectedDownloader] =
useState<Downloader | null>(null);
const [hasWritePermission, setHasWritePermission] = useState<boolean | null>(
@@ -72,6 +81,21 @@ export function DownloadSettingsModal({
return getDownloadersForUris(repack?.uris ?? []);
}, [repack?.uris]);
const getDefaultDownloader = useCallback(
(availableDownloaders: Downloader[]) => {
if (availableDownloaders.includes(Downloader.TorBox)) {
return Downloader.TorBox;
}
if (availableDownloaders.includes(Downloader.RealDebrid)) {
return Downloader.RealDebrid;
}
return availableDownloaders[0];
},
[]
);
useEffect(() => {
if (userPreferences?.downloadsPath) {
setSelectedPath(userPreferences.downloadsPath);
@@ -89,13 +113,9 @@ export function DownloadSettingsModal({
return true;
});
/* Gives preference to TorBox */
const selectedDownloader = filteredDownloaders.includes(Downloader.TorBox)
? Downloader.TorBox
: filteredDownloaders[0];
setSelectedDownloader(selectedDownloader ?? null);
setSelectedDownloader(getDefaultDownloader(filteredDownloaders));
}, [
getDefaultDownloader,
userPreferences?.downloadsPath,
downloaders,
userPreferences?.realDebridApiToken,
@@ -122,7 +142,8 @@ export function DownloadSettingsModal({
const response = await startDownload(
repack,
selectedDownloader!,
selectedPath
selectedPath,
automaticExtractionEnabled
);
if (response.ok) {
@@ -217,6 +238,14 @@ export function DownloadSettingsModal({
</p>
</div>
<CheckboxField
label={t("automatically_extract_downloaded_files")}
checked={automaticExtractionEnabled}
onChange={() =>
setAutomaticExtractionEnabled(!automaticExtractionEnabled)
}
/>
<Button
onClick={handleStartClick}
disabled={

View File

@@ -23,6 +23,20 @@
}
}
&__cloud-sync-label {
display: flex;
gap: globals.$spacing-unit;
align-items: center;
}
&__cloud-sync-hydra-cloud {
background: linear-gradient(270deg, #16b195 50%, #3e62c0 100%);
color: #fff;
padding: 0 globals.$spacing-unit;
border-radius: 4px;
font-size: globals.$small-font-size;
}
&__row {
display: flex;
gap: globals.$spacing-unit;

View File

@@ -1,6 +1,6 @@
import { useContext, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { Button, Modal, TextField } from "@renderer/components";
import { Button, CheckboxField, Modal, TextField } from "@renderer/components";
import type { LibraryGame } from "@types";
import { gameDetailsContext } from "@renderer/context";
import { DeleteGameModal } from "@renderer/pages/downloads/delete-game-modal";
@@ -34,12 +34,17 @@ export function GameOptionsModal({
achievements,
} = useContext(gameDetailsContext);
const { hasActiveSubscription } = useUserDetails();
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [showRemoveGameModal, setShowRemoveGameModal] = useState(false);
const [launchOptions, setLaunchOptions] = useState(game.launchOptions ?? "");
const [showResetAchievementsModal, setShowResetAchievementsModal] =
useState(false);
const [isDeletingAchievements, setIsDeletingAchievements] = useState(false);
const [automaticCloudSync, setAutomaticCloudSync] = useState(
game.automaticCloudSync ?? false
);
const {
removeGameInstaller,
@@ -129,6 +134,7 @@ export function GameOptionsModal({
const handleClearExecutablePath = async () => {
await window.electron.updateExecutablePath(game.shop, game.objectId, null);
updateGame();
};
@@ -183,6 +189,20 @@ export function GameOptionsModal({
}
};
const handleToggleAutomaticCloudSync = async (
event: React.ChangeEvent<HTMLInputElement>
) => {
setAutomaticCloudSync(event.target.checked);
await window.electron.toggleAutomaticCloudSync(
game.shop,
game.objectId,
event.target.checked
);
updateGame();
};
return (
<>
<DeleteGameModal
@@ -266,6 +286,20 @@ export function GameOptionsModal({
</div>
</div>
<CheckboxField
label={
<div className="game-options-modal__cloud-sync-label">
{t("enable_automatic_cloud_sync")}
<span className="game-options-modal__cloud-sync-hydra-cloud">
Hydra Cloud
</span>
</div>
}
checked={automaticCloudSync}
disabled={!hasActiveSubscription || !game.executablePath}
onChange={handleToggleAutomaticCloudSync}
/>
{shouldShowWinePrefixConfiguration && (
<div className="game-options-modal__wine-prefix">
<div className="game-options-modal__header">

View File

@@ -16,7 +16,8 @@ export interface RepacksModalProps {
startDownload: (
repack: GameRepack,
downloader: Downloader,
downloadPath: string
downloadPath: string,
automaticallyExtract: boolean
) => Promise<{ ok: boolean; error?: string }>;
onClose: () => void;
}

View File

@@ -119,7 +119,8 @@ export function AddDownloadSourceModal({
downloadSourcesWorker.postMessage(["IMPORT_DOWNLOAD_SOURCE", url]);
channel.onmessage = async () => {
channel.onmessage = () => {
window.electron.createDownloadSources([url]);
setIsLoading(false);
putDownloadSource();

View File

@@ -20,6 +20,7 @@ export function SettingsBehavior() {
runAtStartup: false,
startMinimized: false,
disableNsfwAlert: false,
enableAutoInstall: false,
seedAfterDownloadComplete: false,
showHiddenAchievementsDescription: false,
});
@@ -34,6 +35,7 @@ export function SettingsBehavior() {
runAtStartup: userPreferences.runAtStartup ?? false,
startMinimized: userPreferences.startMinimized ?? false,
disableNsfwAlert: userPreferences.disableNsfwAlert ?? false,
enableAutoInstall: userPreferences.enableAutoInstall ?? false,
seedAfterDownloadComplete:
userPreferences.seedAfterDownloadComplete ?? false,
showHiddenAchievementsDescription:
@@ -99,6 +101,16 @@ export function SettingsBehavior() {
</div>
)}
{window.electron.platform === "linux" && (
<CheckboxField
label={t("enable_auto_install")}
checked={form.enableAutoInstall}
onChange={() =>
handleChange({ enableAutoInstall: !form.enableAutoInstall })
}
/>
)}
<CheckboxField
label={t("disable_nsfw_alert")}
checked={form.disableNsfwAlert}

View File

@@ -53,4 +53,9 @@
text-decoration: none;
}
}
&__buttons-container {
display: flex;
gap: globals.$spacing-unit;
}
}

View File

@@ -1,10 +1,20 @@
import { useContext, useEffect, useState } from "react";
import { TextField, Button, Badge } from "@renderer/components";
import {
TextField,
Button,
Badge,
ConfirmationModal,
} from "@renderer/components";
import { useTranslation } from "react-i18next";
import type { DownloadSource } from "@types";
import { NoEntryIcon, PlusCircleIcon, SyncIcon } from "@primer/octicons-react";
import {
NoEntryIcon,
PlusCircleIcon,
SyncIcon,
TrashIcon,
} from "@primer/octicons-react";
import { AddDownloadSourceModal } from "./add-download-source-modal";
import { useAppDispatch, useRepacks, useToast } from "@renderer/hooks";
import { DownloadSourceStatus } from "@shared";
@@ -16,6 +26,10 @@ import { setFilters, clearFilters } from "@renderer/features";
import "./settings-download-sources.scss";
export function SettingsDownloadSources() {
const [
showConfirmationDeleteAllSourcesModal,
setShowConfirmationDeleteAllSourcesModal,
] = useState(false);
const [showAddDownloadSourceModal, setShowAddDownloadSourceModal] =
useState(false);
const [downloadSources, setDownloadSources] = useState<DownloadSource[]>([]);
@@ -23,6 +37,7 @@ export function SettingsDownloadSources() {
useState(false);
const [isRemovingDownloadSource, setIsRemovingDownloadSource] =
useState(false);
const [isFetchingSources, setIsFetchingSources] = useState(true);
const { sourceUrl, clearSourceUrl } = useContext(settingsContext);
@@ -41,6 +56,9 @@ export function SettingsDownloadSources() {
.sortBy("createdAt")
.then((sources) => {
setDownloadSources(sources.reverse());
})
.finally(() => {
setIsFetchingSources(false);
});
};
@@ -52,14 +70,20 @@ export function SettingsDownloadSources() {
if (sourceUrl) setShowAddDownloadSourceModal(true);
}, [sourceUrl]);
const handleRemoveSource = (id: number) => {
const handleRemoveSource = (downloadSource: DownloadSource) => {
setIsRemovingDownloadSource(true);
const channel = new BroadcastChannel(`download_sources:delete:${id}`);
const channel = new BroadcastChannel(
`download_sources:delete:${downloadSource.id}`
);
downloadSourcesWorker.postMessage(["DELETE_DOWNLOAD_SOURCE", id]);
downloadSourcesWorker.postMessage([
"DELETE_DOWNLOAD_SOURCE",
downloadSource.id,
]);
channel.onmessage = () => {
showSuccessToast(t("removed_download_source"));
window.electron.removeDownloadSource(downloadSource.url);
getDownloadSources();
setIsRemovingDownloadSource(false);
@@ -68,6 +92,25 @@ export function SettingsDownloadSources() {
};
};
const handleRemoveAllDownloadSources = () => {
setIsRemovingDownloadSource(true);
const id = crypto.randomUUID();
const channel = new BroadcastChannel(`download_sources:delete_all:${id}`);
downloadSourcesWorker.postMessage(["DELETE_ALL_DOWNLOAD_SOURCES", id]);
channel.onmessage = () => {
showSuccessToast(t("removed_download_sources"));
window.electron.removeDownloadSource("", true);
getDownloadSources();
setIsRemovingDownloadSource(false);
setShowConfirmationDeleteAllSourcesModal(false);
channel.close();
updateRepacks();
};
};
const handleAddDownloadSource = async () => {
await getDownloadSources();
showSuccessToast(t("added_download_source"));
@@ -115,6 +158,17 @@ export function SettingsDownloadSources() {
onClose={handleModalClose}
onAddDownloadSource={handleAddDownloadSource}
/>
<ConfirmationModal
cancelButtonLabel={t("cancel_button_confirmation_delete_all_sources")}
confirmButtonLabel={t("confirm_button_confirmation_delete_all_sources")}
descriptionText={t("description_confirmation_delete_all_sources")}
clickOutsideToClose={false}
onConfirm={handleRemoveAllDownloadSources}
visible={showConfirmationDeleteAllSourcesModal}
title={t("title_confirmation_delete_all_sources")}
onClose={() => setShowConfirmationDeleteAllSourcesModal(false)}
buttonsIsDisabled={isRemovingDownloadSource}
/>
<p>{t("download_sources_description")}</p>
@@ -125,7 +179,8 @@ export function SettingsDownloadSources() {
disabled={
!downloadSources.length ||
isSyncingDownloadSources ||
isRemovingDownloadSource
isRemovingDownloadSource ||
isFetchingSources
}
onClick={syncDownloadSources}
>
@@ -133,15 +188,36 @@ export function SettingsDownloadSources() {
{t("sync_download_sources")}
</Button>
<Button
type="button"
theme="outline"
onClick={() => setShowAddDownloadSourceModal(true)}
disabled={isSyncingDownloadSources}
>
<PlusCircleIcon />
{t("add_download_source")}
</Button>
<div className="settings-download-sources__buttons-container">
<Button
type="button"
theme="danger"
onClick={() => setShowConfirmationDeleteAllSourcesModal(true)}
disabled={
isRemovingDownloadSource ||
isSyncingDownloadSources ||
!downloadSources.length ||
isFetchingSources
}
>
<TrashIcon />
{t("button_delete_all_sources")}
</Button>
<Button
type="button"
theme="outline"
onClick={() => setShowAddDownloadSourceModal(true)}
disabled={
isSyncingDownloadSources ||
isFetchingSources ||
isRemovingDownloadSource
}
>
<PlusCircleIcon />
{t("add_download_source")}
</Button>
</div>
</div>
<ul className="settings-download-sources__list">
@@ -183,7 +259,7 @@ export function SettingsDownloadSources() {
<Button
type="button"
theme="outline"
onClick={() => handleRemoveSource(downloadSource.id)}
onClick={() => handleRemoveSource(downloadSource)}
disabled={isRemovingDownloadSource}
>
<NoEntryIcon />

Some files were not shown because too many files have changed in this diff Show More