Compare commits

...

266 Commits

Author SHA1 Message Date
Chubby Granny Chaser
b8647a3300 Merge branch 'main' of https://github.com/hydralauncher/hydra into feat/custom-achievement-sound 2025-11-10 22:57:59 +00:00
Chubby Granny Chaser
95894484f1 feat: adding slider to achievement sound 2025-11-10 22:57:35 +00:00
Chubby Granny Chaser
6fc5a70722 feat: adding slider to achievement sound 2025-11-10 22:55:49 +00:00
Chubby Granny Chaser
399669a94c feat: adding slider to achievement sound 2025-11-10 22:55:17 +00:00
Chubby Granny Chaser
77b2fc3946 Merge pull request #1848 from hydralauncher/fix/library-ui
feat: library ui changes and searchbar removal
2025-11-10 22:55:02 +00:00
Chubby Granny Chaser
3472e90858 Merge branch 'main' into fix/library-ui 2025-11-10 22:53:52 +00:00
Chubby Granny Chaser
d80daa59d0 Merge branch 'main' into feat/custom-achievement-sound 2025-11-10 22:21:51 +00:00
Chubby Granny Chaser
46df34e8a5 feat: improving library 2025-11-10 22:20:44 +00:00
Chubby Granny Chaser
272b047ded Merge pull request #1844 from expload233/main
Add new translation for Chinese
2025-11-10 21:51:44 +00:00
Chubby Granny Chaser
dfba38aeed Merge branch 'main' into main 2025-11-10 21:51:20 +00:00
Moyasee
d54ff9a949 fix: eslint issues 2025-11-09 15:34:24 +02:00
Moyasee
e272470a7b feat: using theme name for folder instead themeid 2025-11-09 15:28:52 +02:00
Moyasee
53bc3551e1 Merge branch 'feat/custom-achievement-sound' of https://github.com/hydralauncher/hydra into feat/custom-achievement-sound 2025-11-09 04:20:48 +02:00
Moyasee
3daf28c882 fix: handling exception and ESLint issues 2025-11-09 04:19:52 +02:00
Moyase
e128dad4dd Merge branch 'main' into feat/custom-achievement-sound 2025-11-09 04:14:31 +02:00
Moyasee
65e2bb38a0 ci: formatting 2025-11-08 19:26:13 +02:00
Moyasee
5f09321728 Merge branch 'fix/library-ui' of https://github.com/hydralauncher/hydra into fix/library-ui 2025-11-08 19:25:29 +02:00
Moyasee
011559b499 fix: removed VirtualList component from large view 2025-11-08 19:24:43 +02:00
Moyasee
482d9b2f96 fix: ensure consistent custom sound detection across main and renderer processes 2025-11-08 15:14:12 +02:00
Moyase
c890b0fd56 Merge branch 'main' into fix/library-ui 2025-11-08 14:52:35 +02:00
Moyasee
cf48627a8d fix: extracter ternary operation 2025-11-08 14:51:10 +02:00
Moyasee
196413ee28 fix: duplicated lines 2025-11-08 14:48:42 +02:00
Moyasee
c3a4990a50 ci: performance optimizing in library 2025-11-08 14:28:54 +02:00
Chubby Granny Chaser
fa4c11e458 Merge pull request #1849 from hydralauncher/fix/surprise-me-button
Fix: surprise me button functionality
2025-11-08 08:18:13 +00:00
Chubby Granny Chaser
50b0a82204 feat: improving styles on randomizer button 2025-11-08 08:17:19 +00:00
Moyasee
b6bbf05da6 fix: theme editor layout positioning 2025-11-07 20:12:50 +02:00
Moyasee
154b6271a1 fix: removed unused function 2025-11-07 17:50:47 +02:00
Moyasee
a6cbaf6dc1 feat: custom achievement sound and volume changing) 2025-11-07 17:48:56 +02:00
Moyasee
6e6e0f7bb7 fix: duplicate next suggestion styling removal 2025-11-07 13:35:50 +02:00
Moyasee
893802be55 fix: next suggestion and title not being showed 2025-11-07 13:27:24 +02:00
Moyasee
3bef0c9269 feat: library ui changes and searchbar removal 2025-11-06 18:26:56 +02:00
Moyase
754e9c14b8 Merge pull request #1821 from iam-sahil/feat/library
Feat/library
2025-11-06 17:22:58 +02:00
ctrlcat0x
5e653be4c3 fix: add error logging in handleActionClick for better debugging 2025-11-06 19:11:20 +05:30
ctrlcat0x
cedf7e6e37 style: improve color contrast in various components and update prop types to readonly 2025-11-06 19:03:23 +05:30
Moyase
518a0e1cf4 Merge branch 'main' into feat/library 2025-11-06 14:57:22 +02:00
expload233
5f56a3d517 add translation 2025-11-04 15:47:44 +08:00
expload
2359c4cc5e Merge branch 'hydralauncher:main' into main 2025-11-04 15:31:03 +08:00
expload233
66bb5221c1 fix lint 2025-11-04 15:24:42 +08:00
Chubby Granny Chaser
7fa50dc5a7 feat: adding ws client back 2025-11-03 12:02:52 +00:00
expload233
088feaffc2 add missing translation for zh-CN 2025-11-03 19:02:11 +08:00
expload233
aa6b595b18 Fill in the missing entries 2025-11-03 18:53:02 +08:00
Chubby Granny Chaser
f49fea3032 ci: bump version
Some checks failed
Build Renderer / build (push) Has been cancelled
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
2025-11-02 21:17:34 +00:00
Chubby Granny Chaser
595d39986d Merge pull request #1843 from hydralauncher/fix/fixing-datanodes
fix: fixing datanodes
2025-11-02 21:15:53 +00:00
Chubby Granny Chaser
ac01930d68 Merge branch 'main' into fix/fixing-datanodes 2025-11-02 21:14:49 +00:00
Chubby Granny Chaser
37caeb8047 fix: fixing datanodes 2025-11-02 21:13:44 +00:00
Chubby Granny Chaser
7d6eddb17e Merge pull request #1826 from hydralauncher/feat/reviews-in-profile
Feat: Showing User Reviews in profile
2025-11-02 20:46:47 +00:00
Chubby Granny Chaser
48775e57fc feat: adding reviews to profile 2025-11-02 20:43:59 +00:00
Chubby Granny Chaser
fdc3fecd6f feat: adding reviews to profile 2025-11-02 20:42:42 +00:00
Chubby Granny Chaser
f0dc7478cf feat: adding reviews to profile 2025-11-02 20:29:16 +00:00
Chubby Granny Chaser
e7a437e839 Merge branch 'main' into feat/library 2025-11-02 20:23:44 +00:00
Chubby Granny Chaser
2e8da53d1a feat: adding infinite scroll 2025-11-02 20:23:12 +00:00
Chubby Granny Chaser
8794fbc742 Merge branch 'main' of https://github.com/hydralauncher/hydra into feat/reviews-in-profile 2025-11-02 17:31:06 +00:00
Chubby Granny Chaser
bf387aef3f feat: improving animations 2025-11-02 17:30:45 +00:00
Chubby Granny Chaser
c2a26b9750 Merge pull request #1832 from hydralauncher/feat/playtime-in-reviews
Feat: Playtime showing in review message
2025-11-02 17:29:28 +00:00
Moyase
3dc2a29114 Merge branch 'main' into feat/playtime-in-reviews 2025-11-02 19:25:31 +02:00
Chubby Granny Chaser
6ebf7766aa Merge branch 'main' into feat/reviews-in-profile 2025-11-02 17:04:58 +00:00
Zamitto
19bf99ff11 chore: add sleep to aur script 2025-10-31 16:16:03 -03:00
Zamitto
9c00a17193 Merge branch 'release/v3.7.2' 2025-10-31 13:58:14 -03:00
Zamitto
d167628ed4 fix: prevent crash when detectedLanguage is null
Some checks failed
Build Renderer / build (push) Has been cancelled
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
2025-10-31 13:57:15 -03:00
Zamitto
59cfce86ae Merge pull request #1841 from JarEXE/fix/achievement-notification-position
Fix: [Linux] achievement notification positioning on multi-monitor setups
2025-10-31 13:23:41 -03:00
Zamitto
51c4e4f5b3 chore: bump version 2025-10-31 13:07:06 -03:00
jarexe
138120460c fix: correct achievement notification positioning on multi-monitor setups 2025-10-31 10:57:44 -03:00
Zamitto
c71f5947ba feat: use new ep to track game playtime 2025-10-31 10:20:11 -03:00
Chubby Granny Chaser
ff8a61ff7a fix: fixing review partial 2025-10-31 12:05:24 +00:00
Chubby Granny Chaser
d1d46971b6 fix: fixing review partial 2025-10-31 12:03:35 +00:00
Chubby Granny Chaser
b8af69b0fb fix: fixing review partial 2025-10-31 12:01:42 +00:00
Zamitto
1af69465c1 Merge pull request #1839 from hydralauncher/fix/custom-games-requests
fix: requests for custom games
2025-10-31 07:08:51 -03:00
Chubby Granny Chaser
f6c12c22b5 Merge branch 'main' into feat/reviews-in-profile 2025-10-31 07:29:22 +00:00
Chubby Granny Chaser
539010d817 Merge branch 'main' into feat/playtime-in-reviews 2025-10-31 07:29:13 +00:00
Zamitto
aa148c0b70 fix: trim
Some checks failed
Build Renderer / build (push) Has been cancelled
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
2025-10-30 20:01:47 -03:00
Moyase
a83a96f214 Merge pull request #1840 from hydralauncher/feat/catalogue-manual-pagination
fix: removed ability to enter non-number symbols to pagination
2025-10-31 00:46:39 +02:00
Moyasee
aadbda770b fix: linting issue, marked props as read-only 2025-10-31 00:19:49 +02:00
Moyasee
bd059cc7fa feat: update cursorrules 2025-10-30 23:45:29 +02:00
Moyasee
bbbf861594 fix: deleted comments 2025-10-30 23:36:41 +02:00
Moyasee
80e0adcd49 fix: removed ability to enter non-number symbols to pagination 2025-10-30 23:33:07 +02:00
Zamitto
2aa31c0db0 feat: limit game text search to 255 chars 2025-10-30 15:34:49 -03:00
Zamitto
4bfe6d7f86 feat: limit game text search to 255 chars 2025-10-30 15:32:08 -03:00
Zamitto
aadf648a2b chore: unnecessary casting 2025-10-30 07:58:43 -03:00
Zamitto
87dbd548d0 Merge branch 'release/v3.7.2' into fix/custom-games-requests 2025-10-30 07:58:31 -03:00
Zamitto
459bf73121 fix: request download-sources on custom game
Some checks failed
Build Renderer / build (push) Has been cancelled
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
2025-10-30 07:36:23 -03:00
Zamitto
a2ef0f304d fix: playtime count and custom games request on process watcher 2025-10-30 07:35:49 -03:00
Zamitto
b04561986e Merge pull request #1838 from hydralauncher/main
Some checks failed
Build Renderer / build (push) Has been cancelled
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
sync release 3.7.2
2025-10-29 23:07:16 -03:00
Zamitto
1bd88e6c6e Merge pull request #1837 from Stormm232/main
Updating Hungarian Translation
2025-10-29 23:06:22 -03:00
Kiwo.2
4ff8dc4fa7 Fix with Prettier 2025-10-30 02:32:18 +01:00
Kiwo.2
dcc671f999 Mistake Correction 2025-10-30 02:15:35 +01:00
Kiwo.2
6e76111e23 Missing Comma Fix 2025-10-30 02:10:02 +01:00
Kiwo.2
3fce26f1f7 Update to 3.7.2 2025-10-30 01:55:15 +01:00
Kiwo.2
90c5ccb796 Update to 3.7.2 2025-10-30 01:40:36 +01:00
Kiwo.2
41092c2dd4 Update to 3.7.2 2025-10-30 01:35:54 +01:00
Kiwo.2
6383b728bc Fix to the translation 2025-10-30 01:26:51 +01:00
Kiwo.2
4dd28bbbf1 Hungarian Translation 3.7.2 2025-10-30 01:12:29 +01:00
Zamitto
21074322fa Merge pull request #1836 from Wkeynhk/patch-6
Some checks failed
Build Renderer / build (push) Has been cancelled
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
Update Russian Translation
2025-10-29 20:32:30 -03:00
Wkeynhk
0e7e53478a Update translation.json 2025-10-30 00:47:30 +03:00
Zamitto
65e49550ad chore: fix aur package 2025-10-29 18:10:27 -03:00
Zamitto
0990951183 chore: fix aur package 2025-10-29 18:06:46 -03:00
Zamitto
53c162f0e4 feat: add i18n 2025-10-29 17:55:55 -03:00
Zamitto
2fb44a6c0e chore: remove build renderer trigger on main 2025-10-29 15:49:43 -03:00
Zamitto
49c2bc34d1 Merge branch 'release/v3.7.2' 2025-10-29 15:48:44 -03:00
Zamitto
ef52d710ed Merge branch 'main' into feat/reviews-in-profile 2025-10-29 15:28:39 -03:00
Zamitto
31d57a784e Merge branch 'main' into feat/playtime-in-reviews 2025-10-29 15:28:37 -03:00
Zamitto
49df40650c chore: prettier 2025-10-29 15:27:36 -03:00
Chubby Granny Chaser
499a830e3e chore: sync with main 2025-10-29 18:23:06 +00:00
Chubby Granny Chaser
437b0a3b19 Merge branch 'main' of https://github.com/hydralauncher/hydra into release/v3.7.2 2025-10-29 18:21:39 +00:00
Chubby Granny Chaser
d59ff5c484 Merge pull request #1814 from whintersnow0/refactor/remove-unnecessary-usememo
refactor: remove unnecessary useMemo hooks
2025-10-29 18:20:21 +00:00
Chubby Granny Chaser
dcf13a5920 Merge branch 'main' into refactor/remove-unnecessary-usememo 2025-10-29 18:19:39 +00:00
Chubby Granny Chaser
51861752a1 Merge pull request #1816 from hydralauncher/feat/improving-sources
feat: moving sources to worker
2025-10-29 18:19:35 +00:00
Chubby Granny Chaser
fb29152a0b Merge branch 'main' into feat/improving-sources 2025-10-29 18:19:07 +00:00
Zamitto
19bbbbfb77 Merge pull request #1835 from hydralauncher/fix/custom-assets-sizing
fix: images with big height breaking layout
2025-10-29 15:13:21 -03:00
Chubby Granny Chaser
952d4d9ee2 Merge branch 'main' into feat/improving-sources 2025-10-29 18:13:05 +00:00
Moyasee
ad588b5600 fix: images with big height breaking layout 2025-10-29 19:51:09 +02:00
Chubby Granny Chaser
2fce12eba7 Merge branch 'main' into feat/reviews-in-profile 2025-10-29 16:55:36 +00:00
Chubby Granny Chaser
1427775c98 Merge branch 'main' into feat/playtime-in-reviews 2025-10-29 16:55:26 +00:00
Zamitto
c24ad34bc7 fix: hltb and achievements being called for custom games
Some checks failed
Build Renderer / build (push) Has been cancelled
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
2025-10-29 12:42:39 -03:00
Zamitto
4f2c3105ce Merge pull request #1834 from hydralauncher/fix/custom-games-unnecessary-requests
fix: disabling unnecessary api calls when game is custom
2025-10-29 12:07:40 -03:00
Moyasee
feedcb1dc7 feat: disabled assets request for custom games 2025-10-29 16:49:51 +02:00
Moyasee
4b8d64c72b feat: disabled favorite/unfavorite get request for custom games 2025-10-29 16:44:48 +02:00
Moyasee
dff68a3e26 fix: removed comments 2025-10-29 16:22:12 +02:00
Moyasee
58bdbdab71 fix: disabling unnecessary api calls if game is custom 2025-10-29 16:16:11 +02:00
Chubby Granny Chaser
e143fadf38 fix: fixing import
Some checks failed
Build Renderer / build (push) Has been cancelled
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
2025-10-29 02:55:37 +00:00
Chubby Granny Chaser
f99f8d9554 feat: forcing dev tools 2025-10-29 02:32:45 +00:00
Chubby Granny Chaser
274080069f feat: forcing dev tools 2025-10-29 02:12:17 +00:00
Chubby Granny Chaser
b1069426e4 chore: sync with main 2025-10-29 01:47:35 +00:00
Chubby Granny Chaser
dc6d578462 chore: sync with main 2025-10-28 23:49:09 +00:00
Chubby Granny Chaser
8a12c6e088 chore: sync with main 2025-10-28 23:26:28 +00:00
Chubby Granny Chaser
b795cea599 Merge branch 'main' of https://github.com/hydralauncher/hydra into release/v3.7.2 2025-10-28 23:20:44 +00:00
Chubby Granny Chaser
574a012d8c Merge pull request #1833 from hydralauncher/feat/settings-trailer-and-launch
Some checks failed
Build Renderer / build (push) Has been cancelled
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
Feat: New behavior settings: Trailers Auto-play and Hide on game launch
2025-10-28 23:11:57 +00:00
Chubby Granny Chaser
8e3bf29a21 Merge branch 'main' into feat/settings-trailer-and-launch 2025-10-28 23:11:19 +00:00
Chubby Granny Chaser
f6d21baff5 Merge pull request #1831 from hydralauncher/feat/catalogue-manual-pagination
Feat: Manual selection of the page in catalogue.
2025-10-28 23:09:53 +00:00
Chubby Granny Chaser
b2b7b36f70 Merge branch 'main' into feat/catalogue-manual-pagination 2025-10-28 23:08:46 +00:00
Chubby Granny Chaser
dc8a19e845 ci: adding ci vars 2025-10-28 23:02:40 +00:00
Chubby Granny Chaser
ce0619bbe3 ci: adding releases 2025-10-28 22:40:06 +00:00
Chubby Granny Chaser
4acb7f7001 Merge branch 'feat/improving-sources' of https://github.com/hydralauncher/hydra into feat/improving-sources 2025-10-28 21:38:24 +00:00
Chubby Granny Chaser
a11b3e8877 ci: fixing release pipeline 2025-10-28 21:38:07 +00:00
Chubby Granny Chaser
6b96c99bb1 ci: fixing release pipeline 2025-10-28 21:37:28 +00:00
Moyasee
dbf5d7afc7 fix: multiple imports 2025-10-28 17:43:19 +02:00
Moyasee
bfc4bb1a83 ci: formatting 2025-10-28 17:36:11 +02:00
Moyasee
120aad6c1c feat: Hide to tray on game startup and ability to disable trailers auto-play 2025-10-28 17:34:20 +02:00
Zamitto
1123aaa65e chore: remove zod dep 2025-10-28 06:48:42 -03:00
Zamitto
2f8d241ded Merge pull request #1829 from hydralauncher/chore/bump-electron-version
chore: bump electron version
2025-10-27 18:16:35 -03:00
Zamitto
61072aa02a fix: add theme editor dev tools back
Some checks failed
Build Renderer / build (push) Has been cancelled
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
2025-10-27 15:25:18 -03:00
Zamitto
ddd6af0d4c fix: add theme editor dev tools back 2025-10-27 15:22:47 -03:00
Moyasee
5c770bc7e7 fix: unnecessary assertion 2025-10-27 20:12:24 +02:00
Moyasee
b431ed479c fix: converted conditional to boolean 2025-10-27 20:07:08 +02:00
Moyasee
9e09a5decb fix: translation key fix and formatting 2025-10-27 19:28:29 +02:00
Moyasee
1e1a1c61c9 feat: showing playtime in review and changed positions of elements in review 2025-10-27 19:22:59 +02:00
Chubby Granny Chaser
fc6068d603 fix: fixing dlls 2025-10-27 12:33:29 +00:00
Chubby Granny Chaser
3ab1e29578 ci: testing windows 2025-10-27 10:47:19 +00:00
Chubby Granny Chaser
1effa80311 ci: testing windows 2025-10-27 10:35:20 +00:00
Chubby Granny Chaser
549e1270ee ci: testing windows 2025-10-27 09:56:45 +00:00
Chubby Granny Chaser
eb006c5e90 ci: testing windows 2019 2025-10-27 09:51:11 +00:00
Chubby Granny Chaser
54632bd06d ci: testing windows 2019 2025-10-27 09:16:55 +00:00
Zamitto
a073cf7f8c chore: bump electron vite version 2025-10-27 04:50:32 -03:00
Chubby Granny Chaser
6921bfa3ff ci: testing windows 2019 2025-10-27 07:41:58 +00:00
Chubby Granny Chaser
2835207d79 fix: fixing download sources import status 2025-10-27 04:12:19 +00:00
Chubby Granny Chaser
2a90faeb42 fix: persisting is remote 2025-10-27 01:42:02 +00:00
Chubby Granny Chaser
87a57f7a37 feat: adding sources migration 2025-10-26 23:22:20 +00:00
Chubby Granny Chaser
7c272aeed8 Merge branch 'main' into feat/improving-sources 2025-10-26 21:42:58 +00:00
Moyasee
6565ce5316 fix: moved component out of parent component 2025-10-26 19:59:43 +02:00
Moyasee
fee3a4522a fix: duplications 2025-10-26 19:49:15 +02:00
Moyasee
cb3e52de34 fix: go to page button did not appear correctly for the last pages 2025-10-26 19:37:57 +02:00
Moyasee
7f2343413e feat: added manual page selection and changed functionality of pagination 2025-10-26 17:26:25 +02:00
Zamitto
ee35bc24b2 chore: undo remove hydra api logs
Some checks failed
Build Renderer / build (push) Has been cancelled
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
2025-10-24 21:11:56 -03:00
Zamitto
4471bf0f8b chore: bump to electron 37 2025-10-24 21:05:40 -03:00
Zamitto
f239562bb3 Merge branch 'main' into chore/bump-electron-version 2025-10-24 21:05:00 -03:00
Zamitto
11c19f5fe5 chore: downgrade to latest of 34 2025-10-24 20:47:24 -03:00
Chubby Granny Chaser
0c7767de36 fix: remove unused useRef import
Some checks failed
Build Renderer / build (push) Has been cancelled
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
2025-10-24 18:22:46 +01:00
Chubby Granny Chaser
a3d700bb60 feat: improving skeleton 2025-10-24 18:01:37 +01:00
Chubby Granny Chaser
881564daa7 fix: fixing game hero 2025-10-24 17:13:36 +01:00
Zamitto
ab50271399 Merge pull request #1830 from Lianela/main
Some checks failed
Build Renderer / build (push) Has been cancelled
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
feat: added missing strings and fixed typo
2025-10-24 06:57:48 -03:00
Zamitto
2c1a8bf639 Remove extra newline in Spanish translation file 2025-10-24 06:42:39 -03:00
Kyatto
362774a3cc fix: comma 2025-10-23 23:53:16 -07:00
Kyatto
dec0af8a80 feat: translated new strings 2025-10-23 23:49:18 -07:00
Moyasee
8de6c92d28 ci: formatting 2025-10-24 08:19:55 +03:00
Zamitto
29e822f2f1 fix: node version on gh actions files 2025-10-23 17:56:45 -03:00
Zamitto
a388acf948 chore: update node version on gh actions 2025-10-23 17:51:15 -03:00
Zamitto
40f7e6e2ad chore: bump electron version to 35 2025-10-23 17:47:54 -03:00
Zamitto
8aaa85e009 Merge pull request #1825 from GrimmDevel/translation-update
Some checks failed
Build Renderer / build (push) Has been cancelled
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
Added Latvian translation
2025-10-23 17:43:14 -03:00
Zamitto
d1c09299b1 Merge branch 'main' into translation-update 2025-10-23 17:19:02 -03:00
Zamitto
0a8db2a976 Fix newline at end of .env.example file 2025-10-23 17:18:46 -03:00
Zamitto
ef8c6c90fb remove debug 2025-10-23 17:16:54 -03:00
Zamitto
03770c03f1 debug 2025-10-23 17:14:08 -03:00
Zamitto
e23ee8940c debug 2025-10-23 17:06:55 -03:00
Zamitto
089d417950 debug 2025-10-23 17:04:35 -03:00
GrimmDevel
8a64b5e245 Fix 2025-10-23 23:04:02 +03:00
Grimm
fb93f06901 Merge branch 'main' into translation-update 2025-10-23 23:01:23 +03:00
Zamitto
7fc9962e04 chore: create builder user to run makepkg 2025-10-23 16:55:51 -03:00
Zamitto
2179086285 install missing arch package 2025-10-23 16:34:47 -03:00
Zamitto
0814c08459 update aur 2025-10-23 16:30:01 -03:00
Zamitto
9e84cd970e update aur 2025-10-23 16:24:49 -03:00
Zamitto
321d170634 force different version to test script 2025-10-23 16:13:24 -03:00
Zamitto
b96e6095dc update aur 2025-10-23 16:03:51 -03:00
Moyasee
29e1713824 fix: upvote/downvote button arent being disabled after click 2025-10-23 20:06:37 +03:00
Zamitto
e12fdf8f8f remove js script 2025-10-23 12:45:09 -03:00
Zamitto
52714e3323 remove v from tag 2025-10-23 12:38:31 -03:00
Zamitto
2529bdf5ca fix 2025-10-23 12:33:19 -03:00
Zamitto
6545c7d7cd update aur 2025-10-23 12:22:08 -03:00
Zamitto
7f28929c68 update yml 2025-10-23 12:19:12 -03:00
Zamitto
19d8a09f9d update aur 2025-10-23 12:17:02 -03:00
Zamitto
00e716375e clone with https for testing 2025-10-23 12:13:13 -03:00
Zamitto
95a5c3716c update-aur 2025-10-23 12:06:37 -03:00
Zamitto
4d3ba51b61 update-aur.yml 2025-10-23 11:47:38 -03:00
Zamitto
a1552020c0 chore: update aur
Some checks failed
Build Renderer / build (push) Has been cancelled
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
2025-10-23 11:22:54 -03:00
Zamitto
65ae5991e7 chore: update-aur 2025-10-23 11:14:41 -03:00
Zamitto
805d67d2d1 fix: add missing package 2025-10-23 10:56:39 -03:00
Moyasee
81a77411cc ci: fix gap between game image and game name in reviews 2025-10-23 16:54:18 +03:00
Zamitto
313f2cd585 chore: try fixing action 2025-10-23 10:52:53 -03:00
Zamitto
1e8983d0c0 chore: add openssh in arch packages 2025-10-23 10:23:40 -03:00
Zamitto
face259167 chore: dont apply aur changes on workflow_dispatch 2025-10-23 10:02:31 -03:00
Zamitto
09b54addc1 Merge pull request #1827 from hydralauncher/chore/update-aur-on-release
chore: action to update aur
2025-10-23 09:59:35 -03:00
Zamitto
9a278dc614 chore: action to update aur 2025-10-23 09:49:30 -03:00
Moyasee
cc95deb709 fix: proreply reseting user reviews on profile changing 2025-10-23 14:40:02 +03:00
Moyasee
daf9751cf6 ci: import formatting 2025-10-23 14:27:03 +03:00
Moyasee
d21ec52814 ci: deleted comments 2025-10-23 12:06:23 +03:00
Moyasee
f539977431 fix: refactoring functions to prevent nesting more than 4 lvls 2025-10-23 11:53:35 +03:00
Moyasee
3ff20417d5 fix: extracted ternary operation 2025-10-23 11:37:50 +03:00
Moyasee
65f83399f5 ci: merge 2025-10-23 11:29:27 +03:00
Moyasee
eb34f051e1 Merge branch 'feat/reviews-in-profile' of https://github.com/hydralauncher/hydra into feat/reviews-in-profile 2025-10-23 11:28:40 +03:00
Moyasee
ab27f3295e fix: duplicate selectors and if statements should not be the only statement 2025-10-23 11:26:29 +03:00
Chubby Granny Chaser
3782f79100 Merge branch 'main' into feat/reviews-in-profile 2025-10-23 09:15:57 +01:00
Chubby Granny Chaser
214e8f9538 Merge pull request #1824 from spectre365/game-page-interface-changes
Interface modification for the default game page
2025-10-23 09:12:18 +01:00
Moyasee
86ab5b107b ci: formatting 2025-10-23 10:34:15 +03:00
Moyasee
acf8f340dd ci: review message ui change and fix loading reviews positioning 2025-10-23 10:33:29 +03:00
ctrlcat0x
f5470b29c0 style: adjust hover effects and dimensions for game cards; refine context menu actions 2025-10-23 10:58:31 +05:30
GrimmDevel
3df07fefe5 fixed lint error 2025-10-23 00:59:38 +03:00
GrimmDevel
00e597c910 Added Latvian translation 2025-10-23 00:51:00 +03:00
spectre365
a7b5bdb3b4 Interface modification for the default game page 2025-10-22 18:03:24 -03:00
spectre365
99e34ce060 Interface modification for the default game page 2025-10-22 17:56:20 -03:00
spectre365
f0421d9fe0 Interface modification for the default game page 2025-10-22 17:51:31 -03:00
spectre365
ca35da37ed Interface modification for the default game page 2025-10-22 17:20:48 -03:00
spectre365
7435bff64f Interface modification for the default game page 2025-10-22 17:17:37 -03:00
spectre365
864fd282f0 Interface modification for the default game page 2025-10-22 16:42:21 -03:00
spectre365
945173f48e Interface modification for the default game page 2025-10-22 16:32:39 -03:00
Moyasee
035f6e8d24 ci: formatting 2025-10-22 22:05:05 +03:00
Moyasee
362d6b634e feat: added reviews in profile and tabs 2025-10-22 21:13:05 +03:00
ctrlcat0x
a0a967aacd style: update compact view styles for game cards; adjust grid layout and add button order 2025-10-22 18:28:24 +05:30
ctrlcat0x
e19102ea66 style: update active state styles for filter and view options; adjust achievement progress bar styles 2025-10-22 16:12:12 +05:30
ctrlcat0x
107b61f663 style: update active state colors for filter and view options 2025-10-22 14:46:25 +05:30
ctrlcat0x
811a6ad955 refactor: remove unused imports and download logic from LibraryGameCard 2025-10-22 14:42:47 +05:30
ctrlcat0x
6fb8bbf744 he commit 2025-10-22 14:29:55 +05:30
ctrlcat0x
459017a4a6 Merge branch 'main' of https://github.com/hydralauncher/hydra into feat/game-library 2025-10-22 14:28:00 +05:30
Sahil Rana
d6ff8f670e Merge branch 'main' into feat/game-library 2025-10-22 14:26:09 +05:30
ctrlcat0x
33e0d50966 feat: add achievements tracking to game library
- Updated `get-library.ts` to include unlocked and total achievement counts for each game.
- Removed `library-game-card-detailed.tsx` and its associated styles as part of the refactor.
- Enhanced `library-game-card-large.tsx` to display achievements with progress bars.
- Modified `library-game-card.scss` and `library-game-card-large.scss` to style the achievements section.
- Introduced a new `search-bar` component for filtering the game library.
- Implemented fuzzy search functionality in the library view.
- Updated `view-options` to improve UI consistency.
- Added achievement-related properties to the `LibraryGame` type in `index.ts`.
- Created a new `copilot-instructions.md` for project guidelines.
2025-10-22 14:24:04 +05:30
Chubby Granny Chaser
73e378e26a Merge branch 'main' into refactor/remove-unnecessary-usememo 2025-10-21 21:11:37 +01:00
Zamitto
0d60ec8801 Merge pull request #1818 from slakgosh/main
Some checks failed
Build Renderer / build (push) Has been cancelled
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
Added finnish translation
2025-10-21 15:46:19 -03:00
Chubby Granny Chaser
3782d74ad2 Merge branch 'feat/improving-sources' of github.com:hydralauncher/hydra into feat/improving-sources 2025-10-21 04:22:17 +01:00
Chubby Granny Chaser
8a40c678f7 feat: using api download sources 2025-10-21 04:21:56 +01:00
Chubby Granny Chaser
e1ce5bc6cb feat: using api download sources 2025-10-21 04:20:11 +01:00
Chubby Granny Chaser
48ce9a2476 feat: using api download sources 2025-10-21 04:18:11 +01:00
Chubby Granny Chaser
2909be312a Merge branch 'main' into feat/improving-sources 2025-10-21 03:59:49 +01:00
Sahil Rana
361073d3f8 Merge branch 'main' into feat/game-library 2025-10-20 23:51:13 +05:30
ctrlcat0x
d168e20385 feat(library): implement large game card and enhance library UI
- Added `LibraryGameCardLarge` component for displaying games in a larger format with improved styling and animations.
- Introduced SCSS styles for the large game card, including hover effects and gradient overlays.
- Updated `LibraryGameCard` component to support mouse enter and leave events for better interaction.
- Enhanced the library view options with new styles and functionality for switching between grid, compact, and large views.
- Improved overall layout and responsiveness of the library page, ensuring a better user experience across different screen sizes.
- Added tooltips for playtime information and context menus for game actions.
2025-10-20 23:43:47 +05:30
Zamitto
0575e837c8 fix: game achievement cache
Some checks failed
Build Renderer / build (push) Has been cancelled
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
2025-10-20 08:26:28 -03:00
Domas Linkis
5ff15b30b2 Added finnish lang
Worstest 2 hours of my life
2025-10-19 13:47:42 +05:00
Domas Linkis
2f1185bbf9 Finnish Language 2025-10-19 13:38:04 +05:00
Domas Linkis
19a57cb1e0 Added ukrainian lang 2025-10-19 13:30:10 +05:00
Chubby Granny Chaser
d2089ec7d4 Merge branch 'main' into feat/improving-sources 2025-10-18 14:08:31 +01:00
Chubby Granny Chaser
c2273dbf71 feat: moving sources to worker 2025-10-18 14:07:44 +01:00
Chubby Granny Chaser
bbd9ff76c4 Merge pull request #1815 from hydralauncher/feat/adding-review-translations
Some checks failed
Build Renderer / build (push) Has been cancelled
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
feat: adding translations
2025-10-17 15:38:30 +01:00
Chubby Granny Chaser
39e76f458f Merge branch 'main' into feat/adding-review-translations 2025-10-17 15:38:20 +01:00
Chubby Granny Chaser
393c55738c feat: removing pre and code 2025-10-17 15:33:10 +01:00
Chubby Granny Chaser
24f7ecb795 feat: adding translations 2025-10-17 15:04:42 +01:00
Zamitto
97b27a1785 fix: handle user not found on user-profile.context
Some checks failed
Build Renderer / build (push) Has been cancelled
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
2025-10-16 11:01:20 -03:00
whintersnow0
3dc71a8d1f refactor: remove unnecessary useMemo hooks 2025-10-15 19:19:08 +02:00
Chubby Granny Chaser
e7ee049df5 ci: fixing env var
Some checks failed
Build Renderer / build (push) Has been cancelled
Release / build (ubuntu-latest) (push) Has been cancelled
Release / build (windows-2022) (push) Has been cancelled
2025-10-15 16:39:59 +01:00
Chubby Granny Chaser
f5a6a5c359 Merge pull request #1813 from hydralauncher/feat/remove-dexie
Feat/remove dexie
2025-10-15 16:05:41 +01:00
Kiwo.2
8203399eda Matching to Latest Update 2025-10-12 23:27:20 +02:00
158 changed files with 8365 additions and 3146 deletions

37
.cursorrules Normal file
View File

@@ -0,0 +1,37 @@
# Hydra Project Rules
## Logging
- **Always use `logger` instead of `console` for logging** in both main and renderer processes
- In main process: `import { logger } from "@main/services";`
- In renderer process: `import { logger } from "@renderer/logger";`
- Replace all instances of:
- `console.log()` → `logger.log()`
- `console.error()` → `logger.error()`
- `console.warn()` → `logger.warn()`
- `console.info()` → `logger.info()`
- `console.debug()` → `logger.debug()`
- Do not use `console` for any logging purposes
## Internationalization (i18n)
- All user-facing strings must be translated using i18next
- Use the `useTranslation` hook in React components: `const { t } = useTranslation("namespace");`
- Add new translation keys to `src/locales/en/translation.json`
- Never hardcode English strings in the UI code
- Placeholder text in form fields must also be translated
## Code Style
- Use ESLint and Prettier for code formatting
- Follow TypeScript strict mode conventions
- Use async/await instead of promises when possible
- Prefer named exports over default exports for utilities and services
## Comments
- Keep comments concise and purposeful; avoid verbose explanations.
- Focus on the "why" or non-obvious context, not restating the code.
- Prefer self-explanatory naming and structure over excessive comments.
- Do not comment every line or obvious behavior; remove stale comments.
- Use docblocks only where they add value (public APIs, complex logic).

View File

@@ -3,3 +3,4 @@ MAIN_VITE_AUTH_URL=
MAIN_VITE_WS_URL=
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID=
RENDERER_VITE_TORBOX_REFERRAL_CODE=
MAIN_VITE_LAUNCHER_SUBDOMAIN=

View File

@@ -6,23 +6,37 @@ concurrency:
on:
push:
branches: main
branches:
- release/**
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
env:
NODE_OPTIONS: --max-old-space-size=4096
BRANCH_NAME: ${{ github.ref_name }}
steps:
- name: Check out Git repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install Node.js
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20.18.0
node-version: 22.21.0
cache: "yarn"
- name: Enable Corepack (Yarn)
run: corepack enable
- name: Install dependencies
run: yarn --frozen-lockfile --ignore-scripts
run: yarn install --frozen-lockfile --ignore-scripts
- name: Build Renderer
run: yarn build
@@ -36,5 +50,5 @@ jobs:
run: |
npx --yes wrangler@3 pages deploy out/renderer \
--project-name="hydra" \
--commit-dirty=true \
--branch="main"
--branch "$BRANCH_NAME" \
--commit-dirty

View File

@@ -1,11 +1,12 @@
name: Build
on:
pull_request:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on: pull_request
jobs:
build:
strategy:
@@ -22,7 +23,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20.18.3
node-version: 22.21.0
- name: Install dependencies
run: yarn --frozen-lockfile
@@ -38,11 +39,15 @@ jobs:
- name: Build with cx_Freeze
run: python python_rpc/setup.py build
- name: Copy OpenSSL DLLs
if: matrix.os == 'windows-2022'
run: |
cp hydra-python-rpc/lib/libcrypto-1_1.dll hydra-python-rpc/lib/libcrypto-1_1-x64.dll
cp hydra-python-rpc/lib/libssl-1_1.dll hydra-python-rpc/lib/libssl-1_1-x64.dll
- name: Build Linux
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libarchive-tools
yarn build:linux
env:
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_STAGING_API_URL }}
@@ -98,5 +103,4 @@ jobs:
dist/*.tar.gz
dist/*.yml
dist/*.blockmap
dist/*.pacman
dist/*.AppImage

View File

@@ -17,7 +17,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20.18.3
node-version: 22.21.0
- name: Install dependencies
run: yarn --frozen-lockfile

View File

@@ -6,7 +6,8 @@ concurrency:
on:
push:
branches: [main]
branches:
- release/**
jobs:
build:
@@ -23,7 +24,7 @@ jobs:
- name: Install Node.js
uses: actions/setup-node@v4
with:
node-version: 20.18.3
node-version: 22.21.0
- name: Install dependencies
run: yarn --frozen-lockfile
@@ -39,11 +40,15 @@ jobs:
- name: Build with cx_Freeze
run: python python_rpc/setup.py build
- name: Copy OpenSSL DLLs
if: matrix.os == 'windows-2022'
run: |
cp hydra-python-rpc/lib/libcrypto-1_1.dll hydra-python-rpc/lib/libcrypto-1_1-x64.dll
cp hydra-python-rpc/lib/libssl-1_1.dll hydra-python-rpc/lib/libssl-1_1-x64.dll
- name: Build Linux
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libarchive-tools
yarn build:linux
env:
MAIN_VITE_API_URL: ${{ vars.MAIN_VITE_API_URL }}
@@ -57,7 +62,7 @@ jobs:
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
MAIN_VITE_RENDERER_URL: ${{ vars.MAIN_VITE_RENDERER_URL }}
MAIN_VITE_LAUNCHER_SUBDOMAIN: ${{ vars.MAIN_VITE_LAUNCHER_SUBDOMAIN }}
- name: Build Windows
if: matrix.os == 'windows-2022'
@@ -74,7 +79,7 @@ jobs:
RENDERER_VITE_SENTRY_DSN: ${{ vars.SENTRY_DSN }}
RENDERER_VITE_REAL_DEBRID_REFERRAL_ID: ${{ vars.RENDERER_VITE_REAL_DEBRID_REFERRAL_ID }}
RENDERER_VITE_TORBOX_REFERRAL_CODE: ${{ vars.RENDERER_VITE_TORBOX_REFERRAL_CODE }}
MAIN_VITE_RENDERER_URL: ${{ vars.MAIN_VITE_RENDERER_URL }}
MAIN_VITE_LAUNCHER_SUBDOMAIN: ${{ vars.MAIN_VITE_LAUNCHER_SUBDOMAIN }}
- name: Create artifact
uses: actions/upload-artifact@v4
@@ -90,7 +95,6 @@ jobs:
dist/*.tar.gz
dist/*.yml
dist/*.blockmap
dist/*.pacman
- name: Upload build
env:
@@ -119,6 +123,5 @@ jobs:
dist/*.tar.gz
dist/*.yml
dist/*.blockmap
dist/*.pacman
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

161
.github/workflows/update-aur.yml vendored Normal file
View File

@@ -0,0 +1,161 @@
name: Update AUR Package
on:
workflow_dispatch:
release:
types: [published]
jobs:
update-aur:
runs-on: ubuntu-latest
container:
image: archlinux:latest
steps:
- name: Install dependencies
run: |
pacman -Syu --noconfirm
pacman -S --noconfirm nodejs npm git base-devel openssh jq pacman-contrib
- name: Create builder user
run: |
# Create builder user with home directory
useradd -m -s /bin/bash builder
# Add builder to wheel group for sudo access
usermod -aG wheel builder
# Configure sudo for builder user (no password required)
echo 'builder ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers
- name: Setup SSH for AUR
run: |
mkdir -p ~/.ssh
echo "${{ secrets.AUR_SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
chmod 700 ~/.ssh
# Add AUR host key to known_hosts
ssh-keyscan aur.archlinux.org >> ~/.ssh/known_hosts
# Configure SSH to use the key
cat > ~/.ssh/config << EOF
Host aur.archlinux.org
IdentityFile ~/.ssh/id_rsa
IdentitiesOnly yes
User aur
UserKnownHostsFile ~/.ssh/known_hosts
StrictHostKeyChecking no
EOF
# Start SSH agent and add key
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_rsa
export GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa -F ~/.ssh/config -o UserKnownHostsFile=$SSH_PATH/known_hosts"
git clone ssh://aur@aur.archlinux.org/hydra-launcher-bin.git
# Give builder user ownership of the repository
chown -R builder:builder hydra-launcher-bin
- name: Get version to update
id: get-version
run: |
if [ "${{ github.event_name }}" = "release" ]; then
VERSION="${{ github.event.release.tag_name }}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "source=release" >> $GITHUB_OUTPUT
else
echo "Getting latest release version"
VERSION=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name' | sed 's/^v//')
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "source=latest" >> $GITHUB_OUTPUT
fi
echo "Version to update: $VERSION"
- name: Check if update is needed
id: check-update
run: |
CURRENT_VERSION=$(grep '^pkgver=' hydra-launcher-bin/PKGBUILD | cut -d'=' -f2)
NEW_VERSION="${{ steps.get-version.outputs.version }}"
echo "Current AUR version: $CURRENT_VERSION"
echo "New version: $NEW_VERSION"
if [ "$CURRENT_VERSION" = "$NEW_VERSION" ]; then
echo "update_needed=false" >> $GITHUB_OUTPUT
echo "No update needed - versions are the same"
else
echo "update_needed=true" >> $GITHUB_OUTPUT
echo "Update needed"
fi
- name: Update PKGBUILD and .SRCINFO
if: steps.check-update.outputs.update_needed == 'true'
run: |
# sleeps for 1 minute to be sure GH updated the release info
sleep 60
# Update pkgver in PKGBUILD
cd hydra-launcher-bin
NEW_VERSION="${{ steps.get-version.outputs.version }}"
NEW_VERSION="${NEW_VERSION#v}"
echo "Updating PKGBUILD pkgver to $NEW_VERSION"
# Read PKGBUILD and update pkgver line
sed -i "s/^pkgver=.*/pkgver=$NEW_VERSION/" ./PKGBUILD
# Reset pkgrel to 1 when version changes
sed -i "s/^pkgrel=.*/pkgrel=1/" ./PKGBUILD
echo "✅ Successfully updated pkgver to $NEW_VERSION in ./PKGBUILD"
# Update package checksums and generate .SRCINFO as builder user
sudo -u builder updpkgsums
sudo -u builder makepkg --printsrcinfo > .SRCINFO
- name: Commit and push changes
if: steps.check-update.outputs.update_needed == 'true'
run: |
cd hydra-launcher-bin
git config --global --add safe.directory .
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
git add PKGBUILD .SRCINFO
echo "## Git Diff Preview"
echo "Changes that would be made:"
git diff PKGBUILD .SRCINFO || echo "No changes to show"
echo ""
echo "Staged changes:"
git add PKGBUILD .SRCINFO
git diff --staged || echo "No staged changes"
if git diff --staged --quiet; then
echo "No changes to commit"
else
COMMIT_MSG="v${{ steps.get-version.outputs.version }}"
git commit -m "$COMMIT_MSG"
export GIT_SSH_COMMAND="ssh -i ~/.ssh/id_rsa -F ~/.ssh/config -o UserKnownHostsFile=$SSH_PATH/known_hosts"
git push origin master
echo "Successfully updated AUR package to version ${{ steps.get-version.outputs.version }}"
fi
- name: Create summary
if: always()
run: |
echo "## AUR Update Summary" >> $GITHUB_STEP_SUMMARY
echo "- **Version**: ${{ steps.get-version.outputs.version }}" >> $GITHUB_STEP_SUMMARY
echo "- **Source**: ${{ steps.get-version.outputs.source }}" >> $GITHUB_STEP_SUMMARY
echo "- **Update needed**: ${{ steps.check-update.outputs.update_needed }}" >> $GITHUB_STEP_SUMMARY
if [ "${{ steps.check-update.outputs.update_needed }}" = "true" ]; then
echo "- **Status**: ✅ AUR package updated successfully" >> $GITHUB_STEP_SUMMARY
else
echo "- **Status**: ⏭️ No update needed" >> $GITHUB_STEP_SUMMARY
fi

View File

@@ -56,7 +56,6 @@ linux:
- AppImage
- snap
- deb
- pacman
- rpm
maintainer: electronjs.org
category: Game

View File

@@ -1,6 +1,6 @@
{
"name": "hydralauncher",
"version": "3.7.1",
"version": "3.7.4",
"description": "Hydra",
"main": "./out/main/index.js",
"author": "Los Broxas",
@@ -75,6 +75,7 @@
"react-dnd-html5-backend": "^16.0.1",
"react-hook-form": "^7.53.0",
"react-i18next": "^14.1.0",
"react-infinite-scroll-component": "^6.1.0",
"react-loading-skeleton": "^3.4.0",
"react-redux": "^9.1.1",
"react-router-dom": "^6.22.3",
@@ -90,8 +91,7 @@
"winreg": "^1.2.5",
"ws": "^8.18.1",
"yaml": "^2.6.1",
"yup": "^1.5.0",
"zod": "^3.24.1"
"yup": "^1.5.0"
},
"devDependencies": {
"@aws-sdk/client-s3": "^3.705.0",
@@ -116,9 +116,9 @@
"@types/winreg": "^1.2.36",
"@types/ws": "^8.18.1",
"@vitejs/plugin-react": "^4.2.1",
"electron": "^33.4.11",
"electron": "^37.7.1",
"electron-builder": "^26.0.12",
"electron-vite": "^3.0.0",
"electron-vite": "^4.0.1",
"eslint": "^8.56.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.4",
@@ -130,7 +130,7 @@
"sass-embedded": "^1.80.6",
"ts-node": "^10.9.2",
"typescript": "^5.3.3",
"vite": "5.4.20",
"vite": "5.4.21",
"vite-plugin-svgr": "^4.5.0"
},
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"

View File

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

View File

@@ -13,6 +13,7 @@
},
"sidebar": {
"catalogue": "Catalogue",
"library": "Library",
"downloads": "Downloads",
"settings": "Settings",
"my_library": "My library",
@@ -92,8 +93,10 @@
},
"header": {
"search": "Search games",
"search_library": "Search library",
"home": "Home",
"catalogue": "Catalogue",
"library": "Library",
"downloads": "Downloads",
"search_results": "Search results",
"settings": "Settings",
@@ -223,6 +226,7 @@
"show_more": "Show more",
"show_less": "Show less",
"reviews": "Reviews",
"review_played_for": "Played for",
"leave_a_review": "Leave a Review",
"write_review_placeholder": "Share your thoughts about this game...",
"sort_newest": "Newest",
@@ -357,7 +361,14 @@
"delete_review_modal_description": "This action cannot be undone.",
"delete_review_modal_delete_button": "Delete",
"delete_review_modal_cancel_button": "Cancel",
"vote_failed": "Failed to register your vote. Please try again."
"vote_failed": "Failed to register your vote. Please try again.",
"show_original": "Show original",
"show_translation": "Show translation",
"show_original_translated_from": "Show original (translated from {{language}})",
"hide_original": "Hide original",
"review_from_blocked_user": "Review from blocked user",
"show": "Show",
"hide": "Hide"
},
"activation": {
"title": "Activate Hydra",
@@ -424,6 +435,9 @@
"validate_download_source": "Validate",
"remove_download_source": "Remove",
"add_download_source": "Add source",
"adding": "Adding…",
"failed_add_download_source": "Failed to add download source. Please try again.",
"download_source_already_exists": "This download source URL already exists.",
"download_count_zero": "No download options",
"download_count_one": "{{countFormatted}} download option",
"download_count_other": "{{countFormatted}} download options",
@@ -431,9 +445,16 @@
"add_download_source_description": "Insert the URL of the .json file",
"download_source_up_to_date": "Up-to-date",
"download_source_errored": "Errored",
"download_source_pending_matching": "Updating soon",
"download_source_matched": "Up to date",
"download_source_matching": "Updating",
"download_source_failed": "Error",
"download_source_no_information": "No information available",
"sync_download_sources": "Sync sources",
"removed_download_source": "Download source removed",
"removed_download_sources": "Download sources removed",
"removed_all_download_sources": "All download sources removed",
"download_sources_synced_successfully": "All download sources are synced",
"cancel_button_confirmation_delete_all_sources": "No",
"confirm_button_confirmation_delete_all_sources": "Yes, delete everything",
"title_confirmation_delete_all_sources": "Delete all download sources",
@@ -464,6 +485,7 @@
"seed_after_download_complete": "Seed after download complete",
"show_hidden_achievement_description": "Show hidden achievements description before unlocking them",
"account": "Account",
"hydra_cloud": "Hydra Cloud",
"no_users_blocked": "You have no blocked users",
"subscription_active_until": "Your Hydra Cloud is active until {{date}}",
"manage_subscription": "Manage subscription",
@@ -536,8 +558,19 @@
"platinum": "Platinum",
"hidden": "Hidden",
"test_notification": "Test notification",
"achievement_sound_volume": "Achievement sound volume",
"select_achievement_sound": "Select achievement sound",
"change_achievement_sound": "Change achievement sound",
"remove_achievement_sound": "Remove achievement sound",
"preview_sound": "Preview sound",
"select": "Select",
"preview": "Preview",
"remove": "Remove",
"no_sound_file_selected": "No sound file selected",
"notification_preview": "Achievement Notification Preview",
"enable_friend_start_game_notifications": "When a friend starts playing a game"
"enable_friend_start_game_notifications": "When a friend starts playing a game",
"autoplay_trailers_on_game_page": "Automatically start playing trailers on game page",
"hide_to_tray_on_game_start": "Hide Hydra to tray on game startup"
},
"notifications": {
"download_complete": "Download complete",
@@ -672,7 +705,31 @@
"game_added_to_pinned": "Game added to pinned",
"karma": "Karma",
"karma_count": "karma",
"karma_description": "Earned from positive likes on reviews"
"karma_description": "Earned from positive likes on reviews",
"user_reviews": "Reviews",
"delete_review": "Delete Review",
"loading_reviews": "Loading reviews..."
},
"library": {
"library": "Library",
"play": "Play",
"download": "Download",
"downloading": "Downloading",
"game": "game",
"games": "games",
"grid_view": "Grid view",
"compact_view": "Compact view",
"large_view": "Large view",
"no_games_title": "Your library is empty",
"no_games_description": "Add games from the catalogue or download them to get started",
"amount_hours": "{{amount}} hours",
"amount_minutes": "{{amount}} minutes",
"amount_hours_short": "{{amount}}h",
"amount_minutes_short": "{{amount}}m",
"manual_playtime_tooltip": "This playtime has been manually updated",
"all_games": "All Games",
"recently_played": "Recently Played",
"favorites": "Favorites"
},
"achievement": {
"achievement_unlocked": "Achievement unlocked",

View File

@@ -13,6 +13,7 @@
},
"sidebar": {
"catalogue": "Catálogo",
"library": "Librería",
"downloads": "Descargas",
"settings": "Ajustes",
"my_library": "Mi Librería",
@@ -70,6 +71,24 @@
"edit_game_modal_icon_resolution": "Resolución recomendada: 256x256px",
"edit_game_modal_logo_resolution": "Resolución recomendada: 640x360px",
"edit_game_modal_hero_resolution": "Resolución recomendada: 1920x620px",
"cancel": "Cancelar",
"confirm": "Confirmar",
"decky_plugin_installation_error": "Error instalando plugin Decky: {{error}}",
"decky_plugin_installation_failed": "Falló instalar plugin Decky: {{error}}",
"decky_plugin_installed": "Plugin Decky v{{version}} instalanda exitosamente",
"decky_plugin_installed_version": "Plugin Decky (v{{version}})",
"edit_game_modal_drop_hero_image_here": "Soltá la imagen hero acá",
"edit_game_modal_drop_icon_image_here": "Soltá la imagen de ícono hero acá",
"edit_game_modal_drop_logo_image_here": "Soltá la imagen de logo hero acá",
"edit_game_modal_drop_to_replace_hero": "Soltá para reemplazar hero",
"edit_game_modal_drop_to_replace_icon": "Soltá para reemplazar el ícono",
"edit_game_modal_drop_to_replace_logo": "Soltá para reemplazar el logo",
"install_decky_plugin": "Instalar plugin Decky",
"install_decky_plugin_message": "Esto va a descargar e instalar el plugin de Decky Loader para Hydra. Esto quizás requierea permisos elevados, ¿querés continuar?",
"install_decky_plugin_title": "Instarlar el plugin Decky Hydra",
"update_decky_plugin": "Actualizar plugin Decky",
"update_decky_plugin_message": "Una nueva versión del plugin Decky para Hydra está disponible. ¿Querés actualizarlo ahora?",
"update_decky_plugin_title": "Actualizar plugin Decky para Hydra",
"edit_game_modal_assets": "Recursos"
},
"header": {
@@ -285,9 +304,69 @@
"keyshop_price": "Precio de tiendas de terceros",
"historical_retail": "Precio de tiendas",
"historical_keyshop": "Precio de tiendas de terceros",
"add_to_favorites": "Añadir a favoritos",
"be_first_to_review": "¡Sé la primera persona en compartir lo que pensas de este juego!",
"create_shortcut_simple": "Crear atajo",
"delete_review": "Eliminar reseña",
"delete_review_modal_cancel_button": "Cancelar",
"delete_review_modal_delete_button": "Eliminar",
"delete_review_modal_description": "Esta acción no se puede deshacer.",
"delete_review_modal_title": "¿De verdad querés eliminar esta reseña?",
"failed_remove_files": "Error al eliminar los archivos",
"failed_remove_from_library": "Error al eliminar de la librería",
"failed_update_favorites": "Error al actualizar favoritos",
"files_removed_success": "Archivos eliminados correctamente",
"filter_by_source": "Filtrar por fuente",
"game_removed_from_library": "Juego eliminado de la librería",
"hide_original": "Ocultar original",
"leave_a_review": "Crear una reseña",
"load_more_reviews": "Cargar más reseñas",
"loading_more_reviews": "Cargando más reseñas...",
"loading_reviews": "Cargando reseñas...",
"maybe_later": "Tal vez después",
"no_repacks_found": "Sin fuentes encontradas para este juego",
"no_reviews_yet": "Sin reseñas aún",
"review_played_for": "Jugado por",
"properties": "Propiedades",
"rating": "Calificación",
"rating_count": "Calificación",
"rating_negative": "Negativa",
"rating_neutral": "Neutral",
"rating_positive": "Positiva",
"rating_stats": "Calificación",
"rating_very_negative": "Muy Negativa",
"rating_very_positive": "Muy Positiva",
"remove_from_favorites": "Eliminar de favoritos",
"remove_review": "Eliminar reseña",
"review_cannot_be_empty": "El campo de la reseña no puede estar vacío.",
"review_deleted_successfully": "Reseña eliminada exitosamente.",
"review_deletion_failed": "Error al eliminar reseña. Por favor intentá de nuevo.",
"review_submission_failed": "Error al subir reseña. Por favor intentá de nuevo.",
"review_submitted_successfully": "¡Reseña eliminada exitosamente!",
"reviews": "Reseñas",
"show_less": "Ver menos",
"show_more": "Ver más",
"show_original": "Ver original",
"show_original_translated_from": "Ver original (traducido del {{language}})",
"show_translation": "Ver traducción",
"sort_highest_score": "Puntuación más alta",
"sort_lowest_score": "Puntuación más baja",
"sort_most_voted": "Más votads",
"sort_newest": "Más nuevos",
"sort_oldest": "Más viejos",
"submit_review": "Enviar",
"submitting": "Subiendo...",
"vote_failed": "Error al registrar tu voto. Por favor intentá de nuevo.",
"would_you_recommend_this_game": "¿Querés escribir una reseña para este juego?",
"write_review_placeholder": "Compartí tus pensamientos sobre este juego...",
"yes": "Si",
"you_seemed_to_enjoy_this_game": "Parece que has disfrutado de este juego",
"language": "Idioma",
"caption": "Subtítulo",
"audio": "Audio"
"audio": "Audio",
"review_from_blocked_user": "Reseña de usuario bloqueado",
"show": "Mostrar",
"hide": "Ocultar"
},
"activation": {
"title": "Activar Hydra",
@@ -345,7 +424,7 @@
"enable_real_debrid": "Habilitar Real-Debrid",
"real_debrid_description": "Real-Debrid es un descargador que te permite descargar archivos más rápidos, solo límitado por la velocidad de tu internet.",
"debrid_invalid_token": "Token API inválido",
"debrid_api_token_hint": "Podés obtener la el token de tu API <0>acá</0>",
"debrid_api_token_hint": "Podés obtener el token de tu API <0>acá</0>",
"real_debrid_free_account_error": "La cuenta \"{{username}}\" es una cuenta gratis. Por favor suscribíte a Real-Debrid",
"debrid_linked_message": "Cuenta \"{{username}}\" vinculada",
"save_changes": "Guardar cambios",
@@ -357,7 +436,7 @@
"download_count_zero": "Sin opciones de descarga",
"download_count_one": "{{countFormatted}} opción de descarga",
"download_count_other": "{{countFormatted}} opciones de descarga",
"download_source_url": "Descargar fuente URL",
"download_source_url": "Añadir URL de una fuente",
"add_download_source_description": "Introducí la URL del archivo .json",
"download_source_up_to_date": "Actualizado",
"download_source_errored": "Error",
@@ -409,7 +488,7 @@
"subscription_renew_cancelled": "Renovación automática desactivada",
"subscription_renews_on": "Tu suscripción se renueva el {{date}}",
"bill_sent_until": "Tu próxima factura se enviará este día",
"no_themes": "Parece que no tenés ningún tema aún, pero no te preocupés, presiona acá para hacer tu primera obra maestra.",
"no_themes": "Parece que no tenés ningún tema aún, pero no te preocupes, presiona acá para hacer tu primera obra maestra.",
"editor_tab_code": "Código",
"editor_tab_info": "Info",
"editor_tab_save": "Guardar",
@@ -443,7 +522,7 @@
"enable_friend_request_notifications": "Cuando recibís una solicitud de amistad",
"enable_auto_install": "Descargar actualizaciones automáticamente",
"common_redist": "Common redistributables",
"common_redist_description": "Common redistributables son requeridos para algunos juegos. Es recomendable instalarlos para evitar algunos problemas.",
"common_redist_description": "Los common redistributables son requeridos para algunos juegos. Es recomendable instalarlos para evitar algunos problemas.",
"install_common_redist": "Instalar",
"installing_common_redist": "Instalando…",
"show_download_speed_in_megabytes": "Mostrar velocidad de descarga en megabytes por segundo",
@@ -464,8 +543,18 @@
"platinum": "Platino",
"hidden": "Oculto",
"test_notification": "Probar notificación",
"achievement_sound_volume": "Volumen del sonido de logro",
"select_achievement_sound": "Seleccionar sonido de logro",
"select": "Seleccionar",
"preview": "Vista previa",
"remove": "Remover",
"no_sound_file_selected": "No se seleccionó ningún archivo de sonido",
"notification_preview": "Probar notificación de logro",
"enable_friend_start_game_notifications": "Cuando un amigo está jugando un juego"
"debrid": "Debrid",
"debrid_description": "Los servicios Debrid son descargadores premium sin restricciones que te dejan descargar más rápido archivos alojados en servicios de alojamiento siendo que la única limitación es tu velocidad de internet.",
"enable_friend_start_game_notifications": "Cuando un amigo está jugando un juego",
"autoplay_trailers_on_game_page": "Reproducir trailers automáticamente en la página del juego",
"hide_to_tray_on_game_start": "Ocultar Hydra en la bandeja al iniciar un juego"
},
"notifications": {
"download_complete": "Descarga completada",
@@ -492,6 +581,7 @@
"game_card": {
"available_one": "Disponible",
"available_other": "Disponibles",
"calculating": "Calculando",
"no_downloads": "Sin descargas disponibles"
},
"binary_not_found_modal": {
@@ -593,7 +683,17 @@
"error_adding_friend": "No se pudo enviar la solicitud de amistad. Por favor revisá el código",
"friend_code_length_error": "El código de amistad debe tener mínimo 8 caracteres",
"game_removed_from_pinned": "Juego removido de fijados",
"game_added_to_pinned": "Juego añadido a fijados"
"amount_hours_short": "{{amount}}h",
"amount_minutes_short": "{{amount}}m",
"karma": "Karma",
"karma_count": "karma",
"karma_description": "Conseguido por me gustas positivos en reseñas",
"sort_by": "Filtrar por:",
"game_added_to_pinned": "Juego añadido a fijados",
"user_reviews": "Reseñas",
"loading_reviews": "Cargando reseñas...",
"no_reviews": "Sin reseñas aún",
"delete_review": "Eliminar reseña"
},
"achievement": {
"achievement_unlocked": "Logro desbloqueado",
@@ -623,5 +723,26 @@
"hydra_cloud_feature_found": "¡Acabas de descubrir una característica de Hydra Cloud!",
"learn_more": "Descubrir más",
"debrid_description": "Descargas hasta x4 veces más rápidas con Nimbus"
},
"library": {
"library": "Librería",
"play": "Jugar",
"download": "Descargar",
"downloading": "Descargando",
"game": "juego",
"games": "juegos",
"grid_view": "Vista de cuadrícula",
"compact_view": "Vista compacta",
"large_view": "Vista grande",
"no_games_title": "Tu librería está vacía",
"no_games_description": "Agregá juegos del catálogo o descargalos para comenzar",
"amount_hours": "{{amount}} horas",
"amount_minutes": "{{amount}} minutos",
"amount_hours_short": "{{amount}}h",
"amount_minutes_short": "{{amount}}m",
"manual_playtime_tooltip": "Este tiempo de juego ha sido modificado manualmente",
"all_games": "Todos los Juegos",
"recently_played": "Jugados Recientemente",
"favorites": "Favoritos"
}
}

View File

@@ -0,0 +1,708 @@
{
"language_name": "Suomi",
"app": {
"successfully_signed_in": "Kirjautuminen onnistui"
},
"home": {
"surprise_me": "Yllätä minut",
"no_results": "Ei tuloksia",
"start_typing": "Aloitan kirjoittamisen...",
"hot": "Suosittua nyt",
"weekly": "📅 Viikon parhaat pelit",
"achievements": "🏆 Pelit saavutuksilla"
},
"sidebar": {
"catalogue": "Katalogi",
"downloads": "Lataukset",
"settings": "Asetukset",
"my_library": "Kirjasto",
"downloading_metadata": "{{title}} (Metatietojen lataus…)",
"paused": "{{title}} (Keskeytetty)",
"downloading": "{{title}} ({{percentage}} - Lataa…)",
"filter": "Hae",
"home": "Koti",
"queued": "{{title}} (Jonossa)",
"game_has_no_executable": "Pelin käynnistystiedostoa ei ole valittu",
"sign_in": "Kirjaudu sisään",
"friends": "Kaverit",
"need_help": "Tarvitsetko apua?",
"favorites": "Suosikit",
"playable_button_title": "Näytä vain asennetut pelit.",
"add_custom_game_tooltip": "Lisää mukautettu peli",
"show_playable_only_tooltip": "Näytä vain pelattavissa olevat",
"custom_game_modal": "Lisää mukautettu peli",
"custom_game_modal_description": "Lisää mukautettu peli kirjastoon valitsemalla suoritettava tiedosto",
"custom_game_modal_executable_path": "Suoritettavan tiedoston polku",
"custom_game_modal_select_executable": "Valitse suoritettava tiedosto",
"custom_game_modal_title": "Pelin nimi",
"custom_game_modal_enter_title": "Syötä pelin nimi",
"custom_game_modal_browse": "Selaa",
"custom_game_modal_cancel": "Peruuta",
"custom_game_modal_add": "Lisää peli",
"custom_game_modal_adding": "Lisätään peliä...",
"custom_game_modal_success": "Mukautettu peli lisätty onnistuneesti",
"custom_game_modal_failed": "Mukautetun pelin lisääminen epäonnistui",
"custom_game_modal_executable": "Suoritettava tiedosto",
"edit_game_modal": "Mukauta resursseja",
"edit_game_modal_description": "Mukauta pelin resursseja ja tietoja",
"edit_game_modal_title": "Nimi",
"edit_game_modal_enter_title": "Syötä nimi",
"edit_game_modal_image": "Kuva",
"edit_game_modal_select_image": "Valitse kuva",
"edit_game_modal_browse": "Selaa",
"edit_game_modal_image_preview": "Kuvan esikatselu",
"edit_game_modal_icon": "Kuvake",
"edit_game_modal_select_icon": "Valitse kuvake",
"edit_game_modal_icon_preview": "Kuvakkeen esikatselu",
"edit_game_modal_logo": "Logo",
"edit_game_modal_select_logo": "Valitse logo",
"edit_game_modal_logo_preview": "Logon esikatselu",
"edit_game_modal_hero": "Pelin kansikuva",
"edit_game_modal_select_hero": "Valitse pelin kansikuva",
"edit_game_modal_hero_preview": "Kansikuvan esikatselu",
"edit_game_modal_cancel": "Peruuta",
"edit_game_modal_update": "Päivitä",
"edit_game_modal_updating": "Päivitetään...",
"edit_game_modal_fill_required": "Täytä kaikki pakolliset kentät",
"edit_game_modal_success": "Resurssit päivitetty onnistuneesti",
"edit_game_modal_failed": "Resurssien päivitys epäonnistui",
"edit_game_modal_image_filter": "Kuva",
"edit_game_modal_icon_resolution": "Suositeltu resoluutio: 256x256px",
"edit_game_modal_logo_resolution": "Suositeltu resoluutio: 640x360px",
"edit_game_modal_hero_resolution": "Suositeltu resoluutio: 1920x620px",
"edit_game_modal_assets": "Resurssit",
"edit_game_modal_drop_icon_image_here": "Pudota kuvakkeen kuva tähän",
"edit_game_modal_drop_logo_image_here": "Pudota logon kuva tähän",
"edit_game_modal_drop_hero_image_here": "Pudota kansikuvan kuva tähän",
"edit_game_modal_drop_to_replace_icon": "Pudota korvataksesi kuvake",
"edit_game_modal_drop_to_replace_logo": "Pudota korvataksesi logo",
"edit_game_modal_drop_to_replace_hero": "Pudota korvataksesi kansikuva",
"install_decky_plugin": "Asenna Decky-lisäosa",
"update_decky_plugin": "Päivitä Decky-lisäosa",
"decky_plugin_installed_version": "Decky-lisäosa (v{{version}})",
"install_decky_plugin_title": "Asenna Hydra Decky -lisäosa",
"install_decky_plugin_message": "Tämä lataa ja asentaa Hydra-lisäosan Decky Loaderiin. Saattaa vaatia korotetut oikeudet. Jatketaanko?",
"update_decky_plugin_title": "Päivitä Hydra Decky -lisäosa",
"update_decky_plugin_message": "Uusi Hydra Decky -lisäosan versio on saatavilla. Haluatko päivittää sen nyt?",
"decky_plugin_installed": "Decky-lisäosa v{{version}} asennettu onnistuneesti",
"decky_plugin_installation_failed": "Decky-lisäosan asennus epäonnistui: {{error}}",
"decky_plugin_installation_error": "Decky-lisäosan asennusvirhe: {{error}}",
"confirm": "Vahvista",
"cancel": "Peruuta"
},
"header": {
"search": "Hae",
"home": "Koti",
"catalogue": "Katalogi",
"downloads": "Lataukset",
"search_results": "Hakutulokset",
"settings": "Asetukset",
"version_available_install": "Versio {{version}} saatavilla. Asentaaksesi napsauta tästä.",
"version_available_download": "Versio {{version}} saatavilla. Ladataaksesi napsauta tästä."
},
"bottom_panel": {
"no_downloads_in_progress": "Ei meneillään olevia latauksia",
"downloading_metadata": "Ladataan metatietoja {{title}}…",
"downloading": "Ladataan {{title}}… ({{percentage}} valmis) - Lopetus {{eta}} - {{speed}}",
"calculating_eta": "Ladataan {{title}}… ({{percentage}} valmis) - Lasketaan jäljellä olevaa aikaa…",
"checking_files": "Tarkistetaan tiedostoja {{title}}… ({{percentage}} valmis)",
"installing_common_redist": "{{log}}…",
"installation_complete": "Asennus valmis",
"installation_complete_message": "Kirjastot asennettu onnistuneesti"
},
"catalogue": {
"search": "Suodatin…",
"developers": "Kehittäjät",
"genres": "Genret",
"tags": "Tagit",
"publishers": "Julkaisijat",
"download_sources": "Latauslähteet",
"result_count": "{{resultCount}} tulosta",
"filter_count": "{{filterCount}} saatavilla",
"clear_filters": "Tyhjennä {{filterCount}} valittua"
},
"game_details": {
"open_download_options": "Avaa lähteet",
"download_options_zero": "Ei lähteitä",
"download_options_one": "{{count}} lähde",
"download_options_other": "{{count}} lähdettä",
"updated_at": "Päivitetty {{updated_at}}",
"install": "Asenna",
"resume": "Jatka",
"pause": "Keskeytä",
"cancel": "Peruuta",
"remove": "Poista",
"space_left_on_disk": "{{space}} vapaana levyltä",
"eta": "Lopetus {{eta}}",
"calculating_eta": "Lasketaan jäljellä olevaa aikaa…",
"downloading_metadata": "Ladataan metatietoja…",
"filter": "Hae repackeja",
"requirements": "Järjestelmävaatimukset",
"minimum": "Minimi",
"recommended": "Suositeltu",
"paused": "Keskeytetty",
"release_date": "Julkaistu {{date}}",
"publisher": "Julkaisija {{publisher}}",
"hours": "tuntia",
"minutes": "minuuttia",
"amount_hours": "{{amount}} tuntia",
"amount_minutes": "{{amount}} minuuttia",
"accuracy": "tarkkuus {{accuracy}}%",
"add_to_library": "Lisää kirjastoon",
"already_in_library": "Jo kirjastossa",
"remove_from_library": "Poista kirjastosta",
"no_downloads": "Ei saatavilla olevia lähteitä",
"play_time": "Pelattu {{amount}}",
"last_time_played": "Viimeksi pelattu {{period}}",
"not_played_yet": "Et ole vielä pelannut {{title}}",
"next_suggestion": "Seuraava ehdotus",
"play": "Pelaa",
"deleting": "Poistetaan asennustiedostoa…",
"close": "Sulje",
"playing_now": "Käynnissä",
"change": "Vaihda",
"repacks_modal_description": "Valitse repack ladattavaksi",
"select_folder_hint": "Vaihtaaksesi oletuslatauskansiota, avaa <0>Asetukset</0>",
"download_now": "Lataa nyt",
"no_shop_details": "Kuvausta ei saatu",
"download_options": "Lähteet",
"download_path": "Latauspolku",
"previous_screenshot": "Edellinen kuvakaappaus",
"next_screenshot": "Seuraava kuvakaappaus",
"screenshot": "Kuvakaappaus {{number}}",
"open_screenshot": "Avaa kuvakaappaus {{number}}",
"download_settings": "Latausasetukset",
"downloader": "Lataaja",
"select_executable": "Valitse",
"no_executable_selected": "Tiedostoa ei valittu",
"open_folder": "Avaa kansio",
"open_download_location": "Selaa latauskansio",
"create_shortcut": "Luo työpöydän pikakuvake",
"create_shortcut_simple": "Luo pikakuvake",
"clear": "Tyhjennä",
"remove_files": "Poista tiedostot",
"remove_from_library_title": "Oletko varma?",
"remove_from_library_description": "{{game}} poistetaan kirjastostasi.",
"options": "Asetukset",
"properties": "Ominaisuudet",
"executable_section_title": "Tiedosto",
"executable_section_description": "Polku tiedostoon, joka käynnistetään kun painat \"Pelaa\"",
"downloads_section_title": "Lataukset",
"downloads_section_description": "Tarkista päivitysten tai muiden peliversioiden saatavuus",
"danger_zone_section_title": "Vaaravyöhyke",
"danger_zone_section_description": "Voit poistaa tämän pelin kirjastostasi tai Hydrasta ladatut tiedostot",
"download_in_progress": "Lataus käynnissä",
"download_paused": "Lataus keskeytetty",
"last_downloaded_option": "Viimeisin latausvaihtoehto",
"create_steam_shortcut": "Luo Steam-pikakuvake",
"create_shortcut_success": "Pikakuvake luotu",
"you_might_need_to_restart_steam": "Saattaa olla, että sinun on käynnistettävä Steam uudelleen nähdäksesi muutokset",
"create_shortcut_error": "Pikakuvakkeen luonti epäonnistui",
"add_to_favorites": "Lisää suosikkeihin",
"remove_from_favorites": "Poista suosikeista",
"failed_update_favorites": "Suosikkien päivitys epäonnistui",
"game_removed_from_library": "Peli poistettu kirjastosta",
"failed_remove_from_library": "Poistaminen kirjastosta epäonnistui",
"files_removed_success": "Tiedostot poistettu onnistuneesti",
"failed_remove_files": "Tiedostojen poisto epäonnistui",
"nsfw_content_title": "Tämä peli sisältää sopimatonta sisältöä",
"nsfw_content_description": "{{title}} sisältää sisältöä, joka ei välttämättä sovellu kaikenikäisille. \nOletko varma, että haluat jatkaa?",
"allow_nsfw_content": "Jatka",
"refuse_nsfw_content": "Takaisin",
"stats": "Tilastot",
"download_count": "Lataukset",
"player_count": "Aktiiviset pelaajat",
"download_error": "Tämä latausvaihtoehto ei ole saatavilla",
"download": "Lataa",
"executable_path_in_use": "Suoritettavaa tiedostoa käyttää jo \"{{game}}\"",
"warning": "Varoitus:",
"hydra_needs_to_remain_open": "Tämän latauksen aikana Hydran on pysyttävä auki, kunnes se on valmis. Jos Hydra sulkeutuu ennen valmistumista, menetät edistymisen.",
"achievements": "Saavutukset",
"achievements_count": "Saavutukset {{unlockedCount}}/{{achievementsCount}}",
"show_more": "Näytä enemmän",
"show_less": "Näytä vähemmän",
"reviews": "Arvostelut",
"leave_a_review": "Jätä arvostelu",
"write_review_placeholder": "Jaa ajatuksesi tästä pelistä...",
"sort_newest": "Uusimmat ensin",
"no_reviews_yet": "Ei vielä arvosteluja",
"be_first_to_review": "Ole ensimmäinen, joka jakaa ajatuksensa tästä pelistä!",
"sort_oldest": "Vanhimmat ensin",
"sort_highest_score": "Korkein pistemäärä",
"sort_lowest_score": "Matalin pistemäärä",
"sort_most_voted": "Eniten äänestetyt",
"rating": "Arvio",
"rating_stats": "Arvio",
"rating_very_negative": "Erittäin negatiivinen",
"rating_negative": "Negatiivinen",
"rating_neutral": "Neutraali",
"rating_positive": "Positiivinen",
"rating_very_positive": "Erittäin positiivinen",
"submit_review": "Lähetä",
"submitting": "Lähetetään...",
"review_submitted_successfully": "Arvostelu lähetetty onnistuneesti!",
"review_submission_failed": "Arvostelun lähettäminen epäonnistui. Yritä uudelleen.",
"review_cannot_be_empty": "Arvostelun tekstikenttä ei voi olla tyhjä.",
"review_deleted_successfully": "Arvostelu poistettu onnistuneesti.",
"review_deletion_failed": "Arvostelun poisto epäonnistui. Yritä uudelleen.",
"loading_reviews": "Ladataan arvosteluja...",
"loading_more_reviews": "Ladataan lisää arvosteluja...",
"load_more_reviews": "Lataa lisää arvosteluja",
"you_seemed_to_enjoy_this_game": "Näyttää siltä, että nautit tästä pelistä",
"would_you_recommend_this_game": "Haluatko jättää arvion tästä pelistä?",
"yes": "Kyllä",
"maybe_later": "Ehkä myöhemmin",
"rating_count": "Arvio",
"delete_review": "Poista arvostelu",
"remove_review": "Poista arvostelu",
"delete_review_modal_title": "Haluatko varmasti poistaa arvostelusi?",
"delete_review_modal_description": "Tätä toimintoa ei voi peruuttaa.",
"delete_review_modal_delete_button": "Poista",
"delete_review_modal_cancel_button": "Peruuta",
"show_original": "Näytä alkuperäinen",
"show_translation": "Näytä käännös",
"show_original_translated_from": "Näytä alkuperäinen (käännös kielestä {{language}})",
"hide_original": "Piilota alkuperäinen",
"cloud_save": "Pilvitallennus",
"cloud_save_description": "Tallenna edistymisesi pilveen ja jatka pelaamista millä tahansa laitteella",
"backups": "Varmuuskopiot",
"install_backup": "Asenna",
"delete_backup": "Poista",
"create_backup": "Luo uusi varmuuskopio",
"last_backup_date": "Viimeisin varmuuskopio {{date}}",
"no_backup_preview": "Tallennuksia ei löytynyt tälle otsikolle",
"restoring_backup": "Palautetaan varmuuskopiota ({{progress}} valmis)…",
"uploading_backup": "Ladataan varmuuskopiota…",
"no_backups": "Et ole vielä luonut varmuuskopioita tästä pelistä",
"backup_uploaded": "Varmuuskopio ladattu",
"backup_failed": "Varmuuskopiointi epäonnistui",
"backup_deleted": "Varmuuskopio poistettu",
"backup_restored": "Varmuuskopio palautettu",
"see_all_achievements": "Näytä kaikki saavutukset",
"sign_in_to_see_achievements": "Kirjaudu sisään nähdäksesi saavutukset",
"mapping_method_automatic": "Automaattinen",
"mapping_method_manual": "Manuaalinen",
"mapping_method_label": "Kartoitusmenetelmä",
"files_automatically_mapped": "Tiedostot kartoitetu automaattisesti",
"no_backups_created": "Tälle pelille ei ole luotu varmuuskopioita",
"manage_files": "Hallitse tiedostoja",
"loading_save_preview": "Etsitään tallennuksia…",
"wine_prefix": "Wine-etuliite",
"wine_prefix_description": "Tässä pelissä käytettävä Wine-etuliite",
"launch_options": "Käynnistysvalinnat",
"launch_options_description": "Edistyneet käyttäjät voivat tehdä muutoksia käynnistysvalintoihin",
"launch_options_placeholder": "Valintaa ei määritetty",
"no_download_option_info": "Tietoja ei saatavilla",
"backup_deletion_failed": "Varmuuskopion poisto epäonnistui",
"max_number_of_artifacts_reached": "Tämän pelin enimmäismäärä varmuuskopioita saavutettu",
"achievements_not_sync": "Saavutuksesi eivät ole synkronoidut",
"manage_files_description": "Hallitse tallennettavia ja palautettavia tiedostoja",
"select_folder": "Valitse kansio",
"backup_from": "Varmuuskopio {{date}}",
"automatic_backup_from": "Automaattinen varmuuskopio {{date}}",
"enable_automatic_cloud_sync": "Ota automaattinen pilvisynkronointi käyttöön",
"custom_backup_location_set": "Mukautettu varmuuskopiosijainti asetettu",
"no_directory_selected": "Hakemistoa ei valittu",
"no_write_permission": "Ei voi ladata tähän hakemistoon. Napsauta tästä saadaksesi lisätietoja.",
"reset_achievements": "Nollaa saavutukset",
"reset_achievements_description": "Tämä nollaa kaikki saavutukset pelille {{game}}",
"reset_achievements_title": "Oletko varma?",
"reset_achievements_success": "Saavutukset nollattu onnistuneesti",
"reset_achievements_error": "Saavutusten nollaus epäonnistui",
"download_error_gofile_quota_exceeded": "Olet ylittänyt Gofilen kuukausikiintiön. Odota, kunnes kiintiö palautuu.",
"download_error_real_debrid_account_not_authorized": "Real-Debrid -tilisi ei ole valtuutettu suorittamaan uusia latauksia. Tarkista tilin asetukset ja yritä uudelleen.",
"download_error_not_cached_on_real_debrid": "Tämä lataus ei ole saatavilla Real-Debridissä, eikä lataustilan hakeminen Real-Debridistä ole toistaiseksi mahdollista.",
"update_playtime_title": "Päivitä peliaika",
"update_playtime_description": "Päivitä pelin {{game}} peliaika manuaalisesti",
"update_playtime": "Päivitä peliaika",
"update_playtime_success": "Peliaika päivitetty onnistuneesti",
"update_playtime_error": "Peliajan päivitys epäonnistui",
"update_game_playtime": "Päivitä peliaika",
"manual_playtime_warning": "Pelituntisi merkitään manuaalisesti päivitetyiksi. Tätä toimintoa ei voi peruuttaa.",
"manual_playtime_tooltip": "Tämä peliaika on päivitetty manuaalisesti",
"download_error_not_cached_on_torbox": "Tämä lataus ei ole saatavilla TorBoxissa, eikä lataustilan hakeminen TorBoxista ole toistaiseksi mahdollista.",
"download_error_not_cached_on_hydra": "Tämä lataus ei ole saatavilla Nimbuksessa.",
"game_removed_from_favorites": "Peli poistettu suosikeista",
"game_added_to_favorites": "Peli lisätty suosikkeihin",
"game_removed_from_pinned": "Peli poistettu kiinnitetyistä",
"game_added_to_pinned": "Peli lisätty kiinnitettyihin",
"automatically_extract_downloaded_files": "Pura ladatut tiedostot automaattisesti",
"create_start_menu_shortcut": "Luo Käynnistä-valikon pikakuvake",
"invalid_wine_prefix_path": "Virheellinen Wine-etuliitteen polku",
"invalid_wine_prefix_path_description": "Wine-etuliitteen polku on virheellinen. Tarkista polku ja yritä uudelleen.",
"missing_wine_prefix": "Wine-etuliite vaaditaan varmuuskopiointiin Linuxissa",
"artifact_renamed": "Varmuuskopio nimettiin uudelleen onnistuneesti",
"rename_artifact": "Nimeä varmuuskopio uudelleen",
"rename_artifact_description": "Anna varmuuskopiolle kuvaavampi nimi.",
"artifact_name_label": "Varmuuskopion nimi",
"artifact_name_placeholder": "Syötä nimi varmuuskopiolle",
"save_changes": "Tallenna muutokset",
"required_field": "Tämä kenttä on pakollinen",
"max_length_field": "Tämän kentän on oltava alle {{length}} merkkiä",
"freeze_backup": "Kiinnitä, jotta sitä ei ylikirjoiteta automaattisilla varmuuskopioilla",
"unfreeze_backup": "Poista kiinnitys",
"backup_frozen": "Varmuuskopio kiinnitetty",
"backup_unfrozen": "Varmuuskopion kiinnitys poistettu",
"backup_freeze_failed": "Varmuuskopion kiinnitys epäonnistui",
"backup_freeze_failed_description": "Sinun on jätettävä vähintään yksi paikka vapaaksi automaattisille varmuuskopioille",
"edit_game_modal_button": "Muokkaa pelin tietoja",
"game_details": "Pelin tiedot",
"currency_symbol": "€",
"currency_country": "fi",
"prices": "Hinnat",
"no_prices_found": "Hintoja ei löytynyt",
"view_all_prices": "Napsauta nähdäksesi kaikki hinnat",
"retail_price": "Vähittäishinta",
"keyshop_price": "Keyshop-hinta",
"historical_retail": "Historialliset vähittäishinnat",
"historical_keyshop": "Historialliset keyshop-hinnat",
"language": "Kieli",
"caption": "Tekstitys",
"audio": "Ääni",
"filter_by_source": "Suodata lähteen mukaan",
"no_repacks_found": "Tämän pelin lähteitä ei löytynyt"
},
"activation": {
"title": "Aktivoi Hydra",
"installation_id": "Asennustunnus:",
"enter_activation_code": "Syötä aktivointikoodisi",
"message": "Jos et tiedä mistä sitä pyytää, sinun ei pitäisi sitä olla.",
"activate": "Aktivoi",
"loading": "Ladataan…"
},
"downloads": {
"resume": "Jatka",
"pause": "Keskeytä",
"eta": "Lopetus {{eta}}",
"paused": "Keskeytetty",
"verifying": "Tarkistetaan…",
"completed": "Valmis",
"removed": "Ei ladattu",
"cancel": "Peruuta",
"filter": "Hae ladattuja pelejä",
"remove": "Poista",
"downloading_metadata": "Ladataan metatietoja…",
"deleting": "Poistetaan asennustiedostoa…",
"delete": "Poista asennustiedosto",
"delete_modal_title": "Oletko varma?",
"delete_modal_description": "Tämä poistaa kaikki asennustiedostot tietokoneeltasi",
"install": "Asenna",
"download_in_progress": "Käynnissä",
"queued_downloads": "Jonossa olevat lataukset",
"downloads_completed": "Valmiit",
"queued": "Jonossa",
"no_downloads_title": "Täällä on niin tyhjää...",
"no_downloads_description": "Et ole vielä ladannut mitään Hydran kautta, mutta ei ole koskaan liian myöhäistä aloittaa.",
"checking_files": "Tarkistetaan tiedostoja…",
"seeding": "Jakaminen",
"stop_seeding": "Lopeta jakaminen",
"resume_seeding": "Jatka jakamista",
"options": "Hallinnoi",
"extract": "Pura tiedostot",
"extracting": "Puretaan tiedostoja…"
},
"settings": {
"downloads_path": "Latausten polku",
"change": "Vaihda",
"notifications": "Ilmoitukset",
"enable_download_notifications": "Latauksen valmistuessa",
"enable_repack_list_notifications": "Kun uusi repack lisätään",
"real_debrid_api_token_label": "Real-Debrid API-tunnus",
"quit_app_instead_hiding": "Sovellus sulkeutuu system tray -alueelle sijasta",
"launch_with_system": "Käynnistä Hydra järjestelmän mukana",
"general": "Yleiset",
"behavior": "Käyttäytyminen",
"download_sources": "Latauslähteet",
"language": "Kieli",
"api_token": "API-avain",
"enable_real_debrid": "Ota Real-Debrid käyttöön",
"real_debrid_description": "Real-Debrid on rajoittamaton lataaja, jonka avulla voit ladata nopeasti verkossa olevia tiedostoja tai striimata ne välittömästi soittimeen yksityisen verkon kautta, joka kiertää kaikki estot.",
"debrid_invalid_token": "Virheellinen API-avain",
"debrid_api_token_hint": "API-avain voidaan hankkia <0>täältä</0>",
"real_debrid_free_account_error": "Tili \"{{username}}\" - ei ole tilaus. Ota Real-Debrid-tilaus",
"debrid_linked_message": "Tili \"{{username}}\" linkitetty",
"save_changes": "Tallenna muutokset",
"changes_saved": "Muutokset tallennettu onnistuneesti",
"download_sources_description": "Hydra hakee latauslinkit näistä lähteistä. URL-osoitteen on sisällettävä suora linkki .json-tiedostoon, joka sisältää latauslinkit.",
"validate_download_source": "Vahvista",
"remove_download_source": "Poista",
"add_download_source": "Lisää lähde",
"download_count_zero": "Ei latauksia listassa",
"download_count_one": "{{countFormatted}} lataus listassa",
"download_count_other": "{{countFormatted}} latausta listassa",
"download_source_url": "Lähteen URL-osoite",
"add_download_source_description": "Liitä linkki .json-tiedostoon",
"download_source_up_to_date": "Ajan tasalla",
"download_source_errored": "Virhe",
"sync_download_sources": "Päivitä lähteet",
"removed_download_source": "Lähde poistettu",
"removed_download_sources": "Lähteet poistettu",
"cancel_button_confirmation_delete_all_sources": "Ei",
"confirm_button_confirmation_delete_all_sources": "Kyllä, poista kaikki",
"title_confirmation_delete_all_sources": "Poista kaikki lähteet",
"description_confirmation_delete_all_sources": "Poistat kaikki lähteet",
"button_delete_all_sources": "Poista kaikki lähteet",
"added_download_source": "Lähde lisätty",
"download_sources_synced": "Kaikki lähteet päivitetty",
"insert_valid_json_url": "Liitä kelvollinen JSON-tiedoston URL-osoite",
"found_download_option_zero": "Ei latausvaihtoehtoja löytynyt",
"found_download_option_one": "Löytyi {{countFormatted}} latausvaihtoehto",
"found_download_option_other": "Löytyi {{countFormatted}} latausvaihtoehtoa",
"import": "Tuo",
"importing": "Tuodaan...",
"public": "Julkinen",
"private": "Yksityinen",
"friends_only": "Vain kavereille",
"privacy": "Yksityisyys",
"profile_visibility": "Profiilin näkyvyys",
"profile_visibility_description": "Valitse, kuka voi nähdä profiilisi ja kirjastosi",
"required_field": "Tämä kenttä on pakollinen",
"source_already_exists": "Tämä lähde on jo lisätty",
"must_be_valid_url": "Lähteen on oltava kelvollinen URL-osoite",
"blocked_users": "Estetyt käyttäjät",
"user_unblocked": "Käyttäjä estäminen poistettu",
"enable_achievement_notifications": "Kun saavutus avataan",
"launch_minimized": "Käynnistä Hydra pienennettynä",
"disable_nsfw_alert": "Poista sopimattoman sisällön varoitus käytöstä",
"seed_after_download_complete": "Jaa latauksen valmistumisen jälkeen",
"show_hidden_achievement_description": "Näytä piilotettujen saavutusten kuvaukset ennen niiden ansaitsemista",
"account": "Tili",
"no_users_blocked": "Sinulla ei ole estettyjä käyttäjiä",
"subscription_active_until": "Hydra Cloud -tilisi on voimassa {{date}} asti",
"manage_subscription": "Hallinnoi tilausta",
"update_email": "Päivitä sähköposti",
"update_password": "Päivitä salasana",
"current_email": "Nykyinen sähköposti:",
"no_email_account": "Et ole vielä asettanut sähköpostiosoitetta",
"account_data_updated_successfully": "Tilitiedot päivitetty onnistuneesti",
"renew_subscription": "Uusi Hydra Cloud -tilaus",
"subscription_expired_at": "Tilauksesi vanheni {{date}}",
"no_subscription": "Nauti Hydrasta täysin rinnoin",
"become_subscriber": "Tule Hydra Cloud -tilaajaksi",
"subscription_renew_cancelled": "Automaattinen uusinta peruutettu",
"subscription_renews_on": "Tilauksesi uusiutuu {{date}}",
"bill_sent_until": "Seuraava laskusi lähetetään ennen tätä päivää",
"no_themes": "Näyttää siltä, että sinulla ei vielä ole teemoja, mutta älä huoli, napsauta tästä luodaksesi ensimmäisen mestariteoksesi",
"editor_tab_code": "Koodi",
"editor_tab_info": "Tiedot",
"editor_tab_save": "Tallenna",
"web_store": "Verkkokauppa",
"clear_themes": "Tyhjennä",
"create_theme": "Luo",
"create_theme_modal_title": "Luo mukautettu teema",
"create_theme_modal_description": "Luo uusi teema Hydran ulkoasun mukauttamiseksi",
"theme_name": "Nimi",
"insert_theme_name": "Syötä teeman nimi",
"set_theme": "Aseta teema",
"unset_theme": "Poista teema",
"delete_theme": "Poista teema",
"edit_theme": "Muokkaa teemaa",
"delete_all_themes": "Poista kaikki teemat",
"delete_all_themes_description": "Tämä poistaa kaikki mukautetut teemasi",
"delete_theme_description": "Tämä poistaa teeman {{theme}}",
"cancel": "Peruuta",
"appearance": "Ulkoasu",
"debrid": "Debrid",
"debrid_description": "Debrid-palvelut ovat premium-lataajia ilman rajoituksia, joiden avulla voit ladata tiedostoja nopeasti useista tiedostonjakopalveluista, vain internet-yhteytesi nopeuden rajoittamina.",
"enable_torbox": "Ota TorBox käyttöön",
"torbox_description": "TorBox on premium-palvelusi, joka kilpailee jopa parhaimpien markkinoiden palvelimien kanssa.",
"torbox_account_linked": "TorBox-tili linkitetty",
"create_real_debrid_account": "Napsauta tästä, jos sinulla ei vielä ole Real-Debrid-tiliä",
"create_torbox_account": "Napsauta tästä, jos sinulla ei vielä ole TorBox-tiliä",
"real_debrid_account_linked": "Real-Debrid-tili linkitetty",
"name_min_length": "Teeman nimen on oltava vähintään 3 merkkiä",
"import_theme": "Tuo teema",
"import_theme_description": "Tuot teeman {{theme}} teemakaupasta",
"error_importing_theme": "Virhe teemaa tuotaessa",
"theme_imported": "Teema tuotu onnistuneesti",
"enable_friend_request_notifications": "Kun kaveripyyntö vastaanotetaan",
"enable_auto_install": "Lataa päivitykset automaattisesti",
"common_redist": "Kirjastot",
"common_redist_description": "Joidenkin pelien käyttö vaatii kirjastoja. Ongelmien välttämiseksi on suositeltavaa asentaa ne.",
"install_common_redist": "Asenna",
"installing_common_redist": "Asennetaan…",
"show_download_speed_in_megabytes": "Näytä latausnopeus megatavuina sekunnissa",
"extract_files_by_default": "Pura tiedostot oletusarvoisesti latauksen jälkeen",
"enable_steam_achievements": "Ota Steam-saavutusten haku käyttöön",
"achievement_custom_notification_position": "Saavutusilmoitusten sijainti",
"top-left": "Vasemmalla ylhäällä",
"top-center": "Yläkeskellä",
"top-right": "Oikealla ylhäällä",
"bottom-left": "Vasemmalla alhaalla",
"bottom-center": "Alakeskellä",
"bottom-right": "Oikealla alhaalla",
"enable_achievement_custom_notifications": "Ota saavutusilmoitukset käyttöön",
"alignment": "Tasaus",
"variation": "Muunnelma",
"default": "Oletus",
"rare": "Harvinainen",
"platinum": "Platina",
"hidden": "Piilotettu",
"test_notification": "Testi-ilmoitus",
"notification_preview": "Saavutusilmoituksen esikatselu",
"enable_friend_start_game_notifications": "Kun kaveri aloittaa pelin pelaamisen"
},
"notifications": {
"download_complete": "Lataus valmis",
"game_ready_to_install": "{{title}} valmis asennettavaksi",
"repack_list_updated": "Repack-lista päivitetty",
"repack_count_one": "{{count}} repack lisätty",
"repack_count_other": "{{count}} repackia lisätty",
"new_update_available": "Uusi versio {{version}} saatavilla",
"restart_to_install_update": "Käynnistä Hydra uudelleen asentaaksesi päivityksen",
"notification_achievement_unlocked_title": "Saavutus avattu pelille {{game}}",
"notification_achievement_unlocked_body": "{{achievement}} ja muut {{count}} avattiin",
"new_friend_request_description": "{{displayName}} lähetti sinulle kaveripyynnön",
"new_friend_request_title": "Uusi kaveripyyntö",
"extraction_complete": "Purkaminen valmis",
"game_extracted": "{{title}} purettu onnistuneesti",
"friend_started_playing_game": "{{displayName}} aloitti pelin pelaamisen",
"test_achievement_notification_title": "Tämä on testi-ilmoitus",
"test_achievement_notification_description": "Aika siistiä, eikö?"
},
"system_tray": {
"open": "Avaa Hydra",
"quit": "Lopeta"
},
"game_card": {
"available_one": "Saatavilla",
"available_other": "Saatavilla",
"no_downloads": "Ei saatavilla olevia lähteitä",
"calculating": "Lasketaan"
},
"binary_not_found_modal": {
"title": "Ohjelmia ei asennettu",
"description": "Wine tai Lutris ei löytynyt",
"instructions": "Opi oikea tapa asentaa kumpi tahansa Linux-jakelullesi, jotta peli toimii kunnolla"
},
"modal": {
"close": "Sulje"
},
"forms": {
"toggle_password_visibility": "Näytä salasana"
},
"user_profile": {
"amount_hours": "{{amount}} tuntia",
"amount_minutes": "{{amount}} minuuttia",
"amount_hours_short": "{{amount}}t",
"amount_minutes_short": "{{amount}}min",
"last_time_played": "Viimeisin peli {{period}}",
"activity": "Viimeisin toiminta",
"library": "Kirjasto",
"pinned": "Kiinnitetyt",
"achievements_earned": "Ansaittu saavutukset",
"played_recently": "Äskettäin pelatut",
"playtime": "Peliaika",
"total_play_time": "Yhteensä pelattu",
"manual_playtime_tooltip": "Peliaika on päivitetty manuaalisesti",
"no_recent_activity_title": "Hmm... Täällä ei ole mitään",
"no_recent_activity_description": "Et ole pelannut mitään vähään aikaan. On aika muuttaa se!",
"display_name": "Näyttönimi",
"saving": "Tallennetaan",
"save": "Tallenna",
"edit_profile": "Muokkaa profiilia",
"saved_successfully": "Tallennettu onnistuneesti",
"try_again": "Yritä uudelleen",
"sign_out_modal_title": "Oletko varma?",
"cancel": "Peruuta",
"successfully_signed_out": "Kirjauduttu ulos onnistuneesti",
"sign_out": "Kirjaudu ulos",
"playing_for": "Pelattu {{amount}}",
"sign_out_modal_text": "Kirjastosi on linkitetty nykyiseen tiliisi. Kirjautumalla ulos kirjastosi ei ole käytettävissä, eikä edistymistä tallenneta. Kirjaudu ulos?",
"add_friends": "Lisää kavereita",
"add": "Lisää",
"friend_code": "Kaverikoodi",
"see_profile": "Näytä profiili",
"sending": "Lähetetään",
"friend_request_sent": "Kaveripyyntö lähetetty",
"friends": "Kaverit",
"friends_list": "Kaverilista",
"user_not_found": "Käyttäjää ei löytynyt",
"block_user": "Estä käyttäjä",
"add_friend": "Lisää kaveriksi",
"request_sent": "Pyyntö lähetetty",
"request_received": "Pyyntö vastaanotettu",
"accept_request": "Hyväksy pyyntö",
"ignore_request": "Ohita pyyntö",
"cancel_request": "Peruuta pyyntö",
"undo_friendship": "Poista kaveri",
"request_accepted": "Pyyntö hyväksytty",
"user_blocked_successfully": "Käyttäjä estetty onnistuneesti",
"user_block_modal_text": "{{displayName}} estetään",
"blocked_users": "Estetyt käyttäjät",
"unblock": "Poista esto",
"no_friends_added": "Et ole vielä lisännyt yhtään kaveria",
"pending": "Odottaa",
"no_pending_invites": "Sinulla ei ole vasteita odottavia pyyntöjä",
"no_blocked_users": "Et ole estänyt yhtään käyttäjää",
"friend_code_copied": "Kaverikoodi kopioitu",
"undo_friendship_modal_text": "Tämä purkaa kaverisuhteen käyttäjän {{displayName}} kanssa.",
"privacy_hint": "Määrittääksesi kuka voi nähdä tämän, siirry <0>Asetuksiin</0>.",
"locked_profile": "Tämä profiili on yksityinen",
"image_process_failure": "Kuvan käsittely epäonnistui",
"required_field": "Tämä kenttä on pakollinen",
"displayname_min_length": "Näyttönimen on oltava vähintään 3 merkkiä.",
"displayname_max_length": "Näyttönimen on oltava enintään 50 merkkiä.",
"report_profile": "Ilmianna tämä profiili",
"report_reason": "Miksi ilmiannat tämän profiilin?",
"report_description": "Lisätietoja",
"report_description_placeholder": "Lisätietoja",
"report": "Ilmianna",
"report_reason_hate": "Vihapuhe",
"report_reason_sexual_content": "Seksuaalinen sisältö",
"report_reason_violence": "Väkivalta",
"report_reason_spam": "Roskaposti",
"report_reason_other": "Muu",
"profile_reported": "Profiili-ilmoitus lähetetty",
"your_friend_code": "Kaverikoodisi:",
"upload_banner": "Lataa banneri",
"uploading_banner": "Ladataan banneria...",
"background_image_updated": "Taustakuva päivitetty",
"stats": "Tilastot",
"achievements": "Saavutukset",
"games": "Pelit",
"top_percentile": "Top {{percentile}}%",
"ranking_updated_weekly": "Sijoitus päivitetään viikoittain",
"playing": "Pelaamassa {{game}}",
"achievements_unlocked": "Saavutukset avattu",
"earned_points": "Ansaitut pisteet:",
"show_achievements_on_profile": "Näytä saavutuksesi profiilissasi",
"show_points_on_profile": "Näytä ansaitut pisteet profiilissasi",
"error_adding_friend": "Kaveripyynnön lähettäminen epäonnistui. Tarkista kaverikoodi",
"friend_code_length_error": "Kaverikoodin on oltava 8 merkkiä",
"game_removed_from_pinned": "Peli poistettu kiinnitetyistä",
"game_added_to_pinned": "Peli lisätty kiinnitettyihin",
"karma": "Karma",
"karma_count": "karmaa",
"karma_description": "Ansittu positiivisilla arvosteluäänillä"
},
"achievement": {
"achievement_unlocked": "Saavutus avattu",
"user_achievements": "Käyttäjän {{displayName}} saavutukset",
"your_achievements": "Sinun saavutuksesi",
"unlocked_at": "Avattu: {{date}}",
"subscription_needed": "Hydra Cloud -tilaus tarvitaan tämän sisällön katsomiseen",
"new_achievements_unlocked": "{{achievementCount}} uutta saavutusta avattu {{gameCount}} pelistä",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} saavutusta",
"achievements_unlocked_for_game": "{{achievementCount}} uutta saavutusta avattu pelille {{gameTitle}}",
"hidden_achievement_tooltip": "Tämä on piilotettu saavutus",
"achievement_earn_points": "Ansaitse {{points}} pistettä tällä saavutuksella",
"earned_points": "Ansaitut pisteet:",
"available_points": "Saatavilla olevat pisteet:",
"how_to_earn_achievements_points": "Kuinka ansaita saavutuspisteitä?"
},
"hydra_cloud": {
"subscription_tour_title": "Hydra Cloud -tilaus",
"subscribe_now": "Tilaa nyt",
"cloud_saving": "Pilvitallennus",
"cloud_achievements": "Tallenna saavutuksesi pilveen",
"animated_profile_picture": "Animaoidut profiilikuvat",
"premium_support": "Premium-tuki",
"show_and_compare_achievements": "Näytä ja vertaile saavutuksiasi muiden käyttäjien saavutuksiin",
"animated_profile_banner": "Animoitu profiilin banneri",
"hydra_cloud": "Hydra Cloud",
"hydra_cloud_feature_found": "Olet juuri löytänyt Hydra Cloud -toiminnon!",
"learn_more": "Lue lisää",
"debrid_description": "Lataa 4 kertaa nopeammin Nimbuksella"
}
}

View File

@@ -8,7 +8,7 @@
"no_results": "Nincs találat",
"start_typing": "Kereséshez gépelj...",
"hot": "Most felkapott",
"weekly": "📅 A hét felkapott játékai",
"weekly": "📅 A hét felkapottjai",
"achievements": "🏆 Achievement támogatott"
},
"sidebar": {
@@ -26,7 +26,7 @@
"sign_in": "Bejelentkezés",
"friends": "Barátok",
"need_help": "Elakadtál?",
"favorites": "Kedvenc játékok",
"favorites": "Kedvenc Játékaim",
"playable_button_title": "Csak az azonnal játszható játékokat mutasd",
"add_custom_game_tooltip": "Saját játék hozzáadása",
"show_playable_only_tooltip": "Csak játszható játék mutatása",
@@ -224,7 +224,7 @@
"show_less": "Mutass kevesebbet",
"reviews": "Vélemények",
"leave_a_review": "Hagyd itt a véleményed",
"write_review_placeholder": "Oszd meg a gondolataid a játékról...",
"write_review_placeholder": "Oszd meg gondolatod a játékról...",
"sort_newest": "Legújabb",
"no_reviews_yet": "Még nem lett vélemény megosztva",
"be_first_to_review": "Légy az első, aki megossza a véleményét a játékról!",
@@ -252,7 +252,7 @@
"you_seemed_to_enjoy_this_game": "Úgy látszik élvezted ezt a játékot",
"would_you_recommend_this_game": "Szeretnél véleményt írni erről a játékról?",
"yes": "Igen",
"maybe_later": "Talán Később",
"maybe_later": "Talán később",
"cloud_save": "Mentés felhőben",
"cloud_save_description": "Mentsd el az előrehaladásod a felhőben, majd folytasd egy másik eszközön",
"backups": "Biztonsági másolatok",
@@ -356,13 +356,18 @@
"delete_review_modal_title": "Biztos vagy abban hogy törölni szeretnéd a véleményed?",
"delete_review_modal_description": "Ez a lépés nem vonható vissza.",
"delete_review_modal_delete_button": "Törlés",
"delete_review_modal_cancel_button": "Mégse"
"delete_review_modal_cancel_button": "Mégse",
"vote_failed": "A szavazatod nem regisztrálódott. Kérlek próbáld újra.",
"show_original": "Eredeti megjelenítése",
"show_translation": "Fordítás megjelenítése",
"show_original_translated_from": "Eredeti megjelenítése (fordítva: {{language}})",
"hide_original": "Eredeti elrejtése"
},
"activation": {
"title": "Hydra Aktiválása",
"installation_id": "Telepítési Azonosító:",
"enter_activation_code": "Írd be az aktiválási kódod",
"message": "Ha nem tudod hol kérdezz efelől, akkor nem kéne ilyened legyen.",
"message": "Ha nem tudod merre kérdezz efelől, akkor nem kéne ilyened legyen.",
"activate": "Aktiválás",
"loading": "Töltés…"
},
@@ -386,7 +391,7 @@
"download_in_progress": "Folyamatban lévő",
"queued_downloads": "Várakozósoron lévő letöltések",
"downloads_completed": "Befejezett",
"queued": "Várakozási sorban",
"queued": "Várakozásban",
"no_downloads_title": "Oly üres..",
"no_downloads_description": "Még nem töltöttél le semmit a Hydra segítségével, de soha nem késő elkezdeni.",
"checking_files": "Fájlok ellenőrzése…",
@@ -419,20 +424,30 @@
"debrid_linked_message": "Fiók összekapcsolva: \"{{username}}\" ",
"save_changes": "Változtatások mentése",
"changes_saved": "Változtatások sikeresen mentve",
"download_sources_description": "A Hydra lefogja tölteni a letöltési linkeket a forrásokból. Az URL forrásnak közvetlen linknek kell lennie egy .json fájlhoz, ami tartalmazza a linkeket.",
"download_sources_description": "A Hydra lefogja tölteni a letöltési linkeket a forrásokból. Az URL Forrásnak közvetlen linknek kell lennie egy .json fájlhoz, ami tartalmazza a linkeket.",
"validate_download_source": "Érvényesítés",
"remove_download_source": "Eltávolítás",
"add_download_source": "Forrás hozáadása",
"adding": "Hozzáadás…",
"failed_add_download_source": "Letöltési forrás hozzáadása sikertelen. Kérlek próbáld újra.",
"download_source_already_exists": "Ez a letöltési forrás URL már létezik.",
"download_count_zero": "Nincs letöltési opció",
"download_count_one": "{{countFormatted}} letöltési opció",
"download_count_other": "{{countFormatted}} letöltési opció",
"download_source_url": "URL forrás:",
"download_source_url": "URL Forrás:",
"add_download_source_description": "Helyezd be a .json fájl URL-jét",
"download_source_up_to_date": "Naprakész",
"download_source_errored": "Hiba történt",
"download_source_pending_matching": "Frissítés hamarosan",
"download_source_matched": "Naprakész",
"download_source_matching": "Frissítés..",
"download_source_failed": "Hiba",
"download_source_no_information": "Nincs elérhető információ",
"sync_download_sources": "Források szinkronizálása",
"removed_download_source": "Letöltési forrás eltávolítva",
"removed_download_sources": "Letöltési források eltávolítva",
"removed_all_download_sources": "Összes letöltési forrás eltávolítva",
"download_sources_synced_successfully": "Az összes letöltési forrás szinkronizálva",
"cancel_button_confirmation_delete_all_sources": "Nem",
"confirm_button_confirmation_delete_all_sources": "Igen, törölj mindent",
"title_confirmation_delete_all_sources": "Az összes letöltési forrás törlése",
@@ -445,6 +460,7 @@
"found_download_option_one": "{{countFormatted}} Letöltési opció találva",
"found_download_option_other": "{{countFormatted}} Letöltési opciók találva",
"import": "Importálás",
"importing": "Importálás...",
"public": "Publikus",
"private": "Privát",
"friends_only": "Csak barátok",
@@ -462,6 +478,7 @@
"seed_after_download_complete": "Letöltés utáni seedelés",
"show_hidden_achievement_description": "Rejtett achievementek leírásának megjelenítése feloldás előtt",
"account": "Fiók",
"hydra_cloud": "Hydra Cloud",
"no_users_blocked": "Nincsenek letiltott felhasználóid",
"subscription_active_until": "Hydra Cloud előfizetésed aktív, eddig: {{date}}",
"manage_subscription": "Előfizetés kezelése",
@@ -498,14 +515,14 @@
"cancel": "Mégsem",
"appearance": "Megjelenés",
"debrid": "Debrid",
"debrid_description": "A Debrid szolgáltatások prémium szolgáltatások amelyek lehetővé teszik, hogy gyorsan letölts különböző fájltároló szolgáltatásokon tárolt fájlokat, csak az internet sebességed szab határt.",
"debrid_description": "A Debrid szolgáltatások prémium szolgáltatások amelyek lehetővé teszik, hogy gyorsan letölts különböző fájltároló szolgáltatásokon tárolt fájlokat, és csak az internet sebességed szab határt.",
"enable_torbox": "TorBox bekapcsolása",
"torbox_description": "A TorBox egy olyan premium seedbox szolgáltatás, amely még a piacon elérhető legjobb szerverekkel is felveszi a versenyt.",
"torbox_account_linked": "TorBox fiók összekapcsolva",
"create_real_debrid_account": "Kattints ide ha még nincs Real-Debrid fiókod",
"create_torbox_account": "Kattints ide ha még nincs TorBox fiókod",
"real_debrid_account_linked": "Real-Debrid fiók összekapcsolva",
"name_min_length": "A téma neve legalább 3 karakter hosszú legyen",
"name_min_length": "A téma neve legalább 3 karakter hosszú kell legyen",
"import_theme": "Téma importálása",
"import_theme_description": "Ezt a témát fogod importálni a Témaáruház-ból: {{theme}}",
"error_importing_theme": "Hiba lépett fel a téma importálása közben",
@@ -535,7 +552,9 @@
"hidden": "Rejtett",
"test_notification": "Értesítés tesztelése",
"notification_preview": "Achievement Értesítés Előnézete",
"enable_friend_start_game_notifications": "Amikor egy barátod elkezd játszani egy játékot"
"enable_friend_start_game_notifications": "Amikor egy barátod elkezd játszani egy játékot",
"autoplay_trailers_on_game_page": "Játékelőzetes automatikus lejátszása a játék oldalán",
"hide_to_tray_on_game_start": "Hydra elrejtése játék elindításakor a tálcára"
},
"notifications": {
"download_complete": "Letöltés befejezve",
@@ -563,10 +582,10 @@
"available_one": "Elérhető",
"available_other": "Elérhető",
"no_downloads": "Nincs elérhető letöltés",
"calculating": "Feldolgozás"
"calculating": "Számítás alatt.."
},
"binary_not_found_modal": {
"title": "A programok nincsenek telepítve",
"title": "Hiányzó programok",
"description": "Wine vagy Lutris futtatható fájlok nem találhatók a rendszereden",
"instructions": "Ellenőrízd hogy melyiket kell helyesen telepíteni a Linux disztribúciódra, hogy a játék megfelelően fusson"
},
@@ -585,6 +604,7 @@
"activity": "Legutóbbi tevékenység",
"library": "Könyvtár",
"pinned": "Kitűzve",
"sort_by": "Rendezés:",
"achievements_earned": "Elért achievementek",
"played_recently": "Nemrég játszva",
"playtime": "Játszottidő",
@@ -654,7 +674,7 @@
"uploading_banner": "Borítókép feltöltése…",
"background_image_updated": "Borítókép frissítve",
"stats": "Statisztikák",
"achievements": "achievementek",
"achievements": "achievement",
"games": "Játékok",
"top_percentile": "Top {{percentile}}%",
"ranking_updated_weekly": "A rangsor hetente frissül.",
@@ -669,7 +689,7 @@
"game_added_to_pinned": "Játék hozzáadva a kitűzöttekhez",
"karma": "Karma",
"karma_count": "karma",
"karma_description": "Pozitív értékelésekre kapott pontok alapján"
"karma_description": "Pozitív értékelésekkel szerzett pontok"
},
"achievement": {
"achievement_unlocked": "Achievement feloldva",
@@ -678,7 +698,7 @@
"unlocked_at": "Feloldva: {{date}}",
"subscription_needed": "A tartalom megtekintéséhez Hydra Cloud előfizetés szükséges",
"new_achievements_unlocked": "{{achievementCount}} új achievement feloldva {{gameCount}} játékban",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} achievementek",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} achievement",
"achievements_unlocked_for_game": "{{achievementCount}} új achievement feloldva itt: {{gameTitle}}",
"hidden_achievement_tooltip": "Ez egy rejtett achievement",
"achievement_earn_points": "Szerezz be {{points}} pontot ezzel az achievement-el",

View File

@@ -26,7 +26,9 @@ 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 fi from "./fi/translation.json";
import sv from "./sv/translation.json";
import lv from "./lv/translation.json";
export default {
"pt-BR": ptBR,
@@ -49,6 +51,7 @@ export default {
da,
ar,
fa,
fi,
ro,
ca,
bg,
@@ -58,4 +61,5 @@ export default {
et,
uz,
sv,
lv,
};

View File

@@ -0,0 +1,708 @@
{
"language_name": "Latviešu",
"app": {
"successfully_signed_in": "Veiksmīga pieteikšanās"
},
"home": {
"surprise_me": "Pārsteidz mani",
"no_results": "Nekas nav atrasts",
"start_typing": "Sākt rakstīt...",
"hot": "Šobrīd populārs",
"weekly": "📅 Nedēļas labākās spēles",
"achievements": "🏆 Spēles ar sasniegumiem"
},
"sidebar": {
"catalogue": "Katalogs",
"downloads": "Lejupielādes",
"settings": "Iestatījumi",
"my_library": "Bibliotēka",
"downloading_metadata": "{{title}} (Lejupielādē metadatus…)",
"paused": "{{title}} (Apturēts)",
"downloading": "{{title}} ({{percentage}} - Lejupielādē…)",
"filter": "Meklēt",
"home": "Sākums",
"queued": "{{title}} (Rindā)",
"game_has_no_executable": "Spēles palaišanas fails nav izvēlēts",
"sign_in": "Pieteikties",
"friends": "Draugi",
"need_help": "Nepieciešama palīdzība?",
"favorites": "Izlase",
"playable_button_title": "Rādīt tikai instalētās spēles.",
"add_custom_game_tooltip": "Pievienot pielāgotu spēli",
"show_playable_only_tooltip": "Rādīt tikai spēlēšanai pieejamās",
"custom_game_modal": "Pievienot pielāgotu spēli",
"custom_game_modal_description": "Pievienojiet pielāgotu spēli bibliotēkai, izvēloties izpildāmo failu",
"custom_game_modal_executable_path": "Ceļš uz izpildāmo failu",
"custom_game_modal_select_executable": "Izvēlieties izpildāmo failu",
"custom_game_modal_title": "Spēles nosaukums",
"custom_game_modal_enter_title": "Ievadiet spēles nosaukumu",
"custom_game_modal_browse": "Pārlūkot",
"custom_game_modal_cancel": "Atcelt",
"custom_game_modal_add": "Pievienot spēli",
"custom_game_modal_adding": "Pievieno spēli...",
"custom_game_modal_success": "Pielāgota spēle veiksmīgi pievienota",
"custom_game_modal_failed": "Neizdevās pievienot pielāgotu spēli",
"custom_game_modal_executable": "Izpildāmais fails",
"edit_game_modal": "Konfigurēt resursus",
"edit_game_modal_description": "Konfigurējiet spēles resursus un detaļas",
"edit_game_modal_title": "Nosaukums",
"edit_game_modal_enter_title": "Ievadiet nosaukumu",
"edit_game_modal_image": "Attēls",
"edit_game_modal_select_image": "Izvēlieties attēlu",
"edit_game_modal_browse": "Pārlūkot",
"edit_game_modal_image_preview": "Attēla priekšskatījums",
"edit_game_modal_icon": "Ikona",
"edit_game_modal_select_icon": "Izvēlieties ikonu",
"edit_game_modal_icon_preview": "Ikona priekšskatījums",
"edit_game_modal_logo": "Logotips",
"edit_game_modal_select_logo": "Izvēlieties logotipu",
"edit_game_modal_logo_preview": "Logotipa priekšskatījums",
"edit_game_modal_hero": "Vāka attēls",
"edit_game_modal_select_hero": "Izvēlieties spēles vāka attēlu",
"edit_game_modal_hero_preview": "Spēles vāka attēla priekšskatījums",
"edit_game_modal_cancel": "Atcelt",
"edit_game_modal_update": "Atjaunināt",
"edit_game_modal_updating": "Atjaunina...",
"edit_game_modal_fill_required": "Lūdzu, aizpildiet visus obligātos laukus",
"edit_game_modal_success": "Resursi veiksmīgi atjaunināti",
"edit_game_modal_failed": "Neizdevās atjaunināt resursus",
"edit_game_modal_image_filter": "Attēls",
"edit_game_modal_icon_resolution": "Ieteicamā izšķirtspēja: 256x256px",
"edit_game_modal_logo_resolution": "Ieteicamā izšķirtspēja: 640x360px",
"edit_game_modal_hero_resolution": "Ieteicamā izšķirtspēja: 1920x620px",
"edit_game_modal_assets": "Resursi",
"edit_game_modal_drop_icon_image_here": "Ievelciet ikonas attēlu šeit",
"edit_game_modal_drop_logo_image_here": "Ievelciet logotipa attēlu šeit",
"edit_game_modal_drop_hero_image_here": "Ievelciet vāka attēlu šeit",
"edit_game_modal_drop_to_replace_icon": "Ievelciet, lai aizstātu ikonu",
"edit_game_modal_drop_to_replace_logo": "Ievelciet, lai aizstātu logotipu",
"edit_game_modal_drop_to_replace_hero": "Ievelciet, lai aizstātu vāku",
"install_decky_plugin": "Instalēt Decky spraudni",
"update_decky_plugin": "Atjaunināt Decky spraudni",
"decky_plugin_installed_version": "Decky spraudnis (v{{version}})",
"install_decky_plugin_title": "Instalēt Hydra Decky spraudni",
"install_decky_plugin_message": "Tas lejupielādēs un instalēs Hydra spraudni Decky Loader. Var būt nepieciešamas paaugstinātas atļaujas. Turpināt?",
"update_decky_plugin_title": "Atjaunināt Hydra Decky spraudni",
"update_decky_plugin_message": "Ir pieejama jauna Hydra Decky spraudņa versija. Vai vēlaties to atjaunināt tagad?",
"decky_plugin_installed": "Decky spraudnis v{{version}} veiksmīgi instalēts",
"decky_plugin_installation_failed": "Neizdevās instalēt Decky spraudni: {{error}}",
"decky_plugin_installation_error": "Decky spraudņa instalēšanas kļūda: {{error}}",
"confirm": "Apstiprināt",
"cancel": "Atcelt"
},
"header": {
"search": "Meklēt",
"home": "Sākums",
"catalogue": "Katalogs",
"downloads": "Lejupielādes",
"search_results": "Meklēšanas rezultāti",
"settings": "Iestatījumi",
"version_available_install": "Pieejama versija {{version}}. Noklikšķiniet šeit, lai instalētu.",
"version_available_download": "Pieejama versija {{version}}. Noklikšķiniet šeit, lai lejupielādētu."
},
"bottom_panel": {
"no_downloads_in_progress": "Nav aktīvu lejupielāžu",
"downloading_metadata": "Lejupielādē metadatus {{title}}…",
"downloading": "Lejupielādē {{title}}… ({{percentage}} pabeigts) - Beigsies {{eta}} - {{speed}}",
"calculating_eta": "Lejupielādē {{title}}… ({{percentage}} pabeigts) - Aprēķina atlikušo laiku…",
"checking_files": "Pārbauda failus {{title}}… ({{percentage}} pabeigts)",
"installing_common_redist": "{{log}}…",
"installation_complete": "Instalēšana pabeigta",
"installation_complete_message": "Bibliotēkas veiksmīgi instalētas"
},
"catalogue": {
"search": "Filtrs…",
"developers": "Izstrādātāji",
"genres": "Žanri",
"tags": "Atzīmes",
"publishers": "Izdevēji",
"download_sources": "Lejupielādes avoti",
"result_count": "{{resultCount}} rezultāti",
"filter_count": "{{filterCount}} pieejami",
"clear_filters": "Notīrīt {{filterCount}} atlasītos"
},
"game_details": {
"open_download_options": "Atvērt avotus",
"download_options_zero": "Nav avotu",
"download_options_one": "{{count}} avots",
"download_options_other": "{{count}} avoti",
"updated_at": "Atjaunināts {{updated_at}}",
"install": "Instalēt",
"resume": "Atsākt",
"pause": "Apturēt",
"cancel": "Atcelt",
"remove": "Dzēst",
"space_left_on_disk": "{{space}} brīvs diskā",
"eta": "Beigsies {{eta}}",
"calculating_eta": "Aprēķina atlikušo laiku…",
"downloading_metadata": "Lejupielādē metadatus…",
"filter": "Meklēt repakus",
"requirements": "Sistēmas prasības",
"minimum": "Minimālās",
"recommended": "Ieteicamās",
"paused": "Apturēts",
"release_date": "Izdots {{date}}",
"publisher": "Izdevējs {{publisher}}",
"hours": "stundas",
"minutes": "minūtes",
"amount_hours": "{{amount}} stundas",
"amount_minutes": "{{amount}} minūtes",
"accuracy": "precizitāte {{accuracy}}%",
"add_to_library": "Pievienot bibliotēkai",
"already_in_library": "Jau bibliotēkā",
"remove_from_library": "Dzēst no bibliotēkas",
"no_downloads": "Nav pieejamu avotu",
"play_time": "Spēlēts {{amount}}",
"last_time_played": "Pēdējo reizi spēlēts {{period}}",
"not_played_yet": "Jūs vēl neesat spēlējis {{title}}",
"next_suggestion": "Nākamais ieteikums",
"play": "Spēlēt",
"deleting": "Dzēš instalētāju…",
"close": "Aizvērt",
"playing_now": "Palaists",
"change": "Mainīt",
"repacks_modal_description": "Izvēlieties repaku lejupielādei",
"select_folder_hint": "Lai mainītu noklusējuma lejupielāžu mapi, atveriet <0>Iestatījumus</0>",
"download_now": "Lejupielādēt tagad",
"no_shop_details": "Neizdevās iegūt aprakstu",
"download_options": "Avoti",
"download_path": "Ceļš lejupielādēm",
"previous_screenshot": "Iepriekšējais ekrānuzņēmums",
"next_screenshot": "Nākamais ekrānuzņēmums",
"screenshot": "Ekrānuzņēmums {{number}}",
"open_screenshot": "Atvērt ekrānuzņēmumu {{number}}",
"download_settings": "Lejupielādes parametri",
"downloader": "Lejupielādētājs",
"select_executable": "Izvēlēties",
"no_executable_selected": "Fails nav izvēlēts",
"open_folder": "Atvērt mapi",
"open_download_location": "Pārlūkot lejupielādes mapi",
"create_shortcut": "Izveidot īsceļu uz darbvirsmas",
"create_shortcut_simple": "Izveidot īsceļu",
"clear": "Notīrīt",
"remove_files": "Dzēst failus",
"remove_from_library_title": "Vai esat pārliecināts?",
"remove_from_library_description": "{{game}} tiks dzēsta no jūsu bibliotēkas.",
"options": "Iestatījumi",
"properties": "Īpašības",
"executable_section_title": "Fails",
"executable_section_description": "Ceļš uz failu, kas tiks palaists, nospiežot \"Spēlēt\"",
"downloads_section_title": "Lejupielādes",
"downloads_section_description": "Pārbaudīt atjauninājumu vai citu spēles versiju pieejamību",
"danger_zone_section_title": "Bīstamā zona",
"danger_zone_section_description": "Jūs varat dzēst šo spēli no savas bibliotēkas vai failus, kas lejupielādēti no Hydra",
"download_in_progress": "Notiek lejupielāde",
"download_paused": "Lejupielāde apturēta",
"last_downloaded_option": "Pēdējais lejupielādes variants",
"create_steam_shortcut": "Izveidot Steam īsceļu",
"create_shortcut_success": "Īsceļš izveidots",
"you_might_need_to_restart_steam": "Iespējams, jums būs jāpārstartē Steam, lai redzētu izmaiņas",
"create_shortcut_error": "Neizdevās izveidot īsceļu",
"add_to_favorites": "Pievienot izlasei",
"remove_from_favorites": "Dzēst no izlases",
"failed_update_favorites": "Neizdevās atjaunināt izlasi",
"game_removed_from_library": "Spēle dzēsta no bibliotēkas",
"failed_remove_from_library": "Neizdevās dzēst no bibliotēkas",
"files_removed_success": "Faili veiksmīgi dzēsti",
"failed_remove_files": "Neizdevās dzēst failus",
"nsfw_content_title": "Šajā spēlē ir nepiemērots saturs",
"nsfw_content_description": "{{title}} satur saturu, kas var nebūt piemērots visiem vecumiem. \nVai esat pārliecināts, ka vēlaties turpināt?",
"allow_nsfw_content": "Turpināt",
"refuse_nsfw_content": "Atpakaļ",
"stats": "Statistika",
"download_count": "Lejupielādes",
"player_count": "Aktīvie spēlētāji",
"download_error": "Šis lejupielādes variants nav pieejams",
"download": "Lejupielādēt",
"executable_path_in_use": "Izpildāmais fails jau tiek izmantots \"{{game}}\"",
"warning": "Uzmanību:",
"hydra_needs_to_remain_open": "Lai veiktu šo lejupielādi, Hydra jāpaliek atvērtai līdz beigām. Ja Hydra aizvērsies pirms pabeigšanas, jūs zaudēsiet progresu.",
"achievements": "Sasniegumi",
"achievements_count": "Sasniegumi {{unlockedCount}}/{{achievementsCount}}",
"show_more": "Rādīt vairāk",
"show_less": "Rādīt mazāk",
"reviews": "Atsauksmes",
"leave_a_review": "Atstāt atsauksmi",
"write_review_placeholder": "Dalieties savās domās par šo spēli...",
"sort_newest": "Vispirms jaunākās",
"no_reviews_yet": "Pagaidām nav atsauksmju",
"be_first_to_review": "Esiet pirmais, kurš dalīsies savās domās par šo spēli!",
"sort_oldest": "Vispirms vecākās",
"sort_highest_score": "Augstākais vērtējums",
"sort_lowest_score": "Zemākais vērtējums",
"sort_most_voted": "Vispopulārākās",
"rating": "Vērtējums",
"rating_stats": "Vērtējums",
"rating_very_negative": "Ļoti negatīvs",
"rating_negative": "Negatīvs",
"rating_neutral": "Neitrāls",
"rating_positive": "Pozitīvs",
"rating_very_positive": "Ļoti pozitīvs",
"submit_review": "Iesniegt",
"submitting": "Iesniegšana...",
"review_submitted_successfully": "Atsauksme veiksmīgi iesniegta!",
"review_submission_failed": "Neizdevās iesniegt atsauksmi. Lūdzu, mēģiniet vēlreiz.",
"review_cannot_be_empty": "Atsauksmes teksta lauks nevar būt tukšs.",
"review_deleted_successfully": "Atsauksme veiksmīgi dzēsta.",
"review_deletion_failed": "Neizdevās dzēst atsauksmi. Lūdzu, mēģiniet vēlreiz.",
"loading_reviews": "Ielādē atsauksmes...",
"loading_more_reviews": "Ielādē papildu atsauksmes...",
"load_more_reviews": "Ielādēt vairāk atsauksmju",
"you_seemed_to_enjoy_this_game": "Šķiet, jums patika šī spēle",
"would_you_recommend_this_game": "Vai vēlaties atstāt atsauksmi par šo spēli?",
"yes": "Jā",
"maybe_later": "Varbūt vēlāk",
"rating_count": "Vērtējums",
"delete_review": "Dzēst atsauksmi",
"remove_review": "Dzēst atsauksmi",
"delete_review_modal_title": "Vai esat pārliecināts, ka vēlaties dzēst savu atsauksmi?",
"delete_review_modal_description": "Šo darbību nevar atsaukt.",
"delete_review_modal_delete_button": "Dzēst",
"delete_review_modal_cancel_button": "Atcelt",
"show_original": "Rādīt oriģinālu",
"show_translation": "Rādīt tulkojumu",
"show_original_translated_from": "Rādīt oriģinālu (tulkot no {{language}})",
"hide_original": "Slēpt oriģinālu",
"cloud_save": "Mākoņglabāšana",
"cloud_save_description": "Glabājiet savu progresu mākonī un turpiniet spēlēt jebkurā ierīcē",
"backups": "Rezerves kopijas",
"install_backup": "Instalēt",
"delete_backup": "Dzēst",
"create_backup": "Izveidot jaunu rezerves kopiju",
"last_backup_date": "Pēdējā rezerves kopija no {{date}}",
"no_backup_preview": "Šim nosaukumam saglabājumi nav atrasti",
"restoring_backup": "Atjauno rezerves kopiju ({{progress}} pabeigts)…",
"uploading_backup": "Augšupielādē rezerves kopiju…",
"no_backups": "Jūs vēl neesat izveidojis rezerves kopijas šai spēlei",
"backup_uploaded": "Rezerves kopija augšupielādēta",
"backup_failed": "Rezerves kopēšanas kļūda",
"backup_deleted": "Rezerves kopija dzēsta",
"backup_restored": "Rezerves kopija atjaunota",
"see_all_achievements": "Skatīt visus sasniegumus",
"sign_in_to_see_achievements": "Piesakieties, lai redzētu sasniegumus",
"mapping_method_automatic": "Automātiska",
"mapping_method_manual": "Manuāla",
"mapping_method_label": "Kartēšanas metode",
"files_automatically_mapped": "Faili automātiski kartēti",
"no_backups_created": "Šai spēlei nav izveidotas rezerves kopijas",
"manage_files": "Failu pārvaldība",
"loading_save_preview": "Meklē saglabājumus…",
"wine_prefix": "Wine prefikss",
"wine_prefix_description": "Wine prefikss, ko izmanto šīs spēles palaišanai",
"launch_options": "Palaišanas parametri",
"launch_options_description": "Pieredzējuši lietotāji var veikt izmaiņas palaišanas parametros",
"launch_options_placeholder": "Parametrs nav norādīts",
"no_download_option_info": "Informācija nav pieejama",
"backup_deletion_failed": "Neizdevās dzēst rezerves kopiju",
"max_number_of_artifacts_reached": "Sasniegts maksimālais rezerves kopiju skaits šai spēlei",
"achievements_not_sync": "Jūsu sasniegumi nav sinhronizēti",
"manage_files_description": "Pārvaldiet failus, kas tiks saglabāti un atjaunoti",
"select_folder": "Izvēlēties mapi",
"backup_from": "Rezerves kopija no {{date}}",
"automatic_backup_from": "Automātiska rezerves kopija no {{date}}",
"enable_automatic_cloud_sync": "Iespējot automātisku sinhronizāciju mākonī",
"custom_backup_location_set": "Iestatīta pielāgota rezerves kopēšanas vieta",
"no_directory_selected": "Nav izvēlēts katalogs",
"no_write_permission": "Nevar augšupielādēt šajā direktorijā. Noklikšķiniet šeit, lai uzzinātu vairāk.",
"reset_achievements": "Atiestatīt sasniegumus",
"reset_achievements_description": "Tas atiestatīs visus sasniegumus {{game}} spēlei",
"reset_achievements_title": "Vai esat pārliecināts?",
"reset_achievements_success": "Sasniegumi veiksmīgi atiestatīti",
"reset_achievements_error": "Neizdevās atiestatīt sasniegumus",
"download_error_gofile_quota_exceeded": "Jūs pārsniedzāt Gofile mēneša kvotu. Lūdzu, uzgaidiet, kamēr kvota tiks atjaunota.",
"download_error_real_debrid_account_not_authorized": "Jūsu Real-Debrid konts nav autorizēts jaunām lejupielādēm. Lūdzu, pārbaudiet konta iestatījumus un mēģiniet vēlreiz.",
"download_error_not_cached_on_real_debrid": "Šī lejupielāde nav pieejama Real-Debrid, un Real-Debrid lejupielādes statusu pagaidām nav iespējams iegūt.",
"update_playtime_title": "Atjaunināt spēles laiku",
"update_playtime_description": "Manuāli atjauniniet spēles laiku {{game}} spēlei",
"update_playtime": "Atjaunināt spēles laiku",
"update_playtime_success": "Spēles laiks veiksmīgi atjaunināts",
"update_playtime_error": "Neizdevās atjaunināt spēles laiku",
"update_game_playtime": "Atjaunināt spēles laiku",
"manual_playtime_warning": "Jūsu stundas tiks atzīmētas kā manuāli atjauninātas. Šo darbību nevar atcelt.",
"manual_playtime_tooltip": "Šis spēles laiks tika atjaunināts manuāli",
"download_error_not_cached_on_torbox": "Šī lejupielāde nav pieejama TorBox, un TorBox lejupielādes statusu pagaidām nav iespējams iegūt.",
"download_error_not_cached_on_hydra": "Šī lejupielāde nav pieejama Nimbus.",
"game_removed_from_favorites": "Spēle dzēsta no izlases",
"game_added_to_favorites": "Spēle pievienota izlasei",
"game_removed_from_pinned": "Spēle dzēsta no piespraustajiem",
"game_added_to_pinned": "Spēle pievienota piespraustajiem",
"automatically_extract_downloaded_files": "Automātiska lejupielādēto failu izpakošana",
"create_start_menu_shortcut": "Izveidot saīsni sākuma izvēlnē",
"invalid_wine_prefix_path": "Nederīgs Wine prefiksa ceļš",
"invalid_wine_prefix_path_description": "Wine prefiksa ceļš nav derīgs. Lūdzu, pārbaudiet ceļu un mēģiniet vēlreiz.",
"missing_wine_prefix": "Wine prefikss ir nepieciešams, lai izveidotu rezerves kopiju Linux vidē",
"artifact_renamed": "Rezerves kopija veiksmīgi pārsaukta",
"rename_artifact": "Pārsaukt rezerves kopiju",
"rename_artifact_description": "Pārsauciet rezerves kopiju, piešķirot tai aprakstošāku nosaukumu.",
"artifact_name_label": "Rezerves kopijas nosaukums",
"artifact_name_placeholder": "Ievadiet nosaukumu rezerves kopijai",
"save_changes": "Saglabāt izmaiņas",
"required_field": "Šis lauks ir obligāts",
"max_length_field": "Šim laukam jābūt mazāk par {{length}} simboliem",
"freeze_backup": "Piespraust, lai to nepārrakstītu automātiskās rezerves kopijas",
"unfreeze_backup": "Atspraust",
"backup_frozen": "Rezerves kopija piesprausta",
"backup_unfrozen": "Rezerves kopija atsprausta",
"backup_freeze_failed": "Neizdevās piespraust rezerves kopiju",
"backup_freeze_failed_description": "Jums jāatstāj vismaz viens brīvs slots automātiskajām rezerves kopijām",
"edit_game_modal_button": "Rediģēt spēles detaļas",
"game_details": "Spēles detaļas",
"currency_symbol": "₽",
"currency_country": "ru",
"prices": "Cenas",
"no_prices_found": "Cenas nav atrastas",
"view_all_prices": "Noklikšķiniet, lai skatītu visas cenas",
"retail_price": "Mazumtirdzniecības cena",
"keyshop_price": "Atslēgu veikala cena",
"historical_retail": "Vēsturiskās mazumtirdzniecības cenas",
"historical_keyshop": "Vēsturiskās atslēgu veikalu cenas",
"language": "Valoda",
"caption": "Subtitri",
"audio": "Audio",
"filter_by_source": "Filtrēt pēc avota",
"no_repacks_found": "Avoti šai spēlei nav atrasti"
},
"activation": {
"title": "Aktivizēt Hydra",
"installation_id": "Instalācijas ID:",
"enter_activation_code": "Ievadiet savu aktivizācijas kodu",
"message": "Ja nezināt, kur to pieprasīt, jums to nevajadzētu būt.",
"activate": "Aktivizēt",
"loading": "Ielādēšana…"
},
"downloads": {
"resume": "Atsākt",
"pause": "Apturēt",
"eta": "Beigsies {{eta}}",
"paused": "Apturēts",
"verifying": "Pārbauda…",
"completed": "Pabeigts",
"removed": "Nav lejupielādēts",
"cancel": "Atcelt",
"filter": "Meklēt lejupielādētās spēles",
"remove": "Dzēst",
"downloading_metadata": "Lejupielādē metadatus…",
"deleting": "Dzēš instalētāju…",
"delete": "Dzēst instalētāju",
"delete_modal_title": "Vai esat pārliecināts?",
"delete_modal_description": "Tas dzēsīs visus instalētājus no jūsu datora",
"install": "Instalēt",
"download_in_progress": "Procesā",
"queued_downloads": "Lejupielādes rindā",
"downloads_completed": "Pabeigts",
"queued": "Rindā",
"no_downloads_title": "Šeit ir tik tukšs...",
"no_downloads_description": "Jūs vēl neko neesat lejupielādējis, izmantojot Hydra, bet nekad nav par vēlu sākt.",
"checking_files": "Pārbauda failus…",
"seeding": "Sēdēšana",
"stop_seeding": "Apturēt sēdēšanu",
"resume_seeding": "Turpināt sēdēšanu",
"options": "Pārvaldīt",
"extract": "Izpakot failus",
"extracting": "Izpako failus…"
},
"settings": {
"downloads_path": "Lejupielāžu ceļš",
"change": "Mainīt",
"notifications": "Paziņojumi",
"enable_download_notifications": "Pēc lejupielādes pabeigšanas",
"enable_repack_list_notifications": "Pievienojot jaunu repaku",
"real_debrid_api_token_label": "Real-Debrid API-atslēga",
"quit_app_instead_hiding": "Aizvērt lietotni, nevis minimizēt uz paplātes",
"launch_with_system": "Palaist Hydra kopā ar sistēmu",
"general": "Vispārīgi",
"behavior": "Uzvedība",
"download_sources": "Lejupielādes avoti",
"language": "Valoda",
"api_token": "API atslēga",
"enable_real_debrid": "Iespējot Real-Debrid",
"real_debrid_description": "Real-Debrid ir neierobežots lejupielādētājs, kas ļauj ātri lejupielādēt failus, kas izvietoti internetā, vai uzreiz pārsūtīt tos uz atskaņotāju, izmantojot privātu tīklu, kas ļauj apiet jebkādus bloķējumus.",
"debrid_invalid_token": "Nederīga API atslēga",
"debrid_api_token_hint": "API atslēgu var iegūt <0>šeit</0>",
"real_debrid_free_account_error": "Kontam \"{{username}}\" nav abonementa. Lūdzu, iegādājieties Real-Debrid abonementu",
"debrid_linked_message": "Piesaistīts konts \"{{username}}\"",
"save_changes": "Saglabāt izmaiņas",
"changes_saved": "Izmaiņas veiksmīgi saglabātas",
"download_sources_description": "Hydra saņems lejupielādes saites no šiem avotiem. URL jāietver tieša saite uz .json failu ar lejupielādes saitēm.",
"validate_download_source": "Pārbaudīt",
"remove_download_source": "Dzēst",
"add_download_source": "Pievienot avotu",
"download_count_zero": "Sarakstā nav lejupielāžu",
"download_count_one": "{{countFormatted}} lejupielāde sarakstā",
"download_count_other": "{{countFormatted}} lejupielādes sarakstā",
"download_source_url": "Saite uz avotu",
"add_download_source_description": "Ievietojiet saiti uz .json failu",
"download_source_up_to_date": "Atjaunināts",
"download_source_errored": "Kļūda",
"sync_download_sources": "Atjaunināt avotus",
"removed_download_source": "Avots dzēsts",
"removed_download_sources": "Avoti dzēsti",
"cancel_button_confirmation_delete_all_sources": "Nē",
"confirm_button_confirmation_delete_all_sources": "Jā, dzēst visus",
"title_confirmation_delete_all_sources": "Dzēst visus avotus",
"description_confirmation_delete_all_sources": "Jūs dzēsīsiet visus avotus",
"button_delete_all_sources": "Dzēst visus avotus",
"added_download_source": "Avots pievienots",
"download_sources_synced": "Visi avoti atjaunināti",
"insert_valid_json_url": "Ievietojiet derīgu JSON faila URL",
"found_download_option_zero": "Nav atrasts lejupielādes variantu",
"found_download_option_one": "Atrasts {{countFormatted}} lejupielādes variants",
"found_download_option_other": "Atrasti {{countFormatted}} lejupielādes varianti",
"import": "Importēt",
"importing": "Importē...",
"public": "Publisks",
"private": "Privāts",
"friends_only": "Tikai draugiem",
"privacy": "Konfidencialitāte",
"profile_visibility": "Profila redzamība",
"profile_visibility_description": "Izvēlieties, kurš var redzēt jūsu profilu un bibliotēku",
"required_field": "Šis lauks ir obligāts",
"source_already_exists": "Šis avots jau ir pievienots",
"must_be_valid_url": "Avotam jābūt pareizam URL",
"blocked_users": "Bloķētie lietotāji",
"user_unblocked": "Lietotājs atbloķēts",
"enable_achievement_notifications": "Kad sasniegums ir atbloķēts",
"launch_minimized": "Palaist Hydra minimizētā veidā",
"disable_nsfw_alert": "Atspējot brīdinājumu par neķītru saturu",
"seed_after_download_complete": "Sēdēt pēc lejupielādes pabeigšanas",
"show_hidden_achievement_description": "Rādīt slēpto sasniegumu aprakstu pirms to iegūšanas",
"account": "Konts",
"no_users_blocked": "Jums nav bloķētu lietotāju",
"subscription_active_until": "Jūsu Hydra Cloud abonements ir aktīvs līdz {{date}}",
"manage_subscription": "Pārvaldīt abonementu",
"update_email": "Atjaunināt e-pastu",
"update_password": "Atjaunināt paroli",
"current_email": "Pašreizējais e-pasts:",
"no_email_account": "Jūs vēl neesat iestatījis e-pastu",
"account_data_updated_successfully": "Konta dati veiksmīgi atjaunināti",
"renew_subscription": "Atjaunot Hydra Cloud abonementu",
"subscription_expired_at": "Jūsu abonementa termiņš beidzās {{date}}",
"no_subscription": "Izbaudiet Hydra pilnībā",
"become_subscriber": "Kļūstiet par Hydra Cloud īpašnieku",
"subscription_renew_cancelled": "Automātiskā atjaunošana atspējota",
"subscription_renews_on": "Jūsu abonements tiek atjaunots {{date}}",
"bill_sent_until": "Jūsu nākamais rēķins tiks nosūtīts līdz šai dienai",
"no_themes": "Šķiet, ka jums vēl nav tēmu, bet neuztraucieties, noklikšķiniet šeit, lai izveidotu savu pirmo šedevru",
"editor_tab_code": "Kods",
"editor_tab_info": "Informācija",
"editor_tab_save": "Saglabāt",
"web_store": "Tīmekļa veikals",
"clear_themes": "Notīrīt",
"create_theme": "Izveidot",
"create_theme_modal_title": "Izveidot pielāgotu tēmu",
"create_theme_modal_description": "Izveidot jaunu tēmu, lai pielāgotu Hydra izskatu",
"theme_name": "Nosaukums",
"insert_theme_name": "Ievietot tēmas nosaukumu",
"set_theme": "Iestatīt tēmu",
"unset_theme": "Noņemt tēmu",
"delete_theme": "Dzēst tēmu",
"edit_theme": "Rediģēt tēmu",
"delete_all_themes": "Dzēst visas tēmas",
"delete_all_themes_description": "Tas dzēsīs visas jūsu pielāgotās tēmas",
"delete_theme_description": "Tas dzēsīs tēmu {{theme}}",
"cancel": "Atcelt",
"appearance": "Izskats",
"debrid": "Debrid",
"debrid_description": "Debrid servisi ir premium lejupielādētāji bez ierobežojumiem, kas ļauj ātri lejupielādēt failus no dažādiem failu apmaiņas servisiem, ierobežojoties tikai ar jūsu interneta ātrumu.",
"enable_torbox": "Iespējot TorBox",
"torbox_description": "TorBox ir jūsu premium serviss, kas konkurē pat ar labākajiem serveriem tirgū.",
"torbox_account_linked": "TorBox konts piesaistīts",
"create_real_debrid_account": "Noklikšķiniet šeit, ja jums vēl nav Real-Debrid konta",
"create_torbox_account": "Noklikšķiniet šeit, ja jums vēl nav TorBox konta",
"real_debrid_account_linked": "Real-Debrid konts piesaistīts",
"name_min_length": "Tēmas nosaukumam jābūt vismaz 3 simbolus garam",
"import_theme": "Importēt tēmu",
"import_theme_description": "Jūs importēsiet {{theme}} no tēmu veikala",
"error_importing_theme": "Kļūda importējot tēmu",
"theme_imported": "Tēma veiksmīgi importēta",
"enable_friend_request_notifications": "Saņemot draudzības pieprasījumu",
"enable_auto_install": "Automātiski lejupielādēt atjauninājumus",
"common_redist": "Bibliotēkas",
"common_redist_description": "Dažu spēļu palaišanai ir nepieciešamas bibliotēkas. Lai izvairītos no problēmām, ieteicams tās instalēt.",
"install_common_redist": "Instalēt",
"installing_common_redist": "Instalēšana…",
"show_download_speed_in_megabytes": "Rādīt lejupielādes ātrumu megabaitos sekundē",
"extract_files_by_default": "Izpakot failus pēc noklusējuma pēc lejupielādes",
"enable_steam_achievements": "Iespējot Steam sasniegumu meklēšanu",
"achievement_custom_notification_position": "Sasniegumu paziņojumu pozīcija",
"top-left": "Augšējais kreisais stūris",
"top-center": "Augšējais centrs",
"top-right": "Augšējais labais stūris",
"bottom-left": "Apakšējais kreisais stūris",
"bottom-center": "Apakšējais centrs",
"bottom-right": "Apakšējais labais stūris",
"enable_achievement_custom_notifications": "Iespējot sasniegumu paziņojumus",
"alignment": "Izlīdzināšana",
"variation": "Variācija",
"default": "Pēc noklusējuma",
"rare": "Retais",
"platinum": "Platīna",
"hidden": "Slēpts",
"test_notification": "Testa paziņojums",
"notification_preview": "Sasnieguma paziņojuma priekšskatījums",
"enable_friend_start_game_notifications": "Kad draugs sāk spēlēt spēli"
},
"notifications": {
"download_complete": "Lejupielāde pabeigta",
"game_ready_to_install": "{{title}} ir gatava instalēšanai",
"repack_list_updated": "Repaku saraksts atjaunināts",
"repack_count_one": "{{count}} repaks pievienots",
"repack_count_other": "{{count}} repaki pievienoti",
"new_update_available": "Pieejama jauna versija {{version}}",
"restart_to_install_update": "Pārstartējiet Hydra, lai instalētu atjauninājumu",
"notification_achievement_unlocked_title": "Sasniegums atbloķēts spēlei {{game}}",
"notification_achievement_unlocked_body": "tika atbloķēti {{achievement}} un citi {{count}}",
"new_friend_request_description": "{{displayName}} nosūtīja jums draudzības pieprasījumu",
"new_friend_request_title": "Jauns draudzības pieprasījums",
"extraction_complete": "Izpakošana pabeigta",
"game_extracted": "{{title}} veiksmīgi izpakots",
"friend_started_playing_game": "{{displayName}} sāka spēlēt spēli",
"test_achievement_notification_title": "Šis ir testa paziņojums",
"test_achievement_notification_description": "Diezgan forši, vai ne?"
},
"system_tray": {
"open": "Atvērt Hydra",
"quit": "Iziet"
},
"game_card": {
"available_one": "Pieejams",
"available_other": "Pieejams",
"no_downloads": "Nav pieejamu avotu",
"calculating": "Aprēķina"
},
"binary_not_found_modal": {
"title": "Programmas nav instalētas",
"description": "Wine vai Lutris nav atrasti",
"instructions": "Uzziniet pareizo veidu, kā instalēt kādu no tiem jūsu Linux distribūcijā, lai spēle varētu normāli darboties"
},
"modal": {
"close": "Aizvērt"
},
"forms": {
"toggle_password_visibility": "Rādīt paroli"
},
"user_profile": {
"amount_hours": "{{amount}} stundas",
"amount_minutes": "{{amount}} minūtes",
"amount_hours_short": "{{amount}}h",
"amount_minutes_short": "{{amount}}m",
"last_time_played": "Pēdējā spēle {{period}}",
"activity": "Nesenā aktivitāte",
"library": "Bibliotēka",
"pinned": "Piespraustās",
"achievements_earned": "Nopelnītie sasniegumi",
"played_recently": "Nesen spēlētās",
"playtime": "Spēles laiks",
"total_play_time": "Kopējais spēles laiks",
"manual_playtime_tooltip": "Spēles laiks tika atjaunināts manuāli",
"no_recent_activity_title": "Hmmmm... Šeit nav nekā",
"no_recent_activity_description": "Jūs sen neesat neko spēlējis. Ir laiks to mainīt!",
"display_name": "Parādāmais vārds",
"saving": "Saglabāšana",
"save": "Saglabāt",
"edit_profile": "Rediģēt profilu",
"saved_successfully": "Veiksmīgi saglabāts",
"try_again": "Lūdzu, mēģiniet vēlreiz",
"sign_out_modal_title": "Vai esat pārliecināts?",
"cancel": "Atcelt",
"successfully_signed_out": "Veiksmīga izrakstīšanās no konta",
"sign_out": "Iziet",
"playing_for": "Spēlēts {{amount}}",
"sign_out_modal_text": "Jūsu bibliotēka ir saistīta ar pašreizējo kontu. Izejot no sistēmas, jūsu bibliotēka kļūs nepieejama, un progress netiks saglabāts. Iziet?",
"add_friends": "Pievienot draugus",
"add": "Pievienot",
"friend_code": "Drauga kods",
"see_profile": "Skatīt profilu",
"sending": "Sūtīšana",
"friend_request_sent": "Draudzības pieprasījums nosūtīts",
"friends": "Draugi",
"friends_list": "Draugu saraksts",
"user_not_found": "Lietotājs nav atrasts",
"block_user": "Bloķēt lietotāju",
"add_friend": "Pievienot draugu",
"request_sent": "Pieprasījums nosūtīts",
"request_received": "Pieprasījums saņemts",
"accept_request": "Pieņemt pieprasījumu",
"ignore_request": "Ignorēt pieprasījumu",
"cancel_request": "Atcelt pieprasījumu",
"undo_friendship": "Dzēst draugu",
"request_accepted": "Pieprasījums pieņemts",
"user_blocked_successfully": "Lietotājs veiksmīgi bloķēts",
"user_block_modal_text": "{{displayName}} tiks bloķēts",
"blocked_users": "Bloķētie lietotāji",
"unblock": "Atbloķēt",
"no_friends_added": "Jūs vēl neesat pievienojis nevienu draugu",
"pending": "Gaida",
"no_pending_invites": "Jums nav pieprasījumu, kas gaida atbildi",
"no_blocked_users": "Jūs neesat bloķējis nevienu lietotāju",
"friend_code_copied": "Drauga kods kopēts",
"undo_friendship_modal_text": "Tas atcels jūsu draudzību ar {{displayName}}.",
"privacy_hint": "Lai norādītu, kurš to var redzēt, dodieties uz <0>Iestatījumiem</0>.",
"locked_profile": "Šis profils ir privāts",
"image_process_failure": "Attēlu apstrādes kļūme",
"required_field": "Šis lauks ir obligāts",
"displayname_min_length": "Parādāmam vārdam jābūt vismaz 3 simbolus garam.",
"displayname_max_length": "Parādāmam vārdam jābūt ne vairāk kā 50 simboliem.",
"report_profile": "Ziņot par šo profilu",
"report_reason": "Kāpēc jūs ziņojat par šo profilu?",
"report_description": "Papildu informācija",
"report_description_placeholder": "Papildu informācija",
"report": "Ziņot",
"report_reason_hate": "Naida runa",
"report_reason_sexual_content": "Seksuāls saturs",
"report_reason_violence": "Vardarbība",
"report_reason_spam": "Surogātpasts",
"report_reason_other": "Cits",
"profile_reported": "Ziņojums par profilu nosūtīts",
"your_friend_code": "Jūsu drauga kods:",
"upload_banner": "Augšupielādēt reklāmkarogu",
"uploading_banner": "Augšupielādē reklāmkarogu...",
"background_image_updated": "Fona attēls atjaunināts",
"stats": "Statistika",
"achievements": "Sasniegumi",
"games": "Spēles",
"top_percentile": "Top {{percentile}}%",
"ranking_updated_weekly": "Reitings tiek atjaunināts katru nedēļu",
"playing": "Spēlē {{game}}",
"achievements_unlocked": "Sasniegumi atbloķēti",
"earned_points": "Nopelnītie punkti:",
"show_achievements_on_profile": "Rādīt savus sasniegumus profilā",
"show_points_on_profile": "Rādīt nopelnītos punktus savā profilā",
"error_adding_friend": "Neizdevās nosūtīt draudzības pieprasījumu. Lūdzu, pārbaudiet drauga kodu",
"friend_code_length_error": "Drauga kodam jāsatur 8 simboli",
"game_removed_from_pinned": "Spēle dzēsta no piespraustajiem",
"game_added_to_pinned": "Spēle pievienota piespraustajiem",
"karma": "Karma",
"karma_count": "karma",
"karma_description": "Nopelnīta ar pozitīviem atsauksmju vērtējumiem"
},
"achievement": {
"achievement_unlocked": "Sasniegums atbloķēts",
"user_achievements": "{{displayName}} sasniegumi",
"your_achievements": "Jūsu sasniegumi",
"unlocked_at": "Atbloķēts: {{date}}",
"subscription_needed": "Šī satura apskatīšanai nepieciešams Hydra Cloud abonements",
"new_achievements_unlocked": "Atbloķēti {{achievementCount}} jauni sasniegumi no {{gameCount}} spēlēm",
"achievement_progress": "{{unlockedCount}}/{{totalCount}} sasniegumi",
"achievements_unlocked_for_game": "Atbloķēti {{achievementCount}} jauni sasniegumi spēlei {{gameTitle}}",
"hidden_achievement_tooltip": "Šis ir slēpts sasniegums",
"achievement_earn_points": "Nopelniet {{points}} punktus ar šo sasniegumu",
"earned_points": "Nopelnītie punkti:",
"available_points": "Pieejamie punkti:",
"how_to_earn_achievements_points": "Kā nopelnīt sasniegumu punktus?"
},
"hydra_cloud": {
"subscription_tour_title": "Hydra Cloud abonements",
"subscribe_now": "Abonējiet tūlīt",
"cloud_saving": "Saglabāšana mākonī",
"cloud_achievements": "Saglabājiet savus sasniegumus mākonī",
"animated_profile_picture": "Animētas profila bildes",
"premium_support": "Premium atbalsts",
"show_and_compare_achievements": "Rādiet un salīdziniet savus sasniegumus ar citu lietotāju sasniegumiem",
"animated_profile_banner": "Animēts profila reklāmkarogs",
"hydra_cloud": "Hydra Cloud",
"hydra_cloud_feature_found": "Jūs tikko atklājāt Hydra Cloud funkciju!",
"learn_more": "Uzzināt vairāk",
"debrid_description": "Lejupielādējiet 4 reizes ātrāk ar Nimbus"
}
}

View File

@@ -13,6 +13,7 @@
},
"sidebar": {
"catalogue": "Catálogo",
"library": "Biblioteca",
"downloads": "Downloads",
"settings": "Ajustes",
"my_library": "Biblioteca",
@@ -317,6 +318,7 @@
"sort_lowest_score": "Menor Nota",
"sort_most_voted": "Mais Votadas",
"no_reviews_yet": "Ainda não há avaliações",
"review_played_for": "Jogado por",
"be_first_to_review": "Seja o primeiro a compartilhar seus pensamentos sobre este jogo!",
"rating": "Avaliação",
"rating_stats": "Avaliação",
@@ -345,7 +347,14 @@
"delete_review_modal_description": "Esta ação não pode ser desfeita.",
"delete_review_modal_delete_button": "Excluir",
"delete_review_modal_cancel_button": "Cancelar",
"rating_count": "Avaliação"
"show_original": "Mostrar original",
"show_translation": "Mostrar tradução",
"show_original_translated_from": "Mostrar original (traduzido do {{language}})",
"hide_original": "Ocultar original",
"rating_count": "Avaliação",
"review_from_blocked_user": "Avaliação de usuário bloqueado",
"show": "Mostrar",
"hide": "Ocultar"
},
"activation": {
"title": "Ativação",
@@ -412,6 +421,9 @@
"validate_download_source": "Validar",
"remove_download_source": "Remover",
"add_download_source": "Adicionar fonte",
"adding": "Adicionando…",
"failed_add_download_source": "Falha ao adicionar fonte de download. Tente novamente.",
"download_source_already_exists": "Esta URL de fonte de download já existe.",
"download_count_zero": "Sem downloads na lista",
"download_count_one": "{{countFormatted}} download na lista",
"download_count_other": "{{countFormatted}} downloads na lista",
@@ -419,7 +431,13 @@
"add_download_source_description": "Insira a URL contendo o arquivo .json",
"download_source_up_to_date": "Sincronizada",
"download_source_errored": "Falhou",
"download_source_pending_matching": "Importando em breve",
"download_source_matched": "Sincronizada",
"download_source_matching": "Sincronizando",
"download_source_failed": "Erro",
"download_source_no_information": "Sem informações",
"sync_download_sources": "Sincronizar",
"download_sources_synced_successfully": "Fontes de download sincronizadas",
"removed_download_source": "Fonte removida",
"removed_download_sources": "Fontes removidas",
"cancel_button_confirmation_delete_all_sources": "Não",
@@ -524,8 +542,16 @@
"platinum": "Platina",
"hidden": "Oculta",
"test_notification": "Testar notificação",
"achievement_sound_volume": "Volume do som de conquista",
"select_achievement_sound": "Selecionar som de conquista",
"select": "Selecionar",
"preview": "Reproduzir",
"remove": "Remover",
"no_sound_file_selected": "Nenhum arquivo de som selecionado",
"notification_preview": "Prévia da Notificação de Conquistas",
"enable_friend_start_game_notifications": "Quando um amigo iniciar um jogo"
"enable_friend_start_game_notifications": "Quando um amigo iniciar um jogo",
"autoplay_trailers_on_game_page": "Reproduzir trailers automaticamente na página do jogo",
"hide_to_tray_on_game_start": "Ocultar o Hydra na bandeja ao iniciar um jogo"
},
"notifications": {
"download_complete": "Download concluído",
@@ -678,7 +704,11 @@
"karma": "Karma",
"karma_count": "karma",
"karma_description": "Ganho a partir de curtidas positivas em avaliações",
"manual_playtime_tooltip": "Este tempo de jogo foi atualizado manualmente"
"manual_playtime_tooltip": "Este tempo de jogo foi atualizado manualmente",
"user_reviews": "Avaliações",
"loading_reviews": "Carregando avaliações...",
"no_reviews": "Ainda não há avaliações",
"delete_review": "Excluir avaliação"
},
"achievement": {
"achievement_unlocked": "Conquista desbloqueada",
@@ -708,5 +738,26 @@
"hydra_cloud_feature_found": "Você descobriu uma funcionalidade Hydra Cloud!",
"learn_more": "Saiba mais",
"debrid_description": "Baixe até 4x mais rápido com Nimbus"
},
"library": {
"library": "Biblioteca",
"play": "Jogar",
"download": "Baixar",
"downloading": "Baixando",
"game": "jogo",
"games": "jogos",
"grid_view": "Visualização em grade",
"compact_view": "Visualização compacta",
"large_view": "Visualização grande",
"no_games_title": "Sua biblioteca está vazia",
"no_games_description": "Adicione jogos do catálogo ou baixe-os para começar",
"amount_hours": "{{amount}} horas",
"amount_minutes": "{{amount}} minutos",
"amount_hours_short": "{{amount}}h",
"amount_minutes_short": "{{amount}}m",
"manual_playtime_tooltip": "Este tempo de jogo foi atualizado manualmente",
"all_games": "Todos os Jogos",
"recently_played": "Jogados Recentemente",
"favorites": "Favoritos"
}
}

View File

@@ -180,7 +180,11 @@
"download_error_not_cached_on_torbox": "Este download não está disponível no TorBox e a verificação do status do download não está disponível.",
"game_removed_from_favorites": "Jogo removido dos favoritos",
"game_added_to_favorites": "Jogo adicionado aos favoritos",
"create_start_menu_shortcut": "Criar atalho no Menu Iniciar"
"create_start_menu_shortcut": "Criar atalho no Menu Iniciar",
"review_from_blocked_user": "Avaliação de utilizador bloqueado",
"show": "Mostrar",
"hide": "Ocultar",
"review_played_for": "Jogado por"
},
"activation": {
"title": "Ativação",
@@ -252,7 +256,13 @@
"add_download_source_description": "Insere o URL que contém o ficheiro .json",
"download_source_up_to_date": "Sincronizada",
"download_source_errored": "Falhou",
"download_source_pending_matching": "A atualizar em breve",
"download_source_matched": "Atualizado",
"download_source_matching": "A atualizar",
"download_source_failed": "Erro",
"download_source_no_information": "Sem informações",
"sync_download_sources": "Sincronizar",
"download_sources_synced_successfully": "Fontes de download sincronizadas",
"removed_download_source": "Fonte removida",
"cancel_button_confirmation_delete_all_sources": "Não",
"confirm_button_confirmation_delete_all_sources": "Sim, apague tudo",
@@ -460,7 +470,11 @@
"achievements_unlocked": "Conquistas desbloqueadas",
"earned_points": "Pontos ganhos",
"show_achievements_on_profile": "Mostre as suas conquistas no perfil",
"show_points_on_profile": "Mostre os seus pontos ganhos no perfil"
"show_points_on_profile": "Mostre os seus pontos ganhos no perfil",
"user_reviews": "Avaliações",
"loading_reviews": "A carregar avaliações...",
"no_reviews": "Ainda não há avaliações",
"delete_review": "Eliminar avaliação"
},
"achievement": {
"achievement_unlocked": "Conquista desbloqueada",

View File

@@ -13,6 +13,7 @@
},
"sidebar": {
"catalogue": "Каталог",
"library": "Библиотека",
"downloads": "Загрузки",
"settings": "Настройки",
"my_library": "Библиотека",
@@ -212,6 +213,7 @@
"stats": "Статистика",
"download_count": "Загрузки",
"player_count": "Активные игроки",
"rating_count": "Оценка",
"download_error": "Этот вариант загрузки недоступен",
"download": "Скачать",
"executable_path_in_use": "Исполняемый файл уже используется \"{{game}}\"",
@@ -226,6 +228,7 @@
"write_review_placeholder": "Поделитесь своими мыслями об этой игре...",
"sort_newest": "Сначала новые",
"no_reviews_yet": "Пока нет отзывов",
"review_played_for": "Играли",
"be_first_to_review": "Станьте первым, кто поделится своими мыслями об этой игре!",
"sort_oldest": "Сначала старые",
"sort_highest_score": "Высший балл",
@@ -252,13 +255,6 @@
"would_you_recommend_this_game": "Хотите оставить отзыв об этой игре?",
"yes": "Да",
"maybe_later": "Возможно позже",
"rating_count": "Оценка",
"delete_review": "Удалить отзыв",
"remove_review": "Удалить отзыв",
"delete_review_modal_title": "Вы уверены, что хотите удалить свой отзыв?",
"delete_review_modal_description": "Это действие нельзя отменить.",
"delete_review_modal_delete_button": "Удалить",
"delete_review_modal_cancel_button": "Отмена",
"cloud_save": "Облачное сохранение",
"cloud_save_description": "Сохраняйте ваш прогресс в облаке и продолжайте играть на любом устройстве",
"backups": "Резервные копии",
@@ -356,7 +352,21 @@
"caption": "Субтитры",
"audio": "Аудио",
"filter_by_source": "Фильтр по источнику",
"no_repacks_found": "Источники для этой игры не найдены"
"no_repacks_found": "Источники для этой игры не найдены",
"show": "Показать",
"hide": "Скрыть",
"delete_review": "Удалить отзыв",
"remove_review": "Удалить отзыв",
"delete_review_modal_title": "Вы уверены, что хотите удалить свой отзыв?",
"delete_review_modal_description": "Это действие нельзя отменить.",
"delete_review_modal_delete_button": "Удалить",
"delete_review_modal_cancel_button": "Отмена",
"vote_failed": "Не удалось зарегистрировать ваш голос. Пожалуйста, попробуйте снова.",
"show_original": "Показать оригинал",
"show_translation": "Показать перевод",
"show_original_translated_from": "Показать оригинал (переведено с {{language}})",
"hide_original": "Скрыть оригинал",
"review_from_blocked_user": "Отзыв от заблокированного пользователя"
},
"activation": {
"title": "Активировать Hydra",
@@ -423,6 +433,9 @@
"validate_download_source": "Проверить",
"remove_download_source": "Удалить",
"add_download_source": "Добавить источник",
"adding": "Добавление…",
"failed_add_download_source": "Не удалось добавить источник. Пожалуйста, попробуйте снова.",
"download_source_already_exists": "Этот URL источника уже существует.",
"download_count_zero": "В списке нет загрузок",
"download_count_one": "{{countFormatted}} загрузка в списке",
"download_count_other": "{{countFormatted}} загрузок в списке",
@@ -430,9 +443,16 @@
"add_download_source_description": "Вставьте ссылку на .json-файл",
"download_source_up_to_date": "Обновлён",
"download_source_errored": "Ошибка",
"download_source_pending_matching": "Скоро обновится",
"download_source_matched": "Обновлен",
"download_source_matching": "Обновление",
"download_source_failed": "Ошибка",
"download_source_no_information": "Информация отсутствует",
"sync_download_sources": "Обновить источники",
"removed_download_source": "Источник удален",
"removed_download_sources": "Источники удалены",
"removed_all_download_sources": "Все источники удалены",
"download_sources_synced_successfully": "Все источники синхронизированы",
"cancel_button_confirmation_delete_all_sources": "Нет",
"confirm_button_confirmation_delete_all_sources": "Да, удалить все",
"title_confirmation_delete_all_sources": "Удалить все источники",
@@ -463,6 +483,7 @@
"seed_after_download_complete": "Раздавать после завершения загрузки",
"show_hidden_achievement_description": "Показывать описание скрытых достижений перед их получением",
"account": "Аккаунт",
"hydra_cloud": "Hydra Cloud",
"no_users_blocked": "У вас нет заблокированных пользователей",
"subscription_active_until": "Ваша подписка на Hydra Cloud активна до {{date}}",
"manage_subscription": "Управлять подпиской",
@@ -535,8 +556,16 @@
"platinum": "Платиновый",
"hidden": "Скрытый",
"test_notification": "Тестовое уведомление",
"achievement_sound_volume": "Громкость звука достижения",
"select_achievement_sound": "Выбрать звук достижения",
"select": "Выбрать",
"preview": "Предпросмотр",
"remove": "Удалить",
"no_sound_file_selected": "Файл звука не выбран",
"notification_preview": "Предварительный просмотр уведомления о достижении",
"enable_friend_start_game_notifications": "Когда друг начинает играть в игру"
"enable_friend_start_game_notifications": "Когда друг начинает играть в игру",
"autoplay_trailers_on_game_page": "Автоматически начинать воспроизведение трейлеров на странице игры",
"hide_to_tray_on_game_start": "Скрывать Hydra в трей при запуске игры"
},
"notifications": {
"download_complete": "Загрузка завершена",
@@ -586,6 +615,7 @@
"activity": "Недавняя активность",
"library": "Библиотека",
"pinned": "Закрепленные",
"sort_by": "Сортировать по:",
"achievements_earned": "Заработанные достижения",
"played_recently": "Недавно сыгранные",
"playtime": "Время игры",
@@ -670,7 +700,11 @@
"game_added_to_pinned": "Игра добавлена в закрепленные",
"karma": "Карма",
"karma_count": "карма",
"karma_description": "Заработана положительными оценками отзывов"
"karma_description": "Заработана положительными оценками отзывов",
"user_reviews": "Отзывы",
"loading_reviews": "Загрузка отзывов...",
"no_reviews": "Пока нет отзывов",
"delete_review": "Удалить отзыв"
},
"achievement": {
"achievement_unlocked": "Достижение разблокировано",
@@ -700,5 +734,26 @@
"hydra_cloud_feature_found": "Вы только что открыли для себя функцию Hydra Cloud!",
"learn_more": "Подробнее",
"debrid_description": "Скачивайте в 4 раза быстрее с Nimbus"
},
"library": {
"library": "Библиотека",
"play": "Играть",
"download": "Скачать",
"downloading": "Скачивание",
"game": "игра",
"games": "игры",
"grid_view": "Вид сетки",
"compact_view": "Компактный вид",
"large_view": "Большой вид",
"no_games_title": "Ваша библиотека пуста",
"no_games_description": "Добавьте игры из каталога или скачайте их, чтобы начать",
"amount_hours": "{{amount}} часов",
"amount_minutes": "{{amount}} минут",
"amount_hours_short": "{{amount}}ч",
"amount_minutes_short": "{{amount}}м",
"manual_playtime_tooltip": "Время игры было обновлено вручную",
"all_games": "Все игры",
"recently_played": "Недавно сыгранные",
"favorites": "Избранное"
}
}

View File

@@ -27,7 +27,68 @@
"friends": "好友",
"favorites": "收藏",
"need_help": "需要帮助?",
"playable_button_title": "仅显示现在可以游玩的游戏"
"playable_button_title": "仅显示现在可以游玩的游戏",
"add_custom_game_tooltip": "添加自定义游戏",
"cancel": "取消",
"confirm": "确认",
"custom_game_modal": "添加自定义游戏",
"custom_game_modal_add": "添加游戏",
"custom_game_modal_adding": "正在添加游戏...",
"custom_game_modal_browse": "浏览",
"custom_game_modal_cancel": "取消",
"custom_game_modal_description": "通过选择可执行文件将自定义游戏添加到您的库中",
"custom_game_modal_enter_title": "输入标题",
"custom_game_modal_executable": "可执行文件",
"custom_game_modal_executable_path": "可执行文件路径",
"custom_game_modal_failed": "添加自定义游戏失败",
"custom_game_modal_select_executable": "选择可执行文件",
"custom_game_modal_success": "自定义游戏添加成功",
"custom_game_modal_title": "标题",
"decky_plugin_installation_error": "安装 Decky 插件出错: {{error}}",
"decky_plugin_installation_failed": "Decky 插件安装失败: {{error}}",
"decky_plugin_installed": "Decky 插件 v{{version}} 安装成功",
"decky_plugin_installed_version": "Decky 插件 (v{{version}})",
"edit_game_modal": "自定义资源",
"edit_game_modal_assets": "资源",
"edit_game_modal_browse": "浏览",
"edit_game_modal_cancel": "取消",
"edit_game_modal_description": "自定义游戏资源和详情",
"edit_game_modal_drop_hero_image_here": "拖放主图像到此处",
"edit_game_modal_drop_icon_image_here": "拖放图标到此处",
"edit_game_modal_drop_logo_image_here": "拖放Logo到此处",
"edit_game_modal_drop_to_replace_hero": "拖放以替换主图像",
"edit_game_modal_drop_to_replace_icon": "拖放以替换图标",
"edit_game_modal_drop_to_replace_logo": "拖放以替换Logo",
"edit_game_modal_enter_title": "输入标题",
"edit_game_modal_failed": "资源更新失败",
"edit_game_modal_fill_required": "请填写所有必填项",
"edit_game_modal_hero": "库主图",
"edit_game_modal_hero_preview": "库主图预览",
"edit_game_modal_hero_resolution": "推荐分辨率: 1920x620px",
"edit_game_modal_icon": "图标",
"edit_game_modal_icon_preview": "图标预览",
"edit_game_modal_icon_resolution": "推荐分辨率: 256x256px",
"edit_game_modal_image": "图片",
"edit_game_modal_image_filter": "图片",
"edit_game_modal_image_preview": "图片预览",
"edit_game_modal_logo": "Logo",
"edit_game_modal_logo_preview": "Logo预览",
"edit_game_modal_logo_resolution": "推荐分辨率: 640x360px",
"edit_game_modal_select_hero": "选择库主图",
"edit_game_modal_select_icon": "选择图标",
"edit_game_modal_select_image": "选择图片",
"edit_game_modal_select_logo": "选择Logo",
"edit_game_modal_success": "资源更新成功",
"edit_game_modal_title": "标题",
"edit_game_modal_update": "更新",
"edit_game_modal_updating": "正在更新...",
"install_decky_plugin": "安装 Decky 插件",
"install_decky_plugin_message": "这将下载并安装 Hydra 的 Decky Loader 插件。可能需要提升权限。继续吗?",
"install_decky_plugin_title": "安装 Hydra Decky 插件",
"show_playable_only_tooltip": "仅显示可游玩",
"update_decky_plugin": "更新 Decky 插件",
"update_decky_plugin_message": "有新版本的 Hydra Decky 插件可用。现在要更新吗?",
"update_decky_plugin_title": "更新 Hydra Decky 插件"
},
"header": {
"search": "搜索游戏",
@@ -218,7 +279,93 @@
"reset_achievements_title": "您确定吗?",
"save_changes": "保存更改",
"unfreeze_backup": "取消固定",
"you_might_need_to_restart_steam": "您可能需要重启Steam才能看到更改"
"you_might_need_to_restart_steam": "您可能需要重启Steam才能看到更改",
"add_to_favorites": "添加到收藏",
"already_in_library": "已在游戏库中",
"audio": "音频",
"backup_failed": "备份失败",
"be_first_to_review": "成为第一个分享游戏感受的人!",
"caption": "标题",
"create_shortcut_simple": "创建快捷方式",
"currency_country": "zh",
"currency_symbol": "¥",
"delete_review": "删除评价",
"delete_review_modal_cancel_button": "取消",
"delete_review_modal_delete_button": "删除",
"delete_review_modal_description": "此操作无法撤销。",
"delete_review_modal_title": "确定要删除您的评价吗?",
"edit_game_modal_button": "自定义游戏资源",
"failed_remove_files": "文件删除失败",
"failed_remove_from_library": "移出游戏库失败",
"failed_update_favorites": "收藏更新失败",
"files_removed_success": "文件已成功删除",
"filter_by_source": "按来源筛选",
"game_added_to_pinned": "游戏已添加到置顶",
"game_details": "游戏详情",
"game_removed_from_library": "游戏已从库中移除",
"game_removed_from_pinned": "游戏已从置顶移除",
"hide": "隐藏",
"hide_original": "隐藏原文",
"historical_keyshop": "历史密钥商店",
"historical_retail": "历史零售",
"keyshop_price": "密钥商店价格",
"language": "语言",
"leave_a_review": "留下评价",
"load_more_reviews": "加载更多评价",
"loading_more_reviews": "正在加载更多评价...",
"loading_reviews": "正在加载评价...",
"manual_playtime_tooltip": "该游戏时长已手动更新",
"manual_playtime_warning": "您的游戏时长将被标记为手动更新,且无法撤销。",
"maybe_later": "以后再说",
"no_prices_found": "未找到价格信息",
"no_repacks_found": "未找到该游戏的下载来源",
"no_reviews_yet": "暂无评价",
"prices": "价格",
"properties": "属性",
"rating": "评分",
"rating_count": "评分数",
"rating_negative": "差评",
"rating_neutral": "中性",
"rating_positive": "好评",
"rating_stats": "评分统计",
"rating_very_negative": "极差",
"rating_very_positive": "极好",
"remove_from_favorites": "移出收藏",
"remove_review": "移除评价",
"retail_price": "零售价格",
"review_cannot_be_empty": "评价内容不能为空。",
"review_deleted_successfully": "评价已成功删除。",
"review_deletion_failed": "评价删除失败,请重试。",
"review_from_blocked_user": "来自被屏蔽用户的评价",
"review_played_for": "已游玩",
"review_submission_failed": "评价提交失败,请重试。",
"review_submitted_successfully": "评价提交成功!",
"reviews": "评价",
"show": "显示",
"show_less": "收起",
"show_more": "展开",
"show_original": "显示原文",
"show_original_translated_from": "显示原文(由{{language}}翻译)",
"show_translation": "显示翻译",
"sort_highest_score": "最高分",
"sort_lowest_score": "最低分",
"sort_most_voted": "最多投票",
"sort_newest": "最新",
"sort_oldest": "最旧",
"submit_review": "提交",
"submitting": "正在提交...",
"update_game_playtime": "更新游戏时长",
"update_playtime": "更新时长",
"update_playtime_description": "手动更新 {{game}} 的游玩时长",
"update_playtime_error": "游戏时长更新失败",
"update_playtime_success": "游戏时长已成功更新",
"update_playtime_title": "更新游戏时长",
"view_all_prices": "点击查看所有价格",
"vote_failed": "投票失败,请重试。",
"would_you_recommend_this_game": "您想为此游戏留下评价吗?",
"write_review_placeholder": "分享您对本游戏的看法...",
"yes": "是",
"you_seemed_to_enjoy_this_game": "您似乎很喜欢这款游戏"
},
"activation": {
"title": "激活 Hydra",
@@ -394,7 +541,24 @@
"update_email": "更新邮箱",
"update_password": "更新密码",
"variation": "变体",
"web_store": "网络商店"
"web_store": "网络商店",
"adding": "添加中…",
"autoplay_trailers_on_game_page": "在游戏页面自动播放预告片",
"debrid": "Debrid下载服务",
"debrid_description": "Debrid服务是一种高级不限速下载器可让您以最快的网速下载托管在各类网盘上的文件仅受您的网络速度限制。",
"download_source_already_exists": "该下载源URL已存在。",
"download_source_failed": "出错",
"download_source_matched": "已更新",
"download_source_matching": "正在更新",
"download_source_no_information": "暂无信息",
"download_source_pending_matching": "即将更新",
"download_sources_synced_successfully": "所有下载源已同步",
"enable_steam_achievements": "启用Steam成就搜索",
"failed_add_download_source": "添加下载源失败,请重试。",
"hide_to_tray_on_game_start": "启动游戏时隐藏到托盘",
"hydra_cloud": "Hydra Cloud",
"importing": "导入中…",
"removed_all_download_sources": "已移除所有下载源"
},
"notifications": {
"download_complete": "下载完成",
@@ -421,7 +585,8 @@
"game_card": {
"no_downloads": "无可用下载选项",
"available_one": "可用",
"available_other": "可用"
"available_other": "可用",
"calculating": "正在计算"
},
"binary_not_found_modal": {
"title": "程序未安装",
@@ -515,7 +680,23 @@
"show_achievements_on_profile": "在您的个人资料上显示成就",
"show_points_on_profile": "在您的个人资料上显示获得的积分",
"stats": "统计",
"top_percentile": "前 {{percentile}}%"
"top_percentile": "前 {{percentile}}%",
"achievements_earned": "已获得成就",
"amount_hours_short": "{{amount}}小时",
"amount_minutes_short": "{{amount}}分钟",
"delete_review": "删除评价",
"game_added_to_pinned": "游戏已添加到置顶",
"game_removed_from_pinned": "游戏已从置顶移除",
"karma": "业力",
"karma_count": "业力值",
"karma_description": "通过评论获得的点赞",
"loading_reviews": "正在加载评价...",
"manual_playtime_tooltip": "该游戏时长已手动更新",
"pinned": "已置顶",
"played_recently": "最近游玩",
"playtime": "游戏时长",
"sort_by": "排序方式:",
"user_reviews": "用户评价"
},
"achievement": {
"achievement_unlocked": "成就已解锁",

View File

@@ -41,8 +41,12 @@ export const appVersion = app.getVersion() + (isStaging ? "-staging" : "");
export const ASSETS_PATH = path.join(SystemPath.getPath("userData"), "Assets");
export const THEMES_PATH = path.join(SystemPath.getPath("userData"), "themes");
export const MAIN_LOOP_INTERVAL = 2000;
export const DEFAULT_ACHIEVEMENT_SOUND_VOLUME = 0.15;
export const DECKY_PLUGINS_LOCATION = path.join(
SystemPath.getPath("home"),
"homebrew",

View File

@@ -6,6 +6,10 @@ import { gamesShopAssetsSublevel, levelKeys } from "@main/level";
const LOCAL_CACHE_EXPIRATION = 1000 * 60 * 60 * 8; // 8 hours
export const getGameAssets = async (objectId: string, shop: GameShop) => {
if (shop === "custom") {
return null;
}
const cachedAssets = await gamesShopAssetsSublevel.get(
levelKeys.game(shop, objectId)
);

View File

@@ -26,6 +26,8 @@ const getGameShopDetails = async (
shop: GameShop,
language: string
): Promise<ShopDetailsWithAssets | null> => {
if (shop === "custom") return null;
if (shop === "steam") {
const [cachedData, cachedAssets] = await Promise.all([
gamesShopCacheSublevel.get(

View File

@@ -10,6 +10,10 @@ const getGameStats = async (
objectId: string,
shop: GameShop
) => {
if (shop === "custom") {
return null;
}
const cachedStats = await gamesStatsCacheSublevel.get(
levelKeys.game(shop, objectId)
);

View File

@@ -1,76 +1,50 @@
import { registerEvent } from "../register-event";
import { downloadSourcesSublevel, repacksSublevel } from "@main/level";
import { HydraApi, logger } from "@main/services";
import { importDownloadSourceToLocal } from "./helpers";
import { HydraApi } from "@main/services/hydra-api";
import { downloadSourcesSublevel } from "@main/level";
import type { DownloadSource } from "@types";
import { logger } from "@main/services";
const addDownloadSource = async (
_event: Electron.IpcMainInvokeEvent,
url: string
) => {
const result = await importDownloadSourceToLocal(url, true);
if (!result) {
throw new Error("Failed to import download source");
}
try {
const existingSources = await downloadSourcesSublevel.values().all();
const urlExists = existingSources.some((source) => source.url === url);
// Verify that repacks were actually written to the database (read-after-write)
// This ensures all async operations are complete before proceeding
let repackCount = 0;
for await (const [, repack] of repacksSublevel.iterator()) {
if (repack.downloadSourceId === result.id) {
repackCount++;
if (urlExists) {
throw new Error("Download source with this URL already exists");
}
}
await HydraApi.post("/profile/download-sources", {
urls: [url],
});
const downloadSource = await HydraApi.post<DownloadSource>(
"/download-sources",
{
url,
},
{ needsAuth: false }
);
const { fingerprint } = await HydraApi.put<{ fingerprint: string }>(
"/download-sources",
{
objectIds: result.objectIds,
},
{ needsAuth: false }
);
if (HydraApi.isLoggedIn() && HydraApi.hasActiveSubscription()) {
try {
await HydraApi.post("/profile/download-sources", {
urls: [url],
});
} catch (error) {
logger.error("Failed to add download source to profile:", error);
}
}
// Update the source with fingerprint
const updatedSource = await downloadSourcesSublevel.get(`${result.id}`);
if (updatedSource) {
await downloadSourcesSublevel.put(`${result.id}`, {
...updatedSource,
fingerprint,
updatedAt: new Date(),
await downloadSourcesSublevel.put(downloadSource.id, {
...downloadSource,
isRemote: true,
createdAt: new Date().toISOString(),
});
}
// Final verification: ensure the source with fingerprint is persisted
const finalSource = await downloadSourcesSublevel.get(`${result.id}`);
if (!finalSource || !finalSource.fingerprint) {
throw new Error("Failed to persist download source with fingerprint");
return downloadSource;
} catch (error) {
logger.error("Failed to add download source:", error);
throw error;
}
// Verify repacks still exist after fingerprint update
let finalRepackCount = 0;
for await (const [, repack] of repacksSublevel.iterator()) {
if (repack.downloadSourceId === result.id) {
finalRepackCount++;
}
}
if (finalRepackCount !== repackCount) {
logger.warn(
`Repack count mismatch! Before: ${repackCount}, After: ${finalRepackCount}`
);
} else {
logger.info(
`Final verification passed: ${finalRepackCount} repacks confirmed`
);
}
return {
...result,
fingerprint,
};
};
registerEvent("addDownloadSource", addDownloadSource);

View File

@@ -1,17 +0,0 @@
import { registerEvent } from "../register-event";
import { downloadSourcesSublevel } from "@main/level";
const checkDownloadSourceExists = async (
_event: Electron.IpcMainInvokeEvent,
url: string
): Promise<boolean> => {
for await (const [, source] of downloadSourcesSublevel.iterator()) {
if (source.url === url) {
return true;
}
}
return false;
};
registerEvent("checkDownloadSourceExists", checkDownloadSourceExists);

View File

@@ -1,13 +0,0 @@
import { registerEvent } from "../register-event";
import { downloadSourcesSublevel, repacksSublevel } from "@main/level";
import { invalidateIdCaches } from "./helpers";
const deleteAllDownloadSources = async (
_event: Electron.IpcMainInvokeEvent
) => {
await Promise.all([repacksSublevel.clear(), downloadSourcesSublevel.clear()]);
invalidateIdCaches();
};
registerEvent("deleteAllDownloadSources", deleteAllDownloadSources);

View File

@@ -1,28 +0,0 @@
import { registerEvent } from "../register-event";
import { downloadSourcesSublevel, repacksSublevel } from "@main/level";
import { invalidateIdCaches } from "./helpers";
const deleteDownloadSource = async (
_event: Electron.IpcMainInvokeEvent,
id: number
) => {
const repacksToDelete: string[] = [];
for await (const [key, repack] of repacksSublevel.iterator()) {
if (repack.downloadSourceId === id) {
repacksToDelete.push(key);
}
}
const batch = repacksSublevel.batch();
for (const key of repacksToDelete) {
batch.del(key);
}
await batch.write();
await downloadSourcesSublevel.del(`${id}`);
invalidateIdCaches();
};
registerEvent("deleteDownloadSource", deleteDownloadSource);

View File

@@ -1,19 +0,0 @@
import { registerEvent } from "../register-event";
import { downloadSourcesSublevel, DownloadSource } from "@main/level";
const getDownloadSourcesList = async (_event: Electron.IpcMainInvokeEvent) => {
const sources: DownloadSource[] = [];
for await (const [, source] of downloadSourcesSublevel.iterator()) {
sources.push(source);
}
// Sort by createdAt descending
sources.sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
return sources;
};
registerEvent("getDownloadSourcesList", getDownloadSourcesList);

View File

@@ -1,8 +1,10 @@
import { HydraApi } from "@main/services";
import { downloadSourcesSublevel } from "@main/level";
import { registerEvent } from "../register-event";
import { orderBy } from "lodash-es";
const getDownloadSources = async (_event: Electron.IpcMainInvokeEvent) => {
return HydraApi.get("/profile/download-sources");
const allSources = await downloadSourcesSublevel.values().all();
return orderBy(allSources, "createdAt", "desc");
};
registerEvent("getDownloadSources", getDownloadSources);

View File

@@ -1,367 +0,0 @@
import axios from "axios";
import { z } from "zod";
import { downloadSourcesSublevel, repacksSublevel } from "@main/level";
import { DownloadSourceStatus } from "@shared";
import crypto from "node:crypto";
import { logger, ResourceCache } from "@main/services";
export const downloadSourceSchema = z.object({
name: z.string().max(255),
downloads: z.array(
z.object({
title: z.string().max(255),
uris: z.array(z.string()),
uploadDate: z.string().max(255),
fileSize: z.string().max(255),
})
),
});
export type TitleHashMapping = Record<string, number[]>;
let titleHashMappingCache: TitleHashMapping | null = null;
export const getTitleHashMapping = async (): Promise<TitleHashMapping> => {
if (titleHashMappingCache) {
return titleHashMappingCache;
}
try {
const cached =
ResourceCache.getCachedData<TitleHashMapping>("sources-manifest");
if (cached) {
titleHashMappingCache = cached;
return cached;
}
const fetched = await ResourceCache.fetchAndCache<TitleHashMapping>(
"sources-manifest",
"https://cdn.losbroxas.org/sources-manifest.json",
10000
);
titleHashMappingCache = fetched;
return fetched;
} catch (error) {
logger.error("Failed to fetch title hash mapping:", error);
return {} as TitleHashMapping;
}
};
export const hashTitle = (title: string): string => {
return crypto.createHash("sha256").update(title).digest("hex");
};
export type SteamGamesByLetter = Record<string, { id: string; name: string }[]>;
export type FormattedSteamGame = {
id: string;
name: string;
formattedName: string;
};
export type FormattedSteamGamesByLetter = Record<string, FormattedSteamGame[]>;
export const formatName = (name: string) => {
return name
.normalize("NFD")
.replaceAll(/[\u0300-\u036f]/g, "")
.toLowerCase()
.replaceAll(/[^a-z0-9]/g, "");
};
export const formatRepackName = (name: string) => {
return formatName(name.replace("[DL]", ""));
};
interface DownloadSource {
id: number;
url: string;
name: string;
etag: string | null;
status: number;
downloadCount: number;
objectIds: string[];
fingerprint?: string;
createdAt: Date;
updatedAt: Date;
}
const getDownloadSourcesMap = async (): Promise<
Map<string, DownloadSource>
> => {
const map = new Map();
for await (const [key, source] of downloadSourcesSublevel.iterator()) {
map.set(key, source);
}
return map;
};
export const checkUrlExists = async (url: string): Promise<boolean> => {
const sources = await getDownloadSourcesMap();
for (const source of sources.values()) {
if (source.url === url) {
return true;
}
}
return false;
};
let steamGamesFormattedCache: FormattedSteamGamesByLetter | null = null;
export const getSteamGames = async (): Promise<FormattedSteamGamesByLetter> => {
if (steamGamesFormattedCache) {
return steamGamesFormattedCache;
}
let steamGames: SteamGamesByLetter;
const cached = ResourceCache.getCachedData<SteamGamesByLetter>(
"steam-games-by-letter"
);
if (cached) {
steamGames = cached;
} else {
steamGames = await ResourceCache.fetchAndCache<SteamGamesByLetter>(
"steam-games-by-letter",
`${import.meta.env.MAIN_VITE_EXTERNAL_RESOURCES_URL}/steam-games-by-letter.json`
);
}
const formattedData: FormattedSteamGamesByLetter = {};
for (const [letter, games] of Object.entries(steamGames)) {
formattedData[letter] = games.map((game) => ({
...game,
formattedName: formatName(game.name),
}));
}
steamGamesFormattedCache = formattedData;
return formattedData;
};
export type SublevelIterator = AsyncIterable<[string, { id: number }]>;
export interface SublevelWithId {
iterator: () => SublevelIterator;
}
let maxRepackId: number | null = null;
let maxDownloadSourceId: number | null = null;
export const getNextId = async (sublevel: SublevelWithId): Promise<number> => {
const isRepackSublevel = sublevel === repacksSublevel;
const isDownloadSourceSublevel = sublevel === downloadSourcesSublevel;
if (isRepackSublevel && maxRepackId !== null) {
return ++maxRepackId;
}
if (isDownloadSourceSublevel && maxDownloadSourceId !== null) {
return ++maxDownloadSourceId;
}
let maxId = 0;
for await (const [, value] of sublevel.iterator()) {
if (value.id > maxId) {
maxId = value.id;
}
}
if (isRepackSublevel) {
maxRepackId = maxId;
} else if (isDownloadSourceSublevel) {
maxDownloadSourceId = maxId;
}
return maxId + 1;
};
export const invalidateIdCaches = () => {
maxRepackId = null;
maxDownloadSourceId = null;
};
export const addNewDownloads = async (
downloadSource: { id: number; name: string },
downloads: z.infer<typeof downloadSourceSchema>["downloads"],
steamGames: FormattedSteamGamesByLetter
) => {
const now = new Date();
const objectIdsOnSource = new Set<string>();
let nextRepackId = await getNextId(repacksSublevel);
const batch = repacksSublevel.batch();
const titleHashMapping = await getTitleHashMapping();
let hashMatchCount = 0;
let fuzzyMatchCount = 0;
let noMatchCount = 0;
for (const download of downloads) {
let objectIds: string[] = [];
let usedHashMatch = false;
const titleHash = hashTitle(download.title);
const steamIdsFromHash = titleHashMapping[titleHash];
if (steamIdsFromHash && steamIdsFromHash.length > 0) {
hashMatchCount++;
usedHashMatch = true;
objectIds = steamIdsFromHash.map(String);
}
if (!usedHashMatch) {
let gamesInSteam: FormattedSteamGame[] = [];
const formattedTitle = formatRepackName(download.title);
if (formattedTitle && formattedTitle.length > 0) {
const [firstLetter] = formattedTitle;
const games = steamGames[firstLetter] || [];
gamesInSteam = games.filter((game) =>
formattedTitle.startsWith(game.formattedName)
);
if (gamesInSteam.length === 0) {
gamesInSteam = games.filter(
(game) =>
formattedTitle.includes(game.formattedName) ||
game.formattedName.includes(formattedTitle)
);
}
if (gamesInSteam.length === 0) {
for (const letter of Object.keys(steamGames)) {
const letterGames = steamGames[letter] || [];
const matches = letterGames.filter(
(game) =>
formattedTitle.includes(game.formattedName) ||
game.formattedName.includes(formattedTitle)
);
if (matches.length > 0) {
gamesInSteam = matches;
break;
}
}
}
if (gamesInSteam.length > 0) {
fuzzyMatchCount++;
objectIds = gamesInSteam.map((game) => String(game.id));
} else {
noMatchCount++;
}
} else {
noMatchCount++;
}
}
for (const id of objectIds) {
objectIdsOnSource.add(id);
}
const repack = {
id: nextRepackId++,
objectIds: objectIds,
title: download.title,
uris: download.uris,
fileSize: download.fileSize,
repacker: downloadSource.name,
uploadDate: download.uploadDate,
downloadSourceId: downloadSource.id,
createdAt: now,
updatedAt: now,
};
batch.put(`${repack.id}`, repack);
}
await batch.write();
logger.info(
`Matching stats for ${downloadSource.name}: Hash=${hashMatchCount}, Fuzzy=${fuzzyMatchCount}, None=${noMatchCount}`
);
const existingSource = await downloadSourcesSublevel.get(
`${downloadSource.id}`
);
if (existingSource) {
await downloadSourcesSublevel.put(`${downloadSource.id}`, {
...existingSource,
objectIds: Array.from(objectIdsOnSource),
});
}
return Array.from(objectIdsOnSource);
};
export const importDownloadSourceToLocal = async (
url: string,
throwOnDuplicate = false
) => {
const urlExists = await checkUrlExists(url);
if (urlExists) {
if (throwOnDuplicate) {
throw new Error("Download source with this URL already exists");
}
return null;
}
const response = await axios.get<z.infer<typeof downloadSourceSchema>>(url);
const steamGames = await getSteamGames();
const now = new Date();
const nextId = await getNextId(downloadSourcesSublevel);
const downloadSource = {
id: nextId,
url,
name: response.data.name,
etag: response.headers["etag"] || null,
status: DownloadSourceStatus.UpToDate,
downloadCount: response.data.downloads.length,
objectIds: [],
createdAt: now,
updatedAt: now,
};
await downloadSourcesSublevel.put(`${downloadSource.id}`, downloadSource);
const objectIds = await addNewDownloads(
downloadSource,
response.data.downloads,
steamGames
);
// Invalidate ID caches after creating new repacks to prevent ID collisions
invalidateIdCaches();
return {
...downloadSource,
objectIds,
};
};
export const updateDownloadSourcePreservingTimestamp = async (
existingSource: DownloadSource,
url: string
) => {
const response = await axios.get<z.infer<typeof downloadSourceSchema>>(url);
const updatedSource = {
...existingSource,
name: response.data.name,
etag: response.headers["etag"] || null,
status: DownloadSourceStatus.UpToDate,
downloadCount: response.data.downloads.length,
updatedAt: new Date(),
// Preserve the original createdAt timestamp
};
await downloadSourcesSublevel.put(`${existingSource.id}`, updatedSource);
return updatedSource;
};

View File

@@ -1,18 +1,27 @@
import { HydraApi } from "@main/services";
import { downloadSourcesSublevel } from "@main/level";
import { registerEvent } from "../register-event";
const removeDownloadSource = async (
_event: Electron.IpcMainInvokeEvent,
url?: string,
removeAll = false
removeAll = false,
downloadSourceId?: string
) => {
const params = new URLSearchParams({
all: removeAll.toString(),
});
if (url) params.set("url", url);
if (downloadSourceId) params.set("downloadSourceId", downloadSourceId);
return HydraApi.delete(`/profile/download-sources?${params.toString()}`);
if (HydraApi.isLoggedIn() && HydraApi.hasActiveSubscription()) {
void HydraApi.delete(`/profile/download-sources?${params.toString()}`);
}
if (removeAll) {
await downloadSourcesSublevel.clear();
} else if (downloadSourceId) {
await downloadSourcesSublevel.del(downloadSourceId);
}
};
registerEvent("removeDownloadSource", removeDownloadSource);

View File

@@ -1,19 +0,0 @@
import { HydraApi, logger } from "@main/services";
import { importDownloadSourceToLocal, checkUrlExists } from "./helpers";
export const syncDownloadSourcesFromApi = async () => {
try {
const apiSources = await HydraApi.get<
{ url: string; createdAt: string; updatedAt: string }[]
>("/profile/download-sources");
for (const apiSource of apiSources) {
const exists = await checkUrlExists(apiSource.url);
if (!exists) {
await importDownloadSourceToLocal(apiSource.url, false);
}
}
} catch (error) {
logger.error("Failed to sync download sources from API:", error);
}
};

View File

@@ -1,114 +1,28 @@
import { HydraApi } from "@main/services";
import { registerEvent } from "../register-event";
import axios, { AxiosError } from "axios";
import { downloadSourcesSublevel, repacksSublevel } from "@main/level";
import { DownloadSourceStatus } from "@shared";
import {
invalidateIdCaches,
downloadSourceSchema,
getSteamGames,
addNewDownloads,
} from "./helpers";
import { downloadSourcesSublevel } from "@main/level";
import type { DownloadSource } from "@types";
const syncDownloadSources = async (
_event: Electron.IpcMainInvokeEvent
): Promise<number> => {
let newRepacksCount = 0;
const syncDownloadSources = async (_event: Electron.IpcMainInvokeEvent) => {
const downloadSources = await downloadSourcesSublevel.values().all();
try {
const downloadSources: Array<{
id: number;
url: string;
name: string;
etag: string | null;
status: number;
downloadCount: number;
objectIds: string[];
fingerprint?: string;
createdAt: Date;
updatedAt: Date;
}> = [];
for await (const [, source] of downloadSourcesSublevel.iterator()) {
downloadSources.push(source);
}
const response = await HydraApi.post<DownloadSource[]>(
"/download-sources/sync",
{
ids: downloadSources.map((downloadSource) => downloadSource.id),
},
{ needsAuth: false }
);
const existingRepacks: Array<{
id: number;
title: string;
uris: string[];
repacker: string;
fileSize: string | null;
objectIds: string[];
uploadDate: Date | string | null;
downloadSourceId: number;
createdAt: Date;
updatedAt: Date;
}> = [];
for await (const [, repack] of repacksSublevel.iterator()) {
existingRepacks.push(repack);
}
// Handle sources with missing fingerprints individually, don't delete all sources
const sourcesWithFingerprints = downloadSources.filter(
(source) => source.fingerprint
);
const sourcesWithoutFingerprints = downloadSources.filter(
(source) => !source.fingerprint
for (const downloadSource of response) {
const existingDownloadSource = downloadSources.find(
(source) => source.id === downloadSource.id
);
// For sources without fingerprints, just continue with normal sync
// They will get fingerprints updated later by updateMissingFingerprints
const allSourcesToSync = [
...sourcesWithFingerprints,
...sourcesWithoutFingerprints,
];
for (const downloadSource of allSourcesToSync) {
const headers: Record<string, string> = {};
if (downloadSource.etag) {
headers["If-None-Match"] = downloadSource.etag;
}
try {
const response = await axios.get(downloadSource.url, {
headers,
});
const source = downloadSourceSchema.parse(response.data);
const steamGames = await getSteamGames();
const repacks = source.downloads.filter(
(download) =>
!existingRepacks.some((repack) => repack.title === download.title)
);
await downloadSourcesSublevel.put(`${downloadSource.id}`, {
...downloadSource,
etag: response.headers["etag"] || null,
downloadCount: source.downloads.length,
status: DownloadSourceStatus.UpToDate,
});
await addNewDownloads(downloadSource, repacks, steamGames);
newRepacksCount += repacks.length;
} catch (err: unknown) {
const isNotModified = (err as AxiosError).response?.status === 304;
await downloadSourcesSublevel.put(`${downloadSource.id}`, {
...downloadSource,
status: isNotModified
? DownloadSourceStatus.UpToDate
: DownloadSourceStatus.Errored,
});
}
}
invalidateIdCaches();
return newRepacksCount;
} catch (err) {
return -1;
await downloadSourcesSublevel.put(downloadSource.id, {
...existingDownloadSource,
...downloadSource,
});
}
};

View File

@@ -1,67 +0,0 @@
import { registerEvent } from "../register-event";
import { downloadSourcesSublevel } from "@main/level";
import { HydraApi, logger } from "@main/services";
const updateMissingFingerprints = async (
_event: Electron.IpcMainInvokeEvent
): Promise<number> => {
const sourcesNeedingFingerprints: Array<{
id: number;
objectIds: string[];
}> = [];
for await (const [, source] of downloadSourcesSublevel.iterator()) {
if (
!source.fingerprint &&
source.objectIds &&
source.objectIds.length > 0
) {
sourcesNeedingFingerprints.push({
id: source.id,
objectIds: source.objectIds,
});
}
}
if (sourcesNeedingFingerprints.length === 0) {
return 0;
}
logger.info(
`Updating fingerprints for ${sourcesNeedingFingerprints.length} sources`
);
await Promise.all(
sourcesNeedingFingerprints.map(async (source) => {
try {
const { fingerprint } = await HydraApi.put<{ fingerprint: string }>(
"/download-sources",
{
objectIds: source.objectIds,
},
{ needsAuth: false }
);
const existingSource = await downloadSourcesSublevel.get(
`${source.id}`
);
if (existingSource) {
await downloadSourcesSublevel.put(`${source.id}`, {
...existingSource,
fingerprint,
updatedAt: new Date(),
});
}
} catch (error) {
logger.error(
`Failed to update fingerprint for source ${source.id}:`,
error
);
}
})
);
return sourcesNeedingFingerprints.length;
};
registerEvent("updateMissingFingerprints", updateMissingFingerprints);

View File

@@ -1,32 +0,0 @@
import { registerEvent } from "../register-event";
import axios from "axios";
import { z } from "zod";
const downloadSourceSchema = z.object({
name: z.string().max(255),
downloads: z.array(
z.object({
title: z.string().max(255),
uris: z.array(z.string()),
uploadDate: z.string().max(255),
fileSize: z.string().max(255),
})
),
});
const validateDownloadSource = async (
_event: Electron.IpcMainInvokeEvent,
url: string
) => {
const response = await axios.get<z.infer<typeof downloadSourceSchema>>(url);
const { name } = downloadSourceSchema.parse(response.data);
return {
name,
etag: response.headers["etag"] || null,
downloadCount: response.data.downloads.length,
};
};
registerEvent("validateDownloadSource", validateDownloadSource);

View File

@@ -18,6 +18,7 @@ import "./library/close-game";
import "./library/delete-game-folder";
import "./library/get-game-by-object-id";
import "./library/get-library";
import "./library/refresh-library-assets";
import "./library/extract-game-download";
import "./library/open-game";
import "./library/open-game-executable-path";
@@ -63,14 +64,7 @@ import "./autoupdater/restart-and-install-update";
import "./user-preferences/authenticate-real-debrid";
import "./user-preferences/authenticate-torbox";
import "./download-sources/add-download-source";
import "./download-sources/update-missing-fingerprints";
import "./download-sources/delete-download-source";
import "./download-sources/delete-all-download-sources";
import "./download-sources/validate-download-source";
import "./download-sources/sync-download-sources";
import "./download-sources/get-download-sources-list";
import "./download-sources/check-download-source-exists";
import "./repacks/get-all-repacks";
import "./auth/sign-out";
import "./auth/open-auth-window";
import "./auth/get-session-hash";
@@ -98,6 +92,11 @@ import "./themes/get-custom-theme-by-id";
import "./themes/get-active-custom-theme";
import "./themes/close-editor-window";
import "./themes/toggle-custom-theme";
import "./themes/copy-theme-achievement-sound";
import "./themes/remove-theme-achievement-sound";
import "./themes/get-theme-sound-path";
import "./themes/get-theme-sound-data-url";
import "./themes/import-theme-sound-from-store";
import "./download-sources/remove-download-source";
import "./download-sources/get-download-sources";
import { isPortableVersion } from "@main/helpers";

View File

@@ -37,6 +37,7 @@ const addCustomGameToLibrary = async (
logoImageUrl: logoImageUrl || "",
logoPosition: null,
coverImageUrl: iconUrl || "",
downloadSources: [],
};
await gamesShopAssetsSublevel.put(gameKey, assets);

View File

@@ -13,7 +13,9 @@ const addGameToFavorites = async (
const game = await gamesSublevel.get(gameKey);
if (!game) return;
HydraApi.put(`/profile/games/${shop}/${objectId}/favorite`).catch(() => {});
if (shop !== "custom") {
HydraApi.put(`/profile/games/${shop}/${objectId}/favorite`).catch(() => {});
}
try {
await gamesSublevel.put(gameKey, {

View File

@@ -4,6 +4,7 @@ import {
downloadsSublevel,
gamesShopAssetsSublevel,
gamesSublevel,
gameAchievementsSublevel,
} from "@main/level";
const getLibrary = async (): Promise<LibraryGame[]> => {
@@ -18,14 +19,32 @@ const getLibrary = async (): Promise<LibraryGame[]> => {
const download = await downloadsSublevel.get(key);
const gameAssets = await gamesShopAssetsSublevel.get(key);
let unlockedAchievementCount = 0;
let achievementCount = 0;
try {
const achievements = await gameAchievementsSublevel.get(key);
if (achievements) {
achievementCount = achievements.achievements.length;
unlockedAchievementCount =
achievements.unlockedAchievements.length;
}
} catch {
// No achievements data for this game
}
return {
id: key,
...game,
download: download ?? null,
unlockedAchievementCount,
achievementCount,
// Spread gameAssets last to ensure all image URLs are properly set
...gameAssets,
// Ensure compatibility with LibraryGame type
libraryHeroImageUrl:
game.libraryHeroImageUrl ?? gameAssets?.libraryHeroImageUrl,
// Preserve custom image URLs from game if they exist
customIconUrl: game.customIconUrl,
customLogoImageUrl: game.customLogoImageUrl,
customHeroImageUrl: game.customHeroImageUrl,
} as LibraryGame;
})
);

View File

@@ -0,0 +1,8 @@
import { registerEvent } from "../register-event";
import { mergeWithRemoteGames } from "@main/services";
const refreshLibraryAssets = async () => {
await mergeWithRemoteGames();
};
registerEvent("refreshLibraryAssets", refreshLibraryAssets);

View File

@@ -13,7 +13,11 @@ const removeGameFromFavorites = async (
const game = await gamesSublevel.get(gameKey);
if (!game) return;
HydraApi.put(`/profile/games/${shop}/${objectId}/unfavorite`).catch(() => {});
if (shop !== "custom") {
HydraApi.put(`/profile/games/${shop}/${objectId}/unfavorite`).catch(
() => {}
);
}
try {
await gamesSublevel.put(gameKey, {

View File

@@ -84,7 +84,7 @@ const removeGameFromLibrary = async (
await resetShopAssets(gameKey);
}
if (game?.remoteId) {
if (game.remoteId) {
HydraApi.delete(`/profile/games/${game.remoteId}`).catch(() => {});
}

View File

@@ -1,16 +0,0 @@
import { registerEvent } from "../register-event";
import { repacksSublevel, GameRepack } from "@main/level";
const getAllRepacks = async (_event: Electron.IpcMainInvokeEvent) => {
const repacks: GameRepack[] = [];
for await (const [, repack] of repacksSublevel.iterator()) {
if (Array.isArray(repack.objectIds)) {
repacks.push(repack);
}
}
return repacks;
};
registerEvent("getAllRepacks", getAllRepacks);

View File

@@ -0,0 +1,40 @@
import { registerEvent } from "../register-event";
import fs from "node:fs";
import path from "node:path";
import { getThemePath } from "@main/helpers";
import { themesSublevel } from "@main/level";
const copyThemeAchievementSound = async (
_event: Electron.IpcMainInvokeEvent,
themeId: string,
sourcePath: string
): Promise<void> => {
if (!sourcePath || !fs.existsSync(sourcePath)) {
throw new Error("Source file does not exist");
}
const theme = await themesSublevel.get(themeId);
if (!theme) {
throw new Error("Theme not found");
}
const themeDir = getThemePath(themeId, theme.name);
if (!fs.existsSync(themeDir)) {
fs.mkdirSync(themeDir, { recursive: true });
}
const fileExtension = path.extname(sourcePath);
const destinationPath = path.join(themeDir, `achievement${fileExtension}`);
await fs.promises.copyFile(sourcePath, destinationPath);
await themesSublevel.put(themeId, {
...theme,
hasCustomSound: true,
originalSoundPath: sourcePath,
updatedAt: new Date(),
});
};
registerEvent("copyThemeAchievementSound", copyThemeAchievementSound);

View File

@@ -0,0 +1,40 @@
import { registerEvent } from "../register-event";
import { getThemeSoundPath } from "@main/helpers";
import { themesSublevel } from "@main/level";
import fs from "node:fs";
import path from "node:path";
import { logger } from "@main/services";
const getThemeSoundDataUrl = async (
_event: Electron.IpcMainInvokeEvent,
themeId: string
): Promise<string | null> => {
try {
const theme = await themesSublevel.get(themeId);
const soundPath = getThemeSoundPath(themeId, theme?.name);
if (!soundPath || !fs.existsSync(soundPath)) {
return null;
}
const buffer = await fs.promises.readFile(soundPath);
const ext = path.extname(soundPath).toLowerCase().slice(1);
const mimeTypes: Record<string, string> = {
mp3: "audio/mpeg",
wav: "audio/wav",
ogg: "audio/ogg",
m4a: "audio/mp4",
};
const mimeType = mimeTypes[ext] || "audio/mpeg";
const base64 = buffer.toString("base64");
return `data:${mimeType};base64,${base64}`;
} catch (error) {
logger.error("Failed to get theme sound data URL", error);
return null;
}
};
registerEvent("getThemeSoundDataUrl", getThemeSoundDataUrl);

View File

@@ -0,0 +1,13 @@
import { registerEvent } from "../register-event";
import { getThemeSoundPath } from "@main/helpers";
import { themesSublevel } from "@main/level";
const getThemeSoundPathEvent = async (
_event: Electron.IpcMainInvokeEvent,
themeId: string
): Promise<string | null> => {
const theme = await themesSublevel.get(themeId);
return getThemeSoundPath(themeId, theme?.name);
};
registerEvent("getThemeSoundPath", getThemeSoundPathEvent);

View File

@@ -0,0 +1,60 @@
import { registerEvent } from "../register-event";
import fs from "node:fs";
import path from "node:path";
import axios from "axios";
import { getThemePath } from "@main/helpers";
import { themesSublevel } from "@main/level";
import { logger } from "@main/services";
const importThemeSoundFromStore = async (
_event: Electron.IpcMainInvokeEvent,
themeId: string,
themeName: string,
storeUrl: string
): Promise<void> => {
const theme = await themesSublevel.get(themeId);
if (!theme) {
throw new Error("Theme not found");
}
const formats = ["wav", "mp3", "ogg", "m4a"];
for (const format of formats) {
try {
const soundUrl = `${storeUrl}/themes/${themeName.toLowerCase()}/achievement.${format}`;
const response = await axios.get(soundUrl, {
responseType: "arraybuffer",
timeout: 10000,
});
const themeDir = getThemePath(themeId, theme.name);
if (!fs.existsSync(themeDir)) {
fs.mkdirSync(themeDir, { recursive: true });
}
const destinationPath = path.join(themeDir, `achievement.${format}`);
await fs.promises.writeFile(destinationPath, response.data);
await themesSublevel.put(themeId, {
...theme,
hasCustomSound: true,
updatedAt: new Date(),
});
logger.log(`Successfully imported sound for theme ${themeName}`);
return;
} catch (error) {
logger.error(
`Failed to import ${format} sound for theme ${themeName}`,
error
);
continue;
}
}
logger.log(`No sound file found for theme ${themeName} in store`);
};
registerEvent("importThemeSoundFromStore", importThemeSoundFromStore);

View File

@@ -0,0 +1,48 @@
import { registerEvent } from "../register-event";
import fs from "node:fs";
import { getThemePath } from "@main/helpers";
import { themesSublevel } from "@main/level";
import { THEMES_PATH } from "@main/constants";
import path from "node:path";
const removeThemeAchievementSound = async (
_event: Electron.IpcMainInvokeEvent,
themeId: string
): Promise<void> => {
const theme = await themesSublevel.get(themeId);
if (!theme) {
throw new Error("Theme not found");
}
const themeDir = getThemePath(themeId, theme.name);
const legacyThemeDir = path.join(THEMES_PATH, themeId);
const removeFromDir = async (dir: string) => {
if (!fs.existsSync(dir)) {
return;
}
const formats = ["wav", "mp3", "ogg", "m4a"];
for (const format of formats) {
const soundPath = path.join(dir, `achievement.${format}`);
if (fs.existsSync(soundPath)) {
await fs.promises.unlink(soundPath);
}
}
};
await removeFromDir(themeDir);
if (themeDir !== legacyThemeDir) {
await removeFromDir(legacyThemeDir);
}
await themesSublevel.put(themeId, {
...theme,
hasCustomSound: false,
originalSoundPath: undefined,
updatedAt: new Date(),
});
};
registerEvent("removeThemeAchievementSound", removeThemeAchievementSound);

View File

@@ -2,6 +2,8 @@ import axios from "axios";
import { JSDOM } from "jsdom";
import UserAgent from "user-agents";
import path from "node:path";
import fs from "node:fs";
import { THEMES_PATH } from "@main/constants";
export const getFileBuffer = async (url: string) =>
fetch(url, { method: "GET" }).then((response) =>
@@ -31,9 +33,64 @@ export const isPortableVersion = () => {
};
export const normalizePath = (str: string) =>
path.posix.normalize(str).replace(/\\/g, "/");
path.posix.normalize(str).replaceAll("\\", "/");
export const addTrailingSlash = (str: string) =>
str.endsWith("/") ? str : `${str}/`;
const sanitizeFolderName = (name: string): string => {
return name
.toLowerCase()
.replaceAll(/[^a-z0-9-_\s]/g, "")
.replaceAll(/\s+/g, "-")
.replaceAll(/-+/g, "-")
.replaceAll(/(^-|-$)/g, "");
};
export const getThemePath = (themeId: string, themeName?: string): string => {
if (themeName) {
const sanitizedName = sanitizeFolderName(themeName);
if (sanitizedName) {
return path.join(THEMES_PATH, sanitizedName);
}
}
return path.join(THEMES_PATH, themeId);
};
export const getThemeSoundPath = (
themeId: string,
themeName?: string
): string | null => {
const themeDir = getThemePath(themeId, themeName);
const legacyThemeDir = themeName ? path.join(THEMES_PATH, themeId) : null;
const checkDir = (dir: string): string | null => {
if (!fs.existsSync(dir)) {
return null;
}
const formats = ["wav", "mp3", "ogg", "m4a"];
for (const format of formats) {
const soundPath = path.join(dir, `achievement.${format}`);
if (fs.existsSync(soundPath)) {
return soundPath;
}
}
return null;
};
const soundPath = checkDir(themeDir);
if (soundPath) {
return soundPath;
}
if (legacyThemeDir) {
return checkDir(legacyThemeDir);
}
return null;
};
export * from "./reg-parser";

View File

@@ -0,0 +1,27 @@
import { downloadSourcesSublevel } from "@main/level";
import { HydraApi } from "@main/services/hydra-api";
import { DownloadSource } from "@types";
export const migrateDownloadSources = async () => {
const downloadSources = downloadSourcesSublevel.iterator();
for await (const [key, value] of downloadSources) {
if (!value.isRemote) {
const downloadSource = await HydraApi.post<DownloadSource>(
"/download-sources",
{
url: value.url,
},
{ needsAuth: false }
);
await downloadSourcesSublevel.put(downloadSource.id, {
...downloadSource,
isRemote: true,
createdAt: new Date().toISOString(),
});
await downloadSourcesSublevel.del(key);
}
}
};

View File

@@ -1,18 +1,6 @@
import { db } from "../level";
import { levelKeys } from "./keys";
export interface DownloadSource {
id: number;
name: string;
url: string;
status: number;
objectIds: string[];
downloadCount: number;
fingerprint?: string;
etag: string | null;
createdAt: Date;
updatedAt: Date;
}
import type { DownloadSource } from "@types";
export const downloadSourcesSublevel = db.sublevel<string, DownloadSource>(
levelKeys.downloadSources,

View File

@@ -7,4 +7,3 @@ export * from "./game-achievements";
export * from "./keys";
export * from "./themes";
export * from "./download-sources";
export * from "./repacks";

View File

@@ -18,5 +18,4 @@ export const levelKeys = {
screenState: "screenState",
rpcPassword: "rpcPassword",
downloadSources: "downloadSources",
repacks: "repacks",
};

View File

@@ -1,22 +0,0 @@
import { db } from "../level";
import { levelKeys } from "./keys";
export interface GameRepack {
id: number;
title: string;
uris: string[];
repacker: string;
fileSize: string | null;
objectIds: string[];
uploadDate: Date | string | null;
downloadSourceId: number;
createdAt: Date;
updatedAt: Date;
}
export const repacksSublevel = db.sublevel<string, GameRepack>(
levelKeys.repacks,
{
valueEncoding: "json",
}
);

View File

@@ -16,15 +16,13 @@ import {
Ludusavi,
Lock,
DeckyPlugin,
ResourceCache,
WSClient,
} from "@main/services";
import { migrateDownloadSources } from "./helpers/migrate-download-sources";
export const loadState = async () => {
await Lock.acquireLock();
ResourceCache.initialize();
await ResourceCache.updateResourcesOnStartup();
const userPreferences = await db.get<string, UserPreferences | null>(
levelKeys.userPreferences,
{
@@ -53,9 +51,13 @@ export const loadState = async () => {
DeckyPlugin.checkAndUpdateIfOutdated();
}
await HydraApi.setupApi().then(() => {
await HydraApi.setupApi().then(async () => {
uploadGamesBatch();
// WSClient.connect();
void migrateDownloadSources();
const { syncDownloadSourcesFromApi } = await import("./services/user");
void syncDownloadSourcesFromApi();
WSClient.connect();
});
const downloads = await downloadsSublevel

View File

@@ -167,6 +167,8 @@ export class AchievementWatcherManager {
shop: GameShop,
objectId: string
) {
if (shop === "custom") return;
const gameKey = levelKeys.game(shop, objectId);
if (this.alreadySyncedGames.get(gameKey)) return;

View File

@@ -5,15 +5,18 @@ import { logger } from "../logger";
import { db, gameAchievementsSublevel, levelKeys } from "@main/level";
import { AxiosError } from "axios";
const LOCAL_CACHE_EXPIRATION = 1000 * 60 * 60; // 1 hour
const getModifiedSinceHeader = (
cachedAchievements: GameAchievement | undefined
cachedAchievements: GameAchievement | undefined,
userLanguage: string
): Date | undefined => {
if (!cachedAchievements) {
return undefined;
}
if (userLanguage != cachedAchievements.language) {
return undefined;
}
return cachedAchievements.updatedAt
? new Date(cachedAchievements.updatedAt)
: undefined;
@@ -24,17 +27,15 @@ export const getGameAchievementData = async (
shop: GameShop,
useCachedData: boolean
) => {
if (shop === "custom") {
return [];
}
const gameKey = levelKeys.game(shop, objectId);
const cachedAchievements = await gameAchievementsSublevel.get(gameKey);
if (cachedAchievements?.achievements && useCachedData)
return cachedAchievements.achievements;
if (
cachedAchievements?.achievements &&
Date.now() < (cachedAchievements.updatedAt ?? 0) + LOCAL_CACHE_EXPIRATION
) {
if (cachedAchievements?.achievements && useCachedData) {
return cachedAchievements.achievements;
}
@@ -50,14 +51,15 @@ export const getGameAchievementData = async (
language,
},
{
ifModifiedSince: getModifiedSinceHeader(cachedAchievements),
ifModifiedSince: getModifiedSinceHeader(cachedAchievements, language),
}
)
.then(async (achievements) => {
await gameAchievementsSublevel.put(gameKey, {
unlockedAchievements: cachedAchievements?.unlockedAchievements ?? [],
achievements,
updatedAt: Date.now() + LOCAL_CACHE_EXPIRATION,
updatedAt: Date.now(),
language,
});
return achievements;

View File

@@ -37,6 +37,7 @@ const saveAchievementsOnLocal = async (
achievements: gameAchievement?.achievements ?? [],
unlockedAchievements: unlockedAchievements,
updatedAt: gameAchievement?.updatedAt,
language: gameAchievement?.language,
});
if (!sendUpdateEvent) return;

View File

@@ -1,6 +1,7 @@
import axios, { AxiosResponse } from "axios";
import { wrapper } from "axios-cookiejar-support";
import { CookieJar } from "tough-cookie";
import { logger } from "@main/services";
export class DatanodesApi {
private static readonly jar = new CookieJar();
@@ -20,51 +21,42 @@ export class DatanodesApi {
await this.jar.setCookie("lang=english;", "https://datanodes.to");
const payload = new URLSearchParams({
op: "download2",
id: fileCode,
method_free: "Free Download >>",
dl: "1",
});
const formData = new FormData();
formData.append("op", "download2");
formData.append("id", fileCode);
formData.append("rand", "");
formData.append("referer", "https://datanodes.to/download");
formData.append("method_free", "Free Download >>");
formData.append("method_premium", "");
formData.append("__dl", "1");
const response: AxiosResponse = await this.session.post(
"https://datanodes.to/download",
payload,
formData,
{
headers: {
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:135.0) Gecko/20100101 Firefox/135.0",
accept: "*/*",
"accept-language": "en-US,en;q=0.9",
priority: "u=1, i",
"sec-ch-ua":
'"Google Chrome";v="141", "Not?A_Brand";v="8", "Chromium";v="141"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
Referer: "https://datanodes.to/download",
Origin: "https://datanodes.to",
"Content-Type": "application/x-www-form-urlencoded",
},
maxRedirects: 0,
validateStatus: (status: number) => status === 302 || status < 400,
}
);
if (response.status === 302) {
return response.headers["location"];
}
if (typeof response.data === "object" && response.data.url) {
return decodeURIComponent(response.data.url);
}
const htmlContent = String(response.data);
if (!htmlContent) {
throw new Error("Empty response received");
}
const downloadLinkRegex = /href=["'](https:\/\/[^"']+)["']/;
const downloadLinkMatch = downloadLinkRegex.exec(htmlContent);
if (downloadLinkMatch) {
return downloadLinkMatch[1];
}
throw new Error("Failed to get the download link");
} catch (error) {
console.error("Error fetching download URL:", error);
logger.error("Error fetching download URL:", error);
throw error;
}
}

View File

@@ -11,6 +11,7 @@ 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";
export interface HydraApiOptions {
needsAuth?: boolean;
@@ -46,7 +47,7 @@ export class HydraApi {
return this.userAuth.authToken !== "";
}
private static hasActiveSubscription() {
public static hasActiveSubscription() {
const expiresAt = new Date(this.userAuth.subscription?.expiresAt ?? 0);
return expiresAt > new Date();
}
@@ -103,12 +104,10 @@ export class HydraApi {
await clearGamesRemoteIds();
uploadGamesBatch();
// WSClient.close();
// WSClient.connect();
WSClient.close();
WSClient.connect();
const { syncDownloadSourcesFromApi } = await import(
"../events/download-sources/sync-download-sources-from-api"
);
const { syncDownloadSourcesFromApi } = await import("./user");
syncDownloadSourcesFromApi();
}
}

View File

@@ -18,4 +18,4 @@ export * from "./library-sync";
export * from "./wine";
export * from "./lock";
export * from "./decky-plugin";
export * from "./resource-cache";
export * from "./user";

View File

@@ -3,6 +3,10 @@ import { HydraApi } from "../hydra-api";
import { gamesSublevel, levelKeys } from "@main/level";
export const createGame = async (game: Game) => {
if (game.shop === "custom") {
return;
}
return HydraApi.post(`/profile/games`, {
objectId: game.objectId,
playTimeInMilliseconds: Math.trunc(game.playTimeInMilliseconds ?? 0),

View File

@@ -60,18 +60,26 @@ export const mergeWithRemoteGames = async () => {
const localGameShopAsset = await gamesShopAssetsSublevel.get(gameKey);
// Construct coverImageUrl if not provided by backend (Steam games use predictable pattern)
const coverImageUrl =
game.coverImageUrl ||
(game.shop === "steam"
? `https://shared.steamstatic.com/store_item_assets/steam/apps/${game.objectId}/library_600x900_2x.jpg`
: null);
await gamesShopAssetsSublevel.put(gameKey, {
updatedAt: Date.now(),
...localGameShopAsset,
shop: game.shop,
objectId: game.objectId,
title: localGame?.title || game.title, // Preserve local title if it exists
coverImageUrl: game.coverImageUrl,
coverImageUrl,
libraryHeroImageUrl: game.libraryHeroImageUrl,
libraryImageUrl: game.libraryImageUrl,
logoImageUrl: game.logoImageUrl,
iconUrl: game.iconUrl,
logoPosition: game.logoPosition,
downloadSources: game.downloadSources,
});
}
})

View File

@@ -1,12 +1,16 @@
import type { Game } from "@types";
import { HydraApi } from "../hydra-api";
export const updateGamePlaytime = async (
export const trackGamePlaytime = async (
game: Game,
deltaInMillis: number,
lastTimePlayed: Date
) => {
return HydraApi.put(`/profile/games/${game.remoteId}`, {
if (game.shop === "custom") {
return;
}
return HydraApi.put(`/profile/games/${game.shop}/${game.objectId}`, {
playTimeDeltaInSeconds: Math.trunc(deltaInMillis / 1000),
lastTimePlayed,
});

View File

@@ -11,9 +11,10 @@ import { NotificationOptions, toXmlString } from "./xml";
import { logger } from "../logger";
import { WindowManager } from "../window-manager";
import type { Game, UserPreferences, UserProfile } from "@types";
import { db, levelKeys } from "@main/level";
import { db, levelKeys, themesSublevel } from "@main/level";
import { restartAndInstallUpdate } from "@main/events/autoupdater/restart-and-install-update";
import { SystemPath } from "../system-path";
import { getThemeSoundPath } from "@main/helpers";
async function downloadImage(url: string | null) {
if (!url) return undefined;
@@ -40,6 +41,27 @@ async function downloadImage(url: string | null) {
});
}
async function getAchievementSoundPath(): Promise<string> {
try {
const allThemes = await themesSublevel.values().all();
const activeTheme = allThemes.find((theme) => theme.isActive);
if (activeTheme?.hasCustomSound) {
const themeSoundPath = getThemeSoundPath(
activeTheme.id,
activeTheme.name
);
if (themeSoundPath) {
return themeSoundPath;
}
}
} catch (error) {
logger.error("Failed to get theme sound path", error);
}
return achievementSoundPath;
}
export const publishDownloadCompleteNotification = async (game: Game) => {
const userPreferences = await db.get<string, UserPreferences>(
levelKeys.userPreferences,
@@ -145,7 +167,8 @@ export const publishCombinedNewAchievementNotification = async (
if (WindowManager.mainWindow) {
WindowManager.mainWindow.webContents.send("on-achievement-unlocked");
} else if (process.platform !== "linux") {
sound.play(achievementSoundPath);
const soundPath = await getAchievementSoundPath();
sound.play(soundPath);
}
};
@@ -205,6 +228,7 @@ export const publishNewAchievementNotification = async (info: {
if (WindowManager.mainWindow) {
WindowManager.mainWindow.webContents.send("on-achievement-unlocked");
} else if (process.platform !== "linux") {
sound.play(achievementSoundPath);
const soundPath = await getAchievementSoundPath();
sound.play(soundPath);
}
};

View File

@@ -1,10 +1,10 @@
import { WindowManager } from "./window-manager";
import { createGame, updateGamePlaytime } from "./library-sync";
import type { Game, GameRunning } from "@types";
import { createGame, trackGamePlaytime } from "./library-sync";
import type { Game, GameRunning, UserPreferences } from "@types";
import { PythonRPC } from "./python-rpc";
import axios from "axios";
import { ProcessPayload } from "./download/types";
import { gamesSublevel, levelKeys } from "@main/level";
import { db, gamesSublevel, levelKeys } from "@main/level";
import { CloudSync } from "./cloud-sync";
import { logger } from "./logger";
import path from "path";
@@ -198,19 +198,32 @@ 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,
lastSyncTick: now,
});
// Hide Hydra to tray on game startup if enabled
db.get<string, UserPreferences | null>(levelKeys.userPreferences, {
valueEncoding: "json",
})
.then((userPreferences) => {
if (userPreferences?.hideToTrayOnGameStart) {
WindowManager.mainWindow?.hide();
}
})
.catch(() => {});
if (game.shop === "custom") return;
AchievementWatcherManager.firstSyncWithRemoteIfNeeded(
game.shop,
game.objectId
);
if (game.remoteId) {
updateGamePlaytime(
trackGamePlaytime(
game,
game.unsyncedDeltaPlayTimeInMilliseconds ?? 0,
new Date()
@@ -244,43 +257,46 @@ function onTickGame(game: Game) {
const delta = now - gamePlaytime.lastTick;
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
const updatedGame: Game = {
...game,
playTimeInMilliseconds: (game.playTimeInMilliseconds ?? 0) + delta,
lastTimePlayed: new Date(),
});
};
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), updatedGame);
gamesPlaytime.set(levelKeys.game(game.shop, game.objectId), {
...gamePlaytime,
lastTick: now,
});
if (currentTick % TICKS_TO_UPDATE_API === 0) {
if (currentTick % TICKS_TO_UPDATE_API === 0 && game.shop !== "custom") {
const deltaToSync =
now -
gamePlaytime.lastSyncTick +
(game.unsyncedDeltaPlayTimeInMilliseconds ?? 0);
const gamePromise = game.remoteId
? updateGamePlaytime(game, deltaToSync, game.lastTimePlayed!)
? trackGamePlaytime(game, deltaToSync, game.lastTimePlayed!)
: createGame(game);
gamePromise
.then(() => {
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
...game,
...updatedGame,
unsyncedDeltaPlayTimeInMilliseconds: 0,
});
})
.catch(() => {
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
...game,
...updatedGame,
unsyncedDeltaPlayTimeInMilliseconds: deltaToSync,
});
})
.finally(() => {
gamesPlaytime.set(levelKeys.game(game.shop, game.objectId), {
...gamePlaytime,
lastTick: now,
lastSyncTick: now,
});
});
@@ -288,11 +304,24 @@ function onTickGame(game: Game) {
}
const onCloseGame = (game: Game) => {
const now = performance.now();
const gamePlaytime = gamesPlaytime.get(
levelKeys.game(game.shop, game.objectId)
)!;
gamesPlaytime.delete(levelKeys.game(game.shop, game.objectId));
const delta = now - gamePlaytime.lastTick;
const updatedGame: Game = {
...game,
playTimeInMilliseconds: (game.playTimeInMilliseconds ?? 0) + delta,
lastTimePlayed: new Date(),
};
gamesSublevel.put(levelKeys.game(game.shop, game.objectId), updatedGame);
if (game.shop === "custom") return;
if (game.remoteId) {
if (game.automaticCloudSync) {
CloudSync.uploadSaveGame(
@@ -304,20 +333,20 @@ const onCloseGame = (game: Game) => {
}
const deltaToSync =
performance.now() -
now -
gamePlaytime.lastSyncTick +
(game.unsyncedDeltaPlayTimeInMilliseconds ?? 0);
return updateGamePlaytime(game, deltaToSync, game.lastTimePlayed!)
return trackGamePlaytime(game, deltaToSync, game.lastTimePlayed!)
.then(() => {
return gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
...game,
...updatedGame,
unsyncedDeltaPlayTimeInMilliseconds: 0,
});
})
.catch(() => {
return gamesSublevel.put(levelKeys.game(game.shop, game.objectId), {
...game,
...updatedGame,
unsyncedDeltaPlayTimeInMilliseconds: deltaToSync,
});
});

View File

@@ -106,7 +106,7 @@ export class PythonRPC {
"main.py"
);
const childProcess = cp.spawn("python3", [scriptPath, ...commonArgs], {
const childProcess = cp.spawn("python", [scriptPath, ...commonArgs], {
stdio: ["inherit", "inherit"],
});

View File

@@ -1,157 +0,0 @@
import { app } from "electron";
import axios from "axios";
import fs from "node:fs";
import path from "node:path";
import { logger } from "./logger";
interface CachedResource<T = unknown> {
data: T;
etag: string | null;
}
export class ResourceCache {
private static cacheDir: string;
static initialize() {
this.cacheDir = path.join(app.getPath("userData"), "resource-cache");
if (!fs.existsSync(this.cacheDir)) {
fs.mkdirSync(this.cacheDir, { recursive: true });
}
}
private static getCacheFilePath(resourceName: string): string {
return path.join(this.cacheDir, `${resourceName}.json`);
}
private static getEtagFilePath(resourceName: string): string {
return path.join(this.cacheDir, `${resourceName}.etag`);
}
private static readCachedResource<T = unknown>(
resourceName: string
): CachedResource<T> | null {
const dataPath = this.getCacheFilePath(resourceName);
const etagPath = this.getEtagFilePath(resourceName);
if (!fs.existsSync(dataPath)) {
return null;
}
try {
const data = JSON.parse(fs.readFileSync(dataPath, "utf-8")) as T;
const etag = fs.existsSync(etagPath)
? fs.readFileSync(etagPath, "utf-8")
: null;
return { data, etag };
} catch (error) {
logger.error(`Failed to read cached resource ${resourceName}:`, error);
return null;
}
}
private static writeCachedResource<T = unknown>(
resourceName: string,
data: T,
etag: string | null
): void {
const dataPath = this.getCacheFilePath(resourceName);
const etagPath = this.getEtagFilePath(resourceName);
try {
fs.writeFileSync(dataPath, JSON.stringify(data), "utf-8");
if (etag) {
fs.writeFileSync(etagPath, etag, "utf-8");
}
logger.info(
`Cached resource ${resourceName} with etag: ${etag || "none"}`
);
} catch (error) {
logger.error(`Failed to write cached resource ${resourceName}:`, error);
}
}
static async fetchAndCache<T = unknown>(
resourceName: string,
url: string,
timeout: number = 10000
): Promise<T> {
const cached = this.readCachedResource<T>(resourceName);
const headers: Record<string, string> = {};
if (cached?.etag) {
headers["If-None-Match"] = cached.etag;
}
try {
const response = await axios.get<T>(url, {
headers,
timeout,
});
const newEtag = response.headers["etag"] || null;
this.writeCachedResource(resourceName, response.data, newEtag);
return response.data;
} catch (error: unknown) {
const axiosError = error as {
response?: { status?: number };
message?: string;
};
if (axiosError.response?.status === 304 && cached) {
logger.info(`Resource ${resourceName} not modified, using cache`);
return cached.data;
}
if (cached) {
logger.warn(
`Failed to fetch ${resourceName}, using cached version:`,
axiosError.message || "Unknown error"
);
return cached.data;
}
logger.error(
`Failed to fetch ${resourceName} and no cache available:`,
error
);
throw error;
}
}
static getCachedData<T = unknown>(resourceName: string): T | null {
const cached = this.readCachedResource<T>(resourceName);
return cached?.data || null;
}
static async updateResourcesOnStartup(): Promise<void> {
logger.info("Starting background resource cache update...");
const resources = [
{
name: "steam-games-by-letter",
url: `${process.env.MAIN_VITE_EXTERNAL_RESOURCES_URL}/steam-games-by-letter.json`,
},
{
name: "sources-manifest",
url: "https://cdn.losbroxas.org/sources-manifest.json",
},
];
await Promise.allSettled(
resources.map(async (resource) => {
try {
await this.fetchAndCache(resource.name, resource.url);
} catch (error) {
logger.error(`Failed to update ${resource.name} on startup:`, error);
}
})
);
logger.info("Resource cache update complete");
}
}

View File

@@ -16,7 +16,7 @@ export const requestSteam250 = async (path: string) => {
if (!steamGameUrl) return null;
return {
title: $title.textContent,
title: $title.getAttribute("data-title") || "",
objectId: steamGameUrl.split("/").pop(),
} as Steam250Game;
})

View File

@@ -0,0 +1,2 @@
export * from "./get-user-data";
export * from "./sync-download-sources";

View File

@@ -0,0 +1,41 @@
import { HydraApi, logger } from "../";
import { downloadSourcesSublevel } from "@main/level";
import type { DownloadSource } from "@types";
export const syncDownloadSourcesFromApi = async () => {
if (!HydraApi.isLoggedIn() || !HydraApi.hasActiveSubscription()) {
return;
}
try {
const profileSources = await HydraApi.get<DownloadSource[]>(
"/profile/download-sources"
);
const existingSources = await downloadSourcesSublevel.values().all();
const existingUrls = new Set(existingSources.map((source) => source.url));
for (const downloadSource of profileSources) {
if (!existingUrls.has(downloadSource.url)) {
try {
await downloadSourcesSublevel.put(downloadSource.id, {
...downloadSource,
isRemote: true,
createdAt: new Date().toISOString(),
});
logger.log(
`Synced download source from profile: ${downloadSource.url}`
);
} catch (error) {
logger.error(
`Failed to sync download source ${downloadSource.url}:`,
error
);
}
}
}
} catch (error) {
logger.error("Failed to sync download sources from API:", error);
}
};

View File

@@ -25,6 +25,7 @@ import type {
} from "@types";
import { AuthPage, generateAchievementCustomNotificationTest } from "@shared";
import { isStaging } from "@main/constants";
import { logger } from "./logger";
export class WindowManager {
public static mainWindow: Electron.BrowserWindow | null = null;
@@ -54,21 +55,25 @@ export class WindowManager {
show: false,
};
private static formatVersionNumber(version: string) {
return version.replaceAll(".", "-");
}
private static async loadWindowURL(window: BrowserWindow, hash: string = "") {
// HMR for renderer base on electron-vite cli.
// Load the remote URL for development or the local html file for production.
if (is.dev && process.env["ELECTRON_RENDERER_URL"]) {
window.loadURL(`${process.env["ELECTRON_RENDERER_URL"]}#/${hash}`);
} else if (import.meta.env.MAIN_VITE_RENDERER_URL) {
} else if (import.meta.env.MAIN_VITE_LAUNCHER_SUBDOMAIN) {
// Try to load from remote URL in production
try {
await window.loadURL(
`${import.meta.env.MAIN_VITE_RENDERER_URL}#/${hash}`
`https://release-v${this.formatVersionNumber(app.getVersion())}.${import.meta.env.MAIN_VITE_LAUNCHER_SUBDOMAIN}#/${hash}`
);
} catch (error) {
// Fall back to local file if remote URL fails
console.error(
"Failed to load from MAIN_VITE_RENDERER_URL, falling back to local file:",
logger.error(
"Failed to load from MAIN_VITE_LAUNCHER_SUBDOMAIN, falling back to local file:",
error
);
window.loadFile(path.join(__dirname, "../renderer/index.html"), {
@@ -284,12 +289,6 @@ export class WindowManager {
}
}
private static loadNotificationWindowURL() {
if (this.notificationWindow) {
this.loadWindowURL(this.notificationWindow, "achievement-notification");
}
}
private static readonly NOTIFICATION_WINDOW_WIDTH = 360;
private static readonly NOTIFICATION_WINDOW_HEIGHT = 140;
@@ -297,46 +296,58 @@ export class WindowManager {
position: AchievementCustomNotificationPosition | undefined
) {
const display = screen.getPrimaryDisplay();
const { width, height } = display.workAreaSize;
const {
x: displayX,
y: displayY,
width: displayWidth,
height: displayHeight,
} = display.bounds;
if (position === "bottom-left") {
return {
x: 0,
y: height - this.NOTIFICATION_WINDOW_HEIGHT,
x: displayX,
y: displayY + displayHeight - this.NOTIFICATION_WINDOW_HEIGHT,
};
}
if (position === "bottom-center") {
return {
x: (width - this.NOTIFICATION_WINDOW_WIDTH) / 2,
y: height - this.NOTIFICATION_WINDOW_HEIGHT,
x: displayX + (displayWidth - this.NOTIFICATION_WINDOW_WIDTH) / 2,
y: displayY + displayHeight - this.NOTIFICATION_WINDOW_HEIGHT,
};
}
if (position === "bottom-right") {
return {
x: width - this.NOTIFICATION_WINDOW_WIDTH,
y: height - this.NOTIFICATION_WINDOW_HEIGHT,
x: displayX + displayWidth - this.NOTIFICATION_WINDOW_WIDTH,
y: displayY + displayHeight - this.NOTIFICATION_WINDOW_HEIGHT,
};
}
if (position === "top-left") {
return {
x: displayX,
y: displayY,
};
}
if (position === "top-center") {
return {
x: (width - this.NOTIFICATION_WINDOW_WIDTH) / 2,
y: 0,
x: displayX + (displayWidth - this.NOTIFICATION_WINDOW_WIDTH) / 2,
y: displayY,
};
}
if (position === "top-right") {
return {
x: width - this.NOTIFICATION_WINDOW_WIDTH,
y: 0,
x: displayX + displayWidth - this.NOTIFICATION_WINDOW_WIDTH,
y: displayY,
};
}
return {
x: 0,
y: 0,
x: displayX,
y: displayY,
};
}
@@ -382,7 +393,7 @@ export class WindowManager {
this.notificationWindow.setIgnoreMouseEvents(true);
this.notificationWindow.setAlwaysOnTop(true, "screen-saver", 1);
this.loadNotificationWindowURL();
this.loadWindowURL(this.notificationWindow, "achievement-notification");
if (!app.isPackaged || isStaging) {
this.notificationWindow.webContents.openDevTools();
@@ -462,6 +473,7 @@ export class WindowManager {
editorWindow.once("ready-to-show", () => {
editorWindow.show();
this.mainWindow?.webContents.openDevTools();
if (!app.isPackaged || isStaging) {
editorWindow.webContents.openDevTools();
}
@@ -469,11 +481,12 @@ export class WindowManager {
editorWindow.webContents.on("before-input-event", (_event, input) => {
if (input.key === "F12") {
editorWindow.webContents.toggleDevTools();
this.mainWindow?.webContents.toggleDevTools();
}
});
editorWindow.on("close", () => {
this.mainWindow?.webContents.closeDevTools();
this.editorWindows.delete(themeId);
});
}

View File

@@ -7,7 +7,7 @@ interface ImportMetaEnv {
readonly MAIN_VITE_CHECKOUT_URL: string;
readonly MAIN_VITE_EXTERNAL_RESOURCES_URL: string;
readonly MAIN_VITE_WS_URL: string;
readonly MAIN_VITE_RENDERER_URL: string;
readonly MAIN_VITE_LAUNCHER_SUBDOMAIN: string;
readonly ELECTRON_RENDERER_URL: string;
}

View File

@@ -99,22 +99,10 @@ contextBridge.exposeInMainWorld("electron", {
/* Download sources */
addDownloadSource: (url: string) =>
ipcRenderer.invoke("addDownloadSource", url),
updateMissingFingerprints: () =>
ipcRenderer.invoke("updateMissingFingerprints"),
removeDownloadSource: (url: string, removeAll?: boolean) =>
ipcRenderer.invoke("removeDownloadSource", url, removeAll),
getDownloadSources: () => ipcRenderer.invoke("getDownloadSources"),
deleteDownloadSource: (id: number) =>
ipcRenderer.invoke("deleteDownloadSource", id),
deleteAllDownloadSources: () =>
ipcRenderer.invoke("deleteAllDownloadSources"),
validateDownloadSource: (url: string) =>
ipcRenderer.invoke("validateDownloadSource", url),
syncDownloadSources: () => ipcRenderer.invoke("syncDownloadSources"),
getDownloadSourcesList: () => ipcRenderer.invoke("getDownloadSourcesList"),
checkDownloadSourceExists: (url: string) =>
ipcRenderer.invoke("checkDownloadSourceExists", url),
getAllRepacks: () => ipcRenderer.invoke("getAllRepacks"),
/* Library */
toggleAutomaticCloudSync: (
@@ -208,6 +196,7 @@ contextBridge.exposeInMainWorld("electron", {
verifyExecutablePathInUse: (executablePath: string) =>
ipcRenderer.invoke("verifyExecutablePathInUse", executablePath),
getLibrary: () => ipcRenderer.invoke("getLibrary"),
refreshLibraryAssets: () => ipcRenderer.invoke("refreshLibraryAssets"),
openGameInstaller: (shop: GameShop, objectId: string) =>
ipcRenderer.invoke("openGameInstaller", shop, objectId),
openGameInstallerPath: (shop: GameShop, objectId: string) =>
@@ -582,6 +571,25 @@ contextBridge.exposeInMainWorld("electron", {
getActiveCustomTheme: () => ipcRenderer.invoke("getActiveCustomTheme"),
toggleCustomTheme: (themeId: string, isActive: boolean) =>
ipcRenderer.invoke("toggleCustomTheme", themeId, isActive),
copyThemeAchievementSound: (themeId: string, sourcePath: string) =>
ipcRenderer.invoke("copyThemeAchievementSound", themeId, sourcePath),
removeThemeAchievementSound: (themeId: string) =>
ipcRenderer.invoke("removeThemeAchievementSound", themeId),
getThemeSoundPath: (themeId: string) =>
ipcRenderer.invoke("getThemeSoundPath", themeId),
getThemeSoundDataUrl: (themeId: string) =>
ipcRenderer.invoke("getThemeSoundDataUrl", themeId),
importThemeSoundFromStore: (
themeId: string,
themeName: string,
storeUrl: string
) =>
ipcRenderer.invoke(
"importThemeSoundFromStore",
themeId,
themeName,
storeUrl
),
/* Editor */
openEditorWindow: (themeId: string) =>

View File

@@ -90,6 +90,7 @@ img {
progress[value] {
-webkit-appearance: none;
appearance: none;
}
.container {

View File

@@ -1,5 +1,4 @@
import { useCallback, useEffect, useRef } from "react";
import achievementSound from "@renderer/assets/audio/achievement.wav";
import { Sidebar, BottomPanel, Header, Toast } from "@renderer/components";
import {
@@ -7,7 +6,6 @@ import {
useAppSelector,
useDownload,
useLibrary,
useRepacks,
useToast,
useUserDetails,
} from "@renderer/hooks";
@@ -20,14 +18,18 @@ import {
setUserDetails,
setProfileBackground,
setGameRunning,
setIsImportingSources,
} from "@renderer/features";
import { useTranslation } from "react-i18next";
import { UserFriendModal } from "./pages/shared-modals/user-friend-modal";
import { useSubscription } from "./hooks/use-subscription";
import { HydraCloudModal } from "./pages/shared-modals/hydra-cloud/hydra-cloud-modal";
import { injectCustomCss, removeCustomCss } from "./helpers";
import {
injectCustomCss,
removeCustomCss,
getAchievementSoundUrl,
getAchievementSoundVolume,
} from "./helpers";
import "./app.scss";
export interface AppProps {
@@ -40,8 +42,6 @@ export function App() {
const { t } = useTranslation("app");
const { updateRepacks } = useRepacks();
const { clearDownload, setLastPacket } = useDownload();
const {
@@ -199,36 +199,6 @@ export function App() {
});
}, [dispatch, draggingDisabled]);
useEffect(() => {
(async () => {
dispatch(setIsImportingSources(true));
try {
// Initial repacks load
await updateRepacks();
// Sync all local sources (check for updates)
const newRepacksCount = await window.electron.syncDownloadSources();
if (newRepacksCount > 0) {
window.electron.publishNewRepacksNotification(newRepacksCount);
}
// Update fingerprints for sources that don't have them
await window.electron.updateMissingFingerprints();
// Update repacks AFTER all syncing and fingerprint updates are complete
await updateRepacks();
} catch (error) {
console.error("Error syncing download sources:", error);
// Still update repacks even if sync fails
await updateRepacks();
} finally {
dispatch(setIsImportingSources(false));
}
})();
}, [updateRepacks, dispatch]);
const loadAndApplyTheme = useCallback(async () => {
const activeTheme = await window.electron.getActiveCustomTheme();
if (activeTheme?.code) {
@@ -250,9 +220,11 @@ export function App() {
return () => unsubscribe();
}, [loadAndApplyTheme]);
const playAudio = useCallback(() => {
const audio = new Audio(achievementSound);
audio.volume = 0.2;
const playAudio = useCallback(async () => {
const soundUrl = await getAchievementSoundUrl();
const volume = await getAchievementSoundVolume();
const audio = new Audio(soundUrl);
audio.volume = volume;
audio.play();
}, []);
@@ -313,7 +285,11 @@ export function App() {
<article className="container">
<Header />
<section ref={contentRef} className="container__content">
<section
ref={contentRef}
id="scrollableDiv"
className="container__content"
>
<Outlet />
</section>
</article>

View File

@@ -1,5 +1,5 @@
import { DownloadIcon, PeopleIcon } from "@primer/octicons-react";
import type { GameStats } from "@types";
import type { GameStats, ShopAssets } from "@types";
import SteamLogo from "@renderer/assets/steam-logo.svg?react";
@@ -8,15 +8,15 @@ import "./game-card.scss";
import { useTranslation } from "react-i18next";
import { Badge } from "../badge/badge";
import { StarRating } from "../star-rating/star-rating";
import { useCallback, useState, useMemo } from "react";
import { useFormat, useRepacks } from "@renderer/hooks";
import { useCallback, useState } from "react";
import { useFormat } from "@renderer/hooks";
export interface GameCardProps
extends React.DetailedHTMLProps<
React.ButtonHTMLAttributes<HTMLButtonElement>,
HTMLButtonElement
> {
game: any;
game: ShopAssets;
}
const shopIcon = {
@@ -28,13 +28,6 @@ export function GameCard({ game, ...props }: GameCardProps) {
const [stats, setStats] = useState<GameStats | null>(null);
const { getRepacksForObjectId } = useRepacks();
const repacks = getRepacksForObjectId(game.objectId);
const uniqueRepackers = Array.from(
new Set(repacks.map((repack) => repack.repacker))
);
const handleHover = useCallback(() => {
if (!stats) {
window.electron.getGameStats(game.objectId, game.shop).then((stats) => {
@@ -45,15 +38,6 @@ export function GameCard({ game, ...props }: GameCardProps) {
const { numberFormatter } = useFormat();
const firstThreeRepackers = useMemo(
() => uniqueRepackers.slice(0, 3),
[uniqueRepackers]
);
const remainingCount = useMemo(
() => uniqueRepackers.length - 3,
[uniqueRepackers]
);
return (
<button
{...props}
@@ -75,18 +59,20 @@ export function GameCard({ game, ...props }: GameCardProps) {
<p className="game-card__title">{game.title}</p>
</div>
{uniqueRepackers.length > 0 ? (
{game.downloadSources.length > 0 ? (
<ul className="game-card__download-options">
{firstThreeRepackers.map((repacker) => (
<li key={repacker}>
<Badge>{repacker}</Badge>
{game.downloadSources.slice(0, 3).map((sourceName) => (
<li key={sourceName}>
<Badge>{sourceName}</Badge>
</li>
))}
{remainingCount > 0 && (
{game.downloadSources.length > 3 && (
<li>
<Badge>
+{remainingCount}{" "}
{t("game_card:available", { count: remainingCount })}
+{game.downloadSources.length - 3}{" "}
{t("game_card:available", {
count: game.downloadSources.length - 3,
})}
</Badge>
</li>
)}

View File

@@ -70,8 +70,10 @@ export function GameContextMenu({
onClick: () => {
if (isGameRunning) {
void handleCloseGame();
} else {
} else if (canPlay) {
void handlePlayGame();
} else {
handleOpenDownloadOptions();
}
},
disabled: isDeleting,

View File

@@ -7,12 +7,13 @@ import { useAppDispatch, useAppSelector } from "@renderer/hooks";
import "./header.scss";
import { AutoUpdateSubHeader } from "./auto-update-sub-header";
import { setFilters } from "@renderer/features";
import { setFilters, setLibrarySearchQuery } from "@renderer/features";
import cn from "classnames";
const pathTitle: Record<string, string> = {
"/": "home",
"/catalogue": "catalogue",
"/library": "library",
"/downloads": "downloads",
"/settings": "settings",
};
@@ -27,10 +28,20 @@ export function Header() {
(state) => state.window
);
const searchValue = useAppSelector(
const catalogueSearchValue = useAppSelector(
(state) => state.catalogueSearch.filters.title
);
const librarySearchValue = useAppSelector(
(state) => state.library.searchQuery
);
const isOnLibraryPage = location.pathname.startsWith("/library");
const searchValue = isOnLibraryPage
? librarySearchValue
: catalogueSearchValue;
const dispatch = useAppDispatch();
const [isFocused, setIsFocused] = useState(false);
@@ -41,6 +52,8 @@ export function Header() {
if (location.pathname.startsWith("/game")) return headerTitle;
if (location.pathname.startsWith("/achievements")) return headerTitle;
if (location.pathname.startsWith("/profile")) return headerTitle;
if (location.pathname.startsWith("/library"))
return headerTitle || t("library");
if (location.pathname.startsWith("/search")) return t("search_results");
return t(pathTitle[location.pathname]);
@@ -60,18 +73,29 @@ export function Header() {
};
const handleSearch = (value: string) => {
dispatch(setFilters({ title: value }));
if (isOnLibraryPage) {
dispatch(setLibrarySearchQuery(value.slice(0, 255)));
} else {
dispatch(setFilters({ title: value.slice(0, 255) }));
if (!location.pathname.startsWith("/catalogue")) {
navigate("/catalogue");
}
}
};
if (!location.pathname.startsWith("/catalogue")) {
navigate("/catalogue");
const handleClearSearch = () => {
if (isOnLibraryPage) {
dispatch(setLibrarySearchQuery(""));
} else {
dispatch(setFilters({ title: "" }));
}
};
useEffect(() => {
if (!location.pathname.startsWith("/catalogue") && searchValue) {
if (!location.pathname.startsWith("/catalogue") && catalogueSearchValue) {
dispatch(setFilters({ title: "" }));
}
}, [location.pathname, searchValue, dispatch]);
}, [location.pathname, catalogueSearchValue, dispatch]);
return (
<>
@@ -120,7 +144,7 @@ export function Header() {
ref={inputRef}
type="text"
name="search"
placeholder={t("search")}
placeholder={isOnLibraryPage ? t("search_library") : t("search")}
value={searchValue}
className="header__search-input"
onChange={(event) => handleSearch(event.target.value)}
@@ -131,7 +155,7 @@ export function Header() {
{searchValue && (
<button
type="button"
onClick={() => dispatch(setFilters({ title: "" }))}
onClick={handleClearSearch}
className="header__action-button"
>
<XIcon />

View File

@@ -3,6 +3,7 @@ import {
DownloadIcon,
GearIcon,
HomeIcon,
BookIcon,
} from "@primer/octicons-react";
export const routes = [
@@ -16,6 +17,11 @@ export const routes = [
nameKey: "catalogue",
render: () => <AppsIcon />,
},
{
path: "/library",
nameKey: "library",
render: () => <BookIcon />,
},
{
path: "/downloads",
nameKey: "downloads",

View File

@@ -1,9 +1,7 @@
import React, { useId, useMemo, useState } from "react";
import React, { useId, useState } from "react";
import { EyeClosedIcon, EyeIcon } from "@primer/octicons-react";
import { useTranslation } from "react-i18next";
import cn from "classnames";
import "./text-field.scss";
export interface TextFieldProps
@@ -42,44 +40,30 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
) => {
const id = useId();
const [isFocused, setIsFocused] = useState(false);
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
const { t } = useTranslation("forms");
const showPasswordToggleButton = props.type === "password";
const inputType = useMemo(() => {
if (props.type === "password" && isPasswordVisible) return "text";
return props.type ?? "text";
}, [props.type, isPasswordVisible]);
const hintContent = useMemo(() => {
if (error)
return (
<small className="text-field-container__error-label">{error}</small>
);
if (hint) return <small>{hint}</small>;
return null;
}, [hint, error]);
const inputType =
props.type === "password" && isPasswordVisible
? "text"
: (props.type ?? "text");
const hintContent = error ? (
<small className="text-field-container__error-label">{error}</small>
) : hint ? (
<small>{hint}</small>
) : null;
const handleFocus: React.FocusEventHandler<HTMLInputElement> = (event) => {
setIsFocused(true);
if (props.onFocus) props.onFocus(event);
props.onFocus?.(event);
};
const handleBlur: React.FocusEventHandler<HTMLInputElement> = (event) => {
setIsFocused(false);
if (props.onBlur) props.onBlur(event);
props.onBlur?.(event);
};
const hasError = !!error;
return (
<div className="text-field-container" {...containerProps}>
{label && <label htmlFor={id}>{label}</label>}
<div className="text-field-container__text-field-wrapper">
<div
className={cn(
@@ -104,7 +88,6 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
onBlur={handleBlur}
type={inputType}
/>
{showPasswordToggleButton && (
<button
type="button"
@@ -120,14 +103,11 @@ export const TextField = React.forwardRef<HTMLInputElement, TextFieldProps>(
</button>
)}
</div>
{rightContent}
</div>
{hintContent}
</div>
);
}
);
TextField.displayName = "TextField";

View File

@@ -98,6 +98,11 @@ export function CloudSyncContextProvider({
);
const getGameArtifacts = useCallback(async () => {
if (shop === "custom") {
setArtifacts([]);
return;
}
const params = new URLSearchParams({
objectId,
shop,

View File

@@ -1,11 +1,4 @@
import {
createContext,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { createContext, useCallback, useEffect, useRef, useState } from "react";
import { setHeaderTitle } from "@renderer/features";
import { getSteamLanguage } from "@renderer/helpers";
@@ -13,11 +6,11 @@ import {
useAppDispatch,
useAppSelector,
useDownload,
useRepacks,
useUserDetails,
} from "@renderer/hooks";
import type {
GameRepack,
GameShop,
GameStats,
LibraryGame,
@@ -84,12 +77,7 @@ export function GameDetailsContextProvider({
const [isGameRunning, setIsGameRunning] = useState(false);
const [showRepacksModal, setShowRepacksModal] = useState(false);
const [showGameOptionsModal, setShowGameOptionsModal] = useState(false);
const { getRepacksForObjectId } = useRepacks();
const repacks = useMemo(() => {
return getRepacksForObjectId(objectId);
}, [getRepacksForObjectId, objectId]);
const [repacks, setRepacks] = useState<GameRepack[]>([]);
const { i18n } = useTranslation("game_details");
const location = useLocation();
@@ -142,10 +130,12 @@ export function GameDetailsContextProvider({
}
});
window.electron.getGameStats(objectId, shop).then((result) => {
if (abortController.signal.aborted) return;
setStats(result);
});
if (shop !== "custom") {
window.electron.getGameStats(objectId, shop).then((result) => {
if (abortController.signal.aborted) return;
setStats(result);
});
}
const assetsPromise = window.electron.getGameAssets(objectId, shop);
@@ -167,7 +157,7 @@ export function GameDetailsContextProvider({
setIsLoading(false);
});
if (userDetails) {
if (userDetails && shop !== "custom") {
window.electron
.getUnlockedAchievements(objectId, shop)
.then((achievements) => {
@@ -287,19 +277,6 @@ export function GameDetailsContextProvider({
}
}, [location]);
const lastDownloadedOption = useMemo(() => {
if (game?.download) {
const repack = repacks.find((repack) =>
repack.uris.some((uri) => uri.includes(game.download!.uri))
);
if (!repack) return null;
return repack;
}
return null;
}, [game?.download, repacks]);
useEffect(() => {
const unsubscribe = window.electron.onUpdateAchievements(
objectId,
@@ -315,6 +292,36 @@ export function GameDetailsContextProvider({
};
}, [objectId, shop, userDetails]);
useEffect(() => {
if (shop === "custom") return;
const fetchDownloadSources = async () => {
try {
const sources = await window.electron.getDownloadSources();
const params = {
take: 100,
skip: 0,
downloadSourceIds: sources.map((source) => source.id),
};
const downloads = await window.electron.hydraApi.get<GameRepack[]>(
`/games/${shop}/${objectId}/download-sources`,
{
params,
needsAuth: false,
}
);
setRepacks(downloads);
} catch (error) {
console.error("Failed to fetch download sources:", error);
}
};
fetchDownloadSources();
}, [shop, objectId]);
const getDownloadsPath = async () => {
if (userPreferences?.downloadsPath) return userPreferences.downloadsPath;
return window.electron.getDefaultDownloadsPath();
@@ -359,7 +366,7 @@ export function GameDetailsContextProvider({
stats,
achievements,
hasNSFWContentBlocked,
lastDownloadedOption,
lastDownloadedOption: null,
setHasNSFWContentBlocked,
selectGameExecutable,
updateGame,

View File

@@ -14,12 +14,15 @@ export interface UserProfileContext {
isMe: boolean;
userStats: UserStats | null;
getUserProfile: () => Promise<void>;
getUserLibraryGames: (sortBy?: string) => Promise<void>;
getUserLibraryGames: (sortBy?: string, reset?: boolean) => Promise<void>;
loadMoreLibraryGames: (sortBy?: string) => Promise<boolean>;
setSelectedBackgroundImage: React.Dispatch<React.SetStateAction<string>>;
backgroundImage: string;
badges: Badge[];
libraryGames: UserGame[];
pinnedGames: UserGame[];
hasMoreLibraryGames: boolean;
isLoadingLibraryGames: boolean;
}
export const DEFAULT_USER_PROFILE_BACKGROUND = "#151515B3";
@@ -30,12 +33,15 @@ export const userProfileContext = createContext<UserProfileContext>({
isMe: false,
userStats: null,
getUserProfile: async () => {},
getUserLibraryGames: async (_sortBy?: string) => {},
getUserLibraryGames: async (_sortBy?: string, _reset?: boolean) => {},
loadMoreLibraryGames: async (_sortBy?: string) => false,
setSelectedBackgroundImage: () => {},
backgroundImage: "",
badges: [],
libraryGames: [],
pinnedGames: [],
hasMoreLibraryGames: false,
isLoadingLibraryGames: false,
});
const { Provider } = userProfileContext;
@@ -62,14 +68,14 @@ export function UserProfileContextProvider({
DEFAULT_USER_PROFILE_BACKGROUND
);
const [selectedBackgroundImage, setSelectedBackgroundImage] = useState("");
const [libraryPage, setLibraryPage] = useState(0);
const [hasMoreLibraryGames, setHasMoreLibraryGames] = useState(true);
const [isLoadingLibraryGames, setIsLoadingLibraryGames] = useState(false);
const isMe = userDetails?.id === userProfile?.id;
const getHeroBackgroundFromImageUrl = async (imageUrl: string) => {
const output = await average(imageUrl, {
amount: 1,
format: "hex",
});
const output = await average(imageUrl, { amount: 1, format: "hex" });
return `linear-gradient(135deg, ${darkenColor(output as string, 0.5)}, ${darkenColor(output as string, 0.6, 0.5)})`;
};
@@ -96,7 +102,13 @@ export function UserProfileContextProvider({
}, [userId]);
const getUserLibraryGames = useCallback(
async (sortBy?: string) => {
async (sortBy?: string, reset = true) => {
if (reset) {
setLibraryPage(0);
setHasMoreLibraryGames(true);
setIsLoadingLibraryGames(true);
}
try {
const params = new URLSearchParams();
params.append("take", "12");
@@ -118,45 +130,98 @@ export function UserProfileContextProvider({
if (response) {
setLibraryGames(response.library);
setPinnedGames(response.pinnedGames);
setHasMoreLibraryGames(response.library.length === 12);
} else {
setLibraryGames([]);
setPinnedGames([]);
setHasMoreLibraryGames(false);
}
} catch (error) {
setLibraryGames([]);
setPinnedGames([]);
setHasMoreLibraryGames(false);
} finally {
setIsLoadingLibraryGames(false);
}
},
[userId]
);
const loadMoreLibraryGames = useCallback(
async (sortBy?: string): Promise<boolean> => {
if (isLoadingLibraryGames || !hasMoreLibraryGames) {
return false;
}
setIsLoadingLibraryGames(true);
try {
const nextPage = libraryPage + 1;
const params = new URLSearchParams();
params.append("take", "12");
params.append("skip", String(nextPage * 12));
if (sortBy) {
params.append("sortBy", sortBy);
}
const queryString = params.toString();
const url = queryString
? `/users/${userId}/library?${queryString}`
: `/users/${userId}/library`;
const response = await window.electron.hydraApi.get<{
library: UserGame[];
pinnedGames: UserGame[];
}>(url);
if (response && response.library.length > 0) {
setLibraryGames((prev) => {
const existingIds = new Set(prev.map((game) => game.objectId));
const newGames = response.library.filter(
(game) => !existingIds.has(game.objectId)
);
return [...prev, ...newGames];
});
setLibraryPage(nextPage);
setHasMoreLibraryGames(response.library.length === 12);
return true;
} else {
setHasMoreLibraryGames(false);
return false;
}
} catch (error) {
setHasMoreLibraryGames(false);
return false;
} finally {
setIsLoadingLibraryGames(false);
}
},
[userId, libraryPage, hasMoreLibraryGames, isLoadingLibraryGames]
);
const getUserProfile = useCallback(async () => {
getUserStats();
getUserLibraryGames();
return window.electron.hydraApi
.get<UserProfile | null>(`/users/${userId}`)
.get<UserProfile>(`/users/${userId}`)
.then((userProfile) => {
if (userProfile) {
setUserProfile(userProfile);
setUserProfile(userProfile);
if (userProfile.profileImageUrl) {
getHeroBackgroundFromImageUrl(userProfile.profileImageUrl).then(
(color) => setHeroBackground(color)
);
}
} else {
showErrorToast(t("user_not_found"));
navigate(-1);
if (userProfile.profileImageUrl) {
getHeroBackgroundFromImageUrl(userProfile.profileImageUrl).then(
(color) => setHeroBackground(color)
);
}
})
.catch(() => {
showErrorToast(t("user_not_found"));
navigate(-1);
});
}, [navigate, getUserStats, getUserLibraryGames, showErrorToast, userId, t]);
const getBadges = useCallback(async () => {
const language = i18n.language.split("-")[0];
const params = new URLSearchParams({
locale: language,
});
const params = new URLSearchParams({ locale: language });
const badges = await window.electron.hydraApi.get<Badge[]>(
`/badges?${params.toString()}`,
@@ -170,6 +235,8 @@ export function UserProfileContextProvider({
setLibraryGames([]);
setPinnedGames([]);
setHeroBackground(DEFAULT_USER_PROFILE_BACKGROUND);
setLibraryPage(0);
setHasMoreLibraryGames(true);
getUserProfile();
getBadges();
@@ -183,12 +250,15 @@ export function UserProfileContextProvider({
isMe,
getUserProfile,
getUserLibraryGames,
loadMoreLibraryGames,
setSelectedBackgroundImage,
backgroundImage: getBackgroundImageUrl(),
userStats,
badges,
libraryGames,
pinnedGames,
hasMoreLibraryGames,
isLoadingLibraryGames,
}}
>
{children}

View File

@@ -31,8 +31,6 @@ import type {
Game,
DiskUsage,
DownloadSource,
DownloadSourceValidationResult,
GameRepack,
} from "@types";
import type { AxiosProgressEvent } from "axios";
@@ -161,6 +159,7 @@ declare global {
) => Promise<void>;
verifyExecutablePathInUse: (executablePath: string) => Promise<Game>;
getLibrary: () => Promise<LibraryGame[]>;
refreshLibraryAssets: () => Promise<void>;
openGameInstaller: (shop: GameShop, objectId: string) => Promise<boolean>;
openGameInstallerPath: (shop: GameShop, objectId: string) => Promise<void>;
openGameExecutablePath: (shop: GameShop, objectId: string) => Promise<void>;
@@ -210,20 +209,12 @@ declare global {
/* Download sources */
addDownloadSource: (url: string) => Promise<DownloadSource>;
updateMissingFingerprints: () => Promise<number>;
removeDownloadSource: (url: string, removeAll?: boolean) => Promise<void>;
getDownloadSources: () => Promise<
Pick<DownloadSource, "url" | "createdAt" | "updatedAt">[]
>;
deleteDownloadSource: (id: number) => Promise<void>;
deleteAllDownloadSources: () => Promise<void>;
validateDownloadSource: (
url: string
) => Promise<DownloadSourceValidationResult>;
syncDownloadSources: () => Promise<number>;
getDownloadSourcesList: () => Promise<DownloadSource[]>;
checkDownloadSourceExists: (url: string) => Promise<boolean>;
getAllRepacks: () => Promise<GameRepack[]>;
removeDownloadSource: (
removeAll = false,
downloadSourceId?: string
) => Promise<void>;
getDownloadSources: () => Promise<DownloadSource[]>;
syncDownloadSources: () => Promise<void>;
/* Hardware */
getDiskFreeSpace: (path: string) => Promise<DiskUsage>;
@@ -419,6 +410,18 @@ declare global {
getCustomThemeById: (themeId: string) => Promise<Theme | null>;
getActiveCustomTheme: () => Promise<Theme | null>;
toggleCustomTheme: (themeId: string, isActive: boolean) => Promise<void>;
copyThemeAchievementSound: (
themeId: string,
sourcePath: string
) => Promise<void>;
removeThemeAchievementSound: (themeId: string) => Promise<void>;
getThemeSoundPath: (themeId: string) => Promise<string | null>;
getThemeSoundDataUrl: (themeId: string) => Promise<string | null>;
importThemeSoundFromStore: (
themeId: string,
themeName: string,
storeUrl: string
) => Promise<void>;
/* Editor */
openEditorWindow: (themeId: string) => Promise<void>;

View File

@@ -1,21 +0,0 @@
import { createSlice } from "@reduxjs/toolkit";
export interface DownloadSourcesState {
isImporting: boolean;
}
const initialState: DownloadSourcesState = {
isImporting: false,
};
export const downloadSourcesSlice = createSlice({
name: "downloadSources",
initialState,
reducers: {
setIsImportingSources: (state, action) => {
state.isImporting = action.payload;
},
},
});
export const { setIsImportingSources } = downloadSourcesSlice.actions;

View File

@@ -6,6 +6,4 @@ export * from "./toast-slice";
export * from "./user-details-slice";
export * from "./game-running.slice";
export * from "./subscription-slice";
export * from "./repacks-slice";
export * from "./download-sources-slice";
export * from "./catalogue-search";

View File

@@ -5,10 +5,12 @@ import type { LibraryGame } from "@types";
export interface LibraryState {
value: LibraryGame[];
searchQuery: string;
}
const initialState: LibraryState = {
value: [],
searchQuery: "",
};
export const librarySlice = createSlice({
@@ -18,7 +20,10 @@ export const librarySlice = createSlice({
setLibrary: (state, action: PayloadAction<LibraryState["value"]>) => {
state.value = action.payload;
},
setLibrarySearchQuery: (state, action: PayloadAction<string>) => {
state.searchQuery = action.payload;
},
},
});
export const { setLibrary } = librarySlice.actions;
export const { setLibrary, setLibrarySearchQuery } = librarySlice.actions;

View File

@@ -121,3 +121,35 @@ export const formatNumber = (num: number): string => {
export const generateUUID = (): string => {
return uuidv4();
};
export const getAchievementSoundUrl = async (): Promise<string> => {
const defaultSound = (await import("@renderer/assets/audio/achievement.wav"))
.default;
try {
const activeTheme = await window.electron.getActiveCustomTheme();
if (activeTheme?.hasCustomSound) {
const soundDataUrl = await window.electron.getThemeSoundDataUrl(
activeTheme.id
);
if (soundDataUrl) {
return soundDataUrl;
}
}
} catch (error) {
console.error("Failed to get theme sound", error);
}
return defaultSound;
};
export const getAchievementSoundVolume = async (): Promise<number> => {
try {
const prefs = await window.electron.getUserPreferences();
return prefs?.achievementSoundVolume ?? 0.15;
} catch (error) {
console.error("Failed to get sound volume", error);
return 0.15;
}
};

View File

@@ -5,5 +5,5 @@ export * from "./use-toast";
export * from "./redux";
export * from "./use-user-details";
export * from "./use-format";
export * from "./use-repacks";
export * from "./use-feature";
export * from "./use-game-card";

View File

@@ -2,6 +2,7 @@ import axios from "axios";
import { useCallback, useEffect, useState } from "react";
import { useAppDispatch } from "./redux";
import { setGenres, setTags } from "@renderer/features";
import type { DownloadSource } from "@types";
export const externalResourcesInstance = axios.create({
baseURL: import.meta.env.RENDERER_VITE_EXTERNAL_RESOURCES_URL,
@@ -12,6 +13,7 @@ export function useCatalogue() {
const [steamPublishers, setSteamPublishers] = useState<string[]>([]);
const [steamDevelopers, setSteamDevelopers] = useState<string[]>([]);
const [downloadSources, setDownloadSources] = useState<DownloadSource[]>([]);
const getSteamUserTags = useCallback(() => {
externalResourcesInstance.get("/steam-user-tags.json").then((response) => {
@@ -37,17 +39,25 @@ export function useCatalogue() {
});
}, []);
const getDownloadSources = useCallback(() => {
window.electron.getDownloadSources().then((results) => {
setDownloadSources(results.filter((source) => !!source.fingerprint));
});
}, []);
useEffect(() => {
getSteamUserTags();
getSteamGenres();
getSteamPublishers();
getSteamDevelopers();
getDownloadSources();
}, [
getSteamUserTags,
getSteamGenres,
getSteamPublishers,
getSteamDevelopers,
getDownloadSources,
]);
return { steamPublishers, steamDevelopers };
return { steamPublishers, downloadSources, steamDevelopers };
}

View File

@@ -0,0 +1,66 @@
import { useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { useFormat } from "./use-format";
import { useTranslation } from "react-i18next";
import { buildGameDetailsPath } from "@renderer/helpers";
import { MAX_MINUTES_TO_SHOW_IN_PLAYTIME } from "@renderer/constants";
import { LibraryGame } from "@types";
export function useGameCard(
game: LibraryGame,
onContextMenu: (game: LibraryGame, position: { x: number; y: number }) => void
) {
const { t } = useTranslation("library");
const { numberFormatter } = useFormat();
const navigate = useNavigate();
const formatPlayTime = useCallback(
(playTimeInMilliseconds = 0, isShort = false) => {
const minutes = playTimeInMilliseconds / 60000;
if (minutes < MAX_MINUTES_TO_SHOW_IN_PLAYTIME) {
return t(isShort ? "amount_minutes_short" : "amount_minutes", {
amount: minutes.toFixed(0),
});
}
const hours = minutes / 60;
const hoursKey = isShort ? "amount_hours_short" : "amount_hours";
const hoursAmount = isShort
? Math.floor(hours)
: numberFormatter.format(hours);
return t(hoursKey, { amount: hoursAmount });
},
[numberFormatter, t]
);
const handleCardClick = useCallback(() => {
navigate(buildGameDetailsPath(game));
}, [navigate, game]);
const handleContextMenuClick = useCallback(
(e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
onContextMenu(game, { x: e.clientX, y: e.clientY });
},
[game, onContextMenu]
);
const handleMenuButtonClick = useCallback(
(e: React.MouseEvent) => {
e.stopPropagation();
const rect = e.currentTarget.getBoundingClientRect();
onContextMenu(game, { x: rect.right, y: rect.bottom });
},
[game, onContextMenu]
);
return {
formatPlayTime,
handleCardClick,
handleContextMenuClick,
handleMenuButtonClick,
};
}

View File

@@ -1,26 +0,0 @@
import { setRepacks } from "@renderer/features";
import { useCallback } from "react";
import { RootState } from "@renderer/store";
import { useSelector } from "react-redux";
import { useAppDispatch } from "./redux";
export function useRepacks() {
const dispatch = useAppDispatch();
const repacks = useSelector((state: RootState) => state.repacks.value);
const getRepacksForObjectId = useCallback(
(objectId: string) => {
return repacks.filter((repack) => repack.objectIds.includes(objectId));
},
[repacks]
);
const updateRepacks = useCallback(async () => {
const repacks = await window.electron.getAllRepacks();
dispatch(
setRepacks(repacks.filter((repack) => Array.isArray(repack.objectIds)))
);
}, [dispatch]);
return { getRepacksForObjectId, updateRepacks };
}

View File

@@ -3,12 +3,14 @@ import { useState, useCallback } from "react";
interface SectionCollapseState {
pinned: boolean;
library: boolean;
reviews: boolean;
}
export function useSectionCollapse() {
const [collapseState, setCollapseState] = useState<SectionCollapseState>({
pinned: false,
library: false,
reviews: false,
});
const toggleSection = useCallback((section: keyof SectionCollapseState) => {
@@ -23,5 +25,6 @@ export function useSectionCollapse() {
toggleSection,
isPinnedCollapsed: collapseState.pinned,
isLibraryCollapsed: collapseState.library,
isReviewsCollapsed: collapseState.reviews,
};
}

View File

@@ -29,6 +29,7 @@ import Settings from "./pages/settings/settings";
import Profile from "./pages/profile/profile";
import Achievements from "./pages/achievements/achievements";
import ThemeEditor from "./pages/theme-editor/theme-editor";
import Library from "./pages/library/library";
import { AchievementNotification } from "./pages/achievements/notification/achievement-notification";
console.log = logger.log;
@@ -64,6 +65,7 @@ ReactDOM.createRoot(document.getElementById("root")!).render(
<Route element={<App />}>
<Route path="/" element={<Home />} />
<Route path="/catalogue" element={<Catalogue />} />
<Route path="/library" element={<Library />} />
<Route path="/downloads" element={<Downloads />} />
<Route path="/game/:shop/:objectId" element={<GameDetails />} />
<Route path="/settings" element={<Settings />} />

View File

@@ -1,11 +1,15 @@
import { useCallback, useEffect, useRef, useState } from "react";
import achievementSound from "@renderer/assets/audio/achievement.wav";
import { useTranslation } from "react-i18next";
import {
AchievementCustomNotificationPosition,
AchievementNotificationInfo,
} from "@types";
import { injectCustomCss, removeCustomCss } from "@renderer/helpers";
import {
injectCustomCss,
removeCustomCss,
getAchievementSoundUrl,
getAchievementSoundVolume,
} from "@renderer/helpers";
import { AchievementNotificationItem } from "@renderer/components/achievements/notification/achievement-notification";
import app from "../../../app.scss?inline";
import styles from "../../../components/achievements/notification/achievement-notification.scss?inline";
@@ -33,9 +37,11 @@ export function AchievementNotification() {
const [shadowRootRef, setShadowRootRef] = useState<HTMLElement | null>(null);
const playAudio = useCallback(() => {
const audio = new Audio(achievementSound);
audio.volume = 0.1;
const playAudio = useCallback(async () => {
const soundUrl = await getAchievementSoundUrl();
const volume = await getAchievementSoundVolume();
const audio = new Audio(soundUrl);
audio.volume = volume;
audio.play();
}, []);

View File

@@ -1,4 +1,8 @@
import type { CatalogueSearchResult, DownloadSource } from "@types";
import type {
CatalogueSearchResult,
CatalogueSearchPayload,
DownloadSource,
} from "@types";
import { useAppDispatch, useAppSelector, useFormat } from "@renderer/hooks";
import { useEffect, useMemo, useRef, useState } from "react";
@@ -29,13 +33,12 @@ export default function Catalogue() {
const abortControllerRef = useRef<AbortController | null>(null);
const cataloguePageRef = useRef<HTMLDivElement>(null);
const { steamDevelopers, steamPublishers } = useCatalogue();
const { steamDevelopers, steamPublishers, downloadSources } = useCatalogue();
const { steamGenres, steamUserTags } = useAppSelector(
const { steamGenres, steamUserTags, filters, page } = useAppSelector(
(state) => state.catalogueSearch
);
const [downloadSources, setDownloadSources] = useState<DownloadSource[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [results, setResults] = useState<CatalogueSearchResult[]>([]);
@@ -44,31 +47,46 @@ export default function Catalogue() {
const { formatNumber } = useFormat();
const { filters, page } = useAppSelector((state) => state.catalogueSearch);
const dispatch = useAppDispatch();
const { t, i18n } = useTranslation("catalogue");
const debouncedSearch = useRef(
debounce(async (filters, pageSize, offset) => {
const abortController = new AbortController();
abortControllerRef.current = abortController;
debounce(
async (
filters: CatalogueSearchPayload,
downloadSources: DownloadSource[],
pageSize: number,
offset: number
) => {
const abortController = new AbortController();
abortControllerRef.current = abortController;
const response = await window.electron.hydraApi.post<{
edges: CatalogueSearchResult[];
count: number;
}>("/catalogue/search", {
data: { ...filters, take: pageSize, skip: offset },
needsAuth: false,
});
const requestData = {
...filters,
take: pageSize,
skip: offset,
downloadSourceIds: downloadSources.map(
(downloadSource) => downloadSource.id
),
};
if (abortController.signal.aborted) return;
const response = await window.electron.hydraApi.post<{
edges: CatalogueSearchResult[];
count: number;
}>("/catalogue/search", {
data: requestData,
needsAuth: false,
});
setResults(response.edges);
setItemsCount(response.count);
setIsLoading(false);
}, 500)
if (abortController.signal.aborted) return;
setResults(response.edges);
setItemsCount(response.count);
setIsLoading(false);
},
500
)
).current;
const decodeHTML = (s: string) =>
@@ -79,18 +97,17 @@ export default function Catalogue() {
setIsLoading(true);
abortControllerRef.current?.abort();
debouncedSearch(filters, PAGE_SIZE, (page - 1) * PAGE_SIZE);
debouncedSearch(
filters,
downloadSources,
PAGE_SIZE,
(page - 1) * PAGE_SIZE
);
return () => {
debouncedSearch.cancel();
};
}, [filters, page, debouncedSearch]);
useEffect(() => {
window.electron.getDownloadSourcesList().then((sources) => {
setDownloadSources(sources.filter((source) => !!source.fingerprint));
});
}, []);
}, [filters, downloadSources, page, debouncedSearch]);
const language = i18n.language.split("-")[0];
@@ -168,7 +185,7 @@ export default function Catalogue() {
value: publisher,
})),
];
}, [filters, steamUserTags, steamGenresMapping, language, downloadSources]);
}, [filters, steamUserTags, downloadSources, steamGenresMapping, language]);
const filterSections = useMemo(() => {
return [

View File

@@ -1,6 +1,6 @@
import { Badge } from "@renderer/components";
import { buildGameDetailsPath } from "@renderer/helpers";
import { useAppSelector, useRepacks, useLibrary } from "@renderer/hooks";
import { useAppSelector, useLibrary } from "@renderer/hooks";
import { useMemo, useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
@@ -23,10 +23,6 @@ export function GameItem({ game }: GameItemProps) {
const { steamGenres } = useAppSelector((state) => state.catalogueSearch);
const { getRepacksForObjectId } = useRepacks();
const repacks = getRepacksForObjectId(game.objectId);
const [isAddingToLibrary, setIsAddingToLibrary] = useState(false);
const [added, setAdded] = useState(false);
@@ -63,10 +59,6 @@ export function GameItem({ game }: GameItemProps) {
}
};
const uniqueRepackers = useMemo(() => {
return Array.from(new Set(repacks.map((repack) => repack.repacker)));
}, [repacks]);
const genres = useMemo(() => {
return game.genres?.map((genre) => {
const index = steamGenres["en"]?.findIndex(
@@ -117,8 +109,8 @@ export function GameItem({ game }: GameItemProps) {
<span className="game-item__genres">{genres.join(", ")}</span>
<div className="game-item__repackers">
{uniqueRepackers.map((repacker) => (
<Badge key={repacker}>{repacker}</Badge>
{game.downloadSources.map((sourceName) => (
<Badge key={sourceName}>{sourceName}</Badge>
))}
</div>
</div>

View File

@@ -1,3 +1,5 @@
@use "../../scss/globals.scss";
.pagination {
display: flex;
gap: 4px;
@@ -18,4 +20,31 @@
font-size: 16px;
}
}
&__page-input {
box-sizing: border-box;
width: 40px;
min-width: 40px;
max-width: 40px;
min-height: 40px;
border-radius: 8px;
border: solid 1px globals.$border-color;
background-color: transparent;
color: globals.$muted-color;
text-align: center;
font-size: 12px;
padding: 0 6px;
outline: none;
}
&__double-chevron {
display: flex;
align-items: center;
justify-content: center;
font-size: 0; // remove whitespace node width between SVGs
}
&__double-chevron > svg + svg {
margin-left: -8px; // pull the second chevron closer
}
}

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