Compare commits

...

195 Commits

Author SHA1 Message Date
Zamitto
cb758cceda chore: change header 2025-06-11 07:13:57 -03:00
Zamitto
8b804271bd Merge pull request #1770 from hydrasources/patch-17
Update translation.json
2025-06-11 06:03:55 -03:00
Zamitto
57813784d2 Merge pull request #1759 from hydralauncher/feat/optimize-achievements-sync
feat: optimize achievements sync [HYD-863]
2025-06-11 06:03:44 -03:00
Zamitto
7cbb8a00c4 chor: bump version 2025-06-11 05:59:44 -03:00
Zamitto
4a4cb57348 feat: refactor 2025-06-11 05:58:56 -03:00
Zamitto
7a82467933 feat: refactor 2025-06-11 05:49:37 -03:00
Zamitto
6a65d191af fix: text alignment 2025-06-11 05:47:23 -03:00
hydrasources
d047d7a105 Update translation.json 2025-06-09 14:13:07 +03:00
Zamitto
f05d0c2047 chore: remove unwanted files 2025-06-06 17:13:27 -03:00
Zamitto
98e5b70f2e chore: unused import 2025-06-06 17:12:06 -03:00
Zamitto
100ddd79aa feat: add If-Modified-Since header to get-game-achievement-data 2025-06-06 16:52:40 -03:00
Zamitto
0b2d4e2ba0 feat: update CatalogueSearchResult types 2025-06-03 16:22:21 -03:00
Zamitto
0c379d6c49 feat: combined notification when add game to library 2025-06-03 16:04:29 -03:00
Zamitto
8e6f9fdb00 fix: syncing achievements when game is deleted 2025-06-03 15:53:40 -03:00
Zamitto
6b1713e54b fix: achievement notification showing 0 points 2025-06-03 15:32:50 -03:00
Zamitto
44db5f9813 feat: use promise all on pre search achievement 2025-06-03 15:32:30 -03:00
Zamitto
7d0fbbd960 feat: small refactor 2025-06-03 13:40:23 -03:00
Zamitto
4552256038 feat: small refactor 2025-06-03 12:23:50 -03:00
Zamitto
c5be5e94e8 feat: add delay to pre search achievements 2025-06-03 11:26:01 -03:00
Zamitto
3a6693c8b1 feat: small refactor to achievements 2025-06-03 09:47:08 -03:00
Zamitto
e9032ae6e4 feat: better friend code validation on add friend modal 2025-06-03 07:12:53 -03:00
Zamitto
7202f740d3 feat: sync on open game 2025-06-02 21:46:00 -03:00
Zamitto
2a74526b0f feat: optimize achievements sync 2025-06-02 21:42:21 -03:00
Chubby Granny Chaser
bacf6804e4 ci: bumping version 2025-05-31 22:05:05 +01:00
Chubby Granny Chaser
c60584c613 ci: bumping version 2025-05-31 19:01:32 +01:00
Chubby Granny Chaser
e2482a6c8f Merge pull request #1746 from hydralauncher/feat/HYD-858
Feat/hyd 858
2025-05-30 14:18:29 +01:00
Chubby Granny Chaser
27cbe755bf Merge branch 'main' into feat/HYD-858 2025-05-30 14:15:59 +01:00
Chubby Granny Chaser
046debffa0 fix: fixing typo on length 2025-05-30 14:14:39 +01:00
Zamitto
4b32015a73 Merge pull request #1747 from hydralauncher/feature/hyd-859
feat: sync favorite games
2025-05-30 10:13:02 -03:00
Chubby Granny Chaser
4d950b30fb feat: adding motion 2025-05-30 14:07:59 +01:00
Zamitto
f50f1e51e4 fix: sync data overwriting local game data 2025-05-30 10:03:07 -03:00
Chubby Granny Chaser
97a414e77f fix: adding correct binary path 2025-05-30 13:20:21 +01:00
Zamitto
46a6c8c987 chore: remove debug constant 2025-05-30 08:51:52 -03:00
Zamitto
5305e5ca18 feat: better handle friend code input 2025-05-30 08:40:55 -03:00
Zamitto
6757ebe13c feat: syncing favorite games 2025-05-30 08:39:40 -03:00
Chubby Granny Chaser
1dc91562ec Merge branch 'feat/HYD-858' of github.com:hydralauncher/hydra into feat/HYD-858 2025-05-30 12:37:36 +01:00
Chubby Granny Chaser
2e2785c33c fix: fixing duplicate css 2025-05-30 12:36:43 +01:00
Chubby Granny Chaser
cf14f5a758 Merge branch 'main' into feat/HYD-858 2025-05-30 12:30:38 +01:00
Chubby Granny Chaser
2ebd43d55c feat: removing test backup 2025-05-30 12:29:56 +01:00
Chubby Granny Chaser
dac29767bd feat: adding backup freezing 2025-05-30 12:28:27 +01:00
Chubby Granny Chaser
4571c7cf33 Merge pull request #1732 from CallMeLeon167/main
add missing German translations
2025-05-30 05:56:34 +01:00
Chubby Granny Chaser
b8e3756dd9 Merge branch 'main' into main 2025-05-30 05:45:50 +01:00
Chubby Granny Chaser
df92852123 Merge pull request #1726 from v1ctorsales/fix-catalogue-decode-publishers
[fix] decoding html chars for publishers
2025-05-30 05:45:34 +01:00
Chubby Granny Chaser
d6afcff5d2 Merge branch 'main' into fix-catalogue-decode-publishers 2025-05-30 05:38:27 +01:00
Chubby Granny Chaser
9e9adfcc07 Merge branch 'main' of github.com:hydralauncher/hydra 2025-05-30 04:25:53 +01:00
Chubby Granny Chaser
7c425eeccc feat: adding freezing backups logic 2025-05-30 04:25:35 +01:00
Zamitto
b55e33f61a Merge pull request #1734 from hydralauncher/feat/HYD-832
feat: sync game data when opening game page
2025-05-26 12:21:21 -03:00
Zamitto
358c15163a fix: remove unneeded call to current game 2025-05-26 11:57:54 -03:00
Zamitto
0225e31947 feat: add await to update level game 2025-05-26 11:50:08 -03:00
Zamitto
d2b3508b5b fix: refactor and fix types 2025-05-26 11:47:36 -03:00
Zamitto
1c6bc49ed0 feat: sync game data when opening game page 2025-05-26 10:06:19 -03:00
Leon
e2ecfa3e3c Grammar error in German text 2025-05-26 00:18:20 +02:00
Leon
875ef47938 add apostrophe to user_achievements German translation 2025-05-26 00:05:24 +02:00
Leon
550ac383e9 add missing German translations 2025-05-25 23:56:42 +02:00
Chubby Granny Chaser
7a196e4315 chore: changing ping pong method 2025-05-24 20:30:46 +01:00
Victor Sales
8a6ed411ef fix: decoding html chars for publishers 2025-05-24 14:02:33 -03:00
Zamitto
4893d61ee3 Merge pull request #1721 from Shyzuuu/patch-4
Update French translation with new translations
2025-05-22 14:49:44 -03:00
Zamitto
a8482b2311 Merge pull request #1722 from hydralauncher/feat/persist-playtime-if-sync-fails-after-closing-app
feat: persist playtime if sync fails after closing app
2025-05-22 14:49:21 -03:00
Zamitto
e734b6937a fix: staging hard coded true 2025-05-22 14:45:30 -03:00
Zamitto
eab9f92b3e feat: ensure playtime is not lost when hydra closes 2025-05-22 13:12:45 -03:00
Zamitto
772aea69a9 feat: persist playtime that failed to sync on leveldb 2025-05-22 13:11:32 -03:00
Zamitto
f9d5cfce73 fix: change ticks so it syncs playtime each 2 minutes 2025-05-22 13:08:24 -03:00
Shyzuu
33cad40d5c Update French translation with new translations 2025-05-22 17:15:42 +02:00
Zamitto
ab7f29099d Merge pull request #1709 from hydralauncher/chore/update-deps
chore: update some dependencies
2025-05-21 19:14:18 -03:00
Chubby Granny Chaser
70d63934a6 Merge branch 'main' of github.com:hydralauncher/hydra 2025-05-21 19:45:14 +01:00
Chubby Granny Chaser
61dae4cf84 fix: fixing ping pong 2025-05-21 19:44:24 +01:00
Zamitto
022673322b Merge branch 'main' into chore/update-deps 2025-05-21 15:40:05 -03:00
Zamitto
9e321e9c69 Merge pull request #1716 from rexobo/Update-Swedish-translation
Update translation.json
2025-05-21 15:39:52 -03:00
Zamitto
0fc46236fc Merge pull request #1710 from mikropsoft/patch-1
Update Turkish
2025-05-21 15:38:19 -03:00
Zamitto
91c03ef5a5 Merge pull request #1712 from Hoaper/main
Feature: Show only playable games at sidebar
2025-05-21 15:37:56 -03:00
Zamitto
ba6d04ced7 Merge branch 'main' into main 2025-05-21 15:22:43 -03:00
rexobo
d26635784f Merge branch 'main' into Update-Swedish-translation 2025-05-21 20:19:10 +02:00
rexobo
12fc2fc1fb Update translation.json 2025-05-21 20:15:08 +02:00
Chubby Granny Chaser
05e8d53783 Merge branch 'main' of github.com:hydralauncher/hydra 2025-05-21 19:08:10 +01:00
Chubby Granny Chaser
ae77444b2d fix: adding ludusavi copy 2025-05-21 19:07:18 +01:00
mx
88dae597ea Merge branch 'main' into main 2025-05-21 22:49:44 +05:00
Hoaper
4dd11db8f4 prettier changes 2025-05-21 22:48:48 +05:00
Hoaper
4ac8f1f246 Working on suggestions of greptile-apps bot 2025-05-21 22:41:27 +05:00
Zamitto
72f031b0ae fix: prevent unexpected undefined playTimeInMilliseconds value 2025-05-21 11:06:49 -03:00
Zamitto
c8c492bf1a chore: remove duplicated level call 2025-05-21 11:06:19 -03:00
𝗛𝗼𝗹𝗶
eb3c1a0c8b Update translation.json 2025-05-21 16:13:10 +03:00
𝗛𝗼𝗹𝗶
c8ad04b065 Merge branch 'main' into patch-1 2025-05-21 15:55:42 +03:00
Zamitto
efbdaab27b Merge pull request #1711 from Wkeynhk/patch-1
Update Russian Translation
2025-05-21 09:48:44 -03:00
Hoaper
5f7b6158a2 Removed unused package.json propetry 2025-05-19 22:50:39 +05:00
Hoaper
51931df2d2 Merge branch 'main' of https://github.com/hydralauncher/hydra 2025-05-19 22:39:18 +05:00
Hoaper
2b8fd61c16 updated to merge 2025-05-19 22:39:04 +05:00
Hoaper
7c2a847024 playable button 2025-05-19 22:38:36 +05:00
Hoaper
659f811c09 package.json updated to merge 2025-05-19 22:36:01 +05:00
Hoaper
2224b00c57 Added playable button feauture that shows only games I could play now 2025-05-19 22:33:32 +05:00
𝗛𝗼𝗹𝗶
b56ed48855 Update src/locales/tr/translation.json
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-05-19 18:51:48 +03:00
𝗛𝗼𝗹𝗶
d3ed8dee7c Update src/locales/tr/translation.json
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-05-19 18:51:40 +03:00
𝗛𝗼𝗹𝗶
54a40d0ccc Update src/locales/tr/translation.json
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-05-19 18:51:34 +03:00
Wkeynhk
186837d9f9 Update translation.json 2025-05-19 18:46:19 +03:00
𝗛𝗼𝗹𝗶
ec3920fc34 Update Turkish
I have updated the translation according to the latest update; the previous contributor made significant changes, but I find my version more accurate. For easier comparison and separation of changes, I also left blank spaces between sections. We can also get his feedback if needed.
2025-05-19 18:46:03 +03:00
Zamitto
5a3aa7e8c6 chore: update some dependencies 2025-05-19 11:34:52 -03:00
Zamitto
7bb7d2e388 feat: missing change on PR (update ref on shadow dom section) 2025-05-19 11:33:41 -03:00
Zamitto
b1fc9073d6 Merge pull request #1704 from bankov4eto/main
Update translation.json
2025-05-19 11:27:37 -03:00
Zamitto
a1a86c7045 Merge pull request #1707 from rexobo/add-swedish-translation
feat: Add Swedish translation
2025-05-19 11:27:04 -03:00
Zamitto
81cecfe558 Merge pull request #1708 from hydralauncher/fix/HYD-828
fix: ensure achievement notification preview has same styles as actual notification [HYD-828]
2025-05-19 11:26:17 -03:00
Zamitto
9a0e3bfc65 Update src/locales/en/translation.json
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2025-05-19 11:14:42 -03:00
Zamitto
f7b88b6d31 chore: bump version 2025-05-19 10:59:54 -03:00
Zamitto
a996519bd8 feat: open dev tools for theme editor 2025-05-19 07:28:55 -03:00
Zamitto
e85d08422e feat: add option to disable friend starting game notification 2025-05-19 07:23:46 -03:00
Zamitto
73de69b5a6 feat: use shadow dom on theme editor for achievement notifications 2025-05-18 21:28:45 -03:00
rexobo
9172098027 Update translation.json 2025-05-18 18:39:56 +02:00
rexobo
f19391200c Update translation.json 2025-05-18 18:25:23 +02:00
rexobo
5e51877660 fix: Resolve formatting issues in Swedish translation 2025-05-18 18:21:25 +02:00
rexobo
6bc2d83ffb feat: Add Swedish translation 2025-05-18 17:07:03 +02:00
bankov4eto
75ac9e8281 Update translation.json
Updated translations and added new missing strings.
2025-05-18 10:39:29 +03:00
Zamitto
650b02e673 Merge pull request #1703 from hydralauncher/fix/HYD-827
fix: shadow dom to isolate achievements window and custom css refactor
2025-05-18 00:04:25 -03:00
Zamitto
93929ae15f chore: bump version 2025-05-18 00:02:13 -03:00
Zamitto
95eecb7161 feat: remove console log and ensure background is transparent on browser window 2025-05-17 23:55:22 -03:00
Zamitto
0b83554565 feat: shadow dom to isolate achievements window and custom css refactor 2025-05-17 23:48:30 -03:00
Chubby Granny Chaser
4485f62946 Merge pull request #1701 from hydralauncher/fix/HYD-826
fix: fixing aria2c binary
2025-05-18 02:56:29 +01:00
Chubby Granny Chaser
42c3671965 fix: fixing aria2c binary 2025-05-18 01:38:29 +01:00
Zamitto
a5aabe0ad7 fix: notifications not working on first run 2025-05-17 20:32:24 -03:00
Zamitto
276c098fbc chore: bump version 2025-05-17 19:35:02 -03:00
Zamitto
3455812a43 Merge pull request #1698 from hydralauncher/feat/HYD-822
feat: new game achievement notification [hyd-822]
2025-05-17 19:33:53 -03:00
Zamitto
87a994f0f0 feat: i18n and preview fix 2025-05-17 19:32:32 -03:00
Zamitto
15ddc71445 feat: i18n 2025-05-17 19:24:02 -03:00
Zamitto
ee916b998a feat: refactor test notiication event 2025-05-17 19:06:01 -03:00
Chubby Granny Chaser
914942d328 feat: renaming class names to BEM 2025-05-17 23:00:33 +01:00
Chubby Granny Chaser
5ae67a3dc7 feat: renaming class names to BEM 2025-05-17 22:59:38 +01:00
Zamitto
5475708b36 feat: i18n 2025-05-17 18:55:01 -03:00
Zamitto
c85f46844e feat: adjust gradient angle 2025-05-17 18:40:07 -03:00
Zamitto
1247a105a0 feat: info for rare and platinum achievements 2025-05-17 18:07:18 -03:00
Zamitto
3cc4ee3ee4 feat: trophy gradient for variations 2025-05-17 17:42:46 -03:00
Zamitto
7fca31338c fix: trophy and ellipses 2025-05-17 17:18:49 -03:00
Zamitto
0d747d03ab feat: refactor css 2025-05-17 17:14:09 -03:00
Zamitto
6a59036e21 feat: alignments 2025-05-17 16:56:09 -03:00
Zamitto
baddd4a99b feat: animation and borders 2025-05-17 14:29:01 -03:00
Zamitto
c40d26ef0a feat: refactor and adding variation animations 2025-05-17 02:36:12 -03:00
Zamitto
e4f7747200 feat: improve and fix animations 2025-05-16 18:14:18 -03:00
Zamitto
bc06ae5c03 feat: notification preview on theme editor 2025-05-16 16:18:19 -03:00
Zamitto
39c073634c feat: animation update 2025-05-16 06:56:21 -03:00
Zamitto
c5beeb861e feat: i18n update 2025-05-16 05:45:06 -03:00
Zamitto
0a4bdf160c feat: i18n and refactor 2025-05-16 05:19:33 -03:00
Zamitto
6f43da8d28 feat: achievement notification custom position and animations 2025-05-15 19:42:23 -03:00
Zamitto
42e8a68c08 Merge branch 'main' into feat/HYD-822 2025-05-14 19:55:43 -03:00
Zamitto
f960bb4f6f feat: set achievements cache only if game has achievements 2025-05-14 19:52:43 -03:00
Chubby Granny Chaser
7f988c0bba Merge pull request #1689 from hydralauncher/feat/HYD-819
feat: adding possibility to create steam shortcut
2025-05-14 21:57:54 +01:00
Chubby Granny Chaser
dcf05d3386 Merge branch 'main' into feat/HYD-819 2025-05-14 21:52:53 +01:00
Zamitto
96385d90d8 feat: custom achievement notification position 2025-05-14 17:42:30 -03:00
Zamitto
96cfa8c015 feat: re adding achievement notification window 2025-05-14 16:37:49 -03:00
Zamitto
ae067efd5e feat: delay SystemPath validations 2025-05-14 08:44:58 -03:00
Chubby Granny Chaser
8c16779052 feat: adding logging to steam copy 2025-05-14 10:58:40 +01:00
Chubby Granny Chaser
5c7a289299 fix: adding greptile fixes 2025-05-14 10:17:44 +01:00
Chubby Granny Chaser
e8e524182a feat: only downloading files once 2025-05-14 10:11:29 +01:00
Chubby Granny Chaser
521d9faa0c feat: automatically adding wine prefix 2025-05-14 00:50:30 +01:00
Chubby Granny Chaser
ca7ac73836 feat: adding shortcut for all users 2025-05-14 00:17:40 +01:00
Chubby Granny Chaser
ed42935e7b feat: adding shortcut for all users 2025-05-14 00:16:48 +01:00
Chubby Granny Chaser
f0c5ec6f1a feat: adding dynamic dir to get steam user id 2025-05-13 23:38:47 +01:00
Chubby Granny Chaser
66ced3c779 fix: fixing error message for path being used 2025-05-13 23:16:38 +01:00
Chubby Granny Chaser
4f8212f8e3 feat: using hydralauncher fork 2025-05-13 23:12:43 +01:00
Chubby Granny Chaser
86de5aa89e feat: adding possibility to create steam shortcut 2025-05-13 22:57:33 +01:00
Chubby Granny Chaser
00065ab0c9 Merge pull request #1679 from hydralauncher/feat/cross-cloud-save
feat: adding cross cloud save
2025-05-13 19:58:22 +01:00
Chubby Granny Chaser
e89202f750 feat: adding standalone aria2c 2025-05-13 01:24:29 +01:00
Chubby Granny Chaser
1df2353f06 fix: using realpath for fedora wine prefix 2025-05-12 17:31:47 +01:00
Chubby Granny Chaser
475ab4119b fix: removing replace from transformation 2025-05-12 12:55:55 +01:00
Chubby Granny Chaser
1346ff49a5 fix: adding path transformation for wine 2025-05-12 12:11:37 +01:00
Chubby Granny Chaser
4ff0132d53 fix: fixing home dir mapping 2025-05-12 11:54:57 +01:00
Chubby Granny Chaser
749a88b2b6 feat: adding wine prefix to backup creation on linux 2025-05-12 10:55:44 +01:00
Chubby Granny Chaser
427b77c597 fix: fixing return logic for wine prefix 2025-05-12 02:25:32 +00:00
Chubby Granny Chaser
e901df9ac7 fix: fixing casing for appimage 2025-05-11 19:04:15 -07:00
Chubby Granny Chaser
43e565bcc9 ci: adding appimage to build 2025-05-11 18:46:15 -07:00
Chubby Granny Chaser
f4e710c7d1 fix: fixing ludusavi download 2025-05-12 01:58:29 +01:00
Chubby Granny Chaser
592ac45740 feat: adding cross cloud save 2025-05-11 19:07:30 +01:00
Chubby Granny Chaser
6c55d667bd chore: bump version 2025-05-10 19:38:03 +01:00
Chubby Granny Chaser
a4bdc3b5f0 Merge pull request #1671 from hydralauncher/feat/HYD-781
Feat/hyd 781
2025-05-10 19:06:30 +01:00
Chubby Granny Chaser
44dc8f73e8 feat: adding staging urls for ws 2025-05-10 18:54:55 +01:00
Chubby Granny Chaser
8b8ead531d ci: updating build process 2025-05-10 18:08:47 +01:00
Chubby Granny Chaser
ec40dfdb0b ci: adding sonar ignore 2025-05-10 17:49:03 +01:00
Chubby Granny Chaser
74d93da9b3 chore: adding protobuf ts as dev dependency 2025-05-10 17:46:17 +01:00
Chubby Granny Chaser
216f813771 feat: adding new friend session notification 2025-05-10 17:43:09 +01:00
Chubby Granny Chaser
fee9cfb3e8 Merge branch 'main' of github.com:hydralauncher/hydra into feat/HYD-781 2025-05-10 17:20:57 +01:00
Chubby Granny Chaser
dcd00cda98 Merge pull request #1662 from hydralauncher/feat/get-image-assets-from-api
feat: getting image assets from api [HYD-811]
2025-05-10 17:20:36 +01:00
Zamitto
c9135715fa feat: refactor 2025-05-09 18:59:32 -03:00
Zamitto
64cea7ff85 feat: simplify get user event 2025-05-09 18:50:14 -03:00
Zamitto
382a618c3f feat: refactor assets in game details page 2025-05-09 18:30:45 -03:00
Chubby Granny Chaser
d906e3f145 ci: updating build to support ws url 2025-05-09 20:55:29 +01:00
Chubby Granny Chaser
e987b27aec ci: updating build to support ws url 2025-05-09 20:53:53 +01:00
Chubby Granny Chaser
18815a027f ci: updating build to support ws url 2025-05-09 20:53:21 +01:00
Chubby Granny Chaser
6c44cc0cc4 ci: updating build to support ws url 2025-05-09 20:52:03 +01:00
Zamitto
171c728616 Merge branch 'main' into feat/get-image-assets-from-api 2025-05-09 16:03:37 -03:00
Chubby Granny Chaser
b4ff16cfa4 Merge pull request #1667 from hydralauncher/fix/HYD-807
fix: storing rpc encrypted password
2025-05-09 15:06:23 +01:00
Chubby Granny Chaser
eb6317e659 fix: adding fallback to language 2025-05-09 13:18:38 +01:00
Zamitto
8377f85f0b fix: undo isStaging change 2025-05-09 08:49:58 -03:00
Zamitto
10504cdaf8 Merge branch 'main' into feat/get-image-assets-from-api 2025-05-09 08:49:37 -03:00
Chubby Granny Chaser
a01c77b424 fix: storing rpc encrypted password 2025-05-09 10:29:25 +01:00
Zamitto
408adb566c fix: add game to library failing 2025-05-08 21:24:52 -03:00
Zamitto
cf818d0f4f feat: get game assets from stats 2025-05-08 09:19:12 -03:00
Zamitto
48e9536169 feat: adjust isLoading on game details context 2025-05-08 09:05:49 -03:00
Zamitto
00c589a138 feat: get image assets from api 2025-05-08 08:53:51 -03:00
Zamitto
30584492af feat: get image assets from api 2025-05-07 21:05:50 -03:00
Chubby Granny Chaser
aa18b57ada feat: adding ws 2025-04-29 10:05:27 +01:00
153 changed files with 6308 additions and 2080 deletions

View File

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

View File

@@ -4,3 +4,4 @@ out
.gitignore
migration.stub
hydra-python-rpc/
src/main/generated/

View File

@@ -48,6 +48,7 @@ jobs:
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_STAGING_URL }}
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -63,6 +64,7 @@ jobs:
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
MAIN_VITE_AUTH_URL: ${{ vars.MAIN_VITE_STAGING_AUTH_URL }}
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_STAGING_CHECKOUT_URL }}
MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_STAGING_URL }}
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
@@ -97,3 +99,4 @@ jobs:
dist/*.yml
dist/*.blockmap
dist/*.pacman
dist/*.AppImage

View File

@@ -51,6 +51,7 @@ jobs:
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
@@ -66,6 +67,7 @@ jobs:
MAIN_VITE_CHECKOUT_URL: ${{ vars.MAIN_VITE_CHECKOUT_URL }}
RENDERER_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
MAIN_VITE_EXTERNAL_RESOURCES_URL: ${{ vars.EXTERNAL_RESOURCES_URL }}
MAIN_VITE_WS_URL: ${{ vars.MAIN_VITE_WS_URL }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}

3
.gitignore vendored
View File

@@ -7,7 +7,8 @@ out
*.log*
.env
.vite
ludusavi/
ludusavi/**
!ludusavi/config.yaml
hydra-python-rpc/
.python-version

3
.gitmodules vendored Normal file
View File

@@ -0,0 +1,3 @@
[submodule "proto"]
path = proto
url = https://github.com/hydralauncher/hydra-protos.git

BIN
binaries/aria2c Executable file

Binary file not shown.

BIN
binaries/aria2c.exe Executable file

Binary file not shown.

View File

@@ -3,7 +3,6 @@ productName: Hydra
directories:
buildResources: build
extraResources:
- aria2
- ludusavi
- hydra-python-rpc
- seeds
@@ -21,6 +20,7 @@ asarUnpack:
win:
executableName: Hydra
extraResources:
- from: binaries/aria2c.exe
- from: binaries/7z.exe
- from: binaries/7z.dll
target:
@@ -51,6 +51,7 @@ dmg:
linux:
extraResources:
- from: binaries/7zzs
- from: binaries/aria2c
target:
- AppImage
- snap

6
ludusavi/config.yaml Normal file
View File

@@ -0,0 +1,6 @@
manifest:
enable: false
secondary:
- url: https://cdn.losbroxas.org/manifest.yaml
enable: true
customGames: []

View File

@@ -1,6 +1,6 @@
{
"name": "hydralauncher",
"version": "3.4.8",
"version": "3.6.1",
"description": "Hydra",
"main": "./out/main/index.js",
"author": "Los Broxas",
@@ -28,7 +28,8 @@
"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"
"prepare": "husky",
"protoc": "npx protoc --ts_out src/main/generated --proto_path proto proto/*.proto"
},
"dependencies": {
"@electron-toolkit/preload": "^3.0.0",
@@ -48,32 +49,37 @@
"classnames": "^2.5.1",
"color": "^4.2.3",
"color.js": "^1.2.0",
"crc": "^4.3.2",
"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.6.2",
"file-type": "^19.6.0",
"file-type": "^20.5.0",
"framer-motion": "^12.15.0",
"i18next": "^23.11.2",
"i18next-browser-languagedetector": "^7.2.1",
"jsdom": "^24.0.0",
"jsonwebtoken": "^9.0.2",
"lodash-es": "^4.17.21",
"parse-torrent": "^11.0.17",
"piscina": "^4.7.0",
"rc-virtual-list": "^3.16.1",
"parse-torrent": "^11.0.18",
"rc-virtual-list": "^3.18.3",
"react-hook-form": "^7.53.0",
"react-i18next": "^14.1.0",
"react-loading-skeleton": "^3.4.0",
"react-redux": "^9.1.1",
"react-router-dom": "^6.22.3",
"react-tooltip": "^5.28.0",
"react-shadow": "^20.6.0",
"react-tooltip": "^5.28.1",
"sound-play": "^1.1.0",
"steam-shortcut-editor": "https://github.com/hydralauncher/steam-shortcut-editor",
"sudo-prompt": "^9.2.1",
"tar": "^7.4.3",
"tough-cookie": "^5.1.1",
"user-agents": "^1.1.387",
"winreg": "^1.2.5",
"ws": "^8.18.1",
"yaml": "^2.6.1",
"yup": "^1.5.0",
"zod": "^3.24.1"
@@ -85,6 +91,7 @@
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
"@electron-toolkit/eslint-config-ts": "^2.0.0",
"@electron-toolkit/tsconfig": "^1.0.1",
"@protobuf-ts/plugin": "^2.10.0",
"@swc/core": "^1.4.16",
"@types/auto-launch": "^5.0.5",
"@types/color": "^3.0.6",
@@ -97,10 +104,12 @@
"@types/react-dom": "^18.2.18",
"@types/sound-play": "^1.1.3",
"@types/user-agents": "^1.0.4",
"@types/winreg": "^1.2.36",
"@types/ws": "^8.18.1",
"@vitejs/plugin-react": "^4.2.1",
"electron": "^31.7.7",
"electron": "^32.3.3",
"electron-builder": "^26.0.12",
"electron-vite": "^2.3.0",
"electron-vite": "^3.0.0",
"eslint": "^8.56.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.4",
@@ -114,5 +123,6 @@
"typescript": "^5.3.3",
"vite": "^5.0.12",
"vite-plugin-svgr": "^4.2.0"
}
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
}

1
proto Submodule

Submodule proto added at 7a23620f93

View File

@@ -102,7 +102,7 @@ def process_list():
if auth_error:
return auth_error
process_list = [proc.info for proc in psutil.process_iter(['exe', 'pid', 'name'])]
process_list = [proc.info for proc in psutil.process_iter(['exe', 'cwd', 'pid', 'name', 'environ'])]
return jsonify(process_list), 200
@app.route("/profile-image", methods=["POST"])

View File

@@ -3,7 +3,6 @@ const tar = require("tar");
const util = require("node:util");
const fs = require("node:fs");
const path = require("node:path");
const { spawnSync } = require("node:child_process");
const exec = util.promisify(require("node:child_process").exec);
@@ -15,8 +14,18 @@ const fileName = {
darwin: `ludusavi-v${ludusaviVersion}-mac.tar.gz`,
};
const ludusaviBinaryName = {
win32: "ludusavi.exe",
linux: "ludusavi",
darwin: "ludusavi",
};
const downloadLudusavi = async () => {
if (fs.existsSync("ludusavi")) {
if (
fs.existsSync(
path.join(process.cwd(), "ludusavi", ludusaviBinaryName[process.platform])
)
) {
console.log("Ludusavi already exists, skipping download...");
return;
}
@@ -58,79 +67,4 @@ const downloadLudusavi = async () => {
});
};
const downloadAria2WindowsAndLinux = async () => {
const file =
process.platform === "win32"
? "aria2-1.37.0-win-64bit-build1.zip"
: "aria2-1.37.0-1-x86_64.pkg.tar.zst";
const downloadUrl =
process.platform === "win32"
? `https://github.com/aria2/aria2/releases/download/release-1.37.0/${file}`
: "https://archlinux.org/packages/extra/x86_64/aria2/download/";
console.log(`Downloading ${file}...`);
const response = await axios.get(downloadUrl, { responseType: "stream" });
const stream = response.data.pipe(fs.createWriteStream(file));
stream.on("finish", async () => {
console.log(`Downloaded ${file}, extracting...`);
if (process.platform === "win32") {
await exec(`npx extract-zip ${file}`);
console.log("Extracted. Renaming folder...");
fs.mkdirSync("aria2");
fs.copyFileSync(
path.join(file.replace(".zip", ""), "aria2c.exe"),
"aria2/aria2c.exe"
);
fs.rmSync(file.replace(".zip", ""), { recursive: true });
} else {
await exec(`tar --zstd -xvf ${file} usr/bin/aria2c`);
console.log("Extracted. Copying binary file...");
fs.mkdirSync("aria2");
fs.copyFileSync("usr/bin/aria2c", "aria2/aria2c");
fs.rmSync("usr", { recursive: true });
}
console.log(`Extracted ${file}, removing compressed downloaded file...`);
fs.rmSync(file);
});
};
const copyAria2Macos = async () => {
console.log("Checking if aria2 is installed...");
const isAria2Installed = spawnSync("which", ["aria2c"]).status;
if (isAria2Installed != 0) {
console.log("Please install aria2");
console.log("brew install aria2");
return;
}
console.log("Copying aria2 binary...");
fs.mkdirSync("aria2");
await exec(`cp $(which aria2c) aria2/aria2c`);
};
const copyAria2 = () => {
const aria2Path =
process.platform === "win32" ? "aria2/aria2c.exe" : "aria2/aria2c";
if (fs.existsSync(aria2Path)) {
console.log("Aria2 already exists, skipping download...");
return;
}
if (process.platform == "darwin") {
copyAria2Macos();
} else {
downloadAria2WindowsAndLinux();
}
};
copyAria2();
downloadLudusavi();

View File

@@ -20,7 +20,7 @@ const s3 = new S3Client({
const dist = path.resolve(__dirname, "..", "dist");
const extensionsToUpload = [".deb", ".exe", ".pacman"];
const extensionsToUpload = [".deb", ".exe", ".pacman", ".AppImage"];
fs.readdir(dist, async (err, files) => {
if (err) throw err;

File diff suppressed because one or more lines are too long

1
sonar-project.properties Normal file
View File

@@ -0,0 +1 @@
sonar.exclusions=src/main/generated/**

View File

@@ -1,88 +1,88 @@
{
"language_name": "Български",
"app": {
"successfully_signed_in": "Успешно вписване"
"successfully_signed_in": "Успешно влизане"
},
"home": {
"featured": "Препоръчани",
"surprise_me": "Изненадай ме",
"no_results": "Не са намерени резултати",
"start_typing": "Търсене...",
"hot": "Актуално сега",
"weekly": "📅 Най-доброто от седмицата",
"achievements": "🏆 Игри, които да победите"
"no_results": "Няма намерени резултати",
"start_typing": "Започнете да пишете за търсене...",
"hot": "Горещи сега",
"weekly": "📅 Топ игри на седмицата",
"achievements": "🏆 Игри които да победите"
},
"sidebar": {
"catalogue": "Каталог",
"downloads": "Изтегляния",
"settings": "Настройки",
"my_library": "Моята библиотека",
"downloading_metadata": "{{title}} (Сваляне на метаданни…)",
"paused": "{{title}} (Пауза)",
"downloading_metadata": "{{title}} (Изтегляне на метаданни…)",
"paused": "{{title}} (На пауза)",
"downloading": "{{title}} ({{percentage}} - Изтегляне…)",
"filter": "Търсене по име",
"filter": "Филтрирай библиотеката",
"home": "Начало",
"queued": "{{title}} (Опашка)",
"game_has_no_executable": "Играта няма избран изпълним файл",
"sign_in": "Вписване",
"queued": "{{title}} (В опашката)",
"game_has_no_executable": "Няма избран изпълним файл за играта",
"sign_in": "Вход",
"friends": "Приятели",
"need_help": "Имате нужда от помощ??",
"favorites": "Любими игри"
"need_help": "Нужда от помощ?",
"favorites": "Любими"
},
"header": {
"search": "Търсене",
"search": "Търси игри",
"home": "Начало",
"catalogue": "Каталог",
"downloads": "Изтегляния",
"search_results": "Резултати от търсене",
"search_results": "Резултати от търсенето",
"settings": "Настройки",
"version_available_install": "Версия {{version}} е налична. Кликни тук, за да рестартирате и инсталирате.",
"version_available_download": "Версия {{version}} е налична. Кликни тук за изтегляне."
"version_available_install": "Версия {{version}} е налична. Кликнете тук за рестарт и инсталация.",
"version_available_download": "Версия {{version}} е налична. Кликнете тук за изтегляне."
},
"bottom_panel": {
"no_downloads_in_progress": "Няма изтегляния в ход",
"downloading_metadata": "Сваляне на {{title}} метадата…",
"downloading": "Изтегляне на {{title}}… ({{percentage}} готово) - Остават {{eta}} - {{speed}}",
"calculating_eta": "Изтегляне на {{title}}… ({{percentage}} готово) - Изчисляване на оставащо време…",
"checking_files": "Проверка на {{title}} файловете… ({{percentage}} готово)"
"no_downloads_in_progress": "Няма текущи изтегляния",
"downloading_metadata": "Изтегляне на метаданни за {{title}}…",
"downloading": "Изтегля се {{title}}… ({{percentage}} завършено) - Завършване {{eta}} - {{speed}}",
"calculating_eta": "Изтегля се {{title}}… ({{percentage}} завършено) - Изчисляване на оставащо време…",
"checking_files": "Проверка на файловете за {{title}}… ({{percentage}} завършено)",
"installing_common_redist": "{{log}}…",
"installation_complete": "Инсталацията завършена",
"installation_complete_message": "Общите компоненти са инсталирани успешно"
},
"catalogue": {
"search": "Филтър…",
"search": "Филтрирай…",
"developers": "Разработчици",
"genres": "Жанрове",
"tags": "Тагове",
"publishers": "Издатели",
"download_sources": "Източници за изтегляне",
"result_count": "{{resultCount}} резултати",
"result_count": "{{resultCount}} резултата",
"filter_count": "{{filterCount}} налични",
"clear_filters": "Изчисти {{filterCount}} избрани"
},
"game_details": {
"launch_options": "Опции за стартиране",
"launch_options_description": "Напредналите потребители могат да въведат модификации на своите опции за стартиране (экспериментальный)",
"launch_options_placeholder": "Няма зададен параметър",
"open_download_options": "Варианти за изтегляне",
"download_options_zero": "Няма варианти за изтегляне",
"download_options_one": "{{count}} варианти за изтегляне",
"download_options_other": "{{count}} варианти за изтегляне",
"open_download_options": "Отвори опциите за изтегляне",
"download_options_zero": "Няма опции за изтегляне",
"download_options_one": "{{count}} опция за изтегляне",
"download_options_other": "{{count}} опции за изтегляне",
"updated_at": "Обновено на {{updated_at}}",
"install": "Инсталирай",
"resume": "Продължи",
"pause": "Пауза",
"cancel": "Отказ",
"remove": "Премахни",
"space_left_on_disk": "{{space}} място на диска",
"eta": "Заклчение {{eta}}",
"calculating_eta": "Калкулиране на оставащо време…",
"downloading_metadata": "Изтегляне на метадата…",
"filter": "Филтрирай repacks",
"space_left_on_disk": "{{space}} свободно на диска",
"eta": "Завършване {{eta}}",
"calculating_eta": "Изчисляване на оставащо време…",
"downloading_metadata": "Изтегляне на метаданни…",
"filter": "Филтрирай репаковки",
"requirements": "Системни изисквания",
"minimum": "Минимални",
"recommended": "Препоръчителни",
"paused": "Паузирано",
"release_date": "Издадено на {{date}}",
"publisher": "Публикувано от {{publisher}}",
"hours": "часове",
"paused": "На пауза",
"release_date": "Издадена на {{date}}",
"publisher": "Издател: {{publisher}}",
"hours": "часа",
"minutes": "минути",
"amount_hours": "{{amount}} часа",
"amount_minutes": "{{amount}} минути",
@@ -90,333 +90,425 @@
"add_to_library": "Добави в библиотеката",
"remove_from_library": "Премахни от библиотеката",
"no_downloads": "Няма налични изтегляния",
"play_time": "Игрално време {{amount}}",
"last_time_played": "Последно пускане {{period}}",
"not_played_yet": "Не сте играли {{title}} все още",
"play_time": "Играно: {{amount}}",
"last_time_played": "Последно играно: {{period}}",
"not_played_yet": "Все още не сте играли {{title}}",
"next_suggestion": "Следващо предложение",
"play": "Пускане",
"deleting": "Изтриване на инсталация…",
"play": "Играй",
"deleting": "Изтриване на инсталатора…",
"close": "Затвори",
"playing_now": "Играй сега",
"change": "Промяна",
"repacks_modal_description": "Избери repack който искаш да изтеглиш",
"select_folder_hint": "За да промените стандартната папка отидете в <0>Настройки</0>",
"playing_now": "Играе се сега",
"change": "Промени",
"repacks_modal_description": "Изберете репак за изтегляне",
"select_folder_hint": "За да промените папката по подразбиране, отидете в <0>Настройки</0>",
"download_now": "Изтегли сега",
"no_shop_details": "Не може да се извлекат данни за магазина.",
"download_options": "Опции за сваляне",
"download_path": "Път за сваляне",
"previous_screenshot": "Предишна снимка",
"next_screenshot": "Следваща снимка",
"screenshot": "Снимка {{number}}",
"open_screenshot": "Отвори снимки {{number}}",
"download_settings": "Настройки за сваляне",
"downloader": "Downloader",
"no_shop_details": "Неуспешно извличане на детайли от магазина.",
"download_options": "Опции за изтегляне",
"download_path": "Път за изтегляне",
"previous_screenshot": "Предишен скрийншот",
"next_screenshot": "Следващ скрийншот",
"screenshot": "Скрийншот {{number}}",
"open_screenshot": "Отвори скрийншот {{number}}",
"download_settings": "Настройки за изтегляне",
"downloader": "Изтегляч",
"select_executable": "Избери",
"no_executable_selected": "Няма избран стартиращ файл",
"no_executable_selected": "Няма избран изпълним файл",
"open_folder": "Отвори папка",
"open_download_location": "Виж свалените файлове",
"create_shortcut": "Пряк път на Десктопа",
"open_download_location": "Виж изтеглените файлове",
"create_shortcut": "Създай пряк път на работния плот",
"clear": "Изчисти",
"remove_files": "Премахни файловете",
"remove_from_library_title": "Сигурен ли си?",
"remove_from_library_description": "Това ще премахне {{game}} от Библиотеката",
"remove_from_library_title": "Сигурни ли сте?",
"remove_from_library_description": "Това ще премахне {{game}} от вашата библиотека",
"options": "Опции",
"executable_section_title": "Стартиращ файл",
"executable_section_description": "Пътят на файла, който ще се изпълни, когато се щракне върху \"Пускане\"",
"downloads_section_title": "Свалени",
"downloads_section_description": "Вижте актуализации или други версии на тази игра",
"executable_section_title": "Изпълним файл",
"executable_section_description": "Пътят на файла, който ще се изпълни при \"Играй\"",
"downloads_section_title": "Изтегляния",
"downloads_section_description": "Вижте обновления или други версии на тази игра",
"danger_zone_section_title": "Опасна зона",
"danger_zone_section_description": "Премахнете тази игра от библиотеката си или от файловете, изтеглени от Hydra",
"download_in_progress": "Изтегляне в ход",
"download_paused": "Изтеглянето е паузирано",
"last_downloaded_option": "Опция от последно изтегляне",
"danger_zone_section_description": "Премахнете тази игра от библиотеката или файловете, изтеглени от Hydra",
"download_in_progress": "Изтеглянето е в ход",
"download_paused": "Изтеглянето е на пауза",
"last_downloaded_option": "Последно изтеглена опция",
"create_steam_shortcut": "Създай пряк път за Steam",
"create_shortcut_success": "Прекият път е създаден успешно",
"create_shortcut_error": "Грешка при създаването на пряк път",
"you_might_need_to_restart_steam": "Може да е необходимо да рестартирате Steam, за да видите промените",
"create_shortcut_error": "Грешка при създаване на пряк път",
"nsfw_content_title": "Тази игра съдържа неподходящо съдържание",
"nsfw_content_description": "{{title}} съдържа съдържание, което може да не е подходящо за всички възрасти. Сигурни ли сте, че искате да продължите?",
"allow_nsfw_content": "Продължи",
"refuse_nsfw_content": "Назад",
"refuse_nsfw_content": "Върни се",
"stats": "Статистики",
"download_count": "Сваляния",
"download_count": "Изтегляния",
"player_count": "Активни играчи",
"download_error": "Тази опция за изтегляне не е налична",
"download": "Свали",
"download": "Изтегли",
"executable_path_in_use": "Изпълнимият файл вече се използва от \"{{game}}\"",
"warning": "Внимание:",
"hydra_needs_to_remain_open": "за това изтегляне, Hydra трябва да остане отворена, когато е завършено. Ако Hydra се затвори преди завършването, ще загубите напредъка си..",
"hydra_needs_to_remain_open": "за това изтегляне, Hydra трябва да остане отворена до завършване. Ако затворите преди завършване, ще загубите прогреса.",
"achievements": "Постижения",
"achievements_count": "Постижения {{unlockedCount}}/{{achievementsCount}}",
"cloud_save": "Запазване в облака",
"cloud_save_description": "Запазете напредъка си в облака и продължете да играете на всяко устройство",
"backups": "Резервни копия",
"cloud_save": "Облачно запазване",
"cloud_save_description": "Запазете прогреса си в облака и продължете да играете на всяко устройство",
"backups": "Архиви",
"install_backup": "Инсталирай",
"delete_backup": "Изтрий",
"create_backup": "Ново копие",
"last_backup_date": "Последно копие от {{date}}",
"no_backup_preview": "Не бяха намерени запазени игри за това заглавие",
"restoring_backup": "Възстановяване на резервно копие ({{progress}} готово)…",
"uploading_backup": "Качване на резервно копие…",
"no_backups": "Все още не сте създали резервни копия за тази игра",
"backup_uploaded": "Качено резервно копие",
"backup_deleted": "Изтрито резервно копие",
"backup_restored": "Възстановен бекъп",
"see_all_achievements": "Вижте всички постижения",
"create_backup": "Нов архив",
"last_backup_date": "Последен архив на {{date}}",
"no_backup_preview": "Не са намерени запазени игри за това заглавие",
"restoring_backup": "Възстановяване на архив ({{progress}} завършено)…",
"uploading_backup": "Качване на архив…",
"no_backups": "Не сте създали архиви за тази игра",
"backup_uploaded": "Архивът е качен",
"backup_deleted": "Архивът е изтрит",
"backup_restored": "Архивът е възстановен",
"see_all_achievements": "Виж всички постижения",
"sign_in_to_see_achievements": "Влезте, за да видите постиженията",
"mapping_method_automatic": "Автоматично",
"mapping_method_manual": "Ръчно",
"mapping_method_label": "Метод на картографиране",
"files_automatically_mapped": "Автоматично картографиране на файлове",
"no_backups_created": "Не са създадени резервни копия за тази игра",
"manage_files": "Управление на файлове",
"mapping_method_label": "Метод на съпоставяне",
"files_automatically_mapped": "Файловете са съпоставени автоматично",
"no_backups_created": "Няма създадени архиви за тази игра",
"manage_files": "Управлявай файлове",
"loading_save_preview": "Търсене на запазени игри…",
"wine_prefix": "Wine Префикс",
"wine_prefix_description": "Wine prefix използван за тази игра",
"no_download_option_info": "Няма налични данни",
"backup_deletion_failed": "Неуспешно изтриване на резервно копие",
"max_number_of_artifacts_reached": "Достигнат максимален брой резервни копия за тази игра",
"achievements_not_sync": "Постиженията не са синхронизирани",
"manage_files_description": "Управлявайте кои файлове ще бъдат архивирани и възстановени",
"wine_prefix": "Wine префикс",
"wine_prefix_description": "Wine префикс, използван за стартиране на тази игра",
"launch_options": "Опции за стартиране",
"launch_options_description": "Напреднали потребители могат да въведат модификации (експериментална функция)",
"launch_options_placeholder": "Няма зададен параметър",
"no_download_option_info": "Няма налична информация",
"backup_deletion_failed": "Неуспешно изтриване на архив",
"max_number_of_artifacts_reached": "Достигнат е максималният брой архиви за тази игра",
"achievements_not_sync": "Вижте как да синхронизирате постиженията си",
"manage_files_description": "Управлявайте кои файлове ще се архивират и възстановяват",
"select_folder": "Избери папка",
"backup_from": "Резервно копие от {{date}}",
"custom_backup_location_set": "Задаване на персонализирано местоположение за архивиране"
"backup_from": "Архив от {{date}}",
"automatic_backup_from": "Автоматичен архив от {{date}}",
"enable_automatic_cloud_sync": "Включи автоматична синхронизация с облака",
"custom_backup_location_set": "Зададено е персонализирано място за архив",
"no_directory_selected": "Няма избрана директория",
"no_write_permission": "Не може да се изтегли в тази директория. Кликнете тук за повече информация.",
"reset_achievements": "Нулирай постиженията",
"reset_achievements_description": "Това ще нулира всички постижения за {{game}}",
"reset_achievements_title": "Сигурни ли сте?",
"reset_achievements_success": "Постиженията са нулирани успешно",
"reset_achievements_error": "Неуспешно нулиране на постиженията",
"download_error_gofile_quota_exceeded": "Превишихте месечната си квота в Gofile. Моля, изчакайте тя да се възстанови.",
"download_error_real_debrid_account_not_authorized": "Вашият Real-Debrid акаунт не е упълномощен за нови изтегляния. Моля, проверете настройките на акаунта и опитайте отново.",
"download_error_not_cached_on_real_debrid": "Това изтегляне не е налично в Real-Debrid и не може да се следи статуса.",
"download_error_not_cached_on_torbox": "Това изтегляне не е налично в TorBox и не може да се следи статуса.",
"download_error_not_cached_on_hydra": "Това изтегляне не е налично в Nimbus.",
"game_removed_from_favorites": "Играта е премахната от любими",
"game_added_to_favorites": "Играта е добавена в любими",
"automatically_extract_downloaded_files": "Автоматично извличане на изтеглени файлове",
"create_start_menu_shortcut": "Създай пряк път в старт менюто",
"invalid_wine_prefix_path": "Невалиден път до Wine префикса",
"invalid_wine_prefix_path_description": "Пътят до Wine префикса е невалиден. Моля, проверете го и опитайте отново.",
"missing_wine_prefix": "Wine префикс е необходим за създаване на архив в Linux"
},
"activation": {
"title": "Активирай Hydra",
"installation_id": дентификатор на инсталацията:",
"enter_activation_code": "Въведете кода за активиране",
"message": "Ако не знаете къде да попитате за това, значи не трябва да го имате..",
"installation_id": "Инсталационен ID:",
"enter_activation_code": "Въведете активационен код",
"message": "Ако не знаете къде да попитате за това, не бива да го имате.",
"activate": "Активирай",
"loading": "Зареждане…"
},
"downloads": {
"seeding": "Сийдване",
"stop_seeding": "Спри сийдването",
"resume_seeding": "Продължи сийдването",
"options": "Управление",
"resume": "Продължи",
"pause": "Пауза",
"eta": "Conclusion {{eta}}",
"paused": "Паузирано",
"eta": "Завършване {{eta}}",
"paused": "На пауза",
"verifying": "Проверка…",
"completed": "Готово",
"removed": "Не е изтеглен",
"completed": "Завършено",
"removed": "Не е изтеглено",
"cancel": "Отказ",
"filter": "Филтриране на изтеглени игри",
"filter": "Филтрирай изтеглените игри",
"remove": "Премахни",
"downloading_metadata": "Изтегляне на метаданни…",
"deleting": "Изтриване на инсталатора…",
"delete": "Премахване на инсталатора",
"delete_modal_title": "Сигурени ли сте?",
"delete_modal_description": "Това ще премахне всички инсталационни файлове от компютъра ви.",
"delete": "Премахни инсталатора",
"delete_modal_title": "Сигурни ли сте?",
"delete_modal_description": "Това ще премахне всички инсталационни файлове от компютъра ви",
"install": "Инсталирай",
"download_in_progress": "В процес на изпълнение",
"queued_downloads": "Изтеглени файлове в опашката",
"downloads_completed": "Приключени",
"queued": "В опашка",
"download_in_progress": "В процес",
"queued_downloads": "Изтегляния на опашка",
"downloads_completed": "Завършени",
"queued": "В опашката",
"no_downloads_title": "Толкова е празно",
"no_downloads_description": "Все още не сте изтеглили нищо с Hydra, но никога не е късно да започнете...",
"checking_files": "Проверка на файлове…"
"no_downloads_description": "Все още не сте изтеглили нищо с Hydra, но никога не е късно да започнете.",
"checking_files": "Проверка на файлове…",
"seeding": "Сийдване",
"stop_seeding": "Спри сийдването",
"resume_seeding": "Продължи сийдването",
"options": "Управлявай",
"extract": "Извлечи файловете",
"extracting": "Извличане на файловете…"
},
"settings": {
"seed_after_download_complete": "Сийд след завършване на изтеглянето",
"show_hidden_achievement_description": "Показвай описанието на скритите постижения преди отключването им",
"downloads_path": "Инсталационен път",
"change": "Актуализиране",
"downloads_path": "Път за изтегляния",
"change": "Обнови",
"notifications": "Известия",
"enable_download_notifications": "Когато изтеглянето е завършено",
"enable_repack_list_notifications": "Когато се добави нов repack",
"enable_download_notifications": "Когато изтеглянето приключи",
"enable_repack_list_notifications": "Когато бъде добавен нов репак",
"real_debrid_api_token_label": "Real-Debrid API токен",
"quit_app_instead_hiding": "Не скривайте Hydra при затваряне",
"launch_with_system": "Стартиране на Hydra при стартиране на системата",
"quit_app_instead_hiding": "Не скривай Hydra при затваряне",
"launch_with_system": "Стартирай Hydra при стартиране на системата",
"general": "Общи",
"behavior": "Поведение",
"download_sources": "Източници за изтегляне",
"language": "Език",
"api_token": "API Токен",
"api_token": "API токен",
"enable_real_debrid": "Включи Real-Debrid",
"real_debrid_description": "Real-Debrid е неограничен даунлоудър, който ви позволява бързо да изтегляте файлове, ограничени само от скоростта на интернет..",
"real_debrid_description": "Real-Debrid е неограничен изтегляч, който ви позволява да теглите бързо, ограничено само от интернет връзката ви.",
"debrid_invalid_token": "Невалиден API токен",
"debrid_api_token_hint": "Вземете своя API токен <0>тук</0>",
"real_debrid_free_account_error": "Акаунтът \"{{username}}\" е безплатен акаунт. Моля абонирай се за Real-Debrid",
"debrid_linked_message": "Акаунтът \"{{username}}\" е свързан",
"debrid_api_token_hint": "Може да получите вашия API токен <0>тук</0>",
"real_debrid_free_account_error": "Акаунтът \"{{username}}\" е безплатен. Моля, абонирайте се за Real-Debrid",
"debrid_linked_message": "Акаунт \"{{username}}\" е свързан",
"save_changes": "Запази промените",
"changes_saved": "Промените са успешно запазни",
"download_sources_description": "Hydra ще извлича връзките за изтегляне от тези източници. URL адресът на източника трябва да е директна връзка към .json файл, съдържащ връзките за изтегляне.",
"validate_download_source": "Валидиране",
"changes_saved": "Промените са запазени успешно",
"download_sources_description": "Hydra ще взема линкове за изтегляне от тези източници. URL адресът трябва да сочи към .json файл с линкове.",
"validate_download_source": "Валидирай",
"remove_download_source": "Премахни",
"add_download_source": "Добави източник",
"download_count_zero": "Няма опции за сваляне",
"download_count_one": "{{countFormatted}} опции за сваляне",
"download_count_other": "{{countFormatted}} опции за сваляне",
"download_source_url": "URL адрес на източника за изтегляне",
"add_download_source_description": "Вмъкнете URL адреса на файла .json",
"download_source_up_to_date": "Актуален",
"download_source_errored": "Сгрешен",
"sync_download_sources": "Синхронизирай източниците",
"removed_download_source": "Източника за сваляне е премахнат",
"cancel_button_confirmation_delete_all_sources": "не",
"confirm_button_confirmation_delete_all_sources": "Да, удалить все",
"description_confirmation_delete_all_sources": "Вы удалите все источники загрузки",
"title_confirmation_delete_all_sources": "Удалить все источники загрузки",
"removed_download_sources": "Шрифты удалены",
"button_delete_all_sources": "Удалить все источники загрузки",
"added_download_source": "Добавен източник за сваляне",
"download_sources_synced": "Всички източници за сваляне са синхронизирани",
"insert_valid_json_url": "Добавете ваиден JSON линк",
"found_download_option_zero": "Няма намерени опции за сваляне",
"found_download_option_one": "Намерени {{countFormatted}} опции за сваляне",
"found_download_option_other": "Намерени {{countFormatted}} опции за сваляне",
"import": "Внеси",
"public": "Публичен",
"private": "Личен",
"download_count_zero": "Няма опции за изтегляне",
"download_count_one": "{{countFormatted}} опция за изтегляне",
"download_count_other": "{{countFormatted}} опции за изтегляне",
"download_source_url": "URL на източника",
"add_download_source_description": "Въведете URL на .json файла",
"download_source_up_to_date": "Актуализиран",
"download_source_errored": "Грешка",
"sync_download_sources": "Синхронизирай източници",
"removed_download_source": "Източникът е премахнат",
"removed_download_sources": "Източниците са премахнати",
"cancel_button_confirmation_delete_all_sources": "Не",
"confirm_button_confirmation_delete_all_sources": "Да, изтрий всичко",
"title_confirmation_delete_all_sources": "Изтрий всички източници",
"description_confirmation_delete_all_sources": "Ще изтриете всички източници",
"button_delete_all_sources": "Премахни всички",
"added_download_source": "Източникът е добавен",
"download_sources_synced": "Всички източници са синхронизирани",
"insert_valid_json_url": "Въведете валиден JSON url",
"found_download_option_zero": "Не е намерена опция за изтегляне",
"found_download_option_one": "Намерена е {{countFormatted}} опция за изтегляне",
"found_download_option_other": "Намерени са {{countFormatted}} опции за изтегляне",
"import": "Импортирай",
"public": "Публично",
"private": "Частно",
"friends_only": "Само за приятели",
"privacy": "Поверителност",
"profile_visibility": "Видимост на профила",
"profile_visibility_description": "Изберете кой може да вижда вашия профил и библиотека",
"required_field": "Това поле е задължително",
"source_already_exists": "Този източник вече е добавен",
"must_be_valid_url": "Източникът трябва да е валиден URL адрес.",
"must_be_valid_url": "Източникът трябва да е валиден URL",
"blocked_users": "Блокирани потребители",
"user_unblocked": "Потребителят е бил деблокиран",
"enable_achievement_notifications": "Когато е отключено постижение",
"launch_minimized": "Стартиране на Hydra минимизирано",
"disable_nsfw_alert": "Деактивиране на предупреждението NSFW"
"user_unblocked": "Потребителят е деблокиран",
"enable_achievement_notifications": "Когато бъде отключено постижение",
"launch_minimized": "Стартирай Hydra минимизирано",
"disable_nsfw_alert": "Изключи NSFW предупреждението",
"seed_after_download_complete": "Сийдвай след завършване на изтеглянето",
"show_hidden_achievement_description": "Показвай описанието на скритите постижения преди отключване",
"account": "Акаунт",
"no_users_blocked": "Нямате блокирани потребители",
"subscription_active_until": "Hydra Cloud е активен до {{date}}",
"manage_subscription": "Управлявай абонамента",
"update_email": "Обнови имейл",
"update_password": "Обнови парола",
"current_email": "Текущ имейл:",
"no_email_account": "Все още не сте задали имейл",
"account_data_updated_successfully": "Данните на акаунта са обновени успешно",
"renew_subscription": "Поднови Hydra Cloud",
"subscription_expired_at": "Абонаментът изтече на {{date}}",
"no_subscription": "Наслаждавайте се на Hydra по най-добрия начин",
"become_subscriber": "Станете абонат на Hydra Cloud",
"subscription_renew_cancelled": "Автоматичното подновяване е изключено",
"subscription_renews_on": "Абонаментът се подновява на {{date}}",
"bill_sent_until": "Следващата фактура ще бъде изпратена до този ден",
"no_themes": "Изглежда, че все още нямате теми. Кликнете тук, за да създадете първата си.",
"editor_tab_code": "Код",
"editor_tab_info": "Информация",
"editor_tab_save": "Запази",
"web_store": "Уеб магазин",
"clear_themes": "Изчисти",
"create_theme": "Създай",
"create_theme_modal_title": "Създай персонализирана тема",
"create_theme_modal_description": "Създайте нова тема за персонализиране на външния вид на Hydra",
"theme_name": "Име",
"insert_theme_name": "Въведете име на тема",
"set_theme": "Задай тема",
"unset_theme": "Премахни тема",
"delete_theme": "Изтрий тема",
"edit_theme": "Редактирай тема",
"delete_all_themes": "Изтрий всички теми",
"delete_all_themes_description": "Това ще изтрие всички ваши персонализирани теми",
"delete_theme_description": "Това ще изтрие темата {{theme}}",
"cancel": "Отказ",
"appearance": "Външен вид",
"enable_torbox": "Включи TorBox",
"torbox_description": "TorBox е вашият премиум seedbox, съперничещ на най-добрите сървъри на пазара.",
"torbox_account_linked": "TorBox акаунтът е свързан",
"create_real_debrid_account": "Кликнете тук, ако все още нямате Real-Debrid акаунт",
"create_torbox_account": "Кликнете тук, ако все още нямате TorBox акаунт",
"real_debrid_account_linked": "Real-Debrid акаунтът е свързан",
"name_min_length": "Името на темата трябва да е поне 3 символа",
"import_theme": "Импортирай тема",
"import_theme_description": "Ще импортирате {{theme}} от магазина с теми",
"error_importing_theme": "Грешка при импортиране на тема",
"theme_imported": "Темата е импортирана успешно",
"enable_friend_request_notifications": "Когато получите заявка за приятелство",
"enable_auto_install": "Автоматично изтегляй обновления",
"common_redist": "Общи компоненти",
"common_redist_description": "Общите компоненти са нужни за някои игри. Препоръчва се инсталация.",
"install_common_redist": "Инсталирай",
"installing_common_redist": "Инсталиране…",
"show_download_speed_in_megabytes": "Показвай скоростта на изтегляне в MB/s",
"extract_files_by_default": "Извличай файловете по подразбиране след изтегляне"
},
"notifications": {
"download_complete": "Изтеглянето е завършено",
"game_ready_to_install": "{{title}} е готово за инсталиране",
"repack_list_updated": "Repack лист е обновен",
"repack_count_one": "{{count}} repack е добавен",
"repack_count_other": "{{count}} repacks добавени",
"new_update_available": "Версия {{version}} е налична",
"restart_to_install_update": "Рестартирайте Hydra, за да инсталирате актуализацията",
"download_complete": "Изтеглянето завърши",
"game_ready_to_install": "{{title}} е готова за инсталация",
"repack_list_updated": "Списъкът с репаци е обновен",
"repack_count_one": "Добавен е {{count}} репак",
"repack_count_other": "Добавени са {{count}} репака",
"new_update_available": "Налична е версия {{version}}",
"restart_to_install_update": "Рестартирайте Hydra за инсталиране на обновлението",
"notification_achievement_unlocked_title": "Отключено постижение за {{game}}",
"notification_achievement_unlocked_body": "{{achievement}} и други {{count}} са отклщчени"
"notification_achievement_unlocked_body": "{{achievement}} и още {{count}} бяха отключени",
"new_friend_request_description": "{{displayName}} ви изпрати заявка за приятелство",
"new_friend_request_title": "Нова заявка за приятелство",
"extraction_complete": "Извличането завърши",
"game_extracted": "{{title}} е извлечена успешно",
"friend_started_playing_game": "{{displayName}} започна да играе игра"
},
"system_tray": {
"open": "Отвори Hydra",
"quit": "Изход"
},
"game_card": {
"available_one": "Налично",
"available_other": "Налично",
"no_downloads": "Няма налични изтегляния"
},
"binary_not_found_modal": {
"title": "Не инсталирани програми",
"description": "Wine или Lutris изпълними файлове не бяха открити на вашата система",
"instructions": "Проверете правилния начин за инсталиране на някоя от тях на вашата дистрибуция на Linux, за да може играта да работи нормално"
"title": "Програмите не са инсталирани",
"description": "Wine или Lutris не са открити на вашата система",
"instructions": "Проверете как да инсталирате някоя от тях за вашата Linux дистрибуция, за да може играта да работи."
},
"modal": {
"close": "Бутон за затваряне"
},
"forms": {
"toggle_password_visibility": ревключване на видимостта на паролата"
"toggle_password_visibility": оказване/скриване на паролата"
},
"user_profile": {
"stats": "Статистики",
"achievements": "Постижения",
"games": "Игри",
"top_percentile": "Топ {{percentile}}%",
"ranking_updated_weekly": "Класацията се актуализира седмично",
"playing": "Играе {{game}}",
"achievements_unlocked": "Отключени постижения",
"earned_points": "Спечелени точки",
"show_achievements_on_profile": "Показвай своите постижения в профила",
"show_points_on_profile": "Показвай спечелените точки в профила",
"amount_hours": "{{amount}} часове",
"amount_hours": "{{amount}} часа",
"amount_minutes": "{{amount}} минути",
"last_time_played": "Последно играно {{period}}",
"activity": "Скорошна активност",
"last_time_played": "Последно играно: {{period}}",
"activity": "Последна активност",
"library": "Библиотека",
"total_play_time": "Общо време за игра",
"no_recent_activity_title": "Хмм… няма нищо тук",
"no_recent_activity_description": "Не сте играли игри напоследък. Време е да промените това.!",
"display_name": "Показване на името",
"no_recent_activity_title": "Хммм… няма нищо тук",
"no_recent_activity_description": "Не сте играли игри наскоро. Време е да го промените!",
"display_name": "Показвано име",
"saving": "Запазване",
"save": "Запис",
"edit_profile": "Редактиране на профила",
"saved_successfully": "Запазено успешно",
"try_again": "Моля, опитайте пак",
"save": "Запази",
"edit_profile": "Редактирай профил",
"saved_successfully": "Успешно запазено",
"try_again": "Моля, опитайте отново",
"sign_out_modal_title": "Сигурни ли сте?",
"cancel": "Отказ",
"successfully_signed_out": "Успешно се отписахте",
"sign_out": "Отписване",
"playing_for": "В игра от {{amount}}",
"sign_out_modal_text": "Вашата библиотека е свързана с текущата ви сметка. Когато се отпишете, библиотеката ви вече няма да е видима и напредъкът няма да бъде запазен. Продължете с отписването?",
"successfully_signed_out": "Успешно излязохте",
"sign_out": "Изход",
"playing_for": "Играе се от {{amount}}",
"sign_out_modal_text": "Библиотеката ви е свързана с този акаунт. При изход, тя няма да е видима, а прогресът няма да се запази. Продължавате ли?",
"add_friends": "Добави приятели",
"add": "Добави",
"friend_code": "Приятелски код",
"friend_code": "Код за приятелство",
"see_profile": "Виж профила",
"sending": "Изпращане",
"friend_request_sent": "Изпратена покана за приятелство",
"friend_request_sent": "Заявката е изпратена",
"friends": "Приятели",
"friends_list": "Списък с приятели",
"user_not_found": "Не е намерен потребител",
"user_not_found": "Потребителят не е намерен",
"block_user": "Блокирай потребител",
"add_friend": "Добави приятел",
"request_sent": "Изпратена покана",
"request_received": "Получена покана",
"accept_request": "Приеми поканата",
"ignore_request": "Игнирирай поканата",
"cancel_request": "Откажи поканата",
"undo_friendship": "Отмяна на приятелството",
"request_accepted": "Поканата е приета",
"request_sent": "Заявката е изпратена",
"request_received": "Получена заявка",
"accept_request": "Приеми заявката",
"ignore_request": "Игнорирай заявката",
"cancel_request": "Отмени заявката",
"undo_friendship": "Премахни приятелството",
"request_accepted": "Заявката е приета",
"user_blocked_successfully": "Потребителят е блокиран успешно",
"user_block_modal_text": "Това ще блокира {{displayName}}",
"blocked_users": "Блокирани потребители",
"unblock": "Отблокирай",
"no_friends_added": "Не сте добавили приятели",
"unblock": "Деблокирай",
"no_friends_added": "Нямате добавени приятели",
"pending": "Чакащи",
"no_pending_invites": "Нямате чакащи покани",
"no_blocked_users": "Нямате блокирани потребители",
"friend_code_copied": "Приятелския код е копиран",
"undo_friendship_modal_text": "Това ще отмени приятелството ви с {{displayName}}",
"privacy_hint": "За да настроите кой може да вижда това, отидете в <0>Настройки</0>",
"locked_profile": "Този профил е личен",
"image_process_failure": "Грешка при обработката на изображението",
"friend_code_copied": "Кодът за приятелство е копиран",
"undo_friendship_modal_text": "Това ще премахне приятелството ви с {{displayName}}",
"privacy_hint": "За да промените кой вижда това, отидете в <0>Настройки</0>",
"locked_profile": "Този профил е частен",
"image_process_failure": "Грешка при обработка на изображението",
"required_field": "Това поле е задължително",
"displayname_min_length": "Името трябва да е дълго поне 3 символа",
"displayname_max_length": "Името трябва да е с дължина не повече от 50 символа.",
"displayname_min_length": "Показваното име трябва да съдържа поне 3 символа",
"displayname_max_length": "Показваното име трябва да съдържа най-много 50 символа",
"report_profile": "Докладвай този профил",
"report_reason": "Защо докладвате този профил?",
"report_description": "Допълнителна информация",
"report_description_placeholder": "Допълнителна информация",
"report": "Докладвай",
"report_reason_hate": "Омразна реч",
"report_reason_hate": "Реч на омразата",
"report_reason_sexual_content": "Сексуално съдържание",
"report_reason_violence": "Насилия",
"report_reason_violence": "Насилие",
"report_reason_spam": "Спам",
"report_reason_other": "Друго",
"profile_reported": "Профилът е докладван",
"your_friend_code": "Вашия приятелски код:",
"your_friend_code": "Вашият код за приятелство:",
"upload_banner": "Качи банер",
"uploading_banner": "Качване на банер…",
"background_image_updated": "Обновено фоново изображение"
"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": "Показвай спечелените точки в профила"
},
"achievement": {
"achievement_unlocked": "Отключено постижение",
"user_achievements": "Постижения на {{displayName}}",
"your_achievements": "Вашите постижения",
"unlocked_at": "Отключено на: {{date}}",
"subscription_needed": "Изисква се абонамент за Hydra Cloud за този съдържание",
"new_achievements_unlocked": "Отключени {{achievementCount}} нови постижения от {{gameCount}} игри",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} постижения",
"achievements_unlocked_for_game": "Отключени {{achievementCount}} нови постижения за {{gameTitle}}",
"hidden_achievement_tooltip": "Това е скрито постижение",
"achievement_earn_points": "Спечели {{points}} точки с това постижение",
"achievement_earn_points": "Спечелете {{points}} точки с това постижение",
"earned_points": "Спечелени точки:",
"available_points": "Налични точки:",
"how_to_earn_achievements_points": "Как да спечелиш точки за постижения?",
"achievement_unlocked": "Постижението е отключено",
"user_achievements": "Постиженията на {{displayName}} ",
"your_achievements": "Вашите Постижения",
"unlocked_at": "Отключено на: {{date}}",
"subscription_needed": "Необходим е абонамент за Hydra Cloud, за да видите това съдържание",
"new_achievements_unlocked": "Отключени {{achievementCount}} нови постижения от {{gameCount}} игра",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} постижения",
"achievements_unlocked_for_game": "Отключени {{achievementCount}} нови постижения за {{gameTitle}}"
"how_to_earn_achievements_points": "Как се печелят точки от постижения?"
},
"hydra_cloud": {
"subscription_tour_title": "Абонамент за Hydra Cloud",
"subscribe_now": "Абонирай се сега",
"cloud_saving": "Облачно запазване",
"cloud_achievements": "Запазете постиженията си в облака",
"animated_profile_picture": "Анимирани профилни снимки",
"premium_support": "Премиум поддръжка",
"show_and_compare_achievements": "Показвайте и сравнявайте постиженията си с други потребители",
"animated_profile_banner": "Анимирани профилни банери",
"hydra_cloud": "Hydra Cloud",
"hydra_cloud_feature_found": "Открихте функция на Hydra Cloud!",
"learn_more": "Научете повече",
"subscription_tour_title": "Hydra Cloud Абонамент",
"subscribe_now": "Абонирай се сега",
"cloud_saving": "Запазване в облака",
"cloud_achievements": "Запазете постиженията си в облака",
"animated_profile_picture": "Анимирана профилна снимка",
"premium_support": "Премиум поддръжка",
"show_and_compare_achievements": "Показвайте и сравнявайте постиженията си с тези на други потребители",
"animated_profile_banner": "Анимиран профилен банер"
"debrid_description": "Изтегляйте до 4 пъти по-бързо с Nimbus"
}
}

View File

@@ -6,7 +6,11 @@
"home": {
"featured": "Empfohlen",
"surprise_me": "Überrasche mich",
"no_results": "Keine Ergebnisse gefunden"
"no_results": "Keine Ergebnisse gefunden",
"start_typing": "Tippe, um zu suchen...",
"hot": "Jetzt beliebt",
"weekly": "📅 Top-Spiele der Woche",
"achievements": "🏆 Spiele zum Meistern"
},
"sidebar": {
"catalogue": "Katalog",
@@ -21,11 +25,13 @@
"queued": "{{title}} (In Warteschlange)",
"game_has_no_executable": "Spiel hat keine ausführbare Datei gewählt",
"sign_in": "Anmelden",
"favorites": "Favoriten"
"friends": "Freunde",
"need_help": "Brauchst du Hilfe?",
"favorites": "Favoriten",
"playable_button_title": "Nur Spiele anzeigen, die du jetzt spielen kannst"
},
"header": {
"search": "Spiele suchen",
"home": "Home",
"catalogue": "Katalog",
"downloads": "Downloads",
@@ -39,9 +45,21 @@
"downloading_metadata": "Metadaten von {{title}} werden heruntergeladen…",
"downloading": "{{title}} wird heruntergeladen… ({{percentage}} abgeschlossen) - Abschluss {{eta}} - {{speed}}",
"calculating_eta": "{{title}} wird heruntergeladen… ({{percentage}} abgeschlossen) - Verbleibende Zeit wird berechnet…",
"checking_files": "Prüfe Dateien von {{title}}… ({{percentage}} abgeschlossen)"
"checking_files": "Prüfe Dateien von {{title}}… ({{percentage}} abgeschlossen)",
"installing_common_redist": "{{log}}…",
"installation_complete": "Installation abgeschlossen",
"installation_complete_message": "Allgemeine Redistributables erfolgreich installiert"
},
"catalogue": {
"search": "Filtern…",
"developers": "Entwickler",
"genres": "Genres",
"tags": "Tags",
"publishers": "Publisher",
"download_sources": "Download-Quellen",
"result_count": "{{resultCount}} Ergebnisse",
"filter_count": "{{filterCount}} verfügbar",
"clear_filters": "{{filterCount}} ausgewählte löschen",
"next_page": "Nächste Seite",
"previous_page": "Vorherige Seite"
},
@@ -101,6 +119,7 @@
"open_folder": "Verzeichnis öffnen",
"open_download_location": "Heruntergeladene Dateien anzeigen",
"create_shortcut": "Desktop-Verknüpfung erstellen",
"clear": "Löschen",
"remove_files": "Dateien entfernen",
"remove_from_library_title": "Bist du dir sicher?",
"remove_from_library_description": "Dies wird {{game}} aus deiner Bibliothek entfernen",
@@ -114,8 +133,81 @@
"download_in_progress": "Download erfolgt",
"download_paused": "Download ist pausiert",
"last_downloaded_option": "Letzte Download-Option",
"create_steam_shortcut": "Steam-Verknüpfung erstellen",
"create_shortcut_success": "Verknüpfung erfolgreich erstellt",
"create_shortcut_error": "Fehler bei Erstellung von Verknüpfung"
"you_might_need_to_restart_steam": "Möglicherweise musst du Steam neu starten, um die Änderungen zu sehen",
"create_shortcut_error": "Fehler bei Erstellung von Verknüpfung",
"nsfw_content_title": "Dieses Spiel enthält unangemessene Inhalte",
"nsfw_content_description": "{{title}} enthält Inhalte, die möglicherweise nicht für alle Altersgruppen geeignet sind. Bist du sicher, dass du fortfahren möchtest?",
"allow_nsfw_content": "Fortfahren",
"refuse_nsfw_content": "Zurück",
"stats": "Statistiken",
"download_count": "Downloads",
"player_count": "Aktive Spieler",
"download_error": "Diese Download-Option ist nicht verfügbar",
"download": "Download",
"executable_path_in_use": "Ausführbare Datei wird bereits von \"{{game}}\" verwendet",
"warning": "Warnung:",
"hydra_needs_to_remain_open": "Für diesen Download muss Hydra geöffnet bleiben, bis er abgeschlossen ist. Wenn Hydra vor Abschluss geschlossen wird, verlierst du deinen Fortschritt.",
"achievements": "Erfolge",
"achievements_count": "Erfolge {{unlockedCount}}/{{achievementsCount}}",
"cloud_save": "Cloud-Speicherstand",
"cloud_save_description": "Speichere deinen Fortschritt in der Cloud und spiele auf jedem Gerät weiter",
"backups": "Sicherungen",
"install_backup": "Installieren",
"delete_backup": "Löschen",
"create_backup": "Neue Sicherung",
"last_backup_date": "Letzte Sicherung am {{date}}",
"no_backup_preview": "Keine Spielstände für diesen Titel gefunden",
"restoring_backup": "Sicherung wird wiederhergestellt ({{progress}} abgeschlossen)…",
"uploading_backup": "Sicherung wird hochgeladen…",
"no_backups": "Du hast noch keine Sicherungen für dieses Spiel erstellt",
"backup_uploaded": "Sicherung hochgeladen",
"backup_deleted": "Sicherung gelöscht",
"backup_restored": "Sicherung wiederhergestellt",
"see_all_achievements": "Alle Erfolge anzeigen",
"sign_in_to_see_achievements": "Anmelden, um Erfolge zu sehen",
"mapping_method_automatic": "Automatisch",
"mapping_method_manual": "Manuell",
"mapping_method_label": "Zuordnungsmethode",
"files_automatically_mapped": "Dateien automatisch zugeordnet",
"no_backups_created": "Keine Sicherungen für dieses Spiel erstellt",
"manage_files": "Dateien verwalten",
"loading_save_preview": "Suche nach Spielständen…",
"wine_prefix": "Wine-Präfix",
"wine_prefix_description": "Das Wine-Präfix, das zum Ausführen dieses Spiels verwendet wird",
"launch_options": "Startoptionen",
"launch_options_description": "Fortgeschrittene Benutzer können Modifikationen ihrer Startoptionen eingeben (experimentelle Funktion)",
"launch_options_placeholder": "Kein Parameter angegeben",
"no_download_option_info": "Keine Informationen verfügbar",
"backup_deletion_failed": "Sicherung konnte nicht gelöscht werden",
"max_number_of_artifacts_reached": "Maximale Anzahl von Sicherungen für dieses Spiel erreicht",
"achievements_not_sync": "Sieh, wie du deine Erfolge synchronisieren kannst",
"manage_files_description": "Verwalte, welche Dateien gesichert und wiederhergestellt werden",
"select_folder": "Ordner auswählen",
"backup_from": "Sicherung vom {{date}}",
"automatic_backup_from": "Automatische Sicherung vom {{date}}",
"enable_automatic_cloud_sync": "Automatische Cloud-Synchronisierung aktivieren",
"custom_backup_location_set": "Benutzerdefinierter Sicherungsort festgelegt",
"no_directory_selected": "Kein Verzeichnis ausgewählt",
"no_write_permission": "Kann nicht in dieses Verzeichnis herunterladen. Klicke hier, um mehr zu erfahren.",
"reset_achievements": "Erfolge zurücksetzen",
"reset_achievements_description": "Dies wird alle Erfolge für {{game}} zurücksetzen",
"reset_achievements_title": "Bist du dir sicher?",
"reset_achievements_success": "Erfolge erfolgreich zurückgesetzt",
"reset_achievements_error": "Fehler beim Zurücksetzen der Erfolge",
"download_error_gofile_quota_exceeded": "Du hast dein monatliches Gofile-Kontingent überschritten. Bitte warte, bis das Kontingent zurückgesetzt wird.",
"download_error_real_debrid_account_not_authorized": "Dein Real-Debrid-Konto ist nicht für neue Downloads autorisiert. Bitte überprüfe deine Kontoeinstellungen und versuche es erneut.",
"download_error_not_cached_on_real_debrid": "Dieser Download ist nicht auf Real-Debrid verfügbar und das Abrufen des Download-Status von Real-Debrid ist noch nicht verfügbar.",
"download_error_not_cached_on_torbox": "Dieser Download ist nicht auf TorBox verfügbar und das Abrufen des Download-Status von TorBox ist noch nicht verfügbar.",
"download_error_not_cached_on_hydra": "Dieser Download ist nicht auf Nimbus verfügbar.",
"game_removed_from_favorites": "Spiel aus Favoriten entfernt",
"game_added_to_favorites": "Spiel zu Favoriten hinzugefügt",
"automatically_extract_downloaded_files": "Heruntergeladene Dateien automatisch entpacken",
"create_start_menu_shortcut": "Startmenü-Verknüpfung erstellen",
"invalid_wine_prefix_path": "Ungültiger Wine-Präfix-Pfad",
"invalid_wine_prefix_path_description": "Der Pfad zum Wine-Präfix ist ungültig. Bitte überprüfe den Pfad und versuche es erneut.",
"missing_wine_prefix": "Wine-Präfix ist erforderlich, um eine Sicherung unter Linux zu erstellen"
},
"activation": {
"title": "Hydra aktivieren",
@@ -148,7 +240,13 @@
"queued": "In Warteschlange",
"no_downloads_title": "Welch Leere",
"no_downloads_description": "Du hast mit Hydra noch nichts heruntergeladen, aber es ist nie zu spät anzufangen.",
"checking_files": "Dateien werden überprüft…"
"checking_files": "Dateien werden überprüft…",
"seeding": "Seeding",
"stop_seeding": "Seeding stoppen",
"resume_seeding": "Seeding fortsetzen",
"options": "Verwalten",
"extract": "Dateien entpacken",
"extracting": "Dateien werden entpackt…"
},
"settings": {
"downloads_path": "Download-Pfad",
@@ -185,11 +283,11 @@
"download_source_errored": "Fehlgeschlagen",
"sync_download_sources": "Quellen synchronisieren",
"removed_download_source": "Download-Quelle entfernt",
"removed_download_sources": "Download-Quellen entfernt",
"cancel_button_confirmation_delete_all_sources": "Nein",
"confirm_button_confirmation_delete_all_sources": "Ja, alles löschen",
"description_confirmation_delete_all_sources": "Du löschen alle Downloadquellen",
"title_confirmation_delete_all_sources": "Löschen du alle Downloadquellen",
"removed_download_sources": "Download-Quellen entfernt",
"title_confirmation_delete_all_sources": "Möchtest du alle Downloadquellen löschen",
"description_confirmation_delete_all_sources": "Möchtest du alle Downloadquellen löschen",
"button_delete_all_sources": "Entfernen Sie alle Downloadquellen",
"added_download_source": "Download-Quelle hinzugefügt",
"download_sources_synced": "Alle Download-Quellen sind synchronisiert",
@@ -197,7 +295,95 @@
"found_download_option_zero": "Keine Download-Option gefunden",
"found_download_option_one": "{{countFormatted}} Download-Option gefunden",
"found_download_option_other": "{{countFormatted}} Download-Optionen gefunden",
"import": "Importieren"
"import": "Importieren",
"public": "Öffentlich",
"private": "Privat",
"friends_only": "Nur Freunde",
"privacy": "Privatsphäre",
"profile_visibility": "Profilsichtbarkeit",
"profile_visibility_description": "Wähle, wer dein Profil und deine Bibliothek sehen kann",
"required_field": "Dieses Feld ist erforderlich",
"source_already_exists": "Diese Quelle wurde bereits hinzugefügt",
"must_be_valid_url": "Die Quelle muss eine gültige URL sein",
"blocked_users": "Blockierte Benutzer",
"user_unblocked": "Benutzer wurde freigegeben",
"enable_achievement_notifications": "Wenn ein Erfolg freigeschaltet wird",
"launch_minimized": "Hydra minimiert starten",
"disable_nsfw_alert": "NSFW-Warnung deaktivieren",
"seed_after_download_complete": "Nach Download-Abschluss seeden",
"show_hidden_achievement_description": "Versteckte Erfolgsbeschreibungen vor dem Freischalten anzeigen",
"account": "Konto",
"no_users_blocked": "Du hast keine blockierten Benutzer",
"subscription_active_until": "Deine Hydra Cloud ist aktiv bis {{date}}",
"manage_subscription": "Abonnement verwalten",
"update_email": "E-Mail aktualisieren",
"update_password": "Passwort aktualisieren",
"current_email": "Aktuelle E-Mail:",
"no_email_account": "Du hast noch keine E-Mail festgelegt",
"account_data_updated_successfully": "Kontodaten erfolgreich aktualisiert",
"renew_subscription": "Hydra Cloud erneuern",
"subscription_expired_at": "Dein Abonnement ist am {{date}} abgelaufen",
"no_subscription": "Genieße Hydra auf die bestmögliche Weise",
"become_subscriber": "Werde Hydra Cloud",
"subscription_renew_cancelled": "Automatische Verlängerung ist deaktiviert",
"subscription_renews_on": "Dein Abonnement verlängert sich am {{date}}",
"bill_sent_until": "Deine nächste Rechnung wird bis zu diesem Tag gesendet",
"no_themes": "Scheint, als hättest du noch keine Themes, aber keine Sorge, klicke hier, um dein erstes Meisterwerk zu erstellen.",
"editor_tab_code": "Code",
"editor_tab_info": "Info",
"editor_tab_save": "Speichern",
"web_store": "Web Store",
"clear_themes": "Löschen",
"create_theme": "Erstellen",
"create_theme_modal_title": "Benutzerdefiniertes Theme erstellen",
"create_theme_modal_description": "Erstelle ein neues Theme, um das Aussehen von Hydra anzupassen",
"theme_name": "Name",
"insert_theme_name": "Theme-Namen eingeben",
"set_theme": "Theme festlegen",
"unset_theme": "Theme entfernen",
"delete_theme": "Theme löschen",
"edit_theme": "Theme bearbeiten",
"delete_all_themes": "Alle Themes löschen",
"delete_all_themes_description": "Dies wird alle deine benutzerdefinierten Themes löschen",
"delete_theme_description": "Dies wird das Theme {{theme}} löschen",
"cancel": "Abbrechen",
"appearance": "Erscheinungsbild",
"enable_torbox": "TorBox aktivieren",
"torbox_description": "TorBox ist dein Premium-Seedbox-Service, der sogar mit den besten Servern auf dem Markt konkurriert.",
"torbox_account_linked": "TorBox-Konto verknüpft",
"create_real_debrid_account": "Klicke hier, wenn du noch kein Real-Debrid-Konto hast",
"create_torbox_account": "Klicke hier, wenn du noch kein TorBox-Konto hast",
"real_debrid_account_linked": "Real-Debrid-Konto verknüpft",
"name_min_length": "Theme-Name muss mindestens 3 Zeichen lang sein",
"import_theme": "Theme importieren",
"import_theme_description": "Du wirst {{theme}} aus dem Theme Store importieren",
"error_importing_theme": "Fehler beim Importieren des Themes",
"theme_imported": "Theme erfolgreich importiert",
"enable_friend_request_notifications": "Wenn eine Freundschaftsanfrage empfangen wird",
"enable_auto_install": "Updates automatisch herunterladen",
"common_redist": "Allgemeine Redistributables",
"common_redist_description": "Allgemeine Redistributables sind erforderlich, um einige Spiele auszuführen. Es wird empfohlen, sie zu installieren, um Probleme zu vermeiden.",
"install_common_redist": "Installieren",
"installing_common_redist": "Installiere…",
"show_download_speed_in_megabytes": "Download-Geschwindigkeit in Megabyte pro Sekunde anzeigen",
"extract_files_by_default": "Dateien nach dem Download standardmäßig entpacken",
"achievement_custom_notification_position": "Position der benutzerdefinierten Erfolgsbenachrichtigung",
"top-left": "Oben links",
"top-center": "Oben mittig",
"top-right": "Oben rechts",
"bottom-left": "Unten links",
"bottom-center": "Unten mittig",
"bottom-right": "Unten rechts",
"enable_achievement_custom_notifications": "Benutzerdefinierte Erfolgsbenachrichtigungen aktivieren",
"alignment": "Ausrichtung",
"variation": "Variation",
"default": "Standard",
"rare": "Selten",
"platinum": "Platin",
"hidden": "Versteckt",
"test_notification": "Testbenachrichtigung",
"notification_preview": "Vorschau der Erfolgsbenachrichtigung",
"enable_friend_start_game_notifications": "Wenn ein Freund ein Spiel startet"
},
"notifications": {
"download_complete": "Download abgeschlossen",
@@ -206,13 +392,24 @@
"repack_count_one": "{{count}} Repack hinzugefügt",
"repack_count_other": "{{count}} Repacks hinzugefügt",
"new_update_available": "Version {{version}} verfügbar",
"restart_to_install_update": "Um das Update zu installieren, starte Hydra neu"
"restart_to_install_update": "Um das Update zu installieren, starte Hydra neu",
"notification_achievement_unlocked_title": "Erfolg für {{game}} freigeschaltet",
"notification_achievement_unlocked_body": "{{achievement}} und {{count}} weitere wurden freigeschaltet",
"new_friend_request_description": "{{displayName}} hat dir eine Freundschaftsanfrage gesendet",
"new_friend_request_title": "Neue Freundschaftsanfrage",
"extraction_complete": "Entpacken abgeschlossen",
"game_extracted": "{{title}} erfolgreich entpackt",
"friend_started_playing_game": "{{displayName}} hat begonnen, ein Spiel zu spielen",
"test_achievement_notification_title": "Dies ist eine Testbenachrichtigung",
"test_achievement_notification_description": "Ziemlich cool, oder?"
},
"system_tray": {
"open": "Hydra öffnen",
"quit": "Schließen"
},
"game_card": {
"available_one": "Verfügbar",
"available_other": "Verfügbar",
"no_downloads": "Keine Downloads verfügbar"
},
"binary_not_found_modal": {
@@ -274,6 +471,66 @@
"no_pending_invites": "Du hast keine ausstehenden Einladungen",
"no_blocked_users": "Du hast keine blockierten Nutzer",
"friend_code_copied": "Freundescode kopiert",
"undo_friendship_modal_text": "Freundschaft mit {{displayName}} wird dadurch gekündigt"
"undo_friendship_modal_text": "Freundschaft mit {{displayName}} wird dadurch gekündigt",
"privacy_hint": "Um anzupassen, wer dies sehen kann, gehe zu den <0>Einstellungen</0>",
"locked_profile": "Dieses Profil ist privat",
"image_process_failure": "Fehler bei der Bildverarbeitung",
"required_field": "Dieses Feld ist erforderlich",
"displayname_min_length": "Anzeigename muss mindestens 3 Zeichen lang sein",
"displayname_max_length": "Anzeigename darf maximal 50 Zeichen lang sein",
"report_profile": "Dieses Profil melden",
"report_reason": "Warum meldest du dieses Profil?",
"report_description": "Zusätzliche Informationen",
"report_description_placeholder": "Zusätzliche Informationen",
"report": "Melden",
"report_reason_hate": "Hassrede",
"report_reason_sexual_content": "Sexuelle Inhalte",
"report_reason_violence": "Gewalt",
"report_reason_spam": "Spam",
"report_reason_other": "Sonstiges",
"profile_reported": "Profil gemeldet",
"your_friend_code": "Dein Freundescode:",
"upload_banner": "Banner hochladen",
"uploading_banner": "Banner wird hochgeladen…",
"background_image_updated": "Hintergrundbild aktualisiert",
"stats": "Statistiken",
"achievements": "Erfolge",
"games": "Spiele",
"top_percentile": "Top {{percentile}}%",
"ranking_updated_weekly": "Rangliste wird wöchentlich aktualisiert",
"playing": "Spielt {{game}}",
"achievements_unlocked": "Erfolge freigeschaltet",
"earned_points": "Verdiente Punkte",
"show_achievements_on_profile": "Zeige deine Erfolge auf deinem Profil",
"show_points_on_profile": "Zeige deine verdienten Punkte auf deinem Profil"
},
"achievement": {
"achievement_unlocked": "Erfolg freigeschaltet",
"user_achievements": "{{displayName}}'s Erfolge",
"your_achievements": "Deine Erfolge",
"unlocked_at": "Freigeschaltet am: {{date}}",
"subscription_needed": "Ein Hydra Cloud-Abonnement ist erforderlich, um diesen Inhalt zu sehen",
"new_achievements_unlocked": "{{achievementCount}} neue Erfolge von {{gameCount}} Spielen freigeschaltet",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} Erfolge",
"achievements_unlocked_for_game": "{{achievementCount}} neue Erfolge für {{gameTitle}} freigeschaltet",
"hidden_achievement_tooltip": "Dies ist ein versteckter Erfolg",
"achievement_earn_points": "Verdiene {{points}} Punkte mit diesem Erfolg",
"earned_points": "Verdiente Punkte:",
"available_points": "Verfügbare Punkte:",
"how_to_earn_achievements_points": "Wie verdient man Erfolgspunkte?"
},
"hydra_cloud": {
"subscription_tour_title": "Hydra Cloud-Abonnement",
"subscribe_now": "Jetzt abonnieren",
"cloud_saving": "Cloud-Speicherung",
"cloud_achievements": "Speichere deine Erfolge in der Cloud",
"animated_profile_picture": "Animierte Profilbilder",
"premium_support": "Premium-Support",
"show_and_compare_achievements": "Zeige und vergleiche deine Erfolge mit anderen Nutzern",
"animated_profile_banner": "Animiertes Profilbanner",
"hydra_cloud": "Hydra Cloud",
"hydra_cloud_feature_found": "Du hast gerade eine Hydra Cloud-Funktion entdeckt!",
"learn_more": "Mehr erfahren",
"debrid_description": "Lade bis zu 4x schneller mit Nimbus herunter"
}
}

View File

@@ -27,7 +27,8 @@
"sign_in": "Sign in",
"friends": "Friends",
"need_help": "Need help?",
"favorites": "Favorites"
"favorites": "Favorites",
"playable_button_title": "Show only games you can play now"
},
"header": {
"search": "Search games",
@@ -130,9 +131,11 @@
"download_in_progress": "Download in progress",
"download_paused": "Download paused",
"last_downloaded_option": "Last downloaded option",
"create_steam_shortcut": "Create Steam shortcut",
"create_shortcut_success": "Shortcut created successfully",
"you_might_need_to_restart_steam": "You might need to restart Steam to see the changes",
"create_shortcut_error": "Error creating shortcut",
"nsfw_content_title": "This game contains innapropriate content",
"nsfw_content_title": "This game contains inappropriate content",
"nsfw_content_description": "{{title}} contains content that may not be suitable for all ages. Are you sure you want to continue?",
"allow_nsfw_content": "Continue",
"refuse_nsfw_content": "Go back",
@@ -199,7 +202,24 @@
"game_removed_from_favorites": "Game removed from favorites",
"game_added_to_favorites": "Game added to favorites",
"automatically_extract_downloaded_files": "Automatically extract downloaded files",
"create_start_menu_shortcut": "Create Start Menu shortcut"
"create_start_menu_shortcut": "Create Start Menu shortcut",
"invalid_wine_prefix_path": "Invalid Wine prefix path",
"invalid_wine_prefix_path_description": "The path to the Wine prefix is invalid. Please check the path and try again.",
"missing_wine_prefix": "Wine prefix is required to create a backup on Linux",
"artifact_renamed": "Backup renamed successfully",
"rename_artifact": "Rename Backup",
"rename_artifact_description": "Rename the backup to a more descriptive name",
"artifact_name_label": "Backup name",
"artifact_name_placeholder": "Enter a name for the backup",
"save_changes": "Save changes",
"required_field": "This field is required",
"max_length_field": "This field must be less than {{length}} characters",
"freeze_backup": "Pin it so it's not overwritten by automatic backups",
"unfreeze_backup": "Unpin it",
"backup_frozen": "Backup pinned",
"backup_unfrozen": "Backup unpinned",
"backup_freeze_failed": "Failed to freeze backup",
"backup_freeze_failed_description": "You must leave at least one free slot for automatic backups"
},
"activation": {
"title": "Activate Hydra",
@@ -358,7 +378,24 @@
"install_common_redist": "Install",
"installing_common_redist": "Installing…",
"show_download_speed_in_megabytes": "Show download speed in megabytes per second",
"extract_files_by_default": "Extract files by default after download"
"extract_files_by_default": "Extract files by default after download",
"achievement_custom_notification_position": "Achievement custom notification position",
"top-left": "Top left",
"top-center": "Top center",
"top-right": "Top right",
"bottom-left": "Bottom left",
"bottom-center": "Bottom center",
"bottom-right": "Bottom right",
"enable_achievement_custom_notifications": "Enable achievement custom notifications",
"alignment": "Alignment",
"variation": "Variation",
"default": "Default",
"rare": "Rare",
"platinum": "Platinum",
"hidden": "Hidden",
"test_notification": "Test notification",
"notification_preview": "Achievement Notification Preview",
"enable_friend_start_game_notifications": "When a friend starts playing a game"
},
"notifications": {
"download_complete": "Download complete",
@@ -370,10 +407,13 @@
"restart_to_install_update": "Restart Hydra to install the update",
"notification_achievement_unlocked_title": "Achievement unlocked for {{game}}",
"notification_achievement_unlocked_body": "{{achievement}} and other {{count}} were unlocked",
"new_friend_request_description": "You have received a new friend request",
"new_friend_request_description": "{{displayName}} sent you a friend request",
"new_friend_request_title": "New friend request",
"extraction_complete": "Extraction complete",
"game_extracted": "{{title}} extracted successfully"
"game_extracted": "{{title}} extracted successfully",
"friend_started_playing_game": "{{displayName}} started playing a game",
"test_achievement_notification_title": "This is a test notification",
"test_achievement_notification_description": "Pretty cool, huh?"
},
"system_tray": {
"open": "Open Hydra",
@@ -474,7 +514,9 @@
"achievements_unlocked": "Achievements Unlocked",
"earned_points": "Earned points",
"show_achievements_on_profile": "Show your achievements on your profile",
"show_points_on_profile": "Show your earned points on your profile"
"show_points_on_profile": "Show your earned points on your profile",
"error_adding_friend": "Could not send friend request. Please check friend code",
"friend_code_length_error": "Friend code must have 8 characters"
},
"achievement": {
"achievement_unlocked": "Achievement unlocked",

View File

@@ -27,7 +27,8 @@
"sign_in": "Iniciar sesión",
"friends": "Amigos",
"need_help": "¿Necesitas ayuda?",
"favorites": "Favoritos"
"favorites": "Favoritos",
"playable_button_title": "Mostrar solo juegos que puedes jugar ahora"
},
"header": {
"search": "Buscar juegos",
@@ -130,8 +131,10 @@
"danger_zone_section_description": "Eliminar este juego de tu librería o los archivos descargados por Hydra (Esto solo eliminará los archivos de instalación y no el juego instalado)",
"download_in_progress": "Descarga en progreso",
"download_paused": "Descarga pausada",
"create_steam_shortcut": "Crear atajo de Steam",
"last_downloaded_option": "Última opción descargada",
"create_shortcut_success": "Atajo creado con éxito",
"you_might_need_to_restart_steam": "Es posible que necesites reiniciar Steam para ver los cambios",
"create_shortcut_error": "Error al crear un atajo",
"nsfw_content_title": "Este juego contiene contenido inapropiado.",
"nsfw_content_description": "{{title}} puede ser no adecuado para todas las edades por su contenido. \n¿Deseas continuar de igual forma?",
@@ -198,7 +201,10 @@
"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"
"game_removed_from_favorites": "Juego removido de favoritos",
"invalid_wine_prefix_path": "Ruta de prefixo Wine inválida",
"invalid_wine_prefix_path_description": "La ruta al prefixo Wine es inválida. Por favor, verifica la ruta y vuelve a intentarlo.",
"missing_wine_prefix": ""
},
"activation": {
"title": "Activar Hydra",
@@ -371,7 +377,8 @@
"notification_achievement_unlocked_title": "Logro desbloqueado de {{game}}",
"notification_achievement_unlocked_body": "{{achievement}} y otros {{count}} fueron desbloqueados",
"new_friend_request_title": "Nueva solicitud de amistad",
"new_friend_request_description": "Has recibido una nueva solicitud de amistad"
"new_friend_request_description": "{{displayName}} te envió una solicitud de amistad",
"friend_started_playing_game": "{{displayName}} está jugando"
},
"system_tray": {
"open": "Abrir Hydra",

View File

@@ -27,7 +27,8 @@
"sign_in": "Se connecter",
"friends": "Amis",
"need_help": "Besoin d'aide ?",
"favorites": "Favoris"
"favorites": "Favoris",
"playable_button_title": "Afficher uniquement les jeux que vous pouvez jouer maintenant"
},
"header": {
"search": "Rechercher",
@@ -356,7 +357,17 @@
"common_redist_description": "Certains jeux nécessitent les redistribuables communs. L'installation est recommandée.",
"install_common_redist": "Installer",
"installing_common_redist": "Installation…",
"show_download_speed_in_megabytes": "Afficher la vitesse de téléchargement en mégaoctets par seconde"
"show_download_speed_in_megabytes": "Afficher la vitesse de téléchargement en mégaoctets par seconde",
"extract_files_by_default": "Extraire les fichiers par défaut après le téléchargement",
"enable_achievement_custom_notifications": "Activer les notifications personnalisées de succès",
"achievement_custom_notification_position": "Position de la notification personnalisée de succès",
"top-left": "En haut à gauche",
"top-center": "En haut au centre",
"top-right": "En haut à droite",
"bottom-left": "En bas à gauche",
"bottom-center": "En bas au centre",
"bottom-right": "En bas à droite",
"enable_friend_start_game_notifications": "Quand un ami commence à jouer à un jeu"
},
"notifications": {
"download_complete": "Téléchargement terminé",

View File

@@ -26,6 +26,7 @@ import nb from "./nb/translation.json";
import et from "./et/translation.json";
import bg from "./bg/translation.json";
import uz from "./uz/translation.json";
import sv from "./sv/translation.json";
export default {
"pt-BR": ptBR,
@@ -56,4 +57,5 @@ export default {
nb,
et,
uz,
sv,
};

View File

@@ -15,7 +15,8 @@
"downloading": "{{title}} ({{percentage}} - Download…)",
"filter": "Filtra libreria",
"home": "Home",
"favorites": "Preferiti"
"favorites": "Preferiti",
"playable_button_title": "Mostra solo i giochi che puoi giocare ora"
},
"header": {
"search": "Cerca",

View File

@@ -13,9 +13,10 @@
"downloading_metadata": "{{title}} (Pobieranie metadata…)",
"paused": "{{title}} (Zatrzymano)",
"downloading": "{{title}} ({{percentage}} - Pobieranie…)",
"filter": "Filtruj biblioteke",
"filter": "Filtruj bibliotekę",
"home": "Główna",
"favorites": "Ulubione"
"favorites": "Ulubione",
"playable_button_title": "Pokaż tylko gry, w które możesz grać teraz"
},
"header": {
"search": "Szukaj",

View File

@@ -118,7 +118,9 @@
"download_in_progress": "Download em andamento",
"download_paused": "Download pausado",
"last_downloaded_option": "Última opção baixada",
"create_steam_shortcut": "Criar atalho na Steam",
"create_shortcut_success": "Atalho criado com sucesso",
"you_might_need_to_restart_steam": "Você pode precisar reiniciar a Steam para ver as alterações",
"create_shortcut_error": "Erro ao criar atalho",
"nsfw_content_title": "Este jogo contém conteúdo inapropriado",
"nsfw_content_description": "{{title}} contém conteúdo que pode não ser apropriado para todas as idades. Você deseja continuar?",
@@ -188,7 +190,23 @@
"game_removed_from_favorites": "Jogo removido dos favoritos",
"game_added_to_favorites": "Jogo adicionado aos favoritos",
"automatically_extract_downloaded_files": "Extrair automaticamente os arquivos baixados",
"create_start_menu_shortcut": "Criar atalho no Menu Iniciar"
"create_start_menu_shortcut": "Criar atalho no Menu Iniciar",
"invalid_wine_prefix_path": "Caminho do prefixo Wine inválido",
"invalid_wine_prefix_path_description": "O caminho para o prefixo Wine é inválido. Por favor, verifique o caminho e tente novamente.",
"artifact_renamed": "Backup renomeado com sucesso",
"rename_artifact": "Renomear Backup",
"rename_artifact_description": "Renomeie o backup para um nome mais descritivo",
"artifact_name_label": "Nome do backup",
"artifact_name_placeholder": "Insira um nome para o backup",
"save_changes": "Salvar mudanças",
"required_field": "Este campo é obrigatório",
"max_length_field": "Este campo deve ter menos de {{length}} caracteres",
"freeze_backup": "Fixar para não ser apagado por backups automáticos",
"unfreeze_backup": "Remover dos fixados",
"backup_frozen": "Backup fixado",
"backup_unfrozen": "Backup removido dos fixados",
"backup_freeze_failed": "Falha ao fixar backup",
"backup_freeze_failed_description": "Você deve deixar pelo menos um espaço livre para backups automáticos"
},
"activation": {
"title": "Ativação",
@@ -345,7 +363,24 @@
"install_common_redist": "Instalar",
"installing_common_redist": "Instalando…",
"show_download_speed_in_megabytes": "Exibir taxas de download em megabytes por segundo",
"extract_files_by_default": "Extrair arquivos automaticamente após o download"
"extract_files_by_default": "Extrair arquivos automaticamente após o download",
"enable_achievement_custom_notifications": "Habilitar notificações customizadas de conquistas",
"top-left": "Superior esquerdo",
"top-center": "Superior central",
"top-right": "Superior direito",
"bottom-left": "Inferior esquerdo",
"bottom-right": "Inferior direito",
"bottom-center": "Inferior central",
"achievement_custom_notification_position": "Posição das notificações customizadas de conquista",
"alignment": "Alinhamento",
"variation": "Variação",
"default": "Padrão",
"rare": "Rara",
"platinum": "Platina",
"hidden": "Oculta",
"test_notification": "Testar notificação",
"notification_preview": "Prévia da Notificação de Conquistas",
"enable_friend_start_game_notifications": "Quando um amigo iniciar um jogo"
},
"notifications": {
"download_complete": "Download concluído",
@@ -356,9 +391,12 @@
"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": "{{displayName}} te enviou um pedido de amizade",
"extraction_complete": "Extração concluída",
"game_extracted": "{{title}} extraído com sucesso"
"game_extracted": "{{title}} extraído com sucesso",
"friend_started_playing_game": "{{displayName}} começou a jogar",
"test_achievement_notification_title": "Esta é uma notificação de teste",
"test_achievement_notification_description": "Bem legal, né?"
},
"system_tray": {
"open": "Abrir Hydra",
@@ -469,7 +507,9 @@
"achievements_unlocked": "Conquistas desbloqueadas",
"earned_points": "Pontos ganhos",
"show_achievements_on_profile": "Exiba suas conquistas no perfil",
"show_points_on_profile": "Exiba seus pontos ganhos no perfil"
"show_points_on_profile": "Exiba seus pontos ganhos no perfil",
"error_adding_friend": "Não foi possível enviar o pedido de amizade. Verifique o código de amizade inserido",
"friend_code_length_error": "Código de amigo deve ter 8 caracteres"
},
"achievement": {
"achievement_unlocked": "Conquista desbloqueada",

View File

@@ -341,7 +341,8 @@
"new_update_available": "Versão {{version}} disponível",
"restart_to_install_update": "Reinicia o Hydra para instalar a nova versão",
"new_friend_request_title": "Novo pedido de amizade",
"new_friend_request_description": "Recebeste um novo pedido de amizade"
"new_friend_request_description": "{{displayName}} te enviou um pedido de amizade",
"friend_started_playing_game": "{{displayName}} começou a jogar"
},
"system_tray": {
"open": "Abrir o Hydra",

View File

@@ -27,7 +27,8 @@
"sign_in": "Войти",
"friends": "Друзья",
"need_help": "Нужна помощь?",
"favorites": "Избранное"
"favorites": "Избранное",
"playable_button_title": "Показать только игры, в которые можно играть сейчас"
},
"header": {
"search": "Поиск",
@@ -197,7 +198,14 @@
"download_error_not_cached_on_torbox": "Эта загрузка недоступна на TorBox, и получить статус загрузки с TorBox пока невозможно.",
"game_added_to_favorites": "Игра добавлена в избранное",
"game_removed_from_favorites": "Игра удалена из избранного",
"automatically_extract_downloaded_files": "Автоматическая распаковка загруженных файлов"
"automatically_extract_downloaded_files": "Автоматическая распаковка загруженных файлов",
"create_steam_shortcut": "Создать ярлык Steam",
"you_might_need_to_restart_steam": "Возможно, вам потребуется перезапустить Steam, чтобы увидеть изменения",
"create_start_menu_shortcut": "Создать ярлык в меню «Пуск»",
"invalid_wine_prefix_path": "Недопустимый путь префикса Wine",
"invalid_wine_prefix_path_description": "Путь к префиксу Wine недействителен. Пожалуйста, проверьте путь и попробуйте снова.",
"missing_wine_prefix": "Префикс Wine необходим для создания резервной копии в Linux",
"download_error_not_cached_on_hydra": "Эта загрузка недоступна на Nimbus."
},
"activation": {
"title": "Активировать Hydra",
@@ -355,7 +363,25 @@
"common_redist_description": "Для запуска некоторых игр требуются библиотеки. Во избежание проблем рекомендуется установить их.",
"install_common_redist": "Установить",
"installing_common_redist": "Установка…",
"show_download_speed_in_megabytes": "Показать скорость загрузки в мегабайтах в секунду"
"show_download_speed_in_megabytes": "Показать скорость загрузки в мегабайтах в секунду",
"extract_files_by_default": "Извлекать файлы по умолчанию после загрузки",
"achievement_custom_notification_position": "Позиция уведомлений достижений",
"top-left": "Верхний левый угол",
"top-center": "Верхний центр",
"top-right": "Верхний правый угол",
"bottom-left": "Нижний левый угол",
"bottom-center": "Нижний центр",
"bottom-right": "Нижний правый угол",
"enable_achievement_custom_notifications": "Включить уведомления о достижениях",
"alignment": "Выравнивание",
"variation": "Вариация",
"default": "По умолчанию",
"rare": "Редкое",
"platinum": "Платиновый",
"hidden": "Скрытый",
"test_notification": "Тестовое уведомление",
"notification_preview": "Предварительный просмотр уведомления о достижении",
"enable_friend_start_game_notifications": "Когда друг начинает играть в игру"
},
"notifications": {
"download_complete": "Загрузка завершена",
@@ -370,7 +396,10 @@
"new_friend_request_title": "Новый запрос на добавление в друзья",
"new_friend_request_description": "Вы получили новый запрос на добавление в друзья",
"extraction_complete": "Распаковка завершена",
"game_extracted": "{{title}} успешно распакован"
"game_extracted": "{{title}} успешно распакован",
"friend_started_playing_game": "{{displayName}} начал играть в игру",
"test_achievement_notification_title": "Это тестовое уведомление",
"test_achievement_notification_description": "Довольно круто, да?"
},
"system_tray": {
"open": "Открыть Hydra",

View File

@@ -0,0 +1,533 @@
{
"language_name": "Svenska",
"app": {
"successfully_signed_in": "Inloggningen lyckades"
},
"home": {
"featured": "Utvalt",
"surprise_me": "Överraska mig",
"no_results": "Inga resultat hittades",
"start_typing": "Börja skriva för att söka...",
"hot": "Hetast just nu",
"weekly": "📅 Veckans topplista",
"achievements": "🏆 Spel att klara av"
},
"sidebar": {
"catalogue": "Katalog",
"downloads": "Nedladdningar",
"settings": "Inställningar",
"my_library": "Mitt bibliotek",
"downloading_metadata": "{{title}} (Hämtar metadata…)",
"paused": "{{title}} (Pausad)",
"downloading": "{{title}} ({{percentage}} - Hämtar…)",
"filter": "Filtrera bibliotek",
"home": "Hem",
"queued": "{{title}} (I kö)",
"game_has_no_executable": "Spelet har ingen vald körbar fil",
"sign_in": "Logga in",
"friends": "Vänner",
"need_help": "Behöver du hjälp?",
"favorites": "Favoriter"
},
"header": {
"search": "Sök spel",
"home": "Hem",
"catalogue": "Katalog",
"downloads": "Nedladdningar",
"search_results": "Sökresultat",
"settings": "Inställningar",
"version_available_install": "Version {{version}} är tillgänglig. Klicka här för att starta om och installera.",
"version_available_download": "Version {{version}} är tillgänglig. Klicka här för att ladda ner."
},
"bottom_panel": {
"no_downloads_in_progress": "Inga nedladdningar pågår",
"downloading_metadata": "Laddar ner metadata för {{title}}…",
"downloading": "Laddar ner {{title}}… ({{percentage}} klart) - Klart om {{eta}} - {{speed}}",
"calculating_eta": "Laddar ner {{title}}… ({{percentage}} klart) - Beräknar återstående tid…",
"checking_files": "Kontrollerar filer för {{title}}… ({{percentage}} klart)",
"installing_common_redist": "{{log}}…",
"installation_complete": "Installation klar",
"installation_complete_message": "Nödvändiga systemkomponenter installerade framgångsrikt"
},
"catalogue": {
"search": "Filter…",
"developers": "Utvecklare",
"genres": "Genrer",
"tags": "Taggar",
"publishers": "Utgivare",
"download_sources": "Nedladdningskällor",
"result_count": "{{resultCount}} resultat",
"filter_count": "{{filterCount}} tillgängliga",
"clear_filters": "Rensa {{filterCount}} valda"
},
"game_details": {
"open_download_options": "Öppna nedladdningsalternativ",
"download_options_zero": "Inget nedladdningsalternativ",
"download_options_one": "{{count}} nedladdningsalternativ",
"download_options_other": "{{count}} nedladdningsalternativ",
"updated_at": "Uppdaterad {{updated_at}}",
"install": "Installera",
"resume": "Återuppta",
"pause": "Pausa",
"cancel": "Avbryt",
"remove": "Ta bort",
"space_left_on_disk": "{{space}} ledigt på disken",
"eta": "Klart om {{eta}}",
"calculating_eta": "Beräknar återstående tid…",
"downloading_metadata": "Laddar ner metadata…",
"filter": "Filtrera repacks",
"requirements": "Systemkrav",
"minimum": "Minimum",
"recommended": "Rekommenderat",
"paused": "Pausat",
"release_date": "Släpptes den {{date}}",
"publisher": "Utgiven av {{publisher}}",
"hours": "timmar",
"minutes": "minuter",
"amount_hours": "{{amount}} timmar",
"amount_minutes": "{{amount}} minuter",
"accuracy": "{{accuracy}}% träffsäkerhet",
"add_to_library": "Lägg till i biblioteket",
"remove_from_library": "Ta bort från biblioteket",
"no_downloads": "Inga nedladdningar tillgängliga",
"play_time": "Spelad i {{amount}}",
"last_time_played": "Senast spelad {{period}}",
"not_played_yet": "Du har inte spelat {{title}} än",
"next_suggestion": "Nästa förslag",
"play": "Spela",
"deleting": "Tar bort installationsfil…",
"close": "Stäng",
"playing_now": "Spelar nu",
"change": "Byt",
"repacks_modal_description": "Välj den repack du vill ladda ner",
"select_folder_hint": "För att ändra standardmappen, gå till <0>Inställningar</0>",
"download_now": "Ladda ner nu",
"no_shop_details": "Kunde inte hämta butikens information.",
"download_options": "Nedladdningsalternativ",
"download_path": "Nedladdningsplats",
"previous_screenshot": "Föregående skärmdump",
"next_screenshot": "Nästa skärmdump",
"screenshot": "Skärmdump {{number}}",
"open_screenshot": "Öppna skärmdump {{number}}",
"download_settings": "Nedladdningsinställningar",
"downloader": "Nedladdare",
"select_executable": "Välj",
"no_executable_selected": "Ingen körbar fil vald",
"open_folder": "Öppna mapp",
"open_download_location": "Visa nedladdade filer",
"create_shortcut": "Skapa genväg på skrivbordet",
"clear": "Rensa",
"remove_files": "Ta bort filer",
"remove_from_library_title": "Är du säker?",
"remove_from_library_description": "Detta kommer ta bort {{game}} från ditt bibliotek",
"options": "Alternativ",
"executable_section_title": "Körbar fil",
"executable_section_description": "Sökväg till filen som körs när du klickar på \"Spela\"",
"downloads_section_title": "Nedladdningar",
"downloads_section_description": "Kolla uppdateringar eller andra versioner av detta spel",
"danger_zone_section_title": "Danger zone",
"danger_zone_section_description": "Ta bort detta spel från ditt bibliotek eller filer nedladdade av Hydra",
"download_in_progress": "Nedladdning pågår",
"download_paused": "Nedladdning pausad",
"last_downloaded_option": "Senast nedladdade alternativ",
"create_steam_shortcut": "Skapa Steam-genväg",
"create_shortcut_success": "Genväg skapad",
"you_might_need_to_restart_steam": "Du kan behöva starta om Steam för att se ändringarna",
"create_shortcut_error": "Fel vid skapande av genväg",
"nsfw_content_title": "Det här spelet innehåller olämpligt innehåll",
"nsfw_content_description": "{{title}} innehåller innehåll som kanske inte är lämpligt för alla åldrar. Vill du fortsätta?",
"allow_nsfw_content": "Fortsätt",
"refuse_nsfw_content": "Gå tillbaka",
"stats": "Statistik",
"download_count": "Nedladdningar",
"player_count": "Aktiva spelare",
"download_error": "Det här nedladdningsalternativet är inte tillgängligt",
"download": "Ladda ner",
"executable_path_in_use": "Körbar fil används redan av \"{{game}}\"",
"warning": "Varning:",
"hydra_needs_to_remain_open": "för denna nedladdning behöver Hydra vara öppen tills den är klar. Om Hydra stängs innan nedladdningen är klar förlorar du dina framsteg.",
"achievements": "Prestationer",
"achievements_count": "Prestationer {{unlockedCount}}/{{achievementsCount}}",
"cloud_save": "Molnspara",
"cloud_save_description": "Spara dina framsteg i molnet och fortsätt spela på vilken enhet som helst",
"backups": "Säkerhetskopior",
"install_backup": "Installera",
"delete_backup": "Ta bort",
"create_backup": "Ny säkerhetskopia",
"last_backup_date": "Senaste säkerhetskopia {{date}}",
"no_backup_preview": "Inga sparfiler hittades för detta spel",
"restoring_backup": "Återställer säkerhetskopia ({{progress}} klart)…",
"uploading_backup": "Laddar upp säkerhetskopia…",
"no_backups": "Du har inte skapat några säkerhetskopior för detta spel än",
"backup_uploaded": "Säkerhetskopia uppladdad",
"backup_deleted": "Säkerhetskopia borttagen",
"backup_restored": "Säkerhetskopia återställd",
"see_all_achievements": "Se alla prestationer",
"sign_in_to_see_achievements": "Logga in för att se prestationer",
"mapping_method_automatic": "Automatisk",
"mapping_method_manual": "Manuell",
"mapping_method_label": "Kartläggningsmetod",
"files_automatically_mapped": "Filer kartlagda automatiskt",
"no_backups_created": "Inga säkerhetskopior skapade för detta spel",
"manage_files": "Hantera filer",
"loading_save_preview": "Söker efter sparfiler…",
"wine_prefix": "Wine-prefix",
"wine_prefix_description": "Wine-prefixet som används för att köra detta spel",
"launch_options": "Startalternativ",
"launch_options_description": "Avancerade användare kan lägga till modifieringar till sina startalternativ (experimentell funktion)",
"launch_options_placeholder": "Inga parametrar angivna",
"no_download_option_info": "Ingen information tillgänglig",
"backup_deletion_failed": "Misslyckades med att ta bort säkerhetskopian",
"max_number_of_artifacts_reached": "Maximalt antal säkerhetskopior nått för detta spel",
"achievements_not_sync": "Se hur du synkroniserar dina prestationer",
"manage_files_description": "Hantera vilka filer som ska säkerhetskopieras och återställas",
"select_folder": "Välj mapp",
"backup_from": "Säkerhetskopia från {{date}}",
"automatic_backup_from": "Automatisk säkerhetskopia från {{date}}",
"enable_automatic_cloud_sync": "Aktivera automatisk molnsynkronisering",
"custom_backup_location_set": "Anpassad plats för säkerhetskopior inställd",
"no_directory_selected": "Ingen mapp vald",
"no_write_permission": "Kan inte ladda ner till denna mapp. Klicka här för att läsa mer.",
"reset_achievements": "Återställ prestationer",
"reset_achievements_description": "Detta kommer att återställa alla prestationer för {{game}}",
"reset_achievements_title": "Är du säker?",
"reset_achievements_success": "Prestationer återställda",
"reset_achievements_error": "Misslyckades med att återställa prestationer",
"download_error_gofile_quota_exceeded": "Du har överskridit din månadsgräns för Gofile. Vänta tills kvoten återställs.",
"download_error_real_debrid_account_not_authorized": "Ditt Real-Debrid-konto är inte auktoriserat att göra nya nedladdningar. Kontrollera dina kontoinställningar och försök igen.",
"download_error_not_cached_on_real_debrid": "Denna nedladdning finns inte på Real-Debrid och statusövervakning från Real-Debrid är ännu inte tillgänglig.",
"download_error_not_cached_on_torbox": "Denna nedladdning finns inte på TorBox och statusövervakning från TorBox är ännu inte tillgänglig.",
"download_error_not_cached_on_hydra": "Denna nedladdning finns inte på Nimbus.",
"game_removed_from_favorites": "Spelet togs bort från favoriter",
"game_added_to_favorites": "Spelet lades till i favoriter",
"automatically_extract_downloaded_files": "Extrahera nedladdade filer automatiskt",
"create_start_menu_shortcut": "Skapa genväg i Startmenyn",
"invalid_wine_prefix_path": "Ogiltig sökväg för Wine-prefix",
"invalid_wine_prefix_path_description": "Sökvägen till Wine-prefixet är ogiltig. Kontrollera sökvägen och försök igen.",
"missing_wine_prefix": "Wine-prefix krävs för att skapa en säkerhetskopia på Linux"
},
"activation": {
"title": "Aktivera Hydra",
"installation_id": "Installations ID:",
"enter_activation_code": "Ange din aktiveringskod",
"message": "Om du inte vet var du ska fråga efter denna, borde du inte ha den.",
"activate": "Aktivera",
"loading": "Laddar…"
},
"downloads": {
"resume": "Fortsätt",
"pause": "Pausa",
"eta": "Slutförs {{eta}}",
"paused": "Pausad",
"verifying": "Verifierar…",
"completed": "Slutförd",
"removed": "Ej nedladdad",
"cancel": "Avbryt",
"filter": "Filtrera nedladdade spel",
"remove": "Ta bort",
"downloading_metadata": "Laddar metadata…",
"deleting": "Tar bort installationsfil…",
"delete": "Ta bort installationsfil",
"delete_modal_title": "Är du säker?",
"delete_modal_description": "Detta tar bort alla installationsfiler från din dator",
"install": "Installera",
"download_in_progress": "Pågår",
"queued_downloads": "Köade nedladdningar",
"downloads_completed": "Klart",
"queued": "I kö",
"no_downloads_title": "Så tomt",
"no_downloads_description": "Du har inte laddat ner något med Hydra än, men det är aldrig för sent att börja.",
"checking_files": "Kontrollerar filer…",
"seeding": "Seedar",
"stop_seeding": "Sluta seeda",
"resume_seeding": "Fortsätt seeda",
"options": "Hantera",
"extract": "Packa upp filer",
"extracting": "Packar upp filer…"
},
"settings": {
"downloads_path": "Nedladdningssökväg",
"change": "Uppdatera",
"notifications": "Aviseringar",
"enable_download_notifications": "När en nedladdning är klar",
"enable_repack_list_notifications": "När en ny repack läggs till",
"real_debrid_api_token_label": "Real-Debrid API-token",
"quit_app_instead_hiding": "Stäng Hydra istället för att minimera",
"launch_with_system": "Starta Hydra vid systemstart",
"general": "Allmänt",
"behavior": "Beteende",
"download_sources": "Nedladdningskällor",
"language": "Språk",
"api_token": "API-token",
"enable_real_debrid": "Aktivera Real-Debrid",
"real_debrid_description": "Real-Debrid är en obegränsad nedladdningstjänst som låter dig ladda ner filer snabbt, endast begränsad av din internetanslutning.",
"debrid_invalid_token": "Ogiltig API-token",
"debrid_api_token_hint": "Du kan hämta din API-token <0>här</0>",
"real_debrid_free_account_error": "Kontot \"{{username}}\" är ett gratiskonto. Prenumerera på Real-Debrid",
"debrid_linked_message": "Kontot \"{{username}}\" kopplat",
"save_changes": "Spara ändringar",
"changes_saved": "Ändringar sparades",
"download_sources_description": "Hydra hämtar nedladdningslänkar från dessa källor. Källans URL måste vara en direktlänk till en .json-fil med nedladdningslänkar.",
"validate_download_source": "Validera",
"remove_download_source": "Ta bort",
"add_download_source": "Lägg till källa",
"download_count_zero": "Inga nedladdningsalternativ",
"download_count_one": "{{countFormatted}} nedladdningsalternativ",
"download_count_other": "{{countFormatted}} nedladdningsalternativ",
"download_source_url": "URL till nedladdningskälla",
"add_download_source_description": "Ange URL:en till .json-filen",
"download_source_up_to_date": "Uppdaterad",
"download_source_errored": "Fel uppstod",
"sync_download_sources": "Synkronisera källor",
"removed_download_source": "Nedladdningskälla borttagen",
"removed_download_sources": "Nedladdningskällor borttagna",
"cancel_button_confirmation_delete_all_sources": "Nej",
"confirm_button_confirmation_delete_all_sources": "Ja, ta bort allt",
"title_confirmation_delete_all_sources": "Ta bort alla nedladdningskällor",
"description_confirmation_delete_all_sources": "Du kommer att ta bort alla nedladdningskällor",
"button_delete_all_sources": "Ta bort alla",
"added_download_source": "Nedladdningskälla tillagd",
"download_sources_synced": "Alla nedladdningskällor är synkroniserade",
"insert_valid_json_url": "Ange en giltig JSON-URL",
"found_download_option_zero": "Inga nedladdningsalternativ hittades",
"found_download_option_one": "Hittade {{countFormatted}} nedladdningsalternativ",
"found_download_option_other": "Hittade {{countFormatted}} nedladdningsalternativ",
"import": "Importera",
"public": "Offentlig",
"private": "Privat",
"friends_only": "Endast vänner",
"privacy": "Integritet",
"profile_visibility": "Profilens synlighet",
"profile_visibility_description": "Välj vem som kan se din profil och ditt bibliotek",
"required_field": "Detta fält är obligatoriskt",
"source_already_exists": "Denna källa har redan lagts till",
"must_be_valid_url": "Källan måste vara en giltig URL",
"blocked_users": "Blockerade användare",
"user_unblocked": "Användaren har avblockerats",
"enable_achievement_notifications": "När en prestation låses upp",
"launch_minimized": "Starta Hydra minimerad",
"disable_nsfw_alert": "Inaktivera NSFW-varning",
"seed_after_download_complete": "Seeda efter att nedladdningen är klar",
"show_hidden_achievement_description": "Visa beskrivning av dolda prestationer innan de låses upp",
"account": "Konto",
"no_users_blocked": "Du har inga blockerade användare",
"subscription_active_until": "Ditt Hydra Cloud är aktivt till {{date}}",
"manage_subscription": "Hantera prenumeration",
"update_email": "Uppdatera e-postadress",
"update_password": "Uppdatera lösenord",
"current_email": "Nuvarande e-postadress:",
"no_email_account": "Du har ännu inte angett någon e-postadress",
"account_data_updated_successfully": "Kontoinformationen har uppdaterats",
"renew_subscription": "Förnya Hydra Cloud",
"subscription_expired_at": "Din prenumeration gick ut den {{date}}",
"no_subscription": "Njut av Hydra på bästa möjliga sätt",
"become_subscriber": "Bli Hydra Cloud-prenumerant",
"subscription_renew_cancelled": "Automatisk förnyelse är inaktiverad",
"subscription_renews_on": "Din prenumeration förnyas den {{date}}",
"bill_sent_until": "Din nästa faktura skickas senast detta datum",
"no_themes": "Det verkar som att du inte har några teman ännu, men ingen fara klicka här för att skapa ditt första mästerverk.",
"editor_tab_code": "Kod",
"editor_tab_info": "Info",
"editor_tab_save": "Spara",
"web_store": "Webbutik",
"clear_themes": "Rensa",
"create_theme": "Skapa",
"create_theme_modal_title": "Skapa eget tema",
"create_theme_modal_description": "Skapa ett nytt tema för att anpassa Hydras utseende",
"theme_name": "Namn",
"insert_theme_name": "Ange temats namn",
"set_theme": "Aktivera tema",
"unset_theme": "Avaktivera tema",
"delete_theme": "Ta bort tema",
"edit_theme": "Redigera tema",
"delete_all_themes": "Ta bort alla teman",
"delete_all_themes_description": "Detta kommer att ta bort alla dina egna teman",
"delete_theme_description": "Detta kommer att ta bort temat {{theme}}",
"cancel": "Avbryt",
"appearance": "Utseende",
"enable_torbox": "Aktivera TorBox",
"torbox_description": "TorBox är din premium seedbox-tjänst som konkurrerar med de bästa servrarna på marknaden.",
"torbox_account_linked": "TorBox-konto kopplat",
"create_real_debrid_account": "Klicka här om du ännu inte har ett Real-Debrid-konto",
"create_torbox_account": "Klicka här om du ännu inte har ett TorBox-konto",
"real_debrid_account_linked": "Real-Debrid-konto kopplat",
"name_min_length": "Temanamnet måste innehålla minst 3 tecken",
"import_theme": "Importera tema",
"import_theme_description": "Du kommer att importera {{theme}} från temabutiken",
"error_importing_theme": "Fel vid import av tema",
"theme_imported": "Temat har importerats",
"enable_friend_request_notifications": "När en vänförfrågan tas emot",
"enable_auto_install": "Ladda ner uppdateringar automatiskt",
"common_redist": "Nödvändiga systemkomponenter",
"common_redist_description": "Nödvändiga systemkomponenter krävs för att vissa spel ska fungera. Det rekommenderas att installera dem för att undvika problem.",
"install_common_redist": "Installera",
"installing_common_redist": "Installerar…",
"show_download_speed_in_megabytes": "Visa nedladdningshastighet i megabyte per sekund",
"extract_files_by_default": "Extrahera filer automatiskt efter nedladdning",
"achievement_custom_notification_position": "Anpassad position för prestationmeddelande",
"top-left": "Övre vänster",
"top-center": "Övre mitten",
"top-right": "Övre höger",
"bottom-left": "Nedre vänster",
"bottom-center": "Nedre mitten",
"bottom-right": "Nedre höger",
"enable_achievement_custom_notifications": "Aktivera anpassade prestationmeddelanden",
"alignment": "Justering",
"variation": "Variation",
"default": "Standard",
"rare": "Sällsynt",
"platinum": "Platina",
"hidden": "Dold",
"test_notification": "Testa meddelande",
"notification_preview": "Förhandsvisning av prestationmeddelande",
"enable_friend_start_game_notifications": "När en vän börjar spela ett spel"
},
"notifications": {
"download_complete": "Nedladdning klar",
"game_ready_to_install": "{{title}} är redo att installeras",
"repack_list_updated": "Repack-listan har uppdaterats",
"repack_count_one": "{{count}} repack tillagd",
"repack_count_other": "{{count}} repacks tillagda",
"new_update_available": "Version {{version}} tillgänglig",
"restart_to_install_update": "Starta om Hydra för att installera uppdateringen",
"notification_achievement_unlocked_title": "Prestation upplåst för {{game}}",
"notification_achievement_unlocked_body": "{{achievement}} och {{count}} andra har låsts upp",
"new_friend_request_description": "{{displayName}} har skickat en vänförfrågan",
"new_friend_request_title": "Ny vänförfrågan",
"extraction_complete": "Extrahering slutförd",
"game_extracted": "{{title}} har extraherats",
"friend_started_playing_game": "{{displayName}} började spela ett spel",
"test_achievement_notification_title": "Detta är ett testmeddelande",
"test_achievement_notification_description": "Ganska coolt, eller hur?"
},
"system_tray": {
"open": "Öppna Hydra",
"quit": "Avsluta"
},
"game_card": {
"available_one": "Tillgänglig",
"available_other": "Tillgänglig",
"no_downloads": "Inga nedladdningar tillgängliga"
},
"binary_not_found_modal": {
"title": "Program inte installerade",
"description": "Wine- eller Lutris-körbara filer hittades inte på ditt system",
"instructions": "Kontrollera hur du installerar dem korrekt på din Linux-distribution så att spelet kan köras normalt"
},
"modal": {
"close": "Stäng-knapp"
},
"forms": {
"toggle_password_visibility": "Visa/dölj lösenord"
},
"user_profile": {
"amount_hours": "{{amount}} timmar",
"amount_minutes": "{{amount}} minuter",
"last_time_played": "Senast spelad {{period}}",
"activity": "Senaste aktivitet",
"library": "Bibliotek",
"total_play_time": "Total speltid",
"no_recent_activity_title": "Hmmm… ingenting här",
"no_recent_activity_description": "Du har inte spelat några spel nyligen. Dags att ändra på det!",
"display_name": "Visningsnamn",
"saving": "Sparar",
"save": "Spara",
"edit_profile": "Redigera profil",
"saved_successfully": "Sparat",
"try_again": "Försök igen",
"sign_out_modal_title": "Är du säker?",
"cancel": "Avbryt",
"successfully_signed_out": "Utloggningen lyckades",
"sign_out": "Logga ut",
"playing_for": "Spelar sedan {{amount}}",
"sign_out_modal_text": "Ditt bibliotek är kopplat till det aktuella kontot. När du loggar ut kommer biblioteket inte längre vara synligt, och framstegen kommer inte att sparas. Vill du fortsätta logga ut?",
"add_friends": "Lägg till vänner",
"add": "Lägg till",
"friend_code": "Vänkod",
"see_profile": "Visa profil",
"sending": "Skickar",
"friend_request_sent": "Vänförfrågan skickad",
"friends": "Vänner",
"friends_list": "Vänlista",
"user_not_found": "Användare hittades inte",
"block_user": "Blockera användare",
"add_friend": "Lägg till vän",
"request_sent": "Förfrågan skickad",
"request_received": "Förfrågan mottagen",
"accept_request": "Acceptera förfrågan",
"ignore_request": "Ignorera förfrågan",
"cancel_request": "Avbryt förfrågan",
"undo_friendship": "Ta bort vänskap",
"request_accepted": "Förfrågan accepterad",
"user_blocked_successfully": "Användaren har blockerats",
"user_block_modal_text": "Detta kommer att blockera {{displayName}}",
"blocked_users": "Blockerade användare",
"unblock": "Avblockera",
"no_friends_added": "Du har inte lagt till några vänner",
"pending": "Väntande",
"no_pending_invites": "Du har inga väntande inbjudningar",
"no_blocked_users": "Du har inga blockerade användare",
"friend_code_copied": "Vänkod kopierad",
"undo_friendship_modal_text": "Detta kommer att ta bort din vänskap med {{displayName}}",
"privacy_hint": "För att justera vem som kan se detta, gå till <0>Inställningar</0>",
"locked_profile": "Denna profil är privat",
"image_process_failure": "Fel vid bildbehandling",
"required_field": "Detta fält är obligatoriskt",
"displayname_min_length": "Visningsnamnet måste vara minst 3 tecken långt",
"displayname_max_length": "Visningsnamnet får vara högst 50 tecken långt",
"report_profile": "Anmäl denna profil",
"report_reason": "Varför anmäler du denna profil?",
"report_description": "Ytterligare information",
"report_description_placeholder": "Ytterligare information",
"report": "Anmäl",
"report_reason_hate": "Hatretorik",
"report_reason_sexual_content": "Sexuellt innehåll",
"report_reason_violence": "Våld",
"report_reason_spam": "Spam",
"report_reason_other": "Annat",
"profile_reported": "Profil anmäld",
"your_friend_code": "Din vänkod:",
"upload_banner": "Ladda upp banner",
"uploading_banner": "Laddar upp banner…",
"background_image_updated": "Bakgrundsbild uppdaterad",
"stats": "Statistik",
"achievements": "prestationer",
"games": "Spel",
"top_percentile": "Topp {{percentile}}%",
"ranking_updated_weekly": "Rankingen uppdateras varje vecka",
"playing": "Spelar {{game}}",
"achievements_unlocked": "Prestationer upplåsta",
"earned_points": "Intjänade poäng",
"show_achievements_on_profile": "Visa dina prestationer på profilen",
"show_points_on_profile": "Visa dina intjänade poäng på din profil"
},
"achievement": {
"achievement_unlocked": "Prestationer upplåst",
"user_achievements": "Prestationer för {{displayName}}",
"your_achievements": "Dina prestationer",
"unlocked_at": "Upplåst den: {{date}}",
"subscription_needed": "Ett Hydra Cloud-abonnemang krävs för att se detta innehåll",
"new_achievements_unlocked": "Upplåste {{achievementCount}} nya prestationer från {{gameCount}} spel",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} prestationer",
"achievements_unlocked_for_game": "Upplåste {{achievementCount}} nya prestationer för {{gameTitle}}",
"hidden_achievement_tooltip": "Detta är en dold prestation",
"achievement_earn_points": "Tjäna {{points}} poäng med denna prestation",
"earned_points": "Tjänade poäng:",
"available_points": "Tillgängliga poäng:",
"how_to_earn_achievements_points": "Hur tjänar man poäng på prestationer?"
},
"hydra_cloud": {
"subscription_tour_title": "Hydra Cloud-abonnemang",
"subscribe_now": "Prenumerera nu",
"cloud_saving": "Spara i molnet",
"cloud_achievements": "Spara dina prestationer i molnet",
"animated_profile_picture": "Animerade profilbilder",
"premium_support": "Premium-support",
"show_and_compare_achievements": "Visa och jämför dina prestationer med andra användare",
"animated_profile_banner": "Animerad profilbanner",
"hydra_cloud": "Hydra Cloud",
"hydra_cloud_feature_found": "Du har just upptäckt en Hydra Cloud-funktion!",
"learn_more": "Läs mer",
"debrid_description": "Ladda ner upp till 4x snabbare med Nimbus"
}
}

View File

@@ -8,46 +8,46 @@
"surprise_me": "Beni Şaşırt",
"no_results": "Sonuç bulunamadı",
"start_typing": "Aramak için yazmaya başlayın...",
"hot": "Şu anda popüler",
"weekly": "📅 Haftanın en iyi oyunları",
"achievements": "🏆 Tamamlanacak oyunlar"
"hot": "Şu anda Popüler",
"weekly": "📅 Haftanın En İyi Oyunları",
"achievements": "🏆 Bitirilecek Oyunlar"
},
"sidebar": {
"catalogue": "Katalog",
"downloads": "İndirilenler",
"settings": "Ayarlar",
"my_library": "Kütüphane",
"my_library": "Kütüphanem",
"downloading_metadata": "{{title}} (Meta verileri indiriliyor…)",
"paused": "{{title}} (Durduruldu)",
"downloading": "{{title}} ({{percentage}} - İndiriliyor…)",
"filter": "Kütüphaneyi filtrele",
"paused": "{{title}} (Duraklatıldı)",
"downloading": "{{title}} (%{{percentage}} - İndiriliyor…)",
"filter": "Kütüphanede filtrele",
"home": "Ana Sayfa",
"queued": "{{title}} (Sırada)",
"game_has_no_executable": "Oyun için bir çalıştırılabilir dosya seçilmedi",
"sign_in": "Giriş yap",
"game_has_no_executable": "Bu oyun için çalıştırılabilir dosya seçilmedi",
"sign_in": "Giriş Yap",
"friends": "Arkadaşlar",
"need_help": "Yardıma mı ihtiyacınız var?",
"favorites": "Favoriler"
},
"header": {
"search": "Oyunları ara",
"search": "Oyunlarda Ara",
"home": "Ana Sayfa",
"catalogue": "Katalog",
"downloads": "İndirilenler",
"search_results": "Arama sonuçları",
"search_results": "Arama Sonuçları",
"settings": "Ayarlar",
"version_available_install": "{{version}} sürümü mevcut. Yüklemek ve yeniden başlatmak için buraya tıklayın.",
"version_available_download": "{{version}} sürümü mevcut. İndirmek için buraya tıklayın."
"version_available_install": "{{version}} sürümü mevcut. Yeniden başlatıp yüklemek için tıklayın.",
"version_available_download": "{{version}} sürümü mevcut. İndirmek için tıklayın."
},
"bottom_panel": {
"no_downloads_in_progress": "Devam eden indirme yok",
"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ı)",
"downloading": "{{title}} indiriliyor… (%{{percentage}} tamamlandı) - Bitiş: {{eta}} - Hız: {{speed}}",
"calculating_eta": "{{title}} indiriliyor… (%{{percentage}} tamamlandı) - Kalan süre hesaplanıyor…",
"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."
"installation_complete": "Kurulum tamamlandı",
"installation_complete_message": "Gerekli paketler başarıyla yüklendi"
},
"catalogue": {
"search": "Filtrele…",
@@ -58,7 +58,7 @@
"download_sources": "İndirme kaynakları",
"result_count": "{{resultCount}} sonuç",
"filter_count": "{{filterCount}} mevcut",
"clear_filters": "{{filterCount}} seçili filtreyi temizle"
"clear_filters": "{{filterCount}} seçiliyi temizle"
},
"game_details": {
"open_download_options": "İndirme seçeneklerini aç",
@@ -67,32 +67,32 @@
"download_options_other": "{{count}} indirme seçeneği",
"updated_at": "{{updated_at}} tarihinde güncellendi",
"install": "Yükle",
"resume": "Devam et",
"pause": "Durdur",
"cancel": "İptal et",
"resume": "Devam Et",
"pause": "Duraklat",
"cancel": "İptal Et",
"remove": "Kaldır",
"space_left_on_disk": "Diskte {{space}} boş alan kaldı",
"eta": "{{eta}} tahmini bitiş",
"eta": "Bitiş: {{eta}}",
"calculating_eta": "Kalan süre hesaplanıyor…",
"downloading_metadata": "Meta veriler indiriliyor…",
"filter": "Paketleri filtrele",
"requirements": "Sistem gereksinimleri",
"requirements": "Sistem Gereksinimleri",
"minimum": "Minimum",
"recommended": "Önerilen",
"paused": "Durduruldu",
"paused": "Duraklatıldı",
"release_date": "{{date}} tarihinde yayımlandı",
"publisher": "{{publisher}} tarafından yayımlandı",
"hours": "saat",
"minutes": "dakika",
"amount_hours": "{{amount}} saat",
"amount_minutes": "{{amount}} dakika",
"accuracy": "{{accuracy}}% doğruluk",
"accuracy": "%{{accuracy}} doğruluk",
"add_to_library": "Kütüphaneye ekle",
"remove_from_library": "Kütüphaneden kaldır",
"no_downloads": "İndirilebilir içerik yok",
"play_time": "{{amount}} süre oynandı",
"last_time_played": "Son oynama {{period}} önce",
"not_played_yet": "{{title}} henüz oynanmadı",
"no_downloads": "İndirme mevcut değil",
"play_time": "{{amount}} oynandı",
"last_time_played": "Son oynanma: {{period}}",
"not_played_yet": "{{title}} oyununu henüz oynamadınız",
"next_suggestion": "Sonraki öneri",
"play": "Oyna",
"deleting": "Yükleyici siliniyor…",
@@ -107,134 +107,140 @@
"download_path": "İndirme yolu",
"previous_screenshot": "Önceki ekran görüntüsü",
"next_screenshot": "Sonraki ekran görüntüsü",
"screenshot": "{{number}} ekran görüntüsü",
"open_screenshot": "{{number}} ekran görüntüsünü aç",
"screenshot": "Ekran görüntüsü {{number}}",
"open_screenshot": "Ekran görüntüsünü aç ({{number}})",
"download_settings": "İndirme ayarları",
"downloader": "İndirici",
"select_executable": "Seç",
"no_executable_selected": "Hiçbir çalıştırılabilir dosya seçilmedi",
"no_executable_selected": "Çalıştırılabilir dosya seçilmedi",
"open_folder": "Klasörü aç",
"open_download_location": "İndirilen dosyaları gör",
"open_download_location": "İndirilen dosyaları görüntüle",
"create_shortcut": "Masaüstü kısayolu oluştur",
"clear": "Temizle",
"remove_files": "Dosyaları kaldır",
"remove_from_library_title": "Emin misiniz?",
"remove_from_library_description": "Bu işlem sonrasında {{game}} oyunu kütüphanenizden kaldıracaktır",
"remove_from_library_description": "{{game}} oyununu kütüphanenizden kaldıracaktır",
"options": "Seçenekler",
"executable_section_title": "Çalıştırılabilir dosya",
"executable_section_description": "\"Oyna\" butonuna tıklandığında çalıştırılacak dosyanın yolu",
"downloads_section_title": "İndirmeler",
"downloads_section_description": "Bu oyun için güncellemeleri veya diğer sürümleri kontrol edin",
"danger_zone_section_title": "Tehlike bölgesi",
"danger_zone_section_description": "Bu oyunu kütüphanenizden kaldırın veya Hydra tarafından indirilen dosyaları silin.",
"download_in_progress": "İndirme devam ediyor",
"download_paused": "İndirme durduruldu",
"executable_section_description": "\"Oyna\" seçildiğinde çalışacak dosyanın yolu",
"downloads_section_title": "İndirilenler",
"downloads_section_description": "Bu oyunun güncelleme veya diğer sürümlerine göz atın",
"danger_zone_section_title": "Tehlikeli Alan",
"danger_zone_section_description": "Bu oyunu kütüphanenizden veya Hydra tarafından indirilen dosyalardan kaldırın",
"download_in_progress": "İndirme sürüyor",
"download_paused": "İndirme duraklatıldı",
"last_downloaded_option": "Son indirilen seçenek",
"create_steam_shortcut": "Steam kısayolu oluştur",
"create_shortcut_success": "Kısayol başarıyla oluşturuldu",
"you_might_need_to_restart_steam": "Değişiklikleri görmek için Steam'i yeniden başlatmanız gerekebilir",
"create_shortcut_error": "Kısayol oluşturulurken hata oluştu",
"nsfw_content_title": "Bu oyun uygunsuz içerik içeriyor",
"nsfw_content_description": "{{title}} her yaş için uygun olmayabilecek içeriklere sahiptir. Devam etmek istediğinizden emin misiniz?",
"nsfw_content_title": "Bu oyun uygunsuz içerik barındırıyor",
"nsfw_content_description": "{{title}} bazı kullanıcılar için uygun olmayabilecek içerik barındırıyor. Devam etmek istediğinizden emin misiniz?",
"allow_nsfw_content": "Devam et",
"refuse_nsfw_content": "Geri dön",
"stats": "İstatistikler",
"download_count": "İndirme sayısı",
"download_count": "İndirme",
"player_count": "Aktif oyuncular",
"download_error": "Bu indirme seçeneği mevcut değil",
"download_error": "Bu indirme seçeneği kullanılamıyor",
"download": "İndir",
"executable_path_in_use": "\"{{game}}\" tarafından kullanılan çalıştırılabilir dosya",
"executable_path_in_use": "Çalıştırılabilir dosya zaten \"{{game}}\" tarafından kullanılıyor",
"warning": "Uyarı:",
"hydra_needs_to_remain_open": "Bu indirmenin tamamlanması için Hydra açık kalmalıdır. Eğer Hydra kapanırsa, ilerleme kaydedilmez.",
"hydra_needs_to_remain_open": "Bu indirme için, Hydra programının tamamlanana kadar açık kalması gerekir. Hydra kapanırsa, ilerlemeniz kaybolacaktır.",
"achievements": "Başarımlar",
"achievements_count": "Başarımlar {{unlockedCount}}/{{achievementsCount}}",
"cloud_save": "Bulut kaydı",
"cloud_save_description": "İlerlemenizi buluta kaydedin ve herhangi bir cihazda oynamaya devam edin",
"cloud_save": "Bulut Kaydı",
"cloud_save_description": "İlerlemenizi buluta kaydedin ve herhangi bir cihazdan devam edin",
"backups": "Yedekler",
"install_backup": "Yükle",
"delete_backup": "Sil",
"create_backup": "Yeni yedek oluştur",
"last_backup_date": "{{date}} tarihindeki son yedek",
"no_backup_preview": "Bu oyun için bir kayıt dosyası bulunamadı",
"restoring_backup": "Yedek geri yükleniyor ({{progress}} tamamlandı)…",
"create_backup": "Yeni Yedek",
"last_backup_date": "Son yedekleme: {{date}}",
"no_backup_preview": "Bu başlık için kayıtlı oyun bulunamadı",
"restoring_backup": "Yedek geri yükleniyor (%{{progress}} tamamlandı)…",
"uploading_backup": "Yedek yükleniyor…",
"no_backups": "Bu oyun için henüz bir yedek oluşturmadınız",
"no_backups": "Bu oyun için henüz yedek oluşturmadınız",
"backup_uploaded": "Yedek yüklendi",
"backup_deleted": "Yedek silindi",
"backup_restored": "Yedek geri yüklendi",
"see_all_achievements": "Tüm başarımları gör",
"sign_in_to_see_achievements": "Başarımları görmek için oturum açın",
"see_all_achievements": "Tüm başarımları görüntüle",
"sign_in_to_see_achievements": "Başarımları görmek için giriş yapın",
"mapping_method_automatic": "Otomatik",
"mapping_method_manual": "Manuel",
"mapping_method_label": "Eşleme yöntemi",
"files_automatically_mapped": "Dosyalar otomatik olarak eşlendi",
"no_backups_created": "Bu oyun için yedek oluşturulmadı",
"mapping_method_label": "Eşleme metodu",
"files_automatically_mapped": "Dosyalar otomatik eşlendi",
"no_backups_created": "Bu oyun için oluşturulmuş yedek yok",
"manage_files": "Dosyaları yönet",
"loading_save_preview": "Kayıtlı oyunlar aranıyor…",
"wine_prefix": "Wine Prefix",
"wine_prefix_description": "Bu oyunu çalıştırmak için kullanılan Wine Prefix",
"wine_prefix": "Wine Ön Ek",
"wine_prefix_description": "Bu oyunu çalıştırmak için kullanılan Wine ön eki",
"launch_options": "Başlatma Seçenekleri",
"launch_options_description": "İleri düzey kullanıcılar, başlatma seçeneklerine parametreler girebilir (deneysel özellik)",
"launch_options_placeholder": "Belirtilen bir parametre yok",
"launch_options_description": "Gelişmiş kullanıcılar için başlatma parametreleri tanımlayın (deneysel özellik)",
"launch_options_placeholder": "Parametre belirtilmedi",
"no_download_option_info": "Bilgi mevcut değil",
"backup_deletion_failed": "Yedek silinemedi",
"max_number_of_artifacts_reached": "Bu oyun için maksimum yedek sayısına ulaşıldı",
"achievements_not_sync": "Başarımlarınızı senkronize etmeyi öğrenin",
"manage_files_description": "Hangi dosyaların yedeklenip geri yükleneceğini yönetin",
"backup_deletion_failed": "Yedek silme işlemi başarısız oldu",
"max_number_of_artifacts_reached": "Bu oyun için azami yedekleme sayısına ulaşıldı",
"achievements_not_sync": "Başarımlarını eşitlemeyi öğren",
"manage_files_description": "Hangi dosyaların yedekleneceğini ve geri yükleneceğini yönetin",
"select_folder": "Klasör seç",
"backup_from": "{{date}} tarihinden yedek",
"automatic_backup_from": "{{date}} tarihinden otomatik kayıt",
"enable_automatic_cloud_sync": "Otomatik bulut kaydı senkronizasyonunu aktifleştir",
"custom_backup_location_set": "Özel yedekleme konumu ayarlandı",
"no_directory_selected": "Bir dizin seçilmedi",
"no_write_permission": "Bu dizine indirme yapılamaz. Daha fazla bilgi için buraya tıklayın.",
"backup_from": "{{date}} tarihli yedek",
"automatic_backup_from": "{{date}} tarihli otomatik yedek",
"enable_automatic_cloud_sync": "Otomatik bulut eşitlemesini etkinleştir",
"custom_backup_location_set": "Özel yedekleme konumu belirlendi",
"no_directory_selected": "Klasör seçilmedi",
"no_write_permission": "Bu klasöre indirme yapılamıyor. Detaylar için buraya tıklayın.",
"reset_achievements": "Başarımları sıfırla",
"reset_achievements_description": "Bu işlem {{game}} için tüm başarımları sıfırlar",
"reset_achievements_description": "{{game}} için tüm başarımlar sıfırlanacak",
"reset_achievements_title": "Emin misiniz?",
"reset_achievements_success": "Başarımlar başarıyla sıfırlandı",
"reset_achievements_error": "Başarımlar sıfırlanamadı",
"download_error_gofile_quota_exceeded": "Gofile aylık kotanızı doldurdunuz. Kotanın yenilenmesini bekleyin.",
"download_error_real_debrid_account_not_authorized": "Real-Debrid hesabınız yeni indirme işlemleri yapmak için yetkilendirilmemiş. Lütfen hesap ayarlarınızı kontrol edip tekrar deneyin.",
"download_error_not_cached_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.",
"download_error_not_cached_on_hydra": "Bu indirme Nimbus'ta mevcut değil.",
"game_removed_from_favorites": "Oyun favorilerden silindi",
"download_error_gofile_quota_exceeded": "Gofile aylık kotanızı aştınız. Lütfen kotanın sıfırlanmasını bekleyin.",
"download_error_real_debrid_account_not_authorized": "Real-Debrid hesabınız yeni indirmeler için yetkili değil. Hesap ayarlarınızı kontrol edip tekrar deneyin.",
"download_error_not_cached_on_real_debrid": "Bu indirme Real-Debrid üzerinde mevcut değil ve durum sorgulanamıyor.",
"download_error_not_cached_on_torbox": "Bu indirme TorBox üzerinde mevcut değil ve durum sorgulanamıyor.",
"download_error_not_cached_on_hydra": "Bu indirme Nimbus üzerinde mevcut değil.",
"game_removed_from_favorites": "Oyun favorilerden kaldırıldı",
"game_added_to_favorites": "Oyun favorilere eklendi",
"automatically_extract_downloaded_files": "Yüklenmiş dosyaları otomatik olarak çıkart"
"automatically_extract_downloaded_files": "İndirilen dosyaları otomatik çıkart",
"create_start_menu_shortcut": "Başlat Menüsüne kısayol oluştur",
"invalid_wine_prefix_path": "Geçersiz Wine ön ek yolu",
"invalid_wine_prefix_path_description": "Wine ön ek yolu hatalı. Lütfen yolu kontrol edin ve tekrar deneyin.",
"missing_wine_prefix": "Linux'ta yedekleme oluşturmak için Wine ön eki gereklidir"
},
"activation": {
"title": "Hydra'yı Aktive Et",
"installation_id": "Kurulum Kimliği:",
"enter_activation_code": "Aktivasyon kodunuzu girin",
"message": "Bunu nasıl edineceğini bilmiyorsan, buna sahip olmamalısın.",
"activate": "Aktive Et",
"title": "Hydra'yı Etkinleştir",
"installation_id": "Kurulum ID:",
"enter_activation_code": "Etkinleştirme kodunu girin",
"message": "Bu kodun nereden alınacağını bilmiyorsanız, zaten bu kodu kullanmamanız gerekir.",
"activate": "Etkinleştir",
"loading": "Yükleniyor…"
},
"downloads": {
"resume": "Devam Et",
"pause": "Duraklat",
"eta": "Tamamlama {{eta}}",
"eta": "Bitiş: {{eta}}",
"paused": "Duraklatıldı",
"verifying": "Doğrulanıyor…",
"completed": "Tamamlandı",
"removed": "İndirilmedi",
"cancel": "İptal Et",
"filter": "İndirilen oyunları filtrele",
"filter": "İndirilen oyunlarda filtrele",
"remove": "Kaldır",
"downloading_metadata": "Meta verileri indiriliyor…",
"deleting": "Yükleyici siliniyor…",
"delete": "Yükleyiciyi kaldır",
"delete_modal_title": "Emin misiniz?",
"delete_modal_description": "Bu işlem, tüm kurulum dosyalarını bilgisayarınızdan kaldıracaktır",
"install": "Kur",
"delete_modal_description": "Tüm kurulum dosyaları bilgisayarınızdan kaldırılacaktır",
"install": "Yükle",
"download_in_progress": "Devam ediyor",
"queued_downloads": "Sıradaki indirmeler",
"downloads_completed": "Tamamlananlar",
"queued": "Sırada",
"no_downloads_title": "Bomboş",
"no_downloads_description": "Henüz Hydra ile hbir şey indirmediniz, ancak başlamak için asla geç değil.",
"no_downloads_title": "Çok boş görünüyor",
"no_downloads_description": "Hydra ile henüz bir şey indirmediniz, başlamak için asla geç değildir.",
"checking_files": "Dosyalar kontrol ediliyor…",
"seeding": "Paylaşılıyor",
"stop_seeding": "Paylaşımı durdur",
"resume_seeding": "Paylaşımı sürdür",
"seeding": "Seed yapılıyor",
"stop_seeding": "Seed yapmayı durdur",
"resume_seeding": "Seed yapmaya devam et",
"options": "Yönet",
"extract": "Dosyaları çıkart",
"extracting": "Dosyalar çıkartılıyor…"
@@ -243,181 +249,202 @@
"downloads_path": "İndirme yolu",
"change": "Güncelle",
"notifications": "Bildirimler",
"enable_download_notifications": "Bir indirme tamamlandığında",
"enable_repack_list_notifications": "Yeni bir repack eklendiğinde",
"enable_download_notifications": "İndirme tamamlandığında",
"enable_repack_list_notifications": "Yeni bir paket eklendiğinde",
"real_debrid_api_token_label": "Real-Debrid API anahtarı",
"quit_app_instead_hiding": "Hydra'yı kapatınca sistem tepsisine gitmesin",
"launch_with_system": "Hydra'yı sistem başlatıldığında çalıştır",
"quit_app_instead_hiding": "Hydra kapatıldığında gizleme",
"launch_with_system": "Sistem başlatıldığında Hydra'yı",
"general": "Genel",
"behavior": "Davranış",
"download_sources": "İndirme kaynakları",
"language": "Dil",
"api_token": "API Anahtarı",
"enable_real_debrid": "Real-Debrid'i Etkinleştir",
"real_debrid_description": "Real-Debrid, yalnızca internet hızınızla sınırlı olarak hızlı dosya indirmenizi sağlayan sınırsız bir indirici.",
"enable_real_debrid": "Real-Debridi etkinleştir",
"real_debrid_description": "Real-Debrid, yalnızca internet hızınızla sınırlı olarak dosyaları hızlı indirmenizi sağlayan sınırsız bir indirme servisidir.",
"debrid_invalid_token": "Geçersiz API anahtarı",
"debrid_api_token_hint": "API anahtarınızı <0>buradan</0> alabilirsiniz",
"real_debrid_free_account_error": "\"{{username}}\" hesabı ücretsiz bir hesaptır. Lütfen Real-Debrid abonesi olun",
"real_debrid_free_account_error": "\"{{username}}\" hesabı ücretsizdir. Lütfen Real-Debride abone olun",
"debrid_linked_message": "\"{{username}}\" hesabı bağlandı",
"save_changes": "Değişiklikleri Kaydet",
"changes_saved": "Değişiklikler başarıyla kaydedildi",
"download_sources_description": "Hydra, indirme bağlantılarını bu kaynaklardan alacak. Kaynak URL, indirme bağlantılarını içeren bir .json dosyasına doğrudan bir bağlantı olmalıdır.",
"download_sources_description": "Hydra, indirme bağlantılarını bu kaynaklardan alacaktır. Kaynak URLsi, bağlantıların bulunduğu bir .json dosyasına doğrudan bağlantı olmalıdır.",
"validate_download_source": "Doğrula",
"remove_download_source": "Kaldır",
"add_download_source": "Kaynak ekle",
"cancel_button_confirmation_delete_all_sources": "Hayır",
"confirm_button_confirmation_delete_all_sources": "Evet, her şeyi sil",
"description_confirmation_delete_all_sources": "Tüm indirme kaynaklarını sileceksiniz",
"title_confirmation_delete_all_sources": "Tüm indirme kaynaklarını sil",
"removed_download_sources": "Yazı tipleri kaldırıldı",
"button_delete_all_sources": "Tüm indirme kaynaklarını kaldır",
"download_count_zero": "İndirme seçeneği yok",
"download_count_one": "{{countFormatted}} indirme seçeneği",
"download_count_other": "{{countFormatted}} indirme seçeneği",
"download_source_url": "İndirme kaynağı URL'si",
"add_download_source_description": ".json dosyasının URL'sini girin",
"add_download_source_description": ".json dosyasının URLsini girin",
"download_source_up_to_date": "Güncel",
"download_source_errored": "Hatalı",
"sync_download_sources": "Kaynakları senkronize et",
"sync_download_sources": "Kaynakları eşitle",
"removed_download_source": "İndirme kaynağı kaldırıldı",
"removed_download_sources": "İndirme kaynakları kaldırıldı",
"cancel_button_confirmation_delete_all_sources": "Hayır",
"confirm_button_confirmation_delete_all_sources": "Evet, hepsini sil",
"title_confirmation_delete_all_sources": "Tüm indirme kaynaklarını sil",
"description_confirmation_delete_all_sources": "Tüm indirme kaynaklarını sileceksiniz",
"button_delete_all_sources": "Tümünü kaldır",
"added_download_source": "İndirme kaynağı eklendi",
"download_sources_synced": "Tüm indirme kaynakları senkronize edildi",
"insert_valid_json_url": "Geçerli bir JSON URL'si girin",
"found_download_option_zero": "Hiçbir indirme seçeneği bulunamadı",
"download_sources_synced": "Tüm indirme kaynakları eşitlendi",
"insert_valid_json_url": "Geçerli bir JSON URLsi girin",
"found_download_option_zero": "İndirme seçeneği bulunamadı",
"found_download_option_one": "{{countFormatted}} indirme seçeneği bulundu",
"found_download_option_other": "{{countFormatted}} indirme seçeneği bulundu",
"import": "İçe aktar",
"public": "Herkese açık",
"import": "İçe Aktar",
"public": "Herkese Açık",
"private": "Gizli",
"friends_only": "Sadece arkadaşlar",
"friends_only": "Yalnızca Arkadaşlar",
"privacy": "Gizlilik",
"profile_visibility": "Profil görünürlüğü",
"profile_visibility": "Profil Görünürlüğü",
"profile_visibility_description": "Profilinizi ve kütüphanenizi kimlerin görebileceğini seçin",
"required_field": "Bu alan gereklidir",
"source_already_exists": "Bu kaynak zaten eklenmiş",
"must_be_valid_url": "Kaynak geçerli bir URL olmalıdır",
"source_already_exists": "Bu kaynak zaten eklendi",
"must_be_valid_url": "Kaynak geçerli bir URL olmalı",
"blocked_users": "Engellenen kullanıcılar",
"user_unblocked": "Kullanıcının engeli kaldırıldı",
"enable_achievement_notifications": "Bir başarım kilidiıldığında",
"launch_minimized": "Hydra'yı küçültülmüş başlat",
"user_unblocked": "Kullanıcı engeli kaldırıldı",
"enable_achievement_notifications": "Bir başarııldığında",
"launch_minimized": "Hydra'yı küçük aç",
"disable_nsfw_alert": "NSFW uyarısını devre dışı bırak",
"seed_after_download_complete": "İndirme tamamlandıktan sonra paylaş",
"show_hidden_achievement_description": "Gizli başarımıklamalarını kilitlenmeden önce göster",
"seed_after_download_complete": "İndirme sonrası seed yap",
"show_hidden_achievement_description": "ılmadan önce gizli başarııklamasını göster",
"account": "Hesap",
"no_users_blocked": "Hiçbir kullanıcıyı engellemediniz",
"subscription_active_until": "Hydra Cloud'unuz {{date}} tarihine kadar aktif",
"subscription_active_until": "Hydra Cloud üyeliğiniz {{date}} tarihine kadar aktif",
"manage_subscription": "Aboneliği yönet",
"update_email": "E-posta'yı güncelle",
"update_email": "E-postayı güncelle",
"update_password": "Şifreyi güncelle",
"current_email": "Aktif e-posta'nız",
"no_email_account": "Henüz ayarlanmış bir e-postanız yok",
"account_data_updated_successfully": "Hesap bilgileri başarıyla güncellendi",
"current_email": "Mevcut e-posta:",
"no_email_account": "Henüz bir e-posta tanımlanmadı",
"account_data_updated_successfully": "Hesap verileri başarıyla güncellendi",
"renew_subscription": "Hydra Cloud'u yenile",
"subscription_expired_at": "Aboneliğiniz {{date}} tarihinde sona erdi",
"no_subscription": "Hydra'yı en iyi şekilde deneyimleyin",
"become_subscriber": "Hydra Cloud'lu ol",
"subscription_renew_cancelled": "Otomatik yenileme devre dışı",
"no_subscription": "Hydra'yı en iyi şekilde kullanın",
"become_subscriber": "Hydra Cloud Ol",
"subscription_renew_cancelled": "Otomatik yenileme devre dışı bırakıldı",
"subscription_renews_on": "Aboneliğiniz {{date}} tarihinde yenilenecek",
"bill_sent_until": "Bir sonraki faturanız bu tarihe kadar gönderilecek",
"no_themes": "Henüz bir temanız yok gibi görünüyor, ama endişelenmeyin, ilk şaheserinizi oluşturmak için buraya tıklayın.",
"bill_sent_until": "Sonraki fatura bu güne kadar gönderilecek",
"no_themes": "Henüz bir temanız yok gibi görünüyor, endişelenmeyin, ilk şaheserinizi oluşturmak için buraya tıklayın.",
"editor_tab_code": "Kod",
"editor_tab_info": "Bilgi",
"editor_tab_save": "Kaydet",
"web_store": "İnternet mağazası",
"web_store": "Web Mağaza",
"clear_themes": "Temizle",
"create_theme": "Oluştur",
"create_theme_modal_title": "Tema oluştur",
"create_theme_modal_description": "Hydra'nın görünümünü özelleştirmek için yeni bir tema oluştur",
"theme_name": "İsim",
"insert_theme_name": "Tema ismini gir",
"set_theme": "Temayı seç",
"unset_theme": "Tema seçimini kaldır",
"create_theme_modal_title": "Özel tema oluştur",
"create_theme_modal_description": "Hydranın görünümünü özelleştirmek için yeni bir tema oluşturun",
"theme_name": "Tema adı",
"insert_theme_name": "Tema adı girin",
"set_theme": "Temayı ayarla",
"unset_theme": "Temayı kaldır",
"delete_theme": "Temayı sil",
"edit_theme": "Temayı düzenle",
"delete_all_themes": "Tüm temaları sil",
"delete_all_themes_description": "Bu tüm temalarınızı silecektir",
"delete_theme_description": "Bu {{theme}} temasını silecektir",
"delete_all_themes_description": "Tüm özel temalarınız silinecek",
"delete_theme_description": "{{theme}} teması silinecek",
"cancel": "İptal",
"appearance": "Görünüm",
"enable_torbox": "TorBox'u etkinleştir",
"torbox_description": "TorBox, piyasadaki en iyi sunucularla bile rekabet edebilen premium seedbox hizmetinizdir.",
"enable_torbox": "TorBox'u Etkinleştir",
"torbox_description": "TorBox, piyasadaki en iyi sunucularla yarışan premium seedbox hizmetinizdir.",
"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",
"create_real_debrid_account": "Henüz Real Debrid hesabınız yoksa buraya tıklayın",
"create_torbox_account": "Henüz 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",
"name_min_length": "Tema adı en az 3 karakter olmalıdır",
"import_theme": "Tema içe aktar",
"import_theme_description": "{{theme}} temasını tema mağazasından içe aktaracaksınız",
"error_importing_theme": "Tema içe aktarılırken hata oluştu",
"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…",
"show_download_speed_in_megabytes": "İndirme hızını megabayt/saniye (MB/s) cinsinden göster"
"enable_auto_install": "Güncellemeleri otomatik indir",
"common_redist": "Gereksinim Paketleri",
"common_redist_description": "Bazı oyunların çalışması için gereksinim paketleri gerekir. Sorun yaşamamak için kurulması önerilir.",
"install_common_redist": "Kur",
"installing_common_redist": "Kuruluyor…",
"show_download_speed_in_megabytes": "İndirme hızını megabayt cinsinden göster",
"extract_files_by_default": "İndirme sonrası varsayılan olarak dosyaları çıkar",
"achievement_custom_notification_position": "Başarı özel bildirim konumu",
"top-left": "Sol üst",
"top-center": "Üst orta",
"top-right": "Sağ üst",
"bottom-left": "Sol alt",
"bottom-center": "Alt orta",
"bottom-right": "Sağ alt",
"enable_achievement_custom_notifications": "Başarı özel bildirimlerini etkinleştir",
"alignment": "Hizalama",
"variation": "Çeşit",
"default": "Varsayılan",
"rare": "Nadir",
"platinum": "Platin",
"hidden": "Gizli",
"test_notification": "Test bildirimi",
"notification_preview": "Başarı Bildirimi Önizlemesi",
"enable_friend_start_game_notifications": "Bir arkadaşınız oyun oynamaya başladığında"
},
"notifications": {
"download_complete": "İndirme tamamlandı",
"game_ready_to_install": "{{title}} kurulmaya hazır",
"repack_list_updated": "Repack listesi güncellendi",
"repack_count_one": "{{count}} repack eklendi",
"repack_count_other": "{{count}} repack eklendi",
"game_ready_to_install": "{{title}} yüklenmeye hazır",
"repack_list_updated": "Paket listesi güncellendi",
"repack_count_one": "{{count}} paket eklendi",
"repack_count_other": "{{count}} paket eklendi",
"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ıldı",
"notification_achievement_unlocked_body": "{{achievement}} ve diğer {{count}} başarımıldı",
"new_friend_request_description": "Yeni bir arkadaşlık isteğin var",
"restart_to_install_update": "Güncellemeyi yüklemek için Hydrayı yeniden başlatın",
"notification_achievement_unlocked_title": "{{game}} için başarııldı",
"notification_achievement_unlocked_body": "{{achievement}} ve {{count}} diğer başarııldı",
"new_friend_request_description": "{{displayName}} size bir arkadaşlık isteği gönderdi",
"new_friend_request_title": "Yeni arkadaşlık isteği",
"extraction_complete": ıkartma tamamlandı",
"game_extracted": "{{title}} başarıyla çıkartıldı"
"extraction_complete": ıkarma tamamlandı",
"game_extracted": "{{title}} başarıyla çıkarıldı",
"friend_started_playing_game": "{{displayName}} bir oyun oynamaya başladı",
"test_achievement_notification_title": "Bu bir test bildirimi",
"test_achievement_notification_description": "Oldukça havalı, değil mi?"
},
"system_tray": {
"open": "Hydra'yı Aç",
"quit": ık"
},
"game_card": {
"no_downloads": "İndirilebilir içerik bulunmuyor",
"available_one": "Mevcut",
"available_other": "Mevcut"
"available_other": "Mevcut",
"no_downloads": "İndirme mevcut değil"
},
"binary_not_found_modal": {
"title": "Programlar Yüklü Değil",
"description": "Wine veya Lutris çalıştırılabilir dosyaları sisteminizde bulunamadı",
"instructions": "Oyunun normal çalışabilmesi için bunlardan herhangi birini Linux dağıtımınıza uygun şekilde nasıl kuracağınızı kontrol edin"
"description": "Sisteminizde Wine veya Lutris çalıştırılabilir dosyaları bulunamadı",
"instructions": "Oyunun sorunsuz çalışması için Linux dağıtımınızda bunların nasıl kurulacağını kontrol edin"
},
"modal": {
"close": "Kapat düğmesi"
},
"forms": {
"toggle_password_visibility": "Şifre görünürlüğünü değiştir"
"toggle_password_visibility": "Şifreyister/gizle"
},
"user_profile": {
"amount_hours": "{{amount}} saat",
"amount_minutes": "{{amount}} dakika",
"last_time_played": "Son oynanma {{period}}",
"last_time_played": "Son oynanma: {{period}}",
"activity": "Son Etkinlik",
"library": "Kütüphane",
"total_play_time": "Toplam oynama süresi",
"no_recent_activity_title": "Hmmm… burada bir şey yok",
"no_recent_activity_description": "Son zamanlarda hiç oyun oynamamışsınız. Bunu değiştirmenin zamanı geldi!",
"display_name": "Görünen isim",
"no_recent_activity_description": "Son zamanlarda hiç oyun oynamadınız. Bunu değiştirmenin zamanı geldi!",
"display_name": "Kullanıcı adı",
"saving": "Kaydediliyor",
"save": "Kaydet",
"edit_profile": "Profili Düzenle",
"saved_successfully": "Başarıyla kaydedildi",
"try_again": "Lütfen tekrar deneyin",
"sign_out_modal_title": "Emin misiniz?",
"sign_out_modal_title": "Çıkmak istediğinizden emin misiniz?",
"cancel": "İptal",
"successfully_signed_out": "Başarıyla çıkış yapıldı",
"sign_out": ıkış yap",
"playing_for": "{{amount}} oynanıyor",
"sign_out_modal_text": "Kütüphaneniz mevcut hesabınıza bağlı. Oturumu kapattığınızda kütüphaneniz görünür olmayacak ve herhangi bir ilerleme kaydedilmeyecek. Oturumu kapatmaya devam etmek istiyor musunuz?",
"sign_out_modal_text": "Kütüphaneniz mevcut hesabınıza bağlı. Çıkış yaparsanız, kütüphaneniz görünmeyecek ve ilerlemeniz kaydedilmeyecek. Yine de çıkış yapılsın mı?",
"add_friends": "Arkadaş Ekle",
"add": "Ekle",
"friend_code": "Arkadaş kodu",
"see_profile": "Profili gör",
"see_profile": "Profili Görüntüle",
"sending": "Gönderiliyor",
"friend_request_sent": "Arkadaşlık isteği gönderildi",
"friends": "Arkadaşlar",
@@ -428,79 +455,79 @@
"request_sent": "İstek gönderildi",
"request_received": "İstek alındı",
"accept_request": "İsteği kabul et",
"ignore_request": "İsteği yok say",
"ignore_request": "İsteği görmezden gel",
"cancel_request": "İsteği iptal et",
"undo_friendship": "Arkadaşlığı sonlandır",
"undo_friendship": "Arkadaşlığı kaldır",
"request_accepted": "İstek kabul edildi",
"user_blocked_successfully": "Kullanıcı başarıyla engellendi",
"user_block_modal_text": "Bu işlem {{displayName}} adlı kullanıcıyı engelleyecek",
"user_block_modal_text": "{{displayName}} engellenecek",
"blocked_users": "Engellenen kullanıcılar",
"unblock": "Engeli kaldır",
"no_friends_added": "Hiç arkadaş eklemediniz",
"pending": "Bekliyor",
"no_friends_added": "Hiç arkadaşınız yok",
"pending": "Bekleyen",
"no_pending_invites": "Bekleyen davetiniz yok",
"no_blocked_users": "Engellenmiş kullanıcı yok",
"no_blocked_users": "Engellenen kullanıcı yok",
"friend_code_copied": "Arkadaş kodu kopyalandı",
"undo_friendship_modal_text": "Bu işlem {{displayName}} ile arkadaşlığınızı sonlandıracak",
"privacy_hint": "Bunu kimin görebileceğini ayarlamak için <0>Ayarlar</0> bölümüne gidin",
"undo_friendship_modal_text": "Bu işlemle {{displayName}} ile arkadaşlığınız kaldırılacak",
"privacy_hint": "Bunu kimlerin görebileceğini <0>Ayarlar</0> bölümünden değiştirebilirsiniz",
"locked_profile": "Bu profil gizli",
"image_process_failure": "Görüntü işleme başarısız oldu",
"required_field": "Bu alan gerekli",
"displayname_min_length": "Görünen isim en az 3 karakter uzunluğunda olmalıdır",
"displayname_max_length": "Görünen isim en fazla 50 karakter uzunluğunda olabilir",
"report_profile": "Bu profili bildir",
"report_reason": "Bu profili neden bildiriyorsunuz?",
"image_process_failure": "Resim işlenirken hata oluştu",
"required_field": "Bu alan gereklidir",
"displayname_min_length": "Kullanıcı adı en az 3 karakter olmalıdır",
"displayname_max_length": "Kullanıcı adı en fazla 50 karakter olmalıdır",
"report_profile": "Bu profili şikayet et",
"report_reason": "Bu profili neden şikayet ediyorsunuz?",
"report_description": "Ek bilgi",
"report_description_placeholder": "Ek bilgi",
"report": "Bildir",
"report": "Şikayet et",
"report_reason_hate": "Nefret söylemi",
"report_reason_sexual_content": "Cinsel içerik",
"report_reason_violence": "Şiddet",
"report_reason_spam": "Spam",
"report_reason_other": "Diğer",
"profile_reported": "Profil bildirildi",
"profile_reported": "Profil şikayet edildi",
"your_friend_code": "Arkadaş kodunuz:",
"upload_banner": "Afiş yükle",
"uploading_banner": "Afiş yükleniyor…",
"background_image_updated": "Arka plan görüntüsü güncellendi",
"upload_banner": "Banner yükle",
"uploading_banner": "Banner yükleniyor…",
"background_image_updated": "Arka plan resmi güncellendi",
"stats": "İstatistikler",
"achievements": "Başarımlar",
"games": "Oyunlar",
"top_percentile": "En üst {{percentile}}%",
"ranking_updated_weekly": "Sıralama haftalık olarak güncellenir",
"top_percentile": "En iyi %{{percentile}}",
"ranking_updated_weekly": "Sıralama haftalık güncellenir",
"playing": "{{game}} oynanıyor",
"achievements_unlocked": "Başarımlarıldı",
"achievements_unlocked": "ılan başarımlar",
"earned_points": "Kazanılan puanlar",
"show_achievements_on_profile": "Başarımlarınızı profilinizde gösterin",
"show_points_on_profile": "Kazandığınız puanları profilinizde gösterin"
"show_achievements_on_profile": "Başarımlarını profilinde göster",
"show_points_on_profile": "Kazanılan puanlarını profilinde göster"
},
"achievement": {
"achievement_unlocked": "Başarım açıldı",
"user_achievements": "{{displayName}} oyununun Başarımları",
"user_achievements": "{{displayName}}'nın Başarımları",
"your_achievements": "Başarımlarınız",
"unlocked_at": "Açılma zamanı: {{date}}",
"subscription_needed": "Bu içeriği görmek için bir Hydra Cloud aboneliği gereklidir",
"new_achievements_unlocked": "{{gameCount}} oyundan {{achievementCount}} yeni başarımıldı",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} başarım",
"achievements_unlocked_for_game": "{{gameTitle}} oyunu için {{achievementCount}} yeni başarımıldı",
"hidden_achievement_tooltip": "Bu gizli bir başarımdır",
"achievement_earn_points": "Bu başarım ile {{points}} puan kazanın",
"unlocked_at": "Açıldığı tarih: {{date}}",
"subscription_needed": "Bu içeriği görmek için Hydra Cloud aboneliği gereklidir",
"new_achievements_unlocked": "{{gameCount}} oyunda {{achievementCount}} yeni başarııldı",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} başarı",
"achievements_unlocked_for_game": "{{gameTitle}} için {{achievementCount}} yeni başarııldı",
"hidden_achievement_tooltip": "Bu gizli bir başarıdır",
"achievement_earn_points": "Bu başarı ile {{points}} puan kazan",
"earned_points": "Kazanılan puanlar:",
"available_points": "Mevcut puanlar:",
"how_to_earn_achievements_points": "Başarım puanları nasıl kazanılır?"
"how_to_earn_achievements_points": "Başarı puanları nasıl kazanılır?"
},
"hydra_cloud": {
"subscription_tour_title": "Hydra Cloud Aboneliği",
"subscribe_now": "Şimdi abone olun",
"cloud_saving": "Bulut kaydetme",
"cloud_achievements": "Başarımlarınızı buluta kaydedin",
"subscribe_now": "Şimdi abone ol",
"cloud_saving": "Bulut kaydı",
"cloud_achievements": "Başarımlarınızı bulutta saklayın",
"animated_profile_picture": "Animasyonlu profil resimleri",
"premium_support": "Premium Destek",
"premium_support": "Öncelikli Destek",
"show_and_compare_achievements": "Başarımlarınızı diğer kullanıcılarla karşılaştırın ve gösterin",
"animated_profile_banner": "Animasyonlu profil afişi",
"hydra_cloud": "Hydra Cloud",
"hydra_cloud_feature_found": "Bir Hydra Cloud özelliği keşfettiniz!",
"learn_more": "Daha Fazla Bilgi Edinin",
"hydra_cloud_feature_found": "Bir Hydra Cloud özelliğini keşfettiniz!",
"learn_more": "Daha fazla bilgi al",
"debrid_description": "Nimbus ile 4 kata kadar daha hızlı indirin"
}
}

View File

@@ -27,7 +27,8 @@
"sign_in": "Увійти",
"favorites": "Улюблені",
"friends": "Друзі",
"need_help": "Потрібна допомога?"
"need_help": "Потрібна допомога?",
"playable_button_title": "Показати лише ігри, які можна грати зараз"
},
"header": {
"search": "Пошук",

View File

@@ -2,8 +2,6 @@ import { app } from "electron";
import path from "node:path";
import { SystemPath } from "./services/system-path";
export const LUDUSAVI_MANIFEST_URL = "https://cdn.losbroxas.org/manifest.yaml";
export const defaultDownloadsPath = SystemPath.getPath("downloads");
export const isStaging = import.meta.env.MAIN_VITE_API_URL.includes("staging");
@@ -16,6 +14,8 @@ export const windowsStartMenuPath = path.join(
"Programs"
);
export const publicProfilePath = "C:/Users/Public";
export const levelDatabasePath = path.join(
SystemPath.getPath("userData"),
`hydra-db${isStaging ? "-staging" : ""}`
@@ -26,11 +26,10 @@ export const commonRedistPath = path.join(
"CommonRedist"
);
export const logsPath = path.join(SystemPath.getPath("userData"), "logs");
export const seedsPath = app.isPackaged
? path.join(process.resourcesPath, "seeds")
: path.join(__dirname, "..", "..", "seeds");
export const logsPath = path.join(
SystemPath.getPath("userData"),
`logs${isStaging ? "-staging" : ""}`
);
export const achievementSoundPath = app.isPackaged
? path.join(process.resourcesPath, "achievement.wav")
@@ -40,4 +39,6 @@ export const backupsPath = path.join(SystemPath.getPath("userData"), "Backups");
export const appVersion = app.getVersion() + (isStaging ? "-staging" : "");
export const ASSETS_PATH = path.join(SystemPath.getPath("userData"), "Assets");
export const MAIN_LOOP_INTERVAL = 1500;

View File

@@ -1,5 +1,10 @@
import { registerEvent } from "../register-event";
import { DownloadManager, HydraApi, gamesPlaytime } from "@main/services";
import {
DownloadManager,
HydraApi,
WSClient,
gamesPlaytime,
} from "@main/services";
import { db, downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
@@ -30,6 +35,8 @@ const signOut = async (_event: Electron.IpcMainInvokeEvent) => {
databaseOperations,
HydraApi.post("/auth/logout").catch(() => {}),
]);
WSClient.close();
};
registerEvent("signOut", signOut);

View File

@@ -1,6 +1,7 @@
import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services";
import { CatalogueCategory } from "@shared";
import { ShopAssets } from "@types";
const getCatalogue = async (
_event: Electron.IpcMainInvokeEvent,
@@ -11,7 +12,7 @@ const getCatalogue = async (
skip: "0",
});
return HydraApi.get(
return HydraApi.get<ShopAssets[]>(
`/catalogue/${category}?${params.toString()}`,
{},
{ needsAuth: false }

View File

@@ -1,10 +1,13 @@
import { getSteamAppDetails, logger } from "@main/services";
import type { ShopDetails, GameShop } from "@types";
import type { ShopDetails, GameShop, ShopDetailsWithAssets } from "@types";
import { registerEvent } from "../register-event";
import { steamGamesWorker } from "@main/workers";
import { gamesShopCacheSublevel, levelKeys } from "@main/level";
import {
gamesShopAssetsSublevel,
gamesShopCacheSublevel,
levelKeys,
} from "@main/level";
const getLocalizedSteamAppDetails = async (
objectId: string,
@@ -14,22 +17,7 @@ const getLocalizedSteamAppDetails = async (
return getSteamAppDetails(objectId, language);
}
return getSteamAppDetails(objectId, language).then(
async (localizedAppDetails) => {
const steamGame = await steamGamesWorker.run(Number(objectId), {
name: "getById",
});
if (steamGame && localizedAppDetails) {
return {
...localizedAppDetails,
name: steamGame.name,
};
}
return null;
}
);
return getSteamAppDetails(objectId, language);
};
const getGameShopDetails = async (
@@ -37,34 +25,44 @@ const getGameShopDetails = async (
objectId: string,
shop: GameShop,
language: string
): Promise<ShopDetails | null> => {
): Promise<ShopDetailsWithAssets | null> => {
if (shop === "steam") {
const cachedData = await gamesShopCacheSublevel.get(
levelKeys.gameShopCacheItem(shop, objectId, language)
);
const [cachedData, cachedAssets] = await Promise.all([
gamesShopCacheSublevel.get(
levelKeys.gameShopCacheItem(shop, objectId, language)
),
gamesShopAssetsSublevel.get(levelKeys.game(shop, objectId)),
]);
const appDetails = getLocalizedSteamAppDetails(objectId, language).then(
(result) => {
if (result) {
result.name = cachedAssets?.title ?? result.name;
gamesShopCacheSublevel
.put(levelKeys.gameShopCacheItem(shop, objectId, language), result)
.catch((err) => {
logger.error("Could not cache game details", err);
});
return {
...result,
assets: cachedAssets ?? null,
};
}
return result;
return null;
}
);
if (cachedData) {
return {
...cachedData,
objectId,
} as ShopDetails;
assets: cachedAssets ?? null,
};
}
return Promise.resolve(appDetails);
return appDetails;
}
throw new Error("Not implemented");

View File

@@ -6,17 +6,17 @@ import type { TrendingGame } from "@types";
const getTrendingGames = async (_event: Electron.IpcMainInvokeEvent) => {
const language = await db
.get<string, string>(levelKeys.language, {
valueEncoding: "utf-8",
valueEncoding: "utf8",
})
.then((language) => language || "en");
const trendingGames = await HydraApi.get<TrendingGame[]>(
"/games/trending",
"/games/featured",
{ language },
{ needsAuth: false }
).catch(() => []);
return trendingGames;
return trendingGames.slice(0, 1);
};
registerEvent("getTrendingGames", getTrendingGames);

View File

@@ -0,0 +1,16 @@
import type { GameShop, ShopAssets } from "@types";
import { gamesShopAssetsSublevel, levelKeys } from "@main/level";
import { registerEvent } from "../register-event";
const saveGameShopAssets = async (
_event: Electron.IpcMainInvokeEvent,
objectId: string,
shop: GameShop,
assets: ShopAssets
): Promise<void> => {
const key = levelKeys.game(shop, objectId);
const existingAssets = await gamesShopAssetsSublevel.get(key);
return gamesShopAssetsSublevel.put(key, { ...existingAssets, ...assets });
};
registerEvent("saveGameShopAssets", saveGameShopAssets);

View File

@@ -1,74 +1,93 @@
import { HydraApi, logger, Ludusavi, WindowManager } from "@main/services";
import { CloudSync, HydraApi, logger, WindowManager } from "@main/services";
import fs from "node:fs";
import * as tar from "tar";
import { registerEvent } from "../register-event";
import axios from "axios";
import os from "node:os";
import path from "node:path";
import { backupsPath } from "@main/constants";
import type { GameShop } from "@types";
import { backupsPath, publicProfilePath } from "@main/constants";
import type { GameShop, LudusaviBackupMapping } from "@types";
import YAML from "yaml";
import { normalizePath } from "@main/helpers";
import { addTrailingSlash, normalizePath } from "@main/helpers";
import { SystemPath } from "@main/services/system-path";
import { gamesSublevel, levelKeys } from "@main/level";
export interface LudusaviBackup {
files: {
[key: string]: {
hash: string;
size: number;
};
};
}
export const transformLudusaviBackupPathIntoWindowsPath = (
backupPath: string,
winePrefixPath?: string | null
) => {
return backupPath
.replace(winePrefixPath ? addTrailingSlash(winePrefixPath) : "", "")
.replace("drive_c", "C:");
};
const replaceLudusaviBackupWithCurrentUser = (
export const addWinePrefixToWindowsPath = (
windowsPath: string,
winePrefixPath?: string | null
) => {
if (!winePrefixPath) {
return windowsPath;
}
return path.join(winePrefixPath, windowsPath.replace("C:", "drive_c"));
};
const restoreLudusaviBackup = (
backupPath: string,
title: string,
homeDir: string
homeDir: string,
winePrefixPath?: string | null,
artifactWinePrefixPath?: string | null
) => {
const gameBackupPath = path.join(backupPath, title);
const mappingYamlPath = path.join(gameBackupPath, "mapping.yaml");
const data = fs.readFileSync(mappingYamlPath, "utf8");
const manifest = YAML.parse(data) as {
backups: LudusaviBackup[];
backups: LudusaviBackupMapping[];
drives: Record<string, string>;
};
const currentHomeDir = normalizePath(SystemPath.getPath("home"));
const userProfilePath =
CloudSync.getWindowsLikeUserProfilePath(winePrefixPath);
/* Renaming logic */
if (os.platform() === "win32") {
const mappedHomeDir = path.join(
gameBackupPath,
path.join("drive-C", homeDir.replace("C:", ""))
);
if (fs.existsSync(mappedHomeDir)) {
fs.renameSync(
mappedHomeDir,
path.join(gameBackupPath, "drive-C", currentHomeDir.replace("C:", ""))
manifest.backups.forEach((backup) => {
Object.keys(backup.files).forEach((key) => {
const sourcePathWithDrives = Object.entries(manifest.drives).reduce(
(prev, [driveKey, driveValue]) => {
return prev.replace(driveValue, driveKey);
},
key
);
}
}
const backups = manifest.backups.map((backup: LudusaviBackup) => {
const files = Object.entries(backup.files).reduce((prev, [key, value]) => {
const updatedKey = key.replace(homeDir, currentHomeDir);
const sourcePath = path.join(gameBackupPath, sourcePathWithDrives);
return {
...prev,
[updatedKey]: value,
};
}, {});
logger.info(`Source path: ${sourcePath}`);
return {
...backup,
files,
};
const destinationPath = transformLudusaviBackupPathIntoWindowsPath(
key,
artifactWinePrefixPath
)
.replace(
homeDir,
addWinePrefixToWindowsPath(userProfilePath, winePrefixPath)
)
.replace(
publicProfilePath,
addWinePrefixToWindowsPath(publicProfilePath, winePrefixPath)
);
logger.info(`Moving ${sourcePath} to ${destinationPath}`);
fs.mkdirSync(path.dirname(destinationPath), { recursive: true });
if (fs.existsSync(destinationPath)) {
fs.unlinkSync(destinationPath);
}
fs.renameSync(sourcePath, destinationPath);
});
});
fs.writeFileSync(mappingYamlPath, YAML.stringify({ ...manifest, backups }));
};
const downloadGameArtifact = async (
@@ -78,10 +97,18 @@ const downloadGameArtifact = async (
gameArtifactId: string
) => {
try {
const { downloadUrl, objectKey, homeDir } = await HydraApi.post<{
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
const {
downloadUrl,
objectKey,
homeDir,
winePrefixPath: artifactWinePrefixPath,
} = await HydraApi.post<{
downloadUrl: string;
objectKey: string;
homeDir: string;
winePrefixPath: string | null;
}>(`/profile/games/artifacts/${gameArtifactId}/download`);
const zipLocation = path.join(SystemPath.getPath("userData"), objectKey);
@@ -109,34 +136,34 @@ const downloadGameArtifact = async (
response.data.pipe(writer);
writer.on("error", (err) => {
logger.error("Failed to write zip", err);
logger.error("Failed to write tar file", err);
throw err;
});
fs.mkdirSync(backupPath, { recursive: true });
writer.on("close", () => {
tar
.x({
file: zipLocation,
cwd: backupPath,
})
.then(async () => {
replaceLudusaviBackupWithCurrentUser(
backupPath,
objectId,
normalizePath(homeDir)
);
writer.on("close", async () => {
await tar.x({
file: zipLocation,
cwd: backupPath,
});
Ludusavi.restoreBackup(backupPath).then(() => {
WindowManager.mainWindow?.webContents.send(
`on-backup-download-complete-${objectId}-${shop}`,
true
);
});
});
restoreLudusaviBackup(
backupPath,
objectId,
normalizePath(homeDir),
game?.winePrefixPath,
artifactWinePrefixPath
);
WindowManager.mainWindow?.webContents.send(
`on-backup-download-complete-${objectId}-${shop}`,
true
);
});
} catch (err) {
logger.error("Failed to download game artifact", err);
WindowManager.mainWindow?.webContents.send(
`on-backup-download-complete-${objectId}-${shop}`,
false

View File

@@ -0,0 +1,14 @@
import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services";
const renameGameArtifact = async (
_event: Electron.IpcMainInvokeEvent,
gameArtifactId: string,
label: string
) => {
await HydraApi.put(`/profile/games/artifacts/${gameArtifactId}`, {
label,
});
};
registerEvent("renameGameArtifact", renameGameArtifact);

View File

@@ -0,0 +1,16 @@
import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services";
const toggleArtifactFreeze = async (
_event: Electron.IpcMainInvokeEvent,
gameArtifactId: string,
freeze: boolean
) => {
if (freeze) {
await HydraApi.put(`/profile/games/artifacts/${gameArtifactId}/freeze`);
} else {
await HydraApi.put(`/profile/games/artifacts/${gameArtifactId}/unfreeze`);
}
};
registerEvent("toggleArtifactFreeze", toggleArtifactFreeze);

View File

@@ -3,6 +3,7 @@ import { ipcMain } from "electron";
import "./catalogue/get-catalogue";
import "./catalogue/get-game-shop-details";
import "./catalogue/save-game-shop-assets";
import "./catalogue/get-how-long-to-beat";
import "./catalogue/get-random-game";
import "./catalogue/search-games";
@@ -19,6 +20,7 @@ import "./library/create-game-shortcut";
import "./library/close-game";
import "./library/delete-game-folder";
import "./library/get-game-by-object-id";
import "./library/sync-game-by-object-id";
import "./library/get-library";
import "./library/extract-game-download";
import "./library/open-game";
@@ -33,6 +35,8 @@ import "./library/remove-game-from-library";
import "./library/select-game-wine-prefix";
import "./library/reset-game-achievements";
import "./library/toggle-automatic-cloud-sync";
import "./library/get-default-wine-prefix-selection-path";
import "./library/create-steam-shortcut";
import "./misc/open-checkout";
import "./misc/open-external";
import "./misc/show-open-dialog";
@@ -83,7 +87,11 @@ import "./cloud-save/get-game-backup-preview";
import "./cloud-save/upload-save-game";
import "./cloud-save/delete-game-artifact";
import "./cloud-save/select-game-backup-path";
import "./cloud-save/toggle-artifact-freeze";
import "./cloud-save/rename-game-artifact";
import "./notifications/publish-new-repacks-notification";
import "./notifications/update-achievement-notification-window";
import "./notifications/show-achievement-test-notification";
import "./themes/add-custom-theme";
import "./themes/delete-custom-theme";
import "./themes/get-all-custom-themes";

View File

@@ -1,5 +1,6 @@
import { registerEvent } from "../register-event";
import { gamesSublevel, levelKeys } from "@main/level";
import { HydraApi } from "@main/services";
import type { GameShop } from "@types";
const addGameToFavorites = async (
@@ -12,6 +13,8 @@ const addGameToFavorites = async (
const game = await gamesSublevel.get(gameKey);
if (!game) return;
HydraApi.put(`/profile/games/${shop}/${objectId}/favorite`).catch(() => {});
try {
await gamesSublevel.put(gameKey, {
...game,

View File

@@ -1,12 +1,13 @@
import { registerEvent } from "../register-event";
import type { GameShop } from "@types";
import { steamGamesWorker } from "@main/workers";
import { createGame } from "@main/services/library-sync";
import { steamUrlBuilder } from "@shared";
import { updateLocalUnlockedAchievements } from "@main/services/achievements/update-local-unlocked-achivements";
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
import {
downloadsSublevel,
gamesShopAssetsSublevel,
gamesSublevel,
levelKeys,
} from "@main/level";
import { AchievementWatcherManager } from "@main/services/achievements/achievement-watcher-manager";
const addGameToLibrary = async (
_event: Electron.IpcMainInvokeEvent,
@@ -17,25 +18,18 @@ const addGameToLibrary = async (
const gameKey = levelKeys.game(shop, objectId);
let game = await gamesSublevel.get(gameKey);
const gameAssets = await gamesShopAssetsSublevel.get(gameKey);
if (game) {
await downloadsSublevel.del(gameKey);
await gamesSublevel.put(gameKey, {
...game,
isDeleted: false,
});
game.isDeleted = false;
await gamesSublevel.put(gameKey, game);
} else {
const steamGame = await steamGamesWorker.run(Number(objectId), {
name: "getById",
});
const iconUrl = steamGame?.clientIcon
? steamUrlBuilder.icon(objectId, steamGame.clientIcon)
: null;
game = {
title,
iconUrl,
iconUrl: gameAssets?.iconUrl ?? null,
objectId,
shop,
remoteId: null,
@@ -44,12 +38,15 @@ const addGameToLibrary = async (
lastTimePlayed: null,
};
await gamesSublevel.put(levelKeys.game(shop, objectId), game);
await gamesSublevel.put(gameKey, game);
}
await createGame(game).catch(() => {});
updateLocalUnlockedAchievements(game);
AchievementWatcherManager.firstSyncWithRemoteIfNeeded(
game.shop,
game.objectId
);
};
registerEvent("addGameToLibrary", addGameToLibrary);

View File

@@ -0,0 +1,181 @@
import { registerEvent } from "../register-event";
import type { GameShop, GameStats } from "@types";
import { gamesSublevel, levelKeys } from "@main/level";
import {
composeSteamShortcut,
getSteamLocation,
getSteamShortcuts,
getSteamUsersIds,
HydraApi,
logger,
SystemPath,
writeSteamShortcuts,
} from "@main/services";
import fs from "node:fs";
import axios from "axios";
import path from "node:path";
import { ASSETS_PATH } from "@main/constants";
const downloadAsset = async (downloadPath: string, url?: string | null) => {
try {
if (fs.existsSync(downloadPath)) {
return downloadPath;
}
if (!url) {
return null;
}
fs.mkdirSync(path.dirname(downloadPath), { recursive: true });
const response = await axios.get(url, { responseType: "arraybuffer" });
fs.writeFileSync(downloadPath, response.data);
return downloadPath;
} catch (error) {
logger.error("Failed to download asset", error);
return null;
}
};
const downloadAssetsFromSteam = async (
shop: GameShop,
objectId: string,
assets: GameStats["assets"]
) => {
const gameAssetsPath = path.join(ASSETS_PATH, `${shop}-${objectId}`);
return await Promise.all([
downloadAsset(path.join(gameAssetsPath, "icon.ico"), assets?.iconUrl),
downloadAsset(
path.join(gameAssetsPath, "hero.jpg"),
assets?.libraryHeroImageUrl
),
downloadAsset(path.join(gameAssetsPath, "logo.png"), assets?.logoImageUrl),
downloadAsset(
path.join(gameAssetsPath, "cover.jpg"),
assets?.coverImageUrl
),
downloadAsset(
path.join(gameAssetsPath, "library.jpg"),
assets?.libraryImageUrl
),
]);
};
const copyAssetIfExists = async (
sourcePath: string | null,
destinationPath: string
) => {
if (sourcePath && fs.existsSync(sourcePath)) {
logger.info("Copying Steam asset", sourcePath, destinationPath);
await fs.promises.cp(sourcePath, destinationPath);
}
};
const createSteamShortcut = async (
_event: Electron.IpcMainInvokeEvent,
shop: GameShop,
objectId: string
) => {
const gameKey = levelKeys.game(shop, objectId);
const game = await gamesSublevel.get(gameKey);
if (game) {
if (!game.executablePath) {
throw new Error("No executable path found for game");
}
const { assets } = await HydraApi.get<GameStats>(
`/games/stats?objectId=${objectId}&shop=${shop}`
);
const steamUserIds = await getSteamUsersIds();
if (!steamUserIds.length) {
logger.error("No Steam user ID found");
return;
}
const [iconImage, heroImage, logoImage, coverImage, libraryImage] =
await downloadAssetsFromSteam(game.shop, game.objectId, assets);
const newShortcut = composeSteamShortcut(
game.title,
game.executablePath,
iconImage
);
for (const steamUserId of steamUserIds) {
logger.info("Adding shortcut for Steam user", steamUserId);
const steamShortcuts = await getSteamShortcuts(steamUserId);
if (steamShortcuts.some((shortcut) => shortcut.appname === game.title)) {
continue;
}
const gridPath = path.join(
await getSteamLocation(),
"userdata",
steamUserId.toString(),
"config",
"grid"
);
await fs.promises.mkdir(gridPath, { recursive: true });
await Promise.all([
copyAssetIfExists(
heroImage,
path.join(gridPath, `${newShortcut.appid}_hero.jpg`)
),
copyAssetIfExists(
logoImage,
path.join(gridPath, `${newShortcut.appid}_logo.png`)
),
copyAssetIfExists(
coverImage,
path.join(gridPath, `${newShortcut.appid}p.jpg`)
),
copyAssetIfExists(
libraryImage,
path.join(gridPath, `${newShortcut.appid}.jpg`)
),
]);
steamShortcuts.push(newShortcut);
logger.info(newShortcut);
logger.info("Writing Steam shortcuts", steamShortcuts);
await writeSteamShortcuts(steamUserId, steamShortcuts);
}
if (process.platform === "linux" && !game.winePrefixPath) {
const steamWinePrefixes = path.join(
SystemPath.getPath("home"),
".local",
"share",
"Steam",
"steamapps",
"compatdata"
);
const winePrefixPath = path.join(
steamWinePrefixes,
newShortcut.appid.toString(),
"pfx"
);
await fs.promises.mkdir(winePrefixPath, { recursive: true });
await gamesSublevel.put(gameKey, {
...game,
winePrefixPath,
});
}
}
};
registerEvent("createSteamShortcut", createSteamShortcut);

View File

@@ -0,0 +1,30 @@
import { logger, SystemPath } from "@main/services";
import fs from "node:fs";
import path from "node:path";
import { registerEvent } from "../register-event";
const getDefaultWinePrefixSelectionPath = async (
_event: Electron.IpcMainInvokeEvent
) => {
try {
const steamWinePrefixes = path.join(
SystemPath.getPath("home"),
".local",
"share",
"Steam",
"steamapps",
"compatdata"
);
return await fs.promises.realpath(steamWinePrefixes);
} catch (err) {
logger.error("Failed to get default wine prefix selection path", err);
return null;
}
};
registerEvent(
"getDefaultWinePrefixSelectionPath",
getDefaultWinePrefixSelectionPath
);

View File

@@ -1,6 +1,10 @@
import type { LibraryGame } from "@types";
import { registerEvent } from "../register-event";
import { downloadsSublevel, gamesSublevel } from "@main/level";
import {
downloadsSublevel,
gamesShopAssetsSublevel,
gamesSublevel,
} from "@main/level";
const getLibrary = async (): Promise<LibraryGame[]> => {
return gamesSublevel
@@ -12,11 +16,13 @@ const getLibrary = async (): Promise<LibraryGame[]> => {
.filter(([_key, game]) => game.isDeleted === false)
.map(async ([key, game]) => {
const download = await downloadsSublevel.get(key);
const gameAssets = await gamesShopAssetsSublevel.get(key);
return {
id: key,
...game,
download: download ?? null,
...gameAssets,
};
})
);

View File

@@ -12,16 +12,14 @@ const openGameInstallerPath = async (
) => {
const download = await downloadsSublevel.get(levelKeys.game(shop, objectId));
if (!download || !download.folderName || !download.downloadPath) return true;
if (!download?.folderName || !download.downloadPath) return;
const gamePath = path.join(
download.downloadPath ?? (await getDownloadsPath()),
download.folderName!
download.folderName
);
shell.showItemInFolder(gamePath);
return true;
};
registerEvent("openGameInstallerPath", openGameInstallerPath);

View File

@@ -1,5 +1,6 @@
import { registerEvent } from "../register-event";
import { gamesSublevel, levelKeys } from "@main/level";
import { HydraApi } from "@main/services";
import type { GameShop } from "@types";
const removeGameFromFavorites = async (
@@ -12,6 +13,8 @@ const removeGameFromFavorites = async (
const game = await gamesSublevel.get(gameKey);
if (!game) return;
HydraApi.put(`/profile/games/${shop}/${objectId}/unfavorite`).catch(() => {});
try {
await gamesSublevel.put(gameKey, {
...game,

View File

@@ -16,7 +16,8 @@ const resetGameAchievements = async (
objectId: string
) => {
try {
const game = await gamesSublevel.get(levelKeys.game(shop, objectId));
const levelKey = levelKeys.game(shop, objectId);
const game = await gamesSublevel.get(levelKey);
if (!game) return;
@@ -29,8 +30,6 @@ const resetGameAchievements = async (
}
}
const levelKey = levelKeys.game(game.shop, game.objectId);
await gameAchievementsSublevel
.get(levelKey)
.then(async (gameAchievements) => {

View File

@@ -1,5 +1,7 @@
import { registerEvent } from "../register-event";
import fs from "node:fs";
import { levelKeys, gamesSublevel } from "@main/level";
import { Wine } from "@main/services";
import type { GameShop } from "@types";
const selectGameWinePrefix = async (
@@ -14,9 +16,24 @@ const selectGameWinePrefix = async (
if (!game) return;
if (!winePrefixPath) {
await gamesSublevel.put(gameKey, {
...game,
winePrefixPath: null,
});
return;
}
const realWinePrefixPath = await fs.promises.realpath(winePrefixPath);
if (!Wine.validatePrefix(realWinePrefixPath)) {
throw new Error("Invalid wine prefix path");
}
await gamesSublevel.put(gameKey, {
...game,
winePrefixPath: winePrefixPath,
winePrefixPath: realWinePrefixPath,
});
};

View File

@@ -0,0 +1,32 @@
import { registerEvent } from "../register-event";
import { gamesSublevel, levelKeys } from "@main/level";
import { HydraApi } from "@main/services";
import type { GameShop, UserGameDetails } from "@types";
const syncGameByObjectId = async (
_event: Electron.IpcMainInvokeEvent,
shop: GameShop,
objectId: string
) => {
return HydraApi.get<UserGameDetails>(
`/profile/games/${shop}/${objectId}`
).then(async (res) => {
const { id, playTimeInSeconds, isFavorite, ...rest } = res;
const gameKey = levelKeys.game(shop, objectId);
const currentData = await gamesSublevel.get(gameKey);
await gamesSublevel.put(gameKey, {
...currentData,
...rest,
remoteId: id,
playTimeInMilliseconds: playTimeInSeconds * 1000,
favorite: isFavorite ?? currentData?.favorite,
});
return res;
});
};
registerEvent("syncGameByObjectId", syncGameByObjectId);

View File

@@ -7,11 +7,11 @@ const verifyExecutablePathInUse = async (
) => {
for await (const game of gamesSublevel.values()) {
if (game.executablePath === executablePath) {
return true;
return game;
}
}
return false;
return null;
};
registerEvent("verifyExecutablePathInUse", verifyExecutablePathInUse);

View File

@@ -6,7 +6,7 @@ import { db, levelKeys } from "@main/level";
const getBadges = async (_event: Electron.IpcMainInvokeEvent) => {
const language = await db
.get<string, string>(levelKeys.language, {
valueEncoding: "utf-8",
valueEncoding: "utf8",
})
.then((language) => language || "en");

View File

@@ -0,0 +1,15 @@
import { registerEvent } from "../register-event";
import { WindowManager } from "@main/services";
const showAchievementTestNotification = async (
_event: Electron.IpcMainInvokeEvent
) => {
setTimeout(() => {
WindowManager.showAchievementTestNotification();
}, 1000);
};
registerEvent(
"showAchievementTestNotification",
showAchievementTestNotification
);

View File

@@ -0,0 +1,29 @@
import { db, levelKeys } from "@main/level";
import { registerEvent } from "../register-event";
import { WindowManager } from "@main/services";
import { UserPreferences } from "@types";
const updateAchievementCustomNotificationWindow = async (
_event: Electron.IpcMainInvokeEvent
) => {
const userPreferences = await db.get<string, UserPreferences>(
levelKeys.userPreferences,
{
valueEncoding: "json",
}
);
WindowManager.closeNotificationWindow();
if (
userPreferences.achievementNotificationsEnabled !== false &&
userPreferences.achievementCustomNotificationsEnabled !== false
) {
WindowManager.createNotificationWindow();
}
};
registerEvent(
"updateAchievementCustomNotificationWindow",
updateAchievementCustomNotificationWindow
);

View File

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

View File

@@ -1,5 +1,6 @@
import { themesSublevel } from "@main/level";
import { registerEvent } from "../register-event";
import { WindowManager } from "@main/services";
const toggleCustomTheme = async (
_event: Electron.IpcMainInvokeEvent,
@@ -17,6 +18,8 @@ const toggleCustomTheme = async (
isActive,
updatedAt: new Date(),
});
WindowManager.notificationWindow?.webContents.send("on-custom-theme-updated");
};
registerEvent("toggleCustomTheme", toggleCustomTheme);

View File

@@ -20,7 +20,10 @@ const updateCustomTheme = async (
});
if (theme.isActive) {
WindowManager.mainWindow?.webContents.send("css-injected", code);
WindowManager.mainWindow?.webContents.send("on-custom-theme-updated");
WindowManager.notificationWindow?.webContents.send(
"on-custom-theme-updated"
);
}
};

View File

@@ -1,11 +1,14 @@
import { registerEvent } from "../register-event";
import type { Download, StartGameDownloadPayload } from "@types";
import { DownloadManager, HydraApi, logger } from "@main/services";
import { steamGamesWorker } from "@main/workers";
import { createGame } from "@main/services/library-sync";
import { Downloader, DownloadError, steamUrlBuilder } from "@shared";
import { downloadsSublevel, gamesSublevel, levelKeys } from "@main/level";
import { Downloader, DownloadError } from "@shared";
import {
downloadsSublevel,
gamesShopAssetsSublevel,
gamesSublevel,
levelKeys,
} from "@main/level";
import { AxiosError } from "axios";
const startGameDownload = async (
@@ -36,6 +39,7 @@ const startGameDownload = async (
}
const game = await gamesSublevel.get(gameKey);
const gameAssets = await gamesShopAssetsSublevel.get(gameKey);
/* Delete any previous download */
await downloadsSublevel.del(gameKey);
@@ -46,17 +50,9 @@ const startGameDownload = async (
isDeleted: false,
});
} else {
const steamGame = await steamGamesWorker.run(Number(objectId), {
name: "getById",
});
const iconUrl = steamGame?.clientIcon
? steamUrlBuilder.icon(objectId, steamGame.clientIcon)
: null;
await gamesSublevel.put(gameKey, {
title,
iconUrl,
iconUrl: gameAssets?.iconUrl ?? null,
objectId,
shop,
remoteId: null,

View File

@@ -16,7 +16,7 @@ const updateUserPreferences = async (
if (preferences.language) {
await db.put<string, string>(levelKeys.language, preferences.language, {
valueEncoding: "utf-8",
valueEncoding: "utf8",
});
i18next.changeLanguage(preferences.language);

View File

@@ -3,6 +3,7 @@ import { registerEvent } from "../register-event";
import { HydraApi } from "@main/services";
import { db, levelKeys } from "@main/level";
import { AchievementWatcherManager } from "@main/services/achievements/achievement-watcher-manager";
const getComparedUnlockedAchievements = async (
_event: Electron.IpcMainInvokeEvent,
@@ -10,6 +11,8 @@ const getComparedUnlockedAchievements = async (
shop: GameShop,
userId: string
) => {
await AchievementWatcherManager.firstSyncWithRemoteIfNeeded(shop, objectId);
const userPreferences = await db.get<string, UserPreferences | null>(
levelKeys.userPreferences,
{

View File

@@ -2,6 +2,7 @@ import type { GameShop, UserAchievement, UserPreferences } from "@types";
import { registerEvent } from "../register-event";
import { getGameAchievementData } from "@main/services/achievements/get-game-achievement-data";
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
import { AchievementWatcherManager } from "@main/services/achievements/achievement-watcher-manager";
export const getUnlockedAchievements = async (
objectId: string,
@@ -62,7 +63,7 @@ export const getUnlockedAchievements = async (
!achievementData.hidden || showHiddenAchievementsDescription
? achievementData.description
: undefined,
} as UserAchievement;
};
})
.sort((a, b) => {
if (a.unlocked && !b.unlocked) return -1;
@@ -79,6 +80,7 @@ const getUnlockedAchievementsEvent = async (
objectId: string,
shop: GameShop
): Promise<UserAchievement[]> => {
AchievementWatcherManager.firstSyncWithRemoteIfNeeded(shop, objectId);
return getUnlockedAchievements(objectId, shop, false);
};

View File

@@ -1,97 +1,12 @@
import { registerEvent } from "../register-event";
import { HydraApi, logger } from "@main/services";
import { steamGamesWorker } from "@main/workers";
import { HydraApi } from "@main/services";
import type { UserProfile } from "@types";
import { steamUrlBuilder } from "@shared";
const getSteamGame = async (objectId: string) => {
try {
const steamGame = await steamGamesWorker.run(Number(objectId), {
name: "getById",
});
return {
title: steamGame.name as string,
iconUrl: steamUrlBuilder.icon(objectId, steamGame.clientIcon),
};
} catch (err) {
logger.error("Failed to get Steam game", err);
return null;
}
};
const getUser = async (
_event: Electron.IpcMainInvokeEvent,
userId: string
): Promise<UserProfile | null> => {
try {
const profile = await HydraApi.get<UserProfile | null>(`/users/${userId}`);
if (!profile) return null;
const recentGames = await Promise.all(
profile.recentGames
.map(async (game) => {
const steamGame = await getSteamGame(game.objectId);
return {
...game,
...steamGame,
};
})
.filter((game) => game)
);
const libraryGames = await Promise.all(
profile.libraryGames
.map(async (game) => {
const steamGame = await getSteamGame(game.objectId);
return {
...game,
...steamGame,
};
})
.filter((game) => game)
);
if (profile.currentGame) {
const steamGame = await getSteamGame(profile.currentGame.objectId);
if (steamGame) {
profile.currentGame = {
...profile.currentGame,
...steamGame,
};
}
}
const friends = await Promise.all(
profile.friends.map(async (friend) => {
if (!friend.currentGame) return friend;
const currentGame = await getSteamGame(friend.currentGame.objectId);
return {
...friend,
currentGame: {
...friend.currentGame,
...currentGame,
},
};
})
);
return {
...profile,
friends,
libraryGames,
recentGames,
};
} catch (err) {
return null;
}
return HydraApi.get<UserProfile>(`/users/${userId}`).catch(() => null);
};
registerEvent("getUser", getUser);

View File

@@ -0,0 +1,352 @@
// @generated by protobuf-ts 2.10.0
// @generated from protobuf file "envelope.proto" (syntax proto3)
// tslint:disable
import type { BinaryWriteOptions } from "@protobuf-ts/runtime";
import type { IBinaryWriter } from "@protobuf-ts/runtime";
import { WireType } from "@protobuf-ts/runtime";
import type { BinaryReadOptions } from "@protobuf-ts/runtime";
import type { IBinaryReader } from "@protobuf-ts/runtime";
import { UnknownFieldHandler } from "@protobuf-ts/runtime";
import type { PartialMessage } from "@protobuf-ts/runtime";
import { reflectionMergePartial } from "@protobuf-ts/runtime";
import { MessageType } from "@protobuf-ts/runtime";
/**
* @generated from protobuf message FriendRequest
*/
export interface FriendRequest {
/**
* @generated from protobuf field: int32 friend_request_count = 1;
*/
friendRequestCount: number;
/**
* @generated from protobuf field: optional string sender_id = 2;
*/
senderId?: string;
}
/**
* @generated from protobuf message FriendGameSession
*/
export interface FriendGameSession {
/**
* @generated from protobuf field: string object_id = 1;
*/
objectId: string;
/**
* @generated from protobuf field: string shop = 2;
*/
shop: string;
/**
* @generated from protobuf field: string friend_id = 3;
*/
friendId: string;
}
/**
* @generated from protobuf message Envelope
*/
export interface Envelope {
/**
* @generated from protobuf oneof: payload
*/
payload:
| {
oneofKind: "friendRequest";
/**
* @generated from protobuf field: FriendRequest friend_request = 1;
*/
friendRequest: FriendRequest;
}
| {
oneofKind: "friendGameSession";
/**
* @generated from protobuf field: FriendGameSession friend_game_session = 2;
*/
friendGameSession: FriendGameSession;
}
| {
oneofKind: undefined;
};
}
// @generated message type with reflection information, may provide speed optimized methods
class FriendRequest$Type extends MessageType<FriendRequest> {
constructor() {
super("FriendRequest", [
{
no: 1,
name: "friend_request_count",
kind: "scalar",
T: 5 /*ScalarType.INT32*/,
},
{
no: 2,
name: "sender_id",
kind: "scalar",
opt: true,
T: 9 /*ScalarType.STRING*/,
},
]);
}
create(value?: PartialMessage<FriendRequest>): FriendRequest {
const message = globalThis.Object.create(this.messagePrototype!);
message.friendRequestCount = 0;
if (value !== undefined)
reflectionMergePartial<FriendRequest>(this, message, value);
return message;
}
internalBinaryRead(
reader: IBinaryReader,
length: number,
options: BinaryReadOptions,
target?: FriendRequest
): FriendRequest {
let message = target ?? this.create(),
end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* int32 friend_request_count */ 1:
message.friendRequestCount = reader.int32();
break;
case /* optional string sender_id */ 2:
message.senderId = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(
this.typeName,
message,
fieldNo,
wireType,
d
);
}
}
return message;
}
internalBinaryWrite(
message: FriendRequest,
writer: IBinaryWriter,
options: BinaryWriteOptions
): IBinaryWriter {
/* int32 friend_request_count = 1; */
if (message.friendRequestCount !== 0)
writer.tag(1, WireType.Varint).int32(message.friendRequestCount);
/* optional string sender_id = 2; */
if (message.senderId !== undefined)
writer.tag(2, WireType.LengthDelimited).string(message.senderId);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(
this.typeName,
message,
writer
);
return writer;
}
}
/**
* @generated MessageType for protobuf message FriendRequest
*/
export const FriendRequest = new FriendRequest$Type();
// @generated message type with reflection information, may provide speed optimized methods
class FriendGameSession$Type extends MessageType<FriendGameSession> {
constructor() {
super("FriendGameSession", [
{ no: 1, name: "object_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 2, name: "shop", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
{ no: 3, name: "friend_id", kind: "scalar", T: 9 /*ScalarType.STRING*/ },
]);
}
create(value?: PartialMessage<FriendGameSession>): FriendGameSession {
const message = globalThis.Object.create(this.messagePrototype!);
message.objectId = "";
message.shop = "";
message.friendId = "";
if (value !== undefined)
reflectionMergePartial<FriendGameSession>(this, message, value);
return message;
}
internalBinaryRead(
reader: IBinaryReader,
length: number,
options: BinaryReadOptions,
target?: FriendGameSession
): FriendGameSession {
let message = target ?? this.create(),
end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* string object_id */ 1:
message.objectId = reader.string();
break;
case /* string shop */ 2:
message.shop = reader.string();
break;
case /* string friend_id */ 3:
message.friendId = reader.string();
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(
this.typeName,
message,
fieldNo,
wireType,
d
);
}
}
return message;
}
internalBinaryWrite(
message: FriendGameSession,
writer: IBinaryWriter,
options: BinaryWriteOptions
): IBinaryWriter {
/* string object_id = 1; */
if (message.objectId !== "")
writer.tag(1, WireType.LengthDelimited).string(message.objectId);
/* string shop = 2; */
if (message.shop !== "")
writer.tag(2, WireType.LengthDelimited).string(message.shop);
/* string friend_id = 3; */
if (message.friendId !== "")
writer.tag(3, WireType.LengthDelimited).string(message.friendId);
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(
this.typeName,
message,
writer
);
return writer;
}
}
/**
* @generated MessageType for protobuf message FriendGameSession
*/
export const FriendGameSession = new FriendGameSession$Type();
// @generated message type with reflection information, may provide speed optimized methods
class Envelope$Type extends MessageType<Envelope> {
constructor() {
super("Envelope", [
{
no: 1,
name: "friend_request",
kind: "message",
oneof: "payload",
T: () => FriendRequest,
},
{
no: 2,
name: "friend_game_session",
kind: "message",
oneof: "payload",
T: () => FriendGameSession,
},
]);
}
create(value?: PartialMessage<Envelope>): Envelope {
const message = globalThis.Object.create(this.messagePrototype!);
message.payload = { oneofKind: undefined };
if (value !== undefined)
reflectionMergePartial<Envelope>(this, message, value);
return message;
}
internalBinaryRead(
reader: IBinaryReader,
length: number,
options: BinaryReadOptions,
target?: Envelope
): Envelope {
let message = target ?? this.create(),
end = reader.pos + length;
while (reader.pos < end) {
let [fieldNo, wireType] = reader.tag();
switch (fieldNo) {
case /* FriendRequest friend_request */ 1:
message.payload = {
oneofKind: "friendRequest",
friendRequest: FriendRequest.internalBinaryRead(
reader,
reader.uint32(),
options,
(message.payload as any).friendRequest
),
};
break;
case /* FriendGameSession friend_game_session */ 2:
message.payload = {
oneofKind: "friendGameSession",
friendGameSession: FriendGameSession.internalBinaryRead(
reader,
reader.uint32(),
options,
(message.payload as any).friendGameSession
),
};
break;
default:
let u = options.readUnknownField;
if (u === "throw")
throw new globalThis.Error(
`Unknown field ${fieldNo} (wire type ${wireType}) for ${this.typeName}`
);
let d = reader.skip(wireType);
if (u !== false)
(u === true ? UnknownFieldHandler.onRead : u)(
this.typeName,
message,
fieldNo,
wireType,
d
);
}
}
return message;
}
internalBinaryWrite(
message: Envelope,
writer: IBinaryWriter,
options: BinaryWriteOptions
): IBinaryWriter {
/* FriendRequest friend_request = 1; */
if (message.payload.oneofKind === "friendRequest")
FriendRequest.internalBinaryWrite(
message.payload.friendRequest,
writer.tag(1, WireType.LengthDelimited).fork(),
options
).join();
/* FriendGameSession friend_game_session = 2; */
if (message.payload.oneofKind === "friendGameSession")
FriendGameSession.internalBinaryWrite(
message.payload.friendGameSession,
writer.tag(2, WireType.LengthDelimited).fork(),
options
).join();
let u = options.writeUnknownFields;
if (u !== false)
(u == true ? UnknownFieldHandler.onWrite : u)(
this.typeName,
message,
writer
);
return writer;
}
}
/**
* @generated MessageType for protobuf message Envelope
*/
export const Envelope = new Envelope$Type();

View File

@@ -32,3 +32,8 @@ export const isPortableVersion = () => {
export const normalizePath = (str: string) =>
path.posix.normalize(str).replace(/\\/g, "/");
export const addTrailingSlash = (str: string) =>
str.endsWith("/") ? str : `${str}/`;
export * from "./reg-parser";

View File

@@ -0,0 +1,58 @@
type RegValue = string | number | null;
interface RegEntry {
path: string;
timestamp?: string;
values: Record<string, RegValue>;
}
export function parseRegFile(content: string): RegEntry[] {
const lines = content.split(/\r?\n/);
const entries: RegEntry[] = [];
let currentPath: string | null = null;
let currentEntry: RegEntry | null = null;
for (const rawLine of lines) {
const line = rawLine.trim();
if (!line || line.startsWith(";") || line.startsWith(";;")) continue;
if (line.startsWith("#")) {
const match = line.match(/^#time=(\w+)/);
if (match && currentEntry) {
currentEntry.timestamp = match[1];
}
continue;
}
if (line.startsWith("[")) {
const match = line.match(/^\[(.+?)\](?:\s+\d+)?/);
if (match) {
if (currentEntry) entries.push(currentEntry);
currentPath = match[1];
currentEntry = { path: currentPath, values: {} };
}
} else if (currentEntry) {
const kvMatch = line.match(/^"?(.*?)"?=(.*)$/);
if (kvMatch) {
const [, key, rawValue] = kvMatch;
let value: RegValue;
if (rawValue === '""') {
value = "";
} else if (rawValue.startsWith("dword:")) {
value = parseInt(rawValue.slice(6), 16);
} else if (rawValue.startsWith('"') && rawValue.endsWith('"')) {
value = rawValue.slice(1, -1);
} else {
value = rawValue;
}
currentEntry.values[key || "@"] = value;
}
}
}
if (currentEntry) entries.push(currentEntry);
return entries;
}

View File

@@ -4,7 +4,12 @@ import i18n from "i18next";
import path from "node:path";
import url from "node:url";
import { electronApp, optimizer } from "@electron-toolkit/utils";
import { logger, WindowManager } from "@main/services";
import {
logger,
clearGamesPlaytime,
WindowManager,
Lock,
} from "@main/services";
import resources from "@locales";
import { PythonRPC } from "./services/python-rpc";
import { db, levelKeys } from "./level";
@@ -23,7 +28,9 @@ autoUpdater.logger = logger;
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) app.quit();
app.commandLine.appendSwitch("--no-sandbox");
if (process.platform !== "linux") {
app.commandLine.appendSwitch("--no-sandbox");
}
i18n.init({
resources,
@@ -59,9 +66,11 @@ app.whenReady().then(async () => {
await loadState();
const language = await db.get<string, string>(levelKeys.language, {
valueEncoding: "utf-8",
});
const language = await db
.get<string, string>(levelKeys.language, {
valueEncoding: "utf8",
})
.catch(() => "en");
if (language) i18n.changeLanguage(language);
@@ -69,6 +78,7 @@ app.whenReady().then(async () => {
WindowManager.createMainWindow();
}
WindowManager.createNotificationWindow();
WindowManager.createSystemTray(language || "en");
});
@@ -138,9 +148,19 @@ app.on("window-all-closed", () => {
WindowManager.mainWindow = null;
});
app.on("before-quit", () => {
/* Disconnects libtorrent */
PythonRPC.kill();
let canAppBeClosed = false;
app.on("before-quit", async (e) => {
await Lock.releaseLock();
if (!canAppBeClosed) {
e.preventDefault();
/* Disconnects libtorrent */
PythonRPC.kill();
await clearGamesPlaytime();
canAppBeClosed = true;
app.quit();
}
});
app.on("activate", () => {

View File

@@ -0,0 +1,11 @@
import type { ShopAssets } from "@types";
import { db } from "../level";
import { levelKeys } from "./keys";
export const gamesShopAssetsSublevel = db.sublevel<string, ShopAssets>(
levelKeys.gameShopAssets,
{
valueEncoding: "json",
}
);

View File

@@ -1,5 +1,6 @@
export * from "./downloads";
export * from "./games";
export * from "./game-shop-assets";
export * from "./game-shop-cache";
export * from "./game-achievements";
export * from "./keys";

View File

@@ -6,6 +6,7 @@ export const levelKeys = {
user: "user",
auth: "auth",
themes: "themes",
gameShopAssets: "gameShopAssets",
gameShopCache: "gameShopCache",
gameShopCacheItem: (shop: GameShop, objectId: string, language: string) =>
`${shop}:${objectId}:${language}`,
@@ -14,4 +15,5 @@ export const levelKeys = {
userPreferences: "userPreferences",
language: "language",
screenState: "screenState",
rpcPassword: "rpcPassword",
};

View File

@@ -1,18 +1,25 @@
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 { downloadsSublevel } from "./level/sublevels/downloads";
import { sortBy } from "lodash-es";
import { Downloader } from "@shared";
import { levelKeys, db } from "./level";
import type { UserPreferences } from "@types";
import { TorBoxClient } from "./services/download/torbox";
import { CommonRedistManager } from "./services/common-redist-manager";
import { SystemPath } from "./services/system-path";
import {
WSClient,
SystemPath,
CommonRedistManager,
TorBoxClient,
RealDebridClient,
Aria2,
DownloadManager,
HydraApi,
uploadGamesBatch,
startMainLoop,
Ludusavi,
Lock,
} from "@main/services";
export const loadState = async () => {
SystemPath.checkIfPathsAreAvailable();
await Lock.acquireLock();
const userPreferences = await db.get<string, UserPreferences | null>(
levelKeys.userPreferences,
@@ -23,7 +30,9 @@ export const loadState = async () => {
await import("./events");
Aria2.spawn();
if (process.platform !== "darwin") {
Aria2.spawn();
}
if (userPreferences?.realDebridApiToken) {
RealDebridClient.authorize(userPreferences.realDebridApiToken);
@@ -33,10 +42,12 @@ export const loadState = async () => {
TorBoxClient.authorize(userPreferences.torBoxApiToken);
}
Ludusavi.addManifestToLudusaviConfig();
Ludusavi.copyConfigFileToUserData();
Ludusavi.copyBinaryToUserData();
await HydraApi.setupApi().then(() => {
uploadGamesBatch();
WSClient.connect();
});
const downloads = await downloadsSublevel
@@ -70,4 +81,6 @@ export const loadState = async () => {
startMainLoop();
CommonRedistManager.downloadCommonRedist();
SystemPath.checkIfPathsAreAvailable();
};

View File

@@ -7,11 +7,19 @@ import {
findAllAchievementFiles,
getAlternativeObjectIds,
} from "./find-achivement-files";
import type { AchievementFile, Game, UnlockedAchievement } from "@types";
import type {
AchievementFile,
Game,
GameShop,
UnlockedAchievement,
UserPreferences,
} from "@types";
import { achievementsLogger } from "../logger";
import { Cracker } from "@shared";
import { publishCombinedNewAchievementNotification } from "../notifications";
import { gamesSublevel } from "@main/level";
import { db, gamesSublevel, levelKeys } from "@main/level";
import { WindowManager } from "../window-manager";
import { setTimeout } from "node:timers/promises";
const fileStats: Map<string, number> = new Map();
const fltFiles: Map<string, Set<string>> = new Map();
@@ -30,7 +38,7 @@ const watchAchievementsWindows = async () => {
const gameAchievementFiles: AchievementFile[] = [];
for (const objectId of getAlternativeObjectIds(game.objectId)) {
gameAchievementFiles.push(...(achievementFiles.get(objectId) || []));
gameAchievementFiles.push(...(achievementFiles.get(objectId) ?? []));
gameAchievementFiles.push(
...findAchievementFileInExecutableDirectory(game)
@@ -120,6 +128,11 @@ const compareFile = (game: Game, file: AchievementFile) => {
);
return processAchievementFileDiff(game, file);
} catch (err) {
achievementsLogger.error(
"Error reading file",
file.filePath,
err instanceof Error ? err.message : err
);
fileStats.set(file.filePath, -1);
return;
}
@@ -129,20 +142,69 @@ const processAchievementFileDiff = async (
game: Game,
file: AchievementFile
) => {
const unlockedAchievements = parseAchievementFile(file.filePath, file.type);
const parsedAchievements = parseAchievementFile(file.filePath, file.type);
if (unlockedAchievements.length) {
return mergeAchievements(game, unlockedAchievements, true);
if (parsedAchievements.length) {
return mergeAchievements(game, parsedAchievements, true);
}
return 0;
};
export class AchievementWatcherManager {
private static hasFinishedMergingWithRemote = false;
private static _hasFinishedPreSearch = false;
public static get hasFinishedPreSearch() {
return this._hasFinishedPreSearch;
}
public static readonly alreadySyncedGames: Map<string, boolean> = new Map();
public static async firstSyncWithRemoteIfNeeded(
shop: GameShop,
objectId: string
) {
const gameKey = levelKeys.game(shop, objectId);
if (this.alreadySyncedGames.get(gameKey)) return;
const game = await gamesSublevel.get(gameKey).catch(() => null);
if (!game || game.isDeleted) return;
const gameAchievementFiles = findAchievementFiles(game);
const achievementFileInsideDirectory =
findAchievementFileInExecutableDirectory(game);
gameAchievementFiles.push(...achievementFileInsideDirectory);
const unlockedAchievements: UnlockedAchievement[] = [];
for (const achievementFile of gameAchievementFiles) {
const localAchievementFile = parseAchievementFile(
achievementFile.filePath,
achievementFile.type
);
if (localAchievementFile.length) {
unlockedAchievements.push(...localAchievementFile);
}
}
this.alreadySyncedGames.set(gameKey, true);
const newAchievements = await mergeAchievements(
game,
unlockedAchievements,
false
);
if (newAchievements > 0) {
this.notifyCombinedAchievementsUnlocked(1, newAchievements);
}
}
public static watchAchievements() {
if (!this.hasFinishedMergingWithRemote) return;
if (!this.hasFinishedPreSearch) return;
if (process.platform === "win32") {
return watchAchievementsWindows();
@@ -181,10 +243,14 @@ export class AchievementWatcherManager {
}
}
return mergeAchievements(game, unlockedAchievements, false);
if (unlockedAchievements.length) {
return mergeAchievements(game, unlockedAchievements, false);
}
return 0;
}
private static preSearchAchievementsWindows = async () => {
private static async getGameAchievementFilesWindows() {
const games = await gamesSublevel
.values()
.all()
@@ -194,24 +260,24 @@ export class AchievementWatcherManager {
return Promise.all(
games.map((game) => {
const gameAchievementFiles: AchievementFile[] = [];
const achievementFiles: AchievementFile[] = [];
for (const objectId of getAlternativeObjectIds(game.objectId)) {
gameAchievementFiles.push(
achievementFiles.push(
...(gameAchievementFilesMap.get(objectId) || [])
);
gameAchievementFiles.push(
achievementFiles.push(
...findAchievementFileInExecutableDirectory(game)
);
}
return this.preProcessGameAchievementFiles(game, gameAchievementFiles);
return { game, achievementFiles };
})
);
};
}
private static preSearchAchievementsWithWine = async () => {
private static async getGameAchievementFilesLinux() {
const games = await gamesSublevel
.values()
.all()
@@ -219,42 +285,76 @@ export class AchievementWatcherManager {
return Promise.all(
games.map((game) => {
const gameAchievementFiles = findAchievementFiles(game);
const achievementFiles = findAchievementFiles(game);
const achievementFileInsideDirectory =
findAchievementFileInExecutableDirectory(game);
gameAchievementFiles.push(...achievementFileInsideDirectory);
achievementFiles.push(...achievementFileInsideDirectory);
return this.preProcessGameAchievementFiles(game, gameAchievementFiles);
return { game, achievementFiles };
})
);
};
}
private static async notifyCombinedAchievementsUnlocked(
totalNewGamesWithAchievements: number,
totalNewAchievements: number
) {
const userPreferences = await db.get<string, UserPreferences>(
levelKeys.userPreferences,
{
valueEncoding: "json",
}
);
if (userPreferences.achievementCustomNotificationsEnabled !== false) {
WindowManager.notificationWindow?.webContents.send(
"on-combined-achievements-unlocked",
totalNewGamesWithAchievements,
totalNewAchievements,
userPreferences.achievementCustomNotificationPosition ?? "top-left"
);
} else {
publishCombinedNewAchievementNotification(
totalNewAchievements,
totalNewGamesWithAchievements
);
}
}
public static async preSearchAchievements() {
try {
const newAchievementsCount =
const gameAchievementFiles =
process.platform === "win32"
? await this.preSearchAchievementsWindows()
: await this.preSearchAchievementsWithWine();
? await this.getGameAchievementFilesWindows()
: await this.getGameAchievementFilesLinux();
const newAchievementsCount = await Promise.all(
gameAchievementFiles.map(({ game, achievementFiles }) => {
return this.preProcessGameAchievementFiles(game, achievementFiles);
})
);
const totalNewGamesWithAchievements = newAchievementsCount.filter(
(achievements) => achievements
).length;
const totalNewAchievements = newAchievementsCount.reduce(
(acc, val) => acc + val,
0
);
if (totalNewAchievements > 0) {
publishCombinedNewAchievementNotification(
totalNewAchievements,
totalNewGamesWithAchievements
await setTimeout(4000);
this.notifyCombinedAchievementsUnlocked(
totalNewGamesWithAchievements,
totalNewAchievements
);
}
} catch (err) {
achievementsLogger.error("Error on preSearchAchievements", err);
}
this.hasFinishedMergingWithRemote = true;
this._hasFinishedPreSearch = true;
}
}

View File

@@ -303,7 +303,7 @@ export const findAchievementFileInExecutableDirectory = (
"achievements.ini"
),
},
];
].filter((file) => fs.existsSync(file.filePath)) as AchievementFile[];
};
const mapFileLocationWithObjectId = (

View File

@@ -1,44 +1,65 @@
import { HydraApi } from "../hydra-api";
import type { GameShop, SteamAchievement } from "@types";
import type { GameAchievement, GameShop, SteamAchievement } from "@types";
import { UserNotLoggedInError } from "@shared";
import { logger } from "../logger";
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
import { AxiosError } from "axios";
const LOCAL_CACHE_EXPIRATION = 1000 * 60 * 30; // 30 minutes
const getModifiedSinceHeader = (
cachedAchievements: GameAchievement | undefined
): Date | undefined => {
if (!cachedAchievements) {
return undefined;
}
return cachedAchievements.updatedAt
? new Date(cachedAchievements.updatedAt)
: undefined;
};
export const getGameAchievementData = async (
objectId: string,
shop: GameShop,
useCachedData: boolean
) => {
const cachedAchievements = await gameAchievementsSublevel.get(
levelKeys.game(shop, objectId)
);
const gameKey = levelKeys.game(shop, objectId);
if (cachedAchievements && useCachedData)
const cachedAchievements = await gameAchievementsSublevel.get(gameKey);
if (cachedAchievements?.achievements && useCachedData)
return cachedAchievements.achievements;
if (
cachedAchievements &&
Date.now() < (cachedAchievements.cacheExpiresTimestamp ?? 0)
cachedAchievements?.achievements &&
Date.now() < (cachedAchievements.updatedAt ?? 0) + LOCAL_CACHE_EXPIRATION
) {
return cachedAchievements.achievements;
}
const language = await db
.get<string, string>(levelKeys.language, {
valueEncoding: "utf-8",
valueEncoding: "utf8",
})
.then((language) => language || "en");
return HydraApi.get<SteamAchievement[]>("/games/achievements", {
shop,
objectId,
language,
})
return HydraApi.get<SteamAchievement[]>(
"/games/achievements",
{
shop,
objectId,
language,
},
{
ifModifiedSince: getModifiedSinceHeader(cachedAchievements),
}
)
.then(async (achievements) => {
await gameAchievementsSublevel.put(levelKeys.game(shop, objectId), {
await gameAchievementsSublevel.put(gameKey, {
unlockedAchievements: cachedAchievements?.unlockedAchievements ?? [],
achievements,
cacheExpiresTimestamp: Date.now() + 1000 * 60 * 30, // 30 minutes
updatedAt: Date.now() + LOCAL_CACHE_EXPIRATION,
});
return achievements;
@@ -48,8 +69,14 @@ export const getGameAchievementData = async (
throw err;
}
const isNotModified = (err as AxiosError)?.response?.status === 304;
if (isNotModified) {
return cachedAchievements?.achievements ?? [];
}
logger.error("Failed to get game achievements for", objectId, err);
return [];
return cachedAchievements?.achievements ?? [];
});
};

View File

@@ -1,4 +1,5 @@
import type {
AchievementNotificationInfo,
Game,
GameShop,
UnlockedAchievement,
@@ -12,6 +13,14 @@ import { publishNewAchievementNotification } from "../notifications";
import { SubscriptionRequiredError } from "@shared";
import { achievementsLogger } from "../logger";
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
import { getGameAchievementData } from "./get-game-achievement-data";
import { AchievementWatcherManager } from "./achievement-watcher-manager";
const isRareAchievement = (points: number) => {
const rawPercentage = (50 - Math.sqrt(points)) * 2;
return rawPercentage < 10;
};
const saveAchievementsOnLocal = async (
objectId: string,
@@ -27,7 +36,7 @@ const saveAchievementsOnLocal = async (
await gameAchievementsSublevel.put(levelKey, {
achievements: gameAchievement?.achievements ?? [],
unlockedAchievements: unlockedAchievements,
cacheExpiresTimestamp: gameAchievement?.cacheExpiresTimestamp,
updatedAt: gameAchievement?.updatedAt,
});
if (!sendUpdateEvent) return;
@@ -48,12 +57,20 @@ export const mergeAchievements = async (
achievements: UnlockedAchievement[],
publishNotification: boolean
) => {
const [localGameAchievement, userPreferences] = await Promise.all([
gameAchievementsSublevel.get(levelKeys.game(game.shop, game.objectId)),
db.get<string, UserPreferences>(levelKeys.userPreferences, {
const gameKey = levelKeys.game(game.shop, game.objectId);
let localGameAchievement = await gameAchievementsSublevel.get(gameKey);
const userPreferences = await db.get<string, UserPreferences>(
levelKeys.userPreferences,
{
valueEncoding: "json",
}),
]);
}
);
if (!localGameAchievement) {
await getGameAchievementData(game.objectId, game.shop, false);
localGameAchievement = await gameAchievementsSublevel.get(gameKey);
}
const achievementsData = localGameAchievement?.achievements ?? [];
const unlockedAchievements = localGameAchievement?.unlockedAchievements ?? [];
@@ -84,9 +101,9 @@ export const mergeAchievements = async (
if (
newAchievements.length &&
publishNotification &&
userPreferences?.achievementNotificationsEnabled
userPreferences.achievementNotificationsEnabled !== false
) {
const achievementsInfo = newAchievements
const filteredAchievements = newAchievements
.toSorted((a, b) => {
return a.unlockTime - b.unlockTime;
})
@@ -98,24 +115,54 @@ export const mergeAchievements = async (
);
});
})
.filter((achievement) => Boolean(achievement))
.map((achievement) => {
.filter((achievement) => !!achievement);
const achievementsInfo: AchievementNotificationInfo[] =
filteredAchievements.map((achievement, index) => {
return {
displayName: achievement!.displayName,
iconUrl: achievement!.icon,
title: achievement.displayName,
description: achievement.description,
points: achievement.points,
isHidden: achievement.hidden,
isRare: achievement.points
? isRareAchievement(achievement.points)
: false,
isPlatinum:
index === filteredAchievements.length - 1 &&
newAchievements.length + unlockedAchievements.length ===
achievementsData.length,
iconUrl: achievement.icon,
};
});
publishNewAchievementNotification({
achievements: achievementsInfo,
unlockedAchievementCount: mergedLocalAchievements.length,
totalAchievementCount: achievementsData.length,
gameTitle: game.title,
gameIcon: game.iconUrl,
});
achievementsLogger.log(
"Publishing achievement notification",
game.objectId,
game.title
);
if (userPreferences.achievementCustomNotificationsEnabled !== false) {
WindowManager.notificationWindow?.webContents.send(
"on-achievement-unlocked",
userPreferences.achievementCustomNotificationPosition ?? "top-left",
achievementsInfo
);
} else {
publishNewAchievementNotification({
achievements: achievementsInfo,
unlockedAchievementCount: mergedLocalAchievements.length,
totalAchievementCount: achievementsData.length,
gameTitle: game.title,
gameIcon: game.iconUrl,
});
}
}
if (game.remoteId) {
const shouldSyncWithRemote =
game.remoteId &&
(newAchievements.length || AchievementWatcherManager.hasFinishedPreSearch);
if (shouldSyncWithRemote) {
await HydraApi.put<UpdatedUnlockedAchievements | undefined>(
"/profile/games/achievements",
{
@@ -156,8 +203,11 @@ export const mergeAchievements = async (
mergedLocalAchievements,
publishNotification
);
})
.finally(() => {
AchievementWatcherManager.alreadySyncedGames.set(gameKey, true);
});
} else {
} else if (newAchievements.length) {
await saveAchievementsOnLocal(
game.objectId,
game.shop,

View File

@@ -1,31 +0,0 @@
import {
findAchievementFiles,
findAchievementFileInExecutableDirectory,
} from "./find-achivement-files";
import { parseAchievementFile } from "./parse-achievement-file";
import { mergeAchievements } from "./merge-achievements";
import type { Game, UnlockedAchievement } from "@types";
export const updateLocalUnlockedAchievements = async (game: Game) => {
const gameAchievementFiles = findAchievementFiles(game);
const achievementFileInsideDirectory =
findAchievementFileInExecutableDirectory(game);
gameAchievementFiles.push(...achievementFileInsideDirectory);
const unlockedAchievements: UnlockedAchievement[] = [];
for (const achievementFile of gameAchievementFiles) {
const localAchievementFile = parseAchievementFile(
achievementFile.filePath,
achievementFile.type
);
if (localAchievementFile.length) {
unlockedAchievements.push(...localAchievementFile);
}
}
mergeAchievements(game, unlockedAchievements, false);
};

View File

@@ -7,8 +7,8 @@ export class Aria2 {
public static spawn() {
const binaryPath = app.isPackaged
? path.join(process.resourcesPath, "aria2", "aria2c")
: path.join(__dirname, "..", "..", "aria2", "aria2c");
? path.join(process.resourcesPath, "aria2c")
: path.join(__dirname, "..", "..", "binaries", "aria2c");
this.process = cp.spawn(
binaryPath,

View File

@@ -7,7 +7,7 @@ import os from "node:os";
import type { GameShop, User } from "@types";
import { backupsPath } from "@main/constants";
import { HydraApi } from "./hydra-api";
import { normalizePath } from "@main/helpers";
import { normalizePath, parseRegFile } from "@main/helpers";
import { logger } from "./logger";
import { WindowManager } from "./window-manager";
import axios from "axios";
@@ -17,6 +17,39 @@ import i18next, { t } from "i18next";
import { SystemPath } from "./system-path";
export class CloudSync {
public static getWindowsLikeUserProfilePath(winePrefixPath?: string | null) {
if (process.platform === "linux") {
if (!winePrefixPath) {
throw new Error("Wine prefix path is required");
}
const userReg = fs.readFileSync(
path.join(winePrefixPath, "user.reg"),
"utf8"
);
const entries = parseRegFile(userReg);
const volatileEnvironment = entries.find(
(entry) => entry.path === "Volatile Environment"
);
if (!volatileEnvironment) {
throw new Error("Volatile environment not found in user.reg");
}
const { values } = volatileEnvironment;
const userProfile = String(values["USERPROFILE"]);
if (userProfile) {
return normalizePath(userProfile);
} else {
throw new Error("User profile not found in user.reg");
}
}
return normalizePath(SystemPath.getPath("home"));
}
public static getBackupLabel(automatic: boolean) {
const language = i18next.language;
@@ -102,9 +135,12 @@ export class CloudSync {
shop,
objectId,
hostname: os.hostname(),
homeDir: normalizePath(SystemPath.getPath("home")),
winePrefixPath: game?.winePrefixPath
? fs.realpathSync(game.winePrefixPath)
: null,
homeDir: this.getWindowsLikeUserProfilePath(game?.winePrefixPath ?? null),
downloadOptionTitle,
platform: os.platform(),
platform: process.platform,
label,
});

View File

@@ -1 +1,3 @@
export * from "./download-manager";
export * from "./real-debrid";
export * from "./torbox";

View File

@@ -31,6 +31,8 @@ export interface ProcessPayload {
exe: string | null;
pid: number;
name: string;
environ: Record<string, string> | null;
cwd: string | null;
}
export interface PauseSeedingPayload {

View File

@@ -11,10 +11,12 @@ import { getUserData } from "./user/get-user-data";
import { db } from "@main/level";
import { levelKeys } from "@main/level/sublevels";
import type { Auth, User } from "@types";
import { WSClient } from "./ws/ws-client";
interface HydraApiOptions {
needsAuth?: boolean;
needsSubscription?: boolean;
ifModifiedSince?: Date;
}
interface HydraApiUserAuth {
@@ -41,7 +43,7 @@ export class HydraApi {
subscription: null,
};
private static isLoggedIn() {
public static isLoggedIn() {
return this.userAuth.authToken !== "";
}
@@ -101,6 +103,8 @@ export class HydraApi {
WindowManager.mainWindow.webContents.send("on-signin");
await clearGamesRemoteIds();
uploadGamesBatch();
WSClient.close();
WSClient.connect();
}
}
@@ -334,8 +338,13 @@ export class HydraApi {
) {
await this.validateOptions(options);
const headers = {
...this.getAxiosConfig().headers,
"Hydra-If-Modified-Since": options?.ifModifiedSince?.toUTCString(),
};
return this.instance
.get<T>(url, { params, ...this.getAxiosConfig() })
.get<T>(url, { params, ...this.getAxiosConfig(), headers })
.then((response) => response.data)
.catch(this.handleUnauthorizedError);
}

View File

@@ -12,3 +12,8 @@ export * from "./7zip";
export * from "./game-files-manager";
export * from "./common-redist-manager";
export * from "./aria2";
export * from "./ws";
export * from "./system-path";
export * from "./library-sync";
export * from "./wine";
export * from "./lock";

View File

@@ -1,15 +1,20 @@
import { ShopAssets } from "@types";
import { HydraApi } from "../hydra-api";
import { steamGamesWorker } from "@main/workers";
import { steamUrlBuilder } from "@shared";
import { gamesSublevel, levelKeys } from "@main/level";
import { gamesShopAssetsSublevel, gamesSublevel, levelKeys } from "@main/level";
type ProfileGame = {
id: string;
lastTimePlayed: Date | null;
playTimeInMilliseconds: number;
isFavorite?: boolean;
} & ShopAssets;
export const mergeWithRemoteGames = async () => {
return HydraApi.get("/profile/games")
return HydraApi.get<ProfileGame[]>("/profile/games")
.then(async (response) => {
for (const game of response) {
const localGame = await gamesSublevel.get(
levelKeys.game(game.shop, game.objectId)
);
const gameKey = levelKeys.game(game.shop, game.objectId);
const localGame = await gamesSublevel.get(gameKey);
if (localGame) {
const updatedLastTimePlayed =
@@ -24,32 +29,38 @@ export const mergeWithRemoteGames = async () => {
? game.playTimeInMilliseconds
: localGame.playTimeInMilliseconds;
await gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
await gamesSublevel.put(gameKey, {
...localGame,
remoteId: game.id,
lastTimePlayed: updatedLastTimePlayed,
playTimeInMilliseconds: updatedPlayTime,
favorite: game.isFavorite ?? localGame.favorite,
});
} else {
const steamGame = await steamGamesWorker.run(Number(game.objectId), {
name: "getById",
});
const iconUrl = steamGame?.clientIcon
? steamUrlBuilder.icon(game.objectId, steamGame.clientIcon)
: null;
await gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
await gamesSublevel.put(gameKey, {
objectId: game.objectId,
title: steamGame?.name,
title: game.title,
remoteId: game.id,
shop: game.shop,
iconUrl,
iconUrl: game.iconUrl,
lastTimePlayed: game.lastTimePlayed,
playTimeInMilliseconds: game.playTimeInMilliseconds,
isDeleted: false,
favorite: game.isFavorite ?? false,
});
}
await gamesShopAssetsSublevel.put(gameKey, {
shop: game.shop,
objectId: game.objectId,
title: game.title,
coverImageUrl: game.coverImageUrl,
libraryHeroImageUrl: game.libraryHeroImageUrl,
libraryImageUrl: game.libraryImageUrl,
logoImageUrl: game.logoImageUrl,
iconUrl: game.iconUrl,
logoPosition: game.logoPosition,
});
}
})
.catch(() => {});

View File

@@ -15,7 +15,7 @@ export const uploadGamesBatch = async () => {
);
});
const gamesChunks = chunk(games, 50);
const gamesChunks = chunk(games, 30);
for (const chunk of gamesChunks) {
await HydraApi.post(
@@ -26,6 +26,7 @@ export const uploadGamesBatch = async () => {
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds),
shop: game.shop,
lastTimePlayed: game.lastTimePlayed,
isFavorite: game.favorite,
};
})
).catch(() => {});

39
src/main/services/lock.ts Normal file
View File

@@ -0,0 +1,39 @@
import path from "node:path";
import fs from "node:fs";
import { SystemPath } from "./system-path";
import { logger } from "./logger";
export class Lock {
private static lockFilePath = path.join(
SystemPath.getPath("temp"),
"hydra-launcher.lock"
);
public static async acquireLock() {
return new Promise<void>((resolve, reject) => {
fs.writeFile(this.lockFilePath, "", (err) => {
if (err) {
logger.error("Error acquiring the lock", err);
reject(err);
}
logger.info("Acquired the lock");
resolve();
});
});
}
public static async releaseLock() {
return new Promise<void>((resolve, reject) => {
fs.unlink(this.lockFilePath, (err) => {
if (err) {
logger.error("Error releasing the lock", err);
reject(err);
}
logger.info("Released the lock");
resolve();
});
});
}
}

View File

@@ -1,70 +1,102 @@
import type { GameShop, LudusaviBackup, LudusaviConfig } from "@types";
import Piscina from "piscina";
import { app } from "electron";
import fs from "node:fs";
import path from "node:path";
import YAML from "yaml";
import ludusaviWorkerPath from "../workers/ludusavi.worker?modulePath";
import { LUDUSAVI_MANIFEST_URL } from "@main/constants";
import cp from "node:child_process";
import { SystemPath } from "./system-path";
export class Ludusavi {
private static ludusaviPath = path.join(
SystemPath.getPath("appData"),
private static ludusaviResourcesPath = app.isPackaged
? path.join(process.resourcesPath, "ludusavi")
: path.join(__dirname, "..", "..", "ludusavi");
private static configPath = path.join(
SystemPath.getPath("userData"),
"ludusavi"
);
private static ludusaviConfigPath = path.join(
this.ludusaviPath,
"config.yaml"
);
private static binaryPath = app.isPackaged
? path.join(process.resourcesPath, "ludusavi", "ludusavi")
: path.join(__dirname, "..", "..", "ludusavi", "ludusavi");
private static binaryName =
process.platform === "win32" ? "ludusavi.exe" : "ludusavi";
private static worker = new Piscina({
filename: ludusaviWorkerPath,
workerData: {
binaryPath: this.binaryPath,
},
maxThreads: 1,
});
static async getConfig() {
if (!fs.existsSync(this.ludusaviConfigPath)) {
await this.worker.run(undefined, { name: "generateConfig" });
}
private static binaryPath = path.join(this.configPath, this.binaryName);
public static async getConfig() {
const config = YAML.parse(
fs.readFileSync(this.ludusaviConfigPath, "utf-8")
fs.readFileSync(path.join(this.configPath, "config.yaml"), "utf-8")
) as LudusaviConfig;
return config;
}
static async backupGame(
_shop: GameShop,
objectId: string,
backupPath: string,
winePrefix?: string | null
): Promise<LudusaviBackup> {
return this.worker.run(
{ title: objectId, backupPath, winePrefix },
{ name: "backupGame" }
);
public static async copyConfigFileToUserData() {
if (!fs.existsSync(this.configPath)) {
fs.mkdirSync(this.configPath, { recursive: true });
fs.cpSync(
path.join(this.ludusaviResourcesPath, "config.yaml"),
path.join(this.configPath, "config.yaml")
);
}
}
static async getBackupPreview(
public static async copyBinaryToUserData() {
if (!fs.existsSync(this.binaryPath)) {
fs.cpSync(
path.join(this.ludusaviResourcesPath, this.binaryName),
this.binaryPath
);
}
}
public static async backupGame(
_shop: GameShop,
objectId: string,
backupPath?: string | null,
winePrefix?: string | null,
preview?: boolean
): Promise<LudusaviBackup> {
return new Promise((resolve, reject) => {
const args = [
"--config",
this.configPath,
"backup",
objectId,
"--api",
"--force",
];
if (preview) args.push("--preview");
if (backupPath) args.push("--path", backupPath);
if (winePrefix) args.push("--wine-prefix", winePrefix);
cp.execFile(
this.binaryPath,
args,
(err: cp.ExecFileException | null, stdout: string) => {
if (err) {
return reject(err);
}
return resolve(JSON.parse(stdout) as LudusaviBackup);
}
);
});
}
public static async getBackupPreview(
_shop: GameShop,
objectId: string,
winePrefix?: string | null
): Promise<LudusaviBackup | null> {
const config = await this.getConfig();
const backupData = await this.worker.run(
{ title: objectId, winePrefix, preview: true },
{ name: "backupGame" }
const backupData = await this.backupGame(
_shop,
objectId,
null,
winePrefix,
true
);
const customGame = config.customGames.find(
@@ -77,19 +109,6 @@ export class Ludusavi {
};
}
static async restoreBackup(backupPath: string) {
return this.worker.run(backupPath, { name: "restoreBackup" });
}
static async addManifestToLudusaviConfig() {
const config = await this.getConfig();
config.manifest.enable = false;
config.manifest.secondary = [{ url: LUDUSAVI_MANIFEST_URL, enable: true }];
fs.writeFileSync(this.ludusaviConfigPath, YAML.stringify(config));
}
static async addCustomGame(title: string, savePath: string | null) {
const config = await this.getConfig();
const filteredGames = config.customGames.filter(
@@ -105,6 +124,10 @@ export class Ludusavi {
}
config.customGames = filteredGames;
fs.writeFileSync(this.ludusaviConfigPath, YAML.stringify(config));
fs.writeFileSync(
path.join(this.configPath, "config.yaml"),
YAML.stringify(config)
);
}
}

View File

@@ -3,7 +3,6 @@ import { DownloadManager } from "./download";
import { watchProcesses } from "./process-watcher";
import { AchievementWatcherManager } from "./achievements/achievement-watcher-manager";
import { UpdateManager } from "./update-manager";
import { watchFriendRequests } from "@main/events/profile/sync-friend-requests";
import { MAIN_LOOP_INTERVAL } from "@main/constants";
export const startMainLoop = async () => {
@@ -11,7 +10,6 @@ export const startMainLoop = async () => {
while (true) {
await Promise.allSettled([
watchProcesses(),
watchFriendRequests(),
DownloadManager.watchDownloads(),
AchievementWatcherManager.watchAchievements(),
DownloadManager.getSeedStatus(),

View File

@@ -10,7 +10,7 @@ import icon from "@resources/icon.png?asset";
import { NotificationOptions, toXmlString } from "./xml";
import { logger } from "../logger";
import { WindowManager } from "../window-manager";
import type { Game, UserPreferences } from "@types";
import type { Game, GameStats, UserPreferences, UserProfile } from "@types";
import { db, levelKeys } from "@main/level";
import { restartAndInstallUpdate } from "@main/events/autoupdater/restart-and-install-update";
import { SystemPath } from "../system-path";
@@ -81,7 +81,9 @@ export const publishNotificationUpdateReadyToInstall = async (
.show();
};
export const publishNewFriendRequestNotification = async () => {
export const publishNewFriendRequestNotification = async (
user: UserProfile
) => {
const userPreferences = await db.get<string, UserPreferences | null>(
levelKeys.userPreferences,
{
@@ -97,8 +99,27 @@ export const publishNewFriendRequestNotification = async () => {
}),
body: t("new_friend_request_description", {
ns: "notifications",
displayName: user.displayName,
}),
icon: trayIcon,
icon: user?.profileImageUrl
? await downloadImage(user.profileImageUrl)
: trayIcon,
}).show();
};
export const publishFriendStartedPlayingGameNotification = async (
friend: UserProfile,
game: GameStats
) => {
new Notification({
title: t("friend_started_playing_game", {
ns: "notifications",
displayName: friend.displayName,
}),
body: game.assets?.title,
icon: friend?.profileImageUrl
? await downloadImage(friend.profileImageUrl)
: trayIcon,
}).show();
};
@@ -141,7 +162,7 @@ export const publishExtractionCompleteNotification = async (game: Game) => {
};
export const publishNewAchievementNotification = async (info: {
achievements: { displayName: string; iconUrl: string }[];
achievements: { title: string; iconUrl: string }[];
unlockedAchievementCount: number;
totalAchievementCount: number;
gameTitle: string;
@@ -155,12 +176,12 @@ export const publishNewAchievementNotification = async (info: {
gameTitle: info.gameTitle,
achievementCount: info.achievements.length,
}),
body: info.achievements.map((a) => a.displayName).join(", "),
body: info.achievements.map((a) => a.title).join(", "),
icon: (await downloadImage(info.gameIcon)) ?? icon,
}
: {
title: t("achievement_unlocked", { ns: "achievement" }),
body: info.achievements[0].displayName,
body: info.achievements[0].title,
icon: (await downloadImage(info.achievements[0].iconUrl)) ?? icon,
};

View File

@@ -3,15 +3,12 @@ import { createGame, updateGamePlaytime } from "./library-sync";
import type { Game, GameRunning } from "@types";
import { PythonRPC } from "./python-rpc";
import axios from "axios";
import { exec } from "child_process";
import { ProcessPayload } from "./download/types";
import { gamesSublevel, levelKeys } from "@main/level";
import { CloudSync } from "./cloud-sync";
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 ""}'`,
findWineExecutables: `lsof -c wine 2>/dev/null | grep '\\.exe$' | awk '{for(i=9;i<=NF;i++) printf "%s ", $i; print ""}'`,
};
import { logger } from "./logger";
import path from "path";
import { AchievementWatcherManager } from "./achievements/achievement-watcher-manager";
export const gamesPlaytime = new Map<
string,
@@ -28,11 +25,10 @@ interface GameExecutables {
[key: string]: ExecutableInfo[];
}
const TICKS_TO_UPDATE_API = 120;
const TICKS_TO_UPDATE_API = 80;
let currentTick = 1;
const isWindowsPlatform = process.platform === "win32";
const isLinuxPlatform = process.platform === "linux";
const platform = process.platform;
const getGameExecutables = async () => {
const gameExecutables = (
@@ -49,18 +45,20 @@ const getGameExecutables = async () => {
Object.keys(gameExecutables).forEach((key) => {
gameExecutables[key] = gameExecutables[key]
.filter((executable) => {
if (isWindowsPlatform) {
if (platform === "win32") {
return executable.os === "win32";
} else if (isLinuxPlatform) {
} else if (platform === "linux") {
return executable.os === "linux" || executable.os === "win32";
}
return false;
})
.map((executable) => {
return {
name: isWindowsPlatform
? executable.name.replace(/\//g, "\\")
: executable.name,
name:
platform === "win32"
? executable.name.replace(/\//g, "\\")
: executable.name,
os: executable.os,
exe: executable.name.slice(executable.name.lastIndexOf("/") + 1),
};
@@ -72,8 +70,9 @@ const getGameExecutables = async () => {
const gameExecutables = await getGameExecutables();
const findGamePathByProcess = (
const findGamePathByProcess = async (
processMap: Map<string, Set<string>>,
winePrefixMap: Map<string, string>,
gameId: string
) => {
const executables = gameExecutables[gameId];
@@ -82,32 +81,26 @@ const findGamePathByProcess = (
const pathSet = processMap.get(executable.exe);
if (pathSet) {
pathSet.forEach(async (path) => {
for (const path of pathSet) {
if (path.toLowerCase().endsWith(executable.name)) {
const gameKey = levelKeys.game("steam", gameId);
const game = await gamesSublevel.get(gameKey);
if (game) {
gamesSublevel.put(gameKey, {
const updatedGame: Game = {
...game,
executablePath: path,
});
}
};
if (isLinuxPlatform) {
exec(commands.findWineDir, (err, out) => {
if (err) return;
if (process.platform === "linux" && winePrefixMap.has(path)) {
updatedGame.winePrefixPath = winePrefixMap.get(path)!;
}
if (game) {
gamesSublevel.put(gameKey, {
...game,
winePrefixPath: out.trim().replace("/drive_c/windows", ""),
});
}
});
await gamesSublevel.put(gameKey, updatedGame);
logger.info("Set game path", gameKey, path);
}
}
});
}
}
}
};
@@ -117,50 +110,29 @@ const getSystemProcessMap = async () => {
(await PythonRPC.rpc.get<ProcessPayload[] | null>("/process-list")).data ||
[];
const map = new Map<string, Set<string>>();
const processMap = new Map<string, Set<string>>();
const winePrefixMap = new Map<string, string>();
processes.forEach((process) => {
const key = process.name?.toLowerCase();
const value = process.exe;
const value =
platform === "win32"
? process.exe
: path.join(process.cwd ?? "", process.name ?? "");
if (!key || !value) return;
const currentSet = map.get(key) ?? new Set();
map.set(key, currentSet.add(value));
const STEAM_COMPAT_DATA_PATH = process.environ?.STEAM_COMPAT_DATA_PATH;
if (STEAM_COMPAT_DATA_PATH) {
winePrefixMap.set(value, STEAM_COMPAT_DATA_PATH);
}
const currentSet = processMap.get(key) ?? new Set();
processMap.set(key, currentSet.add(value));
});
if (isLinuxPlatform) {
await new Promise((res) => {
exec(commands.findWineExecutables, (err, out) => {
if (err) {
res(null);
return;
}
const pathSet = new Set(
out
.trim()
.split("\n")
.map((path) => path.trim())
);
pathSet.forEach((path) => {
if (path.startsWith("/usr")) return;
const key = path.slice(path.lastIndexOf("/") + 1).toLowerCase();
if (!key || !path) return;
const currentSet = map.get(key) ?? new Set();
map.set(key, currentSet.add(path));
});
res(null);
});
});
}
return map;
return { processMap, winePrefixMap };
};
export const watchProcesses = async () => {
@@ -173,19 +145,20 @@ export const watchProcesses = async () => {
if (!games.length) return;
const processMap = await getSystemProcessMap();
const { processMap, winePrefixMap } = await getSystemProcessMap();
for (const game of games) {
const executablePath = game.executablePath;
if (!executablePath) {
if (gameExecutables[game.objectId]) {
findGamePathByProcess(processMap, game.objectId);
await findGamePathByProcess(processMap, winePrefixMap, game.objectId);
}
continue;
}
const executable = executablePath
.slice(executablePath.lastIndexOf(isWindowsPlatform ? "\\" : "/") + 1)
.slice(executablePath.lastIndexOf(platform === "win32" ? "\\" : "/") + 1)
.toLowerCase();
const hasProcess = processMap.get(executable)?.has(executablePath);
@@ -218,6 +191,11 @@ export const watchProcesses = async () => {
function onOpenGame(game: Game) {
const now = performance.now();
AchievementWatcherManager.firstSyncWithRemoteIfNeeded(
game.shop,
game.objectId
);
gamesPlaytime.set(levelKeys.game(game.shop, game.objectId), {
lastTick: now,
firstTick: now,
@@ -225,7 +203,18 @@ function onOpenGame(game: Game) {
});
if (game.remoteId) {
updateGamePlaytime(game, 0, new Date()).catch(() => {});
updateGamePlaytime(
game,
game.unsyncedDeltaPlayTimeInMilliseconds ?? 0,
new Date()
)
.then(() => {
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
...game,
unsyncedDeltaPlayTimeInMilliseconds: 0,
});
})
.catch(() => {});
if (game.automaticCloudSync) {
CloudSync.uploadSaveGame(
@@ -250,13 +239,7 @@ function onTickGame(game: Game) {
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
...game,
playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
lastTimePlayed: new Date(),
});
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
...game,
playTimeInMilliseconds: game.playTimeInMilliseconds + delta,
playTimeInMilliseconds: (game.playTimeInMilliseconds ?? 0) + delta,
lastTimePlayed: new Date(),
});
@@ -266,22 +249,34 @@ function onTickGame(game: Game) {
});
if (currentTick % TICKS_TO_UPDATE_API === 0) {
const deltaToSync =
now -
gamePlaytime.lastSyncTick +
(game.unsyncedDeltaPlayTimeInMilliseconds ?? 0);
const gamePromise = game.remoteId
? updateGamePlaytime(
game,
now - gamePlaytime.lastSyncTick,
game.lastTimePlayed!
)
? updateGamePlaytime(game, deltaToSync, game.lastTimePlayed!)
: createGame(game);
gamePromise
.then(() => {
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
...game,
unsyncedDeltaPlayTimeInMilliseconds: 0,
});
})
.catch(() => {
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
...game,
unsyncedDeltaPlayTimeInMilliseconds: deltaToSync,
});
})
.finally(() => {
gamesPlaytime.set(levelKeys.game(game.shop, game.objectId), {
...gamePlaytime,
lastSyncTick: now,
});
})
.catch(() => {});
});
}
}
@@ -292,12 +287,6 @@ const onCloseGame = (game: Game) => {
gamesPlaytime.delete(levelKeys.game(game.shop, game.objectId));
if (game.remoteId) {
updateGamePlaytime(
game,
performance.now() - gamePlaytime.lastSyncTick,
game.lastTimePlayed!
).catch(() => {});
if (game.automaticCloudSync) {
CloudSync.uploadSaveGame(
game.objectId,
@@ -306,7 +295,38 @@ const onCloseGame = (game: Game) => {
CloudSync.getBackupLabel(true)
);
}
const deltaToSync =
performance.now() -
gamePlaytime.lastSyncTick +
(game.unsyncedDeltaPlayTimeInMilliseconds ?? 0);
return updateGamePlaytime(game, deltaToSync, game.lastTimePlayed!)
.then(() => {
return gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
...game,
unsyncedDeltaPlayTimeInMilliseconds: 0,
});
})
.catch(() => {
return gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
...game,
unsyncedDeltaPlayTimeInMilliseconds: deltaToSync,
});
});
} else {
createGame(game).catch(() => {});
return createGame(game).catch(() => {});
}
};
export const clearGamesPlaytime = async () => {
for (const game of gamesPlaytime.keys()) {
const gameData = await gamesSublevel.get(game);
if (gameData) {
await onCloseGame(gameData);
}
}
gamesPlaytime.clear();
};

View File

@@ -8,6 +8,7 @@ import crypto from "node:crypto";
import { pythonRpcLogger } from "./logger";
import { Readable } from "node:stream";
import { app, dialog } from "electron";
import { db, levelKeys } from "@main/level";
interface GamePayload {
game_id: string;
@@ -21,26 +22,15 @@ 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";
private static readonly RPC_PASSWORD = crypto.randomBytes(32).toString("hex");
private static pythonProcess: cp.ChildProcess | null = null;
public static readonly rpc = axios.create({
baseURL: `http://localhost:${this.RPC_PORT}`,
headers: {
"x-hydra-rpc-password": this.RPC_PASSWORD,
},
});
private static pythonProcess: cp.ChildProcess | null = null;
private static logStderr(readable: Readable | null) {
if (!readable) return;
@@ -48,30 +38,34 @@ export class PythonRPC {
readable.on("data", pythonRpcLogger.log);
}
public static spawn(
private static async getRPCPassword() {
const existingPassword = await db.get(levelKeys.rpcPassword, {
valueEncoding: "utf8",
});
if (existingPassword) return existingPassword;
const newPassword = crypto.randomBytes(32).toString("hex");
await db.put(levelKeys.rpcPassword, newPassword, {
valueEncoding: "utf8",
});
return newPassword;
}
public static async spawn(
initialDownload?: GamePayload,
initialSeeding?: GamePayload[]
) {
const rpcPassword = await this.getRPCPassword();
const commonArgs = [
this.BITTORRENT_PORT,
this.RPC_PORT,
this.RPC_PASSWORD,
rpcPassword,
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) {
@@ -116,6 +110,8 @@ export class PythonRPC {
this.pythonProcess = childProcess;
}
this.rpc.defaults.headers.common["x-hydra-rpc-password"] = rpcPassword;
}
public static kill() {

View File

@@ -1,8 +1,14 @@
import axios from "axios";
import path from "node:path";
import fs from "node:fs";
import { crc32 } from "crc";
import WinReg from "winreg";
import { parseBuffer, writeBuffer } from "steam-shortcut-editor";
import type { SteamAppDetails } from "@types";
import type { SteamAppDetails, SteamShortcut } from "@types";
import { logger } from "./logger";
import { SystemPath } from "./system-path";
export interface SteamAppDetailsResponse {
[key: string]: {
@@ -11,6 +17,36 @@ export interface SteamAppDetailsResponse {
};
}
export const getSteamLocation = async () => {
if (process.platform === "linux") {
return path.join(SystemPath.getPath("home"), ".local", "share", "Steam");
}
if (process.platform === "darwin") {
return path.join(
SystemPath.getPath("home"),
"Library",
"Application Support",
"Steam"
);
}
const regKey = new WinReg({
hive: WinReg.HKCU,
key: "\\Software\\Valve\\Steam",
});
return new Promise<string>((resolve, reject) => {
regKey.get("SteamPath", (err, value) => {
if (err) {
reject(err);
}
resolve(value.value);
});
});
};
export const getSteamAppDetails = async (
objectId: string,
language: string
@@ -40,3 +76,86 @@ export const getSteamAppDetails = async (
return null;
});
};
export const getSteamUsersIds = async () => {
const userDataPath = await getSteamLocation();
const userIds = fs.readdirSync(path.join(userDataPath, "userdata"), {
withFileTypes: true,
});
return userIds
.filter((dir) => dir.isDirectory())
.map((dir) => Number(dir.name));
};
export const getSteamShortcuts = async (steamUserId: number) => {
const shortcutsPath = path.join(
await getSteamLocation(),
"userdata",
steamUserId.toString(),
"config",
"shortcuts.vdf"
);
if (!fs.existsSync(shortcutsPath)) {
return [];
}
const shortcuts = parseBuffer(fs.readFileSync(shortcutsPath));
return shortcuts.shortcuts as SteamShortcut[];
};
export const generateSteamShortcutAppId = (
exePath: string,
gameName: string
) => {
const input = exePath + gameName;
const crcValue = crc32(input) >>> 0;
const steamAppId = (crcValue | 0x80000000) >>> 0;
return steamAppId;
};
export const composeSteamShortcut = (
title: string,
executablePath: string,
iconPath: string | null
): SteamShortcut => {
return {
appid: generateSteamShortcutAppId(executablePath, title),
appname: title,
Exe: `"${executablePath}"`,
StartDir: `"${path.dirname(executablePath)}"`,
icon: iconPath ?? "",
ShortcutPath: "",
LaunchOptions: "",
IsHidden: false,
AllowDesktopConfig: true,
AllowOverlay: true,
OpenVR: false,
Devkit: false,
DevkitGameID: "",
DevkitOverrideAppID: false,
LastPlayTime: 0,
FlatpakAppID: "",
};
};
export const writeSteamShortcuts = async (
steamUserId: number,
shortcuts: SteamShortcut[]
) => {
const buffer = writeBuffer({ shortcuts });
return fs.promises.writeFile(
path.join(
await getSteamLocation(),
"userdata",
steamUserId.toString(),
"config",
"shortcuts.vdf"
),
buffer
);
};

View File

@@ -38,7 +38,7 @@ export class SystemPath {
try {
return app.getPath(pathName);
} catch (error) {
logger.error(`Error getting path: ${error}`);
console.error(`Error getting path: ${error}`);
return "";
}
}

View File

@@ -6,6 +6,7 @@ import {
Tray,
app,
nativeImage,
screen,
shell,
} from "electron";
import { is } from "@electron-toolkit/utils";
@@ -17,12 +18,17 @@ import { HydraApi } from "./hydra-api";
import UserAgent from "user-agents";
import { db, gamesSublevel, levelKeys } from "@main/level";
import { orderBy, slice } from "lodash-es";
import type { ScreenState, UserPreferences } from "@types";
import { AuthPage } from "@shared";
import type {
AchievementCustomNotificationPosition,
ScreenState,
UserPreferences,
} from "@types";
import { AuthPage, generateAchievementCustomNotificationTest } from "@shared";
import { isStaging } from "@main/constants";
export class WindowManager {
public static mainWindow: Electron.BrowserWindow | null = null;
public static notificationWindow: Electron.BrowserWindow | null = null;
private static readonly editorWindows: Map<string, BrowserWindow> = new Map();
@@ -259,6 +265,153 @@ export class WindowManager {
}
}
private static loadNotificationWindowURL() {
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
this.notificationWindow?.loadURL(
`${process.env["ELECTRON_RENDERER_URL"]}#/achievement-notification`
);
} else {
this.notificationWindow?.loadFile(
path.join(__dirname, "../renderer/index.html"),
{
hash: "achievement-notification",
}
);
}
}
private static readonly NOTIFICATION_WINDOW_WIDTH = 360;
private static readonly NOTIFICATION_WINDOW_HEIGHT = 140;
private static async getNotificationWindowPosition(
position: AchievementCustomNotificationPosition | undefined
) {
const display = screen.getPrimaryDisplay();
const { width, height } = display.workAreaSize;
if (position === "bottom-left") {
return {
x: 0,
y: height - this.NOTIFICATION_WINDOW_HEIGHT,
};
}
if (position === "bottom-center") {
return {
x: (width - this.NOTIFICATION_WINDOW_WIDTH) / 2,
y: height - this.NOTIFICATION_WINDOW_HEIGHT,
};
}
if (position === "bottom-right") {
return {
x: width - this.NOTIFICATION_WINDOW_WIDTH,
y: height - this.NOTIFICATION_WINDOW_HEIGHT,
};
}
if (position === "top-center") {
return {
x: (width - this.NOTIFICATION_WINDOW_WIDTH) / 2,
y: 0,
};
}
if (position === "top-right") {
return {
x: width - this.NOTIFICATION_WINDOW_WIDTH,
y: 0,
};
}
return {
x: 0,
y: 0,
};
}
public static async createNotificationWindow() {
if (this.notificationWindow) return;
const userPreferences = await db.get<string, UserPreferences | undefined>(
levelKeys.userPreferences,
{
valueEncoding: "json",
}
);
if (
userPreferences?.achievementNotificationsEnabled === false ||
userPreferences?.achievementCustomNotificationsEnabled === false
) {
return;
}
const { x, y } = await this.getNotificationWindowPosition(
userPreferences?.achievementCustomNotificationPosition
);
this.notificationWindow = new BrowserWindow({
transparent: true,
maximizable: false,
autoHideMenuBar: true,
minimizable: false,
backgroundColor: "#00000000",
focusable: false,
skipTaskbar: true,
frame: false,
width: this.NOTIFICATION_WINDOW_WIDTH,
height: this.NOTIFICATION_WINDOW_HEIGHT,
x,
y,
webPreferences: {
preload: path.join(__dirname, "../preload/index.mjs"),
sandbox: false,
},
});
this.notificationWindow.setIgnoreMouseEvents(true);
this.notificationWindow.setAlwaysOnTop(true, "screen-saver", 1);
this.loadNotificationWindowURL();
if (!app.isPackaged || isStaging) {
this.notificationWindow.webContents.openDevTools();
}
}
public static async showAchievementTestNotification() {
const userPreferences = await db.get<string, UserPreferences>(
levelKeys.userPreferences,
{
valueEncoding: "json",
}
);
const language = userPreferences.language ?? "en";
this.notificationWindow?.webContents.send(
"on-achievement-unlocked",
userPreferences.achievementCustomNotificationPosition ?? "top-left",
[
generateAchievementCustomNotificationTest(t, language),
generateAchievementCustomNotificationTest(t, language, {
isRare: true,
isHidden: true,
}),
generateAchievementCustomNotificationTest(t, language, {
isPlatinum: true,
}),
]
);
}
public static async closeNotificationWindow() {
if (this.notificationWindow) {
this.notificationWindow.close();
this.notificationWindow = null;
}
}
public static openEditorWindow(themeId: string) {
if (this.mainWindow) {
const existingWindow = this.editorWindows.get(themeId);
@@ -271,13 +424,13 @@ export class WindowManager {
}
const editorWindow = new BrowserWindow({
width: 600,
width: 720,
height: 720,
minWidth: 600,
minHeight: 540,
backgroundColor: "#1c1c1c",
titleBarStyle: process.platform === "linux" ? "default" : "hidden",
...(process.platform === "linux" ? { icon } : {}),
icon,
trafficLightPosition: { x: 16, y: 16 },
titleBarOverlay: {
symbolColor: "#DADBE1",
@@ -308,14 +461,13 @@ export class WindowManager {
editorWindow.once("ready-to-show", () => {
editorWindow.show();
this.mainWindow?.webContents.openDevTools();
if (isStaging) {
if (!app.isPackaged || isStaging) {
editorWindow.webContents.openDevTools();
}
});
editorWindow.webContents.on("before-input-event", (event, input) => {
editorWindow.webContents.on("before-input-event", (_event, input) => {
if (input.key === "F12") {
event.preventDefault();
this.mainWindow?.webContents.toggleDevTools();
}
});

30
src/main/services/wine.ts Normal file
View File

@@ -0,0 +1,30 @@
import fs from "node:fs";
import path from "node:path";
export class Wine {
public static validatePrefix(winePrefixPath: string) {
const requiredFiles = [
{ name: "system.reg", type: "file" },
{ name: "user.reg", type: "file" },
{ name: "userdef.reg", type: "file" },
{ name: "dosdevices", type: "dir" },
{ name: "drive_c", type: "dir" },
];
for (const file of requiredFiles) {
const filePath = path.join(winePrefixPath, file.name);
if (file.type === "file" && !fs.existsSync(filePath)) {
return false;
}
if (file.type === "dir") {
if (!fs.existsSync(filePath) || !fs.lstatSync(filePath).isDirectory()) {
return false;
}
}
}
return true;
}
}

View File

@@ -0,0 +1,27 @@
import type { FriendGameSession } from "@main/generated/envelope";
import { db, levelKeys } from "@main/level";
import { HydraApi } from "@main/services/hydra-api";
import { publishFriendStartedPlayingGameNotification } from "@main/services/notifications";
import type { GameStats, UserPreferences, UserProfile } from "@types";
export const friendGameSessionEvent = async (payload: FriendGameSession) => {
const userPreferences = await db.get<string, UserPreferences | null>(
levelKeys.userPreferences,
{
valueEncoding: "json",
}
);
if (userPreferences?.friendStartGameNotificationsEnabled === false) return;
const [friend, gameStats] = await Promise.all([
HydraApi.get<UserProfile>(`/users/${payload.friendId}`),
HydraApi.get<GameStats>(
`/games/stats?objectId=${payload.objectId}&shop=steam`
),
]).catch(() => [null, null]);
if (friend && gameStats) {
publishFriendStartedPlayingGameNotification(friend, gameStats);
}
};

View File

@@ -0,0 +1,16 @@
import type { FriendRequest } from "@main/generated/envelope";
import { HydraApi } from "@main/services/hydra-api";
import { publishNewFriendRequestNotification } from "@main/services/notifications";
import { WindowManager } from "@main/services/window-manager";
export const friendRequestEvent = async (payload: FriendRequest) => {
WindowManager.mainWindow?.webContents.send("on-sync-friend-requests", {
friendRequestCount: payload.friendRequestCount,
});
const user = await HydraApi.get(`/users/${payload.senderId}`);
if (user) {
publishNewFriendRequestNotification(user);
}
};

View File

@@ -0,0 +1 @@
export * from "./ws-client";

View File

@@ -0,0 +1,123 @@
import { WebSocket } from "ws";
import { HydraApi } from "../hydra-api";
import { Envelope } from "@main/generated/envelope";
import { logger } from "../logger";
import { friendRequestEvent } from "./events/friend-request";
import { friendGameSessionEvent } from "./events/friend-game-session";
export class WSClient {
private static ws: WebSocket | null = null;
private static reconnectInterval = 1_000;
private static readonly maxReconnectInterval = 30_000;
private static shouldReconnect = true;
private static reconnecting = false;
private static heartbeatInterval: NodeJS.Timeout | null = null;
static async connect() {
this.shouldReconnect = true;
try {
const { token } = await HydraApi.post<{ token: string }>("/auth/ws");
this.ws = new WebSocket(import.meta.env.MAIN_VITE_WS_URL, {
headers: {
Authorization: `Bearer ${token}`,
},
});
this.ws.on("open", () => {
logger.info("WS connected");
this.reconnectInterval = 1000;
this.reconnecting = false;
this.startHeartbeat();
});
this.ws.on("message", (message) => {
if (message.toString() === "PONG") {
return;
}
const envelope = Envelope.fromBinary(
new Uint8Array(Buffer.from(message.toString()))
);
logger.info("Received WS envelope:", envelope);
if (envelope.payload.oneofKind === "friendRequest") {
friendRequestEvent(envelope.payload.friendRequest);
}
if (envelope.payload.oneofKind === "friendGameSession") {
friendGameSessionEvent(envelope.payload.friendGameSession);
}
});
this.ws.on("close", () => this.handleDisconnect("close"));
this.ws.on("error", (err) => {
logger.error("WS error:", err);
this.handleDisconnect("error");
});
} catch (err) {
logger.error("Failed to connect WS:", err);
this.handleDisconnect("auth-failed");
}
}
private static handleDisconnect(reason: string) {
logger.warn(`WS disconnected due to ${reason}`);
if (this.shouldReconnect) {
this.cleanupSocket();
this.tryReconnect();
}
}
private static async tryReconnect() {
if (this.reconnecting) return;
this.reconnecting = true;
logger.info(`Reconnecting in ${this.reconnectInterval / 1000}s...`);
setTimeout(async () => {
try {
await this.connect();
} catch (err) {
logger.error("Reconnect failed:", err);
this.reconnectInterval = Math.min(
this.reconnectInterval * 2,
this.maxReconnectInterval
);
this.reconnecting = false;
this.tryReconnect();
}
}, this.reconnectInterval);
}
private static cleanupSocket() {
if (this.ws) {
this.ws.removeAllListeners();
this.ws.close();
this.ws = null;
}
if (this.heartbeatInterval) {
clearInterval(this.heartbeatInterval);
this.heartbeatInterval = null;
}
}
public static close() {
this.shouldReconnect = false;
this.reconnecting = false;
this.cleanupSocket();
}
private static startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send("PING");
}
}, 15_000);
}
}

View File

@@ -6,6 +6,7 @@ interface ImportMetaEnv {
readonly MAIN_VITE_AUTH_URL: string;
readonly MAIN_VITE_CHECKOUT_URL: string;
readonly MAIN_VITE_EXTERNAL_RESOURCES_URL: string;
readonly MAIN_VITE_WS_URL: string;
}
interface ImportMeta {

View File

@@ -1,14 +0,0 @@
import path from "node:path";
import steamGamesWorkerPath from "./steam-games.worker?modulePath";
import Piscina from "piscina";
import { seedsPath } from "@main/constants";
export const steamGamesWorker = new Piscina({
filename: steamGamesWorkerPath,
workerData: {
steamGamesPath: path.join(seedsPath, "steam-games.json"),
},
maxThreads: 1,
});

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