Compare commits

..

162 Commits

Author SHA1 Message Date
Chubby Granny Chaser
fc4043c2c4 Update scripts/postinstall.cjs
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-04-09 08:01:03 +01:00
Chubby Granny Chaser
97cf02577a fix: removing console log 2025-04-09 08:00:15 +01:00
Chubby Granny Chaser
fbce53d61a fix: removing console log 2025-04-09 07:59:34 +01:00
Chubby Granny Chaser
1835adf8b4 fix: fixing multiple connections 2025-04-09 07:57:00 +01:00
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
Zamitto
af2896efc3 chore: bump version 2025-03-04 17:47:25 -03:00
Chubby Granny Chaser
c7735362e0 Merge pull request #1486 from hydralauncher/feat/dynamic-badges
feat: adding dynamic badges
2025-03-04 20:45:36 +00:00
Chubby Granny Chaser
886e176b08 fix: using value-encoding for level value 2025-03-04 20:36:40 +00:00
Chubby Granny Chaser
f522a7c9ef feat: moving props spread 2025-03-04 20:10:04 +00:00
Chubby Granny Chaser
69f4ce821f feat: moving props spread 2025-03-04 20:00:51 +00:00
Chubby Granny Chaser
8bfd6e5547 Update src/renderer/src/components/avatar/avatar.tsx
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-03-04 19:58:27 +00:00
Chubby Granny Chaser
1cbde684e7 fix: fixing duplicate import for level 2025-03-04 19:48:03 +00:00
Chubby Granny Chaser
e5f7e9addc feat: adding dynamic badges 2025-03-04 19:44:35 +00: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
Zamitto
8513f83169 Merge pull request #1467 from hydrasources/patch-13
Update translation.json
2025-02-22 20:17:07 -03:00
hydrasources
b116e29dc0 Update translation.json 2025-02-21 22:24:00 +03:00
hydrasources
52b291fb24 Update translation.json 2025-02-21 07:41:39 +03:00
Zamitto
260a11ba6a Merge pull request #1465 from hydralauncher/fix/lazy-loading-messing-up-custom-css
fix: lazy loading messing up custom css
2025-02-19 18:40:37 -03:00
Zamitto
adf3bf38a8 chore: fix gh actions concurrency 2025-02-18 22:36:49 -03:00
Zamitto
b6193636dd chore: fix gh actions concurrency 2025-02-18 22:33:48 -03:00
Zamitto
923601bdef feat: add readonly to test pipeline 2025-02-18 22:32:13 -03:00
Zamitto
58855a93a8 feat: add readonly to test pipeline 2025-02-18 22:31:19 -03:00
Zamitto
d879f2e3df chore: bump version 2025-02-18 22:29:38 -03:00
Zamitto
73ab3872a7 fix: lazy loading messing up custom css 2025-02-18 22:25:41 -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
132 changed files with 2614 additions and 1106 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

@@ -1,5 +1,9 @@
name: Build
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on: pull_request
jobs:
@@ -43,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'
@@ -57,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 }}
@@ -74,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

@@ -1,5 +1,9 @@
name: Lint
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on: pull_request
jobs:

View File

@@ -1,5 +1,9 @@
name: Release
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
push:
branches: main
@@ -45,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'
@@ -60,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.1",
"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",
@@ -71,9 +69,11 @@
"react-loading-skeleton": "^3.4.0",
"react-redux": "^9.1.1",
"react-router-dom": "^6.22.3",
"react-tooltip": "^5.28.0",
"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",

View File

@@ -1,48 +1,61 @@
import aria2p
class HttpDownloader:
def __init__(self):
self.download = None
self.aria2 = aria2p.API(
aria2p.Client(
host="http://localhost",
port=6800,
secret=""
)
)
def __init__(self):
self.download = None
self.aria2 = aria2p.API(
aria2p.Client(
host="http://localhost",
port=6800,
secret=""
)
)
def start_download(self, url: str, save_path: str, header: str, out: str = None):
if self.download:
self.aria2.resume([self.download])
else:
downloads = self.aria2.add(url, options={"header": header, "dir": save_path, "out": out})
self.download = downloads[0]
def pause_download(self):
if self.download:
self.aria2.pause([self.download])
def cancel_download(self):
if self.download:
self.aria2.remove([self.download])
self.download = None
def start_download(self, url: str, save_path: str, header: str, out: str = None, allow_multiple_connections: bool = False):
if self.download:
self.aria2.resume([self.download])
else:
options = {
"header": header,
"dir": save_path,
"out": out
}
def get_download_status(self):
if self.download == None:
return None
if allow_multiple_connections:
options.update({
"split": "16",
"max-connection-per-server": "16",
"min-split-size": "1M"
})
downloads = self.aria2.add(url, options=options)
self.download = downloads[0]
def pause_download(self):
if self.download:
self.aria2.pause([self.download])
def cancel_download(self):
if self.download:
self.aria2.remove([self.download])
self.download = None
download = self.aria2.get_download(self.download.gid)
def get_download_status(self):
if self.download == None:
return None
response = {
'folderName': download.name,
'fileSize': download.total_length,
'progress': download.completed_length / download.total_length if download.total_length else 0,
'downloadSpeed': download.download_speed,
'numPeers': 0,
'numSeeds': 0,
'status': download.status,
'bytesDownloaded': download.completed_length,
}
download = self.aria2.get_download(self.download.gid)
return response
response = {
'folderName': download.name,
'fileSize': download.total_length,
'progress': download.completed_length / download.total_length if download.total_length else 0,
'downloadSpeed': download.download_speed,
'numPeers': 0,
'numSeeds': 0,
'status': download.status,
'bytesDownloaded': download.completed_length,
}
return response

View File

@@ -147,11 +147,11 @@ def action():
torrent_downloader.start_download(url, data['save_path'])
else:
if existing_downloader and isinstance(existing_downloader, HttpDownloader):
existing_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'))
existing_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'), data.get('allow_multiple_connections', False))
else:
http_downloader = HttpDownloader()
downloads[game_id] = http_downloader
http_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'))
http_downloader.start_download(url, data['save_path'], data.get('header'), data.get('out'), data.get('allow_multiple_connections', False))
downloading_game_id = game_id

View File

@@ -123,3 +123,14 @@ const copyAria2 = () => {
copyAria2();
downloadLudusavi();
if (process.platform !== "win32") {
const binariesPath = path.join(__dirname, "..", "binaries");
if (fs.existsSync(binariesPath)) {
const zzzPath = path.join(binariesPath, "7zz");
const zzzsPath = path.join(binariesPath, "7zzs");
if (fs.existsSync(zzzPath)) fs.chmodSync(zzzPath, 0o755);
if (fs.existsSync(zzzsPath)) fs.chmodSync(zzzsPath, 0o755);
}
}

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": {
@@ -444,9 +472,6 @@
"show_achievements_on_profile": "Show your achievements on your profile",
"show_points_on_profile": "Show your earned points on your profile"
},
"badge": {
"badge_description_theme_creator": "Awarded to those who created a custom theme"
},
"achievement": {
"achievement_unlocked": "Achievement unlocked",
"user_achievements": "{{displayName}}'s Achievements",

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": {
@@ -440,9 +467,6 @@
"show_achievements_on_profile": "Exiba suas conquistas no perfil",
"show_points_on_profile": "Exiba seus pontos ganhos no perfil"
},
"badge": {
"badge_description_theme_creator": "Concedido àqueles que criaram um tema customizado"
},
"achievement": {
"achievement_unlocked": "Conquista desbloqueada",
"your_achievements": "Suas Conquistas",

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,6 +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 "./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,22 @@
import { Badge } from "@types";
import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services";
import { db, levelKeys } from "@main/level";
const getBadges = async (_event: Electron.IpcMainInvokeEvent) => {
const language = await db
.get<string, string>(levelKeys.language, {
valueEncoding: "utf-8",
})
.then((language) => language || "en");
const params = new URLSearchParams({
locale: language,
});
return HydraApi.get<Badge[]>(`/badges?${params.toString()}`, null, {
needsAuth: false,
});
};
registerEvent("getBadges", getBadges);

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,141 +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(levelKeys.language, rest.language);
}
}
})
.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();
};

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

@@ -0,0 +1,74 @@
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) => {
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,18 +2,15 @@ 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",

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,
};
}
@@ -336,6 +371,7 @@ export class DownloadManager {
game_id: downloadId,
url: downloadUrl,
save_path: download.downloadPath,
allow_multiple_connections: true,
};
}
case Downloader.TorBox: {
@@ -348,6 +384,7 @@ export class DownloadManager {
url,
save_path: download.downloadPath,
out: name,
allow_multiple_connections: true,
};
}
}

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) =>
@@ -266,6 +294,9 @@ contextBridge.exposeInMainWorld("electron", {
showItemInFolder: (path: string) =>
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 */
@@ -281,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"),
@@ -294,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) =>
@@ -325,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 () => {
@@ -263,9 +317,7 @@ export function App() {
useEffect(() => {
const unsubscribe = window.electron.onCssInjected((cssString) => {
if (cssString) {
injectCustomCss(cssString);
}
injectCustomCss(cssString);
});
return () => unsubscribe();

View File

@@ -1,29 +0,0 @@
<svg width="240" height="246" viewBox="0 0 240 246" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M117.681 8.44054C120.27 4.75592 125.73 4.75592 128.319 8.44054L149.273 38.2669C151.08 40.8399 154.301 42.0121 157.339 41.203L192.563 31.8236C196.914 30.6649 201.097 34.1747 200.712 38.6612L197.591 74.9784C197.322 78.1113 199.036 81.0795 201.884 82.4128L234.895 97.8691C238.974 99.7785 239.922 105.156 236.743 108.345L211.008 134.16C208.788 136.387 208.193 139.762 209.517 142.614L224.871 175.674C226.767 179.758 224.037 184.486 219.552 184.886L183.245 188.119C180.113 188.398 177.487 190.601 176.669 193.637L167.18 228.832C166.007 233.179 160.876 235.047 157.184 232.47L127.292 211.609C124.714 209.809 121.286 209.809 118.708 211.609L88.8163 232.47C85.1236 235.047 79.9927 233.179 78.8204 228.832L69.3314 193.637C68.5129 190.601 65.8873 188.398 62.7553 188.119L26.4479 184.886C21.9627 184.486 19.2326 179.758 21.1293 175.674L36.4827 142.614C37.8072 139.762 37.212 136.387 34.992 134.16L9.25738 108.345C6.07823 105.156 7.02639 99.7785 11.1045 97.8691L44.1164 82.4128C46.9642 81.0795 48.6778 78.1113 48.4087 74.9784L45.2883 38.6611C44.9028 34.1747 49.0856 30.6649 53.437 31.8236L88.6606 41.203C91.6992 42.0121 94.9198 40.8399 96.7274 38.2669L117.681 8.44054Z" fill="url(#paint0_linear_1378_2496)"/>
<path d="M117.681 8.44054C120.27 4.75592 125.73 4.75592 128.319 8.44054L149.273 38.2669C151.08 40.8399 154.301 42.0121 157.339 41.203L192.563 31.8236C196.914 30.6649 201.097 34.1747 200.712 38.6612L197.591 74.9784C197.322 78.1113 199.036 81.0795 201.884 82.4128L234.895 97.8691C238.974 99.7785 239.922 105.156 236.743 108.345L211.008 134.16C208.788 136.387 208.193 139.762 209.517 142.614L224.871 175.674C226.767 179.758 224.037 184.486 219.552 184.886L183.245 188.119C180.113 188.398 177.487 190.601 176.669 193.637L167.18 228.832C166.007 233.179 160.876 235.047 157.184 232.47L127.292 211.609C124.714 209.809 121.286 209.809 118.708 211.609L88.8163 232.47C85.1236 235.047 79.9927 233.179 78.8204 228.832L69.3314 193.637C68.5129 190.601 65.8873 188.398 62.7553 188.119L26.4479 184.886C21.9627 184.486 19.2326 179.758 21.1293 175.674L36.4827 142.614C37.8072 139.762 37.212 136.387 34.992 134.16L9.25738 108.345C6.07823 105.156 7.02639 99.7785 11.1045 97.8691L44.1164 82.4128C46.9642 81.0795 48.6778 78.1113 48.4087 74.9784L45.2883 38.6611C44.9028 34.1747 49.0856 30.6649 53.437 31.8236L88.6606 41.203C91.6992 42.0121 94.9198 40.8399 96.7274 38.2669L117.681 8.44054Z" fill="url(#paint1_linear_1378_2496)"/>
<path d="M117.681 8.44054C120.27 4.75592 125.73 4.75592 128.319 8.44054L149.273 38.2669C151.08 40.8399 154.301 42.0121 157.339 41.203L192.563 31.8236C196.914 30.6649 201.097 34.1747 200.712 38.6612L197.591 74.9784C197.322 78.1113 199.036 81.0795 201.884 82.4128L234.895 97.8691C238.974 99.7785 239.922 105.156 236.743 108.345L211.008 134.16C208.788 136.387 208.193 139.762 209.517 142.614L224.871 175.674C226.767 179.758 224.037 184.486 219.552 184.886L183.245 188.119C180.113 188.398 177.487 190.601 176.669 193.637L167.18 228.832C166.007 233.179 160.876 235.047 157.184 232.47L127.292 211.609C124.714 209.809 121.286 209.809 118.708 211.609L88.8163 232.47C85.1236 235.047 79.9927 233.179 78.8204 228.832L69.3314 193.637C68.5129 190.601 65.8873 188.398 62.7553 188.119L26.4479 184.886C21.9627 184.486 19.2326 179.758 21.1293 175.674L36.4827 142.614C37.8072 139.762 37.212 136.387 34.992 134.16L9.25738 108.345C6.07823 105.156 7.02639 99.7785 11.1045 97.8691L44.1164 82.4128C46.9642 81.0795 48.6778 78.1113 48.4087 74.9784L45.2883 38.6611C44.9028 34.1747 49.0856 30.6649 53.437 31.8236L88.6606 41.203C91.6992 42.0121 94.9198 40.8399 96.7274 38.2669L117.681 8.44054Z" stroke="url(#paint2_linear_1378_2496)"/>
<g opacity="0.9">
<g style="mix-blend-mode:overlay">
<path d="M113.207 107.103L113.873 107.307L114.295 106.754C120.652 98.4238 129.53 87.9999 139.582 79.6678C149.661 71.3136 160.772 65.1865 171.609 65.1865C172.421 65.1865 173.2 65.5092 173.775 66.0835C174.349 66.6578 174.672 67.4368 174.672 68.249C174.672 79.0868 168.546 90.1972 160.192 100.276C151.861 110.328 141.437 119.207 133.105 125.563L132.551 125.985L132.755 126.651C134.104 131.057 134.402 135.718 133.623 140.26C132.844 144.802 131.011 149.098 128.271 152.803C125.531 156.508 121.96 159.519 117.845 161.594C113.73 163.668 109.186 164.749 104.578 164.749H65.9851C65.3296 164.748 64.6916 164.538 64.1649 164.147C63.6381 163.757 63.2505 163.208 63.059 162.581C62.8674 161.954 62.8821 161.283 63.1008 160.665C63.3195 160.047 63.7307 159.515 64.274 159.149L64.2813 159.144C64.8739 158.736 75.1093 151.439 75.1093 135.28C75.1093 130.672 76.19 126.128 78.2646 122.013C80.3393 117.898 83.35 114.328 87.055 111.587C90.76 108.847 95.056 107.014 99.598 106.235C104.14 105.457 108.801 105.754 113.207 107.103ZM120.25 109.05L119.585 109.911L120.499 110.501C124.047 112.792 127.067 115.811 129.357 119.359L129.948 120.275L130.809 119.608C133.07 117.857 135.204 116.141 137.212 114.46L137.904 113.881L137.399 113.133C134.556 108.926 130.933 105.302 126.725 102.459L125.979 101.955L125.4 102.645C123.714 104.65 121.997 106.785 120.25 109.05ZM141.909 108.878L142.566 109.805L143.402 109.036C161.044 92.7992 166.529 80.4502 168.029 72.9963L168.328 71.5152L166.848 71.8195C159.403 73.3505 147.056 78.8076 130.817 96.4511L130.048 97.2872L130.975 97.9442C135.21 100.946 138.907 104.643 141.909 108.878Z" fill="black"/>
<path d="M113.207 107.103L113.873 107.307L114.295 106.754C120.652 98.4238 129.53 87.9999 139.582 79.6678C149.661 71.3136 160.772 65.1865 171.609 65.1865C172.421 65.1865 173.2 65.5092 173.775 66.0835C174.349 66.6578 174.672 67.4368 174.672 68.249C174.672 79.0868 168.546 90.1972 160.192 100.276C151.861 110.328 141.437 119.207 133.105 125.563L132.551 125.985L132.755 126.651C134.104 131.057 134.402 135.718 133.623 140.26C132.844 144.802 131.011 149.098 128.271 152.803C125.531 156.508 121.96 159.519 117.845 161.594C113.73 163.668 109.186 164.749 104.578 164.749H65.9851C65.3296 164.748 64.6916 164.538 64.1649 164.147C63.6381 163.757 63.2505 163.208 63.059 162.581C62.8674 161.954 62.8821 161.283 63.1008 160.665C63.3195 160.047 63.7307 159.515 64.274 159.149L64.2813 159.144C64.8739 158.736 75.1093 151.439 75.1093 135.28C75.1093 130.672 76.19 126.128 78.2646 122.013C80.3393 117.898 83.35 114.328 87.055 111.587C90.76 108.847 95.056 107.014 99.598 106.235C104.14 105.457 108.801 105.754 113.207 107.103ZM120.25 109.05L119.585 109.911L120.499 110.501C124.047 112.792 127.067 115.811 129.357 119.359L129.948 120.275L130.809 119.608C133.07 117.857 135.204 116.141 137.212 114.46L137.904 113.881L137.399 113.133C134.556 108.926 130.933 105.302 126.725 102.459L125.979 101.955L125.4 102.645C123.714 104.65 121.997 106.785 120.25 109.05ZM141.909 108.878L142.566 109.805L143.402 109.036C161.044 92.7992 166.529 80.4502 168.029 72.9963L168.328 71.5152L166.848 71.8195C159.403 73.3505 147.056 78.8076 130.817 96.4511L130.048 97.2872L130.975 97.9442C135.21 100.946 138.907 104.643 141.909 108.878Z" stroke="url(#paint3_linear_1378_2496)" stroke-width="2"/>
</g>
</g>
<defs>
<linearGradient id="paint0_linear_1378_2496" x1="5.63736e-07" y1="12.92" x2="246" y2="233.08" gradientUnits="userSpaceOnUse">
<stop stop-color="#0CF1CA"/>
<stop offset="1" stop-color="#1DCCEB"/>
</linearGradient>
<linearGradient id="paint1_linear_1378_2496" x1="19.8951" y1="-3.50306e-06" x2="226.105" y2="246" gradientUnits="userSpaceOnUse">
<stop stop-color="#0DDEBB"/>
<stop offset="1" stop-color="#052520"/>
</linearGradient>
<linearGradient id="paint2_linear_1378_2496" x1="-1.9947e-06" y1="18.0561" x2="246" y2="227.944" gradientUnits="userSpaceOnUse">
<stop stop-color="white" stop-opacity="0.7"/>
<stop offset="1" stop-color="white" stop-opacity="0.1"/>
</linearGradient>
<linearGradient id="paint3_linear_1378_2496" x1="61.9253" y1="71.6411" x2="164.664" y2="169.814" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0.2"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 7.8 KiB

View File

@@ -10,6 +10,7 @@
cursor: pointer;
color: globals.$muted-color;
position: relative;
overflow: hidden;
&__image {
height: 100%;

View File

@@ -1,4 +1,5 @@
import { PersonIcon } from "@primer/octicons-react";
import cn from "classnames";
import "./avatar.scss";
@@ -14,11 +15,18 @@ export interface AvatarProps
src?: string | null;
}
export function Avatar({ size, alt, src, ...props }: AvatarProps) {
export function Avatar({ size, alt, src, className, ...props }: AvatarProps) {
return (
<div className="profile-avatar" style={{ width: size, height: size }}>
{src ? (
<img className="profile-avatar__image" alt={alt} src={src} {...props} />
<img
className={cn("profile-avatar__image", className)}
alt={alt}
src={src}
width={size}
height={size}
{...props}
/>
) : (
<PersonIcon size={size * 0.7} />
)}

View File

@@ -6,7 +6,10 @@ export interface BackdropProps {
children: React.ReactNode;
}
export function Backdrop({ isClosing = false, children }: BackdropProps) {
export function Backdrop({
isClosing = false,
children,
}: Readonly<BackdropProps>) {
return (
<div
className={cn("backdrop", {

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

@@ -15,7 +15,7 @@ export function Button({
theme = "primary",
className,
...props
}: ButtonProps) {
}: Readonly<ButtonProps>) {
return (
<button
type="button"

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

@@ -1,6 +1,6 @@
import { darkenColor } from "@renderer/helpers";
import { useAppSelector, useToast } from "@renderer/hooks";
import type { UserProfile, UserStats } from "@types";
import type { Badge, UserProfile, UserStats } from "@types";
import { average } from "color.js";
import { createContext, useCallback, useEffect, useState } from "react";
@@ -16,6 +16,7 @@ export interface UserProfileContext {
getUserProfile: () => Promise<void>;
setSelectedBackgroundImage: React.Dispatch<React.SetStateAction<string>>;
backgroundImage: string;
badges: Badge[];
}
export const DEFAULT_USER_PROFILE_BACKGROUND = "#151515B3";
@@ -28,6 +29,7 @@ export const userProfileContext = createContext<UserProfileContext>({
getUserProfile: async () => {},
setSelectedBackgroundImage: () => {},
backgroundImage: "",
badges: [],
});
const { Provider } = userProfileContext;
@@ -47,6 +49,7 @@ export function UserProfileContextProvider({
const [userStats, setUserStats] = useState<UserStats | null>(null);
const [userProfile, setUserProfile] = useState<UserProfile | null>(null);
const [badges, setBadges] = useState<Badge[]>([]);
const [heroBackground, setHeroBackground] = useState(
DEFAULT_USER_PROFILE_BACKGROUND
);
@@ -101,12 +104,18 @@ export function UserProfileContextProvider({
});
}, [navigate, getUserStats, showErrorToast, userId, t]);
const getBadges = useCallback(async () => {
const badges = await window.electron.getBadges();
setBadges(badges);
}, []);
useEffect(() => {
setUserProfile(null);
setHeroBackground(DEFAULT_USER_PROFILE_BACKGROUND);
getUserProfile();
}, [getUserProfile]);
getBadges();
}, [getUserProfile, getBadges]);
return (
<Provider
@@ -118,6 +127,7 @@ export function UserProfileContextProvider({
setSelectedBackgroundImage,
backgroundImage: getBackgroundImageUrl(),
userStats,
badges,
}}
>
{children}

View File

@@ -30,6 +30,8 @@ import type {
GameRunning,
TorBoxUser,
Theme,
Badge,
Auth,
} from "@types";
import type { AxiosProgressEvent } from "axios";
import type disk from "diskusage";
@@ -86,6 +88,11 @@ declare global {
getDevelopers: () => Promise<string[]>;
/* Library */
toggleAutomaticCloudSync: (
shop: GameShop,
objectId: string,
automaticCloudSync: boolean
) => Promise<void>;
addGameToLibrary: (
shop: GameShop,
objectId: string,
@@ -142,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>
@@ -150,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>;
@@ -217,6 +233,12 @@ declare global {
) => Promise<Electron.OpenDialogReturnValue>;
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 */
@@ -227,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>;
@@ -271,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

@@ -11,6 +11,7 @@ import "@fontsource/noto-sans/500.css";
import "@fontsource/noto-sans/700.css";
import "react-loading-skeleton/dist/skeleton.css";
import "react-tooltip/dist/react-tooltip.css";
import { App } from "./app";
@@ -18,26 +19,17 @@ import { store } from "./store";
import resources from "@locales";
import { SuspenseWrapper } from "./components";
import { logger } from "./logger";
import { addCookieInterceptor } from "./cookies";
const Home = React.lazy(() => import("./pages/home/home"));
const GameDetails = React.lazy(
() => import("./pages/game-details/game-details")
);
const Downloads = React.lazy(() => import("./pages/downloads/downloads"));
const Settings = React.lazy(() => import("./pages/settings/settings"));
const Catalogue = React.lazy(() => import("./pages/catalogue/catalogue"));
const Profile = React.lazy(() => import("./pages/profile/profile"));
const Achievements = React.lazy(
() => import("./pages/achievements/achievements")
);
const ThemeEditor = React.lazy(
() => import("./pages/theme-editor/theme-editor")
);
import * as Sentry from "@sentry/react";
import Catalogue from "./pages/catalogue/catalogue";
import Home from "./pages/home/home";
import Downloads from "./pages/downloads/downloads";
import GameDetails from "./pages/game-details/game-details";
import Settings from "./pages/settings/settings";
import Profile from "./pages/profile/profile";
import Achievements from "./pages/achievements/achievements";
import ThemeEditor from "./pages/theme-editor/theme-editor";
Sentry.init({
dsn: import.meta.env.RENDERER_VITE_SENTRY_DSN,
@@ -82,37 +74,16 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<HashRouter>
<Routes>
<Route element={<App />}>
<Route path="/" element={<SuspenseWrapper Component={Home} />} />
<Route
path="/catalogue"
element={<SuspenseWrapper Component={Catalogue} />}
/>
<Route
path="/downloads"
element={<SuspenseWrapper Component={Downloads} />}
/>
<Route
path="/game/:shop/:objectId"
element={<SuspenseWrapper Component={GameDetails} />}
/>
<Route
path="/settings"
element={<SuspenseWrapper Component={Settings} />}
/>
<Route
path="/profile/:userId"
element={<SuspenseWrapper Component={Profile} />}
/>
<Route
path="/achievements"
element={<SuspenseWrapper Component={Achievements} />}
/>
<Route path="/" element={<Home />} />
<Route path="/catalogue" element={<Catalogue />} />
<Route path="/downloads" element={<Downloads />} />
<Route path="/game/:shop/:objectId" element={<GameDetails />} />
<Route path="/settings" element={<Settings />} />
<Route path="/profile/:userId" element={<Profile />} />
<Route path="/achievements" element={<Achievements />} />
</Route>
<Route
path="/theme-editor"
element={<SuspenseWrapper Component={ThemeEditor} />}
/>
<Route path="/theme-editor" element={<ThemeEditor />} />
</Routes>
</HashRouter>
</Provider>

View File

@@ -31,7 +31,7 @@ $logo-max-width: 200px;
display: flex;
justify-content: center;
width: 100%;
gap: globals.$spacing-unit / 2;
gap: calc(globals.$spacing-unit / 2);
color: globals.$body-color;
cursor: pointer;

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