Compare commits

..

173 Commits

Author SHA1 Message Date
Chubby Granny Chaser
a21f2b9614 Merge pull request #1579 from hydralauncher/feat/ludusavi-upgrade
Feat/ludusavi upgrade
2025-04-18 23:12:48 +01:00
Chubby Granny Chaser
c16cbfe04e Merge branch 'main' into feat/ludusavi-upgrade 2025-04-18 23:09:33 +01:00
Chubby Granny Chaser
4ff41da05a Merge branch 'feat/ludusavi-upgrade' of github.com:hydralauncher/hydra into feat/ludusavi-upgrade 2025-04-18 22:59:56 +01:00
Chubby Granny Chaser
a12e5a15fa chore: bumping version 2025-04-18 22:57:28 +01:00
Zamitto
cc2311529b Merge pull request #1580 from caumeira/patch-1
chore: update create-desktop-shortcuts
2025-04-18 17:04:41 -03:00
Carlos Meira
18b63d735d Update yarn.lock 2025-04-18 16:47:49 -03:00
Zamitto
7379113dc8 chore: update issue template 2025-04-18 12:33:33 -03:00
Carlos Meira
ea85da259c chore: update create-desktop-shortcuts
Fix a issue with bugged icons
2025-04-18 11:27:07 -03:00
Zamitto
b8084d6f67 feat: add await to ensure HydraApi is setup before renderer opens window 2025-04-18 07:22:01 -03:00
Chubby Granny Chaser
97589e63fa Merge branch 'feat/ludusavi-upgrade' of github.com:hydralauncher/hydra into feat/ludusavi-upgrade 2025-04-18 00:07:43 +01:00
Chubby Granny Chaser
2539f67e13 Merge branch 'main' of github.com:hydralauncher/hydra into feat/ludusavi-upgrade 2025-04-18 00:07:36 +01:00
Chubby Granny Chaser
cc4aed2e17 fix: fixing date on backup upload 2025-04-18 00:06:30 +01:00
Chubby Granny Chaser
9ea68e629a Merge branch 'main' into feat/ludusavi-upgrade 2025-04-17 23:48:49 +01:00
Chubby Granny Chaser
8c66b9a499 fix: fixing date on backup upload 2025-04-17 23:44:25 +01:00
Chubby Granny Chaser
8fb183007a Merge pull request #1571 from Lianela/main
Update spanish translation.json
2025-04-17 23:29:23 +01:00
Chubby Granny Chaser
d0ac819da5 feat: upgrading ludusavi 2025-04-17 23:27:22 +01:00
Chubby Granny Chaser
f4e428a574 feat: upgrading ludusavi 2025-04-17 23:23:06 +01:00
Lianela
69879a0303 Update translation.json 2025-04-15 18:29:38 -06:00
Chubby Granny Chaser
f392f9583a feat: adding translation for nimbus 2025-04-15 16:17:09 +01:00
Chubby Granny Chaser
85192f13af feat: adding box shadow 2025-04-15 15:48:58 +01:00
Chubby Granny Chaser
8230229959 feat: adding box shadow 2025-04-15 15:45:51 +01:00
Chubby Granny Chaser
c090a27ff3 chore: removing rust rpc 2025-04-15 15:41:08 +01:00
Zamitto
f2efbfba3a chore: bump version 2025-04-13 13:43:50 -03:00
Zamitto
b7bc5e8cb2 Merge pull request #1560 from hydralauncher/chore/update-deps
chore: update electron-builder and electron-updater
2025-04-13 13:43:22 -03:00
Zamitto
ff447c0ed3 feat: add feature flag check for Nimbus preview on Repack Modal 2025-04-13 13:29:44 -03:00
Zamitto
d1c40b061e chore: remove fail-fast from CI build pipeline 2025-04-13 10:11:50 -03:00
Zamitto
e6a9ea6139 chore: update electron-builder and electron-updater 2025-04-13 09:49:15 -03:00
Zamitto
b519275800 Merge pull request #1559 from hydralauncher/feat/api-requests-optimization
feat: api requests optimization
2025-04-13 09:23:12 -03:00
Zamitto
d56cedb08f Merge branch 'main' into feat/api-requests-optimization 2025-04-13 09:13:49 -03:00
Zamitto
8e8c79185c feat: increase achievements cache to 30 minutes 2025-04-13 09:13:32 -03:00
Zamitto
dd854121f7 Merge pull request #1516 from spydea-tr/translation-tr
[translation] Fixed Turkish translation and added Turkish README
2025-04-13 09:12:28 -03:00
Zamitto
e4776c8e16 Merge branch 'main' into translation-tr 2025-04-13 08:47:38 -03:00
Zamitto
e63738ca7b Merge pull request #1557 from hydrasources/patch-16
Update translation.json
2025-04-13 08:45:54 -03:00
Zamitto
3756249013 Merge branch 'main' into patch-16 2025-04-13 08:30:24 -03:00
Zamitto
33713cfb9f Update src/locales/ru/translation.json
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-04-13 08:30:10 -03:00
Zamitto
41049be522 Merge pull request #1545 from GearCzech/main
Updated czech translation [translation]
2025-04-13 08:29:51 -03:00
Zamitto
d9747b7985 Merge branch 'main' into main 2025-04-13 08:12:40 -03:00
Zamitto
e0c6d2fe3d feat: handle empty response on update achievements 2025-04-13 08:10:52 -03:00
Zamitto
8db03bcccf feat: optimize achievement and debrid request 2025-04-13 07:39:48 -03:00
hydrasources
50acc33a41 Update translation.json 2025-04-13 09:20:44 +03:00
hydrasources
81936d076e Update translation.json 2025-04-13 09:04:47 +03:00
Gear
7d61d1219d added more translations 2025-04-13 00:32:29 +02:00
Chubby Granny Chaser
19613b69cc Merge pull request #1553 from hydralauncher/feat/hydra-debrid
Feat/hydra debrid
2025-04-12 22:42:37 +01:00
Hachi-R
42a768f715 fix: remove etag check from resume validation 2025-04-12 18:41:52 -03:00
Chubby Granny Chaser
456c0ad6ff ci: adding rust build to release 2025-04-12 22:12:11 +01:00
Chubby Granny Chaser
76181342f9 chore: bumping version 2025-04-12 22:10:10 +01:00
Chubby Granny Chaser
8f95fa70d4 chore: merge with main 2025-04-12 21:59:26 +01:00
Chubby Granny Chaser
4743b9f082 Merge pull request #1555 from hydralauncher/fix/download-sources
fix: fixing download sources initial sync
2025-04-12 21:58:48 +01:00
Chubby Granny Chaser
db92ef255d fix: fixing resume 2025-04-12 21:58:07 +01:00
Chubby Granny Chaser
d1cdfc0ba5 fix: improving readability 2025-04-12 21:54:52 +01:00
Chubby Granny Chaser
77a4642b7b feat: importing sources on auth 2025-04-12 21:42:22 +01:00
Chubby Granny Chaser
148e272c4d fix: removing out from start download 2025-04-12 21:34:40 +01:00
Chubby Granny Chaser
3bdd8b90d4 chore: merge with main 2025-04-12 21:27:36 +01:00
Chubby Granny Chaser
6569b66801 chore: merge with main 2025-04-12 21:27:03 +01:00
Chubby Granny Chaser
4a11d741eb feat: limiting nimbus to cloud only 2025-04-12 21:24:06 +01:00
Chubby Granny Chaser
6e8a844a92 Merge branch 'main' of github.com:hydralauncher/hydra into feat/hydra-debrid 2025-04-12 21:19:54 +01:00
Chubby Granny Chaser
3821b9836c Merge pull request #1554 from hydralauncher/ci/rust-rpc
feat: adding rust codebase
2025-04-12 21:19:44 +01:00
Chubby Granny Chaser
57390c814b Merge branch 'ci/rust-rpc' of github.com:hydralauncher/hydra into feat/hydra-debrid 2025-04-12 20:27:21 +01:00
Hachi-R
007fa6f009 fix: add connections limit parameter to http downloader 2025-04-12 16:25:45 -03:00
Chubby Granny Chaser
009cb1d7d7 Merge branch 'ci/rust-rpc' of github.com:hydralauncher/hydra into feat/hydra-debrid 2025-04-12 19:58:27 +01:00
Chubby Granny Chaser
306b49eaf3 fix: merge with ci 2025-04-12 19:58:18 +01:00
Hachi-R
be232d88e4 fix: handle exception in http downloader by returning None 2025-04-12 15:55:59 -03:00
Hachi-R
e3670f5b5a fix: add force download flag in httpdl args 2025-04-12 15:54:45 -03:00
Hachi-R
bd018399fb fix: typo 2025-04-12 15:52:18 -03:00
Hachi-R
975eec96be feat: add force download option to http downloader 2025-04-12 15:42:02 -03:00
Chubby Granny Chaser
44b711f674 fix: fixing download sources initial sync 2025-04-12 18:47:33 +01:00
Chubby Granny Chaser
539ff34b69 fix: fixing download sources initial sync 2025-04-12 18:39:43 +01:00
Hachi-R
f99da1d7bf Merge branch 'ci/rust-rpc' of https://github.com/hydralauncher/hydra into ci/rust-rpc 2025-04-12 14:23:13 -03:00
Hachi-R
75c3bbf858 feat: add option to show download speed in megabits 2025-04-12 14:23:02 -03:00
Chubby Granny Chaser
afa78e4634 feat: removing aria2 2025-04-12 18:16:16 +01:00
Chubby Granny Chaser
ee1dda90d9 ci: building rust on dev 2025-04-12 18:00:20 +01:00
Hachi-R
5b62b9c593 feat: add option to show download speed in megabits 2025-04-11 17:09:33 -03:00
Hachi-R
4d76182f2e feat: add support for custom http headers in downloader 2025-04-11 15:37:51 -03:00
Chubby Granny Chaser
85fb57527a ci: adding artifacts 2025-04-11 18:33:14 +01:00
Hachi-R
9e6b6be0b9 feat: add final log 2025-04-11 14:27:27 -03:00
Hachi-R
3c3f77fc50 fix: adjust chunk size and connection limits in http downloader 2025-04-11 14:19:57 -03:00
Chubby Granny Chaser
614cb8a297 Merge branch 'ci/rust-rpc' of github.com:hydralauncher/hydra into ci/rust-rpc 2025-04-11 18:08:18 +01:00
Chubby Granny Chaser
ba3f010576 ci: adding electron builder for http 2025-04-11 18:06:02 +01:00
Hachi-R
8c442e742a fix: add range request support validation 2025-04-11 14:02:06 -03:00
Hachi-R
555b3dbb1d fix: improve file rename handling 2025-04-11 14:00:22 -03:00
Hachi-R
d2a868b504 fix: update retry backoff 2025-04-11 13:51:32 -03:00
Hachi-R
e27536c6b3 feat: chunks vector allocation 2025-04-11 13:49:16 -03:00
Hachi-R
cd367faec2 fix: oneshot channel 2025-04-11 13:46:41 -03:00
Chubby Granny Chaser
087dd9fb2e feat: adding rust codebase 2025-04-11 17:27:33 +01:00
Hachi-R
c5d8403843 fix: update binary path for hydra-httpdl executable 2025-04-10 17:17:20 -03:00
Hachi-R
8e01142225 feat: pass hydra-httpdl binary path to HttpDownloader 2025-04-10 16:40:40 -03:00
Hachi-R
a0ef59a13c fix: correct path separator for hydra-httpdl executable 2025-04-10 16:34:13 -03:00
Hachi-R
13d5e4469f feat: add hydra-httpdl executable to extra resources 2025-04-10 16:09:55 -03:00
Hachi-R
22e92eb8f6 feat: update download speed formatting to Mbps 2025-04-10 16:09:41 -03:00
Hachi-R
d28bb825a3 feat: add hydra-httpdl executable 2025-04-10 15:44:24 -03:00
Hachi-R
96d59a0fd7 fix: improve game folder deletion logic 2025-04-10 15:43:55 -03:00
Hachi-R
84600ea0dc feat: implement hydra-httpdl for download management 2025-04-10 15:43:38 -03:00
Chubby Granny Chaser
9264fa3664 fix: vibe coding 2025-04-09 17:10:57 +01:00
Chubby Granny Chaser
5b0ea980de fix: vibe coding 2025-04-09 17:07:45 +01:00
Chubby Granny Chaser
622fc393fc fix: vibe coding 2025-04-09 17:06:35 +01:00
Chubby Granny Chaser
f76ba5975d fix: tweaking download options 2025-04-09 16:06:01 +01:00
Chubby Granny Chaser
4da0dac0e6 feat: adding hydra debrid 2025-04-09 16:02:50 +01:00
Hachi-R
7c468ac9bb fix: remove allow_multiple_connections from download method 2025-04-09 11:29:12 -03:00
Hachi-R
2ee3bebfc7 fix: remove allow_multiple_connections from download options 2025-04-09 11:20:42 -03:00
Chubby Granny Chaser
98ed07d6d2 feat: adding hydra debrid 2025-04-09 13:02:22 +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
Spydea
a4dd037cba Update translation.json 2025-04-08 19:05:34 +03:00
Spydea
30be12afeb Update translation.json 2025-04-08 18:59:35 +03:00
Spydea
258f891a12 Add files via upload 2025-04-08 18:58:18 +03:00
Spydea
02662fa993 Merge branch 'hydralauncher:main' into translation-tr 2025-04-08 18:57:52 +03:00
Gear
1746f14adb Updated czech translation 2025-04-07 17:25:37 +02: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
Spydea
554d195d5b Update src/locales/tr/translation.json
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-03-18 19:38:15 +03:00
Spydea
fc899197b4 Update src/locales/tr/translation.json
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-03-18 19:38:08 +03:00
Spydea
7597933aa9 Update translation.json 2025-03-18 19:23:34 +03:00
Spydea
3a36bfb75c Update translation.json 2025-03-18 19:17:22 +03:00
Spydea
93adb070e5 Add files via upload 2025-03-18 19:10:35 +03:00
Spydea
9df58b9918 Merge branch 'hydralauncher:main' into translation-tr 2025-03-18 19:10:09 +03:00
Spydea
0e164acdf2 Update docs/README.et.md
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-03-15 22:02:14 +03:00
Spydea
fffa2d2c01 Update README.md
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-03-15 22:02:02 +03:00
Spydea
2c35d7da13 Update docs/README.tr.md
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-03-15 22:01:39 +03:00
Spydea
9a81a45738 Update README.tr.md 2025-03-15 22:01:17 +03:00
Spydea
d284fc914e Update docs/README.tr.md
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-03-15 22:00:20 +03:00
Spydea
b867eaa34b Update README.tr.md 2025-03-15 21:15:02 +03:00
Spydea
f353d1fc9b Update README.tr.md 2025-03-15 21:14:35 +03:00
Spydea
b03d2a78f8 Add files via upload 2025-03-15 21:14:08 +03:00
Spydea
1a6cb5f866 Update README.uk-UA.md 2025-03-15 21:13:34 +03:00
Spydea
fbc81db2d0 Update README.ru.md 2025-03-15 21:13:26 +03:00
Spydea
663e5a46ba Update README.pt-BR.md 2025-03-15 21:13:15 +03:00
Spydea
bc07f73387 Update README.pl.md 2025-03-15 21:13:01 +03:00
Spydea
a697d4ec8b Update README.nb.md 2025-03-15 21:12:47 +03:00
Spydea
008b88f888 Update README.it.md 2025-03-15 21:12:35 +03:00
Spydea
0e7e5ba920 The Turkish README has been added 2025-03-15 21:12:13 +03:00
Spydea
7c98f4afa7 Update README.fr.md 2025-03-15 21:11:55 +03:00
Spydea
12d6b744d7 The Turkish README has been added 2025-03-15 21:11:41 +03:00
Spydea
ff7c15a013 The Turkish README has been added 2025-03-15 21:11:28 +03:00
Spydea
16d192443f The Turkish README has been added 2025-03-15 21:11:15 +03:00
Spydea
0c6b3f2254 The Turkish README has been added 2025-03-15 21:10:55 +03:00
Spydea
f92d6839c8 The Turkish README has been added 2025-03-15 21:10:32 +03:00
Spydea
d3e383bc1b The Turkish README has been added 2025-03-15 21:10:15 +03:00
Spydea
abec986cc1 The Turkish README has been added 2025-03-15 21:09:51 +03:00
Spydea
135eef986a The Turkish README has been added 2025-03-15 21:07:20 +03:00
107 changed files with 1943 additions and 935 deletions

View File

@@ -33,9 +33,9 @@ body:
attributes:
label: Additional information and data
description: |
Add screenshots and upload your logs file here.
Logs location on Windows: "%appdata%/hydra"
Logs location on Linux: "~/.config/hydra/"
Add screenshots and upload your all logs file here.
Logs location on Windows: "%appdata%/hydralauncher/logs"
Logs location on Linux: "~/.config/hydralauncher/logs"
validations:
required: true
- type: input

View File

@@ -11,6 +11,7 @@ jobs:
strategy:
matrix:
os: [windows-latest, ubuntu-latest]
fail-fast: false
runs-on: ${{ matrix.os }}
@@ -24,7 +25,7 @@ jobs:
node-version: 20.18.0
- name: Install dependencies
run: yarn
run: yarn --frozen-lockfile
- name: Install Python
uses: actions/setup-python@v5
@@ -80,7 +81,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

@@ -20,7 +20,7 @@ jobs:
node-version: 20.18.0
- name: Install dependencies
run: yarn
run: yarn --frozen-lockfile
- name: Validate current commit (last commit) with commitlint
run: npx commitlint --last --verbose

View File

@@ -26,7 +26,7 @@ jobs:
node-version: 20.18.0
- name: Install dependencies
run: yarn
run: yarn --frozen-lockfile
- name: Install Python
uses: actions/setup-python@v5
@@ -88,6 +88,18 @@ jobs:
dist/*.blockmap
dist/*.pacman
- name: Upload build
env:
BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
S3_ENDPOINT: ${{ secrets.S3_ENDPOINT }}
S3_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }}
S3_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }}
S3_BUILDS_BUCKET_NAME: ${{ secrets.S3_BUILDS_BUCKET_NAME }}
BUILDS_URL: ${{ secrets.BUILDS_URL }}
BUILD_WEBHOOK_URL: ${{ secrets.BUILD_WEBHOOK_URL }}
GITHUB_ACTOR: ${{ github.actor }}
run: node scripts/upload-build.cjs
- name: Release
uses: softprops/action-gh-release@v2
with:

3
.gitignore vendored
View File

@@ -9,10 +9,11 @@ out
.vite
ludusavi/
hydra-python-rpc/
aria2/
.python-version
# Sentry Config File
.env.sentry-build-plugin
*storybook.log
aria2/

View File

@@ -25,7 +25,8 @@
[![cs](https://img.shields.io/badge/lang-cs-purple)](./docs/README.cs.md)
[![da](https://img.shields.io/badge/lang-da-red)](./docs/README.da.md)
[![nb](https://img.shields.io/badge/lang-nb-blue)](./docs/README.nb.md)
[![ee](https://img.shields.io/badge/lang-et-blue.svg)](./docs/README.et.md)
[![et](https://img.shields.io/badge/lang-et-blue.svg)](./docs/README.et.md)
[![tr](https://img.shields.io/badge/lang-tr-red.svg)](./docs/README.tr.md)
![Hydra Catalogue](./docs/screenshot.png)

BIN
binaries/7z.dll Normal file

Binary file not shown.

BIN
binaries/7z.exe Normal file

Binary file not shown.

BIN
binaries/7zz Executable file

Binary file not shown.

BIN
binaries/7zzs Executable file

Binary file not shown.

View File

@@ -26,6 +26,7 @@
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
[![et](https://img.shields.io/badge/lang-et-blue.svg)](README.et.md)
[![tr](https://img.shields.io/badge/lang-tr-red.svg)](README.tr.md)
![Hydra Catalogue](screenshot.png)

View File

@@ -26,6 +26,7 @@
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
[![et](https://img.shields.io/badge/lang-et-blue.svg)](README.et.md)
[![tr](https://img.shields.io/badge/lang-tr-red.svg)](README.tr.md)
![Hydra Katalog](screenshot.png)

View File

@@ -25,6 +25,7 @@
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
[![et](https://img.shields.io/badge/lang-et-blue.svg)](README.et.md)
[![tr](https://img.shields.io/badge/lang-tr-red.svg)](README.tr.md)
![Hydra Catalogue](screenshot.png)

View File

@@ -26,6 +26,7 @@
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
[![et](https://img.shields.io/badge/lang-et-blue.svg)](README.et.md)
[![tr](https://img.shields.io/badge/lang-tr-red.svg)](README.tr.md)
![Hydra Katalog](screenshot.png)

View File

@@ -26,6 +26,7 @@
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
[![et](https://img.shields.io/badge/lang-et-blue.svg)](README.et.md)
[![tr](https://img.shields.io/badge/lang-tr-red.svg)](README.tr.md)
![Hydra Catalogue](screenshot.png)

View File

@@ -23,7 +23,8 @@
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
[![ee](https://img.shields.io/badge/lang-et-blue.svg)](README.et.md)
[![et](https://img.shields.io/badge/lang-et-blue.svg)](README.et.md)
[![tr](https://img.shields.io/badge/lang-tr-red.svg)](README.tr.md)
![Hydra Kataloog](screenshot.png)

View File

@@ -26,6 +26,7 @@
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
[![et](https://img.shields.io/badge/lang-et-blue.svg)](README.et.md)
[![tr](https://img.shields.io/badge/lang-tr-red.svg)](README.tr.md)
![Catalogue Hydra](screenshot.png)

View File

@@ -26,6 +26,7 @@
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
[![et](https://img.shields.io/badge/lang-et-blue.svg)](README.et.md)
[![tr](https://img.shields.io/badge/lang-tr-red.svg)](README.tr.md)
![Hydra Catalogue](./screenshot.png)

View File

@@ -25,6 +25,7 @@
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
[![et](https://img.shields.io/badge/lang-et-blue.svg)](README.et.md)
[![tr](https://img.shields.io/badge/lang-tr-red.svg)](README.tr.md)
![Hydra Catalogue](screenshot.png)

View File

@@ -26,6 +26,7 @@
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
[![et](https://img.shields.io/badge/lang-et-blue.svg)](README.et.md)
[![tr](https://img.shields.io/badge/lang-tr-red.svg)](README.tr.md)
![Hydra Catalogue](./screenshot.png)

View File

@@ -26,6 +26,7 @@
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
[![et](https://img.shields.io/badge/lang-et-blue.svg)](README.et.md)
[![tr](https://img.shields.io/badge/lang-tr-red.svg)](README.tr.md)
![Hydra Catalogue](./screenshot.png)

View File

@@ -26,6 +26,7 @@
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
[![et](https://img.shields.io/badge/lang-et-blue.svg)](README.et.md)
[![tr](https://img.shields.io/badge/lang-tr-red.svg)](README.tr.md)
![Hydra Catalogue](./screenshot.png)

194
docs/README.tr.md Normal file
View File

@@ -0,0 +1,194 @@
<br>
<div align="center">
[<img src="../resources/icon.png" width="144"/>](https://help.hydralauncher.gg)
<h1 align="center">Hydra Launcher</h1>
<p align="center">
<strong>Hydra, kendi gömülü BitTorrent istemcisine sahip bir oyun başlatıcısıdır.</strong>
</p>
[![build](https://img.shields.io/github/actions/workflow/status/hydralauncher/hydra/build.yml)](https://github.com/hydralauncher/hydra/actions)
[![release](https://img.shields.io/github/package-json/v/hydralauncher/hydra)](https://github.com/hydralauncher/hydra/releases)
[![pt-BR](https://img.shields.io/badge/lang-pt--BR-green.svg)](README.pt-BR.md)
[![en](https://img.shields.io/badge/lang-en-red.svg)](../README.md)
[![ru](https://img.shields.io/badge/lang-ru-yellow.svg)](README.ru.md)
[![uk-UA](https://img.shields.io/badge/lang-uk--UA-blue)](README.uk-UA.md)
[![be](https://img.shields.io/badge/lang-be-orange)](README.be.md)
[![es](https://img.shields.io/badge/lang-es-red)](README.es.md)
[![fr](https://img.shields.io/badge/lang-fr-blue)](README.fr.md)
[![de](https://img.shields.io/badge/lang-de-black)](README.de.md)
[![ita](https://img.shields.io/badge/lang-it-red)](README.it.md)
[![cs](https://img.shields.io/badge/lang-cs-purple)](README.cs.md)
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
[![et](https://img.shields.io/badge/lang-et-blue.svg)](README.et.md)
[![tr](https://img.shields.io/badge/lang-tr-red.svg)](README.tr.md)
![Hydra Catalogue](screenshot.png)
</div>
## <a name="içindekiler"></a> İçindekiler
- [İçindekiler](#içindekiler)
- [Hakkında](#hakkında)
- [Özellikler](#özellikler)
- [Kurulum](#kurulum)
- [Katkıda bulunma](#katkıda-bulunma)
- [Telegram grubumuza katılın](#telegram-katıl)
- [Repoyu forklayın ve klonlayın](#repo-fork-klon)
- [Katkıda bulunabileceğin yollar](#katkı-yolları)
- [Proje yapısı](#proje-yapısı)
- [Kaynak kodundan derleme](#kaynak-kodundan-derleme)
- [Node.js'i yükleme](#nodejs-yükle)
- [Yarn'ı yükleme](#yarn-yükle)
- [Node bağımlılıklarını yükleme](#node-bağımlılık-yükle)
- [OpenSSL 1.1'i yükleme](#openssl-1-1-yükle)
- [Python 3.9'u yükleme](#python-3-9-yükle)
- [Python bağımlılıklarını yükleme](#python-bağımlılık-yükle)
- [Ortam değişkenleri](#ortam-değişkenleri)
- [Çalıştırma](#çalıştırma)
- [Derleme](#derleme)
- [BitTorrent istemcisini derleme](#bittorrent-istemci-derle)
- [Electron uygulamasını derleme](#electron-uygulama-derle)
- [Katkıda bulunanlar](#katkıda-bulunanlar)
- [Lisans](#lisans)
## <a name="hakkında"></a> Hakkında
**Hydra**, kendi gömülü **BitTorrent istemci**sine sahip bir **oyun başlatıcısı**dır.
<br>
Başlatıcı, torrent sistemini libtorrent kullanarak yöneten Python ve TypeScript (Electron) ile yazılmıştır.
## <a name="özellikler"></a> Özellikler
- Kendi gömülü BitTorrent istemcisi
- Oyun sayfasında How Long To Beat (HLTB) entegrasyonu
- İndirme yolu özelleştirmesi
- Windows ve Linux desteği
- Sürekli güncelleme
- Ve daha fazlası...
## <a name="kurulum"></a> Kurulum
Aşağıdaki adımları izleyerek Hydra'yı kurun:
1. Hydra'nın en son sürümünü [Releases](https://github.com/hydralauncher/hydra/releases/latest) sayfasından indirin.
- Hydra'yı Windows'a kurmak istiyorsanız sadece .exe dosyasını indirin.
- Hydra'yı Linux'a kurmak istiyorsanız .deb, .rpm veya .zip dosyasını indirin (kullandığınız Linux dağıtımına bağlı olarak).
2. İndirilen dosyayı çalıştırın.
3. Hydra'nın keyfini çıkarın!
## <a name="katkıda-bulunma"></a> Katkıda Bulunma
### <a name="telegram-katıl"></a> Telegram grubumuza katılın
Tartışmalarımızı [Telegram](https://t.me/hydralauncher) kanalımız üzerinde yürütüyoruz.
### <a name="repo-fork-klon"></a> Repoyu forklayın ve klonlayın
1. Depoyu fork'layın [(şimdi forklamak için tıklayın)](https://github.com/hydralauncher/hydra/fork)
2. Forkladığınız kodu klonlayın `git clone https://github.com/kullanıcı_adınız/hydra`
3. Yeni bir branch oluşturun
4. Commitlerinizi gönderin (push)
5. Yeni bir Pull Request gönderin
### <a name="katkı-yolları"></a> Katkıda bulunabileceğin yollar
- Çeviri: Hydra'nın mümkün olduğunca fazla kişiye ulaşmasını istiyoruz. Yeni dillere çeviri yapmak ya da mevcut dillere güncelleme ve iyileştirme yapmak için yardımcı olmaktan çekinmeyin.
- Kod: Hydra, Typescript, Electron ve biraz Python ile inşa edilmiştir. Katkıda bulunmak isterseniz, [Telegram](https://t.me/hydralauncher) kanalımıza katılın!
### <a name="proje-yapısı"></a> Proje yapısı
- torrent-client: Torrent indirmelerini yönetmek için libtorrent adlı bir Python kütüphanesini kullanıyoruz.
- src/renderer: Uygulamanın kullanıcı arayüzü burada bulunur.
- src/main: Uygulamanın tüm işleyişi ve iş mantığı bu bölümde bulunur.
## <a name="kaynak-kodundan-derleme"></a> Kaynak kodundan derleme
### <a name="nodejs-yükle"></a> Node.js'i yükleme
Makinenizde Node.js'in yüklü olduğundan emin olun. Yüklü değilse, [nodejs.org](https://nodejs.org/) adresinden indirip kurun.
### <a name="yarn-yükle"></a> Yarn'ı yükleme
Yarn, Node.js için bir paket yöneticisidir. Eğer Yarn'ı henüz kurmadıysanız, [yarnpkg.com](https://classic.yarnpkg.com/lang/en/docs/install/) adresindeki talimatları izleyerek kurabilirsiniz.
### <a name="node-bağımlılık-yükle"></a> Node bağımlılıklarını yükleme
Proje dizinine gidin ve Yarn kullanarak Node bağımlılıklarını yükleyin:
```bash
cd hydra
yarn
```
### <a name="openssl-1-1-yükle"></a> OpenSSL 1.1'i yükleme
Windows ortamlarında libtorrent tarafından gerekli olan [OpenSSL 1.1](https://slproweb.com/download/Win64OpenSSL-1_1_1w.exe)'i indirip yükleyin.
### <a name="python-3-9-yükle"></a> Python 3.9'u yükleme
Makinenizde Python 3.9'un yüklü olduğundan emin olun. Bunu [python.org](https://www.python.org/downloads/release/python-3913/) adresinden indirip kurarak yapabilirsiniz.
### <a name="python-bağımlılık-yükle"></a> Python bağımlılıklarını yükleme
Gerekli Python bağımlılıklarını pip kullanarak yükleyin:
```bash
pip install -r requirements.txt
```
## <a name="ortam-değişkenleri"></a> Ortam değişkenleri
Oyun simgelerini yüklemek için bir SteamGridDB API Anahtarına ihtiyacınız olacak.
Bu anahtara sahip olduktan sonra, `.env.example` dosyasını kopyalayabilir veya adını `.env` olarak değiştirebilir ve `STEAMGRIDDB_API_KEY` değerini buraya ekleyebilirsiniz.
## <a name="çalıştırma"></a> Çalıştırma
Tüm ayarları tamamladıktan sonra, hem Electron sürecini hem de bittorrent istemcisini başlatmak için aşağıdaki komutu çalıştırabilirsiniz:
```bash
yarn dev
```
## <a name="derleme"></a> Derleme
### <a name="bittorrent-istemci-derle"></a> BitTorrent istemcisini derleme
Bittorrent istemcisini aşağıdaki komutla derleyin:
```bash
python torrent-client/setup.py build
```
### <a name="electron-uygulama-derle"></a> Electron uygulamasını derleme
Electron uygulamasını aşağıdaki komutlarla derleyebilirsiniz:
Windows'ta:
```bash
yarn build:win
```
Linux'ta:
```bash
yarn build:linux
```
## <a name="katkıda-bulunanlar"></a> Katkıda bulunanlar
<a href="https://github.com/hydralauncher/hydra/graphs/contributors">
<img src="https://contrib.rocks/image?repo=hydralauncher/hydra" />
</a>
## <a name="lisans"></a> Lisans
Hydra, [MIT Lisansı](https://github.com/hydralauncher/hydra/blob/main/LICENSE) altında lisanlanmıştır.

View File

@@ -26,6 +26,7 @@
[![da](https://img.shields.io/badge/lang-da-red)](README.da.md)
[![nb](https://img.shields.io/badge/lang-nb-blue)](README.nb.md)
[![et](https://img.shields.io/badge/lang-et-blue.svg)](README.et.md)
[![tr](https://img.shields.io/badge/lang-tr-red.svg)](README.tr.md)
![Hydra Catalogue](screenshot.png)

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.3.1",
"version": "3.4.4",
"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",
@@ -45,24 +44,21 @@
"auto-launch": "^5.0.6",
"axios": "^1.7.9",
"axios-cookiejar-support": "^5.0.5",
"better-sqlite3": "^11.7.0",
"classic-level": "^2.0.0",
"classnames": "^2.5.1",
"color": "^4.2.3",
"color.js": "^1.2.0",
"create-desktop-shortcuts": "^1.11.0",
"create-desktop-shortcuts": "^1.11.1",
"date-fns": "^3.6.0",
"dexie": "^4.0.10",
"diskusage": "^1.2.0",
"electron-log": "^5.2.4",
"electron-updater": "^6.3.9",
"electron-updater": "^6.6.2",
"file-type": "^19.6.0",
"i18next": "^23.11.2",
"i18next-browser-languagedetector": "^7.2.1",
"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",
@@ -103,7 +99,7 @@
"@types/user-agents": "^1.0.4",
"@vitejs/plugin-react": "^4.2.1",
"electron": "^31.7.7",
"electron-builder": "^25.1.8",
"electron-builder": "^26.0.12",
"electron-vite": "^2.3.0",
"eslint": "^8.56.0",
"eslint-plugin-jsx-a11y": "^6.10.2",

View File

@@ -35,7 +35,7 @@ if start_download_payload:
http_downloader = HttpDownloader()
downloads[initial_download['game_id']] = http_downloader
try:
http_downloader.start_download(initial_download['url'], initial_download['save_path'], initial_download.get('header'), initial_download.get("out"))
http_downloader.start_download(initial_download['url'], initial_download['save_path'], initial_download.get('header'), initial_download.get('out'))
except Exception as e:
print("Error starting http download", e)
@@ -182,4 +182,3 @@ def action():
if __name__ == "__main__":
app.run(host="0.0.0.0", port=int(http_port))

View File

@@ -1,4 +1,5 @@
const { default: axios } = require("axios");
const tar = require("tar");
const util = require("node:util");
const fs = require("node:fs");
const path = require("node:path");
@@ -6,10 +7,12 @@ const { spawnSync } = require("node:child_process");
const exec = util.promisify(require("node:child_process").exec);
const ludusaviVersion = "0.29.0";
const fileName = {
win32: "ludusavi-v0.25.0-win64.zip",
linux: "ludusavi-v0.25.0-linux.zip",
darwin: "ludusavi-v0.25.0-mac.zip",
win32: `ludusavi-v${ludusaviVersion}-win64.zip`,
linux: `ludusavi-v${ludusaviVersion}-linux.tar.gz`,
darwin: `ludusavi-v${ludusaviVersion}-mac.tar.gz`,
};
const downloadLudusavi = async () => {
@@ -19,7 +22,7 @@ const downloadLudusavi = async () => {
}
const file = fileName[process.platform];
const downloadUrl = `https://github.com/mtkennerly/ludusavi/releases/download/v0.25.0/${file}`;
const downloadUrl = `https://github.com/mtkennerly/ludusavi/releases/download/v${ludusaviVersion}/${file}`;
console.log(`Downloading ${file}...`);
@@ -31,10 +34,18 @@ const downloadLudusavi = async () => {
console.log(`Downloaded ${file}, extracting...`);
const pwd = process.cwd();
const targetPath = path.join(pwd, "ludusavi");
await exec(`npx extract-zip ${file} ${targetPath}`);
await fs.promises.mkdir(targetPath, { recursive: true });
if (process.platform === "win32") {
await exec(`npx extract-zip ${file} ${targetPath}`);
} else {
await tar.x({
file: file,
cwd: targetPath,
});
}
if (process.platform !== "win32") {
fs.chmodSync(path.join(targetPath, "ludusavi"), 0o755);

View File

@@ -188,8 +188,8 @@
"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_on_real_debrid": "هذا التنزيل غير متوفر على Real-Debrid وجلب حالة التنزيل من Real-Debrid غير متاح حاليًا.",
"download_error_not_cached_on_torbox": "هذا التنزيل غير متوفر على TorBox وجلب حالة التنزيل من TorBox غير متاح حاليًا.",
"game_removed_from_favorites": "تمت إزالة اللعبة من المفضلة",
"game_added_to_favorites": "تمت إضافة اللعبة إلى المفضلة"
},
@@ -330,7 +330,7 @@
"delete_theme_description": "سيؤدي هذا إلى حذف السمة {{theme}}",
"cancel": "إلغاء",
"appearance": "المظهر",
"enable_torbox": "تفعيل Torbox",
"enable_torbox": "تفعيل TorBox",
"torbox_description": "TorBox هي خدمة seedbox متميزة تنافس أفضل الخوادم في السوق.",
"torbox_account_linked": "تم ربط حساب TorBox",
"real_debrid_account_linked": "تم ربط حساب Real-Debrid",

View File

@@ -44,11 +44,21 @@
"downloading_metadata": "Stahuji metadata: {{title}}…",
"downloading": "Stahuji {{title}}… ({{percentage}} staženo) - Odhadovaný čas {{eta}} - {{speed}}",
"calculating_eta": "Stahuji {{title}}… ({{percentage}} staženo) - Počítám zbývající čas…",
"checking_files": "Kontroluji soubory: {{title}}… ({{percentage}} ověřeno)"
"checking_files": "Kontroluji soubory: {{title}}… ({{percentage}} ověřeno)",
"installing_common_redist": "{{log}}…",
"installation_complete": "Instalace dokončena",
"installation_complete_message": "Běžné redistribuovatelné komponenty byly úspěšně nainstalovány"
},
"catalogue": {
"next_page": "Další strana",
"previous_page": "Předchozí strana"
"search": "Filtr…",
"developers": "Vývojáři",
"genres": "Žánry",
"tags": "Tagy",
"publishers": "Vydavatelé",
"download_sources": "Zdroje stahování",
"result_count": "Výsledky: {{resultCount}}",
"filter_count": "Dostupné: {{filterCount}}",
"clear_filters": "Vyčistit vybrané filtry: {{filterCount}}"
},
"game_details": {
"open_download_options": "Otevřít možnosti stahování",
@@ -107,6 +117,7 @@
"open_download_location": "Zobrazit stažené soubory",
"create_shortcut": "Vytvořit zástupce na ploše",
"remove_files": "Odebrat soubory",
"clear": "Vyčistit",
"remove_from_library_title": "Jste si jisti?",
"remove_from_library_description": "Tohle odstraní {{game}} z vaší knihovny",
"options": "Možnosti",
@@ -160,6 +171,9 @@
"loading_save_preview": "Hledání uložených her...",
"wine_prefix": "Wine Prefix",
"wine_prefix_description": "Wine Prefix použit pro spuštění této hry",
"launch_options": "Možnosti spuštění",
"launch_options_description": "Pokročilí uživatelé mohou zadat speciální parametry spuštění (experimentální funkce)",
"launch_options_placeholder": "Žádné parametery nebyly specifikovány",
"no_download_option_info": "Žádné informace nejsou dostupny",
"backup_deletion_failed": "Nepovedlo se odstranit zálohu",
"max_number_of_artifacts_reached": "Dosáhli jste maximálního počtu záloh pro tuto hru",
@@ -167,7 +181,23 @@
"manage_files_description": "Spravovat, které soubory budou zálohovány a obnoveny",
"select_folder": "Vybrat složku",
"backup_from": "Zálohy z {{date}}",
"custom_backup_location_set": "Vlastní umístění záloh nastaveno"
"custom_backup_location_set": "Vlastní umístění záloh nastaveno",
"automatic_backup_from": "Automatická záloha z {{date}}",
"enable_automatic_cloud_sync": "Povolit automatické zálohy v cloudu",
"no_directory_selected": "Žádná složka není zvolena",
"no_write_permission": "Nemohu stahovat do této složky. Klikni sem pro více informací.",
"reset_achievements": "Resetovat achievementy",
"reset_achievements_description": "Toto zresetuje všechny achievementy pro hru {{game}}",
"reset_achievements_title": "Jste si jisti?",
"reset_achievements_success": "Achievementy úspěšně resetovány",
"reset_achievements_error": "Nepodařilo se resetovat achievementy",
"download_error_gofile_quota_exceeded": "Překročili jste vaši měsíční GoFile kvótu. Prosím vyčkejte na resetování kvóty.",
"download_error_real_debrid_account_not_authorized": "Váš Real-Debrid účet není autorizován pro vytváření nových stahování. Prosím zkontrolujte nastavení vašeho účtu a zkuste to znovu.",
"download_error_not_cached_on_real_debrid": "Toto stahování není dostupné na Real-Debrid a získávání informací o stahování z Real-Debrid není zatím dostupné.",
"download_error_not_cached_on_torbox": "Toto stahování není dostupné na TorBox a získávání informací o stahování z TorBox není zatím dostupné.",
"game_removed_from_favorites": "Hra odebrána z oblíbených",
"game_added_to_favorites": "Hra přidána do oblíbených",
"automatically_extract_downloaded_files": "Automaticky rozbalit stažené soubory"
},
"activation": {
"title": "Aktivovat hydru",
@@ -200,7 +230,13 @@
"queued": "V řadě",
"no_downloads_title": "Prázdno..",
"no_downloads_description": "Ještě jsi zatím nic nestáhl přes Hydru, ale furt není pozdě začít.",
"checking_files": "Kontroluji soubory…"
"checking_files": "Kontroluji soubory…",
"seeding": "Seedování",
"stop_seeding": "Zastavování seedování",
"resume_seeding": "Obnovit seedování",
"options": "Spravovat",
"extract": "Rozbalit soubory",
"extracting": "Rozbalování souborů…"
},
"settings": {
"downloads_path": "Umístění stahování",
@@ -261,9 +297,65 @@
"must_be_valid_url": "Zdroj musí být platký odkaz URL",
"blocked_users": "Zablokovaní uživatelé",
"user_unblocked": "Uživatel byl odblokován",
"enable_achievement_notifications": "Když je odemknut achievement",
"enable_achievement_notifications": "Když je odemčen achievement",
"launch_minimized": "Spustit v minimalizovaném režimu",
"disable_nsfw_alert": "Deaktivovat upozornění na nevhodný obsah"
"disable_nsfw_alert": "Deaktivovat upozornění na nevhodný obsah",
"seed_after_download_complete": "Seedovat až skončí stahování",
"show_hidden_achievement_description": "Zobrazit popisy skrytých achievementů dříve, než jsou odemčeny",
"account": "Účet",
"no_users_blocked": "Nemáte žádného uživatele zablokovaného",
"subscription_active_until": "Vaše Hydra cloud předplatné je platné do {{date}}",
"manage_subscription": "Spravovat předplatné",
"update_email": "Změnit email",
"update_password": "Změnit heslo",
"current_email": "Současný email:",
"no_email_account": "Zatím nemáte nastavený email",
"account_data_updated_successfully": "Data vašeho účtu byly úspěšně upraveny",
"renew_subscription": "Obnovit předplatné Hydra Cloud",
"subscription_expired_at": "Vaše předplatné vypršelo {{date}}",
"no_subscription": "Užijte si Hydru v nejlepší možné podobě",
"become_subscriber": "Připojit se k předplatnému Hydra Cloud",
"subscription_renew_cancelled": "Automatické obnovování je zrušenu",
"subscription_renews_on": "Vaše předplatné se obnoví {{date}}",
"bill_sent_until": "Vaše příští faktura bude odeslána nejpozději do tohoto dne",
"no_themes": "Vypadá to že ještě nemáte žádné vzhledy, ale nebojte, klikněte sem pro vytvoření vašeho prvního mistrovského díla!",
"editor_tab_code": "Kód",
"editor_tab_info": "Info",
"editor_tab_save": "Uložit",
"web_store": "Webový obchod",
"clear_themes": "Vyčistit",
"create_theme": "Vytvořit",
"create_theme_modal_title": "Vytvořit vlastní vzhled",
"create_theme_modal_description": "Vytvořte si vlastní styl, abyste si mohli ozdobit Hydru",
"theme_name": "Název",
"insert_theme_name": "Vložte název vzhledu",
"set_theme": "Nastavit vzhled",
"unset_theme": "Zrušit vzhled",
"delete_theme": "Odstranit vzhled",
"edit_theme": "Upravit vzhled",
"delete_all_themes": "Smazat všechny vzhledy",
"delete_all_themes_description": "Toto smaže všechny vaše vzhledy",
"delete_theme_description": "Toto smaže vzhled {{theme}}",
"cancel": "Zrušit",
"appearance": "Vzhled",
"enable_torbox": "Povolit TorBox",
"torbox_description": "TorBox je prémiový seedbox server který se vyrovná i těm nejlepším seedbox serverům na trhu.",
"torbox_account_linked": "TorBox účet propojen",
"create_real_debrid_account": "Klikni sem pokud ještě nemáš Real-Debrid účet",
"create_torbox_account": "Klikni sem pokud ještě nemáš TorBox účet",
"real_debrid_account_linked": "Real-Debrid účet propojen",
"name_min_length": "Název vzhledu musí být minimálně 3 znaky dlouhý",
"import_theme": "Vložit vzhled",
"import_theme_description": "Chystáte se vložit vzhled {{theme}} z obchodu vzhledů",
"error_importing_theme": "Nastala chyba při vkládání vzhledu",
"theme_imported": "Vzhled úspěšně vložen",
"enable_friend_request_notifications": "Při obdržení žádosti o přátelství",
"enable_auto_install": "Automaticky stahovat aktualizace",
"common_redist": "Běžné redistribuovatelné komponenty",
"common_redist_description": "Běžné redistribuovatelné komponenty jsou potřeba pro spuštění určitých her. Je doporučeno je nainstalovat, aby se předešlo problémům.",
"install_common_redist": "Nainstalovat",
"installing_common_redist": "Instalování…",
"show_download_speed_in_megabytes": "Ukázat rychlost stahování v megabytech"
},
"notifications": {
"download_complete": "Stahování dokončeno",
@@ -273,14 +365,20 @@
"repack_count_other": "{{count}} repacky přidány",
"new_update_available": "Version {{version}} je dostupná",
"restart_to_install_update": "Restartuj Hydru pro aktualizaci",
"notification_achievement_unlocked_title": "Achievement pro {{game}} byl odemknut",
"notification_achievement_unlocked_body": "{{achievement}} a dalších {{count}} byly odemknuty"
"notification_achievement_unlocked_title": "Achievement pro {{game}} byl odemčen",
"notification_achievement_unlocked_body": "{{achievement}} a dalších {{count}} byly odemčeny",
"new_friend_request_description": "Obdrželi jste novou žádost o přátelství",
"new_friend_request_title": "Nová žádost o přátelství",
"extraction_complete": "Rozbalování dokončeno",
"game_extracted": "{{title}} úspěšně rozbaleno"
},
"system_tray": {
"open": "Otevřít Hydru",
"quit": "Odejít"
},
"game_card": {
"available_one": "Dostupné",
"available_other": "Dostupné",
"no_downloads": "Žádné možnosti stahování nenalezeny"
},
"binary_not_found_modal": {
@@ -363,7 +461,17 @@
"your_friend_code": "Tvůj kód přítele:",
"upload_banner": "Nahrát banner profilu",
"uploading_banner": "Nahrávání banneru",
"background_image_updated": "Obrázek pozadí byl změněn"
"background_image_updated": "Obrázek pozadí byl změněn",
"stats": "Statistiky",
"achievements": "Achievementy",
"games": "Hry",
"top_percentile": "Top {{percentile}}%",
"ranking_updated_weekly": "Žebříčky jsou aktualizovány každý týden",
"playing": "Hraje {{game}}",
"achievements_unlocked": "Achievements odemčen",
"earned_points": "Získané body",
"show_achievements_on_profile": "Zobrazit vaše odemčené achievementy na profilu",
"show_points_on_profile": "Zobrazit vaše získané body na profilu"
},
"achievement": {
"achievement_unlocked": "Achievement odemčen",
@@ -373,7 +481,12 @@
"subscription_needed": "Je vyžadováno předplatné Hydra Cloud pro zobrazení tohoto obsahu",
"new_achievements_unlocked": "Odemčeno {{achievementCount}} nových achievementů z {{gameCount}} her",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} achievementů",
"achievements_unlocked_for_game": "Odemčeno {{achievementCount}} nových achievementů pro {{gameTitle}}"
"achievements_unlocked_for_game": "Odemčeno {{achievementCount}} nových achievementů pro {{gameTitle}}",
"hidden_achievement_tooltip": "Toho je skrytý achievement",
"achievement_earn_points": "Získej {{points}} bodů tímto achievementem",
"earned_points": "Získané body",
"available_points": "Dostupné body:",
"how_to_earn_achievements_points": "Jak získat body achievementů?"
},
"hydra_cloud": {
"subscription_tour_title": "Předplatné Hydra Cloud",
@@ -383,6 +496,10 @@
"animated_profile_picture": "Animované profilové obrázky",
"premium_support": "Prémiová podpora",
"show_and_compare_achievements": "Zobraz a porovnej achievementy s ostatními uživateli",
"animated_profile_banner": "Animovaný banner na profilu"
"animated_profile_banner": "Animovaný banner na profilu",
"hydra_cloud": "Hydra Cloud",
"hydra_cloud_feature_found": "Právě jste objevili funkci předplatného Hydra Cloud!",
"learn_more": "Zjistit více",
"debrid_description": "Stahovat až 4x rychleji s Nimbus"
}
}

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…",
@@ -190,10 +193,12 @@
"reset_achievements_error": "Failed to reset achievements",
"download_error_gofile_quota_exceeded": "You have exceeded your Gofile monthly quota. Please await the quota to reset.",
"download_error_real_debrid_account_not_authorized": "Your Real-Debrid account is not authorized to make new downloads. Please check your account settings and try again.",
"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.",
"download_error_not_cached_on_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_on_torbox": "This download is not available on TorBox and polling download status from TorBox is not yet available.",
"download_error_not_cached_on_hydra": "This download is not available on Nimbus.",
"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",
@@ -230,7 +235,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",
@@ -332,7 +339,7 @@
"delete_theme_description": "This will delete the theme {{theme}}",
"cancel": "Cancel",
"appearance": "Appearance",
"enable_torbox": "Enable Torbox",
"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",
@@ -344,7 +351,12 @@
"error_importing_theme": "Error importing theme",
"theme_imported": "Theme imported successfully",
"enable_friend_request_notifications": "When a friend request is received",
"enable_auto_install": "Download updates automatically"
"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…",
"show_download_speed_in_megabytes": "Show download speed in megabytes per second"
},
"notifications": {
"download_complete": "Download complete",
@@ -357,7 +369,9 @@
"notification_achievement_unlocked_title": "Achievement unlocked for {{game}}",
"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"
"new_friend_request_title": "New friend request",
"extraction_complete": "Extraction complete",
"game_extracted": "{{title}} extracted successfully"
},
"system_tray": {
"open": "Open Hydra",
@@ -486,6 +500,7 @@
"animated_profile_banner": "Animated profile banner",
"hydra_cloud": "Hydra Cloud",
"hydra_cloud_feature_found": "You've just discovered a Hydra Cloud feature!",
"learn_more": "Learn More"
"learn_more": "Learn More",
"debrid_description": "Download up to 4x faster with Nimbus"
}
}

View File

@@ -44,6 +44,9 @@
"downloading_metadata": "Descargando metadatos de {{title}}…",
"downloading": "Descargando {{title}}… ({{percentage}} completado) - Finalizando {{eta}} - {{speed}}",
"calculating_eta": "Descargando {{title}}… ({{percentage}} completado) - Calculando tiempo restante…",
"installation_complete": "Instalación completada",
"installation_complete_message": "Common redistributables instalados exitosamente",
"installing_common_redist": "{{log}}…",
"checking_files": "Verificando archivos de {{title}}… ({{percentage}} completado)"
},
"catalogue": {
@@ -59,6 +62,8 @@
},
"game_details": {
"open_download_options": "Ver opciones de descargas",
"automatically_extract_downloaded_files": "Extraer automáticamente archivos descargados",
"download_error_not_cached_on_hydra": "Esta descarga no está disponible en Nimbus.",
"download_options_zero": "No hay opciones de descargas disponibles",
"download_options_one": "{{count}} opción de descarga",
"download_options_other": "{{count}} opciones de descargas",
@@ -190,8 +195,8 @@
"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.",
"download_error_not_cached_on_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_on_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"
},
@@ -230,10 +235,19 @@
"seeding": "Seeding",
"stop_seeding": "Detener seeding",
"resume_seeding": "Continuar seeding",
"extract": "Extraer archivos",
"extracting": "Extrayendo archivos…",
"options": "Gestionar"
},
"settings": {
"downloads_path": "Ruta de descarga",
"common_redist": "Common redistributables",
"common_redist_description": "Las Common redistributables son requeridos para ejecutar algunos juegos. Es recomendado instalarlos para evitar problemas.",
"create_real_debrid_account": "Presiona acá si no tienes una cuenta de Real-Debrid aún",
"create_torbox_account": "Presiona acá si no tienes una cuenta de TorBox aún",
"install_common_redist": "Instalar",
"installing_common_redist": "Instalando…",
"show_download_speed_in_megabytes": "Mostrar velocidad de descargar en megabytes por segundo",
"change": "Cambiar",
"notifications": "Notificaciones",
"enable_download_notifications": "Cuando se completa una descarga",
@@ -326,7 +340,7 @@
"editor_tab_code": "Código",
"editor_tab_info": "Info",
"editor_tab_save": "Guardar",
"enable_torbox": "Habilitar Torbox",
"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",
@@ -346,6 +360,8 @@
},
"notifications": {
"download_complete": "Descarga completada",
"extraction_complete": "Extracción completada",
"game_extracted": "{{title}} extraído exitosamente",
"game_ready_to_install": "{{title}} está listo para instalarse",
"repack_list_updated": "Lista de repacks actualizadas",
"repack_count_one": "{{count}} repack ha sido añadido",
@@ -475,6 +491,7 @@
},
"hydra_cloud": {
"subscription_tour_title": "Suscripción Hydra Cloud",
"debrid_description": "Descargas hasta x4 más rápidas con Nimbus",
"subscribe_now": "Suscribirse ahora",
"cloud_saving": "Guardado en la nube",
"cloud_achievements": "Guarda tus logros en la nube",

View File

@@ -44,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",
@@ -179,10 +182,12 @@
"no_write_permission": "Não é possível baixar nesse diretório. Clique aqui para saber mais.",
"download_error_gofile_quota_exceeded": "Você excedeu sua cota mensal do Gofile. Por favor, aguarde a cota resetar.",
"download_error_real_debrid_account_not_authorized": "Sua conta do Real-Debrid não está autorizada a fazer novos downloads. Por favor, verifique 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.",
"download_error_not_cached_on_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_on_torbox": "Este download não está disponível no TorBox e a verificação do status do download não está disponível.",
"download_error_not_cached_on_hydra": "Este download não está disponível no Nimbus.",
"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",
@@ -219,7 +224,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",
@@ -319,7 +326,7 @@
"delete_theme_description": "Isso irá deletar o tema {{theme}}",
"cancel": "Cancelar",
"appearance": "Aparência",
"enable_torbox": "Habilitar Torbox",
"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",
@@ -331,7 +338,12 @@
"error_importing_theme": "Erro ao importar tema",
"theme_imported": "Tema importado com sucesso",
"enable_friend_request_notifications": "Quando um pedido de amizade é recebido",
"enable_auto_install": "Baixar atualizações automaticamente"
"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…",
"show_download_speed_in_megabytes": "Exibir taxas de download em megabytes por segundo"
},
"notifications": {
"download_complete": "Download concluído",
@@ -342,7 +354,9 @@
"new_update_available": "Versão {{version}} disponível",
"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"
"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",
@@ -481,6 +495,7 @@
"animated_profile_banner": "Banner animado no perfil",
"cloud_saving": "Saves de jogos em nuvem",
"hydra_cloud_feature_found": "Você descobriu uma funcionalidade Hydra Cloud!",
"learn_more": "Saiba mais"
"learn_more": "Saiba mais",
"debrid_description": "Baixe até 4x mais rápido com Nimbus"
}
}

View File

@@ -175,8 +175,8 @@
"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.",
"download_error_not_cached_on_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_on_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"
},
@@ -321,7 +321,7 @@
"delete_theme_description": "Isto irá apagar o tema {{theme}}",
"cancel": "Cancelar",
"appearance": "Aparência",
"enable_torbox": "Ativar Torbox",
"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",

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": {
@@ -44,13 +44,16 @@
"downloading_metadata": "Загрузка метаданных {{title}}…",
"downloading": "Загрузка {{title}}… ({{percentage}} завершено) - Окончание {{eta}} - {{speed}}",
"calculating_eta": "Загрузка {{title}}… ({{percentage}} завершено) - Подсчёт оставшегося времени…",
"checking_files": "Проверка файлов {{title}}… ({{percentage}} завершено)"
"checking_files": "Проверка файлов {{title}}… ({{percentage}} завершено)",
"installing_common_redist": "{{log}}…",
"installation_complete": "Установка завершена",
"installation_complete_message": "Библиотеки успешно установлены"
},
"catalogue": {
"search": "Фильтр…",
"developers": "Разработчики",
"genres": "Жанры",
"tags": "Маркеры",
"tags": "Теги",
"publishers": "Издательства",
"download_sources": "Источники загрузки",
"result_count": "{{resultCount}} результатов",
@@ -183,15 +186,18 @@
"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_on_real_debrid": "Эта загрузка недоступна на Real-Debrid, и получение статуса загрузки с Real-Debrid пока недоступно.",
"download_error_not_cached_on_torbox": "Эта загрузка недоступна на TorBox, и получить статус загрузки с TorBox пока невозможно.",
"game_added_to_favorites": "Игра добавлена в избранное",
"game_removed_from_favorites": "Игра удалена из избранного"
"game_removed_from_favorites": "Игра удалена из избранного",
"automatically_extract_downloaded_files": "Автоматическая распаковка загруженных файлов"
},
"activation": {
"title": "Активировать Hydra",
@@ -228,7 +234,9 @@
"seeding": "Раздача",
"stop_seeding": "Остановить раздачу",
"resume_seeding": "Продолжить раздачу",
"options": "Управлять"
"options": "Управлять",
"extract": "Распаковать файлы",
"extracting": "Распаковка файлов…"
},
"settings": {
"downloads_path": "Путь загрузок",
@@ -267,15 +275,15 @@
"download_source_up_to_date": "Обновлён",
"download_source_errored": "Ошибка",
"sync_download_sources": "Обновить источники",
"removed_download_source": "Источник загрузок удален",
"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": "Все источники загрузок синхронизированы",
"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}} вариант загрузки",
@@ -283,7 +291,7 @@
"import": "Импортировать",
"blocked_users": "Заблокированные пользователи",
"friends_only": "Только для друзей",
"must_be_valid_url": "Источник должен быть действительным URL-адресом.",
"must_be_valid_url": "У источника должен быть правильный URL",
"privacy": "Конфиденциальность",
"private": "Частный",
"profile_visibility": "Видимость профиля",
@@ -330,17 +338,24 @@
"delete_theme_description": "Это приведет к удалению темы {{theme}}",
"cancel": "Отменить",
"appearance": "Внешний вид",
"enable_torbox": "Включить Torbox",
"enable_torbox": "Включить TorBox",
"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": "Тема успешно импортирована",
"enable_friend_request_notifications": "При получении запроса на добавление в друзья",
"enable_auto_install": "Загружать обновления автоматически"
"enable_auto_install": "Загружать обновления автоматически",
"common_redist": "Библиотеки",
"common_redist_description": "Для запуска некоторых игр требуются библиотеки. Во избежание проблем рекомендуется установить их.",
"install_common_redist": "Установить",
"installing_common_redist": "Установка…",
"show_download_speed_in_megabytes": "Показать скорость загрузки в мегабайтах в секунду"
},
"notifications": {
"download_complete": "Загрузка завершена",
@@ -353,7 +368,9 @@
"notification_achievement_unlocked_title": "Достижение разблокировано для {{game}}",
"notification_achievement_unlocked_body": "были разблокированы {{achievement}} и другие {{count}}",
"new_friend_request_title": "Новый запрос на добавление в друзья",
"new_friend_request_description": "Вы получили новый запрос на добавление в друзья"
"new_friend_request_description": "Вы получили новый запрос на добавление в друзья",
"extraction_complete": "Распаковка завершена",
"game_extracted": "{{title}} успешно распакован"
},
"system_tray": {
"open": "Открыть Hydra",
@@ -446,11 +463,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": "Показывать заработанные очки в своем профиле"
},
@@ -480,6 +499,7 @@
"animated_profile_banner": "Анимированный баннер профиля",
"hydra_cloud": "Hydra Cloud",
"hydra_cloud_feature_found": "Вы только что открыли для себя функцию Hydra Cloud!",
"learn_more": "Подробнее"
"learn_more": "Подробнее",
"debrid_description": "Скачивайте в 4 раза быстрее с Nimbus"
}
}

View File

@@ -44,7 +44,10 @@
"downloading_metadata": "{{title}} meta verileri indiriliyor…",
"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ı)"
"checking_files": "{{title}} dosyaları kontrol ediliyor… ({{percentage}} tamamlandı)",
"installing_common_redist": "{{log}}…",
"installation_complete": "İndirme tamamlandı",
"installation_complete_message": "Genel bağımlılıklar başarıyla yüklendi."
},
"catalogue": {
"search": "Filtrele…",
@@ -190,10 +193,11 @@
"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.",
"download_error_not_cached_on_real_debrid": "Bu indirme Real-Debrid üzerinde mevcut değil ve Real-Debrid'den indirme durumu henüz sorgulanamıyor.",
"download_error_not_cached_on_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"
"game_added_to_favorites": "Oyun favorilere eklendi",
"automatically_extract_downloaded_files": "Yüklenmiş dosyaları otomatik olarak çıkart"
},
"activation": {
"title": "Hydra'yı Aktive Et",
@@ -230,7 +234,9 @@
"seeding": "Paylaşılıyor",
"stop_seeding": "Paylaşımı durdur",
"resume_seeding": "Paylaşımı sürdür",
"options": "Yönet"
"options": "Yönet",
"extract": "Dosyaları çıkart",
"extracting": "Dosyalar çıkartılıyor…"
},
"settings": {
"downloads_path": "İndirme yolu",
@@ -332,15 +338,23 @@
"delete_theme_description": "Bu {{theme}} temasını silecektir",
"cancel": "İptal",
"appearance": "Görünüm",
"enable_torbox": "Torbox'u etkinleştir",
"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",
"torbox_account_linked": "TorBox hesabı bağlandı",
"create_real_debrid_account": "Henüz bir Real-Debrid hesabınız yoksa buraya tıklayın",
"create_torbox_account": "Henüz bir TorBox hesabınız yoksa buraya tıklayın",
"real_debrid_account_linked": "Real-Debrid hesabı bağlandı",
"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ı"
"theme_imported": "Tema başarıyla içe aktarıldı",
"enable_friend_request_notifications": "Bir arkadaşlık isteği alındığında",
"enable_auto_install": "Güncellemeleri otomatik yükle",
"common_redist": "Ortak bağımlılıklar",
"common_redist_description": "Bazı oyunların çalışabilmesi için genel bağımlılıklar gereklidir. Sorun yaşamamak için bunların yüklenmesi önerilir.",
"install_common_redist": "Yükle",
"installing_common_redist": "Yükleniyor…"
},
"notifications": {
"download_complete": "İndirme tamamlandı",
@@ -351,7 +365,11 @@
"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ım kilidi açıldı",
"notification_achievement_unlocked_body": "{{achievement}} ve diğer {{count}} başarım açıldı"
"notification_achievement_unlocked_body": "{{achievement}} ve diğer {{count}} başarım açıldı",
"new_friend_request_description": "Yeni bir arkadaşlık isteğin var",
"new_friend_request_title": "Yeni arkadaşlık isteği",
"extraction_complete": ıkartma tamamlandı",
"game_extracted": "{{title}} başarıyla çıkartıldı"
},
"system_tray": {
"open": "Hydra'yı Aç",

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");

View File

@@ -1,8 +1,6 @@
import { CloudSync } from "@main/services";
import { registerEvent } from "../register-event";
import type { GameShop } from "@types";
import { t } from "i18next";
import { format } from "date-fns";
const uploadSaveGame = async (
_event: Electron.IpcMainInvokeEvent,
@@ -14,10 +12,7 @@ const uploadSaveGame = async (
objectId,
shop,
downloadOptionTitle,
t("backup_from", {
ns: "game_details",
date: format(new Date(), "dd/MM/yyyy"),
})
CloudSync.getBackupLabel(false)
);
};

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";
@@ -38,12 +39,15 @@ 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";
import "./torrenting/start-game-download";
import "./torrenting/pause-game-seed";
import "./torrenting/resume-game-seed";
import "./torrenting/check-debrid-availability";
import "./user-preferences/get-user-preferences";
import "./user-preferences/update-user-preferences";
import "./user-preferences/auto-launch";
@@ -90,6 +94,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

@@ -13,35 +13,42 @@ const deleteGameFolder = async (
objectId: string
): Promise<void> => {
const downloadKey = levelKeys.game(shop, objectId);
const download = await downloadsSublevel.get(downloadKey);
if (!download) return;
if (!download?.folderName) return;
if (download.folderName) {
const folderPath = path.join(
download.downloadPath ?? (await getDownloadsPath()),
download.folderName
);
const folderPath = path.join(
download.downloadPath ?? (await getDownloadsPath()),
download.folderName
);
if (fs.existsSync(folderPath)) {
const metaPath = `${folderPath}.meta`;
const deleteFile = async (filePath: string, isDirectory = false) => {
if (fs.existsSync(filePath)) {
await new Promise<void>((resolve, reject) => {
fs.rm(
folderPath,
{ recursive: true, force: true, maxRetries: 5, retryDelay: 200 },
filePath,
{
recursive: isDirectory,
force: true,
maxRetries: 5,
retryDelay: 200,
},
(error) => {
if (error) {
logger.error(error);
reject();
}
resolve();
}
);
});
}
}
};
await deleteFile(folderPath, true);
await deleteFile(metaPath);
await downloadsSublevel.del(downloadKey);
};

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

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

View File

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

View File

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

View File

@@ -0,0 +1,11 @@
import { HydraDebridClient } from "@main/services/download/hydra-debrid";
import { registerEvent } from "../register-event";
const checkDebridAvailability = async (
_event: Electron.IpcMainInvokeEvent,
magnets: string[]
) => {
return HydraDebridClient.getAvailableMagnets(magnets);
};
registerEvent("checkDebridAvailability", checkDebridAvailability);

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

@@ -3,12 +3,10 @@ import updater from "electron-updater";
import i18n from "i18next";
import path from "node:path";
import url from "node:url";
import kill from "kill-port";
import { electronApp, optimizer } from "@electron-toolkit/utils";
import { logger, WindowManager } from "@main/services";
import resources from "@locales";
import { PythonRPC } from "./services/python-rpc";
import { Aria2 } from "./services/aria2";
import { db, levelKeys } from "./level";
import { loadState } from "./main";
@@ -59,7 +57,7 @@ app.whenReady().then(async () => {
return net.fetch(url.pathToFileURL(decodeURI(filePath)).toString());
});
await kill(PythonRPC.RPC_PORT).finally(() => loadState());
await loadState();
const language = await db.get<string, string>(levelKeys.language, {
valueEncoding: "utf-8",
@@ -143,7 +141,6 @@ app.on("window-all-closed", () => {
app.on("before-quit", () => {
/* Disconnects libtorrent */
PythonRPC.kill();
Aria2.kill();
});
app.on("activate", () => {

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,6 +13,5 @@ export const levelKeys = {
downloads: "downloads",
userPreferences: "userPreferences",
language: "language",
sqliteMigrationDone: "sqliteMigrationDone",
screenState: "screenState",
};

View File

@@ -1,31 +1,22 @@
import { DownloadManager, logger, Ludusavi, startMainLoop } from "./services";
import { Aria2, DownloadManager, Ludusavi, startMainLoop } from "./services";
import { RealDebridClient } from "./services/download/real-debrid";
import { HydraApi } from "./services/hydra-api";
import { uploadGamesBatch } from "./services/library-sync";
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");
@@ -41,7 +32,7 @@ export const loadState = async () => {
Ludusavi.addManifestToLudusaviConfig();
HydraApi.setupApi().then(() => {
await HydraApi.setupApi().then(() => {
uploadGamesBatch();
});
@@ -52,6 +43,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(
@@ -65,138 +65,6 @@ export const loadState = async () => {
await DownloadManager.startRPC(nextItemOnQueue, downloadsToSeed);
startMainLoop();
};
const migrateFromSqlite = async () => {
const sqliteMigrationDone = await db.get(levelKeys.sqliteMigrationDone);
if (sqliteMigrationDone) {
return;
}
const migrateGames = knexClient("game")
.select("*")
.then((games) => {
return gamesSublevel.batch(
games.map((game) => ({
type: "put",
key: levelKeys.game(game.shop, game.objectID),
value: {
objectId: game.objectID,
shop: game.shop,
title: game.title,
iconUrl: game.iconUrl,
playTimeInMilliseconds: game.playTimeInMilliseconds,
lastTimePlayed: game.lastTimePlayed,
remoteId: game.remoteId,
winePrefixPath: game.winePrefixPath,
launchOptions: game.launchOptions,
executablePath: game.executablePath,
isDeleted: game.isDeleted === 1,
},
}))
);
})
.then(() => {
logger.info("Games migrated successfully");
});
const migrateUserPreferences = knexClient("user_preferences")
.select("*")
.then(async (userPreferences) => {
if (userPreferences.length > 0) {
const { realDebridApiToken, ...rest } = userPreferences[0];
await db.put<string, UserPreferences>(
levelKeys.userPreferences,
{
...rest,
realDebridApiToken,
preferQuitInsteadOfHiding: rest.preferQuitInsteadOfHiding === 1,
runAtStartup: rest.runAtStartup === 1,
startMinimized: rest.startMinimized === 1,
disableNsfwAlert: rest.disableNsfwAlert === 1,
seedAfterDownloadComplete: rest.seedAfterDownloadComplete === 1,
showHiddenAchievementsDescription:
rest.showHiddenAchievementsDescription === 1,
downloadNotificationsEnabled:
rest.downloadNotificationsEnabled === 1,
repackUpdatesNotificationsEnabled:
rest.repackUpdatesNotificationsEnabled === 1,
achievementNotificationsEnabled:
rest.achievementNotificationsEnabled === 1,
},
{ valueEncoding: "json" }
);
if (rest.language) {
await db.put<string, string>(levelKeys.language, rest.language, {
valueEncoding: "utf-8",
});
}
}
})
.then(() => {
logger.info("User preferences migrated successfully");
});
const migrateAchievements = knexClient("game_achievement")
.select("*")
.then((achievements) => {
return gameAchievementsSublevel.batch(
achievements.map((achievement) => ({
type: "put",
key: levelKeys.game(achievement.shop, achievement.objectId),
value: {
achievements: JSON.parse(achievement.achievements),
unlockedAchievements: JSON.parse(achievement.unlockedAchievements),
},
}))
);
})
.then(() => {
logger.info("Achievements migrated successfully");
});
const migrateUser = knexClient("user_auth")
.select("*")
.then(async (users) => {
if (users.length > 0) {
await db.put<string, User>(
levelKeys.user,
{
id: users[0].userId,
displayName: users[0].displayName,
profileImageUrl: users[0].profileImageUrl,
backgroundImageUrl: users[0].backgroundImageUrl,
subscription: users[0].subscription,
},
{
valueEncoding: "json",
}
);
await db.put<string, Auth>(
levelKeys.auth,
{
accessToken: users[0].accessToken,
refreshToken: users[0].refreshToken,
tokenExpirationTimestamp: users[0].tokenExpirationTimestamp,
},
{
valueEncoding: "json",
}
);
}
})
.then(() => {
logger.info("User data migrated successfully");
});
return Promise.allSettled([
migrateGames,
migrateUserPreferences,
migrateAchievements,
migrateUser,
]);
CommonRedistManager.downloadCommonRedist();
};

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

@@ -16,6 +16,13 @@ export const getGameAchievementData = async (
if (cachedAchievements && useCachedData)
return cachedAchievements.achievements;
if (
cachedAchievements &&
Date.now() < (cachedAchievements.cacheExpiresTimestamp ?? 0)
) {
return cachedAchievements.achievements;
}
const language = await db
.get<string, string>(levelKeys.language, {
valueEncoding: "utf-8",
@@ -31,6 +38,7 @@ export const getGameAchievementData = async (
await gameAchievementsSublevel.put(levelKeys.game(shop, objectId), {
unlockedAchievements: cachedAchievements?.unlockedAchievements ?? [],
achievements,
cacheExpiresTimestamp: Date.now() + 1000 * 60 * 30, // 30 minutes
});
return achievements;

View File

@@ -2,6 +2,7 @@ import type {
Game,
GameShop,
UnlockedAchievement,
UpdatedUnlockedAchievements,
UserPreferences,
} from "@types";
import { WindowManager } from "../window-manager";
@@ -26,6 +27,7 @@ const saveAchievementsOnLocal = async (
await gameAchievementsSublevel.put(levelKey, {
achievements: gameAchievement?.achievements ?? [],
unlockedAchievements: unlockedAchievements,
cacheExpiresTimestamp: gameAchievement?.cacheExpiresTimestamp,
});
if (!sendUpdateEvent) return;
@@ -114,7 +116,7 @@ export const mergeAchievements = async (
}
if (game.remoteId) {
await HydraApi.put(
await HydraApi.put<UpdatedUnlockedAchievements | undefined>(
"/profile/games/achievements",
{
id: game.remoteId,
@@ -123,10 +125,19 @@ export const mergeAchievements = async (
{ needsSubscription: !newAchievements.length }
)
.then((response) => {
if (response) {
return saveAchievementsOnLocal(
response.objectId,
response.shop,
response.achievements,
publishNotification
);
}
return saveAchievementsOnLocal(
response.objectId,
response.shop,
response.achievements,
game.objectId,
game.shop,
mergedLocalAchievements,
publishNotification
);
})

View File

@@ -2,8 +2,6 @@ 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;

View File

@@ -13,9 +13,28 @@ import { logger } from "./logger";
import { WindowManager } from "./window-manager";
import axios from "axios";
import { Ludusavi } from "./ludusavi";
import { SubscriptionRequiredError } from "@shared";
import { formatDate, SubscriptionRequiredError } from "@shared";
import i18next, { t } from "i18next";
export class CloudSync {
public static getBackupLabel(automatic: boolean) {
const language = i18next.language;
const date = formatDate(new Date(), language);
if (automatic) {
return t("automatic_backup_from", {
ns: "game_details",
date,
});
}
return t("backup_from", {
ns: "game_details",
date,
});
}
private static async bundleBackup(
shop: GameShop,
objectId: string,
@@ -25,7 +44,11 @@ export class CloudSync {
// Remove existing backup
if (fs.existsSync(backupPath)) {
fs.rmSync(backupPath, { recursive: true });
try {
await fs.promises.rm(backupPath, { recursive: true });
} catch (error) {
logger.error("Failed to remove backup path", error);
}
}
await Ludusavi.backupGame(shop, objectId, backupPath, winePrefix);
@@ -101,11 +124,10 @@ export class CloudSync {
true
);
fs.rm(bundleLocation, (err) => {
if (err) {
logger.error("Failed to remove tar file", err);
throw err;
}
});
try {
await fs.promises.unlink(bundleLocation);
} catch (error) {
logger.error("Failed to remove tar file", error);
}
}
}

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,4 +1,4 @@
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";
@@ -22,6 +22,8 @@ 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";
import { HydraDebridClient } from "./hydra-debrid";
export class DownloadManager {
private static downloadingGameId: string | null = null;
@@ -136,6 +138,8 @@ export class DownloadManager {
);
}
const shouldExtract = download.automaticallyExtract;
if (progress === 1 && download) {
publishDownloadCompleteNotification(game);
@@ -143,23 +147,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()
@@ -285,6 +314,8 @@ export class DownloadManager {
url: downloadLink,
save_path: download.downloadPath,
header: `Cookie: accountToken=${token}`,
allow_multiple_connections: true,
connections_limit: 8,
};
}
case Downloader.PixelDrain: {
@@ -336,13 +367,14 @@ export class DownloadManager {
case Downloader.RealDebrid: {
const downloadUrl = await RealDebridClient.getDownloadUrl(download.uri);
if (!downloadUrl) throw new Error(DownloadError.NotCachedInRealDebrid);
if (!downloadUrl) throw new Error(DownloadError.NotCachedOnRealDebrid);
return {
action: "start",
game_id: downloadId,
url: downloadUrl,
save_path: download.downloadPath,
allow_multiple_connections: true,
};
}
case Downloader.TorBox: {
@@ -355,6 +387,22 @@ export class DownloadManager {
url,
save_path: download.downloadPath,
out: name,
allow_multiple_connections: true,
};
}
case Downloader.Hydra: {
const downloadUrl = await HydraDebridClient.getDownloadUrl(
download.uri
);
if (!downloadUrl) throw new Error(DownloadError.NotCachedOnHydra);
return {
action: "start",
game_id: downloadId,
url: downloadUrl,
save_path: download.downloadPath,
allow_multiple_connections: true,
};
}
}

View File

@@ -0,0 +1,27 @@
import { HydraApi } from "../hydra-api";
export class HydraDebridClient {
public static getAvailableMagnets(
magnets: string[]
): Promise<Record<string, boolean>> {
return HydraApi.put(
"/debrid/check-availability",
{
magnets,
},
{ needsAuth: false }
);
}
public static async getDownloadUrl(magnet: string) {
try {
const response = await HydraApi.post("/debrid/request-file", {
magnet,
});
return response.downloadUrl;
} catch (error) {
return null;
}
}
}

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

@@ -8,3 +8,7 @@ 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";
export * from "./aria2";

View File

@@ -128,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,9 +6,7 @@ import axios from "axios";
import { exec } from "child_process";
import { ProcessPayload } from "./download/types";
import { gamesSublevel, levelKeys } from "@main/level";
import { t } from "i18next";
import { CloudSync } from "./cloud-sync";
import { format } 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 ""}'`,
@@ -234,10 +232,7 @@ function onOpenGame(game: Game) {
game.objectId,
game.shop,
null,
t("automatic_backup_from", {
ns: "game_details",
date: format(new Date(), "dd/MM/yyyy"),
})
CloudSync.getBackupLabel(true)
);
}
} else {
@@ -308,10 +303,7 @@ const onCloseGame = (game: Game) => {
game.objectId,
game.shop,
null,
t("automatic_backup_from", {
ns: "game_details",
date: format(new Date(), "dd/MM/yyyy"),
})
CloudSync.getBackupLabel(true)
);
}
} else {

View File

@@ -21,6 +21,12 @@ const binaryNameByPlatform: Partial<Record<NodeJS.Platform, string>> = {
win32: "hydra-python-rpc.exe",
};
const rustBinaryNameByPlatform: Partial<Record<NodeJS.Platform, string>> = {
darwin: "hydra-httpdl",
linux: "hydra-httpdl",
win32: "hydra-httpdl.exe",
};
export class PythonRPC {
public static readonly BITTORRENT_PORT = "5881";
public static readonly RPC_PORT = "8084";
@@ -52,6 +58,20 @@ export class PythonRPC {
this.RPC_PASSWORD,
initialDownload ? JSON.stringify(initialDownload) : "",
initialSeeding ? JSON.stringify(initialSeeding) : "",
app.isPackaged
? path.join(
process.resourcesPath,
rustBinaryNameByPlatform[process.platform]!
)
: path.join(
__dirname,
"..",
"..",
"rust_rpc",
"target",
"debug",
rustBinaryNameByPlatform[process.platform]!
),
];
if (app.isPackaged) {

View File

@@ -55,6 +55,8 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.on("on-seeding-status", listener);
return () => ipcRenderer.removeListener("on-seeding-status", listener);
},
checkDebridAvailability: (magnets: string[]) =>
ipcRenderer.invoke("checkDebridAvailability", magnets),
/* Catalogue */
searchGames: (payload: CatalogueSearchPayload, take: number, skip: number) =>
@@ -100,6 +102,11 @@ 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: (
@@ -173,6 +180,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">[]
@@ -195,6 +204,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) =>
@@ -279,6 +297,8 @@ contextBridge.exposeInMainWorld("electron", {
ipcRenderer.invoke("showItemInFolder", path),
getFeatures: () => ipcRenderer.invoke("getFeatures"),
getBadges: () => ipcRenderer.invoke("getBadges"),
canInstallCommonRedist: () => ipcRenderer.invoke("canInstallCommonRedist"),
installCommonRedist: () => ipcRenderer.invoke("installCommonRedist"),
platform: process.platform,
/* Auto update */
@@ -294,6 +314,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"),

View File

@@ -137,6 +137,15 @@ export function App() {
}, [fetchUserDetails, updateUserDetails, dispatch]);
const onSignIn = useCallback(() => {
window.electron.getDownloadSources().then((sources) => {
sources.forEach((source) => {
downloadSourcesWorker.postMessage([
"IMPORT_DOWNLOAD_SOURCE",
source.url,
]);
});
});
fetchUserDetails().then((response) => {
if (response) {
updateUserDetails(response);
@@ -224,18 +233,28 @@ export function App() {
const downloadSources = await downloadSourcesTable.toArray();
downloadSources
.filter((source) => !source.fingerprint)
.forEach(async (downloadSource) => {
const { fingerprint } = await window.electron.putDownloadSource(
downloadSource.objectIds
);
await Promise.all(
downloadSources
.filter((source) => !source.fingerprint)
.map(async (downloadSource) => {
const { fingerprint } = await window.electron.putDownloadSource(
downloadSource.objectIds
);
downloadSourcesTable.update(downloadSource.id, { fingerprint });
});
return downloadSourcesTable.update(downloadSource.id, {
fingerprint,
});
})
);
channel.close();
};
downloadSourcesWorker.postMessage(["SYNC_DOWNLOAD_SOURCES", id]);
return () => {
channel.close();
};
}, [updateRepacks]);
useEffect(() => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,24 @@
<svg width="20" height="21" viewBox="0 0 20 21" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Meteor">
<g id="Vector">
<path d="M10.6242 13.0003C10.6242 13.6184 10.4409 14.2226 10.0975 14.7365C9.75415 15.2504 9.26609 15.6509 8.69507 15.8875C8.12405 16.124 7.49572 16.1859 6.88953 16.0653C6.28334 15.9447 5.72652 15.6471 5.28948 15.2101C4.85244 14.773 4.55481 14.2162 4.43423 13.61C4.31366 13.0038 4.37554 12.3755 4.61206 11.8045C4.84859 11.2334 5.24913 10.7454 5.76303 10.402C6.27693 10.0586 6.88112 9.87535 7.49919 9.87535C8.32799 9.87535 9.12285 10.2046 9.7089 10.7906C10.2949 11.3767 10.6242 12.1715 10.6242 13.0003ZM16.432 10.0582L12.682 13.8082C12.5647 13.9254 12.4988 14.0845 12.4988 14.2503C12.4988 14.4162 12.5647 14.5753 12.682 14.6925C12.7993 14.8098 12.9583 14.8757 13.1242 14.8757C13.29 14.8757 13.4491 14.8098 13.5664 14.6925L17.3164 10.9425C17.3744 10.8845 17.4205 10.8155 17.4519 10.7397C17.4834 10.6638 17.4995 10.5825 17.4995 10.5003C17.4995 10.4182 17.4834 10.3369 17.4519 10.261C17.4205 10.1852 17.3744 10.1162 17.3164 10.0582C17.2583 10.0001 17.1894 9.95403 17.1135 9.9226C17.0376 9.89118 16.9563 9.875 16.8742 9.875C16.7921 9.875 16.7107 9.89118 16.6349 9.9226C16.559 9.95403 16.4901 10.0001 16.432 10.0582ZM14.8164 9.06754C14.8744 9.00947 14.9205 8.94053 14.9519 8.86466C14.9834 8.78879 14.9995 8.70747 14.9995 8.62535C14.9995 8.54323 14.9834 8.46191 14.9519 8.38604C14.9205 8.31017 14.8744 8.24123 14.8164 8.18316C14.7583 8.12509 14.6894 8.07903 14.6135 8.0476C14.5376 8.01617 14.4563 8 14.3742 8C14.2921 8 14.2107 8.01617 14.1349 8.0476C14.059 8.07903 13.9901 8.12509 13.932 8.18316L12.057 10.0582C11.9397 10.1754 11.8738 10.3345 11.8738 10.5003C11.8738 10.6662 11.9397 10.8253 12.057 10.9425C12.1743 11.0598 12.3333 11.1257 12.4992 11.1257C12.665 11.1257 12.8241 11.0598 12.9414 10.9425L14.8164 9.06754ZM17.9414 5.05816C17.8833 5.00005 17.8144 4.95395 17.7385 4.9225C17.6627 4.89105 17.5813 4.87486 17.4992 4.87486C17.4171 4.87486 17.3357 4.89105 17.2599 4.9225C17.184 4.95395 17.115 5.00005 17.057 5.05816L15.807 6.30816C15.6897 6.42544 15.6238 6.5845 15.6238 6.75035C15.6238 6.9162 15.6897 7.07526 15.807 7.19254C15.9243 7.30981 16.0833 7.37569 16.2492 7.3757C16.415 7.3757 16.5741 7.30981 16.6914 7.19254L17.9414 5.94254C17.9995 5.88449 18.0456 5.81556 18.077 5.73969C18.1085 5.66381 18.1247 5.58248 18.1247 5.50035C18.1247 5.41821 18.1085 5.33688 18.077 5.26101C18.0456 5.18514 17.9995 5.11621 17.9414 5.05816ZM9.557 8.44254C9.61505 8.50065 9.68398 8.54674 9.75985 8.5782C9.83572 8.60965 9.91705 8.62584 9.99919 8.62584C10.0813 8.62584 10.1627 8.60965 10.2385 8.5782C10.3144 8.54674 10.3833 8.50065 10.4414 8.44254L16.0664 2.81754C16.1244 2.75947 16.1705 2.69053 16.2019 2.61466C16.2334 2.53879 16.2495 2.45747 16.2495 2.37535C16.2495 2.29323 16.2334 2.21191 16.2019 2.13604C16.1705 2.06017 16.1244 1.99123 16.0664 1.93316C16.0083 1.87509 15.9394 1.82903 15.8635 1.7976C15.7876 1.76618 15.7063 1.75 15.6242 1.75C15.5421 1.75 15.4607 1.76618 15.3849 1.7976C15.309 1.82903 15.2401 1.87509 15.182 1.93316L9.557 7.55816C9.49889 7.61621 9.45279 7.68514 9.42134 7.76101C9.38989 7.83688 9.3737 7.91821 9.3737 8.00035C9.3737 8.08248 9.38989 8.16381 9.42134 8.23969C9.45279 8.31556 9.49889 8.38449 9.557 8.44254ZM10.5929 16.0941C9.77242 16.9146 8.65957 17.3756 7.49919 17.3756C6.33881 17.3756 5.22595 16.9146 4.40544 16.0941C3.58492 15.2736 3.12396 14.1607 3.12396 13.0003C3.12396 11.84 3.58492 10.7271 4.40544 9.9066L10.8703 3.44253C10.9284 3.38447 10.9744 3.31553 11.0058 3.23966C11.0373 3.16379 11.0534 3.08247 11.0534 3.00035C11.0534 2.91823 11.0373 2.83691 11.0058 2.76104C10.9744 2.68517 10.9284 2.61623 10.8703 2.55816C10.8122 2.50009 10.7433 2.45403 10.6674 2.4226C10.5915 2.39118 10.5102 2.375 10.4281 2.375C10.346 2.375 10.2647 2.39118 10.1888 2.4226C10.1129 2.45403 10.044 2.50009 9.98591 2.55816L3.52184 9.023C2.99253 9.54377 2.57156 10.1642 2.28322 10.8484C1.99488 11.5327 1.84487 12.2673 1.84184 13.0098C1.83882 13.7524 1.98284 14.4882 2.2656 15.1747C2.54836 15.8613 2.96427 16.4852 3.48932 17.0102C4.01438 17.5353 4.63819 17.9512 5.32479 18.2339C6.01138 18.5167 6.74717 18.6607 7.4897 18.6577C8.23223 18.6547 8.96682 18.5047 9.65109 18.2163C10.3354 17.928 10.9558 17.507 11.4765 16.9777C11.5888 16.8595 11.6505 16.7022 11.6484 16.5392C11.6463 16.3762 11.5806 16.2205 11.4654 16.1053C11.3501 15.99 11.1944 15.9243 11.0314 15.9223C10.8684 15.9202 10.7111 15.9818 10.5929 16.0941Z" fill="black"/>
<path d="M10.6242 13.0003C10.6242 13.6184 10.4409 14.2226 10.0975 14.7365C9.75415 15.2504 9.26609 15.6509 8.69507 15.8875C8.12405 16.124 7.49572 16.1859 6.88953 16.0653C6.28334 15.9447 5.72652 15.6471 5.28948 15.2101C4.85244 14.773 4.55481 14.2162 4.43423 13.61C4.31366 13.0038 4.37554 12.3755 4.61206 11.8045C4.84859 11.2334 5.24913 10.7454 5.76303 10.402C6.27693 10.0586 6.88112 9.87535 7.49919 9.87535C8.32799 9.87535 9.12285 10.2046 9.7089 10.7906C10.2949 11.3767 10.6242 12.1715 10.6242 13.0003ZM16.432 10.0582L12.682 13.8082C12.5647 13.9254 12.4988 14.0845 12.4988 14.2503C12.4988 14.4162 12.5647 14.5753 12.682 14.6925C12.7993 14.8098 12.9583 14.8757 13.1242 14.8757C13.29 14.8757 13.4491 14.8098 13.5664 14.6925L17.3164 10.9425C17.3744 10.8845 17.4205 10.8155 17.4519 10.7397C17.4834 10.6638 17.4995 10.5825 17.4995 10.5003C17.4995 10.4182 17.4834 10.3369 17.4519 10.261C17.4205 10.1852 17.3744 10.1162 17.3164 10.0582C17.2583 10.0001 17.1894 9.95403 17.1135 9.9226C17.0376 9.89118 16.9563 9.875 16.8742 9.875C16.7921 9.875 16.7107 9.89118 16.6349 9.9226C16.559 9.95403 16.4901 10.0001 16.432 10.0582ZM14.8164 9.06754C14.8744 9.00947 14.9205 8.94053 14.9519 8.86466C14.9834 8.78879 14.9995 8.70747 14.9995 8.62535C14.9995 8.54323 14.9834 8.46191 14.9519 8.38604C14.9205 8.31017 14.8744 8.24123 14.8164 8.18316C14.7583 8.12509 14.6894 8.07903 14.6135 8.0476C14.5376 8.01617 14.4563 8 14.3742 8C14.2921 8 14.2107 8.01617 14.1349 8.0476C14.059 8.07903 13.9901 8.12509 13.932 8.18316L12.057 10.0582C11.9397 10.1754 11.8738 10.3345 11.8738 10.5003C11.8738 10.6662 11.9397 10.8253 12.057 10.9425C12.1743 11.0598 12.3333 11.1257 12.4992 11.1257C12.665 11.1257 12.8241 11.0598 12.9414 10.9425L14.8164 9.06754ZM17.9414 5.05816C17.8833 5.00005 17.8144 4.95395 17.7385 4.9225C17.6627 4.89105 17.5813 4.87486 17.4992 4.87486C17.4171 4.87486 17.3357 4.89105 17.2599 4.9225C17.184 4.95395 17.115 5.00005 17.057 5.05816L15.807 6.30816C15.6897 6.42544 15.6238 6.5845 15.6238 6.75035C15.6238 6.9162 15.6897 7.07526 15.807 7.19254C15.9243 7.30981 16.0833 7.37569 16.2492 7.3757C16.415 7.3757 16.5741 7.30981 16.6914 7.19254L17.9414 5.94254C17.9995 5.88449 18.0456 5.81556 18.077 5.73969C18.1085 5.66381 18.1247 5.58248 18.1247 5.50035C18.1247 5.41821 18.1085 5.33688 18.077 5.26101C18.0456 5.18514 17.9995 5.11621 17.9414 5.05816ZM9.557 8.44254C9.61505 8.50065 9.68398 8.54674 9.75985 8.5782C9.83572 8.60965 9.91705 8.62584 9.99919 8.62584C10.0813 8.62584 10.1627 8.60965 10.2385 8.5782C10.3144 8.54674 10.3833 8.50065 10.4414 8.44254L16.0664 2.81754C16.1244 2.75947 16.1705 2.69053 16.2019 2.61466C16.2334 2.53879 16.2495 2.45747 16.2495 2.37535C16.2495 2.29323 16.2334 2.21191 16.2019 2.13604C16.1705 2.06017 16.1244 1.99123 16.0664 1.93316C16.0083 1.87509 15.9394 1.82903 15.8635 1.7976C15.7876 1.76618 15.7063 1.75 15.6242 1.75C15.5421 1.75 15.4607 1.76618 15.3849 1.7976C15.309 1.82903 15.2401 1.87509 15.182 1.93316L9.557 7.55816C9.49889 7.61621 9.45279 7.68514 9.42134 7.76101C9.38989 7.83688 9.3737 7.91821 9.3737 8.00035C9.3737 8.08248 9.38989 8.16381 9.42134 8.23969C9.45279 8.31556 9.49889 8.38449 9.557 8.44254ZM10.5929 16.0941C9.77242 16.9146 8.65957 17.3756 7.49919 17.3756C6.33881 17.3756 5.22595 16.9146 4.40544 16.0941C3.58492 15.2736 3.12396 14.1607 3.12396 13.0003C3.12396 11.84 3.58492 10.7271 4.40544 9.9066L10.8703 3.44253C10.9284 3.38447 10.9744 3.31553 11.0058 3.23966C11.0373 3.16379 11.0534 3.08247 11.0534 3.00035C11.0534 2.91823 11.0373 2.83691 11.0058 2.76104C10.9744 2.68517 10.9284 2.61623 10.8703 2.55816C10.8122 2.50009 10.7433 2.45403 10.6674 2.4226C10.5915 2.39118 10.5102 2.375 10.4281 2.375C10.346 2.375 10.2647 2.39118 10.1888 2.4226C10.1129 2.45403 10.044 2.50009 9.98591 2.55816L3.52184 9.023C2.99253 9.54377 2.57156 10.1642 2.28322 10.8484C1.99488 11.5327 1.84487 12.2673 1.84184 13.0098C1.83882 13.7524 1.98284 14.4882 2.2656 15.1747C2.54836 15.8613 2.96427 16.4852 3.48932 17.0102C4.01438 17.5353 4.63819 17.9512 5.32479 18.2339C6.01138 18.5167 6.74717 18.6607 7.4897 18.6577C8.23223 18.6547 8.96682 18.5047 9.65109 18.2163C10.3354 17.928 10.9558 17.507 11.4765 16.9777C11.5888 16.8595 11.6505 16.7022 11.6484 16.5392C11.6463 16.3762 11.5806 16.2205 11.4654 16.1053C11.3501 15.99 11.1944 15.9243 11.0314 15.9223C10.8684 15.9202 10.7111 15.9818 10.5929 16.0941Z" fill="url(#paint0_linear_2850_16638)"/>
<path d="M10.6242 13.0003C10.6242 13.6184 10.4409 14.2226 10.0975 14.7365C9.75415 15.2504 9.26609 15.6509 8.69507 15.8875C8.12405 16.124 7.49572 16.1859 6.88953 16.0653C6.28334 15.9447 5.72652 15.6471 5.28948 15.2101C4.85244 14.773 4.55481 14.2162 4.43423 13.61C4.31366 13.0038 4.37554 12.3755 4.61206 11.8045C4.84859 11.2334 5.24913 10.7454 5.76303 10.402C6.27693 10.0586 6.88112 9.87535 7.49919 9.87535C8.32799 9.87535 9.12285 10.2046 9.7089 10.7906C10.2949 11.3767 10.6242 12.1715 10.6242 13.0003ZM16.432 10.0582L12.682 13.8082C12.5647 13.9254 12.4988 14.0845 12.4988 14.2503C12.4988 14.4162 12.5647 14.5753 12.682 14.6925C12.7993 14.8098 12.9583 14.8757 13.1242 14.8757C13.29 14.8757 13.4491 14.8098 13.5664 14.6925L17.3164 10.9425C17.3744 10.8845 17.4205 10.8155 17.4519 10.7397C17.4834 10.6638 17.4995 10.5825 17.4995 10.5003C17.4995 10.4182 17.4834 10.3369 17.4519 10.261C17.4205 10.1852 17.3744 10.1162 17.3164 10.0582C17.2583 10.0001 17.1894 9.95403 17.1135 9.9226C17.0376 9.89118 16.9563 9.875 16.8742 9.875C16.7921 9.875 16.7107 9.89118 16.6349 9.9226C16.559 9.95403 16.4901 10.0001 16.432 10.0582ZM14.8164 9.06754C14.8744 9.00947 14.9205 8.94053 14.9519 8.86466C14.9834 8.78879 14.9995 8.70747 14.9995 8.62535C14.9995 8.54323 14.9834 8.46191 14.9519 8.38604C14.9205 8.31017 14.8744 8.24123 14.8164 8.18316C14.7583 8.12509 14.6894 8.07903 14.6135 8.0476C14.5376 8.01617 14.4563 8 14.3742 8C14.2921 8 14.2107 8.01617 14.1349 8.0476C14.059 8.07903 13.9901 8.12509 13.932 8.18316L12.057 10.0582C11.9397 10.1754 11.8738 10.3345 11.8738 10.5003C11.8738 10.6662 11.9397 10.8253 12.057 10.9425C12.1743 11.0598 12.3333 11.1257 12.4992 11.1257C12.665 11.1257 12.8241 11.0598 12.9414 10.9425L14.8164 9.06754ZM17.9414 5.05816C17.8833 5.00005 17.8144 4.95395 17.7385 4.9225C17.6627 4.89105 17.5813 4.87486 17.4992 4.87486C17.4171 4.87486 17.3357 4.89105 17.2599 4.9225C17.184 4.95395 17.115 5.00005 17.057 5.05816L15.807 6.30816C15.6897 6.42544 15.6238 6.5845 15.6238 6.75035C15.6238 6.9162 15.6897 7.07526 15.807 7.19254C15.9243 7.30981 16.0833 7.37569 16.2492 7.3757C16.415 7.3757 16.5741 7.30981 16.6914 7.19254L17.9414 5.94254C17.9995 5.88449 18.0456 5.81556 18.077 5.73969C18.1085 5.66381 18.1247 5.58248 18.1247 5.50035C18.1247 5.41821 18.1085 5.33688 18.077 5.26101C18.0456 5.18514 17.9995 5.11621 17.9414 5.05816ZM9.557 8.44254C9.61505 8.50065 9.68398 8.54674 9.75985 8.5782C9.83572 8.60965 9.91705 8.62584 9.99919 8.62584C10.0813 8.62584 10.1627 8.60965 10.2385 8.5782C10.3144 8.54674 10.3833 8.50065 10.4414 8.44254L16.0664 2.81754C16.1244 2.75947 16.1705 2.69053 16.2019 2.61466C16.2334 2.53879 16.2495 2.45747 16.2495 2.37535C16.2495 2.29323 16.2334 2.21191 16.2019 2.13604C16.1705 2.06017 16.1244 1.99123 16.0664 1.93316C16.0083 1.87509 15.9394 1.82903 15.8635 1.7976C15.7876 1.76618 15.7063 1.75 15.6242 1.75C15.5421 1.75 15.4607 1.76618 15.3849 1.7976C15.309 1.82903 15.2401 1.87509 15.182 1.93316L9.557 7.55816C9.49889 7.61621 9.45279 7.68514 9.42134 7.76101C9.38989 7.83688 9.3737 7.91821 9.3737 8.00035C9.3737 8.08248 9.38989 8.16381 9.42134 8.23969C9.45279 8.31556 9.49889 8.38449 9.557 8.44254ZM10.5929 16.0941C9.77242 16.9146 8.65957 17.3756 7.49919 17.3756C6.33881 17.3756 5.22595 16.9146 4.40544 16.0941C3.58492 15.2736 3.12396 14.1607 3.12396 13.0003C3.12396 11.84 3.58492 10.7271 4.40544 9.9066L10.8703 3.44253C10.9284 3.38447 10.9744 3.31553 11.0058 3.23966C11.0373 3.16379 11.0534 3.08247 11.0534 3.00035C11.0534 2.91823 11.0373 2.83691 11.0058 2.76104C10.9744 2.68517 10.9284 2.61623 10.8703 2.55816C10.8122 2.50009 10.7433 2.45403 10.6674 2.4226C10.5915 2.39118 10.5102 2.375 10.4281 2.375C10.346 2.375 10.2647 2.39118 10.1888 2.4226C10.1129 2.45403 10.044 2.50009 9.98591 2.55816L3.52184 9.023C2.99253 9.54377 2.57156 10.1642 2.28322 10.8484C1.99488 11.5327 1.84487 12.2673 1.84184 13.0098C1.83882 13.7524 1.98284 14.4882 2.2656 15.1747C2.54836 15.8613 2.96427 16.4852 3.48932 17.0102C4.01438 17.5353 4.63819 17.9512 5.32479 18.2339C6.01138 18.5167 6.74717 18.6607 7.4897 18.6577C8.23223 18.6547 8.96682 18.5047 9.65109 18.2163C10.3354 17.928 10.9558 17.507 11.4765 16.9777C11.5888 16.8595 11.6505 16.7022 11.6484 16.5392C11.6463 16.3762 11.5806 16.2205 11.4654 16.1053C11.3501 15.99 11.1944 15.9243 11.0314 15.9223C10.8684 15.9202 10.7111 15.9818 10.5929 16.0941Z" stroke="url(#paint1_linear_2850_16638)" stroke-width="0.3"/>
</g>
</g>
<defs>
<linearGradient id="paint0_linear_2850_16638" x1="1.95109" y1="1.75" x2="21.5698" y2="11.5208" gradientUnits="userSpaceOnUse">
<stop stop-color="#0CF1CA"/>
<stop offset="0.264423" stop-color="#0BD2B0"/>
<stop offset="0.307692" stop-color="#0CF1CA"/>
<stop offset="0.427885" stop-color="#0CF1CA"/>
<stop offset="0.466346" stop-color="#0FAF94"/>
<stop offset="0.591346" stop-color="#0CA288"/>
<stop offset="1" stop-color="#086253"/>
</linearGradient>
<linearGradient id="paint1_linear_2850_16638" x1="1.8418" y1="2.25694" x2="21.3121" y2="11.25" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

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

@@ -0,0 +1,11 @@
.debrid-badge {
display: flex;
align-items: center;
gap: 8px;
border-radius: 4px;
border: 1px solid rgba(12, 241, 202, 0.3);
background: rgba(12, 241, 202, 0.05);
color: #0cf1ca;
padding: 4px 8px;
font-size: 12px;
}

View File

@@ -0,0 +1,18 @@
import Meteor from "@renderer/assets/meteor.svg?react";
import "./debrid-badge.scss";
import { useTranslation } from "react-i18next";
export interface DebridBadgeProps {
collapsed?: boolean;
}
export function DebridBadge({ collapsed }: Readonly<DebridBadgeProps>) {
const { t } = useTranslation("hydra_cloud");
return (
<div className="debrid-badge">
<Meteor />
{!collapsed && t("debrid_description")}
</div>
);
}

View File

@@ -14,3 +14,4 @@ export * from "./toast/toast";
export * from "./badge/badge";
export * from "./confirmation-modal/confirmation-modal";
export * from "./suspense-wrapper/suspense-wrapper";
export * from "./debrid-badge/debrid-badge";

View File

@@ -11,6 +11,7 @@ export const DOWNLOADER_NAME = {
[Downloader.Datanodes]: "Datanodes",
[Downloader.Mediafire]: "Mediafire",
[Downloader.TorBox]: "TorBox",
[Downloader.Hydra]: "Nimbus",
};
export const MAX_MINUTES_TO_SHOW_IN_PLAYTIME = 120;

View File

@@ -86,7 +86,10 @@ export function GameDetailsContextProvider({
const [showGameOptionsModal, setShowGameOptionsModal] = useState(false);
const { getRepacksForObjectId } = useRepacks();
const repacks = getRepacksForObjectId(objectId);
const repacks = useMemo(() => {
return getRepacksForObjectId(objectId);
}, [getRepacksForObjectId, objectId]);
const { i18n } = useTranslation("game_details");

View File

@@ -59,6 +59,9 @@ declare global {
cb: (value: SeedingStatus[]) => void
) => () => Electron.IpcRenderer;
onHardDelete: (cb: () => void) => () => Electron.IpcRenderer;
checkDebridAvailability: (
magnets: string[]
) => Promise<Record<string, boolean>>;
/* Catalogue */
searchGames: (
@@ -149,6 +152,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>
@@ -157,14 +162,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>;
@@ -225,6 +237,11 @@ declare global {
showItemInFolder: (path: string) => Promise<void>;
getFeatures: () => Promise<string[]>;
getBadges: () => Promise<Badge[]>;
canInstallCommonRedist: () => Promise<boolean>;
installCommonRedist: () => Promise<void>;
onCommonRedistProgress: (
cb: (value: { log: string; complete: boolean }) => void
) => () => Electron.IpcRenderer;
platform: NodeJS.Platform;
/* Auto update */

View File

@@ -10,20 +10,11 @@ export interface HowLongToBeatEntry {
updatedAt: Date;
}
export interface CatalogueCache {
id?: number;
category: string;
games: { objectId: string; shop: GameShop }[];
createdAt: Date;
updatedAt: Date;
expiresAt: Date;
}
export const db = new Dexie("Hydra");
db.version(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,6 @@
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 { useTranslation } from "react-i18next";
export function useDate() {
@@ -21,22 +8,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 +17,7 @@ export function useDate() {
try {
return formatDistance(date, baseDate, {
...options,
locale: getDateLocale(),
locale: getDateLocale(language),
});
} catch (err) {
return "";
@@ -61,7 +32,7 @@ export function useDate() {
try {
return formatDistance(subMilliseconds(new Date(), millis), baseDate, {
...options,
locale: getDateLocale(),
locale: getDateLocale(language),
});
} catch (err) {
return "";
@@ -69,18 +40,13 @@ export function useDate() {
},
formatDateTime: (date: number | Date | string): string => {
const locale = getDateLocale();
return format(
date,
locale == enUS ? "MM/dd/yyyy - HH:mm" : "dd/MM/yyyy HH:mm"
language == "en" ? "MM-dd-yyyy - hh:mm a" : "dd/MM/yyyy HH:mm",
{ locale: getDateLocale(language) }
);
},
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

@@ -15,12 +15,14 @@ import type {
StartGameDownloadPayload,
} from "@types";
import { useDate } from "./use-date";
import { formatBytes } from "@shared";
import { formatBytes, formatBytesToMbps } from "@shared";
export function useDownload() {
const { updateLibrary } = useLibrary();
const { formatDistance } = useDate();
const userPrefs = useAppSelector((state) => state.userPreferences.value);
const { lastPacket, gamesWithDeletionInProgress } = useAppSelector(
(state) => state.download
);
@@ -99,8 +101,14 @@ export function useDownload() {
return gamesWithDeletionInProgress.includes(objectId);
};
const formatDownloadSpeed = (downloadSpeed: number): string => {
return userPrefs?.showDownloadSpeedInMegabytes
? `${formatBytes(downloadSpeed)}/s`
: formatBytesToMbps(downloadSpeed);
};
return {
downloadSpeed: `${formatBytes(lastPacket?.downloadSpeed ?? 0)}/s`,
downloadSpeed: formatDownloadSpeed(lastPacket?.downloadSpeed ?? 0),
progress: formatDownloadProgress(lastPacket?.progress ?? 0),
lastPacket,
eta: calculateETA(),

View File

@@ -1,8 +1,10 @@
import { useEffect, useState } from "react";
import { useEffect, useState, useCallback } from "react";
enum Feature {
CheckDownloadWritePermission = "CHECK_DOWNLOAD_WRITE_PERMISSION",
Torbox = "TORBOX",
TorBox = "TORBOX",
Nimbus = "NIMBUS",
NimbusPreview = "NIMBUS_PREVIEW",
}
export function useFeature() {
@@ -15,14 +17,17 @@ export function useFeature() {
});
}, []);
const isFeatureEnabled = (feature: Feature) => {
if (!features) {
const features = JSON.parse(localStorage.getItem("features") ?? "[]");
return features.includes(feature);
}
const isFeatureEnabled = useCallback(
(feature: Feature) => {
if (!features) {
const features = JSON.parse(localStorage.getItem("features") ?? "[]");
return features.includes(feature);
}
return features.includes(feature);
};
return features.includes(feature);
},
[features]
);
return {
isFeatureEnabled,

View File

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

View File

@@ -73,8 +73,11 @@
min-height: 140px;
max-height: 140px;
position: relative;
}
&--hydra {
box-shadow: 0px 0px 16px 0px rgba(12, 241, 202, 0.15);
}
}
&__cover {
width: 280px;
min-width: 280px;
@@ -145,4 +148,14 @@
padding: 8px;
min-height: unset;
}
&__hydra-gradient {
background: linear-gradient(90deg, #01483c 0%, #0cf1ca 50%, #01483c 100%);
box-shadow: 0px 0px 8px 0px rgba(12, 241, 202, 0.15);
width: 100%;
position: absolute;
bottom: 0;
height: 2px;
z-index: 1;
}
}

View File

@@ -1,4 +1,5 @@
import { useNavigate } from "react-router-dom";
import cn from "classnames";
import type { GameShop, LibraryGame, SeedingStatus } from "@types";
@@ -10,11 +11,11 @@ import {
import { Downloader, formatBytes, steamUrlBuilder } from "@shared";
import { DOWNLOADER_NAME } from "@renderer/constants";
import { useAppSelector, useDownload } from "@renderer/hooks";
import { useAppSelector, useDownload, useLibrary } from "@renderer/hooks";
import "./download-group.scss";
import { useTranslation } from "react-i18next";
import { useMemo } from "react";
import { useCallback, useMemo } from "react";
import {
DropdownMenu,
DropdownMenuItem,
@@ -22,6 +23,7 @@ import {
import {
ColumnsIcon,
DownloadIcon,
FileDirectoryIcon,
LinkIcon,
PlayIcon,
QuestionIcon,
@@ -31,8 +33,6 @@ import {
XCircleIcon,
} from "@primer/octicons-react";
import torBoxLogo from "@renderer/assets/icons/torbox.webp";
export interface DownloadGroupProps {
library: LibraryGame[];
title: string;
@@ -56,6 +56,8 @@ export function DownloadGroup({
(state) => state.userPreferences.value
);
const { updateLibrary } = useLibrary();
const {
lastPacket,
progress,
@@ -89,6 +91,14 @@ export function DownloadGroup({
return map;
}, [seedingStatus]);
const extractGameDownload = useCallback(
async (shop: GameShop, objectId: string) => {
await window.electron.extractGameDownload(shop, objectId);
updateLibrary();
},
[updateLibrary]
);
const getGameInfo = (game: LibraryGame) => {
const download = game.download!;
@@ -96,6 +106,10 @@ export function DownloadGroup({
const finalDownloadSize = getFinalDownloadSize(game);
const seedingStatus = seedingMap.get(game.id);
if (download.extracting) {
return <p>{t("extracting")}</p>;
}
if (isGameDeleting(game.id)) {
return <p>{t("deleting")}</p>;
}
@@ -197,6 +211,14 @@ export function DownloadGroup({
},
icon: <DownloadIcon />,
},
{
label: t("extract"),
disabled: game.download.extracting,
icon: <FileDirectoryIcon />,
onClick: () => {
extractGameDownload(game.shop, game.objectId);
},
},
{
label: t("stop_seeding"),
disabled: deleting,
@@ -287,7 +309,13 @@ export function DownloadGroup({
<ul className="download-group__downloads">
{library.map((game) => {
return (
<li key={game.id} className="download-group__item">
<li
key={game.id}
className={cn("download-group__item", {
"download-group__item--hydra":
game.download?.downloader === Downloader.Hydra,
})}
>
<div className="download-group__cover">
<div className="download-group__cover-backdrop">
<img
@@ -297,20 +325,7 @@ export function DownloadGroup({
/>
<div className="download-group__cover-content">
{game.download?.downloader === Downloader.TorBox ? (
<Badge>
<img
src={torBoxLogo}
alt="TorBox"
style={{ width: 13 }}
/>
<span>TorBox</span>
</Badge>
) : (
<Badge>
{DOWNLOADER_NAME[game.download!.downloader]}
</Badge>
)}
<Badge>{DOWNLOADER_NAME[game.download!.downloader]}</Badge>
</div>
</div>
</div>
@@ -351,6 +366,10 @@ export function DownloadGroup({
</DropdownMenu>
)}
</div>
{game.download?.downloader === Downloader.Hydra && (
<div className="download-group__hydra-gradient" />
)}
</li>
);
})}

View File

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

View File

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

View File

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

View File

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

View File

@@ -134,6 +134,7 @@ export function GameOptionsModal({
const handleClearExecutablePath = async () => {
await window.electron.updateExecutablePath(game.shop, game.objectId, null);
updateGame();
};

View File

@@ -1,14 +1,20 @@
import { useContext, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { Badge, Button, Modal, TextField } from "@renderer/components";
import {
Badge,
Button,
DebridBadge,
Modal,
TextField,
} from "@renderer/components";
import type { GameRepack } from "@types";
import { DownloadSettingsModal } from "./download-settings-modal";
import { gameDetailsContext } from "@renderer/context";
import { Downloader } from "@shared";
import { orderBy } from "lodash-es";
import { useDate } from "@renderer/hooks";
import { useDate, useFeature } from "@renderer/hooks";
import "./repacks-modal.scss";
export interface RepacksModalProps {
@@ -16,7 +22,8 @@ export interface RepacksModalProps {
startDownload: (
repack: GameRepack,
downloader: Downloader,
downloadPath: string
downloadPath: string,
automaticallyExtract: boolean
) => Promise<{ ok: boolean; error?: string }>;
onClose: () => void;
}
@@ -30,15 +37,57 @@ export function RepacksModal({
const [repack, setRepack] = useState<GameRepack | null>(null);
const [showSelectFolderModal, setShowSelectFolderModal] = useState(false);
const [hashesInDebrid, setHashesInDebrid] = useState<Record<string, boolean>>(
{}
);
const { repacks, game } = useContext(gameDetailsContext);
const { t } = useTranslation("game_details");
const { formatDate } = useDate();
const getHashFromMagnet = (magnet: string) => {
if (!magnet || typeof magnet !== "string") {
return null;
}
const hashRegex = /xt=urn:btih:([a-zA-Z0-9]+)/i;
const match = magnet.match(hashRegex);
return match ? match[1].toLowerCase() : null;
};
const { isFeatureEnabled, Feature } = useFeature();
useEffect(() => {
if (!isFeatureEnabled(Feature.NimbusPreview)) {
return;
}
const magnets = repacks.flatMap((repack) =>
repack.uris.filter((uri) => uri.startsWith("magnet:"))
);
window.electron.checkDebridAvailability(magnets).then((availableHashes) => {
setHashesInDebrid(availableHashes);
});
}, [repacks, isFeatureEnabled, Feature]);
const sortedRepacks = useMemo(() => {
return orderBy(repacks, (repack) => repack.uploadDate, "desc");
}, [repacks]);
return orderBy(
repacks,
[
(repack) => {
const magnet = repack.uris.find((uri) => uri.startsWith("magnet:"));
const hash = magnet ? getHashFromMagnet(magnet) : null;
return hash ? (hashesInDebrid[hash] ?? false) : false;
},
(repack) => repack.uploadDate,
],
["desc", "desc"]
);
}, [repacks, hashesInDebrid]);
useEffect(() => {
setFilteredRepacks(sortedRepacks);
@@ -109,6 +158,10 @@ export function RepacksModal({
{repack.fileSize} - {repack.repacker} -{" "}
{repack.uploadDate ? formatDate(repack.uploadDate) : ""}
</p>
{hashesInDebrid[getHashFromMagnet(repack.uris[0]) ?? ""] && (
<DebridBadge />
)}
</Button>
);
})}

View File

@@ -25,7 +25,7 @@ import "./sidebar.scss";
const achievementsPlaceholder: UserAchievement[] = [
{
displayName: "Timber!!",
name: "",
name: "1",
hidden: false,
description: "Chop down your first tree.",
icon: "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/105600/0fbb33098c9da39d1d4771d8209afface9c46e81.jpg",
@@ -36,7 +36,7 @@ const achievementsPlaceholder: UserAchievement[] = [
},
{
displayName: "Supreme Helper Minion!",
name: "",
name: "2",
hidden: false,
icon: "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/105600/0a6ff6a36670c96ceb4d30cf6fd69d2fdf55f38e.jpg",
icongray:
@@ -46,7 +46,7 @@ const achievementsPlaceholder: UserAchievement[] = [
},
{
displayName: "Feast of Midas",
name: "",
name: "3",
hidden: false,
icon: "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/apps/105600/2d10311274fe7c92ab25cc29afdca86b019ad472.jpg",
icongray:
@@ -122,8 +122,8 @@ export function Sidebar() {
<h3>{t("sign_in_to_see_achievements")}</h3>
</div>
<ul className="list achievements-placeholder__blur">
{achievementsPlaceholder.map((achievement, index) => (
<li key={index}>
{achievementsPlaceholder.map((achievement) => (
<li key={achievement.name}>
<div className="list__item">
<img
className={`list__item-image achievements-placeholder__blur ${

View File

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

View File

@@ -23,6 +23,7 @@ export function SettingsBehavior() {
enableAutoInstall: false,
seedAfterDownloadComplete: false,
showHiddenAchievementsDescription: false,
showDownloadSpeedInMegabytes: false,
});
const { t } = useTranslation("settings");
@@ -40,6 +41,8 @@ export function SettingsBehavior() {
userPreferences.seedAfterDownloadComplete ?? false,
showHiddenAchievementsDescription:
userPreferences.showHiddenAchievementsDescription ?? false,
showDownloadSpeedInMegabytes:
userPreferences.showDownloadSpeedInMegabytes ?? false,
});
}
}, [userPreferences]);
@@ -139,6 +142,16 @@ export function SettingsBehavior() {
})
}
/>
<CheckboxField
label={t("show_download_speed_in_megabytes")}
checked={form.showDownloadSpeedInMegabytes}
onChange={() =>
handleChange({
showDownloadSpeedInMegabytes: !form.showDownloadSpeedInMegabytes,
})
}
/>
</>
);
}

View File

@@ -70,14 +70,20 @@ export function SettingsDownloadSources() {
if (sourceUrl) setShowAddDownloadSourceModal(true);
}, [sourceUrl]);
const handleRemoveSource = (id: number) => {
const handleRemoveSource = (downloadSource: DownloadSource) => {
setIsRemovingDownloadSource(true);
const channel = new BroadcastChannel(`download_sources:delete:${id}`);
const channel = new BroadcastChannel(
`download_sources:delete:${downloadSource.id}`
);
downloadSourcesWorker.postMessage(["DELETE_DOWNLOAD_SOURCE", id]);
downloadSourcesWorker.postMessage([
"DELETE_DOWNLOAD_SOURCE",
downloadSource.id,
]);
channel.onmessage = () => {
showSuccessToast(t("removed_download_source"));
window.electron.removeDownloadSource(downloadSource.url);
getDownloadSources();
setIsRemovingDownloadSource(false);
@@ -96,7 +102,7 @@ export function SettingsDownloadSources() {
channel.onmessage = () => {
showSuccessToast(t("removed_download_sources"));
window.electron.removeDownloadSource("", true);
getDownloadSources();
setIsRemovingDownloadSource(false);
setShowConfirmationDeleteAllSourcesModal(false);
@@ -253,7 +259,7 @@ export function SettingsDownloadSources() {
<Button
type="button"
theme="outline"
onClick={() => handleRemoveSource(downloadSource.id)}
onClick={() => handleRemoveSource(downloadSource)}
disabled={isRemovingDownloadSource}
>
<NoEntryIcon />

View File

@@ -5,8 +5,12 @@
flex-direction: column;
gap: globals.$spacing-unit * 2;
&__notifications-title {
&__section-title {
margin-top: calc(globals.$spacing-unit * 2);
margin-bottom: globals.$spacing-unit;
}
&__common-redist-button {
align-self: flex-start;
}
}

View File

@@ -12,6 +12,8 @@ import languageResources from "@locales";
import { orderBy } from "lodash-es";
import { settingsContext } from "@renderer/context";
import "./settings-general.scss";
import { DesktopDownloadIcon } from "@primer/octicons-react";
import { logger } from "@renderer/logger";
interface LanguageOption {
option: string;
@@ -27,6 +29,9 @@ export function SettingsGeneral() {
(state) => state.userPreferences.value
);
const [canInstallCommonRedist, setCanInstallCommonRedist] = useState(false);
const [installingCommonRedist, setInstallingCommonRedist] = useState(false);
const [form, setForm] = useState({
downloadsPath: "",
downloadNotificationsEnabled: false,
@@ -47,6 +52,16 @@ export function SettingsGeneral() {
setDefaultDownloadsPath(path);
});
window.electron.canInstallCommonRedist().then((canInstall) => {
setCanInstallCommonRedist(canInstall);
});
const interval = setInterval(() => {
window.electron.canInstallCommonRedist().then((canInstall) => {
setCanInstallCommonRedist(canInstall);
});
}, 1000 * 5);
setLanguageOptions(
orderBy(
Object.entries(languageResources).map(([language, value]) => {
@@ -59,6 +74,10 @@ export function SettingsGeneral() {
"asc"
)
);
return () => {
clearInterval(interval);
};
}, []);
useEffect(() => {
@@ -90,7 +109,9 @@ export function SettingsGeneral() {
}
}, [userPreferences, defaultDownloadsPath]);
const handleLanguageChange = (event) => {
const handleLanguageChange = (
event: React.ChangeEvent<HTMLSelectElement>
) => {
const value = event.target.value;
handleChange({ language: value });
@@ -114,6 +135,28 @@ export function SettingsGeneral() {
}
};
useEffect(() => {
const unlisten = window.electron.onCommonRedistProgress(
({ log, complete }) => {
if (log === "Installation timed out" || complete) {
setInstallingCommonRedist(false);
}
}
);
return () => unlisten();
}, []);
const handleInstallCommonRedist = async () => {
setInstallingCommonRedist(true);
try {
await window.electron.installCommonRedist();
} catch (err) {
logger.error(err);
setInstallingCommonRedist(false);
}
};
return (
<div className="settings-general">
<TextField
@@ -139,9 +182,7 @@ export function SettingsGeneral() {
}))}
/>
<p className="settings-general__notifications-title">
{t("notifications")}
</p>
<h2 className="settings-general__section-title">{t("notifications")}</h2>
<CheckboxField
label={t("enable_download_notifications")}
@@ -185,6 +226,23 @@ export function SettingsGeneral() {
})
}
/>
<h2 className="settings-general__section-title">{t("common_redist")}</h2>
<p className="settings-general__common-redist-description">
{t("common_redist_description")}
</p>
<Button
onClick={handleInstallCommonRedist}
className="settings-general__common-redist-button"
disabled={!canInstallCommonRedist || installingCommonRedist}
>
<DesktopDownloadIcon />
{installingCommonRedist
? t("installing_common_redist")
: t("install_common_redist")}
</Button>
</div>
);
}

View File

@@ -16,7 +16,7 @@ const TORBOX_URL = torBoxReferralCode
: "https://torbox.app";
const TORBOX_API_TOKEN_URL = "https://torbox.app/settings";
export function SettingsTorbox() {
export function SettingsTorBox() {
const userPreferences = useAppSelector(
(state) => state.userPreferences.value
);

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